diff --git a/.env.example b/.env.example index f66bb501f..098e93aac 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,59 @@ -HOLESKY_ETH_NODE_URL= -HOLESKY_OWNER_PRIVATE_KEY= -MAINNET_ETH_NODE_URL= -MAINNET_OWNER_PRIVATE_KEY= -GAS_PRICE= -GAS= +# ── RPC endpoints ── +MAINNET_RPC_URL= +HOODI_RPC_URL= + +# ── Signer private keys ── +# Must match the on-chain owner for live upgrades. +MAINNET_PRIVATE_KEY= +HOODI_PRIVATE_KEY= + +# ── Optional SSV token address overrides for Hardhat network config ── +MAINNET_SSVTOKEN_ADDRESS= +HOODI_SSVTOKEN_ADDRESS= + +# ── Block explorer verification ── ETHERSCAN_KEY= -NODE_PROVIDER_KEY= -MINIMUM_BLOCKS_BEFORE_LIQUIDATION=100800 -MINIMUM_LIQUIDATION_COLLATERAL=200000000 -OPERATOR_MAX_FEE_INCREASE=3 -DECLARE_OPERATOR_FEE_PERIOD=259200 # 3 days -EXECUTE_OPERATOR_FEE_PERIOD=345600 # 4 days -VALIDATORS_PER_OPERATOR_LIMIT=500 -SSVTOKEN_ADDRESS= + +# ── Fork test configuration ── +# These are ONLY used when running fork tests manually (npx hardhat test --network hardhat_forked). +# When using `just test-fork `, all values are loaded from deployments//config.json +# and these .env values are ignored. + +# Fork infrastructure (addresses, block, network) +FORK_BLOCK_NUMBER= +FORK_TEST_NETWORK=hardhat_forked +FORK_CONFIG_PATH= +FORK_SSV_NETWORK_ADDRESS= +FORK_SSV_NETWORK_VIEWS= +FORK_SSV_TOKEN= +FORK_CSSV_TOKEN= +FORK_DAO_ADDRESS= + +# Fork test behavior flags +FORK_USE_DEPLOYED_STATE=true +FORK_STRICT_DEPLOYED_STATE=false +FORK_ALLOW_DEPLOYED_FALLBACK=true + +# Fork protocol parameter overrides (manual runs only) +# For deployments and upgrades, the source of truth is deployments//config.json. +FORK_NETWORK_FEE_ETH=3550900000 +FORK_NETWORK_FEE_SSV= +FORK_MIN_OPERATOR_ETH_FEE=1065200000 +FORK_MAX_OPERATOR_ETH_FEE=5326300000 +FORK_OPERATOR_MAX_FEE_INCREASE=1000 +FORK_DECLARE_OPERATOR_FEE_PERIOD=604800 +FORK_EXECUTE_OPERATOR_FEE_PERIOD=604800 +FORK_MIN_LIQ_COLLATERAL=940000000000000 +FORK_VALIDATORS_PER_OPERATOR_LIMIT=3000 +FORK_DEFAULT_ORACLE_IDS=1,2,3,4 +FORK_DEFAULT_UNSTAKE_COOLDOWN=604800 + +# ── Test and gas-report toggles ── +RUN_FORK= +NO_GAS_ENFORCE= +REPORT_GAS= +GAS_REPORT_DIR=. +BASELINE_TAG=v1.2.0 +CURRENT_LABEL=current +GAS_COMPARE_OUTPUT=gas-compare.txt +COVERAGE= diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000..087a33bb2 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "github>ssvlabs/shared-configs//renovate/renovate.json" + ] +} \ No newline at end of file diff --git a/.github/workflows/code-coverage.yaml b/.github/workflows/code-coverage.yaml index f5ebd89d6..b2c424533 100644 --- a/.github/workflows/code-coverage.yaml +++ b/.github/workflows/code-coverage.yaml @@ -8,10 +8,13 @@ jobs: name: Solidity code coverage steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '22.x' - run: npm ci env: GH_TOKEN: ${{ secrets.github_token }} - - run: SOLIDITY_COVERAGE=true NO_GAS_ENFORCE=1 npx hardhat coverage + - run: npx hardhat test --coverage + env: + NO_GAS_ENFORCE: '1' + COVERAGE: 'true' diff --git a/.github/workflows/echidna.yaml b/.github/workflows/echidna.yaml new file mode 100644 index 000000000..0f82457d0 --- /dev/null +++ b/.github/workflows/echidna.yaml @@ -0,0 +1,80 @@ +name: Echidna Fuzzing + +on: + push: + paths: + - 'contracts/**' + - 'test/echidna/**' + - 'foundry.toml' + - '.github/workflows/echidna.yaml' + workflow_dispatch: + +jobs: + discover-contracts: + runs-on: ubuntu-latest + outputs: + contracts: ${{ steps.discover.outputs.contracts }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Discover Echidna harnesses + id: discover + run: | + python3 - <<'PY' >> "$GITHUB_OUTPUT" + import json + from pathlib import Path + + contracts = sorted(path.stem for path in Path("test/echidna").glob("*Echidna.sol")) + if not contracts: + raise SystemExit("No Echidna harnesses found under test/echidna") + + print(f"contracts={json.dumps(contracts)}") + PY + + echidna: + needs: discover-contracts + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + contract: ${{ fromJSON(needs.discover-contracts.outputs.contracts) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + + - name: Install dependencies + run: npm ci + env: + GH_TOKEN: ${{ secrets.github_token }} + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run Echidna + uses: crytic/echidna-action@v2 + with: + files: test/echidna/${{ matrix.contract }}.sol + contract: ${{ matrix.contract }} + config: test/echidna/echidna-ci.yaml + crytic-args: --compile-force-framework foundry + test-mode: property + + - name: Upload corpus + uses: actions/upload-artifact@v4 + if: always() + with: + name: echidna-corpus-${{ matrix.contract }} + path: crytic-export/ + retention-days: 7 diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 24d7bd56f..968e43098 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -9,10 +9,10 @@ jobs: name: Solidity linter steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '22.x' - run: npm ci env: GH_TOKEN: ${{ secrets.github_token }} - - run: npx hardhat check + - run: npx solhint 'contracts/**/*.sol' --ignore-path .solhintignore diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 5ac6a2a48..765bf835c 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,9 +10,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22.x' registry-url: 'https://registry.npmjs.org' - name: Install dependencies diff --git a/.github/workflows/slither.yaml b/.github/workflows/slither.yaml index 41f996173..b98405861 100644 --- a/.github/workflows/slither.yaml +++ b/.github/workflows/slither.yaml @@ -7,10 +7,23 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install dependencies + run: npm ci + env: + GH_TOKEN: ${{ secrets.github_token }} + + - name: Compile contracts (Hardhat) + run: npx hardhat compile --force + - name: Run Slither uses: crytic/slither-action@v0.3.2 id: slither with: - node-version: 20 fail-on: high - slither-args: --exclude controlled-delegatecall,incorrect-return + target: . + slither-args: --hardhat-ignore-compile --exclude controlled-delegatecall,incorrect-return --filter-paths "contracts/test/" --exclude-informational --exclude-dependencies \ No newline at end of file diff --git a/.github/workflows/tests-forked.yaml b/.github/workflows/tests-forked.yaml index 0e399fa6f..729deacfd 100644 --- a/.github/workflows/tests-forked.yaml +++ b/.github/workflows/tests-forked.yaml @@ -1,20 +1,39 @@ name: Run tests -on: [push] +on: [push, pull_request] jobs: ci: runs-on: ubuntu-latest - name: Hardhat unit test (forked network) - env: # Set environment variables for all steps in this job - FORK_TESTING_ENABLED: true + name: Hardhat fork tests + env: GH_TOKEN: ${{ secrets.github_token }} - MAINNET_ETH_NODE_URL: ${{ secrets.mainnet_eth_node_url }} - NODE_PROVIDER_KEY: ${{ secrets.node_provider_key }} + FORK_BLOCK_NUMBER: ${{ secrets.fork_block_number }} + HOODI_RPC_URL: ${{ secrets.hoodi_rpc_url }} + MAINNET_RPC_URL: ${{ secrets.mainnet_rpc_url }} steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + + - uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '22.x' + cache: 'npm' + - run: npm ci - - run: npx hardhat test test-forked/*.ts + env: + GH_TOKEN: ${{ secrets.github_token }} + + - name: Compile contracts + run: npx hardhat compile + env: + FORK_BLOCK_NUMBER: ${{ secrets.fork_block_number }} + HOODI_RPC_URL: ${{ secrets.hoodi_rpc_url }} + + - name: Run fork tests + run: npx hardhat test test/test-forked/v2.0.0/fullIntegrationForked.test.ts + env: + REPORT_GAS: 'true' + NO_GAS_ENFORCE: '1' + FORK_BLOCK_NUMBER: ${{ secrets.fork_block_number }} + HOODI_RPC_URL: ${{ secrets.hoodi_rpc_url }} + MAINNET_RPC_URL: ${{ secrets.mainnet_rpc_url }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 361f388f9..8cc13825c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,6 +1,6 @@ name: Run tests -on: [push] +on: [push, pull_request] jobs: ci: @@ -8,10 +8,113 @@ jobs: name: Hardhat unit test steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + + - uses: actions/setup-node@v4 with: - node-version: '20.x' + node-version: '22.x' + cache: 'npm' + - run: npm ci env: GH_TOKEN: ${{ secrets.github_token }} - - run: npx hardhat test --parallel + + - name: Compile contracts + run: npx hardhat compile + env: + FORK_BLOCK_NUMBER: ${{ secrets.fork_block_number }} + HOODI_RPC_URL: ${{ secrets.hoodi_rpc_url }} + + - name: Run tests with gas tracking + run: npx hardhat test + env: + REPORT_GAS: 'true' + NO_GAS_ENFORCE: '1' + FORK_BLOCK_NUMBER: ${{ secrets.fork_block_number }} + HOODI_RPC_URL: ${{ secrets.hoodi_rpc_url }} + MAINNET_RPC_URL: ${{ secrets.mainnet_rpc_url }} + + - name: Compare gas usage with limits + id: gas-compare + run: npx tsx scripts/gas-compare.ts + continue-on-error: false + + - name: Upload gas report + uses: actions/upload-artifact@v4 + with: + name: gas-report + path: gas-report.json + retention-days: 30 + + - name: Upload gas comparison report + uses: actions/upload-artifact@v4 + with: + name: gas-compare + path: gas-compare.txt + retention-days: 30 + + - name: Comment on PR with gas report + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + let report; + try { + report = JSON.parse(fs.readFileSync('gas-report.json', 'utf8')); + } catch (e) { + console.log('No gas report found'); + return; + } + + const exceeded = report.entries.filter(e => e.txCount > 0 && !e.withinLimit); + + let body = `## Gas Usage Report\n\n`; + body += `**Commit:** \`${report.commit || 'unknown'}\`\n`; + body += `**Branch:** \`${report.branch || 'unknown'}\`\n`; + body += `**All within limits:** ${report.summary.allWithinLimits ? 'Yes' : 'No'}\n\n`; + + if (exceeded.length === 0) { + body += `All operations are within gas limits.\n`; + } else { + body += `### Exceeded Limits\n\n`; + body += `| Operation | Limit | Actual | Over by |\n`; + body += `|-----------|-------|--------|--------|\n`; + for (const e of exceeded) { + const over = e.average - e.maxLimit; + const pct = ((over / e.maxLimit) * 100).toFixed(2); + body += `| ${e.name} | ${e.maxLimit.toLocaleString()} | ${e.average.toLocaleString()} | +${pct}% |\n`; + } + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find(c => + c.user.type === 'Bot' && c.body.includes('## Gas Usage Report') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + - name: Fail on gas limit exceeded + if: steps.gas-compare.outcome == 'failure' + run: | + echo "Gas limits exceeded! See the comparison output above." + exit 1 diff --git a/.gitignore b/.gitignore index 8cd9e58d3..8040c6b99 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,15 @@ node_modules # Hardhat files /cache /artifacts +/types # TypeChain files /typechain /typechain-types +# IDE related files +.idea + # solidity-coverage files /coverage /coverage.json @@ -16,4 +20,23 @@ node_modules .DS_Store .history .dccache -abis \ No newline at end of file +out/ + +gas-report.json +crytic-export/combined_solc.json + +# local release / agent scratch files +.claude/ + +# generated / local-only ABI extras +abis/Mock*.json +abis/Std*.json +abis/Test.json +abis/Vm*.json +abis/std*.json + +# subtask working files +.subtask/ + +# internal review workspace +ssv-review/ diff --git a/.openzeppelin/goerli.json b/.openzeppelin/goerli.json deleted file mode 100644 index 6b2088288..000000000 --- a/.openzeppelin/goerli.json +++ /dev/null @@ -1,7804 +0,0 @@ -{ - "manifestVersion": "3.2", - "proxies": [ - { - "address": "0x3A23a7F455E853058d900f5dc86f1Bb1589b54F9", - "txHash": "0x9802d184ef00cd873e770ecabe377fb98e5fd7c422946c5d46a2866f42dc93d2", - "kind": "uups" - }, - { - "address": "0x4ddfE2966f7Cdfe1F7d4f7d48949b3AB16BCc6B5", - "txHash": "0x7f46d97d62a3f25fe62473d9839871949408b23a07bd4541b52eb4a03090523f", - "kind": "uups" - }, - { - "address": "0xAfdb141Dd99b5a101065f40e3D7636262dce65b3", - "txHash": "0xc7a324e98962c685088d1ad056d33c9a57f64770e9ee8d755dd1133552419c38", - "kind": "uups" - }, - { - "address": "0x8dB45282d7C4559fd093C26f677B3837a5598914", - "txHash": "0xb8a3be822bd5dda92d8a03a0ced6dbac1f60aedacf4970ef912a81e25cbc5bec", - "kind": "uups" - }, - { - "address": "0x78ccf8eD5A7324866B1F663938dc0923bd2Fa8Df", - "txHash": "0x4fda09cc41369a54f3b750f8f757f01721427f4f81c9f3a9924ef94d7f971e63", - "kind": "uups" - }, - { - "address": "0x4935780f792bBc06BBbA6933d900698F7E74a51a", - "txHash": "0x54836afb5d593fce4c5aa4ee36c1d6c34d1b6f379984301632e0c96d1ecb3d7e", - "kind": "uups" - }, - { - "address": "0x9883B43048697382e2d27436Dc3e7C5E44cd858C", - "txHash": "0x7eefc191d7b6fa26999d4d2d4db52c45315191d95be96a667453c55345a4a0e8", - "kind": "uups" - }, - { - "address": "0x4B133c68A084B8A88f72eDCd7944B69c8D545f03", - "txHash": "0x653a2a3ec82d5d44ca8a4f4a99cb5c7f3e65b5fa42927f6e9ae5637292142a46", - "kind": "uups" - }, - { - "address": "0x0d7F42f447Db3819b7Cd227F7b5a208C8672F29C", - "txHash": "0x77d062dbf0ab1c3f0ba58d34fe6030920e566affff2d0648677be1a413f1a44b", - "kind": "uups" - }, - { - "address": "0x15C59e2a9be515bD002be918738d43d2CC915601", - "txHash": "0x0a89a6ab4fffb4a19672ddc97915f2465672ea9993a475b5681ab7dd3f24c4e2", - "kind": "uups" - }, - { - "address": "0x55660cbfDcD33649062B6182E2ee0E4930CdCFa7", - "txHash": "0xacc63b265d8741b3636caf4dd4369688ef362b1bf5a2de398047a1fdea935240", - "kind": "uups" - }, - { - "address": "0xB7D5Aa053315c9902825CE9E30F3A9cfA148dc2a", - "txHash": "0x1842096c7db0a02b25d2efcdb18f0488bde07ca65e524c8e9dbadb0e85fa228a", - "kind": "uups" - }, - { - "address": "0x45B831727DC96035e6a2f77AAAcE4835195a54Af", - "txHash": "0x31d03fed514af9138a6fca1784aade113e22794485b80ed6c8a975879b0716b4", - "kind": "uups" - }, - { - "address": "0x6F47C9Dbe0e3a369aE0ccDFF982183881CcDfb42", - "txHash": "0xe3cfdd1b88ac3b1d654c7761e982907f799f765865375e964512e09742b88b52", - "kind": "uups" - }, - { - "address": "0x9d3F908cB3b132379A97b0E0f8171F0B42756E28", - "txHash": "0x340d08b660b8055728de7f10ef6bef6daf2537b45f8e65237ed95b2e1cd9ce5f", - "kind": "uups" - }, - { - "address": "0x5b10c20D163Ed06Fe80630935439010295AE4C3B", - "txHash": "0xa90d4a0e1be7a3c257fc7a76d58289bd7c15b025174c55f890bd96e3f05389d1", - "kind": "uups" - }, - { - "address": "0x13F6DDF7B84dF02Cad4d75c39602Dc2cb2a275E9", - "txHash": "0x0cd98b8410dddac987d2de8c221713ea8250938550e037a4ef1581251518c178", - "kind": "uups" - }, - { - "address": "0x17dbb473c152Ff977607d82BBE7Bc7B9597cEF22", - "txHash": "0x2777281a792d66e198ebede6d8d7b5fdf89dfcbc8fd60f5e933039e8a6c98ac0", - "kind": "uups" - }, - { - "address": "0x56EEd6e3a358EaaA8Bc9BABb3be9C30b450833e8", - "txHash": "0x68529993097f5e848600a8c94b7edd859cf95f7833f93daa27bf9f83bc38b31c", - "kind": "uups" - }, - { - "address": "0xB4f76eC1cF546BcB2b091d3316F159179Dfbc2d3", - "txHash": "0xaf564eecba8a6623c2f6c9225c9401fc0bab7e226562b7c79f71b01f999df78c", - "kind": "uups" - }, - { - "address": "0x5a03e2a7e3A63E403f4Bd08421c88B4726eCbfB7", - "txHash": "0xd8c70ccf52ac3c957f1c936f76db3c563ca3d362cdd557bcb938e7d3f939e82e", - "kind": "uups" - }, - { - "address": "0x807E241D3118fC8F231948C60aa42a4C606C2545", - "txHash": "0x92c5acb7a80fc7456f09cfa170be120b1f087685d1d199d4350b1cb59dbd08f1", - "kind": "uups" - }, - { - "address": "0xC3CD9A0aE89Fff83b71b58b6512D43F8a41f363D", - "txHash": "0x40fb3000b9aca259b09fd24a83e92d017885f42b7245b8ca804a39e9584282f6", - "kind": "uups" - }, - { - "address": "0xAE2C84c48272F5a1746150ef333D5E5B51F68763", - "txHash": "0x7aa677a741c4b779346c8b179b0f3f47007dd94d90c7073b47a826ba969b86a5", - "kind": "uups" - }, - { - "address": "0xd6b633304Db2DD59ce93753FA55076DA367e5b2c", - "txHash": "0x48e42568ba5cea62bddf9f00d348623677ed38fb083acd760f830f454f290500", - "kind": "uups" - }, - { - "address": "0xcDc4423E9ffa9542d4CdDf42a70859C84859d2A9", - "txHash": "0x964e728e77bd4afa121c93bfd55076c36a5de0b764214dbb9ee574fa1976a9ad", - "kind": "uups" - }, - { - "address": "0xFe35A31e57946E8aadd25158BdF303A36dEf3332", - "txHash": "0x73d4d1df08c7c3d95e8b34545aa55b9ceb7e7e07f7138cb524c7565c56b03e91", - "kind": "uups" - } - ], - "impls": { - "84c23f7724698de84eb813dbfda03172032dfda80fc9218f7edeef2aa8404809": { - "address": "0xC3f92f9F001De4Fe36f9aF7A093842d7fc1a8718", - "txHash": "0x7206af5ce75169aac83dcaf88d81a0744f58834e456cf984a89af704e1a57ea5", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:40" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1833_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "_validatorPKs", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_struct(Validator)1812_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "version", - "offset": 0, - "slot": "256", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "257", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "259", - "type": "t_struct(DAO)1858_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "260", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "261", - "type": "t_struct(Network)1865_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "262", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1812_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1833_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1858_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1865_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1833_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1822_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1840_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1822_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1812_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "660ed01ee43f3f2af646276a03204bac7cb226ab7013ef4ca1a2a5900bd9b6c2": { - "address": "0xE858F79E4220fC552522563aE2C55Be6f5d661f4", - "txHash": "0xbbedf6dd62602c37ad101e4575336fa1bc815365581c3d1dc0e79490c9da3b5b", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(SSVNetwork)5018", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:26" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:30" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(SSVNetwork)5018": { - "label": "contract SSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1e164419e5042d71372eda74f4b4650521a5473bbd51b5c1e1cee4b82afe94b7": { - "address": "0xe7A57a7a489d884C30946573A61C173928e03F9B", - "txHash": "0x50f9eca193ce521eecbdd47b73cb5718767b23db6f85d93c363df8f0ed5b5deb", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:40" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1833_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "_validatorPKs", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_struct(Validator)1812_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "version", - "offset": 0, - "slot": "256", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "257", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "259", - "type": "t_struct(DAO)1858_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "260", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "261", - "type": "t_struct(Network)1865_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "262", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1812_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1833_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1858_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1865_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1833_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1822_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1840_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1822_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1812_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c7f446c2126eeceebac6ff1bbb1cc583a527079acd7f8929332ade86b5852b55": { - "address": "0x2fe8da61509Fd14a1ac00ad589Ffd0Bf1145956D", - "txHash": "0x740ae16c8cf791324207db865b65aceb45ecacb2bcb75306c57fa410de574589", - "layout": { - "solcVersion": "0.8.16", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:40" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1833_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "_validatorPKs", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_struct(Validator)1812_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "version", - "offset": 0, - "slot": "256", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "257", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "257", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "259", - "type": "t_struct(DAO)1858_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "260", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "261", - "type": "t_struct(Network)1865_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "262", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1812_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1833_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1840_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1858_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1865_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1833_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1822_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1840_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1822_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1812_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c4480254e15501bebfa4ff3227ffafce858a7084193ac664b9626254f8e6b23a": { - "address": "0x6b2CA261957B4b2f795aEeF5A806EdCc6bE04eB9", - "txHash": "0xcd619ab625bebdd3436c5b1158d1aeb76fb5b834c137a336b35c51050d454a93", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2148_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:39" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)2612_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:45" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_struct(Validator)2591_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "version", - "offset": 0, - "slot": "257", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "258", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:53" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 8, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "260", - "type": "t_struct(DAO)2637_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "261", - "type": "t_contract(IERC20)2095", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "262", - "type": "t_struct(Network)2644_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "263", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2095": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2591_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2612_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2148_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2637_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2644_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2612_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2601_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2619_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2601_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2591_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "106a1e0908460b53147eafb4015f9beae2025df135b0a046f54cfb62b99ab5c4": { - "address": "0x8383d719377047b1B8824CbB7f8ba7f24F12c715", - "txHash": "0x923fe64b46b47b4a91612bb457980cca97017d569c91ed2343ec7ad04cac8693", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(SSVNetwork)5302", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:25" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(SSVNetwork)5302": { - "label": "contract SSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "2eb98242bc5110431e468ef0b6b4893fc5af474b8bfada3b85b4ffdbf6fbaf5c": { - "address": "0xDea29CF8d8769c0b015360636E07e5f9953F2dDd", - "txHash": "0xb03f69580afbd2d728030fc53fc391f071b75176e05876113a044ab9d74bf99f", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1401_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:39" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)1865_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:45" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1872_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_struct(Validator)1844_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "version", - "offset": 0, - "slot": "257", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "258", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:53" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 8, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "260", - "type": "t_struct(DAO)1890_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "261", - "type": "t_contract(IERC20)1395", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "262", - "type": "t_struct(Network)1897_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "263", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1395": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1844_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1865_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1872_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1401_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1890_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)1897_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1865_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1854_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1872_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1854_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1844_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "983c95fc77dd302eb14262414744b549e13786067160e21a04a61f5def161f3e": { - "address": "0xad77AFA0c42a2056AB310cfd1f13dd5CCE5cF584", - "txHash": "0x0709e6ac3b6fe7ce4b2e1ffaab2d00131d8626436748ad3a49b3551a99ea94b0", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:211" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2148_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:39" - }, - { - "label": "operators", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_uint64,t_struct(Operator)2612_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:45" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:46" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "clusters", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_struct(Validator)2591_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "version", - "offset": 0, - "slot": "257", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "258", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:53" - }, - { - "label": "declareOperatorFeePeriod", - "offset": 4, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "executeOperatorFeePeriod", - "offset": 12, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:55" - }, - { - "label": "operatorMaxFeeIncrease", - "offset": 20, - "slot": "258", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 0, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:57" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 8, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "dao", - "offset": 0, - "slot": "260", - "type": "t_struct(DAO)2637_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:60" - }, - { - "label": "_token", - "offset": 0, - "slot": "261", - "type": "t_contract(IERC20)2095", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "network", - "offset": 0, - "slot": "262", - "type": "t_struct(Network)2644_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "__gap", - "offset": 0, - "slot": "263", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:66" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2095": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2591_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2612_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2619_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2148_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2637_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2644_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2612_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2601_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2619_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2601_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2591_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 20, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "ae20f2d8556f8a2bf11dfc6c88ce1515ad5a155729bfba9541b7140d324fb981": { - "address": "0xBB09D3d97f5AF7e96a782158aa0c55d3c5BAaC1F", - "txHash": "0x04fa9049c4a67eb8d6312379a5e8299c7d1a6e56a2a37a336d19b143d08383ba", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "201", - "type": "t_contract(ISSVNetwork)1935", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:22" - }, - { - "label": "authorization", - "offset": 0, - "slot": "202", - "type": "t_mapping(t_address,t_struct(Authorization)2215_storage)", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:24" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)1935": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_mapping(t_address,t_struct(Authorization)2215_storage)": { - "label": "mapping(address => struct IRegisterAuth.Authorization)", - "numberOfBytes": "32" - }, - "t_struct(Authorization)2215_storage": { - "label": "struct IRegisterAuth.Authorization", - "members": [ - { - "label": "registerOperator", - "type": "t_bool", - "offset": 0, - "slot": "0" - }, - { - "label": "registerValidator", - "type": "t_bool", - "offset": 1, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "3e3e3c0dffab2beea5c1f587b838efbb3ef4872be93e0fe0f285a3e77f4812a6": { - "address": "0x746C33ccC28b1363c35c09baDAF41b2FFa7E6D56", - "txHash": "0x8841dc01dcfa20c49f931101540ffc8111ad46f8e1d78d4df4e0c9f50d58adb2", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1408_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)1963_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)1942_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)1977_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)1995_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)1402", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2002_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1402": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1942_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1963_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1408_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1995_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2002_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1963_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1952_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1970_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)1977_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1952_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1942_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1ccb2436e943c40bde1109243cc48355f47a96ed143c09aa5164f32ba5a8d6ac": { - "address": "0xE436092ce35Ad4bca28e210F38D48E6adf1A7bdd", - "txHash": "0xa3d0a364576c6f5e2c0e972c201cdfee3b2a76a630db53561cdc91ea6cdae7ba", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVNetwork)1935", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:21" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:25" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)1935": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "da4b22c408604c799c21f702c80cbc4b28a5e686332301a5ea685abfdbd59d12": { - "address": "0x2fD5091C7cCCE39c3cA8267BBf7F6e73e8aF3De3", - "txHash": "0xb63f4e40372c683d6fda6945db349afc64daaff7554903e6581ca27211ea0ad0", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2155_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)2710_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)2689_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)2724_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)2742_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)2102", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2749_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2102": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2689_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2710_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2155_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2742_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2749_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2710_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2699_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2717_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)2724_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2699_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2689_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1ee8c0149a057c048f20b833a01728a0e20412639ef24f718b562ddfb6a55a16": { - "address": "0x74d8aC7C183b38DF8f34C6b2c9048aaDcA4F1B8f", - "txHash": "0x75bb9b28c921f2774b5a503a25702f18ca98d4d3572e8345c6cb8e9fe6b3a942", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2155_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)2710_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)2689_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)2724_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)2742_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)2102", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2749_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2102": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2689_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2710_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2155_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2742_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2749_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2710_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2699_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2717_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)2724_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2699_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2689_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "f7b3d32617efa532ec2ed3b21818c645afaa65acc745236500f0974b4b3d1d5e": { - "address": "0x703FcfeFF4cC45be17eA2Cfe90E7FD1b0d16BF23", - "txHash": "0x7b347767d3df89f0dc287be2650f1add0aaf8e49a8eacbae73628937cf50bdac", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "201", - "type": "t_contract(ISSVNetwork)2682", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:22" - }, - { - "label": "authorization", - "offset": 0, - "slot": "202", - "type": "t_mapping(t_address,t_struct(Authorization)2961_storage)", - "contract": "RegisterAuth", - "src": "contracts/RegisterAuth.sol:24" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)2682": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_mapping(t_address,t_struct(Authorization)2961_storage)": { - "label": "mapping(address => struct IRegisterAuth.Authorization)", - "numberOfBytes": "32" - }, - "t_struct(Authorization)2961_storage": { - "label": "struct IRegisterAuth.Authorization", - "members": [ - { - "label": "registerOperator", - "type": "t_bool", - "offset": 0, - "slot": "0" - }, - { - "label": "registerValidator", - "type": "t_bool", - "offset": 1, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "65580310b4febb04a9c3e4042265ca50fcd8f9a33e23a1f904fcb65f9f1369cd": { - "address": "0x0a59b28a3D4d6F715eb42FB80D02c2474cBebCc7", - "txHash": "0x6d2b29a6e7b1cf3bd30c84eabf21e1dc937c650bab042d830dcaadf981693e93", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)2155_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)2710_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)2689_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)2724_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)2742_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)2102", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2749_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)2102": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)2689_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)2710_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)2717_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)2155_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)2742_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2749_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)2710_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)2699_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)2717_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)2724_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)2699_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)2689_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "e98130a58d8e48a4d04e9bc712106b357518d6734da0f381d35a64479a761798": { - "address": "0xf05E0930eFD59CA23EFCbcA66b82Ed42cA3ADf73", - "txHash": "0x7465bda45bfddaddef837ae6cfe383506e8b9a80f24433f5a7ca2c6588975a5c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "_ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVNetwork)2682", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:21" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:25" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVNetwork)2682": { - "label": "contract ISSVNetwork", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1efc026838fe83210dbd8c56ac7e1f6759bc502143b5879da8be1d153a23d7dd": { - "address": "0x9b25f247C6d055C21c9a85a224CF32078523011a", - "txHash": "0xd9711d06798086fbd7da5017e3fff20632c421c8aa62d124649393d749bd314c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "lastOperatorId", - "offset": 0, - "slot": "251", - "type": "t_struct(Counter)1408_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:41" - }, - { - "label": "operatorsPKs", - "offset": 0, - "slot": "252", - "type": "t_mapping(t_bytes32,t_uint64)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:47" - }, - { - "label": "operators", - "offset": 0, - "slot": "253", - "type": "t_mapping(t_uint64,t_struct(Operator)1963_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:48" - }, - { - "label": "operatorsWhitelist", - "offset": 0, - "slot": "254", - "type": "t_mapping(t_uint64,t_address)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:49" - }, - { - "label": "operatorFeeChangeRequests", - "offset": 0, - "slot": "255", - "type": "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:50" - }, - { - "label": "clusters", - "offset": 0, - "slot": "256", - "type": "t_mapping(t_bytes32,t_bytes32)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:51" - }, - { - "label": "validatorPKs", - "offset": 0, - "slot": "257", - "type": "t_mapping(t_bytes32,t_struct(Validator)1942_storage)", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:52" - }, - { - "label": "version", - "offset": 0, - "slot": "258", - "type": "t_bytes32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:54" - }, - { - "label": "validatorsPerOperatorLimit", - "offset": 0, - "slot": "259", - "type": "t_uint32", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:56" - }, - { - "label": "minimumBlocksBeforeLiquidation", - "offset": 4, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:58" - }, - { - "label": "minimumLiquidationCollateral", - "offset": 12, - "slot": "259", - "type": "t_uint64", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:59" - }, - { - "label": "operatorFeeConfig", - "offset": 0, - "slot": "260", - "type": "t_struct(OperatorFeeConfig)1977_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:61" - }, - { - "label": "dao", - "offset": 0, - "slot": "261", - "type": "t_struct(DAO)1995_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:62" - }, - { - "label": "token", - "offset": 0, - "slot": "262", - "type": "t_contract(IERC20)1402", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:63" - }, - { - "label": "network", - "offset": 0, - "slot": "263", - "type": "t_struct(Network)2002_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:64" - }, - { - "label": "__gap", - "offset": 0, - "slot": "264", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetwork", - "src": "contracts/SSVNetwork.sol:71" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" - }, - "t_contract(IERC20)1402": { - "label": "contract IERC20", - "numberOfBytes": "20" - }, - "t_mapping(t_bytes32,t_bytes32)": { - "label": "mapping(bytes32 => bytes32)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_struct(Validator)1942_storage)": { - "label": "mapping(bytes32 => struct ISSVNetworkCore.Validator)", - "numberOfBytes": "32" - }, - "t_mapping(t_bytes32,t_uint64)": { - "label": "mapping(bytes32 => uint64)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_address)": { - "label": "mapping(uint64 => address)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(Operator)1963_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.Operator)", - "numberOfBytes": "32" - }, - "t_mapping(t_uint64,t_struct(OperatorFeeChangeRequest)1970_storage)": { - "label": "mapping(uint64 => struct ISSVNetworkCore.OperatorFeeChangeRequest)", - "numberOfBytes": "32" - }, - "t_struct(Counter)1408_storage": { - "label": "struct Counters.Counter", - "members": [ - { - "label": "_value", - "type": "t_uint256", - "offset": 0, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(DAO)1995_storage": { - "label": "struct ISSVNetworkCore.DAO", - "members": [ - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 0, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 4, - "slot": "0" - }, - { - "label": "block", - "type": "t_uint64", - "offset": 12, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Network)2002_storage": { - "label": "struct ISSVNetworkCore.Network", - "members": [ - { - "label": "networkFee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "networkFeeIndex", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "networkFeeIndexBlockNumber", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Operator)1963_storage": { - "label": "struct ISSVNetworkCore.Operator", - "members": [ - { - "label": "owner", - "type": "t_address", - "offset": 0, - "slot": "0" - }, - { - "label": "fee", - "type": "t_uint64", - "offset": 20, - "slot": "0" - }, - { - "label": "validatorCount", - "type": "t_uint32", - "offset": 28, - "slot": "0" - }, - { - "label": "snapshot", - "type": "t_struct(Snapshot)1952_storage", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_struct(OperatorFeeChangeRequest)1970_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeChangeRequest", - "members": [ - { - "label": "fee", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "approvalBeginTime", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "approvalEndTime", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(OperatorFeeConfig)1977_storage": { - "label": "struct ISSVNetworkCore.OperatorFeeConfig", - "members": [ - { - "label": "declareOperatorFeePeriod", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "executeOperatorFeePeriod", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "operatorMaxFeeIncrease", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Snapshot)1952_storage": { - "label": "struct ISSVNetworkCore.Snapshot", - "members": [ - { - "label": "block", - "type": "t_uint64", - "offset": 0, - "slot": "0" - }, - { - "label": "index", - "type": "t_uint64", - "offset": 8, - "slot": "0" - }, - { - "label": "balance", - "type": "t_uint64", - "offset": 16, - "slot": "0" - } - ], - "numberOfBytes": "32" - }, - "t_struct(Validator)1942_storage": { - "label": "struct ISSVNetworkCore.Validator", - "members": [ - { - "label": "hashedOperatorIds", - "type": "t_bytes32", - "offset": 0, - "slot": "0" - }, - { - "label": "active", - "type": "t_bool", - "offset": 0, - "slot": "1" - } - ], - "numberOfBytes": "64" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, - "t_uint64": { - "label": "uint64", - "numberOfBytes": "8" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "0f9a95bf887a1d295574106829b58f2b931d31b43347c5d7ebf5c432f0d1cf82": { - "address": "0x0Eb002b608133f761A5496A7AD68fFb9a5ae70d1", - "txHash": "0x0e0e17e15a72f233157c5f4a4964a0b5a9ec755199150b9adbb8a58da1ccd5e4", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "07f0ad8d638b80e3e0c46a0ffab2878d79893dbc88ef70a1e05f76ae796932ca": { - "address": "0xC0DE4424F1C2B9BC9b80F19cC479b0adaD38Af44", - "txHash": "0x02b2a5d2c7d92c2fa5a82d1f5694c9225da8e51b7d16792546fbb344c0c167ef", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(IFnSSVViews)3530", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(IFnSSVViews)3530": { - "label": "contract IFnSSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "064c555064aab91647555e9d6781b7254cafff19ce48d4c940fb3dbec372bff0": { - "address": "0xe31b3B1455CCe05030feB1d43a53fB49a5448C30", - "txHash": "0xd91d392a1368afa223ae9947b12551893424425f0d0261baf7f37418a023d81f", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "54c5f42e38cd6e80926108ca0d604abbc840af9bd2d1006bfc27edbf9ab86b0c": { - "address": "0xaaA153ed386F353B793c20868C4F0179B4Ad7604", - "txHash": "0x31682999189ec4b2b2495e6350e97fca5bffeb0a035aab8363abc818b9fa8892", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "06c55e88f7f16ecefd8ce649742571d8712c1164ba0a39a3f273716195d24be1": { - "address": "0xc7fCFeEc5FB9962bDC2234A7a25dCec739e27f9f", - "txHash": "0x59707232c74530e4af16199656dda3dc63b9dfe5a5232fba421daa6cf185a2f3", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)3521", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)3521": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "5d5e858f3c695d1a8c3e7e337239b4cbff1683e92f0839c61c0024f83661cd8b": { - "address": "0xc108c97aD33e10A5A3c87aE686eDb2a7d7c2f45C", - "txHash": "0x27717075d4d11ff649e2cd6523d4afdf9646c007249d4d7a787bdb6efc60807b", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "e6676711f192bb030762553ef7a425b58ee1593a0fc019a3fa85de4b34586bfa": { - "address": "0xE1914816A5AA165A9D828b822Fd4Aa068e1669f8", - "txHash": "0x275ce6a384977642576396837c3c2c3adb78a6a44ad764b06693c7d68c0e6d33", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "d3dca384c0bca6f3341cfc9d6d0fbcdecb40c156c3132b6887361c512f1b8f82": { - "address": "0xA772Ad9BD8b8F5e482dFB4225eDB80a450C0D66A", - "txHash": "0xcc32549ed0dbfaf8da2f084f8b998a9bf994943a39e84a27764205d920c93415", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "4520774aa64c18c9ddf1ef74c95a2ccf9bb21a0078a21ea1067552fdbd846048": { - "address": "0x74C82BD3F46Ab4F2A98635eaEa1f84E1BA5BE98c", - "txHash": "0x8d83beee85106b30f181eaa1abe432a4d75230e10965422d43bb8678f2cbdba9", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4181", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4181": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "3f9800ada4d2a9cecd5dbe695c48130fa7bc0dcdc84b9b238164a16c6a2802fe": { - "address": "0xEcd38CFb1c04C73AEa71350c742dD2A6613861C8", - "txHash": "0x6926c384301d3c540df529e49f0e253be4e178eaba77e9b46d99bc717b990313", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "0b47d2abd2279e76837ff94805e14a88d019593eed05755759cc40256af16f0e": { - "address": "0x296C821446f8756A6d30784C6CF63B65c2B82863", - "txHash": "0xc449b9dd299fe868cc5e284843897df96fb79db91b313450c96ee7e6c3b80a69", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "368acb5d5852996f01c66abefb8ca54bce00f1977c8aaed5efcf0c75f250303d": { - "address": "0xE20E557C5173D505a58eEBf3C4E6aD2672c57Fd1", - "txHash": "0xcde725501175a0ce2045f30df0fc6b3ed3fed0f2baab220f25163ec7c4f93933", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4189", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4189": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "1e550124c6e28f236ee8632b9abc9a68dac2f537aff421981b339520c87ad539": { - "address": "0x0097bBea812414d42D2AD6d76c7da1c794AA16A9", - "txHash": "0xf3a3e1c1742cd1d11b7271abf2768c6046122eb06a2f66971a931021b25763c5", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "d876760172e5575262f8fe267daaa9f5045a6c995da21c74c61e769c00d0cb79": { - "address": "0x5fBf3Fe05112DE129Bf26dB70B03630Bc9A7233a", - "txHash": "0xa7cdfbbb6c6085cf5a574d3c1a93fbbb270987c566a33bcaa9f305d29347b2ea", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c319dae004e883cdd24ab7834715e77958c6beccd0f2cea7e46e804b8f89f295": { - "address": "0xFA2e88093a4Ad20E204290f6169410CcF96e8858", - "txHash": "0xa476d771b2ad2793f613e84194fbfd393e61d6641371592337cadddbca813245", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "8a3382629da06790720d2d37ed76d51b1f949d6c3b17919f08f3b6842b9de108": { - "address": "0x7450a96d73E070210a52ceC327029F52fd156043", - "txHash": "0x9b95840ebe867c296beabe54ce9e7421ea9eb720ee26234d9e8418479cd7903d", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4293", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4293": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "e311afb1f25419f9f90569ca2bf47a87990372364d587ec21789bb9aa6e83966": { - "address": "0xc25ad8Ebf84E08797b3076bb861C35056D3728e9", - "txHash": "0x7c1551d856363b706c688ef21bb39ab5cf30154806d2198d9a3666af45e40b7c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "ea22005a44aa654c8170296ef0052ed50bdb1eb52b46b0b5a9aefc6275bf48c0": { - "address": "0x5888116f5863CA1A311F51ab79a03fBd34Ac6487", - "txHash": "0x1754042f1c35ae05c7ea6c1090e2af0bd38dcf99ce86374720222ed1c66b8fba", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - } - } -} diff --git a/.openzeppelin/holesky.json b/.openzeppelin/holesky.json deleted file mode 100644 index fe420fd3e..000000000 --- a/.openzeppelin/holesky.json +++ /dev/null @@ -1,1762 +0,0 @@ -{ - "manifestVersion": "3.2", - "proxies": [ - { - "address": "0x0d33801785340072C452b994496B19f196b7eE15", - "txHash": "0x9c6605dab4a2d23e480bf90f476b34498bd6d7a4e3c232835544bd8c94971427", - "kind": "uups" - }, - { - "address": "0x656d5cC4e7d49EaCC063cBB8D3e072F2841D68b4", - "txHash": "0x05fec282860f01d95aa19fea01907f0e377d529685c33da59dd0b7c85f96b0b2", - "kind": "uups" - }, - { - "address": "0x38A4794cCEd47d3baf7370CcC43B560D3a1beEFA", - "txHash": "0x998c38ff37b47e69e23c21a8079168b7e0e0ade7244781587b00be3f08a725c6", - "kind": "uups" - }, - { - "address": "0x352A18AEe90cdcd825d1E37d9939dCA86C00e281", - "txHash": "0xf36e0e114031e961a1e3452477ed71658cf0f809be94832b4dc4a99a293ef664", - "kind": "uups" - }, - { - "address": "0x4fA60408D9c0428b43FCa0E26c2f9aAa510cCeE2", - "txHash": "0x72d9a2c0e2442046a87816203f08b0ad4cb65ef25de40c59c5fe4f83b8834370", - "kind": "uups" - }, - { - "address": "0xEa0Fd295Be44Fb909d654dA90198c8E9d766FB74", - "txHash": "0x8cbbb4f8c3fa01e88d0aed6ffcccab3c063d332bf465541a4515fe3070177687", - "kind": "uups" - }, - { - "address": "0x4404f2EBBFc2Ab622C161fA8531404C68606260a", - "txHash": "0xcebce7af4e88fed3dab9c20188ed72b9165e48f12ff70b1ba9aa14e26441967d", - "kind": "uups" - }, - { - "address": "0xAd76Ff4a0931ce5F856044507A0400bA4eA59FB3", - "txHash": "0x53e06d337a8a9f8f000b5b9c9ec552ebc8751d6b69331297ff85d4c7b41de8d3", - "kind": "uups" - }, - { - "address": "0xC9A1594b2F8d48b1e8e84ffbB1448Ebcde00c154", - "txHash": "0x88bd899c0f8ff164525289cccf2fd60c4125dec28bd22fd6aa4f61a1616370e9", - "kind": "uups" - } - ], - "impls": { - "d876760172e5575262f8fe267daaa9f5045a6c995da21c74c61e769c00d0cb79": { - "address": "0x116522F4D00b42Efce0aA77f7ddAd1d27705F36b", - "txHash": "0xd04d7ebb1f3211c5006d965f1c3762c866e8eb77027428ab8989904b4af28a16", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "368acb5d5852996f01c66abefb8ca54bce00f1977c8aaed5efcf0c75f250303d": { - "address": "0xa9d0096bdbf97401F1B5E8D5330Ee8b7f0cb975D", - "txHash": "0x6fbd987b485c9f50a953b1ad38de6bcca366fbe9f9cb1ae88d81aa9b085fb3c6", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)2185", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)2185": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "6cbab0be5caec362fa591f99e9ab60ab2ec706c3cb7d8437dff9b7e9a204232e": { - "address": "0x58EabC62cC2c254AC43E35Edbb0D1f74f3DAd508", - "txHash": "0xd28b5d89a5f212ac291259398cbc79d035ef497bf8f42099edb229e8800aae6e", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "c319dae004e883cdd24ab7834715e77958c6beccd0f2cea7e46e804b8f89f295": { - "address": "0xE74C70Ea8A688de29d3b1F631b1FF8decAd52833", - "txHash": "0x5500f4fcf7c7487ea6c16c5f11fb7e81abc28a6dc0e33207ab5a15e696287aef", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "8a3382629da06790720d2d37ed76d51b1f949d6c3b17919f08f3b6842b9de108": { - "address": "0x7118C9B049E834B2351C1c9a0ECEE12610A1a29E", - "txHash": "0x1f189d0a9a3a88acfeefcfd6bec1309f969d6249550cac23396bfff6e4e39d17", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4293", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4293": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "141551db18457f12435a89572b53feb0543d6d3f184915332fb095bfb8164fdf": { - "address": "0x21A40764aFEb2DA98eEB95Ce720212A15F87c5d7", - "txHash": "0x5433d717bd39e46df0e0345050bf5a0a7f136f96289b6d20b6f9b40b51dcab90", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "e311afb1f25419f9f90569ca2bf47a87990372364d587ec21789bb9aa6e83966": { - "address": "0x249e2769bF15082e1e44D15D819c0230d4500d54", - "txHash": "0x61e70aa81e1c355fdfd3a0554a42043651d7a032435c3a87d06de6f67854ddf8", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "ea22005a44aa654c8170296ef0052ed50bdb1eb52b46b0b5a9aefc6275bf48c0": { - "address": "0x990B226E8D74B42414F1296CCf2d8BA454879621", - "txHash": "0xfdde435768aa2ea9fe64206e700dfb6eac13345e5d733beca1a723e59a578683", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "cace716c99b7c3331ef14bea41153e9b79ee38508d038ed764ebfa2f3addf8d6": { - "address": "0xEFccE07BeC6418e32e72a28aFf0cdf442AEc14ea", - "txHash": "0x3da46b8992ed89a293c740d4e6a724eb0f24fc7b8fd2257bcd8af434ca1f956c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "39c6606e3e0309dc1e40c1da00f5f6fea1a7f681100fa39bffeab8a902d3e822": { - "address": "0xc4267540782292Bb143d1ac4791a870174F76B26", - "txHash": "0x6182c8cadd407d87fbef290d92f0e026e1a3203e00a2806fa8dad7df075060b6", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "d7d016016e8901e15ef80cd31dabc235c6db346324f22178ef7f0075e8481236": { - "address": "0x2fd5fd777bB818bf10A7ab803A9c3ae510E06Ea2", - "txHash": "0x2e05685de9deb43f83ba2b427f8eb31c7ef675d0aff896b4f9c86136c6d0b46e", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4946", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4946": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "a3d9e24ff7a910e9c585a007869df7fef9bed3087299d4534d453e5cd2bcfb5a": { - "address": "0x9Fe9ae58ABe43271313E87DCEAECB2780bE6E2c1", - "txHash": "0xcc870a141b68547d2fa9e71df10f273359e4509dd8948c958e6096cf1e5f2392", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "7e9059cf74010ecbc029661757fcbec0cf7eee8077142bd25354cb83a7152fa8": { - "address": "0x8A8543f0323Fdf9c67Cb5d10B869F565FA737177", - "txHash": "0xf1a132ce867fb1ede636f649cef559d29668cbe1c44af46d4abfe491fb37c26f", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "da06f711e239a6b927747cdeefb5954a754f3a4e01a17212488672f6689ac6f1": { - "address": "0x753B24E62c90B468Bd410b552555717552eC56Ff", - "txHash": "0xce31a279871c200cba53c2726dcbfc4493e4b86ffdd31862713323f9cf60a4cb", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4950", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4950": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "350f8e1c7a01c817bb83103cd78dcb1d0b3510b15601eff2bea87d6506ccd751": { - "address": "0x5Dbf9a62BbcC8135AF60912A8B0212a73e4a6629", - "txHash": "0xcdaf8e8d6193ad7d0fb8b358031620a874d5d5f16b9a8044e8fa4bfba01c141c", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - } - } -} diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json deleted file mode 100644 index ca45b2928..000000000 --- a/.openzeppelin/mainnet.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "manifestVersion": "3.2", - "proxies": [ - { - "address": "0x42Cd8D240E30102B715d7516f97864ECeC4441Ab", - "txHash": "0x2c2e2e6fcd70e75f404d8bc5e09d9a1e4cff2bb9e6c80195b47c6227f06a8a63", - "kind": "uups" - }, - { - "address": "0xb54E555A7f8a0143C829C67F85fCe71523621E45", - "txHash": "0xe3250f79b6fb013dceb5628662e30943a1cbecaa765f52b9de8a12cdea70fcdb", - "kind": "uups" - }, - { - "address": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", - "txHash": "0x4a11a560d3c2f693e96f98abb1feb447646b01b36203ecab0a96a1cf45fd650b", - "kind": "uups" - }, - { - "address": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", - "txHash": "0x98028d64fb4e2428b8a523c085bb4edc2e5e2bd32542802784adb6aa7a12a2e2", - "kind": "uups" - } - ], - "impls": { - "064c555064aab91647555e9d6781b7254cafff19ce48d4c940fb3dbec372bff0": { - "address": "0x99a26a746d950a2E117E1220a765a018beDB0029", - "txHash": "0x320eb3ce44973dd6cfb31bc464d65314c4a97a9e33618db3d77bebbe62f3d909", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "07f0ad8d638b80e3e0c46a0ffab2878d79893dbc88ef70a1e05f76ae796932ca": { - "address": "0x2c14476920E931Eb1DA21EdB4215792A68bEAeA6", - "txHash": "0x880b616c1c13a1065b62c1cc624555f6f34375bb79370fabd4853acb7a85a2d5", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(IFnSSVViews)3548", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:20" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:24" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(IFnSSVViews)3548": { - "label": "contract IFnSSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "5d5e858f3c695d1a8c3e7e337239b4cbff1683e92f0839c61c0024f83661cd8b": { - "address": "0xdc1E8E50673B893c16c18D88e81e13B4415F6292", - "txHash": "0xe6536ad3ddd1fd7b66930558431355ea92a730415a9bd09a6037dcc843ce279c", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "06c55e88f7f16ecefd8ce649742571d8712c1164ba0a39a3f273716195d24be1": { - "address": "0xe183d6eEac469B1544f19Cb5a37Fe6eBFc913C4E", - "txHash": "0xc20fc9836f5f9bcfa3dee10efe883183f3afc93b2b2f6a864e6e49236cc1b460", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)3521", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "private__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)3521": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "400b4079d3549b56702f7dfadb688c5395e64dd3211c072e6f584ae7ae02f79f": { - "address": "0xE2d1Cf93CD4D5E0EEEF1b33ca51Bb82c829A1b75", - "txHash": "0x5c1c55b3073cc2579fd426616636484a188f1205a4278dc37063bc891c397494", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "0bbad3c785dcdd02f463ea2ab702971e2ed300422a4e44ee35105568b16edf1b": { - "address": "0x7B6C84186be89bf0f28A3b5fAacFEd0b4d9D1c01", - "txHash": "0x6e4654a9681d21c10744dfb2e5c81e6acfda103ca3a26ff0bc7bc372b565f11d", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "723c9fc111a6d734d5819515928ae5423cf51ac95735557de2cdd68e886f1a04": { - "address": "0x050E94A68440531f3E89e93C33F349270e9D1750", - "txHash": "0x960563ae4a7743c7668e43d8a0c3f409e7b03d2c96c616ed098750ba530af4c8", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "00fc0f9456fc94965feb33fc5cbf61cbcb1758dd875f7adcab6b65862efa4474": { - "address": "0xfe11c3811eD58C518F5Bd23aDb1FAac487a16cBC", - "txHash": "0x9a5321fc1fa20140c23c9a6b07dc8bd0f4e230480f722f800fe930cf16803124", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:197" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:107" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4228", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4228": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - } - } - }, - "cace716c99b7c3331ef14bea41153e9b79ee38508d038ed764ebfa2f3addf8d6": { - "address": "0x9c0D5400F82561EbE54110f2aD73Ad76f2917943", - "txHash": "0x2aea34e54f6e3382ace77acc6f4a09f14ed4fe432ff529523f7bad99f531e8aa", - "layout": { - "solcVersion": "0.8.18", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:27" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "350f8e1c7a01c817bb83103cd78dcb1d0b3510b15601eff2bea87d6506ccd751": { - "address": "0xA2f1DaDBb9E836B7Ec47330fF9E5947D2f36FC35", - "txHash": "0xca8c8769030dd820f01373318e271b4f7507cfb1596c8f451ec06bfe7fc6d28b", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - }, - "da06f711e239a6b927747cdeefb5954a754f3a4e01a17212488672f6689ac6f1": { - "address": "0x052E5F6bD9DB71C08Db38377596875ceC5708a94", - "txHash": "0xfe4a43b2032e6b82e39dbb833e2a8ecbf4346370c39fcd786c058c7a86e9317b", - "layout": { - "solcVersion": "0.8.24", - "storage": [ - { - "label": "_initialized", - "offset": 0, - "slot": "0", - "type": "t_uint8", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", - "retypedFrom": "bool" - }, - { - "label": "_initializing", - "offset": 1, - "slot": "0", - "type": "t_bool", - "contract": "Initializable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" - }, - { - "label": "__gap", - "offset": 0, - "slot": "1", - "type": "t_array(t_uint256)50_storage", - "contract": "ERC1967UpgradeUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" - }, - { - "label": "__gap", - "offset": 0, - "slot": "51", - "type": "t_array(t_uint256)50_storage", - "contract": "UUPSUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" - }, - { - "label": "__gap", - "offset": 0, - "slot": "101", - "type": "t_array(t_uint256)50_storage", - "contract": "ContextUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" - }, - { - "label": "_owner", - "offset": 0, - "slot": "151", - "type": "t_address", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" - }, - { - "label": "__gap", - "offset": 0, - "slot": "152", - "type": "t_array(t_uint256)49_storage", - "contract": "OwnableUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" - }, - { - "label": "_pendingOwner", - "offset": 0, - "slot": "201", - "type": "t_address", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:21" - }, - { - "label": "__gap", - "offset": 0, - "slot": "202", - "type": "t_array(t_uint256)49_storage", - "contract": "Ownable2StepUpgradeable", - "src": "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:70" - }, - { - "label": "ssvNetwork", - "offset": 0, - "slot": "251", - "type": "t_contract(ISSVViews)4950", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:19" - }, - { - "label": "__gap", - "offset": 0, - "slot": "252", - "type": "t_array(t_uint256)50_storage", - "contract": "SSVNetworkViews", - "src": "contracts/SSVNetworkViews.sol:23" - } - ], - "types": { - "t_address": { - "label": "address", - "numberOfBytes": "20" - }, - "t_array(t_uint256)49_storage": { - "label": "uint256[49]", - "numberOfBytes": "1568" - }, - "t_array(t_uint256)50_storage": { - "label": "uint256[50]", - "numberOfBytes": "1600" - }, - "t_bool": { - "label": "bool", - "numberOfBytes": "1" - }, - "t_contract(ISSVViews)4950": { - "label": "contract ISSVViews", - "numberOfBytes": "20" - }, - "t_uint256": { - "label": "uint256", - "numberOfBytes": "32" - }, - "t_uint8": { - "label": "uint8", - "numberOfBytes": "1" - } - }, - "namespaces": {} - } - } - } -} diff --git a/.solhint.json b/.solhint.json index 0b9ee47cb..60d2daaf8 100644 --- a/.solhint.json +++ b/.solhint.json @@ -3,20 +3,26 @@ "plugins": [], "rules": { "avoid-suicide": "error", - "avoid-sha3": "warn", - "comprehensive-interface": "warn", + "avoid-sha3": "off", + "comprehensive-interface": "off", + "no-global-import": "off", + "no-inline-assembly": "off", + "one-contract-per-file": "off", + "gas-custom-errors": "off", + "explicit-types": "off", + "func-name-mixedcase": "off", + "no-empty-blocks": "off", + "payable-fallback": "off", + "no-unused-vars": "off", + "immutable-vars-naming": "off", "func-visibility": [ "warn", { "ignoreConstructors": true } ], - "ordering": "warn", - "mark-callable-contracts": "off", - "max-line-length": [ - "error", - 120 - ], + "ordering": "off", + "max-line-length": "off", "compiler-version": "off", "not-rely-on-time": "off" } diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 000000000..67889358a --- /dev/null +++ b/.solhintignore @@ -0,0 +1,5 @@ +contracts/**/test/** +contracts/**/tests/** +contracts/**/harness/** +contracts/**/mocks/** +contracts/**/deprecated/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 19ae1a8c4..328c59c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,47 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Released] +### [v2.0.0] 2026-03-23 + +Major protocol upgrade introducing ETH-based accounting, effective-balance-aware fee charging, SSV staking, and the legacy SSV-to-ETH migration path. + +See also: + +- [RELEASE_NOTES.md](RELEASE_NOTES.md) +- [docs/SPEC.md](docs/SPEC.md) +- [docs/FLOWS.md](docs/FLOWS.md) +- [docs/UPGRADE_PLAYBOOK.md](docs/UPGRADE_PLAYBOOK.md) + +#### Added + +- ETH-based cluster accounting for new clusters +- Effective-balance-aware fee accounting through `vUnits` +- Oracle-committed Merkle root flow for effective balance updates +- SSV staking with `cSSV`, including stake, unstake-request, unlock-withdraw, fee sync, and ETH reward claim flows +- One-way `migrateClusterToETH` flow for legacy SSV clusters +- Expanded deployment, SAFE-batch, attestation, smoke-test, and verification tooling for staged and mainnet rollout + +#### Changed + +- New clusters now use ETH as the fee asset instead of SSV +- Legacy SSV clusters continue in a restricted compatibility mode rather than as full-featured pre-upgrade clusters +- ETH cluster solvency, liquidation, and fee burn are now tied to effective balance rather than flat validator-count-only charging +- Environment-driven operations are centered on `just` recipes and `deployments//config.json` +- The repository documentation is reorganized around architecture, setup, tasks, local development, roles, operators, specification, flows, and upgrade playbooks + +#### Behavioral changes + +- Depositing into a liquidated ETH cluster is allowed +- Withdrawing from a liquidated ETH cluster is allowed +- Reactivation can depend on a stale on-chain effective balance snapshot until the next valid EB update +- Removed operators may be skipped during migration or reactivation flows, allowing continued operation with reduced operator coverage where valid + +#### Operational notes + +- The production upgrade path is designed around the `v1.2.0 -> v2.0.0` rollout +- Mainnet rollout uses the repository deployment scripts, deployment attestation, and SAFE batch generation flow +- Post-upgrade verification is part of the intended release process, not an optional follow-up step + ### [v1.2.0] 2024-07-01 - [b7cfe2f] (https://github.com/ssvlabs/ssv-network/commit/11b4e67) - Support for mulitple whitelist addresses for operators (see [docs/operators.md](docs/operators.md)). - [b7cfe2f] (https://github.com/ssvlabs/ssv-network/commit/11b4e67) - Support for external whitelisting contracts (see [docs/operators.md](docs/operators.md)). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..e305a0380 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,261 @@ +# CLAUDE.md — SSV Network Smart Contracts + +This file guides Claude Code when working with the SSV Network smart contracts repository. Read this fully before making any changes. + +## Project Overview + +SSV Network is a decentralized Ethereum staking infrastructure using Secret Shared Validators (SSV/DVT). This repository contains the on-chain smart contracts that manage operators, validators, clusters, and protocol economics. + +**Current Release Target: v2.0.0 — "SSV Staking"** + +This release introduces three tightly coupled upgrades: +1. **ETH Payments** — transition from SSV-token fees to native ETH-denominated fees +2. **Effective Balance (EB) Accounting** — fees scale with actual validator effective balance instead of fixed 32 ETH assumption +3. **SSV Staking** — SSV holders stake tokens, receive cSSV, and earn pro-rata ETH protocol revenue + +## Build & Test Commands + +```bash +npm install # Install dependencies +just build # Compile contracts (force recompile) +just test # Run all tests +just test-unit # Run unit tests only (test/unit/) +just test-integration # Run integration tests only (test/integration/) +just test-forked # Run fork tests (requires MAINNET_RPC_URL in .env) +just coverage # Generate coverage report + HTML output +``` + +**Foundry (for Echidna fuzzing):** +```bash +forge build # Build with Foundry +# Echidna tests are in test/echidna/ +``` + +## Architecture + +### Module System (UUPS Proxy + Delegatecall) + +SSVNetwork.sol is a UUPS upgradeable proxy that routes calls via `delegatecall` to specialized modules: + +``` +SSVNetwork (proxy, UUPS, Ownable2Step) + ├── SSV_OPERATORS → SSVOperators.sol + ├── SSV_CLUSTERS → SSVClusters.sol + ├── SSV_DAO → SSVDAO.sol + ├── SSV_VIEWS → SSVViews.sol (also fallback) + ├── SSV_OPERATORS_WHITELIST → SSVOperatorsWhitelist.sol + ├── SSV_STAKING → SSVStaking.sol + └── SSV_VALIDATORS → SSVValidators.sol +``` + +### Storage Pattern (Diamond/EIP-2535 style) + +All state is stored at deterministic slots via `keccak256(slot) - 1` with inline assembly. **Never add storage variables to module contracts directly** — all state goes through storage libraries. + +| Storage | Slot Key | Purpose | +|---|---|---| +| SSVStorage | `ssv.network.storage.main` | Operators, clusters, validators, module addresses, token | +| SSVStorageProtocol | `ssv.network.storage.protocol` | Fee indices, DAO balances, liquidation params (both SSV and ETH) | +| SSVStorageEB | `ssv.network.storage.eb` | Merkle roots, cluster EB snapshots, oracle voting, operator vUnits | +| SSVStorageStaking | `ssv.network.storage.staking` | Staking state, rewards accumulator, oracles, withdrawal requests | +| SSVStorageReentrancy | `ssv.network.storage.reentrancy` | Custom reentrancy guard status | + +### Dual Cluster System + +The protocol maintains two parallel cluster records during the transition period: +- `s.clusters[hash]` — legacy SSV-denominated clusters (VERSION_SSV = 0) +- `s.ethClusters[hash]` — new ETH-denominated clusters (VERSION_ETH = 1) + +Each operator tracks dual snapshots: SSV (`.snapshot`, `.fee`, `.validatorCount`) and ETH (`.ethSnapshot`, `.ethFee`, `.ethValidatorCount`). + +### Packed Types (Critical for Precision) + +``` +PackedSSV (uint64): actual_value = raw * 10_000_000 (DEDUCTED_DIGITS) +PackedETH (uint64): actual_value = raw * 100_000 (ETH_DEDUCTED_DIGITS) +``` + +Values not divisible by the precision factor revert with `MaxPrecisionExceeded`. + +## Key Accounting Rules + +### ETH Cluster Fee Calculation (vUnit Model) + +``` +vUnits = ceil(effectiveBalanceETH * 10_000 / 32) +operatorFee = blockDiff * ethFee * effectiveVUnits / BPS_DENOMINATOR +networkFee = (networkFeeIndexDelta * effectiveVUnits) / BPS_DENOMINATOR +totalFees = (operatorFeeUnits + networkFeeUnits) * ETH_DEDUCTED_DIGITS +cluster.balance -= totalFees +``` + +- Implicit EB (default): `vUnits = validatorCount * 10_000` (assumes 32 ETH/validator) +- Explicit EB: set after first `updateClusterBalance` oracle update + +### SSV Cluster Fee Calculation (Legacy) + +``` +fees = (operatorIndexDelta + networkFeeIndexDelta) * validatorCount +cluster.balance -= unpack(fees) +``` + +### ETH Liquidation Check + +``` +liquidatable IF: + balance < minimumLiquidationCollateral (0.00094 ETH) + OR balance < minimumBlocksBeforeLiquidation * (burnRate + networkFee) * vUnits / BPS_DENOMINATOR * ETH_DEDUCTED_DIGITS +``` + +### Staking Rewards (Accumulator Pattern) + +``` +accEthPerShare += (newFeesWei * 1e18) / totalCSSVSupply +pendingReward = cSSVBalance * (accEthPerShare - userIndex) / 1e18 +``` + +Rewards settle on: stake, requestUnstake, claimEthRewards, cSSV transfer (via onCSSVTransfer hook). + +## Governance Parameters (DIP-X Proposed Values) + +| Parameter | Value | Update Function | +|---|---|---| +| ethNetworkFee | 0.000000003550900000 ETH/block (~0.00928 ETH/year) | `updateNetworkFee(uint256)` | +| minimumLiquidationCollateral | 0.00094 ETH | `updateMinimumLiquidationCollateral(uint256)` | +| minimumBlocksBeforeLiquidation | 50190 (~7 days) | `updateLiquidationThresholdPeriod(uint64)` | +| defaultOperatorETHFee | 0.000000001775400000 ETH/block (~0.00464 ETH/year) | Hardcoded in contract | +| cooldownDuration | 604,800 seconds (7 days) | `updateUnstakeCooldownDuration(uint64)` | +| quorumBps | 7500 (75%) | `updateQuorumBps(uint16)` | +| Oracle set | 4 oracles, 3-of-4 threshold | `replaceOracle(uint32, address)` | + +## Security Rules — MUST Follow + +### Reentrancy +- All functions that transfer ETH or tokens MUST use the `nonReentrant` modifier +- The custom reentrancy guard lives at a deterministic storage slot (NOT inherited state) +- Currently protected: `liquidate`, `liquidateSSV`, `withdraw`, `updateClusterBalance`, all operator withdrawals, all staking functions, `withdrawNetworkSSVEarnings` +- Intentionally NOT protected (no external calls before state writes): `reactivate`, `deposit`, `migrateClusterToETH`, validator register/remove + +### Storage Safety +- NEVER add storage variables to module contracts — use the diamond storage pattern +- NEVER modify existing storage struct field order — append only +- When adding new storage fields, add them at the END of the struct +- Verify storage slot computation matches the pattern: `keccak256(abi.encode(SLOT_STRING)) - 1` + +### Access Control +- Owner-only functions are enforced at the SSVNetwork proxy level (Ownable2Step), not in modules +- Oracle-only: `commitRoot` checks `oracleIdOf[msg.sender] != 0` +- cSSV-only: `onCSSVTransfer` checks `msg.sender == CSSV_ADDRESS` +- Operator owner: `operator.checkOwner()` verifies `msg.sender == operator.owner` +- Cluster owner: keyed by `keccak256(owner, operatorIds)` — only owner can call cluster management functions + +### Upgrade Safety +- UUPS pattern — `_authorizeUpgrade` is owner-only +- New initializers use `reinitializer(N)` (current: N=3 for v2.0.0) +- `UPGRADE_TIMESTAMP` immutable in SSVOperators prevents pre-migration fee declarations from being executed post-migration + +### Integer Overflow/Precision +- All fee calculations use packed types — be aware of precision loss from packing/unpacking +- vUnit conversions use ceiling division for ETH→vUnits, floor for vUnits→ETH +- Cluster balance underflow: use `max(0, balance - fees)` pattern, never allow negative + +### Oracle Security +- Merkle proofs use OpenZeppelin's double-hash convention: `keccak256(keccak256(abi.encode(clusterID, effectiveBalance)))` +- EB limits enforced: min 32 ETH/validator, max 2048 ETH/validator +- Block numbers must be strictly monotonically increasing (`blockNum > latestCommittedBlock`) +- Quorum is weighted by equal cSSV splits across oracle slots + +## Backward Compatibility (Critical) + +Any changes to events or function signatures can break external integrations (oracle, liquidator bots, SDK, webapp). Before modifying: + +1. **Events**: The SSV Oracle (`github.com/ssvlabs/ssv-oracle`) subscribes to: `ValidatorAdded`, `ValidatorRemoved`, `ClusterLiquidated`, `ClusterReactivated`, `ClusterWithdrawn`, `ClusterDeposited`, `ClusterMigratedToETH`, `ClusterBalanceUpdated`, `RootCommitted`, `WeightedRootProposed`. Changing these signatures requires oracle client updates. + +2. **Function signatures**: `registerValidator`, `bulkRegisterValidator`, `deposit`, `reactivate` have already changed (removed `amount` param, added `payable`). The `getBalance` view now returns `(uint256 balance, uint256 ebBalance)` instead of just `uint256`. + +3. **Cluster struct**: `(uint32 validatorCount, uint64 networkFeeIndex, uint64 index, bool active, uint256 balance)` — changing this struct breaks ALL event decoding and function calls. + +4. When in doubt, check the oracle repo at `github.com/ssvlabs/ssv-oracle` for ABI dependencies. + +## Working Branch & Git Workflow + +- **Working branch**: `ssv-staking` (contains all v2.0.0 changes) +- **Create feature branches off `ssv-staking`** for each task, then PR back +- Follow existing commit message conventions in the repo + +## Project Structure + +``` +contracts/ +├── SSVNetwork.sol # UUPS proxy + routing +├── SSVNetworkViews.sol # Read-only views contract +├── SSVProxy.sol # Delegatecall base +├── abstract/SSVReentrancyGuard.sol # Custom reentrancy guard +├── interfaces/ # All interfaces (ISSVClusters, ISSVDAO, ISSVStaking, etc.) +├── libraries/ +│ ├── ClusterLib.sol # Cluster operations (balance, liquidation, hashing) +│ ├── OperatorLib.sol # Operator operations (snapshots, fees, validation) +│ ├── ValidatorLib.sol # Validator registration/removal logic +│ ├── ProtocolLib.sol # Protocol-level accounting (DAO, indices) +│ ├── CoreLib.sol # Token transfers, module management +│ ├── SSVPackedLib.sol # Packed type packing/unpacking +│ ├── SSVCoreTypes.sol # Type definitions (PackedSSV, PackedETH, Snapshot, etc.) +│ └── storage/ # Diamond storage structs +├── modules/ +│ ├── SSVClusters.sol # Cluster lifecycle (deposit, withdraw, liquidate, migrate, updateEB) +│ ├── SSVDAO.sol # Governance, oracle management, fee params +│ ├── SSVOperators.sol # Operator management, fee changes, earnings withdrawal +│ ├── SSVOperatorsWhitelist.sol # Whitelist management (bitmap + contracts) +│ ├── SSVStaking.sol # SSV staking, cSSV rewards, unstaking +│ ├── SSVValidators.sol # Validator register/remove/exit +│ └── SSVViews.sol # View function implementations +├── token/ +│ ├── SSVToken.sol # SSV ERC-20 token +│ └── CSSVToken.sol # cSSV receipt token (mint/burn by SSVStaking only) +├── whitelisting/BasicWhitelisting.sol +└── upgrades/stage/hoodi/ # Upgrade initializer (reinitializer(3)) +scripts/ # Deployment & upgrade scripts (TypeScript) +test/ +├── unit/ # Per-module unit tests +├── integration/ # Full integration tests +├── sanity/ # Sanity/regression tests +├── echidna/ # Foundry-based fuzzing +├── test-forked/ # Fork tests against v1.2.0 +├── helpers/ # Test utilities +├── common/ # Constants, errors, events, types +└── setup/ # Deploy, fixtures, fork setup +``` + +## Key Constants + +``` +BPS_DENOMINATOR = 10_000 +MAX_EB_PER_VALIDATOR = 2048 ETH +DEFAULT_EB_PER_VALIDATOR = 32 ETH +ETH_DEDUCTED_DIGITS = 100_000 +DEDUCTED_DIGITS = 10_000_000 +DEFAULT_OPERATOR_ETH_FEE = 1_770_000_000 wei (1.77 gwei/vUnit/block) +MINIMAL_LIQUIDATION_THRESHOLD = 21_480 blocks +MAX_PENDING_REQUESTS = 2000 +MINIMAL_STAKING_AMOUNT = 1_000_000_000 +MAX_DELEGATION_SLOTS = 4 +VERSION_SSV = 0 +VERSION_ETH = 1 +``` + +## Reference Documentation + +- **docs/SPEC.md** — Full DIP-X specification with detailed accounting formulas, storage layout, and all function/event signatures +- **docs/FLOWS.md** — Step-by-step contract flows with state mutations, invariants, and sequence diagrams +- **ssv-review/** — Original proposal documents and mainnet readiness coverage report + +## Test Expectations + +When writing tests: +- Use the existing test helper patterns in `test/helpers/contract-helpers.ts` +- Follow the Mocha + Chai + ethers v6 patterns used in existing tests +- Include both happy path and revert/edge case tests +- Verify event emissions with exact parameter matching +- Check balance invariants before and after operations (contract ETH balance, SSV token balance, operator earnings, cluster balances) +- For migration tests: verify both SSV balance refund AND ETH deposit correctness +- For staking tests: verify accEthPerShare accumulator math with precision diff --git a/Justfile b/Justfile new file mode 100644 index 000000000..d321618a5 --- /dev/null +++ b/Justfile @@ -0,0 +1,123 @@ +# Compile all contracts from scratch (force recompile) +build: + npx hardhat compile --force + +# Remove Hardhat build artifacts and cache +clean: + npx hardhat clean + +# Run test suite without gas enforcement (allows larger txs) +test: + NO_GAS_ENFORCE=true npx hardhat test + +# Run unit tests only (test/unit/) +test-unit: + NO_GAS_ENFORCE=true npx hardhat test $(find test/unit -name "*.test.ts" | xargs) + +# Run integration tests only (test/integration/) +test-integration: + NO_GAS_ENFORCE=true npx hardhat test $(find test/integration -maxdepth 1 -name "*.test.ts" | xargs) + +# Run fork tests against mainnet state (requires MAINNET_RPC_URL in .env) +test-forked: + NO_GAS_ENFORCE=true RUN_FORK=true npx hardhat test $(find test/forked -name "*.test.ts" | xargs) + +# Run tests with coverage report, then generate HTML report +coverage: + COVERAGE=true npx hardhat test --coverage + genhtml coverage/lcov.info -o coverage/html + +# Compile contracts and display contract bytecode sizes +sizes: + npx hardhat compile --force + npx tsx ./scripts/contract-sizes.ts + +# === Env-based Workflows === +# All env-based recipes take `env` as the first arg. +# The network is auto-resolved from the env name (hoodi-* -> hoodi, mainnet -> mainnet, etc.). +# Pass an explicit network override as the second arg when needed (e.g. deploy to local with hoodi config). + +# Fresh deployment (all contracts including proxies) +# Example: just deploy-fresh local +# just deploy-fresh hoodi-stage (deploys to hoodi) +# just deploy-fresh hoodi-stage local (deploys to local with hoodi-stage config) +deploy-fresh env="local" network="": + npx hardhat compile --force + npx tsx scripts/deploy-fresh.ts --env {{env}} {{ if network == "" { "" } else { "--network " + network } }} + +# Deploy implementations + modules (no proxy upgrade) +# Example: just deploy hoodi-prod +# just deploy mainnet +deploy env network="": + npx hardhat compile --force + npx tsx scripts/deploy.ts --env {{env}} {{ if network == "" { "" } else { "--network " + network } }} + +# Upgrade on fork (pre-deployment validation) +upgrade-fork env="hoodi-stage": + npx hardhat compile --force + npx tsx scripts/upgrade.ts --env {{env}} --fork --network local + +# Fork tests +test-fork env="hoodi-stage": + npx hardhat compile --force + npx tsx scripts/run-forked-tests.ts --env {{env}} --fork-network hardhat_forked --use-deployed-state true --strict-deployed-state true --allow-deployed-fallback false --no-gas-enforce true + +# End-to-end fork workflow: upgrade then run strict tests +upgrade-test-fork env="hoodi-stage": + just upgrade-fork {{env}} + just test-fork {{env}} + +# Live upgrade (owner key required) +# Example: just upgrade hoodi-stage +# just upgrade hoodi-prod +upgrade env network="": + npx hardhat compile --force + npx tsx scripts/upgrade.ts --env {{env}} {{ if network == "" { "" } else { "--network " + network } }} + +# Generate SAFE multi-sig batch +generate-safe-batch env="mainnet": + npx tsx scripts/generate-safe-batch.ts --env {{env}} + +# Generate deployment attestation (bytecode hashes + config summary for committee review) +generate-attestation env="mainnet" network="": + npx tsx scripts/generate-deployment-attestation.ts --env {{env}} {{ if network == "" { "" } else { "--network " + network } }} + +# Verify on-chain state (backward-compatible alias) +verify-upgrade env network="": + npx hardhat compile --force + npx tsx scripts/verify-post-upgrade-config.ts --env {{env}} {{ if network == "" { "" } else { "--network " + network } }} + +# Post-upgrade smoke test on a local fork of the just-upgraded network state. +# Exercises register / deposit / stake / oracle commit / liquidate / unstake end-to-end. +# Requires the mainnet fork to be running locally (or --network override). +# Example: just smoke-test mainnet local +# just smoke-test hoodi-stage local +smoke-test env network="": + npx hardhat compile --force + npx tsx scripts/smoke-test.ts --env {{env}} {{ if network == "" { "" } else { "--network " + network } }} + +# === One-off Utilities === + +# Deploy a specific module contract (e.g., SSVOperators, SSVClusters) +deploy-module module network *args: + npx hardhat compile --force + npx tsx scripts/deploy-module.ts --network {{network}} --module {{module}} {{ if args == "" { "" } else { "--args '[\"" + replace(args, " ", "\",\"") + "\"]'" } }} + +# Attach an existing deployed module to the env-configured proxy +attach-module env module module-address network="": + npx hardhat compile --force + npx tsx scripts/attach-module.ts --env {{env}} --module {{module}} --module-address {{module-address}} {{ if network == "" { "" } else { "--network " + network } }} + +# Upgrade a contract via UUPS proxy pattern (optionally with pre-deployed impl) +upgrade-contract contract proxy network *impl: + npx hardhat compile --force + npx tsx scripts/upgrade-contract.ts --network {{network}} --contract {{contract}} --proxy-address {{proxy}} {{ if impl == "" { "" } else { "--impl-address " + impl } }} + +# Verify contract source code on Etherscan/block explorer +verify address network: + npx hardhat verify --network "{{network}}" "{{address}}" + +# Export contract ABIs to JSON files for external use +abis: + npx hardhat compile --force + npx tsx scripts/common/export-abis.ts diff --git a/README.md b/README.md index 2755b3ad9..2535aa86b 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,58 @@ - # SSV Network Smart Contracts - -### Intro | [Architecture](./docs/architecture.md) | [Setup](./docs/setup.md) | [Tasks](./docs/tasks.md) | [Local development](./docs/local-dev.md) | [Roles](./docs/roles.md) | [Publish](./docs/publish.md) | [Operator owners](./docs/operators.md) -This repository contains the Solidity smart contracts for the SSV Network. The SSV Network is a decentralized network for the operation of Ethereum validators. It allows for secure, scalable, and decentralized staking on the Ethereum blockchain. The key elements of this system are represented through several Ethereum smart contracts, all of which are outlined below. +### Intro | [Architecture](./docs/architecture.md) | [Setup](./docs/setup.md) | [Tasks](./docs/tasks.md) | [Local development](./docs/local-dev.md) | [Roles](./docs/roles.md) | [Operator owners](./docs/operators.md) +### Deep docs | [Specification](./docs/SPEC.md) | [Flows](./docs/FLOWS.md) | [Mainnet upgrade playbook](./docs/UPGRADE_PLAYBOOK.md) | [Deployments](./deployments/README.md) + +This repository contains the Solidity smart contracts for the SSV Network `v2.0.0`. + +The upgraded system keeps the modular SSV Network architecture while adding ETH-based fee accounting for new clusters, effective-balance-aware charging, an oracle-driven effective balance update flow, SSV staking through `cSSV`, and one-way migration for legacy SSV clusters. The documentation is divided into different sections: -- **Architecture** Provides an overview of the system and all its components. -- **Setup** The basic setup of the repository to be able to compile the contracts, run tests, etc. -- **Tasks** Detailed instructions to run useful tools, deploy, and upgrade the contracts. -- **Local development** Guide to setup the local environment to work with the contracts. -- **Roles** Detailed information about the privileged roles in the system. +- **Architecture** explains the system layout, the contract modules, the v2 feature set, the legacy-to-ETH migration model, and the main design assumptions to keep in mind. +- **Setup** covers local prerequisites, environment configuration, and the minimum steps needed to compile and test the repository. +- **Tasks** lists the `just` recipes used for day-to-day development, testing, deployment support, and verification. +- **Local development** focuses on local deployment, fork-based upgrade validation, smoke tests, and where environment-specific configuration lives. +- **Roles** summarizes the operational actors in the system, including the owner, deployer, oracle set, operator owners, cluster owners, and stakers. +- **Operator owners** documents operator registration, privacy and whitelisting, ETH fee management, and earnings withdrawal. +- **Specification / Flows** remain the deep technical source of truth for rules, invariants, and exact execution behavior. -## SSV Documentation +## Quick start -Check the **[Smart contracts](https://docs.ssv.network/developers/smart-contracts)** official documentation for more information about contracts' functionalities, official releases, etc. +```bash +npm install +cp .env.example .env +just build +just test-unit +``` -## How to contribute +For environment-driven deployments, upgrades, SAFE batch generation, and post-upgrade verification, use [deployments/README.md](./deployments/README.md). -### Join the Buidlers +## Repository map -Start getting familiar with DVT staking, go to [SSV Discord](https://discord.gg/5vT22pRBrf) and check out `#dev-support` channel. If you cannot see it claim a role. +- `contracts/` core contracts, modules, storage libraries, interfaces, and upgrade entrypoints +- `contracts/modules/` protocol logic split across operators, clusters, validators, DAO, staking, views, and whitelisting modules +- `contracts/upgrades/mainnet/` dedicated upgrade implementation used for the v2 mainnet rollout +- `deployments/` per-environment config, results, attestations, and SAFE batch artifacts +- `scripts/` deployment, upgrade, verification, and support scripts used by the `just` recipes +- `test/` unit, integration, fork, sanity, and Echidna test suites -### Fix errors +## SSV documentation -We love to receive feedback and input from the community, so if you found a potential bug or have an enhancement you want to share, please **Open a PR!**. +Check the official [SSV Network smart contracts documentation](https://docs.ssv.network/developers/smart-contracts) for protocol-facing documentation, releases, and higher-level integration guidance. -### Suggest improvements +## How to contribute -Do you think some things could be done better in the repo or have new ideas? -**Open an issue** in the repo and share it in the `#dev-support channel`. +### Join the builders -## Bug Bounty Program +Start getting familiar with DVT staking in the [SSV Discord](https://discord.gg/5vT22pRBrf) and use the `#dev-support` channel for protocol and repository questions. + +### Suggest improvements -SSV Network is committed to ensuring the security of our smart contracts. We've partnered with [Immunefi](https://immunefi.com/) to host a dedicated bug bounty program. +If you have an improvement for the contracts, tests, or deployment flow, open an issue or PR and include the protocol or operational context behind the suggestion. -If you believe you've identified a vulnerability in our smart contracts, we encourage you to report it via our [Immunefi Bug Bounty page](https://immunefi.com/bounty/ssvnetwork/). All submissions and communications regarding vulnerabilities will be managed by the Immunefi team. +## Bug bounty program -Visit our [bounty page](https://immunefi.com/bounty/ssvnetwork/) to get detailed information on the types of vulnerabilities we're interested in, potential reward amounts, and the guidelines for participation. +SSV Network runs its smart contract bug bounty program through [Immunefi](https://immunefi.com/bounty/ssvnetwork/). -Please note: Failing to abide by the participation guidelines may result in disqualification from the program and forfeiture of potential rewards. +If you believe you found a vulnerability, report it through the [SSV Network Immunefi page](https://immunefi.com/bounty/ssvnetwork/). Follow the program rules there to stay eligible for review and rewards. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 01e3ef8a5..d13501d07 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,59 +1,143 @@ # Release Notes -## [v1.2.0] 2024-07-07 - -### Functions - -#### Removed -- `setOperatorWhitelist(uint64 operatorId, address whitelisted)` - -#### Added - -**SSVNetwork** -- `function setOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses)` -- `function removeOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses)` -- `function setOperatorsWhitelistingContract(uint64[] calldata operatorIds, ISSVWhitelistingContract whitelistingContract)` -- `function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds)` -- `function setOperatorsPublicUnchecked(uint64[] calldata operatorIds)` -- `function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds)` - -**SSVNetworkViews** -- `function getWhitelistedOperators(uint64[] calldata operatorIds, address whitelistedAddress) external view returns (uint64[] memory whitelistedOperatorIds)` -- `function isWhitelistingContract(address contractAddress) external view returns (bool)` -- `function isAddressWhitelistedInWhitelistingContract(address addressToCheck, uint256 operatorId, address whitelistingContract) external view returns (bool sWhitelisted)` - -#### Modified -- `function registerOperator(bytes calldata publicKey, uint256 fee, bool setPrivate) external returns (uint64 id)` - -### Errors - -#### New -- `error CallerNotOwnerWithData(address caller, address owner); // 0x163678e9` -- `error CallerNotWhitelistedWithData(uint64 operatorId); // 0xb7f529fe` -- `error ExceedValidatorLimitWithData(uint64 operatorId); // 0x8ddf7de4` -- `error TargetModuleDoesNotExistWithData(uint8 moduleId); // 0x208bb85d` -- `error InvalidContractAddress(); // 0xa710429d` -- `error AddressIsWhitelistingContract(address contractAddress); // 0x71cadba7` -- `error InvalidWhitelistingContract(address contractAddress); // 0x886e6a03` -- `error InvalidWhitelistAddressesLength(); // 0xcbb362dc` -- `error ZeroAddressNotAllowed(); // 0x8579befe` - -#### Deprecated -- `error CallerNotOwner(); // 0x5cd83192` -- `error CallerNotWhitelisted(); // 0x8c6e5d71` -- `error ExceedValidatorLimit(); // 0x6df5ab76` -- `error TargetModuleDoesNotExist(); // 0x8f9195fb` - -### Events - -#### Removed -- `event OperatorWhitelistUpdated(uint64 indexed operatorId, address whitelisted);` - -#### Added -- `event OperatorMultipleWhitelistUpdated(uint64[] operatorIds, address[] whitelistAddresses);` -- `event OperatorMultipleWhitelistRemoved(uint64[] operatorIds, address[] whitelistAddresses);` -- `event OperatorWhitelistingContractUpdated(uint64[] operatorIds, address whitelistingContract);` -- `event OperatorPrivacyStatusUpdated(uint64[] operatorIds, bool toPrivate);` - -### New Interface -- `interface ISSVWhitelistingContract` for whitelisting contracts. \ No newline at end of file +## [v2.0.0] + +`v2.0.0` is the main SSV staking and ETH-accounting upgrade for the SSV Network contracts. + +This release changes the network model in a material way: + +- new clusters use ETH as the fee asset +- fee accounting for ETH clusters becomes effective-balance-aware +- effective balance updates are driven by an oracle-committed Merkle root flow +- SSV staking is introduced through `cSSV` +- legacy SSV clusters remain supported in a constrained compatibility mode and can migrate one-way to ETH + +For the source-of-truth technical design, see [docs/SPEC.md](docs/SPEC.md). For exact execution behavior, see [docs/FLOWS.md](docs/FLOWS.md). For deployment and upgrade operations, see [docs/UPGRADE_PLAYBOOK.md](docs/UPGRADE_PLAYBOOK.md) and [deployments/README.md](deployments/README.md). + +### Highlights + +- **ETH-based cluster accounting** + New clusters deposit and pay in ETH instead of SSV. Operator earnings and network earnings for the ETH branch are accrued in ETH. + +- **Effective balance accounting** + ETH cluster accounting now scales with effective balance through `vUnits`, rather than flat validator-count-only charging. + +- **Oracle-based EB updates** + Oracles commit Merkle roots for cluster effective balances, and clusters consume fresh EB data through `updateClusterBalance`. + +- **SSV staking** + SSV holders can stake SSV, receive `cSSV`, and earn ETH rewards sourced from protocol fee revenue. + +- **One-way migration from legacy SSV clusters** + Existing SSV clusters can migrate to ETH. The migration path is irreversible. + +## Behavioral changes + +### ETH clusters are the forward path + +After `v2.0.0`, the protocol is intentionally split between: + +- **ETH clusters**, which are the standard path for new validator operations +- **legacy SSV clusters**, which remain for backward compatibility and migration + +### Legacy SSV clusters are restricted + +Legacy SSV clusters no longer behave like fully featured pre-upgrade clusters. + +Notable consequences: + +- new validator registration continues on the ETH path, not the legacy SSV path +- legacy SSV clusters remain supported for compatibility flows such as removal, exit, liquidation on the SSV branch, migration, and EB snapshot updates +- migration to ETH is the intended long-term path + +### Withdraw and deposit behavior changed for liquidated ETH clusters + +For ETH clusters: + +- depositing into a liquidated cluster is allowed +- withdrawing from a liquidated cluster is allowed + +This is part of the intended solvency and reactivation model, not an accidental side effect. + +### Reactivation can depend on stale EB state + +If a cluster is reactivated while its on-chain EB snapshot is stale, the solvency check may not fully reflect the latest real beacon-chain effective balance. In practice, this means operators should treat reactivation funding conservatively and not assume the on-chain EB snapshot is always current for inactive clusters. + +### Removed operators can be skipped in migration or reactivation flows + +The upgraded system tolerates some historical operator-state asymmetry. During migration or reactivation, removed operators may be skipped and the cluster can continue with reduced operator coverage if the remaining configuration is valid. + +## New protocol capabilities + +### Staking and `cSSV` + +This release introduces: + +- `stake` +- `requestUnstake` +- `withdrawUnlocked` +- `claimEthRewards` +- `syncFees` + +`cSSV` is minted on stake, burned on unstake request, and participates in reward settlement through the transfer hook integration with the staking module. + +### Oracle and governance controls + +This release adds or expands governance around: + +- oracle replacement through `replaceOracle` +- oracle quorum configuration through `updateQuorumBps` +- effective-balance update throttling through `updateMinBlocksBetweenUpdates` +- staking cooldown control through `updateUnstakeCooldownDuration` +- ETH fee and collateral configuration for the upgraded accounting model + +### Cluster migration and EB updates + +This release introduces or formalizes: + +- `migrateClusterToETH` +- `updateClusterBalance` +- the ETH reactivation, liquidation, deposit, and withdrawal model tied to EB-aware accounting + +## Module and architecture impact + +The `v2.0.0` system surface materially expands around the following modules: + +- `SSVClusters` +- `SSVValidators` +- `SSVOperators` +- `SSVDAO` +- `SSVStaking` +- `SSVViews` + +It also introduces the staking receipt token: + +- `CSSVToken` + +The mainnet rollout uses a dedicated upgrade implementation: + +- `contracts/upgrades/mainnet/SSVNetworkSSVStakingUpgrade.sol` + +## Upgrade and rollout notes + +This release is designed to be rolled out as an upgrade from `v1.2.0` to `v2.0.0`. + +Operationally important points: + +- the upgrade path uses the repository deployment and SAFE batch tooling +- `initializeSSVStaking` must run as part of the mainnet upgrade path where applicable +- environment configuration and release artifacts live under `deployments/` +- post-upgrade verification should be performed with the env-aware verification flow + +Use these documents for the rollout: + +- [docs/UPGRADE_PLAYBOOK.md](docs/UPGRADE_PLAYBOOK.md) +- [deployments/README.md](deployments/README.md) + +## Reference docs + +- [README.md](README.md) for the repository entry point +- [docs/architecture.md](docs/architecture.md) for the system overview +- [docs/SPEC.md](docs/SPEC.md) for rules, formulas, and invariants +- [docs/FLOWS.md](docs/FLOWS.md) for execution details +- [docs/operators.md](docs/operators.md) for operator-owner behavior diff --git a/abis/BasicWhitelisting.json b/abis/BasicWhitelisting.json new file mode 100644 index 000000000..99e88db5b --- /dev/null +++ b/abis/BasicWhitelisting.json @@ -0,0 +1,149 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressRemovedFromWhitelist", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressWhitelisted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "addWhitelistedAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "isWhitelisted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "removeWhitelistedAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/CSSVToken.json b/abis/CSSVToken.json new file mode 100644 index 000000000..94edff199 --- /dev/null +++ b/abis/CSSVToken.json @@ -0,0 +1,342 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "ssvStaking_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "NotSSVStaking", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "ssvStaking", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/ISSVStaking.json b/abis/ISSVStaking.json new file mode 100644 index 000000000..3a3a67e12 --- /dev/null +++ b/abis/ISSVStaking.json @@ -0,0 +1,25 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "onCSSVTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVClusters.json b/abis/SSVClusters.json new file mode 100644 index 000000000..3dcecc905 --- /dev/null +++ b/abis/SSVClusters.json @@ -0,0 +1,1203 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SafeCastOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterBalanceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ethDeposited", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ssvRefunded", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterMigratedToETH", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterReactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeExecuted", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "liquidate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "liquidateSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "migrateClusterToETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "reactivate", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + }, + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + }, + { + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + }, + { + "internalType": "bytes32[]", + "name": "merkleProof", + "type": "bytes32[]" + } + ], + "name": "updateClusterBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVDAO.json b/abis/SSVDAO.json new file mode 100644 index 000000000..6ac668f18 --- /dev/null +++ b/abis/SSVDAO.json @@ -0,0 +1,1018 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_cssv", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SafeCastOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "newCooldownDuration", + "type": "uint64" + } + ], + "name": "CooldownDurationUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "DeclareOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "ExecuteOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "LiquidationThresholdPeriodSSVUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "LiquidationThresholdPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "newMinBlocksBetweenUpdates", + "type": "uint32" + } + ], + "name": "MinBlocksBetweenUpdatesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "MinimumLiquidationCollateralSSVUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "MinimumLiquidationCollateralUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "minFee", + "type": "uint256" + } + ], + "name": "MinimumOperatorEthFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "NetworkEarningsWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "NetworkFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "NetworkFeeUpdatedSSV", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "OperatorFeeIncreaseLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "maxFee", + "type": "uint256" + } + ], + "name": "OperatorMaximumFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldOracle", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOracle", + "type": "address" + } + ], + "name": "OracleReplaced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "newQuorum", + "type": "uint16" + } + ], + "name": "QuorumUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + } + ], + "name": "RootCommitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accumulatedWeight", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "quorum", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "address", + "name": "oracle", + "type": "address" + } + ], + "name": "WeightedRootProposed", + "type": "event" + }, + { + "inputs": [], + "name": "CSSV_ADDRESS", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + } + ], + "name": "commitRoot", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + }, + { + "internalType": "address", + "name": "newOracle", + "type": "address" + } + ], + "name": "replaceOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateDeclareOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateExecuteOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blocks", + "type": "uint64" + } + ], + "name": "updateLiquidationThresholdPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blocks", + "type": "uint64" + } + ], + "name": "updateLiquidationThresholdPeriodSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxFee", + "type": "uint256" + } + ], + "name": "updateMaximumOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "blocks", + "type": "uint32" + } + ], + "name": "updateMinBlocksBetweenUpdates", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "updateMinimumLiquidationCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "updateMinimumLiquidationCollateralSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "minFee", + "type": "uint256" + } + ], + "name": "updateMinimumOperatorEthFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "updateNetworkFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "updateNetworkFeeSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "percentage", + "type": "uint64" + } + ], + "name": "updateOperatorFeeIncreaseLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "quorum", + "type": "uint16" + } + ], + "name": "updateQuorumBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + } + ], + "name": "updateUnstakeCooldownDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawNetworkSSVEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVNetwork.json b/abis/SSVNetwork.json new file mode 100644 index 000000000..a1073da1e --- /dev/null +++ b/abis/SSVNetwork.json @@ -0,0 +1,3261 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterBalanceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ethDeposited", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ssvRefunded", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterMigratedToETH", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterReactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "newCooldownDuration", + "type": "uint64" + } + ], + "name": "CooldownDurationUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "DeclareOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ERC20Rescued", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "ExecuteOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipientAddress", + "type": "address" + } + ], + "name": "FeeRecipientAddressUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newFeesWei", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accEthPerShare", + "type": "uint256" + } + ], + "name": "FeesSynced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "LiquidationThresholdPeriodSSVUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "LiquidationThresholdPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint32", + "name": "newMinBlocksBetweenUpdates", + "type": "uint32" + } + ], + "name": "MinBlocksBetweenUpdatesUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "MinimumLiquidationCollateralSSVUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "MinimumLiquidationCollateralUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "minFee", + "type": "uint256" + } + ], + "name": "MinimumOperatorEthFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "enum SSVModules", + "name": "moduleId", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "address", + "name": "moduleAddress", + "type": "address" + } + ], + "name": "ModuleUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "NetworkEarningsWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "NetworkFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "NetworkFeeUpdatedSSV", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorFeeDeclarationCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeDeclared", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "OperatorFeeIncreaseLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "maxFee", + "type": "uint256" + } + ], + "name": "OperatorMaximumFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bool", + "name": "toPrivate", + "type": "bool" + } + ], + "name": "OperatorPrivacyStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "address", + "name": "whitelisted", + "type": "address" + } + ], + "name": "OperatorWhitelistUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "OperatorWhitelistingContractUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "OperatorWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "OperatorWithdrawnSSV", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldOracle", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOracle", + "type": "address" + } + ], + "name": "OracleReplaced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "newQuorum", + "type": "uint16" + } + ], + "name": "QuorumUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RewardsClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "pending", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accrued", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "userIndex", + "type": "uint256" + } + ], + "name": "RewardsSettled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + } + ], + "name": "RootCommitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "SSVNetworkUpgradeBlock", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "unlockTime", + "type": "uint256" + } + ], + "name": "UnstakeRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "UnstakedWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "shares", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accumulatedWeight", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "quorum", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + }, + { + "indexed": false, + "internalType": "address", + "name": "oracle", + "type": "address" + } + ], + "name": "WeightedRootProposed", + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "fallback" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "bulkExitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes[]", + "name": "sharesData", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRegisterValidator", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRemoveValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "cancelDeclaredOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "claimEthRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "merkleRoot", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + } + ], + "name": "commitRoot", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "declareOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "executeOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "exitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getVersion", + "outputs": [ + { + "internalType": "string", + "name": "version", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token_", + "type": "address" + }, + { + "internalType": "contract ISSVOperators", + "name": "ssvOperators_", + "type": "address" + }, + { + "internalType": "contract ISSVClusters", + "name": "ssvClusters_", + "type": "address" + }, + { + "internalType": "contract ISSVDAO", + "name": "ssvDAO_", + "type": "address" + }, + { + "internalType": "contract ISSVViews", + "name": "ssvViews_", + "type": "address" + }, + { + "components": [ + { + "internalType": "uint64", + "name": "minimumBlocksBeforeLiquidation", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "minimumLiquidationCollateral", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validatorsPerOperatorLimit", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "declareOperatorFeePeriod", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "executeOperatorFeePeriod", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "operatorMaxFeeIncrease", + "type": "uint64" + }, + { + "internalType": "uint32[4]", + "name": "defaultOracleIds", + "type": "uint32[4]" + }, + { + "internalType": "uint16", + "name": "quorumBps", + "type": "uint16" + } + ], + "internalType": "struct ISSVNetwork.NetworkInitParams", + "name": "params", + "type": "tuple" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "liquidate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "liquidateSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "migrateClusterToETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "onCSSVTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "reactivate", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "reduceOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "setPrivate", + "type": "bool" + } + ], + "name": "registerOperator", + "outputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes", + "name": "sharesData", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "registerValidator", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "removeOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "removeOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "removeOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "removeValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + }, + { + "internalType": "address", + "name": "newOracle", + "type": "address" + } + ], + "name": "replaceOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "requestUnstake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "rescueERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipientAddress", + "type": "address" + } + ], + "name": "setFeeRecipientAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPrivateUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPublicUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "contract ISSVWhitelistingContract", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "setOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "setOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "syncFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + }, + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + }, + { + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + }, + { + "internalType": "bytes32[]", + "name": "merkleProof", + "type": "bytes32[]" + } + ], + "name": "updateClusterBalance", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateDeclareOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateExecuteOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blocks", + "type": "uint64" + } + ], + "name": "updateLiquidationThresholdPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blocks", + "type": "uint64" + } + ], + "name": "updateLiquidationThresholdPeriodSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxFee", + "type": "uint256" + } + ], + "name": "updateMaximumOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "blocks", + "type": "uint32" + } + ], + "name": "updateMinBlocksBetweenUpdates", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "updateMinimumLiquidationCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "updateMinimumLiquidationCollateralSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "minFee", + "type": "uint256" + } + ], + "name": "updateMinimumOperatorEthFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum SSVModules", + "name": "moduleId", + "type": "uint8" + }, + { + "internalType": "address", + "name": "moduleAddress", + "type": "address" + } + ], + "name": "updateModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "updateNetworkFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "updateNetworkFeeSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "percentage", + "type": "uint64" + } + ], + "name": "updateOperatorFeeIncreaseLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "quorum", + "type": "uint16" + } + ], + "name": "updateQuorumBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "duration", + "type": "uint64" + } + ], + "name": "updateUnstakeCooldownDuration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllOperatorEarningsSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllVersionOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawNetworkSSVEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawOperatorEarningsSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawUnlocked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVNetworkViews.json b/abis/SSVNetworkViews.json new file mode 100644 index 000000000..3b8ff821b --- /dev/null +++ b/abis/SSVNetworkViews.json @@ -0,0 +1,1888 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "accEthPerShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "cooldownDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getActiveOracleIds", + "outputs": [ + { + "internalType": "uint32[4]", + "name": "", + "type": "uint32[4]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBalanceSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBurnRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBurnRateSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "getClusterAssetType", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + } + ], + "name": "getCommittedRoot", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getEffectiveBalance", + "outputs": [ + { + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidationThresholdPeriod", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidationThresholdPeriodSSV", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaximumOperatorFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaximumOperatorFeeSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumLiquidationCollateral", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumLiquidationCollateralSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumOperatorEthFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkEarningsSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkFeeSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkValidatorsCount", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorById", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "address", + "name": "whitelistedAddress", + "type": "address" + }, + { + "internalType": "bool", + "name": "isPrivate", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorByIdSSV", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "address", + "name": "whitelistedAddress", + "type": "address" + }, + { + "internalType": "bool", + "name": "isPrivate", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorDeclaredFee", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "isFeeDeclared", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "approvalBeginTime", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "approvalEndTime", + "type": "uint64" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorDeclaredFeeData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "name": "getOperatorEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "name": "getOperatorEarningsSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeeIncreaseLimit", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeePeriods", + "outputs": [ + { + "components": [ + { + "internalType": "uint64", + "name": "declarePeriod", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "executePeriod", + "type": "uint64" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorFeePeriodsData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorFeeSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + } + ], + "name": "getOracle", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + } + ], + "name": "getOracleWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getQuorumBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "getValidator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidatorsPerOperatorLimit", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address", + "name": "whitelistedAddress", + "type": "address" + } + ], + "name": "getWhitelistedOperators", + "outputs": [ + { + "internalType": "uint64[]", + "name": "whitelistedOperatorIds", + "type": "uint64[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ISSVViews", + "name": "ssvNetwork_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addressToCheck", + "type": "address" + }, + { + "internalType": "uint256", + "name": "operatorId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "isAddressWhitelistedInWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "isWhitelisted", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidatableSSV", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidated", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "isWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "pendingUnstake", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "unlockTime", + "type": "uint256" + } + ], + "internalType": "struct ISSVViewsTypes.UnstakeRequestsData[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "previewClaimableEth", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "ssvNetwork", + "outputs": [ + { + "internalType": "contract ISSVViews", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "stakedBalanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stakingEthPoolBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalStaked", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVOperators.json b/abis/SSVOperators.json new file mode 100644 index 000000000..353eb3e08 --- /dev/null +++ b/abis/SSVOperators.json @@ -0,0 +1,924 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "upgradeTimestamp", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SafeCastOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipientAddress", + "type": "address" + } + ], + "name": "FeeRecipientAddressUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorFeeDeclarationCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeDeclared", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bool", + "name": "toPrivate", + "type": "bool" + } + ], + "name": "OperatorPrivacyStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "address", + "name": "whitelisted", + "type": "address" + } + ], + "name": "OperatorWhitelistUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "OperatorWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "OperatorWithdrawnSSV", + "type": "event" + }, + { + "inputs": [], + "name": "UPGRADE_TIMESTAMP", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "cancelDeclaredOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "declareOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "executeOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "reduceOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "setPrivate", + "type": "bool" + } + ], + "name": "registerOperator", + "outputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "removeOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPrivateUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPublicUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllOperatorEarningsSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllVersionOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawOperatorEarningsSSV", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVOperatorsWhitelist.json b/abis/SSVOperatorsWhitelist.json new file mode 100644 index 000000000..8b79aeb8b --- /dev/null +++ b/abis/SSVOperatorsWhitelist.json @@ -0,0 +1,577 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "OperatorWhitelistingContractUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "removeOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "removeOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "contract ISSVWhitelistingContract", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "setOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "setOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVStaking.json b/abis/SSVStaking.json new file mode 100644 index 000000000..4162669ea --- /dev/null +++ b/abis/SSVStaking.json @@ -0,0 +1,737 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_cssv", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SafeCastOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ERC20Rescued", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "newFeesWei", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accEthPerShare", + "type": "uint256" + } + ], + "name": "FeesSynced", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "RewardsClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "pending", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "accrued", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "userIndex", + "type": "uint256" + } + ], + "name": "RewardsSettled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Staked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "unlockTime", + "type": "uint256" + } + ], + "name": "UnstakeRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "UnstakedWithdrawn", + "type": "event" + }, + { + "inputs": [], + "name": "CSSV_ADDRESS", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimEthRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "onCSSVTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "requestUnstake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "rescueERC20", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "syncFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawUnlocked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVToken.json b/abis/SSVToken.json new file mode 100644 index 000000000..470d8e8a8 --- /dev/null +++ b/abis/SSVToken.json @@ -0,0 +1,378 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVValidators.json b/abis/SSVValidators.json new file mode 100644 index 000000000..1ad364e45 --- /dev/null +++ b/abis/SSVValidators.json @@ -0,0 +1,882 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SafeCastOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "shares", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorRemoved", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "bulkExitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes[]", + "name": "sharesData", + "type": "bytes[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRegisterValidator", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRemoveValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "exitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes", + "name": "sharesData", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "registerValidator", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "removeValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/abis/SSVViews.json b/abis/SSVViews.json new file mode 100644 index 000000000..a6bea9d0c --- /dev/null +++ b/abis/SSVViews.json @@ -0,0 +1,1693 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_cssv", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyVoted", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EBBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "EBExceedsMaximum", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "FutureBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "operatorVersion", + "type": "uint8" + } + ], + "name": "IncorrectOperatorVersion", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorFeeRange", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracleId", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQuorum", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidToken", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "LegacyOperatorFeeDeclarationInvalid", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPrecisionExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MaxRequestsAmountReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MustUseLatestRoot", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotCSSV", + "type": "error" + }, + { + "inputs": [], + "name": "NotOracle", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "OracleAlreadyAssigned", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "RootNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "SafeCastOverflow", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "SameOracleAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "StakeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "StaleBlockNumber", + "type": "error" + }, + { + "inputs": [], + "name": "StaleUpdate", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "UnstakeAmountExceedsBalance", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateTooFrequent", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ValidatorAlreadyRegistered", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroCSSVSupply", + "type": "error" + }, + { + "inputs": [], + "name": "CSSV_ADDRESS", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "accEthPerShare", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cooldownDuration", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getActiveOracleIds", + "outputs": [ + { + "internalType": "uint32[4]", + "name": "", + "type": "uint32[4]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBalanceSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBurnRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBurnRateSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "getClusterAssetType", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blockNum", + "type": "uint64" + } + ], + "name": "getCommittedRoot", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getEffectiveBalance", + "outputs": [ + { + "internalType": "uint32", + "name": "effectiveBalance", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidationThresholdPeriod", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidationThresholdPeriodSSV", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaximumOperatorFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaximumOperatorFeeSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumLiquidationCollateral", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumLiquidationCollateralSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumOperatorEthFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkEarningsSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkFeeSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkValidatorsCount", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorById", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "address", + "name": "whitelistedAddress", + "type": "address" + }, + { + "internalType": "bool", + "name": "isPrivate", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorData", + "name": "op", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorByIdSSV", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "address", + "name": "whitelistedAddress", + "type": "address" + }, + { + "internalType": "bool", + "name": "isPrivate", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorData", + "name": "op", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorDeclaredFee", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "isFeeDeclared", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "approvalBeginTime", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "approvalEndTime", + "type": "uint64" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorDeclaredFeeData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "name": "getOperatorEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "name": "getOperatorEarningsSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeeIncreaseLimit", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeePeriods", + "outputs": [ + { + "components": [ + { + "internalType": "uint64", + "name": "declarePeriod", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "executePeriod", + "type": "uint64" + } + ], + "internalType": "struct ISSVViewsTypes.OperatorFeePeriodsData", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorFeeSSV", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + } + ], + "name": "getOracle", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "oracleId", + "type": "uint32" + } + ], + "name": "getOracleWeight", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getQuorumBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "getValidator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidatorsPerOperatorLimit", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address", + "name": "addressToCheck", + "type": "address" + } + ], + "name": "getWhitelistedOperators", + "outputs": [ + { + "internalType": "uint64[]", + "name": "whitelistedOperatorIds", + "type": "uint64[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addressToCheck", + "type": "address" + }, + { + "internalType": "uint256", + "name": "operatorId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "isAddressWhitelistedInWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidatableSSV", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidated", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "isWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "pendingUnstake", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "unlockTime", + "type": "uint256" + } + ], + "internalType": "struct ISSVViewsTypes.UnstakeRequestsData[]", + "name": "data", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "previewClaimableEth", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "stakedBalanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "stakingEthPoolBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalStaked", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/contracts/SSVNetwork.sol b/contracts/SSVNetwork.sol index 469c4aef7..f1e5c8cdb 100644 --- a/contracts/SSVNetwork.sol +++ b/contracts/SSVNetwork.sol @@ -4,22 +4,22 @@ pragma solidity 0.8.24; import "./interfaces/ISSVNetwork.sol"; import "./interfaces/ISSVClusters.sol"; +import "./interfaces/ISSVValidators.sol"; import "./interfaces/ISSVOperators.sol"; import "./interfaces/ISSVOperatorsWhitelist.sol"; import "./interfaces/ISSVDAO.sol"; import "./interfaces/ISSVViews.sol"; - +import "./interfaces/ISSVStaking.sol"; import "./interfaces/external/ISSVWhitelistingContract.sol"; -import "./libraries/Types.sol"; -import "./libraries/CoreLib.sol"; -import "./libraries/SSVStorage.sol"; -import "./libraries/SSVStorageProtocol.sol"; +import {PackedETHLib} from "./libraries/SSVPackedLib.sol"; +import {CoreLib} from "./libraries/CoreLib.sol"; +import {StorageProtocol, SSVStorageProtocol} from "./libraries/storage/SSVStorageProtocol.sol"; +import {StorageData, SSVModules} from "./libraries/storage/SSVStorage.sol"; +import {SSVStorageStaking, StorageStaking} from "./libraries/storage/SSVStorageStaking.sol"; import "./SSVProxy.sol"; -import {SSVModules} from "./libraries/SSVStorage.sol"; - import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; @@ -31,11 +31,11 @@ contract SSVNetwork is ISSVOperators, ISSVOperatorsWhitelist, ISSVClusters, + ISSVValidators, ISSVDAO, + ISSVStaking, SSVProxy { - using Types256 for uint256; - /****************/ /* Initializers */ /****************/ @@ -46,27 +46,17 @@ contract SSVNetwork is ISSVClusters ssvClusters_, ISSVDAO ssvDAO_, ISSVViews ssvViews_, - uint64 minimumBlocksBeforeLiquidation_, - uint256 minimumLiquidationCollateral_, - uint32 validatorsPerOperatorLimit_, - uint64 declareOperatorFeePeriod_, - uint64 executeOperatorFeePeriod_, - uint64 operatorMaxFeeIncrease_ + NetworkInitParams calldata params ) external override initializer onlyProxy { __UUPSUpgradeable_init(); - __Ownable_init_unchained(); + __Ownable2Step_init(); __SSVNetwork_init_unchained( token_, ssvOperators_, ssvClusters_, ssvDAO_, ssvViews_, - minimumBlocksBeforeLiquidation_, - minimumLiquidationCollateral_, - validatorsPerOperatorLimit_, - declareOperatorFeePeriod_, - executeOperatorFeePeriod_, - operatorMaxFeeIncrease_ + params ); } @@ -76,26 +66,24 @@ contract SSVNetwork is ISSVClusters ssvClusters_, ISSVDAO ssvDAO_, ISSVViews ssvViews_, - uint64 minimumBlocksBeforeLiquidation_, - uint256 minimumLiquidationCollateral_, - uint32 validatorsPerOperatorLimit_, - uint64 declareOperatorFeePeriod_, - uint64 executeOperatorFeePeriod_, - uint64 operatorMaxFeeIncrease_ + NetworkInitParams calldata params ) internal onlyInitializing { StorageData storage s = SSVStorage.load(); StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageStaking storage ss = SSVStorageStaking.load(); s.token = token_; s.ssvContracts[SSVModules.SSV_OPERATORS] = address(ssvOperators_); s.ssvContracts[SSVModules.SSV_CLUSTERS] = address(ssvClusters_); s.ssvContracts[SSVModules.SSV_DAO] = address(ssvDAO_); s.ssvContracts[SSVModules.SSV_VIEWS] = address(ssvViews_); - sp.minimumBlocksBeforeLiquidation = minimumBlocksBeforeLiquidation_; - sp.minimumLiquidationCollateral = minimumLiquidationCollateral_.shrink(); - sp.validatorsPerOperatorLimit = validatorsPerOperatorLimit_; - sp.declareOperatorFeePeriod = declareOperatorFeePeriod_; - sp.executeOperatorFeePeriod = executeOperatorFeePeriod_; - sp.operatorMaxFeeIncrease = operatorMaxFeeIncrease_; + sp.minimumBlocksBeforeLiquidation = params.minimumBlocksBeforeLiquidation; + sp.minimumLiquidationCollateral = PackedETHLib.pack(params.minimumLiquidationCollateral); + sp.validatorsPerOperatorLimit = params.validatorsPerOperatorLimit; + sp.declareOperatorFeePeriod = params.declareOperatorFeePeriod; + sp.executeOperatorFeePeriod = params.executeOperatorFeePeriod; + sp.operatorMaxFeeIncrease = params.operatorMaxFeeIncrease; + ss.defaultOracleIds = params.defaultOracleIds; + ss.quorumBps = params.quorumBps; } /// @custom:oz-upgrades-unsafe-allow constructor @@ -158,7 +146,7 @@ contract SSVNetwork is _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS]); } - function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external { + function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS]); } @@ -190,6 +178,18 @@ contract SSVNetwork is _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS]); } + function withdrawAllVersionOperatorEarnings(uint64 operatorId) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS]); + } + + function withdrawOperatorEarningsSSV(uint64 operatorId, uint256 amount) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS]); + } + + function withdrawAllOperatorEarningsSSV(uint64 operatorId) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS]); + } + /*******************************/ /* Address External Functions */ /*******************************/ @@ -198,6 +198,38 @@ contract SSVNetwork is emit FeeRecipientAddressUpdated(msg.sender, recipientAddress); } + /*******************************/ + /* Staking External Functions */ + /*******************************/ + + function syncFees() external { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_STAKING]); + } + + function stake(uint256 amount) external { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_STAKING]); + } + + function requestUnstake(uint256 amount) external { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_STAKING]); + } + + function withdrawUnlocked() external { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_STAKING]); + } + + function claimEthRewards() external { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_STAKING]); + } + + function rescueERC20(address token, address to, uint256 amount) external onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_STAKING]); + } + + function onCSSVTransfer(address from, address to, uint256 amount) external { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_STAKING]); + } + /*******************************/ /* Validator External Functions */ /*******************************/ @@ -206,20 +238,18 @@ contract SSVNetwork is bytes calldata publicKey, uint64[] calldata operatorIds, bytes calldata sharesData, - uint256 amount, ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); + ) external payable override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_VALIDATORS]); } function bulkRegisterValidator( bytes[] calldata publicKeys, uint64[] calldata operatorIds, bytes[] calldata sharesData, - uint256 amount, ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); + ) external payable override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_VALIDATORS]); } function removeValidator( @@ -227,7 +257,7 @@ contract SSVNetwork is uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster ) external override { - _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_VALIDATORS]); } function bulkRemoveValidator( @@ -235,31 +265,37 @@ contract SSVNetwork is uint64[] calldata operatorIds, Cluster memory cluster ) external override { - _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_VALIDATORS]); } function liquidate( address clusterOwner, uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster - ) external { + ) external override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } - function reactivate( + function liquidateSSV( + address clusterOwner, uint64[] calldata operatorIds, - uint256 amount, ISSVNetworkCore.Cluster memory cluster ) external override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } + function reactivate( + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external payable override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); + } + function deposit( address clusterOwner, uint64[] calldata operatorIds, - uint256 amount, ISSVNetworkCore.Cluster memory cluster - ) external override { + ) external payable override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } @@ -271,19 +307,41 @@ contract SSVNetwork is _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } - function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override { + function updateClusterBalance( + uint64 blockNum, + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint32 effectiveBalance, + bytes32[] calldata merkleProof + ) external override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } - function bulkExitValidator(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external override { + function migrateClusterToETH( + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external payable override { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS]); } + function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_VALIDATORS]); + } + + function bulkExitValidator(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_VALIDATORS]); + } + function updateNetworkFee(uint256 fee) external override onlyOwner { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); } - function withdrawNetworkEarnings(uint256 amount) external override onlyOwner { + function updateNetworkFeeSSV(uint256 fee) external override onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function withdrawNetworkSSVEarnings(uint256 amount) external override onlyOwner { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); } @@ -303,11 +361,43 @@ contract SSVNetwork is _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); } + function updateLiquidationThresholdPeriodSSV(uint64 blocks) external onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + function updateMinimumLiquidationCollateral(uint256 amount) external override onlyOwner { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); } - function updateMaximumOperatorFee(uint64 maxFee) external override onlyOwner { + function updateMinimumLiquidationCollateralSSV(uint256 amount) external onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function updateMaximumOperatorFee(uint256 maxFee) external override onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function updateMinimumOperatorEthFee(uint256 minFee) external override onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function commitRoot(bytes32 merkleRoot, uint64 blockNum) external override { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function updateUnstakeCooldownDuration(uint64 duration) external onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function updateMinBlocksBetweenUpdates(uint32 blocks) external override onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function replaceOracle(uint32 oracleId, address newOracle) external override onlyOwner { + _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); + } + + function updateQuorumBps(uint16 quorum) external override onlyOwner { _delegate(SSVStorage.load().ssvContracts[SSVModules.SSV_DAO]); } diff --git a/contracts/SSVNetworkViews.sol b/contracts/SSVNetworkViews.sol index 18159e581..12a5e5151 100644 --- a/contracts/SSVNetworkViews.sol +++ b/contracts/SSVNetworkViews.sol @@ -2,17 +2,15 @@ pragma solidity 0.8.24; import "./interfaces/ISSVViews.sol"; -import "./libraries/Types.sol"; import "./libraries/ClusterLib.sol"; import "./libraries/OperatorLib.sol"; import "./libraries/ProtocolLib.sol"; +import {MAX_DELEGATION_SLOTS} from "./libraries/storage/SSVStorageStaking.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews { - using Types256 for uint256; - using Types64 for uint64; using ClusterLib for Cluster; using OperatorLib for Operator; @@ -31,7 +29,7 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews function initialize(ISSVViews ssvNetwork_) external initializer onlyProxy { __UUPSUpgradeable_init(); - __Ownable_init_unchained(); + __Ownable2Step_init(); ssvNetwork = ssvNetwork_; } @@ -51,16 +49,26 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews return ssvNetwork.getOperatorFee(operatorId); } - function getOperatorDeclaredFee(uint64 operatorId) external view override returns (bool, uint256, uint64, uint64) { + function getOperatorFeeSSV(uint64 operatorId) external view override returns (uint256) { + return ssvNetwork.getOperatorFeeSSV(operatorId); + } + + function getOperatorDeclaredFee(uint64 operatorId) external view override returns (OperatorDeclaredFeeData memory) { return ssvNetwork.getOperatorDeclaredFee(operatorId); } function getOperatorById( uint64 operatorId - ) external view override returns (address, uint256, uint32, address, bool, bool) { + ) external view override returns (OperatorData memory) { return ssvNetwork.getOperatorById(operatorId); } + function getOperatorByIdSSV( + uint64 operatorId + ) external view override returns (OperatorData memory) { + return ssvNetwork.getOperatorByIdSSV(operatorId); + } + function getWhitelistedOperators( uint64[] calldata operatorIds, address whitelistedAddress @@ -92,6 +100,14 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews return ssvNetwork.isLiquidatable(clusterOwner, operatorIds, cluster); } + function isLiquidatableSSV( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view override returns (bool) { + return ssvNetwork.isLiquidatableSSV(clusterOwner, operatorIds, cluster); + } + function isLiquidated( address clusterOwner, uint64[] calldata operatorIds, @@ -104,10 +120,18 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster - ) external view returns (uint256) { + ) external view override returns (uint256) { return ssvNetwork.getBurnRate(clusterOwner, operatorIds, cluster); } + function getBurnRateSSV( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view override returns (uint256) { + return ssvNetwork.getBurnRateSSV(clusterOwner, operatorIds, cluster); + } + /***********************************/ /* Balance External View Functions */ /***********************************/ @@ -116,14 +140,34 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews return ssvNetwork.getOperatorEarnings(id); } + function getOperatorEarningsSSV(uint64 id) external view override returns (uint256) { + return ssvNetwork.getOperatorEarningsSSV(id); + } + function getBalance( address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster - ) external view override returns (uint256) { + ) external view override returns (uint256 balance) { return ssvNetwork.getBalance(clusterOwner, operatorIds, cluster); } + function getBalanceSSV( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view override returns (uint256 balance) { + return ssvNetwork.getBalanceSSV(clusterOwner, operatorIds, cluster); + } + + function getEffectiveBalance( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view returns (uint32 effectiveBalance) { + return ssvNetwork.getEffectiveBalance(clusterOwner, operatorIds, cluster); + } + /*******************************/ /* DAO External View Functions */ /*******************************/ @@ -136,15 +180,31 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews return ssvNetwork.getNetworkEarnings(); } + function getNetworkFeeSSV() external view override returns (uint256) { + return ssvNetwork.getNetworkFeeSSV(); + } + + function getNetworkEarningsSSV() external view override returns (uint256) { + return ssvNetwork.getNetworkEarningsSSV(); + } + function getOperatorFeeIncreaseLimit() external view override returns (uint64) { return ssvNetwork.getOperatorFeeIncreaseLimit(); } - function getMaximumOperatorFee() external view override returns (uint64) { + function getMaximumOperatorFee() external view override returns (uint256) { return ssvNetwork.getMaximumOperatorFee(); } - function getOperatorFeePeriods() external view override returns (uint64, uint64) { + function getMaximumOperatorFeeSSV() external view override returns (uint256) { + return ssvNetwork.getMaximumOperatorFeeSSV(); + } + + function getMinimumOperatorEthFee() external view override returns (uint256) { + return ssvNetwork.getMinimumOperatorEthFee(); + } + + function getOperatorFeePeriods() external view override returns (OperatorFeePeriodsData memory) { return ssvNetwork.getOperatorFeePeriods(); } @@ -152,10 +212,18 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews return ssvNetwork.getLiquidationThresholdPeriod(); } + function getLiquidationThresholdPeriodSSV() external view override returns (uint64) { + return ssvNetwork.getLiquidationThresholdPeriodSSV(); + } + function getMinimumLiquidationCollateral() external view override returns (uint256) { return ssvNetwork.getMinimumLiquidationCollateral(); } + function getMinimumLiquidationCollateralSSV() external view override returns (uint256) { + return ssvNetwork.getMinimumLiquidationCollateralSSV(); + } + function getValidatorsPerOperatorLimit() external view override returns (uint32) { return ssvNetwork.getValidatorsPerOperatorLimit(); } @@ -164,6 +232,58 @@ contract SSVNetworkViews is UUPSUpgradeable, Ownable2StepUpgradeable, ISSVViews return ssvNetwork.getNetworkValidatorsCount(); } + function getClusterAssetType(address owner, uint64[] calldata operatorIds) external view override returns (uint8) { + return ssvNetwork.getClusterAssetType(owner, operatorIds); + } + + function cooldownDuration() external view override returns (uint256) { + return ssvNetwork.cooldownDuration(); + } + + function totalStaked() external view override returns (uint256) { + return ssvNetwork.totalStaked(); + } + + function stakedBalanceOf(address user) external view override returns (uint256) { + return ssvNetwork.stakedBalanceOf(user); + } + + function pendingUnstake(address user) external view override returns (UnstakeRequestsData[] memory) { + return ssvNetwork.pendingUnstake(user); + } + + function accEthPerShare() external view override returns (uint256) { + return ssvNetwork.accEthPerShare(); + } + + function stakingEthPoolBalance() external view override returns (uint256) { + return ssvNetwork.stakingEthPoolBalance(); + } + + function previewClaimableEth(address user) external view override returns (uint256) { + return ssvNetwork.previewClaimableEth(user); + } + + function getOracle(uint32 oracleId) external view override returns (address) { + return ssvNetwork.getOracle(oracleId); + } + + function getOracleWeight(uint32 oracleId) external view override returns (uint256) { + return ssvNetwork.getOracleWeight(oracleId); + } + + function getActiveOracleIds() external view override returns (uint32[MAX_DELEGATION_SLOTS] memory) { + return ssvNetwork.getActiveOracleIds(); + } + + function getQuorumBps() external view override returns (uint16) { + return ssvNetwork.getQuorumBps(); + } + + function getCommittedRoot(uint64 blockNum) external view override returns (bytes32) { + return ssvNetwork.getCommittedRoot(blockNum); + } + function getVersion() external view override returns (string memory) { return ssvNetwork.getVersion(); } diff --git a/contracts/SSVProxy.sol b/contracts/SSVProxy.sol index 82c820e35..0bd2e73a0 100644 --- a/contracts/SSVProxy.sol +++ b/contracts/SSVProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import {SSVModules, SSVStorage, StorageData} from "./libraries/SSVStorage.sol"; +import {SSVModules, SSVStorage, StorageData} from "./libraries/storage/SSVStorage.sol"; abstract contract SSVProxy { function _delegate(address implementation) internal { diff --git a/contracts/abstract/SSVReentrancyGuard.sol b/contracts/abstract/SSVReentrancyGuard.sol new file mode 100644 index 000000000..462d94b5c --- /dev/null +++ b/contracts/abstract/SSVReentrancyGuard.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {SSVReentrancyGuardLib} from "../libraries/SSVReentrancyGuardLib.sol"; + +/** + * @title SSV Reentrancy Guard + * @author SSV Labs + * @dev An abstract contract that provides a modifier to prevent reentrancy attacks + */ +abstract contract SSVReentrancyGuard { + /** + * @dev Prevents a contract from re-calling itself, directly or indirectly + */ + modifier nonReentrant() { + SSVReentrancyGuardLib._nonReentrantBefore(); + _; + SSVReentrancyGuardLib._nonReentrantAfter(); + } +} \ No newline at end of file diff --git a/contracts/audits/2026-04-10_Quantstamp_v2.0.0.pdf b/contracts/audits/2026-04-10_Quantstamp_v2.0.0.pdf new file mode 100644 index 000000000..05c0f5da6 Binary files /dev/null and b/contracts/audits/2026-04-10_Quantstamp_v2.0.0.pdf differ diff --git a/contracts/interfaces/ICSSVToken.sol b/contracts/interfaces/ICSSVToken.sol new file mode 100644 index 000000000..cbc03ba32 --- /dev/null +++ b/contracts/interfaces/ICSSVToken.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title CSSV Token Interface + * @author SSV Labs + */ +interface ICSSVToken is IERC20 { + /** + * @dev Mints a specified amount of tokens to an address + * @param to The address that will receive the minted tokens + * @param amount The amount of tokens to mint + */ + function mint(address to, uint256 amount) external; + + /** + * @dev Burns a specified amount of tokens from an address + * @param from The address from which tokens will be burned + * @param amount The amount of tokens to burn + */ + function burn(address from, uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/interfaces/ISSVClusters.sol b/contracts/interfaces/ISSVClusters.sol index 0da81ff81..f8c6e51dc 100644 --- a/contracts/interfaces/ISSVClusters.sol +++ b/contracts/interfaces/ISSVClusters.sol @@ -3,151 +3,162 @@ pragma solidity ^0.8.20; import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; +/** + * @title SSV Clusters Interface + * @author SSV Labs + * @dev Interface for managing SSV clusters, including migration, liquidation, reactivation, deposits, withdrawals, and balance updates + */ interface ISSVClusters is ISSVNetworkCore { - /// @notice Registers a new validator on the SSV Network - /// @param publicKey The public key of the new validator - /// @param operatorIds Array of IDs of operators managing this validator - /// @param sharesData Encrypted shares related to the new validator - /// @param amount Amount of SSV tokens to be deposited - /// @param cluster Cluster to be used with the new validator - function registerValidator( - bytes calldata publicKey, - uint64[] memory operatorIds, - bytes calldata sharesData, - uint256 amount, - Cluster memory cluster - ) external; + /// @dev Context structure for updating cluster balances + struct UpdateCtx { + /// @dev The owner of the cluster + address clusterOwner; + /// @dev The unique identifier for the cluster + bytes32 clusterId; + /// @dev The block number for the update + uint64 blockNum; + /// @dev The effective balance of the cluster + uint32 effectiveBalance; + /// @dev The merkle proof for validation + bytes32[] merkleProof; + /// @dev The version of the cluster + uint8 version; + } - /// @notice Registers new validators on the SSV Network - /// @param publicKeys The public keys of the new validators - /// @param operatorIds Array of IDs of operators managing this validator - /// @param sharesData Encrypted shares related to the new validators - /// @param amount Amount of SSV tokens to be deposited - /// @param cluster Cluster to be used with the new validator - function bulkRegisterValidator( - bytes[] calldata publicKeys, - uint64[] memory operatorIds, - bytes[] calldata sharesData, - uint256 amount, - Cluster memory cluster - ) external; + /** + * @dev Emitted when a cluster is liquidated + * @param owner The owner of the liquidated cluster + * @param operatorIds The operator IDs managing the cluster + * @param cluster The liquidated cluster data + */ + event ClusterLiquidated(address indexed owner, uint64[] operatorIds, Cluster cluster); - /// @notice Removes an existing validator from the SSV Network - /// @param publicKey The public key of the validator to be removed - /// @param operatorIds Array of IDs of operators managing the validator - /// @param cluster Cluster associated with the validator - function removeValidator(bytes calldata publicKey, uint64[] memory operatorIds, Cluster memory cluster) external; - - /// @notice Bulk removes a set of existing validators in the same cluster from the SSV Network - /// @notice Reverts if publicKeys contains duplicates or non-existent validators - /// @param publicKeys The public keys of the validators to be removed - /// @param operatorIds Array of IDs of operators managing the validator - /// @param cluster Cluster associated with the validator - function bulkRemoveValidator( - bytes[] calldata publicKeys, - uint64[] memory operatorIds, - Cluster memory cluster - ) external; + /** + * @dev Emitted when a cluster is reactivated + * @param owner The owner of the reactivated cluster + * @param operatorIds The operator IDs managing the cluster + * @param cluster The reactivated cluster data + */ + event ClusterReactivated(address indexed owner, uint64[] operatorIds, Cluster cluster); - /**************************/ - /* Cluster External Functions */ - /**************************/ + /** + * @dev Emitted when a legacy SSV cluster is migrated to ETH + * @param owner The owner of the migrated cluster + * @param operatorIds The operator IDs managing the cluster + * @param ethDeposited The amount of ETH supplied during migration + * @param ssvRefunded The amount of SSV tokens refunded to the owner + * @param effectiveBalance Cluster effective balance in wei + * @param cluster The migrated cluster data (ETH version) + */ + event ClusterMigratedToETH( + address indexed owner, + uint64[] operatorIds, + uint256 ethDeposited, + uint256 ssvRefunded, + uint32 effectiveBalance, + Cluster cluster + ); - /// @notice Liquidates a cluster - /// @param owner The owner of the cluster - /// @param operatorIds Array of IDs of operators managing the cluster - /// @param cluster Cluster to be liquidated - function liquidate(address owner, uint64[] memory operatorIds, Cluster memory cluster) external; + /** + * @dev Emitted when tokens are withdrawn from a cluster + * @param owner The owner of the cluster + * @param operatorIds The operator IDs managing the cluster + * @param value The amount of tokens withdrawn + * @param cluster The cluster from which tokens were withdrawn + */ + event ClusterWithdrawn(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); - /// @notice Reactivates a cluster - /// @param operatorIds Array of IDs of operators managing the cluster - /// @param amount Amount of SSV tokens to be deposited for reactivation - /// @param cluster Cluster to be reactivated - function reactivate(uint64[] memory operatorIds, uint256 amount, Cluster memory cluster) external; - - /******************************/ - /* Balance External Functions */ - /******************************/ - - /// @notice Deposits tokens into a cluster - /// @param owner The owner of the cluster - /// @param operatorIds Array of IDs of operators managing the cluster - /// @param amount Amount of SSV tokens to be deposited - /// @param cluster Cluster where the deposit will be made - function deposit(address owner, uint64[] memory operatorIds, uint256 amount, Cluster memory cluster) external; - - /// @notice Withdraws tokens from a cluster - /// @param operatorIds Array of IDs of operators managing the cluster - /// @param tokenAmount Amount of SSV tokens to be withdrawn - /// @param cluster Cluster where the withdrawal will be made - function withdraw(uint64[] memory operatorIds, uint256 tokenAmount, Cluster memory cluster) external; + /** + * @dev Emitted when tokens are deposited into a cluster + * @param owner The owner of the cluster + * @param operatorIds The operator IDs managing the cluster + * @param value The amount of ETH deposited + * @param cluster The cluster into which ETH was deposited + */ + event ClusterDeposited(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); - /// @notice Fires the exit event for a validator - /// @param publicKey The public key of the validator to be exited - /// @param operatorIds Array of IDs of operators managing the validator - function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external; + /** + * @dev Emitted when a cluster's balance is updated + * @param owner The owner of the cluster + * @param operatorIds The operator IDs managing the cluster + * @param blockNum The block number of the update + * @param effectiveBalance The new effective balance + * @param cluster The updated cluster data + */ + event ClusterBalanceUpdated( + address indexed owner, + uint64[] operatorIds, + uint64 indexed blockNum, + uint32 effectiveBalance, + ISSVNetworkCore.Cluster cluster + ); - /// @notice Fires the exit event for a set of validators - /// @param publicKeys The public keys of the validators to be exited - /// @param operatorIds Array of IDs of operators managing the validators - function bulkExitValidator(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external; /** - * @dev Emitted when the validator has been added. - * @param publicKey The public key of a validator. - * @param operatorIds The operator ids list. - * @param shares snappy compressed shares(a set of encrypted and public shares). - * @param cluster All the cluster data. + * @notice Migrates an SSV cluster to ETH, returning any SSV balance and accepting ETH top-up + * @param operatorIds Array of IDs of operators managing the cluster + * @param cluster Cluster data to migrate */ - event ValidatorAdded(address indexed owner, uint64[] operatorIds, bytes publicKey, bytes shares, Cluster cluster); + function migrateClusterToETH(uint64[] calldata operatorIds, Cluster memory cluster) external payable; /** - * @dev Emitted when the validator is removed. - * @param publicKey The public key of a validator. - * @param operatorIds The operator ids list. - * @param cluster All the cluster data. + * @notice Liquidates a cluster + * @param owner The owner of the cluster + * @param operatorIds Array of IDs of operators managing the cluster + * @param cluster Cluster to be liquidated */ - event ValidatorRemoved(address indexed owner, uint64[] operatorIds, bytes publicKey, Cluster cluster); + function liquidate(address owner, uint64[] memory operatorIds, Cluster memory cluster) external; /** - * @dev Emitted when a cluster is liquidated. - * @param owner The owner of the liquidated cluster. - * @param operatorIds The operator IDs managing the cluster. - * @param cluster The liquidated cluster data. + * @notice Liquidates a cluster using SSV + * @param owner The owner of the cluster + * @param operatorIds Array of IDs of operators managing the cluster + * @param cluster Cluster to be liquidated */ - event ClusterLiquidated(address indexed owner, uint64[] operatorIds, Cluster cluster); + function liquidateSSV(address owner, uint64[] memory operatorIds, Cluster memory cluster) external; /** - * @dev Emitted when a cluster is reactivated. - * @param owner The owner of the reactivated cluster. - * @param operatorIds The operator IDs managing the cluster. - * @param cluster The reactivated cluster data. + * @notice Reactivates a cluster + * @param operatorIds Array of IDs of operators managing the cluster + * @param cluster Cluster to be reactivated */ - event ClusterReactivated(address indexed owner, uint64[] operatorIds, Cluster cluster); + function reactivate(uint64[] memory operatorIds, Cluster memory cluster) external payable; /** - * @dev Emitted when tokens are withdrawn from a cluster. - * @param owner The owner of the cluster. - * @param operatorIds The operator IDs managing the cluster. - * @param value The amount of tokens withdrawn. - * @param cluster The cluster from which tokens were withdrawn. + * @notice Deposits ETH into a cluster + * @param owner The owner of the cluster + * @param operatorIds Array of IDs of operators managing the cluster + * @param cluster Cluster to which the deposit will be made */ - event ClusterWithdrawn(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); + function deposit( + address owner, + uint64[] memory operatorIds, + Cluster memory cluster + ) external payable; /** - * @dev Emitted when tokens are deposited into a cluster. - * @param owner The owner of the cluster. - * @param operatorIds The operator IDs managing the cluster. - * @param value The amount of SSV tokens deposited. - * @param cluster The cluster into which SSV tokens were deposited. + * @notice Withdraws ETH from a cluster + * @param operatorIds Array of IDs of operators managing the cluster + * @param tokenAmount Amount of SSV tokens to be withdrawn + * @param cluster Cluster where the withdrawal will be made */ - event ClusterDeposited(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); + function withdraw(uint64[] memory operatorIds, uint256 tokenAmount, Cluster memory cluster) external; /** - * @dev Emitted when a validator begins the exit process. - * @param owner The owner of the exiting validator. - * @param operatorIds The operator IDs managing the validator. - * @param publicKey The public key of the exiting validator. + * @notice Updates the balance of a cluster + * @param blockNum The block number for the update + * @param clusterOwner The owner of the cluster + * @param operatorIds Array of operator IDs + * @param cluster The cluster data. + * @param effectiveBalance The new effective balance + * @param merkleProof The merkle proof for validation */ - event ValidatorExited(address indexed owner, uint64[] operatorIds, bytes publicKey); + function updateClusterBalance( + uint64 blockNum, + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster, + uint32 effectiveBalance, + bytes32[] calldata merkleProof + ) external; } diff --git a/contracts/interfaces/ISSVDAO.sol b/contracts/interfaces/ISSVDAO.sol index a91dc3b52..0f51d6602 100644 --- a/contracts/interfaces/ISSVDAO.sol +++ b/contracts/interfaces/ISSVDAO.sol @@ -3,62 +3,233 @@ pragma solidity ^0.8.20; import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; +/** + * @title SSV DAO Interface + * @author SSV Labs + * @dev Interface for DAO operations in the SSV network, including fee updates, period adjustments, and other governance functions + */ interface ISSVDAO is ISSVNetworkCore { - /// @notice Updates the network fee - /// @param fee The new network fee (SSV) to be set + + /** + * @dev Emitted when the operator fee increase limit is updated + * @param value The new limit value + */ + event OperatorFeeIncreaseLimitUpdated(uint64 value); + + /** + * @dev Emitted when the declare operator fee period is updated + * @param value The new period value in seconds + */ + event DeclareOperatorFeePeriodUpdated(uint64 value); + + /** + * @dev Emitted when the execute operator fee period is updated + * @param value The new period value in seconds + */ + event ExecuteOperatorFeePeriodUpdated(uint64 value); + + /** + * @dev Emitted when the liquidation threshold period is updated + * @param value The new threshold in blocks + */ + event LiquidationThresholdPeriodUpdated(uint64 value); + + /** + * @dev Emitted when the SSV liquidation threshold period is updated + * @param value The new threshold in blocks + */ + event LiquidationThresholdPeriodSSVUpdated(uint64 value); + + /** + * @dev Emitted when the minimum liquidation collateral is updated + * @param value The new collateral amount + */ + event MinimumLiquidationCollateralUpdated(uint256 value); + + /** + * @dev Emitted when the SSV minimum liquidation collateral is updated + * @param value The new collateral amount + */ + event MinimumLiquidationCollateralSSVUpdated(uint256 value); + + /** + * @dev Emitted when the network fee is updated + * @param oldFee The previous fee + * @param newFee The new fee + */ + event NetworkFeeUpdated(uint256 oldFee, uint256 newFee); + + /** + * @dev Emitted when the SSV network fee is updated + * @param oldFee The previous fee + * @param newFee The new fee + */ + event NetworkFeeUpdatedSSV(uint256 oldFee, uint256 newFee); + + /** + * @dev Emitted when network earnings are withdrawn + * @param value The amount withdrawn + * @param recipient The address receiving the funds + */ + event NetworkEarningsWithdrawn(uint256 value, address recipient); + + /** + * @dev Emitted when the maximum operator fee is updated + * @param maxFee The new maximum fee + */ + event OperatorMaximumFeeUpdated(uint256 maxFee); + + /** + * @dev Emitted when the minimum operator ETH fee is updated + * @param minFee The new minimum fee + */ + event MinimumOperatorEthFeeUpdated(uint256 minFee); + + /** + * @dev Emitted when an EB Merkle root is committed for a given block + * @param merkleRoot The committed Merkle root + * @param blockNum The block number the root corresponds to + */ + event RootCommitted(bytes32 indexed merkleRoot, uint64 indexed blockNum); + + /** + * @dev Emitted when the unstake cooldown duration is updated + * @param newCooldownDuration The new duration in seconds + */ + event CooldownDurationUpdated(uint64 newCooldownDuration); + + /** + * @dev Emitted when a weighted root is proposed + * @param merkleRoot The proposed Merkle root + * @param blockNum The block number + * @param accumulatedWeight The accumulated weight + * @param quorum The quorum value + * @param oracleId The oracle ID + * @param oracle The oracle address + */ + event WeightedRootProposed(bytes32 indexed merkleRoot, uint64 indexed blockNum, uint256 accumulatedWeight, uint256 quorum, uint32 oracleId, address oracle); + + /** + * @dev Emitted when an oracle is replaced + * @param oracleId The oracle ID + * @param oldOracle The old oracle address + * @param newOracle The new oracle address + */ + event OracleReplaced(uint32 indexed oracleId, address indexed oldOracle, address indexed newOracle); + + /** + * @dev Emitted when the quorum is updated + * @param newQuorum The new quorum value + */ + event QuorumUpdated(uint16 newQuorum); + + /** + * @dev Emitted when the minimum block interval between EB updates is updated + * @param newMinBlocksBetweenUpdates The new minimum block interval + */ + event MinBlocksBetweenUpdatesUpdated(uint32 newMinBlocksBetweenUpdates); + + /** + * @notice Updates the network fee (ETH post-migration) + * @param fee The new network fee (ETH) to be set + */ function updateNetworkFee(uint256 fee) external; - /// @notice Withdraws network earnings - /// @param amount The amount (SSV) to be withdrawn - function withdrawNetworkEarnings(uint256 amount) external; + /** + * @notice Updates the legacy network fee (SSV pre-migration) + * @param fee The new network fee (SSV) to be set + */ + function updateNetworkFeeSSV(uint256 fee) external; - /// @notice Updates the limit on the percentage increase in operator fees - /// @param percentage The new percentage limit + /** + * @notice Withdraws legacy network earnings (SSV pre-migration) + * @param amount The amount (SSV) to be withdrawn + */ + function withdrawNetworkSSVEarnings(uint256 amount) external; + + /** + * @notice Updates the limit on the percentage increase in operator fees + * @param percentage The new percentage limit + */ function updateOperatorFeeIncreaseLimit(uint64 percentage) external; - /// @notice Updates the period for declaring operator fees - /// @param timeInSeconds The new period in seconds + /** + * @notice Updates the period for declaring operator fees + * @param timeInSeconds The new period in seconds + */ function updateDeclareOperatorFeePeriod(uint64 timeInSeconds) external; - /// @notice Updates the period for executing operator fees - /// @param timeInSeconds The new period in seconds + /** + * @notice Updates the period for executing operator fees + * @param timeInSeconds The new period in seconds + */ function updateExecuteOperatorFeePeriod(uint64 timeInSeconds) external; - /// @notice Updates the liquidation threshold period - /// @param blocks The new liquidation threshold in blocks + /** + * @notice Updates the liquidation threshold period + * @param blocks The new liquidation threshold in blocks + */ function updateLiquidationThresholdPeriod(uint64 blocks) external; - /// @notice Updates the minimum collateral required to prevent liquidation - /// @param amount The new minimum collateral amount (SSV) - function updateMinimumLiquidationCollateral(uint256 amount) external; + /** + * @notice Updates the SSV liquidation threshold period + * @param blocks The new liquidation threshold in blocks + */ + function updateLiquidationThresholdPeriodSSV(uint64 blocks) external; - /// @notice Updates the maximum fee an operator that uses SSV token can set - /// @param maxFee The new maximum fee (SSV) - function updateMaximumOperatorFee(uint64 maxFee) external; + /** + * @notice Updates the minimum collateral required to prevent liquidation + * @param amount The new minimum collateral amount + */ + function updateMinimumLiquidationCollateral(uint256 amount) external; - event OperatorFeeIncreaseLimitUpdated(uint64 value); + /** + * @notice Updates the SSV minimum collateral required to prevent liquidation + * @param amount The new minimum collateral amount (SSV) + */ + function updateMinimumLiquidationCollateralSSV(uint256 amount) external; - event DeclareOperatorFeePeriodUpdated(uint64 value); + /** + * @notice Updates the maximum fee an operator that uses SSV token can set + * @param maxFee The new maximum fee + */ + function updateMaximumOperatorFee(uint256 maxFee) external; - event ExecuteOperatorFeePeriodUpdated(uint64 value); + /** + * @notice Updates the minimum operator ETH fee + * @param minFee The new minimum fee (ETH) + */ + function updateMinimumOperatorEthFee(uint256 minFee) external; - event LiquidationThresholdPeriodUpdated(uint64 value); + /** + * @notice Commit Merkle root of all cluster EBs + * @param merkleRoot Root of Merkle tree containing all cluster EBs + * @param blockNum Block number when oracle computed this data (must be finalized and strictly increasing) + */ + function commitRoot(bytes32 merkleRoot, uint64 blockNum) external; - event MinimumLiquidationCollateralUpdated(uint256 value); + /** + * @notice Sets the unstake cooldown duration + * @param duration The new duration in seconds + */ + function updateUnstakeCooldownDuration(uint64 duration) external; /** - * @dev Emitted when the network fee is updated. - * @param oldFee The old fee - * @param newFee The new fee + * @notice Sets the minimum block interval between EB updates for the same cluster + * @param blocks The new minimum interval in blocks (must be non-zero) */ - event NetworkFeeUpdated(uint256 oldFee, uint256 newFee); + function updateMinBlocksBetweenUpdates(uint32 blocks) external; /** - * @dev Emitted when transfer fees are withdrawn. - * @param value The amount of tokens withdrawn. - * @param recipient The recipient address. + * @notice Replace oracle address at a stable oracle ID + * @param oracleId Stable oracle ID to update + * @param newOracle New oracle address */ - event NetworkEarningsWithdrawn(uint256 value, address recipient); + function replaceOracle(uint32 oracleId, address newOracle) external; - event OperatorMaximumFeeUpdated(uint64 maxFee); + /** + * @notice Sets the quorum BPS + * @param quorum The new quorum value + */ + function updateQuorumBps(uint16 quorum) external; } diff --git a/contracts/interfaces/ISSVNetwork.sol b/contracts/interfaces/ISSVNetwork.sol index b71465c0d..a8a19caa5 100644 --- a/contracts/interfaces/ISSVNetwork.sol +++ b/contracts/interfaces/ISSVNetwork.sol @@ -1,34 +1,86 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.20; -import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; import {ISSVOperators} from "./ISSVOperators.sol"; import {ISSVClusters} from "./ISSVClusters.sol"; +import {ISSVValidators} from "./ISSVValidators.sol"; import {ISSVDAO} from "./ISSVDAO.sol"; import {ISSVViews} from "./ISSVViews.sol"; -import {SSVModules} from "../libraries/SSVStorage.sol"; +import {SSVModules} from "../libraries/storage/SSVStorage.sol"; +import {MAX_DELEGATION_SLOTS} from "../libraries/storage/SSVStorageStaking.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +/** + * @title SSV Network Interface + * @author SSV Labs + * @dev Main interface for the SSV Network contract, providing initialization and configuration functions + */ interface ISSVNetwork { + /** + * @dev Struct containing initialization parameters for the network + */ + struct NetworkInitParams { + /// @dev Minimum blocks before a cluster can be liquidated + uint64 minimumBlocksBeforeLiquidation; + /// @dev Minimum collateral required to avoid liquidation + uint256 minimumLiquidationCollateral; + /// @dev Maximum validators per operator + uint32 validatorsPerOperatorLimit; + /// @dev Period for declaring operator fee changes + uint64 declareOperatorFeePeriod; + /// @dev Period for executing operator fee changes + uint64 executeOperatorFeePeriod; + /// @dev Maximum percentage increase for operator fees + uint64 operatorMaxFeeIncrease; + /// @dev Default oracle IDs + uint32[MAX_DELEGATION_SLOTS] defaultOracleIds; + /// @dev Quorum percentage needed to commit a root + uint16 quorumBps; + } + + /** + * @notice Emitted when the SSV Network contract is upgraded + * @param version The version string of the upgrade + * @param blockNumber The block number at which the upgrade occurred + */ + event SSVNetworkUpgradeBlock(string version, uint256 blockNumber); + + /** + * @notice Initializes the SSV Network contract + * @param token_ The ERC20 (SSV) token used in the network + * @param ssvOperators_ The SSVOperators module address + * @param ssvClusters_ The SSVClusters module address + * @param ssvDAO_ The SSVDAO module address + * @param ssvViews_ The SSVViews module address + * @param params The initialization parameters + */ function initialize( IERC20 token_, ISSVOperators ssvOperators_, ISSVClusters ssvClusters_, ISSVDAO ssvDAO_, ISSVViews ssvViews_, - uint64 minimumBlocksBeforeLiquidation_, - uint256 minimumLiquidationCollateral_, - uint32 validatorsPerOperatorLimit_, - uint64 declareOperatorFeePeriod_, - uint64 executeOperatorFeePeriod_, - uint64 operatorMaxFeeIncrease_ + NetworkInitParams calldata params ) external; + /** + * @notice Returns the version of the contract + * @return version The version string + */ function getVersion() external pure returns (string memory version); + /** + * @notice Sets the fee recipient address + * @param feeRecipientAddress The new fee recipient address + */ function setFeeRecipientAddress(address feeRecipientAddress) external; + /** + * @notice Updates a module address + * @param moduleId The ID of the module to update + * @param moduleAddress The new address of the module + */ function updateModule(SSVModules moduleId, address moduleAddress) external; -} +} \ No newline at end of file diff --git a/contracts/interfaces/ISSVNetworkCore.sol b/contracts/interfaces/ISSVNetworkCore.sol index b36812ae2..91fc8bd18 100644 --- a/contracts/interfaces/ISSVNetworkCore.sol +++ b/contracts/interfaces/ISSVNetworkCore.sol @@ -1,19 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.20; +import {PackedSSV, PackedETH} from "../libraries/SSVCoreTypes.sol"; + interface ISSVNetworkCore { - /***********/ - /* Structs */ - /***********/ + /// @notice Represents a snapshot of an SSV operator's or a SSV DAO's state at a certain block + struct Snapshot { + /// @dev The block number when the snapshot was taken + uint32 block; + /// @dev The last index calculated by the formula index += (currentBlock - block) * fee + uint64 index; + /// @dev Total accumulated earnings calculated by the formula accumulated + lastIndex * validatorCount + PackedSSV balance; + } /// @notice Represents a snapshot of an operator's or a DAO's state at a certain block - struct Snapshot { + struct EthSnapshot { /// @dev The block number when the snapshot was taken uint32 block; /// @dev The last index calculated by the formula index += (currentBlock - block) * fee uint64 index; /// @dev Total accumulated earnings calculated by the formula accumulated + lastIndex * validatorCount - uint64 balance; + PackedETH balance; } /// @notice Represents an SSV operator @@ -21,13 +29,20 @@ interface ISSVNetworkCore { /// @dev The number of validators associated with this operator uint32 validatorCount; /// @dev The fee charged by the operator, set to zero for private operators and cannot be increased once set - uint64 fee; + PackedSSV fee; /// @dev The address of the operator's owner address owner; /// @dev private flag for this operator bool whitelisted; /// @dev The state snapshot of the operator Snapshot snapshot; + + /// @dev The number of validators associated with this operator in eth + uint32 ethValidatorCount; + /// @dev The fee charged by the operator in eth, set to zero for private operators and cannot be increased once set + PackedETH ethFee; + /// @dev The state snapshot of the operator for eth + EthSnapshot ethSnapshot; } /// @notice Represents a request to change an operator's fee @@ -54,53 +69,354 @@ interface ISSVNetworkCore { uint256 balance; } - /**********/ - /* Errors */ - /**********/ + /** + * @dev Thrown when the caller is not the owner of the called entity (operator, cluster) + */ + error CallerNotOwnerWithData(address caller, address owner); // 0x8907fc65 - error CallerNotOwnerWithData(address caller, address owner); // 0x163678e9 + /** + * @dev Thrown when the caller is trying to create a cluster with an operator who did not whitelist the caller + */ error CallerNotWhitelistedWithData(uint64 operatorId); // 0xb7f529fe + + /** + * @dev Thrown when trying to use a fee that is below a minimum allowed + */ error FeeTooLow(); // 0x732f9413 + + /** + * @dev Thrown when trying to increase the fee above the allowed limit + */ error FeeExceedsIncreaseLimit(); // 0x958065d9 + + /** + * @dev Thrown when trying executee a fee without declaration + */ error NoFeeDeclared(); // 0x1d226c30 + + /** + * @dev Thrown when trying to execute fee change outside approval timeframe + */ error ApprovalNotWithinTimeframe(); // 0x97e4b518 + + /** + * @dev Thrown when operator does not exist + */ error OperatorDoesNotExist(); // 0x961e3e8c + + /** + * @dev Thrown when balance required to perform an action is insufficient + */ error InsufficientBalance(); // 0xf4d678b8 + + /** + * @dev Thrown when validator does not exist + */ error ValidatorDoesNotExist(); // 0xe51315d2 + + /** + * @dev Thrown when cluster is not liquidatable + */ error ClusterNotLiquidatable(); // 0x60300a8d + + /** + * @dev Thrown when public key length is invalid + */ error InvalidPublicKeyLength(); // 0x637297a4 + + /** + * @dev Thrown when operator IDs length is invalid (allowed only 4, 7, 10 and 13) + */ error InvalidOperatorIdsLength(); // 0x38186224 + + /** + * @dev Thrown when trying to reactive active cluster + */ error ClusterAlreadyEnabled(); // 0x3babafd2 + + /** + * @dev Thrown when trying to interact with a liquidated cluster + */ error ClusterIsLiquidated(); // 0x95a0cf33 - error ClusterDoesNotExists(); // 0x185e2b16 + + /** + * @dev Thrown when cluster does not exist + */ + error ClusterDoesNotExist(); // 0x25d92f88 + + /** + * @dev Thrown when the provided data is incorrect + */ error IncorrectClusterState(); // 0x12e04c87 + + /** + * @dev Thrown when operators list is unsorted + */ error UnsortedOperatorsList(); // 0xdd020e25 + + /** + * @dev Thrown when new block period is below minimum + */ error NewBlockPeriodIsBelowMinimum(); // 0x6e6c9cac - error ExceedValidatorLimitWithData(uint64 operatorId); // 0x8ddf7de4 + + /** + * @dev Thrown when registering a validator, but validator limit is exceeded + */ + error ExceedValidatorLimitWithData(uint64 operatorId); // 0x639f5851 + + /** + * @dev Thrown when token transfer fails + */ error TokenTransferFailed(); // 0x045c4b02 + + /** + * @dev Thrown when trying to change fee to the same value + */ error SameFeeChangeNotAllowed(); // 0xc81272f8 + + /** + * @dev Thrown when trying to increase fee of a free operator + */ error FeeIncreaseNotAllowed(); // 0x410a2b6c - error NotAuthorized(); // 0xea8e4eb5 + + /** + * @dev Thrown when operators list is not unique and has duplicates + */ error OperatorsListNotUnique(); // 0xa5a1ff5d + + /** + * @dev Thrown when operator with the same public key already exists + */ error OperatorAlreadyExists(); // 0x289c9494 + + /** + * @dev Thrown when target module does not exist + */ error TargetModuleDoesNotExistWithData(uint8 moduleId); // 0x208bb85d + + /** + * @dev Thrown when maximum value is exceeded for the target type + */ error MaxValueExceeded(); // 0x91aa3017 + + /** + * @dev Thrown when precision is exceeded (e.g., division with remainder) + */ + error MaxPrecisionExceeded(); // 0x24756546 + + /** + * @dev Thrown when the provided fee is too high + */ error FeeTooHigh(); // 0xcd4e6167 + + /** + * @dev Thrown when public keys and shares arrays length mismatch + */ error PublicKeysSharesLengthMismatch(); // 0x9ad467b8 + + /** + * @dev Thrown when validator state is incorrect + */ error IncorrectValidatorStateWithData(bytes publicKey); // 0x89307938 - error ValidatorAlreadyExistsWithData(bytes publicKey); // 0x388e7999 - error EmptyPublicKeysList(); // df83e679 - error InvalidContractAddress(); // 0xa710429d + + /** + * @dev Thrown when trying to register a validator that is already registered + */ + error ValidatorAlreadyRegistered(bytes publicKey, address owner); // 0x75106a26 + + /** + * @dev Thrown when public keys list is empty + */ + error EmptyPublicKeysList(); // 0xdf83e679 + + /** + * @dev Thrown when address is a whitelisting contract + */ error AddressIsWhitelistingContract(address contractAddress); // 0x71cadba7 + + /** + * @dev Thrown when whitelisting contract is invalid + */ error InvalidWhitelistingContract(address contractAddress); // 0x886e6a03 + + /** + * @dev Thrown when whitelist addresses length is invalid + */ error InvalidWhitelistAddressesLength(); // 0xcbb362dc + + /** + * @dev Thrown when trying to use zero address + */ error ZeroAddressNotAllowed(); // 0x8579befe + /** + * @dev Thrown when operator version is incorrect + */ + error IncorrectOperatorVersion(uint8 operatorVersion); // 0xf222e863 + + /** + * @dev Thrown when cluster version is incorrect + */ + error IncorrectClusterVersion(); // 0xf6749746 + + /** + * @dev Thrown when ETH transfer fails + */ + error ETHTransferFailed(); // 0xb12d13eb + + /** + * @dev Thrown when legacy operator fee declaration (before migration) is invalid for current configuration + */ + error LegacyOperatorFeeDeclarationInvalid(); // 0x9e593e76 + + /** + * @dev Thrown when the provided block number is stale + */ + error StaleBlockNumber(); // 0x305c3e93 + + /** + * @dev Thrown when commiting a block number that is in future + */ + error FutureBlockNumber(); // 0x252f8a0e + + /** + * @dev Thrown when the merkle for a specific block root was not found + */ + error RootNotFound(); // 0x3033b0ff + + /** + * @dev Thrown when eb update is happening too frequent + */ + error UpdateTooFrequent(); // 0x53f7a6ee + + /** + * @dev Thrown when eb update is stale + */ + error StaleUpdate(); // 0x666a2814 + + /** + * @dev Thrown when eb update does not use latest committed root block + */ + error MustUseLatestRoot(); + + /** + * @dev Thrown when the merkle proof is invalid + */ + error InvalidProof(); // 0x09bde339 + + /** + * @dev Thrown when EB exceeds maximum allowed + */ + error EBExceedsMaximum(); // 0xf5ca7cb9 + + /** + * @dev Thrown when EB is below minimum + */ + error EBBelowMinimum(); // 0x9fecdce5 + + /** + * @dev Thrown when no cSSV supply exists for root voting + */ + error ZeroCSSVSupply(); + + /** + * @dev Thrown when cSSV supply exists but truncates to zero oracle weight + */ + error InsufficientCSSVSupply(); + + /** + * @dev Thrown when the caller is not cSSV token + */ + error NotCSSV(); // 0x1598959e + + /** + * @dev Thrown when trying to use zero address + */ + error ZeroAddress(); // 0xd92e233d + + /** + * @dev Thrown when trying to configure a quorum higher than 100% + */ + error InvalidQuorum(); // 0xd1735779 + + /** + * @dev Thrown when trying to configure operator fee increase limit above 100% + */ + error InvalidOperatorFeeIncreaseLimit(); // 0x602d89dd + + /** + * @dev Thrown when trying to configure inconsistent operator fee bounds + */ + error InvalidOperatorFeeRange(); // 0x44b0758c + + /** + * @dev Thrown when amount is zero + */ + error ZeroAmount(); // 0x1f2a2005 + + /** + * @dev Thrown when token is invalid + */ + error InvalidToken(); // 0xc1ab6dc1 + + /** + * @dev Thrown when user has nothing to claim + */ + error NothingToClaim(); // 0x969bf728 + + /** + * @dev Thrown when user has nothing to withdraw + */ + error NothingToWithdraw(); // 0xd0d04f60 + + /** + * @dev Thrown when unstake amount exceeds staked balance + */ + error UnstakeAmountExceedsBalance(); // 0x02a19f57 + + /** + * @dev Thrown when stake amount is less than minimum allowed + */ + error StakeTooLow(); // 0x1cc3b37b + + /** + * @dev Thrown when the caller is not an oracle + */ + error NotOracle(); // 0x1bc2178f + + /** + * @dev Thrown when the oracle already voted for this root + */ + error AlreadyVoted(); // 0x7c9a1cf9 + + /** + * @dev Thrown when oracle is already assigned with the selected address + */ + error OracleAlreadyAssigned(); // 0xa97938cb + + /** + * @dev Thrown when attempting to replace an oracle with the same address + */ + error SameOracleAddressNotAllowed(); // 0xe991f7e9 + + /** + * @dev Thrown when oracleId exceeds the maximum allowed oracle slots + */ + error InvalidOracleId(); + + /** + * @dev Thrown when the maximum unstake requests amount reached + */ + error MaxRequestsAmountReached(); // 0xee0e82ff + + // legacy errors error ValidatorAlreadyExists(); // 0x8d09a73e + error ValidatorAlreadyExistsWithData(bytes publicKey); // 0x388e7999 error IncorrectValidatorState(); // 0x2feda3c1 error ExceedValidatorLimit(uint64 operatorId); // 0x6df5ab76 error CallerNotOwner(); // 0x5cd83192 error TargetModuleDoesNotExist(); // 0x8f9195fb error CallerNotWhitelisted(); // 0x8c6e5d71 + } diff --git a/contracts/interfaces/ISSVOperators.sol b/contracts/interfaces/ISSVOperators.sol index 19548c37e..c89944441 100644 --- a/contracts/interfaces/ISSVOperators.sol +++ b/contracts/interfaces/ISSVOperators.sol @@ -3,87 +3,201 @@ pragma solidity ^0.8.20; import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; +/** + * @title SSV Operators Interface + * @author SSV Labs + * @notice Interface for managing SSV operators including registration, fee management, earnings withdrawal and privacy settings + */ interface ISSVOperators is ISSVNetworkCore { - /// @notice Registers a new operator - /// @param publicKey The public key of the operator - /// @param fee The operator's fee (SSV) - /// @param setPrivate Flag indicating whether the operator should be set as private or not - function registerOperator(bytes calldata publicKey, uint256 fee, bool setPrivate) external returns (uint64); - - /// @notice Removes an existing operator - /// @param operatorId The ID of the operator to be removed + /** + * @dev Emitted when a new operator is registered + * @param operatorId The ID assigned to the new operator + * @param owner The address that owns and can collect fees from this operator + * @param publicKey The operator's public key used for encrypting validator shares + * @param fee The fee set for this operator + */ + event OperatorAdded( + uint64 indexed operatorId, + address indexed owner, + bytes publicKey, + uint256 fee + ); + + /** + * @dev Emitted when an operator is removed + * @param operatorId The ID of the removed operator + */ + event OperatorRemoved(uint64 indexed operatorId); + + /** + * @dev Emitted when an operator fee is declared + * @param owner The owner of the operator + * @param operatorId The ID of the operator + * @param blockNumber The block number when the declaration was made + * @param fee The proposed fee value + */ + event OperatorFeeDeclared( + address indexed owner, + uint64 indexed operatorId, + uint256 blockNumber, + uint256 fee + ); + + /** + * @dev Emitted when a declared operator fee is cancelled + * @param owner The owner of the operator + * @param operatorId The ID of the operator + */ + event OperatorFeeDeclarationCancelled( + address indexed owner, + uint64 indexed operatorId + ); + + /** + * @dev Emitted when a declared operator fee is executed + * @param owner The owner of the operator + * @param operatorId The ID of the operator + * @param blockNumber The block number from which the new fee applies + * @param fee The new active fee value + */ + event OperatorFeeExecuted( + address indexed owner, + uint64 indexed operatorId, + uint256 blockNumber, + uint256 fee + ); + + /** + * @dev Emitted when operator ETH earnings are withdrawn + * @param owner The owner of the operator + * @param operatorId The ID of the operator + * @param value The amount withdrawn + */ + event OperatorWithdrawn( + address indexed owner, + uint64 indexed operatorId, + uint256 value + ); + + /** + * @dev Emitted when operator legacy SSV earnings are withdrawn + * @param owner The owner of the operator + * @param operatorId The ID of the operator + * @param value The amount withdrawn + */ + event OperatorWithdrawnSSV( + address indexed owner, + uint64 indexed operatorId, + uint256 value + ); + + /** + * @dev Emitted when an operator changes privacy status + * @param operatorIds The IDs of the affected operators + * @param toPrivate True = set to private, False = set to public + */ + event OperatorPrivacyStatusUpdated(uint64[] operatorIds, bool toPrivate); + + /** + * @dev Emitted when an operator's whitelist address is updated + * @param operatorId The ID of the operator + * @param whitelisted The new whitelisted address + */ + event OperatorWhitelistUpdated(uint64 indexed operatorId, address whitelisted); + + /** + * @dev Emitted when operator fee recipient address is updated + * @param owner The owner of the operator + * @param recipientAddress The new fee recipient address + */ + event FeeRecipientAddressUpdated(address indexed owner, address recipientAddress); + + /** + * @notice Registers a new operator + * @param publicKey The public key of the operator + * @param fee The operator's fee (in ETH) + * @param setPrivate Flag indicating whether the operator should be private + * @return operatorId The newly assigned operator ID + */ + function registerOperator( + bytes calldata publicKey, + uint256 fee, + bool setPrivate + ) external returns (uint64); + + /** + * @notice Removes an existing ETH operator + * @param operatorId The ID of the operator to remove + */ function removeOperator(uint64 operatorId) external; - /// @notice Declares the operator's fee - /// @param operatorId The ID of the operator - /// @param fee The fee to be declared (SSV) + /** + * @notice Declares a new fee for the operator + * @param operatorId The ID of the operator + * @param fee The new fee value to propose (in SSV units) + */ function declareOperatorFee(uint64 operatorId, uint256 fee) external; - /// @notice Executes the operator's fee - /// @param operatorId The ID of the operator + /** + * @notice Executes a previously declared operator fee + * @param operatorId The ID of the operator + */ function executeOperatorFee(uint64 operatorId) external; - /// @notice Cancels the declared operator's fee - /// @param operatorId The ID of the operator + /** + * @notice Cancels a previously declared (but not yet executed) operator fee + * @param operatorId The ID of the operator + */ function cancelDeclaredOperatorFee(uint64 operatorId) external; - /// @notice Reduces the operator's fee - /// @param operatorId The ID of the operator - /// @param fee The new Operator's fee (SSV) + /** + * @notice Reduces the operator's fee (can only decrease) + * @param operatorId The ID of the operator + * @param fee The new (lower) fee value + */ function reduceOperatorFee(uint64 operatorId, uint256 fee) external; - /// @notice Withdraws operator earnings - /// @param operatorId The ID of the operator - /// @param tokenAmount The amount of tokens to withdraw (SSV) - function withdrawOperatorEarnings(uint64 operatorId, uint256 tokenAmount) external; + /** + * @notice Withdraws a specified amount of operator earnings in ETH (post-migration) + * @param operatorId The ID of the operator + * @param ethAmount The amount of ETH to withdraw + */ + function withdrawOperatorEarnings(uint64 operatorId, uint256 ethAmount) external; - /// @notice Withdraws all operator earnings - /// @param operatorId The ID of the operator + /** + * @notice Withdraws all available operator earnings in ETH (post-migration) + * @param operatorId The ID of the operator + */ function withdrawAllOperatorEarnings(uint64 operatorId) external; - /// @notice Set the list of operators as private without checking for any whitelisting address - /// @notice The operators are considered private when registering validators - /// @param operatorIds The operator IDs to set as private - function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external; - - /// @notice Set the list of operators as public without removing any whitelisting address - /// @notice The operators still keep its adresses whitelisted (external contract or EOAs/generic contracts) - /// @notice The operators are considered public when registering validators - /// @param operatorIds The operator IDs to set as public - function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external; - /** - * @dev Emitted when a new operator has been added. - * @param operatorId operator's ID. - * @param owner Operator's ethereum address that can collect fees. - * @param publicKey Operator's public key. Will be used to encrypt secret shares of validators keys. - * @param fee Operator's fee. + * @notice Withdraws all available operator earnings (both ETH and legacy SSV) in one call + * @param operatorId The ID of the operator */ - event OperatorAdded(uint64 indexed operatorId, address indexed owner, bytes publicKey, uint256 fee); + function withdrawAllVersionOperatorEarnings(uint64 operatorId) external; /** - * @dev Emitted when operator has been removed. - * @param operatorId operator's ID. + * @notice Withdraws a specified amount of legacy SSV operator earnings (pre-migration) + * @param operatorId The ID of the operator + * @param tokenAmount The amount of SSV tokens to withdraw */ - event OperatorRemoved(uint64 indexed operatorId); + function withdrawOperatorEarningsSSV(uint64 operatorId, uint256 tokenAmount) external; - event OperatorFeeDeclared(address indexed owner, uint64 indexed operatorId, uint256 blockNumber, uint256 fee); + /** + * @notice Withdraws all available legacy SSV operator earnings (pre-migration) + * @param operatorId The ID of the operator + */ + function withdrawAllOperatorEarningsSSV(uint64 operatorId) external; - event OperatorFeeDeclarationCancelled(address indexed owner, uint64 indexed operatorId); /** - * @dev Emitted when an operator's fee is updated. - * @param owner Operator's owner. - * @param blockNumber from which block number. - * @param fee updated fee value. + * @notice Sets multiple operators as private without checking whitelist addresses + * @param operatorIds Array of operator IDs to set as private */ - event OperatorFeeExecuted(address indexed owner, uint64 indexed operatorId, uint256 blockNumber, uint256 fee); - event OperatorWithdrawn(address indexed owner, uint64 indexed operatorId, uint256 value); - event FeeRecipientAddressUpdated(address indexed owner, address recipientAddress); + function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external; /** - * @dev Emitted when the operators changed its privacy status - * @param operatorIds operators' IDs. - * @param toPrivate Flag that indicates if the operators are being set to private (true) or public (false). + * @notice Sets multiple operators as public while keeping existing whitelist addresses + * @param operatorIds Array of operator IDs to set as public */ - event OperatorPrivacyStatusUpdated(uint64[] operatorIds, bool toPrivate); -} + function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external; +} \ No newline at end of file diff --git a/contracts/interfaces/ISSVOperatorsWhitelist.sol b/contracts/interfaces/ISSVOperatorsWhitelist.sol index fea9fd533..ed35db71d 100644 --- a/contracts/interfaces/ISSVOperatorsWhitelist.sol +++ b/contracts/interfaces/ISSVOperatorsWhitelist.sol @@ -4,49 +4,68 @@ pragma solidity ^0.8.20; import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; import {ISSVWhitelistingContract} from "./external/ISSVWhitelistingContract.sol"; +/** + * @title SSV Operators Whitelist Interface + * @author SSV Labs + * @notice Interface for managing whitelists for SSV operators including setting and removing whitelisted addresses and contracts + */ interface ISSVOperatorsWhitelist is ISSVNetworkCore { - /// @notice Sets a list of whitelisted addresses (EOAs or generic contracts) for a list of operators - /// @notice Changes to an operator's whitelist will not impact existing validators registered with that operator - /// @notice Only new validator registrations will adhere to the updated whitelist rules - /// @param operatorIds The operator IDs to set the whitelists for - /// @param whitelistAddresses The list of addresses to be whitelisted - function setOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses) external; - - /// @notice Removes a list of whitelisted addresses (EOAs or generic contracts) for a list of operators - /// @param operatorIds Operator IDs for which whitelists are removed - /// @param whitelistAddresses List of addresses to be removed from the whitelist - function removeOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses) external; - - /// @notice Sets a whitelisting contract for a list of operators - /// @param operatorIds The operator IDs to set the whitelisting contract for - /// @param whitelistingContract The address of a whitelisting contract - function setOperatorsWhitelistingContract( - uint64[] calldata operatorIds, - ISSVWhitelistingContract whitelistingContract - ) external; - - /// @notice Removes the whitelisting contract set for a list of operators - /// @param operatorIds The operator IDs to remove the whitelisting contract for - function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds) external; - /** - * @dev Emitted when a list of adresses are whitelisted for a set of operators. - * @param operatorIds operators' IDs. - * @param whitelistAddresses operators' new whitelist addresses (EOAs or generic contracts). + * @dev Emitted when multiple addresses are added to whitelists for operators + * @param operatorIds The IDs of the affected operators + * @param whitelistAddresses The addresses added to the whitelists */ event OperatorMultipleWhitelistUpdated(uint64[] operatorIds, address[] whitelistAddresses); /** - * @dev Emitted when a list of adresses are de-whitelisted for a set of operators. - * @param operatorIds operators' IDs. - * @param whitelistAddresses operators' list of whitelist addresses to be removed (EOAs or generic contracts). + * @dev Emitted when multiple addresses are removed from whitelists for operators + * @param operatorIds The IDs of the affected operators + * @param whitelistAddresses The addresses removed from the whitelists */ event OperatorMultipleWhitelistRemoved(uint64[] operatorIds, address[] whitelistAddresses); /** - * @dev Emitted when the whitelisting contract of an operator is updated. - * @param operatorIds operators' IDs. - * @param whitelistingContract operators' new whitelisting contract address. + * @dev Emitted when the whitelisting contract is updated for operators + * @param operatorIds The IDs of the affected operators + * @param whitelistingContract The new whitelisting contract address */ event OperatorWhitelistingContractUpdated(uint64[] operatorIds, address whitelistingContract); -} + + /** + * @notice Sets whitelisted addresses (EOAs or contracts) for multiple operators + * @notice Updates do not affect existing validators + * @notice Only new registrations use the updated whitelist + * @param operatorIds Array of operator IDs to update + * @param whitelistAddresses Array of addresses to whitelist + */ + function setOperatorsWhitelists( + uint64[] calldata operatorIds, + address[] calldata whitelistAddresses + ) external; + + /** + * @notice Removes whitelisted addresses from multiple operators + * @param operatorIds Array of operator IDs to update + * @param whitelistAddresses Array of addresses to remove + */ + function removeOperatorsWhitelists( + uint64[] calldata operatorIds, + address[] calldata whitelistAddresses + ) external; + + /** + * @notice Sets a whitelisting contract for multiple operators + * @param operatorIds Array of operator IDs to update + * @param whitelistingContract The whitelisting contract address + */ + function setOperatorsWhitelistingContract( + uint64[] calldata operatorIds, + ISSVWhitelistingContract whitelistingContract + ) external; + + /** + * @notice Removes the whitelisting contract from multiple operators + * @param operatorIds Array of operator IDs to update + */ + function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds) external; +} \ No newline at end of file diff --git a/contracts/interfaces/ISSVStaking.sol b/contracts/interfaces/ISSVStaking.sol new file mode 100644 index 000000000..370ecbe45 --- /dev/null +++ b/contracts/interfaces/ISSVStaking.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; + +/** + * @title SSV Staking Interface + * @author SSV Labs + * @notice Interface for SSV staking operations including staking tokens, requesting unstakes, withdrawing, claiming rewards and managing fees + */ +interface ISSVStaking is ISSVNetworkCore { + /** + * @dev Emitted when SSV tokens are staked + * @param user The user who staked tokens + * @param amount The amount of SSV staked + */ + event Staked(address indexed user, uint256 amount); + + /** + * @dev Emitted when an unstake is requested + * @param user The user requesting the unstake + * @param amount The amount of cSSV burned (matches SSV amount) + * @param unlockTime When the SSV can be withdrawn + */ + event UnstakeRequested(address indexed user, uint256 amount, uint256 unlockTime); + + /** + * @dev Emitted when unstaked SSV is withdrawn + * @param user The user withdrawing + * @param amount The amount of SSV withdrawn + */ + event UnstakedWithdrawn(address indexed user, uint256 amount); + + /** + * @dev Emitted when fees are synced within the protocol + * @param newFeesWei New fees amount in wei + * @param accEthPerShare Updated accumulated ETH per share + */ + event FeesSynced(uint256 newFeesWei, uint256 accEthPerShare); + + /** + * @dev Emitted when a user's rewards are settled + * @param user The user's address + * @param pending Pending rewards for this settlement + * @param accrued Total accrued rewards + * @param userIndex User's reward index after settlement + */ + event RewardsSettled(address indexed user, uint256 pending, uint256 accrued, uint256 userIndex); + + /** + * @dev Emitted when ETH rewards are claimed + * @param user The user claiming + * @param amount The ETH amount claimed + */ + event RewardsClaimed(address indexed user, uint256 amount); + + /** + * @dev Emitted when ERC20 tokens are rescued + * @param token The token rescued + * @param to The recipient + * @param amount The amount rescued + */ + event ERC20Rescued(address indexed token, address indexed to, uint256 amount); + + /** + * @notice Updates the global ETH reward index from protocol storage + */ + function syncFees() external; + + /** + * @notice Stakes SSV tokens to mint cSSV and earn ETH rewards + * @param amount Amount of SSV to stake + */ + function stake(uint256 amount) external; + + /** + * @notice Requests to unstake SSV by burning cSSV + * @notice Starts cooldown period + * @param amount Amount of cSSV to burn (1:1 with SSV) + */ + function requestUnstake(uint256 amount) external; + + /** + * @notice Withdraws unlocked SSV after cooldown + */ + function withdrawUnlocked() external; + + /** + * @notice Claims earned ETH rewards + */ + function claimEthRewards() external; + + /** + * @notice Rescues stuck ERC20 tokens (not SSV or cSSV) + * @param token Token address + * @param to Recipient + * @param amount Amount to rescue + */ + function rescueERC20(address token, address to, uint256 amount) external; + + /** + * @dev Hook for cSSV transfers + * @dev Updates rewards for sender and receiver + * @param from Sender + * @param to Recipient + * @param amount cSSV amount + */ + function onCSSVTransfer(address from, address to, uint256 amount) external; +} diff --git a/contracts/interfaces/ISSVValidators.sol b/contracts/interfaces/ISSVValidators.sol new file mode 100644 index 000000000..834e0366b --- /dev/null +++ b/contracts/interfaces/ISSVValidators.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; + +/** + * @title SSV Validators Interface + * @author SSV Labs + * @notice Interface for managing validators in the SSV network including registration, removal and exit operations + */ +interface ISSVValidators is ISSVNetworkCore { + /** + * @dev Emitted when a validator is added + * @param owner The owner of the validator (and cluster) + * @param operatorIds The operator IDs managing the validator + * @param publicKey The validator's public key + * @param shares The shares data + * @param cluster The cluster data + */ + event ValidatorAdded( + address indexed owner, + uint64[] operatorIds, + bytes publicKey, + bytes shares, + Cluster cluster + ); + + /** + * @dev Emitted when a validator is removed + * @param owner The owner of the validator + * @param operatorIds The operator IDs managing the validator + * @param publicKey The validator's public key + * @param cluster The cluster data + */ + event ValidatorRemoved( + address indexed owner, + uint64[] operatorIds, + bytes publicKey, + Cluster cluster + ); + + /** + * @dev Emitted when a validator exits + * @param owner The owner of the validator + * @param operatorIds The operator IDs managing the validator + * @param publicKey The validator's public key + */ + event ValidatorExited( + address indexed owner, + uint64[] operatorIds, + bytes publicKey + ); + + /** + * @notice Registers a new validator + * @param publicKey Validator public key + * @param operatorIds Operator IDs managing the validator + * @param sharesData Encrypted shares data + * @param cluster Cluster data + */ + function registerValidator( + bytes calldata publicKey, + uint64[] memory operatorIds, + bytes calldata sharesData, + Cluster memory cluster + ) external payable; + + /** + * @notice Registers multiple new validators + * @param publicKeys Array of validator public keys + * @param operatorIds Operator IDs managing the validators + * @param sharesData Array of encrypted shares data + * @param cluster Cluster data + */ + function bulkRegisterValidator( + bytes[] calldata publicKeys, + uint64[] memory operatorIds, + bytes[] calldata sharesData, + Cluster memory cluster + ) external payable; + + /** + * @notice Removes an existing validator + * @param publicKey Validator public key + * @param operatorIds Operator IDs managing the validator + * @param cluster Cluster data + */ + function removeValidator( + bytes calldata publicKey, + uint64[] memory operatorIds, + Cluster memory cluster + ) external; + + /** + * @notice Removes multiple existing validators from the same cluster + * @notice Reverts on duplicates or non-existent validators + * @param publicKeys Array of validator public keys + * @param operatorIds Operator IDs managing the validators + * @param cluster Cluster data + */ + function bulkRemoveValidator( + bytes[] calldata publicKeys, + uint64[] memory operatorIds, + Cluster memory cluster + ) external; + + /** + * @notice Initiates exit for a validator + * @param publicKey Validator public key + * @param operatorIds Operator IDs managing the validator + */ + function exitValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds + ) external; + + /** + * @notice Initiates exit for multiple validators + * @param publicKeys Array of validator public keys + * @param operatorIds Operator IDs managing the validators + */ + function bulkExitValidator( + bytes[] calldata publicKeys, + uint64[] calldata operatorIds + ) external; +} \ No newline at end of file diff --git a/contracts/interfaces/ISSVViews.sol b/contracts/interfaces/ISSVViews.sol index 2e06ae318..c787a142c 100644 --- a/contracts/interfaces/ISSVViews.sol +++ b/contracts/interfaces/ISSVViews.sol @@ -2,160 +2,451 @@ pragma solidity ^0.8.20; import {ISSVNetworkCore} from "./ISSVNetworkCore.sol"; +import {MAX_DELEGATION_SLOTS} from "../libraries/storage/SSVStorageStaking.sol"; -interface ISSVViews is ISSVNetworkCore { - /// @notice Gets the validator status - /// @param owner The address of the validator's owner - /// @param publicKey The public key of the validator - /// @return active A boolean indicating if the validator is active. If it does not exist, returns false. +/** + * @title SSV Views Types Interface + * @author SSV Labs + * @notice Interface providing strict data types to be used as return values in SSV Views getters + */ +interface ISSVViewsTypes { + /// @notice Contains data about a declared (pending) operator fee change + struct OperatorDeclaredFeeData { + /// @dev Whether the operator has an active fee declaration + bool isFeeDeclared; + /// @dev The fee value that was declared + uint256 fee; + /// @dev Timestamp when the approval window for this declaration begins + uint64 approvalBeginTime; + /// @dev Timestamp when the approval window for this declaration ends + uint64 approvalEndTime; + } + + /// @notice Contains core information about an operator + struct OperatorData { + /// @dev The address that owns and manages the operator + address owner; + /// @dev The current fee charged by the operator + uint256 fee; + /// @dev The number of validators currently registered to this operator + uint32 validatorCount; + /// @dev The address whitelisted for this operator + address whitelistedAddress; + /// @dev Whether the operator is private + bool isPrivate; + /// @dev Whether the operator is currently active + bool isActive; + } + + /// @notice Contains the time periods used for operator fee change workflow + struct OperatorFeePeriodsData { + /// @dev Duration (in seconds) of the declaration period + uint64 declarePeriod; + /// @dev Duration (in seconds) of the approval/execution period + uint64 executePeriod; + } + + /// @notice Represents a single pending unstake request + struct UnstakeRequestsData { + /// @dev The amount of SSV requested to be unstaked + uint256 amount; + /// @dev Timestamp after which the unstaked amount becomes withdrawable + uint256 unlockTime; + } +} + +/** + * @title SSV Views Interface + * @author SSV Labs + * @notice Interface providing view functions to retrieve network state, operator data, validator status, cluster information, fees, and staking details + */ +interface ISSVViews is ISSVNetworkCore, ISSVViewsTypes { + /** + * @notice Returns whether a validator is active + * @param owner Owner of the validator + * @param publicKey Validator public key + * @return active True if validator exists and is active + */ function getValidator(address owner, bytes calldata publicKey) external view returns (bool); - /// @notice Gets the operator fee - /// @param operatorId The ID of the operator - /// @return fee The fee associated with the operator (SSV). If the operator does not exist, the returned value is 0. + /** + * @notice Returns the current ETH fee of an operator + * @param operatorId The operator ID + * @return fee Current operator fee in ETH + */ function getOperatorFee(uint64 operatorId) external view returns (uint256 fee); - /// @notice Gets the declared operator fee - /// @param operatorId The ID of the operator - /// @return isFeeDeclared A boolean indicating if the fee is declared - /// @return fee The declared operator fee (SSV) - /// @return approvalBeginTime The time when the fee approval process begins - /// @return approvalEndTime The time when the fee approval process ends + /** + * @notice Returns the legacy SSV fee of an operator + * @param operatorId The operator ID + * @return fee Current operator fee in SSV + */ + function getOperatorFeeSSV(uint64 operatorId) external view returns (uint256 fee); + + /** + * @notice Gets the declared operator fee + * @param operatorId The ID of the operator + * @return data Declaration data + */ function getOperatorDeclaredFee( uint64 operatorId - ) external view returns (bool isFeeDeclared, uint256 fee, uint64 approvalBeginTime, uint64 approvalEndTime); - - /// @notice Gets operator details by ID - /// @param operatorId The ID of the operator - /// @return owner The owner of the operator - /// @return fee The fee associated with the operator (SSV) - /// @return validatorCount The count of validators associated with the operator - /// @return whitelistedAddress The whitelisted address of the operator. It can be and EOA or generic contract (legacy) or a whitelisting contract - /// @return isPrivate A boolean indicating if the operator is private (uses whitelisting contract or SSV Whitelisting module) - /// @return active A boolean indicating if the operator is active + ) external view returns (OperatorDeclaredFeeData memory); + + /** + * @notice Gets operator details by ID + * @param operatorId The ID of the operator + * @return The struct with operator details + */ function getOperatorById( uint64 operatorId ) external view - returns ( - address owner, - uint256 fee, - uint32 validatorCount, - address whitelistedAddress, - bool isPrivate, - bool active - ); - - /// @notice Gets the list of operators that have the given whitelisted address (EOA or generic contract) - /// @param operatorIds The list of operator IDs to check - /// @param whitelistedAddress The address whitelisted for the operators - /// @return whitelistedOperatorIds The list of operator IDs that have the given whitelisted address + returns (OperatorData memory); + + /** + * @notice Gets legacy SSV operator details by ID + * @param operatorId The ID of the operator + * @return The struct with operator details + */ + function getOperatorByIdSSV( + uint64 operatorId + ) + external + view + returns (OperatorData memory); + + /** + * @notice Returns which operators have the given address whitelisted + * @param operatorIds List of operator IDs to check + * @param whitelistedAddress Address to check + * @return whitelistedOperatorIds List of operators where address is whitelisted + */ function getWhitelistedOperators( uint64[] calldata operatorIds, address whitelistedAddress ) external view returns (uint64[] memory whitelistedOperatorIds); - /// @notice Checks if the given address is a whitelisting contract (implements ISSVWhitelistingContract) - /// @param contractAddress The address to check - /// @return isWhitelistingContract A boolean indicating if the address is a whitelisting contract - function isWhitelistingContract(address contractAddress) external view returns (bool isWhitelistingContract); - - /// @notice Checks if the given address is whitelisted in a specific whitelisting contract. - /// @notice It's up to the whitelisting contract implementation to use the operatorId parameter or not. - /// @param addressToCheck The address to check - /// @param operatorId The operator ID to check in combination with addressToCheck - /// @param whitelistingContract The whitelisting contract address - /// @return isWhitelisted A boolean indicating if the address is whitelisted in the given whitelisting contract for the given operator + /** + * @notice Checks if an address is a valid whitelisting contract + * @param contractAddress Address to check + * @return isWhitelistingContract True if address implements ISSVWhitelistingContract + */ + function isWhitelistingContract(address contractAddress) external view returns (bool); + + /** + * @notice Checks if an address is whitelisted in a specific whitelisting contract + * @param addressToCheck Address to verify + * @param operatorId Operator ID (usage depends on contract implementation) + * @param whitelistingContract Whitelisting contract address + * @return isWhitelisted Whether the address is whitelisted + */ function isAddressWhitelistedInWhitelistingContract( address addressToCheck, uint256 operatorId, address whitelistingContract ) external view returns (bool isWhitelisted); - /// @notice Checks if the cluster can be liquidated - /// @param owner The owner address of the cluster - /// @param operatorIds The IDs of the operators in the cluster - /// @return isLiquidatable A boolean indicating if the cluster can be liquidated + /** + * @notice Checks if a cluster is eligible for liquidation + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return isLiquidatable True if cluster can be liquidated + */ function isLiquidatable( address owner, uint64[] calldata operatorIds, Cluster memory cluster ) external view returns (bool isLiquidatable); - /// @notice Checks if the cluster is liquidated - /// @param owner The owner address of the cluster - /// @param operatorIds The IDs of the operators in the cluster - /// @return isLiquidated A boolean indicating if the cluster is liquidated + /** + * @notice Checks if a legacy SSV cluster is eligible for liquidation + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return isLiquidatable True if cluster can be liquidated + */ + function isLiquidatableSSV( + address owner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view returns (bool isLiquidatable); + + /** + * @notice Checks if a cluster is already liquidated + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return isLiquidated True if cluster is liquidated + */ function isLiquidated( address owner, uint64[] memory operatorIds, Cluster memory cluster ) external view returns (bool isLiquidated); - /// @notice Gets the burn rate of the cluster - /// @param owner The owner address of the cluster - /// @param operatorIds The IDs of the operators in the cluster - /// @return burnRate The burn rate of the cluster (SSV) + /** + * @notice Returns the current burn rate of a cluster + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return burnRate Current burn rate in SSV per block + */ function getBurnRate( address owner, uint64[] memory operatorIds, Cluster memory cluster ) external view returns (uint256 burnRate); - /// @notice Gets operator earnings - /// @param operatorId The ID of the operator - /// @return earnings The earnings associated with the operator (SSV) + /** + * @notice Returns the burn rate of a legacy SSV cluster + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return burnRate Current burn rate in SSV per block + */ + function getBurnRateSSV( + address owner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view returns (uint256 burnRate); + + /** + * @notice Returns accumulated operator earnings (ETH) + * @param operatorId The operator ID + * @return earnings Total ETH earnings + */ function getOperatorEarnings(uint64 operatorId) external view returns (uint256 earnings); - /// @notice Gets the balance of the cluster - /// @param owner The owner address of the cluster - /// @param operatorIds The IDs of the operators in the cluster - /// @return balance The balance of the cluster (SSV) + /** + * @notice Returns accumulated operator earnings (legacy SSV) + * @param operatorId The operator ID + * @return earnings Total SSV earnings + */ + function getOperatorEarningsSSV(uint64 operatorId) external view returns (uint256 earnings); + + /** + * @notice Returns the balance of a cluster + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return balance Cluster balance in ETH + */ function getBalance( address owner, uint64[] memory operatorIds, Cluster memory cluster ) external view returns (uint256 balance); - /// @notice Gets the network fee - /// @return networkFee The fee associated with the network (SSV) + /** + * @notice Returns the balance of a legacy SSV cluster + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return balance Cluster balance in SSV + */ + function getBalanceSSV( + address owner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view returns (uint256 balance); + + /** + * @notice Returns the effective balance of a cluster + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @param cluster Cluster data + * @return effectiveBalance Effective balance + */ + function getEffectiveBalance( + address owner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view returns (uint32 effectiveBalance); + + /** + * @notice Returns the asset type/version of a cluster + * @param owner Cluster owner + * @param operatorIds Operator IDs in the cluster + * @return version Cluster version (ETH or SSV) + */ + function getClusterAssetType( + address owner, + uint64[] calldata operatorIds + ) external view returns (uint8 version); + + /** + * @notice Returns the current network fee + * @return networkFee Current network fee in ETH + */ function getNetworkFee() external view returns (uint256 networkFee); - /// @notice Gets the network earnings - /// @return networkEarnings The earnings associated with the network (SSV) + /** + * @notice Returns the total network earnings + * @return networkEarnings Total network earnings in ETH + */ function getNetworkEarnings() external view returns (uint256 networkEarnings); - /// @notice Gets the operator fee increase limit - /// @return The maximum limit of operator fee increase + /** + * @notice Returns the legacy network fee (SSV) + * @return networkFee Current network fee in SSV + */ + function getNetworkFeeSSV() external view returns (uint256 networkFee); + + /** + * @notice Returns the legacy network earnings (SSV) + * @return networkEarnings Total network earnings in SSV + */ + function getNetworkEarningsSSV() external view returns (uint256 networkEarnings); + + /** + * @notice Returns the maximum allowed operator fee increase percentage + * @return Maximum fee increase limit + */ function getOperatorFeeIncreaseLimit() external view returns (uint64); - /// @notice Gets the operator maximum fee for operators that use SSV token - /// @return The maximum fee value (SSV) - function getMaximumOperatorFee() external view returns (uint64); + /** + * @notice Returns the maximum allowed operator fee (ETH) + * @return Maximum operator fee + */ + function getMaximumOperatorFee() external view returns (uint256); + + /** + * @notice Returns the maximum allowed operator fee (SSV) + * @return Maximum operator fee + */ + function getMaximumOperatorFeeSSV() external view returns (uint256); - /// @notice Gets the periods of operator fee declaration and execution - /// @return The period for declaring operator fee - /// @return The period for executing operator fee - function getOperatorFeePeriods() external view returns (uint64, uint64); + /** + * @notice Returns the minimum operator ETH fee set by DAO + * @return Minimum operator fee in ETH + */ + function getMinimumOperatorEthFee() external view returns (uint256); - /// @notice Gets the liquidation threshold period - /// @return blocks The number of blocks for the liquidation threshold period + /** + * @notice Returns the declaration and execution periods for operator fee changes + * @return The struct with operator fee periods + */ + function getOperatorFeePeriods() external view returns (OperatorFeePeriodsData memory); + + /** + * @notice Returns the liquidation threshold period (ETH) + * @return blocks Number of blocks + */ function getLiquidationThresholdPeriod() external view returns (uint64 blocks); - /// @notice Gets the minimum liquidation collateral - /// @return amount The minimum amount of collateral for liquidation (SSV) + /** + * @notice Returns the liquidation threshold period (SSV) + * @return blocks Number of blocks + */ + function getLiquidationThresholdPeriodSSV() external view returns (uint64 blocks); + + /** + * @notice Returns the minimum liquidation collateral + * @return amount Minimum collateral in SSV + */ function getMinimumLiquidationCollateral() external view returns (uint256 amount); - /// @notice Gets the maximum limit of validators per operator - /// @return validators The maximum number of validators per operator + /** + * @notice Returns the minimum liquidation collateral (SSV) + * @return amount Minimum collateral in SSV + */ + function getMinimumLiquidationCollateralSSV() external view returns (uint256 amount); + + /** + * @notice Returns the maximum number of validators per operator + * @return validators Maximum validators allowed + */ function getValidatorsPerOperatorLimit() external view returns (uint32 validators); - /// @notice Gets the total number of validators in the network - /// @return validatorsCount The total number of validators in the network + /** + * @notice Returns total number of registered validators in the network + * @return validatorsCount Total validator count + */ function getNetworkValidatorsCount() external view returns (uint32 validatorsCount); - /// @notice Gets the version of the contract - /// @return The version of the contract + /** + * @notice Returns the unstaking cooldown duration + * @return Cooldown period in seconds + */ + function cooldownDuration() external view returns (uint256); + + /** + * @notice Returns total SSV tokens currently staked + * @return Total staked amount + */ + function totalStaked() external view returns (uint256); + + /** + * @notice Returns the staked balance of a user + * @param user User address + * @return Staked balance + */ + function stakedBalanceOf(address user) external view returns (uint256); + + /** + * @notice Returns pending unstake requests for a user + * @param user User address + * @return Array of pending amounts and unstake requests + */ + function pendingUnstake(address user) external view returns (UnstakeRequestsData[] memory); + + /** + * @notice Returns current accumulated ETH per share + * @return Accumulated ETH per share + */ + function accEthPerShare() external view returns (uint256); + + /** + * @notice Returns current ETH balance in the staking pool + * @return ETH pool balance + */ + function stakingEthPoolBalance() external view returns (uint256); + + /** + * @notice Returns claimable ETH rewards for a user + * @param user User address + * @return Claimable ETH amount + */ + function previewClaimableEth(address user) external view returns (uint256); + + /** + * @notice Returns oracle address by ID + * @param oracleId Oracle ID + * @return Oracle address + */ + function getOracle(uint32 oracleId) external view returns (address); + + /** + * @notice Returns weight of a specific oracle + * @param oracleId Oracle ID + * @return Oracle weight + */ + function getOracleWeight(uint32 oracleId) external view returns (uint256); + + /** + * @notice Returns currently active oracle IDs + * @return Array of active oracle IDs + */ + function getActiveOracleIds() external view returns (uint32[MAX_DELEGATION_SLOTS] memory); + + /** + * @notice Returns the required quorum in basis points + * @return Quorum in bps + */ + function getQuorumBps() external view returns (uint16); + + /** + * @notice Returns the committed merkle root for a given block + * @param blockNum Block number + * @return merkleRoot Committed merkle root + */ + function getCommittedRoot(uint64 blockNum) external view returns (bytes32 merkleRoot); + + /** + * @notice Returns the current contract version + * @return Contract version string + */ function getVersion() external view returns (string memory); -} +} \ No newline at end of file diff --git a/contracts/interfaces/external/ISSVWhitelistingContract.sol b/contracts/interfaces/external/ISSVWhitelistingContract.sol index f073d38e5..b6d14b804 100644 --- a/contracts/interfaces/external/ISSVWhitelistingContract.sol +++ b/contracts/interfaces/external/ISSVWhitelistingContract.sol @@ -1,9 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.20; +/** + * @title SSV Whitelisting Contract Interface + * @author SSV Labs + */ interface ISSVWhitelistingContract { - /// @notice Checks if the caller is whitelisted - /// @param account The account that is being checked for whitelisting - /// @param operatorId The SSV Operator Id which is being checked + /** + * @notice Checks if the caller is whitelisted + * @param account The account that is being checked for whitelisting + * @param operatorId The SSV Operator Id which is being checked + */ function isWhitelisted(address account, uint256 operatorId) external view returns (bool); } diff --git a/contracts/libraries/ClusterLib.sol b/contracts/libraries/ClusterLib.sol index 0a231e4d4..fe0c42d47 100644 --- a/contracts/libraries/ClusterLib.sol +++ b/contracts/libraries/ClusterLib.sol @@ -1,75 +1,174 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "../interfaces/ISSVNetworkCore.sol"; -import {StorageData} from "./SSVStorage.sol"; -import {StorageProtocol} from "./SSVStorageProtocol.sol"; -import "./OperatorLib.sol"; -import "./ProtocolLib.sol"; -import {Types64} from "./Types.sol"; +import {ISSVNetworkCore} from "../interfaces/ISSVNetworkCore.sol"; +import {StorageData} from "./storage/SSVStorage.sol"; +import {StorageProtocol} from "./storage/SSVStorageProtocol.sol"; +import {SSVStorageEB, StorageEB} from "./storage/SSVStorageEB.sol"; +import {OperatorLib} from "./OperatorLib.sol"; +import {ProtocolLib} from "./ProtocolLib.sol"; +import {PackedSSV, PackedETH, VERSION_SSV, VERSION_ETH, ETH_DEDUCTED_DIGITS, DEFAULT_EB_PER_VALIDATOR, BPS_DENOMINATOR} from "../libraries/SSVCoreTypes.sol"; +import {PackedSSVLib, PackedETHLib} from "../libraries/SSVPackedLib.sol"; +/** + * @title SSV Cluster Library + * @author SSV Labs + * @notice Library functions for managing SSV clusters including balance updates, liquidation checks, validations and data operations + */ library ClusterLib { - using Types64 for uint64; using ProtocolLib for StorageProtocol; - function updateBalance( + function updateBalanceSSV( ISSVNetworkCore.Cluster memory cluster, uint64 newIndex, uint64 currentNetworkFeeIndex ) internal pure { uint64 networkFee = uint64(currentNetworkFeeIndex - cluster.networkFeeIndex) * cluster.validatorCount; - uint64 usage = (newIndex - cluster.index) * cluster.validatorCount + networkFee; - cluster.balance = usage.expand() > cluster.balance ? 0 : cluster.balance - usage.expand(); + PackedSSV usage = PackedSSV.wrap((newIndex - cluster.index) * cluster.validatorCount + networkFee); + cluster.balance = PackedSSVLib.unpack(usage) > cluster.balance ? 0 : cluster.balance - PackedSSVLib.unpack(usage); } + /** + * @notice Checks if cluster is liquidatable based on balance thresholds + * @param cluster Cluster data + * @param burnRate Cluster burn rate + * @param networkFee Network fee + * @param minimumBlocksBeforeLiquidation Minimum blocks before liquidation + * @param minimumLiquidationCollateral Minimum collateral for liquidation + * @return liquidatable True if cluster can be liquidated + */ function isLiquidatable( ISSVNetworkCore.Cluster memory cluster, uint64 burnRate, uint64 networkFee, uint64 minimumBlocksBeforeLiquidation, - uint64 minimumLiquidationCollateral + PackedSSV minimumLiquidationCollateral ) internal pure returns (bool liquidatable) { if (cluster.validatorCount != 0) { - if (cluster.balance < minimumLiquidationCollateral.expand()) return true; + if (cluster.balance < PackedSSVLib.unpack(minimumLiquidationCollateral)) return true; uint64 liquidationThreshold = minimumBlocksBeforeLiquidation * (burnRate + networkFee) * cluster.validatorCount; - return cluster.balance < liquidationThreshold.expand(); + return cluster.balance < PackedSSVLib.unpack(PackedSSV.wrap(liquidationThreshold)); } } + /** + * @notice Checks if cluster is liquidatable using effective balance + * @param cluster Cluster data + * @param clusterId Cluster ID + * @param burnRate Cluster burn rate + * @param networkFee Network fee + * @param minimumBlocksBeforeLiquidation Minimum blocks before liquidation + * @param minimumLiquidationCollateral Minimum collateral for liquidation + * @return liquidatable True if cluster can be liquidated + */ + function isLiquidatableWithEB( + ISSVNetworkCore.Cluster memory cluster, + bytes32 clusterId, + uint64 burnRate, + uint64 networkFee, + uint64 minimumBlocksBeforeLiquidation, + PackedETH minimumLiquidationCollateral + ) internal view returns (bool liquidatable) { + if (cluster.validatorCount == 0) return false; + if (cluster.balance < PackedETHLib.unpack(minimumLiquidationCollateral)) return true; + + uint64 vUnits = getVUnits(clusterId, cluster.validatorCount); + uint128 units = vUnits; + uint128 rate = burnRate + networkFee; + uint256 thresholdUnits = (uint256(minimumBlocksBeforeLiquidation) * rate * units) / BPS_DENOMINATOR; + uint256 liquidationThreshold = thresholdUnits * ETH_DEDUCTED_DIGITS; + return cluster.balance < liquidationThreshold; + } + + /** + * @notice Checks if cluster is liquidatable using provided vUnits + * @param cluster Cluster data + * @param vUnits cluster VUnits + * @param burnRate Cluster burn rate + * @param networkFee Network fee + * @param minimumBlocksBeforeLiquidation Minimum blocks before liquidation + * @param minimumLiquidationCollateral Minimum collateral for liquidation + * @return liquidatable True if cluster can be liquidated + */ + function isLiquidatableWithVUnits( + ISSVNetworkCore.Cluster memory cluster, + uint64 vUnits, + uint64 burnRate, + uint64 networkFee, + uint64 minimumBlocksBeforeLiquidation, + PackedETH minimumLiquidationCollateral + ) internal pure returns (bool liquidatable) { + if (cluster.validatorCount == 0) return false; + if (cluster.balance < PackedETHLib.unpack(minimumLiquidationCollateral)) return true; + + uint128 units = vUnits; + uint128 rate = burnRate + networkFee; + uint256 thresholdUnits = (uint256(minimumBlocksBeforeLiquidation) * rate * units) / BPS_DENOMINATOR; + uint256 liquidationThreshold = thresholdUnits * ETH_DEDUCTED_DIGITS; + return cluster.balance < liquidationThreshold; + } + + /** + * @notice Validates that cluster is not liquidated + * @param cluster Cluster data + */ function validateClusterIsNotLiquidated(ISSVNetworkCore.Cluster memory cluster) internal pure { if (!cluster.active) revert ISSVNetworkCore.ClusterIsLiquidated(); } + /** + * @notice Validates and hashes cluster data + * @param cluster Cluster data + * @param owner Cluster owner + * @param operatorIds Operator IDs + * @param s Storage data + * @return hashedCluster Hashed cluster ID + * @return version Cluster version + */ function validateHashedCluster( ISSVNetworkCore.Cluster memory cluster, address owner, uint64[] memory operatorIds, StorageData storage s - ) internal view returns (bytes32 hashedCluster) { + ) internal view returns (bytes32 hashedCluster, uint8 version) { hashedCluster = keccak256(abi.encodePacked(owner, operatorIds)); bytes32 hashedClusterData = hashClusterData(cluster); - bytes32 clusterData = s.clusters[hashedCluster]; + (bytes32 clusterData, uint8 detectedVersion) = getClusterData(hashedCluster, s); if (clusterData == bytes32(0)) { - revert ISSVNetworkCore.ClusterDoesNotExists(); + revert ISSVNetworkCore.ClusterDoesNotExist(); } else if (clusterData != hashedClusterData) { revert ISSVNetworkCore.IncorrectClusterState(); } + + return (hashedCluster, detectedVersion); } + /** + * @notice Updates ETH cluster data with new indexes + * @param cluster Cluster data + * @param clusterIndex New cluster index + * @param currentNetworkFeeIndex Current network fee index + */ function updateClusterData( ISSVNetworkCore.Cluster memory cluster, + bytes32 hashedCluster, uint64 clusterIndex, uint64 currentNetworkFeeIndex - ) internal pure { - updateBalance(cluster, clusterIndex, currentNetworkFeeIndex); + ) internal view { + updateBalanceWithEB(cluster, hashedCluster, clusterIndex, currentNetworkFeeIndex); cluster.index = clusterIndex; cluster.networkFeeIndex = currentNetworkFeeIndex; } + /** + * @notice Hashes cluster data + * @param cluster Cluster data + * @return Hashed cluster data + */ function hashClusterData(ISSVNetworkCore.Cluster memory cluster) internal pure returns (bytes32) { return keccak256( @@ -83,14 +182,29 @@ library ClusterLib { ); } + /** + * @notice Validates cluster state for registration + * @param cluster Cluster data + * @param owner Cluster owner + * @param operatorIds Operator IDs + * @param s Storage data + * @return hashedCluster Hashed cluster ID + */ function validateClusterOnRegistration( ISSVNetworkCore.Cluster memory cluster, + address owner, uint64[] memory operatorIds, StorageData storage s ) internal view returns (bytes32 hashedCluster) { - hashedCluster = keccak256(abi.encodePacked(msg.sender, operatorIds)); + hashedCluster = keccak256(abi.encodePacked(owner, operatorIds)); + + bytes32 clusterData = s.ethClusters[hashedCluster]; + bytes32 clusterDataSSV = s.clusters[hashedCluster]; + + if (clusterData == bytes32(0) && clusterDataSSV!= bytes32(0)) { + revert ISSVNetworkCore.IncorrectClusterVersion(); + } - bytes32 clusterData = s.clusters[hashedCluster]; if (clusterData == bytes32(0)) { if ( cluster.validatorCount != 0 || @@ -108,6 +222,15 @@ library ClusterLib { } } + /** + * @notice Updates cluster on registration + * @param cluster Cluster data + * @param operatorIds Operator IDs + * @param hashedCluster Hashed cluster ID + * @param validatorCountDelta Change in validator count + * @param s Storage data + * @param sp Storage protocol + */ function updateClusterOnRegistration( ISSVNetworkCore.Cluster memory cluster, uint64[] memory operatorIds, @@ -123,24 +246,136 @@ library ClusterLib { sp ); - updateClusterData(cluster, clusterIndex, sp.currentNetworkFeeIndex()); + updateClusterData(cluster, hashedCluster, clusterIndex, sp.currentNetworkFeeIndex()); sp.updateDAO(true, validatorCountDelta); cluster.validatorCount += validatorCountDelta; - if ( - isLiquidatable( - cluster, - burnRate, - sp.networkFee, - sp.minimumBlocksBeforeLiquidation, - sp.minimumLiquidationCollateral - ) - ) { - revert ISSVNetworkCore.InsufficientBalance(); + { + StorageEB storage seb = SSVStorageEB.load(); + uint64 storedVUnits = seb.clusterEB[hashedCluster].vUnits; + uint64 projectedVUnits = storedVUnits > 0 + ? storedVUnits + uint64(validatorCountDelta) * BPS_DENOMINATOR + : uint64(cluster.validatorCount) * BPS_DENOMINATOR; + + if ( + isLiquidatableWithVUnits( + cluster, + projectedVUnits, + burnRate, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral + ) + ) { + revert ISSVNetworkCore.InsufficientBalance(); + } + } + + s.ethClusters[hashedCluster] = hashClusterData(cluster); + } + + /** + * @notice Gets VUnits for cluster + * @param clusterId Cluster ID + * @param validatorCount Validator count + * @return vUnits cluster VUnits + */ + function getVUnits(bytes32 clusterId, uint32 validatorCount) internal view returns (uint64) { + StorageEB storage seb = SSVStorageEB.load(); + uint64 vUnits = seb.clusterEB[clusterId].vUnits; + + if (vUnits == 0) { + // Before any EB is set for this cluster, approximate EB as 32 ETH per validator. + // To preserve legacy accounting, we treat each validator as 1 logical vUnit (32 ETH), + // scaled by BPS_DENOMINATOR for fixed-point arithmetic. + return uint64(validatorCount) * BPS_DENOMINATOR; } - s.clusters[hashedCluster] = hashClusterData(cluster); + return vUnits; + } + + /** + * @notice Updates cluster balance using effective balance + * @param cluster Cluster data + * @param clusterId Cluster ID + * @param newIndex New operator index + * @param currentNetworkFeeIndex Current network fee index + */ + function updateBalanceWithEB( + ISSVNetworkCore.Cluster memory cluster, + bytes32 clusterId, + uint64 newIndex, + uint64 currentNetworkFeeIndex + ) internal view { + uint64 vUnits = getVUnits(clusterId, cluster.validatorCount); + uint128 units = vUnits; + uint128 idxNet = currentNetworkFeeIndex - cluster.networkFeeIndex; + uint128 idxOp = newIndex - cluster.index; + + uint128 networkFeeUnits = (idxNet * units) / BPS_DENOMINATOR; + uint128 usageUnits = (idxOp * units) / BPS_DENOMINATOR + networkFeeUnits; + uint256 usage = uint256(usageUnits) * ETH_DEDUCTED_DIGITS; + cluster.balance = usage > cluster.balance ? 0 : cluster.balance - usage; + } + + /** + * @notice Validates cluster version and throws error if version is not the expected one + * @param clusterVersion Detected version + * @param expectedVersion Expected version + */ + function validateClusterVersion(uint8 clusterVersion, uint8 expectedVersion) internal pure { + if (clusterVersion != expectedVersion) revert ISSVNetworkCore.IncorrectClusterVersion(); + } + + /** + * @notice Gets cluster data from storage + * @param hashedCluster Hashed cluster ID + * @param s Storage data + * @return clusterData Hashed cluster data + * @return version Cluster version + */ + function getClusterData( + bytes32 hashedCluster, + StorageData storage s + ) internal view returns (bytes32 clusterData, uint8 version) { + clusterData = s.ethClusters[hashedCluster]; + bytes32 clusterDataSSV = s.clusters[hashedCluster]; + + if (clusterData != bytes32(0) && clusterDataSSV != bytes32(0)) { + revert ISSVNetworkCore.IncorrectClusterState(); + } + + if (clusterData != bytes32(0)) { + return (clusterData, VERSION_ETH); + } + + if (clusterDataSSV != bytes32(0)) { + return (clusterDataSSV, VERSION_SSV); + } + + revert ISSVNetworkCore.ClusterDoesNotExist(); + } + + /** + * @notice Converts effective balance to v units using ceiling division + * @param effectiveBalance Effective balance in ETH + * @return vUnits v units scaled by precision + */ + function ebToVUnits(uint32 effectiveBalance) internal pure returns (uint64) { + uint256 vUnits = uint256(effectiveBalance) * BPS_DENOMINATOR; + uint256 vUnitsPerValidator = DEFAULT_EB_PER_VALIDATOR / 1 ether; + + return uint64(vUnits == 0 ? 0 : (vUnits - 1) / vUnitsPerValidator + 1); + } + + /** + * @notice Converts v units to effective balance using floor division + * @param vUnits v units scaled by precision + * @return effectiveBalance Effective balance in ETH + */ + function vUnitsToEB(uint64 vUnits) internal pure returns (uint32) { + return uint32((uint256(vUnits) * (DEFAULT_EB_PER_VALIDATOR / 1 ether)) / BPS_DENOMINATOR); } -} +} \ No newline at end of file diff --git a/contracts/libraries/CoreLib.sol b/contracts/libraries/CoreLib.sol index 6e17bd664..a677723ce 100644 --- a/contracts/libraries/CoreLib.sol +++ b/contracts/libraries/CoreLib.sol @@ -1,23 +1,49 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "./SSVStorage.sol"; +import {ISSVNetworkCore} from "../interfaces/ISSVNetworkCore.sol"; +import {SSVModules, SSVStorage} from "./storage/SSVStorage.sol"; +/** + * @title SSV Core Library + * @author SSV Labs + * @notice Library with core utility functions for SSV network including transfers, contract checks and module upgrades + */ library CoreLib { + /** + * @dev Emitted when a module is upgraded + * @param moduleId The module ID + * @param moduleAddress The new module address + */ event ModuleUpgraded(SSVModules indexed moduleId, address moduleAddress); + /** + * @notice Returns the contract version + * @return Version string + */ function getVersion() internal pure returns (string memory) { - return "v1.2.0"; + return "v2.0.0"; } + /** + * @notice Transfers ETH to recipient + * @param to Recipient address + * @param amount Amount to transfer + */ function transferBalance(address to, uint256 amount) internal { - if (!SSVStorage.load().token.transfer(to, amount)) { - revert ISSVNetworkCore.TokenTransferFailed(); + (bool success, ) = payable(to).call{value: amount}(""); + if(!success){ + revert ISSVNetworkCore.ETHTransferFailed(); } } - function deposit(uint256 amount) internal { - if (!SSVStorage.load().token.transferFrom(msg.sender, address(this), amount)) { + /** + * @notice Transfers tokens to recipient + * @param to Recipient address + * @param amount Amount to transfer + */ + function transferTokenBalance(address to, uint256 amount) internal { + if (!SSVStorage.load().token.transfer(to, amount)) { revert ISSVNetworkCore.TokenTransferFailed(); } } @@ -55,6 +81,11 @@ library CoreLib { return size > 0; } + /** + * @notice Sets contract address for a module + * @param moduleId Module ID + * @param moduleAddress New module address + */ function setModuleContract(SSVModules moduleId, address moduleAddress) internal { if (!isContract(moduleAddress)) revert ISSVNetworkCore.TargetModuleDoesNotExistWithData(uint8(moduleId)); diff --git a/contracts/libraries/OperatorLib.sol b/contracts/libraries/OperatorLib.sol index 3fdc17ef8..b3fece9ce 100644 --- a/contracts/libraries/OperatorLib.sol +++ b/contracts/libraries/OperatorLib.sol @@ -1,38 +1,157 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "../interfaces/ISSVNetworkCore.sol"; +import {ISSVNetworkCore} from "../interfaces/ISSVNetworkCore.sol"; import {ISSVWhitelistingContract} from "../interfaces/external/ISSVWhitelistingContract.sol"; -import {StorageData} from "./SSVStorage.sol"; -import {StorageProtocol} from "./SSVStorageProtocol.sol"; -import {Types64} from "./Types.sol"; - -import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; - +import {StorageData} from "./storage/SSVStorage.sol"; +import {StorageProtocol} from "./storage/SSVStorageProtocol.sol"; +import {PackedETH, PackedSSV, DEFAULT_OPERATOR_ETH_FEE, PACKED_ETH_ZERO, PACKED_SSV_ZERO, BPS_DENOMINATOR, _safeUint64} from "../libraries/SSVCoreTypes.sol"; +import {PackedETHLib, PackedSSVLib} from "../libraries/SSVPackedLib.sol"; +import {StorageEB, SSVStorageEB} from "./storage/SSVStorageEB.sol"; +import {ERC165Checker} from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; +import {ISSVOperators} from "../interfaces/ISSVOperators.sol"; + +/** + * @title SSV Operator Library + * @author SSV Labs + * @notice Library functions for managing SSV operators including snapshot updates, cluster operations, whitelists and validations + */ library OperatorLib { - using Types64 for uint64; + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; - function updateSnapshot(ISSVNetworkCore.Operator memory operator) internal view { - uint64 blockDiffFee = (uint32(block.number) - operator.snapshot.block) * operator.fee; + /** + * @notice Updates SSV operator snapshot + * @param operator Operator data + */ + function updateSnapshotSSV(ISSVNetworkCore.Operator memory operator) internal view { + uint64 blockDiffFee = (uint32(block.number) - operator.snapshot.block) * PackedSSV.unwrap(operator.fee); operator.snapshot.index += blockDiffFee; - operator.snapshot.balance += blockDiffFee * operator.validatorCount; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * operator.validatorCount)); operator.snapshot.block = uint32(block.number); } - function updateSnapshotSt(ISSVNetworkCore.Operator storage operator) internal { - uint64 blockDiffFee = (uint32(block.number) - operator.snapshot.block) * operator.fee; + /** + * @notice Updates stored SSV operator snapshot + * @param operator Operator storage reference + */ + function updateSnapshotStSSV(ISSVNetworkCore.Operator storage operator) internal { + uint64 blockDiffFee = (uint32(block.number) - operator.snapshot.block) * PackedSSV.unwrap(operator.fee); operator.snapshot.index += blockDiffFee; - operator.snapshot.balance += blockDiffFee * operator.validatorCount; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * operator.validatorCount)); operator.snapshot.block = uint32(block.number); } - function checkOwner(ISSVNetworkCore.Operator memory operator) internal view { - if (operator.snapshot.block == 0) revert ISSVNetworkCore.OperatorDoesNotExist(); + /** + * @notice Updates stored ETH operator snapshot + * @param operator Operator storage reference + * @param operatorId Operator ID + */ + function updateSnapshotSt( + ISSVNetworkCore.Operator storage operator, + uint64 operatorId + ) internal { + StorageEB storage seb = SSVStorageEB.load(); + uint32 currentBlock = uint32(block.number); + uint64 blockDiffEthFee = (currentBlock - operator.ethSnapshot.block) * PackedETH.unwrap(operator.ethFee); + + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + // storedDeviation = operatorEthVUnits (only non-default EB contributions) + // baseline = ethValidatorCount * BPS_DENOMINATOR + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (uint64(operator.ethValidatorCount) * BPS_DENOMINATOR); + + operator.ethSnapshot.index += blockDiffEthFee; + if (effectiveVUnits != 0 && blockDiffEthFee != 0) { + uint128 delta = (uint128(blockDiffEthFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(_safeUint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + + /** + * @notice Updates ETH operator snapshot + * @param operator Operator data + * @param operatorId Operator ID + */ + function updateSnapshot( + ISSVNetworkCore.Operator memory operator, + uint64 operatorId + ) internal view { + StorageEB storage seb = SSVStorageEB.load(); + uint32 currentBlock = uint32(block.number); + uint64 blockDiffEthFee = (currentBlock - operator.ethSnapshot.block) * PackedETH.unwrap(operator.ethFee); + + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (uint64(operator.ethValidatorCount) * BPS_DENOMINATOR); + + operator.ethSnapshot.index += blockDiffEthFee; + if (effectiveVUnits != 0 && blockDiffEthFee != 0) { + uint128 delta = (uint128(blockDiffEthFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(_safeUint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + + /** + * @notice Returns default ETH fee for operators + * @return Default ETH fee + */ + function defaultOperatorEthFee() internal pure returns (PackedETH) { + return PackedETHLib.pack(DEFAULT_OPERATOR_ETH_FEE); + } + + /** + * @notice Checks operator owner + * @param operator Operator storage reference + */ + function checkOwner(ISSVNetworkCore.Operator storage operator) internal view { + if (operator.snapshot.block == 0 && operator.ethSnapshot.block == 0) { + revert ISSVNetworkCore.OperatorDoesNotExist(); + } if (operator.owner != msg.sender) revert ISSVNetworkCore.CallerNotOwnerWithData(msg.sender, operator.owner); } + /** + * @notice Ensures ETH defaults for operator + * @param operator Operator storage reference + */ + function ensureETHDefaults(ISSVNetworkCore.Operator storage operator, uint64 operatorId) internal { + if (operator.ethSnapshot.block == 0) { + operator.ethSnapshot.block = uint32(block.number); + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + + if (operator.ethFee.eq(PACKED_ETH_ZERO) && operator.fee.neq(PACKED_SSV_ZERO)) { + operator.ethFee = defaultOperatorEthFee(); + emit ISSVOperators.OperatorFeeExecuted(operator.owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE); + } + } + // we don't want to revert here because this will block the migration flow + } + + /** + * @notice Validates operator state for validator registration + * @param operator Operator storage reference + */ + function ensureOperatorExist(ISSVNetworkCore.Operator storage operator) internal view { + if (operator.owner == address(0) || + (operator.ethSnapshot.block == 0 && operator.snapshot.block == 0)) { + revert ISSVNetworkCore.OperatorDoesNotExist(); + } + } + + /** + * @notice Updates cluster operators on registration + * @param operatorIds Operator IDs + * @param deltaValidatorCount Validator count delta + * @param s Storage data + * @param sp Storage protocol + * @return cumulativeIndex Cumulative index + * @return cumulativeFee Cumulative fee + */ function updateClusterOperatorsOnRegistration( uint64[] memory operatorIds, uint32 deltaValidatorCount, @@ -55,12 +174,11 @@ library OperatorLib { revert ISSVNetworkCore.OperatorsListNotUnique(); } } - ISSVNetworkCore.Operator memory operator = s.operators[operatorId]; - - if (operator.snapshot.block == 0) { - revert ISSVNetworkCore.OperatorDoesNotExist(); - } + ISSVNetworkCore.Operator storage operatorSt = s.operators[operatorId]; + ensureOperatorExist(operatorSt); + ensureETHDefaults(operatorSt, operatorId); + ISSVNetworkCore.Operator memory operator = operatorSt; // check if the pending operator is whitelisted (must be backward compatible) if (operator.whitelisted) { // Handle bitmap-based whitelisting @@ -91,18 +209,27 @@ library OperatorLib { } } - updateSnapshot(operator); - if ((operator.validatorCount += deltaValidatorCount) > sp.validatorsPerOperatorLimit) { + updateSnapshot(operator, operatorId); + if ((operator.ethValidatorCount += deltaValidatorCount) > sp.validatorsPerOperatorLimit) { revert ISSVNetworkCore.ExceedValidatorLimitWithData(operatorId); } - - cumulativeFee += operator.fee; - cumulativeIndex += operator.snapshot.index; + cumulativeFee += PackedETH.unwrap(operator.ethFee); + cumulativeIndex += operator.ethSnapshot.index; s.operators[operatorId] = operator; } } + /** + * @notice Updates ETH cluster operators + * @param operatorIds Operator IDs + * @param increaseValidatorCount Increase flag + * @param deltaValidatorCount Validator count delta + * @param s Storage data + * @param sp Storage protocol + * @return cumulativeIndex Cumulative index + * @return cumulativeFee Cumulative fee + */ function updateClusterOperators( uint64[] memory operatorIds, bool increaseValidatorCount, @@ -111,26 +238,196 @@ library OperatorLib { StorageProtocol storage sp ) internal returns (uint64 cumulativeIndex, uint64 cumulativeFee) { uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + // only update active operators (block != 0) + // removed operators have block == 0 and contribute their preserved index + if (operator.ethSnapshot.block != 0) { + updateSnapshotSt(operator, operatorId); + + if (increaseValidatorCount) { + if ((operator.ethValidatorCount += deltaValidatorCount) > sp.validatorsPerOperatorLimit) { + revert ISSVNetworkCore.ExceedValidatorLimitWithData(operatorId); + } + } else { + operator.ethValidatorCount -= deltaValidatorCount; + } + + cumulativeFee += PackedETH.unwrap(operator.ethFee); + } + cumulativeIndex += operator.ethSnapshot.index; + } + } + + /** + * @notice Updates cluster operators on reactivation + * @param operatorIds Operator IDs + * @param deltaValidatorCount Validator count delta + * @param clusterDeviation Cluster deviation + * @param s Storage data + * @param sp Storage protocol + * @param seb Storage EB + * @return cumulativeIndex Cumulative index + * @return cumulativeFee Cumulative fee + */ + function updateClusterOperatorsOnReactivation( + uint64[] memory operatorIds, + uint32 deltaValidatorCount, + uint64 clusterDeviation, + StorageData storage s, + StorageProtocol storage sp, + StorageEB storage seb + ) internal returns (uint64 cumulativeIndex, uint64 cumulativeFee) { + uint256 operatorsLength = operatorIds.length; + uint32 currentBlock = uint32(block.number); + bool hasDeviation = sp.daoTotalEthVUnits != uint64(sp.ethDaoValidatorCount) * BPS_DENOMINATOR; for (uint256 i; i < operatorsLength; ++i) { uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + if (operator.ethSnapshot.block != 0) { + uint64 blockDiffEthFee = (currentBlock - operator.ethSnapshot.block) * PackedETH.unwrap(operator.ethFee); + + if (blockDiffEthFee != 0) { + operator.ethSnapshot.index += blockDiffEthFee; + uint64 effectiveVUnits; + + if (hasDeviation) { + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + effectiveVUnits = storedDeviation + (uint64(operator.ethValidatorCount) * BPS_DENOMINATOR); + } else { + effectiveVUnits = uint64(operator.ethValidatorCount) * BPS_DENOMINATOR; + } + if (effectiveVUnits != 0) { + uint128 delta = (uint128(blockDiffEthFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(_safeUint64(delta))); + } + } + operator.ethSnapshot.block = currentBlock; + + if (clusterDeviation != 0) { + if (hasDeviation) { + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + seb.operatorEthVUnits[operatorId] = storedDeviation + clusterDeviation; + } else { + seb.operatorEthVUnits[operatorId] = clusterDeviation; + } + } + + operator.ethValidatorCount += deltaValidatorCount; + if (operator.ethValidatorCount > sp.validatorsPerOperatorLimit) { + revert ISSVNetworkCore.ExceedValidatorLimitWithData(operatorId); + } + + cumulativeFee += PackedETH.unwrap(operator.ethFee); + } + cumulativeIndex += operator.ethSnapshot.index; + } + } + + /** + * @notice Updates cluster operators on migration + * @param operatorIds Operator IDs + * @param validatorCount Validator count + * @param s Storage data + * @param sp Storage protocol + * @param isClusterLiquidated Liquidated flag + * @return cumulativeIndexSSV Cumulative index SSV + * @return cumulativeIndexETH Cumulative index ETH + * @return cumulativeFeeETH Cumulative fee ETH + */ + function updateClusterOperatorsMigration( + uint64[] memory operatorIds, + uint32 validatorCount, + StorageData storage s, + StorageProtocol storage sp, + bool isClusterLiquidated + ) internal returns (uint64 cumulativeIndexSSV, uint64 cumulativeIndexETH, uint64 cumulativeFeeETH) { + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + uint64 operatorId = operatorIds[i]; ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; if (operator.snapshot.block != 0) { - updateSnapshotSt(operator); + updateSnapshotStSSV(operator); + if (!isClusterLiquidated) { + operator.validatorCount -= validatorCount; + } + } + cumulativeIndexSSV += operator.snapshot.index; + + // Removed operators (both blocks == 0) contribute their frozen index + // but are not mutated (no validator count or fee changes) + if (operator.snapshot.block != 0 || operator.ethSnapshot.block != 0) { + if (operator.ethSnapshot.block == 0) { + // first-time ETH usage or migration + ensureETHDefaults(operator, operatorId); + } else { + // already ETH operator + updateSnapshotSt(operator, operatorId); + } + + // update ETH validator count for both new ETH-initialized and existing ETH-initialized operators + if ((operator.ethValidatorCount += validatorCount) > sp.validatorsPerOperatorLimit) { + revert ISSVNetworkCore.ExceedValidatorLimitWithData(operatorId); + } + + cumulativeFeeETH += PackedETH.unwrap(operator.ethFee); + } + cumulativeIndexETH += operator.ethSnapshot.index; + } + } + + /** + * @notice Updates SSV cluster operators + * @param operatorIds Operator IDs + * @param increaseValidatorCount Increase flag + * @param deltaValidatorCount Validator count delta + * @param s Storage data + * @param sp Storage protocol + * @return cumulativeIndex Cumulative index + * @return cumulativeFee Cumulative fee + */ + function updateClusterOperatorsSSV( + uint64[] memory operatorIds, + bool increaseValidatorCount, + uint32 deltaValidatorCount, + StorageData storage s, + StorageProtocol storage sp + ) internal returns (uint64 cumulativeIndex, uint64 cumulativeFee) { + uint256 operatorsLength = operatorIds.length; + + for (uint256 i; i < operatorsLength; ++i) { + uint64 operatorId = operatorIds[i]; + + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + if (operator.snapshot.block != 0) { + updateSnapshotStSSV(operator); if (!increaseValidatorCount) { operator.validatorCount -= deltaValidatorCount; } else if ((operator.validatorCount += deltaValidatorCount) > sp.validatorsPerOperatorLimit) { revert ISSVNetworkCore.ExceedValidatorLimitWithData(operatorId); } - cumulativeFee += operator.fee; + cumulativeFee += PackedSSV.unwrap(operator.fee); } + cumulativeIndex += operator.snapshot.index; } } + /** + * @notice Updates multiple whitelists + * @param whitelistAddresses Whitelist addresses + * @param operatorIds Operator IDs + * @param registerAddresses Register flag + * @param s Storage data + */ function updateMultipleWhitelists( address[] calldata whitelistAddresses, uint64[] calldata operatorIds, @@ -168,6 +465,14 @@ library OperatorLib { } } + /** + * @notice Generates block masks for operators + * @param operatorIds Operator IDs + * @param checkOperatorsOwnership Ownership check flag + * @param s Storage data + * @return masks Block masks + * @return startBlockIndex Start block index + */ function generateBlockMasks( uint64[] calldata operatorIds, bool checkOperatorsOwnership, @@ -203,6 +508,12 @@ library OperatorLib { } } + /** + * @notice Updates operator privacy status + * @param operatorIds Operator IDs + * @param setPrivate Private flag + * @param s Storage data + */ function updatePrivacyStatus(uint64[] calldata operatorIds, bool setPrivate, StorageData storage s) internal { uint256 operatorsLength = checkOperatorsLength(operatorIds); @@ -216,20 +527,40 @@ library OperatorLib { } } + /** + * @notice Gets bitmap indexes for operator + * @param operatorId Operator ID + * @return blockIndex Block index + * @return bitPosition Bit position + */ function getBitmapIndexes(uint64 operatorId) internal pure returns (uint256 blockIndex, uint256 bitPosition) { blockIndex = operatorId >> 8; // Equivalent to operatorId / 256 bitPosition = operatorId & 0xFF; // Equivalent to operatorId % 256 } + /** + * @notice Checks for zero address + * @param whitelistAddress Address to check + */ function checkZeroAddress(address whitelistAddress) internal pure { if (whitelistAddress == address(0)) revert ISSVNetworkCore.ZeroAddressNotAllowed(); } + /** + * @notice Checks operator IDs length + * @param operatorIds Operator IDs + * @return operatorsLength Length + */ function checkOperatorsLength(uint64[] calldata operatorIds) internal pure returns (uint256 operatorsLength) { operatorsLength = operatorIds.length; if (operatorsLength == 0) revert ISSVNetworkCore.InvalidOperatorIdsLength(); } + /** + * @notice Checks if address is whitelisting contract + * @param whitelistingContract Contract address + * @return True if whitelisting contract + */ function isWhitelistingContract(address whitelistingContract) internal view returns (bool) { return ERC165Checker.supportsInterface(whitelistingContract, type(ISSVWhitelistingContract).interfaceId); } diff --git a/contracts/libraries/ProtocolLib.sol b/contracts/libraries/ProtocolLib.sol index 1a839e23d..33c04bba0 100644 --- a/contracts/libraries/ProtocolLib.sol +++ b/contracts/libraries/ProtocolLib.sol @@ -1,46 +1,151 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "../interfaces/ISSVNetworkCore.sol"; -import {Types256} from "./Types.sol"; -import {StorageProtocol} from "./SSVStorageProtocol.sol"; +import {ISSVNetworkCore} from "../interfaces/ISSVNetworkCore.sol"; +import {PackedSSV, PackedETH, BPS_DENOMINATOR, _safeUint64} from "../libraries/SSVCoreTypes.sol"; +import {PackedSSVLib, PackedETHLib} from "../libraries/SSVPackedLib.sol"; +import {StorageProtocol} from "./storage/SSVStorageProtocol.sol"; +/** + * @title SSV Protocol Library + * @author SSV Labs + * @notice Library functions for managing SSV protocol including network fees, DAO earnings and validator updates + */ library ProtocolLib { - using Types256 for uint256; + using PackedETHLib for PackedETH; - /******************************/ - /* Network internal functions */ - /******************************/ + /** + * @notice Returns current network fee index + * @param sp Storage protocol + * @return Current network fee index + */ function currentNetworkFeeIndex(StorageProtocol storage sp) internal view returns (uint64) { - return sp.networkFeeIndex + uint64(block.number - sp.networkFeeIndexBlockNumber) * sp.networkFee; + return sp.ethNetworkFeeIndex + uint64(block.number - sp.ethNetworkFeeIndexBlockNumber) * PackedETH.unwrap(sp.ethNetworkFee); } + /** + * @notice Returns current SSV network fee index + * @param sp Storage protocol + * @return Current SSV network fee index + */ + function currentNetworkFeeIndexSSV(StorageProtocol storage sp) internal view returns (uint64) { + return sp.networkFeeIndex + uint64(block.number - sp.networkFeeIndexBlockNumber) * PackedSSV.unwrap(sp.networkFee); + } + + /** + * @notice Updates ETH network fee + * @param sp Storage protocol + * @param fee New fee + */ function updateNetworkFee(StorageProtocol storage sp, uint256 fee) internal { updateDAOEarnings(sp); - sp.networkFeeIndex = currentNetworkFeeIndex(sp); + sp.ethNetworkFeeIndex = currentNetworkFeeIndex(sp); + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.ethNetworkFee = PackedETHLib.pack(fee); + } + + /** + * @notice Updates SSV network fee + * @param sp Storage protocol + * @param fee New fee + */ + function updateNetworkFeeSSV(StorageProtocol storage sp, uint256 fee) internal { + updateDAOEarningsSSV(sp); + + sp.networkFeeIndex = currentNetworkFeeIndexSSV(sp); sp.networkFeeIndexBlockNumber = uint32(block.number); - sp.networkFee = fee.shrink(); + sp.networkFee = PackedSSVLib.pack(fee); } - /**************************/ - /* DAO internal functions */ - /**************************/ + /** + * @notice Updates DAO earnings + * @param sp Storage protocol + */ function updateDAOEarnings(StorageProtocol storage sp) internal { - sp.daoBalance = networkTotalEarnings(sp); + sp.ethDaoBalance = networkTotalEarnings(sp); + sp.ethDaoIndexBlockNumber = uint32(block.number); + } + + /** + * @notice Updates SSV DAO earnings + * @param sp Storage protocol + */ + function updateDAOEarningsSSV(StorageProtocol storage sp) internal { + sp.daoBalance = networkTotalEarningsSSV(sp); sp.daoIndexBlockNumber = uint32(block.number); } - function networkTotalEarnings(StorageProtocol storage sp) internal view returns (uint64) { - return sp.daoBalance + (uint64(block.number) - sp.daoIndexBlockNumber) * sp.networkFee * sp.daoValidatorCount; + /** + * @notice Returns total network earnings + * @param sp Storage protocol + * @return Total earnings + */ + function networkTotalEarnings(StorageProtocol storage sp) internal view returns (PackedETH) { + uint128 units = sp.daoTotalEthVUnits; + uint128 idx = uint64(block.number) - sp.ethDaoIndexBlockNumber; + + uint128 earningsUnits = (idx * PackedETH.unwrap(sp.ethNetworkFee) * units) / BPS_DENOMINATOR; + return sp.ethDaoBalance.add(PackedETH.wrap(_safeUint64(earningsUnits))); + } + + /** + * @notice Returns total SSV network earnings + * @param sp Storage protocol + * @return Total earnings + */ + function networkTotalEarningsSSV(StorageProtocol storage sp) internal view returns (PackedSSV) { + return PackedSSV.wrap(PackedSSV.unwrap(sp.daoBalance) + (uint64(block.number) - sp.daoIndexBlockNumber) * PackedSSV.unwrap(sp.networkFee) * sp.daoValidatorCount); } + /** + * @notice Updates DAO validator count + * @param sp Storage protocol + * @param increaseValidatorCount Increase flag + * @param deltaValidatorCount Validator count delta + */ function updateDAO(StorageProtocol storage sp, bool increaseValidatorCount, uint32 deltaValidatorCount) internal { updateDAOEarnings(sp); + uint64 vUnitsDelta = uint64(deltaValidatorCount) * BPS_DENOMINATOR; + if (!increaseValidatorCount) { + sp.ethDaoValidatorCount -= deltaValidatorCount; + sp.daoTotalEthVUnits -= vUnitsDelta; + } else { + if ((sp.ethDaoValidatorCount += deltaValidatorCount) > type(uint32).max) { + revert ISSVNetworkCore.MaxValueExceeded(); + } + sp.daoTotalEthVUnits += vUnitsDelta; + } + } + + /** + * @notice Updates SSV DAO validator count + * @param sp Storage protocol + * @param increaseValidatorCount Increase flag + * @param deltaValidatorCount Validator count delta + */ + function updateDAOSSV(StorageProtocol storage sp, bool increaseValidatorCount, uint32 deltaValidatorCount) internal { + updateDAOEarningsSSV(sp); if (!increaseValidatorCount) { sp.daoValidatorCount -= deltaValidatorCount; } else if ((sp.daoValidatorCount += deltaValidatorCount) > type(uint32).max) { revert ISSVNetworkCore.MaxValueExceeded(); } } + + /** + * @notice Updates DAO ETH v units + * @param sp Storage protocol + * @param oldVUnits Old v units + * @param newVUnits New v units + */ + function updateDAOEthVUnits(StorageProtocol storage sp, uint64 oldVUnits, uint64 newVUnits) internal { + updateDAOEarnings(sp); // Settle ETH earnings first + + if (newVUnits > oldVUnits) { + sp.daoTotalEthVUnits += newVUnits - oldVUnits; + } else { + sp.daoTotalEthVUnits -= oldVUnits - newVUnits; + } + } } diff --git a/contracts/libraries/SSVCoreTypes.sol b/contracts/libraries/SSVCoreTypes.sol new file mode 100644 index 000000000..cdc35e5ea --- /dev/null +++ b/contracts/libraries/SSVCoreTypes.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +type PackedSSV is uint64; +type PackedETH is uint64; + +PackedETH constant PACKED_ETH_ZERO = PackedETH.wrap(0); +PackedSSV constant PACKED_SSV_ZERO = PackedSSV.wrap(0); + +uint8 constant VERSION_SSV = 0; +uint8 constant VERSION_ETH = 1; +uint8 constant VERSION_UNDEFINED = type(uint8).max; + +uint64 constant BPS_DENOMINATOR = 10_000; +uint256 constant DEFAULT_OPERATOR_ETH_FEE = 1778_800_000; +uint256 constant PRECISION = 1e18; + +uint256 constant DEDUCTED_DIGITS = 10_000_000; +uint256 constant ETH_DEDUCTED_DIGITS = 100_000; + +uint256 constant DEFAULT_EB_PER_VALIDATOR = 32 ether; +uint256 constant MAX_EB_PER_VALIDATOR = 2048 ether; + +error SafeCastOverflow(); + +function _safeUint64(uint128 value) pure returns (uint64) { + if (value > type(uint64).max) revert SafeCastOverflow(); + return uint64(value); +} + diff --git a/contracts/libraries/SSVPackedLib.sol b/contracts/libraries/SSVPackedLib.sol new file mode 100644 index 000000000..2e195bbd7 --- /dev/null +++ b/contracts/libraries/SSVPackedLib.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {PackedSSV, PackedETH, DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS} from "./SSVCoreTypes.sol"; +import {ISSVNetworkCore} from "../interfaces/ISSVNetworkCore.sol"; + +library PackingLib { + + function _pack(uint256 value, uint256 scale) internal pure returns (uint64) { + if (value > uint256(type(uint64).max) * scale) revert ISSVNetworkCore.MaxValueExceeded(); + if (value % scale != 0) revert ISSVNetworkCore.MaxPrecisionExceeded(); + + return uint64(value / scale); + } + + function _unpack(uint64 raw, uint256 scale) internal pure returns (uint256) { + return uint256(raw) * scale; + } +} + +library PackedSSVLib { + function pack(uint256 value) internal pure returns (PackedSSV) { + return PackedSSV.wrap(PackingLib._pack(value, DEDUCTED_DIGITS)); + } + + function unpack(PackedSSV packed) internal pure returns (uint256) { + return PackingLib._unpack(PackedSSV.unwrap(packed), DEDUCTED_DIGITS); + } + + function raw(PackedSSV packed) internal pure returns (uint64) { + return PackedSSV.unwrap(packed); + } + + function eq(PackedSSV a, PackedSSV b) internal pure returns (bool) { + return PackedSSV.unwrap(a) == PackedSSV.unwrap(b); + } + + function neq(PackedSSV a, PackedSSV b) internal pure returns (bool) { + return PackedSSV.unwrap(a) != PackedSSV.unwrap(b); + } + + function gt(PackedSSV a, PackedSSV b) internal pure returns (bool) { + return PackedSSV.unwrap(a) > PackedSSV.unwrap(b); + } + + function lt(PackedSSV a, PackedSSV b) internal pure returns (bool) { + return PackedSSV.unwrap(a) < PackedSSV.unwrap(b); + } + + function add(PackedSSV a, PackedSSV b) internal pure returns (PackedSSV) { + return PackedSSV.wrap(PackedSSV.unwrap(a) + PackedSSV.unwrap(b)); + } + + function sub(PackedSSV a, PackedSSV b) internal pure returns (PackedSSV) { + return PackedSSV.wrap(PackedSSV.unwrap(a) - PackedSSV.unwrap(b)); + } +} + +library PackedETHLib { + function pack(uint256 value) internal pure returns (PackedETH) { + return PackedETH.wrap(PackingLib._pack(value, ETH_DEDUCTED_DIGITS)); + } + + function unpack(PackedETH packed) internal pure returns (uint256) { + return PackingLib._unpack(PackedETH.unwrap(packed), ETH_DEDUCTED_DIGITS); + } + + function raw(PackedETH packed) internal pure returns (uint64) { + return PackedETH.unwrap(packed); + } + + function eq(PackedETH a, PackedETH b) internal pure returns (bool) { + return PackedETH.unwrap(a) == PackedETH.unwrap(b); + } + + function neq(PackedETH a, PackedETH b) internal pure returns (bool) { + return PackedETH.unwrap(a) != PackedETH.unwrap(b); + } + + function gt(PackedETH a, PackedETH b) internal pure returns (bool) { + return PackedETH.unwrap(a) > PackedETH.unwrap(b); + } + + function gte(PackedETH a, PackedETH b) internal pure returns (bool) { + return PackedETH.unwrap(a) >= PackedETH.unwrap(b); + } + + function lt(PackedETH a, PackedETH b) internal pure returns (bool) { + return PackedETH.unwrap(a) < PackedETH.unwrap(b); + } + + function lte(PackedETH a, PackedETH b) internal pure returns (bool) { + return PackedETH.unwrap(a) <= PackedETH.unwrap(b); + } + + function add(PackedETH a, PackedETH b) internal pure returns (PackedETH) { + return PackedETH.wrap(PackedETH.unwrap(a) + PackedETH.unwrap(b)); + } + + function sub(PackedETH a, PackedETH b) internal pure returns (PackedETH) { + return PackedETH.wrap(PackedETH.unwrap(a) - PackedETH.unwrap(b)); + } +} diff --git a/contracts/libraries/SSVReentrancyGuardLib.sol b/contracts/libraries/SSVReentrancyGuardLib.sol new file mode 100644 index 000000000..7eea51147 --- /dev/null +++ b/contracts/libraries/SSVReentrancyGuardLib.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {SSVStorageReentrancy, StorageReentrancy} from "./storage/SSVStorageReentrancy.sol"; + +/** + * @title SSV Reentrancy Guard Library + * @author SSV Labs + * @notice Library for preventing reentrant calls using custom storage slot + */ +library SSVReentrancyGuardLib { + uint256 private constant NOT_ENTERED = 1; + uint256 private constant ENTERED = 2; + + /** + * @dev Thrown when caller is trying to perform an unauthorized reentrant call + */ + error ReentrancyGuardReentrantCall(); + + /** + * @notice Starts reentrancy guard + */ + function _nonReentrantBefore() internal { + StorageReentrancy storage s = SSVStorageReentrancy.load(); + if (s.status == ENTERED) revert ReentrancyGuardReentrantCall(); + s.status = ENTERED; + } + + /** + * @notice Ends reentrancy guard + */ + function _nonReentrantAfter() internal { + SSVStorageReentrancy.load().status = NOT_ENTERED; + } +} diff --git a/contracts/libraries/Types.sol b/contracts/libraries/Types.sol deleted file mode 100644 index a5fffc2db..000000000 --- a/contracts/libraries/Types.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.24; - -uint256 constant DEDUCTED_DIGITS = 10_000_000; - -library Types64 { - function expand(uint64 value) internal pure returns (uint256) { - return value * DEDUCTED_DIGITS; - } -} - -library Types256 { - function shrink(uint256 value) internal pure returns (uint64) { - require(value < (2 ** 64 * DEDUCTED_DIGITS), "Max value exceeded"); - return uint64(shrinkable(value) / DEDUCTED_DIGITS); - } - - function shrinkable(uint256 value) internal pure returns (uint256) { - require(value % DEDUCTED_DIGITS == 0, "Max precision exceeded"); - return value; - } -} diff --git a/contracts/libraries/ValidatorLib.sol b/contracts/libraries/ValidatorLib.sol index e04d04590..d9d81f7b4 100644 --- a/contracts/libraries/ValidatorLib.sol +++ b/contracts/libraries/ValidatorLib.sol @@ -1,15 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "../interfaces/ISSVNetworkCore.sol"; -import {StorageData} from "./SSVStorage.sol"; +import {ISSVNetworkCore} from "../interfaces/ISSVNetworkCore.sol"; +import {StorageData} from "./storage/SSVStorage.sol"; +/** + * @title SSV Validator Library + * @author SSV Labs + * @notice Library functions for managing SSV validators including operator validations, public key registrations and state checks + */ library ValidatorLib { uint64 private constant MIN_OPERATORS_LENGTH = 4; uint64 private constant MAX_OPERATORS_LENGTH = 13; uint64 private constant MODULO_OPERATORS_LENGTH = 3; uint64 private constant PUBLIC_KEY_LENGTH = 48; + /** + * @notice Validates operator IDs array length + * @param operatorIds Operator IDs + */ function validateOperatorsLength(uint64[] memory operatorIds) internal pure { uint256 operatorsLength = operatorIds.length; @@ -22,25 +31,48 @@ library ValidatorLib { } } - function registerPublicKey(bytes memory publicKey, uint64[] memory operatorIds, StorageData storage s) internal { + /** + * @notice Registers validator public key + * @param publicKey Validator public key + * @param operatorIds Operator IDs + * @param owner Validator owner + * @param s Storage data + */ + function registerPublicKey( + bytes memory publicKey, + uint64[] memory operatorIds, + address owner, + StorageData storage s + ) internal { if (publicKey.length != PUBLIC_KEY_LENGTH) { revert ISSVNetworkCore.InvalidPublicKeyLength(); } - bytes32 hashedPk = keccak256(abi.encodePacked(publicKey, msg.sender)); + bytes32 hashedPk = keccak256(abi.encodePacked(publicKey, owner)); if (s.validatorPKs[hashedPk] != bytes32(0)) { - revert ISSVNetworkCore.ValidatorAlreadyExistsWithData(publicKey); + revert ISSVNetworkCore.ValidatorAlreadyRegistered(publicKey, owner); } s.validatorPKs[hashedPk] = bytes32(uint256(keccak256(abi.encodePacked(operatorIds))) | uint256(0x01)); // set LSB to 1 } + /** + * @notice Hashes operator IDs + * @param operatorIds Operator IDs + * @return Hashed operator IDs + */ function hashOperatorIds(uint64[] memory operatorIds) internal pure returns (bytes32) { bytes32 mask = ~bytes32(uint256(1)); // All bits set to 1 except LSB return keccak256(abi.encodePacked(operatorIds)) & mask; // Clear LSB of provided operator ids } + /** + * @notice Validates validator state + * @param validatorData Validator data + * @param hashedOperatorIds Hashed operator IDs + * @return True if state is correct + */ function validateCorrectState(bytes32 validatorData, bytes32 hashedOperatorIds) internal pure returns (bool) { // All bits set to 1 except LSB // Clear LSB of stored validator data and compare diff --git a/contracts/libraries/SSVStorage.sol b/contracts/libraries/storage/SSVStorage.sol similarity index 83% rename from contracts/libraries/SSVStorage.sol rename to contracts/libraries/storage/SSVStorage.sol index c0ceaa994..8d07879a7 100644 --- a/contracts/libraries/SSVStorage.sol +++ b/contracts/libraries/storage/SSVStorage.sol @@ -1,16 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "../interfaces/ISSVNetworkCore.sol"; -import "@openzeppelin/contracts/utils/Counters.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; +import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; enum SSVModules { SSV_OPERATORS, SSV_CLUSTERS, SSV_DAO, SSV_VIEWS, - SSV_OPERATORS_WHITELIST + SSV_OPERATORS_WHITELIST, + SSV_STAKING, + SSV_VALIDATORS } /// @title SSV Network Storage Data @@ -38,6 +40,8 @@ struct StorageData { /// @notice that are whitelisted for that address using bitmaps /// @dev The nested mapping's key represents a uint256 slot to handle more than 256 operators per address mapping(address => mapping(uint256 => uint256)) addressWhitelistedForOperators; + /// @notice Maps each cluster's bytes32 identifier to its hashed representation of ISSVNetworkCore.Cluster for eth + mapping(bytes32 => bytes32) ethClusters; } library SSVStorage { diff --git a/contracts/libraries/storage/SSVStorageEB.sol b/contracts/libraries/storage/SSVStorageEB.sol new file mode 100644 index 000000000..5ce2d4df8 --- /dev/null +++ b/contracts/libraries/storage/SSVStorageEB.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +struct ClusterEBSnapshot { + uint64 vUnits; + uint64 lastRootBlockNum; + uint64 lastUpdateBlock; +} + +struct StorageEB { + /// @notice Maps block to EB roots + mapping(uint64 => bytes32) ebRoots; + /// @notice Maps cluster ID to EB snapshot + mapping(bytes32 => ClusterEBSnapshot) clusterEB; + /// @notice Maps operator ID to ETH vUnits + mapping(uint64 => uint64) operatorEthVUnits; + /// @notice Latest block number where EB was committed + uint64 latestCommittedBlock; + /// @notice Minimum blocks between updates + uint32 minBlocksBetweenUpdates; + /// @notice Counts root commitments (accumulated weight) per commitment key (encoded root and block) + mapping(bytes32 => uint256) rootCommitments; + /// @notice Tracks if an oracle ID has voted for a specific commitment key + mapping(bytes32 => mapping(uint32 => bool)) hasVoted; + /// @notice Frozen voting supply (truncated to oracle-count divisibility) at the first vote of each commitment round + mapping(bytes32 => uint256) roundFrozenSupply; +} + +library SSVStorageEB { + uint256 private constant SSV_STORAGE_POSITION = uint256(keccak256("ssv.network.storage.eb")) - 1; + + function load() internal pure returns (StorageEB storage seb) { + uint256 position = SSV_STORAGE_POSITION; + assembly { + seb.slot := position + } + } +} diff --git a/contracts/libraries/SSVStorageProtocol.sol b/contracts/libraries/storage/SSVStorageProtocol.sol similarity index 55% rename from contracts/libraries/SSVStorageProtocol.sol rename to contracts/libraries/storage/SSVStorageProtocol.sol index fa83d7780..558c22ee3 100644 --- a/contracts/libraries/SSVStorageProtocol.sol +++ b/contracts/libraries/storage/SSVStorageProtocol.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; +import {PackedSSV, PackedETH} from "../SSVCoreTypes.sol"; + /// @title SSV Network Storage Protocol /// @notice Represents the operational settings and parameters required by the SSV Network struct StorageProtocol { @@ -13,15 +15,15 @@ struct StorageProtocol { /// @notice The maximum limit of validators per operator uint32 validatorsPerOperatorLimit; /// @notice The current network fee value - uint64 networkFee; + PackedSSV networkFee; /// @notice The current network fee index value uint64 networkFeeIndex; /// @notice The current balance of the DAO - uint64 daoBalance; - /// @notice The minimum number of blocks before a liquidation event can be triggered - uint64 minimumBlocksBeforeLiquidation; - /// @notice The minimum collateral required for liquidation - uint64 minimumLiquidationCollateral; + PackedSSV daoBalance; + /// @notice The minimum number of blocks before a liquidation event can be triggered for SSV cluster + uint64 minimumBlocksBeforeLiquidationSSV; + /// @notice The minimum collateral required for liquidation of SSV clusters + PackedSSV minimumLiquidationCollateralSSV; /// @notice The period in which an operator can declare a fee change uint64 declareOperatorFeePeriod; /// @notice The period in which an operator fee change can be executed @@ -29,7 +31,33 @@ struct StorageProtocol { /// @notice The maximum increase in operator fee that is allowed (percentage) uint64 operatorMaxFeeIncrease; /// @notice The maximum value in operator fee that is allowed (SSV) - uint64 operatorMaxFee; + uint64 operatorMaxFeeSSV; + + // ETH + /// @notice The block number when the network fee index was last updated for eth + uint32 ethNetworkFeeIndexBlockNumber; + /// @notice The count of validators governed by the DAO for eth clusters + uint32 ethDaoValidatorCount; + /// @notice The block number when the DAO index was last updated for eth + uint32 ethDaoIndexBlockNumber; + /// @notice The current network fee value for eth clusters + PackedETH ethNetworkFee; + /// @notice The current network fee index value for eth clusters + uint64 ethNetworkFeeIndex; + /// @notice The current balance of the DAO for eth clusters + PackedETH ethDaoBalance; + /// @notice The minimum collateral required for liquidation + PackedETH minimumLiquidationCollateral; + /// @notice The minimum number of blocks before a liquidation event can be triggered + uint64 minimumBlocksBeforeLiquidation; + /// @notice The maximum value in operator fee that is allowed (ETH) + PackedETH operatorMaxFee; + + // EB + /// @notice The current total ETH vUnits + uint64 daoTotalEthVUnits; + /// @notice The minimum operator ETH fee (DAO-governed) + PackedETH minimumOperatorEthFee; } library SSVStorageProtocol { diff --git a/contracts/libraries/storage/SSVStorageReentrancy.sol b/contracts/libraries/storage/SSVStorageReentrancy.sol new file mode 100644 index 000000000..bf42585ed --- /dev/null +++ b/contracts/libraries/storage/SSVStorageReentrancy.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +/// @title SSV Reentrancy Guard Storage +/// @notice Represents the storage layout for reentrancy protection in the SSV Network +struct StorageReentrancy { + /// @notice The current reentrancy status (1 = non-entered, 2 = entered) + uint256 status; +} + +library SSVStorageReentrancy { + // keccak256("ssv.network.storage.reentrancy") - 1 + uint256 private constant SSV_REENTRANCY_POSITION = + uint256(keccak256("ssv.network.storage.reentrancy")) - 1; + + function slot() internal pure returns (bytes32) { + return bytes32(SSV_REENTRANCY_POSITION); + } + + function load() internal pure returns (StorageReentrancy storage sr) { + uint256 position = SSV_REENTRANCY_POSITION; + assembly { + sr.slot := position + } + } +} diff --git a/contracts/libraries/storage/SSVStorageStaking.sol b/contracts/libraries/storage/SSVStorageStaking.sol new file mode 100644 index 000000000..5ee0f04cf --- /dev/null +++ b/contracts/libraries/storage/SSVStorageStaking.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {PackedETH} from "../SSVCoreTypes.sol"; + +uint256 constant MAX_DELEGATION_SLOTS = 4; + +struct UnstakeRequest { + /// @notice Amount of cSSV burned and pending to be withdrawn as SSV + uint192 amount; + /// @notice Timestamp after which the pending unstake can be withdrawn + uint64 unlockTime; +} + +struct StorageStaking { + /// @notice Unstake cooldown duration in seconds + uint64 cooldownDuration; + /// @notice Total ETH-denominated rewards (shrunk) allocated to the staking pool + PackedETH stakingEthPoolBalance; + /// @notice Global accumulated ETH rewards per cSSV token (scaled by PRECISION) + uint128 accEthPerShare; + + /// @notice Per-user reward index used to track their last settled accEthPerShare + mapping(address => uint256) userIndex; + /// @notice Accumulated but unclaimed ETH rewards for each user (in wei) + mapping(address => uint256) accrued; + + /// @notice Oracle registry: stable ID => oracle address + mapping(uint32 => address) oracles; + /// @notice Reverse lookup: oracle address => oracle ID (0 if not registered) + mapping(address => uint32) oracleIdOf; + /// @notice Default oracle IDs to use for new delegations (equal split) + uint32[MAX_DELEGATION_SLOTS] defaultOracleIds; + /// @notice Quorum threshold in basis points (e.g. 7000 = 70%) + uint16 quorumBps; + /// @notice The mapping of address to their unstake requests + mapping(address => UnstakeRequest[]) withdrawalRequests; +} + +library SSVStorageStaking { + uint256 private constant SSV_STORAGE_POSITION = uint256(keccak256("ssv.network.storage.staking")) - 1; + + function load() internal pure returns (StorageStaking storage ss) { + uint256 position = SSV_STORAGE_POSITION; + assembly { + ss.slot := position + } + } +} diff --git a/contracts/modules/SSVClusters.sol b/contracts/modules/SSVClusters.sol index 3a77d1dc4..52c6c2cd3 100644 --- a/contracts/modules/SSVClusters.sol +++ b/contracts/modules/SSVClusters.sol @@ -2,180 +2,85 @@ pragma solidity 0.8.24; import {ISSVClusters} from "../interfaces/ISSVClusters.sol"; -import "../libraries/ClusterLib.sol"; -import "../libraries/OperatorLib.sol"; -import "../libraries/ProtocolLib.sol"; -import "../libraries/CoreLib.sol"; -import "../libraries/ValidatorLib.sol"; -import {SSVStorage, StorageData} from "../libraries/SSVStorage.sol"; -import {SSVStorageProtocol, StorageProtocol} from "../libraries/SSVStorageProtocol.sol"; - -contract SSVClusters is ISSVClusters { +import {ClusterLib} from "../libraries/ClusterLib.sol"; +import {OperatorLib} from "../libraries/OperatorLib.sol"; +import {ProtocolLib} from "../libraries/ProtocolLib.sol"; +import {CoreLib} from "../libraries/CoreLib.sol"; +import {PackedSSV, PackedETH, VERSION_ETH, VERSION_SSV, ETH_DEDUCTED_DIGITS, DEFAULT_EB_PER_VALIDATOR, MAX_EB_PER_VALIDATOR, BPS_DENOMINATOR} from "../libraries/SSVCoreTypes.sol"; +import {SSVStorage, StorageData} from "../libraries/storage/SSVStorage.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../libraries/storage/SSVStorageProtocol.sol"; +import { + SSVStorageEB, + StorageEB, + ClusterEBSnapshot +} from "../libraries/storage/SSVStorageEB.sol"; + + +import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import {SSVReentrancyGuard} from "../abstract/SSVReentrancyGuard.sol"; +import {ISSVOperators} from "../interfaces/ISSVOperators.sol"; + +contract SSVClusters is ISSVClusters, SSVReentrancyGuard { using ClusterLib for Cluster; using OperatorLib for Operator; using ProtocolLib for StorageProtocol; - function registerValidator( - bytes calldata publicKey, - uint64[] memory operatorIds, - bytes calldata sharesData, - uint256 amount, - Cluster memory cluster - ) external override { + /** + * @inheritdoc ISSVClusters + */ + function liquidate(address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster) external override nonReentrant { StorageData storage s = SSVStorage.load(); - StorageProtocol storage sp = SSVStorageProtocol.load(); - - ValidatorLib.validateOperatorsLength(operatorIds); - - ValidatorLib.registerPublicKey(publicKey, operatorIds, s); - - bytes32 hashedCluster = cluster.validateClusterOnRegistration(operatorIds, s); - - cluster.balance += amount; - - cluster.updateClusterOnRegistration(operatorIds, hashedCluster, 1, s, sp); - - if (amount != 0) { - CoreLib.deposit(amount); - } - - emit ValidatorAdded(msg.sender, operatorIds, publicKey, sharesData, cluster); - } - - function bulkRegisterValidator( - bytes[] memory publicKeys, - uint64[] memory operatorIds, - bytes[] calldata sharesData, - uint256 amount, - Cluster memory cluster - ) external override { - uint256 validatorsLength = publicKeys.length; - if (validatorsLength == 0) revert EmptyPublicKeysList(); - if (validatorsLength != sharesData.length) revert PublicKeysSharesLengthMismatch(); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_ETH); + cluster.validateClusterIsNotLiquidated(); - StorageData storage s = SSVStorage.load(); StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); - ValidatorLib.validateOperatorsLength(operatorIds); - - for (uint i; i < validatorsLength; ++i) { - ValidatorLib.registerPublicKey(publicKeys[i], operatorIds, s); - } - bytes32 hashedCluster = cluster.validateClusterOnRegistration(operatorIds, s); - - cluster.balance += amount; - - cluster.updateClusterOnRegistration(operatorIds, hashedCluster, uint32(validatorsLength), s, sp); - - if (amount != 0) { - CoreLib.deposit(amount); - } - - for (uint i; i < validatorsLength; ++i) { - bytes memory pk = publicKeys[i]; - bytes memory sh = sharesData[i]; - - emit ValidatorAdded(msg.sender, operatorIds, pk, sh, cluster); - } - } - - function removeValidator( - bytes calldata publicKey, - uint64[] memory operatorIds, - Cluster memory cluster - ) external override { - StorageData storage s = SSVStorage.load(); - - bytes32 hashedCluster = cluster.validateHashedCluster(msg.sender, operatorIds, s); - bytes32 hashedOperatorIds = ValidatorLib.hashOperatorIds(operatorIds); - - bytes32 hashedValidator = keccak256(abi.encodePacked(publicKey, msg.sender)); - bytes32 validatorData = s.validatorPKs[hashedValidator]; - - if (validatorData == bytes32(0)) { - revert ISSVNetworkCore.ValidatorDoesNotExist(); - } - - if (!ValidatorLib.validateCorrectState(validatorData, hashedOperatorIds)) - revert ISSVNetworkCore.IncorrectValidatorStateWithData(publicKey); - - delete s.validatorPKs[hashedValidator]; - - if (cluster.active) { - StorageProtocol storage sp = SSVStorageProtocol.load(); - (uint64 clusterIndex, ) = OperatorLib.updateClusterOperators(operatorIds, false, 1, s, sp); + (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateClusterOperators( + operatorIds, + false, + cluster.validatorCount, + s, + sp + ); - cluster.updateClusterData(clusterIndex, sp.currentNetworkFeeIndex()); + cluster.updateClusterData(hashedCluster, clusterIndex, sp.currentNetworkFeeIndex()); - sp.updateDAO(false, 1); + if ( + clusterOwner != msg.sender && + !cluster.isLiquidatableWithEB( + hashedCluster, + burnRate, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral + ) + ) { + revert ClusterNotLiquidatable(); } - --cluster.validatorCount; - - s.clusters[hashedCluster] = cluster.hashClusterData(); - - emit ValidatorRemoved(msg.sender, operatorIds, publicKey, cluster); + _executeLiquidation(clusterOwner, msg.sender, hashedCluster, operatorIds, cluster, s, sp, seb); } - function bulkRemoveValidator( - bytes[] calldata publicKeys, - uint64[] memory operatorIds, + /** + * @inheritdoc ISSVClusters + */ + function liquidateSSV( + address clusterOwner, + uint64[] calldata operatorIds, Cluster memory cluster - ) external override { - uint256 validatorsLength = publicKeys.length; - - if (validatorsLength == 0) { - revert ISSVNetworkCore.ValidatorDoesNotExist(); - } - StorageData storage s = SSVStorage.load(); - - bytes32 hashedCluster = cluster.validateHashedCluster(msg.sender, operatorIds, s); - bytes32 hashedOperatorIds = ValidatorLib.hashOperatorIds(operatorIds); - - bytes32 hashedValidator; - bytes32 validatorData; - - uint32 validatorsRemoved; - - for (uint i; i < validatorsLength; ++i) { - hashedValidator = keccak256(abi.encodePacked(publicKeys[i], msg.sender)); - validatorData = s.validatorPKs[hashedValidator]; - - if (!ValidatorLib.validateCorrectState(validatorData, hashedOperatorIds)) - revert ISSVNetworkCore.IncorrectValidatorStateWithData(publicKeys[i]); - - delete s.validatorPKs[hashedValidator]; - validatorsRemoved++; - } - - if (cluster.active) { - StorageProtocol storage sp = SSVStorageProtocol.load(); - (uint64 clusterIndex, ) = OperatorLib.updateClusterOperators(operatorIds, false, validatorsRemoved, s, sp); - - cluster.updateClusterData(clusterIndex, sp.currentNetworkFeeIndex()); - - sp.updateDAO(false, validatorsRemoved); - } - - cluster.validatorCount -= validatorsRemoved; - - s.clusters[hashedCluster] = cluster.hashClusterData(); - - for (uint i; i < validatorsLength; ++i) { - emit ValidatorRemoved(msg.sender, operatorIds, publicKeys[i], cluster); - } - } - - function liquidate(address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster) external override { + ) external override nonReentrant { StorageData storage s = SSVStorage.load(); - bytes32 hashedCluster = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_SSV); cluster.validateClusterIsNotLiquidated(); StorageProtocol storage sp = SSVStorageProtocol.load(); - (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateClusterOperators( + (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateClusterOperatorsSSV( operatorIds, false, cluster.validatorCount, @@ -183,7 +88,7 @@ contract SSVClusters is ISSVClusters { sp ); - cluster.updateBalance(clusterIndex, sp.currentNetworkFeeIndex()); + cluster.updateBalanceSSV(clusterIndex, sp.currentNetworkFeeIndexSSV()); uint256 balanceLiquidatable; @@ -191,15 +96,15 @@ contract SSVClusters is ISSVClusters { clusterOwner != msg.sender && !cluster.isLiquidatable( burnRate, - sp.networkFee, - sp.minimumBlocksBeforeLiquidation, - sp.minimumLiquidationCollateral + PackedSSV.unwrap(sp.networkFee), + sp.minimumBlocksBeforeLiquidationSSV, + sp.minimumLiquidationCollateralSSV ) ) { revert ClusterNotLiquidatable(); } - sp.updateDAO(false, cluster.validatorCount); + sp.updateDAOSSV(false, cluster.validatorCount); if (cluster.balance != 0) { balanceLiquidatable = cluster.balance; @@ -212,39 +117,52 @@ contract SSVClusters is ISSVClusters { s.clusters[hashedCluster] = cluster.hashClusterData(); if (balanceLiquidatable != 0) { - CoreLib.transferBalance(msg.sender, balanceLiquidatable); + CoreLib.transferTokenBalance(msg.sender, balanceLiquidatable); } emit ClusterLiquidated(clusterOwner, operatorIds, cluster); } - function reactivate(uint64[] calldata operatorIds, uint256 amount, Cluster memory cluster) external override { + /** + * @inheritdoc ISSVClusters + */ + function reactivate( + uint64[] calldata operatorIds, + Cluster memory cluster + ) external payable override nonReentrant { StorageData storage s = SSVStorage.load(); - bytes32 hashedCluster = cluster.validateHashedCluster(msg.sender, operatorIds, s); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(msg.sender, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_ETH); if (cluster.active) revert ClusterAlreadyEnabled(); StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); - (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateClusterOperators( + uint64 vUnitsCluster = seb.clusterEB[hashedCluster].vUnits; + uint64 baselineVUnits = uint64(cluster.validatorCount) * BPS_DENOMINATOR; + uint64 effectiveVUnits = vUnitsCluster > 0 ? vUnitsCluster : baselineVUnits; + uint64 clusterDeviation = vUnitsCluster > baselineVUnits ? vUnitsCluster - baselineVUnits : 0; + + (uint64 clusterIndex, uint64 burnRate) = OperatorLib.updateClusterOperatorsOnReactivation( operatorIds, - true, cluster.validatorCount, + clusterDeviation, s, - sp + sp, + seb ); - cluster.balance += amount; + cluster.balance += msg.value; cluster.active = true; cluster.index = clusterIndex; cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); - sp.updateDAO(true, cluster.validatorCount); - if ( - cluster.isLiquidatable( + cluster.isLiquidatableWithVUnits( + effectiveVUnits, burnRate, - sp.networkFee, + PackedETH.unwrap(sp.ethNetworkFee), sp.minimumBlocksBeforeLiquidation, sp.minimumLiquidationCollateral ) @@ -252,39 +170,44 @@ contract SSVClusters is ISSVClusters { revert InsufficientBalance(); } - s.clusters[hashedCluster] = cluster.hashClusterData(); - - if (amount > 0) { - CoreLib.deposit(amount); + sp.updateDAO(true, cluster.validatorCount); + if (clusterDeviation > 0) { + sp.daoTotalEthVUnits += clusterDeviation; } + s.ethClusters[hashedCluster] = cluster.hashClusterData(); + emit ClusterReactivated(msg.sender, operatorIds, cluster); } + /** + * @inheritdoc ISSVClusters + */ function deposit( address clusterOwner, uint64[] calldata operatorIds, - uint256 amount, Cluster memory cluster - ) external override { + ) external payable override { StorageData storage s = SSVStorage.load(); - bytes32 hashedCluster = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_ETH); - cluster.balance += amount; - - s.clusters[hashedCluster] = cluster.hashClusterData(); + cluster.balance += msg.value; - CoreLib.deposit(amount); + s.ethClusters[hashedCluster] = cluster.hashClusterData(); - emit ClusterDeposited(clusterOwner, operatorIds, amount, cluster); + emit ClusterDeposited(clusterOwner, operatorIds, msg.value, cluster); } - function withdraw(uint64[] calldata operatorIds, uint256 amount, Cluster memory cluster) external override { + /** + * @inheritdoc ISSVClusters + */ + function withdraw(uint64[] calldata operatorIds, uint256 amount, Cluster memory cluster) external override nonReentrant { StorageData storage s = SSVStorage.load(); - bytes32 hashedCluster = cluster.validateHashedCluster(msg.sender, operatorIds, s); - cluster.validateClusterIsNotLiquidated(); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(msg.sender, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_ETH); StorageProtocol storage sp = SSVStorageProtocol.load(); @@ -294,16 +217,16 @@ contract SSVClusters is ISSVClusters { { uint256 operatorsLength = operatorIds.length; for (uint256 i; i < operatorsLength; ++i) { - Operator storage operator = SSVStorage.load().operators[operatorIds[i]]; + Operator storage operator = s.operators[operatorIds[i]]; clusterIndex += - operator.snapshot.index + - (uint64(block.number) - operator.snapshot.block) * - operator.fee; - burnRate += operator.fee; + operator.ethSnapshot.index + + (uint64(block.number) - operator.ethSnapshot.block) * + PackedETH.unwrap(operator.ethFee); + burnRate += PackedETH.unwrap(operator.ethFee); } } - cluster.updateClusterData(clusterIndex, sp.currentNetworkFeeIndex()); + cluster.updateClusterData(hashedCluster, clusterIndex, sp.currentNetworkFeeIndex()); } if (cluster.balance < amount) revert InsufficientBalance(); @@ -312,9 +235,10 @@ contract SSVClusters is ISSVClusters { if ( cluster.active && cluster.validatorCount != 0 && - cluster.isLiquidatable( + cluster.isLiquidatableWithEB( + hashedCluster, burnRate, - sp.networkFee, + PackedETH.unwrap(sp.ethNetworkFee), sp.minimumBlocksBeforeLiquidation, sp.minimumLiquidationCollateral ) @@ -322,39 +246,372 @@ contract SSVClusters is ISSVClusters { revert InsufficientBalance(); } - s.clusters[hashedCluster] = cluster.hashClusterData(); + s.ethClusters[hashedCluster] = cluster.hashClusterData(); CoreLib.transferBalance(msg.sender, amount); emit ClusterWithdrawn(msg.sender, operatorIds, amount, cluster); } - function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override { + /** + * @inheritdoc ISSVClusters + */ + function migrateClusterToETH(uint64[] calldata operatorIds, Cluster memory cluster) external payable override { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(msg.sender, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_SSV); + bool isLiquidated = !cluster.active; // A liquidated SSV cluster already had its SSV counts removed + + // compute cluster data using ETH fields + (uint64 clusterIndexSSV, uint64 clusterIndexETH, uint64 burnRateETH) = OperatorLib.updateClusterOperatorsMigration( + operatorIds, + cluster.validatorCount, + s, + sp, + isLiquidated + ); + + cluster.updateBalanceSSV(clusterIndexSSV, sp.currentNetworkFeeIndexSSV()); + uint256 ssvClusterBalance = cluster.balance; + + cluster.balance = msg.value; + cluster.active = true; + cluster.index = clusterIndexETH; + cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + + if (!isLiquidated) { + sp.updateDAOSSV(false, cluster.validatorCount); + } + sp.updateDAO(true, cluster.validatorCount); + if ( - !ValidatorLib.validateCorrectState( - SSVStorage.load().validatorPKs[keccak256(abi.encodePacked(publicKey, msg.sender))], - ValidatorLib.hashOperatorIds(operatorIds) + cluster.isLiquidatableWithEB( + hashedCluster, + burnRateETH, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral ) - ) revert ISSVNetworkCore.IncorrectValidatorStateWithData(publicKey); + ) { + revert InsufficientBalance(); + } + + s.ethClusters[hashedCluster] = cluster.hashClusterData(); + delete s.clusters[hashedCluster]; + + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[hashedCluster]; + + // Deviation-only model: baseline added via ethValidatorCount (in updateClusterOperatorsMigration above) + // Only add deviation if cluster has explicit EB tracking + uint64 vUnitsCluster = ebSnapshot.vUnits; + if (vUnitsCluster > 0) { + uint64 baseline = uint64(cluster.validatorCount) * BPS_DENOMINATOR; + + // DAO deviation accounting + if (vUnitsCluster > baseline) { + uint64 deviation = vUnitsCluster - baseline; + sp.daoTotalEthVUnits += deviation; + + // Operator deviation accounting + uint256 n = operatorIds.length; + for (uint256 i; i < n; ++i) { + if (s.operators[operatorIds[i]].ethSnapshot.block == 0) continue; + seb.operatorEthVUnits[operatorIds[i]] += deviation; + } + } + // Note: EB floor is 32 ETH, so vUnitsCluster >= baseline always + // If vUnitsCluster == baseline, deviation is 0, nothing to add + } + // For implicit clusters (vUnitsCluster == 0): no deviation to add + + // For event emission, compute effective balance + uint64 effectiveVUnits = vUnitsCluster > 0 + ? vUnitsCluster + : uint64(cluster.validatorCount) * BPS_DENOMINATOR; + uint32 effectiveBalance = ClusterLib.vUnitsToEB(effectiveVUnits); + + if (ssvClusterBalance != 0) { + CoreLib.transferTokenBalance(msg.sender, ssvClusterBalance); + } + + emit ClusterMigratedToETH(msg.sender, operatorIds, msg.value, ssvClusterBalance, effectiveBalance, cluster); + if (isLiquidated) { + emit ClusterReactivated(msg.sender, operatorIds, cluster); + } + } + + /** + * @inheritdoc ISSVClusters + */ + function updateClusterBalance( + uint64 blockNum, + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster, + uint32 effectiveBalance, + bytes32[] calldata merkleProof + ) external override nonReentrant { + UpdateCtx memory ctx; + StorageData storage s = SSVStorage.load(); + + (ctx.clusterId, ctx.version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ctx.clusterOwner = clusterOwner; + ctx.blockNum = blockNum; + ctx.effectiveBalance = effectiveBalance; + ctx.merkleProof = merkleProof; + + _updateClusterBalanceInternal(operatorIds, cluster, ctx); + } + + function _updateClusterBalanceInternal( + uint64[] calldata operatorIds, + Cluster memory cluster, + UpdateCtx memory ctx + ) internal { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + bytes32 clusterId = ctx.clusterId; + + _verifyEBRoots(ctx, seb); + _verifyEBUpdateFrequency(clusterId, seb); + _verifyEBStaleness(ctx, clusterId, seb); + _verifyMerkleProof(ctx, seb); + _verifyEBLimits(ctx, cluster); + + uint64 newVUnits = ClusterLib.ebToVUnits(ctx.effectiveBalance); + + if (ctx.version == VERSION_ETH) { + // ETH clusters: full accounting flow + uint64 storedVUnits = seb.clusterEB[clusterId].vUnits; + if (storedVUnits == 0) { + storedVUnits = uint64(cluster.validatorCount) * BPS_DENOMINATOR; + } + + uint64 burnRate; + if (cluster.active) { + burnRate = _applyClusterFeeUpdates(operatorIds, cluster, storedVUnits, s, sp); + } + + // Apply new vUnits BEFORE liquidation check so auto-liquidation + if (cluster.active && newVUnits != storedVUnits) { + _updateOperatorVUnits(operatorIds, seb, storedVUnits, newVUnits); + sp.updateDAOEthVUnits(storedVUnits, newVUnits); + } + _updateEBSnapshot(seb, clusterId, ctx.blockNum, newVUnits); + + bool liquidated = _liquidateAfterEBUpdateIfNeeded(cluster, clusterId, ctx.clusterOwner, operatorIds, burnRate, s, sp, seb); + + if (!liquidated && cluster.active) { + s.ethClusters[clusterId] = cluster.hashClusterData(); + } + } else { + // SSV clusters: only update EB snapshot (preparing for future migration) + _updateEBSnapshot(seb, clusterId, ctx.blockNum, newVUnits); + } + + emit ClusterBalanceUpdated(ctx.clusterOwner, operatorIds, ctx.blockNum, ctx.effectiveBalance, cluster); + } + + function _verifyEBRoots(UpdateCtx memory ctx, StorageEB storage seb) internal view { + if (seb.ebRoots[ctx.blockNum] == bytes32(0)) { + revert RootNotFound(); + } + } - emit ValidatorExited(msg.sender, operatorIds, publicKey); + function _verifyEBUpdateFrequency(bytes32 clusterId, StorageEB storage seb) internal view { + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[clusterId]; + if ( + ebSnapshot.lastUpdateBlock != 0 && block.number < ebSnapshot.lastUpdateBlock + seb.minBlocksBetweenUpdates + ) { + revert UpdateTooFrequent(); + } + } + + function _verifyEBStaleness(UpdateCtx memory ctx, bytes32 clusterId, StorageEB storage seb) internal view { + if (ctx.blockNum != seb.latestCommittedBlock) { + revert MustUseLatestRoot(); + } + + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[clusterId]; + if (ebSnapshot.lastRootBlockNum != 0 && ctx.blockNum <= ebSnapshot.lastRootBlockNum) { + revert StaleUpdate(); + } + } + + function _verifyMerkleProof(UpdateCtx memory ctx, StorageEB storage seb) internal view { + bytes32 root = seb.ebRoots[ctx.blockNum]; + + if (!MerkleProof.verify(ctx.merkleProof, root, keccak256(abi.encodePacked(keccak256(abi.encode(ctx.clusterId, ctx.effectiveBalance)))))) { + revert InvalidProof(); + } + } + + function _verifyEBLimits(UpdateCtx memory ctx, Cluster memory cluster) internal pure { + if (ctx.effectiveBalance > uint256(cluster.validatorCount) * (MAX_EB_PER_VALIDATOR / 1 ether)) { + revert EBExceedsMaximum(); + } else if (ctx.effectiveBalance < uint256(cluster.validatorCount) * (DEFAULT_EB_PER_VALIDATOR / 1 ether)) { + revert EBBelowMinimum(); + } + } + + function _applyClusterFeeUpdates( + uint64[] calldata operatorIds, + Cluster memory cluster, + uint64 oldVUnits, + StorageData storage s, + StorageProtocol storage sp + ) internal returns (uint64 burnRate) { + // ETH path: use ethSnapshot, ethFee, ethNetworkFeeIndex + (uint64 clusterIndex, uint64 cumulativeFee) = OperatorLib.updateClusterOperators(operatorIds, false, 0, s, sp); + uint64 currentNetworkFeeIndex = sp.currentNetworkFeeIndex(); + + // Calculate fee deltas BEFORE updating indexes + uint128 units = oldVUnits; + uint128 idxNet = currentNetworkFeeIndex - cluster.networkFeeIndex; + uint128 idxOp = clusterIndex - cluster.index; + + uint128 networkFeeUnits = (idxNet * units) / BPS_DENOMINATOR; + uint128 operatorFeeUnits = (idxOp * units) / BPS_DENOMINATOR; + uint256 totalFees = (uint256(networkFeeUnits) + uint256(operatorFeeUnits)) * ETH_DEDUCTED_DIGITS; + + // Update indexes + cluster.index = clusterIndex; + cluster.networkFeeIndex = currentNetworkFeeIndex; + + if (cluster.balance >= totalFees) { + cluster.balance -= totalFees; + } else { + cluster.balance = 0; + } + + return cumulativeFee; + } + + function _updateOperatorVUnits( + uint64[] calldata operatorIds, + StorageEB storage seb, + uint64 storedVUnits, + uint64 newVUnits + ) internal { + // Caller must ensure newVUnits != storedVUnits + bool deltaPositive = newVUnits > storedVUnits; + uint64 deltaAbs = deltaPositive ? newVUnits - storedVUnits : storedVUnits - newVUnits; + + StorageData storage s = SSVStorage.load(); + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + uint64 operatorId = operatorIds[i]; + if (s.operators[operatorId].ethSnapshot.block == 0) continue; + if (deltaPositive) seb.operatorEthVUnits[operatorId] += deltaAbs; + else seb.operatorEthVUnits[operatorId] -= deltaAbs; + } + } + + function _updateEBSnapshot(StorageEB storage seb, bytes32 clusterId, uint64 blockNum, uint64 newVUnits) internal { + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[clusterId]; + ebSnapshot.vUnits = newVUnits; + ebSnapshot.lastRootBlockNum = blockNum; + ebSnapshot.lastUpdateBlock = uint64(block.number); + } + + function _liquidateAfterEBUpdateIfNeeded( + Cluster memory cluster, + bytes32 clusterId, + address clusterOwner, + uint64[] calldata operatorIds, + uint64 burnRate, + StorageData storage s, + StorageProtocol storage sp, + StorageEB storage seb + ) internal returns (bool liquidated) { + if (!cluster.active || cluster.validatorCount == 0) return false; + + if (cluster.isLiquidatableWithEB( + clusterId, + burnRate, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral + )) { + + for (uint256 i; i < operatorIds.length; ++i) { + ISSVOperators.Operator storage op = s.operators[operatorIds[i]]; + if (op.ethSnapshot.block != 0) { + op.ethValidatorCount -= cluster.validatorCount; + } + } + + _executeLiquidation(clusterOwner, msg.sender, clusterId, operatorIds, cluster, s, sp, seb); + return true; + } + return false; } - function bulkExitValidator(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external override { - if (publicKeys.length == 0) { - revert ISSVNetworkCore.ValidatorDoesNotExist(); + function _executeLiquidation( + address clusterOwner, + address liquidator, + bytes32 clusterId, + uint64[] calldata operatorIds, + Cluster memory cluster, + StorageData storage s, + StorageProtocol storage sp, + StorageEB storage seb + ) internal { + sp.updateDAO(false, cluster.validatorCount); + + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[clusterId]; + uint64 vUnitsCluster = ebSnapshot.vUnits; + + // Deviation-only model: only subtract deviation from operatorEthVUnits + // Baseline is removed via ethValidatorCount decrement (in updateClusterOperators above) + if (vUnitsCluster > 0) { + uint64 baselineVUnits = uint64(cluster.validatorCount) * BPS_DENOMINATOR; + + // DAO deviation accounting + if (vUnitsCluster != baselineVUnits) { + bool moreThanBaseline = vUnitsCluster > baselineVUnits; + uint64 deviation = moreThanBaseline ? vUnitsCluster - baselineVUnits : baselineVUnits - vUnitsCluster; + + if (deviation != 0) { + if (moreThanBaseline) sp.daoTotalEthVUnits -= deviation; + else sp.daoTotalEthVUnits += deviation; + } + + // Operator deviation accounting: only subtract deviation, not baseline + // Note: EB floor is 32 ETH, so vUnitsCluster >= baselineVUnits always + // But we handle both cases for safety + uint256 n = operatorIds.length; + for (uint256 i; i < n; ++i) { + if (s.operators[operatorIds[i]].ethSnapshot.block == 0) continue; + if (moreThanBaseline) { + seb.operatorEthVUnits[operatorIds[i]] -= deviation; + } else { + seb.operatorEthVUnits[operatorIds[i]] += deviation; + } + } + } + // If vUnitsCluster == baselineVUnits, deviation is 0, nothing to update + } - bytes32 hashedOperatorIds = ValidatorLib.hashOperatorIds(operatorIds); + // For implicit clusters (vUnitsCluster == 0): no deviation to remove - for (uint i; i < publicKeys.length; ++i) { - if ( - !ValidatorLib.validateCorrectState( - SSVStorage.load().validatorPKs[keccak256(abi.encodePacked(publicKeys[i], msg.sender))], - hashedOperatorIds - ) - ) revert ISSVNetworkCore.IncorrectValidatorStateWithData(publicKeys[i]); + uint256 balanceLiquidatable = cluster.balance; + cluster.balance = 0; + cluster.active = false; + cluster.index = 0; + cluster.networkFeeIndex = 0; - emit ValidatorExited(msg.sender, operatorIds, publicKeys[i]); + s.ethClusters[clusterId] = cluster.hashClusterData(); + + if (balanceLiquidatable > 0) { + CoreLib.transferBalance(liquidator, balanceLiquidatable); } + + emit ClusterLiquidated(clusterOwner, operatorIds, cluster); } } diff --git a/contracts/modules/SSVDAO.sol b/contracts/modules/SSVDAO.sol index 084375704..1b46f6562 100644 --- a/contracts/modules/SSVDAO.sol +++ b/contracts/modules/SSVDAO.sol @@ -2,61 +2,102 @@ pragma solidity 0.8.24; import {ISSVDAO} from "../interfaces/ISSVDAO.sol"; -import {Types64, Types256} from "../libraries/Types.sol"; -import "../libraries/ProtocolLib.sol"; -import "../libraries/CoreLib.sol"; -import {SSVStorageProtocol, StorageProtocol} from "../libraries/SSVStorageProtocol.sol"; - -contract SSVDAO is ISSVDAO { - using Types64 for uint64; - using Types256 for uint256; +import {ProtocolLib} from "../libraries/ProtocolLib.sol"; +import {CoreLib} from "../libraries/CoreLib.sol"; +import {PackedSSV, PackedETH, BPS_DENOMINATOR} from "../libraries/SSVCoreTypes.sol"; +import {PackedSSVLib, PackedETHLib} from "../libraries/SSVPackedLib.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../libraries/storage/SSVStorageProtocol.sol"; +import {SSVStorageEB, StorageEB} from "../libraries/storage/SSVStorageEB.sol"; +import {ICSSVToken} from "../interfaces/ICSSVToken.sol"; +import {SSVStorageStaking, StorageStaking, MAX_DELEGATION_SLOTS} from "../libraries/storage/SSVStorageStaking.sol"; +import {SSVReentrancyGuard} from "../abstract/SSVReentrancyGuard.sol"; +contract SSVDAO is ISSVDAO, SSVReentrancyGuard { using ProtocolLib for StorageProtocol; + using PackedSSVLib for PackedSSV; + + uint64 private constant MINIMAL_LIQUIDATION_THRESHOLD = 21_480; + address public immutable CSSV_ADDRESS; - uint64 private constant MINIMAL_LIQUIDATION_THRESHOLD = 100_800; + constructor(address _cssv) { + CSSV_ADDRESS = _cssv; + } + /** + * @inheritdoc ISSVDAO + */ function updateNetworkFee(uint256 fee) external override { StorageProtocol storage sp = SSVStorageProtocol.load(); - uint64 previousFee = sp.networkFee; + PackedETH previousFee = sp.ethNetworkFee; sp.updateNetworkFee(fee); - emit NetworkFeeUpdated(previousFee.expand(), fee); + emit NetworkFeeUpdated(PackedETHLib.unpack(previousFee), fee); + } + + /** + * @inheritdoc ISSVDAO + */ + function updateNetworkFeeSSV(uint256 fee) external override { + StorageProtocol storage sp = SSVStorageProtocol.load(); + PackedSSV previousFee = sp.networkFee; + + sp.updateNetworkFeeSSV(fee); + emit NetworkFeeUpdatedSSV(PackedSSVLib.unpack(previousFee), fee); } - function withdrawNetworkEarnings(uint256 amount) external override { + /** + * @inheritdoc ISSVDAO + */ + function withdrawNetworkSSVEarnings(uint256 amount) external override nonReentrant { StorageProtocol storage sp = SSVStorageProtocol.load(); - uint64 shrunkAmount = amount.shrink(); + PackedSSV shrunkAmount = PackedSSVLib.pack(amount); - uint64 networkBalance = sp.networkTotalEarnings(); + PackedSSV networkBalance = sp.networkTotalEarningsSSV(); - if (shrunkAmount > networkBalance) { + if (shrunkAmount.gt(networkBalance)) { revert InsufficientBalance(); } - sp.daoBalance = networkBalance - shrunkAmount; + sp.daoBalance = networkBalance.sub(shrunkAmount); sp.daoIndexBlockNumber = uint32(block.number); - CoreLib.transferBalance(msg.sender, amount); + CoreLib.transferTokenBalance(msg.sender, amount); emit NetworkEarningsWithdrawn(amount, msg.sender); } + /** + * @inheritdoc ISSVDAO + */ function updateOperatorFeeIncreaseLimit(uint64 percentage) external override { + if (percentage > BPS_DENOMINATOR) { + revert InvalidOperatorFeeIncreaseLimit(); + } + SSVStorageProtocol.load().operatorMaxFeeIncrease = percentage; emit OperatorFeeIncreaseLimitUpdated(percentage); } + /** + * @inheritdoc ISSVDAO + */ function updateDeclareOperatorFeePeriod(uint64 timeInSeconds) external override { SSVStorageProtocol.load().declareOperatorFeePeriod = timeInSeconds; emit DeclareOperatorFeePeriodUpdated(timeInSeconds); } + /** + * @inheritdoc ISSVDAO + */ function updateExecuteOperatorFeePeriod(uint64 timeInSeconds) external override { SSVStorageProtocol.load().executeOperatorFeePeriod = timeInSeconds; emit ExecuteOperatorFeePeriodUpdated(timeInSeconds); } + /** + * @inheritdoc ISSVDAO + */ function updateLiquidationThresholdPeriod(uint64 blocks) external override { if (blocks < MINIMAL_LIQUIDATION_THRESHOLD) { revert NewBlockPeriodIsBelowMinimum(); @@ -66,13 +107,171 @@ contract SSVDAO is ISSVDAO { emit LiquidationThresholdPeriodUpdated(blocks); } + /** + * @inheritdoc ISSVDAO + */ + function updateLiquidationThresholdPeriodSSV(uint64 blocks) external { + if (blocks < MINIMAL_LIQUIDATION_THRESHOLD) { + revert NewBlockPeriodIsBelowMinimum(); + } + + SSVStorageProtocol.load().minimumBlocksBeforeLiquidationSSV = blocks; + emit LiquidationThresholdPeriodSSVUpdated(blocks); + } + + /** + * @inheritdoc ISSVDAO + */ function updateMinimumLiquidationCollateral(uint256 amount) external override { - SSVStorageProtocol.load().minimumLiquidationCollateral = amount.shrink(); + SSVStorageProtocol.load().minimumLiquidationCollateral = PackedETHLib.pack(amount); emit MinimumLiquidationCollateralUpdated(amount); } - function updateMaximumOperatorFee(uint64 maxFee) external override { - SSVStorageProtocol.load().operatorMaxFee = maxFee; + /** + * @inheritdoc ISSVDAO + */ + function updateMinimumLiquidationCollateralSSV(uint256 amount) external { + SSVStorageProtocol.load().minimumLiquidationCollateralSSV = PackedSSVLib.pack(amount); + emit MinimumLiquidationCollateralSSVUpdated(amount); + } + + /** + * @inheritdoc ISSVDAO + */ + function updateMaximumOperatorFee(uint256 maxFee) external override { + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (maxFee < PackedETHLib.unpack(sp.minimumOperatorEthFee)) { + revert InvalidOperatorFeeRange(); + } + + sp.operatorMaxFee = PackedETHLib.pack(maxFee); emit OperatorMaximumFeeUpdated(maxFee); } + + + /** + * @inheritdoc ISSVDAO + */ + function updateMinimumOperatorEthFee(uint256 minFee) external override { + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (minFee > PackedETHLib.unpack(sp.operatorMaxFee)) { + revert InvalidOperatorFeeRange(); + } + + sp.minimumOperatorEthFee = PackedETHLib.pack(minFee); + emit MinimumOperatorEthFeeUpdated(minFee); + } + + /** + * @inheritdoc ISSVDAO + */ + function commitRoot(bytes32 merkleRoot, uint64 blockNum) external override { + StorageEB storage seb = SSVStorageEB.load(); + StorageStaking storage s = SSVStorageStaking.load(); + + uint32 oracleId = s.oracleIdOf[msg.sender]; + if (oracleId == 0) revert NotOracle(); + + // Enforce monotonicity - new block must be greater than last + if (blockNum <= seb.latestCommittedBlock) { + revert StaleBlockNumber(); + } + + // Ensure block is not in the future + if (blockNum > block.number) { + revert FutureBlockNumber(); + } + + // block and root combined to keep block-root proposal tied together + bytes32 commitmentKey = keccak256(abi.encodePacked(blockNum, merkleRoot)); + + if (seb.hasVoted[commitmentKey][oracleId]) revert AlreadyVoted(); + seb.hasVoted[commitmentKey][oracleId] = true; + + uint256 oracleCount = s.defaultOracleIds.length; + uint256 totalStaked = seb.roundFrozenSupply[commitmentKey]; + if (totalStaked == 0) { + uint256 rawSupply = ICSSVToken(CSSV_ADDRESS).totalSupply(); + if (rawSupply == 0) revert ZeroCSSVSupply(); + + totalStaked = rawSupply - (rawSupply % oracleCount); + if (totalStaked == 0) revert InsufficientCSSVSupply(); + seb.roundFrozenSupply[commitmentKey] = totalStaked; + } + + uint256 weight = totalStaked / oracleCount; + seb.rootCommitments[commitmentKey] += weight; + + uint256 accumulatedWeight = seb.rootCommitments[commitmentKey]; + + uint256 threshold = (totalStaked * s.quorumBps) / BPS_DENOMINATOR; + + emit WeightedRootProposed(merkleRoot, blockNum, accumulatedWeight, threshold, oracleId, msg.sender); + + if (accumulatedWeight >= threshold) { + seb.ebRoots[blockNum] = merkleRoot; + seb.latestCommittedBlock = blockNum; + + delete seb.rootCommitments[commitmentKey]; + delete seb.roundFrozenSupply[commitmentKey]; + // Do not delete hasVoted to prevent re-voting if same key is somehow reused + + emit RootCommitted(merkleRoot, blockNum); + } + } + + /** + * @inheritdoc ISSVDAO + */ + function replaceOracle(uint32 oracleId, address newOracle) external override { + StorageStaking storage s = SSVStorageStaking.load(); + if (oracleId == 0 || oracleId > MAX_DELEGATION_SLOTS) revert InvalidOracleId(); + if (newOracle == address(0)) revert ZeroAddress(); + + address oldOracle = s.oracles[oracleId]; + if (oldOracle == newOracle) { + revert SameOracleAddressNotAllowed(); + } + + // Clear reverse mapping for old oracle if existed + if (oldOracle != address(0)) { + s.oracleIdOf[oldOracle] = 0; + } + + // Ensure newOracle is not already assigned to another ID + uint32 existing = s.oracleIdOf[newOracle]; + if (existing != 0 && existing != oracleId) revert OracleAlreadyAssigned(); + + s.oracles[oracleId] = newOracle; + s.oracleIdOf[newOracle] = oracleId; + + emit OracleReplaced(oracleId, oldOracle, newOracle); + } + + /** + * @inheritdoc ISSVDAO + */ + function updateQuorumBps(uint16 quorum) external override { + if (quorum == 0 || quorum > BPS_DENOMINATOR) { + revert InvalidQuorum(); + } + SSVStorageStaking.load().quorumBps = quorum; + emit QuorumUpdated(quorum); + } + + /** + * @inheritdoc ISSVDAO + */ + function updateUnstakeCooldownDuration(uint64 duration) external override { + SSVStorageStaking.load().cooldownDuration = duration; + emit CooldownDurationUpdated(duration); + } + + /** + * @inheritdoc ISSVDAO + */ + function updateMinBlocksBetweenUpdates(uint32 blocks) external override { + SSVStorageEB.load().minBlocksBetweenUpdates = blocks; + emit MinBlocksBetweenUpdatesUpdated(blocks); + } } diff --git a/contracts/modules/SSVOperators.sol b/contracts/modules/SSVOperators.sol index 029cbc343..205b2267c 100644 --- a/contracts/modules/SSVOperators.sol +++ b/contracts/modules/SSVOperators.sol @@ -2,53 +2,60 @@ pragma solidity 0.8.24; import {ISSVOperators} from "../interfaces/ISSVOperators.sol"; -import {Types64, Types256} from "../libraries/Types.sol"; -import {SSVStorage, StorageData} from "../libraries/SSVStorage.sol"; -import {SSVStorageProtocol, StorageProtocol} from "../libraries/SSVStorageProtocol.sol"; -import "../libraries/OperatorLib.sol"; -import "../libraries/CoreLib.sol"; +import {PackedSSV, PackedETH, VERSION_ETH, VERSION_SSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO, BPS_DENOMINATOR} from "../libraries/SSVCoreTypes.sol"; +import {PackedSSVLib, PackedETHLib} from "../libraries/SSVPackedLib.sol"; +import {SSVStorage, StorageData} from "../libraries/storage/SSVStorage.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../libraries/storage/SSVStorageProtocol.sol"; +import {OperatorLib} from "../libraries/OperatorLib.sol"; +import {CoreLib} from "../libraries/CoreLib.sol"; +import {SSVStorageEB, StorageEB} from "../libraries/storage/SSVStorageEB.sol"; +import {SSVReentrancyGuard} from "../abstract/SSVReentrancyGuard.sol"; import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; -contract SSVOperators is ISSVOperators { - uint64 private constant MINIMAL_OPERATOR_FEE = 1_000_000_000; - uint64 private constant PRECISION_FACTOR = 10_000; +contract SSVOperators is ISSVOperators, SSVReentrancyGuard { + uint256 public immutable UPGRADE_TIMESTAMP; - using Types256 for uint256; - using Types64 for uint64; using Counters for Counters.Counter; using OperatorLib for Operator; + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; - /*******************************/ - /* Operator External Functions */ - /*******************************/ + constructor(uint256 upgradeTimestamp) { + UPGRADE_TIMESTAMP = upgradeTimestamp; + } + /** + * @inheritdoc ISSVOperators + */ function registerOperator( bytes calldata publicKey, uint256 fee, bool setPrivate ) external override returns (uint64 id) { - if (fee != 0 && fee < MINIMAL_OPERATOR_FEE) { - revert ISSVNetworkCore.FeeTooLow(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (fee != 0 && fee < PackedETHLib.unpack(sp.minimumOperatorEthFee)) { + revert FeeTooLow(); } - if (fee > SSVStorageProtocol.load().operatorMaxFee) { - revert ISSVNetworkCore.FeeTooHigh(); + if (fee > PackedETHLib.unpack(sp.operatorMaxFee)) { + revert FeeTooHigh(); } StorageData storage s = SSVStorage.load(); bytes32 hashedPk = keccak256(publicKey); - if (s.operatorsPKs[hashedPk] != 0) revert ISSVNetworkCore.OperatorAlreadyExists(); + if (s.operatorsPKs[hashedPk] != 0) revert OperatorAlreadyExists(); s.lastOperatorId.increment(); id = uint64(s.lastOperatorId.current()); - s.operators[id] = Operator({ - owner: msg.sender, - snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: 0}), - validatorCount: 0, - fee: fee.shrink(), - whitelisted: setPrivate - }); + Operator storage op = s.operators[id]; + + op.owner = msg.sender; + op.whitelisted = setPrivate; + op.ethFee = PackedETHLib.pack(fee); + + op.ethSnapshot.block = uint32(block.number); s.operatorsPKs[hashedPk] = id; uint64[] memory operatorIds = new uint64[](1); @@ -58,87 +65,118 @@ contract SSVOperators is ISSVOperators { emit OperatorPrivacyStatusUpdated(operatorIds, setPrivate); } - function removeOperator(uint64 operatorId) external override { + /** + * @inheritdoc ISSVOperators + */ + function removeOperator(uint64 operatorId) external override nonReentrant { StorageData storage s = SSVStorage.load(); + Operator storage operator = s.operators[operatorId]; + StorageEB storage seb = SSVStorageEB.load(); - Operator memory operator = s.operators[operatorId]; operator.checkOwner(); - operator.updateSnapshot(); - uint64 currentBalance = operator.snapshot.balance; + PackedETH currentBalanceETH = PACKED_ETH_ZERO; + PackedSSV currentBalanceSSV = PACKED_SSV_ZERO; - operator.snapshot.block = 0; - operator.snapshot.balance = 0; - operator.validatorCount = 0; - operator.fee = 0; + if (operator.snapshot.block != 0) { + OperatorLib.updateSnapshotStSSV(operator); + currentBalanceSSV = operator.snapshot.balance; + } - s.operators[operatorId] = operator; + if (operator.ethSnapshot.block != 0) { + OperatorLib.updateSnapshotSt(operator, operatorId); + currentBalanceETH = operator.ethSnapshot.balance; + } + _resetOperatorState(operator); + + delete seb.operatorEthVUnits[operatorId]; + delete s.operatorFeeChangeRequests[operatorId]; delete s.operatorsWhitelist[operatorId]; - if (currentBalance > 0) { - _transferOperatorBalanceUnsafe(operatorId, currentBalance.expand()); + if (PackedETHLib.raw(currentBalanceETH) > 0) { + _transferOperatorBalanceUnsafe(operatorId, PackedETHLib.unpack(currentBalanceETH)); + } + if (PackedSSVLib.raw(currentBalanceSSV) > 0) { + _transferOperatorTokenBalanceUnsafe(operatorId, PackedSSVLib.unpack(currentBalanceSSV)); } emit OperatorRemoved(operatorId); } + /** + * @inheritdoc ISSVOperators + */ function declareOperatorFee(uint64 operatorId, uint256 fee) external override { StorageData storage s = SSVStorage.load(); s.operators[operatorId].checkOwner(); StorageProtocol storage sp = SSVStorageProtocol.load(); - if (fee != 0 && fee < MINIMAL_OPERATOR_FEE) revert FeeTooLow(); - if (fee > sp.operatorMaxFee) revert FeeTooHigh(); - - uint64 operatorFee = s.operators[operatorId].fee; - uint64 shrunkFee = fee.shrink(); + if (fee != 0 && fee < PackedETHLib.unpack(sp.minimumOperatorEthFee)) revert FeeTooLow(); + if (fee > PackedETHLib.unpack(sp.operatorMaxFee)) revert FeeTooHigh(); + if (s.operators[operatorId].ethSnapshot.block == 0) { + s.operators[operatorId].ensureETHDefaults(operatorId); + } + PackedSSV operatorSSVFee = s.operators[operatorId].fee; + PackedETH operatorFee = s.operators[operatorId].ethFee; + PackedETH shrunkFee = PackedETHLib.pack(fee); - if (operatorFee == shrunkFee) { + if (operatorFee.eq(shrunkFee)) { revert SameFeeChangeNotAllowed(); - } else if (shrunkFee != 0 && operatorFee == 0) { + } else if (shrunkFee.raw() != 0 && operatorFee.raw() == 0 && operatorSSVFee.raw() == 0) { revert FeeIncreaseNotAllowed(); } // @dev 100% = 10000, 10% = 1000 - using 10000 to represent 2 digit precision - uint64 maxAllowedFee = (operatorFee * (PRECISION_FACTOR + sp.operatorMaxFeeIncrease)) / PRECISION_FACTOR; + uint64 maxAllowedFee = (operatorFee.raw() * (BPS_DENOMINATOR + sp.operatorMaxFeeIncrease) + BPS_DENOMINATOR - 1) / BPS_DENOMINATOR; - if (shrunkFee > maxAllowedFee) revert FeeExceedsIncreaseLimit(); + if (shrunkFee.raw() > maxAllowedFee) revert FeeExceedsIncreaseLimit(); s.operatorFeeChangeRequests[operatorId] = OperatorFeeChangeRequest( - shrunkFee, + PackedETH.unwrap(shrunkFee), uint64(block.timestamp) + sp.declareOperatorFeePeriod, uint64(block.timestamp) + sp.declareOperatorFeePeriod + sp.executeOperatorFeePeriod ); emit OperatorFeeDeclared(msg.sender, operatorId, block.number, fee); } + /** + * @inheritdoc ISSVOperators + */ function executeOperatorFee(uint64 operatorId) external override { StorageData storage s = SSVStorage.load(); - Operator memory operator = s.operators[operatorId]; - operator.checkOwner(); + s.operators[operatorId].checkOwner(); OperatorFeeChangeRequest memory feeChangeRequest = s.operatorFeeChangeRequests[operatorId]; if (feeChangeRequest.approvalBeginTime == 0) revert NoFeeDeclared(); + if (feeChangeRequest.approvalBeginTime <= UPGRADE_TIMESTAMP) { + revert LegacyOperatorFeeDeclarationInvalid(); + } + if ( block.timestamp < feeChangeRequest.approvalBeginTime || block.timestamp > feeChangeRequest.approvalEndTime ) { revert ApprovalNotWithinTimeframe(); } - if (feeChangeRequest.fee.expand() > SSVStorageProtocol.load().operatorMaxFee) revert FeeTooHigh(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (PackedETH.wrap(feeChangeRequest.fee).gt(sp.operatorMaxFee)) revert FeeTooHigh(); + if (feeChangeRequest.fee != 0 && feeChangeRequest.fee < PackedETH.unwrap(sp.minimumOperatorEthFee)) revert FeeTooLow(); - operator.updateSnapshot(); - operator.fee = feeChangeRequest.fee; - s.operators[operatorId] = operator; + Operator storage operator = s.operators[operatorId]; + OperatorLib.updateSnapshotSt(operator, operatorId); + operator.ethFee = PackedETH.wrap(feeChangeRequest.fee); delete s.operatorFeeChangeRequests[operatorId]; - emit OperatorFeeExecuted(msg.sender, operatorId, block.number, feeChangeRequest.fee.expand()); + emit OperatorFeeExecuted(msg.sender, operatorId, block.number, PackedETHLib.unpack(PackedETH.wrap(feeChangeRequest.fee))); } + /** + * @inheritdoc ISSVOperators + */ function cancelDeclaredOperatorFee(uint64 operatorId) external override { StorageData storage s = SSVStorage.load(); s.operators[operatorId].checkOwner(); @@ -150,18 +188,26 @@ contract SSVOperators is ISSVOperators { emit OperatorFeeDeclarationCancelled(msg.sender, operatorId); } + /** + * @inheritdoc ISSVOperators + */ function reduceOperatorFee(uint64 operatorId, uint256 fee) external override { StorageData storage s = SSVStorage.load(); - Operator memory operator = s.operators[operatorId]; - operator.checkOwner(); + s.operators[operatorId].checkOwner(); + + if (fee != 0 && fee < PackedETHLib.unpack(SSVStorageProtocol.load().minimumOperatorEthFee)) revert FeeTooLow(); + + if (s.operators[operatorId].ethSnapshot.block == 0) { + s.operators[operatorId].ensureETHDefaults(operatorId); + } - if (fee != 0 && fee < MINIMAL_OPERATOR_FEE) revert FeeTooLow(); + Operator memory operator = s.operators[operatorId]; - uint64 shrunkAmount = fee.shrink(); - if (shrunkAmount >= operator.fee) revert FeeIncreaseNotAllowed(); + PackedETH shrunkAmount = PackedETHLib.pack(fee); + if (shrunkAmount.gte(operator.ethFee)) revert FeeIncreaseNotAllowed(); - operator.updateSnapshot(); - operator.fee = shrunkAmount; + operator.updateSnapshot(operatorId); + operator.ethFee = shrunkAmount; s.operators[operatorId] = operator; delete s.operatorFeeChangeRequests[operatorId]; @@ -169,52 +215,157 @@ contract SSVOperators is ISSVOperators { emit OperatorFeeExecuted(msg.sender, operatorId, block.number, fee); } + /** + * @inheritdoc ISSVOperators + */ function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external override { OperatorLib.updatePrivacyStatus(operatorIds, true, SSVStorage.load()); emit OperatorPrivacyStatusUpdated(operatorIds, true); } + /** + * @inheritdoc ISSVOperators + */ function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external override { OperatorLib.updatePrivacyStatus(operatorIds, false, SSVStorage.load()); emit OperatorPrivacyStatusUpdated(operatorIds, false); } - function withdrawOperatorEarnings(uint64 operatorId, uint256 amount) external override { - _withdrawOperatorEarnings(operatorId, amount); + /** + * @inheritdoc ISSVOperators + */ + function withdrawOperatorEarnings(uint64 operatorId, uint256 amount) external override nonReentrant { + _withdrawOperatorEarnings(operatorId, amount, VERSION_ETH); } - function withdrawAllOperatorEarnings(uint64 operatorId) external override { - _withdrawOperatorEarnings(operatorId, 0); + /** + * @inheritdoc ISSVOperators + */ + function withdrawAllOperatorEarnings(uint64 operatorId) external override nonReentrant { + _withdrawOperatorEarnings(operatorId, 0, VERSION_ETH); } - // private functions - function _withdrawOperatorEarnings(uint64 operatorId, uint256 amount) private { + /** + * @inheritdoc ISSVOperators + */ + function withdrawAllVersionOperatorEarnings(uint64 operatorId) external override nonReentrant { StorageData storage s = SSVStorage.load(); - Operator memory operator = s.operators[operatorId]; + Operator storage operator = s.operators[operatorId]; operator.checkOwner(); - operator.updateSnapshot(); + PackedETH ethBalance = PACKED_ETH_ZERO; + PackedSSV ssvBalance = PACKED_SSV_ZERO; - uint64 shrunkWithdrawn; - uint64 shrunkAmount = amount.shrink(); + if (operator.snapshot.block != 0) { + OperatorLib.updateSnapshotStSSV(operator); + ssvBalance = operator.snapshot.balance; + operator.snapshot.balance = PACKED_SSV_ZERO; + } - if (amount == 0 && operator.snapshot.balance > 0) { - shrunkWithdrawn = operator.snapshot.balance; - } else if (amount > 0 && operator.snapshot.balance >= shrunkAmount) { - shrunkWithdrawn = shrunkAmount; - } else { - revert InsufficientBalance(); + if (operator.ethSnapshot.block != 0) { + OperatorLib.updateSnapshotSt(operator, operatorId); + ethBalance = operator.ethSnapshot.balance; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; } - operator.snapshot.balance -= shrunkWithdrawn; + if (PackedETHLib.raw(ethBalance) > 0) { + _transferOperatorBalanceUnsafe(operatorId, PackedETHLib.unpack(ethBalance)); + } + if (PackedSSVLib.raw(ssvBalance) > 0) { + _transferOperatorTokenBalanceUnsafe(operatorId, PackedSSVLib.unpack(ssvBalance)); + } + } - s.operators[operatorId] = operator; + /** + * @inheritdoc ISSVOperators + */ + function withdrawOperatorEarningsSSV(uint64 operatorId, uint256 amount) external override nonReentrant { + _withdrawOperatorEarnings(operatorId, amount, VERSION_SSV); + } - _transferOperatorBalanceUnsafe(operatorId, shrunkWithdrawn.expand()); + /** + * @inheritdoc ISSVOperators + */ + function withdrawAllOperatorEarningsSSV(uint64 operatorId) external override nonReentrant { + _withdrawOperatorEarnings(operatorId, 0, VERSION_SSV); + } + + // private functions + function _withdrawOperatorEarnings( + uint64 operatorId, + uint256 amount, + uint8 version + ) private { + StorageData storage s = SSVStorage.load(); + Operator storage operator = s.operators[operatorId]; + + operator.checkOwner(); + + if (version == VERSION_ETH) { + if (operator.ethSnapshot.block == 0) revert InsufficientBalance(); + + PackedETH shrunkWithdrawn; + PackedETH shrunkAmount = PackedETHLib.pack(amount); + OperatorLib.updateSnapshotSt(operator, operatorId); + + PackedETH balance = operator.ethSnapshot.balance; + + if (amount == 0) { + if (PackedETHLib.raw(balance) == 0) revert InsufficientBalance(); + shrunkWithdrawn = balance; + } else { + if (PackedETHLib.raw(balance) < PackedETHLib.raw(shrunkAmount)) revert InsufficientBalance(); + shrunkWithdrawn = shrunkAmount; + } + + operator.ethSnapshot.balance = balance.sub(shrunkWithdrawn); + _transferOperatorBalanceUnsafe(operatorId, PackedETHLib.unpack(shrunkWithdrawn)); + + } else if (version == VERSION_SSV) { + if (operator.snapshot.block == 0) revert InsufficientBalance(); + + PackedSSV shrunkWithdrawn; + PackedSSV shrunkAmount = PackedSSVLib.pack(amount); + OperatorLib.updateSnapshotStSSV(operator); + + PackedSSV balance = operator.snapshot.balance; + + if (amount == 0) { + if (PackedSSVLib.raw(balance) == 0) revert InsufficientBalance(); + shrunkWithdrawn = balance; + } else { + if (PackedSSVLib.raw(balance) < PackedSSVLib.raw(shrunkAmount)) revert InsufficientBalance(); + shrunkWithdrawn = shrunkAmount; + } + + operator.snapshot.balance = balance.sub(shrunkWithdrawn); + _transferOperatorTokenBalanceUnsafe(operatorId, PackedSSVLib.unpack(shrunkWithdrawn)); + + } else { + revert IncorrectOperatorVersion(version); + } + } + + function _resetOperatorState(Operator storage operator) private returns (Operator memory) { + operator.ethSnapshot.block = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + operator.ethFee = PACKED_ETH_ZERO; + operator.snapshot.block = 0; + operator.snapshot.balance = PACKED_SSV_ZERO; + operator.fee = PACKED_SSV_ZERO; + operator.ethValidatorCount = 0; + operator.validatorCount = 0; + + return operator; } function _transferOperatorBalanceUnsafe(uint64 operatorId, uint256 amount) private { - CoreLib.transferBalance(msg.sender, amount); + CoreLib.transferBalance(payable(msg.sender), amount); emit OperatorWithdrawn(msg.sender, operatorId, amount); } + + function _transferOperatorTokenBalanceUnsafe(uint64 operatorId, uint256 amount) private { + CoreLib.transferTokenBalance(msg.sender, amount); + emit OperatorWithdrawnSSV(msg.sender, operatorId, amount); + } } diff --git a/contracts/modules/SSVOperatorsWhitelist.sol b/contracts/modules/SSVOperatorsWhitelist.sol index 93eba09b8..0ac1870e1 100644 --- a/contracts/modules/SSVOperatorsWhitelist.sol +++ b/contracts/modules/SSVOperatorsWhitelist.sol @@ -3,19 +3,15 @@ pragma solidity 0.8.24; import {ISSVOperatorsWhitelist} from "../interfaces/ISSVOperatorsWhitelist.sol"; import {ISSVWhitelistingContract} from "../interfaces/external/ISSVWhitelistingContract.sol"; -import {Types64, Types256} from "../libraries/Types.sol"; -import {StorageData, SSVStorage} from "../libraries/SSVStorage.sol"; +import {StorageData, SSVStorage} from "../libraries/storage/SSVStorage.sol"; import {OperatorLib} from "../libraries/OperatorLib.sol"; contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist { - using Types256 for uint256; - using Types64 for uint64; using OperatorLib for Operator; - /*******************************/ - /* Operator External Functions */ - /*******************************/ - + /** + * @inheritdoc ISSVOperatorsWhitelist + */ function setOperatorsWhitelists( uint64[] calldata operatorIds, address[] calldata whitelistAddresses @@ -24,6 +20,9 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist { emit OperatorMultipleWhitelistUpdated(operatorIds, whitelistAddresses); } + /** + * @inheritdoc ISSVOperatorsWhitelist + */ function removeOperatorsWhitelists( uint64[] calldata operatorIds, address[] calldata whitelistAddresses @@ -32,6 +31,9 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist { emit OperatorMultipleWhitelistRemoved(operatorIds, whitelistAddresses); } + /** + * @inheritdoc ISSVOperatorsWhitelist + */ function setOperatorsWhitelistingContract( uint64[] calldata operatorIds, ISSVWhitelistingContract whitelistingContract @@ -67,6 +69,9 @@ contract SSVOperatorsWhitelist is ISSVOperatorsWhitelist { emit OperatorWhitelistingContractUpdated(operatorIds, address(whitelistingContract)); } + /** + * @inheritdoc ISSVOperatorsWhitelist + */ function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds) external { uint256 operatorsLength = OperatorLib.checkOperatorsLength(operatorIds); diff --git a/contracts/modules/SSVStaking.sol b/contracts/modules/SSVStaking.sol new file mode 100644 index 000000000..79e6e1e7d --- /dev/null +++ b/contracts/modules/SSVStaking.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {ISSVStaking} from "../interfaces/ISSVStaking.sol"; +import {ICSSVToken} from "../interfaces/ICSSVToken.sol"; +import {CoreLib} from "../libraries/CoreLib.sol"; +import {ProtocolLib} from "../libraries/ProtocolLib.sol"; +import {SSVStorage} from "../libraries/storage/SSVStorage.sol"; +import {SSVStorageStaking, StorageStaking, UnstakeRequest} from "../libraries/storage/SSVStorageStaking.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../libraries/storage/SSVStorageProtocol.sol"; +import {SSVReentrancyGuard} from "../abstract/SSVReentrancyGuard.sol"; +import {PackedETH} from "../libraries/SSVCoreTypes.sol"; +import {PackedETHLib} from "../libraries/SSVPackedLib.sol"; +import {PRECISION, ETH_DEDUCTED_DIGITS} from "../libraries/SSVCoreTypes.sol"; + +contract SSVStaking is ISSVStaking, SSVReentrancyGuard { + using SafeERC20 for IERC20; + using ProtocolLib for StorageProtocol; + using PackedETHLib for PackedETH; + + uint64 private constant MINIMAL_STAKING_AMOUNT = 1_000_000_000; + uint256 private constant MAX_PENDING_REQUESTS = 2000; + + address public immutable CSSV_ADDRESS; + + constructor(address _cssv) { + CSSV_ADDRESS = _cssv; + } + + /** + * @inheritdoc ISSVStaking + */ + function syncFees() external nonReentrant { + _syncFees(SSVStorageStaking.load()); + } + + /** + * @inheritdoc ISSVStaking + */ + function stake(uint256 amount) external nonReentrant { + if (amount < MINIMAL_STAKING_AMOUNT) { + revert StakeTooLow(); + } + + StorageStaking storage s = SSVStorageStaking.load(); + + _syncFees(s); + _settle(msg.sender, s); + + if (!SSVStorage.load().token.transferFrom(msg.sender, address(this), amount)) { + revert TokenTransferFailed(); + } + + ICSSVToken(CSSV_ADDRESS).mint(msg.sender, amount); + + emit Staked(msg.sender, amount); + } + + /** + * @inheritdoc ISSVStaking + */ + function requestUnstake(uint256 amount) external nonReentrant { + if (amount == 0) { + revert ZeroAmount(); + } + + StorageStaking storage s = SSVStorageStaking.load(); + + _syncFees(s); + + uint256 bal = ICSSVToken(CSSV_ADDRESS).balanceOf(msg.sender); + _settleWithBalance(msg.sender, bal, s); + + if (amount > bal) { + revert UnstakeAmountExceedsBalance(); + } + + UnstakeRequest[] storage requests = s.withdrawalRequests[msg.sender]; + + if (requests.length == MAX_PENDING_REQUESTS) { + revert MaxRequestsAmountReached(); + } + + uint64 unlockTime = uint64(block.timestamp + s.cooldownDuration); + requests.push(UnstakeRequest({amount: uint192(amount), unlockTime: unlockTime})); + + ICSSVToken(CSSV_ADDRESS).burn(msg.sender, amount); + + emit UnstakeRequested(msg.sender, amount, unlockTime); + } + + /** + * @inheritdoc ISSVStaking + */ + function withdrawUnlocked() external nonReentrant { + StorageStaking storage s = SSVStorageStaking.load(); + uint256 amount = calculateTotalUnfrozenBalance(s); + if (amount == 0) revert NothingToWithdraw(); + + if (!SSVStorage.load().token.transfer(msg.sender, amount)) { + revert TokenTransferFailed(); + } + + emit UnstakedWithdrawn(msg.sender, amount); + } + + /** + * @inheritdoc ISSVStaking + */ + function claimEthRewards() external nonReentrant { + StorageStaking storage s = SSVStorageStaking.load(); + + _syncFees(s); + _settle(msg.sender, s); + + uint256 claimable = s.accrued[msg.sender]; + if (claimable == 0) revert NothingToClaim(); + + uint256 payout = claimable - (claimable % ETH_DEDUCTED_DIGITS); + uint256 userBalance = ICSSVToken(CSSV_ADDRESS).balanceOf(msg.sender); + if (payout == 0) { + if (userBalance == 0) { + s.accrued[msg.sender] = 0; + emit RewardsClaimed(msg.sender, 0); + return; + } + revert NothingToClaim(); + } + + PackedETH packedPayout = PackedETHLib.pack(payout); + + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (packedPayout.gt(s.stakingEthPoolBalance)) { + revert InsufficientBalance(); + } + if (packedPayout.gt(sp.ethDaoBalance)) { + revert InsufficientBalance(); + } + + uint256 remainder = claimable - payout; + s.accrued[msg.sender] = (remainder != 0 && userBalance == 0) ? 0 : remainder; + s.stakingEthPoolBalance = s.stakingEthPoolBalance.sub(packedPayout); + sp.ethDaoBalance = sp.ethDaoBalance.sub(packedPayout); + + CoreLib.transferBalance(msg.sender, payout); + emit RewardsClaimed(msg.sender, payout); + } + + /** + * @inheritdoc ISSVStaking + */ + function rescueERC20(address token, address to, uint256 amount) external nonReentrant { + if (token == address(0) || to == address(0)) revert ZeroAddress(); + if (token == address(SSVStorage.load().token) || token == CSSV_ADDRESS) { + revert InvalidToken(); + } + if (amount == 0) { + revert ZeroAmount(); + } + + IERC20(token).safeTransfer(to, amount); + + emit ERC20Rescued(token, to, amount); + } + + /** + * @inheritdoc ISSVStaking + */ + function onCSSVTransfer(address from, address to, uint256 amount) external virtual { + if (msg.sender != CSSV_ADDRESS) revert NotCSSV(); + + StorageStaking storage s = SSVStorageStaking.load(); + + _syncFees(s); + _settle(from, s); + _settle(to, s); + } + + function _syncFees(StorageStaking storage s) internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + + PackedETH current = sp.networkTotalEarnings(); + sp.ethDaoBalance = current; + sp.ethDaoIndexBlockNumber = uint32(block.number); + + PackedETH previous = s.stakingEthPoolBalance; + if (current.lte(previous)) { + s.stakingEthPoolBalance = current; + return; + } + + PackedETH packedNewFees = current.sub(previous); + uint256 newFeesWei; + + uint256 totalStaked = ICSSVToken(CSSV_ADDRESS).totalSupply(); + if (totalStaked != 0) { + newFeesWei = PackedETHLib.unpack(packedNewFees); + s.accEthPerShare += uint128((newFeesWei * PRECISION) / totalStaked); + } + + s.stakingEthPoolBalance = current; + emit FeesSynced(newFeesWei, s.accEthPerShare); + } + + function _settle(address user, StorageStaking storage s) internal { + uint256 bal = ICSSVToken(CSSV_ADDRESS).balanceOf(user); + _settleWithBalance(user, bal, s); + } + + function _settleWithBalance(address user, uint256 bal, StorageStaking storage s) internal { + uint256 idx = s.accEthPerShare; + uint256 userIdx = s.userIndex[user]; + + uint256 pending; + if (bal != 0 && idx != userIdx) { + pending = (bal * (idx - userIdx)) / PRECISION; + if (pending != 0) { + s.accrued[user] += pending; + } + } + + s.userIndex[user] = idx; + emit RewardsSettled(user, pending, s.accrued[user], idx); + } + + function calculateTotalUnfrozenBalance(StorageStaking storage s) internal returns (uint256) { + UnstakeRequest[] storage requests = s.withdrawalRequests[msg.sender]; + uint256 total = 0; + uint256 i = 0; + + while (i < requests.length) { + if (requests[i].unlockTime <= block.timestamp) { + total += requests[i].amount; + requests[i] = requests[requests.length - 1]; + requests.pop(); + } else { + i++; + } + } + return total; + } +} diff --git a/contracts/modules/SSVValidators.sol b/contracts/modules/SSVValidators.sol new file mode 100644 index 000000000..1ca467c2f --- /dev/null +++ b/contracts/modules/SSVValidators.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {ISSVValidators} from "../interfaces/ISSVValidators.sol"; +import {ClusterLib} from "../libraries/ClusterLib.sol"; +import {OperatorLib} from "../libraries/OperatorLib.sol"; +import {ProtocolLib} from "../libraries/ProtocolLib.sol"; +import {ValidatorLib} from "../libraries/ValidatorLib.sol"; +import {VERSION_ETH, VERSION_SSV, BPS_DENOMINATOR} from "../libraries/SSVCoreTypes.sol"; +import {SSVStorage, StorageData} from "../libraries/storage/SSVStorage.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../libraries/storage/SSVStorageProtocol.sol"; +import { + SSVStorageEB, + StorageEB, + ClusterEBSnapshot +} from "../libraries/storage/SSVStorageEB.sol"; + +contract SSVValidators is ISSVValidators { + using ClusterLib for Cluster; + using OperatorLib for Operator; + using ProtocolLib for StorageProtocol; + + /** + * @inheritdoc ISSVValidators + */ + function registerValidator( + bytes calldata publicKey, + uint64[] memory operatorIds, + bytes calldata sharesData, + Cluster memory cluster + ) external payable override { + bytes[] memory publicKeys = new bytes[](1); + publicKeys[0] = publicKey; + + bytes[] memory shares = new bytes[](1); + shares[0] = sharesData; + + _bulkRegisterValidator(msg.sender, msg.value, publicKeys, operatorIds, shares, cluster); + } + + /** + * @inheritdoc ISSVValidators + */ + function bulkRegisterValidator( + bytes[] memory publicKeys, + uint64[] memory operatorIds, + bytes[] calldata sharesData, + Cluster memory cluster + ) external payable override { + _bulkRegisterValidator(msg.sender, msg.value, publicKeys, operatorIds, sharesData, cluster); + } + + /** + * @inheritdoc ISSVValidators + */ + function removeValidator( + bytes calldata publicKey, + uint64[] memory operatorIds, + Cluster memory cluster + ) external override { + bytes[] memory publicKeys = new bytes[](1); + publicKeys[0] = publicKey; + + _bulkRemoveValidator(msg.sender, publicKeys, operatorIds, cluster); + } + + /** + * @inheritdoc ISSVValidators + */ + function bulkRemoveValidator( + bytes[] calldata publicKeys, + uint64[] memory operatorIds, + Cluster memory cluster + ) external override { + _bulkRemoveValidator(msg.sender, publicKeys, operatorIds, cluster); + } + + /** + * @inheritdoc ISSVValidators + */ + function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override { + StorageData storage s = SSVStorage.load(); + _validateExistingValidator(publicKey, msg.sender, ValidatorLib.hashOperatorIds(operatorIds), s); + + emit ValidatorExited(msg.sender, operatorIds, publicKey); + } + + /** + * @inheritdoc ISSVValidators + */ + function bulkExitValidator(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external override { + if (publicKeys.length == 0) { + revert ValidatorDoesNotExist(); + } + StorageData storage s = SSVStorage.load(); + bytes32 hashedOperatorIds = ValidatorLib.hashOperatorIds(operatorIds); + + for (uint i; i < publicKeys.length; ++i) { + _validateExistingValidator(publicKeys[i], msg.sender, hashedOperatorIds, s); + + emit ValidatorExited(msg.sender, operatorIds, publicKeys[i]); + } + } + + function _bulkRegisterValidator( + address owner, + uint256 value, + bytes[] memory publicKeys, + uint64[] memory operatorIds, + bytes[] memory sharesData, + Cluster memory cluster + ) internal virtual { + uint256 validatorsLength = publicKeys.length; + + if (validatorsLength == 0) revert EmptyPublicKeysList(); + if (validatorsLength != sharesData.length) revert PublicKeysSharesLengthMismatch(); + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + ValidatorLib.validateOperatorsLength(operatorIds); + + for (uint i; i < validatorsLength; ++i) { + ValidatorLib.registerPublicKey(publicKeys[i], operatorIds, owner, s); + } + bytes32 hashedCluster = cluster.validateClusterOnRegistration(owner, operatorIds, s); + + cluster.balance += value; + + cluster.updateClusterOnRegistration(operatorIds, hashedCluster, uint32(validatorsLength), s, sp); + + { + // Deviation-only model: baseline comes from ethValidatorCount (already updated above) + // Only update ebSnapshot.vUnits for clusters with explicit EB tracking + // Do NOT update operatorEthVUnits here - deviation unchanged on registration + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[hashedCluster]; + if (ebSnapshot.vUnits > 0) { + // Cluster has explicit EB tracking - add baseline for new validators + ebSnapshot.vUnits += uint64(validatorsLength) * BPS_DENOMINATOR; + } + // operatorEthVUnits NOT updated: deviation doesn't change on registration + } + + for (uint i; i < validatorsLength; ++i) { + bytes memory pk = publicKeys[i]; + bytes memory sh = sharesData[i]; + + emit ValidatorAdded(owner, operatorIds, pk, sh, cluster); + } + } + + function _bulkRemoveValidator( + address owner, + bytes[] memory publicKeys, + uint64[] memory operatorIds, + Cluster memory cluster + ) internal virtual { + uint256 validatorsLength = publicKeys.length; + + if (validatorsLength == 0) { + revert ValidatorDoesNotExist(); + } + StorageData storage s = SSVStorage.load(); + + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(owner, operatorIds, s); + bytes32 hashedOperatorIds = ValidatorLib.hashOperatorIds(operatorIds); + + uint32 validatorsRemoved; + + for (uint i; i < validatorsLength; ++i) { + bytes32 hashedValidator = _validateExistingValidator(publicKeys[i], owner, hashedOperatorIds, s); + + delete s.validatorPKs[hashedValidator]; + validatorsRemoved++; + } + + if (version == VERSION_ETH) { + if (cluster.active) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + // slither-disable-next-line unused-return + (uint64 clusterIndex, ) = OperatorLib.updateClusterOperators( + operatorIds, + false, + validatorsRemoved, + s, + sp + ); + + cluster.updateClusterData(hashedCluster, clusterIndex, sp.currentNetworkFeeIndex()); + + sp.updateDAO(false, validatorsRemoved); + } + + cluster.validatorCount -= validatorsRemoved; + + { + // Deviation-only model: baseline removed via ethValidatorCount (already updated above) + // Do NOT subtract baseline from operatorEthVUnits + // Only handle deviation cleanup for explicit EB clusters + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[hashedCluster]; + + if (ebSnapshot.vUnits > 0) { + // Cluster has explicit EB tracking - subtract baseline from snapshot + uint64 deltaClusterVUnits = uint64(validatorsRemoved) * BPS_DENOMINATOR; + ebSnapshot.vUnits -= deltaClusterVUnits; + + // When cluster becomes empty, clean up any remaining deviation + if (cluster.validatorCount == 0) { + uint64 remainingVUnits = ebSnapshot.vUnits; + if (remainingVUnits > 0 && cluster.active) { + // remainingVUnits is pure deviation (no baseline left since validatorCount=0) + // Skip for liquidated clusters: deviation already cleaned up in _executeLiquidation + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + if (s.operators[operatorIds[i]].ethSnapshot.block == 0) continue; + seb.operatorEthVUnits[operatorIds[i]] -= remainingVUnits; + } + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.updateDAOEthVUnits(remainingVUnits, 0); + } + ebSnapshot.vUnits = 0; + } + } + // For implicit clusters (ebSnapshot.vUnits == 0): nothing to do + // Baseline removal handled via ethValidatorCount decrement + } + + s.ethClusters[hashedCluster] = cluster.hashClusterData(); + } else if (version == VERSION_SSV) { + if (cluster.active) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + // slither-disable-next-line unused-return + (uint64 clusterIndex, ) = OperatorLib.updateClusterOperatorsSSV( + operatorIds, + false, + validatorsRemoved, + s, + sp + ); + uint64 currentNetworkFeeIndexSSV = sp.currentNetworkFeeIndexSSV(); + cluster.updateBalanceSSV(clusterIndex, currentNetworkFeeIndexSSV); + cluster.index = clusterIndex; + cluster.networkFeeIndex = currentNetworkFeeIndexSSV; + sp.updateDAOSSV(false, validatorsRemoved); + } + + cluster.validatorCount -= validatorsRemoved; + s.clusters[hashedCluster] = cluster.hashClusterData(); + } else { + revert IncorrectClusterVersion(); + } + + for (uint i; i < validatorsLength; ++i) { + emit ValidatorRemoved(owner, operatorIds, publicKeys[i], cluster); + } + } + + function _validateExistingValidator( + bytes memory publicKey, + address owner, + bytes32 hashedOperatorIds, + StorageData storage s + ) internal view returns (bytes32 hashedValidator) { + hashedValidator = keccak256(abi.encodePacked(publicKey, owner)); + bytes32 validatorData = s.validatorPKs[hashedValidator]; + if (validatorData == bytes32(0)) { + revert ValidatorDoesNotExist(); + } + if (!ValidatorLib.validateCorrectState(validatorData, hashedOperatorIds)) { + revert IncorrectValidatorStateWithData(publicKey); + } + } + +} diff --git a/contracts/modules/SSVViews.sol b/contracts/modules/SSVViews.sol index 1d50b89ca..b567135f8 100644 --- a/contracts/modules/SSVViews.sol +++ b/contracts/modules/SSVViews.sol @@ -3,25 +3,34 @@ pragma solidity 0.8.24; import {ISSVViews} from "../interfaces/ISSVViews.sol"; import {ISSVWhitelistingContract} from "../interfaces/external/ISSVWhitelistingContract.sol"; -import {Types64} from "../libraries/Types.sol"; -import "../libraries/ClusterLib.sol"; -import "../libraries/OperatorLib.sol"; -import "../libraries/CoreLib.sol"; -import "../libraries/ProtocolLib.sol"; -import {SSVStorage, StorageData} from "../libraries/SSVStorage.sol"; -import {SSVStorageProtocol, StorageProtocol} from "../libraries/SSVStorageProtocol.sol"; +import {ICSSVToken} from "../interfaces/ICSSVToken.sol"; +import {ClusterLib} from "../libraries/ClusterLib.sol"; +import {OperatorLib} from "../libraries/OperatorLib.sol"; +import {CoreLib} from "../libraries/CoreLib.sol"; +import {ProtocolLib} from "../libraries/ProtocolLib.sol"; +import {PackedSSV, PackedETH, VERSION_ETH, VERSION_SSV, DEFAULT_OPERATOR_ETH_FEE, PRECISION, BPS_DENOMINATOR} from "../libraries/SSVCoreTypes.sol"; +import {PackedSSVLib, PackedETHLib} from "../libraries/SSVPackedLib.sol"; +import {SSVStorage, StorageData} from "../libraries/storage/SSVStorage.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../libraries/storage/SSVStorageProtocol.sol"; +import {SSVStorageEB, StorageEB} from "../libraries/storage/SSVStorageEB.sol"; +import {MAX_DELEGATION_SLOTS, SSVStorageStaking, StorageStaking, UnstakeRequest} from "../libraries/storage/SSVStorageStaking.sol"; contract SSVViews is ISSVViews { - using Types64 for uint64; - using ClusterLib for Cluster; using OperatorLib for Operator; using ProtocolLib for StorageProtocol; + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; + + address public immutable CSSV_ADDRESS; - /*************************************/ - /* Validator External View Functions */ - /*************************************/ + constructor(address _cssv) { + CSSV_ADDRESS = _cssv; + } + /** + * @inheritdoc ISSVViews + */ function getValidator(address clusterOwner, bytes calldata publicKey) external view override returns (bool) { bytes32 validatorData = SSVStorage.load().validatorPKs[keccak256(abi.encodePacked(publicKey, clusterOwner))]; @@ -31,50 +40,84 @@ contract SSVViews is ISSVViews { return activeFlag == bytes32(uint256(1)); } - /************************************/ - /* Operator External View Functions */ - /************************************/ - + /** + * @inheritdoc ISSVViews + */ function getOperatorFee(uint64 operatorId) external view override returns (uint256) { - return SSVStorage.load().operators[operatorId].fee.expand(); + Operator storage operator = SSVStorage.load().operators[operatorId]; + if (operator.ethSnapshot.block != 0) { + return PackedETHLib.unpack(operator.ethFee); + } else if (PackedSSV.unwrap(operator.fee) != 0) { + return DEFAULT_OPERATOR_ETH_FEE; + } } - function getOperatorDeclaredFee(uint64 operatorId) external view override returns (bool, uint256, uint64, uint64) { - OperatorFeeChangeRequest memory opFeeChangeRequest = SSVStorage.load().operatorFeeChangeRequests[operatorId]; + /** + * @inheritdoc ISSVViews + */ + function getOperatorFeeSSV(uint64 operatorId) external view override returns (uint256) { + return PackedSSVLib.unpack(SSVStorage.load().operators[operatorId].fee); + } + + /** + * @inheritdoc ISSVViews + */ + function getOperatorDeclaredFee(uint64 operatorId) external view override returns (OperatorDeclaredFeeData memory) { + StorageData storage s = SSVStorage.load(); + OperatorFeeChangeRequest memory opFeeChangeRequest = s.operatorFeeChangeRequests[operatorId]; - return ( + bool isETHOperator = s.operators[operatorId].ethSnapshot.block != 0; + + return OperatorDeclaredFeeData( opFeeChangeRequest.approvalBeginTime != 0, - opFeeChangeRequest.fee.expand(), + isETHOperator ? PackedETHLib.unpack(PackedETH.wrap(opFeeChangeRequest.fee)) : PackedSSVLib.unpack(PackedSSV.wrap(opFeeChangeRequest.fee)), opFeeChangeRequest.approvalBeginTime, opFeeChangeRequest.approvalEndTime ); } + /** + * @inheritdoc ISSVViews + */ function getOperatorById( uint64 operatorId - ) - external - view - override - returns ( - address owner, - uint256 fee, - uint32 validatorCount, - address whitelistedAddress, - bool isPrivate, - bool isActive - ) + ) external view override returns (OperatorData memory op) { - ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[operatorId]; + Operator storage operator = SSVStorage.load().operators[operatorId]; - owner = operator.owner; - fee = operator.fee.expand(); - validatorCount = operator.validatorCount; - whitelistedAddress = SSVStorage.load().operatorsWhitelist[operatorId]; - isPrivate = operator.whitelisted; - isActive = operator.snapshot.block != 0; + op.owner = operator.owner; + if (operator.ethSnapshot.block != 0) { + op.fee = PackedETHLib.unpack(operator.ethFee); + } else if (PackedSSV.unwrap(operator.fee) != 0) { + op.fee = DEFAULT_OPERATOR_ETH_FEE; + } + + op.validatorCount = operator.ethValidatorCount; + op.whitelistedAddress = SSVStorage.load().operatorsWhitelist[operatorId]; + op.isPrivate = operator.whitelisted; + op.isActive = operator.ethSnapshot.block != 0 || operator.snapshot.block != 0; + } + + /** + * @inheritdoc ISSVViews + */ + function getOperatorByIdSSV( + uint64 operatorId + ) external view override returns (OperatorData memory op) + { + Operator storage operator = SSVStorage.load().operators[operatorId]; + + op.owner = operator.owner; + op.fee = PackedSSVLib.unpack(operator.fee); + op.validatorCount = operator.validatorCount; + op.whitelistedAddress = SSVStorage.load().operatorsWhitelist[operatorId]; + op.isPrivate = operator.whitelisted; + op.isActive = operator.ethSnapshot.block != 0 || operator.snapshot.block != 0; } + /** + * @inheritdoc ISSVViews + */ function getWhitelistedOperators( uint64[] calldata operatorIds, address addressToCheck @@ -154,10 +197,16 @@ contract SSVViews is ISSVViews { } } + /** + * @inheritdoc ISSVViews + */ function isWhitelistingContract(address contractAddress) external view override returns (bool) { return OperatorLib.isWhitelistingContract(contractAddress); } + /** + * @inheritdoc ISSVViews + */ function isAddressWhitelistedInWhitelistingContract( address addressToCheck, uint256 operatorId, @@ -167,16 +216,17 @@ contract SSVViews is ISSVViews { return ISSVWhitelistingContract(whitelistingContract).isWhitelisted(addressToCheck, operatorId); } - /***********************************/ - /* Cluster External View Functions */ - /***********************************/ - + /** + * @inheritdoc ISSVViews + */ function isLiquidatable( address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster ) external view override returns (bool) { - cluster.validateHashedCluster(clusterOwner, operatorIds, SSVStorage.load()); + StorageData storage s = SSVStorage.load(); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_ETH); if (!cluster.active) { return false; @@ -186,23 +236,64 @@ contract SSVViews is ISSVViews { uint64 burnRate; uint256 operatorsLength = operatorIds.length; for (uint256 i; i < operatorsLength; ++i) { - Operator memory operator = SSVStorage.load().operators[operatorIds[i]]; - clusterIndex += operator.snapshot.index + (uint64(block.number) - operator.snapshot.block) * operator.fee; - burnRate += operator.fee; + Operator memory operator = s.operators[operatorIds[i]]; + clusterIndex += operator.ethSnapshot.index + (uint64(block.number) - operator.ethSnapshot.block) * PackedETH.unwrap(operator.ethFee); + burnRate += PackedETH.unwrap(operator.ethFee); } StorageProtocol storage sp = SSVStorageProtocol.load(); - cluster.updateBalance(clusterIndex, sp.currentNetworkFeeIndex()); + cluster.updateBalanceWithEB(hashedCluster, clusterIndex, sp.currentNetworkFeeIndex()); return - cluster.isLiquidatable( + cluster.isLiquidatableWithEB( + hashedCluster, burnRate, - sp.networkFee, + PackedETH.unwrap(sp.ethNetworkFee), sp.minimumBlocksBeforeLiquidation, sp.minimumLiquidationCollateral ); } + /** + * @inheritdoc ISSVViews + */ + function isLiquidatableSSV( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view override returns (bool) { + StorageData storage s = SSVStorage.load(); + (, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_SSV); + + if (!cluster.active) { + return false; + } + + uint64 clusterIndex; + uint64 burnRate; + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + Operator memory operator = s.operators[operatorIds[i]]; + clusterIndex += operator.snapshot.index + (uint64(block.number) - operator.snapshot.block) * PackedSSV.unwrap(operator.fee); + burnRate += PackedSSV.unwrap(operator.fee); + } + + StorageProtocol storage sp = SSVStorageProtocol.load(); + + cluster.updateBalanceSSV(clusterIndex, sp.currentNetworkFeeIndexSSV()); + return + cluster.isLiquidatable( + burnRate, + PackedSSV.unwrap(sp.networkFee), + sp.minimumBlocksBeforeLiquidationSSV, + sp.minimumLiquidationCollateralSSV + ); + } + + /** + * @inheritdoc ISSVViews + */ function isLiquidated( address clusterOwner, uint64[] calldata operatorIds, @@ -212,102 +303,405 @@ contract SSVViews is ISSVViews { return !cluster.active; } + /** + * @inheritdoc ISSVViews + */ function getBurnRate( address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster - ) external view returns (uint256) { - cluster.validateHashedCluster(clusterOwner, operatorIds, SSVStorage.load()); + ) external view override returns (uint256) { + StorageData storage s = SSVStorage.load(); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster( + clusterOwner, + operatorIds, + s + ); + ClusterLib.validateClusterVersion(version, VERSION_ETH); + + PackedETH operatorsFee; + uint256 len = operatorIds.length; + for (uint256 i; i < len; ++i) { + Operator memory op = s.operators[operatorIds[i]]; + if (op.owner != address(0)) { + operatorsFee = operatorsFee.add(op.ethFee); + } + } + + PackedETH networkFee = SSVStorageProtocol.load().ethNetworkFee; + + uint64 vUnits = SSVStorageEB.load().clusterEB[hashedCluster].vUnits; + if (vUnits == 0) { + vUnits = uint64(cluster.validatorCount) * BPS_DENOMINATOR; + } - uint64 aggregateFee; + return (PackedETHLib.unpack(networkFee.add(operatorsFee)) * uint256(vUnits)) / BPS_DENOMINATOR; + } + + /** + * @inheritdoc ISSVViews + */ + function getBurnRateSSV( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view override returns (uint256) { + StorageData storage s = SSVStorage.load(); + (, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_SSV); + + PackedSSV aggregateFee; uint256 operatorsLength = operatorIds.length; for (uint256 i; i < operatorsLength; ++i) { - Operator memory operator = SSVStorage.load().operators[operatorIds[i]]; + Operator memory operator = s.operators[operatorIds[i]]; if (operator.owner != address(0)) { - aggregateFee += operator.fee; + aggregateFee = aggregateFee.add(operator.fee); } } - uint64 burnRate = (aggregateFee + SSVStorageProtocol.load().networkFee) * cluster.validatorCount; - return burnRate.expand(); + uint128 burnRate = PackedSSV.unwrap(aggregateFee.add(SSVStorageProtocol.load().networkFee)) * cluster.validatorCount; + return PackedSSVLib.unpack(PackedSSV.wrap(uint64(burnRate))); } - /***********************************/ - /* Balance External View Functions */ - /***********************************/ - + /** + * @inheritdoc ISSVViews + */ function getOperatorEarnings(uint64 id) external view override returns (uint256) { Operator memory operator = SSVStorage.load().operators[id]; - operator.updateSnapshot(); - return operator.snapshot.balance.expand(); + operator.updateSnapshot(id); + return PackedETHLib.unpack(operator.ethSnapshot.balance); + } + + /** + * @inheritdoc ISSVViews + */ + function getOperatorEarningsSSV(uint64 id) external view override returns (uint256) { + Operator memory operator = SSVStorage.load().operators[id]; + + operator.updateSnapshotSSV(); + return PackedSSVLib.unpack(operator.snapshot.balance); } + /** + * @inheritdoc ISSVViews + */ function getBalance( address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster - ) external view override returns (uint256) { - cluster.validateHashedCluster(clusterOwner, operatorIds, SSVStorage.load()); + ) external view override returns (uint256 balance) { + StorageData storage s = SSVStorage.load(); + (bytes32 hashedCluster, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_ETH); cluster.validateClusterIsNotLiquidated(); uint64 clusterIndex; - { - uint256 operatorsLength = operatorIds.length; - for (uint256 i; i < operatorsLength; ++i) { - Operator memory operator = SSVStorage.load().operators[operatorIds[i]]; - clusterIndex += - operator.snapshot.index + - (uint64(block.number) - operator.snapshot.block) * - operator.fee; - } + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + Operator memory operator = s.operators[operatorIds[i]]; + clusterIndex += operator.ethSnapshot.index + (uint64(block.number) - operator.ethSnapshot.block) * PackedETH.unwrap(operator.ethFee); + } + + StorageProtocol storage sp = SSVStorageProtocol.load(); + cluster.updateBalanceWithEB(hashedCluster, clusterIndex, sp.currentNetworkFeeIndex()); + balance = cluster.balance; + } + + /** + * @inheritdoc ISSVViews + */ + function getBalanceSSV( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view override returns (uint256 balance) { + StorageData storage s = SSVStorage.load(); + (, uint8 version) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + ClusterLib.validateClusterVersion(version, VERSION_SSV); + cluster.validateClusterIsNotLiquidated(); + + uint64 clusterIndex; + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + Operator memory operator = s.operators[operatorIds[i]]; + clusterIndex += operator.snapshot.index + (uint64(block.number) - operator.snapshot.block) * PackedSSV.unwrap(operator.fee); } - cluster.updateBalance(clusterIndex, SSVStorageProtocol.load().currentNetworkFeeIndex()); + cluster.updateBalanceSSV(clusterIndex, SSVStorageProtocol.load().currentNetworkFeeIndexSSV()); + balance = cluster.balance; + } + + /** + * @inheritdoc ISSVViews + */ + function getEffectiveBalance( + address clusterOwner, + uint64[] calldata operatorIds, + Cluster memory cluster + ) external view returns (uint32 effectiveBalance) { + StorageData storage s = SSVStorage.load(); + (bytes32 hashedCluster, ) = cluster.validateHashedCluster(clusterOwner, operatorIds, s); + cluster.validateClusterIsNotLiquidated(); + + StorageEB storage seb = SSVStorageEB.load(); + uint64 vUnits = seb.clusterEB[hashedCluster].vUnits; - return cluster.balance; + if (vUnits == 0) { + vUnits = cluster.validatorCount * BPS_DENOMINATOR; + } + + return ClusterLib.vUnitsToEB(vUnits); } - /*******************************/ - /* DAO External View Functions */ - /*******************************/ + /** + * @inheritdoc ISSVViews + */ + function getClusterAssetType(address clusterOwner, uint64[] calldata operatorIds) external view override returns (uint8) { + StorageData storage s = SSVStorage.load(); + bytes32 hashedCluster = keccak256(abi.encodePacked(clusterOwner, operatorIds)); + if (s.ethClusters[hashedCluster] != bytes32(0)) { + return VERSION_ETH; + } + if (s.clusters[hashedCluster] != bytes32(0)) { + return VERSION_SSV; + } + + revert ClusterDoesNotExist(); + } + + /** + * @inheritdoc ISSVViews + */ function getNetworkFee() external view override returns (uint256) { - return SSVStorageProtocol.load().networkFee.expand(); + return PackedETHLib.unpack(SSVStorageProtocol.load().ethNetworkFee); + } + + /** + * @inheritdoc ISSVViews + */ + function getNetworkFeeSSV() external view override returns (uint256) { + return PackedSSVLib.unpack(SSVStorageProtocol.load().networkFee); } + /** + * @inheritdoc ISSVViews + */ function getNetworkEarnings() external view override returns (uint256) { - return SSVStorageProtocol.load().networkTotalEarnings().expand(); + return PackedETHLib.unpack(SSVStorageProtocol.load().networkTotalEarnings()); + } + + /** + * @inheritdoc ISSVViews + */ + function getNetworkEarningsSSV() external view override returns (uint256) { + return PackedSSVLib.unpack(SSVStorageProtocol.load().networkTotalEarningsSSV()); } + /** + * @inheritdoc ISSVViews + */ function getOperatorFeeIncreaseLimit() external view override returns (uint64) { return SSVStorageProtocol.load().operatorMaxFeeIncrease; } - function getMaximumOperatorFee() external view override returns (uint64) { - return SSVStorageProtocol.load().operatorMaxFee; + /** + * @inheritdoc ISSVViews + */ + function getMaximumOperatorFee() external view override returns (uint256) { + return SSVStorageProtocol.load().operatorMaxFee.unpack(); + } + + /** + * @inheritdoc ISSVViews + */ + function getMaximumOperatorFeeSSV() external view override returns (uint256) { + return SSVStorageProtocol.load().operatorMaxFeeSSV; } - function getOperatorFeePeriods() external view override returns (uint64, uint64) { - return (SSVStorageProtocol.load().declareOperatorFeePeriod, SSVStorageProtocol.load().executeOperatorFeePeriod); + /** + * @inheritdoc ISSVViews + */ + function getMinimumOperatorEthFee() external view override returns (uint256) { + return SSVStorageProtocol.load().minimumOperatorEthFee.unpack(); + } + + /** + * @inheritdoc ISSVViews + */ + function getOperatorFeePeriods() external view override returns (OperatorFeePeriodsData memory) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + return OperatorFeePeriodsData( + sp.declareOperatorFeePeriod, + sp.executeOperatorFeePeriod + ); } + /** + * @inheritdoc ISSVViews + */ function getLiquidationThresholdPeriod() external view override returns (uint64) { return SSVStorageProtocol.load().minimumBlocksBeforeLiquidation; } + /** + * @inheritdoc ISSVViews + */ + function getLiquidationThresholdPeriodSSV() external view override returns (uint64) { + return SSVStorageProtocol.load().minimumBlocksBeforeLiquidationSSV; + } + + /** + * @inheritdoc ISSVViews + */ function getMinimumLiquidationCollateral() external view override returns (uint256) { - return SSVStorageProtocol.load().minimumLiquidationCollateral.expand(); + return PackedETHLib.unpack(SSVStorageProtocol.load().minimumLiquidationCollateral); } + /** + * @inheritdoc ISSVViews + */ + function getMinimumLiquidationCollateralSSV() external view override returns (uint256) { + return PackedSSVLib.unpack(SSVStorageProtocol.load().minimumLiquidationCollateralSSV); + } + + /** + * @inheritdoc ISSVViews + */ function getValidatorsPerOperatorLimit() external view override returns (uint32) { return SSVStorageProtocol.load().validatorsPerOperatorLimit; } + /** + * @inheritdoc ISSVViews + */ function getNetworkValidatorsCount() external view override returns (uint32) { - return SSVStorageProtocol.load().daoValidatorCount; + return SSVStorageProtocol.load().ethDaoValidatorCount; + } + + /** + * @inheritdoc ISSVViews + */ + function cooldownDuration() external view override returns (uint256) { + return SSVStorageStaking.load().cooldownDuration; + } + + /** + * @inheritdoc ISSVViews + */ + function totalStaked() external view override returns (uint256) { + return ICSSVToken(CSSV_ADDRESS).totalSupply(); + } + + /** + * @inheritdoc ISSVViews + */ + function stakedBalanceOf(address user) external view override returns (uint256) { + return ICSSVToken(CSSV_ADDRESS).balanceOf(user); + } + + /** + * @inheritdoc ISSVViews + */ + function pendingUnstake(address user) external view override returns (UnstakeRequestsData[] memory data) { + StorageStaking storage s = SSVStorageStaking.load(); + UnstakeRequest[] storage requests = s.withdrawalRequests[user]; + + uint256 len = requests.length; + data = new UnstakeRequestsData[](len); + + for (uint256 i = 0; i < len; i++) { + data[i] = UnstakeRequestsData({ + amount: requests[i].amount, + unlockTime: requests[i].unlockTime + }); + } + } + + /** + * @inheritdoc ISSVViews + */ + function accEthPerShare() external view override returns (uint256) { + return SSVStorageStaking.load().accEthPerShare; + } + + /** + * @inheritdoc ISSVViews + */ + function stakingEthPoolBalance() external view override returns (uint256) { + return SSVStorageStaking.load().stakingEthPoolBalance.unpack(); + } + + /** + * @inheritdoc ISSVViews + */ + function previewClaimableEth(address user) external view override returns (uint256) { + StorageStaking storage s = SSVStorageStaking.load(); + uint256 idx = _previewAccEthPerShare(s); + uint256 bal = ICSSVToken(CSSV_ADDRESS).balanceOf(user); + uint256 delta = idx - s.userIndex[user]; + uint256 pending = (bal * delta) / PRECISION; + return s.accrued[user] + pending; + } + + /** + * @inheritdoc ISSVViews + */ + function getOracle(uint32 oracleId) external view override returns (address) { + return SSVStorageStaking.load().oracles[oracleId]; + } + + /** + * @inheritdoc ISSVViews + */ + function getOracleWeight(uint32 oracleId) external view override returns (uint256) { + uint256 staked = ICSSVToken(CSSV_ADDRESS).totalSupply(); + return staked / SSVStorageStaking.load().defaultOracleIds.length; + } + + /** + * @inheritdoc ISSVViews + */ + function getActiveOracleIds() external view override returns (uint32[MAX_DELEGATION_SLOTS] memory) { + return SSVStorageStaking.load().defaultOracleIds; + } + + /** + * @inheritdoc ISSVViews + */ + function getQuorumBps() external view override returns (uint16) { + return SSVStorageStaking.load().quorumBps; + } + + /** + * @inheritdoc ISSVViews + */ + function getCommittedRoot(uint64 blockNum) external view override returns (bytes32) { + return SSVStorageEB.load().ebRoots[blockNum]; + } + + function _previewAccEthPerShare(StorageStaking storage s) internal view returns (uint256) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + PackedETH current = sp.networkTotalEarnings(); + + uint256 idx = s.accEthPerShare; + PackedETH previous = s.stakingEthPoolBalance; + + uint256 totalStaked_ = ICSSVToken(CSSV_ADDRESS).totalSupply(); + + if (current.lte(previous) || totalStaked_ == 0) { + return idx; + } + + PackedETH packedNewFees = current.sub(previous); + uint256 newFeesWei = PackedETHLib.unpack(packedNewFees); + return idx + (newFeesWei * PRECISION) / totalStaked_; } + /** + * @inheritdoc ISSVViews + */ function getVersion() external pure override returns (string memory) { return CoreLib.getVersion(); } diff --git a/contracts/test/SSVNetworkBasicUpgrade.sol b/contracts/test/SSVNetworkBasicUpgrade.sol deleted file mode 100644 index 0cdde703c..000000000 --- a/contracts/test/SSVNetworkBasicUpgrade.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.24; - -import "../SSVNetwork.sol"; - -contract SSVNetworkBasicUpgrade is SSVNetwork { - using Types256 for uint256; - - function resetNetworkFee(uint256 newFee) external { - SSVStorageProtocol.load().networkFee = newFee.shrink(); - } -} diff --git a/contracts/test/SSVNetworkUpgrade.sol b/contracts/test/SSVNetworkUpgrade.sol deleted file mode 100644 index b6742a0d1..000000000 --- a/contracts/test/SSVNetworkUpgrade.sol +++ /dev/null @@ -1,419 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.24; - -import "./interfaces/ISSVNetworkT.sol"; - -import "../interfaces/ISSVClusters.sol"; -import "../interfaces/ISSVOperators.sol"; -import "../interfaces/ISSVDAO.sol"; -import "../interfaces/ISSVViews.sol"; - -import "../libraries/Types.sol"; -import "../libraries/CoreLib.sol"; -import "../libraries/SSVStorage.sol"; -import "../libraries/SSVStorageProtocol.sol"; -import "../libraries/OperatorLib.sol"; -import "../libraries/ClusterLib.sol"; - -import {SSVModules} from "../libraries/SSVStorage.sol"; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; - -contract SSVNetworkUpgrade is - UUPSUpgradeable, - Ownable2StepUpgradeable, - ISSVNetworkT, - ISSVOperators, - ISSVClusters, - ISSVDAO -{ - using Types256 for uint256; - using ClusterLib for Cluster; - - /****************/ - /* Initializers */ - /****************/ - - function initialize( - IERC20 token_, - ISSVOperators ssvOperators_, - ISSVClusters ssvClusters_, - ISSVDAO ssvDAO_, - ISSVViews ssvViews_, - uint64 minimumBlocksBeforeLiquidation_, - uint256 minimumLiquidationCollateral_, - uint32 validatorsPerOperatorLimit_, - uint64 declareOperatorFeePeriod_, - uint64 executeOperatorFeePeriod_, - uint64 operatorMaxFeeIncrease_ - ) external override initializer onlyProxy { - __UUPSUpgradeable_init(); - __Ownable_init_unchained(); - __SSVNetwork_init_unchained( - token_, - ssvOperators_, - ssvClusters_, - ssvDAO_, - ssvViews_, - minimumBlocksBeforeLiquidation_, - minimumLiquidationCollateral_, - validatorsPerOperatorLimit_, - declareOperatorFeePeriod_, - executeOperatorFeePeriod_, - operatorMaxFeeIncrease_ - ); - } - - function __SSVNetwork_init_unchained( - IERC20 token_, - ISSVOperators ssvOperators_, - ISSVClusters ssvClusters_, - ISSVDAO ssvDAO_, - ISSVViews ssvViews_, - uint64 minimumBlocksBeforeLiquidation_, - uint256 minimumLiquidationCollateral_, - uint32 validatorsPerOperatorLimit_, - uint64 declareOperatorFeePeriod_, - uint64 executeOperatorFeePeriod_, - uint64 operatorMaxFeeIncrease_ - ) internal onlyInitializing { - StorageData storage s = SSVStorage.load(); - StorageProtocol storage sp = SSVStorageProtocol.load(); - s.token = token_; - s.ssvContracts[SSVModules.SSV_OPERATORS] = address(ssvOperators_); - s.ssvContracts[SSVModules.SSV_CLUSTERS] = address(ssvClusters_); - s.ssvContracts[SSVModules.SSV_DAO] = address(ssvDAO_); - s.ssvContracts[SSVModules.SSV_VIEWS] = address(ssvViews_); - sp.minimumBlocksBeforeLiquidation = minimumBlocksBeforeLiquidation_; - sp.minimumLiquidationCollateral = minimumLiquidationCollateral_.shrink(); - sp.validatorsPerOperatorLimit = validatorsPerOperatorLimit_; - sp.declareOperatorFeePeriod = declareOperatorFeePeriod_; - sp.executeOperatorFeePeriod = executeOperatorFeePeriod_; - sp.operatorMaxFeeIncrease = operatorMaxFeeIncrease_; - } - - /*****************/ - /* UUPS required */ - /*****************/ - - function _authorizeUpgrade(address) internal override onlyOwner {} - - fallback() external { - address ssvViews = SSVStorage.load().ssvContracts[SSVModules.SSV_VIEWS]; - assembly { - calldatacopy(0, 0, calldatasize()) - let result := delegatecall(gas(), ssvViews, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - if eq(result, 0) { - revert(0, returndatasize()) - } - return(0, returndatasize()) - } - } - - /*******************************/ - /* Operator External Functions */ - /*******************************/ - - function registerOperator(bytes calldata publicKey, uint256 fee, bool setPrivate) external override returns (uint64 id) { - bytes memory result = _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("registerOperator(bytes,uint256)", publicKey, fee, setPrivate) - ); - return abi.decode(result, (uint64)); - } - - function removeOperator(uint64 operatorId) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("removeOperator(uint64)", operatorId) - ); - } - - function declareOperatorFee(uint64 operatorId, uint256 fee) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("declareOperatorFee(uint64,uint256)", operatorId, fee) - ); - } - - function executeOperatorFee(uint64 operatorId) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("executeOperatorFee(uint64)", operatorId) - ); - } - - function cancelDeclaredOperatorFee(uint64 operatorId) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("cancelDeclaredOperatorFee(uint64)", operatorId) - ); - } - - function reduceOperatorFee(uint64 operatorId, uint256 fee) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("reduceOperatorFee(uint64,uint256)", operatorId, fee) - ); - } - - function setFeeRecipientAddress(address recipientAddress) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("setFeeRecipientAddress(address)", recipientAddress) - ); - } - - function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("setOperatorsPrivateUnchecked(address)", operatorIds) - ); - } - - function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("setOperatorsPublicUnchecked(address)", operatorIds) - ); - } - - function withdrawOperatorEarnings(uint64 operatorId, uint256 amount) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("withdrawOperatorEarnings(uint64,uint256)", operatorId, amount) - ); - } - - function withdrawAllOperatorEarnings(uint64 operatorId) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_OPERATORS], - abi.encodeWithSignature("withdrawOperatorEarnings(uint64)", operatorId) - ); - } - - function registerValidator( - bytes calldata publicKey, - uint64[] memory operatorIds, - bytes calldata shares, - uint256 amount, - ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "registerValidator(bytes[],uint64[],bytes,uint256,(uint32,uint64,uint64,bool,uint256))", - publicKey, - operatorIds, - shares, - amount, - cluster - ) - ); - } - - function bulkRegisterValidator( - bytes[] calldata publicKey, - uint64[] memory operatorIds, - bytes[] calldata shares, - uint256 amount, - ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "registerValidator(bytes[],uint64[],bytes,uint256,(uint32,uint64,uint64,bool,uint256))", - publicKey, - operatorIds, - shares, - amount, - cluster - ) - ); - } - - function removeValidator( - bytes calldata publicKey, - uint64[] calldata operatorIds, - ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "removeValidator(bytes,uint64[],(uint32,uint64,uint64,bool,uint256))", - publicKey, - operatorIds, - cluster - ) - ); - } - - function bulkRemoveValidator( - bytes[] calldata publicKeys, - uint64[] calldata operatorIds, - ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "bulkRemoveValidator(bytes[],uint64[],(uint32,uint64,uint64,bool,uint256))", - publicKeys, - operatorIds, - cluster - ) - ); - } - - function liquidate(address owner, uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) external { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "liquidate(address,uint64[],(uint32,uint64,uint64,bool,uint256))", - owner, - operatorIds, - cluster - ) - ); - } - - function reactivate( - uint64[] calldata operatorIds, - uint256 amount, - ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "reactivate(uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", - operatorIds, - amount, - cluster - ) - ); - } - - function deposit( - address owner, - uint64[] calldata operatorIds, - uint256 amount, - ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "deposit(address,uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", - owner, - operatorIds, - amount, - cluster - ) - ); - } - - function withdraw( - uint64[] calldata operatorIds, - uint256 amount, - ISSVNetworkCore.Cluster memory cluster - ) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature( - "withdraw(uint64[],uint256,(uint32,uint64,uint64,bool,uint256))", - operatorIds, - amount, - cluster - ) - ); - } - - function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature("exitValidator(bytes,uint64[]))", publicKey, operatorIds) - ); - } - - function bulkExitValidator(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_CLUSTERS], - abi.encodeWithSignature("bulkExitValidator(bytes[],uint64[]))", publicKeys, operatorIds) - ); - } - - function updateNetworkFee(uint256 fee) external override onlyOwner { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("updateNetworkFee(uint256)", fee) - ); - } - - function withdrawNetworkEarnings(uint256 amount) external override onlyOwner { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("withdrawNetworkEarnings(uint256)", amount) - ); - } - - function updateOperatorFeeIncreaseLimit(uint64 percentage) external override onlyOwner { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("updateOperatorFeeIncreaseLimit(uint64)", percentage) - ); - } - - function updateDeclareOperatorFeePeriod(uint64 timeInSeconds) external override onlyOwner { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("updateDeclareOperatorFeePeriod(uint64)", timeInSeconds) - ); - } - - function updateExecuteOperatorFeePeriod(uint64 timeInSeconds) external override onlyOwner { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("updateExecuteOperatorFeePeriod(uint64)", timeInSeconds) - ); - } - - function updateLiquidationThresholdPeriod(uint64 blocks) external override onlyOwner { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("updateLiquidationThresholdPeriod(uint64)", blocks) - ); - } - - function updateMinimumLiquidationCollateral(uint256 amount) external override onlyOwner { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("updateMinimumLiquidationCollateral(uint256)", amount) - ); - } - - function updateMaximumOperatorFee(uint64 maxFee) external override { - _delegateCall( - SSVStorage.load().ssvContracts[SSVModules.SSV_DAO], - abi.encodeWithSignature("updateMaximumOperatorFee(uint64)", maxFee) - ); - } - - function _delegateCall(address ssvModule, bytes memory callMessage) internal returns (bytes memory) { - /// @custom:oz-upgrades-unsafe-allow delegatecall - (bool success, bytes memory result) = ssvModule.delegatecall(callMessage); - if (!success && result.length > 0) { - // solhint-disable-next-line no-inline-assembly - assembly { - let returndata_size := mload(result) - revert(add(32, result), returndata_size) - } - } - return result; - } - - // Upgrade functions - function updateModule(SSVModules moduleId, address moduleAddress) external onlyOwner { - CoreLib.setModuleContract(moduleId, moduleAddress); - } -} diff --git a/contracts/test/harness/PackedLibHarness.sol b/contracts/test/harness/PackedLibHarness.sol new file mode 100644 index 000000000..39c070d6c --- /dev/null +++ b/contracts/test/harness/PackedLibHarness.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {PackedSSV, PackedETH, PACKED_ETH_ZERO, PACKED_SSV_ZERO, VERSION_SSV, VERSION_ETH, VERSION_UNDEFINED, DEFAULT_OPERATOR_ETH_FEE, DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS, _safeUint64} from "../../libraries/SSVCoreTypes.sol"; +import {PackedSSVLib, PackedETHLib, PackingLib} from "../../libraries/SSVPackedLib.sol"; + +contract PackedLibHarness { + using PackedSSVLib for PackedSSV; + using PackedETHLib for PackedETH; + + // ============ Constants ============ + + function getDeductedDigits() external pure returns (uint256) { + return DEDUCTED_DIGITS; + } + + function getEthDeductedDigits() external pure returns (uint256) { + return ETH_DEDUCTED_DIGITS; + } + + function getVersionSSV() external pure returns (uint8) { + return VERSION_SSV; + } + + function getVersionETH() external pure returns (uint8) { + return VERSION_ETH; + } + + function getVersionUndefined() external pure returns (uint8) { + return VERSION_UNDEFINED; + } + + function getDefaultOperatorEthFee() external pure returns (uint256) { + return DEFAULT_OPERATOR_ETH_FEE; + } + + function getPackedEthZero() external pure returns (uint64) { + return PackedETH.unwrap(PACKED_ETH_ZERO); + } + + function getPackedSsvZero() external pure returns (uint64) { + return PackedSSV.unwrap(PACKED_SSV_ZERO); + } + + // ============ PackedETHLib ============ + + function ethPack(uint256 value) external pure returns (uint64) { + return PackedETH.unwrap(PackedETHLib.pack(value)); + } + + function ethUnpack(uint64 raw) external pure returns (uint256) { + return PackedETHLib.unpack(PackedETH.wrap(raw)); + } + + function ethRaw(uint64 raw) external pure returns (uint64) { + return PackedETHLib.raw(PackedETH.wrap(raw)); + } + + function ethEq(uint64 a, uint64 b) external pure returns (bool) { + return PackedETH.wrap(a).eq(PackedETH.wrap(b)); + } + + function ethNeq(uint64 a, uint64 b) external pure returns (bool) { + return PackedETH.wrap(a).neq(PackedETH.wrap(b)); + } + + function ethGt(uint64 a, uint64 b) external pure returns (bool) { + return PackedETH.wrap(a).gt(PackedETH.wrap(b)); + } + + function ethGte(uint64 a, uint64 b) external pure returns (bool) { + return PackedETH.wrap(a).gte(PackedETH.wrap(b)); + } + + function ethLt(uint64 a, uint64 b) external pure returns (bool) { + return PackedETH.wrap(a).lt(PackedETH.wrap(b)); + } + + function ethLte(uint64 a, uint64 b) external pure returns (bool) { + return PackedETH.wrap(a).lte(PackedETH.wrap(b)); + } + + function ethAdd(uint64 a, uint64 b) external pure returns (uint64) { + return PackedETH.unwrap(PackedETH.wrap(a).add(PackedETH.wrap(b))); + } + + function ethSub(uint64 a, uint64 b) external pure returns (uint64) { + return PackedETH.unwrap(PackedETH.wrap(a).sub(PackedETH.wrap(b))); + } + + // ============ PackedSSVLib ============ + + function ssvPack(uint256 value) external pure returns (uint64) { + return PackedSSV.unwrap(PackedSSVLib.pack(value)); + } + + function ssvUnpack(uint64 raw) external pure returns (uint256) { + return PackedSSVLib.unpack(PackedSSV.wrap(raw)); + } + + function ssvRaw(uint64 raw) external pure returns (uint64) { + return PackedSSVLib.raw(PackedSSV.wrap(raw)); + } + + function ssvEq(uint64 a, uint64 b) external pure returns (bool) { + return PackedSSV.wrap(a).eq(PackedSSV.wrap(b)); + } + + function ssvNeq(uint64 a, uint64 b) external pure returns (bool) { + return PackedSSV.wrap(a).neq(PackedSSV.wrap(b)); + } + + function ssvGt(uint64 a, uint64 b) external pure returns (bool) { + return PackedSSV.wrap(a).gt(PackedSSV.wrap(b)); + } + + function ssvLt(uint64 a, uint64 b) external pure returns (bool) { + return PackedSSV.wrap(a).lt(PackedSSV.wrap(b)); + } + + function ssvAdd(uint64 a, uint64 b) external pure returns (uint64) { + return PackedSSV.unwrap(PackedSSV.wrap(a).add(PackedSSV.wrap(b))); + } + + function ssvSub(uint64 a, uint64 b) external pure returns (uint64) { + return PackedSSV.unwrap(PackedSSV.wrap(a).sub(PackedSSV.wrap(b))); + } + + // ============ _safeUint64 ============ + + function safeUint64(uint128 value) external pure returns (uint64) { + return _safeUint64(value); + } +} diff --git a/contracts/test/harness/SSVClustersHarness.sol b/contracts/test/harness/SSVClustersHarness.sol new file mode 100644 index 000000000..27ae8f88e --- /dev/null +++ b/contracts/test/harness/SSVClustersHarness.sol @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import { SSVClusters } from "../../modules/SSVClusters.sol"; +import { SSVValidators } from "../../modules/SSVValidators.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; +import {SSVStorage, StorageData} from "../../libraries/storage/SSVStorage.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../../libraries/storage/SSVStorageProtocol.sol"; +import {SSVStorageEB, StorageEB} from "../../libraries/storage/SSVStorageEB.sol"; +import {PackedETHLib, PackedSSVLib} from "../../libraries/SSVPackedLib.sol"; +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO} from "../../libraries/SSVCoreTypes.sol"; +import "../../libraries/ClusterLib.sol"; +import {OperatorLib} from "../../libraries/OperatorLib.sol"; +import {CoreLib} from "../../libraries/CoreLib.sol"; + +import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract SSVClustersHarness is SSVClusters, SSVValidators { + using Counters for Counters.Counter; + using ClusterLib for Cluster; + + event OperatorFeeExecuted(address indexed owner, uint64 indexed operatorId, uint256 blockNumber, uint256 fee); + + function mockOperator( + bytes calldata publicKey, + address owner, + uint256 fee, + bool setPrivate + ) external returns (uint64 id) { + StorageData storage s = SSVStorage.load(); + + s.lastOperatorId.increment(); + id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PACKED_SSV_ZERO, + owner: owner, + snapshot: ISSVNetworkCore.Snapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_SSV_ZERO + }), + whitelisted: setPrivate, + ethValidatorCount: 0, + ethFee: PackedETHLib.pack(fee), + ethSnapshot: ISSVNetworkCore.EthSnapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_ETH_ZERO + }) + }); + + s.operatorsPKs[keccak256(publicKey)] = id; + } + + function mockValidatorsPerOperatorLimit(uint32 limit) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = limit; + } + + function mockCurrentNetworkFeeIndex(uint64 index) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFeeIndex = index; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + } + + function getCurrentNetworkFeeIndex() external view returns (uint64) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + return sp.ethNetworkFeeIndex + uint64(block.number - sp.ethNetworkFeeIndexBlockNumber) * PackedETH.unwrap(sp.ethNetworkFee); + } + + function getOperatorEthFee(uint64 operatorId) external view returns (uint64) { + return PackedETH.unwrap(SSVStorage.load().operators[operatorId].ethFee); + } + + function getClusterVUnits(bytes32 clusterId) external view returns (uint64) { + return SSVStorageEB.load().clusterEB[clusterId].vUnits; + } + + function getDaoTotalEthVUnits() external view returns (uint64) { + return SSVStorageProtocol.load().daoTotalEthVUnits; + } + + function getValidatorData(bytes calldata publicKey, address owner) external view returns (bytes32) { + bytes32 hashedValidator = keccak256(abi.encodePacked(publicKey, owner)); + return SSVStorage.load().validatorPKs[hashedValidator]; + } + + function getClusterHash(bytes32 hashedCluster) external view returns (bytes32) { + return SSVStorage.load().ethClusters[hashedCluster]; + } + + function getSSVClusterHash(bytes32 hashedCluster) external view returns (bytes32) { + return SSVStorage.load().clusters[hashedCluster]; + } + + function getDaoValidatorCount() external view returns (uint32) { + return SSVStorageProtocol.load().daoValidatorCount; + } + + function getOperatorEthValidatorCount(uint64 operatorId) external view returns (uint32) { + return SSVStorage.load().operators[operatorId].ethValidatorCount; + } + + function getOperatorEthSnapshot(uint64 operatorId) external view returns (uint64 index, uint32 blockNumber, uint64 balance) { + ISSVNetworkCore.EthSnapshot storage snap = SSVStorage.load().operators[operatorId].ethSnapshot; + return (snap.index, snap.block, PackedETH.unwrap(snap.balance)); + } + + function getOperatorSnapshot(uint64 operatorId) external view returns (uint64 index, uint32 blockNumber, uint64 balance) { + ISSVNetworkCore.Snapshot storage snap = SSVStorage.load().operators[operatorId].snapshot; + return (snap.index, snap.block, PackedSSV.unwrap(snap.balance)); + } + + function getOperatorValidatorCount(uint64 operatorId) external view returns (uint32) { + return SSVStorage.load().operators[operatorId].validatorCount; + } + + function getOperatorSSVFee(uint64 operatorId) external view returns (uint64) { + return PackedSSV.unwrap(SSVStorage.load().operators[operatorId].fee); + } + + function getDaoEthValidatorCount() external view returns (uint32) { + return SSVStorageProtocol.load().ethDaoValidatorCount; + } + + function getDaoEthBalance() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().ethDaoBalance); + } + + function getDaoEthIndexBlockNumber() external view returns (uint32) { + return SSVStorageProtocol.load().ethDaoIndexBlockNumber; + } + + function getOperatorEthVUnits(uint64 operatorId) external view returns (uint64) { + return SSVStorageEB.load().operatorEthVUnits[operatorId]; + } + + function getEffectiveOperatorVUnits(uint64 operatorId) external view returns (uint64) { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint64 baseline = uint64(s.operators[operatorId].ethValidatorCount) * BPS_DENOMINATOR; + uint64 deviation = seb.operatorEthVUnits[operatorId]; + return baseline + deviation; + } + + function mockSetOperatorEthVUnits(uint64 operatorId, uint64 vUnits) external { + SSVStorageEB.load().operatorEthVUnits[operatorId] = vUnits; + } + + function mockSetDaoTotalEthVUnits(uint64 vUnits) external { + SSVStorageProtocol.load().daoTotalEthVUnits = vUnits; + } + + function mockSetEBRoot(uint64 blockNum, bytes32 root) external { + StorageEB storage seb = SSVStorageEB.load(); + seb.ebRoots[blockNum] = root; + if (blockNum > seb.latestCommittedBlock) { + seb.latestCommittedBlock = blockNum; + } + } + + function mockSetMinBlocksBetweenUpdates(uint32 blocks) external { + SSVStorageEB.load().minBlocksBetweenUpdates = blocks; + } + + function mockRemoveOperator(uint64 operatorId) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + operator.ethSnapshot.block = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + operator.ethFee = PACKED_ETH_ZERO; + operator.snapshot.block = 0; + operator.snapshot.balance = PACKED_SSV_ZERO; + operator.fee = PACKED_SSV_ZERO; + operator.ethValidatorCount = 0; + operator.validatorCount = 0; + SSVStorageEB.load().operatorEthVUnits[operatorId] = 0; + } + + /// @notice Simulates removeOperator() accounting + payout without owner checks. + /// @dev Settles snapshots, resets operator state, then transfers settled ETH/SSV balances to recipient. + function mockRemoveOperatorAndPayout(uint64 operatorId, address recipient) external returns (uint256 ethPaid, uint256 ssvPaid) { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + PackedETH currentBalanceETH = PACKED_ETH_ZERO; + PackedSSV currentBalanceSSV = PACKED_SSV_ZERO; + + if (operator.snapshot.block != 0) { + OperatorLib.updateSnapshotStSSV(operator); + currentBalanceSSV = operator.snapshot.balance; + } + + if (operator.ethSnapshot.block != 0) { + OperatorLib.updateSnapshotSt(operator, operatorId); + currentBalanceETH = operator.ethSnapshot.balance; + } + + operator.ethSnapshot.block = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + operator.ethFee = PACKED_ETH_ZERO; + operator.snapshot.block = 0; + operator.snapshot.balance = PACKED_SSV_ZERO; + operator.fee = PACKED_SSV_ZERO; + operator.ethValidatorCount = 0; + operator.validatorCount = 0; + + if (PackedETHLib.raw(currentBalanceETH) > 0) { + ethPaid = PackedETHLib.unpack(currentBalanceETH); + CoreLib.transferBalance(recipient, ethPaid); + } + if (PackedSSVLib.raw(currentBalanceSSV) > 0) { + ssvPaid = PackedSSVLib.unpack(currentBalanceSSV); + CoreLib.transferTokenBalance(recipient, ssvPaid); + } + } + + function mockSetOperatorFee(uint64 operatorId, uint256 fee) external { + SSVStorage.load().operators[operatorId].ethFee = PackedETHLib.pack(fee); + } + + function mockExecuteAllOperatorFees(uint64[] calldata operatorIds, uint256 fee) external { + StorageData storage s = SSVStorage.load(); + for (uint256 i = 0; i < operatorIds.length; i++) { + uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (operator.ethSnapshot.block == 0) { + continue; + } + OperatorLib.updateSnapshotSt(operator, operatorId); + operator.ethFee = PackedETHLib.pack(fee); + emit OperatorFeeExecuted(msg.sender, operatorId, block.number, fee); + } + } + + function mockEthNetworkFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFee = PackedETH.wrap(fee); + } + + function mockMinimumBlocksBeforeLiquidation(uint64 blocks) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumBlocksBeforeLiquidation = blocks; + } + + function mockMinimumLiquidationCollateral(uint64 collateral) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumLiquidationCollateral = PackedETH.wrap(collateral); + } + + function mockMinimumBlocksBeforeLiquidationSSV(uint64 blocks) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumBlocksBeforeLiquidationSSV = blocks; + } + + function mockMinimumLiquidationCollateralSSV(uint64 collateral) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumLiquidationCollateralSSV = PackedSSV.wrap(collateral); + } + + function mockSSVNetworkFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.networkFee = PackedSSV.wrap(fee); + } + + function mockCurrentNetworkFeeIndexSSV(uint64 index) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.networkFeeIndex = index; + sp.networkFeeIndexBlockNumber = uint32(block.number); + } + + function getCurrentNetworkFeeIndexSSV() external view returns (uint64) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + return sp.networkFeeIndex + uint64(block.number - sp.networkFeeIndexBlockNumber) * PackedSSV.unwrap(sp.networkFee); + } + + function getNetworkFeeIndexSSV() external view returns (uint64) { + return SSVStorageProtocol.load().networkFeeIndex; + } + + function mockOperatorSSVFee(uint64 operatorId, uint64 fee) external { + StorageData storage s = SSVStorage.load(); + s.operators[operatorId].fee = PackedSSVLib.pack(fee); + s.operators[operatorId].snapshot.block = uint32(block.number); + } + + function mockRegisterSSVValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + address owner, + Cluster memory cluster + ) external returns (bytes32 hashedCluster) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + hashedCluster = keccak256(abi.encodePacked(owner, operatorIds)); + + s.clusters[hashedCluster] = cluster.hashClusterData(); + + sp.daoValidatorCount += uint32(cluster.validatorCount); + + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + operator.validatorCount += uint32(cluster.validatorCount); + if (operator.snapshot.block == 0) { + operator.snapshot.block = uint32(block.number); + } + } + + bytes32 hashedValidator = keccak256(abi.encodePacked(publicKey, owner)); + s.validatorPKs[hashedValidator] = bytes32(uint256(keccak256(abi.encodePacked(operatorIds))) | uint256(0x01)); + } + + function mockSetClusterVUnits(bytes32 clusterId, uint64 vUnits) external { + StorageEB storage seb = SSVStorageEB.load(); + seb.clusterEB[clusterId].vUnits = vUnits; + } + + function mockSetClusterLiquidated(address owner, uint64[] calldata operatorIds) external { + StorageData storage s = SSVStorage.load(); + bytes32 hashedCluster = keccak256(abi.encodePacked(owner, operatorIds)); + s.ethClusters[hashedCluster] = keccak256(abi.encodePacked(uint32(0), uint64(0), uint64(0), uint256(0), false)); + } + + function mockSetOperatorLegacySSV(uint64 operatorId, uint64 ssvFee) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + operator.fee = PackedSSV.wrap(ssvFee); + operator.snapshot.block = uint32(block.number); + operator.ethFee = PACKED_ETH_ZERO; + operator.ethSnapshot.block = 0; + operator.ethSnapshot.index = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + } + + function mockSetOperatorFeeChangeRequest( + uint64 operatorId, + uint64 fee, + uint64 approvalBeginTime, + uint64 approvalEndTime + ) external { + SSVStorage.load().operatorFeeChangeRequests[operatorId] = ISSVNetworkCore.OperatorFeeChangeRequest( + fee, + approvalBeginTime, + approvalEndTime + ); + } + + function mockSetToken(address token) external { + SSVStorage.load().token = IERC20(token); + } + + function mockWithdrawAllEthEarnings(uint64 operatorId) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + OperatorLib.updateSnapshotSt(operator, operatorId); + PackedETH balance = operator.ethSnapshot.balance; + if (PackedETHLib.raw(balance) == 0) return; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + CoreLib.transferBalance(msg.sender, PackedETHLib.unpack(balance)); + } +} diff --git a/contracts/test/harness/SSVDAOHarness.sol b/contracts/test/harness/SSVDAOHarness.sol new file mode 100644 index 000000000..0c8fab8ab --- /dev/null +++ b/contracts/test/harness/SSVDAOHarness.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {SSVDAO} from "../../modules/SSVDAO.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../../libraries/storage/SSVStorageProtocol.sol"; +import {SSVStorageStaking, StorageStaking} from "../../libraries/storage/SSVStorageStaking.sol"; +import {SSVStorageEB, StorageEB} from "../../libraries/storage/SSVStorageEB.sol"; +import {SSVStorage, StorageData} from "../../libraries/storage/SSVStorage.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PackedETH, PackedSSV} from "../../libraries/SSVCoreTypes.sol"; +import {PackedETHLib} from "../../libraries/SSVPackedLib.sol"; + +contract SSVDAOHarness is SSVDAO { + + constructor(address cssvAddress) SSVDAO(cssvAddress) {} + + function mockSetNetworkFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFee = PackedETHLib.pack(fee); + } + + function mockSetDaoBalance(uint64 balance) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.daoBalance = PackedSSV.wrap(balance); + sp.daoIndexBlockNumber = uint32(block.number); + } + + function mockSetEthDaoBalance(uint64 balance) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethDaoBalance = PackedETH.wrap(balance); + sp.ethDaoIndexBlockNumber = uint32(block.number); + } + + function mockSetDaoValidatorCount(uint32 count) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.daoValidatorCount = count; + } + + function mockSetDaoTotalEthVUnits(uint64 vUnits) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.daoTotalEthVUnits = vUnits; + } + + function mockSetNetworkFeeIndex(uint64 index) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.networkFeeIndex = index; + sp.networkFeeIndexBlockNumber = uint32(block.number); + } + + function mockSetEthNetworkFeeIndex(uint64 index) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFeeIndex = index; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + } + + function mockSetMinimumBlocksBeforeLiquidation(uint64 blocks) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumBlocksBeforeLiquidation = blocks; + } + + function mockSetMinimumBlocksBeforeLiquidationSSV(uint64 blocks) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumBlocksBeforeLiquidationSSV = blocks; + } + + function mockSetMinimumLiquidationCollateral(uint64 collateral) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumLiquidationCollateral = PackedETH.wrap(collateral); + } + + function mockSetMinimumLiquidationCollateralSSV(uint64 collateral) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumLiquidationCollateralSSV = PackedSSV.wrap(collateral); + } + + function mockSetOperatorMaxFee(uint64 maxFee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.operatorMaxFee = PackedETHLib.pack(maxFee); + } + + function mockSetOperatorMaxFeeIncrease(uint64 increase) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.operatorMaxFeeIncrease = increase; + } + + function mockSetDeclareOperatorFeePeriod(uint64 period) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.declareOperatorFeePeriod = period; + } + + function mockSetExecuteOperatorFeePeriod(uint64 period) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.executeOperatorFeePeriod = period; + } + + function mockSetOracle(uint32 oracleId, address oracle) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.oracles[oracleId] = oracle; + if (oracle != address(0)) { + s.oracleIdOf[oracle] = oracleId; + } + } + + function mockupdateQuorumBps(uint16 quorum) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.quorumBps = quorum; + } + + function mockSetCooldownDuration(uint64 duration) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.cooldownDuration = duration; + } + + function mockSetLatestCommittedBlock(uint64 blockNum) external { + StorageEB storage seb = SSVStorageEB.load(); + seb.latestCommittedBlock = blockNum; + } + + function mockSetEBRoot(uint64 blockNum, bytes32 root) external { + StorageEB storage seb = SSVStorageEB.load(); + seb.ebRoots[blockNum] = root; + } + + function mockSetMinBlocksBetweenUpdates(uint32 blocks) external { + SSVStorageEB.load().minBlocksBetweenUpdates = blocks; + } + + function mockSetToken(address token) external { + SSVStorage.load().token = IERC20(token); + } + + function getNetworkFee() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().ethNetworkFee); + } + + function getNetworkFeeSSV() external view returns (uint64) { + return PackedSSV.unwrap(SSVStorageProtocol.load().networkFee); + } + + function getDaoBalance() external view returns (uint64) { + return PackedSSV.unwrap(SSVStorageProtocol.load().daoBalance); + } + + function getEthDaoBalance() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().ethDaoBalance); + } + + function getOperatorMaxFeeIncrease() external view returns (uint64) { + return SSVStorageProtocol.load().operatorMaxFeeIncrease; + } + + function getDeclareOperatorFeePeriod() external view returns (uint64) { + return SSVStorageProtocol.load().declareOperatorFeePeriod; + } + + function getExecuteOperatorFeePeriod() external view returns (uint64) { + return SSVStorageProtocol.load().executeOperatorFeePeriod; + } + + function getMinimumBlocksBeforeLiquidation() external view returns (uint64) { + return SSVStorageProtocol.load().minimumBlocksBeforeLiquidation; + } + + function getMinimumBlocksBeforeLiquidationSSV() external view returns (uint64) { + return SSVStorageProtocol.load().minimumBlocksBeforeLiquidationSSV; + } + + function getMinimumLiquidationCollateral() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().minimumLiquidationCollateral); + } + + function getMinimumLiquidationCollateralSSV() external view returns (uint64) { + return PackedSSV.unwrap(SSVStorageProtocol.load().minimumLiquidationCollateralSSV); + } + + function getOperatorMaxFee() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().operatorMaxFee); + } + + function getMinimumOperatorEthFee() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().minimumOperatorEthFee); + } + + function getQuorumBps() external view returns (uint16) { + return SSVStorageStaking.load().quorumBps; + } + + function getCooldownDuration() external view returns (uint64) { + return SSVStorageStaking.load().cooldownDuration; + } + + function getLatestCommittedBlock() external view returns (uint64) { + return SSVStorageEB.load().latestCommittedBlock; + } + + function getEBRoot(uint64 blockNum) external view returns (bytes32) { + return SSVStorageEB.load().ebRoots[blockNum]; + } + + function getMinBlocksBetweenUpdates() external view returns (uint32) { + return SSVStorageEB.load().minBlocksBetweenUpdates; + } + + function getOracleAddress(uint32 oracleId) external view returns (address) { + return SSVStorageStaking.load().oracles[oracleId]; + } + + function getOracleId(address oracle) external view returns (uint32) { + return SSVStorageStaking.load().oracleIdOf[oracle]; + } + + function getRootCommitmentWeight(bytes32 commitmentKey) external view returns (uint256) { + return SSVStorageEB.load().rootCommitments[commitmentKey]; + } + + function hasOracleVoted(bytes32 commitmentKey, uint32 oracleId) external view returns (bool) { + return SSVStorageEB.load().hasVoted[commitmentKey][oracleId]; + } + + function getRoundFrozenSupply(bytes32 commitmentKey) external view returns (uint256) { + return SSVStorageEB.load().roundFrozenSupply[commitmentKey]; + } +} diff --git a/contracts/test/harness/SSVOperatorsHarness.sol b/contracts/test/harness/SSVOperatorsHarness.sol new file mode 100644 index 000000000..101ea07df --- /dev/null +++ b/contracts/test/harness/SSVOperatorsHarness.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {SSVOperators} from "../../modules/SSVOperators.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../../libraries/storage/SSVStorageProtocol.sol"; +import {SSVStorage, StorageData} from "../../libraries/storage/SSVStorage.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; +import {ISSVOperators} from "../../interfaces/ISSVOperators.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PackedETH, PackedSSV, PACKED_ETH_ZERO} from "../../libraries/SSVCoreTypes.sol"; +import {PackedETHLib} from "../../libraries/SSVPackedLib.sol"; +import {SSVStorageEB} from "../../libraries/storage/SSVStorageEB.sol"; + +contract SSVOperatorsHarness is SSVOperators { + + constructor(uint256 upgradeTimestamp) SSVOperators(upgradeTimestamp) { + + } + + function mockSetOperatorMaxFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.operatorMaxFee = PackedETHLib.pack(fee); + } + + function mockSetFeePeriods(uint64 declarePeriod, uint64 executePeriod) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.declareOperatorFeePeriod = declarePeriod; + sp.executeOperatorFeePeriod = executePeriod; + } + + function mockSetOperatorMaxFeeIncrease(uint64 increase) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.operatorMaxFeeIncrease = increase; + } + + function mockSetMinimumOperatorEthFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumOperatorEthFee = PackedETHLib.pack(fee); + } + + function getOperator(uint64 operatorId) external view returns (Operator memory) { + return SSVStorage.load().operators[operatorId]; + } + + function getOperatorFeeChangeRequest(uint64 operatorId) external view returns (OperatorFeeChangeRequest memory) { + return SSVStorage.load().operatorFeeChangeRequests[operatorId]; + } + + function getOperatorWhitelist(uint64 operatorId) external view returns (address) { + return SSVStorage.load().operatorsWhitelist[operatorId]; + } + + function mockSetOperator( + uint64 operatorId, + ISSVNetworkCore.Operator memory operator + ) external { + SSVStorage.load().operators[operatorId] = operator; + } + + function mockSetOperatorBalances( + uint64 operatorId, + uint64 ethSnapshotBalance, + uint64 ssvSnapshotBalance + ) external { + StorageData storage s = SSVStorage.load(); + s.operators[operatorId].ethSnapshot.balance = PackedETH.wrap(ethSnapshotBalance); + s.operators[operatorId].snapshot.balance = PackedSSV.wrap(ssvSnapshotBalance); + } + + function mockSetToken(address token) external { + SSVStorage.load().token = IERC20(token); + } + + function mockSetOperatorFeeChangeRequest( + uint64 operatorId, + uint64 fee, + uint64 approvalBeginTime, + uint64 approvalEndTime + ) external { + SSVStorage.load().operatorFeeChangeRequests[operatorId] = OperatorFeeChangeRequest( + fee, + approvalBeginTime, + approvalEndTime + ); + } + + function mockSetOperatorLegacySSV(uint64 operatorId, uint64 ssvFee) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + operator.fee = PackedSSV.wrap(ssvFee); + operator.snapshot.block = uint32(block.number); + operator.ethFee = PACKED_ETH_ZERO; + operator.ethSnapshot.block = 0; + operator.ethSnapshot.index = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + } + + function getUpgradeTimestamp() external view returns (uint256) { + return UPGRADE_TIMESTAMP; + } + + function getOperatorEthVUnits(uint64 operatorId) external view returns (uint64) { + return SSVStorageEB.load().operatorEthVUnits[operatorId]; + } + + function mockSetOperatorEthVUnits(uint64 operatorId, uint64 vUnits) external { + SSVStorageEB.load().operatorEthVUnits[operatorId] = vUnits; + } +} diff --git a/contracts/test/harness/SSVStakingHarness.sol b/contracts/test/harness/SSVStakingHarness.sol new file mode 100644 index 000000000..d75d71e31 --- /dev/null +++ b/contracts/test/harness/SSVStakingHarness.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {SSVStaking} from "../../modules/SSVStaking.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../../libraries/storage/SSVStorageProtocol.sol"; +import { + MAX_DELEGATION_SLOTS, + SSVStorageStaking, + StorageStaking, + UnstakeRequest +} from "../../libraries/storage/SSVStorageStaking.sol"; +import {SSVStorage, StorageData} from "../../libraries/storage/SSVStorage.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PackedETH} from "../../libraries/SSVCoreTypes.sol"; +import {PackedETHLib} from "../../libraries/SSVPackedLib.sol"; + +contract SSVStakingHarness is SSVStaking { + + constructor(address cssvAddress) SSVStaking(cssvAddress) {} + + // ============ Mock Setters ============ + + function mockSetToken(address token) external { + SSVStorage.load().token = IERC20(token); + } + + function mockSetCooldownDuration(uint64 duration) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.cooldownDuration = duration; + } + + function mockSetAccEthPerShare(uint128 value) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.accEthPerShare = value; + } + + function mockSetStakingEthPoolBalance(uint64 balance) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.stakingEthPoolBalance = PackedETH.wrap(balance); + } + + function mockSetUserIndex(address user, uint256 index) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.userIndex[user] = index; + } + + function mockSetUserAccrued(address user, uint256 amount) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.accrued[user] = amount; + } + + function mockSetWithdrawal(address user, uint192 amount, uint64 unlockTime) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.withdrawalRequests[user].push(UnstakeRequest({amount: amount, unlockTime: unlockTime})); + } + + function mockSetDefaultOracleIds(uint32[MAX_DELEGATION_SLOTS] calldata oracleIds) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.defaultOracleIds = oracleIds; + } + + function mockSetOracle(uint32 oracleId, address oracle) external { + StorageStaking storage s = SSVStorageStaking.load(); + s.oracles[oracleId] = oracle; + if (oracle != address(0)) { + s.oracleIdOf[oracle] = oracleId; + } + } + + function mockSetEthDaoBalance(uint64 balance) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethDaoBalance = PackedETH.wrap(balance); + sp.ethDaoIndexBlockNumber = uint32(block.number); + } + + function mockSetEthNetworkFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFee = PackedETH.wrap(fee); + } + + function mockSetDaoTotalEthVUnits(uint64 vUnits) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.daoTotalEthVUnits = vUnits; + } + + function mockSetEthNetworkFeeIndex(uint64 index) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFeeIndex = index; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + } + + // ============ Getters ============ + + function getCooldownDuration() external view returns (uint64) { + return SSVStorageStaking.load().cooldownDuration; + } + + function getAccEthPerShare() external view returns (uint128) { + return SSVStorageStaking.load().accEthPerShare; + } + + function getStakingEthPoolBalance() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageStaking.load().stakingEthPoolBalance); + } + + function getUserIndex(address user) external view returns (uint256) { + return SSVStorageStaking.load().userIndex[user]; + } + + function getUserAccrued(address user) external view returns (uint256) { + return SSVStorageStaking.load().accrued[user]; + } + + function getWithdrawal(address user) external view returns (uint192 amount, uint64 unlockTime) { + UnstakeRequest[] storage requests = SSVStorageStaking.load().withdrawalRequests[user]; + if (requests.length == 0) { + return (0, 0); + } + + UnstakeRequest memory req = requests[requests.length - 1]; + return (req.amount, req.unlockTime); + } + + function getWithdrawalRequestsCount(address user) external view returns (uint256) { + return SSVStorageStaking.load().withdrawalRequests[user].length; + } + + function getWithdrawalRequest(address user, uint256 index) external view returns (uint192 amount, uint64 unlockTime) { + UnstakeRequest storage req = SSVStorageStaking.load().withdrawalRequests[user][index]; + return (req.amount, req.unlockTime); + } + + function getActiveOracleIds() external view returns (uint32[MAX_DELEGATION_SLOTS] memory) { + return SSVStorageStaking.load().defaultOracleIds; + } + + function getOracleAddress(uint32 oracleId) external view returns (address) { + return SSVStorageStaking.load().oracles[oracleId]; + } + + function getOracleId(address oracle) external view returns (uint32) { + return SSVStorageStaking.load().oracleIdOf[oracle]; + } + + function getEthDaoBalance() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().ethDaoBalance); + } + + function getEthNetworkFee() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().ethNetworkFee); + } + + function getDaoTotalEthVUnits() external view returns (uint64) { + return SSVStorageProtocol.load().daoTotalEthVUnits; + } + + // ============ Receive ETH for testing ============ + + receive() external payable {} +} diff --git a/contracts/test/harness/SSVValidatorsHarness.sol b/contracts/test/harness/SSVValidatorsHarness.sol new file mode 100644 index 000000000..778246b6c --- /dev/null +++ b/contracts/test/harness/SSVValidatorsHarness.sol @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import { SSVValidators } from "../../modules/SSVValidators.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; +import {SSVStorage, StorageData} from "../../libraries/storage/SSVStorage.sol"; +import {SSVStorageProtocol, StorageProtocol} from "../../libraries/storage/SSVStorageProtocol.sol"; +import {SSVStorageEB, StorageEB} from "../../libraries/storage/SSVStorageEB.sol"; +import {PackedETHLib, PackedSSVLib} from "../../libraries/SSVPackedLib.sol"; +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO} from "../../libraries/SSVCoreTypes.sol"; + +import "../../libraries/ClusterLib.sol"; + +import {Counters} from "@openzeppelin/contracts/utils/Counters.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract SSVValidatorsHarness is SSVValidators { + using Counters for Counters.Counter; + using ClusterLib for Cluster; + + function mockOperator( + bytes calldata publicKey, + address owner, + uint256 fee, + bool setPrivate + ) external returns (uint64 id) { + StorageData storage s = SSVStorage.load(); + + s.lastOperatorId.increment(); + id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PACKED_SSV_ZERO, + owner: owner, + snapshot: ISSVNetworkCore.Snapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_SSV_ZERO + }), + whitelisted: setPrivate, + ethValidatorCount: 0, + ethFee: PackedETHLib.pack(fee), + ethSnapshot: ISSVNetworkCore.EthSnapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_ETH_ZERO + }) + }); + + s.operatorsPKs[keccak256(publicKey)] = id; + } + + function mockValidatorsPerOperatorLimit(uint32 limit) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = limit; + } + + function mockCurrentNetworkFeeIndex(uint64 index) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFeeIndex = index; + } + + function getCurrentNetworkFeeIndex() external view returns (uint64) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + return sp.ethNetworkFeeIndex; + } + + function getOperatorEthFee(uint64 operatorId) external view returns (uint64) { + return PackedETH.unwrap(SSVStorage.load().operators[operatorId].ethFee); + } + + function getClusterVUnits(bytes32 clusterId) external view returns (uint64) { + StorageEB storage seb = SSVStorageEB.load(); + return seb.clusterEB[clusterId].vUnits; + } + + function getValidatorData(bytes calldata publicKey, address owner) external view returns (bytes32) { + bytes32 hashedValidator = keccak256(abi.encodePacked(publicKey, owner)); + return SSVStorage.load().validatorPKs[hashedValidator]; + } + + function getClusterHash(bytes32 hashedCluster) external view returns (bytes32) { + return SSVStorage.load().ethClusters[hashedCluster]; + } + + function getSSVClusterHash(bytes32 hashedCluster) external view returns (bytes32) { + return SSVStorage.load().clusters[hashedCluster]; + } + + function getOperatorValidatorCount(uint64 operatorId) external view returns (uint32) { + return SSVStorage.load().operators[operatorId].validatorCount; + } + + function getDaoValidatorCount() external view returns (uint32) { + return SSVStorageProtocol.load().daoValidatorCount; + } + + function getOperatorSnapshot(uint64 operatorId) external view returns (uint64 index, uint32 blockNumber, uint64 balance) { + ISSVNetworkCore.Snapshot storage snap = SSVStorage.load().operators[operatorId].snapshot; + return (snap.index, snap.block, PackedSSV.unwrap(snap.balance)); + } + + function getOperatorEthValidatorCount(uint64 operatorId) external view returns (uint32) { + return SSVStorage.load().operators[operatorId].ethValidatorCount; + } + + function getOperatorEthSnapshot(uint64 operatorId) external view returns (uint64 index, uint32 blockNumber, uint64 balance) { + ISSVNetworkCore.EthSnapshot storage snap = SSVStorage.load().operators[operatorId].ethSnapshot; + return (snap.index, snap.block, PackedETH.unwrap(snap.balance)); + } + + function getDaoEthValidatorCount() external view returns (uint32) { + return SSVStorageProtocol.load().ethDaoValidatorCount; + } + + function getDaoEthBalance() external view returns (uint64) { + return PackedETH.unwrap(SSVStorageProtocol.load().ethDaoBalance); + } + + function getDaoEthIndexBlockNumber() external view returns (uint32) { + return SSVStorageProtocol.load().ethDaoIndexBlockNumber; + } + + function getOperatorEthVUnits(uint64 operatorId) external view returns (uint64) { + return SSVStorageEB.load().operatorEthVUnits[operatorId]; + } + + function getEffectiveOperatorVUnits(uint64 operatorId) external view returns (uint64) { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint64 baseline = uint64(s.operators[operatorId].ethValidatorCount) * BPS_DENOMINATOR; + uint64 deviation = seb.operatorEthVUnits[operatorId]; + return baseline + deviation; + } + + function mockEthNetworkFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFee = PackedETH.wrap(fee); + } + + function mockMinimumBlocksBeforeLiquidation(uint64 blocks) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumBlocksBeforeLiquidation = blocks; + } + + function mockMinimumLiquidationCollateral(uint64 collateral) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumLiquidationCollateral = PackedETH.wrap(collateral); + } + + function mockMinimumBlocksBeforeLiquidationSSV(uint64 blocks) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumBlocksBeforeLiquidationSSV = blocks; + } + + function mockMinimumLiquidationCollateralSSV(uint64 collateral) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.minimumLiquidationCollateralSSV = PackedSSV.wrap(collateral); + } + + function mockSSVNetworkFee(uint64 fee) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.networkFee = PackedSSV.wrap(fee); + } + + function mockCurrentNetworkFeeIndexSSV(uint64 index) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.networkFeeIndex = index; + sp.networkFeeIndexBlockNumber = uint32(block.number); + } + + function getCurrentNetworkFeeIndexSSV() external view returns (uint64) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + return sp.networkFeeIndex + uint64(block.number - sp.networkFeeIndexBlockNumber) * PackedSSV.unwrap(sp.networkFee); + } + + function getNetworkFeeIndexSSV() external view returns (uint64) { + return SSVStorageProtocol.load().networkFeeIndex; + } + + function mockOperatorSSVFee(uint64 operatorId, uint64 fee) external { + StorageData storage s = SSVStorage.load(); + s.operators[operatorId].fee = PackedSSV.wrap(fee); + s.operators[operatorId].snapshot.block = uint32(block.number); + } + + function mockSetOperatorLegacySSV(uint64 operatorId, uint64 ssvFee) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + operator.fee = PackedSSV.wrap(ssvFee); + operator.snapshot.block = uint32(block.number); + operator.ethFee = PACKED_ETH_ZERO; + operator.ethSnapshot.block = 0; + operator.ethSnapshot.index = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + } + + function mockRegisterSSVValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + address owner, + Cluster memory cluster + ) external returns (bytes32 hashedCluster) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + hashedCluster = keccak256(abi.encodePacked(owner, operatorIds)); + + s.clusters[hashedCluster] = cluster.hashClusterData(); + + sp.daoValidatorCount += uint32(cluster.validatorCount); + + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + operator.validatorCount += uint32(cluster.validatorCount); + if (operator.snapshot.block == 0) { + operator.snapshot.block = uint32(block.number); + } + } + + bytes32 hashedValidator = keccak256(abi.encodePacked(publicKey, owner)); + s.validatorPKs[hashedValidator] = bytes32(uint256(keccak256(abi.encodePacked(operatorIds))) | uint256(0x01)); + } + + function mockSetClusterVUnits(bytes32 clusterId, uint64 vUnits) external { + StorageEB storage seb = SSVStorageEB.load(); + seb.clusterEB[clusterId].vUnits = vUnits; + } + + function mockSetClusterLiquidated(address owner, uint64[] calldata operatorIds) external { + StorageData storage s = SSVStorage.load(); + bytes32 hashedCluster = keccak256(abi.encodePacked(owner, operatorIds)); + s.ethClusters[hashedCluster] = keccak256(abi.encodePacked(uint32(0), uint64(0), uint64(0), uint256(0), false)); + } + + function mockSetToken(address token) external { + SSVStorage.load().token = IERC20(token); + } + + function mockRemoveOperator(uint64 operatorId) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + operator.ethSnapshot.block = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + operator.ethFee = PACKED_ETH_ZERO; + operator.snapshot.block = 0; + operator.snapshot.balance = PACKED_SSV_ZERO; + operator.fee = PACKED_SSV_ZERO; + operator.ethValidatorCount = 0; + operator.validatorCount = 0; + } +} diff --git a/contracts/test/harness/SSVViewsHarness.sol b/contracts/test/harness/SSVViewsHarness.sol new file mode 100644 index 000000000..73521e705 --- /dev/null +++ b/contracts/test/harness/SSVViewsHarness.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {SSVViews} from "../../modules/SSVViews.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; +import {SSVStorage, StorageData} from "../../libraries/storage/SSVStorage.sol"; +import {SSVStorageProtocol} from "../../libraries/storage/SSVStorageProtocol.sol"; +import {SSVStorageEB} from "../../libraries/storage/SSVStorageEB.sol"; +import {PackedETHLib, PackedSSVLib} from "../../libraries/SSVPackedLib.sol"; +import "../../libraries/ClusterLib.sol"; + +/// @title SSVViewsHarness +/// @author SSV Labs +/// @notice Test-only harness that seeds storage for direct SSVViews unit testing. +contract SSVViewsHarness is SSVViews { + using ClusterLib for ISSVNetworkCore.Cluster; + + /// @notice Deploys the SSVViews harness. + /// @param cssv The cSSV token address used by SSVViews. + constructor(address cssv) SSVViews(cssv) {} + + /// @notice Sets operator accounting fields for ETH and SSV paths. + /// @param operatorId Operator id to configure. + /// @param owner Operator owner address. + /// @param ethFee ETH fee in unpacked units. + /// @param ssvFee SSV fee in unpacked units. + /// @param ethValidatorCount ETH validator count. + /// @param ssvValidatorCount SSV validator count. + function mockSetOperator( + uint64 operatorId, + address owner, + uint256 ethFee, + uint256 ssvFee, + uint32 ethValidatorCount, + uint32 ssvValidatorCount + ) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + operator.owner = owner; + operator.ethFee = PackedETHLib.pack(ethFee); + operator.fee = PackedSSVLib.pack(ssvFee); + operator.ethValidatorCount = ethValidatorCount; + operator.validatorCount = ssvValidatorCount; + operator.ethSnapshot.block = uint32(block.number); + operator.snapshot.block = uint32(block.number); + } + + /// @notice Sets stored ETH and SSV operator earnings snapshots. + /// @param operatorId Operator id to configure. + /// @param ethEarnings ETH earnings in unpacked units. + /// @param ssvEarnings SSV earnings in unpacked units. + function mockSetOperatorEarnings(uint64 operatorId, uint256 ethEarnings, uint256 ssvEarnings) external { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + operator.ethSnapshot.balance = PackedETHLib.pack(ethEarnings); + operator.snapshot.balance = PackedSSVLib.pack(ssvEarnings); + operator.ethSnapshot.block = uint32(block.number); + operator.snapshot.block = uint32(block.number); + } + + /// @notice Sets network ETH fee. + /// @param fee ETH network fee in unpacked units. + function mockSetNetworkFeeETH(uint256 fee) external { + SSVStorageProtocol.load().ethNetworkFee = PackedETHLib.pack(fee); + } + + /// @notice Sets network SSV fee. + /// @param fee SSV network fee in unpacked units. + function mockSetNetworkFeeSSV(uint256 fee) external { + SSVStorageProtocol.load().networkFee = PackedSSVLib.pack(fee); + } + + /// @notice Returns SSV snapshot and fee raw values for an operator. + /// @param operatorId Operator id to query. + /// @return feeRaw Packed SSV fee raw value. + /// @return index SSV snapshot index raw value. + /// @return blockNumber SSV snapshot block number. + /// @return balanceRaw Packed SSV snapshot balance raw value. + function getOperatorSSVSnapshot( + uint64 operatorId + ) external view returns (uint64 feeRaw, uint64 index, uint32 blockNumber, uint64 balanceRaw) { + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[operatorId]; + return ( + PackedSSV.unwrap(operator.fee), + operator.snapshot.index, + operator.snapshot.block, + PackedSSV.unwrap(operator.snapshot.balance) + ); + } + + /// @notice Returns SSV network fee and fee-index state. + /// @return feeRaw Packed SSV network fee raw value. + /// @return index SSV network fee index raw value. + /// @return indexBlockNumber SSV network fee index block number. + function getNetworkFeeStateSSV() external view returns (uint64 feeRaw, uint64 index, uint32 indexBlockNumber) { + return ( + PackedSSV.unwrap(SSVStorageProtocol.load().networkFee), + SSVStorageProtocol.load().networkFeeIndex, + SSVStorageProtocol.load().networkFeeIndexBlockNumber + ); + } + + /// @notice Inserts an ETH cluster record into storage. + /// @param clusterOwner Cluster owner address. + /// @param operatorIds Operator ids composing the cluster. + /// @param cluster Cluster state to store. + function mockRegisterETHCluster( + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster calldata cluster + ) external { + bytes32 hashedCluster = keccak256(abi.encodePacked(clusterOwner, operatorIds)); + SSVStorage.load().ethClusters[hashedCluster] = cluster.hashClusterData(); + } + + /// @notice Inserts an SSV cluster record into storage. + /// @param clusterOwner Cluster owner address. + /// @param operatorIds Operator ids composing the cluster. + /// @param cluster Cluster state to store. + function mockRegisterSSVCluster( + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster calldata cluster + ) external { + bytes32 hashedCluster = keccak256(abi.encodePacked(clusterOwner, operatorIds)); + SSVStorage.load().clusters[hashedCluster] = cluster.hashClusterData(); + } + + /// @notice Seeds the EB snapshot vUnits for a cluster (ETH or SSV) in SSVStorageEB. + /// @param clusterOwner Cluster owner address. + /// @param operatorIds Operator ids composing the cluster. + /// @param vUnits vUnits value to store (0 = implicit EB fallback to validatorCount * BPS_DENOMINATOR). + function mockSetClusterEB( + address clusterOwner, + uint64[] calldata operatorIds, + uint64 vUnits + ) external { + bytes32 hashedCluster = keccak256(abi.encodePacked(clusterOwner, operatorIds)); + SSVStorageEB.load().clusterEB[hashedCluster].vUnits = vUnits; + } +} diff --git a/contracts/test/interfaces/ISSVNetworkT.sol b/contracts/test/interfaces/ISSVNetworkT.sol deleted file mode 100644 index 7d5b9b21e..000000000 --- a/contracts/test/interfaces/ISSVNetworkT.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.20; - -import "../../interfaces/ISSVNetworkCore.sol"; -import "../../interfaces/ISSVOperators.sol"; -import "../../interfaces/ISSVClusters.sol"; -import "../../interfaces/ISSVDAO.sol"; -import "../../interfaces/ISSVViews.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -interface ISSVNetworkT { - function initialize( - IERC20 token_, - ISSVOperators ssvOperators_, - ISSVClusters ssvClusters_, - ISSVDAO ssvDAO_, - ISSVViews ssvViews_, - uint64 minimumBlocksBeforeLiquidation_, - uint256 minimumLiquidationCollateral_, - uint32 validatorsPerOperatorLimit_, - uint64 declareOperatorFeePeriod_, - uint64 executeOperatorFeePeriod_, - uint64 operatorMaxFeeIncrease_ - ) external; - - function setFeeRecipientAddress(address feeRecipientAddress) external; -} diff --git a/contracts/test/libraries/CoreLibT.sol b/contracts/test/libraries/CoreLibT.sol index da8957137..db21b8516 100644 --- a/contracts/test/libraries/CoreLibT.sol +++ b/contracts/test/libraries/CoreLibT.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "../../libraries/SSVStorage.sol"; +import "../../libraries/storage/SSVStorage.sol"; library CoreLibT { function getVersion() internal pure returns (string memory) { diff --git a/contracts/test/libraries/SSVStorageT.sol b/contracts/test/libraries/SSVStorageT.sol index 0662b1ac1..500733eeb 100644 --- a/contracts/test/libraries/SSVStorageT.sol +++ b/contracts/test/libraries/SSVStorageT.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.24; import "../../interfaces/ISSVNetworkCore.sol"; -import "../../libraries/Types.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -15,7 +14,6 @@ enum SSVModules { library SSVStorageT { using Counters for Counters.Counter; - using Types64 for uint64; uint256 constant SSV_STORAGE_POSITION = uint256(keccak256("ssv.network.storage")) - 1; diff --git a/contracts/test/mocks/AttackerWhitelistingContract.sol b/contracts/test/mocks/AttackerWhitelistingContract.sol index a60a2a0bf..a8c56838e 100644 --- a/contracts/test/mocks/AttackerWhitelistingContract.sol +++ b/contracts/test/mocks/AttackerWhitelistingContract.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; import "../../interfaces/external/ISSVWhitelistingContract.sol"; -import "../../interfaces/ISSVClusters.sol"; +import "../../interfaces/ISSVValidators.sol"; import "./BeneficiaryContract.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; @@ -17,9 +17,8 @@ contract AttackerContract { bytes calldata _publicKey, uint64[] memory _operatorIds, bytes calldata _sharesData, - uint256 _amount, - ISSVNetworkCore.Cluster memory _cluserData + ISSVNetworkCore.Cluster memory _clusterData ) external { - ISSVClusters(ssvContract).registerValidator(_publicKey, _operatorIds, _sharesData, _amount, _cluserData); + ISSVValidators(ssvContract).registerValidator(_publicKey, _operatorIds, _sharesData, _clusterData); } } diff --git a/contracts/test/mocks/EffectiveBalanceTest.sol b/contracts/test/mocks/EffectiveBalanceTest.sol new file mode 100644 index 000000000..d6b91d92f --- /dev/null +++ b/contracts/test/mocks/EffectiveBalanceTest.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../libraries/ClusterLib.sol"; + +contract EffectiveBalanceTest { + function testRoundtrip(uint32 effectiveBalance) public pure returns (uint64 vUnits, uint32 result, bool success) { + vUnits = ClusterLib.ebToVUnits(effectiveBalance); + result = ClusterLib.vUnitsToEB(vUnits); + success = (result == effectiveBalance); + } +} diff --git a/contracts/test/mocks/FakeWhitelistingContract.sol b/contracts/test/mocks/FakeWhitelistingContract.sol index 9db27c3db..8ccaaee64 100644 --- a/contracts/test/mocks/FakeWhitelistingContract.sol +++ b/contracts/test/mocks/FakeWhitelistingContract.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import "../../interfaces/external/ISSVWhitelistingContract.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -/// @notice Whitelisted contract that passes the validatity check of supporting ISSVWhitelistingContract +/// @notice Whitelisted contract that passes the validation check of supporting ISSVWhitelistingContract /// and tries to re-enter SSVNetwork.registerValidator function. contract FakeWhitelistingContract is ERC165 { struct Cluster { @@ -32,13 +32,13 @@ contract FakeWhitelistingContract is ERC165 { uint64[] memory _operatorIds, bytes calldata _sharesData, uint256 _amount, - Cluster memory _cluserData + Cluster memory _clusterData ) external { publicKey = _publicKey; operatorIds = _operatorIds; sharesData = _sharesData; amount = _amount; - clusterData = _cluserData; + clusterData = _clusterData; } function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { diff --git a/contracts/test/mocks/GenericWhitelistContract.sol b/contracts/test/mocks/GenericWhitelistContract.sol index 987b0cfc6..2d3ca8527 100644 --- a/contracts/test/mocks/GenericWhitelistContract.sol +++ b/contracts/test/mocks/GenericWhitelistContract.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.24; -import "../../interfaces/ISSVClusters.sol"; +import "../../interfaces/ISSVValidators.sol"; import "../../interfaces/ISSVNetworkCore.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract GenericWhitelistContract { - ISSVClusters private ssvContract; + ISSVValidators private ssvContract; IERC20 private ssvToken; - constructor(ISSVClusters _ssvContract, IERC20 _ssvToken) { + constructor(ISSVValidators _ssvContract, IERC20 _ssvToken) { ssvContract = _ssvContract; ssvToken = _ssvToken; } @@ -18,10 +18,8 @@ contract GenericWhitelistContract { bytes calldata _publicKey, uint64[] memory _operatorIds, bytes calldata _sharesData, - uint256 _amount, - ISSVNetworkCore.Cluster memory _cluserData + ISSVNetworkCore.Cluster memory _clusterData ) external { - ssvToken.approve(address(ssvContract), _amount); - ssvContract.registerValidator(_publicKey, _operatorIds, _sharesData, _amount, _cluserData); + ssvContract.registerValidator(_publicKey, _operatorIds, _sharesData, _clusterData); } } diff --git a/contracts/test/mocks/MaliciousClaimEthRewards.sol b/contracts/test/mocks/MaliciousClaimEthRewards.sol new file mode 100644 index 000000000..1e275f00e --- /dev/null +++ b/contracts/test/mocks/MaliciousClaimEthRewards.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVStaking} from "../../interfaces/ISSVStaking.sol"; + +contract MaliciousClaimEthRewards { + address public ssvNetwork; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function attack() external { + ISSVStaking(ssvNetwork).claimEthRewards(); + } + + receive() external payable { + ISSVStaking(ssvNetwork).claimEthRewards(); + } +} diff --git a/contracts/test/mocks/MaliciousLiquidate.sol b/contracts/test/mocks/MaliciousLiquidate.sol new file mode 100644 index 000000000..771ebfc1c --- /dev/null +++ b/contracts/test/mocks/MaliciousLiquidate.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVClusters} from "../../interfaces/ISSVClusters.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; +import {ISSVValidators} from "../../interfaces/ISSVValidators.sol"; + +contract MaliciousLiquidate { + address public ssvNetwork; + address public targetOwner; + uint64[] public ops; + ISSVNetworkCore.Cluster public cl; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function setParams(uint64[] memory _ops, ISSVNetworkCore.Cluster memory _cl) external { + ops = _ops; + cl = _cl; + } + + function registerValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + ISSVValidators(ssvNetwork).registerValidator{value: msg.value}(publicKey, operatorIds, sharesData, cluster); + } + + function attack() external { + ISSVClusters(ssvNetwork).liquidate(address(this), ops, cl); + } + + receive() external payable { + ISSVClusters(ssvNetwork).liquidate(address(this), ops, cl); + } +} \ No newline at end of file diff --git a/contracts/test/mocks/MaliciousReactivate.sol b/contracts/test/mocks/MaliciousReactivate.sol new file mode 100644 index 000000000..e3e2ca487 --- /dev/null +++ b/contracts/test/mocks/MaliciousReactivate.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVClusters} from "../../interfaces/ISSVClusters.sol"; +import {ISSVValidators} from "../../interfaces/ISSVValidators.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; + +contract MaliciousReactivate { + address public ssvNetwork; + uint64[] public ops; + ISSVNetworkCore.Cluster public cl; + + uint64[] public reactivateOps; + ISSVNetworkCore.Cluster public reactivateCl; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function setParams( + uint64[] memory _ops, + ISSVNetworkCore.Cluster memory _cl + ) external { + ops = _ops; + cl = _cl; + } + + function setReactivateParams( + uint64[] memory _ops, + ISSVNetworkCore.Cluster memory _cl + ) external { + reactivateOps = _ops; + reactivateCl = _cl; + } + + function registerValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + ISSVValidators(ssvNetwork).registerValidator{value: msg.value}(publicKey, operatorIds, sharesData, cluster); + } + + function attack() external { + ISSVClusters(ssvNetwork).withdraw(ops, 1, cl); + } + + receive() external payable { + ISSVClusters(ssvNetwork).reactivate{value: msg.value}(reactivateOps, reactivateCl); + } +} diff --git a/contracts/test/mocks/MaliciousUpdateClusterBalance.sol b/contracts/test/mocks/MaliciousUpdateClusterBalance.sol new file mode 100644 index 000000000..d65c25e70 --- /dev/null +++ b/contracts/test/mocks/MaliciousUpdateClusterBalance.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVClusters} from "../../interfaces/ISSVClusters.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; +import {ISSVValidators} from "../../interfaces/ISSVValidators.sol"; + +contract MaliciousUpdateClusterBalance { + address public immutable ssvNetwork; + + uint64 public blockNum; + uint32 public effectiveBalance; + uint64[] public liquidationOps; + bytes32[] public merkleProof; + ISSVNetworkCore.Cluster public liquidationCluster; + + uint64[] public withdrawOps; + uint256 public withdrawAmount; + ISSVNetworkCore.Cluster public withdrawCluster; + + bool public attemptedReenter; + bool public reenterSucceeded; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function setLiquidationParams( + uint64 _blockNum, + uint64[] memory _ops, + ISSVNetworkCore.Cluster memory _cluster, + uint32 _effectiveBalance, + bytes32[] memory _proof + ) external { + blockNum = _blockNum; + liquidationOps = _ops; + liquidationCluster = _cluster; + effectiveBalance = _effectiveBalance; + merkleProof = _proof; + } + + function setReentryParams( + uint64[] memory _ops, + uint256 _withdrawAmount, + ISSVNetworkCore.Cluster memory _cluster + ) external { + withdrawOps = _ops; + withdrawAmount = _withdrawAmount; + withdrawCluster = _cluster; + } + + function registerValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + ISSVValidators(ssvNetwork).registerValidator{value: msg.value}(publicKey, operatorIds, sharesData, cluster); + } + + function attack() external { + attemptedReenter = false; + reenterSucceeded = false; + + ISSVClusters(ssvNetwork).updateClusterBalance( + blockNum, + address(this), + liquidationOps, + liquidationCluster, + effectiveBalance, + merkleProof + ); + } + + receive() external payable { + if (attemptedReenter) return; + attemptedReenter = true; + + try ISSVClusters(ssvNetwork).withdraw(withdrawOps, withdrawAmount, withdrawCluster) { + reenterSucceeded = true; + } catch { + reenterSucceeded = false; + } + } +} diff --git a/contracts/test/mocks/MaliciousWithdraw.sol b/contracts/test/mocks/MaliciousWithdraw.sol new file mode 100644 index 000000000..9134e2003 --- /dev/null +++ b/contracts/test/mocks/MaliciousWithdraw.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVClusters} from "../../interfaces/ISSVClusters.sol"; +import {ISSVValidators} from "../../interfaces/ISSVValidators.sol"; +import {ISSVNetworkCore} from "../../interfaces/ISSVNetworkCore.sol"; + +contract MaliciousWithdraw { + address public ssvNetwork; + uint64[] public ops; + uint256 public amount; + ISSVNetworkCore.Cluster public cl; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function setParams(uint64[] memory _ops, ISSVNetworkCore.Cluster memory _cl) external { + ops = _ops; + cl = _cl; + } + + function registerValidator( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + ISSVValidators(ssvNetwork).registerValidator{value: msg.value}(publicKey, operatorIds, sharesData, cluster); + } + + function attack() external { + ISSVClusters(ssvNetwork).withdraw(ops, amount, cl); + } + + receive() external payable { + ISSVClusters(ssvNetwork).withdraw(ops, amount, cl); + } +} \ No newline at end of file diff --git a/contracts/test/mocks/MaliciousWithdrawAllOperatorEarnings.sol b/contracts/test/mocks/MaliciousWithdrawAllOperatorEarnings.sol new file mode 100644 index 000000000..946a4a430 --- /dev/null +++ b/contracts/test/mocks/MaliciousWithdrawAllOperatorEarnings.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVOperators} from "../../interfaces/ISSVOperators.sol"; +import {ISSVOperatorsWhitelist} from "../../interfaces/ISSVOperatorsWhitelist.sol"; + +contract MaliciousWithdrawAllOperatorEarnings { + address public ssvNetwork; + uint64 public opId; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function setParams(uint64 _opId) external { + opId = _opId; + } + + function registerOperator(bytes memory publicKey, uint256 fee, bool setPrivate) external returns (uint64) { + return ISSVOperators(ssvNetwork).registerOperator(publicKey, fee, setPrivate); + } + + function setOperatorsWhitelists(uint64[] memory operators, address[] memory addresses) external { + ISSVOperatorsWhitelist(ssvNetwork).setOperatorsWhitelists(operators, addresses); + } + + function attack() external { + ISSVOperators(ssvNetwork).withdrawAllOperatorEarnings(opId); + } + + receive() external payable { + ISSVOperators(ssvNetwork).withdrawAllOperatorEarnings(opId); + } +} \ No newline at end of file diff --git a/contracts/test/mocks/MaliciousWithdrawAllVersionOperatorEarnings.sol b/contracts/test/mocks/MaliciousWithdrawAllVersionOperatorEarnings.sol new file mode 100644 index 000000000..e55187dd9 --- /dev/null +++ b/contracts/test/mocks/MaliciousWithdrawAllVersionOperatorEarnings.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVOperators} from "../../interfaces/ISSVOperators.sol"; +import {ISSVOperatorsWhitelist} from "../../interfaces/ISSVOperatorsWhitelist.sol"; + +contract MaliciousWithdrawAllVersionOperatorEarnings { + address public ssvNetwork; + uint64 public opId; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function setParams(uint64 _opId) external { + opId = _opId; + } + + function registerOperator(bytes memory publicKey, uint256 fee, bool setPrivate) external returns (uint64) { + return ISSVOperators(ssvNetwork).registerOperator(publicKey, fee, setPrivate); + } + + function setOperatorsWhitelists(uint64[] memory operators, address[] memory addresses) external { + ISSVOperatorsWhitelist(ssvNetwork).setOperatorsWhitelists(operators, addresses); + } + + function attack() external { + ISSVOperators(ssvNetwork).withdrawAllVersionOperatorEarnings(opId); + } + + receive() external payable { + ISSVOperators(ssvNetwork).withdrawAllVersionOperatorEarnings(opId); + } +} \ No newline at end of file diff --git a/contracts/test/mocks/MaliciousWithdrawOperatorEarnings.sol b/contracts/test/mocks/MaliciousWithdrawOperatorEarnings.sol new file mode 100644 index 000000000..a259c2721 --- /dev/null +++ b/contracts/test/mocks/MaliciousWithdrawOperatorEarnings.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ISSVOperators} from "../../interfaces/ISSVOperators.sol"; +import {ISSVOperatorsWhitelist} from "../../interfaces/ISSVOperatorsWhitelist.sol"; + +contract MaliciousWithdrawOperatorEarnings { + address public ssvNetwork; + uint64 public opId; + uint256 public amount; + + constructor(address _ssvNetwork) { + ssvNetwork = _ssvNetwork; + } + + function setParams(uint64 _opId, uint256 _amount) external { + opId = _opId; + amount = _amount; + } + + function registerOperator(bytes memory publicKey, uint256 fee, bool setPrivate) external returns (uint64) { + return ISSVOperators(ssvNetwork).registerOperator(publicKey, fee, setPrivate); + } + + function setOperatorsWhitelists(uint64[] memory operators, address[] memory addresses) external { + ISSVOperatorsWhitelist(ssvNetwork).setOperatorsWhitelists(operators, addresses); + } + + function attack() external payable { + ISSVOperators(ssvNetwork).withdrawOperatorEarnings(opId, amount); + } + + receive() external payable { + ISSVOperators(ssvNetwork).withdrawOperatorEarnings(opId, amount); + } +} \ No newline at end of file diff --git a/contracts/test/mocks/MockCSSV.sol b/contracts/test/mocks/MockCSSV.sol new file mode 100644 index 000000000..ef1da068f --- /dev/null +++ b/contracts/test/mocks/MockCSSV.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +interface ISSVStaking { + function onCSSVTransfer(address from, address to, uint256 amount) external; +} + +contract MockCSSV is ERC20 { + + constructor() ERC20("cSSV", "cSSV") {} + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + super._beforeTokenTransfer(from, to, amount); + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external { + _burn(from, amount); + } +} diff --git a/contracts/test/mocks/MockMultisig.sol b/contracts/test/mocks/MockMultisig.sol new file mode 100644 index 000000000..0fb756ee5 --- /dev/null +++ b/contracts/test/mocks/MockMultisig.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +contract MockMultisig { + function exec(address target, bytes calldata data) external returns (bytes memory) { + (bool success, bytes memory result) = target.call(data); + require(success, "MockMultisig: call failed"); + return result; + } + + receive() external payable {} +} diff --git a/contracts/test/mocks/MockToken.sol b/contracts/test/mocks/MockToken.sol new file mode 100644 index 000000000..377cb57b0 --- /dev/null +++ b/contracts/test/mocks/MockToken.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockToken is ERC20 { + constructor() ERC20("MockToken", "MOCK") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} diff --git a/contracts/test/mocks/NonStandardERC20Mock.sol b/contracts/test/mocks/NonStandardERC20Mock.sol new file mode 100644 index 000000000..a157d10b7 --- /dev/null +++ b/contracts/test/mocks/NonStandardERC20Mock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +contract NonStandardERC20Mock { + string public constant name = "NonStandardToken"; + string public constant symbol = "NST"; + uint8 public constant decimals = 18; + + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + + event Transfer(address indexed from, address indexed to, uint256 value); + + function mint(address to, uint256 amount) external { + totalSupply += amount; + balanceOf[to] += amount; + emit Transfer(address(0), to, amount); + } + + function transfer(address to, uint256 amount) external { + uint256 senderBalance = balanceOf[msg.sender]; + require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); + + unchecked { + balanceOf[msg.sender] = senderBalance - amount; + } + balanceOf[to] += amount; + + emit Transfer(msg.sender, to, amount); + } +} diff --git a/contracts/test/mocks/OperatorEarningsReentrancy.sol b/contracts/test/mocks/OperatorEarningsReentrancy.sol new file mode 100644 index 000000000..0e3d1658c --- /dev/null +++ b/contracts/test/mocks/OperatorEarningsReentrancy.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../interfaces/ISSVOperators.sol"; + +contract OperatorEarningsReentrancy { + ISSVOperators public immutable operators; + + uint64 public operatorId; + uint256 public reenterAmount; + bool public reentered; + bool public reenterSucceeded; + + constructor(address operators_) { + operators = ISSVOperators(operators_); + } + + function registerOperator(bytes calldata publicKey, uint256 fee, bool setPrivate) external returns (uint64 id) { + id = operators.registerOperator(publicKey, fee, setPrivate); + operatorId = id; + } + + function setReenterAmount(uint256 amount) external { + reenterAmount = amount; + } + + function triggerWithdraw(uint256 amount) external { + operators.withdrawOperatorEarnings(operatorId, amount); + } + + receive() external payable { + if (reentered) return; + reentered = true; + + try operators.withdrawOperatorEarnings(operatorId, reenterAmount) { + reenterSucceeded = true; + } catch { + reenterSucceeded = false; + } + } +} diff --git a/contracts/test/mocks/OperatorEarningsReentrancySSV.sol b/contracts/test/mocks/OperatorEarningsReentrancySSV.sol new file mode 100644 index 000000000..2b8907c1d --- /dev/null +++ b/contracts/test/mocks/OperatorEarningsReentrancySSV.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../interfaces/ISSVOperators.sol"; +import "../../interfaces/ISSVNetworkCore.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract OperatorEarningsReentrancySSV { + ISSVOperators public immutable operators; + IERC20 public immutable token; + + uint64 public operatorId; + uint256 public reenterAmount; + bool public reentered; + bool public reenterSucceeded; + + constructor(address operators_, address token_) { + operators = ISSVOperators(operators_); + token = IERC20(token_); + } + + function registerOperator(bytes calldata publicKey, uint256 fee, bool setPrivate) external returns (uint64 id) { + id = operators.registerOperator(publicKey, fee, setPrivate); + operatorId = id; + } + + function setReenterAmount(uint256 amount) external { + reenterAmount = amount; + } + + function triggerWithdraw(uint256 amount) external { + operators.withdrawOperatorEarningsSSV(operatorId, amount); + } + + // Callback for ReentrantTokenMock + function onTransferReceived(address, uint256) external returns (bool) { + if (reentered) return true; + reentered = true; + + try operators.withdrawOperatorEarningsSSV(operatorId, reenterAmount) { + reenterSucceeded = true; + } catch { + reenterSucceeded = false; + } + return true; + } +} diff --git a/contracts/test/mocks/ReentrantTokenMock.sol b/contracts/test/mocks/ReentrantTokenMock.sol new file mode 100644 index 000000000..46a1039a4 --- /dev/null +++ b/contracts/test/mocks/ReentrantTokenMock.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +interface ITokenCallback { + function onTransferReceived(address from, uint256 amount) external returns (bool); +} + +contract ReentrantTokenMock is ERC20 { + constructor() ERC20("ReentrantToken", "RTK") {} + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + function transfer(address to, uint256 amount) public override returns (bool) { + bool success = super.transfer(to, amount); + if (success && to.code.length > 0) { + try ITokenCallback(to).onTransferReceived(msg.sender, amount) {} catch {} + } + return success; + } +} diff --git a/contracts/test/modules/SSVOperatorsUpdate.sol b/contracts/test/modules/SSVOperatorsUpdate.sol deleted file mode 100644 index d8f3d99fb..000000000 --- a/contracts/test/modules/SSVOperatorsUpdate.sol +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.24; - -import "../../interfaces/ISSVOperators.sol"; -import "../../libraries/Types.sol"; -import "../../libraries/SSVStorage.sol"; -import "../../libraries/SSVStorageProtocol.sol"; -import "../../libraries/OperatorLib.sol"; -import "../../libraries/CoreLib.sol"; - -import "@openzeppelin/contracts/utils/Counters.sol"; - -contract SSVOperatorsUpdate is ISSVOperators { - uint64 private constant MINIMAL_OPERATOR_FEE = 100_000_000; - uint64 private constant PRECISION_FACTOR = 10_000; - - using Types256 for uint256; - using Types64 for uint64; - using Counters for Counters.Counter; - using OperatorLib for Operator; - - /*******************************/ - /* Operator External Functions */ - /*******************************/ - - function registerOperator( - bytes calldata publicKey, - uint256 fee, - bool setPrivate - ) external override returns (uint64 id) { - if (fee != 0 && fee < MINIMAL_OPERATOR_FEE) { - revert ISSVNetworkCore.FeeTooLow(); - } - StorageData storage s = SSVStorage.load(); - - bytes32 hashedPk = keccak256(publicKey); - if (s.operatorsPKs[hashedPk] != 0) revert ISSVNetworkCore.OperatorAlreadyExists(); - - s.lastOperatorId.increment(); - id = uint64(s.lastOperatorId.current()); - s.operators[id] = Operator({ - owner: msg.sender, - snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: 0}), - validatorCount: 0, - fee: fee.shrink(), - whitelisted: setPrivate - }); - s.operatorsPKs[hashedPk] = id; - - uint64[] memory operatorIds = new uint64[](1); - operatorIds[0] = id; - - emit OperatorAdded(id, msg.sender, publicKey, fee); - emit OperatorPrivacyStatusUpdated(operatorIds, setPrivate); - } - - function removeOperator(uint64 operatorId) external override { - StorageData storage s = SSVStorage.load(); - Operator memory operator = s.operators[operatorId]; - operator.checkOwner(); - - operator.updateSnapshot(); - uint64 currentBalance = operator.snapshot.balance; - - operator.snapshot.block = 0; - operator.snapshot.balance = 0; - operator.validatorCount = 0; - operator.fee = 0; - - s.operators[operatorId] = operator; - - if (s.operatorsWhitelist[operatorId] != address(0)) { - delete s.operatorsWhitelist[operatorId]; - } - - if (currentBalance > 0) { - _transferOperatorBalanceUnsafe(operatorId, currentBalance.expand()); - } - emit OperatorRemoved(operatorId); - } - - function declareOperatorFee(uint64 operatorId, uint256 fee) external override {} - - function executeOperatorFee(uint64 operatorId) external override { - StorageData storage s = SSVStorage.load(); - Operator memory operator = s.operators[operatorId]; - operator.checkOwner(); - - OperatorFeeChangeRequest memory feeChangeRequest = s.operatorFeeChangeRequests[operatorId]; - - if (feeChangeRequest.approvalBeginTime == 0) revert NoFeeDeclared(); - - if ( - block.timestamp < feeChangeRequest.approvalBeginTime || block.timestamp > feeChangeRequest.approvalEndTime - ) { - revert ApprovalNotWithinTimeframe(); - } - - operator.updateSnapshot(); - operator.fee = feeChangeRequest.fee; - s.operators[operatorId] = operator; - - delete s.operatorFeeChangeRequests[operatorId]; - - emit OperatorFeeExecuted(msg.sender, operatorId, block.number, feeChangeRequest.fee.expand()); - } - - function cancelDeclaredOperatorFee(uint64 operatorId) external override { - SSVStorage.load().operators[operatorId].checkOwner(); - - if (SSVStorage.load().operatorFeeChangeRequests[operatorId].approvalBeginTime == 0) revert NoFeeDeclared(); - - delete SSVStorage.load().operatorFeeChangeRequests[operatorId]; - - emit OperatorFeeDeclarationCancelled(msg.sender, operatorId); - } - - function reduceOperatorFee(uint64 operatorId, uint256 fee) external override { - StorageData storage s = SSVStorage.load(); - Operator memory operator = s.operators[operatorId]; - operator.checkOwner(); - - uint64 shrunkAmount = fee.shrink(); - if (shrunkAmount >= operator.fee) revert FeeIncreaseNotAllowed(); - - operator.updateSnapshot(); - operator.fee = shrunkAmount; - s.operators[operatorId] = operator; - - if (s.operatorFeeChangeRequests[operatorId].approvalBeginTime != 0) - delete s.operatorFeeChangeRequests[operatorId]; - emit OperatorFeeExecuted(msg.sender, operatorId, block.number, fee); - } - - function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external override { - OperatorLib.updatePrivacyStatus(operatorIds, true, SSVStorage.load()); - emit OperatorPrivacyStatusUpdated(operatorIds, true); - } - - function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external override { - OperatorLib.updatePrivacyStatus(operatorIds, false, SSVStorage.load()); - emit OperatorPrivacyStatusUpdated(operatorIds, false); - } - - function withdrawOperatorEarnings(uint64 operatorId, uint256 amount) external override { - _withdrawOperatorEarnings(operatorId, amount); - } - - function withdrawAllOperatorEarnings(uint64 operatorId) external override { - _withdrawOperatorEarnings(operatorId, 0); - } - - // private functions - function _withdrawOperatorEarnings(uint64 operatorId, uint256 amount) private { - StorageData storage s = SSVStorage.load(); - Operator memory operator = s.operators[operatorId]; - operator.checkOwner(); - - operator.updateSnapshot(); - - uint64 shrunkWithdrawn; - uint64 shrunkAmount = amount.shrink(); - - if (amount == 0 && operator.snapshot.balance > 0) { - shrunkWithdrawn = operator.snapshot.balance; - } else if (amount > 0 && operator.snapshot.balance >= shrunkAmount) { - shrunkWithdrawn = shrunkAmount; - } else { - revert InsufficientBalance(); - } - - operator.snapshot.balance -= shrunkWithdrawn; - - s.operators[operatorId] = operator; - - _transferOperatorBalanceUnsafe(operatorId, shrunkWithdrawn.expand()); - } - - function _transferOperatorBalanceUnsafe(uint64 operatorId, uint256 amount) private { - CoreLib.transferBalance(msg.sender, amount); - emit OperatorWithdrawn(msg.sender, operatorId, amount); - } -} diff --git a/contracts/token/CSSVToken.sol b/contracts/token/CSSVToken.sol new file mode 100644 index 000000000..aca4db316 --- /dev/null +++ b/contracts/token/CSSVToken.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +interface ISSVStaking { + function onCSSVTransfer(address from, address to, uint256 amount) external; +} + +contract CSSVToken is ERC20 { + error NotSSVStaking(); + error ZeroAddress(); + + address public immutable ssvStaking; + + modifier onlySSVStaking() { + if (msg.sender != ssvStaking) revert NotSSVStaking(); + _; + } + + constructor(address ssvStaking_) ERC20("cSSV", "cSSV") { + if (ssvStaking_ == address(0)) revert ZeroAddress(); + ssvStaking = ssvStaking_; + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + if (from != to && from != address(0) && to != address(0) && msg.sender != ssvStaking && amount > 0) { + ISSVStaking(ssvStaking).onCSSVTransfer(from, to, amount); + } + super._beforeTokenTransfer(from, to, amount); + } + + function mint(address to, uint256 amount) external onlySSVStaking { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlySSVStaking { + _burn(from, amount); + } +} diff --git a/contracts/upgrades/mainnet/SSVNetworkSSVStakingUpgrade.sol b/contracts/upgrades/mainnet/SSVNetworkSSVStakingUpgrade.sol new file mode 100644 index 000000000..eb18eea30 --- /dev/null +++ b/contracts/upgrades/mainnet/SSVNetworkSSVStakingUpgrade.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../SSVNetwork.sol"; +import {MAX_DELEGATION_SLOTS} from "../../libraries/storage/SSVStorageStaking.sol"; + +contract SSVNetworkSSVStakingUpgrade is SSVNetwork { + /// @notice One-time initializer for the SSV Staking upgrade + /// @param cooldownDuration Unstake cooldown duration in seconds (e.g. 604800 for 7 days) + /// @param defaultOracleIds Default oracle IDs for new delegations + /// @param quorumBps Oracle quorum in basis points + function initializeSSVStaking( + uint64 cooldownDuration, + uint32[MAX_DELEGATION_SLOTS] memory defaultOracleIds, + uint16 quorumBps + ) external onlyOwner reinitializer(3) { + if (quorumBps == 0 || quorumBps > 10_000) revert InvalidQuorum(); + + // save staking storage updates + StorageStaking storage s = SSVStorageStaking.load(); + s.cooldownDuration = cooldownDuration; + s.defaultOracleIds = defaultOracleIds; + s.quorumBps = quorumBps; + + emit CooldownDurationUpdated(cooldownDuration); + emit QuorumUpdated(quorumBps); + emit SSVNetworkUpgradeBlock("v2.0.0", block.number); + } +} diff --git a/contracts/upgrades/stage/goerli/SSVNetworkValidatorsPerOperator.sol b/contracts/upgrades/stage/goerli/SSVNetworkValidatorsPerOperator.sol deleted file mode 100644 index c05a4a148..000000000 --- a/contracts/upgrades/stage/goerli/SSVNetworkValidatorsPerOperator.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.24; - -import "../../../SSVNetwork.sol"; - -contract SSVNetworkValidatorsPerOperator is SSVNetwork { - function initializev2(uint32 validatorsPerOperatorLimit_) external reinitializer(_getInitializedVersion() + 1) { - SSVStorageProtocol.load().validatorsPerOperatorLimit = validatorsPerOperatorLimit_; - } -} diff --git a/contracts/upgrades/stage/holesky/SSVNetworkUpgradeValidatorsPerOperator.sol b/contracts/upgrades/stage/holesky/SSVNetworkUpgradeValidatorsPerOperator.sol deleted file mode 100644 index 7133b5c53..000000000 --- a/contracts/upgrades/stage/holesky/SSVNetworkUpgradeValidatorsPerOperator.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.24; - -import "../../../SSVNetwork.sol"; - -contract SSVNetworkUpgradeValidatorsPerOperator is SSVNetwork { - function initializev2(uint32 validatorsPerOperatorLimit_) external reinitializer(_getInitializedVersion() + 1) { - SSVStorageProtocol.load().validatorsPerOperatorLimit = validatorsPerOperatorLimit_; - } -} diff --git a/deployments/README.md b/deployments/README.md new file mode 100644 index 000000000..02965b24d --- /dev/null +++ b/deployments/README.md @@ -0,0 +1,122 @@ +# Deployments + +Per-environment deployment configs and results for SSV Network. + +## Environments + +| Env | Network | Owner | Purpose | +|---|---|---|---| +| `mainnet` | Ethereum L1 | SAFE multi-sig | Production | +| `hoodi-prod` | Hoodi | Dev team | Stable testnet, mirrors mainnet flow | +| `hoodi-stage` | Hoodi | Dev team | Experimental / staging | +| `local` | Hardhat/Anvil | Dev team | Local dev and testing | + +Each env directory contains: +``` +/ + config.json # Input — edit this + deploy-result.json # Symlink → deploy-result..json (latest) + deploy-result.v2.0.0.json # Versioned output of `just deploy` + upgrade-result.json # Symlink → upgrade-result..json (latest) + upgrade-result.v2.0.0.json # Versioned output of `just upgrade` + deployment-attestation.json # Bytecode hashes + config for committee review + multisig-batch.json # SAFE Transaction Builder JSON (mainnet) +``` + +Result files are versioned by the contract's `getVersion()` string. The fixed-name symlinks always point to the latest. Old versioned files are preserved for traceability. + +## Quick Start + +```bash +# Fork + test before any live upgrade +anvil --fork-url "$HOODI_RPC_URL" --port 8545 +just upgrade-test-fork hoodi-stage # upgrade on fork, then run tests + +# Live upgrade (hoodi-stage / hoodi-prod) +just upgrade hoodi-stage + +# Mainnet: deploy impls first, generate attestation, then SAFE batch +just deploy mainnet +just generate-attestation mainnet # -> mainnet/deployment-attestation.json +just generate-safe-batch mainnet # -> mainnet/multisig-batch.json +# Import into SAFE Transaction Builder, review, sign + +# Post-upgrade verification only +just verify-upgrade mainnet + +# Fresh local deployment +just deploy-fresh local +``` + +## config.json + +Copy `template-config.json` as a starting point. + +| Field | Required | Description | +|---|---|---| +| `currentVersion` | **Required** | Version the proxy currently reports on-chain. Upgrade aborts if it doesn't match. | +| `targetVersion` | **Required** | Version the new implementation will report after upgrade. Used as the result file suffix (e.g. `upgrade-result.v2.0.1.json`). Can equal `currentVersion` for hotfixes. | +| `ssvNetworkProxy` | Required | SSVNetwork proxy address | +| `ssvNetworkViews` | Required | SSVNetworkViews proxy address | +| `ssvToken` | Required | SSV ERC-20 token address | +| `owner` | Optional | Defaults to on-chain `owner()` | +| `cooldownDuration` | Optional | Unstake cooldown in seconds (default: `604800` = 7 days) | +| `upgradeTimestamp` | Optional | `SSVOperators` constructor arg (default: `0`) | +| `quorumBps` | Optional | Oracle quorum in basis points (e.g. `7500` = 75%) | +| `defaultOracleIds` | Optional | Array of 4 oracle IDs (default: `[1,2,3,4]`) | +| `skipInitializer` | Optional | Set `true` for patch upgrades where `initializeSSVStaking` was already run. Uses `upgradeTo` instead of `upgradeToAndCall`. Default: `false`. | +| `cssvToken` | Optional | Reuse existing CSSVToken address; deploys new one if omitted | +| `oracles` | Optional | Oracle ID → address map | +| `protocolParams` | Optional | Governance parameters (see below) | + +### protocolParams + +```json +"protocolParams": { + "networkFeeEth": "3550900000", + "maxOperatorEthFee": "5326300000", + "minOperatorEthFee": "1065200000", + "minimumLiquidationCollateralEth": "940000000000000", + "liquidationThresholdPeriod": "35800", + "operatorFeeIncreaseLimit": "1000", + "declareOperatorFeePeriod": "604800", + "executeOperatorFeePeriod": "604800" +} +``` + +All values are strings or numbers (wei / blocks). **Omit any field to leave the on-chain value unchanged.** + +### Version pre-flight + +`upgrade.ts` reads `getVersion()` from the proxy before doing anything. If it doesn't match `config.currentVersion`, the script aborts: + +``` +Error: Version mismatch: config.currentVersion is "v2.0.0" but proxy reports "v1.9.0". +Wrong config or proxy address? +``` + +This prevents running the wrong config against the wrong proxy. + +## Scripts + +| Script | `just` recipe | Purpose | +|---|---|---| +| `deploy.ts` | `just deploy ` | Deploy impls + modules (no proxy upgrade) | +| `upgrade.ts` | `just upgrade ` | Upgrade proxy + attach modules + apply params | +| `upgrade.ts --fork` | `just upgrade-fork ` | Same, on local Anvil fork | +| `verify-post-upgrade-config.ts` | `just verify-upgrade ` or `just verify-post-upgrade-config ` | Read on-chain state, no writes | +| `generate-deployment-attestation.ts` | `just generate-attestation ` | Bytecode hashes + config attestation for committee | +| `generate-safe-batch.ts` | `just generate-safe-batch ` | Encode SAFE multisig batch | +| `deploy-fresh.ts` | `just deploy-fresh ` | Full greenfield deployment | +| `attach-module.ts` | `just attach-module ` | Attach one module to the env-configured proxy and update the latest deploy-result | +| `run-forked-tests.ts` | `just test-fork ` | Integration tests against fork | + +## Troubleshooting + +**Version mismatch** — Check `config.version` matches the current on-chain `getVersion()`. Update the field if you're upgrading to a new version. + +**Owner mismatch** — Set `HOODI_PRIVATE_KEY` (or `MAINNET_PRIVATE_KEY`) in `.env` to the owner key, or use `--fork` to impersonate. + +**No contract code** — Anvil must be running on `127.0.0.1:8545` and forked from the correct network. + +**Stale result JSON** — Re-run `just upgrade-fork ` then `just test-fork `. diff --git a/deployments/hoodi-prod/config.json b/deployments/hoodi-prod/config.json new file mode 100644 index 000000000..0384682ab --- /dev/null +++ b/deployments/hoodi-prod/config.json @@ -0,0 +1,31 @@ +{ + "currentVersion": "v2.0.0", + "targetVersion": "v2.0.0", + "skipInitializer": true, + "owner": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "ssvNetworkProxy": "0x58410Bef803ECd7E63B23664C586A6DB72DAf59c", + "ssvNetworkViews": "0x5AdDb3f1529C5ec70D77400499eE4bbF328368fe", + "ssvToken": "0x9F5d4Ec84fC4785788aB44F9de973cF34F7A038e", + "cssvToken": "0x6e1a5d27361c666f681af06535c8Ac773E571d4d", + "cooldownDuration": 604800, + "upgradeTimestamp": 1771497638, + "quorumBps": 7500, + "defaultOracleIds": [1, 2, 3, 4], + "protocolParams": { + "networkFeeEth": "3550900000", + "maxOperatorEthFee": "5326300000", + "minOperatorEthFee": "1065200000", + "minimumLiquidationCollateralEth": "940000000000000", + "liquidationThresholdPeriod": "35800", + "minBlocksBetweenUpdates": "0", + "operatorFeeIncreaseLimit": "1000", + "declareOperatorFeePeriod": "604800", + "executeOperatorFeePeriod": "604800" + }, + "oracles": { + "1": "0x6b6fa15717beeb5a40fac6610c3e92776037a30e", + "2": "0x01E7e108eD97B4EA08e1a184aBA793b9D282E565", + "3": "0xef6d2263a1d96eac2a4530ba327bcc2e6c948feb", + "4": "0xFe33f6cb66ee2A85748458556D6ccEC3716D2173" + } +} diff --git a/deployments/hoodi-prod/deploy-result.json b/deployments/hoodi-prod/deploy-result.json new file mode 120000 index 000000000..03fd85e25 --- /dev/null +++ b/deployments/hoodi-prod/deploy-result.json @@ -0,0 +1 @@ +deploy-result.v2.0.0.json \ No newline at end of file diff --git a/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade1.json b/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade1.json new file mode 100644 index 000000000..dba60b200 --- /dev/null +++ b/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade1.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-02-19T00:07:50.289Z", + "blockNumber": 2262983, + "ssvToken": "0x9F5d4Ec84fC4785788aB44F9de973cF34F7A038e", + "ssvNetworkProxy": "0x58410Bef803ECd7E63B23664C586A6DB72DAf59c", + "ssvNetworkViews": "0x5AdDb3f1529C5ec70D77400499eE4bbF328368fe", + "cssvToken": "0x6e1a5d27361c666f681af06535c8Ac773E571d4d", + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0xc51b63d68188936d71EF82b3794d6157bc351B89", + "SSVNetworkViews": "0xdf0355E29F9288ae922cC863977A9aE3cE94B6a1" + }, + "modules": { + "SSVOperators": "0x2E3F725C5B3954978FA5fc6f05195B32f2Cc6657", + "SSVClusters": "0xddA2b40DE56b1a3e7F2868580A431b8044d89b20", + "SSVDAO": "0xa47Be8062aCbB3Bc816bf7e186d3cE11537F1A66", + "SSVViews": "0xcF4074E0cfF1F41aa49117b4E3447AD8356bc199", + "SSVOperatorsWhitelist": "0xfb71359DA4b2268cB2950740abD994407E241483", + "SSVStaking": "0x871d5A127C7FAA5070E36A364BCED3E5728d5614", + "SSVValidators": "0x579cA021C1C9DAc52Fd61D36E7730CeA5c8ddd5b" + } +} diff --git a/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade2.json b/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade2.json new file mode 100644 index 000000000..7778b73fb --- /dev/null +++ b/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade2.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-17T12:18:01.926Z", + "blockNumber": 2434756, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x80Be9D66831Ab6a40f16c10f78a81e6CDfB15057", + "SSVNetworkViews": "0xf70E9396a801BabDfe972D74ECce9716f164335b" + }, + "cssvToken": { + "address": "0x6e1a5d27361c666f681af06535c8Ac773E571d4d", + "deployed": false + }, + "modules": { + "SSVOperators": "0x9Be8F85b316A5E5AF4a30Ac30a364A2BD781554B", + "SSVClusters": "0x0A37b36c6a9602Fe56c77EebFa6DF3878Fa249ff", + "SSVDAO": "0xf0d4336AF410E0d3F8a96b760AC25aBCDBe3f3FC", + "SSVViews": "0xAbc3b496Cf75b2eF09c143C7aE5fB7D9E33AB378", + "SSVOperatorsWhitelist": "0x0a1d8027288ddfaC07e306f7d16C99c87855DB45", + "SSVStaking": "0x23E23C65E8Ce7D1B913cAe197506A419E4944f29", + "SSVValidators": "0xD82C35e8C647CB467b968E5854d37800190e8199" + } +} diff --git a/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade3.json b/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade3.json new file mode 100644 index 000000000..6f2959bfc --- /dev/null +++ b/deployments/hoodi-prod/deploy-result.v2.0.0-upgrade3.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-17T12:18:01.926Z", + "blockNumber": 2434756, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x80Be9D66831Ab6a40f16c10f78a81e6CDfB15057", + "SSVNetworkViews": "0xf70E9396a801BabDfe972D74ECce9716f164335b" + }, + "cssvToken": { + "address": "0x6e1a5d27361c666f681af06535c8Ac773E571d4d", + "deployed": false + }, + "modules": { + "SSVOperators": "0x3437257DcC8c6C0697c5f204DD0F93DcdFfD6A4d", + "SSVClusters": "0x983eaE89444e543A8CBE439379cB5C5c0e04666A", + "SSVDAO": "0xb1885a27A0E44e6baF9298390cd798133B8D8961", + "SSVViews": "0xb7EbBd80Bf2f2722FE893dEFcF228CfC235c7421", + "SSVOperatorsWhitelist": "0x28A41FC0E7Fcbb7B26590321E2bC91cBe2f2d47d", + "SSVStaking": "0xc72C04fE3B813d37Fb0adc7A8A48710315c876B8", + "SSVValidators": "0x432dAe087e4Bb4eB4835C2d84b3c66C7b64E6e45" + } +} \ No newline at end of file diff --git a/deployments/hoodi-prod/deploy-result.v2.0.0.json b/deployments/hoodi-prod/deploy-result.v2.0.0.json new file mode 100644 index 000000000..ec8194c4e --- /dev/null +++ b/deployments/hoodi-prod/deploy-result.v2.0.0.json @@ -0,0 +1,25 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-17T12:18:01.926Z", + "blockNumber": 2434756, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x80Be9D66831Ab6a40f16c10f78a81e6CDfB15057", + "SSVNetworkViews": "0xf70E9396a801BabDfe972D74ECce9716f164335b" + }, + "cssvToken": { + "address": "0x6e1a5d27361c666f681af06535c8Ac773E571d4d", + "deployed": false + }, + "modules": { + "SSVOperators": "0x6D4AeD4a18fAb66733EE3C52Eeb795b1ef660a85", + "SSVClusters": "0xD2E740424D339F36a6912e53684d901db2dfD236", + "SSVDAO": "0xf3929A559Aa14C75F41Bc59BfeB5BdCEd0e2ea1D", + "SSVViews": "0xc3d2F9eBa309a666C8c3Fd8580365366530FC1AF", + "SSVOperatorsWhitelist": "0xCd273a679f617144f290B5DBe81575E221965060", + "SSVStaking": "0x6C9aae90F5AFF6d282cE529aEFe258EaF7bd4d70", + "SSVValidators": "0x023Af0382D4243637F4b5264bebA97Ab81AE27A6" + }, + "updatedAt": "2026-04-07T12:17:51.362Z" +} diff --git a/deployments/hoodi-prod/upgrade-result.json b/deployments/hoodi-prod/upgrade-result.json new file mode 120000 index 000000000..c1578f083 --- /dev/null +++ b/deployments/hoodi-prod/upgrade-result.json @@ -0,0 +1 @@ +upgrade-result.v2.0.0.json \ No newline at end of file diff --git a/deployments/hoodi-prod/upgrade-result.v2.0.0.json b/deployments/hoodi-prod/upgrade-result.v2.0.0.json new file mode 100644 index 000000000..b485b951a --- /dev/null +++ b/deployments/hoodi-prod/upgrade-result.v2.0.0.json @@ -0,0 +1,67 @@ +{ + "currentVersion": "v2.0.0", + "targetVersion": "v2.0.0", + "skipInitializer": true, + "owner": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "ssvNetworkProxy": "0x58410Bef803ECd7E63B23664C586A6DB72DAf59c", + "ssvNetworkViews": "0x5AdDb3f1529C5ec70D77400499eE4bbF328368fe", + "ssvToken": "0x9F5d4Ec84fC4785788aB44F9de973cF34F7A038e", + "cooldownDuration": 604800, + "upgradeTimestamp": 2219200, + "quorumBps": 7500, + "defaultOracleIds": [ + 1, + 2, + 3, + 4 + ], + "protocolParams": { + "networkFeeEth": "3550900000", + "maxOperatorEthFee": "5326300000", + "minOperatorEthFee": "1065200000", + "minimumLiquidationCollateralEth": "940000000000000", + "liquidationThresholdPeriod": "35800", + "operatorFeeIncreaseLimit": "1000", + "declareOperatorFeePeriod": "604800", + "executeOperatorFeePeriod": "604800", + "networkFeeSSV": "382640000000", + "minimumLiquidationCollateralSSV": "1000000000000000000", + "validatorsPerOperatorLimit": "3000", + "unstakeCooldownDuration": "604800" + }, + "oracles": { + "1": "0x6b6fa15717beeb5a40fac6610c3e92776037a30e", + "2": "0x01E7e108eD97B4EA08e1a184aBA793b9D282E565", + "3": "0xef6d2263a1d96eac2a4530ba327bcc2e6c948feb", + "4": "0xFe33f6cb66ee2A85748458556D6ccEC3716D2173" + }, + "cssvToken": "0x6e1a5d27361c666f681af06535c8Ac773E571d4d", + "deployBlockNumber": 2266091, + "modules": { + "SSVOperators": "0x2E3F725C5B3954978FA5fc6f05195B32f2Cc6657", + "SSVClusters": "0xddA2b40DE56b1a3e7F2868580A431b8044d89b20", + "SSVDAO": "0xa47Be8062aCbB3Bc816bf7e186d3cE11537F1A66", + "SSVViews": "0xcF4074E0cfF1F41aa49117b4E3447AD8356bc199", + "SSVOperatorsWhitelist": "0xfb71359DA4b2268cB2950740abD994407E241483", + "SSVStaking": "0x871d5A127C7FAA5070E36A364BCED3E5728d5614", + "SSVValidators": "0x579cA021C1C9DAc52Fd61D36E7730CeA5c8ddd5b" + }, + "deployments": { + "ssvNetworkStakingUpgradeImplementation": "0x0a18da6EDAF94b146e8C305425b242d232B929d2", + "ssvNetworkViewsImplementation": "0x6200cFa8b7EdcD5289AbA464e602AFDCe6c4385c", + "cssvToken": "0x6e1a5d27361c666f681af06535c8Ac773E571d4d", + "modules": { + "SSVOperators": "0x2E3F725C5B3954978FA5fc6f05195B32f2Cc6657", + "SSVClusters": "0xddA2b40DE56b1a3e7F2868580A431b8044d89b20", + "SSVDAO": "0xa47Be8062aCbB3Bc816bf7e186d3cE11537F1A66", + "SSVViews": "0xcF4074E0cfF1F41aa49117b4E3447AD8356bc199", + "SSVOperatorsWhitelist": "0xfb71359DA4b2268cB2950740abD994407E241483", + "SSVStaking": "0x871d5A127C7FAA5070E36A364BCED3E5728d5614", + "SSVValidators": "0x579cA021C1C9DAc52Fd61D36E7730CeA5c8ddd5b" + }, + "targetNetwork": "local", + "deployBlockNumber": 2266091, + "chainId": "560048", + "updatedAt": "2026-02-19T11:33:48.006Z" + } +} diff --git a/deployments/hoodi-stage/config.json b/deployments/hoodi-stage/config.json new file mode 100644 index 000000000..98c474e32 --- /dev/null +++ b/deployments/hoodi-stage/config.json @@ -0,0 +1,31 @@ +{ + "currentVersion": "v2.0.0", + "targetVersion": "v2.0.0", + "skipInitializer": true, + "owner": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "ssvNetworkProxy": "0xc07B3E9671f884FDa67E1e7D43d952E0e1369fd8", + "ssvNetworkViews": "0x3234e84b7d1eE1AF8b586E26814d4e268336D142", + "ssvToken": "0x746C33ccC28b1363c35c09baDAF41b2FFa7E6D56", + "cssvToken": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "cooldownDuration": 604800, + "upgradeTimestamp": 1773141056, + "quorumBps": 7500, + "defaultOracleIds": [1, 2, 3, 4], + "protocolParams": { + "networkFeeEth": "3550900000", + "maxOperatorEthFee": "5326300000", + "minOperatorEthFee": "1065200000", + "minimumLiquidationCollateralEth": "940000000000000", + "liquidationThresholdPeriod": "35800", + "minBlocksBetweenUpdates": "0", + "operatorFeeIncreaseLimit": "1000", + "declareOperatorFeePeriod": "604800", + "executeOperatorFeePeriod": "604800" + }, + "oracles": { + "1": "0x011684890d10eCC4473F2A4A8B0F2cb7F19C66EB", + "2": "0x0CBA9a4c9B2Ea64827C101946E8979c1023fE15C", + "3": "0xaBE4A01beDF8f0B5984648f2fE64a7A120Dab800", + "4": "0xa70d0d0BE1F02cF646D5CDB6716a509A937f3EE5" + } +} diff --git a/deployments/hoodi-stage/deploy-result.json b/deployments/hoodi-stage/deploy-result.json new file mode 120000 index 000000000..03fd85e25 --- /dev/null +++ b/deployments/hoodi-stage/deploy-result.json @@ -0,0 +1 @@ +deploy-result.v2.0.0.json \ No newline at end of file diff --git a/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade3.json b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade3.json new file mode 100644 index 000000000..a3b36b337 --- /dev/null +++ b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade3.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-13T13:23:14.623Z", + "blockNumber": 2409767, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x8f5b078D4E10E7DAd0d22A2f7d18f20b8ee84D90", + "SSVNetworkViews": "0xEf73C138914AaBd9894F111544Af1D96a1b6Bf10" + }, + "cssvToken": { + "address": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "deployed": false + }, + "modules": { + "SSVOperators": "0x61B51E81f293C0cec6347eb414A212aeF988CF33", + "SSVClusters": "0xE6f2E7C4ebf4439B78F3837607bECD2eA9dC194c", + "SSVDAO": "0x78c93937C2100675d138fd973571c648EE3714df", + "SSVViews": "0x41e076FaA5bb4Dab542Ba67Acd4aDDb9b395C962", + "SSVOperatorsWhitelist": "0xD563c66d5B42d1e002f786fF7e87233E8e60aB71", + "SSVStaking": "0xFb86248d70279Ecb433A9Ddc772768aB5f88eDD8", + "SSVValidators": "0x16cDE4542Fd02dD5310Ecdb3f4576E8388Ed0CdD" + } +} diff --git a/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade4.json b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade4.json new file mode 100644 index 000000000..67361427c --- /dev/null +++ b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade4.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-17T10:15:01.803Z", + "blockNumber": 1773756095, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x6749B3Ade59FbA80f4e5c4066A993181843E1c00", + "SSVNetworkViews": "0x4c6f92E6d13748a83E96dF2AA5D3839a9Ed2b607" + }, + "cssvToken": { + "address": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "deployed": false + }, + "modules": { + "SSVOperators": "0xD239f43d942F30F415F6e47fDE0603cdC60Ee7C8", + "SSVClusters": "0x17A9af72A7583C3f72f047d16430cA3E225981f0", + "SSVDAO": "0x824e169e138B3B91977e040F80547Fce3779db97", + "SSVViews": "0xae12ceCA94a14aAC6c15A473FAAD9Cb75B2be53A", + "SSVOperatorsWhitelist": "0x5734A479F463e3DddDBB8fFC2C82253d41777BbC", + "SSVStaking": "0x99f2B313BD913d6E11E91BA7B3c5B51c4E486bE5", + "SSVValidators": "0xc6E3Bb5984C7d8a90C3EB8624DdF135eAcF49142" + } +} diff --git a/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade5.json b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade5.json new file mode 100644 index 000000000..c45cc7688 --- /dev/null +++ b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade5.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-17T10:15:01.803Z", + "blockNumber": 1773756095, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x6749B3Ade59FbA80f4e5c4066A993181843E1c00", + "SSVNetworkViews": "0x4c6f92E6d13748a83E96dF2AA5D3839a9Ed2b607" + }, + "cssvToken": { + "address": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "deployed": false + }, + "modules": { + "SSVOperators": "0xD239f43d942F30F415F6e47fDE0603cdC60Ee7C8", + "SSVClusters": "0xc25333db38d687a70B136286273A04815cDAe5A6", + "SSVDAO": "0x824e169e138B3B91977e040F80547Fce3779db97", + "SSVViews": "0xae12ceCA94a14aAC6c15A473FAAD9Cb75B2be53A", + "SSVOperatorsWhitelist": "0x5734A479F463e3DddDBB8fFC2C82253d41777BbC", + "SSVStaking": "0x99f2B313BD913d6E11E91BA7B3c5B51c4E486bE5", + "SSVValidators": "0x57aE505496ED7BF377A28bb645d4242CC5c74330" + } +} diff --git a/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade6.json b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade6.json new file mode 100644 index 000000000..591ec934a --- /dev/null +++ b/deployments/hoodi-stage/deploy-result.v2.0.0-upgrade6.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-17T10:15:01.803Z", + "blockNumber": 1773756095, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x6749B3Ade59FbA80f4e5c4066A993181843E1c00", + "SSVNetworkViews": "0x4c6f92E6d13748a83E96dF2AA5D3839a9Ed2b607" + }, + "cssvToken": { + "address": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "deployed": false + }, + "modules": { + "SSVOperators": "0x4a4972222E277794E697759Da629B9dB21b19246", + "SSVClusters": "0xc25333db38d687a70B136286273A04815cDAe5A6", + "SSVDAO": "0x824e169e138B3B91977e040F80547Fce3779db97", + "SSVViews": "0xae12ceCA94a14aAC6c15A473FAAD9Cb75B2be53A", + "SSVOperatorsWhitelist": "0x5734A479F463e3DddDBB8fFC2C82253d41777BbC", + "SSVStaking": "0x99f2B313BD913d6E11E91BA7B3c5B51c4E486bE5", + "SSVValidators": "0x57aE505496ED7BF377A28bb645d4242CC5c74330" + } +} diff --git a/deployments/hoodi-stage/deploy-result.v2.0.0.json b/deployments/hoodi-stage/deploy-result.v2.0.0.json new file mode 100644 index 000000000..128e5a00d --- /dev/null +++ b/deployments/hoodi-stage/deploy-result.v2.0.0.json @@ -0,0 +1,24 @@ +{ + "deployer": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "chainId": "560048", + "network": "hoodi", + "deployedAt": "2026-03-17T10:15:01.803Z", + "blockNumber": 1773756095, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0x6749B3Ade59FbA80f4e5c4066A993181843E1c00", + "SSVNetworkViews": "0x4c6f92E6d13748a83E96dF2AA5D3839a9Ed2b607" + }, + "cssvToken": { + "address": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "deployed": false + }, + "modules": { + "SSVOperators": "0x4a4972222E277794E697759Da629B9dB21b19246", + "SSVClusters": "0x5add198304F3d2596c253edF2A260e8e66c2042c", + "SSVDAO": "0x824e169e138B3B91977e040F80547Fce3779db97", + "SSVViews": "0xae12ceCA94a14aAC6c15A473FAAD9Cb75B2be53A", + "SSVOperatorsWhitelist": "0x5734A479F463e3DddDBB8fFC2C82253d41777BbC", + "SSVStaking": "0x99f2B313BD913d6E11E91BA7B3c5B51c4E486bE5", + "SSVValidators": "0x57aE505496ED7BF377A28bb645d4242CC5c74330" + } +} diff --git a/deployments/hoodi-stage/upgrade-result.json b/deployments/hoodi-stage/upgrade-result.json new file mode 120000 index 000000000..c1578f083 --- /dev/null +++ b/deployments/hoodi-stage/upgrade-result.json @@ -0,0 +1 @@ +upgrade-result.v2.0.0.json \ No newline at end of file diff --git a/deployments/hoodi-stage/upgrade-result.v2.0.0.json b/deployments/hoodi-stage/upgrade-result.v2.0.0.json new file mode 100644 index 000000000..cee8df654 --- /dev/null +++ b/deployments/hoodi-stage/upgrade-result.v2.0.0.json @@ -0,0 +1,68 @@ +{ + "currentVersion": "v1.2.0", + "targetVersion": "v2.0.0", + "skipInitializer": false, + "owner": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "ssvNetworkProxy": "0xc07B3E9671f884FDa67E1e7D43d952E0e1369fd8", + "ssvNetworkViews": "0x3234e84b7d1eE1AF8b586E26814d4e268336D142", + "ssvToken": "0x746C33ccC28b1363c35c09baDAF41b2FFa7E6D56", + "cooldownDuration": 604800, + "upgradeTimestamp": 2389830, + "quorumBps": 7500, + "defaultOracleIds": [ + 1, + 2, + 3, + 4 + ], + "protocolParams": { + "networkFeeEth": "3550900000", + "maxOperatorEthFee": "5326300000", + "minOperatorEthFee": "1065200000", + "minimumLiquidationCollateralEth": "940000000000000", + "liquidationThresholdPeriod": "35800", + "minBlocksBetweenUpdates": 0, + "operatorFeeIncreaseLimit": "1000", + "declareOperatorFeePeriod": "604800", + "executeOperatorFeePeriod": "604800", + "networkFeeSSV": "382640000000", + "minimumLiquidationCollateralSSV": "1000000000000000000", + "validatorsPerOperatorLimit": "3000", + "unstakeCooldownDuration": "604800" + }, + "oracles": { + "1": "0x011684890d10eCC4473F2A4A8B0F2cb7F19C66EB", + "2": "0x0CBA9a4c9B2Ea64827C101946E8979c1023fE15C", + "3": "0xaBE4A01beDF8f0B5984648f2fE64a7A120Dab800", + "4": "0xa70d0d0BE1F02cF646D5CDB6716a509A937f3EE5" + }, + "cssvToken": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "deployBlockNumber": 2389931, + "modules": { + "SSVOperators": "0x230a82A11fCe6E471e372464e0f1654B725A006F", + "SSVClusters": "0x4D675d380016865bA537dd5162Fe13fb121e6eE9", + "SSVDAO": "0x89220e1bdDc2887e2d057e51eeF65c98023Ae263", + "SSVViews": "0xD7D89a8804165438eF66C9110dD610016E47442B", + "SSVOperatorsWhitelist": "0xA046BD193cc78B1BC318F2042c2c28186D298C21", + "SSVStaking": "0x30e9eddF53Cf946D45a85a5BFCAB4e9eDb216bBE", + "SSVValidators": "0x069484b31f8B61561fae55dfDa4DbD79897Fe227" + }, + "deployments": { + "ssvNetworkStakingUpgradeImplementation": "0x3019A13fbac4758Ec1fD111ACB978F44e2d52814", + "ssvNetworkViewsImplementation": "0xCfc6d7f14189e3D717747Be49aC331EB219829F8", + "cssvToken": "0x6455a0d83FeB099182Fb6D024B9Ae0c2E26C0859", + "modules": { + "SSVOperators": "0x230a82A11fCe6E471e372464e0f1654B725A006F", + "SSVClusters": "0x4D675d380016865bA537dd5162Fe13fb121e6eE9", + "SSVDAO": "0x89220e1bdDc2887e2d057e51eeF65c98023Ae263", + "SSVViews": "0xD7D89a8804165438eF66C9110dD610016E47442B", + "SSVOperatorsWhitelist": "0xA046BD193cc78B1BC318F2042c2c28186D298C21", + "SSVStaking": "0x30e9eddF53Cf946D45a85a5BFCAB4e9eDb216bBE", + "SSVValidators": "0x069484b31f8B61561fae55dfDa4DbD79897Fe227" + }, + "targetNetwork": "hoodi", + "deployBlockNumber": 2389931, + "chainId": "560048", + "updatedAt": "2026-03-10T12:10:14.191Z" + } +} diff --git a/deployments/local/config.json b/deployments/local/config.json new file mode 100644 index 000000000..616aa56aa --- /dev/null +++ b/deployments/local/config.json @@ -0,0 +1,30 @@ +{ + "currentVersion": "v2.0.0", + "targetVersion": "v2.0.0", + "skipInitializer": true, + "owner": "0xC564AF154621Ee8D0589758d535511aEc8f67b40", + "ssvNetworkProxy": "0x384AC2c8AF4Df1faD7E20F15064B2C2917fAa7a3", + "ssvNetworkViews": "0xb99C1e59579d5148e67FA1cF0e46BC5fE5C39212", + "ssvToken": "0x746c33ccc28b1363c35c09badaf41b2ffa7e6d56", + "cooldownDuration": 604800, + "upgradeTimestamp": 2219200, + "quorumBps": 7500, + "defaultOracleIds": [1, 2, 3, 4], + "protocolParams": { + "networkFeeEth": "3550900000", + "maxOperatorEthFee": "5326300000", + "minOperatorEthFee": "1065200000", + "minimumLiquidationCollateralEth": "940000000000000", + "liquidationThresholdPeriod": "35800", + "minBlocksBetweenUpdates": "7200", + "operatorFeeIncreaseLimit": "1000", + "declareOperatorFeePeriod": "604800", + "executeOperatorFeePeriod": "604800" + }, + "oracles": { + "1": "0x6b6fa15717beeb5a40fac6610c3e92776037a30e", + "2": "0x01E7e108eD97B4EA08e1a184aBA793b9D282E565", + "3": "0xef6d2263a1d96eac2a4530ba327bcc2e6c948feb", + "4": "0xFe33f6cb66ee2A85748458556D6ccEC3716D2173" + } +} diff --git a/deployments/mainnet/config.json b/deployments/mainnet/config.json new file mode 100644 index 000000000..caaf83483 --- /dev/null +++ b/deployments/mainnet/config.json @@ -0,0 +1,31 @@ +{ + "currentVersion": "v1.2.0", + "targetVersion": "v2.0.0", + "skipInitializer": false, + "owner": "0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6", + "ssvNetworkProxy": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "ssvNetworkViews": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + "ssvToken": "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + "cssvToken": "0xe018D31F120A637828F46aFD6c64EC099d960546", + "cooldownDuration": 604800, + "upgradeTimestamp": 1776672000, + "quorumBps": 7500, + "defaultOracleIds": [1, 2, 3, 4], + "protocolParams": { + "networkFeeEth": "3366600000", + "maxOperatorEthFee": "5336500000", + "minOperatorEthFee": "10000000", + "minimumLiquidationCollateralEth": "324862599900000", + "liquidationThresholdPeriod": "42960", + "minBlocksBetweenUpdates": "0", + "minimumLiquidationCollateralSSV": "385795451000000000", + "liquidationThresholdPeriodSSV": "100240" + }, + "initialStakeAmount": "1000000000000000000", + "oracles": { + "1": "0xc61f7bd9ee5a3d011caf47aa0e5411f720593920", + "2": "0xc07332e05cec1c4896555a6d10361233fdf14422", + "3": "0x28bEa5B242362974d5DDb8f17a1E0e525446960B", + "4": "0x3A98EE5f80268Ed91F8A5880d93468b76a9F3bB4" + } +} diff --git a/deployments/mainnet/deploy-result.json b/deployments/mainnet/deploy-result.json new file mode 120000 index 000000000..03fd85e25 --- /dev/null +++ b/deployments/mainnet/deploy-result.json @@ -0,0 +1 @@ +deploy-result.v2.0.0.json \ No newline at end of file diff --git a/deployments/mainnet/deploy-result.v2.0.0.json b/deployments/mainnet/deploy-result.v2.0.0.json new file mode 100644 index 000000000..9d40b7217 --- /dev/null +++ b/deployments/mainnet/deploy-result.v2.0.0.json @@ -0,0 +1,24 @@ +{ + "deployer": "0x3187a42658417a4d60866163A4534Ce00D40C0C8", + "chainId": "1", + "network": "mainnet", + "deployedAt": "2026-04-19T08:25:37.577Z", + "blockNumber": 24912723, + "implementations": { + "SSVNetworkSSVStakingUpgrade": "0xa72a8F31163d74D708664493d09167dfa13008E9", + "SSVNetworkViews": "0xAdEb99eb2307F874D72b1F814fCa106f6BFaA8E9" + }, + "cssvToken": { + "address": "0xe018D31F120A637828F46aFD6c64EC099d960546", + "deployed": false + }, + "modules": { + "SSVOperators": "0x38DaA3fC4B1E5c02742b67F241B27Dceb8BFFA45", + "SSVClusters": "0x5DD8980c3c8B48BAce3A2Ec481bD61f7dE1523a9", + "SSVDAO": "0x94ef691dAa32cC2d31897aE8767e02988f1add4F", + "SSVViews": "0xf73954Ad8C96647c2238e6B7A435557Def23c19F", + "SSVOperatorsWhitelist": "0xE8CEac3f59EF0214c957Fd72F003bc9671a7196B", + "SSVStaking": "0x238C9C4f6026924c7B51400fa63452FAFF8e959A", + "SSVValidators": "0xCFA765F971B1D10b7bf989aad80cab817b92Fe1e" + } +} diff --git a/deployments/mainnet/deployment-attestation.json b/deployments/mainnet/deployment-attestation.json new file mode 100644 index 000000000..c4b4b5433 --- /dev/null +++ b/deployments/mainnet/deployment-attestation.json @@ -0,0 +1,109 @@ +{ + "generatedAt": "2026-04-19T08:28:07.811Z", + "deployment": { + "deployer": "0x3187a42658417a4d60866163A4534Ce00D40C0C8", + "chainId": "1", + "network": "mainnet", + "deployedAt": "2026-04-19T08:25:37.577Z", + "blockNumber": 24912723 + }, + "config": { + "currentVersion": "v1.2.0", + "targetVersion": "v2.0.0", + "ssvNetworkProxy": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "ssvNetworkViews": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + "ssvToken": "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + "cooldownDuration": 604800, + "upgradeTimestamp": 1776672000, + "quorumBps": 7500, + "defaultOracleIds": [ + 1, + 2, + 3, + 4 + ], + "initialStakeAmount": "1000000000000000000", + "protocolParams": { + "networkFeeEth": "3366600000", + "maxOperatorEthFee": "5336500000", + "minOperatorEthFee": "10000000", + "minimumLiquidationCollateralEth": "324862599900000", + "liquidationThresholdPeriod": "42960", + "minBlocksBetweenUpdates": "0", + "minimumLiquidationCollateralSSV": "385795451000000000", + "liquidationThresholdPeriodSSV": "100240" + }, + "oracles": { + "1": "0xc61f7bd9ee5a3d011caf47aa0e5411f720593920", + "2": "0xc07332e05cec1c4896555a6d10361233fdf14422", + "3": "0x28bEa5B242362974d5DDb8f17a1E0e525446960B", + "4": "0x3A98EE5f80268Ed91F8A5880d93468b76a9F3bB4" + } + }, + "contracts": { + "SSVNetworkSSVStakingUpgrade": { + "address": "0xa72a8F31163d74D708664493d09167dfa13008E9", + "constructorArgs": {}, + "initializerArgs": { + "function": "initializeSSVStaking(uint64,uint32[4],uint16)", + "cooldownDuration": "604800", + "defaultOracleIds": "[1,2,3,4]", + "quorumBps": "7500" + }, + "bytecodeHash": "0xf518bf4bb8fa630874f0c6cc737b963750d3ce081575b508da337f2d740f076b" + }, + "SSVNetworkViews": { + "address": "0xAdEb99eb2307F874D72b1F814fCa106f6BFaA8E9", + "constructorArgs": {}, + "bytecodeHash": "0x17be0b90b7000e63338b7d5361038883ed7463f353d8579bd6f5193e61a15b9b" + }, + "CSSVToken": { + "address": "0xe018D31F120A637828F46aFD6c64EC099d960546", + "constructorArgs": {}, + "bytecodeHash": "0x9d14ddbf6e0224f9863297ad56c10f06121aba20c32e2c5b3f62def709362861" + }, + "SSVOperators": { + "address": "0x38DaA3fC4B1E5c02742b67F241B27Dceb8BFFA45", + "constructorArgs": { + "upgradeTimestamp": "1776672000" + }, + "bytecodeHash": "0x0035847a1166dc0f4a1969768ee4e51364067dde0dc910f02864fb64c316fd2e" + }, + "SSVClusters": { + "address": "0x5DD8980c3c8B48BAce3A2Ec481bD61f7dE1523a9", + "constructorArgs": {}, + "bytecodeHash": "0xb5afe06362a4f0e1b5538a720b489a3a8fea3efc6bae3f43756e7b3b48e324db" + }, + "SSVDAO": { + "address": "0x94ef691dAa32cC2d31897aE8767e02988f1add4F", + "constructorArgs": { + "cssvToken": "0xe018D31F120A637828F46aFD6c64EC099d960546" + }, + "bytecodeHash": "0xd8d314f21c630ea5e35e8082dc307bb550bffc57c8003c18cbef0eb023379243" + }, + "SSVViews": { + "address": "0xf73954Ad8C96647c2238e6B7A435557Def23c19F", + "constructorArgs": { + "cssvToken": "0xe018D31F120A637828F46aFD6c64EC099d960546" + }, + "bytecodeHash": "0x7be703041ec8617d01aebe6079a64fbbc3a5de5875c9c1cc657891caadd906f3" + }, + "SSVOperatorsWhitelist": { + "address": "0xE8CEac3f59EF0214c957Fd72F003bc9671a7196B", + "constructorArgs": {}, + "bytecodeHash": "0x7c8400041759248263c5f791e1123b23cc760e77f5da9b46daca5ef4d2815fe6" + }, + "SSVStaking": { + "address": "0x238C9C4f6026924c7B51400fa63452FAFF8e959A", + "constructorArgs": { + "cssvToken": "0xe018D31F120A637828F46aFD6c64EC099d960546" + }, + "bytecodeHash": "0x2036caa06ba7cbcab9fb947944b43a8e307e3c525a14bfaf5acf18180c0797f7" + }, + "SSVValidators": { + "address": "0xCFA765F971B1D10b7bf989aad80cab817b92Fe1e", + "constructorArgs": {}, + "bytecodeHash": "0x7b381a0ae9014ff93fec90fa96ab9dd5ba3cc3e5501a5d6182e74e3b63bf0a34" + } + } +} diff --git a/deployments/mainnet/multisig-batch.json b/deployments/mainnet/multisig-batch.json new file mode 100644 index 000000000..eb2f23bda --- /dev/null +++ b/deployments/mainnet/multisig-batch.json @@ -0,0 +1,132 @@ +{ + "version": "1.0", + "chainId": "1", + "createdAt": 1776587570859, + "meta": { + "name": "SSV Network v2.0.0 Upgrade (mainnet)", + "description": "Upgrade SSVNetwork proxy, attach modules, set protocol parameters, and configure oracles for the mainnet environment.", + "createdFromSafeAddress": "0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6" + }, + "transactions": [ + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x4f1ef286000000000000000000000000a72a8f31163d74d708664493d09167dfa13008e9000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c4d0937f030000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000001d4c00000000000000000000000000000000000000000000000000000000" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000038daa3fc4b1e5c02742b67f241b27dceb8bffa45" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b000000000000000000000000000000000000000000000000000000000000000010000000000000000000000005dd8980c3c8b48bace3a2ec481bd61f7de1523a9" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000094ef691daa32cc2d31897ae8767e02988f1add4f" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000f73954ad8c96647c2238e6b7a435557def23c19f" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000004000000000000000000000000e8ceac3f59ef0214c957fd72f003bc9671a7196b" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000238c9c4f6026924c7b51400fa63452faff8e959a" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe3e324b00000000000000000000000000000000000000000000000000000000000000006000000000000000000000000cfa765f971b1d10b7bf989aad80cab817b92fe1e" + }, + { + "to": "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + "value": "0", + "data": "0x3659cfe6000000000000000000000000adeb99eb2307f874d72b1f814fca106f6bfaa8e9" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x1f1f9fd500000000000000000000000000000000000000000000000000000000c8aa3d40" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x6512447d000000000000000000000000000000000000000000000000000000000000a7d0" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe567ed580000000000000000000000000000000000000000000000000000000000018790" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x11dff2490000000000000000000000000000000000000000000000000000000000000000" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xb4c9c40800000000000000000000000000000000000000000000000000012775f7de2b60" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x9f5c1307000000000000000000000000000000000000000000000000055a9eeb2cd10e00" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x6f4b158d000000000000000000000000000000000000000000000000000000013e148720" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xe9d232cd0000000000000000000000000000000000000000000000000000000000989680" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x9ba0e7000000000000000000000000000000000000000000000000000000000000001d4c" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000c61f7bd9ee5a3d011caf47aa0e5411f720593920" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c07332e05cec1c4896555a6d10361233fdf14422" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d000000000000000000000000000000000000000000000000000000000000000300000000000000000000000028bea5b242362974d5ddb8f17a1e0e525446960b" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0x5d756b1d00000000000000000000000000000000000000000000000000000000000000040000000000000000000000003a98ee5f80268ed91f8a5880d93468b76a9f3bb4" + }, + { + "to": "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + "value": "0", + "data": "0x095ea7b3000000000000000000000000dd9bc35ae942ef0cfa76930954a156b3ff30a4e10000000000000000000000000000000000000000000000000de0b6b3a7640000" + }, + { + "to": "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + "value": "0", + "data": "0xa694fc3a0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + ] +} diff --git a/deployments/params-candidate.json b/deployments/params-candidate.json new file mode 100644 index 000000000..9f3c04dec --- /dev/null +++ b/deployments/params-candidate.json @@ -0,0 +1,12 @@ +{ + "networkFeeEth": "3550900000", + "minimumLiquidationCollateralEth": "940000000000000", + "liquidationThresholdPeriod": "35800", + "minOperatorEthFee": "1065200000", + "maxOperatorEthFee": "5326300000", + "defaultOperatorEthFee": "1778800000", + "quorumBps": 7500, + "cooldownDuration": 604800, + "minBlocksBetweenUpdates": 0, + "defaultOracleIds": [1, 2, 3, 4] +} diff --git a/deployments/template-config.json b/deployments/template-config.json new file mode 100644 index 000000000..b8bc3c652 --- /dev/null +++ b/deployments/template-config.json @@ -0,0 +1,18 @@ +{ + "currentVersion": "", + "targetVersion": "", + "owner": "", + "ssvNetworkProxy": "", + "ssvNetworkViews": "", + "ssvToken": "", + "cooldownDuration": 604800, + "upgradeTimestamp": 0, + "quorumBps": 7500, + "defaultOracleIds": [1, 2, 3, 4], + "cssvToken": "", + "protocolParams": { + "liquidationThresholdPeriodSSV": "100380", + "minBlocksBetweenUpdates": "7200" + }, + "oracles": {} +} diff --git a/docs/FLOWS.md b/docs/FLOWS.md new file mode 100644 index 000000000..0d887cb0c --- /dev/null +++ b/docs/FLOWS.md @@ -0,0 +1,1126 @@ +# SSV Network v2.0.0 — Contract Flows + +This document is the **implementation verification checklist** for the SSV Staking upgrade (v2.0.0). It describes every contract flow with preconditions, step-by-step state mutations, events, postconditions, and invariants. For design intent, rules, and accounting formulas, see [SPEC.md](./SPEC.md). + +| Document | Purpose | +|---|---| +| **SPEC.md** | Design intent · rules · formulas · invariants · source of truth | +| **FLOWS.md** (this file) | Step-by-step execution · preconditions · state mutations · test checklist | + +## Table of Contents + +1. [Cluster Flows](#1-cluster-flows) + - [Register Validator (ETH)](#11-register-validator-eth) + - [Bulk Register Validators (ETH)](#12-bulk-register-validators-eth) + - [Remove Validator](#13-remove-validator) + - [Bulk Remove Validators](#14-bulk-remove-validators) + - [Exit Validator](#15-exit-validator) + - [Bulk Exit Validators](#16-bulk-exit-validators) + - [Deposit ETH](#17-deposit-eth) + - [Withdraw ETH](#18-withdraw-eth) + - [Liquidate (ETH)](#19-liquidate-eth) + - [Liquidate (SSV Legacy)](#110-liquidate-ssv-legacy) + - [Reactivate](#111-reactivate) +2. [Migration Flows](#2-migration-flows) + - [Migrate Cluster to ETH](#21-migrate-cluster-to-eth) +3. [Effective Balance Flows](#3-effective-balance-flows) + - [Commit Root (Oracle)](#31-commit-root-oracle) + - [Update Cluster Balance](#32-update-cluster-balance) +4. [Operator Flows](#4-operator-flows) + - [Register Operator](#41-register-operator) + - [Remove Operator](#42-remove-operator) + - [Declare Operator Fee](#43-declare-operator-fee) + - [Execute Operator Fee](#44-execute-operator-fee) + - [Reduce Operator Fee](#45-reduce-operator-fee) + - [Cancel Declared Operator Fee](#46-cancel-declared-operator-fee) + - [Withdraw Operator Earnings (ETH)](#47-withdraw-operator-earnings-eth) + - [Withdraw Operator Earnings (SSV)](#48-withdraw-operator-earnings-ssv) +5. [Staking Flows](#5-staking-flows) + - [Stake SSV](#51-stake-ssv) + - [Request Unstake](#52-request-unstake) + - [Withdraw Unlocked](#53-withdraw-unlocked) + - [Claim ETH Rewards](#54-claim-eth-rewards) + - [Sync Fees](#55-sync-fees) +6. [DAO Governance Flows](#6-dao-governance-flows) + - [Update Network Fee](#61-update-network-fee) + - [Replace Oracle](#62-replace-oracle) +- [Global Invariants](#global-invariants) + +--- + +## Global Invariants + +### ETH Contract Balance Accounting Invariant + +``` +contract.ETH_balance ≈ Σ(current ETH cluster balances) + Σ(current operator ETH earnings) + ProtocolLib.networkTotalEarnings() +``` + +Where “current” means: + - Cluster balances computed like `SSVViews.getBalance` (it applies pending fees before returning). + File: `contracts/modules/SSVViews.sol` + - Operator earnings computed like `SSVViews.getOperatorEarnings` (it updates snapshots before returning). + File: `contracts/modules/SSVViews.sol` + - DAO/staking pool uses `ProtocolLib.networkTotalEarnings()`. + File: `contracts/libraries/ProtocolLib.sol` + +This invariant holds by construction across all ETH flows. If accounting is correct, every `cluster.balance` is always ≤ `address(this).balance` — no explicit contract-balance guard is needed in `withdraw`. A violation indicates a protocol bug, not a user error. + +--- + +## 1. Cluster Flows + +### 1.1 Register Validator (ETH) + +**Caller:** Cluster owner (or new cluster creator) +**Payable:** Yes (msg.value = ETH to deposit) + +#### Preconditions +- Public key length must be valid (48 bytes) +- Validator must not already exist +- Operator IDs must be sorted ascending, length 4–13 +- All operators must exist and not be removed +- If operators are private, caller must be whitelisted +- If cluster doesn't exist, this creates a new ETH cluster +- If cluster exists, it must be an ETH cluster (VERSION_ETH) +- Cluster must be active (not liquidated) + +#### State Mutations +1. For each operator: + - Update ETH snapshot (accumulate earnings) + - Increment `operator.ethValidatorCount` + - If first ETH interaction: `ensureETHDefaults()` sets ethFee and ethSnapshot.block +2. Store validator: `validatorPKs[hash(pubkey, owner)] = hash(operatorIds | active=true)` +3. Update cluster state: + - `cluster.validatorCount++` + - `cluster.balance += msg.value` + - `cluster.index = current cumulative operator ETH index` + - `cluster.networkFeeIndex = current ETH network fee index` +4. Update DAO: `ethDaoValidatorCount++`, `daoTotalEthVUnits += BPS_DENOMINATOR` — baseline EB of 32 ETH per validator is always applied here for all ETH clusters +5. If cluster has explicit EB (oracle has previously submitted an EB update): also update `ebSnapshot.vUnits` to include the new validators' baseline. Operator and DAO deviation vUnits are NOT updated — new validators start at exactly 32 ETH so their deviation is zero +6. Store cluster hash in `ethClusters` +7. Liquidation check: cluster must not be liquidatable after registration + - Check uses **projected vUnits** (post-registration) not stale storage + - Explicit EB: `storedVUnits + validatorCountDelta * BPS_DENOMINATOR` + - Implicit EB: `cluster.validatorCount * BPS_DENOMINATOR` + +#### Events +```solidity +emit ValidatorAdded(owner, operatorIds, publicKey, shares, cluster); +``` + +#### Postcondition Invariants +- `contract.balance == previous_contract_balance + msg.value` +- `operator.ethValidatorCount == previous + 1` for each operator +- `ethDaoValidatorCount == previous + 1` +- Cluster is not liquidatable +- Validator is retrievable via `getValidator(owner, publicKey)` + +--- + +### 1.2 Bulk Register Validators (ETH) + +Same as 1.1 but for multiple validators in one transaction. Each validator emits a separate `ValidatorAdded` event. `msg.value` is added to cluster balance once (not per validator). + +#### Additional Invariants +- `contract.balance == previous + msg.value` (single ETH deposit) +- `operator.ethValidatorCount == previous + N` for each operator (N = number of validators) +- `ethDaoValidatorCount == previous + N` + +--- + +### 1.3 Remove Validator + +**Caller:** Cluster owner + +#### Preconditions +- Validator must exist and be owned by caller +- Cluster must exist as ETH cluster (VERSION_ETH) or legacy SSV cluster (VERSION_SSV) +- Operator IDs must match the registered operator set + +#### State Mutations (ETH cluster) +1. Update operator ETH snapshots +2. Decrement `operator.ethValidatorCount` +3. Delete validator record +4. Update cluster: + - `cluster.validatorCount--` + - Settle fees up to current block + - Update indices +5. Update DAO: `ethDaoValidatorCount--`, reduce vUnits +6. If last validator removed: cluster balance remains (can withdraw later) + +#### State Mutations (legacy SSV cluster) +1. Delete validator record +2. If cluster is active: + - Update operator SSV snapshots and counts via `updateClusterOperatorsSSV(..., validatorsRemoved=1, ...)` + - Settle cluster balance and indices with SSV fee index (`currentNetworkFeeIndexSSV`) + - Decrement DAO SSV validator count via `updateDAOSSV(false, 1)` +3. If cluster is liquidated: + - Skip SSV operator/DAO settlement in remove path (counts were already updated at liquidation time) +4. Decrement `cluster.validatorCount` +5. Persist updated legacy cluster in `s.clusters[hashedCluster]` + +#### Events +```solidity +emit ValidatorRemoved(owner, operatorIds, publicKey, cluster); +``` + +#### Postcondition Invariants +- `operator.ethValidatorCount == previous - 1` +- `ethDaoValidatorCount == previous - 1` +- Validator no longer retrievable +- Cluster balance reflects settled fees + +#### Postcondition Invariants (legacy SSV cluster) +- If cluster was active: operator/DAO SSV counts decrease by 1 +- If cluster was liquidated: remove does not decrement counts again +- `cluster.validatorCount == previous - 1` +- Validator no longer retrievable +- No EB (`clusterEB` / `operatorEthVUnits`) cleanup is performed in SSV branch + +--- + +### 1.4 Bulk Remove Validators + +**Caller:** Cluster owner + +Same as 1.3 but removes multiple validators in one transaction. All validators must belong to the same cluster (same operator set). Each validator emits a separate `ValidatorRemoved` event. Cluster fee settlement and DAO accounting happen once for the full batch. + +#### Additional Invariants vs 1.3 +- `operator.ethValidatorCount == previous - N` for each operator (N = validators removed) +- `ethDaoValidatorCount == previous - N` +- `cluster.validatorCount == previous - N` +- If cluster had explicit EB tracking (`ebSnapshot.vUnits > 0`): `ebSnapshot.vUnits -= N * BPS_DENOMINATOR` +- If `cluster.validatorCount` reaches 0 and cluster is active: any remaining deviation vUnits are cleaned from `operatorEthVUnits` and DAO + +#### Additional Invariants vs 1.3 (legacy SSV cluster) +- If cluster was active: `operator.validatorCount == previous - N` and `daoValidatorCount == previous - N` +- If cluster was liquidated: remove does not decrement SSV operator/DAO counts again +- `cluster.validatorCount == previous - N` in all cases +- Operation is atomic: if any validator in the batch is invalid, no validator is removed + +--- + +### 1.5 Exit Validator + +**Caller:** Cluster owner +**nonReentrant:** No +**Payable:** No + +#### Preconditions +- Validator must exist and be owned by caller +- Validator must be registered with the given operator set (state check via `validateCorrectState`) + +#### State Mutations +None — `exitValidator` is a pure signal (event emission). No on-chain state is modified. + +#### Events +```solidity +emit ValidatorExited(owner, operatorIds, publicKey); +``` + +#### Postcondition Invariants +- No storage state changes +- Event is emitted; SSV oracle nodes observe it and initiate voluntary exit on the beacon chain +- Validator record remains in storage until `removeValidator` is called + +> **Note:** Exit is a two-step off-chain process. `exitValidator` signals intent; the actual beacon-chain exit is performed by the SSV nodes network upon observing the event. The cluster continues to accrue fees until `removeValidator` is called. + +--- + +### 1.6 Bulk Exit Validators + +**Caller:** Cluster owner +**nonReentrant:** No +**Payable:** No + +Same as 1.5 but signals exit for multiple validators in one transaction. All validators must belong to the same operator set. Each validator emits a separate `ValidatorExited` event. + +#### Preconditions +- `publicKeys.length > 0` (empty list reverts with `ValidatorDoesNotExist`) +- Each validator must exist and be owned by caller with the given operator set + +#### State Mutations +None — pure signal, identical to 1.5 per validator. + +#### Events +```solidity +// emitted once per validator +emit ValidatorExited(owner, operatorIds, publicKeys[i]); +``` + +#### Postcondition Invariants +- No storage state changes +- N `ValidatorExited` events emitted (one per validator) +- All validator records remain in storage until `bulkRemoveValidator` is called + +--- + +### 1.7 Deposit ETH + +**Caller:** Anyone (on behalf of cluster owner) +**Payable:** Yes + +#### Preconditions +- Cluster must exist as ETH cluster (VERSION_ETH) + +> **Note — deposits allowed on liquidated clusters:** `deposit` does not require the cluster to be active. Depositing to a liquidated cluster, and later reactivating it, will accumulate both the deposit and the reactivation amount. + +#### State Mutations +1. `cluster.balance += msg.value` +2. Update stored cluster hash + +#### Events +```solidity +emit ClusterDeposited(owner, operatorIds, msg.value, cluster); +``` + +#### Postcondition Invariants +- `contract.balance == previous_contract_balance + msg.value` +- `cluster.balance == previous_settled_balance + msg.value` +- Cluster state hash is updated + +--- + +### 1.8 Withdraw ETH + +**Caller:** Cluster owner +**nonReentrant:** Yes + +#### Preconditions +- Cluster must exist as ETH cluster (VERSION_ETH) +- `amount <= cluster.balance` (after fee settlement if active) +- If cluster is active and has validators: cluster must not become liquidatable after withdrawal +- ~~Cluster must be active~~ — **liquidated clusters are allowed** (see note below) + +> **Note — withdrawal allowed on liquidated clusters:** `withdraw` does not require the cluster to be active. A liquidated cluster may have received deposits (via `deposit`) in preparation for reactivation. If the owner decides not to reactivate, they can recover those funds via `withdraw`. Fee settlement and the post-withdrawal liquidatability check are skipped for inactive clusters (no burn rate applies). + +#### State Mutations +1. `cluster.balance -= amount` +2. If cluster is active and has validators: liquidation check +3. Update stored cluster hash +4. Transfer `amount` ETH to caller + +#### Events +```solidity +emit ClusterWithdrawn(owner, operatorIds, amount, cluster); +``` + +#### Postcondition Invariants +- `cluster.balance == previous_settled_balance - amount` +- `owner.balance == previous_owner_balance + amount` +- If cluster is active and has validators: cluster is not liquidatable + +> **Accounting invariant:** See [Global Invariants — ETH Contract Balance Accounting Invariant](#eth-contract-balance-accounting-invariant). + +--- + +### 1.9 Liquidate (ETH) + +**Caller:** Anyone (self-liquidation always allowed; third-party only if cluster is liquidatable) +**nonReentrant:** Yes + +#### Preconditions +- Cluster must exist as ETH cluster (VERSION_ETH) +- Cluster must be active +- If caller != owner: cluster must be liquidatable (balance below threshold) + +#### State Mutations +1. Update operator snapshots with fee settlement +2. Decrement `operator.ethValidatorCount` for each operator +3. Reduce operators' effective balance (EB) tracking: decrement `operator.vUnits` by cluster's vUnits +4. Compute liquidation bounty = remaining cluster balance +5. Set cluster state: `active = false, balance = 0, index = 0, networkFeeIndex = 0` +6. Update DAO: `ethDaoValidatorCount -= cluster.validatorCount`, reduce DAO vUnits and EB tracking +7. Update stored cluster hash +8. Transfer bounty ETH to caller (liquidator) + +#### Events +```solidity +emit ClusterLiquidated(owner, operatorIds, cluster); +``` + +#### Postcondition Invariants +- `cluster.active == false` +- `cluster.balance == 0` +- `operator.ethValidatorCount` decreased by cluster's validator count +- `ethDaoValidatorCount` decreased +- Liquidator received bounty ETH +- `contract.balance == previous - bounty` + +--- + +### 1.10 Liquidate (SSV Legacy) + +Same flow as 1.9 but for SSV clusters. Uses `s.clusters` instead of `s.ethClusters`. SSV balance transferred via SSV token transfer (not ETH). + +--- + +### 1.11 Reactivate + +**Caller:** Cluster owner +**Payable:** Yes (msg.value = ETH deposit) + +#### Preconditions +- Cluster must exist as ETH cluster +- Cluster must be liquidated (`active == false`) + + +> **Note — Stale EB risk:** The solvency check uses the stored `clusterEB.vUnits` snapshot, which may be stale if the beacon-chain EB changed during liquidation. Under the current oracle behavior, inactive / liquidated clusters are omitted from the Merkle root, so their on-chain EB snapshot usually cannot be refreshed before reactivation. In practice, deposit sizing should rely on off-chain beacon-chain-aware tooling plus a conservative buffer, not only on the on-chain snapshot. Ref: SPEC §2 "Stale EB Risk on Reactivation" for full analysis and mitigation options. +> +> **Note — operator removal and reactivation:** If one or more operators in a cluster's operator set have been removed (via `removeOperator`), the cluster can still be reactivated, but removed operators are silently skipped during `updateClusterOperatorsOnReactivation` (see `OperatorLib.sol:311`). The cluster will operate with reduced operator coverage (e.g., 3/4 instead of 4/4), which may compromise the cluster's fault tolerance. The reactivation fee calculation excludes removed operators' fees. No on-chain event signals which operators were skipped, but this is detectable off-chain by checking operator states before reactivation. + +#### State Mutations +1. Update operator ETH snapshots +2. Increment `operator.ethValidatorCount` for each operator +3. Increase operators' effective balance (EB) tracking: increment `operator.vUnits` by cluster's vUnits +4. Set cluster: `active = true, balance += msg.value, index = current, networkFeeIndex = current` +5. Update DAO: `ethDaoValidatorCount += cluster.validatorCount`, add DAO vUnits and increase EB tracking +6. Liquidation check: must not be immediately liquidatable (uses stored `clusterEB.vUnits`) +7. Update stored cluster hash + +#### Events +```solidity +emit ClusterReactivated(owner, operatorIds, cluster); +``` + +#### Postcondition Invariants +- `cluster.active == true` +- `cluster.balance += msg.value` +- `contract.balance == previous + msg.value` +- Cluster is not liquidatable + +--- + +## 2. Migration Flows + +### 2.1 Migrate Cluster to ETH + +**Caller:** Cluster owner +**Payable:** Yes (msg.value = ETH for new cluster balance) + +#### Preconditions +- Cluster must exist in `s.clusters` (VERSION_SSV) +- Cluster can be active or liquidated — if liquidated, migration also reactivates it +- Caller must be cluster owner +- msg.value must be sufficient to pass ETH liquidation check + +#### State Mutations + +1. **Operator migration (for each operator):** + - Update SSV snapshot (accumulate final SSV earnings) + - Decrement `operator.validatorCount` (SSV count) — skip if cluster was liquidated + - If first ETH interaction: `ensureETHDefaults()` (set ethFee, ethSnapshot.block) + - Else: update ETH snapshot + - Increment `operator.ethValidatorCount` + +2. **Settle SSV balance:** + - Compute remaining SSV balance after fees + - Store as `ssvClusterBalance` for refund + +3. **Set up ETH cluster:** + - `cluster.balance = msg.value` + - `cluster.active = true` + - `cluster.index = cumulative ETH operator index` + - `cluster.networkFeeIndex = current ETH network fee index` + +4. **DAO accounting:** + - If NOT previously liquidated: `sp.updateDAOSSV(false, validatorCount)` (reduce SSV DAO count) + - Always: `sp.updateDAO(true, validatorCount)` (increase ETH DAO count + baseline vUnits) + +5. **Liquidation check:** Verify ETH cluster is not liquidatable + +6. **Store & delete:** + - `s.ethClusters[key] = cluster.hashClusterData()` + - `delete s.clusters[key]` + +7. **EB deviation sync (if applicable):** + - If cluster had explicit EB snapshot with vUnits > baseline: + - Add deviation to `sp.daoTotalEthVUnits` + - Add deviation to each `seb.operatorEthVUnits[operatorId]` + +8. **Refund SSV:** Transfer remaining SSV balance to owner + +#### Events +```solidity +emit ClusterMigratedToETH(owner, operatorIds, msg.value, ssvRefunded, effectiveBalance, cluster); + +// If the SSV cluster was liquidated, migration also reactivates it: +if (isLiquidated) emit ClusterReactivated(owner, operatorIds, cluster); +``` + +#### Postcondition Invariants +- `s.clusters[key]` is deleted (no longer exists as SSV cluster) +- `s.ethClusters[key]` exists with new ETH cluster data +- `cluster.active == true` +- `cluster.balance == msg.value` +- `contract.balance == previous_contract_balance + msg.value` +- `owner SSV balance == previous + ssvRefunded` +- `operator.validatorCount` decreased (SSV), `operator.ethValidatorCount` increased (ETH) — net zero change in total validators +- `ethDaoValidatorCount` increased, `daoValidatorCount` decreased (unless was liquidated) +- Cluster is not liquidatable under ETH rules +- SSV cluster record is completely removed + +--- + +## 3. Effective Balance Flows + +### 3.1 Commit Root (Oracle) + +**Caller:** Registered oracle only + +#### Preconditions +- `oracleIdOf[msg.sender] != 0` +- `blockNum > latestCommittedBlock` (strictly monotonic) +- `blockNum <= block.number` (not future) +- Raw `cSSV.totalSupply() > 0` and its truncated voting supply is also non-zero; otherwise revert with `ZeroCSSVSupply` or `InsufficientCSSVSupply` +- Oracle has not already voted for this `(blockNum, merkleRoot)` pair + +#### State Mutations + +1. Mark oracle as voted: `hasVoted[commitmentKey][oracleId] = true` +2. On the first vote only, read raw `cSSV.totalSupply()`, truncate it to `frozenVotingSupply = rawSupply - (rawSupply % defaultOracleIds.length)`, and store that truncated value in `roundFrozenSupply[commitmentKey]` +3. Compute weight from stored voting supply: `weight = roundFrozenSupply[commitmentKey] / defaultOracleIds.length` +4. Accumulate: `rootCommitments[commitmentKey] += weight` +5. Compute threshold from the same stored voting supply: `threshold = (roundFrozenSupply[commitmentKey] * quorumBps) / 10_000` +6. **If quorum reached** (`accumulatedWeight >= threshold`): + - Store root: `ebRoots[blockNum] = merkleRoot` + - Update: `latestCommittedBlock = blockNum` + - Cleanup: `delete rootCommitments[commitmentKey]` + - **Note:** `hasVoted` mappings are intentionally NOT deleted to prevent re-voting on the same key +7. **If quorum not reached**: no root storage, no cleanup — see SPEC §4 "Failed Quorum Behavior" for full persistence rules + +The truncated remainder (`rawSupply % defaultOracleIds.length`) is treated as non-voting dust for the round. `roundFrozenSupply` therefore represents frozen voting supply, not the exact raw total supply snapshot. + +#### Events +```solidity +// If quorum reached: +emit RootCommitted(merkleRoot, blockNum); + +// If quorum not reached: +emit WeightedRootProposed(merkleRoot, blockNum, accumulatedWeight, quorum, oracleId, oracle); +``` + +#### Postcondition Invariants +- If quorum reached: `ebRoots[blockNum] == merkleRoot`, `latestCommittedBlock == blockNum`, `rootCommitments[commitmentKey]` deleted +- If quorum NOT reached: storage persists — ref SPEC §4 "Failed Quorum Behavior" +- Oracle cannot vote again for same `(blockNum, merkleRoot)`; can vote same `blockNum` with different root +- Total votes for this commitment <= oracle count + +--- + +### 3.2 Update Cluster Balance + +**Caller:** Anyone (permissionless) +**nonReentrant:** Yes + +#### Preconditions +- Committed root exists for `blockNum`: `ebRoots[blockNum] != bytes32(0)` +- **Latest-root enforcement**: `blockNum == latestCommittedBlock` (reverts with `MustUseLatestRoot` if stale) +- Update frequency check: `block.number >= lastUpdateBlock + minBlocksBetweenUpdates` (configured via `updateMinBlocksBetweenUpdates(uint32)`) +- Staleness check: `blockNum > lastRootBlockNum` (strictly increasing) +- Merkle proof valid: `verify(proof, ebRoots[blockNum], doubleHash(clusterId, effectiveBalance))` +- EB limits: `32 * validatorCount <= effectiveBalance <= 2048 * validatorCount` +- Cluster must exist (ETH or SSV) + +> **Note — Liquidated clusters:** The contract supports updating the EB snapshot while `cluster.active == false` if a valid proof exists, and still skips fee/accounting steps in that case. In production, oracle roots exclude inactive / liquidated clusters, so `updateClusterBalance` for a liquidated cluster is usually not available through the live oracle flow. This means the on-chain EB snapshot may diverge from the real beacon-chain EB until the cluster is active again and re-included in a later root. Ref: SPEC §4 "Behavior on liquidated clusters" for full rules and use cases. + +#### State Mutations (ETH Cluster) + +1. Convert `effectiveBalance` to `newVUnits = ebToVUnits(effectiveBalance)` +2. Compute `effectiveOldVUnits`: + - If `storedVUnits == 0`: `validatorCount * BPS_DENOMINATOR` + - Else: `storedVUnits` +3. If cluster active: settle operator and network fees using OLD vUnits +4. If `newVUnits != effectiveOldVUnits` AND cluster active: + - For each operator: `operatorEthVUnits[opId] += (newVUnits - effectiveOldVUnits)` — **full delta applied to every operator, no division by operator count** + - `daoTotalEthVUnits += (newVUnits - effectiveOldVUnits)` +5. Update EB snapshot: `{vUnits: newVUnits, lastRootBlockNum: blockNum, lastUpdateBlock: block.number}` +6. **Auto-liquidation check** (active clusters only): if cluster now undercollateralized: + - Liquidate immediately (same as liquidate flow) + - Bounty goes to `msg.sender` (updater) +7. If not liquidated: store updated cluster hash + +#### State Mutations (SSV Cluster) +- Only stores EB snapshot: `{vUnits: newVUnits, lastRootBlockNum: blockNum, lastUpdateBlock: block.number}` +- **No balance/fee updates**: SSV clusters continue using `validatorCount`-based accounting (see section 1.10) +- **No vUnit deviation tracking**: operator and DAO vUnit deviations are NOT updated for SSV clusters +- Prepares data for future migration to ETH (see section 2.1) + +#### Events +```solidity +emit ClusterBalanceUpdated(owner, operatorIds, blockNum, effectiveBalance, cluster); + +// If auto-liquidated: +emit ClusterLiquidated(owner, operatorIds, cluster); +``` + +#### Postcondition Invariants +- `clusterEB[clusterId].vUnits == newVUnits` +- `clusterEB[clusterId].lastRootBlockNum == blockNum` +- `clusterEB[clusterId].lastUpdateBlock == block.number` +- If EB increased: future fee accrual is higher +- If EB decreased: future fee accrual is lower +- Sum of all `operatorEthVUnits` deviations + baselines == `daoTotalEthVUnits` +- If auto-liquidated: `cluster.active == false`, bounty transferred to caller + +--- + +## 4. Operator Flows + +### 4.1 Register Operator + +**Caller:** Anyone + +#### Preconditions +- Public key must not already be registered +- Fee must be divisible by ETH_DEDUCTED_DIGITS (100,000) +- Fee must be `0` (public operator) OR within `[minimumOperatorEthFee, operatorMaxFee]` + +#### State Mutations +1. Increment `lastOperatorId` +2. Store operator: `{owner: msg.sender, ethFee: packed(fee), ethSnapshot: {block: block.number, index: 0, balance: 0}}` +3. Store public key mapping +4. If `setPrivate`: mark operator as whitelisted + +#### Events +```solidity +emit OperatorAdded(operatorId, msg.sender, publicKey, fee); +emit OperatorPrivacyStatusUpdated([operatorId], setPrivate); +``` + +#### Postcondition Invariants +- `lastOperatorId == previous + 1` +- `operators[id].owner == msg.sender` +- `operators[id].ethFee == packed(fee)` +- `operators[id].validatorCount == 0` (SSV) +- `operators[id].ethValidatorCount == 0` (ETH) + +--- + +### 4.2 Remove Operator + +**Caller:** Operator owner +**nonReentrant:** Yes + +#### Preconditions +- Operator must exist (`snapshot.block != 0 || ethSnapshot.block != 0`) +- Caller must be operator owner + +#### State Mutations +1. Update SSV snapshot (final earnings) +2. Update ETH snapshot (final earnings) +3. Reset operator state via `_resetOperatorState`: + - Zeros `ethSnapshot.block`, `ethSnapshot.balance`, `snapshot.block`, `snapshot.balance`, `ethFee`, `fee`, `ethValidatorCount`, `validatorCount` + - Keeps `ethSnapshot.index`, `snapshot.index` +4. **`operator.owner` is intentionally preserved** — allows off-chain systems (explorer, `getOperatorById`) to query the original owner after removal +5. Withdraw all SSV earnings to owner (if any) +6. Withdraw all ETH earnings to owner (if any) +7. Delete whitelist mapping +8. Delete fee change request (if any) + +#### Events +```solidity +if (ssvEarnings > 0) emit OperatorWithdrawnSSV(owner, operatorId, ssvEarnings); +if (ethEarnings > 0) emit OperatorWithdrawn(owner, operatorId, ethEarnings); +emit OperatorRemoved(operatorId); +``` + +#### Removed Operator Detection + +After removal, different code paths detect removed operators via different checks — all are consistent: + +| Check | Location | How it detects removed operators | +|-------|----------|--------------------------------| +| `checkOwner` | `OperatorLib.sol:131` | `snapshot.block == 0 && ethSnapshot.block == 0` → reverts `OperatorDoesNotExist` | +| `ensureOperatorExist` | `OperatorLib.sol:159` | `owner == address(0)` OR `(ethSnapshot.block == 0 && snapshot.block == 0)` → reverts (catches via second condition since owner is preserved) | +| `getSSVBurnRate` | `SSVViews.sol:356` | `owner != address(0)` — removed operators pass this but contribute zero fee (fee already zeroed) | +| `getOperatorById` | `SSVViews.sol:83` | Returns preserved `owner`; `isActive = false` (`ethSnapshot.block == 0`) | + +#### Postcondition Invariants +- `operators[id].owner` preserves the original owner address (non-zero) +- All other operator fields are zeroed: snapshots, fees, validator counts +- No earnings remain in the system for this operator +- Public key can be re-registered + +--- + +### 4.3 Declare Operator Fee + +**Caller:** Operator owner + +#### Preconditions +- Operator must exist +- New fee within `[minimumOperatorEthFee, operatorMaxFee]` +- Fee increase limited by `operatorMaxFeeIncrease` (percentage) +- Cannot increase if both SSV fee = 0 AND ETH fee = 0 + +> **Note — Existing pre-upgrade declarations:** Previous declarations (before the upgrade timestamp, `UPGRADE_TIMESTAMP` in `SSVOperators`) are rejected when executing the fee update via `executeOperatorFee`. The operator owner can declare a new fee at any time. + +> **Note — Multiple declarations:** Calling `declareOperatorFee` multiple times within the declare period will override any pending fee change request. The most recent declaration replaces the previous one, resetting the approval begin/end times. Only the last declared fee can be executed. + +#### State Mutations +1. Call `ensureETHDefaults(operatorId)` if `ethSnapshot.block == 0`: + - Initializes `ethSnapshot.block = block.number` + - Assigns `ethFee = DEFAULT_OPERATOR_ETH_FEE` **only if** `ethFee == 0 && SSV fee > 0` + - Emits `OperatorFeeExecuted(owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE)` if default is assigned + - See SPEC §1 "Operator Fee Transition" for complete behavior +2. Store `OperatorFeeChangeRequest{fee: packed(newFee), approvalBeginTime: now + declarePeriod, approvalEndTime: now + declarePeriod + executePeriod}` (overwrites any existing pending request) + +#### Events +```solidity +// If ensureETHDefaults assigned default (legacy SSV operator): +emit OperatorFeeExecuted(owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE); + +// Always: +emit OperatorFeeDeclared(owner, operatorId, block.number, fee); +``` + +--- + +### 4.4 Execute Operator Fee + +**Caller:** Operator owner + +#### Preconditions +- Pending fee change request exists +- `approvalBeginTime > UPGRADE_TIMESTAMP` (reject pre-migration declarations) +- Current time within `[approvalBeginTime, approvalEndTime]` +- Fee still within `operatorMaxFee` + +#### State Mutations +1. Update operator ETH snapshot — ref SPEC §10 "Fee Settlement Rule": settles at old fee up to this block; new fee applies only to future blocks +2. Set `operator.ethFee = request.fee` (packed) +3. Delete fee change request + +#### Events +```solidity +emit OperatorFeeExecuted(owner, operatorId, block.number, fee); +``` + +#### Postcondition Invariants +- `operator.ethFee == request.fee` (packed) +- No pending fee change request +- ETH snapshot block updated to current + +--- + +### 4.5 Reduce Operator Fee + +**Caller:** Operator owner (immediate, no timelock) + +#### Preconditions +- New fee within `[minimumOperatorEthFee, currentFee)` (or 0) +- New fee strictly less than current +- Fee must be 0 OR >= `minimumOperatorEthFee` + +#### State Mutations +1. Call `ensureETHDefaults(operatorId)` if `ethSnapshot.block == 0`: + - Initializes `ethSnapshot.block = block.number` + - Assigns `ethFee = DEFAULT_OPERATOR_ETH_FEE` **only if** `ethFee == 0 && SSV fee > 0` + - Emits `OperatorFeeExecuted(owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE)` if default is assigned + - See SPEC §1 "Operator Fee Transition" for complete behavior +2. Update operator ETH snapshot — ref SPEC §10 "Fee Settlement Rule": settles at old fee (or default if just assigned) up to this block; new fee applies only to future blocks +3. Set `operator.ethFee = packed(newFee)` +4. Delete any pending fee change request + +#### Events +```solidity +// If ensureETHDefaults assigned default (legacy SSV operator): +emit OperatorFeeExecuted(owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE); + +// Always: +emit OperatorFeeExecuted(owner, operatorId, block.number, fee); +``` + +#### Special Cases +- **Legacy SSV operator** (`ethSnapshot.block == 0`, `SSV fee > 0`): Gets `DEFAULT_OPERATOR_ETH_FEE` assigned, then reduced to `newFee` +- **Explicit zero fee**: After `ethSnapshot.block > 0`, operator can set `ethFee = 0` via `reduceOperatorFee(operatorId, 0)`. This explicit zero is preserved during cluster migration. +- **Zero-fee operator** (`SSV fee == 0`): No default assigned, stays at `ethFee = 0` + +#### Postcondition Invariants +- `operator.ethFee < previous ethFee` (strictly less) +- `ethSnapshot.block > 0` (always initialized after this call) +- No pending fee change request + +--- + +### 4.6 Cancel Declared Operator Fee + +**Caller:** Operator owner + +#### Preconditions +- Operator must exist +- Caller must be operator owner +- A pending fee change request must exist (`approvalBeginTime != 0`) + +#### State Mutations +1. Delete the pending `OperatorFeeChangeRequest` for this operator + +#### Events +```solidity +emit OperatorFeeDeclarationCancelled(owner, operatorId); +``` + +#### Postcondition Invariants +- No pending fee change request for this operator +- Operator's current fee is unchanged + +--- + +### 4.7 Withdraw Operator Earnings (ETH) + +**Caller:** Operator owner +**nonReentrant:** Yes + +#### Preconditions +- Operator must exist +- `operator.ethSnapshot.block != 0` (operator must be ETH-initialized; legacy SSV-only operators revert with `InsufficientBalance`) +- `amount <= accumulated ETH earnings` + +#### State Mutations +1. Update ETH snapshot (accumulate latest earnings) +2. Deduct `amount` from snapshot balance +3. Transfer `amount` ETH to operator owner + +#### Events +```solidity +emit OperatorWithdrawn(owner, operatorId, amount); +``` + +#### Postcondition Invariants +- `operator.ethSnapshot.balance == previous_settled - amount` +- `owner.balance == previous + amount` +- `contract.balance == previous - amount` +- `operator.ethSnapshot.block` unchanged (remains non-zero) +- `operator.snapshot.block` unchanged (SSV state untouched) + +--- + +### 4.8 Withdraw Operator Earnings (SSV) + +Same as 4.7 but for SSV-denominated earnings. SSV token transferred instead of ETH. + +#### Preconditions +- Operator must exist +- `operator.snapshot.block != 0` (operator must be SSV-initialized; ETH-only operators revert with `InsufficientBalance`) +- `amount <= accumulated SSV earnings` + +#### Events +```solidity +emit OperatorWithdrawnSSV(owner, operatorId, amount); +``` + +#### Postcondition Invariants +- `operator.snapshot.balance == previous_settled - amount` +- `operator.ethSnapshot.block` unchanged (ETH state untouched) + +--- + +### 4.9 Withdraw All Operator Earnings (ETH + SSV) + +**Caller:** Operator owner +**nonReentrant:** Yes + +#### Preconditions +- Operator must exist + +#### State Mutations +Each version branch is evaluated independently: +- If `operator.snapshot.block != 0`: update SSV snapshot, capture and zero `snapshot.balance` +- If `operator.ethSnapshot.block != 0`: update ETH snapshot, capture and zero `ethSnapshot.balance` +- Transfer ETH earnings to operator owner (if captured amount non-zero) +- Transfer SSV token earnings to operator owner (if captured amount non-zero) + +A legacy SSV-only operator (`snapshot.block != 0`, `ethSnapshot.block == 0`) runs only the SSV branch — `ethSnapshot.block` is **never written**, preserving the legacy state. An ETH-only operator runs only the ETH branch for the same reason. + +#### Events +```solidity +emit OperatorWithdrawn(owner, operatorId, ethAmount); // ETH portion, only if ethAmount > 0 +emit OperatorWithdrawnSSV(owner, operatorId, ssvAmount); // SSV portion, only if ssvAmount > 0 +``` + +#### Postcondition Invariants +- `operator.ethSnapshot.balance == 0` (if ETH branch ran) +- `operator.snapshot.balance == 0` (if SSV branch ran) +- `operator.ethSnapshot.block` unchanged if `ethSnapshot.block` was `0` before call +- `operator.snapshot.block` unchanged if `snapshot.block` was `0` before call +- `owner.balance == previous + ethEarnings` +- `owner.ssvBalance == previous + ssvEarnings` +- `contract.balance == previous - ethEarnings` + +--- + +## 5. Staking Flows + +### 5.1 Stake SSV + +**Caller:** Anyone with SSV tokens +**nonReentrant:** Yes + +#### Preconditions +- `amount > 0` +- `amount >= MINIMAL_STAKING_AMOUNT` (1,000,000,000) +- User has approved SSV token transfer to contract + +> **Note — cSSV supply cap:** `cSSV.totalSupply` can never exceed `SSV.totalSupply` by construction. `mint(amount)` is only called after `transferFrom` succeeds, so cSSV is always backed 1:1 by SSV already held in the contract. No explicit supply cap check is needed. + +#### State Mutations +1. `_syncFees()`: Update `accEthPerShare` with latest DAO ETH earnings +2. `_settle(msg.sender)`: Settle pending rewards for user +3. Transfer `amount` SSV tokens from user to contract +4. Mint `amount` cSSV to user + +#### Events +```solidity +emit FeesSynced(newFeesWei, accEthPerShare); +emit RewardsSettled(user, pending, accrued, userIndex); +emit Staked(user, amount); +``` + +#### Postcondition Invariants +- `cSSV.totalSupply() == previous + amount` +- `cSSV.balanceOf(user) == previous + amount` +- `ssvToken.balanceOf(contract) == previous + amount` +- `ssvToken.balanceOf(user) == previous - amount` +- `userIndex[user] == accEthPerShare` (freshly settled) +- User begins earning pro-rata rewards immediately + +--- + +### 5.2 Request Unstake + +**Caller:** cSSV holder +**nonReentrant:** Yes + +> **Overview:** Multi-request unstaking with per-request cooldown. Ref: SPEC §3 "Unstaking (Two-Step)" for full semantics. + +#### Preconditions +- `amount > 0` +- `amount <= cSSV.balanceOf(msg.sender)` +- Pending unstake requests < MAX_PENDING_REQUESTS (2000) + +#### State Mutations +1. `_syncFees()`: Update `accEthPerShare` +2. `_settleWithBalance(user, balance)`: Settle rewards using CURRENT cSSV balance (before burn) +3. Push `UnstakeRequest{amount, unlockTime: block.timestamp + cooldownDuration}` +4. Burn `amount` cSSV from user + +#### Events +```solidity +emit FeesSynced(newFeesWei, accEthPerShare); +emit RewardsSettled(user, pending, accrued, userIndex); +emit UnstakeRequested(user, amount, unlockTime); +``` + +#### Postcondition Invariants +- `cSSV.totalSupply() == previous - amount` +- `cSSV.balanceOf(user) == previous - amount` +- `withdrawalRequests[user].length == previous + 1` +- Rewards STOP accruing for the burned cSSV portion +- Previously accrued rewards remain claimable +- SSV tokens are NOT yet returned (locked until cooldown) + +#### Request Unstake -> Claim Rewards Interaction + +When user calls `requestUnstake(amount)`: +1. Settlement happens BEFORE cSSV burn -> pending rewards added to s.accrued +2. cSSV is burned -> balanceOf decreases + +If user then calls `claimEthRewards`: +- If they unstaked ALL cSSV: balanceOf == 0 -> dust forfeited +- If they unstaked PARTIAL cSSV: balanceOf > 0 -> remainder preserved + +--- + +### 5.3 Withdraw Unlocked + +**Caller:** User with matured unstake requests +**nonReentrant:** Yes + +> **Overview:** Finalizes all matured unstake requests in one call. Ref: SPEC §3 "Unstaking (Two-Step)" for full semantics. + +#### Preconditions +- At least one `UnstakeRequest` where `unlockTime <= block.timestamp` — reverts with `NothingToWithdraw` if none exist or all are still within cooldown + +#### State Mutations +1. Iterate **all** withdrawal requests in a single pass; remove every matured entry via swap-and-pop (O(1) per removal, order of remaining entries may change) +2. Sum total unlocked amount across all removed entries (`totalAmount = Σ matured request amounts`) +3. Transfer `totalAmount` SSV tokens to user + +> **Note:** Immature requests (where `unlockTime > block.timestamp`) remain untouched in the array and will be processed in a future `withdrawUnlocked` call after their lock period expires. + +#### Events +```solidity +emit UnstakedWithdrawn(user, totalAmount); +``` + +#### Postcondition Invariants +- `ssvToken.balanceOf(user) == previous + totalAmount` +- `ssvToken.balanceOf(contract) == previous - totalAmount` +- All matured requests removed from array +- Immature requests preserved + +--- + +### 5.4 Claim ETH Rewards + +**Caller:** cSSV holder +**nonReentrant:** Yes + +#### Preconditions +- User has s.accrued[user] > 0 OR pending rewards from s.accEthPerShare growth + +#### State Mutations +1. `_syncFees()`: Update `accEthPerShare` +2. `_settle(user)`: Settle latest rewards +3. Read `claimable = accrued[user]`; if `claimable == 0` revert `NothingToClaim` +4. Compute payout: `payout = claimable - (claimable % 100_000)` (precision truncation) +5. Read `userBalance = cSSV.balanceOf(user)` +6. If `payout == 0`: + - If `userBalance == 0`: set `accrued[user] = 0`, emit `RewardsClaimed(user, 0)`, return + - If `userBalance > 0`: revert `NothingToClaim` (state unchanged due revert) +7. If `payout > 0`: set `remainder = claimable - payout` +8. Set `accrued[user]`: + - If `remainder > 0 && userBalance == 0`: zero remainder (forfeit dust) + - Else: store `remainder` +9. Deduct `packed(payout)` from `stakingEthPoolBalance` +10. Deduct `packed(payout)` from `sp.ethDaoBalance` +11. Transfer `payout` ETH to user + +#### Events +```solidity +emit FeesSynced(newFeesWei, accEthPerShare); +emit RewardsSettled(user, pending, accrued, userIndex); +emit RewardsClaimed(user, payout); +``` + +#### Postcondition Invariants +- If `payout > 0`: `user.balance == previous + payout` +- If `payout > 0`: `contract.balance == previous - payout` +- If `payout > 0`: `stakingEthPoolBalance` decreased by packed(payout) +- If `payout > 0`: `ethDaoBalance` decreased by packed(payout) +- If `payout > 0` and `cSSV.balanceOf(user) > 0`: `accrued[user] == remainder` +- If `payout > 0` and `cSSV.balanceOf(user) == 0`: `accrued[user] == 0` (dust forfeited) +- If `payout == 0` and `cSSV.balanceOf(user) == 0`: `accrued[user] == 0`, `RewardsClaimed(user, 0)` emitted, no pool/DAO deductions +- If `payout == 0` and `cSSV.balanceOf(user) > 0`: call reverts with `NothingToClaim` (no state changes) + +--- + +### 5.5 Sync Fees + +**Caller:** Anyone +**nonReentrant:** Yes + +#### Purpose +Publicly callable function to update the global `accEthPerShare` without settling any specific user. Useful for keeping the accumulator current. + +#### State Mutations +1. Compute current DAO ETH earnings +2. If new fees since last sync: update `accEthPerShare` and `stakingEthPoolBalance` + +#### Events +```solidity +emit FeesSynced(newFeesWei, accEthPerShare); +``` + +--- + +### 5.6 cSSV Transfer (Reward Settlement Hook) + +**Caller:** Any cSSV holder (triggered automatically on ERC-20 transfer) +**nonReentrant:** No (hook is called from within the cSSV token contract) + +#### Purpose +Ensures that rewards accrued by the sender up to the moment of transfer remain claimable by the sender, and that the receiver starts accruing rewards only from the moment they receive cSSV. Without this hook, a receiver could claim rewards earned before they held the tokens. + +#### Hook Trigger +`CSSVToken._beforeTokenTransfer` calls `SSVStaking.onCSSVTransfer(from, to, amount)` before every transfer, **except**: +- Mint (`from == address(0)`) +- Burn (`to == address(0)`) +- Self-transfer (`from == to`) +- Calls originating from the staking contract itself (`msg.sender == ssvStaking`) — covers internal mint/burn during `stake` and `requestUnstake` + +#### State Mutations +1. `_syncFees()`: Update global `accEthPerShare` with latest DAO ETH earnings +2. `_settle(from)`: Snapshot sender's accrued rewards at current `accEthPerShare` using their **pre-transfer** balance +3. `_settle(to)`: Snapshot receiver's accrued rewards at current `accEthPerShare` using their **pre-transfer** balance + +After the hook returns, the ERC-20 transfer executes, changing both balances. Future `_settle` calls will compute rewards from the new balances, but only from this block forward. + +#### Events +None emitted by the hook itself. The ERC-20 `Transfer` event is emitted by the token contract after the hook. + +#### Postcondition Invariants +- `userIndex[from] == accEthPerShare` (sender's rewards locked in at pre-transfer share) +- `userIndex[to] == accEthPerShare` (receiver starts accruing from now, not before) +- `accrued[from]` includes all rewards earned up to this block +- `accrued[to]` includes all rewards earned up to this block (on their existing balance, if any) +- If sender's cSSV balance reaches 0 after the transfer, `accrued[from]` is still non-zero and fully claimable via `claimEthRewards()` — rewards are stored in `accrued` independently of cSSV balance + +--- + +## 6. DAO Governance Flows + +### 6.1 Update Network Fee + +**Caller:** Owner only + +#### State Mutations +1. Settle current ETH DAO earnings up to current block +2. Update `ethNetworkFee` to new value +3. Update `ethNetworkFeeIndex` to current +4. Update `ethNetworkFeeIndexBlockNumber` to current block + +#### Events +```solidity +emit NetworkFeeUpdated(oldFee, newFee); +``` + +#### Postcondition Invariants +- All fee accrual up to this block uses old fee +- All fee accrual from this block forward uses new fee +- DAO earnings are settled (no gap or double-counting) + +--- + +### 6.2 Replace Oracle + +**Caller:** Owner only + +#### State Mutations +1. Clear old oracle's `oracleIdOf` mapping +2. Set new oracle's `oracleIdOf` mapping +3. Update `oracles[oracleId]` to new address + +#### Events +```solidity +emit OracleReplaced(oracleId, oldOracle, newOracle); +``` + +#### Postcondition Invariants +- Old oracle can no longer call `commitRoot` +- New oracle can call `commitRoot` +- Outstanding votes by old oracle for pending commitments remain counted + +--- + +## Global Invariants (Must Always Hold) + +These invariants should be verified across all flows: + +1. **ETH conservation**: `contract.ETH_balance ≈ Σ(current ETH cluster balances) + Σ(current operator ETH earnings) + ProtocolLib.networkTotalEarnings()` +2. **SSV conservation**: `contract.SSV_balance ≈ Σ(current SSV cluster balances) + Σ(current operator SSV earnings) + networkTotalEarningsSSV() + stakingHeldSSV` +Where: + - “current” means the view‑computed balances that apply pending fees (see `contracts/modules/SSVViews.sol`). + - `stakingHeldSSV` = total SSV still locked in the `SSVNetwork` contract, including pending unstake requests. + - `cSSV.totalSupply()` is only equal to `stakingHeldSSV` when there are no pending unstake requests. + +3. **Validator count consistency**: `ethDaoValidatorCount == Σ(cluster.validatorCount)` across all active ETH clusters — note: `Σ(operator.ethValidatorCount)` is NOT equivalent because operators are shared across clusters and would double-count +4. **vUnit consistency**: `daoTotalEthVUnits == ethDaoValidatorCount * BPS_DENOMINATOR + Σ(cluster_deviations)` +5. **Cluster hash integrity**: Every cluster operation must end with `s.ethClusters[key] = cluster.hashClusterData()` matching the actual cluster state +6. **cSSV supply**: `cSSV.totalSupply() == Σ(all staked SSV that has not been unstake-requested)` +7. **Rewards conservation**: `accEthPerShare` only increases, never decreases +8. **Oracle monotonicity**: `latestCommittedBlock` only increases +9. **Cluster version exclusivity**: A cluster key exists in EITHER `s.clusters` OR `s.ethClusters`, never both +10. **Operator dual tracking**: SSV validatorCount + ETH validatorCount == total validators using this operator diff --git a/docs/SPEC.md b/docs/SPEC.md new file mode 100644 index 000000000..6fc8732b8 --- /dev/null +++ b/docs/SPEC.md @@ -0,0 +1,1248 @@ +# SSV Network v2.0.0 — Technical Specification + +This document is the **source of truth** for design intent, rules, and accounting formulas for the SSV Staking upgrade (v2.0.0), derived from the DIP-X proposal. For step-by-step execution flows and implementation verification, see [FLOWS.md](./FLOWS.md). + +| Document | Purpose | +|---|---| +| **SPEC.md** (this file) | Design intent · rules · formulas · invariants · source of truth | +| **FLOWS.md** | Step-by-step execution · preconditions · state mutations · test checklist | + +### Task Mapping Guide + +When working on a BUG-X, TEST-Y, or FUZZ-Z task, use this map to find the relevant documentation: + +| Task area | FLOWS section | SPEC section | +|---|---|---| +| Cluster operations (register, remove, deposit, withdraw, liquidate, reactivate) | §1 Cluster Flows | §1 ETH Payments, §2 Effective Balance Accounting | +| Migration (SSV → ETH) | §2 Migration Flows | §1 ETH Payments — Cluster Migration | +| Effective balance / oracle | §3 Effective Balance Flows | §4 Oracle System | +| Operator operations (fees, earnings, whitelist) | §4 Operator Flows | §10 Accounting Formulas — Fee Settlement Rule | +| Staking / unstaking / rewards | §5 Staking Flows | §3 SSV Staking | +| DAO governance | §6 DAO Governance Flows | §11 Governance Parameters | +| Accounting verification | §1.8 Accounting Invariant | §10 Accounting Formulas | +| Access control | §9 Access Control Matrix | §9 Access Control Matrix | +| Error codes | — | §12 Error Codes | +| Constants | — | §13 Constants | + +### Decision Trees + +Use these to quickly locate the right section when resolving a BUG/TEST/FUZZ task. Questions are grouped by topic. + +--- + +#### Cluster Accounting + +**Q: How do I calculate what a cluster currently owes in fees?** +- ETH cluster → SPEC §10 "ETH Cluster Balance Update" + FLOWS §1.1 State Mutations +- SSV cluster (legacy) → SPEC §10 "SSV Cluster Balance Update (Legacy)" + +**Q: What is `cluster.index` and `cluster.networkFeeIndex`?** +- Snapshots of the cumulative operator/network fee indices at the last settlement point. Current debt = `(currentIndex - cluster.index) * vUnits` → SPEC §10 "Accounting Formulas" + +**Q: What is `vUnits` and how does it relate to ETH?** +- Internal accounting unit: `vUnits = ceil(effectiveBalanceETH * 10_000 / 32)`. 1 validator at 32 ETH = 10,000 vUnits → SPEC §2 "vUnit System" + +**Q: When does a cluster switch from implicit to explicit EB?** +- On first successful `updateClusterBalance` call with a valid Merkle proof. Before that, `clusterEB.vUnits == 0` and the system uses `validatorCount * BPS_DENOMINATOR` → SPEC §2 "Implicit vs Explicit EB" + +**Q: Does EB affect SSV legacy cluster fee calculations?** +- No. SSV clusters store the EB snapshot (for future migration) but fees continue using `validatorCount * fee`. EB only affects ETH cluster accounting → SPEC §2 "Implicit vs Explicit EB" note + +**Q: Can a liquidated cluster withdraw ETH?** +- Yes — `withdraw` does not require an active cluster. Fee settlement is skipped; balance is deducted directly → FLOWS §1.8 preconditions + +**Q: Can a liquidated cluster receive deposits?** +- Yes — `deposit` has no active-cluster check. Useful for funding a cluster in preparation for reactivation → FLOWS §1.7, SPEC §1 "Existing Clusters" + +**Q: What is the minimum ETH required to reactivate or migrate a cluster?** +- `max(minimumLiquidationCollateral, burnRateThreshold)` where `burnRateThreshold = minimumBlocksBeforeLiquidation * totalBurnRate * vUnits / BPS_DENOMINATOR * ETH_DEDUCTED_DIGITS` → SPEC §1 "Minimum ETH Calculation" + +--- + +#### Effective Balance & Oracle + +**Q: When is the EB snapshot updated?** +- When a valid proof exists in the latest committed root. The contract can write `clusterEB.vUnits` even for liquidated clusters, but in production inactive/liquidated clusters are omitted from oracle roots, so they typically cannot be updated until they become active again → SPEC §4 "Behavior on liquidated clusters" + +**Q: Does `updateClusterBalance` auto-liquidate?** +- Only for active ETH clusters. If the cluster becomes undercollateralized after the EB update, it is auto-liquidated within the same call → SPEC §4 "Update Flow" step 7 + +**Q: What happens if oracle quorum is not reached?** +- The `commitRoot` call does NOT revert — it emits `WeightedRootProposed` and persists the partial vote. The root is only committed (and `RootCommitted` emitted) when accumulated weight reaches quorum → SPEC §4 "Failed Quorum Behavior" + +**Q: Can oracles re-vote on the same block number with a different root?** +- Yes — `commitmentKey = keccak256(blockNum, merkleRoot)`, so a different root = a different key. Oracles cannot re-vote on the exact same `(blockNum, merkleRoot)` pair → SPEC §4 "Failed Quorum Behavior" + +**Q: What is the risk of reactivating a cluster with a stale EB snapshot?** +- If EB increased during liquidation: solvency check passes with less ETH than needed → risk of auto-liquidation on the first allowed post-reactivation `updateClusterBalance`. If EB decreased, the owner may overfund. Because inactive clusters are omitted from the root, the practical mitigation is off-chain EB awareness and conservative funding, not an on-chain pre-reactivation update → SPEC §2 "Stale EB Risk on Reactivation" + +**Q: How is the Merkle leaf encoded?** +- `keccak256(keccak256(abi.encode(clusterID, effectiveBalance)))` where `effectiveBalance` is `uint32` in whole ETH and `clusterID = keccak256(abi.encodePacked(owner, sortedOperatorIds))` → SPEC §4 "Merkle Tree Structure" + +--- + +#### Operator Fees & Earnings + +**Q: Which fee rate applies after `executeOperatorFee` or `reduceOperatorFee`?** +- Old rate up to (and including) the current block; new rate from the next block onward. The ETH snapshot is settled at the old rate before the new fee is stored → SPEC §10 "Fee Settlement Rule" + +**Q: How is operator ETH earnings balance computed?** +- `operator.ethSnapshot.balance + (block.number - ethSnapshot.block) * PackedETH.unwrap(operator.ethFee) * ethValidatorCount` — but scaled by vUnits for EB-weighted clusters → SPEC §10 "ETH Operator Fee Index" + +**Q: What happens to operator earnings when an operator is removed?** +- Final SSV and ETH snapshots are settled and stored. Earnings remain withdrawable by the owner even after removal. `operator.owner` is preserved (non-zero) → FLOWS §4.2 State Mutations + +**Q: Can an ETH-only operator call `withdrawOperatorEarningsSSV`?** +- No — `_withdrawOperatorEarnings(VERSION_SSV)` reverts with `InsufficientBalance` if `operator.snapshot.block == 0`. Similarly, a legacy SSV-only operator calling `withdrawOperatorEarnings` / `withdrawAllOperatorEarnings` (VERSION_ETH) reverts with `InsufficientBalance` if `operator.ethSnapshot.block == 0`. These guards prevent snapshot state corruption and eliminate the SEC-18 no-op concern → FLOWS §4.7, §4.8 + +**Q: What is `DEFAULT_OPERATOR_ETH_FEE` and when is it applied?** +- 1,770,000,000 wei/block/validator. Applied automatically via `ensureETHDefaults` on first ETH interaction for legacy SSV operators (SSV fee > 0, ethSnapshot.block == 0). Also called by `declareOperatorFee` and `reduceOperatorFee` before fee changes. Operators with SSV fee = 0 get ETH fee = 0. See SPEC §1 "Operator Fee Transition" for complete behavior → SPEC §1 "Operator Fee Transition" + +--- + +#### Staking & Rewards + +**Q: How are ETH rewards distributed to stakers?** +- Accumulator pattern: `accEthPerShare` grows as DAO earns ETH. On `settle(user)`: `pending = cSSVBalance * (accEthPerShare - userIndex) / 1e18`. Rewards stop accruing for burned cSSV → SPEC §3 "Reward Distribution" + +**Q: What happens to rewards when cSSV is transferred?** +- `_beforeTokenTransfer` hook calls `onCSSVTransfer(from, to, amount)` which settles both sender and receiver before the transfer. Rewards earned up to that point stay with the sender → SPEC §3 "cSSV Token Behavior", FLOWS §5.6 + +**Q: How many unstake requests can be pending at once?** +- Up to `MAX_PENDING_REQUESTS = 2000` per user. Exceeding this reverts with `MaxRequestsAmountReached` → SPEC §3 "Unstaking (Two-Step)" + +**Q: Does `withdrawUnlocked` process all matured requests or just one?** +- All matured requests in a single call (swap-and-pop iteration). Immature requests remain untouched → SPEC §3 "Unstaking (Two-Step)" + +**Q: What is the minimum stake amount?** +- `MINIMAL_STAKING_AMOUNT = 1,000,000,000` SSV wei → SPEC §13 "Constants" + +**Q: What happens if `syncFees` is called when `totalStaked == 0`?** +- `accEthPerShare` is not updated (division by zero avoided). DAO balance is still updated. Fees accrued during this period are effectively lost to stakers (see BUG-6) → FLOWS §5.5 + +--- + +#### Cluster Lifecycle & Versioning + +**Q: How do I tell if a cluster is ETH or SSV?** +- Check `validateHashedCluster` return value: `version == VERSION_ETH` (2) → ETH cluster in `s.ethClusters`; `version == VERSION_SSV` (1) → SSV cluster in `s.clusters` → SPEC §6 "Type System & Packing" + +**Q: What operations are blocked on legacy SSV clusters?** +- Blocked: `registerValidator`, `bulkRegisterValidator`, `reactivate`, `deposit` (SSV), `withdraw` (SSV), `liquidate` (ETH path) +- Allowed: `removeValidator`, `bulkRemoveValidator`, `exitValidator`, `bulkExitValidator`, `liquidateSSV`, `migrateClusterToETH`, `updateClusterBalance` → SPEC §1 "Existing Clusters" + +**Q: What happens to removed operators in a cluster?** +- Removed operators are skipped during `updateClusterOperatorsOnReactivation` and migration. The cluster operates with reduced operator coverage (e.g., 3/4). No on-chain event signals which operators were skipped — detectable off-chain by checking operator states → FLOWS §1.8 note, SPEC §1 "Minimum ETH Calculation" special cases + +**Q: How do `removeValidator` / `bulkRemoveValidator` behave on legacy SSV clusters?** +- They execute against `s.clusters` (VERSION_SSV), settle SSV accounting with SSV indices when cluster is active, and update SSV operator/DAO validator counters. +- If an SSV cluster is already liquidated, remove operations still delete validator keys and decrement cluster `validatorCount`, but they do not decrement SSV operator/DAO counts again. +- Legacy SSV remove paths do not update EB-specific storage (`clusterEB`, `operatorEthVUnits`), which is ETH-branch accounting only. + +**Q: Can a cluster be reactivated after migration to ETH?** +- Migration is one-way and irreversible. A migrated cluster that is later liquidated can be reactivated via `reactivate` (ETH flow) → SPEC §1 "Cluster Migration" + +--- + +#### Storage & Data Structures + +**Q: Where is ETH cluster state stored vs SSV cluster state?** +- ETH clusters: `StorageData.ethClusters[hashedCluster]` (hashed `Cluster` struct) +- SSV clusters: `StorageData.clusters[hashedCluster]` +- Both use the same key: `keccak256(abi.encodePacked(owner, sortedOperatorIds))` → SPEC §5 "Storage Layout" + +**Q: Where is EB data stored?** +- `SSVStorageEB.clusterEB[clusterId]` → `ClusterEBSnapshot{vUnits, lastRootBlockNum, lastUpdateBlock}` +- `SSVStorageEB.operatorEthVUnits[operatorId]` → deviation vUnits per operator +- `SSVStorageEB.ebRoots[blockNum]` → committed Merkle root → SPEC §5 "SSVStorageEB" + +**Q: How is `PackedETH` different from raw wei?** +- `PackedETH` stores values divided by `ETH_DEDUCTED_DIGITS` (100,000) to fit in `uint64`. Unpack with `PackedETH.unwrap(x)` which multiplies by 100,000. Operator fees must be divisible by 100,000 → SPEC §6 "Type System & Packing" + +**Q: What does `operator.snapshot.block == 0 && operator.ethSnapshot.block == 0` mean?** +- The operator has been removed (`_resetOperatorState` zeroed all fields except `owner`). Such operators are skipped during cluster operations → SPEC §1 "Minimum ETH Calculation" special cases + +### Version Delta (v1.x → v2.0.0) + +| Area | v1.x | v2.0.0 | +|---|---|---| +| Payment token | SSV | ETH (new clusters); SSV (legacy) | +| Fee unit | SSV/block/validator | ETH/block/validator, scaled by vUnits (EB) | +| Cluster creation | SSV deposit | ETH deposit via `msg.value` | +| Validator count scaling | flat per-validator | EB-weighted via vUnits | +| Operator earnings | SSV | ETH (new) + SSV (legacy accrual continues) | +| Staking | none | SSV → cSSV, earns ETH rewards from network fees | +| Oracle | none | Merkle-root EB oracle with quorum voting | +| Liquidation collateral | SSV-denominated | SSV-denominated (legacy SSV clusters) and ETH-denominated, EB-aware | +| SSV cluster operations | full | partial: remove/bulkRemove/exit/liquidateSSV/migrate/updateClusterBalance allowed; register/deposit/withdraw/reactivate blocked | +| Withdraw from liquidated | blocked | allowed (ETH clusters) | + +### Related Documents + +- [FLOWS.md](./FLOWS.md): Step-by-step contract flows for all external functions. + +## Table of Contents + +1. [ETH Payments](#1-eth-payments) +2. [Effective Balance Accounting](#2-effective-balance-accounting) +3. [SSV Staking](#3-ssv-staking) +4. [Oracle System](#4-oracle-system) +5. [Storage Layout](#5-storage-layout) +6. [Type System & Packing](#6-type-system--packing) +7. [All Events](#7-all-events) +8. [All External Functions](#8-all-external-functions) +9. [Access Control Matrix](#9-access-control-matrix) +10. [Accounting Formulas](#10-accounting-formulas) +11. [Global Invariants](#11-global-invariants) +12. [Governance Parameters](#12-governance-parameters) +13. [Error Codes](#13-error-codes) +14. [Constants](#14-constants) + +--- + +## 1. ETH Payments + +### Overview + +ETH replaces SSV as the payment asset for network and operator fees. All new clusters operate exclusively with ETH. Existing SSV clusters are legacy — they cannot add/remove validators, deposit SSV, or reactivate. The only forward path is migration to ETH. + +### New Clusters (ETH-based) + +- Operator fees paid in ETH +- Network fees paid in ETH +- Operates with EB accounting +- ETH deposited upfront for runway +- Fees scale with effective balance (vUnits), not validator count + +### Existing Clusters (SSV-based, Legacy) + +- Continue running with existing SSV runway +- **Blocked operations**: add validators, remove validators, reactivate, deposit SSV, withdraw SSV +- **Allowed operations**: self-liquidate, migrate to ETH, exit validators +- SSV fee accrual continues normally until runway depletes or migration occurs + +### Cluster Migration (`migrateClusterToETH`) + +- One-way, irreversible +- Single transaction: switches accounting from SSV to ETH +- Only callable by the cluster owner +- Remaining SSV balance refunded to cluster owner +- ETH deposited via `msg.value` as new cluster balance +- Must pass ETH liquidation check post-migration or reverts with `InsufficientBalance` + +**Minimum ETH Calculation (Post-Migration Liquidation Check):** + +The migrated cluster must have sufficient balance to avoid immediate liquidation. The minimum required ETH is computed in steps: + +``` +Step 1: Compute vUnits (EB-normalized accounting units) + vUnits = clusterEB[clusterId].vUnits + if (vUnits == 0): + vUnits = validatorCount * BPS_DENOMINATOR // implicit EB (32 ETH/validator) + +Step 2: Compute total burn rate (operator fees + network fee) + operatorFeeSum = Σ(operator.ethFee) for all operators in cluster // packed wei/block + networkFee = ethNetworkFee // packed wei/block + totalBurnRate = operatorFeeSum + networkFee // packed wei/block + +Step 3: Compute burn-rate-based threshold (how much ETH consumed over liquidation period) + burnRateThresholdUnits = (minimumBlocksBeforeLiquidation * totalBurnRate * vUnits) / BPS_DENOMINATOR + burnRateThreshold = burnRateThresholdUnits * ETH_DEDUCTED_DIGITS // convert to wei + +Step 4: Take maximum of both thresholds + minimumETHRequired = max(minimumLiquidationCollateral, burnRateThreshold) +``` + +**Special Cases:** +- With zero-fee operators: `operatorFeeSum = 0`, so `totalBurnRate = networkFee` only +- The absolute floor is always `minimumLiquidationCollateral` (currently 0.00094 ETH) +- **Removed operators** are skipped during migration (detected by `operator.snapshot.block == 0 && operator.ethSnapshot.block == 0`; their fees do not contribute to `operatorFeeSum`) +- Reactivates a liquidated cluster and emits the `ClusterReactivated` event in addition to `ClusterMigratedToETH` + +### Operator Fee Transition + +**New operators**: Register with ETH fee only (no SSV fee option) + +**Existing operators (Legacy SSV Operators)**: +- SSV fees frozen (cannot modify) +- SSV fee accrual continues for non-migrated clusters +- Default ETH fee assigned automatically on **first ETH interaction** via `ensureETHDefaults`: + - If SSV fee = 0 → ETH fee = 0 + - If SSV fee > 0 → ETH fee = `DEFAULT_OPERATOR_ETH_FEE` (1,770,000,000 wei = ~0.00464 ETH/year per 32 ETH validator) + +**`ensureETHDefaults` is called in:** +- `migrateClusterToETH` (for all operators in the cluster) +- `registerValidator` / `bulkRegisterValidator` (for all operators in the cluster, ETH clusters only) +- `declareOperatorFee` (before declaring new fee) +- `reduceOperatorFee` (before reducing fee) + +**Behavior:** +- Initializes `operator.ethSnapshot.block = block.number` (if currently 0) +- Assigns `operator.ethFee = DEFAULT_OPERATOR_ETH_FEE` **only if** `ethFee == 0 && SSV fee > 0` +- Emits `OperatorFeeExecuted(owner, operatorId, block.number, DEFAULT_OPERATOR_ETH_FEE)` when default is assigned +- After initialization (`ethSnapshot.block > 0`), operators can explicitly set `ethFee = 0` via `reduceOperatorFee(operatorId, 0)` +- Explicit zero fees are preserved during migration (no overwrite to default) + +### Breaking Function Signature Changes + +| Old Signature | New Signature | Change | +|---|---|---| +| `registerValidator(..., uint256 amount, Cluster)` | `registerValidator(..., Cluster) payable` | `amount` removed, now `payable` | +| `bulkRegisterValidator(..., uint256 amount, Cluster)` | `bulkRegisterValidator(..., Cluster) payable` | `amount` removed, now `payable` | +| `deposit(..., uint256 amount, Cluster)` | `deposit(..., Cluster) payable` | `amount` removed, now `payable` | +| `reactivate(..., uint256 amount, Cluster)` | `reactivate(..., Cluster) payable` | `amount` removed, now `payable` | +| `getBalance(...) returns (uint256)` | `getBalance(...) returns (uint256, uint256)` | Now also returns `ebBalance` | + +--- + +## 2. Effective Balance Accounting + +### Overview + +Fees are calculated based on a cluster's total effective balance rather than validator count. Effective balance is always an integer number of ETH (e.g. 32 ETH, 64 ETH) — fractional values are not valid, matching the beacon chain's own representation. This supports post-Pectra validators with variable effective balances (32–2048 ETH per validator). + +### vUnit System + +vUnits are the internal accounting unit that normalizes effective balance: + +``` +ETH → vUnits (ceiling): vUnits = ceil(effectiveBalanceETH * BPS_DENOMINATOR / 32) +vUnits → ETH (floor): effectiveBalanceETH = floor(vUnits * 32 / BPS_DENOMINATOR) + +BPS_DENOMINATOR = 10,000 +``` + +Examples: +- 1 validator at 32 ETH → 10,000 vUnits +- 1 validator at 64 ETH → 20,000 vUnits +- 3 validators at 32 ETH each → 30,000 vUnits + +### Implicit vs Explicit EB + +- **Implicit** (default): `clusterEB.vUnits == 0` → system uses `validatorCount * BPS_DENOMINATOR` +- **Explicit**: Set after first `updateClusterBalance` call with oracle Merkle proof + +> **Note — EB tracking vs EB-based accounting:** While both ETH and SSV clusters can have their EB snapshot updated via `updateClusterBalance`, **only ETH clusters use EB for fee accounting**. SSV legacy clusters store the EB snapshot (for future migration) but continue to use validator-count-based fee calculations (`validatorCount * fee`). The EB snapshot does not affect SSV cluster balance deductions. + +### EB Update Constraints + +- `effectiveBalance >= validatorCount * 32` (minimum 32 ETH per validator) +- `effectiveBalance <= validatorCount * 2048` (maximum 2048 ETH per validator) +- Block numbers must be strictly monotonically increasing +- Minimum blocks between updates enforced (`minBlocksBetweenUpdates`) +- **Latest-root-only enforcement**: `blockNum` must equal `latestCommittedBlock` — prevents stale root griefing attacks + +### DAO vUnit Tracking + +``` +daoTotalEthVUnits = ethDaoValidatorCount * BPS_DENOMINATOR + Σ(cluster_deviations) +``` + +Where deviation = `cluster.vUnits - (cluster.validatorCount * BPS_DENOMINATOR)` for clusters with explicit EB. + +### Operator vUnit Deviation Cleanup on Liquidation + +When a cluster is liquidated (via `liquidate`, `liquidateSSV`, or auto-liquidation in `updateClusterBalance`): +- **Baseline** is removed by decrementing `operator.ethValidatorCount` for each operator +- **Deviation** (explicit EB above baseline) is removed from `operatorEthVUnits[opId]` and `daoTotalEthVUnits` +- Implicit clusters (`clusterEB.vUnits == 0`) have no deviation — only baseline removal applies + +### Stale EB Risk on Reactivation + +**Oracle behavior:** Oracles build the Merkle tree from active clusters only. When a cluster is liquidated / inactive, it is excluded from the root and `updateClusterBalance` is not called for it by the oracle flow. The contract still supports the code path for updating a liquidated cluster if a valid proof exists, but under the current oracle behavior there is usually no proof available for an inactive cluster in the latest committed root. + +**Why this matters:** During the liquidation period, the beacon-chain EB may diverge from the last on-chain snapshot stored in `clusterEB`. This creates a gap between: +- the **on-chain EB snapshot** used by `reactivate` / liquidated-cluster migration solvency checks, and +- the **real beacon-chain EB** observed off-chain. + +During that gap: + +- **EB increases** (e.g. owner consolidates validators): reactivation solvency check uses stale lower EB → cluster passes with less ETH than required → auto-liquidation risk on next `updateClusterBalance` (if not updated before reactivation) +- **EB decreases** (e.g. slashing): reactivation solvency check uses stale higher EB → cluster owner may deposit more ETH than necessary + +There is also a temporary accounting mismatch after reactivation: until the first successful post-reactivation `updateClusterBalance`, fee settlement continues from the stale on-chain EB snapshot rather than the real beacon-chain EB. + +**Practical mitigation:** Since inactive clusters are omitted from the root, the mitigation is operational/off-chain: +- use beacon-chain-aware tooling to estimate the required deposit from the cluster's actual current EB +- add a conservative ETH buffer when reactivating or migrating a liquidated cluster +- expect the on-chain snapshot to be corrected only after the cluster is active again and included in a later oracle root + +Company-operated or third-party webapps can help here by reading the cluster's actual beacon-chain EB off-chain and suggesting a deposit amount that is safer than the stale on-chain snapshot alone. + +--- + +## 3. SSV Staking + +### Overview + +SSV holders stake tokens → receive cSSV (ERC-20, 1:1 ratio) → earn pro-rata share of ETH protocol revenue (network fees). + +### Staking Flow + +1. User approves SSV token transfer +2. User calls `stake(amount)` — minimum `MINIMAL_STAKING_AMOUNT` (1,000,000,000) SSV wei +3. SSV tokens transferred to contract +4. cSSV minted to user at 1:1 ratio +5. Rewards begin accruing immediately + +### Reward Distribution (Accumulator Pattern) + +```solidity +// On syncFees(): +currentDaoEarnings = sp.networkTotalEarnings() // total ETH DAO has earned +newFees = currentDaoEarnings - stakingEthPoolBalance +accEthPerShare += (unpack(newFees) * 1e18) / cSSV.totalSupply() +stakingEthPoolBalance = currentDaoEarnings + +// On settle(user): +pending = (cSSVBalance * (accEthPerShare - userIndex[user])) / 1e18 +accrued[user] += pending +userIndex[user] = accEthPerShare +``` + +### Claiming Rewards + +- Call `claimEthRewards()` at any time +- Payout truncated to ETH_DEDUCTED_DIGITS precision: `payout = accrued - (accrued % 100_000)` +- If `payout > 0`: deduct `packed(payout)` from both `stakingEthPoolBalance` and `sp.ethDaoBalance`, then transfer ETH to user +- If `payout == 0` and `balanceOf(user) == 0`: zero `accrued[user]`, emit `RewardsClaimed(user, 0)`, and return successfully +- If `payout == 0` and `balanceOf(user) > 0`: revert `NothingToClaim` (remainder preserved) + +#### Dust Handling (ETH_DEDUCTED_DIGITS Rounding) + +ETH rewards are packed to PackedETH (uint64) with precision of 100,000 wei (ETH_DEDUCTED_DIGITS). +When claiming rewards: +- `payout = floor(accrued / 100_000) * 100_000` +- `remainder = accrued - payout` +- If `remainder > 0` AND `balanceOf(user) == 0`: remainder is forfeited (zeroed in s.accrued) +- If `balanceOf(user) > 0`: remainder is preserved for future claims + +**Rationale:** Users with zero cSSV balance cannot accrue future rewards (pending will always be 0). +Therefore, sub-100K wei dust can never grow to claimable amounts and is safely forfeited. +Forfeited dust remains in stakingEthPoolBalance, redistributed to remaining stakers. + +#### claimEthRewards Edge Cases + +- If `accrued == 0`: revert `NothingToClaim` +- If `accrued > 0` but `accrued < ETH_DEDUCTED_DIGITS` and `balanceOf(user) > 0`: remainder preserved, revert `NothingToClaim` (can claim later when accrued grows) +- If `accrued > 0` but `accrued < ETH_DEDUCTED_DIGITS` and `balanceOf(user) == 0`: dust zeroed (forfeited), emit `RewardsClaimed(user, 0)`, return success + +### cSSV Token Behavior + +- Mint: only by SSVStaking on `stake()` +- Burn: only by SSVStaking on `requestUnstake()` +- Transfer hook: `_beforeTokenTransfer` calls `SSVStaking.onCSSVTransfer(from, to, amount)` + - Settles rewards for both sender and receiver before transfer + - Ensures rewards accrued up to transfer point stay with original holder +- Retains full DAO governance voting power + +### Unstaking (Two-Step) + +Stakers may submit multiple withdrawal requests over time. When finalizing an unstake, the staker can claim the **cumulative amount of all requests whose lock period has fully elapsed**, while any requests still in their lock period remain locked. A maximum of **2,000 active withdrawal requests per staker** is supported. + +1. **`requestUnstake(amount)`**: Burns cSSV, creates `UnstakeRequest{amount, unlockTime = now + cooldownDuration}`. Reverts with `ZeroAmount` if `amount == 0`, `MaxRequestsAmountReached` if pending request count exceeds `MAX_PENDING_REQUESTS` (2000). + +2. **`withdrawUnlocked()`**: After cooldown, returns SSV at 1:1. Processes **all** matured requests in a single call — iterates the full request array, removes every entry where `unlockTime <= block.timestamp` via swap-and-pop, and transfers the cumulative sum. **Immature requests (still in lock period) remain untouched** in the array. Reverts with `NothingToWithdraw` if no matured requests exist. + +**Rewards behavior:** Rewards STOP accruing for the unstaked portion at the moment of `requestUnstake`. Previously accrued rewards remain claimable via `claimEthRewards`. + +--- + +## 4. Oracle System + +### Overview + +Effective Balance Oracles track validator balances on the beacon chain and commit Merkle roots on-chain. The protocol uses a permissioned set of 4 oracles with a 3-of-4 (75%) quorum threshold. + +**Initialization:** Oracle addresses, cooldown duration, and quorum are bootstrapped during the upgrade via `initializeSSVStaking`, which sets `StorageStaking.defaultOracleIds`, `cooldownDuration`, and `quorumBps` atomically. The initializer validates `quorumBps != 0 && quorumBps <= 10_000` — zero or out-of-range values revert with `InvalidQuorum`. There is no window where the contract is live with oracles uninitialized or quorum unset. + +### Commit Flow (`commitRoot`) + +1. Oracle calls `commitRoot(merkleRoot, blockNum)` +2. Contract validates: `blockNum > latestCommittedBlock` (monotonic), `blockNum <= block.number` (not future) +3. On the first vote of a round, reads raw `cSSV.totalSupply()`, truncates it to `frozenVotingSupply = rawSupply - (rawSupply % 4)`, and stores that truncated value in `roundFrozenSupply`; reverts with `ZeroCSSVSupply` if raw supply is zero and with `InsufficientCSSVSupply` if the truncated voting supply is zero +4. Each oracle has equal weight: `weight = frozenVotingSupply / 4` +5. Accumulated weight tracked per `commitmentKey = keccak256(blockNum, merkleRoot)` +6. When `accumulatedWeight >= (frozenVotingSupply * quorumBps) / 10_000`: + - Root is committed: `ebRoots[blockNum] = merkleRoot` + - `latestCommittedBlock = blockNum` + - Cleanup: `delete rootCommitments[commitmentKey]` + - Emits `RootCommitted` +7. Below quorum: emits `WeightedRootProposed` + +`roundFrozenSupply` therefore stores the truncated frozen voting supply for the round, not the exact raw `cSSV.totalSupply()` observed on the first vote. The remainder `rawSupply % 4` is treated as non-voting dust and does not participate in either accumulated vote weight or quorum threshold math. + +**Failed Quorum Behavior:** +- If a proposal fails to reach quorum (e.g., only 2 of 4 oracles vote), the `hasVoted[commitmentKey][oracleId]` mappings and `rootCommitments[commitmentKey]` persist indefinitely +- Oracles cannot re-vote on the exact same `(blockNum, merkleRoot)` pair (reverts with `AlreadyVoted`) +- Oracles **can** vote on the same `blockNum` with a **different** `merkleRoot` since the `commitmentKey` is computed from both parameters +- No automatic cleanup occurs for failed proposals — storage entries remain until overwritten by future successful commits or contract upgrade +- If the last oracle to vote still does not bring the proposal to quorum, the state remains unchanged (no root is committed, no cleanup occurs) + +### Merkle Tree Structure (OpenZeppelin StandardMerkleTree compatible) + +**Leaf encoding**: `keccak256(keccak256(abi.encode(clusterID, effectiveBalance)))` +- Double-hash prevents second pre-image attacks +- `clusterID`: `keccak256(abi.encodePacked(owner, sortedOperatorIds))` +- `effectiveBalance`: `uint32` in whole ETH + +**Tree construction**: +- Leaves sorted by hash value +- Internal nodes: siblings sorted before hashing (smaller hash first) +- Odd nodes duplicated + +### Update Flow (`updateClusterBalance`) + +Permissionless — anyone can submit a valid proof: + +1. Verify committed root exists for `blockNum` +2. Verify update frequency (min blocks between updates) +3. Verify staleness: + - **Latest-root check**: `blockNum == latestCommittedBlock` (prevents stale root usage) + - **Per-cluster monotonicity**: `blockNum > lastRootBlockNum` for this cluster +4. Verify Merkle proof against committed root +5. Verify EB limits (32–2048 ETH per validator) +6. Convert to vUnits, update EB snapshot +7. **ETH clusters only**: apply fee settlements, update operator/DAO vUnit deviations, auto-liquidate if undercollateralized +8. **SSV clusters**: no fee/accounting updates; EB snapshot stored for future migration only + +**Behavior on liquidated clusters:** If a valid proof exists, the EB snapshot (`clusterEB[clusterId].vUnits`) can still be updated even when `cluster.active == false`; fee settlements, vUnit deviation updates, and the auto-liquidation check are skipped. In production, however, oracle roots exclude inactive / liquidated clusters, so this path is typically unreachable until the cluster becomes active again and re-enters the tree. As a result, the on-chain EB snapshot may remain stale throughout the liquidation period. + +**SSV cluster accounting:** Legacy SSV clusters continue to use `validatorCount`-based fee calculations (see "SSV Cluster Balance Update (Legacy)" in Accounting Formulas). The EB snapshot is stored but does not affect fee deductions — it only prepares the cluster for future migration to ETH. + +### Oracle API (External Reference) + +The SSV Oracle (`github.com/ssvlabs/ssv-oracle`) exposes: +- `GET /api/commit` — latest committed root info +- `GET /api/proof/{clusterId}` — Merkle proof for a specific cluster + +--- + +## 5. Storage Layout + +### SSVStorage (`keccak256("ssv.network.storage.main") - 1`) + +```solidity +struct StorageData { + mapping(bytes32 => bytes32) validatorPKs; // keccak256(pubkey, owner) → hashed(operatorIds | active) + mapping(bytes32 => bytes32) clusters; // SSV clusters: keccak256(owner, opIds) → clusterHash + mapping(bytes32 => uint64) operatorsPKs; // keccak256(pubkey) → operatorId + mapping(SSVModules => address) ssvContracts; // module enum → implementation + mapping(uint64 => address) operatorsWhitelist; // operatorId → whitelist address/contract + mapping(uint64 => OperatorFeeChangeRequest) operatorFeeChangeRequests; + mapping(uint64 => Operator) operators; // operatorId → Operator struct + IERC20 token; // SSV ERC-20 + Counters.Counter lastOperatorId; // auto-increment + mapping(address => mapping(uint256 => uint256)) addressWhitelistedForOperators; // bitmap + mapping(bytes32 => bytes32) ethClusters; // ETH clusters: same key → clusterHash +} +``` + +### Operator Struct + +```solidity +struct Operator { + uint32 validatorCount; // SSV validator count + PackedSSV fee; // SSV fee (packed /10M) + address owner; + bool whitelisted; // private flag + Snapshot snapshot; // SSV earnings: {uint32 block, uint64 index, PackedSSV balance} + uint32 ethValidatorCount; // ETH validator count + PackedETH ethFee; // ETH fee (packed /100K) + EthSnapshot ethSnapshot; // ETH earnings: {uint32 block, uint64 index, PackedETH balance} +} +``` + +### Cluster Struct + +```solidity +struct Cluster { + uint32 validatorCount; + uint64 networkFeeIndex; // snapshot of cumulative network fee index + uint64 index; // snapshot of cumulative operator fee index + bool active; + uint256 balance; // ETH wei (ETH clusters) or SSV tokens (SSV clusters) +} +``` + +### SSVStorageProtocol (`keccak256("ssv.network.storage.protocol") - 1`) + +```solidity +struct StorageProtocol { + // SSV (legacy) fields + uint32 networkFeeIndexBlockNumber; + uint32 daoValidatorCount; + uint32 daoIndexBlockNumber; + uint32 validatorsPerOperatorLimit; + PackedSSV networkFee; + uint64 networkFeeIndex; + PackedSSV daoBalance; + uint64 minimumBlocksBeforeLiquidationSSV; + PackedSSV minimumLiquidationCollateralSSV; + uint64 declareOperatorFeePeriod; + uint64 executeOperatorFeePeriod; + uint64 operatorMaxFeeIncrease; + uint64 operatorMaxFeeSSV; + + // ETH fields + uint32 ethNetworkFeeIndexBlockNumber; + uint32 ethDaoValidatorCount; + uint32 ethDaoIndexBlockNumber; + PackedETH ethNetworkFee; + uint64 ethNetworkFeeIndex; + PackedETH ethDaoBalance; + PackedETH minimumLiquidationCollateral; + uint64 minimumBlocksBeforeLiquidation; + PackedETH operatorMaxFee; + + // EB fields + uint64 daoTotalEthVUnits; + PackedETH minimumOperatorEthFee; +} +``` + +### SSVStorageEB (`keccak256("ssv.network.storage.eb") - 1`) + +```solidity +struct StorageEB { + mapping(uint64 => bytes32) ebRoots; // blockNum → Merkle root + mapping(bytes32 => ClusterEBSnapshot) clusterEB; // clusterId → EB snapshot + mapping(uint64 => uint64) operatorEthVUnits; // operatorId → deviation vUnits + uint64 latestCommittedBlock; + uint32 minBlocksBetweenUpdates; + mapping(bytes32 => uint256) rootCommitments; // commitKey → accumulated weight + mapping(bytes32 => mapping(uint32 => bool)) hasVoted; // commitKey → oracleId → voted +} + +struct ClusterEBSnapshot { + uint64 vUnits; // 0 = implicit (use validatorCount * 10_000) + uint64 lastRootBlockNum; // block of last root used + uint64 lastUpdateBlock; // actual block.number of last update +} +``` + +### SSVStorageStaking (`keccak256("ssv.network.storage.staking") - 1`) + +```solidity +struct StorageStaking { + uint64 cooldownDuration; + PackedETH stakingEthPoolBalance; + uint128 accEthPerShare; // scaled by 1e18 + mapping(address => uint256) userIndex; + mapping(address => uint256) accrued; // unclaimed ETH in wei + mapping(uint32 => address) oracles; // oracleId → address + mapping(address => uint32) oracleIdOf; // address → oracleId + uint32[4] defaultOracleIds; + uint16 quorumBps; + mapping(address => UnstakeRequest[]) withdrawalRequests; +} + +struct UnstakeRequest { + uint192 amount; + uint64 unlockTime; +} +``` + +--- + +## 6. Type System & Packing + +### PackedSSV (uint64) + +``` +Pack: raw = value / 10_000_000 +Unpack: value = raw * 10_000_000 +``` + +Reverts with `MaxPrecisionExceeded` if `value % 10_000_000 != 0`. + +### PackedETH (uint64) + +``` +Pack: raw = value / 100_000 +Unpack: value = raw * 100_000 +``` + +Reverts with `MaxPrecisionExceeded` if `value % 100_000 != 0`. + +### Version Constants + +``` +VERSION_SSV = 0 // Legacy SSV-fee clusters +VERSION_ETH = 1 // New ETH-fee clusters +VERSION_UNDEFINED = 255 +``` + +### Cluster Hashing + +```solidity +keccak256(abi.encodePacked( + cluster.validatorCount, + cluster.networkFeeIndex, + cluster.index, + cluster.balance, + cluster.active +)) +``` + +### Cluster ID (Identity Key) + +```solidity +keccak256(abi.encodePacked(ownerAddress, operatorIds)) +``` + +--- + +## 7. All Events + +### Operator Events + +```solidity +event OperatorAdded(uint64 indexed operatorId, address indexed owner, bytes publicKey, uint256 fee); +event OperatorRemoved(uint64 indexed operatorId); +event OperatorFeeDeclared(address indexed owner, uint64 indexed operatorId, uint256 blockNumber, uint256 fee); +event OperatorFeeDeclarationCancelled(address indexed owner, uint64 indexed operatorId); +event OperatorFeeExecuted(address indexed owner, uint64 indexed operatorId, uint256 blockNumber, uint256 fee); +event OperatorWithdrawn(address indexed owner, uint64 indexed operatorId, uint256 value); +event OperatorWithdrawnSSV(address indexed owner, uint64 indexed operatorId, uint256 value); +event OperatorPrivacyStatusUpdated(uint64[] operatorIds, bool toPrivate); +event FeeRecipientAddressUpdated(address indexed owner, address recipientAddress); +``` + +### Whitelist Events + +```solidity +event OperatorMultipleWhitelistUpdated(uint64[] operatorIds, address[] whitelistAddresses); +event OperatorMultipleWhitelistRemoved(uint64[] operatorIds, address[] whitelistAddresses); +event OperatorWhitelistingContractUpdated(uint64[] operatorIds, address whitelistingContract); +``` + +### Validator Events + +```solidity +event ValidatorAdded(address indexed owner, uint64[] operatorIds, bytes publicKey, bytes shares, Cluster cluster); +event ValidatorRemoved(address indexed owner, uint64[] operatorIds, bytes publicKey, Cluster cluster); +event ValidatorExited(address indexed owner, uint64[] operatorIds, bytes publicKey); +``` + +### Cluster Events + +```solidity +event ClusterLiquidated(address indexed owner, uint64[] operatorIds, Cluster cluster); +event ClusterReactivated(address indexed owner, uint64[] operatorIds, Cluster cluster); +event ClusterMigratedToETH(address indexed owner, uint64[] operatorIds, uint256 ethDeposited, uint256 ssvRefunded, uint32 effectiveBalance, Cluster cluster); +event ClusterWithdrawn(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); +event ClusterDeposited(address indexed owner, uint64[] operatorIds, uint256 value, Cluster cluster); +event ClusterBalanceUpdated(address indexed owner, uint64[] operatorIds, uint64 indexed blockNum, uint32 effectiveBalance, Cluster cluster); +``` + +### DAO Events + +```solidity +event NetworkFeeUpdated(uint256 oldFee, uint256 newFee); +event NetworkFeeUpdatedSSV(uint256 oldFee, uint256 newFee); +event NetworkEarningsWithdrawn(uint256 value, address recipient); +event OperatorFeeIncreaseLimitUpdated(uint64 value); +event DeclareOperatorFeePeriodUpdated(uint64 value); +event ExecuteOperatorFeePeriodUpdated(uint64 value); +event LiquidationThresholdPeriodUpdated(uint64 value); +event LiquidationThresholdPeriodSSVUpdated(uint64 value); +event MinimumLiquidationCollateralUpdated(uint256 value); +event MinimumLiquidationCollateralSSVUpdated(uint256 value); +event OperatorMaximumFeeUpdated(uint256 maxFee); +event MinimumOperatorEthFeeUpdated(uint256 minFee); +event RootCommitted(bytes32 indexed merkleRoot, uint64 indexed blockNum); +event WeightedRootProposed(bytes32 indexed merkleRoot, uint64 indexed blockNum, uint256 accumulatedWeight, uint256 quorum, uint32 oracleId, address oracle); +event OracleReplaced(uint32 indexed oracleId, address indexed oldOracle, address indexed newOracle); +event QuorumUpdated(uint16 newQuorum); +event CooldownDurationUpdated(uint64 newCooldownDuration); +``` + +### Staking Events + +```solidity +event Staked(address indexed user, uint256 amount); +event UnstakeRequested(address indexed user, uint256 amount, uint256 unlockTime); +event UnstakedWithdrawn(address indexed user, uint256 amount); +event FeesSynced(uint256 newFeesWei, uint256 accEthPerShare); +event RewardsSettled(address indexed user, uint256 pending, uint256 accrued, uint256 userIndex); +event RewardsClaimed(address indexed user, uint256 amount); +event ERC20Rescued(address indexed token, address indexed to, uint256 amount); +``` + +### Module Events + +```solidity +event ModuleUpgraded(SSVModules indexed moduleId, address moduleAddress); +``` + +--- + +## 8. All External Functions + +### SSVOperators + +```solidity +function registerOperator(bytes calldata publicKey, uint256 fee, bool setPrivate) external returns (uint64) +function removeOperator(uint64 operatorId) external nonReentrant +function declareOperatorFee(uint64 operatorId, uint256 fee) external +function executeOperatorFee(uint64 operatorId) external +function cancelDeclaredOperatorFee(uint64 operatorId) external +function reduceOperatorFee(uint64 operatorId, uint256 fee) external +function setOperatorsPrivateUnchecked(uint64[] calldata operatorIds) external +function setOperatorsPublicUnchecked(uint64[] calldata operatorIds) external +function withdrawOperatorEarnings(uint64 operatorId, uint256 amount) external nonReentrant +function withdrawAllOperatorEarnings(uint64 operatorId) external nonReentrant +function withdrawAllVersionOperatorEarnings(uint64 operatorId) external nonReentrant +function withdrawOperatorEarningsSSV(uint64 operatorId, uint256 amount) external nonReentrant +function withdrawAllOperatorEarningsSSV(uint64 operatorId) external nonReentrant +``` + +### SSVOperatorsWhitelist + +```solidity +function setOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses) external +function removeOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses) external +function setOperatorsWhitelistingContract(uint64[] calldata operatorIds, ISSVWhitelistingContract whitelistingContract) external +function removeOperatorsWhitelistingContract(uint64[] calldata operatorIds) external +``` + +### SSVValidators + +```solidity +function registerValidator(bytes calldata publicKey, uint64[] memory operatorIds, bytes calldata sharesData, Cluster memory cluster) external payable +function bulkRegisterValidator(bytes[] memory publicKeys, uint64[] memory operatorIds, bytes[] calldata sharesData, Cluster memory cluster) external payable +function removeValidator(bytes calldata publicKey, uint64[] memory operatorIds, Cluster memory cluster) external +function bulkRemoveValidator(bytes[] calldata publicKeys, uint64[] memory operatorIds, Cluster memory cluster) external +function exitValidator(bytes calldata publicKey, uint64[] calldata operatorIds) external +function bulkExitValidator(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external +``` + +### SSVClusters + +```solidity +function liquidate(address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster) external nonReentrant +function liquidateSSV(address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster) external nonReentrant +function reactivate(uint64[] calldata operatorIds, Cluster memory cluster) external payable +function deposit(address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster) external payable +function withdraw(uint64[] calldata operatorIds, uint256 amount, Cluster memory cluster) external nonReentrant +function migrateClusterToETH(uint64[] calldata operatorIds, Cluster memory cluster) external payable +function updateClusterBalance(uint64 blockNum, address clusterOwner, uint64[] calldata operatorIds, Cluster memory cluster, uint32 effectiveBalance, bytes32[] calldata merkleProof) external nonReentrant +``` + +### SSVDAO + +```solidity +function updateNetworkFee(uint256 fee) external // onlyOwner +function updateNetworkFeeSSV(uint256 fee) external // onlyOwner +function withdrawNetworkSSVEarnings(uint256 amount) external nonReentrant // onlyOwner +function updateOperatorFeeIncreaseLimit(uint64 percentage) external // onlyOwner +function updateDeclareOperatorFeePeriod(uint64 timeInSeconds) external // onlyOwner +function updateExecuteOperatorFeePeriod(uint64 timeInSeconds) external // onlyOwner +function updateLiquidationThresholdPeriod(uint64 blocks) external // onlyOwner +function updateLiquidationThresholdPeriodSSV(uint64 blocks) external // onlyOwner +function updateMinimumLiquidationCollateral(uint256 amount) external // onlyOwner +function updateMinimumLiquidationCollateralSSV(uint256 amount) external // onlyOwner +function updateMaximumOperatorFee(uint256 maxFee) external // onlyOwner +function updateMinimumOperatorEthFee(uint256 minFee) external // onlyOwner +function commitRoot(bytes32 merkleRoot, uint64 blockNum) external // oracle only +function replaceOracle(uint32 oracleId, address newOracle) external // onlyOwner +function updateQuorumBps(uint16 quorum) external // onlyOwner +function updateUnstakeCooldownDuration(uint64 duration) external // onlyOwner +``` + +### SSVStaking + +```solidity +function syncFees() external nonReentrant +function stake(uint256 amount) external nonReentrant +function requestUnstake(uint256 amount) external nonReentrant +function withdrawUnlocked() external nonReentrant +function claimEthRewards() external nonReentrant +function rescueERC20(address token, address to, uint256 amount) external nonReentrant // onlyOwner +function onCSSVTransfer(address from, address to, uint256 amount) external // cSSV only +``` + +### SSVNetwork (Proxy-level) + +```solidity +function initialize(...) external initializer onlyProxy +function setFeeRecipientAddress(address recipientAddress) external // anyone +function updateModule(SSVModules moduleId, address moduleAddress) external // onlyOwner +function getVersion() external pure returns (string memory) // "v2.0.0" +``` + +--- + +## 9. Access Control Matrix + +| Role | Who | Functions | +|---|---|---| +| **Owner** | Contract owner (Ownable2Step) | All `update*`, `withdraw*Network*`, `replaceOracle`, `updateQuorumBps`, `updateUnstakeCooldownDuration`, `updateModule`, `rescueERC20`, `_authorizeUpgrade` | +| **Operator Owner** | `msg.sender == operator.owner` | `removeOperator`, `declareOperatorFee`, `executeOperatorFee`, `cancelDeclaredOperatorFee`, `reduceOperatorFee`, `setOperators*`, `withdraw*OperatorEarnings*` | +| **Cluster Owner** | `msg.sender == owner` in cluster key | `reactivate`, `withdraw`, `migrateClusterToETH`, `registerValidator`, `bulkRegisterValidator`, `removeValidator`, `bulkRemoveValidator`, `exitValidator`, `bulkExitValidator` | +| **Oracle** | `oracleIdOf[msg.sender] != 0` | `commitRoot` | +| **cSSV Token** | `msg.sender == CSSV_ADDRESS` | `onCSSVTransfer` | +| **Anyone** | Any address | `liquidate` (if liquidatable), `liquidateSSV` (if liquidatable), `deposit`, `updateClusterBalance`, `registerOperator`, `syncFees`, `stake`, `requestUnstake`, `withdrawUnlocked`, `claimEthRewards`, `setFeeRecipientAddress`, all view functions | + +--- + +## 10. Accounting Formulas + +### Fee Settlement Rule + +When an operator fee changes (`executeOperatorFee`, `reduceOperatorFee`), the operator's ETH snapshot is updated **before** the new fee is stored. This ensures all earnings accrued up to the current block are settled at the **old** fee rate. The new fee applies only to blocks going forward — there is no retroactive impact on cluster index calculations. + +``` +// On fee change: +operator.ethSnapshot.balance += (block.number - ethSnapshot.block) * PackedETH.unwrap(operator.ethFee) +operator.ethSnapshot.block = block.number +operator.ethFee = newFee // takes effect from this block onward +``` + +### ETH Network Fee Index + +``` +currentIndex = sp.ethNetworkFeeIndex + (block.number - sp.ethNetworkFeeIndexBlockNumber) * PackedETH.unwrap(sp.ethNetworkFee) +``` + +### ETH Operator Fee Index + +``` +operator.ethSnapshot.index += (block.number - ethSnapshot.block) * PackedETH.unwrap(operator.ethFee) +``` + +### ETH Operator Earnings (with EB) + +``` +effectiveVUnits = seb.operatorEthVUnits[operatorId] + operator.ethValidatorCount * BPS_DENOMINATOR +operator.ethSnapshot.balance += (blockDiff * ethFee * effectiveVUnits) / BPS_DENOMINATOR +``` + +### ETH Cluster Balance Update + +``` +clusterVUnits = (seb.clusterEB[id].vUnits == 0) ? validatorCount * 10_000 : seb.clusterEB[id].vUnits + +idxOp = clusterIndex - cluster.index +idxNet = currentNetworkFeeIndex - cluster.networkFeeIndex +networkFeeUnits = (idxNet * clusterVUnits) / BPS_DENOMINATOR +operatorFeeUnits = (idxOp * clusterVUnits) / BPS_DENOMINATOR +totalFees = (networkFeeUnits + operatorFeeUnits) * ETH_DEDUCTED_DIGITS + +cluster.balance = max(0, cluster.balance - totalFees) +``` + +### SSV Network Fee Index (Legacy) + +``` +currentIndex = sp.networkFeeIndex + (block.number - sp.networkFeeIndexBlockNumber) * PackedSSV.unwrap(sp.networkFee) +``` + +### SSV Cluster Balance Update (Legacy) + +``` +usage = (clusterIndexSSV - cluster.index + currentNetworkFeeIndexSSV - cluster.networkFeeIndex) * cluster.validatorCount +cluster.balance = max(0, cluster.balance - unpack(usage)) +``` + +### ETH Liquidation Check + +``` +burnRate = Σ PackedETH.unwrap(operator.ethFee) for all operators in cluster +networkFee = PackedETH.unwrap(sp.ethNetworkFee) +thresholdUnits = (minimumBlocksBeforeLiquidation * (burnRate + networkFee) * vUnits) / BPS_DENOMINATOR + +liquidatable = (balance < unpack(minimumLiquidationCollateral)) + || (balance < thresholdUnits * ETH_DEDUCTED_DIGITS) +``` + +### SSV Liquidation Check (Legacy) + +``` +burnRate = Σ PackedSSV.unwrap(operator.fee) +networkFee = PackedSSV.unwrap(sp.networkFee) + +liquidatable = (balance < unpack(minimumLiquidationCollateralSSV)) + || (balance < unpack((burnRate + networkFee) * validatorCount * minimumBlocksBeforeLiquidationSSV)) +``` + +### Staking Reward Accumulation + +``` +// syncFees: +newDaoEarnings = sp.networkTotalEarnings() // ETH DAO total +newFees = newDaoEarnings - stakingEthPoolBalance +accEthPerShare += (unpack(newFees) * 1e18) / cSSV.totalSupply() +stakingEthPoolBalance = newDaoEarnings + +// settle(user): +pending = (cSSVBalance * (accEthPerShare - userIndex[user])) / 1e18 +accrued[user] += pending +userIndex[user] = accEthPerShare +``` + +--- + +## 11. Global Invariants + +These invariants must hold across all contract states. They are critical for verifying protocol correctness and should be checked in comprehensive test suites. + +### 1. ETH Conservation + +``` +contract.ETH_balance ≈ Σ(current ETH cluster balances) + + Σ(current operator ETH earnings) + + ProtocolLib.networkTotalEarnings() +``` + +**Notes:** +- "current" means view-computed balances that apply pending fees (see `contracts/modules/SSVViews.sol`) +- `≈` (approximately equal) accounts for rounding from packing/unpacking operations +- `ProtocolLib.networkTotalEarnings()` includes both `ethDaoBalance` and pending network fee earnings + +### 2. SSV Conservation + +``` +contract.SSV_balance ≈ Σ(current SSV cluster balances) + + Σ(current operator SSV earnings) + + networkTotalEarningsSSV() + + stakingHeldSSV +``` + +**Notes:** +- `stakingHeldSSV` = total SSV still locked in the `SSVNetwork` contract, including pending unstake requests +- `cSSV.totalSupply()` is only equal to `stakingHeldSSV` when there are no pending unstake requests + +### 3. Validator Count Consistency + +``` +ethDaoValidatorCount == Σ(cluster.validatorCount) across all active ETH clusters +``` + +**Note:** `Σ(operator.ethValidatorCount)` is NOT equivalent because operators are shared across clusters and would double-count validators. + +### 4. vUnit Consistency + +``` +daoTotalEthVUnits == ethDaoValidatorCount * BPS_DENOMINATOR + Σ(cluster_deviations) +``` + +Where `cluster_deviations = clusterEB.vUnits - validatorCount * BPS_DENOMINATOR` for clusters with explicit EB. + +### 5. Cluster Hash Integrity + +Every cluster operation must end with: +``` +s.ethClusters[key] = cluster.hashClusterData() +``` + +Matching the actual cluster state: `keccak256(abi.encodePacked(validatorCount, networkFeeIndex, index, balance, active))` + +### 6. cSSV Supply Accounting + +``` +cSSV.totalSupply() == Σ(staked SSV) - Σ(unstake-requested SSV) +``` + +- Mint on `stake()` +- Burn on `requestUnstake()` + +### 7. Accumulator Monotonicity + +``` +accEthPerShare[t+1] >= accEthPerShare[t] +``` + +Staking reward accumulator only increases, never decreases. + +### 8. Oracle Monotonicity + +``` +latestCommittedBlock[t+1] >= latestCommittedBlock[t] +``` + +Committed EB roots are strictly ordered by block number. + +### 9. Cluster Version Exclusivity + +``` +(s.clusters[key] != 0) XOR (s.ethClusters[key] != 0) +``` + +A cluster key exists in EITHER SSV clusters OR ETH clusters, never both. + +### 10. Operator Dual Tracking + +For each operator: +``` +operator.validatorCount + operator.ethValidatorCount == total validators using this operator +``` + +SSV validator count + ETH validator count equals total across both cluster types. + +--- + +## 12. Governance Parameters + +### ETH Cluster Parameters + +| Parameter | Initial Value | Update Function | +|---|---|---| +| `ethNetworkFee` | 0.000000003550929823 ETH/block (~0.00928 ETH/year) | `updateNetworkFee(uint256)` | +| `minimumLiquidationCollateral` | 0.00094 ETH | `updateMinimumLiquidationCollateral(uint256)` | +| `minimumBlocksBeforeLiquidation` | 50,190 blocks (~7 days) | `updateLiquidationThresholdPeriod(uint64)` | +| `operatorMaxFee` | 0.000000005326300000 ETH/block (~0.0140 ETH/year) | `updateMaximumOperatorFee(uint256)` | +| `minimumOperatorEthFee` | 0.000000001065200000 ETH/block (~0.0028 ETH/year) | `updateMinimumOperatorEthFee(uint256)` | + +### SSV Cluster Parameters (Legacy) + +| Parameter | Current Value | Proposed Value | Update Function | +|---|---|---|---| +| `networkFee` (SSV) | current | current | `updateNetworkFeeSSV(uint256)` | +| `minimumLiquidationCollateralSSV` | 1.53 SSV | 0.883 SSV | `updateMinimumLiquidationCollateralSSV(uint256)` | +| `minimumBlocksBeforeLiquidationSSV` | 100,380 (~14 days) | 100,380 (~14 days) | `updateLiquidationThresholdPeriodSSV(uint64)` | +| `operatorMaxFeeSSV` | current | -- | No update function (read-only, frozen) | + +### Staking Parameters + +| Parameter | Initial Value | Update Function | +|---|---|---| +| `cooldownDuration` | 604,800 seconds (7 days) | `updateUnstakeCooldownDuration(uint64)` | + +**Note on units:** `cooldownDuration` is measured in **seconds** (timestamp-based, via `block.timestamp`), not blocks. The value 604,800 = 7 days in seconds. See `SSVStaking.sol:88`: `uint64(block.timestamp + s.cooldownDuration)`. + +### Oracle Parameters + +| Parameter | Initial Value | Update Function | +|---|---|---| +| `quorumBps` | 7,500 (75%) | `updateQuorumBps(uint16)` | +| `minBlocksBetweenUpdates` | 0 blocks | `updateMinBlocksBetweenUpdates(uint32)` | +| Oracle set | 4 oracles | `replaceOracle(uint32, address)` | + +### Operator Fee Parameters + +| Parameter | Value | Update Function | +|---|---|---| +| `defaultOperatorETHFee` | 1,770,000,000 wei (~0.00464 ETH/year) | Hardcoded | +| `declareOperatorFeePeriod` | Governance-set | `updateDeclareOperatorFeePeriod(uint64)` | +| `executeOperatorFeePeriod` | Governance-set | `updateExecuteOperatorFeePeriod(uint64)` | +| `operatorMaxFeeIncrease` | Governance-set | `updateOperatorFeeIncreaseLimit(uint64)` | + +--- + +## 13. Error Codes + +### Cluster Errors +- `ClusterAlreadyEnabled` — reactivating an already active cluster +- `ClusterIsLiquidated` — operating on a liquidated cluster +- `ClusterNotLiquidatable` — liquidation attempted but cluster is solvent +- `ClusterDoesNotExist` — cluster not found +- `InsufficientBalance` — balance too low for operation +- `InvalidPublicKeyLength` — validator public key wrong length +- `ValidatorAlreadyRegistered(bytes publicKey, address owner)` — validator already registered +- `ValidatorDoesNotExist` — validator not found +- `IncorrectClusterState` — submitted cluster struct doesn't match stored hash +- `IncorrectClusterVersion` — operating on wrong cluster version (e.g. SSV cluster for ETH operation) +- `IncorrectValidatorStateWithData(bytes publicKey)` — validator state mismatch +- `NewBlockPeriodIsBelowMinimum` — liquidation threshold too low +- `InvalidOperatorIdsLength` — wrong number of operator IDs +- `UnsortedOperatorsList` — operator IDs not sorted +- `EmptyPublicKeysList` — no public keys provided +- `PublicKeysSharesLengthMismatch` — public keys and shares arrays differ in length + +### Operator Errors +- `CallerNotOwnerWithData(address caller, address owner)` — msg.sender not operator owner +- `CallerNotWhitelistedWithData(uint64 operatorId)` — whitelist check failed +- `OperatorAlreadyExists` — duplicate operator registration +- `OperatorDoesNotExist` — operator not found +- `InsufficientBalance` — insufficient earnings to withdraw +- `FeeTooLow` — fee below minimum operator ETH fee +- `FeeTooHigh` — fee exceeds maximum operator fee +- `FeeExceedsIncreaseLimit` — fee increase exceeds max allowed +- `FeeIncreaseNotAllowed` — zero-fee operator cannot increase +- `SameFeeChangeNotAllowed` — declared fee same as current +- `ApprovalNotWithinTimeframe` — fee execute outside window +- `NoFeeDeclared` — no pending fee change request +- `ExceedValidatorLimitWithData(uint64 operatorId)` — operator at validator capacity +- `TargetModuleDoesNotExistWithData(uint8 moduleId)` — module not registered +- `IncorrectOperatorVersion(uint8 operatorVersion)` — wrong operator version for operation +- `LegacyOperatorFeeDeclarationInvalid` — pre-migration fee declaration +- `OperatorsListNotUnique` — duplicate operator IDs in list + +### Whitelist Errors +- `InvalidContractAddress` — invalid whitelist contract address +- `AddressIsWhitelistingContract(address contractAddress)` — address already a whitelisting contract +- `InvalidWhitelistingContract(address contractAddress)` — contract doesn't implement interface +- `InvalidWhitelistAddressesLength` — whitelist address array length mismatch +- `ZeroAddressNotAllowed` — zero address not permitted + +### Packing Errors +- `MaxValueExceeded` — packed value overflow +- `MaxPrecisionExceeded` — fee value not divisible by precision factor + +### Oracle/EB Errors +- `NotOracle` — caller not registered oracle +- `AlreadyVoted` — oracle already voted for this block +- `StaleBlockNumber` — block number not newer than last committed +- `FutureBlockNumber` — block number in the future +- `InvalidProof` — Merkle proof verification failed +- `RootNotFound` — no committed root for block number +- `StaleUpdate` — EB update is outdated +- `UpdateTooFrequent` — min blocks between updates not met +- `EBBelowMinimum` — effective balance below minimum +- `EBExceedsMaximum` — effective balance above maximum +- `OracleAlreadyAssigned` — oracle address already in use +- `ZeroCSSVSupply` — cSSV totalSupply is zero +- `InsufficientCSSVSupply` — cSSV totalSupply exists but truncates below one oracle weight +- `InvalidQuorum` — quorum value out of valid range + +### Staking Errors +- `NothingToWithdraw` — no unlocked unstake requests +- `NothingToClaim` — no accrued rewards to claim +- `MaxRequestsAmountReached` — exceeded MAX_PENDING_REQUESTS (2000) +- `UnstakeAmountExceedsBalance` — unstake amount exceeds cSSV balance +- `StakeTooLow` — stake amount below MINIMAL_STAKING_AMOUNT +- `ZeroAmount` — amount is zero +- `InvalidToken` — cannot rescue protected tokens +- `NotCSSV` — caller is not the cSSV token contract +- `ZeroAmount` — SSV amount to stake is zero + +### General Errors +- `NotAuthorized` — unauthorized action +- `ZeroAddress` — zero address not allowed +- `ETHTransferFailed` — ETH transfer reverted +- `TokenTransferFailed` — ERC-20 transfer reverted + +--- + +## 14. Constants + +```solidity +// Precision +uint32 constant BPS_DENOMINATOR = 10_000; +uint256 constant ETH_DEDUCTED_DIGITS = 100_000; +uint256 constant DEDUCTED_DIGITS = 10_000_000; + +// EB Limits +uint256 constant MAX_EB_PER_VALIDATOR = 2048 ether; +uint256 constant DEFAULT_EB_PER_VALIDATOR = 32 ether; + +// Operator Defaults +uint256 constant DEFAULT_OPERATOR_ETH_FEE = 1_770_000_000; // 1.77 gwei/vUnit/block + +// Protocol Limits +uint64 constant MINIMAL_LIQUIDATION_THRESHOLD = 21_480; // blocks +uint256 constant MAX_PENDING_REQUESTS = 2000; +uint256 constant MINIMAL_STAKING_AMOUNT = 1_000_000_000; +uint256 constant MAX_DELEGATION_SLOTS = 4; + +// Version +uint8 constant VERSION_SSV = 0; +uint8 constant VERSION_ETH = 1; +uint8 constant VERSION_UNDEFINED = 255; +``` + +--- + +END OF SPEC.md diff --git a/docs/UPGRADE_PLAYBOOK.md b/docs/UPGRADE_PLAYBOOK.md new file mode 100644 index 000000000..97f5cabc9 --- /dev/null +++ b/docs/UPGRADE_PLAYBOOK.md @@ -0,0 +1,386 @@ +# Mainnet Upgrade Playbook + +## Purpose + +This document describes the operational runbook for upgrading the Ethereum mainnet SSV Network deployment from `v1.2.0` to `v2.0.0`. + +It is specific to the production ownership model where: + +- `SSVNetwork` is owned by a SAFE multisig. +- `SSVNetworkViews` is owned by the same SAFE multisig unless explicitly configured otherwise. +- SSV Labs deploys the new implementation contracts and prepares the SAFE batch. +- The multisig committee reviews, signs, and executes the upgrade transactions on SAFE. + +This playbook is aligned with the repository deployment flow in `deployments/README.md` and the scripts: + +- `just deploy mainnet` +- `just generate-safe-batch mainnet` +- `just verify-upgrade mainnet` + +## Version Scope + +- Current on-chain version: `v1.2.0` +- Target version: `v2.0.0` +- Legacy code reference: [`https://github.com/ssvlabs/ssv-network/tree/v1.2.0`](https://github.com/ssvlabs/ssv-network/tree/v1.2.0) +- Target code reference: [`https://github.com/ssvlabs/ssv-network/tree/v2.0.0`](https://github.com/ssvlabs/ssv-network/tree/v2.0.0) + +## Roles and Responsibilities + +### SSV Labs + +- Prepare the final mainnet configuration in `deployments/mainnet/config.json`. +- Set the deployer key in `.env` as `MAINNET_PRIVATE_KEY`. +- Run the mainnet implementation deployment. +- Generate the SAFE Transaction Builder JSON from the deployed addresses and config values. +- Deliver the upgrade instructions, the generated SAFE batch JSON and the implementation addresses from the deployment result to the multisig committee. +- Run post-execution verification. + +### SAFE Multisig Committee + +- Review the upgrade instructions, implementation addresses and the generated SAFE batch JSON. +- Import the generated batch into SAFE Transaction Builder. +- Review every target address and parameter (DIP proposal). +- Sign and execute the batch on Ethereum mainnet. + +## Source of Truth + +For mainnet, the operational source of truth is: + +- Config: `deployments/mainnet/config.json` +- Deployment output: `deployments/mainnet/deploy-result.json` +- SAFE batch output: `deployments/mainnet/multisig-batch.json` + +`config.json` defines the intended upgrade parameters. `deploy-result.json` contains the freshly deployed implementation and module addresses. `multisig-batch.json` is generated from both and is the file to import into SAFE. + +## Mainnet Configuration Template + +Before deployment, populate `deployments/mainnet/config.json` with the intended mainnet values. + +**Configure an `upgradeTimestamp` to be some future block** + +```json +{ + "currentVersion": "v1.2.0", + "targetVersion": "v2.0.0", + "skipInitializer": false, + "owner": "", + "ssvNetworkProxy": "", + "ssvNetworkViews": "", + "ssvToken": "", + "cooldownDuration": 604800, + "upgradeTimestamp": , + "quorumBps": 7500, + "defaultOracleIds": [1, 2, 3, 4], + "protocolParams": { + "networkFeeEth": "3557600000", + "maxOperatorEthFee": "5336500000", + "minOperatorEthFee": "10000000", + "minimumLiquidationCollateralEth": "644852000000000", + "liquidationThresholdPeriod": "21480", + "minBlocksBetweenUpdates": "0", + "minimumLiquidationCollateralSSV": "673652000000000000", + "liquidationThresholdPeriodSSV": "50120" + }, + "initialStakeAmount": "1000000000000000000", + "oracles": { + "1": "0xc61f7bd9ee5a3d011caf47aa0e5411f720593920", + "2": "0xc07332e05cec1c4896555a6d10361233fdf14422", + "3": "0x28bEa5B242362974d5DDb8f17a1E0e525446960B", + "4": "0x3A98EE5f80268Ed91F8A5880d93468b76a9F3bB4" + } +} +``` + +Notes: + +- `currentVersion` must match the version currently reported on-chain by the proxy. The scripts abort on mismatch. +- `skipInitializer` must remain `false` for the `v1.2.0 -> v2.0.0` mainnet upgrade so the staking initializer is executed through `upgradeToAndCall`. +- Any omitted `protocolParams` field is left unchanged on-chain. +- Oracle addresses must be finalized before generating the SAFE batch. + +## Preconditions + +Complete all of the following before touching mainnet: + +1. Validate the release candidate code and contract artifacts for `v2.0.0`. +2. Finalize `deployments/mainnet/config.json`, including oracle addresses and target timestamps. +3. Confirm the SAFE address that owns `SSVNetwork` and `SSVNetworkViews`. +4. Confirm `MAINNET_PRIVATE_KEY` is set in `.env` for the SSV Labs deployer account. +5. Ensure the deployer account has enough ETH to deploy: + - `SSVNetworkSSVStakingUpgrade` + - `SSVNetworkViews` + - `CSSVToken` + - All module implementations +6. Confirm the SAFE holds at least `initialStakeAmount` in SSV tokens (currently 1 SSV). The batch includes an `approve` + `stake` pair that transfers SSV from the SAFE to the SSVNetwork proxy. +7. Estimate the gas cost of the full SAFE batch before mainnet execution: + - Simulate the complete batch against a mainnet fork (`just upgrade-fork mainnet`) or via Tenderly. + - The batch contains roughly 24 transactions (1 `upgradeToAndCall`, 7 `updateModule`, 1 `upgradeTo`, ~10 parameter setters, 1 `updateQuorumBps`, ~4 `replaceOracle`, 1 `approve`, 1 `stake`). At typical mainnet gas prices the total is in the 4–6M gas range. Confirm the SAFE has enough ETH to cover execution at current gas prices. + - If Tenderly is available, import `multisig-batch.json` and run a simulation before delivery to the committee. +8. Dry-run the same flow on a fork or staging environment before mainnet execution. + +## Step 1: Deploy Implementations on Mainnet + +SSV Labs runs: + +```bash +just deploy mainnet +``` + +This step deploys implementations only. It does not upgrade either proxy. + +Expected output includes: + +- `SSVNetworkSSVStakingUpgrade` implementation +- `SSVNetworkViews` implementation +- `CSSVToken` deployment or reuse +- Module implementations: + - `SSVOperators` + - `SSVClusters` + - `SSVDAO` + - `SSVViews` + - `SSVOperatorsWhitelist` + - `SSVStaking` + - `SSVValidators` + +The script writes the results to: + +- `deployments/mainnet/deploy-result.json` +- `deployments/mainnet/deploy-result.v2.0.0.json` as the versioned artifact for this release + +SSV Labs then generates the deployment attestation: + +```bash +just generate-attestation mainnet +``` + +This reads `deploy-result.json` and `config.json`, fetches the runtime bytecode of every deployed contract on-chain, and writes: + +- `deployments/mainnet/deployment-attestation.json` + +The attestation includes: + +- deployment timestamp +- deployer address +- chain ID +- every newly deployed contract address and constructor parameters used on deployment (if any) +- the **bytecode hash** (`keccak256` of the deployed runtime bytecode) for each implementation and module, so the committee can independently verify they are pointing the proxies at the correct compiled artifacts +- the full protocol parameters and oracle addresses from `config.json` + +To independently verify a bytecode hash for any deployed contract: + +```bash +cast keccak $(cast code
--rpc-url $MAINNET_RPC_URL) +``` + +The expected values should be derived from the locally compiled artifacts in `artifacts/build-info/` or by running the same command against the staging deployment. Include the full attestation JSON in the delivery to the committee. + +The script also prints the `keccak256` file hashes of `deployment-attestation.json` and `multisig-batch.json` (if already generated). The committee should verify these hashes match the files they received to confirm nothing was modified after generation. + +## Step 2: Generate the SAFE Batch + +After the implementation deployment is complete, SSV Labs runs: + +```bash +just generate-safe-batch mainnet +``` + +This generates: + +```text +deployments/mainnet/multisig-batch.json +``` + +The batch is built from: + +- `deployments/mainnet/config.json` +- `deployments/mainnet/deploy-result.json` + +This is the recommended path for mainnet because it removes manual entry of target addresses and parameter values in SAFE. + +## Step 3: SAFE Batch Contents + +The generated SAFE batch encodes the owner-governed upgrade and configuration calls in this order. + +### 1. Upgrade `SSVNetwork` + +Because `skipInitializer=false`, the first call is: + +```solidity +SSVNetwork.upgradeToAndCall(, ) +``` + +The initializer payload encodes: + +```solidity +initializeSSVStaking(uint64 cooldownDuration, uint32[4] defaultOracleIds, uint16 quorumBps) +``` + +If a future patch upgrade sets `skipInitializer=true`, the batch uses `upgradeTo(...)` instead. + +### 2. Update module pointers on `SSVNetwork` + +The batch then updates all module slots: + +```solidity +SSVNetwork.updateModule(0, ) +SSVNetwork.updateModule(1, ) +SSVNetwork.updateModule(2, ) +SSVNetwork.updateModule(3, ) +SSVNetwork.updateModule(4, ) +SSVNetwork.updateModule(5, ) +SSVNetwork.updateModule(6, ) +``` + +### 3. Upgrade `SSVNetworkViews` + +The batch upgrades the views proxy separately: + +```solidity +SSVNetworkViews.upgradeTo() +``` + +### 4. Apply governance and protocol parameters + +The batch includes setter calls for every parameter present in `config.json`. For the proposed mainnet config, this includes: + +```solidity +SSVNetwork.updateNetworkFee(...) +SSVNetwork.updateMaximumOperatorFee(...) +SSVNetwork.updateMinimumOperatorEthFee(...) +SSVNetwork.updateMinimumLiquidationCollateral(...) +SSVNetwork.updateLiquidationThresholdPeriod(...) +SSVNetwork.updateMinBlocksBetweenUpdates(...) +SSVNetwork.updateMinimumLiquidationCollateralSSV(...) +SSVNetwork.updateLiquidationThresholdPeriodSSV(...) +``` + +If additional optional fields are present in config, the batch generator will also include their corresponding setters. + +### 5. Replace oracle addresses + +For each oracle entry in `config.json`, the batch includes: + +```solidity +SSVNetwork.replaceOracle(, ) +``` + +### 6. Initial SSV stake + +If `initialStakeAmount` is set in `config.json`, the batch includes the ERC-20 approval and stake call: + +```solidity +SSVToken.approve(SSVNetwork, ) +SSVNetwork.stake() +``` + +This seeds the staking module so that `totalStaked > 0`, which is required for oracle quorum to function. + +## Step 4: SAFE Committee Review and Execution + +The multisig committee should: + +1. Import `deployments/mainnet/multisig-batch.json` into SAFE Transaction Builder. +2. Confirm the SAFE address matches the intended `owner`. +3. Review each transaction target and calldata. +4. Confirm the implementation and module addresses against `deploy-result.json`. +5. Confirm the parameter values against `deployments/mainnet/config.json`. +6. Sign and execute the batch on mainnet. + +Recommended review checklist: + +- `SSVNetwork` proxy address is correct. +- `SSVNetworkViews` proxy address is correct. +- All module addresses are the fresh `v2.0.0` deployments. +- New protocol parameters match the approved release values. +- Oracle IDs and replacement addresses are correct. +- Bytecode hash of each implementation and module address matches the hash provided by SSV Labs: + + ```bash + cast keccak $(cast code
--rpc-url $MAINNET_RPC_URL) + ``` + + Verify this independently for `SSVNetworkSSVStakingUpgrade`, `SSVNetworkViews`, `CSSVToken`, and all seven module implementations before signing. + +## Step 5: Verify Initial Stake +The initial SSV stake is included in the SAFE batch when `initialStakeAmount` is set in `config.json`. No separate transactions are needed. + +## Step 6: Post-Execution Verification + +After the multisig execution completes, SSV Labs runs: + +```bash +just verify-upgrade mainnet +``` + +Verification should confirm: + +- `SSVNetwork` reports the target version. +- `SSVNetworkViews` points to the intended implementation. +- All module pointers were updated. +- Governance parameters exposed through `SSVViews` match `deployments/mainnet/config.json`. +- Oracle replacements were applied. + +Manual completion checks should then confirm: + +- the initial stake is visible on-chain (included in the batch when `initialStakeAmount` is set) + +Note: `minBlocksBetweenUpdates` is configured during the upgrade flow, but it is not exposed through `SSVViews`, so `just verify-upgrade mainnet` cannot assert it directly. + +## Step 7: Post-Upgrade Smoke Test (fork) + +Run the end-to-end smoke test against a fork of the just-upgraded mainnet state: + +```bash +just smoke-test mainnet +``` + +This exercises the full v2.0.0 happy path against the live state — register 4 public operators, register/deposit/bulk-register/remove/exit validators, declare+execute an operator fee change, withdraw operator ETH earnings, stake SSV (impersonating the SSV Foundation) + claim ETH rewards, oracle quorum commit of an EB root, `updateClusterBalance`, mine blocks until liquidatable, third-party liquidation, and request-unstake → cooldown → `withdrawUnlocked`. Finishes with ETH and SSV solvency checks. Unlike `verify-upgrade`, which only confirms configured state, this asserts that real operations succeed end-to-end. + +## Artifacts to Preserve + +Archive the following for auditability: + +- final `deployments/mainnet/config.json` +- final `deployments/mainnet/deploy-result.json` +- final `deployments/mainnet/deployment-attestation.json` +- final `deployments/mainnet/multisig-batch.json` +- SAFE transaction hash(es) +- deployment transaction hash(es) +- verification output +- internal sign-off confirming first stake completion + +## Failure and Abort Conditions + +Abort the mainnet upgrade if any of the following occurs: + +- `currentVersion` does not match the on-chain proxy version. +- The deployed implementation addresses in `deploy-result.json` are incomplete. +- Oracle addresses or governance parameters do not match the proposal. +- SAFE review finds any mismatch between `config.json`, `deploy-result.json`, and the imported batch. +- The batch cannot be fully signed and reviewed before the intended execution window. + +If execution fails mid-process, do not improvise manual fixes from SAFE without first reconciling: + +- which transactions were mined +- the current proxy implementation addresses +- current module pointers +- current governance parameters +- current oracle configuration + +## Mainnet Command Summary + +SSV Labs: + +```bash +just deploy mainnet +just generate-attestation mainnet +just generate-safe-batch mainnet +just verify-upgrade mainnet +``` + +SAFE committee: + +1. Import `deployments/mainnet/multisig-batch.json` +2. Review +3. Sign +4. Execute diff --git a/docs/architecture.md b/docs/architecture.md index cda8e9228..13aabeec7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,40 +1,110 @@ # SSV Network -### [Intro](../README.md) | Architecture | [Setup](setup.md) | [Tasks](tasks.md) | [Local development](local-dev.md) | [Roles](roles.md) | [Publish](publish.md) | [Operator owners](operators.md) +### [Intro](../README.md) | Architecture | [Setup](setup.md) | [Tasks](tasks.md) | [Local development](local-dev.md) | [Roles](roles.md) | [Operator owners](operators.md) +### [Specification](SPEC.md) | [Flows](FLOWS.md) | [Mainnet upgrade playbook](UPGRADE_PLAYBOOK.md) | [Deployments](../deployments/README.md) -## Contract Architecture +## Contract architecture -The architecture of the contracts is based on [EIP-2535 Diamond MultiFacet Proxy](https://eips.ethereum.org/EIPS/eip-2535) with some changes mainly to be compatible with regular block explorers like Etherscan. Main goals: +The upgraded SSV Network keeps the modular proxy-based design used in earlier versions, but the business logic has materially expanded in `v2.0.0`. New clusters are ETH-funded, fee accrual is effective-balance-aware, effective balance updates are driven by an oracle root flow, and SSV staking distributes ETH rewards through `cSSV`. -- **Modularity** - As the system evolves, we need to be able to move fast incorporating or changing functionalities without facing limitations like the contract size or disturbing existing architecture. -- **Upgradeability** - Allowing the DAO to evolve the system or solve issues. The process can be deactivated if such a decision is made. -- **Resilient innovation** - To encourage developer adoption, we designed a system easy to integrate and use. +At a high level: -### Main components +- `SSVNetwork` is the main write entrypoint. It is a UUPS-upgradeable proxy-like router that delegates logic to module contracts. +- `SSVNetworkViews` is the main read entrypoint. It is upgraded separately and exposes the consolidated read surface. +- Stateless module contracts contain protocol logic and are attached to `SSVNetwork` through module slots. +- Diamond-storage libraries keep protocol state out of the entrypoint contracts, which is critical for upgrade safety. +- `CSSVToken` represents staked SSV and is tightly coupled to the staking module. -#### SSVNetwork +## Main components -It's the main entry point for users, used for operations and management. It acts as a proxy for the _module_ contracts, where all functions that contain logic reside. All events are fired from the SSVNetwork contract. +### `SSVNetwork` -It's an [UUPS](https://eips.ethereum.org/EIPS/eip-1822) upgradeable contract. Apart from the state variables inherited by the UUPS Openzeppelin implementation, the contract storage is managed by the [Diamond storage pattern](https://eip2535diamonds.substack.com/i/65777640/diamond-storage) using a specific library. +`SSVNetwork` is the protocol write surface for operators, clusters, validators, DAO/governance actions, and staking. It owns the persistent protocol state through storage libraries and delegates external calls to the configured module addresses. -The fallback function is implemented to delegate all calls to the SSVViews module. -Any module interface can be used with this contract, so then you can access only the functions and events related to the specific interface of the module. This is helpful when you want access to a restricted set of functionalities belonging to Operators, Clusters, etc. +This contract is UUPS-upgradeable. Storage safety still depends on not introducing new mutable state directly into `SSVNetwork` or `SSVNetworkViews`. -#### SSVNetworkViews +### `SSVNetworkViews` -It's the main contract for reading information about the network and its participants. +`SSVNetworkViews` is the canonical read surface. It forwards view calls to `SSVNetwork` and exposes read helpers for operators, clusters, balances, fees, staking, oracle configuration, and protocol parameters. -#### Modules +### Modules -Non-upgradeable, stateless contracts that contain the logic to support Clusters, Operators, and Protocol (DAO / Network) functionalities. +The current module split is: -**Important**: Interacting directly with module contracts is not effective as you are not interacting with the correct state maintained by the main contract `SSVNetwork`. All interactions should be done via main contracts: `SSVNetwork` or `SSVNetworkViews`. +- `SSVOperators` for operator lifecycle, fee governance, and operator earnings +- `SSVOperatorsWhitelist` for private operators and whitelisting +- `SSVClusters` for deposits, withdrawals, liquidation, reactivation, migration, and effective-balance updates +- `SSVValidators` for validator registration, exit, and removal flows +- `SSVDAO` for governance parameters, oracle administration, and protocol-level configuration +- `SSVStaking` for SSV staking, unstake requests, and ETH reward accounting +- `SSVViews` for read-side helpers and derived accounting views -#### Libraries +Direct interaction with module contracts is not meaningful on its own because the protocol state lives behind `SSVNetwork`. -Libraries are a fundamental part of the architecture to support reusable pieces efficiently. Also, `SSVStorage` and `SSVStorageProtocol` implement the Diamond storage pattern. +### Storage libraries -#### SSV Token +Protocol state is organized through storage libraries such as: -The native SSV token is used to facilitate payments between stakers and SSV node operators to maintain their validators. +- `SSVStorage` +- `SSVStorageProtocol` +- `SSVStorageStaking` + +This separation is part of the repo’s upgrade discipline. Logic can evolve across modules and implementations while the storage layout remains explicit and reviewable. + +### Upgrade implementation + +The mainnet rollout uses a dedicated upgrade implementation at `contracts/upgrades/mainnet/SSVNetworkSSVStakingUpgrade.sol`. The operational flow around this implementation is documented in [UPGRADE_PLAYBOOK.md](UPGRADE_PLAYBOOK.md) and [deployments/README.md](../deployments/README.md). + +### `CSSVToken` + +`CSSVToken` is the staking receipt token for staked SSV. It is minted and burned by the staking module and hooks transfers back into staking so accrued ETH rewards are settled before balances move. + +## v2 system model + +### ETH clusters and legacy SSV clusters + +The most important conceptual change in `v2.0.0` is the split between: + +- **ETH clusters**, which are the new standard and pay operator and network fees in ETH +- **Legacy SSV clusters**, which keep their pre-upgrade SSV accounting model and are preserved mainly for continuity and migration + +The system is intentionally asymmetric after the upgrade. ETH clusters are the forward path. Legacy SSV clusters remain supported only within constrained rules. + +### Effective balance and `vUnits` + +ETH clusters are charged using effective-balance-aware accounting. The protocol normalizes effective balance into internal accounting units (`vUnits`) so fee burn scales with validator weight rather than only validator count. + +The detailed formulas live in [SPEC.md](SPEC.md). The important architectural point is that EB data is no longer a peripheral input. It directly affects solvency checks, fee accounting, liquidation risk, and operator/DAO bookkeeping for ETH clusters. + +### Oracle root flow + +Effective balance updates are fed on-chain through an oracle-committed Merkle root, and clusters consume those updates through `updateClusterBalance`. The root-commit mechanics, quorum rules, and Merkle encoding are specified in [SPEC.md](SPEC.md), while the exact execution flow is documented in [FLOWS.md](FLOWS.md). + +### Staking + +The upgraded system adds SSV staking. Users stake SSV, receive `cSSV`, and earn ETH rewards sourced from protocol network fees. The staking module, the DAO fee accounting, and the `cSSV` transfer hook form one accounting system and should be considered together when reviewing changes. + +### Migration + +Legacy SSV clusters can migrate to ETH through a one-way transition. After migration, the cluster follows the ETH accounting model and cannot return to the legacy SSV branch. + +## Design notes and operational gotchas + +These are intentional protocol behaviors worth keeping visible in high-level docs: + +- Legacy SSV clusters are restricted after the upgrade. They can be removed from, exited from, liquidated on the SSV path, migrated to ETH, and EB-updated, but they cannot continue as full-featured SSV clusters. +- Migration from SSV to ETH is one-way and irreversible. +- Effective balance starts as an implicit baseline and becomes explicit only after a successful oracle-backed `updateClusterBalance`. +- ETH deposits into liquidated clusters are allowed. This is useful for preparing reactivation. +- ETH withdrawals from liquidated clusters are allowed. +- Reactivation may rely on a stale on-chain EB snapshot. If the real effective balance increased while the cluster was inactive, the cluster may reactivate with insufficient ETH and later be auto-liquidated on the next valid EB update. +- Removed operators may be skipped during migration or reactivation flows. The cluster can continue with reduced operator coverage if the remaining configuration stays valid. + +These are protocol-level behaviors, not documentation shortcuts. Reviewers and operators should assume they are part of the supported design unless the spec changes. + +## Suggested reading order + +- Start here for the mental model +- Read [SPEC.md](SPEC.md) for rules, formulas, invariants, and access control +- Read [FLOWS.md](FLOWS.md) for function-by-function execution behavior +- Read [UPGRADE_PLAYBOOK.md](UPGRADE_PLAYBOOK.md) and [deployments/README.md](../deployments/README.md) for environment and rollout operations diff --git a/docs/local-dev.md b/docs/local-dev.md index 7b40f3a99..afe809478 100644 --- a/docs/local-dev.md +++ b/docs/local-dev.md @@ -1,138 +1,60 @@ # SSV Network -### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | [Tasks](tasks.md) | Local development | [Roles](roles.md) | [Publish](publish.md) | [Operator owners](operators.md) +### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | [Tasks](tasks.md) | Local development | [Roles](roles.md) | [Operator owners](operators.md) +### [Specification](SPEC.md) | [Flows](FLOWS.md) | [Mainnet upgrade playbook](UPGRADE_PLAYBOOK.md) | [Deployments](../deployments/README.md) -## Running against a local node / testnet +## Local development -You can deploy and run these contracts in a local node like Hardhat's, Ganache, or public testnets. This guide will cover the process. +This repository supports both fresh local deployments and fork-based validation of real environment configs. -### Run [Setup](setup.md) +## Fresh local deployment -Execute the steps to set up all tools needed. +Use the local deployment config and deploy everything from scratch: -### Configure Environment - -Copy [.env.example](../.env.example) to `.env` and edit to suit. - -- `[NETWORK]_ETH_NODE_URL` RPC URL of the node -- `[NETWORK]_OWNER_PRIVATE_KEY` Private key of the deployer account, without 0x prefix -- `GAS_PRICE` Example 30000000000 -- `GAS` Example 8000000 -- `ETHERSCAN_KEY` Etherescan API key to verify deployed contracts -- `SSV_TOKEN_ADDRESS` SSV Token contract address to be used in custom networks. Keep it empty to deploy a mocked SSV token. -- `MINIMUM_BLOCKS_BEFORE_LIQUIDATION` A number of blocks before the cluster enters into a liquidatable state. Example: 214800 = 30 days -- `OPERATOR_MAX_FEE_INCREASE` The fee increase limit in percentage with this format: 100% = 10000, 10% = 1000 - using 10000 to represent 2 digit precision -- `DECLARE_OPERATOR_FEE_PERIOD` The period in which an operator can declare a fee change (seconds) -- `EXECUTE_OPERATOR_FEE_PERIOD` The period in which an operator fee change can be executed (seconds) -- `VALIDATORS_PER_OPERATOR_LIMIT` The number of validators an operator can manage -- `MINIMUM_LIQUIDATION_COLLATERAL` The lowest number in wei a cluster can have before its liquidatable - -#### Network configuration - -In [hardhat.config.ts](../hardhat.config.ts) you can find specific configs for different networks, that are taken into account only when the `[NETWORK]_ETH_NODE_URL` parameter in `.env` file is set. -For example, in `.env` file you can set: - -``` -HOLESKY_ETH_NODE_URL="https://holesky.infura.io/v3/..." -NODE_PROVIDER_KEY="abcd1234..." -HOLESKY_OWNER_PRIVATE_KEY="d79d.." +```bash +just deploy-fresh local ``` -That means Hardhat will pick `config.networks.holesky` section in `hardhat.config.ts` to set the network parameters. +This is the quickest way to get a complete local protocol instance with the v2 module set. -### Start the local node +## Fork-based upgrade validation -To run the local node, execute the command in a separate terminal. +The preferred way to validate an environment upgrade is to run it on a local fork and then execute the strict fork tests. -```sh -npx hardhat node -``` +Example: -For more details about it and how to use MainNet forking you can find [here](https://hardhat.org/hardhat-network/). - -### Deployment - -The inital deployment process involves the deployment of all main modules (SSVClusters, SSVOperators, SSVDAO and SSVViews), SSVNetwork and SSVNetworkViews contracts. - -Note: The SSV token address used when deploying to live networks (holesky, mainnet) is set in the hardhat config file. To deploy the contracts to a custom network defined in the hardhat config file, leave `SSVTOKEN_ADDRESS` empty in the `.env` file. You can set a specific SSV token address for custom networks too, if needed. - -To run the deployment, execute: - -```sh -npx hardhat --network deploy:all +```bash +anvil --fork-url "$HOODI_RPC_URL" --port 8545 +just upgrade-test-fork hoodi-stage ``` -Output of this action will be: - -```sh -Deploying contracts with the account:0xf39... -SSVOperators module deployed to: 0x5Fb... -SSVClsuters module deployed to: 0xe7f1... -SSVDAO module deployed to: 0x9fE4... -SSVViews module deployed to: 0xCf7E... -Deploying SSVNetwork with ssvToken 0x3a9f... -SSVNetwork proxy deployed to: 0x5FC8... -SSVNetwork implementation deployed to: 0xDc64... -Deploying SSVNetworkViews with SSVNetwork 0x5FC8... -SSVNetworkViews proxy deployed to: 0xa513... -SSVNetworkViews implementation deployed to: 0x0165... -``` +Other useful variants: -As general rule, you can target any network configured in the `hardhat.config.ts`, specifying the right [network]\_ETH_NODE_URL and [network]\_OWNER_PRIVATE_KEY in `.env` file. - -### Verification on etherscan (only public networks) - -You can now go to Etherscan and see: - -- `SSVNetwork` proxy contract is deployed to the address shown previously in `SSVNetwork proxy deployed to` -- `SSVNetwork` implementation contract is deployed to the address shown previously in `SSVNetwork implementation deployed to` -- `SSVNetworkViews` proxy contract is deployed to the address shown previously in `SSVNetworkViews proxy deployed to` -- `SSVNetworkViews` implementation contract is deployed to the address shown previously in `SSVNetworkViews implementation deployed to` - -Open `.openzeppelin/.json` file and find `[impls..address]` value which is the implementation smart contract address. -You will find 2 `[impls.]` entries, one for `SSVNetwork` and another for `SSVNetworkViews`. -Run this verification process for both. - -You can take it from the output of the `npx hardhat --network deploy:all` command. - -To verify a proxy contract (SSVNetwork, SSVNetworkViews), run this: - -```sh -npx hardhat verify --network +```bash +just upgrade-fork hoodi-stage +just test-fork hoodi-stage +just smoke-test hoodi-stage ``` -By verifying a contract using its proxy address, the verification process for both the proxy and the implementation contracts is conducted seamlessly. -The proxy contract is automatically linked to the implementation contract. -As a result, users will be able to view interfaces of both the proxy and the implementation contracts on the Etherscan website's contract page, ensuring comprehensive visibility and transparency. - -To verify a module contract (SSVClusters, SSVOperators, SSVDAO, SSVViews), run this: - -```sh -npx hardhat verify --network -``` +## Environment sources of truth -Output of this action will be: +- `deployments//config.json` defines the intended configuration for that environment +- `deployments//deploy-result*.json` stores deployment outputs +- `deployments//upgrade-result*.json` stores upgrade outputs -```sh -Nothing to compile -No need to generate any newer typings. -Successfully submitted source code for contract -contracts/SSVNetwork.sol:SSVNetwork at 0x2279B7... -for verification on the block explorer. Waiting for verification result... +For the detailed schema and expected artifacts, use [deployments/README.md](../deployments/README.md). -Successfully verified contract SSVNetwork on Etherscan. -https://holesky.etherscan.io/address/0x227...#code -``` +## Verification and explorer flows -After this action, you can go to the proxy contract in Etherscan and start interacting with it. +This repo keeps explorer verification and post-upgrade config verification as script-driven workflows rather than documenting long manual steps here. -### How to resolve issues during the verification +Use: -- Error: no such file or directory, open ‘…/artifacts/build-info/XXXX...XXXX.json’ +- [deployments/README.md](../deployments/README.md) for env-aware deployment and verification flows +- [scripts/deployment.md](../scripts/deployment.md) for concise script examples -This issue can be resolved by executing the following commands. +## Troubleshooting -```sh -npx hardhat clean -npx hardhat compile -``` +- If an env-driven command fails early, check that `.env` has the required RPC URL and signer key for that environment. +- If fork tests cannot find deployed state, confirm Anvil is running on `127.0.0.1:8545` and that the chosen env config matches the forked network. +- If verification output looks stale, rerun the relevant deploy or upgrade flow so the latest result artifact is regenerated. diff --git a/docs/operators.md b/docs/operators.md index 7acb41d6e..b8da208b4 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -1,75 +1,72 @@ # SSV Network -### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | [Tasks](tasks.md) | [Local development](local-dev.md) | [Roles](roles.md) | [Publish](publish.md) | Operator owners +### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | [Tasks](tasks.md) | [Local development](local-dev.md) | [Roles](roles.md) | Operator owners +### [Specification](SPEC.md) | [Flows](FLOWS.md) | [Mainnet upgrade playbook](UPGRADE_PLAYBOOK.md) | [Deployments](../deployments/README.md) + +## Operator owners + +The operator lifecycle remains a core part of the SSV Network, but in `v2.0.0` operators now participate in both legacy SSV accounting and the new ETH fee model depending on cluster type and migration state. ## Registering an operator -The function `SSVNetwork.registerOperator()` is used to register a validator. -Input parameters: -`publicKey`: The public key of the operator -`fee`: Should be `0` or greater than `100000000` and less than the value returned by `SSVNetworkViews.getMaximumOperatorFee()` -`setPrivate`: Flag to set the privacy status of the operator. Public means anyone can use the operator for registering validators. Private means only the operator's whitelisted addresses can. -After the operator is registered, the caller becomes the `owner`, the `fee` is set and the `whitelisted` status is set to `false`. -The `whitelisted` flag of the operator indicates if the operator is private (when set to `true`) or public (`false`), +Operators are registered through `SSVNetwork.registerOperator(...)`. + +At registration time, the operator owner chooses: + +- the operator public key +- the initial fee +- whether the operator starts as private or public + +For the precise fee constraints and execution behavior, use [SPEC.md](SPEC.md) and [FLOWS.md](FLOWS.md). -## Whitelisted operators -An operator owner can restrict the usage of it to specific EOAs, generic contracts and whitelisting contracts. -A whitelisting contract is the one that implements the [ISSVWhitelistingContract](../contracts/interfaces/external/ISSVWhitelistingContract.sol) interface. +## Public and private operators -The restriction is only effective when the operator owner sets the privacy status of the operator to *private*. +Operators can be public or private: -To manage the whitelisted addresses, these 2 data structures are used: +- **Public** operators can be used by any eligible caller +- **Private** operators can only be used by addresses authorized through the protocol whitelist mechanisms -`mapping(uint64 => address) operatorsWhitelist`: Keeps the relation between an operator and a whitelisting contract. -`mapping(address => mapping(uint256 => uint256)) addressWhitelistedForOperators`: Links an address (EOA/generic contract) to a list of operators identified by its `operatorId` using bitmaps. +Whitelisting can be managed through: -### What is a Whitelisting Contract? -The operators can choose to whitelist an external contract with custom logic to manage authorized addresses externally. To be used in SSV contracts, it needs to implement the [ISSVWhitelistingContract](../contracts/interfaces/external/ISSVWhitelistingContract.sol) interface, that requires to implement the `isWhitelisted(address account, uint256 operatorId)` function. This function is called in the register validator process, that must return `true/false` to indicate if the caller (`msg.sender`) is whitelisted for the operator. +- direct address-based whitelists +- an external whitelisting contract implementing `ISSVWhitelistingContract` -It's up to the implementation of the whitelisting contract to use the `operatorId` parameter in the `isWhitelisted` function. +This design lets operator owners keep policy on-chain while still supporting custom authorization logic when needed. -To check if a contact is a valid whitelisting contract, use the function `SSVNetworkViews.isWhitelistingContract(address contractAddress)`. +## Whitelisting flows -To check if an account is whitelisted in a whitelisting contract, use the function `SSVNetworkViews.isAddressWhitelistedInWhitelistingContract(address account, uint256 operatorId, address whitelistingContract)`. +Relevant functions include: -### Legacy whitelisted addresses transition process -Up until v1.1.1, operators use the `operatorsWhitelist` mapping to save EOAs and generic contracts. Now in v1.2.0, those type of addresses are stored in `addressWhitelistedForOperators`, leaving `operatorsWhitelist` to save only whitelisting contracts. -When whitelisting a new whitelisting contract, the current address stored in `operatorsWhitelist` will be moved to `addressWhitelistedForOperators`, and the new address stored in `operatorsWhitelist`. -When whitelisting a new EOA/generic contract, it will be saved in `addressWhitelistedForOperators`, leaving the previous address in `operatorsWhitelist` intact. +- `setOperatorsWhitelists` +- `removeOperatorsWhitelists` +- `setOperatorsWhitelistingContract` +- `removeOperatorsWhitelistingContract` +- `setOperatorsPrivateUnchecked` +- `setOperatorsPublicUnchecked` -### Operator whitelist states -The following table shows all possible combinations of whitelisted addresses for a given operator. -| Use legacy EOA/generic contract | Use whitelisting contract | Use EOAs/generic contracts | -|---|---|---| -| Y | | | -| Y | | Y | -| | Y | | -| | | Y | -| | Y | Y | +When a validator is registered against a private operator, the protocol checks whether the caller is authorized for that operator. Existing validators are not retroactively removed if whitelist settings later change. -The operarator status changes to private (`Operator.whitelisted == true`), so only the whitelisted addresses can use the operator's services when the operator owner explicitly sets the *private* status calling `SSVNetwork.setOperatorsPrivateUnchecked()`, no matter if it has whitelisted addresses. +## ETH fee model for operators -The operarator status changes to public (`Operator.whitelisted == false`), so anyone can use the operator's services when the operator owner explicitly sets the public status calling `SSVNetwork.setOperatorsPublicUnchecked()`, no matter if it still has whitelisted addresses. +In the upgraded system, ETH is the fee asset for new clusters. Operator owners should understand: -### Registering whitelist addresses -Functions related to whitelisting contracts: -- Register: `SSVNetwork.setOperatorsWhitelistingContract(uint64[] calldata operatorIds, ISSVWhitelistingContract whitelistingContract)` -- Remove: `SSVNetwork.removeOperatorsWhitelistingContract(uint64[] calldata operatorIds)` +- ETH fee changes follow a declare/execute or immediate-reduce model +- earnings may exist on both ETH and legacy SSV branches depending on operator history +- legacy operators can transition into ETH flows as clusters migrate or register under the new model -Functions related to EOAs/generic contracts: -- Register multiple addresses to multiple operators: `SSVNetwork.setOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses)` -- Remove multiple addresses for multiple operators: `SSVNetwork.removeOperatorsWhitelists(uint64[] calldata operatorIds, address[] calldata whitelistAddresses)` +Detailed fee-settlement rules, default ETH fee behavior, and earnings accounting are defined in [SPEC.md](SPEC.md). -### Registering validators using whitelisted operators -When registering validators using `SSVNetwork.registerValidator` or `SSVNetwork.registerValidator`, the flow to check if the caller is authorized to use a whitelisted operator is the following: -1. Check if the operator is whitelisted via the SSV whitelisting module, using `addressWhitelistedForOperators`. -2. Check if the operator has a whitelisted address in `operatorsWhitelist`. - 1. Check if the caller is the whitelisted address. In this step we keep the whitelisting system backward compatible with previous whitelisted EOAs/generic contracts. - 2. Check if the address is a whitelisting contract. Then call its `isWhitelisted()` function. +## Earnings withdrawal -If the caller is not authorized for any of the whitelisted operators, the transaction will revert with the `CallerNotWhitelistedWithData()` error. +Operator owners can withdraw: -**Important**: Changes to an operator's whitelist will not impact existing validators registered with that operator. Only new validator registrations will adhere to the updated whitelist rules. +- ETH earnings through the ETH withdrawal functions +- legacy SSV earnings through the SSV withdrawal functions where applicable +The repo keeps both branches because the system must support pre-upgrade state while moving the active network model toward ETH. +## Practical notes +- Removing an operator does not erase the historical owner address used for read-side visibility. +- Removed operators may still matter to cluster history and migration logic, so operator removal should be treated as a protocol event, not just a UI cleanup action. +- Private operator policy affects future validator registration attempts, not historical validator membership. diff --git a/docs/publish.md b/docs/publish.md deleted file mode 100644 index ebe0ba6aa..000000000 --- a/docs/publish.md +++ /dev/null @@ -1,44 +0,0 @@ -# SSV Network - -### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | [Tasks](tasks.md) | [Local development](local-dev.md) | [Roles](roles.md) | Publish | [Operator owners](operators.md) - -## Prerequisites - -- Ensure you have [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. -- An npm account. [Sign up here](https://www.npmjs.com/signup) if you don't have one. - -## Prepare Package - -Before publishing, make sure your `package.json` is properly set up: - -- `name`: The package name (must be unique on npm). -- `version`: The current version of the package. -- `description`: A brief description of your package. -- `main`: The entry point of your package (usually `index.js`). -- `scripts`: Any scripts you want to include, like build or test scripts. -- `author`: The author's name and contact information. -- `repository`: The repository URL where your code is located. -- `keywords`: An array of keywords to help users discover your package. -- `files`: An array of file patterns that describes which files should be included when your package is installed. -- `dependencies` and `devDependencies`: Any required packages. - -## Authenticate with npm - -- Log in to your npm account from the command line: - -```bash -npm login -``` - -- Enter your npm username, password, and email address when prompted. - -## Configure GitHub Actions for Automated Publishing - -- Create a [.github/workflows/publish.yaml](../.github/workflows/publish.yaml) file in your project. -- Define the npm publishing process using GitHub Actions: -- Add your npm token `NPM_TOKEN` to the GitHub repository secrets (Settings > Secrets). - -## Publish Package - -- Generate a release in the `main` branch of the `ssv_network` GitHub repository. -- The GitHub Actions workflow will automatically publish the package to npm. diff --git a/docs/roles.md b/docs/roles.md index 8cf1da088..501dcffce 100644 --- a/docs/roles.md +++ b/docs/roles.md @@ -1,54 +1,76 @@ # SSV Network -### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | [Tasks](tasks.md) | [Local development](local-dev.md) | Roles | [Publish](publish.md) | [Operator owners](operators.md) +### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | [Tasks](tasks.md) | [Local development](local-dev.md) | Roles | [Operator owners](operators.md) +### [Specification](SPEC.md) | [Flows](FLOWS.md) | [Mainnet upgrade playbook](UPGRADE_PLAYBOOK.md) | [Deployments](../deployments/README.md) + +## Protocol roles + +The upgraded system has more operational roles than the earlier network versions because the protocol now includes oracle-fed effective balance accounting and staking. ## Contract owner -The contract owner can perform operational actions over the contract and protocol updates. +The contract owner is the governance authority over the deployed protocol. In production this is expected to be a SAFE multisig. + +Owner-controlled responsibilities include: + +- Upgrading `SSVNetwork` and `SSVNetworkViews` +- Updating attached module addresses through `updateModule` +- Updating protocol parameters such as network fees, operator fee bounds, liquidation thresholds, cooldowns, and EB update rate limits +- Replacing oracle addresses and updating quorum +- Withdrawing protocol-controlled SSV earnings and invoking owner-only recovery paths + +The exact owner-only access surface is defined in [SPEC.md](SPEC.md). + +## Deployer / release operator + +The deployer is not a protocol role in the accounting model, but it is an operational role for this repository. + +Typical deployer responsibilities: + +- Deploying new implementations and modules +- Generating deployment attestations +- Generating SAFE batch payloads +- Running fork validation and post-upgrade verification -### Contract operations +These workflows are documented in [deployments/README.md](../deployments/README.md) and [UPGRADE_PLAYBOOK.md](UPGRADE_PLAYBOOK.md). -- Upgrade `SSVNetwork` and `SSVNetworkViews` -- `SSVNetwork.upgradeModule()` - Update any module +## Oracle -### Protocol updates +Registered oracle addresses can submit effective-balance Merkle roots through `commitRoot`. Oracles do not own cluster funds, but they do affect when the on-chain system can consume fresh EB data. -- `SSVNetwork.updateNetworkFee()` - Updates the network fee -- `SSVNetwork.withdrawNetworkEarnings()` - Withdraws network earnings -- `SSVNetwork.updateOperatorFeeIncreaseLimit()` - Updates the limit on the percentage increase in operator fees -- `SSVNetwork.updateDeclareOperatorFeePeriod()` - Updates the period for declaring operator fees -- `SSVNetwork.updateExecuteOperatorFeePeriod()` - Updates the period for executing operator fees -- `SSVNetwork.updateLiquidationThresholdPeriod()` - Updates the liquidation threshold period -- `SSVNetwork.updateMinimumLiquidationCollateral()` - Updates the minimum collateral required to prevent liquidation -- `SSVNetwork.updateMaximumOperatorFee()` - Updates the maximum fee an operator can set +Oracle administration remains owner-controlled through `replaceOracle` and `updateQuorumBps`. ## Operator owner -Only the owner of an operator can execute these functions: - -- `SSVNetwork.removeOperator` - Removes an existing operator -- `SSVNetwork.setOperatorsWhitelists` - Sets a list of whitelisted addresses (EOAs or generic contracts) for a list of operators -- `SSVNetwork.removeOperatorsWhitelists` - Removes a list of whitelisted addresses (EOAs or generic contracts) for a list of operators -- `SSVNetwork.setOperatorsWhitelistingContract` - Sets a whitelisting contract for a list of operators -- `SSVNetwork.removeOperatorsWhitelistingContract` - Removes the whitelisting contract set for a list of operators -- `SSVNetwork.setOperatorsPrivateUnchecked` - Set the list of operators as private without checking for any whitelisting address -- `SSVNetwork.setOperatorsPublicUnchecked` - Set the list of operators as public without removing any whitelisting address -- `SSVNetwork.declareOperatorFee` - Declares the operator's fee change -- `SSVNetwork.executeOperatorFee` - Executes the operator's fee change -- `SSVNetwork.cancelDeclaredOperatorFee` - Cancels the declared operator's fee -- `SSVNetwork.reduceOperatorFee` - Reduces the operator's fee -- `SSVNetwork.withdrawOperatorEarnings` - Withdraws operator earnings -- `SSVNetwork.withdrawAllOperatorEarnings` - Withdraws all operator earnings +An operator owner controls a specific operator record and can: + +- Register and remove operators +- Manage private/public status and whitelisting +- Declare, execute, reduce, or cancel operator fee changes +- Withdraw operator earnings in ETH or legacy SSV paths where applicable + +Operator-specific details are documented in [operators.md](operators.md). ## Cluster owner -Only the owner of a cluster can execute these functions: +A cluster owner controls validator and cluster lifecycle actions for a cluster, including: + +- Registering validators into ETH clusters +- Removing validators +- Signaling validator exit +- Depositing ETH, withdrawing ETH, and reactivating ETH clusters +- Migrating a legacy SSV cluster to ETH + +Some actions are intentionally permissionless: + +- `deposit` can be called by anyone on behalf of a cluster owner +- `updateClusterBalance` is permissionless when the caller has a valid proof against a committed root +- liquidation can be triggered by third parties when the protocol rules allow it + +## Staker + +Any address with SSV can stake into the protocol, receive `cSSV`, request unstake, withdraw unlocked SSV, and claim ETH rewards. + +## Read-only integrator -- `SSVNetwork.registerValidator` - Registers a new validator on the SSV Network -- `SSVNetwork.bulkRegisterValidator` - Registers a set of validators in the same cluster on the SSV Network -- `SSVNetwork.removeValidator` - Removes an existing validator from the SSV Network -- `SSVNetwork.bulkRemoveValidator` - Bulk removes a set of existing validators in the same cluster from the SSV Network -- `SSVNetwork.reactivate` - Reactivates a cluster -- `SSVNetwork.withdraw` - Withdraws tokens from a cluster -- `SSVNetwork.exitValidator` - Starts the exit protocol for an exisiting validator -- `SSVNetwork.bulkExitValidator` - Starts the exit protocol for a set of existing validators +Integrators and indexers generally consume the protocol through `SSVNetworkViews`. `SSVNetworkViews` is treated as the canonical consolidated read surface. diff --git a/docs/setup.md b/docs/setup.md index b4307d7b6..53af8139e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,34 +1,55 @@ # SSV Network -### [Intro](../README.md) | [Architecture](architecture.md) | Setup | [Tasks](tasks.md) | [Local development](local-dev.md) | [Roles](roles.md) | [Publish](publish.md) | [Operator owners](operators.md) +### [Intro](../README.md) | [Architecture](architecture.md) | Setup | [Tasks](tasks.md) | [Local development](local-dev.md) | [Roles](roles.md) | [Operator owners](operators.md) +### [Specification](SPEC.md) | [Flows](FLOWS.md) | [Mainnet upgrade playbook](UPGRADE_PLAYBOOK.md) | [Deployments](../deployments/README.md) -## Developer Setup +## Developer setup -The stack is a simple one: +This repository uses Solidity, Hardhat, TypeScript scripts, npm dependencies, and `just` recipes as the main local workflow. -- Solidity -- JavaScript -- Node/NPM -- HardHat -- Ethers +## Prerequisites -### Install Node (also installs NPM) +- Node.js LTS +- npm +- [`just`](https://github.com/casey/just) for running the repository workflows -- Use the latest [LTS (long-term support) version](https://nodejs.org/en/download/). +Optional but useful: -### Install required Node modules +- Anvil for fork-based testing and upgrade validation +- Slither for static analysis +- Echidna for invariant fuzzing -All NPM resources are project local. No global installs are required. +## Install dependencies -``` -cd path/to/ssv-network +```bash npm install ``` -### Configure Environment +## Configure the environment + +Copy the example file and fill in the values needed for the environments you use: + +```bash +cp .env.example .env +``` + +Common variables: + +- `MAINNET_RPC_URL` and `HOODI_RPC_URL` for RPC access +- `MAINNET_PRIVATE_KEY` and `HOODI_PRIVATE_KEY` for live owner or deployer actions +- `ETHERSCAN_KEY` for block-explorer verification + +The environment-specific deployment source of truth lives under `deployments//config.json`. The `.env` file mainly supplies RPC and signer credentials. + +## Compile and test + +```bash +just build +just test-unit +``` -- Copy [.env.example](../.env.example) to `.env` and edit to suit. -- API keys are only needed for deploying to public networks. -- `.env` is included in `.gitignore` and will not be committed to the repo. +Useful next steps: -At this moment you are ready to run tests, compile contracts and run coverage tests. +- See [tasks.md](tasks.md) for the full `just` recipe list +- See [local-dev.md](local-dev.md) for local deployment and fork flows +- See [deployments/README.md](../deployments/README.md) for environment-driven deployment and upgrade workflows diff --git a/docs/tasks.md b/docs/tasks.md index 2c64cb1a5..fb8139311 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -1,150 +1,72 @@ # SSV Network -### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | Tasks | [Local development](local-dev.md) | [Roles](roles.md) | [Publish](publish.md) | [Operator owners](operators.md) +### [Intro](../README.md) | [Architecture](architecture.md) | [Setup](setup.md) | Tasks | [Local development](local-dev.md) | [Roles](roles.md) | [Operator owners](operators.md) +### [Specification](SPEC.md) | [Flows](FLOWS.md) | [Mainnet upgrade playbook](UPGRADE_PLAYBOOK.md) | [Deployments](../deployments/README.md) -## Development scripts +## Development workflows -All scripts can be executed using `package.json` scripts. +This repository uses `just` recipes for day-to-day work. There are no Hardhat task workflows to document here. -### Build the contracts +## Core recipes -This creates the build artifacts for deployment or testing +### Build and cleanup +```bash +just build +just clean ``` -npm run build -``` - -### Test the contracts -This builds the contracts and runs the unit tests. It also runs the gas reporter and it outputs the report at the end of the tests. +### Test suites +```bash +just test +just test-unit +just test-integration +just test-forked +just coverage +just sizes ``` -npm run test -``` - -### Run the code coverage -This builds the contracts and runs the code coverage. This is slower than testing since it makes sure that every line of our contracts is tested. It outputs the report in folder `coverage`. +### Environment-driven deployment and upgrade +```bash +just deploy-fresh local +just deploy hoodi-stage +just upgrade hoodi-stage +just upgrade-fork hoodi-stage +just test-fork hoodi-stage +just upgrade-test-fork hoodi-stage +just verify-upgrade hoodi-stage +just smoke-test hoodi-stage ``` -npm run solidity-coverage -``` - -### Slither - -Runs the static analyzer [Slither](https://github.com/crytic/slither), to search for common solidity vulnerabilities. By default it analyzes all contracts. -`npm run slither` - -### Size contracts - -Compiles the contracts and report the size of each one. Useful to check to not surpass the 24k limit. - -``` -npm run size-contracts -``` - -## Development tasks -This project uses hardhat tasks to perform the deployment and upgrade of the main contracts and modules. +### Mainnet release support -Following Hardhat's way of working, you must specify the network against which you want to run the task using the `--network` parameter. In all the following examples, the holesky network will be used, but you can specify any defined in your `hardhat.config` file. - -### Deploy all contracts - -Runs the deployment of the main SSVNetwork and SSVNetworkViews contracts, along with their associated modules: - -``` -npx hardhat --network holesky_testnet deploy:all +```bash +just deploy mainnet +just generate-attestation mainnet +just generate-safe-batch mainnet +just verify-upgrade mainnet ``` -When deploying to live networks like Holesky or Mainnet, please double check the environment variables: - -- MINIMUM_BLOCKS_BEFORE_LIQUIDATION -- MINIMUM_LIQUIDATION_COLLATERAL -- VALIDATORS_PER_OPERATOR_LIMIT -- DECLARE_OPERATOR_FEE_PERIOD -- EXECUTE_OPERATOR_FEE_PERIOD -- OPERATOR_MAX_FEE_INCREASE - -## Upgrade process - -We use [UUPS Proxy Upgrade pattern](https://docs.openzeppelin.com/contracts/4.x/api/proxy) for `SSVNetwork` and `SSVNetworkViews` contracts to have an ability to upgrade them later. - -**Important**: It's critical to not add any state variable to `SSVNetwork` nor `SSVNetworkViews` when upgrading. All the state variables are managed by [SSVStorage](../contracts/libraries/SSVStorage.sol) and [SSVStorageProtocol](../contracts/libraries/SSVStorageProtocol.sol). Only modify the logic part of the main contracts or the modules. - -### Upgrade SSVNetwork / SSVNetworkViews - -#### Upgrade contract logic - -In this case, the upgrade add / delete / modify a function, but no other piece in the system is changed (libraries or modules). - -Set `SSVNETWORK_PROXY_ADDRESS` in `.env` file to the right value. - -Run the upgrade task: +### One-off utilities +```bash +just deploy-module [args...] +just attach-module +just upgrade-contract [impl] +just verify
+just abis ``` -Usage: hardhat [GLOBAL OPTIONS] upgrade:proxy [--contract ] [--init-function ] [--proxy-address ] [...params] -OPTIONS: - --contract New contract upgrade - --init-function Function to be executed after upgrading - --proxy-address Proxy address of SSVNetwork / SSVNetworkViews +## Where to find the full process docs -POSITIONAL ARGUMENTS: - params Function parameters +- For deployment environments, config schema, result artifacts, SAFE batches, and verification flows, use [deployments/README.md](../deployments/README.md). +- For quick operational examples around the deployment scripts, use [scripts/deployment.md](../scripts/deployment.md). +- For mainnet-specific upgrade sequencing, use [UPGRADE_PLAYBOOK.md](UPGRADE_PLAYBOOK.md). -Example: -npx hardhat --network holesky_testnet upgrade:proxy --proxy-address 0x1234... --contract SSVNetworkV2 --init-function initializev2 param1 param2 -``` - -It is crucial to verify the upgraded contract using its proxy address. -This ensures that users can interact with the correct, upgraded implementation on Etherscan. - -### Update a module - -Sometimes you only need to perform changes in the logic of a function of a module, add a private function or do something that doesn't affect other components in the architecture. Then you can use the task to update a module. - -This task first deploys a new version of a specified SSV module contract, and then updates the SSVNetwork contract to use this new module version only if `--attach-module` flag is set to `true`. - -``` -Usage: hardhat [GLOBAL OPTIONS] update:module [--attach-module ] [--module ] [--proxy-address ] - -OPTIONS: - - --attach-module Attach module to SSVNetwork contract (default: false) - --module SSV Module - --proxy-address Proxy address of SSVNetwork / SSVNetworkViews (default: null) - - -Example: -Update 'SSVOperators' module contract in the SSVNetwork -npx hardhat --network holesky_testnet update:module --module SSVOperators --attach-module true --proxy-address 0x1234... -``` - -### Upgrade a library - -When you change a library that `SSVNetwork` uses, you need to also update all modules where that library is used. - -Set `SSVNETWORK_PROXY_ADDRESS` in `.env` file to the right value. - -Run the task to upgrade SSVNetwork proxy contract as described in [Upgrade SSVNetwork / SSVNetworkViews](#upgrade-contract-logic) - -Run the right script to update the module affected by the library change, as described in [Update a module](#update-a-module) section. - -### Manual upgrade of SSVNetwork / SSVNetworkViews - -Validates and deploys a new implementation contract. Use this task to prepare an upgrade to be run from an owner address you do not control directly or cannot use from Hardhat. - -``` -Usage: hardhat [GLOBAL OPTIONS] upgrade:prepare [--contract ] [--proxy-address ] - -OPTIONS: - - --contract New contract upgrade (default: null) - --proxy-address Proxy address of SSVNetwork / SSVNetworkViews (default: null) - -Example: -npx hardhat --network holesky_testnet upgrade:prepare --proxy-address 0x1234... --contract SSVNetworkViewsV2 -``` +## Notes -The task will return the new implementation address. After that, you can run `upgradeTo` or `upgradeToAndCall` in SSVNetwork / SSVNetworkViews proxy address, providing it as a parameter. +- Environment-specific values come from `deployments//config.json`. +- RPC URLs and signer keys come from `.env`. +- When changing shared libraries, treat dependent modules as part of the same upgrade surface and redeploy them together. diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 000000000..6409450e6 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,17 @@ +[profile.default] +src = "contracts" +test = "test" +out = "out" +libs = ["node_modules"] +auto_detect_solc = true +via_ir = true +optimizer = true +optimizer_runs = 10000 +evm_version = "cancun" + +remappings = [ + "@openzeppelin/=node_modules/@openzeppelin/" +] + +[rpc_endpoints] +hoodi = "https://hoodi.infura.io/v3/{INFURA_KEY}" diff --git a/hardhat.config.ts b/hardhat.config.ts index aac45bb0e..68c07c00e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,29 +1,35 @@ -import { HardhatUserConfig } from 'hardhat/config'; -import { NetworkUserConfig } from 'hardhat/types'; - import 'dotenv/config'; +import hardhatToolboxMochaEthersPlugin from "@nomicfoundation/hardhat-toolbox-mocha-ethers"; +import { defineConfig, configVariable } from "hardhat/config"; +import '@nomicfoundation/hardhat-ethers-chai-matchers'; +import '@nomicfoundation/hardhat-verify'; -import '@nomicfoundation/hardhat-toolbox-viem'; -import '@nomicfoundation/hardhat-chai-matchers'; -import '@openzeppelin/hardhat-upgrades'; - -import 'hardhat-abi-exporter'; -import 'hardhat-contract-sizer'; -import 'solidity-coverage'; - -import './tasks/deploy'; -import './tasks/update-module'; -import './tasks/upgrade'; - -type SSVNetworkConfig = NetworkUserConfig & { - ssvToken: string; +const isCoverage = process.env.COVERAGE === "true"; +const envValue = (name: string): string | undefined => { + const value = process.env[name]; + return value && value.trim().length > 0 ? value : undefined; }; +const localForkRpcUrl = "http://127.0.0.1:8545"; +const localForkChainId = 31337; +const mainnetRpcUrl = + envValue("MAINNET_RPC_URL") ?? + configVariable("MAINNET_RPC_URL"); -const config: HardhatUserConfig = { - mocha: { - timeout: 40000000000000000, +export default defineConfig({ + plugins: [hardhatToolboxMochaEthersPlugin], + chainDescriptors: { + [localForkChainId]: { + name: "Local Anvil Fork", + chainType: "l1", + hardforkHistory: { + // Local Anvil forks report chainId 31337 with upstream block numbers. + // EDR needs an explicit history for custom chain IDs to execute historical calls. + cancun: { blockNumber: 0 }, + }, + }, }, solidity: { + npmFilesToBuild: ["@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"], compilers: [ { version: '0.8.4', @@ -34,9 +40,10 @@ const config: HardhatUserConfig = { { version: '0.8.24', settings: { + viaIR: true, optimizer: { enabled: true, - runs: 10000, + runs: isCoverage ? 200 : 10000, }, evmVersion: 'cancun', }, @@ -44,95 +51,60 @@ const config: HardhatUserConfig = { ], }, networks: { - ganache: { - chainId: 1337, - url: 'http://127.0.0.1:8585', - ssvToken: process.env.SSVTOKEN_ADDRESS, // if empty, deploy SSV mock token - } as SSVNetworkConfig, hardhat: { + type: 'edr-simulated', + hardfork: 'cancun', allowUnlimitedContractSize: true, + blockGasLimit: 500_000_000, }, + hardhat_forked: { + type: 'edr-simulated', + chainType: "l1", + allowUnlimitedContractSize: true, + blockGasLimit: 100_000_000, + forking: { + url: mainnetRpcUrl, + blockNumber: process.env.FORK_BLOCK_NUMBER ? Number(process.env.FORK_BLOCK_NUMBER) : undefined, + } + }, + local: { + type: "http", + chainType: "l1", + url: localForkRpcUrl, + }, + hoodi: { + type: "http", + chainType: "l1", + url: configVariable("HOODI_RPC_URL"), + accounts: [configVariable("HOODI_PRIVATE_KEY")], + ssvToken: process.env.HOODI_SSVTOKEN_ADDRESS + }, + mainnet: { + type: "http", + chainType: "l1", + url: mainnetRpcUrl, + accounts: [configVariable("MAINNET_PRIVATE_KEY")], + ssvToken: process.env.MAINNET_SSVTOKEN_ADDRESS + } }, - etherscan: { - apiKey: process.env.ETHERSCAN_KEY, - customChains: [ - { - network: 'holesky', - chainId: 17000, - urls: { - apiURL: 'https://api-holesky.etherscan.io/api', - browserURL: 'https://holesky.etherscan.io', - }, - }, - ], - }, - contractSizer: { - alphaSort: true, - disambiguatePaths: false, - runOnCompile: false, - strict: false, - }, - sourcify: { - enabled: false + verify: { + etherscan: { + apiKey: configVariable("ETHERSCAN_KEY"), + }, }, - abiExporter: { - path: './abis', - runOnCompile: true, - clear: true, - flat: true, - spacing: 2, - pretty: false, - only: ['contracts/SSVNetwork.sol', 'contracts/SSVNetworkViews.sol'], + test: { + mocha: { + timeout: 300_000, + }, }, -}; +}); -if (process.env.HOLESKY_ETH_NODE_URL && process.env.HOLESKY_OWNER_PRIVATE_KEY) { - const sharedConfig = { - url: `${process.env.HOLESKY_ETH_NODE_URL}${process.env.NODE_PROVIDER_KEY}`, - accounts: [`0x${process.env.HOLESKY_OWNER_PRIVATE_KEY}`], - gasPrice: +(process.env.GAS_PRICE || ''), - gas: +(process.env.GAS || ''), - }; - //@ts-ignore - config.networks = { - ...config.networks, - holesky_development: { - ...sharedConfig, - ssvToken: '0x68A8DDD7a59A900E0657e9f8bbE02B70c947f25F', - } as SSVNetworkConfig, - holesky_testnet: { - ...sharedConfig, - ssvToken: '0xad45A78180961079BFaeEe349704F411dfF947C6', - } as SSVNetworkConfig, - }; -} +declare module "hardhat/types/config" { + interface HttpNetworkUserConfig { + ssvToken?: string | undefined; + } -if (process.env.MAINNET_ETH_NODE_URL && process.env.MAINNET_OWNER_PRIVATE_KEY) { - //@ts-ignore - config.networks = { - ...config.networks, - mainnet: { - url: `${process.env.MAINNET_ETH_NODE_URL}${process.env.NODE_PROVIDER_KEY}`, - accounts: [`0x${process.env.MAINNET_OWNER_PRIVATE_KEY}`], - gasPrice: +(process.env.GAS_PRICE || ''), - gas: +(process.env.GAS || ''), - ssvToken: '0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54', - } as SSVNetworkConfig, - }; + interface HttpNetworkConfig { + ssvToken?: string | undefined; + } } - -if (process.env.FORK_TESTING_ENABLED) { - config.networks = { - ...config.networks, - hardhat: { - ...config.networks?.hardhat, - forking: { - enabled: process.env.FORK_TESTING_ENABLED === 'true', - url: `${process.env.MAINNET_ETH_NODE_URL}${process.env.NODE_PROVIDER_KEY}`, - blockNumber: 19621100, - }, - }, - }; -} - -export default config; diff --git a/package-lock.json b/package-lock.json index 90df7c8f5..96aa25c22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,32 @@ { "name": "ssv-network", - "version": "1.2.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ssv-network", - "version": "1.2.0", + "version": "2.0.0", "license": "MIT", "devDependencies": { - "@nomicfoundation/edr": "^0.3.4", - "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", - "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", + "@nomicfoundation/hardhat-ethers": "^4.0.3", + "@nomicfoundation/hardhat-ethers-chai-matchers": "^3.0.2", + "@nomicfoundation/hardhat-ignition": "^3.0.6", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@nomicfoundation/hardhat-verify": "^3.0.8", "@openzeppelin/contracts": "^4.9.6", "@openzeppelin/contracts-upgradeable": "^4.9.6", - "@openzeppelin/hardhat-upgrades": "^3.0.5", - "dotenv": "^16.4.5", - "hardhat": "^2.22.4", - "hardhat-abi-exporter": "^2.10.1", - "hardhat-contract-sizer": "^2.10.0", - "solidity-coverage": "^0.8.12", - "ssv-keys": "github:bloxapp/ssv-keys#v1.0.4" + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.2", + "chai": "^5.3.3", + "dotenv": "^17.2.3", + "ethers": "^6.16.0", + "hardhat": "^3.1.0", + "mocha": "^11.7.5", + "solhint": "^5.0.0", + "tsx": "^4.19.0" } }, "node_modules/@adraffy/ens-normalize": { @@ -29,418 +34,479 @@ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "dev": true, - "peer": true + "license": "MIT" }, - "node_modules/@aws-crypto/sha256-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", - "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, + "license": "MIT", "dependencies": { - "@aws-crypto/util": "^1.2.2", - "@aws-sdk/types": "^3.1.0", - "tslib": "^1.11.1" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@aws-crypto/util": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", - "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, - "dependencies": { - "@aws-sdk/types": "^3.1.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/types": { - "version": "3.609.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", - "integrity": "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "@smithy/types": "^3.3.0", - "tslib": "^2.6.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=16.0.0" + "node": ">=18" } }, - "node_modules/@aws-sdk/types/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - }, - "node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.259.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", - "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "node_modules/@esbuild/android-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "tslib": "^2.3.1" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@aws-sdk/util-utf8-browser/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "node_modules/@esbuild/android-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "color-name": "1.1.3" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "cpu": [ + "arm" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.8.0" + "node": ">=18" } }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "cpu": [ + "loong64" + ], "dev": true, + "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.1.90" + "node": ">=18" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "cpu": [ + "mips64el" + ], "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@ethereumjs/common": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.6.5.tgz", - "integrity": "sha512-lRyVQOeCDaIVtgfbowla32pzeDv2Obr8oR8Put5RdUBNRGr1VGPGQNGP6elWIpgK3YdpzqTOh4GyUGOureVeeA==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "crc-32": "^1.2.0", - "ethereumjs-util": "^7.1.5" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@ethereumjs/rlp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", - "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "bin": { - "rlp": "bin/rlp" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/@ethereumjs/tx": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-3.5.2.tgz", - "integrity": "sha512-gQDNJWKrSDGu2w7w0PzVXVBNMzb7wwdDOmOqczmhNjqFxFuIbhVJDwiGEnxFNC2/b8ifcZzY7MLcluizohRzNw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], "dev": true, - "dependencies": { - "@ethereumjs/common": "^2.6.4", - "ethereumjs-util": "^7.1.5" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@ethereumjs/util": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", - "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@ethereumjs/rlp": "^4.0.1", - "ethereum-cryptography": "^2.0.0", - "micro-ftch": "^0.3.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/@ethereumjs/util/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@ethereumjs/util/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">=18" } }, - "node_modules/@ethereumjs/util/node_modules/ethereum-cryptography": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", - "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@noble/curves": "1.4.2", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@ethersproject/abi": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.7.0.tgz", - "integrity": "sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } + "license": "MIT", + "optional": true, + "os": [ + "openbsd" ], - "dependencies": { - "@ethersproject/address": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/hash": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/strings": "^5.7.0" + "engines": { + "node": ">=18" } }, - "node_modules/@ethersproject/abstract-provider": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz", - "integrity": "sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } + "license": "MIT", + "optional": true, + "os": [ + "openharmony" ], - "dependencies": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/networks": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/transactions": "^5.7.0", - "@ethersproject/web": "^5.7.0" + "engines": { + "node": ">=18" } }, - "node_modules/@ethersproject/abstract-signer": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz", - "integrity": "sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } + "license": "MIT", + "optional": true, + "os": [ + "sunos" ], - "dependencies": { - "@ethersproject/abstract-provider": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0" + "engines": { + "node": ">=18" } }, - "node_modules/@ethersproject/address": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.7.0.tgz", - "integrity": "sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" ], - "dependencies": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/rlp": "^5.7.0" + "engines": { + "node": ">=18" } }, - "node_modules/@ethersproject/base64": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.7.0.tgz", - "integrity": "sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "cpu": [ + "ia32" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" ], - "dependencies": { - "@ethersproject/bytes": "^5.7.0" + "engines": { + "node": ">=18" } }, - "node_modules/@ethersproject/basex": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.7.0.tgz", - "integrity": "sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==", + "node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" ], - "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/properties": "^5.7.0" + "engines": { + "node": ">=18" } }, - "node_modules/@ethersproject/bignumber": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.7.0.tgz", - "integrity": "sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==", + "node_modules/@ethersproject/abi": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz", + "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==", "dev": true, "funding": [ { @@ -452,16 +518,23 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "bn.js": "^5.2.1" + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" } }, - "node_modules/@ethersproject/bytes": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.7.0.tgz", - "integrity": "sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==", + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", "dev": true, "funding": [ { @@ -473,14 +546,21 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/logger": "^5.7.0" + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" } }, - "node_modules/@ethersproject/constants": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.7.0.tgz", - "integrity": "sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==", + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", "dev": true, "funding": [ { @@ -492,14 +572,19 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bignumber": "^5.7.0" + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" } }, - "node_modules/@ethersproject/contracts": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz", - "integrity": "sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==", + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", "dev": true, "funding": [ { @@ -511,23 +596,19 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/abi": "^5.7.0", - "@ethersproject/abstract-provider": "^5.7.0", - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/address": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/transactions": "^5.7.0" + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" } }, - "node_modules/@ethersproject/hash": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.7.0.tgz", - "integrity": "sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==", + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", "dev": true, "funding": [ { @@ -539,52 +620,15 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/address": "^5.7.0", - "@ethersproject/base64": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/strings": "^5.7.0" + "@ethersproject/bytes": "^5.8.0" } }, - "node_modules/@ethersproject/hdnode": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.7.0.tgz", - "integrity": "sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/basex": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/pbkdf2": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/sha2": "^5.7.0", - "@ethersproject/signing-key": "^5.7.0", - "@ethersproject/strings": "^5.7.0", - "@ethersproject/transactions": "^5.7.0", - "@ethersproject/wordlists": "^5.7.0" - } - }, - "node_modules/@ethersproject/json-wallets": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz", - "integrity": "sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==", + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", "dev": true, "funding": [ { @@ -596,32 +640,17 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/address": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/hdnode": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/pbkdf2": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/random": "^5.7.0", - "@ethersproject/strings": "^5.7.0", - "@ethersproject/transactions": "^5.7.0", - "aes-js": "3.0.0", - "scrypt-js": "3.0.1" - } - }, - "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", - "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", - "dev": true + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } }, - "node_modules/@ethersproject/keccak256": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.7.0.tgz", - "integrity": "sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==", + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", "dev": true, "funding": [ { @@ -633,31 +662,15 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "js-sha3": "0.8.0" + "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/logger": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.7.0.tgz", - "integrity": "sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ] - }, - "node_modules/@ethersproject/networks": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.7.1.tgz", - "integrity": "sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==", + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", "dev": true, "funding": [ { @@ -669,14 +682,15 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/logger": "^5.7.0" + "@ethersproject/bignumber": "^5.8.0" } }, - "node_modules/@ethersproject/pbkdf2": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz", - "integrity": "sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==", + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", "dev": true, "funding": [ { @@ -688,15 +702,23 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/sha2": "^5.7.0" + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" } }, - "node_modules/@ethersproject/properties": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.7.0.tgz", - "integrity": "sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==", + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", "dev": true, "funding": [ { @@ -708,14 +730,16 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/logger": "^5.7.0" + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" } }, - "node_modules/@ethersproject/providers": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.7.2.tgz", - "integrity": "sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==", + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", "dev": true, "funding": [ { @@ -727,54 +751,12 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "dependencies": { - "@ethersproject/abstract-provider": "^5.7.0", - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/address": "^5.7.0", - "@ethersproject/base64": "^5.7.0", - "@ethersproject/basex": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/hash": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/networks": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/random": "^5.7.0", - "@ethersproject/rlp": "^5.7.0", - "@ethersproject/sha2": "^5.7.0", - "@ethersproject/strings": "^5.7.0", - "@ethersproject/transactions": "^5.7.0", - "@ethersproject/web": "^5.7.0", - "bech32": "1.1.4", - "ws": "7.4.6" - } - }, - "node_modules/@ethersproject/providers/node_modules/ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/@ethersproject/random": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.7.0.tgz", - "integrity": "sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==", + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", "dev": true, "funding": [ { @@ -786,15 +768,15 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0" + "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/rlp": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.7.0.tgz", - "integrity": "sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==", + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", "dev": true, "funding": [ { @@ -806,15 +788,15 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0" + "@ethersproject/logger": "^5.8.0" } }, - "node_modules/@ethersproject/sha2": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.7.0.tgz", - "integrity": "sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==", + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", "dev": true, "funding": [ { @@ -826,16 +808,16 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "hash.js": "1.1.7" + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" } }, "node_modules/@ethersproject/signing-key": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.7.0.tgz", - "integrity": "sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", "dev": true, "funding": [ { @@ -847,43 +829,20 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", "bn.js": "^5.2.1", - "elliptic": "6.5.4", + "elliptic": "6.6.1", "hash.js": "1.1.7" } }, - "node_modules/@ethersproject/solidity": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.7.0.tgz", - "integrity": "sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/sha2": "^5.7.0", - "@ethersproject/strings": "^5.7.0" - } - }, "node_modules/@ethersproject/strings": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.7.0.tgz", - "integrity": "sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", "dev": true, "funding": [ { @@ -895,16 +854,17 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/logger": "^5.7.0" + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" } }, "node_modules/@ethersproject/transactions": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.7.0.tgz", - "integrity": "sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", "dev": true, "funding": [ { @@ -916,22 +876,23 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/address": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/rlp": "^5.7.0", - "@ethersproject/signing-key": "^5.7.0" + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" } }, - "node_modules/@ethersproject/units": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.7.0.tgz", - "integrity": "sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==", + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", "dev": true, "funding": [ { @@ -943,627 +904,471 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/constants": "^5.7.0", - "@ethersproject/logger": "^5.7.0" + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" } }, - "node_modules/@ethersproject/wallet": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.7.0.tgz", - "integrity": "sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==", + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/abstract-provider": "^5.7.0", - "@ethersproject/abstract-signer": "^5.7.0", - "@ethersproject/address": "^5.7.0", - "@ethersproject/bignumber": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/hash": "^5.7.0", - "@ethersproject/hdnode": "^5.7.0", - "@ethersproject/json-wallets": "^5.7.0", - "@ethersproject/keccak256": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/random": "^5.7.0", - "@ethersproject/signing-key": "^5.7.0", - "@ethersproject/transactions": "^5.7.0", - "@ethersproject/wordlists": "^5.7.0" + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" } }, - "node_modules/@ethersproject/web": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.7.1.tgz", - "integrity": "sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], + "license": "ISC", "dependencies": { - "@ethersproject/base64": "^5.7.0", - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/strings": "^5.7.0" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@ethersproject/wordlists": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.7.0.tgz", - "integrity": "sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/bytes": "^5.7.0", - "@ethersproject/hash": "^5.7.0", - "@ethersproject/logger": "^5.7.0", - "@ethersproject/properties": "^5.7.0", - "@ethersproject/strings": "^5.7.0" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { - "node": ">=14" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@noble/ciphers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", "dev": true, + "license": "MIT", "peer": true, "engines": { - "node": ">=6.0.0" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true, - "peer": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@metamask/eth-sig-util": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz", - "integrity": "sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ==", - "dev": true, - "dependencies": { - "ethereumjs-abi": "^0.6.8", - "ethereumjs-util": "^6.2.1", - "ethjs-util": "^0.1.6", - "tweetnacl": "^1.0.3", - "tweetnacl-util": "^0.15.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@metamask/eth-sig-util/node_modules/@types/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@metamask/eth-sig-util/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/@metamask/eth-sig-util/node_modules/ethereumjs-util": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", - "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", - "dev": true, - "dependencies": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, - "peer": true, - "dependencies": { - "@noble/hashes": "1.3.2" + "license": "MIT", + "engines": { + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", "dev": true, + "license": "MIT", "peer": true, "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@nomicfoundation/edr": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.3.8.tgz", - "integrity": "sha512-u2UJ5QpznSHVkZRh6ePWoeVb6kmPrrqh08gCnZ9FHlJV9CITqlrTQHJkacd+INH31jx88pTAJnxePE4XAiH5qg==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.12.0-next.17.tgz", + "integrity": "sha512-Y8Kwqd5JpBmI/Kst6NJ/bZ81FeJea9J6WEwoSRTZnEvwfqW9dk9PI8zJs2UJpOACL1fXEPvN+doETbxT9EhwXA==", "dev": true, + "license": "MIT", "dependencies": { - "@nomicfoundation/edr-darwin-arm64": "0.3.8", - "@nomicfoundation/edr-darwin-x64": "0.3.8", - "@nomicfoundation/edr-linux-arm64-gnu": "0.3.8", - "@nomicfoundation/edr-linux-arm64-musl": "0.3.8", - "@nomicfoundation/edr-linux-x64-gnu": "0.3.8", - "@nomicfoundation/edr-linux-x64-musl": "0.3.8", - "@nomicfoundation/edr-win32-x64-msvc": "0.3.8" + "@nomicfoundation/edr-darwin-arm64": "0.12.0-next.17", + "@nomicfoundation/edr-darwin-x64": "0.12.0-next.17", + "@nomicfoundation/edr-linux-arm64-gnu": "0.12.0-next.17", + "@nomicfoundation/edr-linux-arm64-musl": "0.12.0-next.17", + "@nomicfoundation/edr-linux-x64-gnu": "0.12.0-next.17", + "@nomicfoundation/edr-linux-x64-musl": "0.12.0-next.17", + "@nomicfoundation/edr-win32-x64-msvc": "0.12.0-next.17" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-darwin-arm64": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.3.8.tgz", - "integrity": "sha512-eB0leCexS8sQEmfyD72cdvLj9djkBzQGP4wSQw6SNf2I4Sw4Cnzb3d45caG2FqFFjbvfqL0t+badUUIceqQuMw==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.12.0-next.17.tgz", + "integrity": "sha512-gI9/9ysLeAid0+VSTBeutxOJ0/Rrh00niGkGL9+4lR577igDY+v55XGN0oBMST49ILS0f12J6ZY90LG8sxPXmQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-darwin-x64": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.3.8.tgz", - "integrity": "sha512-JksVCS1N5ClwVF14EvO25HCQ+Laljh/KRfHERMVAC9ZwPbTuAd/9BtKvToCBi29uCHWqsXMI4lxCApYQv2nznw==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.12.0-next.17.tgz", + "integrity": "sha512-zSZtwf584RkIyb8awELDt7ctskogH0p4pmqOC4vhykc8ODOv2XLuG1IgeE4WgYhWGZOufbCtgLfpJQrWqN6mmw==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-arm64-gnu": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.3.8.tgz", - "integrity": "sha512-raCE+fOeNXhVBLUo87cgsHSGvYYRB6arih4eG6B9KGACWK5Veebtm9xtKeiD8YCsdUlUfat6F7ibpeNm91fpsA==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.12.0-next.17.tgz", + "integrity": "sha512-WjdfgV6B7gT5Q0NXtSIWyeK8gzaJX5HK6/jclYVHarWuEtS1LFgePYgMjK8rmm7IRTkM9RsE/PCuQEP1nrSsuA==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-arm64-musl": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.3.8.tgz", - "integrity": "sha512-PwiDp4wBZWMCIy29eKkv8moTKRrpiSDlrc+GQMSZLhOAm8T33JKKXPwD/2EbplbhCygJDGXZdtEKl9x9PaH66A==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.12.0-next.17.tgz", + "integrity": "sha512-26rObKhhCDb9JkZbToyr7JVZo4tSVAFvzoJSJVmvpOl0LOHrfFsgVQu2n/8cNkwMAqulPubKL2E0jdnmEoZjWA==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-x64-gnu": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.3.8.tgz", - "integrity": "sha512-6AcvA/XKoipGap5jJmQ9Y6yT7Uf39D9lu2hBcDCXnXbMcXaDGw4mn1/L4R63D+9VGZyu1PqlcJixCUZlGGIWlg==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.12.0-next.17.tgz", + "integrity": "sha512-dPkHScIf/CU6h6k3k4HNUnQyQcVSLKanviHCAcs5HkviiJPxvVtOMMvtNBxoIvKZRxGFxf2eutcqQW4ZV1wRQQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-linux-x64-musl": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.3.8.tgz", - "integrity": "sha512-cxb0sEmZjlwhYWO28sPsV64VDx31ekskhC1IsDXU1p9ntjHSJRmW4KEIqJ2O3QwJap/kLKfMS6TckvY10gjc6w==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.12.0-next.17.tgz", + "integrity": "sha512-5Ixe/bpyWZxC3AjIb8EomAOK44ajemBVx/lZRHZiWSBlwQpbSWriYAtKjKcReQQPwuYVjnFpAD2AtuCvseIjHw==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/@nomicfoundation/edr-win32-x64-msvc": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.3.8.tgz", - "integrity": "sha512-yVuVPqRRNLZk7TbBMkKw7lzCvI8XO8fNTPTYxymGadjr9rEGRuNTU1yBXjfJ59I1jJU/X2TSkRk1OFX0P5tpZQ==", + "version": "0.12.0-next.17", + "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.12.0-next.17.tgz", + "integrity": "sha512-29YlvdgofSdXG1mUzIuH4kMXu1lmVc1hvYWUGWEH59L+LaakdhfJ/Wu5izeclKkrTh729Amtk/Hk1m29kFOO8A==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 18" + "node": ">= 20" } }, - "node_modules/@nomicfoundation/ethereumjs-common": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.4.tgz", - "integrity": "sha512-9Rgb658lcWsjiicr5GzNCjI1llow/7r0k50dLL95OJ+6iZJcVbi15r3Y0xh2cIO+zgX0WIHcbzIu6FeQf9KPrg==", + "node_modules/@nomicfoundation/hardhat-errors": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-errors/-/hardhat-errors-3.0.6.tgz", + "integrity": "sha512-3x+OVdZv7Rgy3z6os9pB6kiHLxs6q0PCXHRu+WLZflr44PG9zW+7V9o+ehrUqmmivlHcIFr3Qh4M2wZVuoCYww==", "dev": true, + "license": "MIT", "dependencies": { - "@nomicfoundation/ethereumjs-util": "9.0.4" + "@nomicfoundation/hardhat-utils": "^3.0.1" } }, - "node_modules/@nomicfoundation/ethereumjs-rlp": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.4.tgz", - "integrity": "sha512-8H1S3s8F6QueOc/X92SdrA4RDenpiAEqMg5vJH99kcQaCy/a3Q6fgseo75mgWlbanGJXSlAPtnCeG9jvfTYXlw==", - "dev": true, - "bin": { - "rlp": "bin/rlp.cjs" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@nomicfoundation/ethereumjs-tx": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.4.tgz", - "integrity": "sha512-Xjv8wAKJGMrP1f0n2PeyfFCCojHd7iS3s/Ab7qzF1S64kxZ8Z22LCMynArYsVqiFx6rzYy548HNVEyI+AYN/kw==", + "node_modules/@nomicfoundation/hardhat-ethers": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-4.0.3.tgz", + "integrity": "sha512-DtYjmHtPM1BenmNm5ZMVn5fTGD4RdDPGE/ElpaLUjDGbkQnn4ytvhqnGsY+osLaWFvDxKfhdI8fyISg53bk8Qw==", "dev": true, + "license": "MIT", "dependencies": { - "@nomicfoundation/ethereumjs-common": "4.0.4", - "@nomicfoundation/ethereumjs-rlp": "5.0.4", - "@nomicfoundation/ethereumjs-util": "9.0.4", - "ethereum-cryptography": "0.1.3" - }, - "engines": { - "node": ">=18" + "@nomicfoundation/hardhat-errors": "^3.0.2", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "debug": "^4.3.2", + "ethereum-cryptography": "^2.2.1", + "ethers": "^6.14.0" }, "peerDependencies": { - "c-kzg": "^2.1.2" - }, - "peerDependenciesMeta": { - "c-kzg": { - "optional": true - } + "hardhat": "^3.0.7" } }, - "node_modules/@nomicfoundation/ethereumjs-util": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.4.tgz", - "integrity": "sha512-sLOzjnSrlx9Bb9EFNtHzK/FJFsfg2re6bsGqinFinH1gCqVfz9YYlXiMWwDM4C/L4ywuHFCYwfKTVr/QHQcU0Q==", + "node_modules/@nomicfoundation/hardhat-ethers-chai-matchers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers-chai-matchers/-/hardhat-ethers-chai-matchers-3.0.2.tgz", + "integrity": "sha512-nkg+z+fq5PXcRxS/zadyosAA+oPp3sdWrKpuOcASDf0RjqsN2LsNymML0VNNkZF8TF+hYa36fbV+QOas2Fm2BQ==", "dev": true, + "license": "MIT", "dependencies": { - "@nomicfoundation/ethereumjs-rlp": "5.0.4", - "ethereum-cryptography": "0.1.3" - }, - "engines": { - "node": ">=18" + "@nomicfoundation/hardhat-errors": "^3.0.5", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "@types/chai-as-promised": "^8.0.1", + "chai-as-promised": "^8.0.0", + "deep-eql": "^5.0.1" }, "peerDependencies": { - "c-kzg": "^2.1.2" - }, - "peerDependenciesMeta": { - "c-kzg": { - "optional": true - } + "@nomicfoundation/hardhat-ethers": "^4.0.0", + "chai": "^5.1.2", + "ethers": "^6.14.0", + "hardhat": "^3.0.0" } }, - "node_modules/@nomicfoundation/hardhat-chai-matchers": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.0.7.tgz", - "integrity": "sha512-RQfsiTwdf0SP+DtuNYvm4921X6VirCQq0Xyh+mnuGlTwEFSPZ/o27oQC+l+3Y/l48DDU7+ZcYBR+Fp+Rp94LfQ==", + "node_modules/@nomicfoundation/hardhat-ignition": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition/-/hardhat-ignition-3.0.6.tgz", + "integrity": "sha512-o5nkadpYS0LsYQzYO56pTvYngtXmB72FRTZcAMEHG+K9TMjI7EHPn4ecXmatJ5fbUSf/CplkqWxbKkOaVnfqXg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/chai-as-promised": "^7.1.3", - "chai-as-promised": "^7.1.1", - "deep-eql": "^4.0.1", - "ordinal": "^1.0.3" + "@nomicfoundation/hardhat-errors": "^3.0.2", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "@nomicfoundation/ignition-core": "^3.0.6", + "@nomicfoundation/ignition-ui": "^3.0.6", + "chalk": "^5.3.0", + "debug": "^4.3.2", + "json5": "^2.2.3", + "prompts": "^2.4.2" }, "peerDependencies": { - "@nomicfoundation/hardhat-ethers": "^3.0.0", - "chai": "^4.2.0", - "ethers": "^6.1.0", - "hardhat": "^2.9.4" + "@nomicfoundation/hardhat-verify": "^3.0.0", + "hardhat": "^3.0.0" } }, - "node_modules/@nomicfoundation/hardhat-ethers": { + "node_modules/@nomicfoundation/hardhat-ignition-ethers": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.6.tgz", - "integrity": "sha512-/xzkFQAaHQhmIAYOQmvHBPwL+NkwLzT9gRZBsgWUYeV+E6pzXsBQsHfRYbAZ3XEYare+T7S+5Tg/1KDJgepSkA==", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition-ethers/-/hardhat-ignition-ethers-3.0.6.tgz", + "integrity": "sha512-khMIcrX3710uuYr1ejfadZU9bbWz+dgT3i8vXyG8v348j1QTg1445UUkIj86/AoolE/XwePW1bgNF0OmlxZj3g==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "debug": "^4.1.1", - "lodash.isequal": "^4.5.0" + "@nomicfoundation/hardhat-errors": "^3.0.2" }, "peerDependencies": { - "ethers": "^6.1.0", - "hardhat": "^2.0.0" + "@nomicfoundation/hardhat-ethers": "^4.0.0", + "@nomicfoundation/hardhat-ignition": "^3.0.6", + "@nomicfoundation/hardhat-verify": "^3.0.0", + "@nomicfoundation/ignition-core": "^3.0.6", + "ethers": "^6.14.0", + "hardhat": "^3.0.0" } }, - "node_modules/@nomicfoundation/hardhat-ignition": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition/-/hardhat-ignition-0.15.5.tgz", - "integrity": "sha512-Y5nhFXFqt4owA6Ooag8ZBFDF2RAZElMXViknVIsi3m45pbQimS50ti6FU8HxfRkDnBARa40CIn7UGV0hrelzDw==", + "node_modules/@nomicfoundation/hardhat-keystore": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-keystore/-/hardhat-keystore-3.0.3.tgz", + "integrity": "sha512-rkwfdy/GsX/2SV49RGBvMsCuR+SYGJQGD3wcrS5m2Cyap5eQFEgKZbqpua6YQRA2raxRmVVH6antIIftgBFXAQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@nomicfoundation/ignition-core": "^0.15.5", - "@nomicfoundation/ignition-ui": "^0.15.5", - "chalk": "^4.0.0", + "@noble/ciphers": "1.2.1", + "@noble/hashes": "1.7.1", + "@nomicfoundation/hardhat-errors": "^3.0.0", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "@nomicfoundation/hardhat-zod-utils": "^3.0.0", + "chalk": "^5.3.0", "debug": "^4.3.2", - "fs-extra": "^10.0.0", - "prompts": "^2.4.2" + "zod": "^3.23.8" }, "peerDependencies": { - "@nomicfoundation/hardhat-verify": "^2.0.1", - "hardhat": "^2.18.0" + "hardhat": "^3.0.0" } }, - "node_modules/@nomicfoundation/hardhat-ignition-viem": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-ignition-viem/-/hardhat-ignition-viem-0.15.5.tgz", - "integrity": "sha512-+OV6LNAJHg94pvu5znbkS1qVi6YKyD0jWSy8L6dT9Aw4uvuOKVB8bczGUAy74T7/8+CVFtD7nJg1m4nRV+0UPQ==", + "node_modules/@nomicfoundation/hardhat-mocha": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-mocha/-/hardhat-mocha-3.0.8.tgz", + "integrity": "sha512-DsxbzFdUgvgPKmuFfMpp7JPmUWD4OkQ7n/E3wphTjid+dSMrp0d8HLe3CkchvQJFhWLipS9KvB9u05wxJsRUYA==", "dev": true, + "license": "MIT", "peer": true, + "dependencies": { + "@nomicfoundation/hardhat-errors": "^3.0.3", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "@nomicfoundation/hardhat-zod-utils": "^3.0.0", + "chalk": "^5.3.0", + "tsx": "^4.19.3", + "zod": "^3.23.8" + }, "peerDependencies": { - "@nomicfoundation/hardhat-ignition": "^0.15.5", - "@nomicfoundation/hardhat-viem": "^2.0.0", - "@nomicfoundation/ignition-core": "^0.15.5", - "hardhat": "^2.18.0", - "viem": "^2.7.6" + "hardhat": "^3.0.12", + "mocha": "^11.0.0" } }, "node_modules/@nomicfoundation/hardhat-network-helpers": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.11.tgz", - "integrity": "sha512-uGPL7QSKvxrHRU69dx8jzoBvuztlLCtyFsbgfXIwIjnO3dqZRz2GNMHJoO3C3dIiUNM6jdNF4AUnoQKDscdYrA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-3.0.3.tgz", + "integrity": "sha512-FqXD8CPFNdluEhELqNV/Q0grOQtlwRWr28LW+/NTas3rrDAXpNOIPCCq3RIXJIqsdbNPQsG2FpnfKj9myqIsKQ==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "ethereumjs-util": "^7.1.4" + "@nomicfoundation/hardhat-errors": "^3.0.5", + "@nomicfoundation/hardhat-utils": "^3.0.5" }, "peerDependencies": { - "hardhat": "^2.9.5" + "hardhat": "^3.0.0" } }, - "node_modules/@nomicfoundation/hardhat-toolbox-viem": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-toolbox-viem/-/hardhat-toolbox-viem-3.0.0.tgz", - "integrity": "sha512-cr+aRozCtTwaRz5qc9OVY1kegWrnVwyhHZonICmlcm21cvJ31uvJnuPG688tMbjUvwRDw8tpZYZK0kI5M+4CKg==", + "node_modules/@nomicfoundation/hardhat-toolbox-mocha-ethers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-toolbox-mocha-ethers/-/hardhat-toolbox-mocha-ethers-3.0.2.tgz", + "integrity": "sha512-45EZqxWtQxlvwDZilxOI+tSNFn/J+1ITtyqpUQgNhXhYA9+LUdbUx+PmiCWPrtEXzBVANYka6mhNvBr2ZwNBUg==", "dev": true, - "dependencies": { - "chai-as-promised": "^7.1.1" - }, + "license": "MIT", "peerDependencies": { - "@nomicfoundation/hardhat-ignition-viem": "^0.15.0", - "@nomicfoundation/hardhat-network-helpers": "^1.0.0", - "@nomicfoundation/hardhat-verify": "^2.0.0", - "@nomicfoundation/hardhat-viem": "^2.0.0", - "@types/chai": "^4.2.0", - "@types/chai-as-promised": "^7.1.6", - "@types/mocha": ">=9.1.0", - "@types/node": ">=18.0.0", - "chai": "^4.2.0", - "hardhat": "^2.11.0", - "hardhat-gas-reporter": "^1.0.8", - "solidity-coverage": "^0.8.1", - "ts-node": ">=8.0.0", - "typescript": "^5.0.4", - "viem": "^2.7.6" - } - }, - "node_modules/@nomicfoundation/hardhat-verify": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.8.tgz", - "integrity": "sha512-x/OYya7A2Kcz+3W/J78dyDHxr0ezU23DKTrRKfy5wDPCnePqnr79vm8EXqX3gYps6IjPBYyGPZ9K6E5BnrWx5Q==", + "@nomicfoundation/hardhat-ethers": "^4.0.0", + "@nomicfoundation/hardhat-ethers-chai-matchers": "^3.0.0", + "@nomicfoundation/hardhat-ignition": "^3.0.0", + "@nomicfoundation/hardhat-ignition-ethers": "^3.0.0", + "@nomicfoundation/hardhat-keystore": "^3.0.0", + "@nomicfoundation/hardhat-mocha": "^3.0.0", + "@nomicfoundation/hardhat-network-helpers": "^3.0.0", + "@nomicfoundation/hardhat-typechain": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^3.0.0", + "@nomicfoundation/ignition-core": "^3.0.0", + "chai": "^5.1.2", + "ethers": "^6.14.0", + "hardhat": "^3.0.0", + "mocha": "^11.0.0" + } + }, + "node_modules/@nomicfoundation/hardhat-typechain": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-typechain/-/hardhat-typechain-3.0.1.tgz", + "integrity": "sha512-TkeMQhf+/4gZLMIWLxzzyVruNuLz5xW5BZdu4Clic3HFqBJRG+U2fQGWxAknMMLGONhxiZaUipE0Z+JkOugrmg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@ethersproject/abi": "^5.1.2", - "@ethersproject/address": "^5.0.2", - "cbor": "^8.1.0", - "chalk": "^2.4.2", - "debug": "^4.1.1", - "lodash.clonedeep": "^4.5.0", - "semver": "^6.3.0", - "table": "^6.8.0", - "undici": "^5.14.0" + "@nomicfoundation/hardhat-errors": "^3.0.0", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "@nomicfoundation/hardhat-zod-utils": "^3.0.0", + "@typechain/ethers-v6": "^0.5.0", + "debug": "^4.3.2", + "typechain": "^8.3.1", + "zod": "^3.23.8" }, "peerDependencies": { - "hardhat": "^2.0.4" + "@nomicfoundation/hardhat-ethers": "^4.0.0", + "ethers": "^6.14.0", + "hardhat": "^3.0.0" } }, - "node_modules/@nomicfoundation/hardhat-verify/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "peer": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@nomicfoundation/hardhat-verify/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@nomicfoundation/hardhat-verify/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@nomicfoundation/hardhat-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-utils/-/hardhat-utils-3.0.5.tgz", + "integrity": "sha512-5zkQSuSxkwK7fQxKswJ1GGc/3AuWBSmxA7GhczTPLx28dAXQnubRU8nA48SkCkKesJq5x4TROP+XheSE2VkLUA==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@nomicfoundation/hardhat-verify/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "peer": true - }, - "node_modules/@nomicfoundation/hardhat-verify/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@nomicfoundation/hardhat-verify/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" + "@streamparser/json-node": "^0.0.22", + "debug": "^4.3.2", + "env-paths": "^2.2.0", + "ethereum-cryptography": "^2.2.1", + "fast-equals": "^5.0.1", + "json-stream-stringify": "^3.1.6", + "rfdc": "^1.3.1", + "undici": "^6.16.1" } }, - "node_modules/@nomicfoundation/hardhat-verify/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@nomicfoundation/hardhat-verify": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-verify/-/hardhat-verify-3.0.8.tgz", + "integrity": "sha512-AkwFvx/r0AFDk0H53mReYpkw2pvi5Jq34zAyk2+cTM7o/OnOvq0xcAaidw4BQvBf9+FMeFAKjJe+zNYgrsLatg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@ethersproject/abi": "^5.8.0", + "@nomicfoundation/hardhat-errors": "^3.0.3", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "@nomicfoundation/hardhat-zod-utils": "^3.0.0", + "cbor2": "^1.9.0", + "chalk": "^5.3.0", + "debug": "^4.3.2", + "semver": "^7.6.3", + "zod": "^3.23.8" }, - "engines": { - "node": ">=4" + "peerDependencies": { + "hardhat": "^3.0.0" } }, - "node_modules/@nomicfoundation/hardhat-viem": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-viem/-/hardhat-viem-2.0.3.tgz", - "integrity": "sha512-y2eYaHtpshiGrhU2L5My4zYrj/vxxRdCIqbTsg9YP7AjKWhJGvKPkVRYaPTosW68nYlNtkns/+Eb25aXACHd9Q==", + "node_modules/@nomicfoundation/hardhat-zod-utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-zod-utils/-/hardhat-zod-utils-3.0.1.tgz", + "integrity": "sha512-I6/pyYiS9p2lLkzQuedr1ScMocH+ew8l233xTi+LP92gjEiviJDxselpkzgU01MUM0t6BPpfP8yMO958LDEJVg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "abitype": "^0.9.8", - "lodash.memoize": "^4.1.2" + "@nomicfoundation/hardhat-errors": "^3.0.0", + "@nomicfoundation/hardhat-utils": "^3.0.2" }, "peerDependencies": { - "hardhat": "^2.22.62.17.0", - "typescript": "~5.0.0", - "viem": "^2.7.6" + "zod": "^3.23.8" } }, "node_modules/@nomicfoundation/ignition-core": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-core/-/ignition-core-0.15.5.tgz", - "integrity": "sha512-FgvuoIXhakRSP524JzNQ4BviyzBBKpsFaOWubPZ4XACLT4/7vGqlJ/7DIn0D2NL2anQ2qs98/BNBY9WccXUX1Q==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-core/-/ignition-core-3.0.6.tgz", + "integrity": "sha512-o5CTrlQ1PEQW85ppS7fxXCsSVl3j/T/3roTSA795lRJf7SQdJzr5y12rSTvoqR2YbeF5zDxVdqgzEqoMd8n6Cw==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "@ethersproject/address": "5.6.1", + "@nomicfoundation/hardhat-errors": "^3.0.2", + "@nomicfoundation/hardhat-utils": "^3.0.5", "@nomicfoundation/solidity-analyzer": "^0.1.1", - "cbor": "^9.0.0", + "cbor2": "^1.9.0", "debug": "^4.3.2", - "ethers": "^6.7.0", - "fs-extra": "^10.0.0", + "ethers": "^6.14.0", "immer": "10.0.2", - "lodash": "4.17.21", + "lodash-es": "4.17.21", "ndjson": "2.0.0" } }, @@ -1582,7 +1387,7 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], - "peer": true, + "license": "MIT", "dependencies": { "@ethersproject/bignumber": "^5.6.2", "@ethersproject/bytes": "^5.6.1", @@ -1591,31 +1396,18 @@ "@ethersproject/rlp": "^5.6.1" } }, - "node_modules/@nomicfoundation/ignition-core/node_modules/cbor": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", - "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", - "dev": true, - "peer": true, - "dependencies": { - "nofilter": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@nomicfoundation/ignition-ui": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-ui/-/ignition-ui-0.15.5.tgz", - "integrity": "sha512-ZcE4rIn10qKahR4OqS8rl8NM2Fbg2QYiBXgMgj74ZI0++LlCcZgB5HyaBbX+lsnKHjTXtjYD3b+2mtg7jFbAMQ==", - "dev": true, - "peer": true + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ignition-ui/-/ignition-ui-3.0.6.tgz", + "integrity": "sha512-PePoQO4LwLfQyMGZOtbF5eOgYSu/kXCyif/0Jpto1dfFLAtvoUbvaLrecrclM/keCTriRADOauH/zH06ihzvCg==", + "dev": true }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz", "integrity": "sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" }, @@ -1634,6 +1426,7 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.2.tgz", "integrity": "sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -1644,6 +1437,7 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.2.tgz", "integrity": "sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -1654,6 +1448,7 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.2.tgz", "integrity": "sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -1664,6 +1459,7 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.2.tgz", "integrity": "sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -1674,6 +1470,7 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.2.tgz", "integrity": "sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -1684,6 +1481,7 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.2.tgz", "integrity": "sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -1694,6 +1492,7 @@ "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.2.tgz", "integrity": "sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 12" @@ -1703,122 +1502,78 @@ "version": "4.9.6", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.6.tgz", "integrity": "sha512-xSmezSupL+y9VkHZJGDoCBpmnB2ogM13ccaYDWqJTfS3dbuHkgjuwDFUmaFauBCboQMGB/S5UqUl2y54X99BmA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@openzeppelin/contracts-upgradeable": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.6.tgz", "integrity": "sha512-m4iHazOsOCv1DgM7eD7GupTJ+NFVujRZt1wzddDPSVGpWdKq1SKkla5htKG7+IS4d2XOCtzkUNwRZ7Vq5aEUMA==", - "dev": true - }, - "node_modules/@openzeppelin/defender-sdk-base-client": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-base-client/-/defender-sdk-base-client-1.13.4.tgz", - "integrity": "sha512-fZjDxdL5WBt6kjKN8j6WlfIsggZKv37W1KoRkT0XwYv7Jslmr22i2qUs8ZreAzATD3ESYQs7YlO7ge0ElqdOKg==", "dev": true, - "dependencies": { - "amazon-cognito-identity-js": "^6.3.6", - "async-retry": "^1.3.3" - } + "license": "MIT" }, - "node_modules/@openzeppelin/defender-sdk-deploy-client": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-deploy-client/-/defender-sdk-deploy-client-1.13.4.tgz", - "integrity": "sha512-1SbdImpjCYmjpDgK7Bff4vak29r/aECabVuQi5TB+7TdbOuRdVxDHu7vFhEpt3yrcPKW1joaNiUNDEc/noUsNQ==", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, - "dependencies": { - "@openzeppelin/defender-sdk-base-client": "^1.13.4", - "axios": "^1.6.8", - "lodash": "^4.17.21" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" } }, - "node_modules/@openzeppelin/defender-sdk-network-client": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-network-client/-/defender-sdk-network-client-1.13.4.tgz", - "integrity": "sha512-m76WQzqFET4jtFgA74V6Ui4czRoTvBy7leS+BbsIxoKX+NGODhs78y5zq7jSxsLu3c2iY69rujRkzj0Z+sCiiQ==", + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", "dev": true, - "dependencies": { - "@openzeppelin/defender-sdk-base-client": "^1.13.4", - "axios": "^1.6.8", - "lodash": "^4.17.21" + "license": "MIT", + "engines": { + "node": ">=12.22.0" } }, - "node_modules/@openzeppelin/hardhat-upgrades": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-3.2.0.tgz", - "integrity": "sha512-xybXIHQIZK2a1HH7ukMToRbIcU9LHfL49gtB0KYptY6f/r9lqrFOupN8aOBueRZW4Ymhc6HGL9bvj7u7t5lDdQ==", + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", "dev": true, + "license": "MIT", "dependencies": { - "@openzeppelin/defender-sdk-base-client": "^1.10.0", - "@openzeppelin/defender-sdk-deploy-client": "^1.10.0", - "@openzeppelin/defender-sdk-network-client": "^1.10.0", - "@openzeppelin/upgrades-core": "^1.32.0", - "chalk": "^4.1.0", - "debug": "^4.1.1", - "ethereumjs-util": "^7.1.5", - "proper-lockfile": "^4.1.1", - "undici": "^6.11.1" - }, - "bin": { - "migrate-oz-cli-project": "dist/scripts/migrate-oz-cli-project.js" - }, - "peerDependencies": { - "@nomicfoundation/hardhat-ethers": "^3.0.0", - "@nomicfoundation/hardhat-verify": "^2.0.0", - "ethers": "^6.6.0", - "hardhat": "^2.0.2" + "graceful-fs": "4.2.10" }, - "peerDependenciesMeta": { - "@nomicfoundation/hardhat-verify": { - "optional": true - } - } - }, - "node_modules/@openzeppelin/hardhat-upgrades/node_modules/undici": { - "version": "6.19.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.2.tgz", - "integrity": "sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==", - "dev": true, "engines": { - "node": ">=18.17" + "node": ">=12.22.0" } }, - "node_modules/@openzeppelin/upgrades-core": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.34.1.tgz", - "integrity": "sha512-LV3hHm60htmP3HJjn2VoGqXNPn1RLFSSInRyXNbm15Z2oWKGxOfAWSC4+okRckum0yVB5g3k4/SEyqjsJRB07A==", + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true, - "dependencies": { - "cbor": "^9.0.0", - "chalk": "^4.1.0", - "compare-versions": "^6.0.0", - "debug": "^4.1.1", - "ethereumjs-util": "^7.0.3", - "minimist": "^1.2.7", - "proper-lockfile": "^4.1.1", - "solidity-ast": "^0.4.51" - }, - "bin": { - "openzeppelin-upgrades-core": "dist/cli/cli.js" - } + "license": "ISC" }, - "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", - "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", "dev": true, + "license": "MIT", "dependencies": { - "nofilter": "^3.1.0" + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" }, "engines": { - "node": ">=16" + "node": ">=12" } }, "node_modules/@scure/base": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz", - "integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "dev": true, + "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" } @@ -1828,6 +1583,7 @@ "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", "dev": true, + "license": "MIT", "dependencies": { "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", @@ -1837,23 +1593,12 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", - "dev": true, - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" }, @@ -1866,6 +1611,7 @@ "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", "dev": true, + "license": "MIT", "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" @@ -1879,6 +1625,7 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" }, @@ -1887,429 +1634,137 @@ } }, "node_modules/@sentry/core": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.30.0.tgz", - "integrity": "sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==", - "dev": true, - "dependencies": { - "@sentry/hub": "5.30.0", - "@sentry/minimal": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/hub": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.30.0.tgz", - "integrity": "sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==", + "version": "9.47.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.47.1.tgz", + "integrity": "sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==", "dev": true, - "dependencies": { - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - }, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/@sentry/minimal": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.30.0.tgz", - "integrity": "sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==", + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "dev": true, - "dependencies": { - "@sentry/hub": "5.30.0", - "@sentry/types": "5.30.0", - "tslib": "^1.9.3" - }, + "license": "MIT", "engines": { - "node": ">=6" - } - }, - "node_modules/@sentry/node": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.30.0.tgz", - "integrity": "sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==", - "dev": true, - "dependencies": { - "@sentry/core": "5.30.0", - "@sentry/hub": "5.30.0", - "@sentry/tracing": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "cookie": "^0.4.1", - "https-proxy-agent": "^5.0.0", - "lru_map": "^0.3.3", - "tslib": "^1.9.3" + "node": ">=14.16" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@sentry/tracing": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", - "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", + "node_modules/@solidity-parser/parser": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", + "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", "dev": true, - "dependencies": { - "@sentry/hub": "5.30.0", - "@sentry/minimal": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/@sentry/types": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", - "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "node_modules/@streamparser/json": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.22.tgz", + "integrity": "sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==", "dev": true, - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/@sentry/utils": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", - "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "node_modules/@streamparser/json-node": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@streamparser/json-node/-/json-node-0.0.22.tgz", + "integrity": "sha512-sJT2ptNRwqB1lIsQrQlCoWk5rF4tif9wDh+7yluAGijJamAhrHGYpFB/Zg3hJeceoZypi74ftXk8DHzwYpbZSg==", "dev": true, + "license": "MIT", "dependencies": { - "@sentry/types": "5.30.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true, - "engines": { - "node": ">=6" + "@streamparser/json": "^0.0.22" } }, - "node_modules/@smithy/types": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", - "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "defer-to-connect": "^2.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.16" } }, - "node_modules/@smithy/types/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - }, - "node_modules/@solidity-parser/parser": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", - "integrity": "sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==", + "node_modules/@typechain/ethers-v6": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v6/-/ethers-v6-0.5.1.tgz", + "integrity": "sha512-F+GklO8jBWlsaVV+9oHaPh5NJdd6rAKN4tklGfInX1Q7h0xPgVLP39Jl3eCulPB5qexI71ZFHwbljx4ZXNfouA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "antlr4ts": "^0.5.0-alpha.4" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "dependencies": { - "defer-to-connect": "^1.0.1" + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "peer": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "peer": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "peer": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "peer": true - }, - "node_modules/@types/bn.js": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.5.tgz", - "integrity": "sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "peerDependencies": { + "ethers": "6.x", + "typechain": "^8.3.2", + "typescript": ">=4.7.0" } }, "node_modules/@types/chai": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz", - "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==", - "dev": true + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/chai-as-promised": { - "version": "7.1.8", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", - "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-8.0.2.tgz", + "integrity": "sha512-meQ1wDr1K5KRCSvG2lX7n7/5wf70BeptTKst0axGvnN6zqaVpRqegoIbugiAPSqOW9K9aL8gDVrm7a2LXOtn2Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/chai": "*" } }, - "node_modules/@types/concat-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", - "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/figlet": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/@types/figlet/-/figlet-1.5.8.tgz", - "integrity": "sha512-G22AUvy4Tl95XLE7jmUM8s8mKcoz+Hr+Xm9W90gJsppJq9f9tHvOGkrpn4gRX0q/cLtBdNkWtWCKDg2UDZoZvQ==", - "dev": true - }, - "node_modules/@types/form-data": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", - "integrity": "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true + "license": "MIT" }, "node_modules/@types/mocha": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", - "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true, - "peer": true + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", + "version": "22.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node": "*" + "undici-types": "~6.21.0" } }, - "node_modules/@types/qs": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", - "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true, + "license": "MIT", "peer": true }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/secp256k1": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.6.tgz", - "integrity": "sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/underscore": { - "version": "1.11.15", - "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", - "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "node_modules/abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", - "dev": true - }, - "node_modules/abitype": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.9.10.tgz", - "integrity": "sha512-FIS7U4n7qwAT58KibwYig5iFG4K61rbhAqaQh/UWj8v1Y8mjX3F8TC9gd8cz9yT1TYel9f8nS5NO5kZp2RW0jQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "peer": true, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, - "peer": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/adm-zip": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.3.0" } @@ -2319,80 +1774,33 @@ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "dev": true, - "peer": true - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/ajv": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", - "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.4.1" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/amazon-cognito-identity-js": { - "version": "6.3.12", - "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.12.tgz", - "integrity": "sha512-s7NKDZgx336cp+oDeUtB2ZzT8jWJp/v2LWuYl+LQtMEODe22RF1IJ4nRiDATp+rp1pTffCZcm44Quw4jx2bqNg==", - "dev": true, - "dependencies": { - "@aws-crypto/sha256-js": "1.2.2", - "buffer": "4.9.2", - "fast-base64-decode": "^1.0.0", - "isomorphic-unfetch": "^3.0.0", - "js-cookie": "^2.2.1" - } - }, - "node_modules/amdefine": { + "node_modules/ajv-errors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.4.2" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", "dev": true, - "dependencies": { - "string-width": "^4.1.0" + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" } }, "node_modules/ansi-colors": { @@ -2400,854 +1808,651 @@ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/antlr4ts": { - "version": "0.5.0-alpha.4", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", - "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", - "dev": true, - "peer": true - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/antlr4": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz", + "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==", "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">=16" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "peer": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "node_modules/ast-parents": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz", + "integrity": "sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==", "dev": true, - "peer": true, + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-ajv-errors": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-2.0.3.tgz", + "integrity": "sha512-t1vxUP+vYKsaYi/BbKo2K98nEAZmfi4sjwvmRT8aOPDzPJeAtLurfoIDazVkLILxO4K+Sw4YrLYnBQ46l6pePg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "@babel/code-frame": "^7.27.1", + "@humanwhocodes/momoa": "^2.0.4", + "chalk": "^4.1.2", + "jsonpointer": "^5.0.1", + "leven": "^3.1.0 < 4" }, "engines": { - "node": ">= 0.4" + "node": ">= 18.20.6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "ajv": "4.11.8 - 8" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "node_modules/better-ajv-errors/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "peer": true - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "node_modules/better-ajv-errors/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { - "safer-buffer": "~2.1.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "node_modules/better-ajv-errors/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "dev": true, + "license": "MIT" }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" + "balanced-match": "^1.0.0" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", "dev": true, - "engines": { - "node": ">=0.8" - } + "license": "MIT" }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true, - "peer": true, - "engines": { - "node": "*" - } + "license": "ISC" }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", "dev": true, - "peer": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.16" } }, - "node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true - }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "dev": true, + "license": "MIT", "dependencies": { - "retry": "0.13.1" + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "bin": { - "atob": "bin/atob.js" - }, + "license": "MIT", "engines": { - "node": ">= 4.5.0" + "node": ">=6" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "node_modules/cbor2": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/cbor2/-/cbor2-1.12.0.tgz", + "integrity": "sha512-3Cco8XQhi27DogSp9Ri6LYNZLi/TBY/JVnDe+mj06NkBjW/ZYOtekaEU4wZ4xcRMNrFkDv8KNtOAqHyDfz3lYg==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">=18.7" } }, - "node_modules/aws4": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", - "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", - "dev": true - }, - "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base-x": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", - "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "node_modules/chai-as-promised": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.2.tgz", + "integrity": "sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==", "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "check-error": "^2.1.1" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 7" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "dependencies": { - "tweetnacl": "^0.14.3" + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/bcrypt-pbkdf/node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true - }, - "node_modules/bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", - "dev": true - }, - "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">= 16" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 14.16.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/blakejs": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", - "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", - "dev": true - }, - "node_modules/bls-eth-wasm": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bls-eth-wasm/-/bls-eth-wasm-1.2.1.tgz", - "integrity": "sha512-hl4oBzZQmPGNb9Wt5GI+oEuHM6twGc5HzXCzNZMVLMMg+dltsOuvuioRyLolpDFbncC0BJbGPzP1ZTysUGkksw==", - "dev": true - }, - "node_modules/bls-signatures": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/bls-signatures/-/bls-signatures-0.2.5.tgz", - "integrity": "sha512-5TzQNCtR4zWE4lM08EOMIT8l3b4h8g5LNKu50fUYP1PnupaLGSLklAcTto4lnH7VXpyhsar+74L9wNJII4E/4Q==", - "dev": true - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" + "node": ">=12" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { - "side-channel": "^1.0.4" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=0.6" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", - "dev": true, - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "license": "MIT" }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } + "license": "MIT" }, - "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" }, "engines": { - "node": ">= 0.12" + "node": ">=4.0.0" } }, - "node_modules/browserify-sign/node_modules/elliptic": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz", - "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==", + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/browserify-sign/node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/browserify-sign/node_modules/hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==", + "node_modules/command-line-usage/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "color-convert": "^1.9.0" }, "engines": { "node": ">=4" } }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" } }, - "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/browserify-sign/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/command-line-usage/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "safe-buffer": "~5.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "node_modules/command-line-usage/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "base-x": "^3.0.2" + "color-name": "1.1.3" } }, - "node_modules/bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "node_modules/command-line-usage/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true, - "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } + "license": "MIT", + "peer": true }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "node_modules/command-line-usage/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "bin": { - "btoa": "bin/btoa.js" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">= 0.4.0" + "node": ">=0.8.0" } }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "node_modules/command-line-usage/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/buffer-to-arraybuffer": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz", - "integrity": "sha512-3dthu5CYiVB1DEJp61FtApNnNndTckcqe4pFcLdvHtrpG+kcyekCJKg4MRiDcFW7A6AODnXB9U4dwQiCW5kzJQ==", - "dev": true - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true - }, - "node_modules/bufferutil": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", - "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "node_modules/command-line-usage/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "hasInstallScript": true, + "license": "MIT", + "peer": true, "dependencies": { - "node-gyp-build": "^4.3.0" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=6.14.2" + "node": ">=4" } }, - "node_modules/builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, + "license": "MIT", + "peer": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=14" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "engines": { - "node": ">=10.6.0" - } + "license": "MIT", + "peer": true }, - "node_modules/cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "dev": true, + "license": "MIT", "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "engines": { - "node": ">=8" + "ini": "^1.3.4", + "proto-list": "~1.2.1" } }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, + "license": "MIT", "dependencies": { - "pump": "^3.0.0" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3255,1161 +2460,981 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true - }, - "node_modules/cbor": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", - "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "nofilter": "^3.1.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=12.19" - } - }, - "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", - "dev": true, - "peer": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chai-as-promised": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.2.tgz", - "integrity": "sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==", - "dev": true, - "dependencies": { - "check-error": "^1.0.2" + "node": ">=10" }, - "peerDependencies": { - "chai": ">= 2.1.2 < 6" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "peer": true, + "license": "MIT", "engines": { - "node": "*" + "node": ">=6" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">=4.0.0" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, + "license": "MIT", "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=10" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "node_modules/cids": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/cids/-/cids-0.7.5.tgz", - "integrity": "sha512-zT7mPeghoWAu+ppn8+BS1tQ5qGmbMfB4AregnQjA/qHY3GC1m1ptI9GkWNlgeu38r7CuRdXB47uY2XgAYt6QVA==", - "deprecated": "This module has been superseded by the multiformats module", + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "class-is": "^1.1.0", - "multibase": "~0.6.0", - "multicodec": "^1.0.0", - "multihashes": "~0.4.15" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=4.0.0", - "npm": ">=3.0.0" + "node": ">=0.3.1" } }, - "node_modules/cids/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/cids/node_modules/multicodec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-1.0.4.tgz", - "integrity": "sha512-NDd7FeS3QamVtbgfvu5h7fd1IlbaC4EQ0/pgU4zqE2vdHCmBGsUa0TiM8/TdSeG6BMPC92OOCf8F1ocE/Wkrrg==", - "deprecated": "This module has been superseded by the multiformats module", + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, - "dependencies": { - "buffer": "^5.6.0", - "varint": "^5.0.0" - } + "license": "MIT" }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/class-is": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/class-is/-/class-is-1.1.0.tgz", - "integrity": "sha512-rhjH9AG1fvabIDoGRVH587413LPjTZgmDF9fOFCbFJQV4yuocX1mHxxvXI4g3cGwbVY9wAYIoKlg1N79frJKQw==", - "dev": true + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" }, - "node_modules/class-validator": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.13.2.tgz", - "integrity": "sha512-yBUcQy07FPlGzUjoLuUfIOXzgynnQPPruyK1Ge2B74k9ROwnle1E+NxLWnUv5OLU8hA/qL5leAE9XnXq3byaBw==", + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "dependencies": { - "libphonenumber-js": "^1.9.43", - "validator": "^13.7.0" - } + "license": "MIT" }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8.6" } }, - "node_modules/cli-boxes": { + "node_modules/env-paths": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^4.2.0" + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": "10.* || >= 12.*" + "node": ">=18" }, "optionalDependencies": { - "@colors/colors": "1.5.0" + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colors": { + "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.1.90" + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" }, "engines": { - "node": ">= 0.8" + "node": ">=14.0.0" } }, - "node_modules/command-exists": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", - "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "dev": true + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/compare-versions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz", - "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, - "engines": [ - "node >= 0.8" - ], - "peer": true, + "license": "MIT", "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "undici-types": "~6.19.2" } }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true, - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "license": "MIT" }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, - "peer": true + "license": "MIT" }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } + "license": "Apache-2.0" }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/fast-equals": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", + "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=6.0.0" } }, - "node_modules/content-hash": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/content-hash/-/content-hash-2.5.2.tgz", - "integrity": "sha512-FvIQKy0S1JaWV10sMsA7TRx8bpU+pqPkhbsfvOJAdjRXvYxEckAwQWGwtRjiaJfh+E0DvcWUGqcdjwMGFjsSdw==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "dependencies": { - "cids": "^0.7.1", - "multicodec": "^0.5.5", - "multihashes": "^0.4.15" - } + "license": "MIT" }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, - "engines": { - "node": ">= 0.6" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "array-back": "^3.0.1" + }, "engines": { - "node": ">= 0.6" + "node": ">=4.0.0" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true - }, - "node_modules/cookiejar": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", - "dev": true - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" + "flat": "cli.js" } }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, + "license": "ISC", "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" + "license": "MIT", + "engines": { + "node": ">= 14.17" } }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, - "peer": true + "license": "ISC" }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "peer": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "*" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", - "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", - "dev": true - }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, + "license": "ISC", "engines": { - "node": "*" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/d": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", - "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" - }, + "license": "MIT", "engines": { - "node": ">=0.12" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" + "node": ">=10" }, - "engines": { - "node": ">=0.10" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">= 0.4" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=14.16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/death": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/death/-/death-1.1.0.tgz", - "integrity": "sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==", - "dev": true + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" }, - "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "node_modules/hardhat": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-3.1.0.tgz", + "integrity": "sha512-nv9m2QEatqyieC24blPSdaN6FVMXtxCXe6iFPGSx9Pxd6qpucj9rjlADL4MgU1Doq5pLvHkwUxsrXuZY6dK7SQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "@nomicfoundation/edr": "0.12.0-next.17", + "@nomicfoundation/hardhat-errors": "^3.0.6", + "@nomicfoundation/hardhat-utils": "^3.0.5", + "@nomicfoundation/hardhat-zod-utils": "^3.0.1", + "@nomicfoundation/solidity-analyzer": "^0.1.1", + "@sentry/core": "^9.4.0", + "adm-zip": "^0.4.16", + "chalk": "^5.3.0", + "chokidar": "^4.0.3", + "debug": "^4.3.2", + "enquirer": "^2.3.0", + "ethereum-cryptography": "^2.2.1", + "micro-eth-signer": "^0.14.0", + "p-map": "^7.0.2", + "resolve.exports": "^2.0.3", + "semver": "^7.6.3", + "tsx": "^4.19.3", + "ws": "^8.18.0", + "zod": "^3.23.8" }, + "bin": { + "hardhat": "dist/src/cli.js" + } + }, + "node_modules/hardhat/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=6.0" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { - "supports-color": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { "optional": true } } }, - "node_modules/decamelize": { + "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, - "engines": { - "node": ">=0.10" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, - "node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "engines": { - "node": ">=4" + "license": "MIT", + "bin": { + "he": "bin/he" } }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", "dev": true, + "license": "MIT", "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dev": true, + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10.19.0" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 4" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/immer": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.2.tgz", + "integrity": "sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==", "dev": true, - "engines": { - "node": ">=0.4.0" + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" } }, - "node_modules/delete-empty": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/delete-empty/-/delete-empty-3.0.0.tgz", - "integrity": "sha512-ZUyiwo76W+DYnKsL3Kim6M/UOavPdBJgDYWOmuQhYaZvJH0AXAHbUNyEDtRbBra8wqqr686+63/0azfEk1ebUQ==", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.0", - "minimist": "^1.2.0", - "path-starts-with": "^2.0.0", - "rimraf": "^2.6.2" - }, - "bin": { - "delete-empty": "bin/cli.js" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "engines": { - "node": ">= 0.8" + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } + "license": "ISC" }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } + "license": "ISC" }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, - "engines": { - "node": ">=0.3.1" - } + "license": "MIT" }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/difflib": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", - "integrity": "sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "dependencies": { - "heap": ">= 0.2.0" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", - "dev": true - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/duplexer3": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", - "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", - "dev": true - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true + "license": "ISC" }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/emitter-component": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", - "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==", - "dev": true, + "@isaacs/cliui": "^8.0.2" + }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", "dev": true, - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, - "dependencies": { - "once": "^1.4.0" - } + "license": "MIT" }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=8.6" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/json-stream-stringify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz", + "integrity": "sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=7.10.1" } }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } + "license": "ISC" }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "license": "MIT", + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": ">= 0.4" + "node": ">=6" } }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "dependencies": { - "hasown": "^2.0.0" + "license": "MIT", + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/es5-ext": { - "version": "0.10.64", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", - "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" + "json-buffer": "3.0.1" } }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "license": "MIT", + "engines": { + "node": ">=6" } }, - "node_modules/es6-symbol": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", - "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", "dev": true, + "license": "MIT", "dependencies": { - "d": "^1.0.2", - "ext": "^1.7.0" + "package-json": "^8.1.0" }, "engines": { - "node": ">=0.12" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { "node": ">=10" }, @@ -4417,6191 +3442,604 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true, - "dependencies": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=0.12.0" - }, - "optionalDependencies": { - "source-map": "~0.2.0" - } + "license": "MIT" }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eth-ens-namehash": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz", - "integrity": "sha512-VWEI1+KJfz4Km//dadyvBBoBeSQ0MHTXPvr8UIXiLW6IanxvAV+DmlZAijZwAyggqGUfwQBeHf7tc9wzc1piSw==", - "dev": true, - "dependencies": { - "idna-uts46-hx": "^2.3.1", - "js-sha3": "^0.5.7" - } - }, - "node_modules/eth-ens-namehash/node_modules/js-sha3": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.7.tgz", - "integrity": "sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==", - "dev": true - }, - "node_modules/eth-gas-reporter": { - "version": "0.2.27", - "resolved": "https://registry.npmjs.org/eth-gas-reporter/-/eth-gas-reporter-0.2.27.tgz", - "integrity": "sha512-femhvoAM7wL0GcI8ozTdxfuBtBFJ9qsyIAsmKVjlWAHUbdnnXHt+lKzz/kmldM5lA9jLuNHGwuIxorNpLbR1Zw==", - "dev": true, - "peer": true, - "dependencies": { - "@solidity-parser/parser": "^0.14.0", - "axios": "^1.5.1", - "cli-table3": "^0.5.0", - "colors": "1.4.0", - "ethereum-cryptography": "^1.0.3", - "ethers": "^5.7.2", - "fs-readdir-recursive": "^1.1.0", - "lodash": "^4.17.14", - "markdown-table": "^1.1.3", - "mocha": "^10.2.0", - "req-cwd": "^2.0.0", - "sha1": "^1.1.1", - "sync-request": "^6.0.0" - }, - "peerDependencies": { - "@codechecks/client": "^0.1.0" - }, - "peerDependenciesMeta": { - "@codechecks/client": { - "optional": true - } - } - }, - "node_modules/eth-gas-reporter/node_modules/@noble/hashes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", - "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "peer": true - }, - "node_modules/eth-gas-reporter/node_modules/@scure/bip32": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", - "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "peer": true, - "dependencies": { - "@noble/hashes": "~1.2.0", - "@noble/secp256k1": "~1.7.0", - "@scure/base": "~1.1.0" - } - }, - "node_modules/eth-gas-reporter/node_modules/@scure/bip39": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", - "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "peer": true, - "dependencies": { - "@noble/hashes": "~1.2.0", - "@scure/base": "~1.1.0" - } - }, - "node_modules/eth-gas-reporter/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-gas-reporter/node_modules/cli-table3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", - "dev": true, - "peer": true, - "dependencies": { - "object-assign": "^4.1.0", - "string-width": "^2.1.1" - }, - "engines": { - "node": ">=6" - }, - "optionalDependencies": { - "colors": "^1.1.2" - } - }, - "node_modules/eth-gas-reporter/node_modules/ethereum-cryptography": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", - "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", - "dev": true, - "peer": true, - "dependencies": { - "@noble/hashes": "1.2.0", - "@noble/secp256k1": "1.7.1", - "@scure/bip32": "1.1.5", - "@scure/bip39": "1.1.1" - } - }, - "node_modules/eth-gas-reporter/node_modules/ethers": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", - "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "peer": true, - "dependencies": { - "@ethersproject/abi": "5.7.0", - "@ethersproject/abstract-provider": "5.7.0", - "@ethersproject/abstract-signer": "5.7.0", - "@ethersproject/address": "5.7.0", - "@ethersproject/base64": "5.7.0", - "@ethersproject/basex": "5.7.0", - "@ethersproject/bignumber": "5.7.0", - "@ethersproject/bytes": "5.7.0", - "@ethersproject/constants": "5.7.0", - "@ethersproject/contracts": "5.7.0", - "@ethersproject/hash": "5.7.0", - "@ethersproject/hdnode": "5.7.0", - "@ethersproject/json-wallets": "5.7.0", - "@ethersproject/keccak256": "5.7.0", - "@ethersproject/logger": "5.7.0", - "@ethersproject/networks": "5.7.1", - "@ethersproject/pbkdf2": "5.7.0", - "@ethersproject/properties": "5.7.0", - "@ethersproject/providers": "5.7.2", - "@ethersproject/random": "5.7.0", - "@ethersproject/rlp": "5.7.0", - "@ethersproject/sha2": "5.7.0", - "@ethersproject/signing-key": "5.7.0", - "@ethersproject/solidity": "5.7.0", - "@ethersproject/strings": "5.7.0", - "@ethersproject/transactions": "5.7.0", - "@ethersproject/units": "5.7.0", - "@ethersproject/wallet": "5.7.0", - "@ethersproject/web": "5.7.1", - "@ethersproject/wordlists": "5.7.0" - } - }, - "node_modules/eth-gas-reporter/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-gas-reporter/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "peer": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-gas-reporter/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-lib": { - "version": "0.1.29", - "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.1.29.tgz", - "integrity": "sha512-bfttrr3/7gG4E02HoWTDUcDDslN003OlOoBxk9virpAZQ1ja/jDgwkWB8QfJF7ojuEowrqy+lzp9VcJG7/k5bQ==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "nano-json-stream-parser": "^0.1.2", - "servify": "^0.1.12", - "ws": "^3.0.0", - "xhr-request-promise": "^0.1.2" - } - }, - "node_modules/eth-lib/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/eth-lib/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/eth-lib/node_modules/ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "dependencies": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - }, - "node_modules/eth2-keystore-js": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/eth2-keystore-js/-/eth2-keystore-js-1.0.8.tgz", - "integrity": "sha512-H5JLUeo7aiZs7zVAb+9gSJZZxfcx5na8zPxcgFbggNfac+atyO5H6KpvDUPJFRm/umHWM7++MdvS/q5Sbw+f9g==", - "dev": true, - "dependencies": { - "@types/node": "^15.12.2", - "crypto": "^1.0.1", - "ethereumjs-util": "^7.0.10", - "ethereumjs-wallet": "^1.0.1", - "husky": "^6.0.0", - "scrypt-js": "^3.0.1", - "tslint": "^6.1.3", - "tslint-config-prettier": "^1.18.0", - "typescript": "^4.3.2" - } - }, - "node_modules/eth2-keystore-js/node_modules/@types/node": { - "version": "15.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz", - "integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A==", - "dev": true - }, - "node_modules/eth2-keystore-js/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/ethereum-bloom-filters": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.1.0.tgz", - "integrity": "sha512-J1gDRkLpuGNvWYzWslBQR9cDV4nd4kfvVTE/Wy4Kkm4yb3EYRSlyi0eB/inTsSTTVyA0+HyzHgbr95Fn/Z1fSw==", - "dev": true, - "dependencies": { - "@noble/hashes": "^1.4.0" - } - }, - "node_modules/ethereum-bloom-filters/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "dev": true, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz", - "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", - "dev": true, - "dependencies": { - "@types/pbkdf2": "^3.0.0", - "@types/secp256k1": "^4.0.1", - "blakejs": "^1.1.0", - "browserify-aes": "^1.2.0", - "bs58check": "^2.1.2", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "hash.js": "^1.1.7", - "keccak": "^3.0.0", - "pbkdf2": "^3.0.17", - "randombytes": "^2.1.0", - "safe-buffer": "^5.1.2", - "scrypt-js": "^3.0.0", - "secp256k1": "^4.0.1", - "setimmediate": "^1.0.5" - } - }, - "node_modules/ethereumjs-abi": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.8.tgz", - "integrity": "sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.8", - "ethereumjs-util": "^6.0.0" - } - }, - "node_modules/ethereumjs-abi/node_modules/@types/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/ethereumjs-abi/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/ethereumjs-abi/node_modules/ethereumjs-util": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", - "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", - "dev": true, - "dependencies": { - "@types/bn.js": "^4.11.3", - "bn.js": "^4.11.0", - "create-hash": "^1.1.2", - "elliptic": "^6.5.2", - "ethereum-cryptography": "^0.1.3", - "ethjs-util": "0.1.6", - "rlp": "^2.2.3" - } - }, - "node_modules/ethereumjs-util": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz", - "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", - "dev": true, - "dependencies": { - "@types/bn.js": "^5.1.0", - "bn.js": "^5.1.2", - "create-hash": "^1.1.2", - "ethereum-cryptography": "^0.1.3", - "rlp": "^2.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/ethereumjs-wallet": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-1.0.2.tgz", - "integrity": "sha512-CCWV4RESJgRdHIvFciVQFnCHfqyhXWchTPlkfp28Qc53ufs+doi5I/cV2+xeK9+qEo25XCWfP9MiL+WEPAZfdA==", - "dev": true, - "dependencies": { - "aes-js": "^3.1.2", - "bs58check": "^2.1.2", - "ethereum-cryptography": "^0.1.3", - "ethereumjs-util": "^7.1.2", - "randombytes": "^2.1.0", - "scrypt-js": "^3.0.1", - "utf8": "^3.0.0", - "uuid": "^8.3.2" - } - }, - "node_modules/ethereumjs-wallet/node_modules/aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==", - "dev": true - }, - "node_modules/ethers": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.1.tgz", - "integrity": "sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "peer": true, - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "18.15.13", - "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", - "dev": true, - "peer": true - }, - "node_modules/ethers/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true - }, - "node_modules/ethjs-unit": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", - "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", - "dev": true, - "dependencies": { - "bn.js": "4.11.6", - "number-to-bn": "1.7.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/ethjs-unit/node_modules/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", - "dev": true - }, - "node_modules/ethjs-util": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz", - "integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==", - "dev": true, - "dependencies": { - "is-hex-prefixed": "1.0.0", - "strip-hex-prefix": "1.0.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", - "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==", - "dev": true - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "dev": true, - "dependencies": { - "type": "^2.7.2" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-base64-decode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", - "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/figlet": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.7.0.tgz", - "integrity": "sha512-gO8l3wvqo0V7wEFLXPbkX83b7MVjRrk1oRLfYlZXol8nEpb/ON9pcKLI4qpBv5YtOTfrINtqb7b40iYY2FTWFg==", - "dev": true, - "bin": { - "figlet": "bin/index.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", - "dev": true, - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fp-ts": { - "version": "1.19.3", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.19.3.tgz", - "integrity": "sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==", - "dev": true - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "dependencies": { - "minipass": "^2.6.0" - } - }, - "node_modules/fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true, - "peer": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/get-random-values": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-random-values/-/get-random-values-1.2.2.tgz", - "integrity": "sha512-lMyPjQyl0cNNdDf2oR+IQ/fM3itDvpoHy45Ymo2r0L1EjazeSl13SfbKZs7KtZ/3MDCeueiaJiuOEfKqRTsSgA==", - "dev": true, - "dependencies": { - "global": "^4.4.0" - }, - "engines": { - "node": "10 || 12 || >=14" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/ghost-testrpc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/ghost-testrpc/-/ghost-testrpc-0.0.2.tgz", - "integrity": "sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ==", - "dev": true, - "dependencies": { - "chalk": "^2.4.2", - "node-emoji": "^1.10.0" - }, - "bin": { - "testrpc-sc": "index.js" - } - }, - "node_modules/ghost-testrpc/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/ghost-testrpc/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/ghost-testrpc/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ghost-testrpc/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dev": true, - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", - "dev": true, - "dependencies": { - "@types/glob": "^7.1.1", - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/har-validator/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/har-validator/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/hardhat": { - "version": "2.22.6", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.22.6.tgz", - "integrity": "sha512-abFEnd9QACwEtSvZZGSmzvw7N3zhQN1cDKz5SLHAupfG24qTHofCjqvD5kT5Wwsq5XOL0ON1Mq5rr4v0XX5ciw==", - "dev": true, - "dependencies": { - "@ethersproject/abi": "^5.1.2", - "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/edr": "^0.4.1", - "@nomicfoundation/ethereumjs-common": "4.0.4", - "@nomicfoundation/ethereumjs-tx": "5.0.4", - "@nomicfoundation/ethereumjs-util": "9.0.4", - "@nomicfoundation/solidity-analyzer": "^0.1.0", - "@sentry/node": "^5.18.1", - "@types/bn.js": "^5.1.0", - "@types/lru-cache": "^5.1.0", - "adm-zip": "^0.4.16", - "aggregate-error": "^3.0.0", - "ansi-escapes": "^4.3.0", - "boxen": "^5.1.2", - "chalk": "^2.4.2", - "chokidar": "^3.4.0", - "ci-info": "^2.0.0", - "debug": "^4.1.1", - "enquirer": "^2.3.0", - "env-paths": "^2.2.0", - "ethereum-cryptography": "^1.0.3", - "ethereumjs-abi": "^0.6.8", - "find-up": "^2.1.0", - "fp-ts": "1.19.3", - "fs-extra": "^7.0.1", - "glob": "7.2.0", - "immutable": "^4.0.0-rc.12", - "io-ts": "1.10.4", - "keccak": "^3.0.2", - "lodash": "^4.17.11", - "mnemonist": "^0.38.0", - "mocha": "^10.0.0", - "p-map": "^4.0.0", - "raw-body": "^2.4.1", - "resolve": "1.17.0", - "semver": "^6.3.0", - "solc": "0.8.26", - "source-map-support": "^0.5.13", - "stacktrace-parser": "^0.1.10", - "tsort": "0.0.1", - "undici": "^5.14.0", - "uuid": "^8.3.2", - "ws": "^7.4.6" - }, - "bin": { - "hardhat": "internal/cli/bootstrap.js" - }, - "peerDependencies": { - "ts-node": "*", - "typescript": "*" - }, - "peerDependenciesMeta": { - "ts-node": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/hardhat-abi-exporter": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/hardhat-abi-exporter/-/hardhat-abi-exporter-2.10.1.tgz", - "integrity": "sha512-X8GRxUTtebMAd2k4fcPyVnCdPa6dYK4lBsrwzKP5yiSq4i+WadWPIumaLfce53TUf/o2TnLpLOduyO1ylE2NHQ==", - "dev": true, - "dependencies": { - "@ethersproject/abi": "^5.5.0", - "delete-empty": "^3.0.0" - }, - "engines": { - "node": ">=14.14.0" - }, - "peerDependencies": { - "hardhat": "^2.0.0" - } - }, - "node_modules/hardhat-contract-sizer": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/hardhat-contract-sizer/-/hardhat-contract-sizer-2.10.0.tgz", - "integrity": "sha512-QiinUgBD5MqJZJh1hl1jc9dNnpJg7eE/w4/4GEnrcmZJJTDbVFNe3+/3Ep24XqISSkYxRz36czcPHKHd/a0dwA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "cli-table3": "^0.6.0", - "strip-ansi": "^6.0.0" - }, - "peerDependencies": { - "hardhat": "^2.0.0" - } - }, - "node_modules/hardhat-gas-reporter": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-1.0.10.tgz", - "integrity": "sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA==", - "dev": true, - "peer": true, - "dependencies": { - "array-uniq": "1.0.3", - "eth-gas-reporter": "^0.2.25", - "sha1": "^1.1.1" - }, - "peerDependencies": { - "hardhat": "^2.0.2" - } - }, - "node_modules/hardhat/node_modules/@noble/hashes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", - "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr/-/edr-0.4.1.tgz", - "integrity": "sha512-NgrMo2rI9r28uidumvd+K2/AJLdxtXsUlJr3hj/pM6S1FCd/HiWaLeLa/cjCVPcE2u1rYAa3W6UFxLCB7S5Dhw==", - "dev": true, - "dependencies": { - "@nomicfoundation/edr-darwin-arm64": "0.4.1", - "@nomicfoundation/edr-darwin-x64": "0.4.1", - "@nomicfoundation/edr-linux-arm64-gnu": "0.4.1", - "@nomicfoundation/edr-linux-arm64-musl": "0.4.1", - "@nomicfoundation/edr-linux-x64-gnu": "0.4.1", - "@nomicfoundation/edr-linux-x64-musl": "0.4.1", - "@nomicfoundation/edr-win32-x64-msvc": "0.4.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr-darwin-arm64": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.4.1.tgz", - "integrity": "sha512-XuiUUnWAVNw7JYv7nRqDWfpBm21HOxCRBQ8lQnRnmiets9Ss2X5Ul9mvBheIPh/D0wBzwJ8TRtsSrorpwE79cA==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr-darwin-x64": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.4.1.tgz", - "integrity": "sha512-N1MfJqEX5ixaXlyyrHnaYxzwIT27Nc/jUgLI7ts4/9kRvPTvyZRYmXS1ciKhmUFr/WvFckTCix2RJbZoGGtX7g==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr-linux-arm64-gnu": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.4.1.tgz", - "integrity": "sha512-bSPOfmcFjJwDgWOV5kgZHeqg2OWu1cINrHSGjig0aVHehjcoX4Sgayrj6fyAxcOV5NQKA6WcyTFll6NrCxzWRA==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr-linux-arm64-musl": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.4.1.tgz", - "integrity": "sha512-F/+DgOdeBFQDrk+SX4aFffJFBgJfd75ZtE2mjcWNAh/qWiS7NfUxdQX/5OvNo/H6EY4a+3bZH6Bgzqg4mEWvMw==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr-linux-x64-gnu": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.4.1.tgz", - "integrity": "sha512-POHhTWczIXCPhzKtY0Vt/l+VCqqCx5gNR5ErwSrNnLz/arfQobZFAU+nc61BX3Jch82TW8b3AbfGI73Kh7gO0w==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr-linux-x64-musl": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.4.1.tgz", - "integrity": "sha512-uu8oNp4Ozg3H1x1We0FF+rwXfFiAvsOm5GQ+OBx9YYOXnfDPWqguQfGIkhrti9GD0iYhfQ/WOG5wvp0IzzgGSg==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@nomicfoundation/edr-win32-x64-msvc": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.4.1.tgz", - "integrity": "sha512-PaZHFw455z89ZiKYNTnKu+/TiVZVRI+mRJsbRTe2N0VlYfUBS1o2gdXBM12oP1t198HR7xQwEPPAslTFxGBqHA==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/hardhat/node_modules/@scure/bip32": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", - "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "@noble/hashes": "~1.2.0", - "@noble/secp256k1": "~1.7.0", - "@scure/base": "~1.1.0" - } - }, - "node_modules/hardhat/node_modules/@scure/bip39": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", - "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "@noble/hashes": "~1.2.0", - "@scure/base": "~1.1.0" - } - }, - "node_modules/hardhat/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/hardhat/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/hardhat/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/hardhat/node_modules/ethereum-cryptography": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", - "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", - "dev": true, - "dependencies": { - "@noble/hashes": "1.2.0", - "@noble/secp256k1": "1.7.1", - "@scure/bip32": "1.1.5", - "@scure/bip39": "1.1.1" - } - }, - "node_modules/hardhat/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/hardhat/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/hardhat/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/hardhat/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "dev": true, - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/heap": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", - "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==", - "dev": true - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/http-basic": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", - "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", - "dev": true, - "peer": true, - "dependencies": { - "caseless": "^0.12.0", - "concat-stream": "^1.6.2", - "http-response-object": "^3.0.1", - "parse-cache-control": "^1.0.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-https": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/http-https/-/http-https-1.0.0.tgz", - "integrity": "sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg==", - "dev": true - }, - "node_modules/http-response-object": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", - "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "^10.0.3" - } - }, - "node_modules/http-response-object/node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "dev": true, - "peer": true - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/husky": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz", - "integrity": "sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ==", - "dev": true, - "bin": { - "husky": "lib/bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/idna-uts46-hx": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz", - "integrity": "sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA==", - "dev": true, - "dependencies": { - "punycode": "2.1.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/idna-uts46-hx/node_modules/punycode": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha512-Yxz2kRwT90aPiWEMHVYnEf4+rhwF1tBmmZ4KepCP+Wkium9JxtWnUm1nqGwpiAHr/tnTSeHqr3wb++jgSkXjhA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.2.tgz", - "integrity": "sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==", - "dev": true, - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "dev": true - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/io-ts": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-1.10.4.tgz", - "integrity": "sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==", - "dev": true, - "dependencies": { - "fp-ts": "^1.0.0" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", - "dev": true - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hex-prefixed": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", - "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", - "dev": true, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isomorphic-unfetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", - "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.1", - "unfetch": "^4.2.0" - } - }, - "node_modules/isows": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.4.tgz", - "integrity": "sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "peer": true, - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true - }, - "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", - "dev": true - }, - "node_modules/js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", - "dev": true - }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, - "node_modules/jsencrypt": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.2.1.tgz", - "integrity": "sha512-k1sD5QV0KPn+D8uG9AdGzTQuamt82QZ3A3l6f7TRwMU6Oi2Vg0BsL+wZIQBONcraO1pc78ExMdvmBBJ8WhNYUA==", - "dev": true - }, - "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", - "dev": true - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "peer": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonschema": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", - "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dev": true, - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/keccak": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", - "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/libphonenumber-js": { - "version": "1.11.4", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz", - "integrity": "sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q==", - "dev": true - }, - "node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", - "dev": true, - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true, - "peer": true - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "peer": true - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true, - "peer": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "peer": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lru_map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", - "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==", - "dev": true - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "peer": true - }, - "node_modules/markdown-table": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", - "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==", - "dev": true, - "peer": true - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micro-ftch": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", - "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", - "dev": true, - "dependencies": { - "dom-walk": "^0.1.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mkdirp-promise": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz", - "integrity": "sha512-Hepn5kb1lJPtVW84RFT40YG1OddBNTOVUZR2bzQUHc+Z03en8/3uX0+060JDhcEzyO08HmipsN9DcnFMxhIL9w==", - "deprecated": "This package is broken and no longer maintained. 'mkdirp' itself supports promises now, please switch to that.", - "dev": true, - "dependencies": { - "mkdirp": "*" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mnemonist": { - "version": "0.38.5", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.5.tgz", - "integrity": "sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==", - "dev": true, - "dependencies": { - "obliterator": "^2.0.0" - } - }, - "node_modules/mocha": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.6.0.tgz", - "integrity": "sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mocha/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mock-fs": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.14.0.tgz", - "integrity": "sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw==", - "dev": true - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/multibase": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/multibase/-/multibase-0.6.1.tgz", - "integrity": "sha512-pFfAwyTjbbQgNc3G7D48JkJxWtoJoBMaR4xQUOuB8RnCgRqaYmWNFeJTTvrJ2w51bjLq2zTby6Rqj9TQ9elSUw==", - "deprecated": "This module has been superseded by the multiformats module", - "dev": true, - "dependencies": { - "base-x": "^3.0.8", - "buffer": "^5.5.0" - } - }, - "node_modules/multibase/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/multicodec": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/multicodec/-/multicodec-0.5.7.tgz", - "integrity": "sha512-PscoRxm3f+88fAtELwUnZxGDkduE2HD9Q6GHUOywQLjOGT/HAdhjLDYNZ1e7VR0s0TP0EwZ16LNUTFpoBGivOA==", - "deprecated": "This module has been superseded by the multiformats module", - "dev": true, - "dependencies": { - "varint": "^5.0.0" - } - }, - "node_modules/multihashes": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/multihashes/-/multihashes-0.4.21.tgz", - "integrity": "sha512-uVSvmeCWf36pU2nB4/1kzYZjsXD9vofZKpgudqkceYY5g2aZZXJ5r9lxuzoRLl1OAp28XljXsEJ/X/85ZsKmKw==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "multibase": "^0.7.0", - "varint": "^5.0.0" - } - }, - "node_modules/multihashes/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/multihashes/node_modules/multibase": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/multibase/-/multibase-0.7.0.tgz", - "integrity": "sha512-TW8q03O0f6PNFTQDvh3xxH03c8CjGaaYrjkl9UQPG6rz53TQzzxJVCIWVjzcbN/Q5Y53Zd0IBQBMVktVgNx4Fg==", - "deprecated": "This module has been superseded by the multiformats module", - "dev": true, - "dependencies": { - "base-x": "^3.0.8", - "buffer": "^5.5.0" - } - }, - "node_modules/nano-json-stream-parser": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz", - "integrity": "sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew==", - "dev": true - }, - "node_modules/ndjson": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-2.0.0.tgz", - "integrity": "sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==", - "dev": true, - "peer": true, - "dependencies": { - "json-stringify-safe": "^5.0.1", - "minimist": "^1.2.5", - "readable-stream": "^3.6.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "bin": { - "ndjson": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true - }, - "node_modules/node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", - "dev": true - }, - "node_modules/node-emoji": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", - "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", - "dev": true, - "dependencies": { - "lodash": "^4.17.21" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", - "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", - "dev": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-jsencrypt": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-jsencrypt/-/node-jsencrypt-1.0.0.tgz", - "integrity": "sha512-ANQ/XkOVS02R89MtfoelFxarMsLA12nlOT802VS4LVhl+JRSRZIvkLIjvKYZmIar+mENUkR9mBKUdcdiPy/7lA==", - "dev": true, - "dependencies": { - "get-random-values": "^1.2.0" - } - }, - "node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", - "dev": true, - "engines": { - "node": ">=12.19" - } - }, - "node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/number-to-bn": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", - "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", - "dev": true, - "dependencies": { - "bn.js": "4.11.6", - "strip-hex-prefix": "1.0.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/number-to-bn/node_modules/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", - "dev": true - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obliterator": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", - "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==", - "dev": true - }, - "node_modules/oboe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/oboe/-/oboe-2.1.5.tgz", - "integrity": "sha512-zRFWiF+FoicxEs3jNI/WYUrVEgA7DeET/InK0XQuudGHRg8iIob3cNPrJTKaz4004uaA9Pbe+Dwa8iluhjLZWA==", - "dev": true, - "dependencies": { - "http-https": "^1.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ordinal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ordinal/-/ordinal-1.0.3.tgz", - "integrity": "sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==", - "dev": true - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", - "dev": true, - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "dev": true, - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-asn1/node_modules/hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/parse-cache-control": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", - "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", - "dev": true, - "peer": true - }, - "node_modules/parse-headers": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", - "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", - "dev": true - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-starts-with": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-starts-with/-/path-starts-with-2.0.1.tgz", - "integrity": "sha512-wZ3AeiRBRlNwkdUxvBANh0+esnt38DLffHDujZyRHkqkaKHTglnY2EP5UX3b8rdeiSutgO4y9NEJwXezNP5vHg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "peer": true, - "engines": { - "node": "*" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/promise": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", - "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", - "dev": true, - "peer": true, - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "git+ssh://git@github.com/meshin-blox/prompts.git#a22bdac044f6b32ba67adb4eacc2e58322512a2d", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^4.0.1", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", - "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", - "dev": true, - "peer": true, - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "dev": true, - "dependencies": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "dev": true, - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/req-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", - "integrity": "sha512-ueoIoLo1OfB6b05COxAA9UpeoscNpYyM+BqYlA7H6LVF4hKGPXQQSSaD2YmvDVJMkk4UDpAHIeU1zG53IqjvlQ==", - "dev": true, - "peer": true, - "dependencies": { - "req-from": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/req-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/req-from/-/req-from-2.0.0.tgz", - "integrity": "sha512-LzTfEVDVQHBRfjOUMgNBA+V6DWsSnoeKzf42J7l0xa/B4jyPOuuF5MlNSmomLNGemWTnV2TIdjSSLnEn95fOQA==", - "dev": true, - "peer": true, - "dependencies": { - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dev": true, - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/request/node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", - "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, - "dependencies": { - "path-parse": "^1.0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "dev": true, - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/rlp": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.7.tgz", - "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", - "dev": true, - "dependencies": { - "bn.js": "^5.2.0" - }, - "bin": { - "rlp": "bin/rlp" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sc-istanbul": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/sc-istanbul/-/sc-istanbul-0.4.6.tgz", - "integrity": "sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g==", - "dev": true, - "dependencies": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "istanbul": "lib/cli.js" - } - }, - "node_modules/sc-istanbul/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/sc-istanbul/node_modules/glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/sc-istanbul/node_modules/has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sc-istanbul/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/sc-istanbul/node_modules/js-yaml/node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/sc-istanbul/node_modules/resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", - "dev": true - }, - "node_modules/sc-istanbul/node_modules/supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", - "dev": true, - "dependencies": { - "has-flag": "^1.0.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/scrypt-js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", - "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", - "dev": true - }, - "node_modules/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "elliptic": "^6.5.4", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/servify": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/servify/-/servify-0.1.12.tgz", - "integrity": "sha512-/xE6GvsKKqyo1BAY+KxOWXcLpPsUUyji7Qg3bVD7hh1eRze5bR1uYiuDA/k3Gof1s9BTzQZEJK8sNcNGFIzeWw==", - "dev": true, - "dependencies": { - "body-parser": "^1.16.0", - "cors": "^2.8.1", - "express": "^4.14.0", - "request": "^2.79.0", - "xhr": "^2.3.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/sha1": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", - "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", - "dev": true, - "peer": true, - "dependencies": { - "charenc": ">= 0.0.1", - "crypt": ">= 0.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/shelljs": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", - "dev": true, - "dependencies": { - "glob": "^7.0.0", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.2.tgz", - "integrity": "sha512-Ijd/rV5o+mSBBs4F/x9oDPtTx9Zb6X9brmnXvMW4J7IR15ngi9q5xxqWBKU744jTZiaXtxaPL7uHG6vtN8kUkw==", - "dev": true, - "dependencies": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/solc": { - "version": "0.8.26", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.26.tgz", - "integrity": "sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==", - "dev": true, - "dependencies": { - "command-exists": "^1.2.8", - "commander": "^8.1.0", - "follow-redirects": "^1.12.1", - "js-sha3": "0.8.0", - "memorystream": "^0.3.1", - "semver": "^5.5.0", - "tmp": "0.0.33" - }, - "bin": { - "solcjs": "solc.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/solc/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/solidity-ast": { - "version": "0.4.56", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.56.tgz", - "integrity": "sha512-HgmsA/Gfklm/M8GFbCX/J1qkVH0spXHgALCNZ8fA8x5X+MFdn/8CP2gr5OVyXjXw6RZTPC/Sxl2RUDQOXyNMeA==", - "dev": true, - "dependencies": { - "array.prototype.findlast": "^1.2.2" - } - }, - "node_modules/solidity-coverage": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.12.tgz", - "integrity": "sha512-8cOB1PtjnjFRqOgwFiD8DaUsYJtVJ6+YdXQtSZDrLGf8cdhhh8xzTtGzVTGeBf15kTv0v7lYPJlV/az7zLEPJw==", - "dev": true, - "dependencies": { - "@ethersproject/abi": "^5.0.9", - "@solidity-parser/parser": "^0.18.0", - "chalk": "^2.4.2", - "death": "^1.1.0", - "difflib": "^0.2.4", - "fs-extra": "^8.1.0", - "ghost-testrpc": "^0.0.2", - "global-modules": "^2.0.0", - "globby": "^10.0.1", - "jsonschema": "^1.2.4", - "lodash": "^4.17.21", - "mocha": "^10.2.0", - "node-emoji": "^1.10.0", - "pify": "^4.0.1", - "recursive-readdir": "^2.2.2", - "sc-istanbul": "^0.4.5", - "semver": "^7.3.4", - "shelljs": "^0.8.3", - "web3-utils": "^1.3.6" - }, - "bin": { - "solidity-coverage": "plugins/bin.js" - }, - "peerDependencies": { - "hardhat": "^2.11.0" - } - }, - "node_modules/solidity-coverage/node_modules/@solidity-parser/parser": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.18.0.tgz", - "integrity": "sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==", - "dev": true - }, - "node_modules/solidity-coverage/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solidity-coverage/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solidity-coverage/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/solidity-coverage/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/solidity-coverage/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/solidity-coverage/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/solidity-coverage/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/solidity-coverage/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/solidity-coverage/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/solidity-coverage/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solidity-coverage/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", - "dev": true, - "optional": true, - "dependencies": { - "amdefine": ">=0.0.4" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "peer": true, - "dependencies": { - "readable-stream": "^3.0.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "dev": true, - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sshpk/node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true - }, - "node_modules/ssv-keys": { - "version": "1.0.4", - "resolved": "git+ssh://git@github.com/bloxapp/ssv-keys.git#cc9e5cdd4696a0e855fc4642c2868abd62d5141a", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/figlet": "^1.5.4", - "@types/underscore": "^1.11.4", - "@types/yargs": "^17.0.12", - "argparse": "^2.0.1", - "assert": "^2.0.0", - "atob": "^2.1.2", - "bls-eth-wasm": "^1.0.4", - "bls-signatures": "^0.2.5", - "btoa": "^1.2.1", - "class-validator": "^0.13.2", - "colors": "^1.4.0", - "crypto": "^1.0.1", - "eth2-keystore-js": "^1.0.8", - "ethereumjs-util": "^7.1.5", - "ethereumjs-wallet": "^1.0.1", - "ethers": "^5.7.2", - "events": "^3.3.0", - "figlet": "^1.5.2", - "js-base64": "^3.7.2", - "jsencrypt": "3.2.1", - "minimist": "^1.2.6", - "moment": "^2.29.3", - "node-jsencrypt": "^1.0.0", - "prompts": "https://github.com/meshin-blox/prompts.git", - "scrypt-js": "^3.0.1", - "semver": "^7.5.1", - "stream": "^0.0.2", - "underscore": "^1.13.4", - "web3": "1.7.3", - "yargs": "^17.5.1" - }, - "bin": { - "ssv-keys": "dist/tsc/src/cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/ssv-keys/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/ssv-keys/node_modules/ethers": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", - "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/abi": "5.7.0", - "@ethersproject/abstract-provider": "5.7.0", - "@ethersproject/abstract-signer": "5.7.0", - "@ethersproject/address": "5.7.0", - "@ethersproject/base64": "5.7.0", - "@ethersproject/basex": "5.7.0", - "@ethersproject/bignumber": "5.7.0", - "@ethersproject/bytes": "5.7.0", - "@ethersproject/constants": "5.7.0", - "@ethersproject/contracts": "5.7.0", - "@ethersproject/hash": "5.7.0", - "@ethersproject/hdnode": "5.7.0", - "@ethersproject/json-wallets": "5.7.0", - "@ethersproject/keccak256": "5.7.0", - "@ethersproject/logger": "5.7.0", - "@ethersproject/networks": "5.7.1", - "@ethersproject/pbkdf2": "5.7.0", - "@ethersproject/properties": "5.7.0", - "@ethersproject/providers": "5.7.2", - "@ethersproject/random": "5.7.0", - "@ethersproject/rlp": "5.7.0", - "@ethersproject/sha2": "5.7.0", - "@ethersproject/signing-key": "5.7.0", - "@ethersproject/solidity": "5.7.0", - "@ethersproject/strings": "5.7.0", - "@ethersproject/transactions": "5.7.0", - "@ethersproject/units": "5.7.0", - "@ethersproject/wallet": "5.7.0", - "@ethersproject/web": "5.7.1", - "@ethersproject/wordlists": "5.7.0" - } - }, - "node_modules/ssv-keys/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ssv-keys/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/ssv-keys/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dev": true, - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", - "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", - "dev": true, - "dependencies": { - "emitter-component": "^1.1.1" - } - }, - "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-hex-prefix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", - "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", - "dev": true, - "dependencies": { - "is-hex-prefixed": "1.0.0" - }, - "engines": { - "node": ">=6.5.0", - "npm": ">=3" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/swarm-js": { - "version": "0.1.42", - "resolved": "https://registry.npmjs.org/swarm-js/-/swarm-js-0.1.42.tgz", - "integrity": "sha512-BV7c/dVlA3R6ya1lMlSSNPLYrntt0LUq4YMgy3iwpCIc6rZnS5W2wUoctarZ5pXlpKtxDDf9hNziEkcfrxdhqQ==", - "dev": true, - "dependencies": { - "bluebird": "^3.5.0", - "buffer": "^5.0.5", - "eth-lib": "^0.1.26", - "fs-extra": "^4.0.2", - "got": "^11.8.5", - "mime-types": "^2.1.16", - "mkdirp-promise": "^5.0.1", - "mock-fs": "^4.1.0", - "setimmediate": "^1.0.5", - "tar": "^4.0.2", - "xhr-request": "^1.0.1" - } - }, - "node_modules/swarm-js/node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } + "license": "MIT" }, - "node_modules/swarm-js/node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/swarm-js/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } + "license": "MIT", + "peer": true }, - "node_modules/swarm-js/node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/swarm-js/node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/swarm-js/node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, "engines": { "node": ">=10" - } - }, - "node_modules/swarm-js/node_modules/fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/swarm-js/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/swarm-js/node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/swarm-js/node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/swarm-js/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/swarm-js/node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/swarm-js/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/swarm-js/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/swarm-js/node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/swarm-js/node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/swarm-js/node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, - "dependencies": { - "lowercase-keys": "^2.0.0" + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/swarm-js/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "engines": { - "node": ">= 4.0.0" + "license": "ISC" + }, + "node_modules/micro-eth-signer": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/micro-eth-signer/-/micro-eth-signer-0.14.0.tgz", + "integrity": "sha512-5PLLzHiVYPWClEvZIXXFu5yutzpadb73rnQCpUqIHu3No3coFuWQNfE5tkBQJ7djuLYl6aRLaS0MgWJYGoqiBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "micro-packed": "~0.7.2" } }, - "node_modules/sync-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", - "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", + "node_modules/micro-eth-signer/node_modules/@noble/curves": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.2.tgz", + "integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "http-response-object": "^3.0.1", - "sync-rpc": "^1.2.1", - "then-request": "^6.0.0" + "@noble/hashes": "1.7.2" }, "engines": { - "node": ">=8.0.0" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/sync-rpc": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", - "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "node_modules/micro-eth-signer/node_modules/@noble/hashes": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.2.tgz", + "integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==", "dev": true, - "peer": true, - "dependencies": { - "get-port": "^3.1.0" + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/table": { - "version": "6.8.2", - "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", - "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", + "node_modules/micro-packed": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.7.3.tgz", + "integrity": "sha512-2Milxs+WNC00TRlem41oRswvw31146GiSaoCT7s3Xi2gMUglW5QBeqlQaZeHr5tJx9nm3i57LNXPqxOOaWtTYg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" + "@scure/base": "~1.2.5" }, - "engines": { - "node": ">=10.0.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", + "node_modules/micro-packed/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", "dev": true, - "dependencies": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "engines": { - "node": ">=4.5" + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/then-request": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", - "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "dev": true, - "peer": true, - "dependencies": { - "@types/concat-stream": "^1.6.0", - "@types/form-data": "0.0.33", - "@types/node": "^8.0.0", - "@types/qs": "^6.2.31", - "caseless": "~0.12.0", - "concat-stream": "^1.6.0", - "form-data": "^2.2.0", - "http-basic": "^8.1.1", - "http-response-object": "^3.0.1", - "promise": "^8.0.0", - "qs": "^6.4.0" - }, + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/then-request/node_modules/@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", "dev": true, - "peer": true + "license": "MIT" }, - "node_modules/then-request/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "peer": true, + "license": "ISC", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 0.12" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "peer": true, - "dependencies": { - "readable-stream": "3" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" + "license": "MIT", + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" }, "engines": { - "node": ">=0.6.0" + "node": ">=10" } }, - "node_modules/to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, "engines": { - "node": ">=6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ndjson": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-2.0.0.tgz", + "integrity": "sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "is-number": "^7.0.0" + "json-stringify-safe": "^5.0.1", + "minimist": "^1.2.5", + "readable-stream": "^3.6.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "bin": { + "ndjson": "cli.js" }, "engines": { - "node": ">=8.0" + "node": ">=10" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" + "wrappy": "1" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", "dev": true, - "peer": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } + "license": "MIT", + "engines": { + "node": ">=12.20" } }, - "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "peer": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { - "node": ">=0.3.1" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tslint": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", - "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", - "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.13.0", - "tsutils": "^2.29.0" - }, - "bin": { - "tslint": "bin/tslint" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=4.8.0" + "node": ">=10" }, - "peerDependencies": { - "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tslint-config-prettier": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", - "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, - "bin": { - "tslint-config-prettier-check": "bin/check.js" - }, + "license": "MIT", "engines": { - "node": ">=4.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tslint/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" }, "engines": { - "node": ">=4" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tslint/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "license": "BlueOak-1.0.0" }, - "node_modules/tslint/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "callsites": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/tslint/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/tslint/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/tslint/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/tslint/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, "engines": { - "node": ">=0.3.1" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tslint/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=8" } }, - "node_modules/tslint/node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/tslint/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/tslint/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tslint/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "bin": { - "semver": "bin/semver" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/tslint/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 14.16" } }, - "node_modules/tsort": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tsort/-/tsort-0.0.1.tgz", - "integrity": "sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==", - "dev": true + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, - "node_modules/tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "peerDependencies": { - "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" }, "engines": { - "node": "*" + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "dev": true - }, - "node_modules/tweetnacl-util": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz", - "integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==", - "dev": true - }, - "node_modules/type": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", - "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", - "dev": true - }, - "node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, + "license": "MIT", "dependencies": { - "prelude-ls": "~1.1.2" + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 6" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -10609,1311 +4047,1155 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" + "safe-buffer": "^5.1.0" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" }, - "engines": { - "node": ">= 0.4" + "bin": { + "rc": "cli.js" } }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true, - "peer": true - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", "dev": true, + "license": "MIT", "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, "engines": { - "node": ">=12.20" + "node": ">=6" } }, - "node_modules/uglify-js": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.18.0.tgz", - "integrity": "sha512-SyVVbcNBCk0dzr9XL/R/ySrmYf0s372K6/hFklzgcp2lBFyXtw4I7BOdDjlLhE1aVqaI/SHWXWmYdlZxuyF38A==", + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", "dev": true, - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" }, "engines": { - "node": ">=0.8.0" + "node": ">=14" } }, - "node_modules/ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", - "dev": true - }, - "node_modules/undici": { - "version": "5.28.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", - "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, + "license": "MIT", "engines": { - "node": ">=14.0" + "node": ">=0.10.0" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/unfetch": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", - "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", - "dev": true + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true, - "peer": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": ">=4" } }, - "node_modules/unpipe": { + "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, - "engines": { - "node": ">= 0.8" + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, - "dependencies": { - "punycode": "^2.1.0" + "license": "MIT", + "engines": { + "node": ">=10" } }, - "node_modules/url-parse-lax": { + "node_modules/responselike": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "dev": true, + "license": "MIT", "dependencies": { - "prepend-http": "^2.0.0" + "lowercase-keys": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/url-set-query": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-set-query/-/url-set-query-1.0.0.tgz", - "integrity": "sha512-3AChu4NiXquPfeckE5R5cGdiHCMWJx1dwCWOmWIL4KHAziJNOFIYJlpGFeKDvwLPHovZRCxK3cYlwzqI9Vp+Gg==", - "dev": true + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "node-gyp-build": "^4.3.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=6.14.2" + "node": ">=10" } }, - "node_modules/utf8": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", - "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", - "dev": true - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" + "randombytes": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4.0" + "node": ">=8" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "bin": { - "uuid": "dist/bin/uuid" + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true, - "peer": true + "license": "MIT" }, - "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, "engines": { - "node": ">= 0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/varint": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", - "integrity": "sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==", - "dev": true - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 0.8" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "node_modules/solhint": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-5.2.0.tgz", + "integrity": "sha512-9NZC1zt+O2K7zEZOhTT9rFeB6GdxC6qTX5pWX70RaQoflR9RejJQUC+/19LNi+e7K9Ptb4k7XAWO9wY5mkprHg==", "dev": true, - "engines": [ - "node >=0.6.0" - ], + "license": "MIT", "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "@solidity-parser/parser": "^0.20.0", + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "antlr4": "^4.13.1-patch-1", + "ast-parents": "^0.0.1", + "better-ajv-errors": "^2.0.2", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "fs-extra": "^11.1.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "latest-version": "^7.0.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "semver": "^7.5.2", + "strip-ansi": "^6.0.1", + "table": "^6.8.1", + "text-table": "^0.2.0" + }, + "bin": { + "solhint": "solhint.js" + }, + "optionalDependencies": { + "prettier": "^2.8.3" } }, - "node_modules/viem": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.17.3.tgz", - "integrity": "sha512-FY/1uBQWfko4Esy8mU1RamvL64TLy91LZwFyQJ20E6AI3vTTEOctWfSn0pkMKa3okq4Gxs5dJE7q1hmWOQ7xcw==", + "node_modules/solhint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "peer": true, + "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "1.10.0", - "@noble/curves": "1.4.0", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0", - "abitype": "1.0.5", - "isows": "1.0.4", - "ws": "8.17.1" + "color-convert": "^2.0.1" }, - "peerDependencies": { - "typescript": ">=5.0.4" + "engines": { + "node": ">=8" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/viem/node_modules/@adraffy/ens-normalize": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", - "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==", + "node_modules/solhint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "peer": true + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "node_modules/viem/node_modules/@noble/curves": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", - "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "node_modules/solhint/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "@noble/hashes": "1.4.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=14.14" } }, - "node_modules/viem/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "node_modules/solhint/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "peer": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, "engines": { - "node": ">= 16" + "node": ">=12" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/viem/node_modules/abitype": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.5.tgz", - "integrity": "sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==", + "node_modules/solhint/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/web3": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.7.3.tgz", - "integrity": "sha512-UgBvQnKIXncGYzsiGacaiHtm0xzQ/JtGqcSO/ddzQHYxnNuwI72j1Pb4gskztLYihizV9qPNQYHMSCiBlStI9A==", + "node_modules/solhint/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, - "hasInstallScript": true, + "license": "ISC", "dependencies": { - "web3-bzz": "1.7.3", - "web3-core": "1.7.3", - "web3-eth": "1.7.3", - "web3-eth-personal": "1.7.3", - "web3-net": "1.7.3", - "web3-shh": "1.7.3", - "web3-utils": "1.7.3" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=10" } }, - "node_modules/web3-bzz": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.7.3.tgz", - "integrity": "sha512-y2i2IW0MfSqFc1JBhBSQ59Ts9xE30hhxSmLS13jLKWzie24/An5dnoGarp2rFAy20tevJu1zJVPYrEl14jiL5w==", + "node_modules/solhint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "dependencies": { - "@types/node": "^12.12.6", - "got": "9.6.0", - "swarm-js": "^0.1.40" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-bzz/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true + "node_modules/solhint/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } }, - "node_modules/web3-core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.7.3.tgz", - "integrity": "sha512-4RNxueGyevD1XSjdHE57vz/YWRHybpcd3wfQS33fgMyHZBVLFDNwhn+4dX4BeofVlK/9/cmPAokLfBUStZMLdw==", + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "dev": true, + "license": "ISC", "dependencies": { - "@types/bn.js": "^4.11.5", - "@types/node": "^12.12.6", - "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.7.3", - "web3-core-method": "1.7.3", - "web3-core-requestmanager": "1.7.3", - "web3-utils": "1.7.3" - }, - "engines": { - "node": ">=8.0.0" + "readable-stream": "^3.0.0" } }, - "node_modules/web3-core-helpers": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.7.3.tgz", - "integrity": "sha512-qS2t6UKLhRV/6C7OFHtMeoHphkcA+CKUr2vfpxy4hubs3+Nj28K9pgiqFuvZiXmtEEwIAE2A28GBOC3RdcSuFg==", + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "license": "MIT", "dependencies": { - "web3-eth-iban": "1.7.3", - "web3-utils": "1.7.3" - }, - "engines": { - "node": ">=8.0.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/web3-core-helpers/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", + "dev": true, + "license": "WTFPL OR MIT", + "peer": true }, - "node_modules/web3-core-helpers/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/web3-core-method": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.7.3.tgz", - "integrity": "sha512-SeF8YL/NVFbj/ddwLhJeS0io8y7wXaPYA2AVT0h2C2ESYkpvOtQmyw2Bc3aXxBmBErKcbOJjE2ABOKdUmLSmMA==", + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "@ethersproject/transactions": "^5.0.0-beta.135", - "web3-core-helpers": "1.7.3", - "web3-core-promievent": "1.7.3", - "web3-core-subscriptions": "1.7.3", - "web3-utils": "1.7.3" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-core-method/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "node_modules/web3-core-method/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/web3-core-promievent": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.7.3.tgz", - "integrity": "sha512-+mcfNJLP8h2JqcL/UdMGdRVfTdm+bsoLzAFtLpazE4u9kU7yJUgMMAqnK59fKD3Zpke3DjaUJKwz1TyiGM5wig==", + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { - "eventemitter3": "4.0.4" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/web3-core-requestmanager": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.7.3.tgz", - "integrity": "sha512-bC+jeOjPbagZi2IuL1J5d44f3zfPcgX+GWYUpE9vicNkPUxFBWRG+olhMo7L+BIcD57cTmukDlnz+1xBULAjFg==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { - "util": "^0.12.0", - "web3-core-helpers": "1.7.3", - "web3-providers-http": "1.7.3", - "web3-providers-ipc": "1.7.3", - "web3-providers-ws": "1.7.3" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-core-subscriptions": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.7.3.tgz", - "integrity": "sha512-/i1ZCLW3SDxEs5mu7HW8KL4Vq7x4/fDXY+yf/vPoDljlpvcLEOnI8y9r7om+0kYwvuTlM6DUHHafvW0221TyRQ==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.7.3" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-core/node_modules/@types/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "dependencies": { - "@types/node": "*" + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/web3-core/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, - "node_modules/web3-core/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-core/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/web3-eth": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.7.3.tgz", - "integrity": "sha512-BCIRMPwaMlTCbswXyGT6jj9chCh9RirbDFkPtvqozfQ73HGW7kP78TXXf9+Xdo1GjutQfxi/fQ9yPdxtDJEpDA==", + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "web3-core": "1.7.3", - "web3-core-helpers": "1.7.3", - "web3-core-method": "1.7.3", - "web3-core-subscriptions": "1.7.3", - "web3-eth-abi": "1.7.3", - "web3-eth-accounts": "1.7.3", - "web3-eth-contract": "1.7.3", - "web3-eth-ens": "1.7.3", - "web3-eth-iban": "1.7.3", - "web3-eth-personal": "1.7.3", - "web3-net": "1.7.3", - "web3-utils": "1.7.3" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" } }, - "node_modules/web3-eth-abi": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.7.3.tgz", - "integrity": "sha512-ZlD8DrJro0ocnbZViZpAoMX44x5aYAb73u2tMq557rMmpiluZNnhcCYF/NnVMy6UIkn7SF/qEA45GXA1ne6Tnw==", + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "@ethersproject/abi": "5.0.7", - "web3-utils": "1.7.3" + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" }, "engines": { "node": ">=8.0.0" } }, - "node_modules/web3-eth-abi/node_modules/@ethersproject/abi": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.0.7.tgz", - "integrity": "sha512-Cqktk+hSIckwP/W8O47Eef60VwmoSC/L3lY0+dIBhQPCNn9E4V7rwmm2aFrNRRDJfFlGuZ1khkQUOc3oBX+niw==", + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", "dev": true, - "dependencies": { - "@ethersproject/address": "^5.0.4", - "@ethersproject/bignumber": "^5.0.7", - "@ethersproject/bytes": "^5.0.4", - "@ethersproject/constants": "^5.0.4", - "@ethersproject/hash": "^5.0.4", - "@ethersproject/keccak256": "^5.0.3", - "@ethersproject/logger": "^5.0.5", - "@ethersproject/properties": "^5.0.3", - "@ethersproject/strings": "^5.0.4" + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" } }, - "node_modules/web3-eth-abi/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-eth-abi/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-eth-accounts": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.7.3.tgz", - "integrity": "sha512-aDaWjW1oJeh0LeSGRVyEBiTe/UD2/cMY4dD6pQYa8dOhwgMtNQjxIQ7kacBBXe7ZKhjbIFZDhvXN4mjXZ82R2Q==", + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { - "@ethereumjs/common": "^2.5.0", - "@ethereumjs/tx": "^3.3.2", - "crypto-browserify": "3.12.0", - "eth-lib": "0.2.8", - "ethereumjs-util": "^7.0.10", - "scrypt-js": "^3.0.1", - "uuid": "3.3.2", - "web3-core": "1.7.3", - "web3-core-helpers": "1.7.3", - "web3-core-method": "1.7.3", - "web3-utils": "1.7.3" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": ">=8.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/web3-eth-accounts/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-eth-accounts/node_modules/eth-lib": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz", - "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==", + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "dependencies": { - "bn.js": "^4.11.6", - "elliptic": "^6.4.0", - "xhr-request-promise": "^0.1.2" - } + "license": "MIT" }, - "node_modules/web3-eth-accounts/node_modules/uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "bin": { - "uuid": "bin/uuid" - } + "license": "MIT" }, - "node_modules/web3-eth-accounts/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-eth-contract": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.7.3.tgz", - "integrity": "sha512-7mjkLxCNMWlQrlfM/MmNnlKRHwFk5XrZcbndoMt3KejcqDP6dPHi2PZLutEcw07n/Sk8OMpSamyF3QiGfmyRxw==", + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "dependencies": { - "@types/bn.js": "^4.11.5", - "web3-core": "1.7.3", - "web3-core-helpers": "1.7.3", - "web3-core-method": "1.7.3", - "web3-core-promievent": "1.7.3", - "web3-core-subscriptions": "1.7.3", - "web3-eth-abi": "1.7.3", - "web3-utils": "1.7.3" - }, - "engines": { - "node": ">=8.0.0" - } + "license": "MIT" }, - "node_modules/web3-eth-contract/node_modules/@types/bn.js": { - "version": "4.11.6", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz", - "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==", + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, + "license": "MIT", "dependencies": { - "@types/node": "*" + "readable-stream": "3" } }, - "node_modules/web3-eth-contract/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-eth-contract/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/ts-command-line-args": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", + "integrity": "sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==", "dev": true, + "license": "ISC", + "peer": true, "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "chalk": "^4.1.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.1.0", + "string-format": "^2.0.0" }, - "engines": { - "node": ">=8.0.0" + "bin": { + "write-markdown": "dist/write-markdown.js" } }, - "node_modules/web3-eth-ens": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.7.3.tgz", - "integrity": "sha512-q7+hFGHIc0mBI3LwgRVcLCQmp6GItsWgUtEZ5bjwdjOnJdbjYddm7PO9RDcTDQ6LIr7hqYaY4WTRnDHZ6BEt5Q==", + "node_modules/ts-command-line-args/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "content-hash": "^2.5.2", - "eth-ens-namehash": "2.0.8", - "web3-core": "1.7.3", - "web3-core-helpers": "1.7.3", - "web3-core-promievent": "1.7.3", - "web3-eth-abi": "1.7.3", - "web3-eth-contract": "1.7.3", - "web3-utils": "1.7.3" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/web3-eth-ens/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-eth-ens/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "node": ">=8" }, - "engines": { - "node": ">=8.0.0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/web3-eth-iban": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.7.3.tgz", - "integrity": "sha512-1GPVWgajwhh7g53mmYDD1YxcftQniIixMiRfOqlnA1w0mFGrTbCoPeVaSQ3XtSf+rYehNJIZAUeDBnONVjXXmg==", + "node_modules/ts-command-line-args/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "bn.js": "^4.11.9", - "web3-utils": "1.7.3" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/web3-eth-iban/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-eth-iban/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "node": ">=10" }, - "engines": { - "node": ">=8.0.0" + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/web3-eth-personal": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.7.3.tgz", - "integrity": "sha512-iTLz2OYzEsJj2qGE4iXC1Gw+KZN924fTAl0ESBFs2VmRhvVaM7GFqZz/wx7/XESl3GVxGxlRje3gNK0oGIoYYQ==", + "node_modules/ts-command-line-args/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "@types/node": "^12.12.6", - "web3-core": "1.7.3", - "web3-core-helpers": "1.7.3", - "web3-core-method": "1.7.3", - "web3-net": "1.7.3", - "web3-utils": "1.7.3" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-eth-personal/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, - "node_modules/web3-eth-personal/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-eth-personal/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" + "license": "MIT", + "peer": true, + "peerDependencies": { + "typescript": ">=3.7.0" } }, - "node_modules/web3-eth/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" }, - "node_modules/web3-eth/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, + "license": "MIT", "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">=8.0.0" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, - "node_modules/web3-net": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.7.3.tgz", - "integrity": "sha512-zAByK0Qrr71k9XW0Adtn+EOuhS9bt77vhBO6epAeQ2/VKl8rCGLAwrl3GbeEl7kWa8s/su72cjI5OetG7cYR0g==", + "node_modules/typechain": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-8.3.2.tgz", + "integrity": "sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "web3-core": "1.7.3", - "web3-core-method": "1.7.3", - "web3-utils": "1.7.3" + "@types/prettier": "^2.1.1", + "debug": "^4.3.1", + "fs-extra": "^7.0.0", + "glob": "7.1.7", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.3.1", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" }, - "engines": { - "node": ">=8.0.0" + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.3.0" } }, - "node_modules/web3-net/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/web3-net/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/typechain/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, - "engines": { - "node": ">=8.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/web3-providers-http": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.7.3.tgz", - "integrity": "sha512-TQJfMsDQ5Uq9zGMYlu7azx1L7EvxW+Llks3MaWn3cazzr5tnrDbGh6V17x6LN4t8tFDHWx0rYKr3mDPqyTjOZw==", + "node_modules/typechain/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", + "peer": true, "dependencies": { - "web3-core-helpers": "1.7.3", - "xhr2-cookies": "1.1.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=8.0.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/web3-providers-ipc": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.7.3.tgz", - "integrity": "sha512-Z4EGdLKzz6I1Bw+VcSyqVN4EJiT2uAro48Am1eRvxUi4vktGoZtge1ixiyfrRIVb6nPe7KnTFl30eQBtMqS0zA==", + "node_modules/typechain/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", + "peer": true, "dependencies": { - "oboe": "2.1.5", - "web3-core-helpers": "1.7.3" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8.0.0" + "node": "*" } }, - "node_modules/web3-providers-ws": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.7.3.tgz", - "integrity": "sha512-PpykGbkkkKtxPgv7U4ny4UhnkqSZDfLgBEvFTXuXLAngbX/qdgfYkhIuz3MiGplfL7Yh93SQw3xDjImXmn2Rgw==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "dependencies": { - "eventemitter3": "4.0.4", - "web3-core-helpers": "1.7.3", - "websocket": "^1.0.32" + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=8.0.0" + "node": ">=14.17" } }, - "node_modules/web3-shh": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.7.3.tgz", - "integrity": "sha512-bQTSKkyG7GkuULdZInJ0osHjnmkHij9tAySibpev1XjYdjLiQnd0J9YGF4HjvxoG3glNROpuCyTaRLrsLwaZuw==", + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "web3-core": "1.7.3", - "web3-core-method": "1.7.3", - "web3-core-subscriptions": "1.7.3", - "web3-net": "1.7.3" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">=8.0.0" + "node": ">=8" } }, - "node_modules/web3-utils": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", - "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", + "node_modules/undici": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", + "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", "dev": true, - "dependencies": { - "@ethereumjs/util": "^8.1.0", - "bn.js": "^5.2.1", - "ethereum-bloom-filters": "^1.0.6", - "ethereum-cryptography": "^2.1.2", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": ">=18.17" } }, - "node_modules/web3-utils/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } + "license": "MIT" }, - "node_modules/web3-utils/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, + "license": "MIT", + "peer": true, "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "node": ">= 4.0.0" } }, - "node_modules/web3-utils/node_modules/ethereum-cryptography": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", - "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@noble/curves": "1.4.2", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0" + "punycode": "^2.1.0" } }, - "node_modules/web3/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" }, - "node_modules/web3/node_modules/web3-utils": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.7.3.tgz", - "integrity": "sha512-g6nQgvb/bUpVUIxJE+ezVN+rYwYmlFyMvMIRSuqpi1dk6ApDD00YNArrk7sPcZnjvxOJ76813Xs2vIN2rgh4lg==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { - "bn.js": "^4.11.9", - "ethereum-bloom-filters": "^1.0.6", - "ethereumjs-util": "^7.1.0", - "ethjs-unit": "0.1.6", - "number-to-bn": "1.7.0", - "randombytes": "^2.1.0", - "utf8": "3.0.0" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" }, "engines": { - "node": ">=8.0.0" + "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/websocket": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", - "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { - "bufferutil": "^4.0.1", - "debug": "^2.2.0", - "es5-ext": "^0.10.63", - "typedarray-to-buffer": "^3.1.5", - "utf-8-validate": "^5.0.2", - "yaeti": "^0.0.6" + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" }, "engines": { - "node": ">=4.0.0" + "node": ">=8.0.0" } }, - "node_modules/websocket/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", "dev": true, - "dependencies": { - "ms": "2.0.0" + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" } }, - "node_modules/websocket/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } + "license": "Apache-2.0" }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, - "bin": { - "which": "bin/which" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" + "color-convert": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true - }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "peer": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -11930,109 +5212,43 @@ } } }, - "node_modules/xhr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", - "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", - "dev": true, - "dependencies": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/xhr-request": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/xhr-request/-/xhr-request-1.1.0.tgz", - "integrity": "sha512-Y7qzEaR3FDtL3fP30k9wO/e+FBnBByZeybKOhASsGP30NIkRAAkKD/sCnLvgEfAIEC1rcmK7YG8f4oEnIrrWzA==", - "dev": true, - "dependencies": { - "buffer-to-arraybuffer": "^0.0.5", - "object-assign": "^4.1.1", - "query-string": "^5.0.1", - "simple-get": "^2.7.0", - "timed-out": "^4.0.1", - "url-set-query": "^1.0.0", - "xhr": "^2.0.4" - } - }, - "node_modules/xhr-request-promise": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/xhr-request-promise/-/xhr-request-promise-0.1.3.tgz", - "integrity": "sha512-YUBytBsuwgitWtdRzXDDkWAXzhdGB8bYm0sSzMPZT7Z2MBjMSTHFsyCT1yCRATY+XC69DUrQraRAEgcoCRaIPg==", - "dev": true, - "dependencies": { - "xhr-request": "^1.1.0" - } - }, - "node_modules/xhr2-cookies": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/xhr2-cookies/-/xhr2-cookies-1.1.0.tgz", - "integrity": "sha512-hjXUA6q+jl/bd8ADHcVfFsSPIf+tyLIjuO9TwJC9WI6JP2zKcS7C+p56I9kCLLsaCiNT035iYvEUUzdEFj/8+g==", - "dev": true, - "dependencies": { - "cookiejar": "^2.1.1" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", - "dev": true, - "engines": { - "node": ">=0.10.32" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-unparser": { @@ -12040,6 +5256,7 @@ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, + "license": "MIT", "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -12050,14 +5267,26 @@ "node": ">=10" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "peer": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/yocto-queue": { @@ -12065,12 +5294,23 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index a248d6ba1..c8a8b7a7a 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "ssv-network", - "version": "1.2.0", + "version": "2.0.0", "description": "Solidity smart contracts for the SSV Network", "author": "SSV.Network", + "type": "module", "repository": { "type": "git", - "url": "https://github.com/bloxapp/ssv-network.git" + "url": "https://github.com/ssvlabs/ssv-network.git" }, "license": "MIT", "keywords": [ @@ -20,36 +21,61 @@ "!contracts/**/mocks/**", "!contracts/**/test/**", "!contracts/**/upgrades/**", - "abis/*.json", - "tasks/", + "abis/CSSVToken.json", + "abis/ISSVStaking.json", + "abis/SSVClusters.json", + "abis/SSVDAO.json", + "abis/SSVNetwork.json", + "abis/SSVNetworkViews.json", + "abis/SSVOperators.json", + "abis/SSVOperatorsWhitelist.json", + "abis/SSVStaking.json", + "abis/SSVToken.json", + "abis/SSVValidators.json", + "abis/SSVViews.json", "docs/", "README.md", + "RELEASE_NOTES.md", "LICENSE", "CHANGELOG.md" ], "scripts": { "build": "npx hardhat compile", - "test": "npx hardhat test --parallel", - "test-forked": "FORK_TESTING_ENABLED=true npx hardhat test test-forked/*.ts", + "test": "npx hardhat test", + "test:gas": "npx hardhat test --gas-stats", + "test:unit": "npx hardhat test test/unit/**/*.test.ts", + "test:unit:gas": "npx hardhat test test/unit/**/*.test.ts --gas-stats", + "test:integration": "npx hardhat test test/integration/*.test.ts", + "test:e2e": "npx hardhat test test/e2e/**/*.test.ts", + "test:integration:gas": "npx hardhat test test/integration/*.test.ts --gas-stats", + "test-forked": "FORK_TESTING_ENABLED=true npx hardhat test test/forked/**/*.test.ts", + "gas:report": "REPORT_GAS=true npx hardhat test", + "gas:compare": "npx tsx scripts/gas-compare.ts", + "gas:ci": "REPORT_GAS=true NO_GAS_ENFORCE=1 npx hardhat test && npx tsx scripts/gas-compare.ts", "lint": "eslint . --ext .ts", "lint:fix": "eslint --fix . --ext .ts", - "solidity-coverage": "SOLIDITY_COVERAGE=true NO_GAS_ENFORCE=1 npx hardhat coverage", - "slither": "slither contracts --solc-remaps @openzeppelin=node_modules/@openzeppelin", + "solidity-coverage": "NO_GAS_ENFORCE=1 npx hardhat test --coverage", + "slither": "npx hardhat compile --force && slither . --hardhat-ignore-compile --solc-remaps @openzeppelin=node_modules/@openzeppelin --filter-paths \"contracts/test/\" --exclude-informational --exclude-dependencies", "size-contracts": "npx hardhat size-contracts" }, "devDependencies": { - "@nomicfoundation/edr": "^0.3.4", - "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", - "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", + "@nomicfoundation/hardhat-ethers": "^4.0.3", + "@nomicfoundation/hardhat-ethers-chai-matchers": "^3.0.2", + "@nomicfoundation/hardhat-ignition": "^3.0.6", + "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.2", + "@nomicfoundation/hardhat-verify": "^3.0.8", "@openzeppelin/contracts": "^4.9.6", "@openzeppelin/contracts-upgradeable": "^4.9.6", - "@openzeppelin/hardhat-upgrades": "^3.0.5", - "dotenv": "^16.4.5", - "hardhat": "^2.22.4", - "hardhat-abi-exporter": "^2.10.1", - "hardhat-contract-sizer": "^2.10.0", - "solidity-coverage": "^0.8.12", - "ssv-keys": "github:bloxapp/ssv-keys#v1.0.4" + "@types/chai": "^4.3.20", + "@types/chai-as-promised": "^8.0.2", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.2", + "chai": "^5.3.3", + "dotenv": "^17.2.3", + "ethers": "^6.16.0", + "hardhat": "^3.1.0", + "mocha": "^11.7.5", + "solhint": "^5.0.0", + "tsx": "^4.19.0" } } diff --git a/scripts/attach-module.ts b/scripts/attach-module.ts new file mode 100644 index 000000000..90198900a --- /dev/null +++ b/scripts/attach-module.ts @@ -0,0 +1,74 @@ +import { readFile, realpath, writeFile } from "node:fs/promises"; +import { attachModule, getEthers, parseArg } from "./common/helpers.ts"; +import { + type ModuleAddressesConfig, + type ModuleName, + type UpgradeConfig, + parseOptionalArg, + requireAddress, + resolveConfigPath, + resolveDeployResultPath, + resolveNetworkFromEnv, +} from "./common/config.ts"; + +type AttachModuleDeployResult = { + modules?: ModuleAddressesConfig; + updatedAt?: string; + [key: string]: unknown; +}; + +async function updateLatestDeployResult( + env: string, + moduleName: ModuleName, + moduleAddress: string +): Promise { + const latestResultPath = resolveDeployResultPath(env); + const versionedResultPath = await realpath(latestResultPath).catch(() => latestResultPath); + const raw = await readFile(versionedResultPath, "utf8"); + const deployResult = JSON.parse(raw) as AttachModuleDeployResult; + + deployResult.modules = { + ...(deployResult.modules ?? {}), + [moduleName]: moduleAddress, + }; + deployResult.updatedAt = new Date().toISOString(); + + await writeFile(versionedResultPath, `${JSON.stringify(deployResult, null, 2)}\n`, "utf8"); + return versionedResultPath; +} + +async function main() { + const envFlag = parseArg("env"); + const moduleName = parseArg("module") as ModuleName; + const moduleAddress = requireAddress(parseArg("module-address"), "module-address"); + const configPath = resolveConfigPath(envFlag); + const rawConfig = await readFile(configPath, "utf8"); + const config = JSON.parse(rawConfig) as UpgradeConfig; + + const targetNetwork = parseOptionalArg("network") ?? resolveNetworkFromEnv(envFlag) ?? "local"; + const proxyAddress = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + + console.log(`Environment: ${envFlag}`); + console.log(`Resolved SSVNetwork proxy from config: ${proxyAddress}`); + + const ethers = await getEthers(targetNetwork); + const proxyCode = await ethers.provider.getCode(proxyAddress); + if (proxyCode === "0x") { + throw new Error(`No contract code at proxy ${proxyAddress} on ${targetNetwork}`); + } + + const moduleCode = await ethers.provider.getCode(moduleAddress); + if (moduleCode === "0x") { + throw new Error(`No contract code at module ${moduleAddress} on ${targetNetwork}`); + } + + await attachModule(ethers, proxyAddress, moduleName, moduleAddress); + + const updatedResultPath = await updateLatestDeployResult(envFlag, moduleName, moduleAddress); + console.log(`Updated deploy result: ${updatedResultPath}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/common/config.ts b/scripts/common/config.ts new file mode 100644 index 000000000..1d264b91b --- /dev/null +++ b/scripts/common/config.ts @@ -0,0 +1,423 @@ +import { readFile, symlink, unlink } from "node:fs/promises"; +import { resolve, join, basename, dirname } from "node:path"; +import { isAddress } from "ethers"; +import { SSVModules } from "./modules.ts"; + +// ── Types ── + +export type ModuleName = keyof typeof SSVModules; +export type ModuleAddresses = Record; +export type OracleEntry = { id: number; address: string }; +export type OraclesConfig = Record | OracleEntry[]; +export type ModuleAddressesConfig = Partial>; + +export type DeployResultJson = { + ssvNetworkStakingUpgradeImplementation?: string; + ssvNetworkViewsImplementation?: string; + cssvToken?: string; + modules?: ModuleAddressesConfig; + targetNetwork?: string; + forkBlockNumber?: number; + deployBlockNumber?: number; + chainId?: string; + deployer?: string; + updatedAt?: string; +}; + +export type UpgradeConfig = { + currentVersion: string; + targetVersion: string; + skipInitializer?: boolean; + owner?: string; + viewsOwner?: string; + ssvNetworkProxy: string; + ssvNetworkViews: string; + ssvToken: string; + cooldownDuration?: string | number; + upgradeTimestamp?: string | number; + defaultOracleIds?: number[]; + quorumBps?: number; + oracles?: OraclesConfig; + cssvToken?: string; + deployBlockNumber?: number; + modules?: ModuleAddressesConfig; + deployments?: DeployResultJson; + protocolParams?: ProtocolParams; + // Legacy flat fields (supported for backward compat, prefer protocolParams) + networkFeeEth?: string | number; + networkFeeSSV?: string | number; + maxOperatorEthFee?: string | number; + minOperatorEthFee?: string | number; + operatorFeeIncreaseLimit?: string | number; + declareOperatorFeePeriod?: string | number; + executeOperatorFeePeriod?: string | number; + liquidationThresholdPeriod?: string | number; + liquidationThresholdPeriodSSV?: string | number; + minBlocksBetweenUpdates?: string | number; + minimumLiquidationCollateralEth?: string | number; + minimumLiquidationCollateralSSV?: string | number; + validatorsPerOperatorLimit?: string | number; + unstakeCooldownDuration?: string | number; + initialStakeAmount?: string | number; +}; + +export type ProtocolParams = { + networkFeeEth?: string | number; + networkFeeSSV?: string | number; + maxOperatorEthFee?: string | number; + minOperatorEthFee?: string | number; + operatorFeeIncreaseLimit?: string | number; + declareOperatorFeePeriod?: string | number; + executeOperatorFeePeriod?: string | number; + liquidationThresholdPeriod?: string | number; + liquidationThresholdPeriodSSV?: string | number; + minBlocksBetweenUpdates?: string | number; + minimumLiquidationCollateralEth?: string | number; + minimumLiquidationCollateralSSV?: string | number; + validatorsPerOperatorLimit?: string | number; + unstakeCooldownDuration?: string | number; +}; + +/** Resolved protocol params — merges nested protocolParams over legacy flat fields. */ +export type ResolvedProtocolParams = { + networkFeeEth?: bigint; + networkFeeSSV?: bigint; + maxOperatorEthFee?: bigint; + minOperatorEthFee?: bigint; + operatorFeeIncreaseLimit?: bigint; + declareOperatorFeePeriod?: bigint; + executeOperatorFeePeriod?: bigint; + liquidationThresholdPeriod?: bigint; + liquidationThresholdPeriodSSV?: bigint; + minBlocksBetweenUpdates?: bigint; + minimumLiquidationCollateralEth?: bigint; + minimumLiquidationCollateralSSV?: bigint; + validatorsPerOperatorLimit?: bigint; + unstakeCooldownDuration?: bigint; +}; + +// ── Constants ── + +export const MODULE_ORDER: ModuleName[] = [ + "SSVOperators", + "SSVClusters", + "SSVDAO", + "SSVViews", + "SSVOperatorsWhitelist", + "SSVStaking", + "SSVValidators", +]; + +export const LOCAL_FORK_RPC_URL = "http://127.0.0.1:8545"; +export const DEPLOYMENTS_DIR = resolve(process.cwd(), "deployments"); + +// Default cooldown: 7 days in seconds +const DEFAULT_COOLDOWN = 7n * 24n * 60n * 60n; + +/** + * Resolves the Hardhat network name from an --env flag. + * Returns undefined if the env doesn't map to a known network. + */ +export function resolveNetworkFromEnv(env: string | undefined): string | undefined { + if (!env) return undefined; + if (env === "mainnet") return "mainnet"; + if (env === "local") return "local"; + if (env.startsWith("hoodi")) return "hoodi"; + return undefined; +} + +// ── Parsing helpers ── + +export function parseUint(value: unknown, label: string): bigint | undefined { + if (value === undefined || value === null) return undefined; + if (typeof value === "number") { + if (!Number.isInteger(value) || value < 0) { + throw new Error(`Invalid ${label} (must be a non-negative integer)`); + } + return BigInt(value); + } + if (typeof value === "string") { + if (!/^\d+$/.test(value)) { + throw new Error(`Invalid ${label} (string must be an integer)`); + } + return BigInt(value); + } + throw new Error(`Invalid ${label} (expected string or number)`); +} + +export function parseQuorum(value: unknown): number | undefined { + if (value === undefined || value === null) return undefined; + if (typeof value !== "number") { + throw new Error("Invalid quorumBps (must be a number)"); + } + if (!Number.isInteger(value) || value < 0 || value > 10_000) { + throw new Error("Invalid quorumBps (must be 0..10000)"); + } + return value; +} + +export function requireAddress(value: string, label: string): string { + if (!isAddress(value)) { + throw new Error(`Invalid ${label}: ${value}`); + } + return value; +} + +export function bigintToJsonNumberOrString(value: bigint): number | string { + if (value <= BigInt(Number.MAX_SAFE_INTEGER)) { + return Number(value); + } + return value.toString(); +} + +// ── CLI arg helpers ── + +export function parseOptionalArg(argName: string): string | undefined { + const index = process.argv.indexOf(`--${argName}`); + if (index === -1) return undefined; + const value = process.argv[index + 1]; + if (!value) throw new Error(`Missing value for --${argName}`); + return value; +} + +export function parseOptionalBooleanArg(argName: string, fallback: boolean): boolean { + const index = process.argv.indexOf(`--${argName}`); + if (index === -1) return fallback; + const next = process.argv[index + 1]; + if (!next || next.startsWith("--")) return true; + if (next === "true") return true; + if (next === "false") return false; + throw new Error(`Invalid --${argName} value: ${next}. Use true|false`); +} + +// ── Oracle helpers ── + +export function normalizeOracles(oracles: OraclesConfig | undefined): OracleEntry[] { + if (!oracles) return []; + + const source = Array.isArray(oracles) + ? oracles + : Object.entries(oracles).map(([id, address]) => ({ id: Number(id), address })); + + const seen = new Set(); + const normalized = source.map(({ id, address }) => { + if (!Number.isInteger(id) || id <= 0 || id > 0xffffffff) { + throw new Error(`Invalid oracle id: ${id}`); + } + if (!isAddress(address)) { + throw new Error(`Invalid oracle address: ${address}`); + } + if (seen.has(id)) { + throw new Error(`Duplicate oracle id: ${id}`); + } + seen.add(id); + return { id, address }; + }); + + return normalized.sort((a, b) => a.id - b.id); +} + +export function normalizeOracleIds(ids: number[]): [number, number, number, number] { + if (ids.length !== 4) { + throw new Error("defaultOracleIds must contain exactly 4 ids"); + } + + const validated = ids.map((id) => { + if (!Number.isInteger(id) || id <= 0 || id > 0xffffffff) { + throw new Error(`Invalid default oracle id: ${id}`); + } + return id; + }); + + return [validated[0], validated[1], validated[2], validated[3]]; +} + +export function resolveDefaultOracleIds( + config: UpgradeConfig, + oracles: OracleEntry[] +): [number, number, number, number] { + if (Array.isArray(config.defaultOracleIds) && config.defaultOracleIds.length > 0) { + return normalizeOracleIds(config.defaultOracleIds); + } + if (oracles.length > 0) { + return normalizeOracleIds(oracles.map((oracle) => oracle.id)); + } + const env = process.env.DEFAULT_ORACLE_IDS ?? "1,2,3,4"; + const parsed = env + .split(",") + .map((value) => Number(value.trim())) + .filter((value) => Number.isInteger(value) && value > 0 && value <= 0xffffffff); + return normalizeOracleIds(parsed); +} + +export function toOracleConfig(oracles: OracleEntry[]): Record { + return Object.fromEntries(oracles.map(({ id, address }) => [String(id), address])); +} + +// ── Config loading ── + +/** + * Resolves the config directory path from --env flag. + * --env mainnet -> deployments/mainnet/ + * --env hoodi-stage -> deployments/hoodi-stage/ + */ +export function resolveEnvDir(env: string): string { + return join(DEPLOYMENTS_DIR, env); +} + +export function resolveConfigPath(env: string): string { + return join(resolveEnvDir(env), "config.json"); +} + +export function resolveDeployResultPath(env: string): string { + return join(resolveEnvDir(env), "deploy-result.json"); +} + +export function resolveUpgradeResultPath(env: string): string { + return join(resolveEnvDir(env), "upgrade-result.json"); +} + +export function resolveVersionedDeployResultPath(env: string, version: string): string { + return join(resolveEnvDir(env), `deploy-result.${version}.json`); +} + +export function resolveVersionedUpgradeResultPath(env: string, version: string): string { + return join(resolveEnvDir(env), `upgrade-result.${version}.json`); +} + +/** + * Writes `versionedPath` as the target and updates the fixed-name symlink to point to it. + * The symlink uses a relative target so it stays valid if the directory is moved. + * Falls back to overwriting the fixed file directly if symlinks are unavailable. + */ +export async function updateLatestSymlink(versionedPath: string, fixedPath: string): Promise { + const relTarget = basename(versionedPath); + try { + await unlink(fixedPath); + } catch { + // ignore ENOENT — symlink/file didn't exist yet + } + try { + await symlink(relTarget, fixedPath); + } catch { + // symlinks unavailable (e.g. some CI environments) — leave the fixed file as-is + // The versioned file is the canonical record; the fixed name is convenience only + } +} + +/** + * Loads and parses config.json from the given env directory. + */ +export async function loadConfig(env: string): Promise { + const configPath = resolveConfigPath(env); + const raw = await readFile(configPath, "utf8"); + return JSON.parse(raw) as UpgradeConfig; +} + +/** + * Tries to load deploy-result.json. Returns undefined if not found. + */ +export async function loadDeployResult(env: string): Promise { + try { + const resultPath = resolveDeployResultPath(env); + const raw = await readFile(resultPath, "utf8"); + return JSON.parse(raw) as DeployResultJson; + } catch { + return undefined; + } +} + +/** + * Resolves protocol parameters, merging nested protocolParams over legacy flat fields. + * protocolParams takes precedence over flat fields when both exist. + */ +export function resolveProtocolParams(config: UpgradeConfig): ResolvedProtocolParams { + const pp = config.protocolParams ?? {}; + return { + networkFeeEth: parseUint(pp.networkFeeEth ?? config.networkFeeEth, "networkFeeEth"), + networkFeeSSV: parseUint(pp.networkFeeSSV ?? config.networkFeeSSV, "networkFeeSSV"), + maxOperatorEthFee: parseUint(pp.maxOperatorEthFee ?? config.maxOperatorEthFee, "maxOperatorEthFee"), + minOperatorEthFee: parseUint(pp.minOperatorEthFee ?? config.minOperatorEthFee, "minOperatorEthFee"), + operatorFeeIncreaseLimit: parseUint( + pp.operatorFeeIncreaseLimit ?? config.operatorFeeIncreaseLimit, + "operatorFeeIncreaseLimit" + ), + declareOperatorFeePeriod: parseUint( + pp.declareOperatorFeePeriod ?? config.declareOperatorFeePeriod, + "declareOperatorFeePeriod" + ), + executeOperatorFeePeriod: parseUint( + pp.executeOperatorFeePeriod ?? config.executeOperatorFeePeriod, + "executeOperatorFeePeriod" + ), + liquidationThresholdPeriod: parseUint( + pp.liquidationThresholdPeriod ?? config.liquidationThresholdPeriod, + "liquidationThresholdPeriod" + ), + liquidationThresholdPeriodSSV: parseUint( + pp.liquidationThresholdPeriodSSV ?? config.liquidationThresholdPeriodSSV, + "liquidationThresholdPeriodSSV" + ), + minBlocksBetweenUpdates: parseUint( + pp.minBlocksBetweenUpdates ?? config.minBlocksBetweenUpdates, + "minBlocksBetweenUpdates" + ), + minimumLiquidationCollateralEth: parseUint( + pp.minimumLiquidationCollateralEth ?? config.minimumLiquidationCollateralEth, + "minimumLiquidationCollateralEth" + ), + minimumLiquidationCollateralSSV: parseUint( + pp.minimumLiquidationCollateralSSV ?? config.minimumLiquidationCollateralSSV, + "minimumLiquidationCollateralSSV" + ), + validatorsPerOperatorLimit: parseUint( + pp.validatorsPerOperatorLimit ?? config.validatorsPerOperatorLimit, + "validatorsPerOperatorLimit" + ), + unstakeCooldownDuration: parseUint( + pp.unstakeCooldownDuration ?? config.unstakeCooldownDuration, + "unstakeCooldownDuration" + ), + }; +} + +/** + * Resolves the cooldown duration from config, falling back to the default 7 days. + */ +export function resolveCooldownDuration(config: UpgradeConfig): bigint { + return parseUint(config.cooldownDuration, "cooldownDuration") ?? DEFAULT_COOLDOWN; +} + +/** + * Resolves the upgrade timestamp from config, defaulting to 0. + */ +export function resolveUpgradeTimestamp(config: UpgradeConfig): bigint { + return parseUint(config.upgradeTimestamp, "upgradeTimestamp") ?? 0n; +} + +// ── Legacy config path resolution (for backward compat with --config flag) ── + +export function resolveDeployedConfigPath(initConfigPath: string, outputArg?: string): string { + if (outputArg) { + return resolve(outputArg); + } + if (initConfigPath.endsWith("-upgrade.config.json")) { + return initConfigPath.replace(/-upgrade\.config\.json$/, "-upgrade.result.json"); + } + if (initConfigPath.endsWith("-deploy.config.json")) { + return initConfigPath.replace(/-deploy\.config\.json$/, "-deploy.result.json"); + } + if (initConfigPath.endsWith(".result.json")) { + return initConfigPath; + } + if (initConfigPath.endsWith("-deployed.config.json")) { + return initConfigPath; + } + if (initConfigPath.endsWith(".config.json")) { + return initConfigPath.replace(/\.config\.json$/, ".result.json"); + } + if (initConfigPath.endsWith(".json")) { + return initConfigPath.replace(/\.json$/, ".result.json"); + } + return `${initConfigPath}.result.json`; +} diff --git a/scripts/common/export-abis.ts b/scripts/common/export-abis.ts new file mode 100644 index 000000000..2dbf6b687 --- /dev/null +++ b/scripts/common/export-abis.ts @@ -0,0 +1,58 @@ +import hre from "hardhat"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +async function main() { + const artifactsPath = hre.config.paths.artifacts; + const buildInfoDir = path.join(artifactsPath, "build-info"); + const abisDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "abis"); + + const skippedFolders = ["test", "deprecated", "upgrades", "libraries", "interfaces", "lib"]; + + if (fs.existsSync(abisDir)) { + fs.rmSync(abisDir, { recursive: true }); + } + fs.mkdirSync(abisDir); + + for (const file of fs.readdirSync(buildInfoDir)) { + const fullPath = path.join(buildInfoDir, file); + const buildInfo = JSON.parse(fs.readFileSync(fullPath, "utf8")); + + if ( + !buildInfo || + !buildInfo.output || + !buildInfo.output.contracts || + typeof buildInfo.output.contracts !== "object" + ) { + continue; + } + + const contracts = buildInfo.output.contracts; + + for (const fileName of Object.keys(contracts)) { + const shouldSkipFolder = skippedFolders.some(folder => fileName.includes(`/${folder}/`)); + + const isOpenZeppelin = fileName.includes("@openzeppelin/contracts"); + + if (shouldSkipFolder || isOpenZeppelin) continue; + + for (const contractName of Object.keys(contracts[fileName])) { + const c = contracts[fileName][contractName]; + + const abi = c?.abi; + if (!abi || abi.length === 0) continue; + + const abiPath = path.join(abisDir, `${contractName}.json`); + fs.writeFileSync(abiPath, JSON.stringify(abi, null, 2)); + } + } + } + + console.log("ABIs saved to Abis folder."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/common/fork-test.ts b/scripts/common/fork-test.ts new file mode 100644 index 000000000..3a02d7e37 --- /dev/null +++ b/scripts/common/fork-test.ts @@ -0,0 +1,144 @@ +import { Contract, JsonRpcProvider } from "ethers"; +import { LOCAL_FORK_RPC_URL } from "./config.ts"; + +export type ForkConfigFile = { + ssvNetworkProxy?: string; + ssvNetworkAddress?: string; + ssvNetworkViews?: string; + forkBlockNumber?: string | number; + deployments?: { + forkBlockNumber?: string | number; + }; + protocolParams?: { + networkFeeSSV?: string | number; + networkFeeEth?: string | number; + maxOperatorEthFee?: string | number; + minOperatorEthFee?: string | number; + operatorFeeIncreaseLimit?: string | number; + declareOperatorFeePeriod?: string | number; + executeOperatorFeePeriod?: string | number; + liquidationThresholdPeriod?: string | number; + liquidationThresholdPeriodSSV?: string | number; + minimumLiquidationCollateralEth?: string | number; + minimumLiquidationCollateralSSV?: string | number; + validatorsPerOperatorLimit?: string | number; + unstakeCooldownDuration?: string | number; + }; + // Legacy flat fields (backward compat with older result files) + networkFeeSSV?: string | number; + networkFeeEth?: string | number; + maxOperatorEthFee?: string | number; + minOperatorEthFee?: string | number; + operatorFeeIncreaseLimit?: string | number; + declareOperatorFeePeriod?: string | number; + executeOperatorFeePeriod?: string | number; + liquidationThresholdPeriod?: string | number; + liquidationThresholdPeriodSSV?: string | number; + minimumLiquidationCollateralEth?: string | number; + minimumLiquidationCollateralSSV?: string | number; + validatorsPerOperatorLimit?: string | number; + defaultOracleIds?: number[]; + unstakeCooldownDuration?: string | number; + cooldownDuration?: string | number; +}; + +export function toEnvValue(value: string | number | undefined): string | undefined { + if (value === undefined) return undefined; + return String(value); +} + +export function resolveSourceRpcUrl(): string { + return LOCAL_FORK_RPC_URL; +} + +export async function preflightSourceRpc(config: ForkConfigFile): Promise { + const sourceRpcUrl = resolveSourceRpcUrl(); + const viewsAddress = config.ssvNetworkViews; + const networkAddress = config.ssvNetworkProxy ?? config.ssvNetworkAddress; + + if (!viewsAddress || !networkAddress) { + throw new Error( + "Deployed config is missing ssvNetworkViews or ssvNetworkProxy/ssvNetworkAddress" + ); + } + + const provider = new JsonRpcProvider(sourceRpcUrl); + const viewsCode = await provider.getCode(viewsAddress); + const networkCode = await provider.getCode(networkAddress); + if (viewsCode === "0x") { + throw new Error(`No code at ssvNetworkViews=${viewsAddress} on source RPC ${sourceRpcUrl}`); + } + if (networkCode === "0x") { + throw new Error(`No code at ssvNetworkProxy=${networkAddress} on source RPC ${sourceRpcUrl}`); + } + + const views = new Contract( + viewsAddress, + [ + "function getVersion() view returns (string)", + "function getNetworkFee() view returns (uint256)", + "function getActiveOracleIds() view returns (uint32[4])", + ], + provider + ); + + try { + await views.getVersion(); + await views.getNetworkFee(); + await views.getActiveOracleIds(); + } catch (err: any) { + const block = await provider.getBlockNumber(); + const shortMessage = err?.shortMessage ?? err?.message ?? "unknown error"; + const data = err?.data ? ` data=${err.data}` : ""; + throw new Error( + `Source RPC preflight failed at block ${block} for SSVNetworkViews=${viewsAddress}. ` + + `Cannot read getVersion/getNetworkFee/getActiveOracleIds. ${shortMessage}${data}` + ); + } +} + +/** + * Builds the environment variables block for the forked test runner. + * Reads from protocolParams (preferred) with fallback to legacy flat fields. + */ +export function buildForkTestEnv( + config: ForkConfigFile, + opts: { + configPath: string; + forkNetwork: string; + useDeployedState: string; + strictDeployedState: string; + allowDeployedFallback: string; + noGasEnforce: string; + forkBlockNumber: string; + } +): Record { + const pp = config.protocolParams ?? {}; + return { + ...process.env, + RUN_FORK: "true", + FORK_TEST_NETWORK: opts.forkNetwork, + FORK_CONFIG_PATH: opts.configPath, + FORK_USE_DEPLOYED_STATE: opts.useDeployedState, + FORK_STRICT_DEPLOYED_STATE: opts.strictDeployedState, + FORK_ALLOW_DEPLOYED_FALLBACK: opts.allowDeployedFallback, + NO_GAS_ENFORCE: opts.noGasEnforce, + FORK_BLOCK_NUMBER: opts.forkBlockNumber, + FORK_NETWORK_FEE_ETH: toEnvValue(pp.networkFeeEth ?? config.networkFeeEth), + FORK_NETWORK_FEE_SSV: toEnvValue(pp.networkFeeSSV ?? config.networkFeeSSV), + FORK_MAX_OPERATOR_ETH_FEE: toEnvValue(pp.maxOperatorEthFee ?? config.maxOperatorEthFee), + FORK_MIN_OPERATOR_ETH_FEE: toEnvValue(pp.minOperatorEthFee ?? config.minOperatorEthFee), + FORK_OPERATOR_MAX_FEE_INCREASE: toEnvValue(pp.operatorFeeIncreaseLimit ?? config.operatorFeeIncreaseLimit), + FORK_DECLARE_OPERATOR_FEE_PERIOD: toEnvValue(pp.declareOperatorFeePeriod ?? config.declareOperatorFeePeriod), + FORK_EXECUTE_OPERATOR_FEE_PERIOD: toEnvValue(pp.executeOperatorFeePeriod ?? config.executeOperatorFeePeriod), + FORK_MIN_LIQ_COLLATERAL: toEnvValue( + pp.minimumLiquidationCollateralSSV ?? pp.minimumLiquidationCollateralEth + ?? config.minimumLiquidationCollateralSSV ?? config.minimumLiquidationCollateralEth + ), + FORK_VALIDATORS_PER_OPERATOR_LIMIT: toEnvValue(pp.validatorsPerOperatorLimit ?? config.validatorsPerOperatorLimit), + FORK_DEFAULT_ORACLE_IDS: config.defaultOracleIds?.join(","), + FORK_DEFAULT_UNSTAKE_COOLDOWN: toEnvValue( + pp.unstakeCooldownDuration ?? config.unstakeCooldownDuration ?? config.cooldownDuration + ), + }; +} diff --git a/scripts/common/helpers.ts b/scripts/common/helpers.ts new file mode 100644 index 000000000..6ac0fca6d --- /dev/null +++ b/scripts/common/helpers.ts @@ -0,0 +1,101 @@ +import { network, artifacts } from "hardhat"; +import { Contract, ContractFactory, Signer } from "ethers"; +import type { HardhatEthersHelpers } from "@nomicfoundation/hardhat-ethers/types"; +import { SSVModules } from "./modules.ts"; + +export function parseArg(argName: string): string { + const index = process.argv.indexOf(`--${argName}`); + if (index === -1) throw new Error(`Missing: --${argName}`); + const value = process.argv[index + 1]; + if (!value) throw new Error(`Missing value for --${argName}`); + return value; +} + +export async function getEthers(targetNetwork: string): Promise { + return (await network.connect({ network: targetNetwork })).ethers; +} + +export async function getDeployer(ethers: HardhatEthersHelpers): Promise { + const [deployer] = await ethers.getSigners(); + return deployer; +} + +export async function deployContract( + ethers: HardhatEthersHelpers, + contractName: string, + args: any[] = [], + signer?: Signer +): Promise<{ contract: any; address: string }> { + const network = await ethers.provider.getNetwork() + const factory = await ethers.getContractFactory(contractName, signer); + const contract = await factory.deploy(...args); + await contract.waitForDeployment(); + const address = await contract.getAddress(); + if (!network.name.includes("hardhat")) console.log(`${contractName} deployed at: ${address}`); + return { contract, address }; +} + +export async function deployProxy( + ethers: HardhatEthersHelpers, + deployer: Signer, + implAddress: string, + initData: string +): Promise<{ proxy: any; address: string }> { + const network = await ethers.provider.getNetwork() + const proxyArtifact = await artifacts.readArtifact("ERC1967Proxy"); + const proxyFactory = new ContractFactory(proxyArtifact.abi, proxyArtifact.bytecode, deployer); + const proxy = await proxyFactory.deploy(implAddress, initData); + await proxy.waitForDeployment(); + const address = await proxy.getAddress(); + if (!network.name.includes("hardhat")) console.log(`Proxy at: ${address}`); + return { proxy, address }; +} + +export async function attachModule( + ethers: HardhatEthersHelpers, + proxyAddress: string, + moduleName: string, + moduleAddress: string +): Promise { + const network = await ethers.provider.getNetwork() + const moduleEnumKey = moduleName as keyof typeof SSVModules; + if (SSVModules[moduleEnumKey] === undefined) { + throw new Error(`Invalid module: ${moduleName}`); + } + const networkFactory = await ethers.getContractFactory("SSVNetwork"); + const ssvNetwork = networkFactory.attach(proxyAddress); + if (!network.name.includes("hardhat")) console.log(`Attaching ${moduleName} (${moduleAddress})...`); + const tx = await ssvNetwork.updateModule(SSVModules[moduleEnumKey], moduleAddress); + await tx.wait(); + if (!network.name.includes("hardhat")) console.log(`Attached ${moduleName} at ${moduleAddress}`); +} + +export async function upgradeProxy( + ethers: HardhatEthersHelpers, + deployer: Signer, + proxyAddress: string, + implAddress: string, + contractName: string, + initFunction?: string, + params: any[] = [] +): Promise { + const network = await ethers.provider.getNetwork() + const factory = await ethers.getContractFactory(contractName); + const proxy = await ethers.getContractAt("SSVNetwork", proxyAddress, deployer); + + if (initFunction) { + const fragment = factory.interface.getFunction(initFunction); + if (!fragment) throw new Error(`Function ${initFunction} not found in ${contractName} ABI`); + const initData = factory.interface.encodeFunctionData(fragment, params); + + const tx = await proxy.upgradeToAndCall(implAddress, initData); + await tx.wait(); + if (!network.name.includes("hardhat")) console.log("Upgrade with init done"); + } else { + const tx = await proxy.upgradeTo(implAddress); + await tx.wait(); + if (!network.name.includes("hardhat")) console.log("Upgrade done"); + } + + if (!network.name.includes("hardhat")) console.log(`Proxy now uses: ${implAddress}`); +} diff --git a/scripts/common/impersonation.ts b/scripts/common/impersonation.ts new file mode 100644 index 000000000..0cb45e8b5 --- /dev/null +++ b/scripts/common/impersonation.ts @@ -0,0 +1,86 @@ +// ── Fork impersonation helpers ── + +async function trySend(provider: any, method: string, params: unknown[]) { + try { + await provider.send(method, params); + return true; + } catch { + return false; + } +} + +export async function impersonate(provider: any, address: string) { + const ok = + (await trySend(provider, "hardhat_impersonateAccount", [address])) || + (await trySend(provider, "anvil_impersonateAccount", [address])); + if (!ok) { + throw new Error("Impersonation not supported by the RPC node"); + } +} + +export async function setBalance(provider: any, address: string, balanceHex: string) { + const ok = + (await trySend(provider, "hardhat_setBalance", [address, balanceHex])) || + (await trySend(provider, "anvil_setBalance", [address, balanceHex])); + if (!ok) { + throw new Error("Setting balance not supported by the RPC node"); + } +} + +const TOP_UP_BALANCE = "0x56bc75e2d63100000"; // 100 ETH + +export async function getSignerForAddress( + ethers: any, + address: string, + useGetImpersonatedSigner: boolean +): Promise<{ signer: any; impersonated: boolean }> { + const signers = await ethers.getSigners(); + for (const signer of signers) { + if ((await signer.getAddress()).toLowerCase() === address.toLowerCase()) { + // Best-effort top up to avoid insufficient funds on forks + await trySend(ethers.provider, "hardhat_setBalance", [address, TOP_UP_BALANCE]); + await trySend(ethers.provider, "anvil_setBalance", [address, TOP_UP_BALANCE]); + return { signer, impersonated: false }; + } + } + + if (useGetImpersonatedSigner && typeof ethers.getImpersonatedSigner === "function") { + try { + const signer = await ethers.getImpersonatedSigner(address); + await trySend(ethers.provider, "hardhat_setBalance", [address, TOP_UP_BALANCE]); + await trySend(ethers.provider, "anvil_setBalance", [address, TOP_UP_BALANCE]); + return { signer, impersonated: true }; + } catch { + // Fall back to manual RPC impersonation + } + } + + await impersonate(ethers.provider, address); + await setBalance(ethers.provider, address, TOP_UP_BALANCE); + return { signer: await ethers.getSigner(address), impersonated: true }; +} + +/** + * Determines if the current network supports impersonation (fork mode). + */ +export function canImpersonateOnNetwork(targetNetwork: string, rpcUrl?: string): boolean { + const usesLocalRpc = + !!rpcUrl && (rpcUrl.includes("127.0.0.1") || rpcUrl.includes("localhost")); + return ( + targetNetwork.includes("hardhat") || + targetNetwork.includes("local") || + targetNetwork === "localhost" || + usesLocalRpc + ); +} + +/** + * Resolves the RPC URL for the given target network. + */ +export function resolveRpcUrl(targetNetwork: string): string | undefined { + const isLocalNetwork = targetNetwork === "local" || targetNetwork.endsWith("_local"); + if (isLocalNetwork) return "http://127.0.0.1:8545"; + if (targetNetwork === "hoodi") return process.env.HOODI_RPC_URL; + if (targetNetwork === "mainnet") return process.env.MAINNET_RPC_URL; + return undefined; +} diff --git a/scripts/common/modules.ts b/scripts/common/modules.ts new file mode 100644 index 000000000..34fd1caab --- /dev/null +++ b/scripts/common/modules.ts @@ -0,0 +1,9 @@ +export enum SSVModules { + SSVOperators = 0, + SSVClusters = 1, + SSVDAO = 2, + SSVViews = 3, + SSVOperatorsWhitelist = 4, + SSVStaking = 5, + SSVValidators = 6, +} \ No newline at end of file diff --git a/scripts/common/verify.ts b/scripts/common/verify.ts new file mode 100644 index 000000000..da5d63deb --- /dev/null +++ b/scripts/common/verify.ts @@ -0,0 +1,241 @@ +import type { OracleEntry, ResolvedProtocolParams } from "./config.ts"; + +// ── Formatting helpers ── + +export function normalizeComparable(value: unknown): unknown { + if (typeof value === "bigint") return value.toString(); + if (Array.isArray(value)) return value.map((v) => normalizeComparable(v)); + return value; +} + +export function formatValue(value: unknown): string { + if (typeof value === "bigint") return value.toString(); + if (Array.isArray(value)) return `[${value.map((v) => formatValue(v)).join(", ")}]`; + return String(value); +} + +export function assertEqual(label: string, expected: unknown, actual: unknown): void { + const expectedComparable = normalizeComparable(expected); + const actualComparable = normalizeComparable(actual); + if (JSON.stringify(expectedComparable) !== JSON.stringify(actualComparable)) { + throw new Error( + `[VERIFY] ${label} mismatch. expected=${formatValue(expected)} actual=${formatValue(actual)}` + ); + } + console.log(`[VERIFY] ${label} = ${formatValue(actual)}`); +} + +function assertEqualAndCollect( + label: string, + expected: unknown, + actual: unknown, + mismatches: string[] +): void { + const expectedComparable = normalizeComparable(expected); + const actualComparable = normalizeComparable(actual); + if (JSON.stringify(expectedComparable) !== JSON.stringify(actualComparable)) { + const mismatch = `${label}: expected=${formatValue(expected)} actual=${formatValue(actual)}`; + console.log(`[VERIFY][MISMATCH] ${mismatch}`); + mismatches.push(mismatch); + return; + } + console.log(`[VERIFY] ${label} = ${formatValue(actual)}`); +} + +export function logObserved(label: string, value: unknown): void { + console.log(`[VERIFY] ${label} = ${formatValue(value)}`); +} + +// ── Post-upgrade verification ── + +export type VerifyOptions = { + views: any; // SSVNetworkViews contract instance + params: ResolvedProtocolParams; + cooldownDuration: bigint; + defaultOracleIds: [number, number, number, number]; + quorumBps?: number; + oracles: OracleEntry[]; +}; + +/** + * Queries SSVViews and verifies on-chain state matches expected config. + * Logs all mismatches and throws once at the end; logs observed values when + * no expectation is configured. + */ +export async function verifyPostUpgradeState(opts: VerifyOptions): Promise { + const { views, params, cooldownDuration, defaultOracleIds, quorumBps, oracles } = opts; + const mismatches: string[] = []; + + console.log("[VERIFY] Querying SSVViews for post-upgrade parameters"); + + const viewsVersion = await views.getVersion(); + const actualCooldownDuration = await views.cooldownDuration(); + const actualDefaultOracleIds = await views.getActiveOracleIds(); + const actualQuorumBps = await views.getQuorumBps(); + const actualNetworkFeeEth = await views.getNetworkFee(); + const actualNetworkFeeSSV = await views.getNetworkFeeSSV(); + const actualOperatorFeeIncreaseLimit = await views.getOperatorFeeIncreaseLimit(); + const actualOperatorFeePeriods = await views.getOperatorFeePeriods(); + const actualLiquidationThresholdPeriod = await views.getLiquidationThresholdPeriod(); + const actualLiquidationThresholdPeriodSSV = await views.getLiquidationThresholdPeriodSSV(); + const actualMinimumLiquidationCollateralEth = await views.getMinimumLiquidationCollateral(); + const actualMinimumLiquidationCollateralSSV = await views.getMinimumLiquidationCollateralSSV(); + const actualMaxOperatorEthFee = await views.getMaximumOperatorFee(); + const actualMinOperatorEthFee = await views.getMinimumOperatorEthFee(); + + const expectedCooldownDuration = params.unstakeCooldownDuration ?? cooldownDuration; + + logObserved("views.version", viewsVersion); + assertEqualAndCollect("cooldownDuration", expectedCooldownDuration, actualCooldownDuration, mismatches); + assertEqualAndCollect( + "defaultOracleIds", + defaultOracleIds.map((id) => BigInt(id)), + Array.from(actualDefaultOracleIds), + mismatches + ); + + const checks: Array<{ + label: string; + expected: bigint | undefined; + actual: bigint; + }> = [ + { label: "networkFeeEth", expected: params.networkFeeEth, actual: actualNetworkFeeEth }, + { label: "networkFeeSSV", expected: params.networkFeeSSV, actual: actualNetworkFeeSSV }, + { + label: "operatorFeeIncreaseLimit", + expected: params.operatorFeeIncreaseLimit, + actual: actualOperatorFeeIncreaseLimit, + }, + { + label: "declareOperatorFeePeriod", + expected: params.declareOperatorFeePeriod, + actual: actualOperatorFeePeriods.declarePeriod, + }, + { + label: "executeOperatorFeePeriod", + expected: params.executeOperatorFeePeriod, + actual: actualOperatorFeePeriods.executePeriod, + }, + { + label: "liquidationThresholdPeriod", + expected: params.liquidationThresholdPeriod, + actual: actualLiquidationThresholdPeriod, + }, + { + label: "liquidationThresholdPeriodSSV", + expected: params.liquidationThresholdPeriodSSV, + actual: actualLiquidationThresholdPeriodSSV, + }, + { + label: "minimumLiquidationCollateralEth", + expected: params.minimumLiquidationCollateralEth, + actual: actualMinimumLiquidationCollateralEth, + }, + { + label: "minimumLiquidationCollateralSSV", + expected: params.minimumLiquidationCollateralSSV, + actual: actualMinimumLiquidationCollateralSSV, + }, + { label: "maxOperatorEthFee", expected: params.maxOperatorEthFee, actual: actualMaxOperatorEthFee }, + { label: "minOperatorEthFee", expected: params.minOperatorEthFee, actual: actualMinOperatorEthFee }, + ]; + + if (quorumBps !== undefined) { + assertEqualAndCollect("quorumBps", BigInt(quorumBps), actualQuorumBps, mismatches); + } else { + logObserved("quorumBps", actualQuorumBps); + } + + if (params.minBlocksBetweenUpdates !== undefined) { + console.log( + `[VERIFY] minBlocksBetweenUpdates configured=${params.minBlocksBetweenUpdates.toString()} ` + + "(not verifiable via SSVViews; no getter exposed)" + ); + } + + for (const { label, expected, actual } of checks) { + if (expected !== undefined) { + assertEqualAndCollect(label, expected, actual, mismatches); + } else { + logObserved(label, actual); + } + } + + for (const oracleId of defaultOracleIds) { + const actualOracleAddress = await views.getOracle(oracleId); + const expectedOracleAddress = oracles.find((oracle) => oracle.id === oracleId)?.address; + if (expectedOracleAddress) { + assertEqualAndCollect( + `oracle[${oracleId}]`, + expectedOracleAddress.toLowerCase(), + actualOracleAddress.toLowerCase(), + mismatches + ); + } else { + logObserved(`oracle[${oracleId}]`, actualOracleAddress); + } + } + + if (mismatches.length > 0) { + throw new Error( + `[VERIFY] Found ${mismatches.length} mismatch(es):\n` + + mismatches.map((mismatch) => `- ${mismatch}`).join("\n") + ); + } + + console.log("[VERIFY] All configured checks passed"); +} + +/** + * Returns the actual on-chain values for result JSON output. + */ +export async function readOnChainValues(views: any): Promise<{ + networkFeeEth: string; + networkFeeSSV: string; + maxOperatorEthFee: string; + minOperatorEthFee: string; + operatorFeeIncreaseLimit: string; + declareOperatorFeePeriod: string; + executeOperatorFeePeriod: string; + liquidationThresholdPeriod: string; + liquidationThresholdPeriodSSV: string; + minimumLiquidationCollateralEth: string; + minimumLiquidationCollateralSSV: string; + validatorsPerOperatorLimit: string; + unstakeCooldownDuration: string; + quorumBps: number; + defaultOracleIds: number[]; +}> { + const actualNetworkFeeEth = await views.getNetworkFee(); + const actualNetworkFeeSSV = await views.getNetworkFeeSSV(); + const actualOperatorFeeIncreaseLimit = await views.getOperatorFeeIncreaseLimit(); + const actualOperatorFeePeriods = await views.getOperatorFeePeriods(); + const actualLiquidationThresholdPeriod = await views.getLiquidationThresholdPeriod(); + const actualLiquidationThresholdPeriodSSV = await views.getLiquidationThresholdPeriodSSV(); + const actualMinimumLiquidationCollateralEth = await views.getMinimumLiquidationCollateral(); + const actualMinimumLiquidationCollateralSSV = await views.getMinimumLiquidationCollateralSSV(); + const actualMaxOperatorEthFee = await views.getMaximumOperatorFee(); + const actualMinOperatorEthFee = await views.getMinimumOperatorEthFee(); + const actualValidatorsPerOperatorLimit = await views.getValidatorsPerOperatorLimit(); + const actualCooldownDuration = await views.cooldownDuration(); + const actualQuorumBps = await views.getQuorumBps(); + const actualDefaultOracleIds = await views.getActiveOracleIds(); + + return { + networkFeeEth: actualNetworkFeeEth.toString(), + networkFeeSSV: actualNetworkFeeSSV.toString(), + maxOperatorEthFee: actualMaxOperatorEthFee.toString(), + minOperatorEthFee: actualMinOperatorEthFee.toString(), + operatorFeeIncreaseLimit: actualOperatorFeeIncreaseLimit.toString(), + declareOperatorFeePeriod: actualOperatorFeePeriods.declarePeriod.toString(), + executeOperatorFeePeriod: actualOperatorFeePeriods.executePeriod.toString(), + liquidationThresholdPeriod: actualLiquidationThresholdPeriod.toString(), + liquidationThresholdPeriodSSV: actualLiquidationThresholdPeriodSSV.toString(), + minimumLiquidationCollateralEth: actualMinimumLiquidationCollateralEth.toString(), + minimumLiquidationCollateralSSV: actualMinimumLiquidationCollateralSSV.toString(), + validatorsPerOperatorLimit: actualValidatorsPerOperatorLimit.toString(), + unstakeCooldownDuration: actualCooldownDuration.toString(), + quorumBps: Number(actualQuorumBps), + defaultOracleIds: Array.from(actualDefaultOracleIds).map((id: any) => Number(id)), + }; +} diff --git a/scripts/contract-sizes.ts b/scripts/contract-sizes.ts new file mode 100644 index 000000000..3db7a144d --- /dev/null +++ b/scripts/contract-sizes.ts @@ -0,0 +1,55 @@ +import hre from "hardhat"; +import fs from "fs"; +import path from "path"; + +async function main() { + const artifactsPath = hre.config.paths.artifacts; + const buildInfoDir = path.join(artifactsPath, "build-info"); + + const results: { name: string; size: number }[] = []; + + for (const file of fs.readdirSync(buildInfoDir)) { + const fullPath = path.join(buildInfoDir, file); + const buildInfo = JSON.parse(fs.readFileSync(fullPath, "utf8")); + + if ( + !buildInfo || + !buildInfo.output || + !buildInfo.output.contracts || + typeof buildInfo.output.contracts !== "object" + ) { + continue; + } + + const contracts = buildInfo.output.contracts; + + for (const fileName of Object.keys(contracts)) { + for (const contractName of Object.keys(contracts[fileName])) { + const c = contracts[fileName][contractName]; + + const bytecode = c?.evm?.deployedBytecode?.object; + if (!bytecode || bytecode.length === 0) continue; + + const size = bytecode.length / 2; + + results.push({ + name: contractName, + size, + }); + } + } + } + + results.sort((a, b) => b.size - a.size); + + for (const { name, size } of results) { + const kb = (size / 1024).toFixed(2); + const warn = size > 24576 ? "exceeds 24KB limit!" : ""; + console.log(`${name.padEnd(32)} ${kb} KB (${size} bytes)${warn}`); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/deploy-fresh.ts b/scripts/deploy-fresh.ts new file mode 100644 index 000000000..09612051e --- /dev/null +++ b/scripts/deploy-fresh.ts @@ -0,0 +1,196 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { ethers as ethersLib } from "ethers"; +import { deployContract, deployProxy, getDeployer, getEthers, parseArg } from "./common/helpers.ts"; +import { SSVModules } from "./common/modules.ts"; +import { + parseOptionalArg, + parseUint, + resolveConfigPath, + MODULE_ORDER, +} from "./common/config.ts"; + +type LocalConfig = { + ssvToken?: string; + protocolParams?: { + liquidationThresholdPeriod?: string | number; + liquidationThresholdPeriodSSV?: string | number; + minBlocksBetweenUpdates?: string | number; + minimumLiquidationCollateralEth?: string | number; + validatorsPerOperatorLimit?: string | number; + declareOperatorFeePeriod?: string | number; + executeOperatorFeePeriod?: string | number; + operatorFeeIncreaseLimit?: string | number; + }; + quorumBps?: number; + defaultOracleIds?: number[]; + cooldownDuration?: string | number; + upgradeTimestamp?: string | number; +}; + +async function main() { + const targetNetwork = parseArg("network"); + const envFlag = parseOptionalArg("env") ?? "local"; + const configPath = resolveConfigPath(envFlag); + + let config: LocalConfig; + try { + const raw = await readFile(configPath, "utf8"); + config = JSON.parse(raw) as LocalConfig; + } catch { + console.log(`No config found at ${configPath}, using defaults`); + config = {}; + } + + const ethers = await getEthers(targetNetwork); + const deployer = await getDeployer(ethers); + const deployerAddress = await deployer.getAddress(); + const providerNetwork = await ethers.provider.getNetwork(); + + const pp = config.protocolParams ?? {}; + const quorumBps = config.quorumBps ?? 7500; + const defaultOracleIds = config.defaultOracleIds ?? [1, 2, 3, 4]; + const cooldown = parseUint(config.cooldownDuration, "cooldownDuration") ?? 604800n; + const upgradeTimestamp = parseUint(config.upgradeTimestamp, "upgradeTimestamp") ?? 0n; + + if (defaultOracleIds.length !== 4) { + throw new Error("defaultOracleIds must contain exactly 4 ids"); + } + + console.log(`Fresh deployment on ${targetNetwork}`); + console.log(`Deployer: ${deployerAddress}`); + + // ── 0. Deploy SSVToken (or use existing) ── + let ssvTokenAddress: string; + if (config.ssvToken) { + ssvTokenAddress = config.ssvToken; + console.log(`Using existing SSVToken: ${ssvTokenAddress}`); + } else { + console.log("[0/7] Deploying SSVToken (mock for local/test)"); + const { address } = await deployContract(ethers, "SSVToken"); + ssvTokenAddress = address; + } + + // ── 1. Deploy modules that don't need CSSVToken ── + console.log("[1/7] Deploying cssv-independent modules..."); + const { address: ssvOperatorsAddr } = await deployContract(ethers, "SSVOperators", [upgradeTimestamp]); + const { address: ssvClustersAddr } = await deployContract(ethers, "SSVClusters"); + const { address: ssvOperatorsWhitelistAddr } = await deployContract(ethers, "SSVOperatorsWhitelist"); + const { address: ssvValidatorsAddr } = await deployContract(ethers, "SSVValidators"); + + // ── 2. Deploy SSVNetwork implementation + proxy ── + // initialize() with address(0) for dao/views — they'll be attached after CSSVToken exists. + console.log("[2/7] Deploying SSVNetwork implementation + proxy..."); + const { address: networkImplAddr } = await deployContract(ethers, "SSVNetwork"); + + const networkFactory = await ethers.getContractFactory("SSVNetwork"); + const networkInitData = networkFactory.interface.encodeFunctionData("initialize", [ + ssvTokenAddress, + ssvOperatorsAddr, + ssvClustersAddr, + ethersLib.ZeroAddress, // SSVDAO placeholder — replaced in step 7 + ethersLib.ZeroAddress, // SSVViews placeholder — replaced in step 7 + { + minimumBlocksBeforeLiquidation: parseUint(pp.liquidationThresholdPeriod, "liquidationThresholdPeriod") ?? 214800n, + minimumLiquidationCollateral: parseUint(pp.minimumLiquidationCollateralEth, "minimumLiquidationCollateralEth") ?? 1_000_000_000_000_000n, + validatorsPerOperatorLimit: parseUint(pp.validatorsPerOperatorLimit, "validatorsPerOperatorLimit") ?? 3000n, + declareOperatorFeePeriod: parseUint(pp.declareOperatorFeePeriod, "declareOperatorFeePeriod") ?? 604800n, + executeOperatorFeePeriod: parseUint(pp.executeOperatorFeePeriod, "executeOperatorFeePeriod") ?? 604800n, + operatorMaxFeeIncrease: parseUint(pp.operatorFeeIncreaseLimit, "operatorFeeIncreaseLimit") ?? 10000n, + defaultOracleIds, + quorumBps, + }, + ]); + + const { address: networkProxyAddr } = await deployProxy(ethers, deployer, networkImplAddr, networkInitData); + console.log(`SSVNetwork proxy: ${networkProxyAddr}`); + + // ── 3. Deploy CSSVToken (needs proxy address) ── + console.log("[3/7] Deploying CSSVToken..."); + const { address: cssvTokenAddr } = await deployContract(ethers, "CSSVToken", [networkProxyAddr]); + + // ── 4. Deploy cssv-dependent modules ── + console.log("[4/7] Deploying cssv-dependent modules (SSVDAO, SSVViews, SSVStaking)..."); + const { address: ssvDaoAddr } = await deployContract(ethers, "SSVDAO", [cssvTokenAddr]); + const { address: ssvViewsAddr } = await deployContract(ethers, "SSVViews", [cssvTokenAddr]); + const { address: ssvStakingAddr } = await deployContract(ethers, "SSVStaking", [cssvTokenAddr]); + + // ── 5. Run staking upgrade (reinitializer) ── + console.log("[5/7] Running SSVStaking upgrade (upgradeToAndCall)..."); + const { address: stakingUpgradeImplAddr } = await deployContract(ethers, "SSVNetworkSSVStakingUpgrade"); + + const stakingUpgradeFactory = await ethers.getContractFactory("SSVNetworkSSVStakingUpgrade"); + const stakingInitData = stakingUpgradeFactory.interface.encodeFunctionData( + "initializeSSVStaking(uint64,uint32[4],uint16)", + [cooldown, defaultOracleIds, quorumBps] + ); + + const networkWithSigner = await ethers.getContractAt("SSVNetwork", networkProxyAddr, deployer); + await (await networkWithSigner.upgradeToAndCall(stakingUpgradeImplAddr, stakingInitData)).wait(); + + // ── 6. Deploy SSVNetworkViews implementation + proxy ── + console.log("[6/7] Deploying SSVNetworkViews implementation + proxy..."); + const { address: viewsImplAddr } = await deployContract(ethers, "SSVNetworkViews"); + const viewsFactory = await ethers.getContractFactory("SSVNetworkViews"); + const viewsInitData = viewsFactory.interface.encodeFunctionData("initialize", [networkProxyAddr]); + + const { address: viewsProxyAddr } = await deployProxy(ethers, deployer, viewsImplAddr, viewsInitData); + console.log(`SSVNetworkViews proxy: ${viewsProxyAddr}`); + + // ── 7. Attach all modules ── + console.log("[7/7] Attaching all modules..."); + const modules = { + SSVOperators: ssvOperatorsAddr, + SSVClusters: ssvClustersAddr, + SSVDAO: ssvDaoAddr, + SSVViews: ssvViewsAddr, + SSVOperatorsWhitelist: ssvOperatorsWhitelistAddr, + SSVStaking: ssvStakingAddr, + SSVValidators: ssvValidatorsAddr, + }; + + for (const mod of MODULE_ORDER) { + const moduleId = SSVModules[mod]; + await (await networkWithSigner.updateModule(moduleId, modules[mod])).wait(); + } + + const minBlocksBetweenUpdates = parseUint(pp.minBlocksBetweenUpdates, "minBlocksBetweenUpdates"); + const liquidationThresholdPeriodSSV = parseUint(pp.liquidationThresholdPeriodSSV, "liquidationThresholdPeriodSSV"); + if (liquidationThresholdPeriodSSV !== undefined) { + await (await networkWithSigner.updateLiquidationThresholdPeriodSSV(liquidationThresholdPeriodSSV)).wait(); + } + if (minBlocksBetweenUpdates !== undefined) { + await (await networkWithSigner.updateMinBlocksBetweenUpdates(minBlocksBetweenUpdates)).wait(); + } + + const blockNumber = await ethers.provider.getBlockNumber(); + + const result = { + deployer: deployerAddress, + chainId: providerNetwork.chainId.toString(), + network: targetNetwork, + deployedAt: new Date().toISOString(), + blockNumber, + ssvToken: ssvTokenAddress, + ssvNetworkProxy: networkProxyAddr, + ssvNetworkViews: viewsProxyAddr, + cssvToken: cssvTokenAddr, + implementations: { + SSVNetwork: networkImplAddr, + SSVNetworkSSVStakingUpgrade: stakingUpgradeImplAddr, + SSVNetworkViews: viewsImplAddr, + }, + modules, + }; + + const outputPath = resolve("deployments", envFlag, "deploy-result.json"); + await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); + + console.log("Fresh deployment complete"); + console.log(`Result: ${outputPath}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/deploy-module.ts b/scripts/deploy-module.ts new file mode 100644 index 000000000..112523d7a --- /dev/null +++ b/scripts/deploy-module.ts @@ -0,0 +1,38 @@ +import { parseArg, getEthers, getDeployer, deployContract } from "./common/helpers.ts"; +import { SSVModules } from "./common/modules.ts"; + +async function main() { + const targetNetwork = parseArg("network"); + const ethers = await getEthers(targetNetwork); + await getDeployer(ethers); + + const moduleName = parseArg("module"); + + const moduleEnumKey = moduleName as keyof typeof SSVModules; + if (SSVModules[moduleEnumKey] === undefined) { + throw new Error(`Invalid module: ${moduleName}`); + } + + let args: any[] = []; + const argsIndex = process.argv.indexOf("--args"); + if (argsIndex !== -1) { + const argsValue = process.argv[argsIndex + 1]; + if (argsValue) { + try { + args = JSON.parse(argsValue); + if (!Array.isArray(args)) { + throw new Error("Args must be a JSON array"); + } + } catch (err) { + throw new Error(`Invalid --args JSON: ${argsValue}. Expected array like [1, "hello", true]`); + } + } + } + + await deployContract(ethers, moduleName, args); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/deploy.ts b/scripts/deploy.ts new file mode 100644 index 000000000..60695fdc4 --- /dev/null +++ b/scripts/deploy.ts @@ -0,0 +1,176 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { isAddress } from "ethers"; +import { deployContract, getDeployer, getEthers, parseArg } from "./common/helpers.ts"; +import { + type UpgradeConfig, + type ModuleAddresses, + type ModuleName, + parseOptionalArg, + parseUint, + requireAddress, + resolveConfigPath, + resolveDeployResultPath, + resolveVersionedDeployResultPath, + resolveDeployedConfigPath, + resolveNetworkFromEnv, + updateLatestSymlink, +} from "./common/config.ts"; + +type DeployResult = { + deployer: string; + chainId: string; + network: string; + deployedAt: string; + blockNumber: number; + implementations: { + SSVNetworkSSVStakingUpgrade: string; + SSVNetworkViews: string; + }; + cssvToken: { + address: string; + deployed: boolean; + }; + modules: ModuleAddresses; +}; + +async function main() { + // ── Resolve config ── + const envFlag = parseOptionalArg("env"); + const configFlag = parseOptionalArg("config"); + + let configPath: string; + let outputPath: string; + + if (envFlag) { + configPath = resolveConfigPath(envFlag); + outputPath = resolveDeployResultPath(envFlag); + } else if (configFlag) { + configPath = resolve(configFlag); + outputPath = resolveDeployedConfigPath(configPath, parseOptionalArg("output-config")); + } else { + throw new Error("Provide --env or --config "); + } + + const targetNetwork = parseOptionalArg("network") ?? resolveNetworkFromEnv(envFlag) ?? "local"; + + const raw = await readFile(configPath, "utf8"); + const config = JSON.parse(raw) as UpgradeConfig; + const ssvNetworkProxy = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + const upgradeTimestamp = parseUint(config.upgradeTimestamp, "upgradeTimestamp") ?? 0n; + + const ethers = await getEthers(targetNetwork); + const deployer = await getDeployer(ethers); + const deployerAddress = await deployer.getAddress(); + const providerNetwork = await ethers.provider.getNetwork(); + + const proxyCode = await ethers.provider.getCode(ssvNetworkProxy); + if (proxyCode === "0x") { + throw new Error( + `No contract code at ssvNetworkProxy ${ssvNetworkProxy} on ${targetNetwork}. ` + + `Check your RPC URL and network selection.` + ); + } + + console.log(`Deploying implementations and modules on ${targetNetwork}`); + console.log(`Deployer: ${deployerAddress}`); + console.log(`SSVNetwork proxy: ${ssvNetworkProxy}`); + + // ── Deploy implementations ── + console.log("[1/4] Deploying SSVNetworkSSVStakingUpgrade implementation"); + const { address: stakingUpgradeImplAddr } = await deployContract(ethers, "SSVNetworkSSVStakingUpgrade", [], deployer); + + console.log("[2/4] Deploying SSVNetworkViews implementation"); + const { address: viewsImplAddr } = await deployContract(ethers, "SSVNetworkViews", [], deployer); + + // ── Deploy CSSVToken (conditional) ── + console.log("[3/4] Resolving CSSVToken"); + let cssvAddr: string; + let cssvDeployed = false; + const existingCssv = config.cssvToken; + if (existingCssv && isAddress(existingCssv)) { + const cssvCode = await ethers.provider.getCode(existingCssv); + if (cssvCode !== "0x") { + cssvAddr = existingCssv; + console.log(` Using existing CSSVToken: ${cssvAddr}`); + } else { + console.log(` CSSVToken at ${existingCssv} has no code, deploying new one`); + cssvAddr = (await deployContract(ethers, "CSSVToken", [ssvNetworkProxy], deployer)).address; + cssvDeployed = true; + } + } else { + cssvAddr = (await deployContract(ethers, "CSSVToken", [ssvNetworkProxy], deployer)).address; + cssvDeployed = true; + } + + // ── Deploy modules ── + console.log("[4/4] Deploying all module implementations"); + const { address: ssvOperatorsAddr } = await deployContract(ethers, "SSVOperators", [upgradeTimestamp], deployer); + const { address: ssvClustersAddr } = await deployContract(ethers, "SSVClusters", [], deployer); + const { address: ssvDaoAddr } = await deployContract(ethers, "SSVDAO", [cssvAddr], deployer); + const { address: ssvViewsAddr } = await deployContract(ethers, "SSVViews", [cssvAddr], deployer); + const { address: ssvOperatorsWhitelistAddr } = await deployContract(ethers, "SSVOperatorsWhitelist", [], deployer); + const { address: ssvStakingAddr } = await deployContract(ethers, "SSVStaking", [cssvAddr], deployer); + const { address: ssvValidatorsAddr } = await deployContract(ethers, "SSVValidators", [], deployer); + + const modules: ModuleAddresses = { + SSVOperators: ssvOperatorsAddr, + SSVClusters: ssvClustersAddr, + SSVDAO: ssvDaoAddr, + SSVViews: ssvViewsAddr, + SSVOperatorsWhitelist: ssvOperatorsWhitelistAddr, + SSVStaking: ssvStakingAddr, + SSVValidators: ssvValidatorsAddr, + }; + + const blockNumber = await ethers.provider.getBlockNumber(); + + // Read the deployed version from the staking upgrade impl (pure function, no proxy needed) + let contractVersion = "unknown"; + try { + const implContract = new (await import("ethers")).Contract( + stakingUpgradeImplAddr, + ["function getVersion() external pure returns (string)"], + ethers.provider, + ); + contractVersion = await implContract.getVersion(); + } catch { + // non-fatal — versioned filename falls back to "unknown" + } + + const result: DeployResult = { + deployer: deployerAddress, + chainId: providerNetwork.chainId.toString(), + network: targetNetwork, + deployedAt: new Date().toISOString(), + blockNumber, + implementations: { + SSVNetworkSSVStakingUpgrade: stakingUpgradeImplAddr, + SSVNetworkViews: viewsImplAddr, + }, + cssvToken: { + address: cssvAddr, + deployed: cssvDeployed, + }, + modules, + }; + + // Write versioned file and update the fixed-name symlink + const versionedOutputPath = envFlag + ? resolveVersionedDeployResultPath(envFlag, contractVersion) + : outputPath; + await writeFile(versionedOutputPath, `${JSON.stringify(result, null, 2)}\n`, "utf8"); + if (envFlag) { + await updateLatestSymlink(versionedOutputPath, outputPath); + } + + console.log("Deployment complete (no proxy upgrade performed)"); + console.log(`Config: ${configPath}`); + console.log(`Result: ${versionedOutputPath}`); + console.log(`Latest: ${outputPath} -> ${contractVersion}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/deployment.md b/scripts/deployment.md new file mode 100644 index 000000000..55d9a1c26 --- /dev/null +++ b/scripts/deployment.md @@ -0,0 +1,96 @@ +# Deployment & Upgrade Guide + +This project uses Just recipes and TypeScript scripts to deploy and upgrade SSV Network contracts. + +For full documentation on environments, workflows, and config schema, see [`deployments/README.md`](../deployments/README.md). + +## Quick Reference + +### Fresh deployment (local/test) + +```bash +just deploy-fresh local +``` + +Deploys everything from scratch: SSVToken (mock), all modules, SSVNetwork + proxy, SSVNetworkViews + proxy, CSSVToken, and runs the staking upgrade. Config is read from `deployments/local/config.json`. + +### Deploy implementations (for mainnet/hoodi) + +```bash +just deploy mainnet +``` + +Deploys implementations + modules only (no proxy upgrade). Writes `deployments/mainnet/deploy-result.json`. + +### Fork validation + +```bash +anvil --fork-url "$HOODI_RPC_URL" --port 8545 +just upgrade-test-fork hoodi-stage +``` + +Runs upgrade on local fork and then executes strict fork tests. + +### Live upgrade + +```bash +just upgrade hoodi-stage +``` + +Requires the deployer private key to match the on-chain owner. + +### Generate deployment attestation + +```bash +just generate-attestation mainnet +``` + +Generates `deployments/mainnet/deployment-attestation.json` with bytecode hashes, deployer info, constructor args, and config snapshot for committee review. + +### Generate SAFE multi-sig batch + +```bash +just generate-safe-batch mainnet +``` + +Generates `deployments/mainnet/multisig-batch.json` for import into SAFE Transaction Builder. + +### Verify on-chain state + +```bash +just verify-upgrade mainnet +``` + +Reads `config.json` and verifies all on-chain values match expected parameters. + +## One-off Utilities + +### Upgrade a proxy (deploy new impl + upgrade) + +```bash +just upgrade-contract SSVNetwork 0xPROXY hoodi +``` + +### Upgrade a proxy with pre-deployed implementation + +```bash +just upgrade-contract SSVNetwork 0xPROXY hoodi 0xIMPL +``` + +### Deploy a single module + +```bash +just deploy-module SSVOperators hoodi 12345 +``` + +### Attach a pre-deployed module + +```bash +just attach-module hoodi-stage SSVClusters 0xMODULE +``` + +## Important Notes + +- **Storage safety**: Never add state variables to `SSVNetwork` or `SSVNetworkViews`. All state goes through diamond storage libraries. +- **UUPS pattern**: Upgrades use the [UUPS Proxy pattern](https://docs.openzeppelin.com/contracts/4.x/api/proxy). +- **Library changes**: When modifying a library, you must also redeploy all modules that use it. diff --git a/scripts/gas-compare.ts b/scripts/gas-compare.ts new file mode 100644 index 000000000..22dabf24b --- /dev/null +++ b/scripts/gas-compare.ts @@ -0,0 +1,226 @@ +#!/usr/bin/env npx tsx +import * as fs from 'node:fs'; +import * as path from 'node:path'; +interface GasReportEntry { + name: string; + maxLimit: number; + min: number | null; + max: number | null; + average: number | null; + txCount: number; + withinLimit: boolean; +} + +interface GasReport { + timestamp: string; + commit?: string; + branch?: string; + entries: GasReportEntry[]; + summary: { + totalOperations: number; + operationsWithData: number; + allWithinLimits: boolean; + }; +} + +interface ComparisonResult { + name: string; + baseline: number | null; + current: number | null; + difference: number | null; + percentChange: number | null; +} + +const args = process.argv.slice(2); +let baselinePath = 'test/helpers/v1-gas-report.json'; +let currentPath = 'gas-report.json'; +let baselineLabel = process.env.BASELINE_TAG || 'v1.2.0'; +let currentLabel = process.env.CURRENT_LABEL || 'current'; +let outputPath = process.env.GAS_COMPARE_OUTPUT || 'gas-compare.txt'; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--baseline' && args[i + 1]) { + baselinePath = args[++i]; + } else if (args[i] === '--current' && args[i + 1]) { + currentPath = args[++i]; + } else if (args[i] === '--baseline-label' && args[i + 1]) { + baselineLabel = args[++i]; + } else if (args[i] === '--current-label' && args[i + 1]) { + currentLabel = args[++i]; + } else if (args[i] === '--output' && args[i + 1]) { + outputPath = args[++i]; + } +} + +function padRight(str: string, len: number): string { + return str.length >= len ? str.substring(0, len) : str + ' '.repeat(len - str.length); +} + +function padLeft(str: string, len: number): string { + return str.length >= len ? str : ' '.repeat(len - str.length) + str; +} + +function loadJson(filePath: string): T | null { + const absolutePath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath); + + if (!fs.existsSync(absolutePath)) { + console.error(`File not found: ${absolutePath}`); + return null; + } + + try { + const content = fs.readFileSync(absolutePath, 'utf8'); + return JSON.parse(content) as T; + } catch (error) { + console.error(`Failed to parse ${absolutePath}:`, error); + return null; + } +} + +function buildEntryMap(report: GasReport): Map { + const map = new Map(); + for (const entry of report.entries) { + map.set(entry.name, entry); + } + return map; +} + +function compare(baseline: GasReport, current: GasReport): ComparisonResult[] { + const results: ComparisonResult[] = []; + const baselineEntries = buildEntryMap(baseline); + const currentEntries = buildEntryMap(current); + const names = new Set(); + + for (const entry of baseline.entries) { + if (entry.average !== null) names.add(entry.name); + } + + for (const entry of current.entries) { + if (entry.average !== null) names.add(entry.name); + } + + for (const name of names) { + const baselineEntry = baselineEntries.get(name); + const currentEntry = currentEntries.get(name); + const baselineValue = baselineEntry?.average ?? null; + const currentValue = currentEntry?.average ?? null; + const hasValues = baselineValue !== null && currentValue !== null; + const difference = hasValues ? currentValue - baselineValue : null; + const percentChange = + hasValues && baselineValue !== 0 + ? (difference / baselineValue) * 100 + : null; + + results.push({ + name, + baseline: baselineValue, + current: currentValue, + difference, + percentChange, + }); + } + + return results.sort((a, b) => a.name.localeCompare(b.name)); +} + +function formatNumber(value: number | null): string { + return value === null ? '-' : value.toLocaleString(); +} + +function formatDiff(value: number | null): string { + if (value === null) return '-'; + const sign = value >= 0 ? '+' : ''; + return `${sign}${value.toLocaleString()}`; +} + +function formatPercent(value: number | null): string { + if (value === null) return '-'; + const sign = value >= 0 ? '+' : ''; + return `${sign}${value.toFixed(2)}%`; +} + +function printResults(results: ComparisonResult[]): string { + const baselineWidth = Math.max(12, baselineLabel.length + 2); + const currentWidth = Math.max(12, currentLabel.length + 2); + const lines: string[] = []; + + lines.push(''); + lines.push('='.repeat(100)); + lines.push(' GAS COMPARISON REPORT'); + lines.push('='.repeat(100)); + lines.push(`Baseline: ${baselineLabel}`); + lines.push(`Current: ${currentLabel}`); + lines.push('-'.repeat(100)); + + lines.push( + padRight('Operation', 50) + + padLeft(baselineLabel, baselineWidth) + + padLeft(currentLabel, currentWidth) + + padLeft('Diff', 12) + + padLeft('Change', 12) + ); + lines.push('-'.repeat(100)); + + for (const result of results) { + lines.push( + padRight(result.name, 50) + + padLeft(formatNumber(result.baseline), baselineWidth) + + padLeft(formatNumber(result.current), currentWidth) + + padLeft(formatDiff(result.difference), 12) + + padLeft(formatPercent(result.percentChange), 12) + ); + } + + lines.push('-'.repeat(100)); + + const comparable = results.filter(r => r.difference !== null); + const regressions = comparable.filter(r => (r.difference ?? 0) > 0); + const improvements = comparable.filter(r => (r.difference ?? 0) < 0); + const unchanged = comparable.filter(r => r.difference === 0); + const missingBaseline = results.filter(r => r.baseline === null).length; + const missingCurrent = results.filter(r => r.current === null).length; + + lines.push(''); + lines.push('Summary:'); + lines.push(` Compared: ${comparable.length}`); + lines.push(` Regressions: ${regressions.length}`); + lines.push(` Improvements: ${improvements.length}`); + lines.push(` Unchanged: ${unchanged.length}`); + lines.push(` Missing baseline: ${missingBaseline}`); + lines.push(` Missing current: ${missingCurrent}`); + lines.push('='.repeat(100)); + lines.push(''); + + const output = lines.join('\n'); + console.log(output); + return output; +} + +console.log('Gas Comparison Tool'); +console.log(`Baseline report: ${baselinePath}`); +console.log(`Current report: ${currentPath}`); +console.log(`Labels: ${baselineLabel} -> ${currentLabel}`); + +const baselineReport = loadJson(baselinePath); +if (!baselineReport) { + console.error('Failed to load baseline gas report.'); + process.exit(2); +} + +const currentReport = loadJson(currentPath); +if (!currentReport) { + console.error('Failed to load current gas report. Run tests with SSV_REPORT_GAS=true first.'); + process.exit(2); +} + +const results = compare(baselineReport, currentReport); +const output = printResults(results); + +const resolvedOutputPath = path.isAbsolute(outputPath) + ? outputPath + : path.join(process.cwd(), outputPath); +fs.writeFileSync(resolvedOutputPath, output, 'utf8'); +console.log(`Gas comparison report saved to: ${resolvedOutputPath}`); +process.exit(0); diff --git a/scripts/generate-deployment-attestation.ts b/scripts/generate-deployment-attestation.ts new file mode 100644 index 000000000..0e803f17c --- /dev/null +++ b/scripts/generate-deployment-attestation.ts @@ -0,0 +1,276 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { keccak256 } from "ethers"; +import { + type UpgradeConfig, + parseOptionalArg, + resolveConfigPath, + resolveDeployResultPath, + resolveEnvDir, + resolveUpgradeTimestamp, + requireAddress, +} from "./common/config.ts"; +import { getEthers, parseArg } from "./common/helpers.ts"; + +type DeployResultFile = { + deployer: string; + chainId: string; + network: string; + deployedAt: string; + blockNumber: number; + implementations: { + SSVNetworkSSVStakingUpgrade: string; + SSVNetworkViews: string; + }; + cssvToken: { + address: string; + deployed: boolean; + }; + modules: Record; +}; + +type ContractEntry = { + address: string; + constructorArgs: Record; + initializerArgs?: Record; + bytecodeHash: string; +}; + +type Attestation = { + generatedAt: string; + deployment: { + deployer: string; + chainId: string; + network: string; + deployedAt: string; + blockNumber: number; + }; + config: { + currentVersion: string; + targetVersion: string; + ssvNetworkProxy: string; + ssvNetworkViews: string; + ssvToken: string; + cooldownDuration: number; + upgradeTimestamp: number; + quorumBps: number; + defaultOracleIds: number[]; + initialStakeAmount: string; + protocolParams: Record; + oracles: Record; + }; + contracts: Record; +}; + +async function fetchBytecodeHash(provider: any, address: string): Promise { + const code = await provider.getCode(address); + if (code === "0x") { + throw new Error(`No contract code at ${address}`); + } + return keccak256(code); +} + +async function main() { + const envFlag = parseArg("env"); + const networkOverride = parseOptionalArg("network"); + + // Load config and deploy result + const configPath = resolveConfigPath(envFlag); + const resultPath = resolveDeployResultPath(envFlag); + + const config = JSON.parse(await readFile(configPath, "utf8")) as UpgradeConfig; + const deployResult = JSON.parse(await readFile(resultPath, "utf8")) as DeployResultFile; + + // Resolve network for RPC connection + const targetNetwork = networkOverride ?? deployResult.network ?? "mainnet"; + const ethers = await getEthers(targetNetwork); + + console.log(`Generating deployment attestation for ${envFlag} on ${targetNetwork}...`); + + // Collect all deployed addresses + const allContracts: Record; initializerArgs?: Record }> = {}; + + const upgradeTimestamp = resolveUpgradeTimestamp(config); + const cssvAddr = deployResult.cssvToken.address; + const proxyAddr = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + + // Implementations + const cooldownDuration = config.cooldownDuration ?? 604800; + const defaultOracleIds = config.defaultOracleIds ?? [1, 2, 3, 4]; + const quorumBps = config.quorumBps ?? 7500; + const skipInitializer = config.skipInitializer ?? false; + + allContracts["SSVNetworkSSVStakingUpgrade"] = { + address: deployResult.implementations.SSVNetworkSSVStakingUpgrade, + constructorArgs: {}, + ...(!skipInitializer && { + initializerArgs: { + function: "initializeSSVStaking(uint64,uint32[4],uint16)", + cooldownDuration: String(cooldownDuration), + defaultOracleIds: JSON.stringify(defaultOracleIds), + quorumBps: String(quorumBps), + }, + }), + }; + allContracts["SSVNetworkViews"] = { + address: deployResult.implementations.SSVNetworkViews, + constructorArgs: {}, + }; + + // CSSVToken + allContracts["CSSVToken"] = { + address: cssvAddr, + constructorArgs: deployResult.cssvToken.deployed + ? { ssvNetworkProxy: proxyAddr } + : {}, + }; + + // Modules — constructor args mirror deploy.ts + const moduleArgs: Record> = { + SSVOperators: { upgradeTimestamp: upgradeTimestamp.toString() }, + SSVClusters: {}, + SSVDAO: { cssvToken: cssvAddr }, + SSVViews: { cssvToken: cssvAddr }, + SSVOperatorsWhitelist: {}, + SSVStaking: { cssvToken: cssvAddr }, + SSVValidators: {}, + }; + + for (const [name, address] of Object.entries(deployResult.modules)) { + allContracts[name] = { + address, + constructorArgs: moduleArgs[name] ?? {}, + }; + } + + // Fetch bytecode hashes in parallel + console.log(`Fetching bytecode hashes for ${Object.keys(allContracts).length} contracts...`); + const entries = Object.entries(allContracts); + const hashes = await Promise.all( + entries.map(([name, { address }]) => + fetchBytecodeHash(ethers.provider, address).then( + (hash) => ({ name, hash, error: null }), + (err) => ({ name, hash: null, error: (err as Error).message }), + ), + ), + ); + + const contracts: Record = {}; + for (const { name, hash, error } of hashes) { + if (error) { + console.error(` ERROR fetching ${name}: ${error}`); + continue; + } + const entry = allContracts[name]; + contracts[name] = { + address: entry.address, + constructorArgs: entry.constructorArgs, + ...(entry.initializerArgs && { initializerArgs: entry.initializerArgs }), + bytecodeHash: hash!, + }; + console.log(` ${name}: ${hash}`); + } + + // Build attestation + const pp = config.protocolParams ?? {}; + const oracles = (config.oracles ?? {}) as Record; + + const attestation: Attestation = { + generatedAt: new Date().toISOString(), + deployment: { + deployer: deployResult.deployer, + chainId: deployResult.chainId, + network: deployResult.network, + deployedAt: deployResult.deployedAt, + blockNumber: deployResult.blockNumber, + }, + config: { + currentVersion: config.currentVersion, + targetVersion: config.targetVersion, + ssvNetworkProxy: config.ssvNetworkProxy, + ssvNetworkViews: config.ssvNetworkViews, + ssvToken: config.ssvToken, + cooldownDuration: Number(config.cooldownDuration ?? 604800), + upgradeTimestamp: Number(config.upgradeTimestamp ?? 0), + quorumBps: config.quorumBps ?? 7500, + defaultOracleIds: config.defaultOracleIds ?? [1, 2, 3, 4], + initialStakeAmount: String(config.initialStakeAmount ?? "0"), + protocolParams: Object.fromEntries( + Object.entries(pp).map(([k, v]) => [k, String(v)]), + ), + oracles, + }, + contracts, + }; + + // Write JSON attestation + const outputPath = join(resolveEnvDir(envFlag), "deployment-attestation.json"); + await writeFile(outputPath, `${JSON.stringify(attestation, null, 2)}\n`, "utf8"); + console.log(`\nAttestation written to: ${outputPath}`); + + // Compute file hashes for committee verification + const attestationContent = await readFile(outputPath, "utf8"); + const attestationFileHash = keccak256(new TextEncoder().encode(attestationContent)); + + const batchPath = join(resolveEnvDir(envFlag), "multisig-batch.json"); + let batchFileHash: string | null = null; + try { + const batchContent = await readFile(batchPath, "utf8"); + batchFileHash = keccak256(new TextEncoder().encode(batchContent)); + } catch { + console.warn(`Warning: ${batchPath} not found — run 'just generate-safe-batch' first to include its hash.`); + } + + // Print human-readable summary + console.log("\n" + "=".repeat(80)); + console.log("SSV Network Deployment Attestation"); + console.log("=".repeat(80)); + console.log(`Version: ${config.currentVersion} -> ${config.targetVersion}`); + console.log(`Network: ${deployResult.network} (chain ${deployResult.chainId})`); + console.log(`Deployer: ${deployResult.deployer}`); + console.log(`Deployed: ${deployResult.deployedAt}`); + console.log(`Block: ${deployResult.blockNumber}`); + console.log(""); + console.log("Deployed Contracts:"); + console.log("-".repeat(80)); + const nameWidth = Math.max(...Object.keys(contracts).map((n) => n.length)); + for (const [name, entry] of Object.entries(contracts)) { + console.log(` ${name.padEnd(nameWidth)} ${entry.address}`); + console.log(` ${"".padEnd(nameWidth)} bytecodeHash: ${entry.bytecodeHash}`); + if (Object.keys(entry.constructorArgs).length > 0) { + console.log( + ` ${"".padEnd(nameWidth)} args: ${JSON.stringify(entry.constructorArgs)}`, + ); + } + if (entry.initializerArgs) { + console.log( + ` ${"".padEnd(nameWidth)} initializer: ${JSON.stringify(entry.initializerArgs)}`, + ); + } + } + console.log(""); + console.log("Protocol Parameters:"); + console.log("-".repeat(80)); + for (const [key, value] of Object.entries(attestation.config.protocolParams)) { + console.log(` ${key}: ${value}`); + } + console.log(""); + console.log("Oracles:"); + console.log("-".repeat(80)); + for (const [id, addr] of Object.entries(oracles)) { + console.log(` Oracle ${id}: ${addr}`); + } + console.log(""); + console.log("File Hashes (keccak256 — for committee verification):"); + console.log("-".repeat(80)); + console.log(` deployment-attestation.json: ${attestationFileHash}`); + if (batchFileHash) { + console.log(` multisig-batch.json: ${batchFileHash}`); + } + console.log("=".repeat(80)); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/generate-safe-batch.ts b/scripts/generate-safe-batch.ts new file mode 100644 index 000000000..5c11d1d74 --- /dev/null +++ b/scripts/generate-safe-batch.ts @@ -0,0 +1,325 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { resolve, join } from "node:path"; +import { Interface } from "ethers"; +import { + type UpgradeConfig, + MODULE_ORDER, + parseOptionalArg, + parseQuorum, + normalizeOracles, + resolveDefaultOracleIds, + resolveProtocolParams, + resolveCooldownDuration, + parseUint, + requireAddress, + resolveConfigPath, + resolveDeployResultPath, + resolveEnvDir, +} from "./common/config.ts"; +import { SSVModules } from "./common/modules.ts"; + +type SafeTransaction = { + to: string; + value: string; + data: string; + contractMethod?: { + name: string; + inputs: Array<{ name: string; type: string }>; + }; + contractInputsValues?: Record; +}; + +type SafeBatchJson = { + version: string; + chainId: string; + createdAt: number; + meta: { + name: string; + description: string; + createdFromSafeAddress: string; + }; + transactions: SafeTransaction[]; +}; + +type DeployResultFile = { + implementations?: { + SSVNetworkSSVStakingUpgrade?: string; + SSVNetworkViews?: string; + }; + cssvToken?: { + address?: string; + }; + modules?: Record; + chainId?: string; +}; + +async function main() { + const envFlag = parseOptionalArg("env") ?? "mainnet"; + const configPath = resolveConfigPath(envFlag); + const deployResultPath = resolveDeployResultPath(envFlag); + + const configRaw = await readFile(configPath, "utf8"); + const config = JSON.parse(configRaw) as UpgradeConfig; + + let deployResult: DeployResultFile; + try { + const deployRaw = await readFile(deployResultPath, "utf8"); + deployResult = JSON.parse(deployRaw) as DeployResultFile; + } catch { + throw new Error( + `deploy-result.json not found at ${deployResultPath}. ` + + `Run 'just deploy ${envFlag}' first to deploy implementations and modules.` + ); + } + + const ssvNetworkProxy = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + const ssvNetworkViews = requireAddress(config.ssvNetworkViews, "ssvNetworkViews"); + const ownerAddr = config.owner ? requireAddress(config.owner, "owner") : ssvNetworkProxy; + + const stakingUpgradeImpl = deployResult.implementations?.SSVNetworkSSVStakingUpgrade; + const viewsImpl = deployResult.implementations?.SSVNetworkViews; + const modules = deployResult.modules ?? {}; + + if (!stakingUpgradeImpl) throw new Error("Missing SSVNetworkSSVStakingUpgrade in deploy-result.json"); + if (!viewsImpl) throw new Error("Missing SSVNetworkViews in deploy-result.json"); + + const params = resolveProtocolParams(config); + const cooldownDuration = resolveCooldownDuration(config); + const quorumBps = parseQuorum(config.quorumBps); + const oracles = normalizeOracles(config.oracles); + const defaultOracleIds = resolveDefaultOracleIds(config, oracles); + + const chainId = deployResult.chainId ?? "1"; + const transactions: SafeTransaction[] = []; + + // ── 1. Upgrade SSVNetwork proxy ── + const ssvNetworkIface = new Interface([ + "function upgradeTo(address newImplementation)", + "function upgradeToAndCall(address newImplementation, bytes data)", + "function updateModule(uint8 moduleId, address moduleAddress)", + "function updateNetworkFee(uint256 fee)", + "function updateNetworkFeeSSV(uint256 fee)", + "function updateLiquidationThresholdPeriod(uint64 blocks)", + "function updateLiquidationThresholdPeriodSSV(uint64 blocks)", + "function updateMinBlocksBetweenUpdates(uint32 blocks)", + "function updateMinimumLiquidationCollateral(uint256 amount)", + "function updateMinimumLiquidationCollateralSSV(uint256 amount)", + "function updateLiquidationThresholdPeriodSSV(uint64 blocks)", + "function updateDeclareOperatorFeePeriod(uint64 blocks)", + "function updateExecuteOperatorFeePeriod(uint64 blocks)", + "function updateOperatorFeeIncreaseLimit(uint64 percentage)", + "function updateMaximumOperatorFee(uint256 maxFee)", + "function updateMinimumOperatorEthFee(uint256 minFee)", + "function updateQuorumBps(uint16 quorumBps)", + "function updateUnstakeCooldownDuration(uint64 blocks)", + "function replaceOracle(uint32 oracleId, address oracleAddress)", + ]); + + const viewsIface = new Interface([ + "function upgradeTo(address newImplementation)", + ]); + + if (config.skipInitializer) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("upgradeTo", [stakingUpgradeImpl]), + }); + } else { + const stakingUpgradeIface = new Interface([ + "function initializeSSVStaking(uint64 cooldownDuration, uint32[4] defaultOracleIds, uint16 quorumBps)", + ]); + const initData = stakingUpgradeIface.encodeFunctionData("initializeSSVStaking", [ + cooldownDuration, + defaultOracleIds, + quorumBps, + ]); + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("upgradeToAndCall", [stakingUpgradeImpl, initData]), + }); + } + + // ── 2. updateModule for each module ── + for (const mod of MODULE_ORDER) { + const moduleAddr = modules[mod]; + if (!moduleAddr) { + console.warn(`Warning: no address for module ${mod} in deploy-result.json, skipping`); + continue; + } + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateModule", [SSVModules[mod], moduleAddr]), + }); + } + + // ── 3. Upgrade SSVNetworkViews ── + transactions.push({ + to: ssvNetworkViews, + value: "0", + data: viewsIface.encodeFunctionData("upgradeTo", [viewsImpl]), + }); + + // ── 4. Protocol parameter setters ── + if (params.networkFeeEth !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateNetworkFee", [params.networkFeeEth]), + }); + } + if (params.networkFeeSSV !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateNetworkFeeSSV", [params.networkFeeSSV]), + }); + } + if (params.liquidationThresholdPeriod !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateLiquidationThresholdPeriod", [params.liquidationThresholdPeriod]), + }); + } + if (params.liquidationThresholdPeriodSSV !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateLiquidationThresholdPeriodSSV", [params.liquidationThresholdPeriodSSV]), + }); + } + if (params.minBlocksBetweenUpdates !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateMinBlocksBetweenUpdates", [params.minBlocksBetweenUpdates]), + }); + } + if (params.minimumLiquidationCollateralEth !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateMinimumLiquidationCollateral", [ + params.minimumLiquidationCollateralEth, + ]), + }); + } + if (params.minimumLiquidationCollateralSSV !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateMinimumLiquidationCollateralSSV", [ + params.minimumLiquidationCollateralSSV, + ]), + }); + } + if (params.declareOperatorFeePeriod !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateDeclareOperatorFeePeriod", [params.declareOperatorFeePeriod]), + }); + } + if (params.executeOperatorFeePeriod !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateExecuteOperatorFeePeriod", [params.executeOperatorFeePeriod]), + }); + } + if (params.operatorFeeIncreaseLimit !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateOperatorFeeIncreaseLimit", [params.operatorFeeIncreaseLimit]), + }); + } + if (params.maxOperatorEthFee !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateMaximumOperatorFee", [params.maxOperatorEthFee]), + }); + } + if (params.minOperatorEthFee !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateMinimumOperatorEthFee", [params.minOperatorEthFee]), + }); + } + if (quorumBps !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateQuorumBps", [quorumBps]), + }); + } + if (params.unstakeCooldownDuration !== undefined) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("updateUnstakeCooldownDuration", [params.unstakeCooldownDuration]), + }); + } + + // ── 5. Oracle replacements ── + for (const { id, address } of oracles) { + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: ssvNetworkIface.encodeFunctionData("replaceOracle", [id, address]), + }); + } + + // ── 6. Initial SSV stake (approve + stake) ── + const initialStakeAmount = parseUint(config.initialStakeAmount, "initialStakeAmount"); + if (initialStakeAmount !== undefined && initialStakeAmount > 0n) { + const ssvTokenAddr = requireAddress(config.ssvToken, "ssvToken"); + const erc20Iface = new Interface([ + "function approve(address spender, uint256 amount)", + ]); + const stakingIface = new Interface([ + "function stake(uint256 amount)", + ]); + transactions.push({ + to: ssvTokenAddr, + value: "0", + data: erc20Iface.encodeFunctionData("approve", [ssvNetworkProxy, initialStakeAmount]), + }); + transactions.push({ + to: ssvNetworkProxy, + value: "0", + data: stakingIface.encodeFunctionData("stake", [initialStakeAmount]), + }); + } + + // ── Build SAFE Transaction Builder JSON ── + const batch: SafeBatchJson = { + version: "1.0", + chainId, + createdAt: Date.now(), + meta: { + name: `SSV Network ${config.targetVersion ?? "upgrade"} Upgrade (${envFlag})`, + description: `Upgrade SSVNetwork proxy, attach modules, set protocol parameters, and configure oracles for the ${envFlag} environment.`, + createdFromSafeAddress: ownerAddr, + }, + transactions, + }; + + const outputPath = join(resolveEnvDir(envFlag), "multisig-batch.json"); + await writeFile(outputPath, `${JSON.stringify(batch, null, 2)}\n`, "utf8"); + + console.log(`SAFE Transaction Builder batch generated: ${outputPath}`); + console.log(`Total transactions: ${transactions.length}`); + console.log(`Chain ID: ${chainId}`); + console.log(`Owner (SAFE address): ${ownerAddr}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/run-forked-tests.ts b/scripts/run-forked-tests.ts new file mode 100644 index 000000000..6b47fe0ed --- /dev/null +++ b/scripts/run-forked-tests.ts @@ -0,0 +1,97 @@ +import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { JsonRpcProvider } from "ethers"; +import { parseArg } from "./common/helpers.ts"; +import { + parseOptionalArg, + resolveUpgradeResultPath, +} from "./common/config.ts"; +import { + type ForkConfigFile, + resolveSourceRpcUrl, + preflightSourceRpc, + buildForkTestEnv, +} from "./common/fork-test.ts"; + +async function main() { + // Support both --env and --config + const envFlag = parseOptionalArg("env"); + let configPath: string; + if (envFlag) { + configPath = resolveUpgradeResultPath(envFlag); + } else { + configPath = resolve(parseArg("config")); + } + + const testPath = parseOptionalArg("test") ?? "test/test-forked/v2.0.0/fullIntegrationForked.test.ts"; + const forkNetwork = parseOptionalArg("fork-network") ?? "hardhat_forked"; + const useDeployedState = parseOptionalArg("use-deployed-state") ?? "true"; + const noGasEnforce = parseOptionalArg("no-gas-enforce") ?? "true"; + const strictDeployedState = parseOptionalArg("strict-deployed-state") ?? "false"; + const allowDeployedFallback = parseOptionalArg("allow-deployed-fallback") ?? "true"; + const forkBlockNumberArg = parseOptionalArg("fork-block-number"); + + const rawConfig = await readFile(configPath, "utf8"); + const config = JSON.parse(rawConfig) as ForkConfigFile; + const envForkBlockNumber = process.env.FORK_BLOCK_NUMBER?.trim(); + let forkBlockNumber = forkBlockNumberArg ?? (envForkBlockNumber && envForkBlockNumber.length > 0 ? envForkBlockNumber : undefined); + if (!forkBlockNumber) { + const provider = new JsonRpcProvider(resolveSourceRpcUrl()); + forkBlockNumber = String(await provider.getBlockNumber()); + } + + if (useDeployedState === "true") { + if (strictDeployedState === "true" || allowDeployedFallback === "false") { + await preflightSourceRpc(config); + } else { + try { + await preflightSourceRpc(config); + } catch (err: any) { + const message = err?.message ?? String(err); + console.warn(`[FORK] Source-RPC preflight failed, continuing because fallback is enabled: ${message}`); + } + } + } + + const env = buildForkTestEnv(config, { + configPath, + forkNetwork, + useDeployedState, + strictDeployedState, + allowDeployedFallback, + noGasEnforce, + forkBlockNumber: forkBlockNumber ?? "", + }); + + const args = ["hardhat", "test", testPath]; + console.log(`Running forked tests via: npx ${args.join(" ")}`); + console.log(`FORK_TEST_NETWORK=${forkNetwork}`); + console.log(`FORK_CONFIG_PATH=${configPath}`); + console.log(`FORK_USE_DEPLOYED_STATE=${useDeployedState}`); + console.log(`FORK_STRICT_DEPLOYED_STATE=${strictDeployedState}`); + console.log(`FORK_ALLOW_DEPLOYED_FALLBACK=${allowDeployedFallback}`); + console.log(`NO_GAS_ENFORCE=${noGasEnforce}`); + console.log(`FORK_BLOCK_NUMBER=${forkBlockNumber}`); + + await new Promise((resolvePromise, rejectPromise) => { + const child = spawn("npx", args, { + stdio: "inherit", + env, + }); + + child.on("error", rejectPromise); + child.on("close", (code) => { + if (code === 0) { + resolvePromise(); + return; + } + rejectPromise(new Error(`Forked tests failed with exit code ${code}`)); + }); + }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/smoke-test.ts b/scripts/smoke-test.ts new file mode 100644 index 000000000..28af106a1 --- /dev/null +++ b/scripts/smoke-test.ts @@ -0,0 +1,528 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { getEthers } from "./common/helpers.ts"; +import { + type UpgradeConfig, + parseOptionalArg, + requireAddress, + resolveConfigPath, + resolveNetworkFromEnv, +} from "./common/config.ts"; +import { + canImpersonateOnNetwork, + getSignerForAddress, + resolveRpcUrl, +} from "./common/impersonation.ts"; + +const STEP = (n: number, msg: string) => console.log(`[${n}/20] ${msg}`); + +const SSV_FOUNDATION = "0xeC29418bc30FED20dE85706F32c7D77Da0be7afB"; +// Randomize per run so reruns against the same fork don't collide on OperatorAlreadyExists / validator pk reuse. +const RUN_NONCE = Math.floor(Math.random() * 0x7fff_ffff); +const OP_SEED = RUN_NONCE; +const VAL_SEED = RUN_NONCE + 100_000; +const DEFAULT_SHARES = "0x1234"; +const EB_PER_VALIDATOR = 64; // ETH per validator — 2× default (32); keeps post-bump burn rate modest +const POST_EB_HEADROOM_BLOCKS = 1_000n; // Keep only a small deterministic runway after the EB bump +const ETH_DEDUCTED_DIGITS = 100_000n; +const BPS = 10_000n; +const OP_EARNINGS_BLOCKS = 2; +const STAKING_REWARD_BLOCKS = 2; + +type Cluster = { + validatorCount: number | bigint; + networkFeeIndex: bigint; + index: bigint; + active: boolean; + balance: bigint; +}; + +const EMPTY_CLUSTER: Cluster = { + validatorCount: 0, + networkFeeIndex: 0n, + index: 0n, + active: true, + balance: 0n, +}; + +function makeKey(seed: number): string { + return `0x${seed.toString(16).padStart(96, "0")}`; +} + +function clusterFromEvent(log: any): Cluster { + const c = log.args.cluster; + return { + validatorCount: Number(c.validatorCount), + networkFeeIndex: BigInt(c.networkFeeIndex), + index: BigInt(c.index), + active: c.active, + balance: BigInt(c.balance), + }; +} + +function findEventLog(receipt: any, iface: any, name: string): any { + for (const log of receipt.logs) { + try { + const parsed = iface.parseLog({ topics: [...log.topics], data: log.data }); + if (parsed && parsed.name === name) return { ...log, args: parsed.args }; + } catch { + // skip + } + } + throw new Error(`Event ${name} not found in receipt`); +} + +function doubleHashLeaf(ethers: any, clusterId: string, effectiveBalance: number): string { + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ["bytes32", "uint32"], + [clusterId, effectiveBalance], + ); + return ethers.keccak256(ethers.keccak256(encoded)); +} + +function computeClusterId(ethers: any, owner: string, opIds: bigint[]): string { + return ethers.keccak256(ethers.solidityPacked(["address", "uint64[]"], [owner, opIds])); +} + +function maxBigInt(a: bigint, b: bigint): bigint { + return a > b ? a : b; +} + +async function mineBlocks(provider: any, n: number) { + if (n <= 0) return; + const hex = `0x${n.toString(16)}`; + // Pass interval=0x0 so all N blocks share a timestamp → O(1) fast-forward, no per-block loop. + const ok = + (await trySend(provider, "hardhat_mine", [hex, "0x0"])) || + (await trySend(provider, "hardhat_mine", [hex])) || + (await trySend(provider, "anvil_mine", [hex, "0x0"])) || + (await trySend(provider, "anvil_mine", [hex])); + if (!ok) throw new Error("mine not supported"); +} + +async function increaseTime(provider: any, seconds: number) { + await provider.send("evm_increaseTime", [seconds]); + await provider.send("evm_mine", []); +} + +async function trySend(provider: any, method: string, params: unknown[]): Promise { + try { + await provider.send(method, params); + return true; + } catch { + return false; + } +} + +async function main() { + const envFlag = parseOptionalArg("env"); + const configFlag = parseOptionalArg("config"); + + let initConfigPath: string; + if (envFlag) { + initConfigPath = resolveConfigPath(envFlag); + } else if (configFlag) { + initConfigPath = resolve(configFlag); + } else { + throw new Error("Provide --env or --config "); + } + + const targetNetwork = parseOptionalArg("network") ?? resolveNetworkFromEnv(envFlag) ?? "local"; + const rpcUrl = resolveRpcUrl(targetNetwork); + if (!canImpersonateOnNetwork(targetNetwork, rpcUrl)) { + throw new Error( + `Smoke test requires a fork (local/hardhat) network — got '${targetNetwork}'. ` + + `Run your mainnet fork locally and pass '--network local' (or omit --network for default).`, + ); + } + + const raw = await readFile(initConfigPath, "utf8"); + const config = JSON.parse(raw) as UpgradeConfig & { + ssvToken: string; + cssvToken?: string; + oracles: Record; + }; + + const ssvNetworkProxy = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + const ssvNetworkViews = requireAddress(config.ssvNetworkViews, "ssvNetworkViews"); + const ssvTokenAddr = requireAddress(config.ssvToken, "ssvToken"); + const cssvTokenAddr = requireAddress(config.cssvToken!, "cssvToken"); + + const ethers = await getEthers(targetNetwork); + + STEP(1, "Pre-flight: version + module wiring"); + const code = await ethers.provider.getCode(ssvNetworkProxy); + if (code === "0x") throw new Error(`No code at ssvNetworkProxy ${ssvNetworkProxy}`); + + const network = await ethers.getContractAt("SSVNetwork", ssvNetworkProxy); + const views = await ethers.getContractAt("SSVNetworkViews", ssvNetworkViews); + const erc20Abi = [ + "function balanceOf(address) view returns (uint256)", + "function transfer(address to, uint256 amount) returns (bool)", + "function approve(address spender, uint256 amount) returns (bool)", + ]; + const ssvToken = await ethers.getContractAt(erc20Abi, ssvTokenAddr); + const cssvToken = await ethers.getContractAt(erc20Abi, cssvTokenAddr); + + const onChainVersion = await network.getVersion(); + if (onChainVersion !== config.targetVersion) { + throw new Error( + `Version mismatch: config.targetVersion='${config.targetVersion}' but proxy='${onChainVersion}'. Did you run upgrade first?`, + ); + } + console.log(` version = ${onChainVersion} ✓`); + + // Pick clean EOAs — hardhat's default signer addresses can collide with real mainnet contract code + // on a fork, which makes ETH payouts (e.g. claimEthRewards) revert with ETHTransferFailed. + // Derive fresh wallets deterministically from the run nonce and fund them via setBalance. + async function freshFundedEoa(seed: number) { + // Deterministic but run-unique private key + const priv = ethers.keccak256( + ethers.toUtf8Bytes(`smoke-test:${RUN_NONCE}:${seed}`), + ); + const w = new (ethers as any).Wallet(priv, ethers.provider); + await ethers.provider.send("hardhat_setBalance", [w.address, "0x56bc75e2d63100000"]); // 100 ETH + const code = await ethers.provider.getCode(w.address); + if (code !== "0x") { + // Extremely unlikely but guard anyway + throw new Error(`Generated EOA ${w.address} happens to have code on fork — rerun smoke test`); + } + return w; + } + async function freshEthReceivableEoa(startSeed: number, probeSender: any) { + for (let offset = 0; offset < 32; offset++) { + const candidate = await freshFundedEoa(startSeed + offset); + try { + const before = BigInt(await ethers.provider.getBalance(candidate.address)); + const probeTx = await probeSender.sendTransaction({ to: candidate.address, value: 1n }); + await probeTx.wait(); + const after = BigInt(await ethers.provider.getBalance(candidate.address)); + if (after >= before + 1n) return candidate; + } catch { + // Try the next candidate if the ETH receive probe fails on this fork state. + } + } + throw new Error("Failed to find a fresh ETH-receivable EOA for staking rewards"); + } + const clusterOwner = await freshFundedEoa(1); + const liquidator = await freshFundedEoa(2); + const staker = await freshEthReceivableEoa(3, clusterOwner); + const opOwner = clusterOwner; + console.log(` clusterOwner=${clusterOwner.address} liquidator=${liquidator.address} staker=${staker.address}`); + + const minOpFee = BigInt(await views.getMinimumOperatorEthFee()); + const networkFee = BigInt(await views.getNetworkFee()); + const liqThreshold = BigInt(await views.getLiquidationThresholdPeriod()); + const minLiqCollateral = BigInt(await views.getMinimumLiquidationCollateral()); + + STEP(2, "Register 4 fresh public operators"); + const operatorIds: bigint[] = []; + for (let i = 0; i < 4; i++) { + const key = makeKey(OP_SEED + i); + const id: bigint = await network.connect(opOwner).registerOperator.staticCall(key, minOpFee, false); + await (await network.connect(opOwner).registerOperator(key, minOpFee, false)).wait(); + operatorIds.push(id); + } + console.log(` operatorIds = [${operatorIds.join(", ")}]`); + + STEP(3, "Register first validator (creates new ETH cluster)"); + // Size deposit for the POST-bump liquidation target at the final validator count (2). + // Liquidation checks use: + // max(minLiquidationCollateral, burnRate * minimumBlocksBeforeLiquidation) + // so we only need a small deterministic headroom above that target. + const finalValidatorCount = 2n; // after register + bulk(+2) + removeOne + const postBumpVUnitsTotal = BigInt(Math.ceil((Number(finalValidatorCount) * EB_PER_VALIDATOR * 10_000) / 32)); + const burnPerBlockPerVUnit = minOpFee * 4n + networkFee; // wei per block per 1 vUnit + const postBumpBurnRateWei = burnPerBlockPerVUnit * postBumpVUnitsTotal / BPS; + const postBumpLiquidationTarget = maxBigInt( + minLiqCollateral, + postBumpBurnRateWei * liqThreshold, + ); + let deposit = postBumpLiquidationTarget + postBumpBurnRateWei * POST_EB_HEADROOM_BLOCKS; + deposit = ((deposit + ETH_DEDUCTED_DIGITS - 1n) / ETH_DEDUCTED_DIGITS) * ETH_DEDUCTED_DIGITS; + console.log( + ` postBumpBurnRate=${postBumpBurnRateWei}/blk target=${postBumpLiquidationTarget} deposit=${deposit}`, + ); + + const pk0 = makeKey(VAL_SEED); + const regTx = await network + .connect(clusterOwner) + .registerValidator(pk0, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: deposit }); + let regReceipt = await regTx.wait(); + let cluster: Cluster = clusterFromEvent(findEventLog(regReceipt, network.interface, "ValidatorAdded")); + console.log(` deposit=${deposit} balance=${cluster.balance} validatorCount=${cluster.validatorCount}`); + + STEP(4, "Deposit more ETH into the cluster"); + const topUp = ETH_DEDUCTED_DIGITS; + const depTx = await network + .connect(clusterOwner) + .deposit(clusterOwner.address, operatorIds, cluster, { value: topUp }); + const depReceipt = await depTx.wait(); + cluster = clusterFromEvent(findEventLog(depReceipt, network.interface, "ClusterDeposited")); + + STEP(5, "Bulk register +2 validators"); + const bulkKeys = [makeKey(VAL_SEED + 1), makeKey(VAL_SEED + 2)]; + const bulkShares = [DEFAULT_SHARES, DEFAULT_SHARES]; + const bulkValue = ETH_DEDUCTED_DIGITS; + const bulkTx = await network + .connect(clusterOwner) + .bulkRegisterValidator(bulkKeys, operatorIds, bulkShares, cluster, { value: bulkValue }); + const bulkReceipt = await bulkTx.wait(); + if (!bulkReceipt) throw new Error("bulkRegisterValidator: null receipt"); + // The last ValidatorAdded log carries the up-to-date cluster + let lastAddedArgs: any = null; + for (const log of [...bulkReceipt.logs].reverse()) { + try { + const parsed = network.interface.parseLog({ topics: [...log.topics], data: log.data }); + if (parsed?.name === "ValidatorAdded") { + lastAddedArgs = parsed.args; + break; + } + } catch { + // skip + } + } + if (!lastAddedArgs) throw new Error("No ValidatorAdded log in bulkRegisterValidator receipt"); + cluster = clusterFromEvent({ args: lastAddedArgs }); + console.log(` validatorCount = ${cluster.validatorCount}`); + + STEP(6, "Remove one validator"); + const rmTx = await network.connect(clusterOwner).removeValidator(bulkKeys[0], operatorIds, cluster); + const rmReceipt = await rmTx.wait(); + cluster = clusterFromEvent(findEventLog(rmReceipt, network.interface, "ValidatorRemoved")); + console.log(` validatorCount = ${cluster.validatorCount}`); + + STEP(7, "Exit remaining bulk validator (signal)"); + await (await network.connect(clusterOwner).exitValidator(bulkKeys[1], operatorIds)).wait(); + + STEP(8, "Declare → wait → execute operator fee"); + const periods = await views.getOperatorFeePeriods(); + const declarePeriod = Number(periods.declarePeriod); + const newFee = minOpFee + (minOpFee / 100n); // +1% — small bump within limit + // Pack-align + const newFeeAligned = (newFee / ETH_DEDUCTED_DIGITS) * ETH_DEDUCTED_DIGITS; + await (await network.connect(opOwner).declareOperatorFee(operatorIds[0], newFeeAligned)).wait(); + await increaseTime(ethers.provider, declarePeriod + 1); + await (await network.connect(opOwner).executeOperatorFee(operatorIds[0])).wait(); + + STEP(9, "Withdraw all operator earnings (ETH)"); + // A couple of blocks is enough to make earnings non-zero on the fork; large + // block jumps make Anvil do unnecessary work here. + await mineBlocks(ethers.provider, OP_EARNINGS_BLOCKS); + const earnings0 = BigInt(await views.getOperatorEarnings(operatorIds[0])); + if (earnings0 > 0n) { + await (await network.connect(opOwner).withdrawAllOperatorEarnings(operatorIds[0])).wait(); + console.log(` withdrew ${earnings0} wei from op ${operatorIds[0]}`); + } else { + console.log(` (no earnings yet, skipped withdraw)`); + } + + STEP(10, "Impersonate SSV Foundation, transfer + approve + stake"); + const stakeAmount = 10n * 10n ** 18n; // 10 SSV + const { signer: foundation } = await getSignerForAddress(ethers, SSV_FOUNDATION, true); + const foundationBalance = BigInt(await ssvToken.balanceOf(SSV_FOUNDATION)); + if (foundationBalance < stakeAmount) { + throw new Error(`Foundation has only ${foundationBalance} SSV, need ${stakeAmount}`); + } + await (await (ssvToken.connect(foundation) as any).transfer(staker.address, stakeAmount)).wait(); + const stakerSsvBeforeStake = BigInt(await ssvToken.balanceOf(staker.address)); + const stakerCssvBeforeStake = BigInt(await cssvToken.balanceOf(staker.address)); + const contractSsvBeforeStake = BigInt(await ssvToken.balanceOf(ssvNetworkProxy)); + await (await (ssvToken.connect(staker) as any).approve(ssvNetworkProxy, stakeAmount)).wait(); + await (await network.connect(staker).stake(stakeAmount)).wait(); + const cssvBal = BigInt(await cssvToken.balanceOf(staker.address)); + const stakerSsvAfterStake = BigInt(await ssvToken.balanceOf(staker.address)); + const contractSsvAfterStake = BigInt(await ssvToken.balanceOf(ssvNetworkProxy)); + const cssvMinted = cssvBal - stakerCssvBeforeStake; + if (cssvMinted !== stakeAmount) { + throw new Error(`Stake minted ${cssvMinted} cSSV, expected ${stakeAmount}`); + } + if (stakerSsvBeforeStake - stakerSsvAfterStake !== stakeAmount) { + throw new Error( + `Stake debited ${stakerSsvBeforeStake - stakerSsvAfterStake} SSV from staker, expected ${stakeAmount}`, + ); + } + if (contractSsvAfterStake - contractSsvBeforeStake !== stakeAmount) { + throw new Error( + `Stake credited ${contractSsvAfterStake - contractSsvBeforeStake} SSV to contract, expected ${stakeAmount}`, + ); + } + console.log(` staker=${staker.address} staked=${stakeAmount} cssvMinted=${cssvMinted}`); + + STEP(11, "Accrue protocol fees + check accEthPerShare > 0"); + // syncFees() updates the staking accumulator directly; no need to touch the cluster + // or mine hundreds of blocks just to produce a claimable amount. + await mineBlocks(ethers.provider, STAKING_REWARD_BLOCKS); + await (await network.connect(staker).syncFees()).wait(); + const acc = BigInt(await views.accEthPerShare()); + console.log(` accEthPerShare = ${acc}`); + if (acc === 0n) console.log(` ⚠ accEthPerShare still 0 — network fee may not have accrued yet`); + + STEP(12, "Claim ETH rewards"); + const claimable = BigInt(await views.previewClaimableEth(staker.address)); + const expectedMinPayout = claimable - (claimable % ETH_DEDUCTED_DIGITS); + const stakerEthBeforeClaim = BigInt(await ethers.provider.getBalance(staker.address)); + const proxyEthBalBefore = BigInt(await ethers.provider.getBalance(ssvNetworkProxy)); + console.log(` claimable=${claimable} proxyETH=${proxyEthBalBefore}`); + if (claimable > 0n) { + // Probe with staticCall to surface the exact custom-error selector. + try { + await network.connect(staker).claimEthRewards.staticCall(); + } catch (e: any) { + console.error(` claimEthRewards staticCall reverted — data=${e?.data ?? "(none)"}`); + throw e; + } + const claimReceipt = await (await network.connect(staker).claimEthRewards()).wait(); + const stakerEthAfterClaim = BigInt(await ethers.provider.getBalance(staker.address)); + const claimGas = claimReceipt.gasUsed * claimReceipt.gasPrice; + const claimedDelta = stakerEthAfterClaim + claimGas - stakerEthBeforeClaim; + const claimableAfter = BigInt(await views.previewClaimableEth(staker.address)); + if (claimedDelta % ETH_DEDUCTED_DIGITS !== 0n) { + throw new Error(`Claim transferred non-packed amount ${claimedDelta}`); + } + if (claimedDelta < expectedMinPayout) { + throw new Error(`Claim transferred ${claimedDelta} wei, below preview floor ${expectedMinPayout}`); + } + if (claimableAfter >= claimable) { + throw new Error(`Post-claim preview=${claimableAfter}, expected it to decrease from ${claimable}`); + } + console.log(` claimed = ${claimedDelta} wei`); + } else { + console.log(` (nothing claimable yet)`); + } + + STEP(13, "Oracle commits EB root (3-of-4 quorum) with higher EB"); + const validatorCount = Number(cluster.validatorCount); + if (validatorCount === 0) throw new Error("Cluster has no validators — cannot commit EB"); + // effectiveBalance is the cluster's TOTAL EB in ETH (must be >= validatorCount * 32) + const totalEB = validatorCount * EB_PER_VALIDATOR; + const clusterId = computeClusterId(ethers, clusterOwner.address, operatorIds); + const root = doubleHashLeaf(ethers, clusterId, totalEB); + const latestBlockNum = await ethers.provider.getBlockNumber(); + const commitBlockNum = latestBlockNum; // strictly monotonic — first commit + + const oracleAddrs = [ + config.oracles["1"], + config.oracles["2"], + config.oracles["3"], + ].map((a) => requireAddress(a, "oracle")); + const oracleSigners = await Promise.all( + oracleAddrs.map((a) => getSignerForAddress(ethers, a, true).then((s) => s.signer)), + ); + + await (await network.connect(oracleSigners[0]).commitRoot(root, commitBlockNum)).wait(); + await (await network.connect(oracleSigners[1]).commitRoot(root, commitBlockNum)).wait(); + await (await network.connect(oracleSigners[2]).commitRoot(root, commitBlockNum)).wait(); + console.log(` committed root=${root.slice(0, 10)}... blockNum=${commitBlockNum} totalEB=${totalEB} ETH (${validatorCount} × ${EB_PER_VALIDATOR})`); + + STEP(14, "updateClusterBalance with the committed EB"); + // Read actual current burn rate so we know settle burn accurately. + const preUpdateBurnRate = BigInt(await views.getBurnRate(clusterOwner.address, operatorIds, cluster)); + const preUpdateBalance = BigInt(await views.getBalance(clusterOwner.address, operatorIds, cluster)); + console.log(` pre-update: burnRate=${preUpdateBurnRate}/blk getBalance=${preUpdateBalance}`); + const ubTx = await network + .connect(clusterOwner) + .updateClusterBalance(commitBlockNum, clusterOwner.address, operatorIds, cluster, totalEB, []); + const ubReceipt = await ubTx.wait(); + if (!ubReceipt) throw new Error("updateClusterBalance: null receipt"); + cluster = clusterFromEvent(findEventLog(ubReceipt, network.interface, "ClusterBalanceUpdated")); + console.log(` post-EB balance=${cluster.balance} active=${cluster.active}`); + if (!cluster.active) { + throw new Error( + `Cluster auto-liquidated inside updateClusterBalance (EB=${EB_PER_VALIDATOR} was too high). ` + + `Lower EB_PER_VALIDATOR so a positive balance remains and step 16 can exercise third-party liquidate.`, + ); + } + if (cluster.balance === 0n) { + throw new Error( + `Cluster balance drained to 0 by the EB settle (EB=${EB_PER_VALIDATOR} too high). Lower EB_PER_VALIDATOR.`, + ); + } + + STEP(15, "Mine blocks until cluster is liquidatable (computed jump)"); + // Liquidation occurs once balance drops below: + // max(minLiquidationCollateral, burnRate * minimumBlocksBeforeLiquidation) + const burnRate = BigInt(await views.getBurnRate(clusterOwner.address, operatorIds, cluster)); + if (burnRate === 0n) throw new Error("burnRate is 0 — cannot reach liquidation"); + const liquidationTarget = maxBigInt(minLiqCollateral, burnRate * liqThreshold); + const blocksNeeded = + cluster.balance > liquidationTarget + ? (cluster.balance - liquidationTarget) / burnRate + 20n + : 20n; + console.log( + ` burnRate=${burnRate}/blk, target=${liquidationTarget}, balance=${cluster.balance}, jumping ${blocksNeeded} blocks`, + ); + if (blocksNeeded > 0n) await mineBlocks(ethers.provider, Number(blocksNeeded)); + const preLiqSettledBalance = BigInt(await views.getBalance(clusterOwner.address, operatorIds, cluster)); + const liquidatable = await views.isLiquidatable(clusterOwner.address, operatorIds, cluster); + if (!liquidatable) throw new Error(`Cluster not liquidatable after jumping ${blocksNeeded} blocks`); + console.log(` liquidatable ✓`); + + STEP(16, "Third-party liquidates (receives the cluster balance bounty)"); + const liqBalBefore = await ethers.provider.getBalance(liquidator.address); + const liqTx = await network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, cluster); + const liqReceipt = await liqTx.wait(); + if (!liqReceipt) throw new Error("liquidate: null receipt"); + cluster = clusterFromEvent(findEventLog(liqReceipt, network.interface, "ClusterLiquidated")); + const liqBalAfter = await ethers.provider.getBalance(liquidator.address); + const gas = liqReceipt.gasUsed * liqReceipt.gasPrice; + const bounty = liqBalAfter - liqBalBefore + gas; + if (bounty <= 0n) throw new Error(`Liquidation bounty must be positive, got ${bounty}`); + if (bounty > preLiqSettledBalance) { + throw new Error(`Liquidation bounty ${bounty} exceeds pre-liquidation balance ${preLiqSettledBalance}`); + } + if (cluster.active) throw new Error("Cluster should be inactive after liquidation"); + if (cluster.balance !== 0n) throw new Error(`Liquidated cluster should have zero balance, got ${cluster.balance}`); + console.log(` liquidated — bounty ≈ ${bounty} wei, cluster.active=${cluster.active}`); + + STEP(17, "Request unstake → wait cooldown → withdrawUnlocked"); + const cooldown = Number(await views.cooldownDuration()); + const stakedBal = BigInt(await views.stakedBalanceOf(staker.address)); + const unstakeAmt = stakedBal; // full unstake + await (await network.connect(staker).requestUnstake(unstakeAmt)).wait(); + await increaseTime(ethers.provider, cooldown + 1); + const ssvBalBefore = BigInt(await ssvToken.balanceOf(staker.address)); + await (await network.connect(staker).withdrawUnlocked()).wait(); + const ssvBalAfter = BigInt(await ssvToken.balanceOf(staker.address)); + console.log(` unstaked ${ssvBalAfter - ssvBalBefore} SSV back to user`); + + STEP(18, "ETH conservation check"); + const contractEth = BigInt(await ethers.provider.getBalance(ssvNetworkProxy)); + console.log(` SSVNetwork ETH balance = ${contractEth} wei`); + if (contractEth <= 0n) throw new Error("SSVNetwork ETH balance should remain positive"); + if (cluster.active) throw new Error("Cluster should remain inactive after liquidation"); + // Contract ETH must be ≥ 0 and non-negative accounting components. + // Full-precision invariant (sum of cluster/operator/network earnings == contractEth) would require + // iterating all clusters — instead we assert contract is solvent and the post-liquidation cluster is zeroed. + if (cluster.balance !== 0n) throw new Error(`Liquidated cluster still has balance ${cluster.balance}`); + const netEarnings = BigInt(await views.getNetworkEarnings()); + console.log(` network ETH earnings = ${netEarnings} wei (accrued)`); + + STEP(19, "SSV conservation check"); + const contractSsv = BigInt(await ssvToken.balanceOf(ssvNetworkProxy)); + const totalStaked = BigInt(await views.totalStaked()); + const stakerCssvAfterUnstake = BigInt(await cssvToken.balanceOf(staker.address)); + const stakerStakedBalanceAfterUnstake = BigInt(await views.stakedBalanceOf(staker.address)); + const pendingUnstakeAfter = await views.pendingUnstake(staker.address); + console.log(` SSVNetwork SSV balance = ${contractSsv}, totalStaked = ${totalStaked}`); + if (stakerCssvAfterUnstake !== 0n) { + throw new Error(`Staker still has ${stakerCssvAfterUnstake} cSSV after full unstake`); + } + if (stakerStakedBalanceAfterUnstake !== 0n) { + throw new Error( + `stakedBalanceOf(staker)=${stakerStakedBalanceAfterUnstake} after full unstake, expected 0`, + ); + } + if (pendingUnstakeAfter.length !== 0) { + throw new Error(`pendingUnstake(staker) still has ${pendingUnstakeAfter.length} entries after withdrawUnlocked`); + } + + STEP(20, "SMOKE TEST PASSED ✓"); + console.log(`\n SSV Network v${onChainVersion} smoke test completed successfully on ${targetNetwork}.`); +} + +main().catch((err) => { + console.error("\n✗ SMOKE TEST FAILED"); + console.error(err); + process.exit(1); +}); diff --git a/scripts/upgrade.ts b/scripts/upgrade.ts new file mode 100644 index 000000000..f41d59d80 --- /dev/null +++ b/scripts/upgrade.ts @@ -0,0 +1,393 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { deployContract, getDeployer, getEthers } from "./common/helpers.ts"; +import { SSVModules } from "./common/modules.ts"; +import { + type UpgradeConfig, + type ModuleAddresses, + MODULE_ORDER, + parseOptionalArg, + parseOptionalBooleanArg, + requireAddress, + parseQuorum, + normalizeOracles, + resolveDefaultOracleIds, + toOracleConfig, + resolveProtocolParams, + resolveCooldownDuration, + resolveUpgradeTimestamp, + bigintToJsonNumberOrString, + resolveDeployedConfigPath, + resolveConfigPath, + resolveUpgradeResultPath, + resolveVersionedUpgradeResultPath, + resolveNetworkFromEnv, + updateLatestSymlink, + loadDeployResult, +} from "./common/config.ts"; +import { readOnChainValues } from "./common/verify.ts"; +import { + getSignerForAddress, + canImpersonateOnNetwork, + resolveRpcUrl, +} from "./common/impersonation.ts"; + +async function main() { + // ── Resolve config source ── + // New: --env flag resolves to deployments//config.json + // Legacy: --config flag for direct path (backward compat) + const envFlag = parseOptionalArg("env"); + const configFlag = parseOptionalArg("config"); + const forkFlag = parseOptionalBooleanArg("fork", false); + const useGetImpersonatedSigner = parseOptionalBooleanArg("use-get-impersonated-signer", true); + + let initConfigPath: string; + let resultPath: string; + + if (envFlag) { + initConfigPath = resolveConfigPath(envFlag); + resultPath = resolveUpgradeResultPath(envFlag); + } else if (configFlag) { + initConfigPath = resolve(configFlag); + resultPath = resolveDeployedConfigPath(initConfigPath, parseOptionalArg("output-config")); + } else { + throw new Error("Provide --env or --config to specify the config source"); + } + + const targetNetwork = parseOptionalArg("network") ?? resolveNetworkFromEnv(envFlag); + if (!targetNetwork && !forkFlag) { + throw new Error("Provide --network or --fork to specify the target network"); + } + + const raw = await readFile(initConfigPath, "utf8"); + const config = JSON.parse(raw) as UpgradeConfig; + + const ssvNetworkProxy = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + const ssvNetworkViews = requireAddress(config.ssvNetworkViews, "ssvNetworkViews"); + const ssvToken = requireAddress(config.ssvToken, "ssvToken"); + + const params = resolveProtocolParams(config); + const cooldownDuration = resolveCooldownDuration(config); + const upgradeTimestamp = resolveUpgradeTimestamp(config); + const quorumBps = parseQuorum(config.quorumBps); + const oracles = normalizeOracles(config.oracles); + const defaultOracleIds = resolveDefaultOracleIds(config, oracles); + + // ── Determine network and mode ── + const effectiveNetwork = forkFlag ? (targetNetwork ?? "local") : (targetNetwork ?? "local"); + const ethers = await getEthers(effectiveNetwork); + const providerNetwork = await ethers.provider.getNetwork(); + + const networkCode = await ethers.provider.getCode(ssvNetworkProxy); + if (networkCode === "0x") { + throw new Error( + `No contract code at ssvNetworkProxy ${ssvNetworkProxy} on ${effectiveNetwork}. ` + + `Check your RPC URL and fork/network selection.` + ); + } + const viewsCode = await ethers.provider.getCode(ssvNetworkViews); + if (viewsCode === "0x") { + throw new Error( + `No contract code at ssvNetworkViews ${ssvNetworkViews} on ${effectiveNetwork}. ` + + `Check your RPC URL and fork/network selection.` + ); + } + + const network = await ethers.getContractAt("SSVNetwork", ssvNetworkProxy); + const viewsProxy = await ethers.getContractAt("SSVNetworkViews", ssvNetworkViews); + + // ── Version pre-flight check ── + let onChainVersion: string; + try { + onChainVersion = await network.getVersion(); + } catch { + throw new Error(`Could not read on-chain version from proxy ${ssvNetworkProxy}`); + } + if (onChainVersion !== config.currentVersion) { + throw new Error( + `Version mismatch: config.currentVersion is "${config.currentVersion}" but proxy reports "${onChainVersion}". ` + + `Wrong config or proxy address?` + ); + } + console.log(`[PRE-FLIGHT] currentVersion = ${onChainVersion} ✓`); + + // ── targetVersion pre-flight: parse version from CoreLib.sol source (no deployment, no gas) ── + { + const coreLibPath = resolve(process.cwd(), "contracts/libraries/CoreLib.sol"); + const coreLibSrc = await readFile(coreLibPath, "utf8"); + const match = coreLibSrc.match(/function getVersion\(\)[^{]*\{\s*return\s*"([^"]+)"/); + if (!match) { + throw new Error("Could not parse version from CoreLib.sol — check getVersion() format"); + } + const localImplVersion = match[1]; + if (localImplVersion !== config.targetVersion) { + throw new Error( + `targetVersion mismatch: config expects "${config.targetVersion}" but CoreLib.sol ` + + `getVersion() returns "${localImplVersion}". ` + + `Wrong contract compiled or wrong targetVersion in config?` + ); + } + console.log(`[PRE-FLIGHT] targetVersion = ${localImplVersion} ✓`); + } + + const ownerAddr = config.owner ? requireAddress(config.owner, "owner address") : await network.owner(); + const viewsOwnerAddr = config.viewsOwner + ? requireAddress(config.viewsOwner, "viewsOwner address") + : await viewsProxy.owner(); + + const deployerSigner = await getDeployer(ethers); + const deployerAddress = ((await deployerSigner.getAddress()) as string).toLowerCase(); + const ownerAddressLower = ownerAddr.toLowerCase(); + const viewsOwnerAddressLower = viewsOwnerAddr.toLowerCase(); + const targetRpcUrl = resolveRpcUrl(effectiveNetwork); + const canImpersonate = forkFlag || canImpersonateOnNetwork(effectiveNetwork, targetRpcUrl); + + // ── Resolve signers ── + let ownerSigner = deployerSigner; + let viewsOwnerSigner = deployerSigner; + let networkOwnerImpersonated = false; + let viewsOwnerImpersonated = false; + + if (deployerAddress !== ownerAddressLower || deployerAddress !== viewsOwnerAddressLower) { + if (!canImpersonate) { + throw new Error( + `Deployer ${deployerAddress} is not the required owner(s). ` + + `network.owner=${ownerAddressLower}, views.owner=${viewsOwnerAddressLower}. ` + + `Use the owner private key in env (e.g. HOODI_PRIVATE_KEY) or use --fork for impersonation.` + ); + } + + const ownerResolved = await getSignerForAddress(ethers, ownerAddr, useGetImpersonatedSigner); + ownerSigner = ownerResolved.signer; + networkOwnerImpersonated = ownerResolved.impersonated; + + if (viewsOwnerAddressLower === ownerAddressLower) { + viewsOwnerSigner = ownerSigner; + viewsOwnerImpersonated = networkOwnerImpersonated; + } else { + const viewsResolved = await getSignerForAddress(ethers, viewsOwnerAddr, useGetImpersonatedSigner); + viewsOwnerSigner = viewsResolved.signer; + viewsOwnerImpersonated = viewsResolved.impersonated; + } + } + + const networkOwner = network.connect(ownerSigner); + const viewsOwner = viewsProxy.connect(viewsOwnerSigner); + const views = viewsProxy.connect(ownerSigner); + + console.log(`Network owner: ${ownerAddr}${networkOwnerImpersonated ? " (impersonated)" : ""}`); + console.log(`Views owner: ${viewsOwnerAddr}${viewsOwnerImpersonated ? " (impersonated)" : ""}`); + if (canImpersonate) { + console.log(`Impersonation mode: ${useGetImpersonatedSigner ? "getImpersonatedSigner+fallback" : "manual RPC only"}`); + } + + // ── Check for pre-deployed addresses from deploy-result.json ── + const deployResult = envFlag ? await loadDeployResult(envFlag) : undefined; + + // ── Deploy implementations ── + console.log("[1/6] Deploying implementations (staking upgrade, SSVNetworkViews)"); + const stakingUpgradeImplAddr = deployResult?.ssvNetworkStakingUpgradeImplementation + ?? (await deployContract(ethers, "SSVNetworkSSVStakingUpgrade", [], ownerSigner)).address; + const viewsImplAddr = deployResult?.ssvNetworkViewsImplementation + ?? (await deployContract(ethers, "SSVNetworkViews", [], ownerSigner)).address; + + if (deployResult?.ssvNetworkStakingUpgradeImplementation) { + console.log(` Using pre-deployed SSVNetworkSSVStakingUpgrade: ${stakingUpgradeImplAddr}`); + } + if (deployResult?.ssvNetworkViewsImplementation) { + console.log(` Using pre-deployed SSVNetworkViews impl: ${viewsImplAddr}`); + } + + // ── Deploy CSSVToken (conditional) ── + console.log(`[2/6] Resolving CSSVToken for ${ssvNetworkProxy}`); + let cssvAddr: string; + const existingCssv = config.cssvToken ?? deployResult?.cssvToken; + if (existingCssv) { + const cssvCode = await ethers.provider.getCode(existingCssv); + if (cssvCode !== "0x") { + cssvAddr = existingCssv; + console.log(` Using existing CSSVToken: ${cssvAddr}`); + } else { + console.log(` CSSVToken at ${existingCssv} has no code, deploying new one`); + cssvAddr = (await deployContract(ethers, "CSSVToken", [ssvNetworkProxy], ownerSigner)).address; + } + } else { + cssvAddr = (await deployContract(ethers, "CSSVToken", [ssvNetworkProxy], ownerSigner)).address; + } + + // ── Deploy modules ── + console.log("[3/6] Deploying all module implementations"); + const preDeployedModules = deployResult?.modules ?? {}; + + async function resolveModule(name: string, args: any[]): Promise { + const existing = preDeployedModules[name as keyof typeof preDeployedModules]; + if (existing) { + console.log(` Using pre-deployed ${name}: ${existing}`); + return existing; + } + return (await deployContract(ethers, name, args, ownerSigner)).address; + } + + const ssvOperatorsAddr = await resolveModule("SSVOperators", [upgradeTimestamp]); + const ssvClustersAddr = await resolveModule("SSVClusters", []); + const ssvDaoAddr = await resolveModule("SSVDAO", [cssvAddr]); + const ssvViewsAddr = await resolveModule("SSVViews", [cssvAddr]); + const ssvOperatorsWhitelistAddr = await resolveModule("SSVOperatorsWhitelist", []); + const ssvStakingAddr = await resolveModule("SSVStaking", [cssvAddr]); + const ssvValidatorsAddr = await resolveModule("SSVValidators", []); + + const modules: ModuleAddresses = { + SSVOperators: ssvOperatorsAddr, + SSVClusters: ssvClustersAddr, + SSVDAO: ssvDaoAddr, + SSVViews: ssvViewsAddr, + SSVOperatorsWhitelist: ssvOperatorsWhitelistAddr, + SSVStaking: ssvStakingAddr, + SSVValidators: ssvValidatorsAddr, + }; + + // ── Upgrade proxies ── + console.log("[4/6] Upgrading network proxy and views proxy"); + const minBlocksBetweenUpdates = params.minBlocksBetweenUpdates; + if (config.skipInitializer) { + console.log(" skipInitializer=true: using upgradeTo (no initializer call)"); + await (await networkOwner.upgradeTo(stakingUpgradeImplAddr)).wait(); + } else { + const upgradeFactory = await ethers.getContractFactory("SSVNetworkSSVStakingUpgrade"); + const initData = upgradeFactory.interface.encodeFunctionData( + "initializeSSVStaking(uint64,uint32[4],uint16)", + [cooldownDuration, defaultOracleIds, quorumBps] + ); + await (await networkOwner.upgradeToAndCall(stakingUpgradeImplAddr, initData)).wait(); + } + await (await viewsOwner.upgradeTo(viewsImplAddr)).wait(); + + // ── Attach modules ── + console.log("[5/6] Attaching all modules"); + for (const mod of MODULE_ORDER) { + const moduleId = SSVModules[mod]; + const moduleAddress = modules[mod]; + await (await networkOwner.updateModule(moduleId, moduleAddress)).wait(); + } + + // ── Apply protocol parameters ── + console.log("[6/6] Applying configuration"); + if (params.networkFeeEth !== undefined) { + await (await networkOwner.updateNetworkFee(params.networkFeeEth)).wait(); + } + if (params.networkFeeSSV !== undefined) { + await (await networkOwner.updateNetworkFeeSSV(params.networkFeeSSV)).wait(); + } + if (params.liquidationThresholdPeriod !== undefined) { + await (await networkOwner.updateLiquidationThresholdPeriod(params.liquidationThresholdPeriod)).wait(); + } + if (params.liquidationThresholdPeriodSSV !== undefined) { + await (await networkOwner.updateLiquidationThresholdPeriodSSV(params.liquidationThresholdPeriodSSV)).wait(); + } + if (minBlocksBetweenUpdates !== undefined) { + await (await networkOwner.updateMinBlocksBetweenUpdates(minBlocksBetweenUpdates)).wait(); + } + if (params.minimumLiquidationCollateralEth !== undefined) { + await (await networkOwner.updateMinimumLiquidationCollateral(params.minimumLiquidationCollateralEth)).wait(); + } + if (params.minimumLiquidationCollateralSSV !== undefined) { + await (await networkOwner.updateMinimumLiquidationCollateralSSV(params.minimumLiquidationCollateralSSV)).wait(); + } + if (params.declareOperatorFeePeriod !== undefined) { + await (await networkOwner.updateDeclareOperatorFeePeriod(params.declareOperatorFeePeriod)).wait(); + } + if (params.executeOperatorFeePeriod !== undefined) { + await (await networkOwner.updateExecuteOperatorFeePeriod(params.executeOperatorFeePeriod)).wait(); + } + if (params.operatorFeeIncreaseLimit !== undefined) { + await (await networkOwner.updateOperatorFeeIncreaseLimit(params.operatorFeeIncreaseLimit)).wait(); + } + if (params.maxOperatorEthFee !== undefined) { + await (await networkOwner.updateMaximumOperatorFee(params.maxOperatorEthFee)).wait(); + } + if (params.minOperatorEthFee !== undefined) { + await (await networkOwner.updateMinimumOperatorEthFee(params.minOperatorEthFee)).wait(); + } + if (quorumBps !== undefined) { + await (await networkOwner.updateQuorumBps(quorumBps)).wait(); + } + if (params.unstakeCooldownDuration !== undefined) { + await (await networkOwner.updateUnstakeCooldownDuration(params.unstakeCooldownDuration)).wait(); + } + for (const { id, address } of oracles) { + await (await networkOwner.replaceOracle(id, address)).wait(); + } + + // ── Write result JSON ── + const onChainValues = await readOnChainValues(views); + const blockNumber = await ethers.provider.getBlockNumber(); + + const updatedConfig: UpgradeConfig = { + ...config, + owner: ownerAddr, + ssvNetworkProxy, + ssvNetworkViews, + ssvToken, + cssvToken: cssvAddr, + deployBlockNumber: blockNumber, + cooldownDuration: bigintToJsonNumberOrString(cooldownDuration), + modules, + protocolParams: { + ...config.protocolParams, + networkFeeEth: onChainValues.networkFeeEth, + networkFeeSSV: onChainValues.networkFeeSSV, + maxOperatorEthFee: onChainValues.maxOperatorEthFee, + minOperatorEthFee: onChainValues.minOperatorEthFee, + operatorFeeIncreaseLimit: onChainValues.operatorFeeIncreaseLimit, + declareOperatorFeePeriod: onChainValues.declareOperatorFeePeriod, + executeOperatorFeePeriod: onChainValues.executeOperatorFeePeriod, + liquidationThresholdPeriod: onChainValues.liquidationThresholdPeriod, + liquidationThresholdPeriodSSV: onChainValues.liquidationThresholdPeriodSSV, + ...(params.minBlocksBetweenUpdates !== undefined + ? { minBlocksBetweenUpdates: bigintToJsonNumberOrString(params.minBlocksBetweenUpdates) } + : {}), + minimumLiquidationCollateralEth: onChainValues.minimumLiquidationCollateralEth, + minimumLiquidationCollateralSSV: onChainValues.minimumLiquidationCollateralSSV, + validatorsPerOperatorLimit: onChainValues.validatorsPerOperatorLimit, + unstakeCooldownDuration: onChainValues.unstakeCooldownDuration, + }, + defaultOracleIds: onChainValues.defaultOracleIds, + quorumBps: onChainValues.quorumBps, + deployments: { + ...(config.deployments ?? {}), + ssvNetworkStakingUpgradeImplementation: stakingUpgradeImplAddr, + ssvNetworkViewsImplementation: viewsImplAddr, + cssvToken: cssvAddr, + modules, + targetNetwork: effectiveNetwork, + deployBlockNumber: blockNumber, + chainId: providerNetwork.chainId.toString(), + updatedAt: new Date().toISOString(), + }, + }; + if (oracles.length > 0) { + updatedConfig.oracles = toOracleConfig(oracles); + } + + // targetVersion already verified in pre-flight; use it as the result file suffix + const contractVersion = config.targetVersion; + + const versionedResultPath = envFlag + ? resolveVersionedUpgradeResultPath(envFlag, contractVersion) + : resultPath; + await writeFile(versionedResultPath, `${JSON.stringify(updatedConfig, null, 2)}\n`, "utf8"); + if (envFlag) { + await updateLatestSymlink(versionedResultPath, resultPath); + } + + console.log("Upgrade complete"); + console.log(`Config: ${initConfigPath}`); + console.log(`Result: ${versionedResultPath}`); + console.log(`Latest: ${resultPath} -> ${contractVersion}`); + console.log(`Block: ${blockNumber}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/verify-post-upgrade-config.ts b/scripts/verify-post-upgrade-config.ts new file mode 100644 index 000000000..c0090e062 --- /dev/null +++ b/scripts/verify-post-upgrade-config.ts @@ -0,0 +1,121 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { getDeployer, getEthers } from "./common/helpers.ts"; +import { + type UpgradeConfig, + parseOptionalArg, + parseOptionalBooleanArg, + requireAddress, + parseQuorum, + normalizeOracles, + resolveDefaultOracleIds, + resolveProtocolParams, + resolveCooldownDuration, + resolveConfigPath, + resolveNetworkFromEnv, +} from "./common/config.ts"; +import { verifyPostUpgradeState } from "./common/verify.ts"; + +async function main() { + const envFlag = parseOptionalArg("env"); + const configFlag = parseOptionalArg("config"); + const forkFlag = parseOptionalBooleanArg("fork", false); + + let initConfigPath: string; + if (envFlag) { + initConfigPath = resolveConfigPath(envFlag); + } else if (configFlag) { + initConfigPath = resolve(configFlag); + } else { + throw new Error("Provide --env or --config to specify the config source"); + } + + const targetNetwork = parseOptionalArg("network") ?? resolveNetworkFromEnv(envFlag); + if (!targetNetwork && !forkFlag) { + throw new Error("Provide --network or --fork to specify the target network"); + } + + const raw = await readFile(initConfigPath, "utf8"); + const config = JSON.parse(raw) as UpgradeConfig; + + const ssvNetworkProxy = requireAddress(config.ssvNetworkProxy, "ssvNetworkProxy"); + const ssvNetworkViews = requireAddress(config.ssvNetworkViews, "ssvNetworkViews"); + + const params = resolveProtocolParams(config); + const cooldownDuration = resolveCooldownDuration(config); + const quorumBps = parseQuorum(config.quorumBps); + const oracles = normalizeOracles(config.oracles); + const defaultOracleIds = resolveDefaultOracleIds(config, oracles); + + const effectiveNetwork = forkFlag ? (targetNetwork ?? "local") : (targetNetwork ?? "local"); + const ethers = await getEthers(effectiveNetwork); + + const networkCode = await ethers.provider.getCode(ssvNetworkProxy); + if (networkCode === "0x") { + throw new Error( + `No contract code at ssvNetworkProxy ${ssvNetworkProxy} on ${effectiveNetwork}. ` + + "Check your RPC URL and fork/network selection." + ); + } + const viewsCode = await ethers.provider.getCode(ssvNetworkViews); + if (viewsCode === "0x") { + throw new Error( + `No contract code at ssvNetworkViews ${ssvNetworkViews} on ${effectiveNetwork}. ` + + "Check your RPC URL and fork/network selection." + ); + } + + const network = await ethers.getContractAt("SSVNetwork", ssvNetworkProxy); + const viewsProxy = await ethers.getContractAt("SSVNetworkViews", ssvNetworkViews); + + let onChainVersion: string; + try { + onChainVersion = await network.getVersion(); + } catch { + throw new Error(`Could not read on-chain version from proxy ${ssvNetworkProxy}`); + } + if (onChainVersion !== config.targetVersion) { + throw new Error( + `Version mismatch: config.targetVersion is "${config.targetVersion}" but proxy reports "${onChainVersion}". ` + + "Wrong config or proxy address?" + ); + } + console.log(`[PRE-FLIGHT] currentVersion = ${onChainVersion} ✓`); + + { + const coreLibPath = resolve(process.cwd(), "contracts/libraries/CoreLib.sol"); + const coreLibSrc = await readFile(coreLibPath, "utf8"); + const match = coreLibSrc.match(/function getVersion\(\)[^{]*\{\s*return\s*"([^"]+)"/); + if (!match) { + throw new Error("Could not parse version from CoreLib.sol — check getVersion() format"); + } + const localImplVersion = match[1]; + if (localImplVersion !== config.targetVersion) { + throw new Error( + `targetVersion mismatch: config expects "${config.targetVersion}" but CoreLib.sol ` + + `getVersion() returns "${localImplVersion}". ` + + "Wrong contract compiled or wrong targetVersion in config?" + ); + } + console.log(`[PRE-FLIGHT] targetVersion = ${localImplVersion} ✓`); + } + + const deployerSigner = await getDeployer(ethers); + const views = viewsProxy.connect(deployerSigner); + + console.log("Running post-upgrade config verification"); + await verifyPostUpgradeState({ + views, + params, + cooldownDuration, + defaultOracleIds, + quorumBps, + oracles, + }); + console.log("Verification complete"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tasks/config.ts b/tasks/config.ts deleted file mode 100644 index c4abd6159..000000000 --- a/tasks/config.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum SSVModules { - SSVOperators = 0, - SSVClusters = 1, - SSVDAO = 2, - SSVViews = 3, - SSVOperatorsWhitelist = 4 - } \ No newline at end of file diff --git a/tasks/deploy.ts b/tasks/deploy.ts deleted file mode 100644 index 9c784cc12..000000000 --- a/tasks/deploy.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { task, subtask, types } from 'hardhat/config'; -import { SSVModules } from './config'; - -/** -@title Hardhat task to deploy all required contracts for SSVNetwork. -This task deploys the main SSVNetwork and SSVNetworkViews contracts, along with their associated modules. -It uses the Hardhat Runtime Environment (HRE) to execute the deployment tasks and handle errors. -@returns {void} This function doesn't return anything. If the deployment process encounters an error, -it will be printed to the console, and the process will exit with a non-zero status code. -@example -// Deploy all contracts with the default deployer account -npx hardhat --network holesky_testnet deploy:all -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -This task assumes that the SSVModules enum and deployment tasks for individual contracts have been properly defined. -*/ -task('deploy:all', 'Deploy SSVNetwork, SSVNetworkViews and module contracts').setAction(async ({}, hre) => { - // Triggering compilation - await hre.run('compile'); - - const [deployer] = await ethers.getSigners(); - console.log(`Deploying contracts with the account:${deployer.address}`); - - const ssvTokenAddress = await hre.run('deploy:mock-token'); - const operatorsModAddress = await hre.run('deploy:module', { module: SSVModules[SSVModules.SSVOperators] }); - const clustersModAddress = await hre.run('deploy:module', { module: SSVModules[SSVModules.SSVClusters] }); - const daoModAddress = await hre.run('deploy:module', { module: SSVModules[SSVModules.SSVDAO] }); - const viewsModAddress = await hre.run('deploy:module', { module: SSVModules[SSVModules.SSVViews] }); - - const { ssvNetworkProxyAddress: ssvNetworkAddress } = await hre.run('deploy:ssv-network', { - operatorsModAddress, - clustersModAddress, - daoModAddress, - viewsModAddress, - ssvTokenAddress, - }); - - await hre.run('deploy:ssv-network-views', { - ssvNetworkAddress, - }); -}); - -/** -@title Hardhat task to deploy a main implementation contract for SSVNetwork or SSVNetworkViews. -The contract parameter specifies the name of the contract implementation to be deployed. -@param {string} contract - The name of the contract implementation to deploy. -@returns {void} This function doesn't return anything. If the deployment process encounters an error, -it will be printed to the console, and the process will exit with a non-zero status code. -@example -// Deploy SSVNetwork implementation contract with the default deployer account -npx hardhat --network holesky_testnet deploy:main-impl --contract SSVNetwork -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -This task uses the "deploy:impl" subtask for the actual deployment. -*/ -task('deploy:main-impl', 'Deploys SSVNetwork / SSVNetworkViews implementation contract') - .addParam('contract', 'New contract implemetation', null, types.string) - .setAction(async ({ contract }, hre) => { - await hre.run('deploy:impl', { contract }); - }); - -/** -@title Hardhat task to deploy a basic whitelisting contract implementation. -The deployment process involves running a subtask that handles the actual deployment. -@returns {void} This function doesn't return anything. If the deployment process encounters an error, -it will be printed to the console, and the process will exit with a non-zero status code. -@example -// Deploy BasicWhitelisting contract with the default deployer account -npx hardhat --network holesky_testnet deploy:whitelisting-contract -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -This task uses the "deploy:impl" subtask for the actual deployment, specifying 'BasicWhitelisting' as the contract name. -*/ -task('deploy:whitelisting-contract', 'Deploys a basic whitelisting contract').setAction(async (_, hre) => { - await hre.run('deploy:impl', { contract: 'BasicWhitelisting' }); -}); - -/** -@title Hardhat subtask to deploy an SSV module contract. -The module parameter specifies the name of the SSV module to be deployed. -The name must be one of the pre-specified values in the SSVModules object. -If the specified module doesn't match any of the available SSVModules, an error will be thrown. -@param {string} module - The name of the SSV module to deploy. -@returns {string} The address of the newly deployed module contract. -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -This subtask uses the "deploy:impl" subtask for the actual deployment. -*/ -subtask('deploy:module', 'Deploys a new module contract') - .addParam('module', 'SSV Module', null, types.string) - .setAction(async ({ module }, hre) => { - const moduleValues = Object.values(SSVModules); - if (!moduleValues.includes(module)) { - throw new Error(`Invalid SSVModule: ${module}. Expected one of: ${moduleValues.join(', ')}`); - } - - const moduleAddress = await hre.run('deploy:impl', { contract: module }); - return moduleAddress; - }); - -task('deploy:token', 'Deploys SSV Token').setAction(async ({}, hre) => { - // Triggering compilation - await hre.run('compile'); - - console.log('Deploying SSV Network Token'); - - const ssvTokenFactory = await ethers.getContractFactory('SSVToken'); - const ssvToken = await ssvTokenFactory.deploy(); - await ssvToken.deployed(); - - console.log(`SSV Network Token deployed to: ${ssvToken.address}`); -}); - -/** - * @title Hardhat subtask to deploy or fetch an SSV Token contract. - * The ssvToken parameter in the hardhat's network section, specifies the address of the SSV Token contract. - * If not provided, it will deploy a new MockToken contract. - * @returns {string} The address of the deployed or fetched SSV Token contract. - */ -subtask('deploy:mock-token', 'Deploys / fetch SSV Token').setAction(async ({}, hre) => { - const tokenAddress = hre.network.config.ssvToken; - if (tokenAddress) return tokenAddress; - - // Local networks, deploy mock token - const ssvToken = await hre.viem.deployContract('SSVToken'); - - return ssvToken.address; -}); - -/** -@title Hardhat subtask to deploy a new implementation contract. -This subtask deploys a new implementation contract. -The contract parameter specifies the name of the contract to be deployed. -@param {string} contract - The name of the contract to deploy. -@returns {string} The address of the newly deployed implementation contract. -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -The contract specified should be already compiled and exist in the 'artifacts' directory. -*/ -subtask('deploy:impl', 'Deploys an implementation contract') - .addParam('contract', 'New contract implemetation', null, types.string) - .setAction(async ({ contract }, hre) => { - // Triggering compilation - await hre.run('compile'); - - // Deploy implemetation contract - const contractImpl = await hre.viem.deployContract(contract); - console.log(`${contract} implementation deployed to: ${contractImpl.address}`); - - return contractImpl.address; - }); - -/** -@title Hardhat subtask to deploy the SSVNetwork contract. -This subtask deploys the SSVNetwork contract as a Proxy using the UUPS (Universal Upgradeable Proxy Standard) pattern. -The parameters required are the addresses of the Operators, Clusters, DAO, and Views modules. -These addresses should be for contracts that have already been deployed on the network. -Environment variables are used to initialize SSVNetwork contract parameters, -these should be configured prior to running the subtask. -@param {string} operatorsModAddress - The address of the deployed Operators module. -@param {string} clustersModAddress - The address of the deployed Clusters module. -@param {string} daoModAddress - The address of the deployed DAO module. -@param {string} viewsModAddress - The address of the deployed Views module. -@returns {Object} An object containing the addresses of the deployed SSVNetwork Proxy and the Implementation. -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -The 'SSVNetwork' contract specified should be already compiled and exist in the 'artifacts' directory. -*/ -subtask('deploy:ssv-network', 'Deploys SSVNetwork contract') - .addPositionalParam('operatorsModAddress', 'Operators module address', null, types.string) - .addPositionalParam('clustersModAddress', 'Clusters module address', null, types.string) - .addPositionalParam('daoModAddress', 'DAO module address', null, types.string) - .addPositionalParam('viewsModAddress', 'Views module address', null, types.string) - .addPositionalParam('ssvTokenAddress', 'SSV Token address', null, types.string) - .setAction(async ({ operatorsModAddress, clustersModAddress, daoModAddress, viewsModAddress, ssvTokenAddress }) => { - const ssvNetworkFactory = await ethers.getContractFactory('SSVNetwork'); - - // deploy SSVNetwork - console.log(`Deploying SSVNetwork with ssvToken ${ssvTokenAddress}`); - const ssvNetwork = await upgrades.deployProxy( - ssvNetworkFactory, - [ - ssvTokenAddress, - operatorsModAddress, - clustersModAddress, - daoModAddress, - viewsModAddress, - process.env.MINIMUM_BLOCKS_BEFORE_LIQUIDATION, - process.env.MINIMUM_LIQUIDATION_COLLATERAL, - process.env.VALIDATORS_PER_OPERATOR_LIMIT, - process.env.DECLARE_OPERATOR_FEE_PERIOD, - process.env.EXECUTE_OPERATOR_FEE_PERIOD, - process.env.OPERATOR_MAX_FEE_INCREASE, - ], - { - kind: 'uups', - }, - ); - await ssvNetwork.waitForDeployment(); - - const ssvNetworkProxyAddress = await ssvNetwork.getAddress(); - const ssvNetworkImplAddress = await upgrades.erc1967.getImplementationAddress(ssvNetworkProxyAddress); - - console.log(`SSVNetwork proxy deployed to: ${ssvNetworkProxyAddress}`); - console.log(`SSVNetwork implementation deployed to: ${ssvNetworkImplAddress}`); - - return { ssvNetworkProxyAddress, ssvNetworkImplAddress }; - }); - -/** -@title Hardhat subtask to deploy the SSVNetworkViews contract. -This subtask deploys the SSVNetworkViews contract as a Proxy using the UUPS (Universal Upgradeable Proxy Standard) pattern. -The only parameter required is the address of the SSVNetwork proxy contract which should have been already deployed on the network. -@param {string} ssvNetworkAddress - The address of the deployed SSVNetwork contract. -@returns {Object} An object containing the addresses of the deployed SSVNetworkViews Proxy and the Implementation. -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -The 'SSVNetworkViews' contract specified should be already compiled and exist in the 'artifacts' directory. -*/ -subtask('deploy:ssv-network-views', 'Deploys SSVNetworkViews contract') - .addParam('ssvNetworkAddress', 'SSVNetwork address', null, types.string) - .setAction(async ({ ssvNetworkAddress }) => { - const ssvNetworkViewsFactory = await ethers.getContractFactory('SSVNetworkViews'); - - // deploy SSVNetwork - const ssvNetworkViews = await upgrades.deployProxy(ssvNetworkViewsFactory, [ssvNetworkAddress], { - kind: 'uups', - }); - await ssvNetworkViews.waitForDeployment(); - - const ssvNetworkViewsProxyAddress = await ssvNetworkViews.getAddress(); - const ssvNetworkViewsImplAddress = await upgrades.erc1967.getImplementationAddress(ssvNetworkViewsProxyAddress); - - console.log(`SSVNetworkViews proxy deployed to: ${ssvNetworkViewsProxyAddress}`); - console.log(`SSVNetworkViews implementation deployed to: ${ssvNetworkViewsImplAddress}`); - - return { ssvNetworkViewsProxyAddress, ssvNetworkViewsImplAddress }; - }); diff --git a/tasks/update-module.ts b/tasks/update-module.ts deleted file mode 100644 index dbdc28bbd..000000000 --- a/tasks/update-module.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { task, types } from 'hardhat/config'; -import { SSVModules } from './config'; - -/** -@title Hardhat task to update a module contract in the SSVNetwork. -This task first deploys a new version of a specified SSV module contract, and then updates the SSVNetwork contract to use this new module version. -The module's name is required and it's expected to match one of the SSVModules enumeration values. -The address of the SSVNetwork Proxy is expected to be set in the environment variable SSVNETWORK_PROXY_ADDRESS. -@param {string} module - The name of the SSV module to be updated. -@param {boolean} attachModule - Flag to attach new deployed module to SSVNetwork contract. Dafaults to true. -@param {string} proxyAddress - The proxy address of the SSVNetwork contract. -@example -// Update 'SSVOperators' module contract in the SSVNetwork -npx hardhat --network holesky_testnet update:module --module SSVOperators -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -The module's contract specified should be already compiled and exist in the 'artifacts' directory. -*/ -task('update:module', 'Deploys a new module contract and links it to SSVNetwork') - .addParam('module', 'SSV Module', null, types.string) - .addOptionalParam('attachModule', 'Attach module to SSVNetwork contract', false, types.boolean) - .addOptionalParam('proxyAddress', 'Proxy address of SSVNetwork / SSVNetworkViews', '', types.string) - .setAction(async ({ module, attachModule, proxyAddress }, hre) => { - if (attachModule && !proxyAddress) throw new Error('SSVNetwork proxy address not set.'); - - const [deployer] = await ethers.getSigners(); - console.log(`Deploying contracts with the account: ${deployer.address}`); - const moduleAddress = await hre.run('deploy:module', { module }); - - if (attachModule) { - if (!proxyAddress) throw new Error('SSVNetwork proxy address not set.'); - - const ssvNetworkFactory = await ethers.getContractFactory('SSVNetwork'); - const ssvNetwork = await ssvNetworkFactory.attach(proxyAddress); - - await ssvNetwork.updateModule(SSVModules[module], moduleAddress); - console.log(`${module} module attached to SSVNetwork succesfully`); - } - }); diff --git a/tasks/upgrade.ts b/tasks/upgrade.ts deleted file mode 100644 index cd24e0d80..000000000 --- a/tasks/upgrade.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { task, types } from 'hardhat/config'; - -/** -@title Hardhat task to upgrade a UUPS proxy contract. -This task upgrades a UUPS proxy contract deployed in a network to a new version. -It uses the OpenZeppelin Upgrades plugin for Hardhat to safely perform the upgrade operation. -@param {string} proxyAddress - The address of the existing UUPS proxy contract: SSVNetwork or SSVNetworkViews. -@param {string} contract - The name of the new contract that will replace the old one. -The contract should already be compiled and exist in the artifacts directory. -@param {string} [initFunction] - An optional function to be executed after the upgrade. -This function should be a method of the new contract and will be invoked as part of the upgrade transaction. -If not provided, no function will be called. -@param {Array} [params] - An optional array of parameters to the 'initFunction'. -The parameters should be ordered as expected by the 'initFunction'. -If 'initFunction' is not provided, this parameter has no effect. -@returns {void} This function doesn't return anything. After successfully upgrading, it prints the new implementation address to the console. -@example -// Upgrade the SSVNetwork contract to a new version 'SSVNetworkV2', and call a function 'initializev2' with parameters after upgrade: -npx hardhat --network holesky_testnet upgrade:proxy --proxyAddress 0x1234... --contract SSVNetworkV2 --initFunction initializev2 --params param1 param2 -*/ -task('upgrade:proxy', 'Upgrade SSVNetwork / SSVNetworkViews proxy via hardhat upgrades plugin') - .addParam('proxyAddress', 'Proxy address of SSVNetwork / SSVNetworkViews', null, types.string) - .addParam('contract', 'New contract upgrade', null, types.string) - .addOptionalParam('initFunction', 'Function to be executed after upgrading') - .addOptionalVariadicPositionalParam('params', 'Function parameters') - .setAction(async ({ proxyAddress, contract, initFunction, params }, hre) => { - // Triggering compilation - await hre.run('compile'); - - // Upgrading proxy - const [deployer] = await ethers.getSigners(); - console.log(`Upgading ${proxyAddress} with the account: ${deployer.address}`); - - const SSVUpgradeFactory = await ethers.getContractFactory(contract); - - const ssvUpgrade = await upgrades.upgradeProxy(proxyAddress, SSVUpgradeFactory, { - kind: 'uups', - call: initFunction - ? { - fn: initFunction, - args: params ? params : '', - } - : '', - }); - await ssvUpgrade.waitForDeployment(); - console.log(`${proxyAddress} upgraded successfully`); - - const ssvUpgradeAddress = await ssvUpgrade.getAddress(); - const implAddress = await upgrades.erc1967.getImplementationAddress(ssvUpgradeAddress); - console.log(`Implementation deployed to: ${implAddress}`); - }); - -/** -@title Hardhat task to prepare the upgrade of the SSVNetwork or SSVNetworkViews proxy contract. -This task is responsible for preparing an upgrade to a SSVNetwork or SSVNetworkViews proxy contract using the Hardhat Upgrades Plugin. -The task deploys the new implementation contract for the upgrade and outputs the address of the new implementation. -The function takes as input the proxy address to be upgraded and the contract to use for the upgrade. -The proxy address and contract name must be provided as parameters. -@param {string} proxyAddress - The proxy address of the SSVNetwork or SSVNetworkViews contract to be upgraded. -@param {string} contract - The name of the new implementation contract to deploy for the upgrade. -@example -// Prepare an upgrade for the SSVNetworkViews proxy contract with a new implementation contract named 'SSVNetworkViewsV2' -npx hardhat --network holesky_testnet upgrade:prepare --proxyAddress 0x1234... --contract SSVNetworkViewsV2 -@remarks -The deployer account used will be the first one returned by ethers.getSigners(). -Therefore, it should be appropriately configured in your Hardhat network configuration. -The new implementation contract specified should be already compiled and exist in the 'artifacts' directory. -*/ -task('upgrade:prepare', 'Prepares the upgrade of SSVNetwork / SSVNetworkViews proxy') - .addParam('proxyAddress', 'Proxy address of SSVNetwork / SSVNetworkViews', null, types.string) - .addParam('contract', 'New contract upgrade', null, types.string) - .setAction(async ({ proxyAddress, contract }, hre) => { - // Triggering compilation - await hre.run('compile'); - - const [deployer] = await ethers.getSigners(); - console.log(`Preparing the upgrade of ${proxyAddress} with the account: ${deployer.address}`); - - const SSVUpgradeFactory = await ethers.getContractFactory(contract); - - const implAddress = await upgrades.prepareUpgrade(proxyAddress, SSVUpgradeFactory, { - kind: 'uups', - }); - console.log(`Implementation deployed to: ${implAddress}`); - }); diff --git a/test-forked/operators-whitelist.ts b/test-forked/operators-whitelist.ts deleted file mode 100644 index 770e13bdb..000000000 --- a/test-forked/operators-whitelist.ts +++ /dev/null @@ -1,349 +0,0 @@ -// Declare imports -import hre from 'hardhat'; - -import { setBalance, reset } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers'; - -import { expect } from 'chai'; -import { ethers } from 'hardhat'; - -import { DataGenerator, MOCK_SHARES, publicClient } from '../test/helpers/contract-helpers'; -import { assertPostTxEvent } from '../test/helpers/utils/test'; - -import { Address, TestClient, walletActions, getContract } from 'viem'; - -import { ssvNetworkABI } from './v1.1.1/SSVNetwork'; -import { ssvNetworkViewsABI } from './v1.1.1/SSVNetworkViews'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, ssvToken: any, owners: any[], client: TestClient; - -const ssvNetworkAddress = '0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1'; -const ssvNetworkViewsAddress = '0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4'; -const ssvTokenAddress = '0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54'; - -describe('Whitelisting Tests (fork) - Pre-upgrade SSV Core Contracts Tests', () => { - beforeEach(async () => { - owners = await hre.viem.getWalletClients(); - - client = (await hre.viem.getTestClient()).extend(walletActions); - await client.impersonateAccount({ address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }); - - await setBalance('0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6', 2000000000000000000n); - - ({ ssvNetwork, ssvViews } = await loadContracts()); - - ssvToken = await hre.viem.getContractAt('SSVToken', ssvTokenAddress as Address); - - await upgradeAllContracts(); - }); - - it('Check an existing whitelisted operator is whitelisted but not using an external contract', async () => { - const operatorData = await ssvViews.read.getOperatorById([314]); - - expect(operatorData[3]).to.not.equal(ethers.ZeroAddress); - expect(operatorData[4]).to.equal(true); - expect(operatorData[5]).to.equal(true); - - expect(await ssvViews.read.isWhitelistingContract([operatorData[3]])).to.equal(false); - }); - - it('Register with an operator that uses a non-whitelisting contract reverts "InvalidWhitelistingContract"', async () => { - // SSV contracts owner - await client.impersonateAccount({ address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }); - - // 0xB4084F25DfCb2c1bf6636b420b59eda807953769 -> whitelisted address for operators 314, 315, 316, 317 - const liquidationCollateral = await ssvViews.read.getMinimumLiquidationCollateral(); - const minDepositAmount = liquidationCollateral * 2n; - - // give the sender enough SSV tokens - await ssvToken.write.mint([owners[2].account.address, minDepositAmount], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[2].account, - }); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [314, 315, 316, 317], - MOCK_SHARES, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - ).to.be.rejectedWith('CallerNotWhitelistedWithData'); - }); - - it('Register using legacy whitelisted operators in 4 operators cluster events/logic', async () => { - // get the current number of validators for these operators - const operatorsValidatorsCount = { - '314': (await ssvViews.read.getOperatorById([314]))[2], - '315': (await ssvViews.read.getOperatorById([315]))[2], - '316': (await ssvViews.read.getOperatorById([316]))[2], - '317': (await ssvViews.read.getOperatorById([317]))[2], - }; - - // SSV contracts owner - await client.impersonateAccount({ address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }); - - // 0xB4084F25DfCb2c1bf6636b420b59eda807953769 -> whitelisted address for operators 314, 315, 316, 317 - await client.impersonateAccount({ address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }); - await setBalance('0xB4084F25DfCb2c1bf6636b420b59eda807953769', 500000000000000000n); - - const liquidationCollateral = await ssvViews.read.getMinimumLiquidationCollateral(); - const minDepositAmount = liquidationCollateral * 2n; - - // give the sender enough SSV tokens - await ssvToken.write.mint(['0xB4084F25DfCb2c1bf6636b420b59eda807953769', minDepositAmount], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: { address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }, - }); - - await ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [314, 315, 316, 317], - MOCK_SHARES, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: { address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' } }, - ); - - // event confirms full execution - await assertPostTxEvent([ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - argNames: ['owner'], - argValuesList: [['0xB4084F25DfCb2c1bf6636b420b59eda807953769']], - }, - ]); - - // check the operators increased the number of validators by one - for (let i = 314; i < 318; i++) { - expect((await ssvViews.read.getOperatorById([i]))[2]).to.equal(operatorsValidatorsCount[i] + 1); - } - }); - - it('Replace a whitelisted address by an external whitelisting contract', async () => { - // owner of the operator 314 - await client.impersonateAccount({ address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }); - await setBalance('0xB4084F25DfCb2c1bf6636b420b59eda807953769', 500000000000000000n); - - // get the current whitelisted address - const prevWhitelistedAddress = (await ssvViews.read.getOperatorById([314]))[3]; - - const whitelistingContract = await hre.viem.deployContract( - 'MockWhitelistingContract', - [['0xB4084F25DfCb2c1bf6636b420b59eda807953769']], - { - client: owners[0].client, - }, - ); - const whitelistingContractAddress = await whitelistingContract.address; - // Set the whitelisting contract for operators 1,2,3,4 - await ssvNetwork.write.setOperatorsWhitelistingContract([[314], whitelistingContractAddress], { - account: { address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }, - }); - - // the operator now uses the whitelisting contract - expect((await ssvViews.read.getOperatorById([314]))[3]).to.deep.equal(whitelistingContractAddress); - - // and the previous whitelisted address was passed to the SSV whitelisting module - expect(await ssvViews.read.getWhitelistedOperators([[314], prevWhitelistedAddress])).to.deep.equal([314n]); - }); - - it('Whitelist multiple operators for an already whitelisted operator', async () => { - // owner of the operator 314 - await client.impersonateAccount({ address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }); - await setBalance('0xB4084F25DfCb2c1bf6636b420b59eda807953769', 500000000000000000n); - - // get the current whitelisted address - const prevWhitelistedAddress = (await ssvViews.read.getOperatorById([314]))[3]; - - await ssvNetwork.write.setOperatorsWhitelists([[315, 316, 317], [owners[2].account.address]], { - account: { address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[315, 316, 317], owners[2].account.address])).to.deep.equal([ - 315n, - 316n, - 317n, - ]); - - expect(await ssvViews.read.getWhitelistedOperators([[314], prevWhitelistedAddress])).to.deep.equal([314n]); - - // the operator uses the previous whitelisting main address - expect((await ssvViews.read.getOperatorById([314]))[3]).to.deep.equal(prevWhitelistedAddress); - }); -}); - -//* HELPERS */ - -const upgradeModule = async function (contractName: string, id: number) { - const ssvModule = await hre.viem.deployContract(contractName, [], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - await ssvNetwork.write.updateModule([id, await ssvModule.address], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); -}; - -const loadContracts = async function () { - const ssvNetwork = getContract({ - address: ssvNetworkAddress, - abi: ssvNetworkABI, - client: { - public: publicClient, - wallet: client, - }, - }); - - const ssvViews = getContract({ - address: ssvNetworkViewsAddress, - abi: ssvNetworkViewsABI, - client: { - public: publicClient, - wallet: client, - }, - }); - - return { - ssvNetwork, - ssvViews, - }; -}; - -const upgradeAllContracts = async function () { - await client.impersonateAccount({ address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }); - - const ssvNetworkUpgrade = await hre.viem.deployContract('SSVNetwork', [], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - await ssvNetwork.write.upgradeTo([await ssvNetworkUpgrade.address], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - ssvNetwork = await hre.viem.getContractAt('SSVNetwork', ssvNetworkAddress); - - const ssvViewsUpgrade = await hre.viem.deployContract('SSVNetworkViews', [], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - await ssvViews.write.upgradeTo([await ssvViewsUpgrade.address], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - ssvViews = await hre.viem.getContractAt('SSVNetworkViews', ssvNetworkViewsAddress as Address); - - await upgradeModule('SSVOperators', 0); - await upgradeModule('SSVClusters', 1); - await upgradeModule('SSVViews', 3); - await upgradeModule('SSVOperatorsWhitelist', 4); - - await client.stopImpersonatingAccount({ - address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6', - }); -}; - -describe('Whitelisting Tests (fork) - Ongoing SSV Core Contracts upgrade Tests', () => { - beforeEach(async () => { - await reset(`${process.env.MAINNET_ETH_NODE_URL}${process.env.NODE_PROVIDER_KEY}`, 19621100); - owners = await hre.viem.getWalletClients(); - - client = (await hre.viem.getTestClient()).extend(walletActions); - await client.impersonateAccount({ address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }); - - await setBalance('0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6', 2000000000000000000n); - - ({ ssvNetwork, ssvViews } = await loadContracts()); - - ssvToken = await hre.viem.getContractAt('SSVToken', ssvTokenAddress as Address); - }); - - it('WT-3 - Check backward compatibility with existing generic contracts', async () => { - // owner of the operator 314 - await client.impersonateAccount({ address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }); - await setBalance('0xB4084F25DfCb2c1bf6636b420b59eda807953769', 1200000000000000000n); - - // deploy a generic contract - const genericWhitelistContract = await hre.viem.deployContract( - 'GenericWhitelistContract', - [await ssvNetwork.address, await ssvToken.address], - { - account: { address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }, - }, - ); - - const generiWhitelistContractAddress = await genericWhitelistContract.address; - await ssvNetwork.write.setOperatorWhitelist([314n, generiWhitelistContractAddress], { - account: { address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }, - }); - - const validatorCount = (await ssvViews.read.getOperatorById([314n]))[2]; - - await upgradeAllContracts(); - - // whitelist a different operator using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[315n], ['0xB4084F25DfCb2c1bf6636b420b59eda807953769']], { - account: { address: '0xB4084F25DfCb2c1bf6636b420b59eda807953769' }, - }); - - const minDepositAmount = 1000000000000000000000n; - - await client.impersonateAccount({ address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }); - - // give the generic contract enough SSV tokens - await ssvToken.write.mint([generiWhitelistContractAddress, minDepositAmount], { - account: { address: '0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6' }, - }); - - // use a new account owners[4] to register a validator using - // the operator 314 through the generic contract - await genericWhitelistContract.write.registerValidatorSSV( - [ - DataGenerator.publicKey(1), - [30, 31, 32, 314], - MOCK_SHARES, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[4].account }, - ); - - // event confirms full execution - await assertPostTxEvent([ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - argNames: ['owner'], - argValuesList: [[generiWhitelistContractAddress]], - }, - ]); - - expect((await ssvViews.read.getOperatorById([314n]))[2]).to.equal(validatorCount + 1); - }); -}); diff --git a/test-forked/v1.1.1/SSVNetwork.ts b/test-forked/v1.1.1/SSVNetwork.ts deleted file mode 100644 index ed0cbf31d..000000000 --- a/test-forked/v1.1.1/SSVNetwork.ts +++ /dev/null @@ -1,1877 +0,0 @@ -export const ssvNetworkABI = [ - { - inputs: [], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [], - name: 'ApprovalNotWithinTimeframe', - type: 'error', - }, - { - inputs: [], - name: 'CallerNotOwner', - type: 'error', - }, - { - inputs: [], - name: 'CallerNotWhitelisted', - type: 'error', - }, - { - inputs: [], - name: 'ClusterAlreadyEnabled', - type: 'error', - }, - { - inputs: [], - name: 'ClusterDoesNotExists', - type: 'error', - }, - { - inputs: [], - name: 'ClusterIsLiquidated', - type: 'error', - }, - { - inputs: [], - name: 'ClusterNotLiquidatable', - type: 'error', - }, - { - inputs: [], - name: 'EmptyPublicKeysList', - type: 'error', - }, - { - inputs: [], - name: 'ExceedValidatorLimit', - type: 'error', - }, - { - inputs: [], - name: 'FeeExceedsIncreaseLimit', - type: 'error', - }, - { - inputs: [], - name: 'FeeIncreaseNotAllowed', - type: 'error', - }, - { - inputs: [], - name: 'FeeTooHigh', - type: 'error', - }, - { - inputs: [], - name: 'FeeTooLow', - type: 'error', - }, - { - inputs: [], - name: 'IncorrectClusterState', - type: 'error', - }, - { - inputs: [], - name: 'IncorrectValidatorState', - type: 'error', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - ], - name: 'IncorrectValidatorStateWithData', - type: 'error', - }, - { - inputs: [], - name: 'InsufficientBalance', - type: 'error', - }, - { - inputs: [], - name: 'InvalidOperatorIdsLength', - type: 'error', - }, - { - inputs: [], - name: 'InvalidPublicKeyLength', - type: 'error', - }, - { - inputs: [], - name: 'MaxValueExceeded', - type: 'error', - }, - { - inputs: [], - name: 'NewBlockPeriodIsBelowMinimum', - type: 'error', - }, - { - inputs: [], - name: 'NoFeeDeclared', - type: 'error', - }, - { - inputs: [], - name: 'NotAuthorized', - type: 'error', - }, - { - inputs: [], - name: 'OperatorAlreadyExists', - type: 'error', - }, - { - inputs: [], - name: 'OperatorDoesNotExist', - type: 'error', - }, - { - inputs: [], - name: 'OperatorsListNotUnique', - type: 'error', - }, - { - inputs: [], - name: 'PublicKeysSharesLengthMismatch', - type: 'error', - }, - { - inputs: [], - name: 'SameFeeChangeNotAllowed', - type: 'error', - }, - { - inputs: [], - name: 'TargetModuleDoesNotExist', - type: 'error', - }, - { - inputs: [], - name: 'TokenTransferFailed', - type: 'error', - }, - { - inputs: [], - name: 'UnsortedOperatorsList', - type: 'error', - }, - { - inputs: [], - name: 'ValidatorAlreadyExists', - type: 'error', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - ], - name: 'ValidatorAlreadyExistsWithData', - type: 'error', - }, - { - inputs: [], - name: 'ValidatorDoesNotExist', - type: 'error', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'address', - name: 'previousAdmin', - type: 'address', - }, - { - indexed: false, - internalType: 'address', - name: 'newAdmin', - type: 'address', - }, - ], - name: 'AdminChanged', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'beacon', - type: 'address', - }, - ], - name: 'BeaconUpgraded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - indexed: false, - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'ClusterDeposited', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - indexed: false, - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'ClusterLiquidated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - indexed: false, - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'ClusterReactivated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - indexed: false, - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'ClusterWithdrawn', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint64', - name: 'value', - type: 'uint64', - }, - ], - name: 'DeclareOperatorFeePeriodUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint64', - name: 'value', - type: 'uint64', - }, - ], - name: 'ExecuteOperatorFeePeriodUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'address', - name: 'recipientAddress', - type: 'address', - }, - ], - name: 'FeeRecipientAddressUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint8', - name: 'version', - type: 'uint8', - }, - ], - name: 'Initialized', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint64', - name: 'value', - type: 'uint64', - }, - ], - name: 'LiquidationThresholdPeriodUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'MinimumLiquidationCollateralUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - { - indexed: false, - internalType: 'address', - name: 'recipient', - type: 'address', - }, - ], - name: 'NetworkEarningsWithdrawn', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'oldFee', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'newFee', - type: 'uint256', - }, - ], - name: 'NetworkFeeUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - { - indexed: false, - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'OperatorAdded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'OperatorFeeDeclarationCancelled', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - indexed: false, - internalType: 'uint256', - name: 'blockNumber', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'OperatorFeeDeclared', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - indexed: false, - internalType: 'uint256', - name: 'blockNumber', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'OperatorFeeExecuted', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint64', - name: 'value', - type: 'uint64', - }, - ], - name: 'OperatorFeeIncreaseLimitUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint64', - name: 'maxFee', - type: 'uint64', - }, - ], - name: 'OperatorMaximumFeeUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'OperatorRemoved', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - indexed: false, - internalType: 'address', - name: 'whitelisted', - type: 'address', - }, - ], - name: 'OperatorWhitelistUpdated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'OperatorWithdrawn', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipTransferStarted', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipTransferred', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'implementation', - type: 'address', - }, - ], - name: 'Upgraded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - indexed: false, - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - { - indexed: false, - internalType: 'bytes', - name: 'shares', - type: 'bytes', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - indexed: false, - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'ValidatorAdded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - indexed: false, - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - ], - name: 'ValidatorExited', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - indexed: false, - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - indexed: false, - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'ValidatorRemoved', - type: 'event', - }, - { - stateMutability: 'nonpayable', - type: 'fallback', - }, - { - inputs: [], - name: 'acceptOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'bytes[]', - name: 'publicKeys', - type: 'bytes[]', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - ], - name: 'bulkExitValidator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'bytes[]', - name: 'publicKeys', - type: 'bytes[]', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - internalType: 'bytes[]', - name: 'sharesData', - type: 'bytes[]', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'bulkRegisterValidator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'bytes[]', - name: 'publicKeys', - type: 'bytes[]', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'bulkRemoveValidator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'cancelDeclaredOperatorFee', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'declareOperatorFee', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'clusterOwner', - type: 'address', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'deposit', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'executeOperatorFee', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - ], - name: 'exitValidator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'getVersion', - outputs: [ - { - internalType: 'string', - name: 'version', - type: 'string', - }, - ], - stateMutability: 'pure', - type: 'function', - }, - { - inputs: [ - { - internalType: 'contract IERC20', - name: 'token_', - type: 'address', - }, - { - internalType: 'contract ISSVOperators', - name: 'ssvOperators_', - type: 'address', - }, - { - internalType: 'contract ISSVClusters', - name: 'ssvClusters_', - type: 'address', - }, - { - internalType: 'contract ISSVDAO', - name: 'ssvDAO_', - type: 'address', - }, - { - internalType: 'contract ISSVViews', - name: 'ssvViews_', - type: 'address', - }, - { - internalType: 'uint64', - name: 'minimumBlocksBeforeLiquidation_', - type: 'uint64', - }, - { - internalType: 'uint256', - name: 'minimumLiquidationCollateral_', - type: 'uint256', - }, - { - internalType: 'uint32', - name: 'validatorsPerOperatorLimit_', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'declareOperatorFeePeriod_', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'executeOperatorFeePeriod_', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'operatorMaxFeeIncrease_', - type: 'uint64', - }, - ], - name: 'initialize', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'clusterOwner', - type: 'address', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'liquidate', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'owner', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'pendingOwner', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'proxiableUUID', - outputs: [ - { - internalType: 'bytes32', - name: '', - type: 'bytes32', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'reactivate', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'reduceOperatorFee', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - { - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'registerOperator', - outputs: [ - { - internalType: 'uint64', - name: 'id', - type: 'uint64', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - internalType: 'bytes', - name: 'sharesData', - type: 'bytes', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'registerValidator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'removeOperator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'removeValidator', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'renounceOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'recipientAddress', - type: 'address', - }, - ], - name: 'setFeeRecipientAddress', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - internalType: 'address', - name: 'whitelisted', - type: 'address', - }, - ], - name: 'setOperatorWhitelist', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'transferOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'timeInSeconds', - type: 'uint64', - }, - ], - name: 'updateDeclareOperatorFeePeriod', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'timeInSeconds', - type: 'uint64', - }, - ], - name: 'updateExecuteOperatorFeePeriod', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'blocks', - type: 'uint64', - }, - ], - name: 'updateLiquidationThresholdPeriod', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'maxFee', - type: 'uint64', - }, - ], - name: 'updateMaximumOperatorFee', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'updateMinimumLiquidationCollateral', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'enum SSVModules', - name: 'moduleId', - type: 'uint8', - }, - { - internalType: 'address', - name: 'moduleAddress', - type: 'address', - }, - ], - name: 'updateModule', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'fee', - type: 'uint256', - }, - ], - name: 'updateNetworkFee', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'percentage', - type: 'uint64', - }, - ], - name: 'updateOperatorFeeIncreaseLimit', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'newImplementation', - type: 'address', - }, - ], - name: 'upgradeTo', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'newImplementation', - type: 'address', - }, - { - internalType: 'bytes', - name: 'data', - type: 'bytes', - }, - ], - name: 'upgradeToAndCall', - outputs: [], - stateMutability: 'payable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'withdraw', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'withdrawAllOperatorEarnings', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'withdrawNetworkEarnings', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - { - internalType: 'uint256', - name: 'amount', - type: 'uint256', - }, - ], - name: 'withdrawOperatorEarnings', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const; diff --git a/test-forked/v1.1.1/SSVNetworkViews.ts b/test-forked/v1.1.1/SSVNetworkViews.ts deleted file mode 100644 index 639e2316d..000000000 --- a/test-forked/v1.1.1/SSVNetworkViews.ts +++ /dev/null @@ -1,907 +0,0 @@ -export const ssvNetworkViewsABI = [ - { - inputs: [], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [], - name: 'ApprovalNotWithinTimeframe', - type: 'error', - }, - { - inputs: [], - name: 'CallerNotOwner', - type: 'error', - }, - { - inputs: [], - name: 'CallerNotWhitelisted', - type: 'error', - }, - { - inputs: [], - name: 'ClusterAlreadyEnabled', - type: 'error', - }, - { - inputs: [], - name: 'ClusterDoesNotExists', - type: 'error', - }, - { - inputs: [], - name: 'ClusterIsLiquidated', - type: 'error', - }, - { - inputs: [], - name: 'ClusterNotLiquidatable', - type: 'error', - }, - { - inputs: [], - name: 'EmptyPublicKeysList', - type: 'error', - }, - { - inputs: [], - name: 'ExceedValidatorLimit', - type: 'error', - }, - { - inputs: [], - name: 'FeeExceedsIncreaseLimit', - type: 'error', - }, - { - inputs: [], - name: 'FeeIncreaseNotAllowed', - type: 'error', - }, - { - inputs: [], - name: 'FeeTooHigh', - type: 'error', - }, - { - inputs: [], - name: 'FeeTooLow', - type: 'error', - }, - { - inputs: [], - name: 'IncorrectClusterState', - type: 'error', - }, - { - inputs: [], - name: 'IncorrectValidatorState', - type: 'error', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - ], - name: 'IncorrectValidatorStateWithData', - type: 'error', - }, - { - inputs: [], - name: 'InsufficientBalance', - type: 'error', - }, - { - inputs: [], - name: 'InvalidOperatorIdsLength', - type: 'error', - }, - { - inputs: [], - name: 'InvalidPublicKeyLength', - type: 'error', - }, - { - inputs: [], - name: 'MaxValueExceeded', - type: 'error', - }, - { - inputs: [], - name: 'NewBlockPeriodIsBelowMinimum', - type: 'error', - }, - { - inputs: [], - name: 'NoFeeDeclared', - type: 'error', - }, - { - inputs: [], - name: 'NotAuthorized', - type: 'error', - }, - { - inputs: [], - name: 'OperatorAlreadyExists', - type: 'error', - }, - { - inputs: [], - name: 'OperatorDoesNotExist', - type: 'error', - }, - { - inputs: [], - name: 'OperatorsListNotUnique', - type: 'error', - }, - { - inputs: [], - name: 'PublicKeysSharesLengthMismatch', - type: 'error', - }, - { - inputs: [], - name: 'SameFeeChangeNotAllowed', - type: 'error', - }, - { - inputs: [], - name: 'TargetModuleDoesNotExist', - type: 'error', - }, - { - inputs: [], - name: 'TokenTransferFailed', - type: 'error', - }, - { - inputs: [], - name: 'UnsortedOperatorsList', - type: 'error', - }, - { - inputs: [], - name: 'ValidatorAlreadyExists', - type: 'error', - }, - { - inputs: [ - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - ], - name: 'ValidatorAlreadyExistsWithData', - type: 'error', - }, - { - inputs: [], - name: 'ValidatorDoesNotExist', - type: 'error', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'address', - name: 'previousAdmin', - type: 'address', - }, - { - indexed: false, - internalType: 'address', - name: 'newAdmin', - type: 'address', - }, - ], - name: 'AdminChanged', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'beacon', - type: 'address', - }, - ], - name: 'BeaconUpgraded', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint8', - name: 'version', - type: 'uint8', - }, - ], - name: 'Initialized', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipTransferStarted', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'previousOwner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'OwnershipTransferred', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'implementation', - type: 'address', - }, - ], - name: 'Upgraded', - type: 'event', - }, - { - inputs: [], - name: 'acceptOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'clusterOwner', - type: 'address', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'getBalance', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'clusterOwner', - type: 'address', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'getBurnRate', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getLiquidationThresholdPeriod', - outputs: [ - { - internalType: 'uint64', - name: '', - type: 'uint64', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getMaximumOperatorFee', - outputs: [ - { - internalType: 'uint64', - name: '', - type: 'uint64', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getMinimumLiquidationCollateral', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getNetworkEarnings', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getNetworkFee', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getNetworkValidatorsCount', - outputs: [ - { - internalType: 'uint32', - name: '', - type: 'uint32', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'getOperatorById', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - { - internalType: 'uint32', - name: '', - type: 'uint32', - }, - { - internalType: 'address', - name: '', - type: 'address', - }, - { - internalType: 'bool', - name: '', - type: 'bool', - }, - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'getOperatorDeclaredFee', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - { - internalType: 'uint64', - name: '', - type: 'uint64', - }, - { - internalType: 'uint64', - name: '', - type: 'uint64', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'id', - type: 'uint64', - }, - ], - name: 'getOperatorEarnings', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'operatorId', - type: 'uint64', - }, - ], - name: 'getOperatorFee', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getOperatorFeeIncreaseLimit', - outputs: [ - { - internalType: 'uint64', - name: '', - type: 'uint64', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getOperatorFeePeriods', - outputs: [ - { - internalType: 'uint64', - name: '', - type: 'uint64', - }, - { - internalType: 'uint64', - name: '', - type: 'uint64', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'clusterOwner', - type: 'address', - }, - { - internalType: 'bytes', - name: 'publicKey', - type: 'bytes', - }, - ], - name: 'getValidator', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getValidatorsPerOperatorLimit', - outputs: [ - { - internalType: 'uint32', - name: '', - type: 'uint32', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getVersion', - outputs: [ - { - internalType: 'string', - name: '', - type: 'string', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'contract ISSVViews', - name: 'ssvNetwork_', - type: 'address', - }, - ], - name: 'initialize', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'clusterOwner', - type: 'address', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'isLiquidatable', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'clusterOwner', - type: 'address', - }, - { - internalType: 'uint64[]', - name: 'operatorIds', - type: 'uint64[]', - }, - { - components: [ - { - internalType: 'uint32', - name: 'validatorCount', - type: 'uint32', - }, - { - internalType: 'uint64', - name: 'networkFeeIndex', - type: 'uint64', - }, - { - internalType: 'uint64', - name: 'index', - type: 'uint64', - }, - { - internalType: 'bool', - name: 'active', - type: 'bool', - }, - { - internalType: 'uint256', - name: 'balance', - type: 'uint256', - }, - ], - internalType: 'struct ISSVNetworkCore.Cluster', - name: 'cluster', - type: 'tuple', - }, - ], - name: 'isLiquidated', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'owner', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'pendingOwner', - outputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'proxiableUUID', - outputs: [ - { - internalType: 'bytes32', - name: '', - type: 'bytes32', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'renounceOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'ssvNetwork', - outputs: [ - { - internalType: 'contract ISSVViews', - name: '', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'newOwner', - type: 'address', - }, - ], - name: 'transferOwnership', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'newImplementation', - type: 'address', - }, - ], - name: 'upgradeTo', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'newImplementation', - type: 'address', - }, - { - internalType: 'bytes', - name: 'data', - type: 'bytes', - }, - ], - name: 'upgradeToAndCall', - outputs: [], - stateMutability: 'payable', - type: 'function', - }, -] as const; diff --git a/test/account/deposit.ts b/test/account/deposit.ts deleted file mode 100644 index 948aab928..000000000 --- a/test/account/deposit.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; - -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers'; -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, ssvToken: any, cluster1: any, minDepositAmount: any; - -describe('Deposit Tests', function () { - beforeEach(async function () { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - minDepositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation + 10) * CONFIG.minimalOperatorFee * 4n; - - await coldRegisterValidator(); - - cluster1 = ( - await bulkRegisterValidators( - 4, - 1, - DEFAULT_OPERATOR_IDS[4], - minDepositAmount, - { validatorCount: 0, networkFeeIndex: 0, index: 0, balance: 0n, active: true }, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ) - ).args; - }); - - it('Deposit to a non liquidated cluster I own emits "ClusterDeposited"', async () => { - expect(await ssvViews.read.isLiquidated([cluster1.owner, cluster1.operatorIds, cluster1.cluster])).to.equal(false); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[4].account, - }); - - await assertEvent( - ssvNetwork.write.deposit([owners[4].account.address, cluster1.operatorIds, minDepositAmount, cluster1.cluster], { - account: owners[4].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterDeposited', - }, - ], - ); - }); - - it('Deposit to a cluster I own gas limits', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[4].account, - }); - await trackGas( - ssvNetwork.write.deposit([owners[4].account.address, cluster1.operatorIds, minDepositAmount, cluster1.cluster], { - account: owners[4].account, - }), - [GasGroup.DEPOSIT], - ); - }); - - it('Deposit to a cluster I do not own emits "ClusterDeposited"', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount]); - - await assertEvent( - ssvNetwork.write.deposit([owners[4].account.address, cluster1.operatorIds, minDepositAmount, cluster1.cluster], { - account: owners[0].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterDeposited', - }, - ], - ); - }); - - it('Deposit to a cluster I do not own gas limits', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount]); - await trackGas( - ssvNetwork.write.deposit([owners[4].account.address, cluster1.operatorIds, minDepositAmount, cluster1.cluster], { - account: owners[0].account, - }), - [GasGroup.DEPOSIT], - ); - }); - - it('Deposit to a cluster I do not own with a cluster that does not exist reverts "ClusterDoesNotExists"', async () => { - await expect( - ssvNetwork.write.deposit([owners[1].account.address, [1, 2, 4, 5], minDepositAmount, cluster1.cluster], { - account: owners[4].account, - }), - ).to.be.rejectedWith('ClusterDoesNotExists'); - }); - - it('Deposit to a liquidated cluster emits "ClusterDeposited"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([cluster1.owner, cluster1.operatorIds, cluster1.cluster]), - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect(await ssvViews.read.isLiquidated([cluster1.owner, cluster1.operatorIds, updatedCluster.cluster])).to.equal( - true, - ); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[4].account, - }); - - await assertEvent( - ssvNetwork.write.deposit( - [owners[4].account.address, cluster1.operatorIds, minDepositAmount, updatedCluster.cluster], - { - account: owners[4].account, - }, - ), - [ - { - contract: ssvNetwork, - eventName: 'ClusterDeposited', - }, - ], - ); - }); - - it('Deposit to a cluster I do own with a cluster that does not exist reverts "ClusterDoesNotExists"', async () => { - await expect( - ssvNetwork.write.deposit([owners[1].account.address, cluster1.operatorIds, minDepositAmount, cluster1.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('ClusterDoesNotExists'); - }); -}); diff --git a/test/account/withdraw.ts b/test/account/withdraw.ts deleted file mode 100644 index 0ff5ab2e0..000000000 --- a/test/account/withdraw.ts +++ /dev/null @@ -1,242 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - deposit, - withdraw, - removeValidator, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { mine, loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers'; -import { getAddress } from 'viem'; -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, ssvToken: any, cluster1: any, minDepositAmount: BigInt; - -describe('Withdraw Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * CONFIG.minimalOperatorFee * 4n; - - // cold register - await coldRegisterValidator(); - - cluster1 = ( - await bulkRegisterValidators( - 4, - 1, - DEFAULT_OPERATOR_IDS[4], - minDepositAmount, - { validatorCount: 0, networkFeeIndex: 0, index: 0, balance: 0n, active: true }, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ) - ).args; - }); - - it('Withdraw from cluster emits "ClusterWithdrawn"', async () => { - await assertEvent( - ssvNetwork.write.withdraw([cluster1.operatorIds, CONFIG.minimalOperatorFee, cluster1.cluster], { - account: owners[4].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterWithdrawn', - argNames: ['owner', 'value'], - argValuesList: [[getAddress(owners[4].account.address), CONFIG.minimalOperatorFee]], - }, - ], - ); - }); - - it('Withdraw from cluster gas limits', async () => { - await trackGas( - ssvNetwork.write.withdraw([cluster1.operatorIds, CONFIG.minimalOperatorFee, cluster1.cluster], { - account: owners[4].account, - }), - [GasGroup.WITHDRAW_CLUSTER_BALANCE], - ); - }); - - it('Withdraw from operator balance emits "OperatorWithdrawn"', async () => { - await assertEvent(ssvNetwork.write.withdrawOperatorEarnings([1, CONFIG.minimalOperatorFee]), [ - { - contract: ssvNetwork, - eventName: 'OperatorWithdrawn', - }, - ]); - }); - - it('Withdraw from operator balance gas limits', async () => { - await trackGas(ssvNetwork.write.withdrawOperatorEarnings([1, CONFIG.minimalOperatorFee]), [ - GasGroup.WITHDRAW_OPERATOR_BALANCE, - ]); - }); - - it('Withdraw the total operator balance emits "OperatorWithdrawn"', async () => { - await assertEvent(ssvNetwork.write.withdrawAllOperatorEarnings([1]), [ - { - contract: ssvNetwork, - eventName: 'OperatorWithdrawn', - }, - ]); - }); - - it('Withdraw the total operator balance gas limits', async () => { - await trackGas(ssvNetwork.write.withdrawAllOperatorEarnings([1]), [GasGroup.WITHDRAW_OPERATOR_BALANCE]); - }); - - it('Withdraw from a cluster that has a removed operator emits "ClusterWithdrawn"', async () => { - await ssvNetwork.write.removeOperator([1]); - await assertEvent( - ssvNetwork.write.withdraw([cluster1.operatorIds, CONFIG.minimalOperatorFee, cluster1.cluster], { - account: owners[4].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterWithdrawn', - }, - ], - ); - }); - - it('Withdraw more than the cluster balance reverts "InsufficientBalance"', async () => { - await expect( - ssvNetwork.write.withdraw([cluster1.operatorIds, minDepositAmount, cluster1.cluster], { - account: owners[4].account, - }), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Sequentially withdraw more than the cluster balance reverts "InsufficientBalance"', async () => { - const burnPerBlock = CONFIG.minimalOperatorFee * 4n; - - cluster1 = await deposit(1, cluster1.owner, cluster1.operatorIds, minDepositAmount * 2n, cluster1.cluster); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount * 3n - burnPerBlock * 2n); - - cluster1 = await withdraw(4, cluster1.operatorIds, minDepositAmount, cluster1.cluster); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount * 2n - burnPerBlock * 3n); - - cluster1 = await withdraw(4, cluster1.operatorIds, minDepositAmount, cluster1.cluster); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 4n); - - await expect( - ssvNetwork.write.withdraw([cluster1.operatorIds, minDepositAmount, cluster1.cluster], { - account: owners[4].account, - }), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Withdraw from a liquidatable cluster reverts "InsufficientBalance" (liquidation threshold)', async () => { - await mine(20); - await expect( - ssvNetwork.write.withdraw([cluster1.operatorIds, 4000000000n, cluster1.cluster], { - account: owners[4].account, - }), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Withdraw from a liquidatable cluster reverts "InsufficientBalance" (liquidation collateral)', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation - 10); - await expect( - ssvNetwork.write.withdraw([cluster1.operatorIds, 7500000000n, cluster1.cluster], { - account: owners[4].account, - }), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Withdraw from a liquidatable cluster after liquidation period reverts "InsufficientBalance"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - await expect( - ssvNetwork.write.withdraw([cluster1.operatorIds, CONFIG.minimalOperatorFee, cluster1.cluster], { - account: owners[4].account, - }), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Withdraw balance from an operator I do not own reverts "CallerNotOwnerWithData"', async () => { - await expect( - ssvNetwork.write.withdrawOperatorEarnings([1, minDepositAmount], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Withdraw more than the operator balance reverts "InsufficientBalance"', async () => { - await expect(ssvNetwork.write.withdrawOperatorEarnings([1, minDepositAmount])).to.be.rejectedWith( - 'InsufficientBalance', - ); - }); - - it('Sequentially withdraw more than the operator balance reverts "InsufficientBalance"', async () => { - await ssvNetwork.write.withdrawOperatorEarnings([1, CONFIG.minimalOperatorFee * 3n]); - - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 4n - CONFIG.minimalOperatorFee * 3n, - ); - - await ssvNetwork.write.withdrawOperatorEarnings([1, CONFIG.minimalOperatorFee * 3n]); - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 6n - CONFIG.minimalOperatorFee * 6n, - ); - - await expect(ssvNetwork.write.withdrawOperatorEarnings([1, CONFIG.minimalOperatorFee * 3n])).to.be.rejectedWith( - 'InsufficientBalance', - ); - }); - - it('Withdraw the total balance from an operator I do not own reverts "CallerNotOwnerWithData"', async () => { - await expect( - ssvNetwork.write.withdrawAllOperatorEarnings([12], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Withdraw more than the operator total balance reverts "InsufficientBalance"', async () => { - await expect(ssvNetwork.write.withdrawOperatorEarnings([13, minDepositAmount])).to.be.rejectedWith( - 'InsufficientBalance', - ); - }); - - it('Withdraw from a cluster without validators', async () => { - cluster1 = await removeValidator(4, DataGenerator.publicKey(1), cluster1.operatorIds, cluster1.cluster); - const currentClusterBalance = minDepositAmount - CONFIG.minimalOperatorFee * 4n; - - await assertEvent( - ssvNetwork.write.withdraw([cluster1.operatorIds, currentClusterBalance, cluster1.cluster], { - account: owners[4].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterWithdrawn', - }, - ], - ); - }); -}); diff --git a/test/common/constants.ts b/test/common/constants.ts new file mode 100644 index 000000000..cc0d42b3e --- /dev/null +++ b/test/common/constants.ts @@ -0,0 +1,56 @@ +import { ethers } from "ethers"; +import { SSVModules } from "./types.ts"; +import type { Cluster } from "./types.ts"; +import { envBigInt, envBigIntArray } from "./env-helpers.ts"; + +export const EMPTY_CLUSTER: Cluster = { + validatorCount: 0n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, +}; + +export const SSV_MODULE_CONTRACTS: Record = { + [SSVModules.SSVOperators]: "SSVOperators", + [SSVModules.SSVClusters]: "SSVClusters", + [SSVModules.SSVDAO]: "SSVDAO", + [SSVModules.SSVViews]: "SSVViews", + [SSVModules.SSVOperatorsWhitelist]: "SSVOperatorsWhitelist", + [SSVModules.SSVStaking]: "SSVStaking", + [SSVModules.SSVValidators]: "SSVValidators", +}; +export const DEFAULT_SHARES = "0x1234"; +export const DEFAULT_ETH_REGISTER_VALUE: bigint = ethers.parseEther("10"); +export const SMALL_ETH_REGISTER_VALUE: bigint = ethers.parseEther("1"); +export const DEFAULT_ETH_EB_PER_VALIDATOR: bigint = 32n; +export const CLUSTER_VERSION_SSV = 0n; +export const CLUSTER_VERSION_ETH = 1n; +export const MINIMAL_OPERATOR_ETH_FEE = envBigInt("FORK_MIN_OPERATOR_ETH_FEE", 1778_800_000n); +export const DEFAULT_OPERATOR_ETH_FEE = 1_778_800_000n; +export const MAXIMUM_OPERATORS_FEE = envBigInt("FORK_MAX_OPERATOR_ETH_FEE", 76528650000000n); +export const NETWORK_FEE_ETH = envBigInt("FORK_NETWORK_FEE_ETH", 3000000000n); +export const NETWORK_FEE = envBigInt("FORK_NETWORK_FEE_SSV", 382640000000n); +export const MINIMUM_BLOCKS_BEFORE_LIQUIDATION = envBigInt("FORK_MIN_BLOCKS_BEFORE_LIQUIDATION", 214800n); +export const MINIMUM_LIQUIDATION_PERIOD_COLLATERAL = envBigInt("FORK_MIN_LIQ_COLLATERAL", 1_000_000_000_000_000n); +export const VALIDATORS_PER_OPERATOR_LIMIT = envBigInt("FORK_VALIDATORS_PER_OPERATOR_LIMIT", 3000n); +export const DECLARE_OPERATOR_FEE_PERIOD = envBigInt("FORK_DECLARE_OPERATOR_FEE_PERIOD", 604800n); +export const EXECUTE_OPERATOR_FEE_PERIOD = envBigInt("FORK_EXECUTE_OPERATOR_FEE_PERIOD", 604800n); +export const OPERATOR_MAX_FEE_INCREASE = envBigInt("FORK_OPERATOR_MAX_FEE_INCREASE", 10000n); +export const MINIMAL_LIQUIDATION_THRESHOLD = 21480n; +export const STAKE_AMOUNT = ethers.parseEther("10"); +export const DEFAULT_ORACLES_IDS = envBigIntArray("FORK_DEFAULT_ORACLE_IDS", [1n, 2n, 3n, 4n]); +export const DEFAULT_UNSTAKE_COOLDOWN = envBigInt( + "FORK_DEFAULT_UNSTAKE_COOLDOWN", + 7n * 24n * 60n * 60n +); +export const DEDUCTED_DIGITS = 10_000_000n; +export const ETH_DEDUCTED_DIGITS = 100_000n; +export const OPERATOR_FEE_PRECISION = ETH_DEDUCTED_DIGITS; +export const BPS_DENOMINATOR = 10000n; +export const QUORUM_BPS = envBigInt("FORK_QUORUM_BPS", 7500n); +export const TOKEN_REGISTER_AMOUNT = ethers.parseEther("100"); +export const MINIMAL_OPERATOR_FEE_SSV = 1000000000n; +export const OP_ETH_FEE_RAW = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; +export const DEFAULT_NETWORK_FEE_RAW = 5_000n; +export const DEFAULT_NETWORK_FEE_UNPACKED = DEFAULT_NETWORK_FEE_RAW * ETH_DEDUCTED_DIGITS; diff --git a/test/common/env-helpers.ts b/test/common/env-helpers.ts new file mode 100644 index 000000000..4048cee87 --- /dev/null +++ b/test/common/env-helpers.ts @@ -0,0 +1,14 @@ +export function envBigInt(name: string, fallback: bigint): bigint { + const raw = process.env[name]; + if (!raw || raw.trim() === "") return fallback; + return BigInt(raw); +} + +export function envBigIntArray(name: string, fallback: bigint[]): bigint[] { + const raw = process.env[name]; + if (!raw || raw.trim() === "") return fallback; + return raw + .split(",") + .map((v) => BigInt(v.trim())) + .filter((v) => v > 0n); +} diff --git a/test/common/errors.ts b/test/common/errors.ts new file mode 100644 index 000000000..2f710bf08 --- /dev/null +++ b/test/common/errors.ts @@ -0,0 +1,72 @@ +export const Errors = { + EMPTY_PUBLIC_KEYS_LIST: "EmptyPublicKeysList", + INVALID_PUBLIC_KEYS_LENGTH: "InvalidPublicKeyLength", + PUBLIC_KEYS_SHARES_LENGTH_MISMATCH: "PublicKeysSharesLengthMismatch", + VALIDATOR_ALREADY_REGISTERED: "ValidatorAlreadyRegistered", + INCORRECT_VALIDATOR_STATE_WITH_DATA: "IncorrectValidatorStateWithData", + VALIDATOR_DOES_NOT_EXIST: "ValidatorDoesNotExist", + INVALID_OPERATOR_IDS_LENGTH: "InvalidOperatorIdsLength", + UNSORTED_OPERATORS_LIST: "UnsortedOperatorsList", + OPERATORS_LIST_NOT_UNIQUE: "OperatorsListNotUnique", + CLUSTER_IS_LIQUIDATED: "ClusterIsLiquidated", + CLUSTER_NOT_LIQUIDATABLE: "ClusterNotLiquidatable", + CLUSTER_ALREADY_ENABLED: "ClusterAlreadyEnabled", + INCORRECT_CLUSTER_VERSION: "IncorrectClusterVersion", + INCORRECT_CLUSTER_STATE: "IncorrectClusterState", + CALLER_NOT_WHITELISTED: "CallerNotWhitelistedWithData", + OPERATOR_VALIDATORS_LIMIT_EXCEEDED: "ExceedValidatorLimitWithData", + INSUFFICIENT_BALANCE: "InsufficientBalance", + FEE_TOO_LOW: "FeeTooLow", + FEE_TOO_HIGH: "FeeTooHigh", + FEE_EXCEEDS_INCREASE_LIMIT: "FeeExceedsIncreaseLimit", + NO_FEE_DECLARED: "NoFeeDeclared", + SAME_FEE_CHANGE_NOT_ALLOWED: "SameFeeChangeNotAllowed", + FEE_INCREASE_NOT_ALLOWED: "FeeIncreaseNotAllowed", + OPERATOR_ALREADY_EXISTS: "OperatorAlreadyExists", + OPERATOR_DOES_NOT_EXIST: "OperatorDoesNotExist", + CALLER_NOT_OWNER: "CallerNotOwnerWithData", + INVALID_WHITELIST_ADDRESSES_LENGTH: "InvalidWhitelistAddressesLength", + ZERO_ADDRESS_NOT_ALLOWED: "ZeroAddressNotAllowed", + INVALID_WHITELISTING_CONTRACT: "InvalidWhitelistingContract", + SAME_FEE_CHANGE_NOW_ALLOWED: "SameFeeChangeNotAllowed", + APPROVAL_NOT_WITHIN_TIMEFRAME: "ApprovalNotWithinTimeframe", + OWNABLE_CALLER_NOT_OWNER: "Ownable: caller is not the owner", + NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM: "NewBlockPeriodIsBelowMinimum", + CLUSTER_DOES_NOT_EXIST: "ClusterDoesNotExist", + INCORRECT_VALIDATOR_STATE: "IncorrectValidatorStateWithData", + STAKE_TOO_LOW: "StakeTooLow", + ZERO_AMOUNT: "ZeroAmount", + COOLDOWN_ACTIVE: "CooldownActive", + UNSTAKE_AMOUNT_EXCEEDS_BALANCE: "UnstakeAmountExceedsBalance", + ROOT_NOT_FOUND: "RootNotFound", + EB_BELOW_MINIMUM: "EBBelowMinimum", + INVALID_PROOF: "InvalidProof", + EB_EXCEEDS_MAXIMUM: "EBExceedsMaximum", + STALE_UPDATE: "StaleUpdate", + MUST_USE_LATEST_ROOT: "MustUseLatestRoot", + UPDATE_TOO_FREQUENT: "UpdateTooFrequent", + NOT_ORACLE: "NotOracle", + STALE_BLOCK_NUMBER: "StaleBlockNumber", + FUTURE_BLOCK_NUMBER: "FutureBlockNumber", + ALREADY_VOTED: "AlreadyVoted", + ZERO_ADDRESS: "ZeroAddress", + ORACLE_ALREADY_ASSIGNED: "OracleAlreadyAssigned", + SAME_ORACLE_ADDRESS_NOT_ALLOWED: "SameOracleAddressNotAllowed", + INVALID_ORACLE_ID: "InvalidOracleId", + INVALID_QUORUM: "InvalidQuorum", + INVALID_OPERATOR_FEE_INCREASE_LIMIT: "InvalidOperatorFeeIncreaseLimit", + INVALID_OPERATOR_FEE_RANGE: "InvalidOperatorFeeRange", + MAX_REQUESTS_AMOUNT_REACHED: "MaxRequestsAmountReached", + NOT_CSSV: "NotCSSV", + INVALID_TOKEN: "InvalidToken", + COOLDOWN_NOT_FINISHED: "CooldownNotFinished", + NOTHING_TO_CLAIM: "NothingToClaim", + NOTHING_TO_WITHDRAW: "NothingToWithdraw", + TOKEN_TRANSFER_FAILED: "TokenTransferFailed", + ETH_TRANSFER_FAILED: "ETHTransferFailed", + LEGACY_OPERATOR_FEE_DECLARATION_INVALID: "LegacyOperatorFeeDeclarationInvalid", + ZERO_CSSV_SUPPLY: "ZeroCSSVSupply", + INSUFFICIENT_CSSV_SUPPLY: "InsufficientCSSVSupply", + MAX_VALUE_EXCEEDED: "MaxValueExceeded", + MAX_PRECISION_EXCEEDED: "MaxPrecisionExceeded", +} as const; diff --git a/test/common/events.ts b/test/common/events.ts new file mode 100644 index 000000000..ec95d7d97 --- /dev/null +++ b/test/common/events.ts @@ -0,0 +1,49 @@ +export const Events = { + VALIDATOR_ADDED: "ValidatorAdded", + VALIDATOR_REMOVED: "ValidatorRemoved", + VALIDATOR_EXITED: "ValidatorExited", + CLUSTER_LIQUIDATED: "ClusterLiquidated", + CLUSTER_REACTIVATED: "ClusterReactivated", + CLUSTER_BALANCE_UPDATED: "ClusterBalanceUpdated", + CLUSTER_DEPOSITED: "ClusterDeposited", + CLUSTER_WITHDRAWN: "ClusterWithdrawn", + CLUSTER_MIGRATED_TO_ETH: "ClusterMigratedToETH", + OPERATOR_ADDED: "OperatorAdded", + OPERATOR_PRIVACY_STATUS_UPDATED: "OperatorPrivacyStatusUpdated", + OPERATOR_REMOVED: "OperatorRemoved", + OPERATOR_MULTIPLE_WHITELIST_UPDATED: "OperatorMultipleWhitelistUpdated", + OPERATOR_MULTIPLE_WHITELIST_REMOVED: "OperatorMultipleWhitelistRemoved", + OPERATORS_WHITELISTING_CONTRACT_UPDATED: "OperatorWhitelistingContractUpdated", + OPERATORS_PRIVACY_STATUS_UPDATED: "OperatorPrivacyStatusUpdated", + OPERATOR_FEE_DECLARED: "OperatorFeeDeclared", + OPERATOR_FEE_DECLARATION_CANCELLED: "OperatorFeeDeclarationCancelled", + OPERATOR_FEE_EXECUTED: "OperatorFeeExecuted", + OPERATOR_WITHDRAWN: "OperatorWithdrawn", + OPERATOR_WITHDRAWN_SSV: "OperatorWithdrawnSSV", + FEE_RECIPIENT_ADDRESS_UPDATED: "FeeRecipientAddressUpdated", + OPERATOR_FEE_INCREASE_LIMIT_UPDATED: "OperatorFeeIncreaseLimitUpdated", + DECLARE_OPERATOR_FEE_PERIOD_UPDATED: "DeclareOperatorFeePeriodUpdated", + EXECUTE_OPERATOR_FEE_PERIOD_UPDATED: "ExecuteOperatorFeePeriodUpdated", + LIQUIDATION_THRESHOLD_PERIOD_UPDATED: "LiquidationThresholdPeriodUpdated", + LIQUIDATION_THRESHOLD_PERIOD_UPDATED_SSV: "LiquidationThresholdPeriodSSVUpdated", + MINIMUM_LIQUIDATION_COLLATERAL_UPDATED: "MinimumLiquidationCollateralUpdated", + MINIMUM_LIQUIDATION_COLLATERAL_UPDATED_SSV: "MinimumLiquidationCollateralSSVUpdated", + OPERATOR_MAXIMUM_FEE_UPDATED: "OperatorMaximumFeeUpdated", + MINIMUM_OPERATOR_ETH_FEE_UPDATED: "MinimumOperatorEthFeeUpdated", + STAKED: "Staked", + UNSTAKE_REQUESTED: "UnstakeRequested", + UNSTAKE_WITHDRAWN: "UnstakedWithdrawn", + NETWORK_FEE_UPDATED: "NetworkFeeUpdated", + NETWORK_FEE_UPDATED_SSV: "NetworkFeeUpdatedSSV", + NETWORK_EARNINGS_WITHDRAWN: "NetworkEarningsWithdrawn", + ROOT_COMMITTED: "RootCommitted", + WEIGHTED_ROOT_PROPOSED: "WeightedRootProposed", + COOLDOWN_DURATION_UPDATED: "CooldownDurationUpdated", + MIN_BLOCKS_BETWEEN_UPDATES_UPDATED: "MinBlocksBetweenUpdatesUpdated", + ORACLE_REPLACED: "OracleReplaced", + QUORUM_UPDATED: "QuorumUpdated", + FEES_SYNCED: "FeesSynced", + REWARDS_SETTLED: "RewardsSettled", + REWARDS_CLAIMED: "RewardsClaimed", + ERC20_RESCUED: "ERC20Rescued", +} as const; diff --git a/test/common/helpers.ts b/test/common/helpers.ts new file mode 100644 index 000000000..88867fee2 --- /dev/null +++ b/test/common/helpers.ts @@ -0,0 +1 @@ +export * from "../helpers/index.ts"; diff --git a/test/common/types.ts b/test/common/types.ts new file mode 100644 index 000000000..135527179 --- /dev/null +++ b/test/common/types.ts @@ -0,0 +1,62 @@ +import hre from "hardhat"; + +export interface Cluster { + validatorCount: bigint; + networkFeeIndex: bigint; + index: bigint; + balance: bigint; + active: boolean; +} + +export interface Operator { + owner: string; + ethFee: bigint; + ethValidatorCount: bigint; + whitelistedAddress: string; + isPrivate: boolean; + isActive: boolean; +} + +export interface OperatorSSV { + owner: string; + fee: bigint; + validatorCount: bigint; + whitelistedAddress: string; + isPrivate: boolean; + isActive: boolean; +} + +export interface UnstakeRequest { + amount: bigint; + unlockTime: bigint; +} + +export type ClusterTuple = readonly [ + validatorCount: bigint, + networkFeeIndex: bigint, + index: bigint, + active: boolean, + balance: bigint +]; + +export type OperatorTuple = readonly [ + owner: string, + ethFee: bigint, + ethValidatorCount: bigint, + whitelistedAddress: string, + isPrivate: boolean, + isActive: boolean +]; + +export enum SSVModules { + SSVOperators = 0, + SSVClusters = 1, + SSVDAO = 2, + SSVViews = 3, + SSVOperatorsWhitelist = 4, + SSVStaking = 5, + SSVValidators = 6 +} + +export type NetworkHelpersType = + Awaited>["networkHelpers"]; diff --git a/test/dao/liquidation-collateral.ts b/test/dao/liquidation-collateral.ts deleted file mode 100644 index d80606695..000000000 --- a/test/dao/liquidation-collateral.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Declare imports -import { owners, initializeContract, CONFIG } from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; - -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, networkFee: BigInt; - -describe('Liquidation Collateral Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - - // Define minumum allowed network fee to pass shrinkable validation - networkFee = CONFIG.minimalOperatorFee / 10n; - }); - - it('Change minimum collateral emits "MinimumLiquidationCollateralUpdated"', async () => { - await assertEvent(ssvNetwork.write.updateMinimumLiquidationCollateral([CONFIG.minimumLiquidationCollateral * 2]), [ - { - contract: ssvNetwork, - eventName: 'MinimumLiquidationCollateralUpdated', - argNames: ['value'], - argValuesList: [[CONFIG.minimumLiquidationCollateral * 2]], - }, - ]); - }); - - it('Change minimum collateral gas limits', async () => { - await trackGas(ssvNetwork.write.updateMinimumLiquidationCollateral([CONFIG.minimumLiquidationCollateral * 2]), [ - GasGroup.CHANGE_MINIMUM_COLLATERAL, - ]); - }); - - it('Get minimum collateral', async () => { - expect(await ssvViews.read.getMinimumLiquidationCollateral()).to.equal(CONFIG.minimumLiquidationCollateral); - }); - - it('Change minimum collateral reverts "caller is not the owner"', async () => { - await expect( - ssvNetwork.write.updateMinimumLiquidationCollateral([CONFIG.minimumLiquidationCollateral * 2], { - account: owners[3].account, - }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); -}); diff --git a/test/dao/liquidation-threshold.ts b/test/dao/liquidation-threshold.ts deleted file mode 100644 index 7b1f703c8..000000000 --- a/test/dao/liquidation-threshold.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Declare imports -import { owners, initializeContract, CONFIG } from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; - -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, networkFee: any; - -describe('Liquidation Threshold Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - - // Define minumum allowed network fee to pass shrinkable validation - networkFee = CONFIG.minimalOperatorFee / 10n; - }); - - it('Change liquidation threshold period emits "LiquidationThresholdPeriodUpdated"', async () => { - await assertEvent(ssvNetwork.write.updateLiquidationThresholdPeriod([CONFIG.minimalBlocksBeforeLiquidation + 10]), [ - { - contract: ssvNetwork, - eventName: 'LiquidationThresholdPeriodUpdated', - argNames: ['value'], - argValuesList: [[CONFIG.minimalBlocksBeforeLiquidation + 10]], - }, - ]); - }); - - it('Change liquidation threshold period gas limits', async () => { - await trackGas(ssvNetwork.write.updateLiquidationThresholdPeriod([CONFIG.minimalBlocksBeforeLiquidation + 10]), [ - GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD, - ]); - }); - - it('Get liquidation threshold period', async () => { - expect(await ssvViews.read.getLiquidationThresholdPeriod()).to.equal(CONFIG.minimalBlocksBeforeLiquidation); - }); - - it('Change liquidation threshold period reverts "NewBlockPeriodIsBelowMinimum"', async () => { - await expect( - ssvNetwork.write.updateLiquidationThresholdPeriod([CONFIG.minimalBlocksBeforeLiquidation - 10]), - ).to.be.rejectedWith('NewBlockPeriodIsBelowMinimum'); - }); - - it('Change liquidation threshold period reverts "caller is not the owner"', async () => { - await expect( - ssvNetwork.write.updateLiquidationThresholdPeriod([CONFIG.minimalBlocksBeforeLiquidation], { - account: owners[3].account, - }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); -}); diff --git a/test/dao/network-fee-change.ts b/test/dao/network-fee-change.ts deleted file mode 100644 index 853d2bed9..000000000 --- a/test/dao/network-fee-change.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Declare imports -import { owners, initializeContract, CONFIG } from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; - -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, networkFee: any; - -describe('Network Fee Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - - // Define minumum allowed network fee to pass shrinkable validation - networkFee = CONFIG.minimalOperatorFee / 10n; - }); - - it('Change network fee emits "NetworkFeeUpdated"', async () => { - await assertEvent(ssvNetwork.write.updateNetworkFee([networkFee]), [ - { - contract: ssvNetwork, - eventName: 'NetworkFeeUpdated', - argNames: ['oldFee', 'newFee'], - argValuesList: [[0, networkFee]], - }, - ]); - }); - - it('Change network fee providing UINT64 max value reverts "Max value exceeded"', async () => { - const amount = 2n ** 64n * 100000000n; - await expect(ssvNetwork.write.updateNetworkFee([amount])).to.be.rejectedWith('Max value exceeded'); - }); - - it('Change network fee when it was set emits "NetworkFeeUpdated"', async () => { - const initialNetworkFee = CONFIG.minimalOperatorFee; - await ssvNetwork.write.updateNetworkFee([initialNetworkFee]); - - it('Change network fee emits "NetworkFeeUpdated"', async () => { - await assertEvent(ssvNetwork.write.updateNetworkFee([networkFee]), [ - { - contract: ssvNetwork, - eventName: 'NetworkFeeUpdated', - argNames: ['oldFee', 'newFee'], - argValuesList: [[initialNetworkFee, networkFee]], - }, - ]); - }); - }); - - it('Change network fee gas limit', async () => { - await trackGas(ssvNetwork.write.updateNetworkFee([networkFee]), [GasGroup.NETWORK_FEE_CHANGE]); - }); - - it('Get network fee', async () => { - expect(await ssvViews.read.getNetworkFee()).to.equal(0); - }); - - it('Change the network fee to a number below the minimum fee reverts "Max precision exceeded"', async () => { - await expect(ssvNetwork.write.updateNetworkFee([networkFee - 1n])).to.be.rejectedWith('Max precision exceeded'); - }); - - it('Change the network fee to a number that exceeds allowed type limit reverts "Max value exceeded"', async () => { - const amount = 2n ** 64n * 100000000n; - - await expect(ssvNetwork.write.updateNetworkFee([amount + 1n])).to.be.rejectedWith('Max value exceeded'); - }); - - it('Change network fee from an address thats not the DAO reverts "caller is not the owner"', async () => { - await expect( - ssvNetwork.write.updateNetworkFee([networkFee], { - account: owners[3].account, - }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); -}); diff --git a/test/dao/network-fee-withdraw.ts b/test/dao/network-fee-withdraw.ts deleted file mode 100644 index ba03450f5..000000000 --- a/test/dao/network-fee-withdraw.ts +++ /dev/null @@ -1,115 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; - -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, minDepositAmount: BigInt, burnPerBlock: BigInt, networkFee: BigInt; - -describe('DAO Network Fee Withdraw Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - - // Define minumum allowed network fee to pass shrinkable validation - networkFee = CONFIG.minimalOperatorFee; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - burnPerBlock = CONFIG.minimalOperatorFee * 4n + networkFee; - minDepositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation) * burnPerBlock; - - // Set network fee - await ssvNetwork.write.updateNetworkFee([networkFee]); - - // Register validators - // cold register - await coldRegisterValidator(); - - await bulkRegisterValidators( - 4, - 1, - DEFAULT_OPERATOR_IDS[4], - minDepositAmount, - { validatorCount: 0, networkFeeIndex: 0, index: 0, balance: 0n, active: true }, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ); - await mine(10); - }); - - it('Withdraw network earnings emits "NetworkEarningsWithdrawn"', async () => { - const amount = await ssvViews.read.getNetworkEarnings(); - - await assertEvent(ssvNetwork.write.withdrawNetworkEarnings([amount]), [ - { - contract: ssvNetwork, - eventName: 'NetworkEarningsWithdrawn', - argNames: ['value', 'recipient'], - argValuesList: [[amount, owners[0].account.address]], - }, - ]); - }); - - it('Withdraw network earnings gas limits', async () => { - const amount = await ssvViews.read.getNetworkEarnings(); - await trackGas(ssvNetwork.write.withdrawNetworkEarnings([amount]), [GasGroup.WITHDRAW_NETWORK_EARNINGS]); - }); - - it('Get withdrawable network earnings', async () => { - expect(await ssvViews.read.getNetworkEarnings()).to.above(0); - }); - - it('Get withdrawable network earnings as not owner', async () => { - expect( - await ssvViews.read.getNetworkEarnings([], { - account: owners[3].account, - }), - ).to.equal(CONFIG.minimalOperatorFee * 12n + CONFIG.minimalOperatorFee * 10n); - }); - - it('Withdraw network earnings with not enough balance reverts "InsufficientBalance"', async () => { - const amount = (await ssvViews.read.getNetworkEarnings()) * 2n; - await expect(ssvNetwork.write.withdrawNetworkEarnings([amount])).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Withdraw network earnings from an address thats not the DAO reverts "caller is not the owner"', async () => { - const amount = await ssvViews.read.getNetworkEarnings(); - await expect( - ssvNetwork.write.withdrawNetworkEarnings([amount], { - account: owners[3].account, - }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); - - it('Withdraw network earnings providing UINT64 max value reverts "Max value exceeded"', async () => { - const amount = 2n ** 64n * 100000000n; - await expect(ssvNetwork.write.withdrawNetworkEarnings([amount])).to.be.rejectedWith('Max value exceeded'); - }); - - it('Withdraw network earnings sequentially when not enough balance reverts "InsufficientBalance"', async () => { - const amount = (await ssvViews.read.getNetworkEarnings()) / 2n; - - await ssvNetwork.write.withdrawNetworkEarnings([amount]); - expect(await ssvViews.read.getNetworkEarnings()).to.be.equals(networkFee * 13n + networkFee * 11n - amount); - - await ssvNetwork.write.withdrawNetworkEarnings([amount]); - expect(await ssvViews.read.getNetworkEarnings()).to.be.equals(networkFee * 14n + networkFee * 12n - amount * 2n); - - await expect(ssvNetwork.write.withdrawNetworkEarnings([amount])).to.be.rejectedWith('InsufficientBalance'); - }); -}); diff --git a/test/dao/operational.ts b/test/dao/operational.ts deleted file mode 100644 index 9e5072ba8..000000000 --- a/test/dao/operational.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - bulkRegisterValidators, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; - -import { expect } from 'chai'; - -let ssvNetwork: any, ssvViews: any, firstCluster: any; - -// Declare globals -describe('DAO operational Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - }); - - it('Starting the transfer process does not change owner', async () => { - await ssvNetwork.write.transferOwnership([owners[4].account.address]); - - expect(await ssvNetwork.read.owner()).to.deep.equal(owners[0].account.address); - }); - - it('Ownership is transferred in a 2-step process', async () => { - await ssvNetwork.write.transferOwnership([owners[4].account.address]); - await ssvNetwork.write.acceptOwnership([], { account: owners[4].account }); - - expect(await ssvNetwork.read.owner()).to.deep.equal(owners[4].account.address); - }); - - it('Get the network validators count (add/remove validaotor)', async () => { - await registerOperators(0, 4, CONFIG.minimalOperatorFee); - - const deposit = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * CONFIG.minimalOperatorFee * 13n; - - firstCluster = ( - await bulkRegisterValidators(4, 1, DEFAULT_OPERATOR_IDS[4], deposit, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - - expect(await ssvViews.read.getNetworkValidatorsCount()).to.equal(1); - - await ssvNetwork.write.removeValidator( - [DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], - { account: owners[4].account }, - ); - - expect(await ssvViews.read.getNetworkValidatorsCount()).to.equal(0); - }); - - it('Get the network validators count (add/remove validaotor)', async () => { - await registerOperators(0, 4, CONFIG.minimalOperatorFee); - - const deposit = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * (CONFIG.minimalOperatorFee * 13n); - - firstCluster = ( - await bulkRegisterValidators(4, 1, DEFAULT_OPERATOR_IDS[4], deposit, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - - expect(await ssvViews.read.getNetworkValidatorsCount()).to.equal(1); - - await mine(CONFIG.minimalBlocksBeforeLiquidation); - - await ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster], { - account: owners[4].account, - }); - - expect(await ssvViews.read.getNetworkValidatorsCount()).to.equal(0); - }); -}); diff --git a/test/deployment/deploy.ts b/test/deployment/deploy.ts deleted file mode 100644 index 569b3e672..000000000 --- a/test/deployment/deploy.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Imports -import { - owners, - initializeContract, - DataGenerator, - CONFIG, - publicClient, -} from '../helpers/contract-helpers'; -import { ethers, upgrades } from 'hardhat'; -import { expect } from 'chai'; - -import hre from 'hardhat'; -import { Address, encodeFunctionData } from 'viem'; - -describe('Deployment tests', () => { - let ssvNetwork: any, ssvViews: any, ssvToken: any; - - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - }); - - it('Check default values after deploying', async () => { - expect(await ssvViews.read.getNetworkValidatorsCount()).to.equal(0); - expect(await ssvViews.read.getNetworkEarnings()).to.equal(0); - expect(await ssvViews.read.getOperatorFeeIncreaseLimit()).to.equal(CONFIG.operatorMaxFeeIncrease); - expect(await ssvViews.read.getOperatorFeePeriods()).to.deep.equal([ - CONFIG.declareOperatorFeePeriod, - CONFIG.executeOperatorFeePeriod, - ]); - expect(await ssvViews.read.getLiquidationThresholdPeriod()).to.equal(CONFIG.minimalBlocksBeforeLiquidation); - expect(await ssvViews.read.getMinimumLiquidationCollateral()).to.equal(CONFIG.minimumLiquidationCollateral); - expect(await ssvViews.read.getValidatorsPerOperatorLimit()).to.equal(CONFIG.validatorsPerOperatorLimit); - expect(await ssvViews.read.getOperatorFeeIncreaseLimit()).to.equal(CONFIG.operatorMaxFeeIncrease); - }); - - it('Upgrade SSVNetwork contract. Check new function execution', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }); - - const BasicUpgrade = await ethers.getContractFactory('SSVNetworkBasicUpgrade'); - const ssvNetworkUpgrade = await upgrades.upgradeProxy(ssvNetwork.address, BasicUpgrade, { - kind: 'uups', - unsafeAllow: ['delegatecall'], - }); - await ssvNetworkUpgrade.waitForDeployment(); - const ssvNetworkAddress = await ssvNetworkUpgrade.getAddress(); - - ssvNetwork = await hre.viem.getContractAt('SSVNetworkBasicUpgrade', ssvNetworkAddress as Address); - - await ssvNetwork.write.resetNetworkFee([10000000]); - expect(await ssvViews.read.getNetworkFee()).to.equal(10000000); - }); - - it('Upgrade SSVNetwork contract. Deploy implemetation manually', async () => { - // Get current SSVNetwork proxy - const deployedSSVNetwork = await hre.viem.getContractAt('SSVNetwork', ssvNetwork.address as Address); - - // Deploy a new implementation with another account - const contractImpl = await hre.viem.deployContract('SSVNetworkBasicUpgrade', [], { - client: owners[1].client, - }); - - const newNetworkFee = 10000000n; - const calldata = encodeFunctionData({ - abi: contractImpl.abi, - functionName: 'resetNetworkFee', - args: [newNetworkFee], - }); - - // The owner of SSVNetwork contract peforms the upgrade - await deployedSSVNetwork.write.upgradeToAndCall([contractImpl.address, calldata]); - - expect(await ssvViews.read.getNetworkFee()).to.equal(10000000); - }); - - it('Upgrade SSVNetwork contract. Check base contract is not re-initialized', async () => { - const BasicUpgrade = await ethers.getContractFactory('SSVNetworkBasicUpgrade'); - const ssvNetworkUpgrade = await upgrades.upgradeProxy(ssvNetwork.address, BasicUpgrade, { - kind: 'uups', - unsafeAllow: ['delegatecall'], - }); - await ssvNetworkUpgrade.waitForDeployment(); - - const address = await upgrades.erc1967.getImplementationAddress(await ssvNetworkUpgrade.getAddress()); - - const instance = await hre.viem.getContractAt('SSVNetworkBasicUpgrade', address as Address); - - await expect( - instance.write.initialize( - [ - '0x6471F70b932390f527c6403773D082A0Db8e8A9F', - '0x6471F70b932390f527c6403773D082A0Db8e8A9F', - '0x6471F70b932390f527c6403773D082A0Db8e8A9F', - '0x6471F70b932390f527c6403773D082A0Db8e8A9F', - '0x6471F70b932390f527c6403773D082A0Db8e8A9F', - 2000000n, - 2000000n, - 2000000, - 2000000n, - 2000000n, - 2000n, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('Initializable: contract is already initialized'); - }); - - it('Upgrade SSVNetwork contract. Check state is only changed from proxy contract', async () => { - const BasicUpgrade = await ethers.getContractFactory('SSVNetworkBasicUpgrade'); - const ssvNetworkUpgrade = await upgrades.upgradeProxy(ssvNetwork.address, BasicUpgrade, { - kind: 'uups', - unsafeAllow: ['delegatecall'], - }); - await ssvNetworkUpgrade.waitForDeployment(); - - const address = await upgrades.erc1967.getImplementationAddress(await ssvNetworkUpgrade.getAddress()); - const instance = await hre.viem.getContractAt('SSVNetworkBasicUpgrade', address as Address); - - await instance.write.resetNetworkFee([100000000000n], { account: owners[1].account }); - - expect(await ssvViews.read.getNetworkFee()).to.be.equals(0); - }); - - it('ETH can not be transferred to SSVNetwork / SSVNetwork views', async () => { - const amount = 10000000n; - - await expect( - owners[0].sendTransaction({ - to: ssvNetwork.address, - value: amount, - }), - ).to.be.rejected; - - await expect( - owners[0].sendTransaction({ - to: ssvViews.address, - value: amount, - }), - ).to.be.rejected; - - expect(await publicClient.getBalance({ address: ssvNetwork.address })).to.be.equal(0); - expect(await publicClient.getBalance({ address: ssvViews.address })).to.be.equal(0); - }); -}); diff --git a/test/e2e/COVERAGE-REPORT.md b/test/e2e/COVERAGE-REPORT.md new file mode 100644 index 000000000..fef1c198d --- /dev/null +++ b/test/e2e/COVERAGE-REPORT.md @@ -0,0 +1,107 @@ +# E2E Test Coverage Report + +**Generated:** 2026-02-18 +**Branch:** `implement--e2e-integration-pass` +**Test command:** `npx hardhat test test/e2e/**/*.test.ts` + +## Summary + +| Metric | Count | +|---|---| +| Total scenarios (from SCENARIO-TESTS.md) | 107 | +| Tests implemented | 209 | +| Tests passing | 209 | +| Tests failing | 0 | +| Tests skipped | 0 | +| Missing scenarios | 0 | + +## Scenario Coverage by Module + +### Operators & Validators (OV-1 to OV-35) — 35 scenarios, 100% covered + +| File | Scenarios | Tests | +|---|---|---| +| `operators/operator-lifecycle.test.ts` | OV-1, OV-2, OV-3, OV-11, OV-12, OV-13, OV-14 | 23 | +| `operators/operator-economics.test.ts` | OV-13, OV-15, OV-16, OV-17, OV-18 | 8 | +| `operators/operator-edge-cases.test.ts` | OV-21, OV-23, OV-24, OV-28, OV-29 | 9 | +| `operators/operator-reverts.test.ts` | OV-19 (partial), OV-21 | 6 | +| `validators/validator-lifecycle.test.ts` | OV-4, OV-5, OV-6, OV-7, OV-8, OV-9, OV-10 | 17 | +| `validators/validator-edge-cases.test.ts` | OV-19, OV-20, OV-22, OV-25, OV-26, OV-27, OV-30–OV-35 | 20 | + +### Cluster Mechanics (CM-1 to CM-30) — 30 scenarios, 100% covered + +| File | Scenarios | Tests | +|---|---|---| +| `clusters-eth/cluster-eth-lifecycle.test.ts` | CM-1, CM-2, CM-3, CM-9, CM-10 | 9 | +| `clusters-eth/cluster-eth-liquidation.test.ts` | CM-3 ext, CM-14, CM-15 | 3 | +| `clusters-eth/cluster-eth-eb.test.ts` | CM-12, CM-13 | 2 | +| `clusters-eth/cluster-eth-edge.test.ts` | CM-19, CM-20, CM-23, CM-24, CM-26 | 6 | +| `clusters-eth/cluster-reverts.test.ts` | CM-21 | 3 | +| `clusters-eth/cluster-conservation.test.ts` | CM-16 | 1 | +| `clusters-ssv/cluster-ssv-legacy.test.ts` | CM-4, CM-11 | 6 | +| `clusters-ssv/cluster-ssv-fees.test.ts` | CM-17, CM-25 | 2 | +| `migration/migration-basic.test.ts` | CM-5, CM-6, CM-7, CM-8 | 6 | +| `migration/migration-edge.test.ts` | CM-18, CM-22, CM-27, CM-28, CM-29 | 7 | +| `migration/migration-full-lifecycle.test.ts` | CM-30 | 1 | + +### Effective Balance & Staking (ES-1 to ES-32) — 32 scenarios, 100% covered + +| File | Scenarios | Tests | +|---|---|---| +| `effective-balance/oracle-commits.test.ts` | ES-1, ES-2, ES-3, ES-4, ES-5 | 14 | +| `effective-balance/eb-updates.test.ts` | ES-6, ES-7, ES-8, ES-9, ES-10 | 5 | +| `effective-balance/eb-operator-vunits.test.ts` | ES-11 | 1 | +| `effective-balance/eb-edge-cases.test.ts` | ES-12, ES-13, ES-14 | 15 | +| `staking/staking-lifecycle.test.ts` | ES-15, ES-16, ES-17, ES-18 | 7 | +| `staking/staking-edge-cases.test.ts` | ES-20, ES-21, ES-22, ES-23, ES-26, ES-29 | 11 | +| `staking/staking-rewards.test.ts` | ES-24, ES-25, ES-27, ES-28, ES-31, ES-32 | 8 | +| `staking/staking-transfers.test.ts` | ES-19, ES-30 | 7 | + +### Cross-Cutting (CC-1 to CC-10) — 10 scenarios, 100% covered + +| File | Scenarios | Tests | +|---|---|---| +| `cross-cutting/economics.test.ts` | CC-1, CC-2, CC-5 | 3 | +| `cross-cutting/multi-step-flows.test.ts` | CC-3, CC-7, CC-9 | 4 | +| `cross-cutting/staking-integration.test.ts` | CC-4, CC-6, CC-8 | 3 | +| `cross-cutting/full-lifecycle.test.ts` | CC-10 | 1 | +| `smoke.test.ts` | (smoke) | 1 | + +## Discrepancy Annotations + +14 formal `// TODO(DISC-XX):` annotations added across 7 files, covering 8 discrepancies between code behavior and FLOWS.md specification: + +| ID | Description | Files | +|---|---|---| +| DISC-OV-1 | `registerOperator` always emits `OperatorPrivacyStatusUpdated` even for public operators | `operator-lifecycle.test.ts` | +| DISC-OV-3 | `removeOperator` does NOT check `validatorCount == 0` | `operator-edge-cases.test.ts` | +| DISC-OV-8 | `deposit` does NOT settle fees or update operator snapshots | `cluster-eth-lifecycle.test.ts` (3), `validator-edge-cases.test.ts` | +| DISC-OV-9 | `deposit` does NOT check `cluster.active` | `cluster-eth-lifecycle.test.ts` | +| DISC-CM-3 | `withdraw` does NOT update operator snapshots | `cluster-eth-lifecycle.test.ts`, `cluster-eth-edge.test.ts`, `migration-full-lifecycle.test.ts` | +| DISC-CM-5 | `reactivate` uses additive `balance += msg.value` | `cluster-eth-lifecycle.test.ts` (2) | +| DISC-ES-6 | `_updateOperatorVUnits` applies FULL delta per operator | `eb-operator-vunits.test.ts` | +| DISC-CC-1 | `removeOperator` does NOT delete `operatorFeeChangeRequests` | `operator-edge-cases.test.ts` | + +## Weak Assertion Audit + +The following assertions were strengthened from weak (`closeTo`, `greaterThan(0n)`, `greaterThanOrEqual`) to exact (`equal`) with computed expected values: + +1. `operator-economics.test.ts` — `closeTo` -> `equal` for identical operator earnings comparison +2. `migration-basic.test.ts` — `greaterThanOrEqual(1)` -> `equal(1)` for `ethValidatorCount` +3. `staking-integration.test.ts` — Removed redundant `greaterThanOrEqual(0n)` DAO earnings check (already verified implicitly) + +Remaining weak assertions are intentional (conservation law lower bounds, monotonicity checks, snapshot-dependent computations where exact values depend on operator registration timing). + +## Helpers + +All shared helpers are centralized in `test/e2e/helpers/`: + +| File | Exports | +|---|---| +| `fee-calculator.ts` | `calcOperatorFeeAccrual`, `calcClusterBurn`, `calcNetworkFeeAccrual`, `calcVUnits`, `defaultVUnits`, `calcSSVClusterFees`, `calcLiquidationThreshold`, `calcAccEthPerShareDelta`, `calcStakingReward` | +| `block-helpers.ts` | `mineBlocks`, `getBlockNumber`, `getTxBlock`, `snapshotContractBalance` | +| `balance-tracker.ts` | `BalanceTracker` class for multi-step balance tracking | +| `invariant-checker.ts` | `checkETHConservation` | +| `index.ts` | Re-exports all helpers | + +No duplicate helper code found in test files. diff --git a/test/e2e/clusters-eth/cluster-conservation.test.ts b/test/e2e/clusters-eth/cluster-conservation.test.ts new file mode 100644 index 000000000..a114e1d80 --- /dev/null +++ b/test/e2e/clusters-eth/cluster-conservation.test.ts @@ -0,0 +1,187 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + getCurrentClusterState, + addValidatorsToCluster, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, +} from "../../common/constants.ts"; +import { + mineBlocks, + snapshotContractBalance, + checkETHConservation, +} from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +describe("Conservation Law — Multi-Cluster ETH Balance Tracking", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwnerA: HardhatEthersSigner; + let clusterOwnerB: HardhatEthersSigner; + let clusterOwnerC: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwnerA, clusterOwnerB, clusterOwnerC] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + it("Maintains ETH conservation across deposits, withdrawals, liquidations, and operator withdrawals", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const networkAddress = await network.getAddress(); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + clusterOwnerB.address, + clusterOwnerC.address, + ]); + + const depositA = ethers.parseEther("5"); + await network.connect(clusterOwnerA).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositA }, + ); + let clusterA = await getCurrentClusterState(connection, network, clusterOwnerA.address, operatorIds); + + clusterA = await addValidatorsToCluster( + connection, + network, + [makePublicKey(2)], + [DEFAULT_SHARES], + clusterOwnerA, + operatorIds, + clusterA, + ); + + const depositB = ethers.parseEther("3"); + await network.connect(clusterOwnerB).registerValidator( + makePublicKey(3), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositB }, + ); + let clusterB = await getCurrentClusterState(connection, network, clusterOwnerB.address, operatorIds); + + const depositC = ethers.parseEther("8"); + await network.connect(clusterOwnerC).registerValidator( + makePublicKey(4), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositC }, + ); + let clusterC = await getCurrentClusterState(connection, network, clusterOwnerC.address, operatorIds); + + clusterC = await addValidatorsToCluster( + connection, + network, + [makePublicKey(5), makePublicKey(6)], + [DEFAULT_SHARES, DEFAULT_SHARES], + clusterOwnerC, + operatorIds, + clusterC, + ); + + let contractBalance = await snapshotContractBalance(provider, networkAddress); + const expectedContractBalance = depositA + DEFAULT_ETH_REGISTER_VALUE + depositB + depositC + DEFAULT_ETH_REGISTER_VALUE; + expect(contractBalance).to.equal(expectedContractBalance); + const clusterABalance = BigInt(clusterA.balance); + const clusterBBalance = BigInt(clusterB.balance); + const clusterCBalance = BigInt(clusterC.balance); + await checkETHConservation( + networkAddress, + provider, + [clusterABalance, clusterBBalance, clusterCBalance], + [], + 0n, + ); + + await mineBlocks(provider, 1000); + + const clusterBBalanceView = await views.getBalance( + clusterOwnerB.address, + operatorIds, + clusterB, + ); + + await network.connect(clusterOwnerB).liquidate( + clusterOwnerB.address, + operatorIds, + clusterB, + ); + clusterB = await getCurrentClusterState(connection, network, clusterOwnerB.address, operatorIds); + + + const withdrawAmount = ethers.parseEther("1"); + await network.connect(clusterOwnerA).withdraw( + operatorIds, + withdrawAmount, + clusterA, + ); + clusterA = await getCurrentClusterState(connection, network, clusterOwnerA.address, operatorIds); + + const depositExtra = ethers.parseEther("2"); + await network.connect(clusterOwnerC).deposit( + clusterOwnerC.address, + operatorIds, + clusterC, + { value: depositExtra }, + ); + clusterC = await getCurrentClusterState(connection, network, clusterOwnerC.address, operatorIds); + + const clusterACurrentBalance = BigInt(await views.getBalance( + clusterOwnerA.address, + operatorIds, + clusterA, + )); + + const clusterCCurrentBalance = BigInt(await views.getBalance( + clusterOwnerC.address, + operatorIds, + clusterC, + )); + + const finalClusterBalances: bigint[] = [ + clusterACurrentBalance, + clusterCCurrentBalance, + ]; + + const operatorEarnings: bigint[] = []; + for (const opId of operatorIds) { + const earnings = await views.getOperatorEarnings(BigInt(opId)); + operatorEarnings.push(BigInt(earnings)); + } + + const daoEarnings = await views.getNetworkEarnings(); + await checkETHConservation( + networkAddress, + provider, + finalClusterBalances, + operatorEarnings, + BigInt(daoEarnings), + ); + + expect(await views.isLiquidated(clusterOwnerB, operatorIds, clusterB)).to.be.true; + }); +}); diff --git a/test/e2e/clusters-eth/cluster-eth-eb.test.ts b/test/e2e/clusters-eth/cluster-eth-eb.test.ts new file mode 100644 index 000000000..40eeb886f --- /dev/null +++ b/test/e2e/clusters-eth/cluster-eth-eb.test.ts @@ -0,0 +1,287 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture, ssvNetworkFullPreUpgradeFixture, upgradeToStakingVersion } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + getCurrentClusterState, + parseClusterFromEvent, + registerOperators, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEFAULT_ETH_REGISTER_VALUE, + ETH_DEDUCTED_DIGITS, + MINIMAL_OPERATOR_ETH_FEE, + EMPTY_CLUSTER, + OP_ETH_FEE_RAW, + DEFAULT_NETWORK_FEE_RAW, + DEFAULT_NETWORK_FEE_UNPACKED, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + calcVUnits, + defaultVUnits, +} from "../../helpers/index.ts"; +import { + setupOracles, + commitEBRoot, + computeClusterId, + computeEBRoot, + makeOperatorKey, +} from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +const NUM_OPERATORS = 4n; + +describe("ETH Cluster with Explicit EB", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let staker: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, oracle1, oracle2, oracle3, oracle4, staker] } = await setupTestContext()); + }); + + describe("Fee Scaling With Explicit EB", () => { + const deployFixture = async () => { + const { network, views, ssvToken } = await ssvNetworkFullFixture(connection); + + await network.updateNetworkFee(DEFAULT_NETWORK_FEE_UNPACKED); + await network.updateMinimumLiquidationCollateral(0n); + + await setupOracles(network, ssvToken, staker, [oracle1, oracle2, oracle3, oracle4]); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + it("Fees use old vUnits before EB update and new vUnits after", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const deposit = connection.ethers.parseEther("10"); + const regTx1 = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit / 2n }, + ); + const reg1Receipt = await regTx1.wait(); + const b_reg1 = reg1Receipt!.blockNumber; + let cluster = parseClusterFromEvent(network, reg1Receipt, Events.VALIDATOR_ADDED); + + const regTx2 = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster, + { value: deposit / 2n }, + ); + const regReceipt = await regTx2.wait(); + const b0 = regReceipt!.blockNumber; + cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + expect(cluster.validatorCount).to.equal(2n); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const implicitVUnits = defaultVUnits(2n); + + const effectiveBalance = 96; + const root = computeEBRoot(clusterId, effectiveBalance); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitEBRoot(network, root, rootBlockNum, [oracle1, oracle2, oracle3]); + + const currentBlock = await provider.getBlockNumber(); + const targetBlocks = b0 + 100 - currentBlock - 1; + if (targetBlocks > 0) { + await mineBlocks(provider, targetBlocks); + } + + const updateTx = await network.connect(clusterOwner).updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, + effectiveBalance, [], + ); + const updateReceipt = await updateTx.wait(); + const updateBlock = updateReceipt!.blockNumber; + cluster = parseClusterFromEvent(network, updateReceipt, Events.CLUSTER_BALANCE_UPDATED); + + const feePhase1 = calcClusterBurn({ + blockDiff: BigInt(b0 - b_reg1), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + const feePhase2 = calcClusterBurn({ + blockDiff: BigInt(updateBlock - b0), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: implicitVUnits, + }); + + const expectedBalanceAfterUpdate = deposit - feePhase1 - feePhase2; + expect(cluster.balance).to.equal(expectedBalanceAfterUpdate); + + const newVUnits = calcVUnits(BigInt(effectiveBalance)); + expect(newVUnits).to.equal(30_000n); + + const ebAfterUpdate = await views.getEffectiveBalance( + clusterOwner.address, operatorIds, cluster, + ); + expect(ebAfterUpdate).to.equal(effectiveBalance); + + await mineBlocks(provider, 100); + + const withdrawAmount = connection.ethers.parseEther("1"); + const withdrawTx = await network.connect(clusterOwner).withdraw(operatorIds, withdrawAmount, cluster); + const withdrawReceipt = await withdrawTx.wait(); + const wBlock = withdrawReceipt!.blockNumber; + const clusterAfterWithdraw = parseClusterFromEvent(network, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const blockDiffStep3 = BigInt(wBlock - updateBlock); + const feesStep3 = calcClusterBurn({ + blockDiff: blockDiffStep3, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: newVUnits, + }); + + const expectedBalanceAfterWithdraw = expectedBalanceAfterUpdate - feesStep3 - withdrawAmount; + expect(clusterAfterWithdraw.balance).to.equal(expectedBalanceAfterWithdraw); + + const burnPerBlockOld = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: implicitVUnits, + }); + const burnPerBlockNew = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: newVUnits, + }); + + expect(burnPerBlockNew * 2n).to.equal(burnPerBlockOld * 3n); + }); + }); + + describe("Migration With Explicit EB Deviation Sync", () => { + const deployFixtureCM13 = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), 10_000_000_000n, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), 10_000_000_000n, false); + operatorIds.push(Number(expectedId)); + } + + const ssvDeposit = ethers.parseEther("100"); + await ssvToken.mint(clusterOwner.address, ssvDeposit * 2n); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvDeposit * 2n, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, ssvDeposit, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, ssvDeposit, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + await setupOracles(newNetwork, ssvToken, staker, [oracle1, oracle2, oracle3, oracle4]); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + it("migration syncs EB deviation to operators and DAO", async function () { + const { network, views, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixtureCM13); + const provider = connection.ethers.provider; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 128; + const root = computeEBRoot(clusterId, effectiveBalance); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitEBRoot(network, root, rootBlockNum, [oracle1, oracle2, oracle3]); + + const updateTx = await network.connect(clusterOwner).updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, + effectiveBalance, [], + ); + const updateReceipt = await updateTx.wait(); + const updatedCluster = parseClusterFromEvent(network, updateReceipt, Events.CLUSTER_BALANCE_UPDATED); + + const migrationDeposit = ethers.parseEther("10"); + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, updatedCluster, + { value: migrationDeposit }, + ); + const migrateReceipt = await migrateTx.wait(); + + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + const migratedCluster = parseClusterFromEvent(network, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + const ebAfterMigration = await views.getEffectiveBalance( + clusterOwner.address, operatorIds, migratedCluster, + ); + expect(ebAfterMigration).to.equal(effectiveBalance); + + for (const opId of operatorIds) { + const op = await views.getOperatorById(opId); + expect(op.validatorCount).to.equal(2); + } + + expect(await views.getNetworkValidatorsCount()).to.equal(2); + + expect(migratedCluster.balance).to.equal(migrationDeposit); + expect(migratedCluster.active).to.equal(true); + expect(migratedCluster.validatorCount).to.equal(2n); + + const networkFeeETH = await views.getNetworkFee(); + const networkFeeRawActual = networkFeeETH / ETH_DEDUCTED_DIGITS; + const opFeeRawActual = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + + const burnRate = await views.getBurnRate( + clusterOwner.address, operatorIds, migratedCluster, + ); + const expectedBurn = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: opFeeRawActual, + networkFee: networkFeeRawActual, + effectiveVUnits: calcVUnits(BigInt(effectiveBalance)), + }); + expect(burnRate).to.equal(expectedBurn); + }); + }); +}); diff --git a/test/e2e/clusters-eth/cluster-eth-edge.test.ts b/test/e2e/clusters-eth/cluster-eth-edge.test.ts new file mode 100644 index 000000000..d1e43edf4 --- /dev/null +++ b/test/e2e/clusters-eth/cluster-eth-edge.test.ts @@ -0,0 +1,444 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + registerOperators, + whitelistAddresses, + getCurrentClusterState, + parseClusterFromEvent, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, + MINIMAL_LIQUIDATION_THRESHOLD, + NETWORK_FEE, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + defaultVUnits, + calcLiquidationThreshold, +} from "../../helpers/index.ts"; +import { + setupOracles, + commitEBRoot, + computeClusterId, + computeEBRoot, +} from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +describe("ETH Cluster Edge Cases", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let anotherOwner: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let staker: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, anotherOwner, oracle1, oracle2, oracle3, oracle4, staker] } = await setupTestContext()); + }); + + describe("Withdraw From Empty Cluster (validatorCount == 0)", () => { + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + it("Allows full withdrawal from cluster with 0 validators, skipping liquidation check", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + const depositAmount = ethers.parseEther("5"); + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositAmount }, + ); + const regReceipt = await regTx.wait(); + const regBlock = regReceipt!.blockNumber; + let cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await mineBlocks(provider, 10); + + const removeTx = await network.connect(clusterOwner).removeValidator( + makePublicKey(1), + operatorIds, + cluster, + ); + const removeReceipt = await removeTx.wait(); + const removeBlock = removeReceipt!.blockNumber; + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + const ethFeePacked = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const networkFeePacked = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const blockDiff = BigInt(removeBlock - regBlock); + const feesDeducted = calcClusterBurn({ + blockDiff, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: defaultVUnits(1n), + }); + const expectedBalance = depositAmount - feesDeducted; + + expect(BigInt(cluster.validatorCount)).to.equal(0n); + expect(cluster.active).to.equal(true); + expect(BigInt(cluster.balance)).to.equal(expectedBalance); + + const remainingBalance = BigInt(cluster.balance); + const tx = await network.connect(clusterOwner).withdraw( + operatorIds, + remainingBalance, + cluster, + ); + await tx.wait(); + + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(BigInt(cluster.balance)).to.equal(0n); + expect(cluster.active).to.equal(true); + expect(BigInt(cluster.validatorCount)).to.equal(0n); + }); + }); + + describe("Reactivation With Explicit EB — Deviation Properly Restored", () => { + const REACTIVATION_NETWORK_FEE_RAW = 5_000n; + const REACTIVATION_NETWORK_FEE_UNPACKED = REACTIVATION_NETWORK_FEE_RAW * ETH_DEDUCTED_DIGITS; + const REACTIVATION_ETH_FEE_RAW = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + + const deployReactivationFixture = async () => { + const { network, views, ssvToken } = await ssvNetworkFullFixture(connection); + + await network.updateNetworkFee(REACTIVATION_NETWORK_FEE_UNPACKED); + await network.updateMinimumLiquidationCollateral(0n); + + await setupOracles(network, ssvToken, staker, [oracle1, oracle2, oracle3, oracle4]); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + it("Restores EB deviation to operators and DAO on reactivation", async function () { + const { network, views, operatorIds } = + await networkHelpers.loadFixture(deployReactivationFixture); + const provider = connection.ethers.provider; + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regReceipt = await regTx.wait(); + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 64; + const root = computeEBRoot(clusterId, effectiveBalance); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitEBRoot(network, root, rootBlockNum, [oracle1, oracle2, oracle3]); + + const updateTx = await network.connect(clusterOwner).updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, + effectiveBalance, [], + ); + const updateReceipt = await updateTx.wait(); + cluster = parseClusterFromEvent(network, updateReceipt, Events.CLUSTER_BALANCE_UPDATED); + + const ebBefore = await views.getEffectiveBalance( + clusterOwner.address, operatorIds, cluster, + ); + expect(ebBefore).to.equal(effectiveBalance); + + const liqTx = await network.connect(clusterOwner).liquidate( + clusterOwner.address, + operatorIds, + cluster, + ); + const liqReceipt = await liqTx.wait(); + const liquidatedCluster = parseClusterFromEvent(network, liqReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.equal(false); + + await mineBlocks(provider, 10); + + const reactivateAmount = ethers.parseEther("10"); + const tx = await network.connect(clusterOwner).reactivate( + operatorIds, + liquidatedCluster, + { value: reactivateAmount }, + ); + const reactivateReceipt = await tx.wait(); + const reactivatedCluster = parseClusterFromEvent(network, reactivateReceipt, Events.CLUSTER_REACTIVATED); + + await expect(tx).to.emit(network, Events.CLUSTER_REACTIVATED); + + const ebAfter = await views.getEffectiveBalance( + clusterOwner.address, operatorIds, reactivatedCluster, + ); + expect(ebAfter).to.equal(effectiveBalance); + }); + }); + + describe("Withdraw — Operator Snapshots NOT Updated", () => { + const deployFixture = async () => { + const { network, views } = await ssvNetworkFullFixture(connection); + + await network.updateMinimumLiquidationCollateral(0n); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + it("Correctly computes fees over two withdrawals without updating operator snapshots", async function () { + const { network, views, operatorIds } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const depositAmount = ethers.parseEther("10"); + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositAmount }, + ); + await regTx.wait(); + + let cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds, + ); + + const burnRateAfterReg = await views.getBurnRate( + clusterOwner.address, operatorIds, cluster, + ); + + await mineBlocks(provider, 100); + + const withdrawTx1 = await network.connect(clusterOwner).withdraw( + operatorIds, + ethers.parseEther("1"), + cluster, + ); + const receipt1 = await withdrawTx1.wait(); + cluster = parseClusterFromEvent(network, receipt1, Events.CLUSTER_WITHDRAWN); + + const burnRateAfterW1 = await views.getBurnRate( + clusterOwner.address, operatorIds, cluster, + ); + expect(burnRateAfterW1).to.equal(burnRateAfterReg); + + const earningsAfterW1: bigint[] = []; + for (const opId of operatorIds) { + earningsAfterW1.push(await views.getOperatorEarnings(opId)); + } + for (let i = 1; i < earningsAfterW1.length; i++) { + expect(earningsAfterW1[i]).to.equal(earningsAfterW1[0]); + } + + await mineBlocks(provider, 100); + + const withdrawTx2 = await network.connect(clusterOwner).withdraw( + operatorIds, + ethers.parseEther("1"), + cluster, + ); + const receipt2 = await withdrawTx2.wait(); + cluster = parseClusterFromEvent(network, receipt2, Events.CLUSTER_WITHDRAWN); + + const burnRateAfterW2 = await views.getBurnRate( + clusterOwner.address, operatorIds, cluster, + ); + expect(burnRateAfterW2).to.equal(burnRateAfterReg); + + const earningsAfterW2: bigint[] = []; + for (const opId of operatorIds) { + earningsAfterW2.push(await views.getOperatorEarnings(opId)); + } + for (let i = 0; i < earningsAfterW2.length; i++) { + expect(earningsAfterW2[i]).to.be.greaterThan(earningsAfterW1[i]); + expect(earningsAfterW2[i]).to.equal(earningsAfterW2[0]); + } + }); + }); + + describe("Packing Precision — ETH Values That Aren't Divisible By 100_000", () => { + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + it("Reverts when setting operator ETH fee not divisible by ETH_DEDUCTED_DIGITS", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, clusterOwner, 1); + + await expect( + network.connect(clusterOwner).declareOperatorFee(BigInt(operatorIds[0]), MINIMAL_OPERATOR_ETH_FEE + 1n), + ).to.be.revertedWithCustomError(network, Errors.MAX_PRECISION_EXCEEDED); + + await expect( + network.connect(clusterOwner).declareOperatorFee(BigInt(operatorIds[0]), MINIMAL_OPERATOR_ETH_FEE + 50_000n), + ).to.be.revertedWithCustomError(network, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Accepts operator ETH fee divisible by ETH_DEDUCTED_DIGITS", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, clusterOwner, 1); + const validHigherFee = MINIMAL_OPERATOR_ETH_FEE + ETH_DEDUCTED_DIGITS; + await network.connect(clusterOwner).declareOperatorFee( + BigInt(operatorIds[0]), + validHigherFee, + ); + const { fee } = await views.getOperatorDeclaredFee(operatorIds[0]); + expect(fee).to.be.equal(validHigherFee); + }); + + it("Allows deposit/withdraw of amounts not divisible by ETH_DEDUCTED_DIGITS", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + const oddAmount = 99_999n; + await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + cluster, + { value: oddAmount }, + ); + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + await network.connect(clusterOwner).withdraw(operatorIds, oddAmount, cluster); + }); + }); + + describe("Liquidation Bounty Exactly Equals Post-Settlement Balance", () => { + const BOUNTY_NETWORK_FEE_RAW = 5_000n; + const BOUNTY_NETWORK_FEE_UNPACKED = BOUNTY_NETWORK_FEE_RAW * ETH_DEDUCTED_DIGITS; + const BOUNTY_MIN_BLOCKS = MINIMAL_LIQUIDATION_THRESHOLD; + const BOUNTY_ETH_FEE_RAW = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + + const deployBountyFixture = async () => { + const { network, views } = await ssvNetworkFullFixture(connection); + + await network.updateNetworkFee(BOUNTY_NETWORK_FEE_UNPACKED); + await network.updateLiquidationThresholdPeriod(BOUNTY_MIN_BLOCKS); + await network.updateMinimumLiquidationCollateral(0n); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + it("Bounty equals post-settlement balance, not original balance", async function () { + const { network, operatorIds } = + await networkHelpers.loadFixture(deployBountyFixture); + const provider = connection.ethers.provider; + + const threshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: BOUNTY_MIN_BLOCKS, + numOperators: 4n, + ethFee: BOUNTY_ETH_FEE_RAW, + networkFee: BOUNTY_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: threshold }, + ); + const regReceipt = await regTx.wait(); + const regBlock = regReceipt!.blockNumber; + + const regCluster = await getCurrentClusterState( + connection, + network as any, + clusterOwner.address, + operatorIds, + ); + + await mineBlocks(provider, 20); + + const liquidatorBalanceBefore = await provider.getBalance(anotherOwner.address); + + const liqTx = await network.connect(anotherOwner).liquidate( + clusterOwner.address, + operatorIds, + regCluster, + ); + const liqReceipt = await liqTx.wait(); + const liqBlock = liqReceipt!.blockNumber; + const gasUsed = BigInt(liqReceipt!.gasUsed) * BigInt(liqReceipt!.gasPrice); + + const liquidatorBalanceAfter = await provider.getBalance(anotherOwner.address); + const bounty = liquidatorBalanceAfter - liquidatorBalanceBefore + gasUsed; + + const liquidatedCluster = parseClusterFromEvent( + network, + liqReceipt, + Events.CLUSTER_LIQUIDATED, + ); + + expect(BigInt(liquidatedCluster.balance)).to.equal(0n); + expect(liquidatedCluster.active).to.equal(false); + + const blockDiff = BigInt(liqBlock - regBlock); + const burn = calcClusterBurn({ + blockDiff, + numOperators: 4n, + ethFee: BOUNTY_ETH_FEE_RAW, + networkFee: BOUNTY_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + const expectedBounty = burn >= threshold ? 0n : threshold - burn; + expect(bounty).to.equal(expectedBounty); + }); + }); +}); diff --git a/test/e2e/clusters-eth/cluster-eth-lifecycle.test.ts b/test/e2e/clusters-eth/cluster-eth-lifecycle.test.ts new file mode 100644 index 000000000..68ebb781c --- /dev/null +++ b/test/e2e/clusters-eth/cluster-eth-lifecycle.test.ts @@ -0,0 +1,512 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + parseClusterFromEvent, + registerOperators, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_LIQUIDATION_THRESHOLD, + OP_ETH_FEE_RAW, + DEFAULT_NETWORK_FEE_RAW, + DEFAULT_NETWORK_FEE_UNPACKED, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + calcLiquidationThreshold, + defaultVUnits, + snapshotContractBalance, +} from "../../helpers/index.ts"; + +const MIN_BLOCKS_BEFORE_LIQ = MINIMAL_LIQUIDATION_THRESHOLD; +const NUM_OPERATORS = 4n; + +describe("ETH Cluster Lifecycle", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator] } = await setupTestContext()); + }); + + const deployFixture = async () => { + const { network, views, ssvToken, cssvToken } = await ssvNetworkFullFixture(connection); + + await network.updateNetworkFee(DEFAULT_NETWORK_FEE_UNPACKED); + await network.updateMinimumLiquidationCollateral(0n); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + + describe("ETH Cluster Lifecycle", () => { + it("Creates cluster, deposits, advances blocks, withdraws with correct fee deduction", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regReceipt = await regTx.wait(); + const b0 = regReceipt!.blockNumber; + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + expect(cluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(cluster.validatorCount).to.equal(1n); + expect(cluster.active).to.equal(true); + + await mineBlocks(provider, 49); + const depositVal = connection.ethers.parseEther("5"); + const depTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, operatorIds, cluster, + { value: depositVal }, + ); + const depReceipt = await depTx.wait(); + cluster = parseClusterFromEvent(network, depReceipt, Events.CLUSTER_DEPOSITED); + expect(cluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE + depositVal); + + const currentBlock = await getBlockNumber(provider); + const blocksToMine = (b0 + 100) - currentBlock - 1; + await mineBlocks(provider, blocksToMine); + + const withdrawAmount = connection.ethers.parseEther("2"); + const contractBalBefore = await snapshotContractBalance(provider, await network.getAddress()); + const withdrawTx = await network.connect(clusterOwner).withdraw(operatorIds, withdrawAmount, cluster); + const withdrawReceipt = await withdrawTx.wait(); + const wBlock = withdrawReceipt!.blockNumber; + const clusterAfter = parseClusterFromEvent(network, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const blockDiff = BigInt(wBlock - b0); + const vUnits = defaultVUnits(1n); + const expectedFees = calcClusterBurn({ + blockDiff, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const expectedBalance = DEFAULT_ETH_REGISTER_VALUE + depositVal - expectedFees - withdrawAmount; + expect(clusterAfter.balance).to.equal(expectedBalance); + + const contractBalAfter = await snapshotContractBalance(provider, await network.getAddress()); + expect(contractBalBefore - contractBalAfter).to.equal(withdrawAmount); + }); + + it("Deposit at same block as registration — no fee settlement (edge)", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let cluster = parseClusterFromEvent(network, await regTx.wait(), Events.VALIDATOR_ADDED); + + const secondDeposit = connection.ethers.parseEther("5") + const depTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, operatorIds, cluster, + { value: secondDeposit}, + ); + cluster = parseClusterFromEvent(network, await depTx.wait(), Events.CLUSTER_DEPOSITED); + + expect(cluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE + secondDeposit); + }); + + it("Multiple deposits accumulate without fee settlement (edge)", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let cluster = parseClusterFromEvent(network, await regTx.wait(), Events.VALIDATOR_ADDED); + + await mineBlocks(connection.ethers.provider, 10); + + const secondDep = connection.ethers.parseEther("3"); + let depTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, operatorIds, cluster, { value: secondDep }, + ); + cluster = parseClusterFromEvent(network, await depTx.wait(), Events.CLUSTER_DEPOSITED); + expect(cluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE + secondDep); + + await mineBlocks(connection.ethers.provider, 10); + + const thirdDep = connection.ethers.parseEther("2"); + depTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, operatorIds, cluster, { value: thirdDep }, + ); + cluster = parseClusterFromEvent(network, await depTx.wait(), Events.CLUSTER_DEPOSITED); + + expect(cluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE + secondDep + thirdDep); + }); + }); + + describe(" Withdraw Exactly To Liquidation Threshold", () => { + it("Allows withdraw to exact threshold but rejects 1 more wei", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regReceipt = await regTx.wait(); + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + await mineBlocks(provider, 9); + + const blockDiff = 10n; + const vUnits = defaultVUnits(1n); + const feesAt10 = calcClusterBurn({ + blockDiff, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const balanceAfterFees = DEFAULT_ETH_REGISTER_VALUE - feesAt10; + + const liqThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_BEFORE_LIQ, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const maxWithdrawable = balanceAfterFees - liqThreshold; + + const withdrawTx = await network.connect(clusterOwner).withdraw(operatorIds, maxWithdrawable, cluster); + cluster = parseClusterFromEvent(network, await withdrawTx.wait(), Events.CLUSTER_WITHDRAWN); + + expect(cluster.balance).to.equal(liqThreshold); + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, 1n, cluster), + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("ValidatorCount == 0 allows full withdrawal (edge)", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const deposit = connection.ethers.parseEther("5"); + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const regReceipt = await regTx.wait(); + const regBlock = regReceipt!.blockNumber; + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + await mineBlocks(provider, 5); + + const removeTx = await network.connect(clusterOwner).removeValidator(makePublicKey(1), operatorIds, cluster); + const removeReceipt = await removeTx.wait(); + const removeBlock = removeReceipt!.blockNumber; + cluster = parseClusterFromEvent(network, removeReceipt, Events.VALIDATOR_REMOVED); + expect(cluster.validatorCount).to.equal(0n); + + const blockDiff = BigInt(removeBlock - regBlock); + const vUnits = defaultVUnits(1n); + const fees = calcClusterBurn({ + blockDiff, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + const expectedFullBalance = deposit - fees; + + const fullBalance = cluster.balance; + expect(fullBalance).to.equal(expectedFullBalance); + + const wTx = await network.connect(clusterOwner).withdraw(operatorIds, fullBalance, cluster); + cluster = parseClusterFromEvent(network, await wTx.wait(), Events.CLUSTER_WITHDRAWN); + expect(cluster.balance).to.equal(0n); + }); + }); + + describe("Third-Party Liquidation With Bounty", () => { + it("Liquidates cluster after balance drops below threshold, liquidator receives bounty", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const vUnits = defaultVUnits(1n); + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + const liqThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_BEFORE_LIQ, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const blocksAboveThreshold = 10n; + const deposit = liqThreshold + burnPerBlock * (blocksAboveThreshold + 1n); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const regReceipt = await regTx.wait(); + const b0 = regReceipt!.blockNumber; + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + const blocksUntilLiquidatable = Number((deposit - liqThreshold) / burnPerBlock); + + const currentBlock1 = await getBlockNumber(provider); + const targetForNotLiq = b0 + blocksUntilLiquidatable; + const blocksToMineForNotLiq = targetForNotLiq - currentBlock1 - 1; + if (blocksToMineForNotLiq > 0) await mineBlocks(provider, blocksToMineForNotLiq); + + await expect( + network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, cluster), + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_NOT_LIQUIDATABLE); + + const currentBlock2 = await getBlockNumber(provider); + const targetForLiq = b0 + blocksUntilLiquidatable + 1; + const blocksToMineForLiq = targetForLiq - currentBlock2 - 1; + if (blocksToMineForLiq > 0) await mineBlocks(provider, blocksToMineForLiq); + + const liqBalBefore = await provider.getBalance(liquidator.address); + const liqTx = await network.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + const liqReceipt = await liqTx.wait(); + const liqBlock = liqReceipt!.blockNumber; + const blockDiff = BigInt(liqBlock - b0); + + const totalFees = calcClusterBurn({ + blockDiff, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const balanceAfterFees = deposit - totalFees; + + const liqBalAfter = await provider.getBalance(liquidator.address); + const gasUsed = liqReceipt!.gasUsed * liqReceipt!.gasPrice; + expect(liqBalAfter - liqBalBefore + gasUsed).to.equal(balanceAfterFees); + + const liqCluster = parseClusterFromEvent(network, liqReceipt, Events.CLUSTER_LIQUIDATED); + expect(liqCluster.active).to.equal(false); + expect(liqCluster.balance).to.equal(0n); + expect(liqCluster.index).to.equal(0n); + expect(liqCluster.networkFeeIndex).to.equal(0n); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(opId); + expect(opData.validatorCount).to.equal(0); + } + expect(await views.getNetworkValidatorsCount()).to.equal(0); + }); + + it("Owner can always self-liquidate regardless of balance (edge)", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regReceipt = await regTx.wait(); + const b0 = regReceipt!.blockNumber; + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + const ownerBalBefore = await provider.getBalance(clusterOwner.address); + const selfLiqTx = await network.connect(clusterOwner).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + const selfLiqReceipt = await selfLiqTx.wait(); + const selfLiqBlock = selfLiqReceipt!.blockNumber; + const blockDiff = BigInt(selfLiqBlock - b0); + + const vUnits = defaultVUnits(1n); + const totalFees = calcClusterBurn({ + blockDiff, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + const expectedBounty = DEFAULT_ETH_REGISTER_VALUE - totalFees; + + const ownerBalAfter = await provider.getBalance(clusterOwner.address); + const gasUsed = selfLiqReceipt!.gasUsed * selfLiqReceipt!.gasPrice; + expect(ownerBalAfter - ownerBalBefore + gasUsed).to.equal(expectedBounty); + + const liqCluster = parseClusterFromEvent(network, selfLiqReceipt, Events.CLUSTER_LIQUIDATED); + expect(liqCluster.active).to.equal(false); + expect(liqCluster.balance).to.equal(0n); + }); + }); + + describe("Reactivation After Liquidation", () => { + it("Full lifecycle: create → liquidate → reactivate → verify fee accrual from reactivation point", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const vUnits = defaultVUnits(1n); + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + const liqThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_BEFORE_LIQ, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const deposit = liqThreshold + burnPerBlock * 5n; + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const regReceipt = await regTx.wait(); + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + const blocksUntilLiquidatable = Number((deposit - liqThreshold) / burnPerBlock); + await mineBlocks(provider, blocksUntilLiquidatable); + + const liqTx = await network.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + const liqReceipt = await liqTx.wait(); + cluster = parseClusterFromEvent(network, liqReceipt, Events.CLUSTER_LIQUIDATED); + expect(cluster.active).to.equal(false); + expect(cluster.balance).to.equal(0n); + + await mineBlocks(provider, 76); + + const reactivateAmount = connection.ethers.parseEther("5"); + const reactivateTx = await network.connect(clusterOwner).reactivate( + operatorIds, cluster, { value: reactivateAmount }, + ); + const reactivateReceipt = await reactivateTx.wait(); + const reactivateBlock = reactivateReceipt!.blockNumber; + cluster = parseClusterFromEvent(network, reactivateReceipt, Events.CLUSTER_REACTIVATED); + + expect(cluster.active).to.equal(true); + expect(cluster.balance).to.equal(reactivateAmount); + + await mineBlocks(provider, 99); + const withdrawAmount = connection.ethers.parseEther("1"); + const withdrawTx = await network.connect(clusterOwner).withdraw(operatorIds, withdrawAmount, cluster); + const withdrawReceipt = await withdrawTx.wait(); + const withdrawBlock = withdrawReceipt!.blockNumber; + const clusterAfter = parseClusterFromEvent(network, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const blocksSinceReactivation = BigInt(withdrawBlock - reactivateBlock); + const feesAfterReactivation = calcClusterBurn({ + blockDiff: blocksSinceReactivation, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const expectedBalance = reactivateAmount - feesAfterReactivation - withdrawAmount; + expect(clusterAfter.balance).to.equal(expectedBalance); + }); + }); + + describe("Deposit Into Liquidated Cluster + Reactivation", () => { + it("Deposits into liquidated cluster accumulate, reactivation uses sum", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const vUnits = defaultVUnits(1n); + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + const liqThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_BEFORE_LIQ, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + const deposit = liqThreshold + burnPerBlock * 5n; + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + let cluster = parseClusterFromEvent(network, await regTx.wait(), Events.VALIDATOR_ADDED); + + const blocksUntilLiquidatable = Number((deposit - liqThreshold) / burnPerBlock); + await mineBlocks(provider, blocksUntilLiquidatable); + + const liqTx = await network.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + cluster = parseClusterFromEvent(network, await liqTx.wait(), Events.CLUSTER_LIQUIDATED); + expect(cluster.active).to.equal(false); + expect(cluster.balance).to.equal(0n); + + const deposit1 = connection.ethers.parseEther("3"); + const dep1Tx = await network.connect(clusterOwner).deposit( + clusterOwner.address, operatorIds, cluster, { value: deposit1 }, + ); + cluster = parseClusterFromEvent(network, await dep1Tx.wait(), Events.CLUSTER_DEPOSITED); + expect(cluster.active).to.equal(false); + expect(cluster.balance).to.equal(deposit1); + + const deposit2 = connection.ethers.parseEther("2"); + const dep2Tx = await network.connect(clusterOwner).deposit( + clusterOwner.address, operatorIds, cluster, { value: deposit2 }, + ); + cluster = parseClusterFromEvent(network, await dep2Tx.wait(), Events.CLUSTER_DEPOSITED); + expect(cluster.active).to.equal(false); + expect(cluster.balance).to.equal(deposit1 + deposit2); + + const reactivateAmount = connection.ethers.parseEther("1"); + const reactivateTx = await network.connect(clusterOwner).reactivate( + operatorIds, cluster, { value: reactivateAmount }, + ); + cluster = parseClusterFromEvent(network, await reactivateTx.wait(), Events.CLUSTER_REACTIVATED); + + expect(cluster.active).to.equal(true); + expect(cluster.balance).to.equal(deposit1 + deposit2 + reactivateAmount); + }); + }); +}); diff --git a/test/e2e/clusters-eth/cluster-eth-liquidation.test.ts b/test/e2e/clusters-eth/cluster-eth-liquidation.test.ts new file mode 100644 index 000000000..230975c26 --- /dev/null +++ b/test/e2e/clusters-eth/cluster-eth-liquidation.test.ts @@ -0,0 +1,275 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + parseClusterFromEvent, + registerOperators, + whitelistAddresses, + getCurrentClusterState, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEFAULT_ETH_REGISTER_VALUE, + MINIMAL_LIQUIDATION_THRESHOLD, + EMPTY_CLUSTER, + OP_ETH_FEE_RAW, + DEFAULT_NETWORK_FEE_RAW, + DEFAULT_NETWORK_FEE_UNPACKED, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + calcLiquidationThreshold, + calcVUnits, + defaultVUnits, +} from "../../helpers/index.ts"; +import { + setupOracles, + commitEBRoot, + computeClusterId, + computeEBRoot, +} from "../../helpers/index.ts"; + +const MIN_BLOCKS_LIQ = MINIMAL_LIQUIDATION_THRESHOLD; +const NUM_OPERATORS = 4n; + +describe("ETH Cluster Liquidation", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let staker: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator, oracle1, oracle2, oracle3, oracle4, staker] } = await setupTestContext()); + }); + + const deployFixture = async () => { + const { network, views, ssvToken } = await ssvNetworkFullFixture(connection); + + await network.updateNetworkFee(DEFAULT_NETWORK_FEE_UNPACKED); + await network.updateMinimumLiquidationCollateral(0n); + + await setupOracles(network, ssvToken, staker, [oracle1, oracle2, oracle3, oracle4]); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + describe("Cluster at exact threshold is NOT liquidatable by third party", () => { + it("Balance == threshold is NOT liquidatable, balance < threshold IS", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regReceipt = await regTx.wait(); + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + const vUnits = defaultVUnits(1n); + const perBlockBurn = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + const liqThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_LIQ, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + + await mineBlocks(provider, 9); + + const feesAt10 = calcClusterBurn({ + blockDiff: 10n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: vUnits, + }); + const balAfterFees = DEFAULT_ETH_REGISTER_VALUE - feesAt10; + + const maxWithdraw = balAfterFees - liqThreshold - perBlockBurn; + + const wTx = await network.connect(clusterOwner).withdraw(operatorIds, maxWithdraw, cluster); + const wReceipt = await wTx.wait(); + cluster = parseClusterFromEvent(network, wReceipt, Events.CLUSTER_WITHDRAWN); + expect(cluster.balance).to.equal(liqThreshold + perBlockBurn); + + await expect( + network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, cluster), + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_NOT_LIQUIDATABLE); + + await mineBlocks(provider, 1); + const liqTx = await network.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + await expect(liqTx).to.emit(network, Events.CLUSTER_LIQUIDATED); + }); + }); + + describe("Liquidation With Explicit EB — Deviation Cleanup", () => { + it("Liquidation reverses EB deviation from operators and DAO", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const regTx1 = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let cluster = parseClusterFromEvent(network, await regTx1.wait(), Events.VALIDATOR_ADDED); + + const regTx2 = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regReceipt2 = await regTx2.wait(); + cluster = parseClusterFromEvent(network, regReceipt2, Events.VALIDATOR_ADDED); + expect(cluster.validatorCount).to.equal(2n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const effectiveBalance = 96; + const root = computeEBRoot(clusterId, effectiveBalance); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitEBRoot(network, root, rootBlockNum, [oracle1, oracle2, oracle3]); + + const updateTx = await network.connect(clusterOwner).updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, + effectiveBalance, [], + ); + const updateReceipt = await updateTx.wait(); + cluster = parseClusterFromEvent(network, updateReceipt, Events.CLUSTER_BALANCE_UPDATED); + + const newVUnits = calcVUnits(BigInt(effectiveBalance)); + expect(newVUnits).to.equal(30_000n); + + const ebAfterUpdate = await views.getEffectiveBalance( + clusterOwner.address, operatorIds, cluster, + ); + expect(ebAfterUpdate).to.equal(effectiveBalance); + + const liqThresholdNewVUnits = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_LIQ, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: newVUnits, + }); + const burnPerBlockNewVUnits = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: newVUnits, + }); + const currentBalance = BigInt(cluster.balance); + const blocksUntilLiquidatable = Number((currentBalance - liqThresholdNewVUnits) / burnPerBlockNewVUnits); + await mineBlocks(provider, blocksUntilLiquidatable); + + const liqTx = await network.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + const liqReceipt = await liqTx.wait(); + const liqCluster = parseClusterFromEvent(network, liqReceipt, Events.CLUSTER_LIQUIDATED); + + expect(liqCluster.active).to.equal(false); + expect(liqCluster.balance).to.equal(0n); + + expect(await views.getNetworkValidatorsCount()).to.equal(0); + for (const opId of operatorIds) { + const opData = await views.getOperatorById(opId); + expect(opData.validatorCount).to.equal(0); + } + }); + }); + + describe("Auto-Liquidation via updateClusterBalance", () => { + it("EB increase triggers auto-liquidation, bounty goes to updater", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const implicitVUnits = defaultVUnits(1n); + const implicitThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_LIQ, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: implicitVUnits, + }); + + const deposit = implicitThreshold + (implicitThreshold / 2n); + expect(deposit).to.be.greaterThan(implicitThreshold); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const regReceipt = await regTx.wait(); + let cluster = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + const regBlock = regReceipt!.blockNumber; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const effectiveBalance = 64; + const root = computeEBRoot(clusterId, effectiveBalance); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitEBRoot(network, root, rootBlockNum, [oracle1, oracle2, oracle3]); + + const updaterBalBefore = await provider.getBalance(liquidator.address); + + const updateTx = await network.connect(liquidator).updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, + effectiveBalance, [], + ); + const updateReceipt = await updateTx.wait(); + + await expect(updateTx).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + await expect(updateTx).to.emit(network, Events.CLUSTER_LIQUIDATED); + + const updateBlock = updateReceipt!.blockNumber; + const blockDiff = BigInt(updateBlock - regBlock); + const oldVUnits = defaultVUnits(1n); + const feesAtOldVUnits = calcClusterBurn({ + blockDiff, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: oldVUnits, + }); + const expectedBounty = deposit - feesAtOldVUnits; + + const gasUsed = updateReceipt!.gasUsed * updateReceipt!.gasPrice; + const updaterBalAfter = await provider.getBalance(liquidator.address); + const bountyReceived = updaterBalAfter - updaterBalBefore + gasUsed; + expect(bountyReceived).to.equal(expectedBounty); + + expect(await views.getNetworkValidatorsCount()).to.equal(0); + for (const opId of operatorIds) { + const opData = await views.getOperatorById(opId); + expect(opData.validatorCount).to.equal(0); + } + }); + }); +}); diff --git a/test/e2e/clusters-eth/cluster-reverts.test.ts b/test/e2e/clusters-eth/cluster-reverts.test.ts new file mode 100644 index 000000000..d924dd4bf --- /dev/null +++ b/test/e2e/clusters-eth/cluster-reverts.test.ts @@ -0,0 +1,180 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + getCurrentClusterState, + registerOperators, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_LIQUIDATION_THRESHOLD, + OP_ETH_FEE_RAW, + DEFAULT_NETWORK_FEE_RAW, + DEFAULT_NETWORK_FEE_UNPACKED, +} from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { mineBlocks, calcLiquidationThreshold, calcClusterBurn, defaultVUnits } from "../../helpers/index.ts"; + +const MIN_BLOCKS_BEFORE_LIQ = MINIMAL_LIQUIDATION_THRESHOLD; + +describe("Revert — Liquidate Cluster At Exact Threshold", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let thirdParty: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, thirdParty] } = await setupTestContext()); + }); + + const deployFixture = async () => { + const { network, views } = await ssvNetworkFullFixture(connection); + + await network.updateNetworkFee(DEFAULT_NETWORK_FEE_UNPACKED); + await network.updateLiquidationThresholdPeriod(MIN_BLOCKS_BEFORE_LIQ); + await network.updateMinimumLiquidationCollateral(0n); + + const operatorIds = await registerOperators(network, clusterOwner, 4); + await whitelistAddresses(network, clusterOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + it("Third-party liquidation at exact threshold reverts with ClusterNotLiquidatable", async function () { + const { network, operatorIds } = + await networkHelpers.loadFixture(deployFixture); + + const threshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_BEFORE_LIQ, + numOperators: 4n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: 4n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + const deposit = threshold + burnPerBlock; + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: deposit }, + ); + + const cluster = await getCurrentClusterState( + connection, + network as any, + clusterOwner.address, + operatorIds, + ); + + await expect( + network.connect(thirdParty).liquidate( + clusterOwner.address, + operatorIds, + cluster, + ), + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_NOT_LIQUIDATABLE); + }); + + it("Self-liquidation at exact threshold succeeds (owner bypass)", async function () { + const { network, operatorIds } = + await networkHelpers.loadFixture(deployFixture); + + const threshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_BEFORE_LIQ, + numOperators: 4n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: 4n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + const deposit = threshold + burnPerBlock; + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: deposit }, + ); + + const cluster = await getCurrentClusterState( + connection, + network as any, + clusterOwner.address, + operatorIds, + ); + + const tx = await network.connect(clusterOwner).liquidate( + clusterOwner.address, + operatorIds, + cluster, + ); + await tx.wait(); + + await expect(tx).to.emit(network, Events.CLUSTER_LIQUIDATED); + }); + + it("Third-party liquidation at threshold - 1 wei succeeds", async function () { + const { network, operatorIds } = + await networkHelpers.loadFixture(deployFixture); + + const threshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MIN_BLOCKS_BEFORE_LIQ, + numOperators: 4n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: threshold }, + ); + + const cluster = await getCurrentClusterState( + connection, + network as any, + clusterOwner.address, + operatorIds, + ); + + await mineBlocks(connection.ethers.provider, 1); + + const tx = await network.connect(thirdParty).liquidate( + clusterOwner.address, + operatorIds, + cluster, + ); + await tx.wait(); + + await expect(tx).to.emit(network, Events.CLUSTER_LIQUIDATED); + }); +}); diff --git a/test/e2e/clusters-ssv/cluster-ssv-fees.test.ts b/test/e2e/clusters-ssv/cluster-ssv-fees.test.ts new file mode 100644 index 000000000..a44e8bbf2 --- /dev/null +++ b/test/e2e/clusters-ssv/cluster-ssv-fees.test.ts @@ -0,0 +1,232 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullPreUpgradeFixture, upgradeToStakingVersion } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + getCurrentClusterState, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEDUCTED_DIGITS, + EMPTY_CLUSTER, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + getBlockNumber, +} from "../../helpers/index.ts"; +import { + setupOracles, + commitEBRoot, + computeClusterId, + computeEBRoot, +} from "../../helpers/index.ts"; +import { makeOperatorKey } from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +const OP_SSV_FEE_RAW = 2_000n; +const OP_SSV_FEE_UNPACKED = OP_SSV_FEE_RAW * DEDUCTED_DIGITS; +const NETWORK_FEE_SSV_RAW = 1_000n; +const NETWORK_FEE_SSV_UNPACKED = NETWORK_FEE_SSV_RAW * DEDUCTED_DIGITS; + +describe("CM-17 & CM-25: SSV Cluster Fee Mechanics", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let staker: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, oracle1, oracle2, oracle3, oracle4, staker] } = await setupTestContext()); + }); + + describe("SSV Fee Accrual — Verify Exact SSV Deduction Over N Blocks", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + await legacyNetwork.updateNetworkFee(NETWORK_FEE_SSV_UNPACKED); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvBalance = ethers.parseEther("900"); + await ssvToken.mint(clusterOwner.address, ssvBalance); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvBalance, + ); + + const perValidatorDeposit = ssvBalance / 3n; + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, perValidatorDeposit, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, perValidatorDeposit, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(3), operatorIds, DEFAULT_SHARES, perValidatorDeposit, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + expect(BigInt(cluster.validatorCount)).to.equal(3n); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster, ssvBalance }; + }; + + it("Verifies exact SSV fee deduction after 500 blocks with 3 validators", async function () { + const { network, views, ssvToken, operatorIds, cluster, ssvBalance } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 500); + + const balanceBefore = await views.getBalanceSSV( + clusterOwner.address, operatorIds, cluster, + ); + const burnRate = await views.getBurnRateSSV( + clusterOwner.address, operatorIds, cluster, + ); + expect(balanceBefore).to.be.greaterThan(0n); + + const ownerBalanceBefore = await ssvToken.balanceOf(clusterOwner.address); + + const tx = await network.connect(clusterOwner).liquidateSSV( + clusterOwner.address, operatorIds, cluster, + ); + await tx.wait(); + await expect(tx).to.emit(network, Events.CLUSTER_LIQUIDATED); + + const ownerBalanceAfter = await ssvToken.balanceOf(clusterOwner.address); + const ssvRefund = ownerBalanceAfter - ownerBalanceBefore; + + const expectedRefund = balanceBefore - burnRate; + expect(ssvRefund).to.equal(expectedRefund); + + const totalFeesDeducted = ssvBalance - ssvRefund; + expect(totalFeesDeducted).to.be.greaterThan(0n); + expect(ssvRefund).to.be.lessThan(ssvBalance); + + expect(totalFeesDeducted % DEDUCTED_DIGITS).to.equal(0n); + + const packedFees = totalFeesDeducted / DEDUCTED_DIGITS; + expect(packedFees).to.be.greaterThan(0n); + }); + }); + + describe("updateClusterBalance on SSV Cluster — EB Snapshot Only", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvBalance = ethers.parseEther("100"); + await ssvToken.mint(clusterOwner.address, ssvBalance); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvBalance, + ); + + const halfDeposit = ssvBalance / 2n; + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, halfDeposit, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, halfDeposit, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(2n); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + await setupOracles(newNetwork, ssvToken, staker, [oracle1, oracle2, oracle3, oracle4]); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster, ssvBalance }; + }; + + it("Only updates EB snapshot on SSV cluster, no fee settlement", async function () { + const { network, views, operatorIds, cluster, ssvBalance } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const balanceBefore = await views.getBalanceSSV( + clusterOwner.address, operatorIds, cluster, + ); + + const effectiveBalance = 64; + const root = computeEBRoot(clusterId, effectiveBalance); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitEBRoot(network, root, rootBlockNum, [oracle1, oracle2, oracle3]); + + await mineBlocks(provider, 10); + + const tx = await network.connect(clusterOwner).updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, + effectiveBalance, [], + ); + await tx.wait(); + + await expect(tx).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + + const updatedCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds, + ); + const eb = await views.getEffectiveBalance( + clusterOwner.address, operatorIds, updatedCluster, + ); + expect(eb).to.equal(effectiveBalance); + + const balanceAfter = await views.getBalanceSSV( + clusterOwner.address, operatorIds, updatedCluster, + ); + + expect(balanceAfter).to.be.greaterThan(0n); + expect(balanceAfter).to.be.lessThan(balanceBefore); + }); + }); +}); diff --git a/test/e2e/clusters-ssv/cluster-ssv-legacy.test.ts b/test/e2e/clusters-ssv/cluster-ssv-legacy.test.ts new file mode 100644 index 000000000..735dca026 --- /dev/null +++ b/test/e2e/clusters-ssv/cluster-ssv-legacy.test.ts @@ -0,0 +1,214 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullPreUpgradeFixture, upgradeToStakingVersion } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + getCurrentClusterState, + parseClusterFromEvent, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEFAULT_ETH_REGISTER_VALUE, + DEDUCTED_DIGITS, + EMPTY_CLUSTER, + TOKEN_REGISTER_AMOUNT, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { mineBlocks } from "../../helpers/index.ts"; +import { makeOperatorKey } from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +const OP_SSV_FEE_UNPACKED = 10_000_000_000n; + +describe("SSV Cluster Legacy Operations", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvAmount = TOKEN_REGISTER_AMOUNT * 2n; + await ssvToken.mint(clusterOwner.address, ssvAmount); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvAmount, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + const cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + describe("SSV Cluster Self-Liquidation", () => { + it("Self-liquidation returns correct SSV balance after fee deduction", async function () { + const { network, views, ssvToken, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 50); + + const expectedBalance = await views.getBalanceSSV( + clusterOwner.address, operatorIds, cluster, + ); + const burnRate = await views.getBurnRateSSV( + clusterOwner.address, operatorIds, cluster, + ); + + const ownerSSVBefore = await ssvToken.balanceOf(clusterOwner.address); + + const liqTx = await network.connect(clusterOwner).liquidateSSV( + clusterOwner.address, operatorIds, cluster, + ); + const liqReceipt = await liqTx.wait(); + await expect(liqTx).to.emit(network, Events.CLUSTER_LIQUIDATED); + + const ownerSSVAfter = await ssvToken.balanceOf(clusterOwner.address); + const ssvRefund = ownerSSVAfter - ownerSSVBefore; + + const expectedRefund = expectedBalance - burnRate; + expect(ssvRefund).to.equal(expectedRefund); + expect(ssvRefund).to.be.greaterThan(0n); + expect(ssvRefund).to.be.lessThan(TOKEN_REGISTER_AMOUNT); + + const totalFeesDeducted = TOKEN_REGISTER_AMOUNT - ssvRefund; + expect(totalFeesDeducted % DEDUCTED_DIGITS).to.equal(0n); + + const clusterAfter = parseClusterFromEvent(network, liqReceipt, Events.CLUSTER_LIQUIDATED); + expect(clusterAfter.active).to.equal(false); + expect(clusterAfter.balance).to.equal(0n); + + for (const opId of operatorIds) { + const opSSV = await views.getOperatorByIdSSV(opId); + expect(opSSV.validatorCount).to.equal(0); + } + }); + + it("SSV cluster with near-zero balance — self-liquidation returns 0 SSV (edge)", async function () { + const { network, views, ssvToken, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 300_000_000); + + const balanceBefore = await views.getBalanceSSV( + clusterOwner.address, operatorIds, cluster, + ); + expect(balanceBefore).to.equal(0n); + + const ownerSSVBefore = await ssvToken.balanceOf(clusterOwner.address); + + await network.connect(clusterOwner).liquidateSSV( + clusterOwner.address, operatorIds, cluster, + ); + + const ownerSSVAfter = await ssvToken.balanceOf(clusterOwner.address); + expect(ownerSSVAfter - ownerSSVBefore).to.equal(0n); + }); + + it("Already liquidated SSV cluster reverts (edge)", async function () { + const { network, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + await network.connect(clusterOwner).liquidateSSV( + clusterOwner.address, operatorIds, cluster, + ); + + const liquidatedCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds, + ); + expect(liquidatedCluster.active).to.equal(false); + + await expect( + network.connect(clusterOwner).liquidateSSV( + clusterOwner.address, operatorIds, liquidatedCluster, + ), + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_IS_LIQUIDATED); + }); + }); + + describe("SSV Blocked Operations", () => { + it("ETH operations revert with IncorrectClusterVersion on SSV cluster", async function () { + const { network, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + + const deposit = ethers.parseEther("1"); + await expect( + network.connect(clusterOwner).deposit( + clusterOwner.address, operatorIds, cluster, { value: deposit }, + ), + ).to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + + await expect( + network.connect(clusterOwner).reactivate( + operatorIds, cluster, { value: deposit }, + ), + ).to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, deposit, cluster), + ).to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + + await expect( + network.connect(clusterOwner).liquidate( + clusterOwner.address, operatorIds, cluster, + ), + ).to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + + // removeValidator is allowed on SSV clusters (BUG-12 fix) + const removeTx = await network.connect(clusterOwner).removeValidator(makePublicKey(1), operatorIds, cluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(network, removeReceipt, Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + + await expect( + network.connect(clusterOwner).liquidateSSV(clusterOwner.address, operatorIds, clusterAfterRemove), + ).to.emit(network, Events.CLUSTER_LIQUIDATED); + }); + + it("migrateClusterToETH succeeds on SSV cluster", async function () { + const { network, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + await expect( + network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + }); + }); +}); diff --git a/test/e2e/cross-cutting/economics.test.ts b/test/e2e/cross-cutting/economics.test.ts new file mode 100644 index 000000000..217315579 --- /dev/null +++ b/test/e2e/cross-cutting/economics.test.ts @@ -0,0 +1,373 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType, Cluster } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + parseClusterFromEvent, + generateMerkleForClusterEB, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, + BPS_DENOMINATOR, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + calcOperatorFeeAccrual, + calcVUnits, + defaultVUnits, + snapshotContractBalance, + checkETHConservation, +} from "../../helpers/index.ts"; + +describe("Cross-Cutting: Economics", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, operatorOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Full Economic Conservation Law", () => { + it("conservation holds after every step (deposit, register, advance, withdraw, operator withdrawal)", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const networkAddress = await network.getAddress(); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const deposit1 = ethers.parseEther("10"); + const tx1 = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit1 }, + ); + const receipt1 = await tx1.wait(); + let cluster = parseClusterFromEvent(network, receipt1, Events.VALIDATOR_ADDED); + + let contractETH = await snapshotContractBalance(connection.ethers.provider, networkAddress); + expect(contractETH).to.equal(deposit1); + expect(cluster.balance).to.equal(deposit1); + await checkETHConservation(networkAddress, connection.ethers.provider, [cluster.balance], [0n, 0n, 0n, 0n], 0n); + + const deposit2 = ethers.parseEther("5"); + const tx2 = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster, + { value: deposit2 }, + ); + const receipt2 = await tx2.wait(); + cluster = parseClusterFromEvent(network, receipt2, Events.VALIDATOR_ADDED); + + contractETH = await snapshotContractBalance(connection.ethers.provider, networkAddress); + expect(contractETH).to.equal(deposit1 + deposit2); + + await checkETHConservation( + networkAddress, connection.ethers.provider, + [cluster.balance], [0n, 0n, 0n, 0n], 0n, + ); + + await mineBlocks(connection.ethers.provider, 100); + + await checkETHConservation( + networkAddress, connection.ethers.provider, + [cluster.balance], [0n, 0n, 0n, 0n], 0n, + ); + + const withdrawAmount = ethers.parseEther("1"); + const tx4 = await network.connect(clusterOwner).withdraw( + operatorIds, withdrawAmount, cluster, + ); + const receipt4 = await tx4.wait(); + cluster = parseClusterFromEvent(network, receipt4, Events.CLUSTER_WITHDRAWN); + + contractETH = await snapshotContractBalance(connection.ethers.provider, networkAddress); + expect(contractETH).to.equal(deposit1 + deposit2 - withdrawAmount); + + await checkETHConservation( + networkAddress, connection.ethers.provider, + [cluster.balance], [0n, 0n, 0n, 0n], 0n, + ); + + const tx5 = await network.connect(operatorOwner).withdrawAllOperatorEarnings(operatorIds[0]); + await tx5.wait(); + + const txSettle = await network.connect(clusterOwner).withdraw( + operatorIds, 0n, cluster, + ); + const receiptSettle = await txSettle.wait(); + cluster = parseClusterFromEvent(network, receiptSettle, Events.CLUSTER_WITHDRAWN); + + contractETH = await snapshotContractBalance(connection.ethers.provider, networkAddress); + + const opEarnings: bigint[] = []; + for (let i = 0; i < operatorIds.length; i++) { + const earnings = await views.getOperatorEarnings(BigInt(operatorIds[i])); + opEarnings.push(BigInt(earnings)); + } + + const daoEarnings = BigInt(await views.getNetworkEarnings()); + + await checkETHConservation( + networkAddress, connection.ethers.provider, + [cluster.balance], opEarnings, daoEarnings, + ); + + const totalAccounted = cluster.balance + opEarnings.reduce((a, b) => a + b, 0n) + daoEarnings; + const dust = contractETH - totalAccounted; + expect(dust).to.be.greaterThanOrEqual(0n); + expect(dust).to.be.lessThanOrEqual(10n * ETH_DEDUCTED_DIGITS); + }); + }); + + describe("Register -> Advance -> Verify Full Economics (Exact Numbers)", () => { + it("Produces exact operator earnings, cluster balance, and DAO earnings after 100 blocks", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const networkAddress = await network.getAddress(); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeeWei = BigInt(opData.fee); + const ethFeePacked = ethFeeWei / ETH_DEDUCTED_DIGITS; + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const deposit = ethers.parseEther("10"); + const tx1 = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const receipt1 = await tx1.wait(); + let cluster = parseClusterFromEvent(network, receipt1, Events.VALIDATOR_ADDED); + const registerBlock = receipt1!.blockNumber; + + await mineBlocks(connection.ethers.provider, 100); + + const tx3 = await network.connect(clusterOwner).withdraw( + operatorIds, 0n, cluster, + ); + const receipt3 = await tx3.wait(); + cluster = parseClusterFromEvent(network, receipt3, Events.CLUSTER_WITHDRAWN); + const settlementBlock = receipt3!.blockNumber; + const blockDiff = BigInt(settlementBlock - registerBlock); + + const vUnits = defaultVUnits(1n); + const numOps = 4n; + + const perOpAccrual = calcOperatorFeeAccrual(blockDiff, ethFeePacked, vUnits); + const perOpEarningsWei = perOpAccrual * ETH_DEDUCTED_DIGITS; + const totalOpEarningsWei = perOpEarningsWei * numOps; + + const clusterBurn = calcClusterBurn({ + blockDiff, + numOperators: numOps, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnits, + }); + + const expectedClusterBalance = deposit - clusterBurn; + + const daoEarningsPacked = (blockDiff * networkFeePacked * vUnits) / BPS_DENOMINATOR; + const expectedDaoEarningsWei = daoEarningsPacked * ETH_DEDUCTED_DIGITS; + + expect(cluster.balance).to.equal(expectedClusterBalance); + + for (const opId of operatorIds) { + const earnings = BigInt(await views.getOperatorEarnings(BigInt(opId))); + expect(earnings).to.equal(perOpEarningsWei); + } + + const daoEarnings = BigInt(await views.getNetworkEarnings()); + expect(daoEarnings).to.equal(expectedDaoEarningsWei); + + const totalAccountedWei = expectedClusterBalance + totalOpEarningsWei + expectedDaoEarningsWei; + expect(totalAccountedWei).to.equal(deposit); + + const contractETH = await snapshotContractBalance(connection.ethers.provider, networkAddress); + expect(contractETH).to.equal(deposit); + }); + }); + + describe("Operator Serving Multiple Clusters with Different EBs", () => { + it("Correctly accumulates vUnit deviations and adjusts earnings after liquidation", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const signers = await connection.ethers.getSigners(); + const clusterOwnerA = signers[11]; + const clusterOwnerB = signers[12]; + const staker = signers[13]; + const liquidator = signers[14]; + const networkAddress = await network.getAddress(); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + clusterOwnerB.address, + ]); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeeWei = BigInt(opData.fee); + const ethFeePacked = ethFeeWei / ETH_DEDUCTED_DIGITS; + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const stakeAmount = ethers.parseEther("100"); + await ssvToken.transfer(staker.address, stakeAmount); + await ssvToken.connect(staker).approve(networkAddress, stakeAmount); + await network.connect(staker).stake(stakeAmount); + + const depositA = ethers.parseEther("2"); + const txA1 = await network.connect(clusterOwnerA).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositA }, + ); + const receiptA1 = await txA1.wait(); + let clusterA = parseClusterFromEvent(network, receiptA1, Events.VALIDATOR_ADDED); + + const depositB = ethers.parseEther("50"); + const txB1 = await network.connect(clusterOwnerB).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositB }, + ); + const receiptB1 = await txB1.wait(); + let clusterB = parseClusterFromEvent(network, receiptB1, Events.VALIDATOR_ADDED); + + const opAfterReg = await views.getOperatorById(BigInt(operatorIds[0])); + expect(BigInt(opAfterReg.validatorCount)).to.equal(2n); + + const oracle1 = signers[15]; + const oracle2 = signers[16]; + const oracle3 = signers[17]; + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + + await mineBlocks(provider, 10); + const blockForRoot = await getBlockNumber(provider); + + const clusterIdA = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwnerA.address, operatorIds]), + ); + const clusterIdB = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwnerB.address, operatorIds]), + ); + + const entries = [ + { clusterId: clusterIdA, effectiveBalance: 64 }, + { clusterId: clusterIdB, effectiveBalance: 48 }, + ]; + const { root, proofs } = generateMerkleForClusterEB(connection, entries); + + await mineBlocks(provider, 1); + for (const oracle of [oracle1, oracle2, oracle3]) { + await network.connect(oracle).commitRoot(root, BigInt(blockForRoot)); + } + + const txEBA = await network.updateClusterBalance( + blockForRoot, clusterOwnerA.address, operatorIds, clusterA, 64, proofs[clusterIdA], + ); + const receiptEBA = await txEBA.wait(); + clusterA = parseClusterFromEvent(network, receiptEBA, Events.CLUSTER_BALANCE_UPDATED); + const vUnitsA = calcVUnits(64n); + expect(vUnitsA).to.equal(20000n); + + const txEBB = await network.updateClusterBalance( + blockForRoot, clusterOwnerB.address, operatorIds, clusterB, 48, proofs[clusterIdB], + ); + const receiptEBB = await txEBB.wait(); + clusterB = parseClusterFromEvent(network, receiptEBB, Events.CLUSTER_BALANCE_UPDATED); + const vUnitsB = calcVUnits(48n); + expect(vUnitsB).to.equal(15000n); + + await mineBlocks(provider, 100); + + const postEBBlocks = 100n; + const opEffectiveVUnitsPostEB = 35000n; + const postEBEarningsPacked = calcOperatorFeeAccrual(postEBBlocks, ethFeePacked, opEffectiveVUnitsPostEB); + const postEBEarningsWei = postEBEarningsPacked * ETH_DEDUCTED_DIGITS; + const op1Earnings = BigInt(await views.getOperatorEarnings(BigInt(operatorIds[0]))); + expect(op1Earnings).to.be.greaterThanOrEqual(postEBEarningsWei); + + const burnRateA = calcClusterBurn({ + blockDiff: 1n, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnitsA, + }); + + const isLiqA = await views.isLiquidatable(clusterOwnerA.address, operatorIds, clusterA); + if (!isLiqA) { + const currentBalance = BigInt(clusterA.balance); + const blocksToLiquidation = currentBalance / burnRateA; + await mineBlocks(provider, Number(blocksToLiquidation) + 100); + } + + await network.connect(liquidator).liquidate( + clusterOwnerA.address, operatorIds, clusterA, + ); + + await mineBlocks(provider, 100); + + const daoValCount = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCount).to.equal(1n); + + const txSettleB = await network.connect(clusterOwnerB).withdraw( + operatorIds, 0n, clusterB, + ); + const receiptSettleB = await txSettleB.wait(); + clusterB = parseClusterFromEvent(network, receiptSettleB, Events.CLUSTER_WITHDRAWN); + + const opEarnings: bigint[] = []; + for (const opId of operatorIds) { + opEarnings.push(BigInt(await views.getOperatorEarnings(BigInt(opId)))); + } + + const regBBlock = BigInt(receiptB1!.blockNumber); + const ebBBlock = BigInt(receiptEBB!.blockNumber); + const settleBBlock = BigInt(receiptSettleB!.blockNumber); + const burnPhase1 = calcClusterBurn({ + blockDiff: ebBBlock - regBBlock, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: defaultVUnits(1n), + }); + const burnPhase2 = calcClusterBurn({ + blockDiff: settleBBlock - ebBBlock, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnitsB, + }); + const expectedClusterBBalance = depositB - burnPhase1 - burnPhase2; + expect(clusterB.balance).to.equal(expectedClusterBBalance); + + const postLiqEarningsPacked = calcOperatorFeeAccrual(100n, ethFeePacked, vUnitsB); + const postLiqEarningsWei = postLiqEarningsPacked * ETH_DEDUCTED_DIGITS; + for (const earnings of opEarnings) { + expect(earnings).to.be.greaterThanOrEqual(postLiqEarningsWei); + } + }); + }); +}); diff --git a/test/e2e/cross-cutting/full-lifecycle.test.ts b/test/e2e/cross-cutting/full-lifecycle.test.ts new file mode 100644 index 000000000..d6f3551aa --- /dev/null +++ b/test/e2e/cross-cutting/full-lifecycle.test.ts @@ -0,0 +1,270 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + parseClusterFromEvent, + generateMerkleForClusterEB, + getValidOperatorFeeIncrease, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + getBlockNumber, + calcVUnits, + defaultVUnits, + calcLiquidationThreshold, + snapshotContractBalance, + checkETHConservation, + checkAccumulatorMonotonicity, + checkCSSVSupplyConsistency, +} from "../../helpers/index.ts"; + +describe("Cross-Cutting: Full System Lifecycle", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let stakerA: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, stakerA, oracle1, oracle2, oracle3] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + it("Exercises all modules through a complete system lifecycle", async function () { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + const networkAddress = await network.getAddress(); + + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeeWei = BigInt(opData.fee); + const ethFeePacked = ethFeeWei / ETH_DEDUCTED_DIGITS; + + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const stakeAmount = ethers.parseEther("50"); + await ssvToken.transfer(stakerA.address, stakeAmount); + await ssvToken.connect(stakerA).approve(networkAddress, stakeAmount); + const txStake = await network.connect(stakerA).stake(stakeAmount); + await txStake.wait(); + + await checkCSSVSupplyConsistency(cssvToken, stakeAmount); + let prevAccEthPerShare = BigInt(await views.accEthPerShare()); + + const deposit = ethers.parseEther("10"); + const txReg = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const receiptReg = await txReg.wait(); + let cluster = parseClusterFromEvent(network, receiptReg, Events.VALIDATOR_ADDED); + + expect(cluster.validatorCount).to.equal(1n); + expect(cluster.balance).to.equal(deposit); + expect(cluster.active).to.be.true; + + const daoValCount1 = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCount1).to.equal(1n); + + await checkETHConservation( + networkAddress, connection.ethers.provider, + [cluster.balance], [0n, 0n, 0n, 0n], 0n, + ); + + await mineBlocks(connection.ethers.provider, 100); + + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + + const blockForRoot = await getBlockNumber(connection.ethers.provider); + + const clusterId = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwner.address, operatorIds]), + ); + + const entries = [{ clusterId, effectiveBalance: 48 }]; + const { root, proofs } = generateMerkleForClusterEB(connection, entries); + + await network.connect(oracle1).commitRoot(root, blockForRoot); + await network.connect(oracle2).commitRoot(root, blockForRoot); + await network.connect(oracle3).commitRoot(root, blockForRoot); + + const txEB = await network.updateClusterBalance( + blockForRoot, clusterOwner.address, operatorIds, cluster, 48, proofs[clusterId], + ); + const receiptEB = await txEB.wait(); + cluster = parseClusterFromEvent(network, receiptEB, Events.CLUSTER_BALANCE_UPDATED); + const ebUpdateBlock = receiptEB!.blockNumber; + + const newVUnits = calcVUnits(48n); + expect(newVUnits).to.equal(15000n); + + expect(cluster.balance).to.be.lessThan(deposit); + await mineBlocks(connection.ethers.provider, 100); + + const newFee = await getValidOperatorFeeIncrease(views, BigInt(operatorIds[0])); + const txDecl = await network.connect(operatorOwner).declareOperatorFee( + operatorIds[0], newFee, + ); + await txDecl.wait(); + + const feePeriods = await views.getOperatorFeePeriods(); + const declareTimePeriod = BigInt(feePeriods[0]); + await connection.ethers.provider.send("evm_increaseTime", [Number(declareTimePeriod) + 1]); + await mineBlocks(connection.ethers.provider, 1); + + await network.connect(operatorOwner).executeOperatorFee(operatorIds[0]); + + const opAfterFee = await views.getOperatorById(BigInt(operatorIds[0])); + expect(BigInt(opAfterFee.fee)).to.equal(BigInt(newFee)); + + const txReg2 = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster, + { value: 0n }, + ); + const receiptReg2 = await txReg2.wait(); + cluster = parseClusterFromEvent(network, receiptReg2, Events.VALIDATOR_ADDED); + + expect(cluster.validatorCount).to.equal(2n); + + const daoValCount2 = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCount2).to.equal(2n); + + await mineBlocks(connection.ethers.provider, 100); + + const stakerABalanceBefore = await connection.ethers.provider.getBalance(stakerA.address); + const txClaim = await network.connect(stakerA).claimEthRewards(); + const receiptClaim = await txClaim.wait(); + const stakerABalanceAfter = await connection.ethers.provider.getBalance(stakerA.address); + const claimedAmount = stakerABalanceAfter - stakerABalanceBefore + receiptClaim!.gasUsed * receiptClaim!.gasPrice; + + const remainingDaoEarnings = BigInt(await views.getNetworkEarnings()); + expect(remainingDaoEarnings).to.be.lessThanOrEqual(ETH_DEDUCTED_DIGITS); + const accAtClaim = BigInt(await views.accEthPerShare()); + const expectedRewardRaw = (stakeAmount * accAtClaim) / (10n ** 18n); + const expectedPayout = expectedRewardRaw - (expectedRewardRaw % ETH_DEDUCTED_DIGITS); + expect(claimedAmount).to.equal(expectedPayout); + + const accAfterClaim = BigInt(await views.accEthPerShare()); + checkAccumulatorMonotonicity(prevAccEthPerShare, accAfterClaim); + prevAccEthPerShare = accAfterClaim; + + const txRemove = await network.connect(clusterOwner).removeValidator( + makePublicKey(1), operatorIds, cluster, + ); + const receiptRemove = await txRemove.wait(); + cluster = parseClusterFromEvent(network, receiptRemove, Events.VALIDATOR_REMOVED); + + expect(cluster.validatorCount).to.equal(1n); + expect(cluster.active).to.be.true; + + const daoValCount3 = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCount3).to.equal(1n); + + await mineBlocks(connection.ethers.provider, 100); + + const currentBalance = BigInt( + await views.getBalance(clusterOwner.address, operatorIds, cluster), + ); + + const liqThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: BigInt(await views.getLiquidationThresholdPeriod()), + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: defaultVUnits(1n), + }); + + let withdrawAmount: bigint; + if (currentBalance > liqThreshold * 2n) { + withdrawAmount = currentBalance - liqThreshold * 2n; + } else { + withdrawAmount = 0n; + } + + const txWithdraw = await network.connect(clusterOwner).withdraw( + operatorIds, withdrawAmount, cluster, + ); + const receiptWithdraw = await txWithdraw.wait(); + cluster = parseClusterFromEvent(network, receiptWithdraw, Events.CLUSTER_WITHDRAWN); + const withdrawBlock = receiptWithdraw!.blockNumber; + + if (withdrawAmount > 0n) { + expect(cluster.balance).to.be.lessThan(currentBalance); + } + + const txRemove2 = await network.connect(clusterOwner).removeValidator( + makePublicKey(2), operatorIds, cluster, + ); + const receiptRemove2 = await txRemove2.wait(); + cluster = parseClusterFromEvent(network, receiptRemove2, Events.VALIDATOR_REMOVED); + + expect(cluster.validatorCount).to.equal(0n); + + const daoValCount4 = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCount4).to.equal(0n); + + const txRemoveOp = await network.connect(operatorOwner).removeOperator(operatorIds[0]); + await txRemoveOp.wait(); + + const opRemoved = await views.getOperatorById(BigInt(operatorIds[0])); + expect(opRemoved.isActive).to.be.false; + + const contractETH = await snapshotContractBalance(connection.ethers.provider, networkAddress); + const clusterBalance = cluster.balance; + const opEarnings: bigint[] = []; + for (const opId of operatorIds) { + opEarnings.push(BigInt(await views.getOperatorEarnings(BigInt(opId)))); + } + const daoEarnings = BigInt(await views.getNetworkEarnings()); + + await checkETHConservation( + networkAddress, connection.ethers.provider, + [clusterBalance], opEarnings, daoEarnings, + ); + + const finalDaoValCount = BigInt(await views.getNetworkValidatorsCount()); + let totalOpValCount = 0n; + for (const opId of operatorIds) { + const op = await views.getOperatorById(BigInt(opId)); + totalOpValCount += BigInt(op.validatorCount); + } + expect(finalDaoValCount).to.equal(0n); + + await checkCSSVSupplyConsistency(cssvToken, stakeAmount); + + const finalAcc = BigInt(await views.accEthPerShare()); + checkAccumulatorMonotonicity(prevAccEthPerShare, finalAcc); + + const totalAccounted = clusterBalance + opEarnings.reduce((a, b) => a + b, 0n) + daoEarnings; + expect(contractETH).to.be.greaterThanOrEqual(totalAccounted); + + const dust = contractETH - totalAccounted; + expect(dust).to.be.greaterThanOrEqual(0n); + expect(dust).to.be.lessThanOrEqual(20n * ETH_DEDUCTED_DIGITS); + }); +}); diff --git a/test/e2e/cross-cutting/multi-step-flows.test.ts b/test/e2e/cross-cutting/multi-step-flows.test.ts new file mode 100644 index 000000000..574c07f1a --- /dev/null +++ b/test/e2e/cross-cutting/multi-step-flows.test.ts @@ -0,0 +1,603 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + parseClusterFromEvent, + generateMerkleForClusterEB, + getValidOperatorFeeIncrease, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + calcOperatorFeeAccrual, + calcVUnits, + defaultVUnits, + calcLiquidationThreshold, + snapshotContractBalance, + checkETHConservation, +} from "../../helpers/index.ts"; + +describe("Cross-Cutting: Multi-Step Flows", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let clusterOwner2: HardhatEthersSigner; + let staker: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, clusterOwner2, staker, liquidator, oracle1, oracle2, oracle3] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Register → EB Update → Fee Change → Liquidation", () => { + it("Correctly settles fees across EB update, fee change, and liquidation phases", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const networkAddress = await network.getAddress(); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeeWei = BigInt(opData.fee); + const ethFeePacked = ethFeeWei / ETH_DEDUCTED_DIGITS; + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const stakeAmount = ethers.parseEther("100"); + await ssvToken.transfer(staker.address, stakeAmount); + await ssvToken.connect(staker).approve(networkAddress, stakeAmount); + await network.connect(staker).stake(stakeAmount); + + const deposit = ethers.parseEther("5"); + const tx1 = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const receipt1 = await tx1.wait(); + let cluster = parseClusterFromEvent(network, receipt1, Events.VALIDATOR_ADDED); + + const tx1b = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster, + { value: 0n }, + ); + const receipt1b = await tx1b.wait(); + cluster = parseClusterFromEvent(network, receipt1b, Events.VALIDATOR_ADDED); + + expect(cluster.validatorCount).to.equal(2n); + + await checkETHConservation( + networkAddress, provider, + [cluster.balance], [0n, 0n, 0n, 0n], 0n, + ); + + await mineBlocks(provider, 50); + + const tx2 = await network.connect(clusterOwner).registerValidator( + makePublicKey(3), operatorIds, DEFAULT_SHARES, cluster, + { value: 0n }, + ); + const receipt2 = await tx2.wait(); + cluster = parseClusterFromEvent(network, receipt2, Events.VALIDATOR_ADDED); + + expect(cluster.validatorCount).to.equal(3n); + + expect(cluster.balance).to.be.lessThan(deposit); + + await mineBlocks(provider, 50); + + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + + const blockForRoot = await getBlockNumber(provider); + + const clusterId = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwner.address, operatorIds]), + ); + + const entries = [{ clusterId, effectiveBalance: 192 }]; + const { root, proofs } = generateMerkleForClusterEB(connection, entries); + + await network.connect(oracle1).commitRoot(root, blockForRoot); + await network.connect(oracle2).commitRoot(root, blockForRoot); + await network.connect(oracle3).commitRoot(root, blockForRoot); + + const balanceBeforeEB = cluster.balance; + const txEB = await network.updateClusterBalance( + blockForRoot, clusterOwner.address, operatorIds, cluster, 192, proofs[clusterId], + ); + const receiptEB = await txEB.wait(); + cluster = parseClusterFromEvent(network, receiptEB, Events.CLUSTER_BALANCE_UPDATED); + const step3Block = receiptEB!.blockNumber; + + const expectedVUnits = calcVUnits(192n); + expect(expectedVUnits).to.equal(60000n); + + expect(cluster.balance).to.be.lessThan(balanceBeforeEB); + + const newFee = await getValidOperatorFeeIncrease(views, BigInt(operatorIds[0])); + + const txDecl = await network.connect(operatorOwner).declareOperatorFee( + operatorIds[0], newFee, + ); + await txDecl.wait(); + + const feePeriods = await views.getOperatorFeePeriods(); + const declareTimePeriod = BigInt(feePeriods[0]); + await provider.send("evm_increaseTime", [Number(declareTimePeriod) + 1]); + await mineBlocks(provider, 1); + + const txExec = await network.connect(operatorOwner).executeOperatorFee(operatorIds[0]); + const receiptExec = await txExec.wait(); + const step5Block = receiptExec!.blockNumber; + + const opAfterFee = await views.getOperatorById(BigInt(operatorIds[0])); + expect(BigInt(opAfterFee.fee)).to.equal(BigInt(newFee)); + + const newOpFeePacked = BigInt(newFee) / ETH_DEDUCTED_DIGITS; + const currentBalance = BigInt( + await views.getBalance(clusterOwner.address, operatorIds, cluster), + ); + + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: 3n, + ethFee: ethFeePacked, + networkFee: 0n, + effectiveVUnits: expectedVUnits, + }) + calcClusterBurn({ + blockDiff: 1n, + numOperators: 1n, + ethFee: newOpFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: expectedVUnits, + }); + + if (burnPerBlock > 0n) { + const blocksToLiquidation = currentBalance / burnPerBlock; + await mineBlocks(provider, Number(blocksToLiquidation) + 200); + } + + const isLiq = await views.isLiquidatable(clusterOwner.address, operatorIds, cluster); + if (isLiq) { + const txLiq = await network.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + const receiptLiq = await txLiq.wait(); + const clusterPostLiq = parseClusterFromEvent(network, receiptLiq, Events.CLUSTER_LIQUIDATED); + + expect(clusterPostLiq.active).to.be.false; + expect(clusterPostLiq.balance).to.equal(0n); + + const op1Earnings = BigInt(await views.getOperatorEarnings(BigInt(operatorIds[0]))); + const op1Phase3 = calcOperatorFeeAccrual( + BigInt(step5Block - step3Block), ethFeePacked, expectedVUnits, + ) * ETH_DEDUCTED_DIGITS; + expect(op1Earnings).to.be.greaterThanOrEqual(op1Phase3); + + const txWithdraw = await network.connect(operatorOwner).withdrawAllOperatorEarnings(operatorIds[0]); + await txWithdraw.wait(); + + const op1EarningsAfter = BigInt(await views.getOperatorEarnings(BigInt(operatorIds[0]))); + expect(op1EarningsAfter).to.equal(0n); + + const contractETH = await snapshotContractBalance(provider, networkAddress); + const opEarnings: bigint[] = []; + for (const opId of operatorIds) { + opEarnings.push(BigInt(await views.getOperatorEarnings(BigInt(opId)))); + } + const daoEarnings = BigInt(await views.getNetworkEarnings()); + const totalAccounted = opEarnings.reduce((a, b) => a + b, 0n) + daoEarnings; + const diff = contractETH > totalAccounted + ? contractETH - totalAccounted + : totalAccounted - contractETH; + expect(diff).to.be.lessThanOrEqual(ethers.parseEther("0.01")); + } + }); + }); + + describe("Sequential Registration — Two Clusters, Same Operators", () => { + it("Correctly tracks operator ETH state when two clusters register sequentially", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const networkAddress = await network.getAddress(); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, clusterOwner2.address, + ]); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeeWei = BigInt(opData.fee); + const ethFeePacked = ethFeeWei / ETH_DEDUCTED_DIGITS; + + const depositA = ethers.parseEther("5"); + const txA = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositA }, + ); + const receiptA = await txA.wait(); + let clusterA = parseClusterFromEvent(network, receiptA, Events.VALIDATOR_ADDED); + const blockA = receiptA!.blockNumber; + + for (const opId of operatorIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(BigInt(op.validatorCount)).to.equal(1n); + } + + await mineBlocks(provider, 100); + + const depositB = ethers.parseEther("10"); + const txB1 = await network.connect(clusterOwner2).registerValidator( + makePublicKey(10), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositB }, + ); + const receiptB1 = await txB1.wait(); + let clusterB = parseClusterFromEvent(network, receiptB1, Events.VALIDATOR_ADDED); + const blockB = receiptB1!.blockNumber; + + const txB2 = await network.connect(clusterOwner2).registerValidator( + makePublicKey(11), operatorIds, DEFAULT_SHARES, clusterB, + { value: 0n }, + ); + const receiptB2 = await txB2.wait(); + clusterB = parseClusterFromEvent(network, receiptB2, Events.VALIDATOR_ADDED); + + for (const opId of operatorIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(BigInt(op.validatorCount)).to.equal(3n); + } + + const blockDiffPhase1 = BigInt(blockB - blockA); + + const blockB2 = receiptB2!.blockNumber; + const perOpIndexAtB2 = BigInt(blockB2 - blockA) * ethFeePacked; + const expectedMinIndex = 4n * perOpIndexAtB2; + expect(clusterB.index).to.be.greaterThanOrEqual(expectedMinIndex); + + await mineBlocks(provider, 100); + + const expectedPerOpPhase1 = + calcOperatorFeeAccrual(blockDiffPhase1, ethFeePacked, defaultVUnits(1n)) * ETH_DEDUCTED_DIGITS; + const op1Earnings = BigInt(await views.getOperatorEarnings(BigInt(operatorIds[0]))); + expect(op1Earnings).to.be.greaterThan(expectedPerOpPhase1); + + const clusterABalance = BigInt( + await views.getBalance(clusterOwner.address, operatorIds, clusterA), + ); + const clusterBBalance = BigInt( + await views.getBalance(clusterOwner2.address, operatorIds, clusterB), + ); + const opEarnings: bigint[] = []; + for (const opId of operatorIds) { + opEarnings.push(BigInt(await views.getOperatorEarnings(BigInt(opId)))); + } + const daoEarnings = BigInt(await views.getNetworkEarnings()); + + await checkETHConservation( + networkAddress, provider, + [clusterABalance, clusterBBalance], + opEarnings, daoEarnings, + ); + + const daoValCount = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCount).to.equal(3n); + }); + }); + + describe("Governance Parameter Change Mid-Operation", () => { + describe("Network Fee Update", () => { + it("Correctly applies old fee for first half and new fee for second half", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeePacked = BigInt(opData.fee) / ETH_DEDUCTED_DIGITS; + const oldNetworkFeeWei = BigInt(await views.getNetworkFee()); + const oldNetworkFeePacked = oldNetworkFeeWei / ETH_DEDUCTED_DIGITS; + + const tx1 = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const receipt1 = await tx1.wait(); + let cluster = parseClusterFromEvent(network, receipt1, Events.VALIDATOR_ADDED); + const registerBlock = receipt1!.blockNumber; + + await mineBlocks(provider, 100); + + const newNetworkFeeWei = oldNetworkFeeWei * 2n; + const txFee = await network.updateNetworkFee(newNetworkFeeWei); + const receiptFee = await txFee.wait(); + const feeChangeBlock = receiptFee!.blockNumber; + + const currentFee = BigInt(await views.getNetworkFee()); + expect(currentFee).to.equal(newNetworkFeeWei); + + await mineBlocks(provider, 100); + + const tx3 = await network.connect(clusterOwner).withdraw( + operatorIds, 0n, cluster, + ); + const receipt3 = await tx3.wait(); + cluster = parseClusterFromEvent(network, receipt3, Events.CLUSTER_WITHDRAWN); + const withdrawBlock = receipt3!.blockNumber; + + const vUnits = defaultVUnits(1n); + const blockDiff1 = BigInt(feeChangeBlock - registerBlock); + const blockDiff2 = BigInt(withdrawBlock - feeChangeBlock); + const newNetworkFeePacked = newNetworkFeeWei / ETH_DEDUCTED_DIGITS; + + const burn1 = calcClusterBurn({ + blockDiff: blockDiff1, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: oldNetworkFeePacked, + effectiveVUnits: vUnits, + }); + + const burn2 = calcClusterBurn({ + blockDiff: blockDiff2, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: newNetworkFeePacked, + effectiveVUnits: vUnits, + }); + + const totalBurn = burn1 + burn2; + const expectedBalance = DEFAULT_ETH_REGISTER_VALUE - totalBurn; + + expect(cluster.balance).to.equal(expectedBalance); + + const expectedDaoEarnings = + (blockDiff1 * oldNetworkFeePacked + blockDiff2 * newNetworkFeePacked) * ETH_DEDUCTED_DIGITS; + const daoEarnings = BigInt(await views.getNetworkEarnings()); + expect(daoEarnings).to.equal(expectedDaoEarnings); + }); + }); + + describe("Liquidation Threshold Update", () => { + it("Cluster becomes liquidatable when threshold increases", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeeWei = BigInt(opData.fee); + const ethFeePacked = ethFeeWei / ETH_DEDUCTED_DIGITS; + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const currentThreshold = BigInt(await views.getLiquidationThresholdPeriod()); + + const vUnits = defaultVUnits(1n); + const thresholdBalance = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: currentThreshold, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnits, + }); + + const deposit = thresholdBalance + ethers.parseEther("0.1"); + const tx1 = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const receipt1 = await tx1.wait(); + let cluster = parseClusterFromEvent(network, receipt1, Events.VALIDATOR_ADDED); + + await mineBlocks(provider, 100); + + let isLiq = await views.isLiquidatable(clusterOwner.address, operatorIds, cluster); + expect(isLiq).to.be.false; + + const newThreshold = currentThreshold * 2n; + await network.updateLiquidationThresholdPeriod(newThreshold); + + const updatedThreshold = BigInt(await views.getLiquidationThresholdPeriod()); + expect(updatedThreshold).to.equal(newThreshold); + + const burnPerBlockWei = calcClusterBurn({ + blockDiff: 1n, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnits, + }); + const newThresholdBalance = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: newThreshold, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnits, + }); + + if (burnPerBlockWei > 0n) { + const additionalNeeded = (deposit - newThresholdBalance) / burnPerBlockWei; + const blocksToMine = Number(additionalNeeded) + 200; + await mineBlocks(provider, blocksToMine); + } + + isLiq = await views.isLiquidatable(clusterOwner.address, operatorIds, cluster); + expect(isLiq).to.be.true; + + const txLiq = await network.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + const receiptLiq = await txLiq.wait(); + const liquidatedCluster = parseClusterFromEvent( + network, receiptLiq, Events.CLUSTER_LIQUIDATED, + ); + expect(liquidatedCluster.active).to.be.false; + }); + }); + }); + + describe("Fee declaration interleavings with EB updates", () => { + it("declaring fee, then updating EB, then executing settles pre-exec blocks at old fee", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkAddress = await network.getAddress(); + const stakeAmount = ethers.parseEther("100"); + await ssvToken.transfer(staker.address, stakeAmount); + await ssvToken.connect(staker).approve(networkAddress, stakeAmount); + await network.connect(staker).stake(stakeAmount); + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(9101), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(network, registerReceipt, Events.VALIDATOR_ADDED); + + const clusterId = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwner.address, operatorIds]), + ); + const oldFeeWei = BigInt((await views.getOperatorById(BigInt(operatorIds[0]))).fee); + const oldFeePacked = oldFeeWei / ETH_DEDUCTED_DIGITS; + + const { root: root64, proofs: proofs64 } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + const rootBlock64 = await getBlockNumber(provider); + await network.connect(oracle1).commitRoot(root64, rootBlock64); + await network.connect(oracle2).commitRoot(root64, rootBlock64); + await network.connect(oracle3).commitRoot(root64, rootBlock64); + const txEb64 = await network.updateClusterBalance( + rootBlock64, clusterOwner.address, operatorIds, clusterAfterRegister, 64, proofs64[clusterId], + ); + const clusterAfterEb64 = parseClusterFromEvent(network, await txEb64.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const newFee = await getValidOperatorFeeIncrease(views, BigInt(operatorIds[0])); + await network.connect(operatorOwner).declareOperatorFee(operatorIds[0], newFee); + + const { root: root128, proofs: proofs128 } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 128 }, + ]); + const rootBlock128 = await getBlockNumber(provider); + await network.connect(oracle1).commitRoot(root128, rootBlock128); + await network.connect(oracle2).commitRoot(root128, rootBlock128); + await network.connect(oracle3).commitRoot(root128, rootBlock128); + const txEb128 = await network.updateClusterBalance( + rootBlock128, clusterOwner.address, operatorIds, clusterAfterEb64, 128, proofs128[clusterId], + ); + const receiptEb128 = await txEb128.wait(); + const earningsBeforeExecute = BigInt(await views.getOperatorEarnings(BigInt(operatorIds[0]))); + + const feePeriods = await views.getOperatorFeePeriods(); + const declareDelay = BigInt(feePeriods[0]); + await provider.send("evm_increaseTime", [Number(declareDelay) + 1]); + await mineBlocks(provider, 1); + + const execTx = await network.connect(operatorOwner).executeOperatorFee(operatorIds[0]); + const execReceipt = await execTx.wait(); + const execBlock = BigInt(execReceipt!.blockNumber); + const eb128Block = BigInt(receiptEb128!.blockNumber); + + const earningsAfterExecute = BigInt(await views.getOperatorEarnings(BigInt(operatorIds[0]))); + const expectedDelta = calcOperatorFeeAccrual( + execBlock - eb128Block, + oldFeePacked, + calcVUnits(128n), + ) * ETH_DEDUCTED_DIGITS; + expect(earningsAfterExecute - earningsBeforeExecute).to.equal(expectedDelta); + + const updatedOperator = await views.getOperatorById(BigInt(operatorIds[0])); + expect(BigInt(updatedOperator.fee)).to.equal(BigInt(newFee)); + }); + + it("executeOperatorFee reverts after operator removal on explicit-EB cluster", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkAddress = await network.getAddress(); + const stakeAmount = ethers.parseEther("100"); + await ssvToken.transfer(staker.address, stakeAmount); + await ssvToken.connect(staker).approve(networkAddress, stakeAmount); + await network.connect(staker).stake(stakeAmount); + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(9201), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const clusterAfterRegister = parseClusterFromEvent(network, await registerTx.wait(), Events.VALIDATOR_ADDED); + + const clusterId = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwner.address, operatorIds]), + ); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + const rootBlock = await getBlockNumber(provider); + await network.connect(oracle1).commitRoot(root, rootBlock); + await network.connect(oracle2).commitRoot(root, rootBlock); + await network.connect(oracle3).commitRoot(root, rootBlock); + await network.updateClusterBalance( + rootBlock, clusterOwner.address, operatorIds, clusterAfterRegister, 64, proofs[clusterId], + ); + + const declaredFee = await getValidOperatorFeeIncrease(views, BigInt(operatorIds[0])); + await network.connect(operatorOwner).declareOperatorFee(operatorIds[0], declaredFee); + await network.connect(operatorOwner).removeOperator(operatorIds[0]); + + const feePeriods = await views.getOperatorFeePeriods(); + const declareDelay = BigInt(feePeriods[0]); + await provider.send("evm_increaseTime", [Number(declareDelay) + 1]); + await mineBlocks(provider, 1); + + await expect( + network.connect(operatorOwner).executeOperatorFee(operatorIds[0]), + ).to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + }); +}); diff --git a/test/e2e/cross-cutting/staking-integration.test.ts b/test/e2e/cross-cutting/staking-integration.test.ts new file mode 100644 index 000000000..58242df60 --- /dev/null +++ b/test/e2e/cross-cutting/staking-integration.test.ts @@ -0,0 +1,344 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + parseClusterFromEvent, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + calcClusterBurn, + defaultVUnits, + calcLiquidationThreshold, + checkAccumulatorMonotonicity, + checkCSSVSupplyConsistency, +} from "../../helpers/index.ts"; + +describe("Cross-Cutting: Staking Integration", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let clusterOwner2: HardhatEthersSigner; + let staker: HardhatEthersSigner; + let userA: HardhatEthersSigner; + let userB: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, clusterOwner2, staker, userA, userB] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Multi-Staker Revenue Distribution Through State Changes", () => { + it("Correctly distributes rewards pro-rata across multiple stakers through EB changes", async function () { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const networkAddress = await network.getAddress(); + + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const stakeA = ethers.parseEther("100"); + await ssvToken.transfer(userA.address, stakeA); + await ssvToken.connect(userA).approve(networkAddress, stakeA); + await network.connect(userA).stake(stakeA); + + const cssvBalanceA = BigInt(await cssvToken.balanceOf(userA.address)); + expect(cssvBalanceA).to.equal(stakeA); + + const deposit = ethers.parseEther("10"); + const txReg = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const receiptReg = await txReg.wait(); + const registerBlock = receiptReg!.blockNumber; + + let prevAccEthPerShare = BigInt(await views.accEthPerShare()); + + await mineBlocks(provider, 50); + + const stakeB = ethers.parseEther("300"); + await ssvToken.transfer(userB.address, stakeB); + await ssvToken.connect(userB).approve(networkAddress, stakeB); + const txStakeB = await network.connect(userB).stake(stakeB); + const receiptStakeB = await txStakeB.wait(); + const stakeBBlock = receiptStakeB!.blockNumber; + + let currentAccEthPerShare = BigInt(await views.accEthPerShare()); + checkAccumulatorMonotonicity(prevAccEthPerShare, currentAccEthPerShare); + prevAccEthPerShare = currentAccEthPerShare; + + await checkCSSVSupplyConsistency(cssvToken, stakeA + stakeB); + + await mineBlocks(provider, 50); + + const aBalanceBefore = await provider.getBalance(userA.address); + const txClaimA = await network.connect(userA).claimEthRewards(); + const receiptClaimA = await txClaimA.wait(); + + currentAccEthPerShare = BigInt(await views.accEthPerShare()); + checkAccumulatorMonotonicity(prevAccEthPerShare, currentAccEthPerShare); + + const aBalanceAfter = await provider.getBalance(userA.address); + const claimAAmount = aBalanceAfter - aBalanceBefore + receiptClaimA!.gasUsed * receiptClaimA!.gasPrice; + const claimABlock = receiptClaimA!.blockNumber; + + const PRECISION = 10n ** 18n; + const phase1Blocks = BigInt(stakeBBlock - registerBlock); + const phase1FeesWei = phase1Blocks * networkFeePacked * ETH_DEDUCTED_DIGITS; + const delta1 = (phase1FeesWei * PRECISION) / stakeA; + + const phase2Blocks = BigInt(claimABlock - stakeBBlock); + const phase2FeesWei = phase2Blocks * networkFeePacked * ETH_DEDUCTED_DIGITS; + const totalSupplyPhase2 = stakeA + stakeB; + const delta2 = (phase2FeesWei * PRECISION) / totalSupplyPhase2; + + const expectedClaimARaw = (stakeA * (delta1 + delta2)) / PRECISION; + const expectedClaimA = expectedClaimARaw - (expectedClaimARaw % ETH_DEDUCTED_DIGITS); + expect(claimAAmount).to.equal(expectedClaimA); + + const bBalanceBefore = await provider.getBalance(userB.address); + const txClaimB = await network.connect(userB).claimEthRewards(); + const receiptClaimB = await txClaimB.wait(); + + const bBalanceAfter = await provider.getBalance(userB.address); + const claimBAmount = bBalanceAfter - bBalanceBefore + receiptClaimB!.gasUsed * receiptClaimB!.gasPrice; + const claimBBlock = receiptClaimB!.blockNumber; + + const phase3Blocks = BigInt(claimBBlock - claimABlock); + const phase3FeesWei = phase3Blocks * networkFeePacked * ETH_DEDUCTED_DIGITS; + const delta3 = (phase3FeesWei * PRECISION) / totalSupplyPhase2; + + const expectedClaimBRaw = (stakeB * (delta2 + delta3)) / PRECISION; + const expectedClaimB = expectedClaimBRaw - (expectedClaimBRaw % ETH_DEDUCTED_DIGITS); + expect(claimBAmount).to.equal(expectedClaimB); + + expect(claimAAmount).to.be.greaterThan(claimBAmount); + + const totalClaimed = claimAAmount + claimBAmount; + const expectedTotal = expectedClaimA + expectedClaimB; + expect(totalClaimed).to.equal(expectedTotal); + }); + }); + + describe("Staking Rewards Through Liquidation Event", () => { + it("Correctly adjusts reward rate when cluster is liquidated", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const networkAddress = await network.getAddress(); + + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, clusterOwner2.address, + ]); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + const ethFeeWei = BigInt(opData.fee); + const ethFeePacked = ethFeeWei / ETH_DEDUCTED_DIGITS; + + const stakeAmount = ethers.parseEther("10"); + await ssvToken.transfer(staker.address, stakeAmount); + await ssvToken.connect(staker).approve(networkAddress, stakeAmount); + await network.connect(staker).stake(stakeAmount); + + const vUnits1 = defaultVUnits(1n); + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnits1, + }); + + const liqThresholdPeriod = BigInt(await views.getLiquidationThresholdPeriod()); + const liqThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: liqThresholdPeriod, + numOperators: 4n, + ethFee: ethFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnits1, + }); + + const depositA = liqThreshold + burnPerBlock * 200n; + const txA = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositA }, + ); + const receiptA = await txA.wait(); + let clusterA = parseClusterFromEvent(network, receiptA, Events.VALIDATOR_ADDED); + + const depositB = liqThreshold + burnPerBlock * 500n; + await network.connect(clusterOwner2).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositB }, + ); + + const daoValCount = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCount).to.equal(2n); + + let prevAccEthPerShare = BigInt(await views.accEthPerShare()); + + await mineBlocks(provider, 100); + + await mineBlocks(provider, 200); + + const isLiq = await views.isLiquidatable(clusterOwner.address, operatorIds, clusterA); + expect(isLiq).to.be.true; + + const txLiq = await network.connect(userA).liquidate( + clusterOwner.address, operatorIds, clusterA, + ); + const receiptLiq = await txLiq.wait(); + clusterA = parseClusterFromEvent(network, receiptLiq, Events.CLUSTER_LIQUIDATED); + expect(clusterA.active).to.be.false; + + const daoValCountAfterLiq = BigInt(await views.getNetworkValidatorsCount()); + expect(daoValCountAfterLiq).to.equal(1n); + + await mineBlocks(provider, 100); + + const stakerBalanceBefore = await provider.getBalance(staker.address); + const txClaim = await network.connect(staker).claimEthRewards(); + const receiptClaim = await txClaim.wait(); + const stakerBalanceAfter = await provider.getBalance(staker.address); + const claimedAmount = stakerBalanceAfter - stakerBalanceBefore + receiptClaim!.gasUsed * receiptClaim!.gasPrice; + + const PRECISION = 10n ** 18n; + const accAtClaim = BigInt(await views.accEthPerShare()); + const expectedRewardRaw = (stakeAmount * accAtClaim) / PRECISION; + const expectedPayout = expectedRewardRaw - (expectedRewardRaw % ETH_DEDUCTED_DIGITS); + expect(claimedAmount).to.equal(expectedPayout); + + const remainingDao = BigInt(await views.getNetworkEarnings()); + expect(remainingDao).to.be.lessThanOrEqual(ETH_DEDUCTED_DIGITS); + + const finalAccEthPerShare = BigInt(await views.accEthPerShare()); + checkAccumulatorMonotonicity(prevAccEthPerShare, finalAccEthPerShare); + }); + }); + + describe("cSSV Transfer Mid-Revenue-Accrual", () => { + it("Correctly settles rewards for both parties at pre-transfer balances", async function () { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const networkAddress = await network.getAddress(); + + const networkFeeWei = BigInt(await views.getNetworkFee()); + const networkFeePacked = networkFeeWei / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const stakeA = ethers.parseEther("100"); + await ssvToken.transfer(userA.address, stakeA); + await ssvToken.connect(userA).approve(networkAddress, stakeA); + await network.connect(userA).stake(stakeA); + + const cssvBalA = BigInt(await cssvToken.balanceOf(userA.address)); + expect(cssvBalA).to.equal(stakeA); + const cssvBalB = BigInt(await cssvToken.balanceOf(userB.address)); + expect(cssvBalB).to.equal(0n); + + const deposit = ethers.parseEther("10"); + const txReg = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: deposit }, + ); + const receiptReg = await txReg.wait(); + + let prevAccEthPerShare = BigInt(await views.accEthPerShare()); + + await mineBlocks(provider, 50); + + const transferAmount = ethers.parseEther("50"); + const txTransfer = await cssvToken.connect(userA).transfer(userB.address, transferAmount); + const receiptTransfer = await txTransfer.wait(); + + const cssvBalAAfter = BigInt(await cssvToken.balanceOf(userA.address)); + const cssvBalBAfter = BigInt(await cssvToken.balanceOf(userB.address)); + expect(cssvBalAAfter).to.equal(ethers.parseEther("50")); + expect(cssvBalBAfter).to.equal(ethers.parseEther("50")); + + const accAfterTransfer = BigInt(await views.accEthPerShare()); + checkAccumulatorMonotonicity(prevAccEthPerShare, accAfterTransfer); + prevAccEthPerShare = accAfterTransfer; + + await checkCSSVSupplyConsistency(cssvToken, stakeA); + + await mineBlocks(provider, 50); + + const aBalanceBeforeClaim = await provider.getBalance(userA.address); + const txClaimA = await network.connect(userA).claimEthRewards(); + const receiptClaimA = await txClaimA.wait(); + const aBalanceAfterClaim = await provider.getBalance(userA.address); + const claimAAmount = aBalanceAfterClaim - aBalanceBeforeClaim + receiptClaimA!.gasUsed * receiptClaimA!.gasPrice; + + const accAfterClaimA = BigInt(await views.accEthPerShare()); + checkAccumulatorMonotonicity(prevAccEthPerShare, accAfterClaimA); + + const bBalanceBeforeClaim = await provider.getBalance(userB.address); + const txClaimB = await network.connect(userB).claimEthRewards(); + const receiptClaimB = await txClaimB.wait(); + const bBalanceAfterClaim = await provider.getBalance(userB.address); + const claimBAmount = bBalanceAfterClaim - bBalanceBeforeClaim + receiptClaimB!.gasUsed * receiptClaimB!.gasPrice; + + const claimABlock = receiptClaimA!.blockNumber; + const claimBBlock = receiptClaimB!.blockNumber; + const transferBlock = receiptTransfer!.blockNumber; + const regBlock = receiptReg!.blockNumber; + const PRECISION = 10n ** 18n; + + const phase1Blocks = BigInt(transferBlock - regBlock); + const phase1FeesWei = phase1Blocks * networkFeePacked * ETH_DEDUCTED_DIGITS; + const delta1 = (phase1FeesWei * PRECISION) / stakeA; + + const phase2Blocks = BigInt(claimABlock - transferBlock); + const phase2FeesWei = phase2Blocks * networkFeePacked * ETH_DEDUCTED_DIGITS; + const delta2 = (phase2FeesWei * PRECISION) / stakeA; + + const accruedAFromTransfer = (stakeA * delta1) / PRECISION; + const pendingAAtClaim = (transferAmount * delta2) / PRECISION; + const totalARaw = accruedAFromTransfer + pendingAAtClaim; + const expectedClaimA = totalARaw - (totalARaw % ETH_DEDUCTED_DIGITS); + expect(claimAAmount).to.equal(expectedClaimA); + + const phase3Blocks = BigInt(claimBBlock - claimABlock); + const phase3FeesWei = phase3Blocks * networkFeePacked * ETH_DEDUCTED_DIGITS; + const delta3 = (phase3FeesWei * PRECISION) / stakeA; + + const pendingB = (transferAmount * (delta2 + delta3)) / PRECISION; + const expectedClaimB = pendingB - (pendingB % ETH_DEDUCTED_DIGITS); + + expect(claimBAmount).to.equal(expectedClaimB); + + const totalClaimed = claimAAmount + claimBAmount; + const expectedTotal = expectedClaimA + expectedClaimB; + expect(totalClaimed).to.equal(expectedTotal); + }); + }); +}); diff --git a/test/e2e/cross-cutting/validator-count-invariant.test.ts b/test/e2e/cross-cutting/validator-count-invariant.test.ts new file mode 100644 index 000000000..ba17ed8cf --- /dev/null +++ b/test/e2e/cross-cutting/validator-count-invariant.test.ts @@ -0,0 +1,244 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + parseClusterFromEvent, + getCurrentClusterState, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, +} from "../../common/constants.ts"; +import { + checkValidatorCountConsistency, + type TrackedCluster, +} from "../../helpers/index.ts"; +import { Events } from "../../common/events.ts"; + +describe("Cross-Cutting: Validator Count Invariant", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner1: HardhatEthersSigner; + let owner2: HardhatEthersSigner; + let owner3: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner1, owner2, owner3, operatorOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Validator Count Through Liquidation Cycle", () => { + it("maintains ethDaoValidatorCount == Σ(active clusters) through register → liquidate → reactivate", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + owner1.address, + owner2.address, + owner3.address, + ]); + const clusters: TrackedCluster[] = []; + const tx1 = await network.connect(owner1).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const receipt1 = await tx1.wait(); + const cluster1 = parseClusterFromEvent(network, receipt1, Events.VALIDATOR_ADDED); + + clusters.push({ + owner: owner1.address, + operatorIds: operatorIds.map(BigInt), + validatorCount: 1n, + active: true, + }); + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(1); + const tx2a = await network.connect(owner2).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const receipt2a = await tx2a.wait(); + const cluster2Partial = parseClusterFromEvent(network, receipt2a, Events.VALIDATOR_ADDED); + + const tx2b = await network.connect(owner2).registerValidator( + makePublicKey(3), + operatorIds, + DEFAULT_SHARES, + cluster2Partial, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const receipt2b = await tx2b.wait(); + const cluster2 = parseClusterFromEvent(network, receipt2b, Events.VALIDATOR_ADDED); + + clusters.push({ + owner: owner2.address, + operatorIds: operatorIds.map(BigInt), + validatorCount: 2n, + active: true, + }); + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(3); + const tx3 = await network.connect(owner3).registerValidator( + makePublicKey(4), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const receipt3 = await tx3.wait(); + const cluster3 = parseClusterFromEvent(network, receipt3, Events.VALIDATOR_ADDED); + + clusters.push({ + owner: owner3.address, + operatorIds: operatorIds.map(BigInt), + validatorCount: 1n, + active: true, + }); + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(4); + const cluster1ForLiq = await getCurrentClusterState( + connection, + network, + owner1.address, + operatorIds, + ); + + const txLiq = await network.connect(owner1).liquidate( + owner1.address, + operatorIds, + cluster1ForLiq, + ); + await txLiq.wait(); + clusters[0].active = false; + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(3); + const cluster2Current = await getCurrentClusterState( + connection, + network, + owner2.address, + operatorIds, + ); + + const cluster2ForLiq = await getCurrentClusterState( + connection, + network, + owner2.address, + operatorIds, + ); + + const txLiq2 = await network.connect(owner2).liquidate( + owner2.address, + operatorIds, + cluster2ForLiq, + ); + await txLiq2.wait(); + clusters[1].active = false; + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(1); + const cluster1Liq = await getCurrentClusterState( + connection, + network, + owner1.address, + operatorIds, + ); + + expect(cluster1Liq.active).to.equal(false); + + const txReact = await network.connect(owner1).reactivate( + operatorIds, + cluster1Liq, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + await txReact.wait(); + clusters[0].active = true; + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(2); + const cluster2Liq = await getCurrentClusterState( + connection, + network, + owner2.address, + operatorIds, + ); + + expect(cluster2Liq.active).to.equal(false); + + const txReact2 = await network.connect(owner2).reactivate( + operatorIds, + cluster2Liq, + { value: 2n * DEFAULT_ETH_REGISTER_VALUE }, + ); + await txReact2.wait(); + clusters[1].active = true; + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(4); + }); + + it("prevents double-counting when operators are shared across clusters", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + owner1.address, + owner2.address, + ]); + + const clusters: TrackedCluster[] = []; + await network.connect(owner1).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + clusters.push({ + owner: owner1.address, + operatorIds: operatorIds.map(BigInt), + validatorCount: 1n, + active: true, + }); + + await network.connect(owner2).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + clusters.push({ + owner: owner2.address, + operatorIds: operatorIds.map(BigInt), + validatorCount: 1n, + active: true, + }); + await checkValidatorCountConsistency(views, clusters); + expect(await views.getNetworkValidatorsCount()).to.equal(2); + let totalFromOperators = 0n; + for (const opId of operatorIds) { + const op = await views.getOperatorById(BigInt(opId)); + totalFromOperators += BigInt(op.validatorCount); + } + expect(totalFromOperators).to.equal(8n); + expect(await views.getNetworkValidatorsCount()).to.equal(2n); + }); + }); +}); diff --git a/test/e2e/effective-balance/eb-edge-cases.test.ts b/test/e2e/effective-balance/eb-edge-cases.test.ts new file mode 100644 index 000000000..2a2ff8d65 --- /dev/null +++ b/test/e2e/effective-balance/eb-edge-cases.test.ts @@ -0,0 +1,476 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { Cluster } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + getCurrentClusterState, + generateMerkleForClusterEB, + setupTestContext, +} from "../../common/helpers.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + STAKE_AMOUNT, +} from "../../common/constants.ts"; +import { + mineBlocks, + getBlockNumber, +} from "../../helpers/index.ts"; +import { ethers as ethersLib } from "ethers"; + +async function getClusterFromEBUpdateTx(network: any, tx: any): Promise { + const receipt = await tx.wait(); + for (const log of receipt.logs ?? []) { + let parsed; + try { parsed = network.interface.parseLog(log); } catch { continue; } + if (parsed?.name === Events.CLUSTER_BALANCE_UPDATED || parsed?.name === Events.CLUSTER_LIQUIDATED) { + const ct = parsed.args[parsed.args.length - 1]; + return { + validatorCount: ct[0].toString(), networkFeeIndex: ct[1].toString(), + index: ct[2].toString(), active: ct[3], balance: ct[4].toString(), + }; + } + } + throw new Error("ClusterBalanceUpdated event not found"); +} + +async function setMinBlocksBetweenUpdates( + provider: any, + networkAddress: string, + value: number, +): Promise { + const baseSlot = BigInt(ethersLib.keccak256(ethersLib.toUtf8Bytes("ssv.network.storage.eb"))) - 1n; + const targetSlot = baseSlot + 3n; + const slotHex = "0x" + targetSlot.toString(16).padStart(64, "0"); + + const currentValue = await provider.getStorage(networkAddress, slotHex); + const currentBigInt = BigInt(currentValue); + + const mask32at64 = ((1n << 32n) - 1n) << 64n; + const cleared = currentBigInt & ~mask32at64; + const newValue = cleared | (BigInt(value) << 64n); + + const newValueHex = "0x" + newValue.toString(16).padStart(64, "0"); + await provider.send("hardhat_setStorageAt", [networkAddress, slotHex, newValueHex]); +} + +async function setupCluster(connection: NetworkConnection<"generic">) { + const { network, views, cssvToken, ssvToken } = + await ssvNetworkFullFixture(connection); + + const provider = connection.ethers.provider; + const signers = await connection.ethers.getSigners(); + const [owner, oracle1, oracle2, oracle3, oracle4, staker, clusterOwner] = signers; + + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + await network.replaceOracle(4, oracle4.address); + + await ssvToken.transfer(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, owner, 4); + await whitelistAddresses(network, owner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + const clusterId = connection.ethers.keccak256( + connection.ethers.solidityPacked(["address", "uint64[]"], [clusterOwner.address, operatorIds]), + ); + + const oracles = [oracle1, oracle2, oracle3, oracle4]; + + return { + network, views, cssvToken, ssvToken, provider, + owner, oracles, staker, clusterOwner, + operatorIds, cluster, clusterId, + }; +} + +async function commitRootWithQuorum( + network: any, oracles: HardhatEthersSigner[], root: string, blockNum: number, +) { + await network.connect(oracles[0]).commitRoot(root, blockNum); + await network.connect(oracles[1]).commitRoot(root, blockNum); + await network.connect(oracles[2]).commitRoot(root, blockNum); +} + +async function performEBUpdate( + connection: NetworkConnection<"generic">, + network: any, oracles: HardhatEthersSigner[], provider: any, + clusterOwner: HardhatEthersSigner, operatorIds: number[], + cluster: Cluster, clusterId: string, effectiveBalance: number, +): Promise<{ cluster: Cluster; rootBlockNum: number }> { + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + const tx = await network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, effectiveBalance, proofs[clusterId], + ); + const updatedCluster = await getClusterFromEBUpdateTx(network, tx); + return { cluster: updatedCluster, rootBlockNum }; +} + +describe("EB Edge Cases", () => { + let connection: NetworkConnection<"generic">; + + before(async function () { + ({ connection } = await setupTestContext()); + }); + + describe("EB Limits Enforcement", () => { + it("Reverts when effectiveBalance is below minimum (< validatorCount * 32)", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 63 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 63, proofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.EB_BELOW_MINIMUM); + }); + + it("Succeeds when effectiveBalance is exactly at minimum (validatorCount * 32)", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 64, proofs[clusterId], + ), + ).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + }); + + it("Reverts when effectiveBalance exceeds maximum (> validatorCount * 2048)", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 4097 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 4097, proofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.EB_EXCEEDS_MAXIMUM); + }); + + it("Succeeds when effectiveBalance is exactly at maximum (validatorCount * 2048)", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 4096 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 4096, proofs[clusterId], + ), + ).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + }); + }); + + describe("Merkle Proof Verification", () => { + it("Accepts a valid merkle proof", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 64, proofs[clusterId], + ), + ).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + }); + + it("Reverts with invalid proof path", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + const fakeProof = [connection.ethers.keccak256(connection.ethers.toUtf8Bytes("fake"))]; + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 64, fakeProof, + ), + ).to.be.revertedWithCustomError(network, Errors.INVALID_PROOF); + }); + + it("Reverts when proof is for a different cluster", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const fakeClusterId = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("wrong-cluster")); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId: fakeClusterId, effectiveBalance: 64 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 64, proofs[fakeClusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.INVALID_PROOF); + }); + + it("Reverts when EB value doesn't match the proof", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 96, proofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.INVALID_PROOF); + }); + }); + + describe("Update Frequency and Staleness", () => { + it("Reverts when update is too frequent (minBlocksBetweenUpdates)", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + await setMinBlocksBetweenUpdates(provider, await network.getAddress(), 100); + + const { cluster: clusterAfterFirst } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + + await mineBlocks(provider, 50); + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 96 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, clusterAfterFirst, 96, proofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.UPDATE_TOO_FREQUENT); + }); + + it("Succeeds when enough blocks have passed", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + await setMinBlocksBetweenUpdates(provider, await network.getAddress(), 100); + + const { cluster: clusterAfterFirst } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + + await mineBlocks(provider, 100); + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 96 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, clusterAfterFirst, 96, proofs[clusterId], + ), + ).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + }); + + it("First update always passes frequency check (lastUpdateBlock == 0)", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + await setMinBlocksBetweenUpdates(provider, await network.getAddress(), 1000); + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 64, proofs[clusterId], + ), + ).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + }); + + it("Reverts with MustUseLatestRoot when a newer root has already been committed", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { cluster: clusterAfterFirst, rootBlockNum: rootBlockNum1 } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + + await mineBlocks(provider, 5); + { + const { root } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 96 }, + ]); + const rootBlockNumNew = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNumNew); + } + + const { root: oldRoot, proofs: oldProofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await expect( + network.updateClusterBalance( + rootBlockNum1, clusterOwner.address, operatorIds, clusterAfterFirst, 64, oldProofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.MUST_USE_LATEST_ROOT); + }); + + it("Reverts with StaleUpdate when replaying the latest root after a successful update", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { cluster: clusterAfterFirst, rootBlockNum: rootBlockNum1 } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + + const { proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + + await expect( + network.updateClusterBalance( + rootBlockNum1, clusterOwner.address, operatorIds, clusterAfterFirst, 64, proofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.STALE_UPDATE); + }); + + it("First update always passes staleness check (lastRootBlockNum == 0)", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + await expect( + network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, 64, proofs[clusterId], + ), + ).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + }); + + it("Reverts with MustUseLatestRoot when trying to use an older root after two updates", async function () { + const ctx = await setupCluster(connection); + const { network, provider, oracles, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + await mineBlocks(provider, 5); + const { cluster: clusterAfterFirst, rootBlockNum: rootBlockNum1 } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + + await mineBlocks(provider, 5); + const { cluster: clusterAfterSecond, rootBlockNum: rootBlockNum2 } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + clusterAfterFirst, clusterId, 96, + ); + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await expect( + network.updateClusterBalance( + rootBlockNum1, clusterOwner.address, operatorIds, clusterAfterSecond, 64, proofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.MUST_USE_LATEST_ROOT); + }); + + it("RootNotFound: reverts when no root committed for blockNum", async function () { + const ctx = await setupCluster(connection); + const { network, clusterOwner, operatorIds, cluster, clusterId } = ctx; + + const { proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + + await expect( + network.updateClusterBalance( + 999, clusterOwner.address, operatorIds, cluster, 64, proofs[clusterId], + ), + ).to.be.revertedWithCustomError(network, Errors.ROOT_NOT_FOUND); + }); + }); +}); diff --git a/test/e2e/effective-balance/eb-operator-vunits.test.ts b/test/e2e/effective-balance/eb-operator-vunits.test.ts new file mode 100644 index 000000000..999379496 --- /dev/null +++ b/test/e2e/effective-balance/eb-operator-vunits.test.ts @@ -0,0 +1,169 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { Cluster, NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + getCurrentClusterState, + generateMerkleForClusterEB, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + STAKE_AMOUNT, +} from "../../common/constants.ts"; +import { + mineBlocks, + getBlockNumber, + calcVUnits, +} from "../../helpers/index.ts"; +import { Events } from "../../common/events.ts"; + +async function getClusterFromEBUpdateTx(network: any, tx: any): Promise { + const receipt = await tx.wait(); + for (const log of receipt.logs ?? []) { + let parsed; + try { parsed = network.interface.parseLog(log); } catch { continue; } + if (parsed?.name === Events.CLUSTER_BALANCE_UPDATED || parsed?.name === Events.CLUSTER_LIQUIDATED) { + const ct = parsed.args[parsed.args.length - 1]; + return { + validatorCount: ct[0].toString(), networkFeeIndex: ct[1].toString(), + index: ct[2].toString(), active: ct[3], balance: ct[4].toString(), + }; + } + } + throw new Error("ClusterBalanceUpdated event not found"); +} + +describe("Operator vUnit Tracking", () => { + let connection: NetworkConnection<"generic">; + + before(async function () { + ({ connection } = await setupTestContext()); + }); + + async function commitRootWithQuorum( + network: any, oracles: HardhatEthersSigner[], root: string, blockNum: number, + ) { + await network.connect(oracles[0]).commitRoot(root, blockNum); + await network.connect(oracles[1]).commitRoot(root, blockNum); + await network.connect(oracles[2]).commitRoot(root, blockNum); + } + + async function performEBUpdate( + network: any, oracles: HardhatEthersSigner[], provider: any, + clusterOwner: HardhatEthersSigner, operatorIds: number[], + cluster: Cluster, clusterId: string, effectiveBalance: number, + ): Promise { + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance }, + ]); + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + const tx = await network.updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, effectiveBalance, proofs[clusterId], + ); + return getClusterFromEBUpdateTx(network, tx); + } + + describe("Operator vUnit Tracking Across Multiple Clusters", () => { + it("Accumulates vUnit deviations from multiple clusters for the same operator", async function () { + const { network, views, cssvToken, ssvToken } = + await ssvNetworkFullFixture(connection); + + const provider = connection.ethers.provider; + const signers = await connection.ethers.getSigners(); + const [owner, oracle1, oracle2, oracle3, oracle4, staker, clusterOwnerA, clusterOwnerB] = signers; + const oracles = [oracle1, oracle2, oracle3, oracle4]; + + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + await network.replaceOracle(4, oracle4.address); + + await ssvToken.transfer(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, owner, 4); + + await whitelistAddresses(network, owner, operatorIds, [ + clusterOwnerA.address, clusterOwnerB.address, + ]); + + await network.connect(clusterOwnerA).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let clusterA = await getCurrentClusterState(connection, network, clusterOwnerA.address, operatorIds); + + await network.connect(clusterOwnerA).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, clusterA, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + clusterA = await getCurrentClusterState(connection, network, clusterOwnerA.address, operatorIds); + + const clusterIdA = connection.ethers.keccak256( + connection.ethers.solidityPacked(["address", "uint64[]"], [clusterOwnerA.address, operatorIds]), + ); + + await network.connect(clusterOwnerB).registerValidator( + makePublicKey(10), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let clusterB = await getCurrentClusterState(connection, network, clusterOwnerB.address, operatorIds); + + await network.connect(clusterOwnerB).registerValidator( + makePublicKey(11), operatorIds, DEFAULT_SHARES, clusterB, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + clusterB = await getCurrentClusterState(connection, network, clusterOwnerB.address, operatorIds); + + await network.connect(clusterOwnerB).registerValidator( + makePublicKey(12), operatorIds, DEFAULT_SHARES, clusterB, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + clusterB = await getCurrentClusterState(connection, network, clusterOwnerB.address, operatorIds); + + const clusterIdB = connection.ethers.keccak256( + connection.ethers.solidityPacked(["address", "uint64[]"], [clusterOwnerB.address, operatorIds]), + ); + + expect(BigInt(clusterA.validatorCount)).to.equal(2n); + expect(BigInt(clusterB.validatorCount)).to.equal(3n); + + await mineBlocks(provider, 5); + clusterA = await performEBUpdate( + network, oracles, provider, clusterOwnerA, operatorIds, clusterA, clusterIdA, 64, + ); + + await mineBlocks(provider, 5); + clusterA = await performEBUpdate( + network, oracles, provider, clusterOwnerA, operatorIds, clusterA, clusterIdA, 96, + ); + + await mineBlocks(provider, 5); + clusterB = await performEBUpdate( + network, oracles, provider, clusterOwnerB, operatorIds, clusterB, clusterIdB, 128, + ); + + expect(clusterA.active).to.be.true; + expect(clusterB.active).to.be.true; + + expect(calcVUnits(96n)).to.equal(30000n); + expect(calcVUnits(128n)).to.equal(40000n); + + for (const opId of operatorIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(BigInt(op[2])).to.equal(5n); + } + }); + }); +}); diff --git a/test/e2e/effective-balance/eb-updates.test.ts b/test/e2e/effective-balance/eb-updates.test.ts new file mode 100644 index 000000000..0e0f4bf46 --- /dev/null +++ b/test/e2e/effective-balance/eb-updates.test.ts @@ -0,0 +1,418 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { Cluster, NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + getCurrentClusterState, + generateMerkleForClusterEB, + setupTestContext, +} from "../../common/helpers.ts"; +import { Events } from "../../common/events.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + NETWORK_FEE, + STAKE_AMOUNT, + MINIMAL_LIQUIDATION_THRESHOLD, + ETH_DEDUCTED_DIGITS, + OP_ETH_FEE_RAW, +} from "../../common/constants.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + calcVUnits, + defaultVUnits, + calcLiquidationThreshold, +} from "../../helpers/index.ts"; + +const PACKED_NETWORK_FEE = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + +async function getClusterFromEBUpdateTx( + network: any, + tx: any, +): Promise { + const receipt = await tx.wait(); + for (const log of receipt.logs ?? []) { + let parsed; + try { + parsed = network.interface.parseLog(log); + } catch { + continue; + } + if (parsed?.name === Events.CLUSTER_BALANCE_UPDATED) { + const clusterTuple = parsed.args[parsed.args.length - 1]; + return { + validatorCount: clusterTuple[0].toString(), + networkFeeIndex: clusterTuple[1].toString(), + index: clusterTuple[2].toString(), + active: clusterTuple[3], + balance: clusterTuple[4].toString(), + }; + } + if (parsed?.name === Events.CLUSTER_LIQUIDATED) { + const clusterTuple = parsed.args[parsed.args.length - 1]; + return { + validatorCount: clusterTuple[0].toString(), + networkFeeIndex: clusterTuple[1].toString(), + index: clusterTuple[2].toString(), + active: clusterTuple[3], + balance: clusterTuple[4].toString(), + }; + } + } + throw new Error("ClusterBalanceUpdated/ClusterLiquidated event not found in tx receipt"); +} + +async function setupClusterWithEB( + connection: NetworkConnection<"generic">, + networkHelpers: NetworkHelpersType, + depositValue: bigint = DEFAULT_ETH_REGISTER_VALUE, +) { + const { network, views, cssvToken, ssvToken } = + await ssvNetworkFullFixture(connection); + + const provider = connection.ethers.provider; + const signers = await connection.ethers.getSigners(); + const [owner, oracle1, oracle2, oracle3, oracle4, staker, clusterOwner] = signers; + + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + await network.replaceOracle(4, oracle4.address); + + await ssvToken.transfer(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, owner, 4); + + await whitelistAddresses(network, owner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositValue }, + ); + + let cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds, + ); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: depositValue }, + ); + + cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds, + ); + + const clusterId = connection.ethers.keccak256( + connection.ethers.solidityPacked( + ["address", "uint64[]"], + [clusterOwner.address, operatorIds], + ), + ); + + return { + network, + views, + cssvToken, + ssvToken, + provider, + owner, + oracle1, + oracle2, + oracle3, + oracle4, + staker, + clusterOwner, + operatorIds, + cluster, + clusterId, + }; +} + +async function commitRootWithQuorum( + network: any, + oracles: HardhatEthersSigner[], + root: string, + blockNum: number, +) { + await network.connect(oracles[0]).commitRoot(root, blockNum); + await network.connect(oracles[1]).commitRoot(root, blockNum); + await network.connect(oracles[2]).commitRoot(root, blockNum); +} + +async function prepareEBUpdate( + connection: NetworkConnection<"generic">, + network: any, + oracles: HardhatEthersSigner[], + provider: any, + clusterId: string, + effectiveBalance: number, +) { + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance }, + ]); + + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + + await commitRootWithQuorum(network, oracles, root, rootBlockNum); + + return { root, proof: proofs[clusterId], rootBlockNum }; +} + +async function performEBUpdate( + connection: NetworkConnection<"generic">, + network: any, + oracles: HardhatEthersSigner[], + provider: any, + clusterOwner: HardhatEthersSigner, + operatorIds: number[], + cluster: Cluster, + clusterId: string, + effectiveBalance: number, + caller?: HardhatEthersSigner, +): Promise<{ cluster: Cluster; rootBlockNum: number; tx: any }> { + const { proof, rootBlockNum } = await prepareEBUpdate( + connection, network, oracles, provider, clusterId, effectiveBalance, + ); + + const signer = caller ?? clusterOwner; + const tx = await network.connect(signer).updateClusterBalance( + rootBlockNum, clusterOwner.address, operatorIds, cluster, effectiveBalance, proof, + ); + + const updatedCluster = await getClusterFromEBUpdateTx(network, tx); + return { cluster: updatedCluster, rootBlockNum, tx }; +} + +describe("EB Updates", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + describe("First EB Update — Implicit to Explicit (Same vUnits)", () => { + it("Transitions from implicit to explicit vUnits with no deviation change", async function () { + const ctx = await setupClusterWithEB(connection, networkHelpers); + const { + network, provider, oracle1, oracle2, oracle3, oracle4, + clusterOwner, operatorIds, cluster, clusterId, + } = ctx; + const oracles = [oracle1, oracle2, oracle3, oracle4]; + + const effectiveBalance = 64; + const expectedVUnits = calcVUnits(BigInt(effectiveBalance)); + expect(expectedVUnits).to.equal(defaultVUnits(2n)); + + await mineBlocks(provider, 10); + + const { cluster: updatedCluster, tx } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, effectiveBalance, + ); + + await expect(tx).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + + expect(updatedCluster.active).to.be.true; + expect(BigInt(updatedCluster.balance)).to.be.lessThan(BigInt(cluster.balance)); + }); + }); + + describe("EB Increase — Higher Fee Burn Rate", () => { + it("Updates vUnits upward and increases the fee burn rate proportionally", async function () { + const ctx = await setupClusterWithEB(connection, networkHelpers); + const { + network, provider, oracle1, oracle2, oracle3, oracle4, + clusterOwner, operatorIds, cluster, clusterId, + } = ctx; + const oracles = [oracle1, oracle2, oracle3, oracle4]; + + const { cluster: clusterAfterFirst } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + + await mineBlocks(provider, 50); + + const { cluster: clusterAfterIncrease, tx } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + clusterAfterFirst, clusterId, 96, + ); + + await expect(tx).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + + expect(BigInt(clusterAfterIncrease.balance)).to.be.lessThan(BigInt(clusterAfterFirst.balance)); + expect(clusterAfterIncrease.active).to.be.true; + + expect(calcVUnits(96n)).to.equal(30000n); + + const oldBurn = calcClusterBurn({ + blockDiff: 100n, numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 20000n, + }); + const newBurn = calcClusterBurn({ + blockDiff: 100n, numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 30000n, + }); + expect(newBurn * 20000n).to.equal(oldBurn * 30000n); + }); + }); + + describe("EB Decrease — Lower Fee Burn Rate", () => { + it("Updates vUnits downward and decreases the fee burn rate", async function () { + const ctx = await setupClusterWithEB(connection, networkHelpers); + const { + network, provider, oracle1, oracle2, oracle3, oracle4, + clusterOwner, operatorIds, cluster, clusterId, + } = ctx; + const oracles = [oracle1, oracle2, oracle3, oracle4]; + + const { cluster: clusterAfterFirst } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 96, + ); + + await mineBlocks(provider, 50); + + const { cluster: clusterAfterDecrease, tx } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + clusterAfterFirst, clusterId, 64, + ); + + await expect(tx).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfterDecrease.active).to.be.true; + + expect(calcVUnits(64n)).to.equal(20000n); + + const highBurn = calcClusterBurn({ + blockDiff: 100n, numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 30000n, + }); + const lowBurn = calcClusterBurn({ + blockDiff: 100n, numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 20000n, + }); + expect(lowBurn * 30000n).to.equal(highBurn * 20000n); + }); + }); + + describe("Auto-Liquidation on EB Increase", () => { + it("Auto-liquidates cluster when EB increase pushes balance below threshold", async function () { + const SMALL_DEPOSIT = connection.ethers.parseEther("0.025"); + const ctx = await setupClusterWithEB(connection, networkHelpers, SMALL_DEPOSIT); + const { + network, provider, oracle1, oracle2, oracle3, oracle4, + clusterOwner, operatorIds, cluster, clusterId, + } = ctx; + const oracles = [oracle1, oracle2, oracle3, oracle4]; + + expect(cluster.active).to.be.true; + + const { cluster: clusterAfterFirst } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + expect(clusterAfterFirst.active).to.be.true; + + const balanceAfterFirst = BigInt(clusterAfterFirst.balance); + + const thresholdOld = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MINIMAL_LIQUIDATION_THRESHOLD, + numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 20000n, + }); + const thresholdNew = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MINIMAL_LIQUIDATION_THRESHOLD, + numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 40000n, + }); + + expect(balanceAfterFirst).to.be.greaterThan(thresholdNew); + + const burnPerBlock = calcClusterBurn({ + blockDiff: 1n, numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 20000n, + }); + + const targetBalance = (thresholdOld + thresholdNew) / 2n; + const totalBlocksNeeded = (balanceAfterFirst - targetBalance) / burnPerBlock; + const blocksToMine = totalBlocksNeeded - 6n; + await mineBlocks(provider, Number(blocksToMine)); + + const { cluster: clusterAfterLiquidation, tx } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + clusterAfterFirst, clusterId, 128, oracle4, + ); + + await expect(tx).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + await expect(tx).to.emit(network, Events.CLUSTER_LIQUIDATED); + + expect(clusterAfterLiquidation.active).to.be.false; + expect(BigInt(clusterAfterLiquidation.balance)).to.equal(0n); + }); + }); + + describe("Fee Settlement Uses OLD vUnits — No Gap", () => { + it("Settles fees with old vUnits before applying new vUnits", async function () { + const ctx = await setupClusterWithEB(connection, networkHelpers); + const { + network, provider, oracle1, oracle2, oracle3, oracle4, + clusterOwner, operatorIds, cluster, clusterId, + } = ctx; + const oracles = [oracle1, oracle2, oracle3, oracle4]; + + const { cluster: clusterAfterFirst, tx: tx1 } = await performEBUpdate( + connection, network, oracles, provider, clusterOwner, operatorIds, + cluster, clusterId, 64, + ); + + const balanceAfterFirstUpdate = BigInt(clusterAfterFirst.balance); + const receipt1 = await tx1.wait(); + const blockOfFirstUpdate = BigInt(receipt1.blockNumber); + + await mineBlocks(provider, 100); + + const { proof: proof2, rootBlockNum: rootBlockNum2 } = await prepareEBUpdate( + connection, network, oracles, provider, clusterId, 96, + ); + + const tx2 = await network.updateClusterBalance( + rootBlockNum2, clusterOwner.address, operatorIds, clusterAfterFirst, 96, proof2, + ); + + const clusterAfterSecond = await getClusterFromEBUpdateTx(network, tx2); + + const receipt2 = await tx2.wait(); + const blockOfSecondUpdate = BigInt(receipt2!.blockNumber); + const actualBlockDiff = blockOfSecondUpdate - blockOfFirstUpdate; + + const expectedFees = calcClusterBurn({ + blockDiff: actualBlockDiff, + numOperators: 4n, ethFee: OP_ETH_FEE_RAW, + networkFee: PACKED_NETWORK_FEE, effectiveVUnits: 20000n, + }); + + const expectedBalance = balanceAfterFirstUpdate - expectedFees; + const actualBalance = BigInt(clusterAfterSecond.balance); + + expect(actualBalance).to.equal(expectedBalance); + }); + }); +}); diff --git a/test/e2e/effective-balance/oracle-commits.test.ts b/test/e2e/effective-balance/oracle-commits.test.ts new file mode 100644 index 000000000..1754ddebb --- /dev/null +++ b/test/e2e/effective-balance/oracle-commits.test.ts @@ -0,0 +1,330 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { + STAKE_AMOUNT, +} from "../../common/constants.ts"; +import { + mineBlocks, + getBlockNumber, +} from "../../helpers/index.ts"; + +describe("Oracle Commits", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let staker: HardhatEthersSigner; + let nonOracle: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [oracle1, oracle2, oracle3, oracle4, staker, nonOracle] } = await setupTestContext()); + }); + + const deployFixture = async () => { + const { network, views, cssvToken, ssvToken } = + await ssvNetworkFullFixture(connection); + + const provider = connection.ethers.provider; + + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + await network.replaceOracle(4, oracle4.address); + + await ssvToken.transfer(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const cssvSupply = await cssvToken.totalSupply(); + expect(cssvSupply).to.be.greaterThan(0n); + + return { network, views, cssvToken, ssvToken, provider }; + }; + + describe("Single Oracle Commit — Below Quorum", () => { + it("Stores weight but does not commit root when 1 of 4 oracles votes", async function () { + const { network, views, cssvToken, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + const tx = await network.connect(oracle1).commitRoot(rootA, blockNum); + await tx.wait(); + + await expect(tx).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx).to.not.emit(network, Events.ROOT_COMMITTED); + + const cssvSupply = await cssvToken.totalSupply(); + const weight = cssvSupply / 4n; + const threshold = (cssvSupply * 7500n) / 10000n; + + await expect(tx) + .to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootA, blockNum, weight, threshold, 1, oracle1.address); + }); + }); + + describe("Quorum Reached — 3 of 4 Oracles Agree", () => { + it("Commits root when 3 of 4 oracles vote for the same root", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + const tx1 = await network.connect(oracle1).commitRoot(rootA, blockNum); + await expect(tx1).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx1).to.not.emit(network, Events.ROOT_COMMITTED); + + const tx2 = await network.connect(oracle2).commitRoot(rootA, blockNum); + await expect(tx2).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx2).to.not.emit(network, Events.ROOT_COMMITTED); + + const tx3 = await network.connect(oracle3).commitRoot(rootA, blockNum); + await expect(tx3).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx3).to.emit(network, Events.ROOT_COMMITTED).withArgs(rootA, blockNum); + }); + + it("Prevents Oracle4 from voting for same block after quorum (StaleBlockNumber)", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + await network.connect(oracle2).commitRoot(rootA, blockNum); + await network.connect(oracle3).commitRoot(rootA, blockNum); + + await expect( + network.connect(oracle4).commitRoot(rootA, blockNum), + ).to.be.revertedWithCustomError(network, Errors.STALE_BLOCK_NUMBER); + }); + }); + + describe("Conflicting Roots — Separate Weight Tracking", () => { + it("tracks weight separately for different roots at the same block", async function () { + const { network, provider, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + const rootB = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootB")); + + const cssvSupply = await cssvToken.totalSupply(); + const weight = cssvSupply / 4n; + + const tx1 = await network.connect(oracle1).commitRoot(rootA, blockNum); + await expect(tx1) + .to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootA, blockNum, weight, (cssvSupply * 7500n) / 10000n, 1, oracle1.address); + + const tx2 = await network.connect(oracle2).commitRoot(rootB, blockNum); + await expect(tx2) + .to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootB, blockNum, weight, (cssvSupply * 7500n) / 10000n, 2, oracle2.address); + + const tx3 = await network.connect(oracle3).commitRoot(rootA, blockNum); + await expect(tx3).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx3).to.not.emit(network, Events.ROOT_COMMITTED); + + const tx4 = await network.connect(oracle4).commitRoot(rootA, blockNum); + await expect(tx4).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx4).to.emit(network, Events.ROOT_COMMITTED).withArgs(rootA, blockNum); + }); + }); + + describe("ES-4: Oracle Replacement Mid-Vote", () => { + it("Replacement oracle inherits same oracleId and cannot re-vote", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + + await network.replaceOracle(1, nonOracle.address); + + await mineBlocks(provider, 5); + const newBlockNum = await getBlockNumber(provider); + const rootC = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootC")); + await expect( + network.connect(oracle1).commitRoot(rootC, newBlockNum), + ).to.be.revertedWithCustomError(network, Errors.NOT_ORACLE); + + await expect( + network.connect(nonOracle).commitRoot(rootA, blockNum), + ).to.be.revertedWithCustomError(network, Errors.ALREADY_VOTED); + }); + + it("Old vote's weight still counts toward quorum after replacement", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + + await network.replaceOracle(1, nonOracle.address); + + await network.connect(oracle2).commitRoot(rootA, blockNum); + const tx3 = await network.connect(oracle3).commitRoot(rootA, blockNum); + + await expect(tx3).to.emit(network, Events.ROOT_COMMITTED).withArgs(rootA, blockNum); + }); + }); + + describe("Oracle Edge Cases — Reverts", () => { + describe("Stale block number", () => { + it("Reverts when blockNum equals latestCommittedBlock", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + await network.connect(oracle2).commitRoot(rootA, blockNum); + await network.connect(oracle3).commitRoot(rootA, blockNum); + + const rootB = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootB")); + await expect( + network.connect(oracle1).commitRoot(rootB, blockNum), + ).to.be.revertedWithCustomError(network, Errors.STALE_BLOCK_NUMBER); + }); + + it("Reverts when blockNum is less than latestCommittedBlock", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + await network.connect(oracle2).commitRoot(rootA, blockNum); + await network.connect(oracle3).commitRoot(rootA, blockNum); + + const rootC = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootC")); + await expect( + network.connect(oracle1).commitRoot(rootC, blockNum - 1), + ).to.be.revertedWithCustomError(network, Errors.STALE_BLOCK_NUMBER); + }); + + it("Succeeds when blockNum is greater than latestCommittedBlock", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + await network.connect(oracle2).commitRoot(rootA, blockNum); + await network.connect(oracle3).commitRoot(rootA, blockNum); + + await mineBlocks(provider, 5); + const newBlockNum = await getBlockNumber(provider); + const rootB = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootB")); + + await expect( + network.connect(oracle1).commitRoot(rootB, newBlockNum), + ).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + }); + }); + + describe("Future block number", () => { + it("Reverts when blockNum > block.number", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await expect( + network.connect(oracle1).commitRoot(rootA, blockNum + 100), + ).to.be.revertedWithCustomError(network, Errors.FUTURE_BLOCK_NUMBER); + }); + + it("Succeeds when blockNum == block.number (equality OK)", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + await mineBlocks(provider, 5); + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await expect( + network.connect(oracle1).commitRoot(rootA, blockNum), + ).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + }); + }); + + describe("Zero cSSV supply", () => { + it("Reverts when cSSV totalSupply is 0", async function () { + const { network } = await ssvNetworkFullFixture(connection); + + await network.replaceOracle(1, oracle1.address); + + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await expect( + network.connect(oracle1).commitRoot(rootA, 1), + ).to.be.revertedWithCustomError(network, Errors.ZERO_CSSV_SUPPLY); + }); + }); + + describe("Double vote", () => { + it("Reverts when same oracle votes twice for same (root, blockNum)", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + + await expect( + network.connect(oracle1).commitRoot(rootA, blockNum), + ).to.be.revertedWithCustomError(network, Errors.ALREADY_VOTED); + }); + + it("Allows same oracle to vote for different root at same block", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + const rootB = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootB")); + + await network.connect(oracle1).commitRoot(rootA, blockNum); + + await expect( + network.connect(oracle1).commitRoot(rootB, blockNum), + ).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + }); + + it("Reverts when non-oracle calls commitRoot", async function () { + const { network, provider } = + await networkHelpers.loadFixture(deployFixture); + + const blockNum = await getBlockNumber(provider); + const rootA = connection.ethers.keccak256(connection.ethers.toUtf8Bytes("rootA")); + + await expect( + network.connect(nonOracle).commitRoot(rootA, blockNum), + ).to.be.revertedWithCustomError(network, Errors.NOT_ORACLE); + }); + }); + }); +}); diff --git a/test/e2e/effective-balance/vunits-explicit-eb-scenarios.test.ts b/test/e2e/effective-balance/vunits-explicit-eb-scenarios.test.ts new file mode 100644 index 000000000..f9f835e7a --- /dev/null +++ b/test/e2e/effective-balance/vunits-explicit-eb-scenarios.test.ts @@ -0,0 +1,603 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { Cluster, NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + parseClusterFromEvent, + registerOperators, + setupTestContext, + whitelistAddresses, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_NETWORK_FEE_RAW, + DEFAULT_NETWORK_FEE_UNPACKED, + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, + MINIMAL_LIQUIDATION_THRESHOLD, + OP_ETH_FEE_RAW, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { + calcClusterBurn, + calcLiquidationThreshold, + calcOperatorFeeAccrual, + calcVUnits, + commitEBRoot, + computeClusterId, + computeEBRoot, + defaultVUnits, + getBlockNumber, + mineBlocks, + setupOracles, +} from "../../helpers/index.ts"; + +const NUM_OPERATORS = 4n; +const LIQUIDATION_THRESHOLD_PERIOD = MINIMAL_LIQUIDATION_THRESHOLD; + +describe("Explicit EB vUnits scenarios", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let staker: HardhatEthersSigner; + + before(async function () { + ({ + connection, + networkHelpers, + signers: [operatorOwner, clusterOwner, oracle1, oracle2, oracle3, oracle4, staker], + } = await setupTestContext()); + }); + + const deployFixture = async () => { + const { network, views, ssvToken } = await ssvNetworkFullFixture(connection); + + await network.updateNetworkFee(DEFAULT_NETWORK_FEE_UNPACKED); + await network.updateLiquidationThresholdPeriod(LIQUIDATION_THRESHOLD_PERIOD); + await network.updateMinimumLiquidationCollateral(0n); + + await setupOracles(network, ssvToken, staker, [oracle1, oracle2, oracle3, oracle4]); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + return { network, views, operatorIds }; + }; + + async function registerClusterValidators( + network: any, + operatorIds: number[], + validatorCount: number, + depositPerValidator: bigint = DEFAULT_ETH_REGISTER_VALUE, + ): Promise { + let cluster = EMPTY_CLUSTER; + + for (let i = 1; i <= validatorCount; i++) { + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(i), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: depositPerValidator }, + ); + const receipt = await tx.wait(); + cluster = parseClusterFromEvent(network, receipt, Events.VALIDATOR_ADDED); + } + + return cluster; + } + + async function updateClusterEB( + network: any, + operatorIds: number[], + cluster: Cluster, + effectiveBalance: number, + ): Promise<{ cluster: Cluster; blockNumber: bigint }> { + const provider = connection.ethers.provider; + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root = computeEBRoot(clusterId, effectiveBalance); + + await mineBlocks(provider, 1); + const rootBlockNum = await getBlockNumber(provider); + await commitEBRoot(network, root, rootBlockNum, [oracle1, oracle2, oracle3]); + + const tx = await network.connect(clusterOwner).updateClusterBalance( + rootBlockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [], + ); + const receipt = await tx.wait(); + + return { + cluster: parseClusterFromEvent(network, receipt, Events.CLUSTER_BALANCE_UPDATED), + blockNumber: BigInt(receipt.blockNumber), + }; + } + + it("E-04: higher explicit-EB burn rate is applied after an EB=64 update", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const { cluster: clusterAfterEB64 } = await updateClusterEB(network, operatorIds, cluster, 64); + + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, clusterAfterEB64)).to.equal(64); + + const opEarningsAfterEB64 = await views.getOperatorEarnings(BigInt(operatorIds[0])); + const blocksToMine = 40; + await mineBlocks(provider, blocksToMine); + + const expectedFeesAtEB64 = calcClusterBurn({ + blockDiff: BigInt(blocksToMine), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(64n), + }); + const expectedBalance = clusterAfterEB64.balance - expectedFeesAtEB64; + + expect( + await views.getBalance(clusterOwner.address, operatorIds, clusterAfterEB64), + ).to.equal(expectedBalance); + expect(expectedFeesAtEB64).to.equal(calcClusterBurn({ + blockDiff: BigInt(blocksToMine), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }) * 2n); + expect(await views.getOperatorEarnings(BigInt(operatorIds[0])) - opEarningsAfterEB64).to.equal( + calcOperatorFeeAccrual(BigInt(blocksToMine), OP_ETH_FEE_RAW, calcVUnits(64n)) * ETH_DEDUCTED_DIGITS, + ); + }); + + it("E-06: EB=64 -> EB=128 settles at the old rate and then accrues at the higher rate", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const { cluster: clusterAfterEB64, blockNumber: firstUpdateBlock } = + await updateClusterEB(network, operatorIds, cluster, 64); + + const opEarningsAfterEB64 = await views.getOperatorEarnings(BigInt(operatorIds[0])); + + await mineBlocks(provider, 17); + + const { cluster: clusterAfterEB128, blockNumber: secondUpdateBlock } = + await updateClusterEB(network, operatorIds, clusterAfterEB64, 128); + + const opEarningsAfterEB128 = await views.getOperatorEarnings(BigInt(operatorIds[0])); + expect(opEarningsAfterEB128 - opEarningsAfterEB64).to.equal( + calcOperatorFeeAccrual(secondUpdateBlock - firstUpdateBlock, OP_ETH_FEE_RAW, calcVUnits(64n)) * ETH_DEDUCTED_DIGITS, + ); + + const expectedBalanceAtEB128 = clusterAfterEB64.balance - calcClusterBurn({ + blockDiff: secondUpdateBlock - firstUpdateBlock, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(64n), + }); + + expect(clusterAfterEB128.balance).to.equal(expectedBalanceAtEB128); + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, clusterAfterEB128)).to.equal(128); + + const postUpdateBlocks = 11; + await mineBlocks(provider, postUpdateBlocks); + + const expectedPostUpdateBalance = clusterAfterEB128.balance - calcClusterBurn({ + blockDiff: BigInt(postUpdateBlocks), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(128n), + }); + + expect( + await views.getBalance(clusterOwner.address, operatorIds, clusterAfterEB128), + ).to.equal(expectedPostUpdateBalance); + expect(await views.getOperatorEarnings(BigInt(operatorIds[0])) - opEarningsAfterEB128).to.equal( + calcOperatorFeeAccrual(BigInt(postUpdateBlocks), OP_ETH_FEE_RAW, calcVUnits(128n)) * ETH_DEDUCTED_DIGITS, + ); + }); + + it("E-08: explicit EB=32 -> EB=64 settles at baseline and then accrues at the higher rate", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const { cluster: clusterAfterEB32, blockNumber: firstUpdateBlock } = + await updateClusterEB(network, operatorIds, cluster, 32); + + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, clusterAfterEB32)).to.equal(32); + + await mineBlocks(provider, 13); + + const { cluster: clusterAfterEB64, blockNumber: secondUpdateBlock } = + await updateClusterEB(network, operatorIds, clusterAfterEB32, 64); + + const opEarningsAfterEB64 = await views.getOperatorEarnings(BigInt(operatorIds[0])); + + const expectedBalanceAtEB64 = clusterAfterEB32.balance - calcClusterBurn({ + blockDiff: secondUpdateBlock - firstUpdateBlock, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + expect(clusterAfterEB64.balance).to.equal(expectedBalanceAtEB64); + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, clusterAfterEB64)).to.equal(64); + + const postUpdateBlocks = 9; + await mineBlocks(provider, postUpdateBlocks); + + const expectedPostUpdateBalance = clusterAfterEB64.balance - calcClusterBurn({ + blockDiff: BigInt(postUpdateBlocks), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(64n), + }); + + expect( + await views.getBalance(clusterOwner.address, operatorIds, clusterAfterEB64), + ).to.equal(expectedPostUpdateBalance); + expect(await views.getOperatorEarnings(BigInt(operatorIds[0])) - opEarningsAfterEB64).to.equal( + calcOperatorFeeAccrual(BigInt(postUpdateBlocks), OP_ETH_FEE_RAW, calcVUnits(64n)) * ETH_DEDUCTED_DIGITS, + ); + }); + + it("E-09: 3-validator cluster updated to total EB=96 keeps baseline vUnits and burn rate", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 3); + const { cluster: clusterAfterEB96 } = await updateClusterEB(network, operatorIds, cluster, 96); + + const opEarningsAfterEB96 = await views.getOperatorEarnings(BigInt(operatorIds[0])); + expect(calcVUnits(96n)).to.equal(defaultVUnits(3n)); + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, clusterAfterEB96)).to.equal(96); + + const blocksToMine = 21; + await mineBlocks(provider, blocksToMine); + + const expectedBalance = clusterAfterEB96.balance - calcClusterBurn({ + blockDiff: BigInt(blocksToMine), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(3n), + }); + + expect( + await views.getBalance(clusterOwner.address, operatorIds, clusterAfterEB96), + ).to.equal(expectedBalance); + expect(await views.getOperatorEarnings(BigInt(operatorIds[0])) - opEarningsAfterEB96).to.equal( + calcOperatorFeeAccrual(BigInt(blocksToMine), OP_ETH_FEE_RAW, defaultVUnits(3n)) * ETH_DEDUCTED_DIGITS, + ); + }); + + it("E-11: registering a second validator after EB=64 adds one baseline validator worth of vUnits", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const { cluster: clusterAfterEB64 } = await updateClusterEB(network, operatorIds, cluster, 64); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterEB64, + { value: 0n }, + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterSecondValidator = parseClusterFromEvent( + network, + registerReceipt, + Events.VALIDATOR_ADDED, + ); + + expect(clusterAfterSecondValidator.validatorCount).to.equal(2n); + expect(calcVUnits(64n) + defaultVUnits(1n)).to.equal(calcVUnits(96n)); + expect( + await views.getEffectiveBalance(clusterOwner.address, operatorIds, clusterAfterSecondValidator), + ).to.equal(96); + + const opEarningsAfterSecondReg = await views.getOperatorEarnings(BigInt(operatorIds[0])); + const blocksToMine = 14; + await mineBlocks(provider, blocksToMine); + + const expectedBalance = clusterAfterSecondValidator.balance - calcClusterBurn({ + blockDiff: BigInt(blocksToMine), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(96n), + }); + + expect( + await views.getBalance(clusterOwner.address, operatorIds, clusterAfterSecondValidator), + ).to.equal(expectedBalance); + expect(await views.getOperatorEarnings(BigInt(operatorIds[0])) - opEarningsAfterSecondReg).to.equal( + calcOperatorFeeAccrual(BigInt(blocksToMine), OP_ETH_FEE_RAW, calcVUnits(96n)) * ETH_DEDUCTED_DIGITS, + ); + }); + + it("E-13: withdrawing the maximum allowed amount at explicit EB=64 leaves the cluster exactly at the liquidation boundary", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const { cluster: clusterAfterEB64 } = await updateClusterEB(network, operatorIds, cluster, 64); + + const burnForNextBlock = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(64n), + }); + const thresholdAtEB64 = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: LIQUIDATION_THRESHOLD_PERIOD, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(64n), + }); + const maxWithdrawAmount = clusterAfterEB64.balance - burnForNextBlock - thresholdAtEB64; + + expect(maxWithdrawAmount).to.be.greaterThan(0n); + + const withdrawTx = await network.connect(clusterOwner).withdraw( + operatorIds, + maxWithdrawAmount, + clusterAfterEB64, + ); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent( + network, + withdrawReceipt, + Events.CLUSTER_WITHDRAWN, + ); + + expect(clusterAfterWithdraw.balance).to.equal(thresholdAtEB64); + expect(await views.isLiquidatable(clusterOwner.address, operatorIds, clusterAfterWithdraw)).to.equal(false); + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, 1n, clusterAfterWithdraw), + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("E-14: depositing into an explicit-EB=64 cluster preserves its effective balance and burn rate", async function () { + const { network, views, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const { cluster: clusterAfterEB64, blockNumber: ebUpdateBlock } = + await updateClusterEB(network, operatorIds, cluster, 64); + + const opEarningsAfterEB64 = await views.getOperatorEarnings(BigInt(operatorIds[0])); + + const depositAmount = connection.ethers.parseEther("1"); + const depositTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + clusterAfterEB64, + { value: depositAmount }, + ); + const depositReceipt = await depositTx.wait(); + const clusterAfterDeposit = parseClusterFromEvent(network, depositReceipt, Events.CLUSTER_DEPOSITED); + + expect(clusterAfterDeposit.balance).to.equal(clusterAfterEB64.balance + depositAmount); + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, clusterAfterDeposit)).to.equal(64); + + const blocksToMine = 12; + await mineBlocks(provider, blocksToMine); + const currentBlock = BigInt(await getBlockNumber(provider)); + + const expectedBalance = clusterAfterDeposit.balance - calcClusterBurn({ + blockDiff: currentBlock - ebUpdateBlock, + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: calcVUnits(64n), + }); + + expect( + await views.getBalance(clusterOwner.address, operatorIds, clusterAfterDeposit), + ).to.equal(expectedBalance); + expect(await views.getOperatorEarnings(BigInt(operatorIds[0])) - opEarningsAfterEB64).to.equal( + calcOperatorFeeAccrual(currentBlock - ebUpdateBlock, OP_ETH_FEE_RAW, calcVUnits(64n)) * ETH_DEDUCTED_DIGITS, + ); + }); + + it("R-09: register validator (EB=64) → remove operator → deposit", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const { cluster: clusterAfterEB64 } = await updateClusterEB(network, operatorIds, cluster, 64); + + const removedOperator = operatorIds[0]; + await network.connect(operatorOwner).removeOperator(removedOperator); + + const depositAmount = connection.ethers.parseEther("1"); + const depositTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + clusterAfterEB64, + { value: depositAmount }, + ); + const depositReceipt = await depositTx.wait(); + const clusterAfterDeposit = parseClusterFromEvent(network, depositReceipt, Events.CLUSTER_DEPOSITED); + + expect(clusterAfterDeposit.balance).to.equal(clusterAfterEB64.balance + depositAmount); + }); + + it("RI-02: register validator → remove operator → remove last validator", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const regBlock = await getBlockNumber(provider); + + const removedOperator = operatorIds[0]; + const removeOpReceipt = await (await network.connect(operatorOwner).removeOperator(removedOperator)).wait(); + const removeOpBlock = BigInt(removeOpReceipt!.blockNumber); + + await mineBlocks(provider, 10); + + const removeTx = await network.connect(clusterOwner).removeValidator( + makePublicKey(1), + operatorIds, + cluster + ); + const removeReceipt = await removeTx.wait(); + const removeBlock = BigInt(removeReceipt.blockNumber); + const clusterAfterRemove = parseClusterFromEvent(network, removeReceipt, Events.VALIDATOR_REMOVED); + + // Fees are two-phase: full 4-op rate before removal, 3-op rate after + const expectedFeeDeduction = + calcClusterBurn({ + blockDiff: removeOpBlock - BigInt(regBlock), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }) + + calcClusterBurn({ + blockDiff: removeBlock - removeOpBlock, + numOperators: NUM_OPERATORS - 1n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.be.true; + expect(clusterAfterRemove.balance).to.equal(cluster.balance - expectedFeeDeduction); + }); + + it("RI-03: register validator → remove operator → withdraw", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const regBlock = await getBlockNumber(provider); + + const removedOperator = operatorIds[0]; + const removeOpReceipt2 = await (await network.connect(operatorOwner).removeOperator(removedOperator)).wait(); + const removeOpBlock2 = BigInt(removeOpReceipt2!.blockNumber); + + await mineBlocks(provider, 10); + + const withdrawAmount = connection.ethers.parseEther("0.1"); + const withdrawTx = await network.connect(clusterOwner).withdraw( + operatorIds, + withdrawAmount, + cluster + ); + const withdrawReceipt = await withdrawTx.wait(); + const withdrawBlock = BigInt(withdrawReceipt.blockNumber); + + const clusterAfterWithdraw = parseClusterFromEvent(network, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + // Fees are two-phase: full 4-op rate before removal, 3-op rate after + const expectedFeeDeduction = + calcClusterBurn({ + blockDiff: removeOpBlock2 - BigInt(regBlock), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }) + + calcClusterBurn({ + blockDiff: withdrawBlock - removeOpBlock2, + numOperators: NUM_OPERATORS - 1n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + expect(clusterAfterWithdraw.balance).to.equal(cluster.balance - expectedFeeDeduction - withdrawAmount); + }); + + it("RI-05: register validator → remove operator → reactivate (if liquidated)", async function () { + const { network, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const cluster = await registerClusterValidators(network, operatorIds, 1); + const regBlock = await getBlockNumber(provider); + + const removedOperator = operatorIds[0]; + const removeOpReceipt = await (await network.connect(operatorOwner).removeOperator(removedOperator)).wait(); + const removeOpBlock = BigInt(removeOpReceipt!.blockNumber); + + // Phase 1: 4-op rate from registration to operator removal + const phase1Fees = calcClusterBurn({ + blockDiff: removeOpBlock - BigInt(regBlock), + numOperators: NUM_OPERATORS, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + // After removal, burn rate and threshold use 3 active operators + const threshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: LIQUIDATION_THRESHOLD_PERIOD, + numOperators: NUM_OPERATORS - 1n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + const burnPerBlockAfterRemoval = calcClusterBurn({ + blockDiff: 1n, + numOperators: NUM_OPERATORS - 1n, + ethFee: OP_ETH_FEE_RAW, + networkFee: DEFAULT_NETWORK_FEE_RAW, + effectiveVUnits: defaultVUnits(1n), + }); + + // Remaining balance after phase-1 fees, then drain at 3-op rate + const balanceAfterPhase1 = cluster.balance - phase1Fees; + const blocksToLiquidate = (balanceAfterPhase1 - threshold) / burnPerBlockAfterRemoval + 1n; + await mineBlocks(provider, Number(blocksToLiquidate)); + + const liquidator = oracle1; + const liquidateTx = await network.connect(liquidator).liquidate( + clusterOwner.address, + operatorIds, + cluster + ); + const liquidateReceipt = await liquidateTx.wait(); + const clusterAfterLiquidate = parseClusterFromEvent(network, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + expect(clusterAfterLiquidate.active).to.be.false; + + const reactivateAmount = threshold + connection.ethers.parseEther("1"); + const reactivateTx = await network.connect(clusterOwner).reactivate( + operatorIds, + clusterAfterLiquidate, + { value: reactivateAmount } + ); + const reactivateReceipt = await reactivateTx.wait(); + const clusterAfterReactivate = parseClusterFromEvent(network, reactivateReceipt, Events.CLUSTER_REACTIVATED); + + expect(clusterAfterReactivate.active).to.be.true; + expect(clusterAfterReactivate.balance).to.equal(clusterAfterLiquidate.balance + reactivateAmount); + }); +}); diff --git a/test/e2e/migration/migration-basic.test.ts b/test/e2e/migration/migration-basic.test.ts new file mode 100644 index 000000000..31022bec2 --- /dev/null +++ b/test/e2e/migration/migration-basic.test.ts @@ -0,0 +1,391 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullPreUpgradeFixture, upgradeToStakingVersion } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + getCurrentClusterState, + extractEventArgs, + parseClusterFromEvent, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEFAULT_ETH_REGISTER_VALUE, + EMPTY_CLUSTER, + TOKEN_REGISTER_AMOUNT, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { + mineBlocks, +} from "../../helpers/index.ts"; +import { makeOperatorKey } from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +const OP_SSV_FEE_UNPACKED = 10_000_000_000n; + +describe("Migration SSV → ETH", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + describe("Basic Migration With SSV Refund", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvDeposit = TOKEN_REGISTER_AMOUNT; + await ssvToken.mint(clusterOwner.address, ssvDeposit * 2n); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvDeposit * 2n, + ); + + const halfDeposit = ssvDeposit; + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, halfDeposit, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, halfDeposit, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(2n); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + it("Migrates SSV cluster to ETH with correct SSV refund and ETH deposit", async function () { + const { network, views, ssvToken, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 100); + + const ssvBalanceBefore = await views.getBalanceSSV( + clusterOwner.address, operatorIds, cluster, + ); + const burnRate = await views.getBurnRateSSV( + clusterOwner.address, operatorIds, cluster, + ); + + const ownerSSVBefore = await ssvToken.balanceOf(clusterOwner.address); + + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const receipt = await migrateTx.wait(); + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + const eventArgs = extractEventArgs(network, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(eventArgs.ethDeposited).to.equal(DEFAULT_ETH_REGISTER_VALUE); + + const ownerSSVAfter = await ssvToken.balanceOf(clusterOwner.address); + const ssvRefund = ownerSSVAfter - ownerSSVBefore; + expect(ssvRefund).to.equal(eventArgs.ssvRefunded); + + const expectedRefund = ssvBalanceBefore - burnRate; + expect(ssvRefund).to.equal(expectedRefund); + + const clusterAfter = parseClusterFromEvent(network, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfter.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(clusterAfter.active).to.equal(true); + expect(clusterAfter.validatorCount).to.equal(2n); + expect(clusterAfter.index).to.equal(0n); + + for (const opId of operatorIds) { + const opSSV = await views.getOperatorByIdSSV(opId); + expect(opSSV.validatorCount).to.equal(0); + const opETH = await views.getOperatorById(opId); + expect(opETH.validatorCount).to.equal(2); + } + + expect(await views.getNetworkValidatorsCount()).to.equal(2); + + await expect(migrateTx).to.not.emit(network, Events.CLUSTER_REACTIVATED); + }); + + it("Migration with insufficient ETH reverts (edge)", async function () { + const { network, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + await expect( + network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, { value: 0n }, + ), + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Migration of Liquidated SSV Cluster", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + await ssvToken.mint(clusterOwner.address, TOKEN_REGISTER_AMOUNT); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), TOKEN_REGISTER_AMOUNT, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).liquidate( + clusterOwner.address, operatorIds, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + expect(cluster.active).to.equal(false); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + it("Migrates liquidated SSV cluster — no SSV refund, emits ClusterReactivated", async function () { + const { network, views, ssvToken, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + const ownerSSVBefore = await ssvToken.balanceOf(clusterOwner.address); + + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const receipt = await migrateTx.wait(); + const eventArgs = extractEventArgs(network, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + const ownerSSVAfter = await ssvToken.balanceOf(clusterOwner.address); + expect(ownerSSVAfter - ownerSSVBefore).to.equal(0n); + expect(eventArgs.ssvRefunded).to.equal(0n); + + await expect(migrateTx).to.emit(network, Events.CLUSTER_REACTIVATED); + + const clusterAfter = parseClusterFromEvent(network, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfter.active).to.equal(true); + expect(clusterAfter.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(await views.getNetworkValidatorsCount()).to.equal(1); + }); + }); + + describe("Migration With Mixed Operator ETH State", () => { + const deployFixtureMixed = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + await ssvToken.mint(clusterOwner.address, TOKEN_REGISTER_AMOUNT); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), TOKEN_REGISTER_AMOUNT, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + const cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + it("Operators with different ETH fees produce correct cumulative index after migration", async function () { + const { network, views, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixtureMixed); + const provider = connection.ethers.provider; + + const fees = [2_000_000_000n, 3_000_000_000n, 2_500_000_000n]; + for (let i = 0; i < 3; i++) { + await network.connect(clusterOwner).declareOperatorFee( + BigInt(operatorIds[i]), fees[i], + ); + } + + await provider.send("evm_increaseTime", [604800]); + await mineBlocks(provider, 1); + + for (let i = 0; i < 3; i++) { + await network.connect(clusterOwner).executeOperatorFee(BigInt(operatorIds[i])); + } + + await mineBlocks(provider, 200); + + const ethDeposit = ethers.parseEther("5"); + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: ethDeposit }, + ); + await migrateTx.wait(); + + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + for (const opId of operatorIds) { + const op = await views.getOperatorById(opId); + expect(op.validatorCount).to.equal(1); + } + + for (const opId of operatorIds) { + const opSSV = await views.getOperatorByIdSSV(opId); + expect(opSSV.validatorCount).to.equal(0); + } + + expect(await views.getNetworkValidatorsCount()).to.equal(1); + }); + + it("Migration succeeds with default ETH fees (auto-assigned on migration)", async function () { + const { network, views, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixtureMixed); + + const ethDeposit = ethers.parseEther("10"); + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: ethDeposit }, + ); + + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + const receipt = await migrateTx.wait(); + const clusterAfter = parseClusterFromEvent(network, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfter.balance).to.equal(ethDeposit); + expect(clusterAfter.validatorCount).to.equal(1n); + + for (const opId of operatorIds) { + const op = await views.getOperatorById(opId); + expect(op.validatorCount).to.equal(1); + } + + expect(await views.getNetworkValidatorsCount()).to.equal(1); + }); + }); + + describe("Post-Migration ETH Fee Accrual", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + await ssvToken.mint(clusterOwner.address, TOKEN_REGISTER_AMOUNT * 2n); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), TOKEN_REGISTER_AMOUNT * 2n, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + it("ETH fees accrue correctly after migration, not SSV fees", async function () { + const { network, views, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const migrateReceipt = await migrateTx.wait(); + const migrateBlock = migrateReceipt!.blockNumber; + let migratedCluster = parseClusterFromEvent(network, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + + await mineBlocks(provider, 50); + + const balanceBeforeReg = await views.getBalance( + clusterOwner.address, operatorIds, migratedCluster, + ); + expect(balanceBeforeReg).to.be.lessThan(DEFAULT_ETH_REGISTER_VALUE); + expect(balanceBeforeReg).to.be.greaterThan(0n); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(3), operatorIds, DEFAULT_SHARES, migratedCluster, + { value: 0n }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(network, regReceipt, Events.VALIDATOR_ADDED); + + expect(clusterAfterReg.validatorCount).to.equal(3n); + + expect(BigInt(clusterAfterReg.balance)).to.be.lessThan(DEFAULT_ETH_REGISTER_VALUE); + expect(BigInt(clusterAfterReg.balance)).to.be.greaterThan(0n); + + for (const opId of operatorIds) { + const opSSV = await views.getOperatorByIdSSV(opId); + expect(opSSV.validatorCount).to.equal(0); + const opETH = await views.getOperatorById(opId); + expect(opETH.validatorCount).to.equal(3); + } + }); + }); +}); diff --git a/test/e2e/migration/migration-double-payment.test.ts b/test/e2e/migration/migration-double-payment.test.ts new file mode 100644 index 000000000..5d1ed1884 --- /dev/null +++ b/test/e2e/migration/migration-double-payment.test.ts @@ -0,0 +1,632 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { getTestConnection } from "../../setup/connection.ts"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType, Cluster } from "../../common/types.ts"; +import { createCluster, makePublicKey, parseClusterFromEvent } from "../../common/helpers.ts"; +import { + DEDUCTED_DIGITS, + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + DEFAULT_OPERATOR_ETH_FEE, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { mineBlocks } from "../../helpers/blocks.ts"; + +const HIGH_SSV_FEE_RAW = 1_000n; +const MEDIUM_SSV_FEE_RAW = 500n; +const NETWORK_FEE_SSV_RAW = 100n; +const NETWORK_FEE_ETH_RAW = 1_770n; +const MIN_BLOCKS_LIQ = 10n; +const MIN_LIQ_COLLATERAL_RAW = 0n; + +const getMigratedToETHEventArgs = (contract: any, receipt: any) => { + for (const log of receipt.logs ?? []) { + let parsed; + try { + parsed = contract.interface.parseLog(log); + } catch { + continue; + } + if (parsed?.name === Events.CLUSTER_MIGRATED_TO_ETH) { + return parsed.args; + } + } + throw new Error("ClusterMigratedToETH event not found"); +}; + +const getSnapshotIndexAtBlock = async ( + clusters: any, + operatorId: bigint, + targetBlock: bigint +): Promise => { + const snapshot = await clusters.getOperatorSnapshot(operatorId); + const feeRaw = BigInt(await clusters.getOperatorSSVFee(operatorId)); + return BigInt(snapshot.index) + (targetBlock - BigInt(snapshot.blockNumber)) * feeRaw; +}; + +describe("Migration Regression: removed operator SSV settlement", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let ethClusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [clusterOwner, ethClusterOwner] = await connection.ethers.getSigners(); + }); + + const deployFixture = async () => { + const { clusters, operatorIds } = await ssvClustersHarnessFixture(connection, 4, 0n); + + await clusters.mockOperatorSSVFee(operatorIds[0], HIGH_SSV_FEE_RAW * DEDUCTED_DIGITS); + for (let i = 1; i < operatorIds.length; i++) { + await clusters.mockOperatorSSVFee(operatorIds[i], MEDIUM_SSV_FEE_RAW * DEDUCTED_DIGITS); + } + + await clusters.mockSSVNetworkFee(NETWORK_FEE_SSV_RAW); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + await clusters.mockEthNetworkFee(NETWORK_FEE_ETH_RAW); + await clusters.mockMinimumBlocksBeforeLiquidation(MIN_BLOCKS_LIQ); + await clusters.mockMinimumLiquidationCollateral(MIN_LIQ_COLLATERAL_RAW); + await clusters.mockMinimumBlocksBeforeLiquidationSSV(MIN_BLOCKS_LIQ); + await clusters.mockMinimumLiquidationCollateralSSV(MIN_LIQ_COLLATERAL_RAW); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + await clusters.mockSetToken(await mockToken.getAddress()); + const harnessAddress = await clusters.getAddress(); + await mockToken.mint(harnessAddress, connection.ethers.parseEther("2000")); + + return { clusters, operatorIds, mockToken }; + }; + + it("Baseline: all operators active uses exact SSV refund formula", async function () { + const { clusters, operatorIds, mockToken } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const validatorCount = 2n; + const ssvBalance = ethers.parseEther("500"); + const ssvCluster = createCluster({ + validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: ssvBalance, + active: true, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(1), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + await mineBlocks(provider, 300); + + const migrationBlockExpected = BigInt(await provider.getBlockNumber()) + 1n; + let cumulativeIndex = 0n; + for (const operatorId of operatorIds) { + cumulativeIndex += await getSnapshotIndexAtBlock(clusters, operatorId, migrationBlockExpected); + } + + const networkFeeIndexBefore = BigInt(await clusters.getCurrentNetworkFeeIndexSSV()); + const readBlock = BigInt(await provider.getBlockNumber()); + const ownerBefore = await mockToken.balanceOf(clusterOwner.address); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = getMigratedToETHEventArgs(clusters, receipt); + + const migrationBlock = BigInt(receipt!.blockNumber); + expect(migrationBlock).to.equal(migrationBlockExpected); + + const expectedNetworkFeeIndex = + networkFeeIndexBefore + (migrationBlock - readBlock) * NETWORK_FEE_SSV_RAW; + const operatorUsagePacked = (cumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + const totalUsageWei = (operatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS; + const expectedRefund = ssvBalance > totalUsageWei ? ssvBalance - totalUsageWei : 0n; + + const ownerAfter = await mockToken.balanceOf(clusterOwner.address); + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + expect(ownerAfter - ownerBefore).to.equal(expectedRefund); + }); + + it("Includes removed operator frozen snapshot.index in migration SSV settlement", async function () { + const { clusters, operatorIds, mockToken } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const validatorCount = 2n; + const ssvBalance = ethers.parseEther("500"); + const ssvCluster = createCluster({ + validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: ssvBalance, + active: true, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(2), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + await mineBlocks(provider, 400); + + const removedOperatorId = operatorIds[0]; + const removeBlockExpected = BigInt(await provider.getBlockNumber()) + 1n; + const removedSnapshotBefore = await clusters.getOperatorSnapshot(removedOperatorId); + const removedFeeRaw = BigInt(await clusters.getOperatorSSVFee(removedOperatorId)); + const removedIndexAtRemoval = BigInt(removedSnapshotBefore.index) + + (removeBlockExpected - BigInt(removedSnapshotBefore.blockNumber)) * removedFeeRaw; + + const removeTx = await (clusters as any).mockRemoveOperatorAndPayout(removedOperatorId, clusterOwner.address); + const removeReceipt = await removeTx.wait(); + expect(BigInt(removeReceipt!.blockNumber)).to.equal(removeBlockExpected); + + const removedSnapshotAfter = await clusters.getOperatorSnapshot(removedOperatorId); + expect(BigInt(removedSnapshotAfter.blockNumber)).to.equal(0n); + expect(BigInt(removedSnapshotAfter.index)).to.equal(removedIndexAtRemoval); + + await mineBlocks(provider, 200); + + const migrationBlockExpected = BigInt(await provider.getBlockNumber()) + 1n; + let liveOperatorsCumulativeIndex = 0n; + for (let i = 1; i < operatorIds.length; i++) { + liveOperatorsCumulativeIndex += await getSnapshotIndexAtBlock(clusters, operatorIds[i], migrationBlockExpected); + } + + const networkFeeIndexBefore = BigInt(await clusters.getCurrentNetworkFeeIndexSSV()); + const readBlock = BigInt(await provider.getBlockNumber()); + const ownerBefore = await mockToken.balanceOf(clusterOwner.address); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = getMigratedToETHEventArgs(clusters, receipt); + const migrationBlock = BigInt(receipt!.blockNumber); + expect(migrationBlock).to.equal(migrationBlockExpected); + + const expectedNetworkFeeIndex = + networkFeeIndexBefore + (migrationBlock - readBlock) * NETWORK_FEE_SSV_RAW; + + const correctCumulativeIndex = removedIndexAtRemoval + liveOperatorsCumulativeIndex; + const buggyCumulativeIndex = liveOperatorsCumulativeIndex; + + const correctOperatorUsagePacked = (correctCumulativeIndex - ssvCluster.index) * validatorCount; + const buggyOperatorUsagePacked = (buggyCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + + const correctRefund = ssvBalance > (correctOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + ? ssvBalance - (correctOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + : 0n; + const buggyRefund = ssvBalance > (buggyOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + ? ssvBalance - (buggyOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + : 0n; + + const missingFeesWei = removedIndexAtRemoval * validatorCount * DEDUCTED_DIGITS; + expect(buggyRefund - correctRefund).to.equal(missingFeesWei); + + const ownerAfter = await mockToken.balanceOf(clusterOwner.address); + expect(eventArgs.ssvRefunded).to.equal(correctRefund); + expect(eventArgs.ssvRefunded).to.not.equal(buggyRefund); + expect(ownerAfter - ownerBefore).to.equal(correctRefund); + + expect(await clusters.getOperatorEthValidatorCount(removedOperatorId)).to.equal(0n); + expect(BigInt((await clusters.getOperatorEthSnapshot(removedOperatorId)).blockNumber)).to.equal(0n); + for (let i = 1; i < operatorIds.length; i++) { + expect(await clusters.getOperatorEthValidatorCount(operatorIds[i])).to.equal(validatorCount); + } + }); + + it("Liquidated cluster migration with removed operator preserves SSV counts and skips removed ETH setup", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const validatorCount = 3n; + const ssvCluster: Cluster = createCluster({ + validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(3), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + const removedOperatorId = operatorIds[0]; + await (clusters as any).mockRemoveOperatorAndPayout(removedOperatorId, clusterOwner.address); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, ssvCluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.equal(false); + + const beforeMigrationCounts = []; + for (const operatorId of operatorIds) { + beforeMigrationCounts.push({ + ssv: await clusters.getOperatorValidatorCount(operatorId), + eth: await clusters.getOperatorEthValidatorCount(operatorId), + }); + } + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const migrateReceipt = await migrateTx.wait(); + const eventArgs = getMigratedToETHEventArgs(clusters, migrateReceipt); + + await expect(migrateTx).to.emit(clusters, Events.CLUSTER_REACTIVATED); + expect(eventArgs.ssvRefunded).to.equal(0n); + + for (let i = 0; i < operatorIds.length; i++) { + const operatorId = operatorIds[i]; + const before = beforeMigrationCounts[i]; + + const ssvAfter = await clusters.getOperatorValidatorCount(operatorId); + const ethAfter = await clusters.getOperatorEthValidatorCount(operatorId); + + expect(ssvAfter).to.equal(before.ssv); + if (operatorId === removedOperatorId) { + expect(ethAfter).to.equal(before.eth); + expect(BigInt((await clusters.getOperatorEthSnapshot(operatorId)).blockNumber)).to.equal(0n); + } else { + expect(ethAfter).to.equal(before.eth + validatorCount); + } + } + }); + + it("Accounts two removed operators with different removal times via frozen indices", async function () { + const { clusters, operatorIds, mockToken } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const validatorCount = 3n; + const ssvBalance = ethers.parseEther("700"); + const ssvCluster = createCluster({ + validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: ssvBalance, + active: true, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(4), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + const removedA = operatorIds[0]; + const removedB = operatorIds[1]; + + await mineBlocks(provider, 250); + + const removeABlockExpected = BigInt(await provider.getBlockNumber()) + 1n; + const snapABefore = await clusters.getOperatorSnapshot(removedA); + const feeARaw = BigInt(await clusters.getOperatorSSVFee(removedA)); + const indexAAtRemoval = BigInt(snapABefore.index) + + (removeABlockExpected - BigInt(snapABefore.blockNumber)) * feeARaw; + + await (clusters as any).mockRemoveOperatorAndPayout(removedA, clusterOwner.address); + const snapAAfter = await clusters.getOperatorSnapshot(removedA); + expect(BigInt(snapAAfter.blockNumber)).to.equal(0n); + expect(BigInt(snapAAfter.index)).to.equal(indexAAtRemoval); + + await mineBlocks(provider, 150); + + const removeBBlockExpected = BigInt(await provider.getBlockNumber()) + 1n; + const snapBBefore = await clusters.getOperatorSnapshot(removedB); + const feeBRaw = BigInt(await clusters.getOperatorSSVFee(removedB)); + const indexBAtRemoval = BigInt(snapBBefore.index) + + (removeBBlockExpected - BigInt(snapBBefore.blockNumber)) * feeBRaw; + + await (clusters as any).mockRemoveOperatorAndPayout(removedB, clusterOwner.address); + const snapBAfter = await clusters.getOperatorSnapshot(removedB); + expect(BigInt(snapBAfter.blockNumber)).to.equal(0n); + expect(BigInt(snapBAfter.index)).to.equal(indexBAtRemoval); + + await mineBlocks(provider, 100); + + const migrationBlockExpected = BigInt(await provider.getBlockNumber()) + 1n; + let liveCumulativeIndex = 0n; + for (let i = 2; i < operatorIds.length; i++) { + liveCumulativeIndex += await getSnapshotIndexAtBlock(clusters, operatorIds[i], migrationBlockExpected); + } + + const networkFeeIndexBefore = BigInt(await clusters.getCurrentNetworkFeeIndexSSV()); + const readBlock = BigInt(await provider.getBlockNumber()); + const ownerBefore = await mockToken.balanceOf(clusterOwner.address); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = getMigratedToETHEventArgs(clusters, receipt); + const migrationBlock = BigInt(receipt!.blockNumber); + expect(migrationBlock).to.equal(migrationBlockExpected); + + const expectedNetworkFeeIndex = + networkFeeIndexBefore + (migrationBlock - readBlock) * NETWORK_FEE_SSV_RAW; + const removedCombined = indexAAtRemoval + indexBAtRemoval; + const correctCumulativeIndex = removedCombined + liveCumulativeIndex; + const buggyCumulativeIndex = liveCumulativeIndex; + + const correctOperatorUsagePacked = (correctCumulativeIndex - ssvCluster.index) * validatorCount; + const buggyOperatorUsagePacked = (buggyCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + + const correctRefund = ssvBalance > (correctOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + ? ssvBalance - (correctOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + : 0n; + const buggyRefund = ssvBalance > (buggyOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + ? ssvBalance - (buggyOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + : 0n; + + const missingFeesWei = removedCombined * validatorCount * DEDUCTED_DIGITS; + expect(buggyRefund - correctRefund).to.equal(missingFeesWei); + expect(eventArgs.ssvRefunded).to.equal(correctRefund); + expect(eventArgs.ssvRefunded).to.not.equal(buggyRefund); + + const ownerAfter = await mockToken.balanceOf(clusterOwner.address); + expect(ownerAfter - ownerBefore).to.equal(correctRefund); + + expect(await clusters.getOperatorValidatorCount(removedA)).to.equal(0n); + expect(await clusters.getOperatorValidatorCount(removedB)).to.equal(0n); + expect(await clusters.getOperatorEthValidatorCount(removedA)).to.equal(0n); + expect(await clusters.getOperatorEthValidatorCount(removedB)).to.equal(0n); + expect(BigInt((await clusters.getOperatorSnapshot(removedA)).index)).to.equal(indexAAtRemoval); + expect(BigInt((await clusters.getOperatorSnapshot(removedB)).index)).to.equal(indexBAtRemoval); + }); + + it("Removed operator with zero SSV fee creates zero refund delta vs buggy path", async function () { + const { clusters, operatorIds, mockToken } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await clusters.mockOperatorSSVFee(operatorIds[0], 0n); + + const validatorCount = 2n; + const ssvBalance = ethers.parseEther("300"); + const ssvCluster = createCluster({ + validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: ssvBalance, + active: true, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(5), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + await mineBlocks(provider, 180); + + const removedOperatorId = operatorIds[0]; + await (clusters as any).mockRemoveOperatorAndPayout(removedOperatorId, clusterOwner.address); + const removedSnapshot = await clusters.getOperatorSnapshot(removedOperatorId); + const removedIndex = BigInt(removedSnapshot.index); + expect(removedIndex).to.equal(0n); + + await mineBlocks(provider, 120); + + const migrationBlockExpected = BigInt(await provider.getBlockNumber()) + 1n; + let liveCumulativeIndex = 0n; + for (let i = 1; i < operatorIds.length; i++) { + liveCumulativeIndex += await getSnapshotIndexAtBlock(clusters, operatorIds[i], migrationBlockExpected); + } + + const networkFeeIndexBefore = BigInt(await clusters.getCurrentNetworkFeeIndexSSV()); + const readBlock = BigInt(await provider.getBlockNumber()); + const ownerBefore = await mockToken.balanceOf(clusterOwner.address); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = getMigratedToETHEventArgs(clusters, receipt); + const migrationBlock = BigInt(receipt!.blockNumber); + expect(migrationBlock).to.equal(migrationBlockExpected); + + const expectedNetworkFeeIndex = + networkFeeIndexBefore + (migrationBlock - readBlock) * NETWORK_FEE_SSV_RAW; + const correctCumulativeIndex = removedIndex + liveCumulativeIndex; + const buggyCumulativeIndex = liveCumulativeIndex; + + const correctOperatorUsagePacked = (correctCumulativeIndex - ssvCluster.index) * validatorCount; + const buggyOperatorUsagePacked = (buggyCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + + const correctRefund = ssvBalance > (correctOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + ? ssvBalance - (correctOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + : 0n; + const buggyRefund = ssvBalance > (buggyOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + ? ssvBalance - (buggyOperatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS + : 0n; + + expect(correctRefund).to.equal(buggyRefund); + expect(eventArgs.ssvRefunded).to.equal(correctRefund); + + const ownerAfter = await mockToken.balanceOf(clusterOwner.address); + expect(ownerAfter - ownerBefore).to.equal(correctRefund); + }); + + it("Removed operator frozen ETH index is not charged again on first post-migration settlement", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const ethFeeRaw = 10_000_000_000n; + const validatorCount = 1n; + + await clusters.mockEthNetworkFee(0n); + await clusters.mockCurrentNetworkFeeIndex(0n); + + for (const operatorId of operatorIds) { + await clusters.mockSetOperatorFee(operatorId, ethFeeRaw); + } + + const ethRegisterTx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(7), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const ethCluster = parseClusterFromEvent(clusters, await ethRegisterTx.wait(), Events.VALIDATOR_ADDED); + + await mineBlocks(provider, 120); + + const ethRegister2Tx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(8), + operatorIds, + DEFAULT_SHARES, + ethCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await ethRegister2Tx.wait(); + + const ssvCluster: Cluster = createCluster({ + validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }); + await clusters.mockRegisterSSVValidator( + makePublicKey(9), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + const removedOperatorId = operatorIds[0]; + await mineBlocks(provider, 40); + await (clusters as any).mockRemoveOperatorAndPayout(removedOperatorId, clusterOwner.address); + + const removedEthSnapshot = await clusters.getOperatorEthSnapshot(removedOperatorId); + const removedFrozenIndex = BigInt(removedEthSnapshot.index); + expect(BigInt(removedEthSnapshot.blockNumber)).to.equal(0n); + expect(removedFrozenIndex).to.be.greaterThan(0n); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const migrateReceipt = await migrateTx.wait(); + const migratedCluster = parseClusterFromEvent(clusters, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + + let expectedMigratedIndex = 0n; + const postMigrationEthState: Array<{ index: bigint; blockNumber: bigint; fee: bigint }> = []; + for (const operatorId of operatorIds) { + const ethSnapshot = await clusters.getOperatorEthSnapshot(operatorId); + const feePacked = BigInt(await clusters.getOperatorEthFee(operatorId)); + + expectedMigratedIndex += BigInt(ethSnapshot.index); + postMigrationEthState.push({ + index: BigInt(ethSnapshot.index), + blockNumber: BigInt(ethSnapshot.blockNumber), + fee: feePacked, + }); + } + + expect(migratedCluster.index).to.equal(expectedMigratedIndex); + + await mineBlocks(provider, 50); + + const withdrawTx = await clusters.withdraw(operatorIds, 1n, migratedCluster); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + const withdrawBlock = BigInt(withdrawReceipt!.blockNumber); + + let expectedCurrentClusterIndex = 0n; + for (const operator of postMigrationEthState) { + expectedCurrentClusterIndex += operator.index + (withdrawBlock - operator.blockNumber) * operator.fee; + } + + const expectedOperatorUsageWei = + (expectedCurrentClusterIndex - BigInt(migratedCluster.index)) * validatorCount * ETH_DEDUCTED_DIGITS; + const expectedBalance = DEFAULT_ETH_REGISTER_VALUE - expectedOperatorUsageWei - 1n; + + expect(clusterAfterWithdraw.balance).to.equal(expectedBalance); + + const phantomChargeWei = removedFrozenIndex * validatorCount * ETH_DEDUCTED_DIGITS; + expect(clusterAfterWithdraw.balance).to.not.equal(expectedBalance - phantomChargeWei); + }); + + it("Assigns default ETH fee on migration when legacy operator had ethFee explicitly reset to zero", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + for (const operatorId of operatorIds) { + await clusters.mockSetOperatorLegacySSV(operatorId, HIGH_SSV_FEE_RAW); + } + + const targetOperator = operatorIds[0]; + + await clusters.mockSetOperatorFee(targetOperator, 12_345_000_000n); + await clusters.mockSetOperatorFee(targetOperator, 0n); + + const beforeEthSnapshot = await clusters.getOperatorEthSnapshot(targetOperator); + const beforeEthFeePacked = await clusters.getOperatorEthFee(targetOperator); + expect(BigInt(beforeEthSnapshot.blockNumber)).to.equal(0n); + expect(beforeEthFeePacked).to.equal(0n); + + const ssvCluster = createCluster({ + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }); + await clusters.mockRegisterSSVValidator( + makePublicKey(6), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + + const expectedDefaultPacked = DEFAULT_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const afterEthFeePacked = await clusters.getOperatorEthFee(targetOperator); + const afterEthSnapshot = await clusters.getOperatorEthSnapshot(targetOperator); + + expect(afterEthFeePacked).to.equal(expectedDefaultPacked); + expect(BigInt(afterEthSnapshot.blockNumber)).to.equal(BigInt(receipt!.blockNumber)); + + await expect(migrateTx) + .to.emit(clusters, Events.OPERATOR_FEE_EXECUTED) + .withArgs(clusterOwner.address, targetOperator, BigInt(receipt!.blockNumber), DEFAULT_OPERATOR_ETH_FEE); + }); +}); diff --git a/test/e2e/migration/migration-edge.test.ts b/test/e2e/migration/migration-edge.test.ts new file mode 100644 index 000000000..0eab944ab --- /dev/null +++ b/test/e2e/migration/migration-edge.test.ts @@ -0,0 +1,448 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullPreUpgradeFixture, upgradeToStakingVersion } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + getCurrentClusterState, + extractEventArgs, + parseClusterFromEvent, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEFAULT_ETH_REGISTER_VALUE, + DEDUCTED_DIGITS, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE_ETH, + ETH_DEDUCTED_DIGITS, + EMPTY_CLUSTER, + TOKEN_REGISTER_AMOUNT, + MINIMUM_BLOCKS_BEFORE_LIQUIDATION, + BPS_DENOMINATOR, +} from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + calcLiquidationThreshold, + defaultVUnits, +} from "../../helpers/index.ts"; +import { makeOperatorKey } from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +const OP_SSV_FEE_UNPACKED = 10_000_000_000n; + +describe("Migration Edge Cases", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let clusterOwnerB: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, clusterOwnerB] } = await setupTestContext()); + }); + + describe("Migration — SSV Refund Is Exactly Correct After Extended Fee Accrual", () => { + const OP_SSV_FEE_CUSTOM = 1_500n * DEDUCTED_DIGITS; + + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_CUSTOM, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_CUSTOM, false); + operatorIds.push(Number(expectedId)); + } + + const ssvDeposit = ethers.parseEther("500"); + await ssvToken.mint(clusterOwner.address, ssvDeposit); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvDeposit, + ); + + const halfDeposit = ssvDeposit / 2n; + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, halfDeposit, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, halfDeposit, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster, ssvDeposit }; + }; + + it("SSV refund matches independent fee calculation after 1000 blocks", async function () { + const { network, views, ssvToken, operatorIds, cluster, ssvDeposit } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 1000); + + const ssvBalanceBefore = await views.getBalanceSSV( + clusterOwner.address, operatorIds, cluster, + ); + const burnRate = await views.getBurnRateSSV( + clusterOwner.address, operatorIds, cluster, + ); + + const ownerSSVBefore = await ssvToken.balanceOf(clusterOwner.address); + + const ethDeposit = ethers.parseEther("10"); + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: ethDeposit }, + ); + const receipt = await migrateTx.wait(); + + const eventArgs = extractEventArgs(network, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + const actualRefund = BigInt(eventArgs.ssvRefunded); + + const ownerSSVAfter = await ssvToken.balanceOf(clusterOwner.address); + const tokenRefund = ownerSSVAfter - ownerSSVBefore; + expect(tokenRefund).to.equal(actualRefund); + + const expectedRefund = ssvBalanceBefore - burnRate; + expect(actualRefund).to.equal(expectedRefund); + expect(actualRefund).to.be.lessThan(ssvDeposit); + + const totalFees = ssvDeposit - actualRefund; + expect(totalFees % DEDUCTED_DIGITS).to.equal(0n); + }); + }); + + describe("Migration of Cluster Where Some Operators Were Removed", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + await ssvToken.mint(clusterOwner.address, TOKEN_REGISTER_AMOUNT); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), TOKEN_REGISTER_AMOUNT, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + const cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + it("Migration succeeds when Op1 is removed — removed operator is skipped", async function () { + const { network, views, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + await network.connect(clusterOwner).removeOperator(operatorIds[0]); + + const ethDeposit = ethers.parseEther("10"); + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: ethDeposit }, + ); + await migrateTx.wait(); + + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + const op0 = await views.getOperatorById(operatorIds[0]); + expect(op0.validatorCount).to.equal(0); + + for (let i = 1; i < operatorIds.length; i++) { + const op = await views.getOperatorById(operatorIds[i]); + expect(op.validatorCount).to.equal(1); + } + }); + }); + + describe("DAO Earnings Settlement During Migration", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvDeposit = TOKEN_REGISTER_AMOUNT * 2n; + await ssvToken.mint(clusterOwner.address, ssvDeposit); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvDeposit, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + it("DAO earnings for both SSV and ETH are settled during migration", async function () { + const { network, views, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 100); + + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + await migrateTx.wait(); + + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + const networkValidators = await views.getNetworkValidatorsCount(); + expect(networkValidators).to.equal(2); + + for (const opId of operatorIds) { + const opETH = await views.getOperatorById(opId); + expect(opETH.validatorCount).to.equal(2); + const opSSV = await views.getOperatorByIdSSV(opId); + expect(opSSV.validatorCount).to.equal(0); + } + }); + }); + + describe("Multiple Migrations — Same Operators, Different Clusters", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvDeposit = TOKEN_REGISTER_AMOUNT * 3n; + await ssvToken.mint(clusterOwner.address, ssvDeposit); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvDeposit, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + let clusterA = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, clusterA, + ); + clusterA = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + await ssvToken.mint(clusterOwnerB.address, TOKEN_REGISTER_AMOUNT); + await ssvToken.connect(clusterOwnerB).approve( + await legacyNetwork.getAddress(), TOKEN_REGISTER_AMOUNT, + ); + + await legacyNetwork.connect(clusterOwnerB).registerValidator( + makePublicKey(3), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + const clusterB = await getCurrentClusterState( + connection, legacyNetwork, clusterOwnerB.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, clusterA, clusterB }; + }; + + it("Two clusters with same operators migrate correctly without index corruption", async function () { + const { network, views, operatorIds, clusterA, clusterB } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 100); + + const migrateTx1 = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, clusterA, + { value: ethers.parseEther("5") }, + ); + await migrateTx1.wait(); + await expect(migrateTx1).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + for (const opId of operatorIds) { + const opETH = await views.getOperatorById(opId); + expect(opETH.validatorCount).to.equal(2); + } + + await mineBlocks(provider, 100); + + const migrateTx2 = await network.connect(clusterOwnerB).migrateClusterToETH( + operatorIds, clusterB, + { value: ethers.parseEther("3") }, + ); + const receipt2 = await migrateTx2.wait(); + await expect(migrateTx2).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + for (const opId of operatorIds) { + const opETH = await views.getOperatorById(opId); + expect(opETH.validatorCount).to.equal(3); + } + + for (const opId of operatorIds) { + const opSSV = await views.getOperatorByIdSSV(opId); + expect(opSSV.validatorCount).to.equal(0); + } + + expect(await views.getNetworkValidatorsCount()).to.equal(3); + + const clusterBAfter = parseClusterFromEvent(network, receipt2, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterBAfter.balance).to.equal(ethers.parseEther("3")); + expect(clusterBAfter.validatorCount).to.equal(1n); + }); + }); + + describe("Revert — Migrate With Insufficient ETH For Liquidation Check", () => { + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvDeposit = TOKEN_REGISTER_AMOUNT * 2n; + await ssvToken.mint(clusterOwner.address, ssvDeposit); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvDeposit, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster }; + }; + + const ethFeeRaw = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const networkFeeRaw = NETWORK_FEE_ETH / ETH_DEDUCTED_DIGITS; + + it("Reverts when ETH deposit is below liquidation threshold", async function () { + const { network, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + const threshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MINIMUM_BLOCKS_BEFORE_LIQUIDATION, + numOperators: 4n, + ethFee: ethFeeRaw, + networkFee: networkFeeRaw, + effectiveVUnits: defaultVUnits(2n), + }); + + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: threshold }, + ); + await migrateTx.wait(); + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + }); + + it("Reverts when ETH deposit is 1 wei below threshold", async function () { + const { network, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + const threshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation: MINIMUM_BLOCKS_BEFORE_LIQUIDATION, + numOperators: 4n, + ethFee: ethFeeRaw, + networkFee: networkFeeRaw, + effectiveVUnits: defaultVUnits(2n), + }); + + await expect( + network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: threshold - 1n }, + ), + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Reverts when ETH deposit is 0", async function () { + const { network, operatorIds, cluster } = + await networkHelpers.loadFixture(deployFixture); + + await expect( + network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: 0n }, + ), + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); +}); diff --git a/test/e2e/migration/migration-full-lifecycle.test.ts b/test/e2e/migration/migration-full-lifecycle.test.ts new file mode 100644 index 000000000..d9c94a887 --- /dev/null +++ b/test/e2e/migration/migration-full-lifecycle.test.ts @@ -0,0 +1,172 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullPreUpgradeFixture, upgradeToStakingVersion } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makePublicKey, + getCurrentClusterState, + parseClusterFromEvent, + extractEventArgs, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEDUCTED_DIGITS, + ETH_DEDUCTED_DIGITS, + BPS_DENOMINATOR, + MINIMAL_OPERATOR_ETH_FEE, + EMPTY_CLUSTER, + TOKEN_REGISTER_AMOUNT, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { mineBlocks } from "../../helpers/index.ts"; +import { makeOperatorKey } from "../../helpers/index.ts"; +import { ethers } from "ethers"; + +const OP_SSV_FEE_UNPACKED = 10_000_000_000n; + + +describe("Full End-to-End — SSV Cluster Creation -> Fee Accrual -> Migration -> ETH Fee Accrual -> Withdraw -> Verify All Balances", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => { + const { network: legacyNetwork, views: legacyViews, ssvToken } = + await ssvNetworkFullPreUpgradeFixture(connection); + + const operatorIds: number[] = []; + for (let i = 0; i < 4; i++) { + const expectedId = await legacyNetwork.connect(clusterOwner) + .registerOperator.staticCall(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + await legacyNetwork.connect(clusterOwner) + .registerOperator(makeOperatorKey(i + 1), OP_SSV_FEE_UNPACKED, false); + operatorIds.push(Number(expectedId)); + } + + const ssvDeposit = TOKEN_REGISTER_AMOUNT * 2n; + await ssvToken.mint(clusterOwner.address, ssvDeposit); + await ssvToken.connect(clusterOwner).approve( + await legacyNetwork.getAddress(), ssvDeposit, + ); + + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + let cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + await legacyNetwork.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, cluster, + ); + cluster = await getCurrentClusterState( + connection, legacyNetwork, clusterOwner.address, operatorIds, + ); + + const { newNetwork, newViews } = await upgradeToStakingVersion( + connection, legacyNetwork, legacyViews, + ); + + return { network: newNetwork, views: newViews, ssvToken, operatorIds, cluster, ssvDeposit }; + }; + + it("Verifies complete economic correctness across full lifecycle", async function () { + const { network, views, ssvToken, operatorIds, cluster, ssvDeposit } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await mineBlocks(provider, 500); + + const ssvBalanceBefore = await views.getBalanceSSV( + clusterOwner.address, operatorIds, cluster, + ); + const ssvBurnRate = await views.getBurnRateSSV( + clusterOwner.address, operatorIds, cluster, + ); + expect(ssvBalanceBefore).to.be.greaterThan(0n); + expect(ssvBalanceBefore).to.be.lessThan(ssvDeposit); + + const ownerSSVBefore = await ssvToken.balanceOf(clusterOwner.address); + + const ethDeposit = ethers.parseEther("10"); + const migrateTx = await network.connect(clusterOwner).migrateClusterToETH( + operatorIds, cluster, + { value: ethDeposit }, + ); + const migrateReceipt = await migrateTx.wait(); + await expect(migrateTx).to.emit(network, Events.CLUSTER_MIGRATED_TO_ETH); + + const migrateEventArgs = extractEventArgs(network, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + const actualSSVRefund = BigInt(migrateEventArgs.ssvRefunded); + + const ownerSSVAfter = await ssvToken.balanceOf(clusterOwner.address); + const tokenRefund = ownerSSVAfter - ownerSSVBefore; + expect(tokenRefund).to.equal(actualSSVRefund); + + const expectedRefund = ssvBalanceBefore - ssvBurnRate; + expect(actualSSVRefund).to.equal(expectedRefund); + expect(actualSSVRefund).to.be.lessThan(ssvDeposit); + + const totalSSVFees = ssvDeposit - actualSSVRefund; + expect(totalSSVFees % DEDUCTED_DIGITS).to.equal(0n); + expect(actualSSVRefund + totalSSVFees).to.equal(ssvDeposit); + + const migratedCluster = parseClusterFromEvent( + network, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH, + ); + expect(BigInt(migratedCluster.balance)).to.equal(ethDeposit); + expect(migratedCluster.active).to.equal(true); + expect(BigInt(migratedCluster.validatorCount)).to.equal(2n); + + await mineBlocks(provider, 200); + + const ethBalanceAfterAccrual = await views.getBalance( + clusterOwner.address, operatorIds, migratedCluster, + ); + expect(ethBalanceAfterAccrual).to.be.lessThan(ethDeposit); + expect(ethBalanceAfterAccrual).to.be.greaterThan(0n); + + const withdrawAmount = ethers.parseEther("1"); + const ownerETHBefore = await provider.getBalance(clusterOwner.address); + + const withdrawTx = await network.connect(clusterOwner).withdraw( + operatorIds, + withdrawAmount, + migratedCluster, + ); + const withdrawReceipt = await withdrawTx.wait(); + await expect(withdrawTx).to.emit(network, Events.CLUSTER_WITHDRAWN); + + const clusterAfterWithdraw = parseClusterFromEvent( + network, withdrawReceipt, Events.CLUSTER_WITHDRAWN, + ); + + expect(BigInt(clusterAfterWithdraw.balance)).to.be.lessThan(ethDeposit - withdrawAmount); + expect(BigInt(clusterAfterWithdraw.balance)).to.be.greaterThan(0n); + + const ownerETHAfter = await provider.getBalance(clusterOwner.address); + const gasCost = withdrawReceipt!.gasUsed * withdrawReceipt!.gasPrice; + expect(ownerETHAfter).to.equal(ownerETHBefore + withdrawAmount - gasCost); + + for (const opId of operatorIds) { + const opETH = await views.getOperatorById(opId); + expect(opETH.validatorCount).to.equal(2); + const opSSV = await views.getOperatorByIdSSV(opId); + expect(opSSV.validatorCount).to.equal(0); + } + + expect(await views.getNetworkValidatorsCount()).to.equal(2); + + const finalBalance = await views.getBalance( + clusterOwner.address, operatorIds, clusterAfterWithdraw, + ); + expect(finalBalance).to.be.lessThanOrEqual(BigInt(clusterAfterWithdraw.balance)); + expect(finalBalance).to.be.greaterThan(0n); + }); +}); diff --git a/test/e2e/operators/operator-economics.test.ts b/test/e2e/operators/operator-economics.test.ts new file mode 100644 index 000000000..6b979d202 --- /dev/null +++ b/test/e2e/operators/operator-economics.test.ts @@ -0,0 +1,564 @@ +import { expect } from 'chai'; +import type { NetworkConnection } from 'hardhat/types/network'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { ssvNetworkFullFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import { getCurrentClusterState, makeOperatorKey, makePublicKey, whitelistAddresses, setupTestContext } from '../../common/helpers.ts'; +import { + DECLARE_OPERATOR_FEE_PERIOD, + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, + MINIMAL_OPERATOR_ETH_FEE, +} from '../../common/constants.ts'; +import { calcOperatorFeeAccrual, defaultVUnits, getBlockNumber, getTxBlock, mineBlocks } from '../../helpers/index.ts'; +import { Events } from '../../common/events.ts'; +import { Errors } from '../../common/errors.ts'; +import { ethers } from 'ethers'; + +describe("Operator Economics", function () { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwnerA: HardhatEthersSigner; + let clusterOwnerB: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwnerA, clusterOwnerB] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + async function registerOps( + network: any, + count: number, + fee: bigint, + ): Promise { + const ids: number[] = []; + for (let i = 1; i <= count; i++) { + const id = await network + .connect(operatorOwner) + .registerOperator.staticCall(makeOperatorKey(i), fee, false); + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), fee, false); + ids.push(Number(id)); + } + return ids; + } + + async function fundAndRegisterValidator( + network: any, + provider: any, + signer: HardhatEthersSigner, + operatorIds: number[], + pubkey: string, + depositEth: bigint, + cluster: any, + ) { + return await network + .connect(signer) + .registerValidator(pubkey, operatorIds, DEFAULT_SHARES, cluster, { + value: depositEth, + }); + } + + describe("Operator Earnings Accumulation and Withdrawal", () => { + it("Verifies exact earnings math with partial and full withdrawal", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + ]); + + await fundAndRegisterValidator( + network, + provider, + clusterOwnerA, + operatorIds, + makePublicKey(1), + DEFAULT_ETH_REGISTER_VALUE, + EMPTY_CLUSTER, + ); + + const regBlock = BigInt(await getBlockNumber(provider)); + + await mineBlocks(provider, 100); + + const vUnits = defaultVUnits(1n); + const earningsViewBlock = BigInt(await getBlockNumber(provider)); + const expectedEarnings1 = calcOperatorFeeAccrual(earningsViewBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + const earnings1 = await views.getOperatorEarnings(1n); + expect(earnings1).to.equal(expectedEarnings1); + + const half = (earnings1 / (2n * ETH_DEDUCTED_DIGITS)) * ETH_DEDUCTED_DIGITS; + const partialTx = await network + .connect(operatorOwner) + .withdrawOperatorEarnings(1n, half); + await expect(partialTx).to.emit(network, Events.OPERATOR_WITHDRAWN); + + const partialBlock = BigInt(await getTxBlock(partialTx)); + const expectedAfterPartial = calcOperatorFeeAccrual(partialBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS - half; + const earningsAfterPartial = await views.getOperatorEarnings(1n); + expect(earningsAfterPartial).to.equal(expectedAfterPartial); + + await mineBlocks(provider, 50); + const fullTx = await network + .connect(operatorOwner) + .withdrawAllOperatorEarnings(1n); + await expect(fullTx).to.emit(network, Events.OPERATOR_WITHDRAWN); + }); + }); + + describe("Fee Change During Active Cluster", () => { + it("Verifies continuous fee accrual across fee change boundary", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOps(network, 4, MINIMAL_OPERATOR_ETH_FEE); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + ]); + const deposit = ethers.parseEther("30"); + await fundAndRegisterValidator( + network, + provider, + clusterOwnerA, + operatorIds, + makePublicKey(1), + deposit, + EMPTY_CLUSTER, + ); + + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + + for (let i = 2; i <= 3; i++) { + await network + .connect(clusterOwnerA) + .registerValidator( + makePublicKey(i), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: ethers.parseEther("5") }, + ); + cluster = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + } + + const earningsBeforeDeclare = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + + await mineBlocks(provider, 50); + + const currentFee = await views.getOperatorFee(BigInt(operatorIds[0])); + const currentPacked = currentFee / ETH_DEDUCTED_DIGITS; + const maxIncreaseBps = await views.getOperatorFeeIncreaseLimit(); + const maxAllowedPacked = + (currentPacked * (10_000n + maxIncreaseBps) + 9_999n) / 10_000n; + const newFee = maxAllowedPacked * ETH_DEDUCTED_DIGITS; + + await network + .connect(operatorOwner) + .declareOperatorFee(BigInt(operatorIds[0]), newFee); + + const declareFeePeriod = Number(DECLARE_OPERATOR_FEE_PERIOD); + await provider.send("evm_increaseTime", [declareFeePeriod + 1]); + await provider.send("evm_mine", []); + + const earningsBeforeExecute = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsBeforeExecute).to.be.greaterThan(earningsBeforeDeclare); + + const executeTx = await network + .connect(operatorOwner) + .executeOperatorFee(BigInt(operatorIds[0])); + await executeTx.wait(); + + const feeAfter = await views.getOperatorFee(BigInt(operatorIds[0])); + expect(feeAfter).to.equal(newFee); + + await mineBlocks(provider, 100); + + const earningsAfterNewFee = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAfterNewFee).to.be.greaterThan(earningsBeforeExecute); + + const earningsOp2 = await views.getOperatorEarnings( + BigInt(operatorIds[1]), + ); + expect(earningsAfterNewFee).to.be.greaterThan(earningsOp2); + }); + }); + + describe("Multi-Cluster Operator — Earnings From Multiple Clusters", () => { + it("Operator earns from two clusters, correct accounting on partial removal", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + clusterOwnerB.address, + ]); + + const regA1Tx = await fundAndRegisterValidator( + network, + provider, + clusterOwnerA, + operatorIds, + makePublicKey(1), + DEFAULT_ETH_REGISTER_VALUE, + EMPTY_CLUSTER, + ); + const blockA1 = BigInt(await getTxBlock(regA1Tx)); + let clusterA = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + + const regA2Tx = await network + .connect(clusterOwnerA) + .registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterA, + { value: ethers.parseEther("5") }, + ); + const blockA2 = BigInt(await getTxBlock(regA2Tx)); + + const regBTx = await network.connect(clusterOwnerB).bulkRegisterValidator( + [makePublicKey(10), makePublicKey(11), makePublicKey(12)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES, DEFAULT_SHARES], + EMPTY_CLUSTER, + { value: ethers.parseEther("20") }, + ); + const blockB = BigInt(await getTxBlock(regBTx)); + + const opData = await views.getOperatorById(BigInt(operatorIds[0])); + expect(opData.validatorCount).to.equal(5n); + + await mineBlocks(provider, 100); + + const viewBlock = BigInt(await getBlockNumber(provider)); + const expectedEarningsAt100 = ( + calcOperatorFeeAccrual(blockA2 - blockA1, packedFee, defaultVUnits(1n)) + + calcOperatorFeeAccrual(blockB - blockA2, packedFee, defaultVUnits(2n)) + + calcOperatorFeeAccrual(viewBlock - blockB, packedFee, defaultVUnits(5n)) + ) * ETH_DEDUCTED_DIGITS; + const earningsAt100 = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAt100).to.equal(expectedEarningsAt100); + + clusterA = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + await network + .connect(clusterOwnerA) + .removeValidator(makePublicKey(1), operatorIds, clusterA); + + clusterA = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + await network + .connect(clusterOwnerA) + .removeValidator(makePublicKey(2), operatorIds, clusterA); + + const opDataAfter = await views.getOperatorById( + BigInt(operatorIds[0]), + ); + expect(opDataAfter.validatorCount).to.equal(3n); + + await mineBlocks(provider, 100); + + const earningsAt200 = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAt200).to.be.greaterThan(earningsAt100); + + const earningsOp2 = await views.getOperatorEarnings( + BigInt(operatorIds[1]), + ); + const earningsOp3 = await views.getOperatorEarnings( + BigInt(operatorIds[2]), + ); + const earningsOp4 = await views.getOperatorEarnings( + BigInt(operatorIds[3]), + ); + expect(earningsAt200).to.equal(earningsOp2); + expect(earningsAt200).to.equal(earningsOp3); + expect(earningsAt200).to.equal(earningsOp4); + }); + }); + + describe("Operator Removal After All Validators Removed", () => { + it("Removes validators then operator, verifies final earnings withdrawal", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + ]); + const reg1Tx = await fundAndRegisterValidator( + network, + provider, + clusterOwnerA, + operatorIds, + makePublicKey(1), + DEFAULT_ETH_REGISTER_VALUE, + EMPTY_CLUSTER, + ); + const blockR1 = BigInt(await getTxBlock(reg1Tx)); + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + + const reg2Tx = await network + .connect(clusterOwnerA) + .registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: ethers.parseEther("5") }, + ); + const blockR2 = BigInt(await getTxBlock(reg2Tx)); + + await mineBlocks(provider, 100); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + const removeVal1Tx = await network + .connect(clusterOwnerA) + .removeValidator(makePublicKey(1), operatorIds, cluster); + const blockV1 = BigInt(await getTxBlock(removeVal1Tx)); + + const opAfterRemove1 = await views.getOperatorById( + BigInt(operatorIds[0]), + ); + expect(opAfterRemove1.validatorCount).to.equal(1n); + + await mineBlocks(provider, 50); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwnerA.address, + operatorIds, + ); + const removeVal2Tx = await network + .connect(clusterOwnerA) + .removeValidator(makePublicKey(2), operatorIds, cluster); + const blockV2 = BigInt(await getTxBlock(removeVal2Tx)); + + const opAfterRemove2 = await views.getOperatorById( + BigInt(operatorIds[0]), + ); + expect(opAfterRemove2.validatorCount).to.equal(0n); + + await mineBlocks(provider, 50); + + const expectedEarnings = ( + calcOperatorFeeAccrual(blockR2 - blockR1, packedFee, defaultVUnits(1n)) + + calcOperatorFeeAccrual(blockV1 - blockR2, packedFee, defaultVUnits(2n)) + + calcOperatorFeeAccrual(blockV2 - blockV1, packedFee, defaultVUnits(1n)) + ) * ETH_DEDUCTED_DIGITS; + + const earningsBeforeRemoval = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsBeforeRemoval).to.equal(expectedEarnings); + await mineBlocks(provider, 50); + const earningsAfterMoreBlocks = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAfterMoreBlocks).to.equal(earningsBeforeRemoval); + + const ownerBalBefore = await provider.getBalance( + operatorOwner.address, + ); + const removeTx = await network + .connect(operatorOwner) + .removeOperator(BigInt(operatorIds[0])); + const removeReceipt = await removeTx.wait(); + const removeGas = + removeReceipt!.gasUsed * removeReceipt!.gasPrice; + + await expect(removeTx) + .to.emit(network, Events.OPERATOR_REMOVED) + .withArgs(BigInt(operatorIds[0])); + + const ownerBalAfter = await provider.getBalance( + operatorOwner.address, + ); + const netTransfer = ownerBalAfter - ownerBalBefore + removeGas; + expect(netTransfer).to.equal(expectedEarnings); + expect(netTransfer).to.equal(earningsBeforeRemoval); + + const opAfterRemoval = await views.getOperatorById( + BigInt(operatorIds[0]), + ); + expect(opAfterRemoval.isActive).to.equal(false); + expect(opAfterRemoval.owner).to.equal(operatorOwner.address); + + await expect( + network.connect(operatorOwner).removeOperator(BigInt(operatorIds[0])), + ).to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + }); + + describe("withdrawAllVersionOperatorEarnings — Combined ETH + SSV", () => { + it("Withdraws both ETH and SSV earnings in single call", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + ]); + const regTx = await fundAndRegisterValidator( + network, + provider, + clusterOwnerA, + operatorIds, + makePublicKey(1), + DEFAULT_ETH_REGISTER_VALUE, + EMPTY_CLUSTER, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + const vUnits = defaultVUnits(1n); + + await mineBlocks(provider, 100); + + const ethViewBlock = BigInt(await getBlockNumber(provider)); + const expectedEthEarnings = calcOperatorFeeAccrual(ethViewBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + const ethEarnings = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(ethEarnings).to.equal(expectedEthEarnings); + + const ownerBalBefore = await provider.getBalance( + operatorOwner.address, + ); + const withdrawTx = await network + .connect(operatorOwner) + .withdrawAllVersionOperatorEarnings(BigInt(operatorIds[0])); + const withdrawReceipt = await withdrawTx.wait(); + const gasUsed = + withdrawReceipt!.gasUsed * withdrawReceipt!.gasPrice; + const withdrawBlock = BigInt(await getTxBlock(withdrawTx)); + + const expectedTransfer = calcOperatorFeeAccrual(withdrawBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + const ownerBalAfter = await provider.getBalance( + operatorOwner.address, + ); + const netTransfer = ownerBalAfter - ownerBalBefore + gasUsed; + expect(netTransfer).to.equal(expectedTransfer); + + const earningsAfter = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAfter).to.be.lessThan(ethEarnings); + }); + + it("Only ETH earnings, no SSV — SSV transfer skipped", async () => { + const { network } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOps(network, 4, MINIMAL_OPERATOR_ETH_FEE); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwnerA.address, + ]); + await fundAndRegisterValidator( + network, + provider, + clusterOwnerA, + operatorIds, + makePublicKey(1), + DEFAULT_ETH_REGISTER_VALUE, + EMPTY_CLUSTER, + ); + + await mineBlocks(provider, 50); + + const withdrawTx = await network + .connect(operatorOwner) + .withdrawAllVersionOperatorEarnings(BigInt(operatorIds[0])); + const receipt = await withdrawTx.wait(); + expect(receipt!.status).to.equal(1); + }); + + it("Zero earnings in both versions — no reverts, no transfers", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + const withdrawTx = await network + .connect(operatorOwner) + .withdrawAllVersionOperatorEarnings(1n); + const receipt = await withdrawTx.wait(); + expect(receipt!.status).to.equal(1); + }); + }); +}); diff --git a/test/e2e/operators/operator-edge-cases.test.ts b/test/e2e/operators/operator-edge-cases.test.ts new file mode 100644 index 000000000..3e6b0fd65 --- /dev/null +++ b/test/e2e/operators/operator-edge-cases.test.ts @@ -0,0 +1,415 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + makeOperatorKey, + whitelistAddresses, + getCurrentClusterState, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE, + ETH_DEDUCTED_DIGITS, + BPS_DENOMINATOR, +} from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + getBlockNumber, + getTxBlock, + defaultVUnits, + calcOperatorFeeAccrual, +} from "../../helpers/index.ts"; + +describe("Operator Edge Cases", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let operatorOwner2: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, otherAccount, operatorOwner2] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Operator removal — state verification after removal", () => { + it("Removed operator preserves owner but zeros ethSnapshot.block", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 1); + const opId = BigInt(operatorIds[0]); + + const opBefore = await views.getOperatorById(opId); + expect(opBefore.owner).to.equal(operatorOwner.address); + expect(opBefore.isActive).to.be.true; + + await network.connect(operatorOwner).removeOperator(operatorIds[0]); + + const opAfter = await views.getOperatorById(opId); + expect(opAfter.owner).to.equal(operatorOwner.address); + expect(opAfter.isActive).to.be.false; + expect(opAfter.validatorCount).to.equal(0); + }); + }); + + describe("ensureETHDefaults with zero SSV fee — default fee NOT assigned", () => { + it("Zero-fee operator stays at zero fee after ETH cluster interaction", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const zeroFeeKey = makeOperatorKey(100); + await network + .connect(operatorOwner) + .registerOperator(zeroFeeKey, 0, false); + const opId0 = 1n; + + const opIds: number[] = [Number(opId0)]; + for (let i = 2; i <= 4; i++) { + const key = makeOperatorKey(100 + i); + await network + .connect(operatorOwner) + .registerOperator(key, MINIMAL_OPERATOR_ETH_FEE, false); + opIds.push(i); + } + + await whitelistAddresses(network, operatorOwner, opIds, [ + clusterOwner.address, + ]); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + + const opFee = await views.getOperatorFee(opId0); + expect(opFee).to.equal(0n); + + const opFee2 = await views.getOperatorFee(2n); + expect(opFee2).to.equal(MINIMAL_OPERATOR_ETH_FEE); + + await mineBlocks(provider, 100); + const earnings = await views.getOperatorEarnings(opId0); + expect(earnings).to.equal(0n); + + const currentBlock = BigInt(await getBlockNumber(provider)); + const blockDiff = currentBlock - regBlock; + const earnings2 = await views.getOperatorEarnings(2n); + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + const expectedEarnings = calcOperatorFeeAccrual(blockDiff, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + expect(earnings2).to.equal(expectedEarnings); + }); + + it("Zero-fee operator can never increase fee", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(200), 0, false); + + await expect( + network + .connect(operatorOwner) + .declareOperatorFee(1, MINIMAL_OPERATOR_ETH_FEE), + ).to.be.revertedWithCustomError( + network, + Errors.FEE_INCREASE_NOT_ALLOWED, + ); + }); + }); + + + describe("Precision loss in operator earnings — vUnits division truncation", () => { + it("Operator earnings are exact with standard vUnits (no truncation)", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const opIds: number[] = []; + for (let i = 1; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(300 + i), MINIMAL_OPERATOR_ETH_FEE, false); + opIds.push(i); + } + + await whitelistAddresses(network, operatorOwner, opIds, [ + clusterOwner.address, + ]); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = await getTxBlock(regTx); + + await mineBlocks(provider, 10); + + const earnings = await views.getOperatorEarnings(1n); + const currentBlock = await getBlockNumber(provider); + const blockDiff = BigInt(currentBlock - regBlock); + + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const expectedWei = blockDiff * packedFee * ETH_DEDUCTED_DIGITS; + expect(earnings).to.equal(expectedWei); + }); + + it("Precision is exact with standard vUnits regardless of fee magnitude", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const doubleFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + const opIds: number[] = []; + for (let i = 1; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(400 + i), doubleFee, false); + opIds.push(i); + } + + await whitelistAddresses(network, operatorOwner, opIds, [ + clusterOwner.address, + ]); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = await getTxBlock(regTx); + + await mineBlocks(provider, 10); + + const earnings = await views.getOperatorEarnings(1n); + const currentBlock = await getBlockNumber(provider); + const blockDiff = BigInt(currentBlock - regBlock); + + const packedFee = doubleFee / ETH_DEDUCTED_DIGITS; + const expectedWei = blockDiff * packedFee * ETH_DEDUCTED_DIGITS; + expect(earnings).to.equal(expectedWei); + }); + }); + + describe("Operator index frozen after removal — cluster still functions", () => { + it("Cluster can remove validators after one operator is removed", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const opKey1 = makeOperatorKey(500); + await network + .connect(operatorOwner2) + .registerOperator(opKey1, MINIMAL_OPERATOR_ETH_FEE, false); + const opIds: number[] = [1]; + for (let i = 2; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(500 + i), MINIMAL_OPERATOR_ETH_FEE, false); + opIds.push(i); + } + + await whitelistAddresses(network, operatorOwner, opIds.slice(1), [ + clusterOwner.address, + ]); + await whitelistAddresses(network, operatorOwner2, [opIds[0]], [ + clusterOwner.address, + ]); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + + await mineBlocks(provider, 50); + + const removeOpTx = await network.connect(operatorOwner2).removeOperator(opIds[0]); + const removeOpBlock = BigInt(await getTxBlock(removeOpTx)); + + const op1 = await views.getOperatorById(1n); + expect(op1.isActive).to.be.false; + + await mineBlocks(provider, 50); + + const currentCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + opIds, + ); + + const removeTx = await network.connect(clusterOwner).removeValidator( + makePublicKey(1), + opIds, + currentCluster, + ); + const removeValBlock = BigInt(await getTxBlock(removeTx)); + + await expect(removeTx).to.emit(network, Events.VALIDATOR_REMOVED); + + const finalCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + opIds, + ); + expect(BigInt(finalCluster.validatorCount)).to.equal(0n); + + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + + const opIndexDelta = (removeOpBlock - regBlock) * packedFee + + 3n * (removeValBlock - regBlock) * packedFee; + const netIndexDelta = (removeValBlock - regBlock) * packedNetworkFee; + + const opFeeUnits = (opIndexDelta * vUnits) / BPS_DENOMINATOR; + const netFeeUnits = (netIndexDelta * vUnits) / BPS_DENOMINATOR; + const totalBurn = (opFeeUnits + netFeeUnits) * ETH_DEDUCTED_DIGITS; + const expectedBalance = DEFAULT_ETH_REGISTER_VALUE - totalBurn; + + expect(BigInt(finalCluster.balance)).to.equal(expectedBalance); + }); + }); + + + describe("Concurrent fee changes on multiple operators in same cluster", () => { + it("Cluster pays correct blended rate after operator fee changes", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const opIds: number[] = []; + for (let i = 1; i <= 4; i++) { + const owner = i === 3 ? otherAccount : operatorOwner; + await network + .connect(owner) + .registerOperator( + makeOperatorKey(600 + i), + MINIMAL_OPERATOR_ETH_FEE, + false, + ); + opIds.push(i); + } + + await whitelistAddresses(network, operatorOwner, opIds.filter(id => id !== 3), [ + clusterOwner.address, + ]); + await whitelistAddresses(network, otherAccount, [3], [ + clusterOwner.address, + ]); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + + await mineBlocks(provider, 50); + + const reduceOp3Tx = await network + .connect(otherAccount) + .reduceOperatorFee(3, 0); + const reduceOp3Block = BigInt(await getTxBlock(reduceOp3Tx)); + + const op3Fee = await views.getOperatorFee(3n); + expect(op3Fee).to.equal(0n); + + const increasedFee = 1_900_000_000n; + const packedCurrent = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const packedNew = increasedFee / ETH_DEDUCTED_DIGITS; + + await network + .connect(operatorOwner) + .declareOperatorFee(1, increasedFee); + + const periods = await views.getOperatorFeePeriods(); + const declareWait = Number(periods[0]); + await provider.send("evm_increaseTime", [declareWait + 1]); + await mineBlocks(provider, 1); + + const execOp1Tx = await network.connect(operatorOwner).executeOperatorFee(1); + const execOp1Block = BigInt(await getTxBlock(execOp1Tx)); + + const op1Fee = await views.getOperatorFee(1n); + expect(op1Fee).to.equal(increasedFee); + + await mineBlocks(provider, 50); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + opIds, + ); + + const viewBlock = BigInt(await getBlockNumber(provider)); + + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + + const op1IndexDelta = (execOp1Block - regBlock) * packedCurrent + (viewBlock - execOp1Block) * packedNew; + const op2IndexDelta = (viewBlock - regBlock) * packedCurrent; + const op3IndexDelta = (reduceOp3Block - regBlock) * packedCurrent; + const op4IndexDelta = (viewBlock - regBlock) * packedCurrent; + const clusterIndexDelta = op1IndexDelta + op2IndexDelta + op3IndexDelta + op4IndexDelta; + + const netIndexDelta = (viewBlock - regBlock) * packedNetworkFee; + + const opFeeUnits = (clusterIndexDelta * vUnits) / BPS_DENOMINATOR; + const netFeeUnits = (netIndexDelta * vUnits) / BPS_DENOMINATOR; + const totalBurn = (opFeeUnits + netFeeUnits) * ETH_DEDUCTED_DIGITS; + const expectedBalance = DEFAULT_ETH_REGISTER_VALUE - totalBurn; + + expect(cluster.active).to.be.true; + const settledBalance = await views.getBalance( + clusterOwner.address, + opIds, + cluster, + ); + expect(settledBalance).to.equal(expectedBalance); + + const earnings1 = await views.getOperatorEarnings(1n); + const earnings2 = await views.getOperatorEarnings(2n); + const earnings3 = await views.getOperatorEarnings(3n); + const earnings4 = await views.getOperatorEarnings(4n); + + expect(earnings2).to.equal(earnings4); + expect(earnings3).to.be.lessThan(earnings2); + expect(earnings1).to.be.greaterThanOrEqual(earnings2); + }); + }); +}); diff --git a/test/e2e/operators/operator-lifecycle.test.ts b/test/e2e/operators/operator-lifecycle.test.ts new file mode 100644 index 000000000..db83b20a5 --- /dev/null +++ b/test/e2e/operators/operator-lifecycle.test.ts @@ -0,0 +1,835 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + makeOperatorKey, + makePublicKey, + whitelistAddresses, + getCurrentClusterState, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, + ETH_DEDUCTED_DIGITS, + DECLARE_OPERATOR_FEE_PERIOD, + EXECUTE_OPERATOR_FEE_PERIOD, DEFAULT_ETH_REGISTER_VALUE, +} from '../../common/constants.ts'; +import { + mineBlocks, + getBlockNumber, + getTxBlock, + calcOperatorFeeAccrual, + defaultVUnits, +} from "../../helpers/index.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; + +describe("Operator Lifecycle", function () { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Register Operator (Public, Non-Zero Fee)", () => { + it("Registers public operator with non-zero fee and verifies initial state", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const pubkey = makeOperatorKey(1); + const fee = 1_778_800_000n; // DEFAULT_OPERATOR_ETH_FEE + + const tx = await network + .connect(operatorOwner) + .registerOperator(pubkey, fee, false); + const receipt = await tx.wait(); + const regBlock = BigInt(receipt!.blockNumber); + + const opData = await views.getOperatorById(1n); + expect(opData.owner).to.equal(operatorOwner.address); + expect(opData.fee).to.equal(fee); + expect(opData.validatorCount).to.equal(0n); + expect(opData.isPrivate).to.equal(false); + expect(opData.isActive).to.equal(true); + + const earnings = await views.getOperatorEarnings(1n); + expect(earnings).to.equal(0n); + + await expect(tx) + .to.emit(network, Events.OPERATOR_ADDED) + .withArgs(1n, operatorOwner.address, pubkey, fee); + await expect(tx) + .to.emit(network, Events.OPERATOR_PRIVACY_STATUS_UPDATED) + .withArgs([1n], false); + }); + + it("Register with fee=0 succeeds, operator is free forever", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const pubkey = makeOperatorKey(1); + + await network + .connect(operatorOwner) + .registerOperator(pubkey, 0n, false); + + const opData = await views.getOperatorById(1n); + expect(opData.fee).to.equal(0n); + expect(opData.isPrivate).to.equal(false); + + await expect( + network + .connect(operatorOwner) + .declareOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE), + ).to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + + it("Register with setPrivate=true sets whitelisted flag", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const pubkey = makeOperatorKey(1); + + const tx = await network + .connect(operatorOwner) + .registerOperator(pubkey, MINIMAL_OPERATOR_ETH_FEE, true); + + const opData = await views.getOperatorById(1n); + expect(opData.isPrivate).to.equal(true); + + await expect(tx) + .to.emit(network, Events.OPERATOR_PRIVACY_STATUS_UPDATED) + .withArgs([1n], true); + }); + + it("Register with same pubkey again reverts OperatorAlreadyExists", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const pubkey = makeOperatorKey(1); + await network + .connect(operatorOwner) + .registerOperator(pubkey, MINIMAL_OPERATOR_ETH_FEE, false); + + await expect( + network + .connect(operatorOwner) + .registerOperator(pubkey, MINIMAL_OPERATOR_ETH_FEE, false), + ).to.be.revertedWithCustomError(network, Errors.OPERATOR_ALREADY_EXISTS); + }); + + it("Register with fee not divisible by ETH_DEDUCTED_DIGITS reverts MaxPrecisionExceeded", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const pubkey = makeOperatorKey(1); + const badFee = MINIMAL_OPERATOR_ETH_FEE + 1n; + + await expect( + network + .connect(operatorOwner) + .registerOperator(pubkey, badFee, false), + ).to.be.revertedWithCustomError(network, Errors.MAX_PRECISION_EXCEEDED); + }); + }); + + describe("Register Operator (Private, Zero Fee)", () => { + it("Registers private zero-fee operator and verifies fee immutability", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const pubkey = makeOperatorKey(1); + + await network + .connect(operatorOwner) + .registerOperator(pubkey, 0n, true); + + const opData = await views.getOperatorById(1n); + expect(opData.fee).to.equal(0n); + expect(opData.isPrivate).to.equal(true); + + await expect( + network + .connect(operatorOwner) + .declareOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE), + ).to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + }); + + describe("ensureETHDefaults — Default Fee Assignment", () => { + it("Operator registered with non-zero fee gets correct ethFee", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const fee = MINIMAL_OPERATOR_ETH_FEE; + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), fee, false); + + const opData = await views.getOperatorById(1n); + expect(opData.fee).to.equal(fee); + expect(opData.isActive).to.equal(true); + }); + + it("Operator registered with fee=0 and SSV fee=0 stays free", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), 0n, false); + + const opData = await views.getOperatorById(1n); + expect(opData.fee).to.equal(0n); + }); + }); + + describe("Operator Fee Declaration -> Wait -> Execution", () => { + it("Declares fee, waits, and executes within approval window", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const initialFee = MINIMAL_OPERATOR_ETH_FEE; + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), initialFee, false); + + for (let i = 2; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), initialFee, false); + } + + await whitelistAddresses(network, operatorOwner, [1, 2, 3, 4], [ + clusterOwner.address, + ]); + + await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + [1, 2, 3, 4], + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE}, + ); + + const currentFee = await views.getOperatorFee(1n); + const currentPacked = currentFee / ETH_DEDUCTED_DIGITS; + const maxIncreaseBps = await views.getOperatorFeeIncreaseLimit(); + const maxAllowedPacked = + (currentPacked * (10_000n + maxIncreaseBps) + 9_999n) / 10_000n; + const newFee = maxAllowedPacked * ETH_DEDUCTED_DIGITS; + + const declareTx = await network + .connect(operatorOwner) + .declareOperatorFee(1n, newFee); + await declareTx.wait(); + + await expect(declareTx).to.emit(network, Events.OPERATOR_FEE_DECLARED); + + await expect( + network.connect(operatorOwner).executeOperatorFee(1n), + ).to.be.revertedWithCustomError( + network, + Errors.APPROVAL_NOT_WITHIN_TIMEFRAME, + ); + + const declareFeePeriod = Number(DECLARE_OPERATOR_FEE_PERIOD); + await provider.send("evm_increaseTime", [declareFeePeriod + 1]); + await provider.send("evm_mine", []); + + const executeTx = await network + .connect(operatorOwner) + .executeOperatorFee(1n); + await executeTx.wait(); + + await expect(executeTx).to.emit(network, Events.OPERATOR_FEE_EXECUTED); + + const updatedFee = await views.getOperatorFee(1n); + expect(updatedFee).to.equal(newFee); + }); + + it("Execute after approval window expires reverts", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + const currentFee = await views.getOperatorFee(1n); + const currentPacked = currentFee / ETH_DEDUCTED_DIGITS; + const maxIncreaseBps = await views.getOperatorFeeIncreaseLimit(); + const maxAllowedPacked = + (currentPacked * (10_000n + maxIncreaseBps) + 9_999n) / 10_000n; + const newFee = maxAllowedPacked * ETH_DEDUCTED_DIGITS; + + await network + .connect(operatorOwner) + .declareOperatorFee(1n, newFee); + + const totalPeriod = + Number(DECLARE_OPERATOR_FEE_PERIOD) + + Number(EXECUTE_OPERATOR_FEE_PERIOD) + + 1; + await provider.send("evm_increaseTime", [totalPeriod]); + await provider.send("evm_mine", []); + + await expect( + network.connect(operatorOwner).executeOperatorFee(1n), + ).to.be.revertedWithCustomError( + network, + Errors.APPROVAL_NOT_WITHIN_TIMEFRAME, + ); + }); + + it("Edge: cancel declared fee clears the request", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + const currentFee = await views.getOperatorFee(1n); + const currentPacked = currentFee / ETH_DEDUCTED_DIGITS; + const maxIncreaseBps = await views.getOperatorFeeIncreaseLimit(); + const maxAllowedPacked = + (currentPacked * (10_000n + maxIncreaseBps) + 9_999n) / 10_000n; + const newFee = maxAllowedPacked * ETH_DEDUCTED_DIGITS; + + await network + .connect(operatorOwner) + .declareOperatorFee(1n, newFee); + + const cancelTx = await network + .connect(operatorOwner) + .cancelDeclaredOperatorFee(1n); + await expect(cancelTx).to.emit( + network, + Events.OPERATOR_FEE_DECLARATION_CANCELLED, + ); + + await expect( + network.connect(operatorOwner).executeOperatorFee(1n), + ).to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + + it("Fee increase exceeding limit reverts FeeExceedsIncreaseLimit", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + const currentFee = await views.getOperatorFee(1n); + const currentPacked = currentFee / ETH_DEDUCTED_DIGITS; + const maxIncreaseBps = await views.getOperatorFeeIncreaseLimit(); + const maxAllowedPacked = + (currentPacked * (10_000n + maxIncreaseBps) + 9_999n) / 10_000n; + const excessiveFee = (maxAllowedPacked + 1n) * ETH_DEDUCTED_DIGITS; + + const maxOperatorFee = await views.getMaximumOperatorFee(); + if (excessiveFee <= maxOperatorFee) { + await expect( + network + .connect(operatorOwner) + .declareOperatorFee(1n, excessiveFee), + ).to.be.revertedWithCustomError( + network, + Errors.FEE_EXCEEDS_INCREASE_LIMIT, + ); + } + }); + }); + + describe("Operator Fee Reduction (Immediate, No Timelock)", () => { + it("Reduces fee immediately, preserving earnings at old fee", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const initialFee = 2_000_000_000n; + const packedInitialFee = initialFee / ETH_DEDUCTED_DIGITS; + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), initialFee, false); + for (let i = 2; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), initialFee, false); + } + await whitelistAddresses(network, operatorOwner, [1, 2, 3, 4], [ + clusterOwner.address, + ]); + + await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + [1, 2, 3, 4], + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const regBlock = BigInt(await getBlockNumber(provider)); + + await mineBlocks(provider, 100); + + const reducedFee = MINIMAL_OPERATOR_ETH_FEE; + const reduceTx = await network + .connect(operatorOwner) + .reduceOperatorFee(1n, reducedFee); + const reduceBlock = BigInt(await getTxBlock(reduceTx)); + + await expect(reduceTx).to.emit(network, Events.OPERATOR_FEE_EXECUTED); + + const newFee = await views.getOperatorFee(1n); + expect(newFee).to.equal(reducedFee); + + const blockDiff = reduceBlock - regBlock; + const vUnits = defaultVUnits(1n); + const expectedEarnings = calcOperatorFeeAccrual(blockDiff, packedInitialFee, vUnits) * ETH_DEDUCTED_DIGITS; + const earnings = await views.getOperatorEarnings(1n); + expect(earnings).to.equal(expectedEarnings); + }); + + it("Reduce to exactly current fee reverts FeeIncreaseNotAllowed", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + await expect( + network + .connect(operatorOwner) + .reduceOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE), + ).to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + + it("Reduce to higher fee reverts FeeIncreaseNotAllowed", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + const higherFee = MINIMAL_OPERATOR_ETH_FEE + ETH_DEDUCTED_DIGITS; + await expect( + network + .connect(operatorOwner) + .reduceOperatorFee(1n, higherFee), + ).to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + + it("Reducing fee clears pending fee change request", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + const currentFee = await views.getOperatorFee(1n); + const currentPacked = currentFee / ETH_DEDUCTED_DIGITS; + const maxIncreaseBps = await views.getOperatorFeeIncreaseLimit(); + const maxAllowedPacked = + (currentPacked * (10_000n + maxIncreaseBps) + 9_999n) / 10_000n; + const newFee = maxAllowedPacked * ETH_DEDUCTED_DIGITS; + + await network + .connect(operatorOwner) + .declareOperatorFee(1n, newFee); + + const declareFeePeriod = Number(DECLARE_OPERATOR_FEE_PERIOD); + await connection.ethers.provider.send("evm_increaseTime", [declareFeePeriod + 1]); + await connection.ethers.provider.send("evm_mine", []); + await network + .connect(operatorOwner) + .executeOperatorFee(1n); + + const updatedFee = await views.getOperatorFee(1n); + const updatedPacked = updatedFee / ETH_DEDUCTED_DIGITS; + const maxIncreaseBps2 = await views.getOperatorFeeIncreaseLimit(); + const maxAllowedPacked2 = + (updatedPacked * (10_000n + maxIncreaseBps2) + 9_999n) / 10_000n; + const newFee2 = maxAllowedPacked2 * ETH_DEDUCTED_DIGITS; + await network + .connect(operatorOwner) + .declareOperatorFee(1n, newFee2); + + await network + .connect(operatorOwner) + .reduceOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE); + + await expect( + network.connect(operatorOwner).executeOperatorFee(1n), + ).to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + }); + + describe("Operator Earnings Accumulation and Withdrawal", () => { + it("Accumulates earnings and supports partial + full withdrawal", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + for (let i = 1; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), fee, false); + } + + await whitelistAddresses(network, operatorOwner, [1, 2, 3, 4], [ + clusterOwner.address, + ]); + + const regTx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + [1, 2, 3, 4], + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + const vUnits = defaultVUnits(1n); + + await mineBlocks(provider, 100); + + const earningsBlock = BigInt(await getBlockNumber(provider)); + const expectedEarningsBefore = calcOperatorFeeAccrual(earningsBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + const earningsBefore = await views.getOperatorEarnings(1n); + expect(earningsBefore).to.equal(expectedEarningsBefore); + + const partialAmount = earningsBefore / 2n; + const alignedPartial = + (partialAmount / ETH_DEDUCTED_DIGITS) * ETH_DEDUCTED_DIGITS; + + const ownerBalBefore = await provider.getBalance( + operatorOwner.address, + ); + + const partialTx = await network + .connect(operatorOwner) + .withdrawOperatorEarnings(1n, alignedPartial); + const partialReceipt = await partialTx.wait(); + const partialGas = + partialReceipt!.gasUsed * partialReceipt!.gasPrice; + + await expect(partialTx).to.emit(network, Events.OPERATOR_WITHDRAWN); + + const ownerBalAfter = await provider.getBalance( + operatorOwner.address, + ); + expect(ownerBalAfter - ownerBalBefore + partialGas).to.equal( + alignedPartial, + ); + + await mineBlocks(provider, 100); + + const fullViewBlock = BigInt(await getBlockNumber(provider)); + const expectedEarningsBeforeFull = + calcOperatorFeeAccrual(fullViewBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS - alignedPartial; + const earningsBeforeFull = await views.getOperatorEarnings(1n); + expect(earningsBeforeFull).to.equal(expectedEarningsBeforeFull); + + const ownerBalBefore2 = await provider.getBalance( + operatorOwner.address, + ); + const fullTx = await network + .connect(operatorOwner) + .withdrawAllOperatorEarnings(1n); + const fullReceipt = await fullTx.wait(); + const fullGas = fullReceipt!.gasUsed * fullReceipt!.gasPrice; + const fullBlock = BigInt(await getTxBlock(fullTx)); + + const ownerBalAfter2 = await provider.getBalance( + operatorOwner.address, + ); + + const expectedFullTransfer = + calcOperatorFeeAccrual(fullBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS - alignedPartial; + expect(ownerBalAfter2 - ownerBalBefore2 + fullGas).to.equal( + expectedFullTransfer, + ); + }); + }); + + describe("Operator Fee Reduction — Legacy SSV Operator Edge Cases", () => { + it("Legacy SSV operator can reduce ethFee to 0 without getting default on migration", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + // This test simulates this scenario: + // 1. Legacy SSV operator (simulated by manually clearing ethSnapshot) + // 2. Operator reduces fee to 0 via reduceOperatorFee + // 3. This should initialize ethSnapshot.block > 0 + // 4. Later cluster migration should NOT overwrite ethFee to default + + const initialFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), initialFee, false); + + // Note: In real scenario, legacy operators would have ethSnapshot.block == 0 after upgrade + // For this test, we rely on the contract implementation to handle this correctly + + for (let i = 2; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), initialFee, false); + } + + await whitelistAddresses(network, operatorOwner, [1, 2, 3, 4], [ + clusterOwner.address, + ]); + + // Reduce operator 1's fee to 0 + await network + .connect(operatorOwner) + .reduceOperatorFee(1n, 0n); + + const opFeeAfterReduce = await views.getOperatorFee(1n); + expect(opFeeAfterReduce).to.equal(0n, "Fee should be 0 after reduction"); + + // Register validator (this may trigger ensureETHDefaults) + await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + [1, 2, 3, 4], + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + // Operator 1 should STILL have fee = 0 (not overwritten to default) + const opFeeAfterRegister = await views.getOperatorFee(1n); + expect(opFeeAfterRegister).to.equal(0n, "Fee should remain 0 after validator registration"); + + await mineBlocks(provider, 100); + + // Operator 1 should have 0 earnings (zero fee) + const earnings1 = await views.getOperatorEarnings(1n); + expect(earnings1).to.equal(0n, "Operator with fee=0 should have no earnings"); + + // Operator 2 should have normal earnings + const earnings2 = await views.getOperatorEarnings(2n); + expect(earnings2).to.be.greaterThan(0n, "Operator with non-zero fee should have earnings"); + }); + + it("Operator reduces fee immediately after registration (ethSnapshot already initialized)", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const initialFee = MINIMAL_OPERATOR_ETH_FEE * 3n; + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), initialFee, false); + + // ethSnapshot.block should be > 0 after registration + const opData = await views.getOperatorById(1n); + expect(opData.fee).to.equal(initialFee); + + // Reduce fee immediately + const reducedFee = MINIMAL_OPERATOR_ETH_FEE; + await network + .connect(operatorOwner) + .reduceOperatorFee(1n, reducedFee); + + const opFeeAfter = await views.getOperatorFee(1n); + expect(opFeeAfter).to.equal(reducedFee, "Fee should be reduced"); + }); + + it("Operator can reduce to 0 then cannot increase via declareOperatorFee", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + const initialFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), initialFee, false); + + // Reduce to 0 + await network + .connect(operatorOwner) + .reduceOperatorFee(1n, 0n); + + const opFeeAfter = await views.getOperatorFee(1n); + expect(opFeeAfter).to.equal(0n); + + // Try to increase via declareOperatorFee + await expect( + network + .connect(operatorOwner) + .declareOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE), + ).to.be.revertedWithCustomError( + network, + Errors.FEE_INCREASE_NOT_ALLOWED, + ); + }); + }); + + describe("Remove Operator — Full Cleanup and Final Withdrawal", () => { + it("Removes operator with earnings, transfers funds, and cleans up state", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + + for (let i = 1; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), fee, false); + } + + await network + .connect(operatorOwner) + .setOperatorsPrivateUnchecked([1n]); + + await whitelistAddresses(network, operatorOwner, [1, 2, 3, 4], [ + clusterOwner.address, + ]); + + const valRegTx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + [1, 2, 3, 4], + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = BigInt(await getTxBlock(valRegTx)); + const packedFee = fee / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + + await mineBlocks(provider, 50); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + [1, 2, 3, 4], + ); + const removeValTx = await network + .connect(clusterOwner) + .removeValidator(makePublicKey(1), [1, 2, 3, 4], cluster); + const removeValBlock = BigInt(await getTxBlock(removeValTx)); + + const expectedEarnings = calcOperatorFeeAccrual(removeValBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + const earningsBefore = await views.getOperatorEarnings(1n); + expect(earningsBefore).to.equal(expectedEarnings); + + const ownerBalBefore = await provider.getBalance( + operatorOwner.address, + ); + const removeTx = await network + .connect(operatorOwner) + .removeOperator(1n); + const removeReceipt = await removeTx.wait(); + const removeGas = + removeReceipt!.gasUsed * removeReceipt!.gasPrice; + + await expect(removeTx).to.emit(network, Events.OPERATOR_REMOVED).withArgs(1n); + await expect(removeTx).to.emit(network, Events.OPERATOR_WITHDRAWN); + + const ownerBalAfter = await provider.getBalance( + operatorOwner.address, + ); + expect(ownerBalAfter - ownerBalBefore + removeGas).to.equal( + expectedEarnings, + ); + + const opData = await views.getOperatorById(1n); + expect(opData.isActive).to.equal(false); + + expect(opData.owner).to.equal(operatorOwner.address); + }); + + it("Remove operator with 0 earnings in both versions", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + + const earningsBefore = await views.getOperatorEarnings(1n); + expect(earningsBefore).to.equal(0n); + + const removeTx = await network + .connect(operatorOwner) + .removeOperator(1n); + await expect(removeTx).to.emit(network, Events.OPERATOR_REMOVED).withArgs(1n); + }); + + it("After removal, registering validator with removed operator reverts", async () => { + const { network } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + for (let i = 1; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), MINIMAL_OPERATOR_ETH_FEE, false); + } + + await whitelistAddresses(network, operatorOwner, [1, 2, 3, 4], [ + clusterOwner.address, + ]); + + await network + .connect(operatorOwner) + .removeOperator(1n); + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(1), + [1, 2, 3, 4], + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Double removal reverts OperatorDoesNotExist", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + await network.connect(operatorOwner).removeOperator(1n); + + await expect( + network.connect(operatorOwner).removeOperator(1n), + ).to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + }); +}); diff --git a/test/e2e/operators/operator-reverts.test.ts b/test/e2e/operators/operator-reverts.test.ts new file mode 100644 index 000000000..027fd3588 --- /dev/null +++ b/test/e2e/operators/operator-reverts.test.ts @@ -0,0 +1,157 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + makeOperatorKey, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, +} from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; + +describe("Operator Reverts", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, otherAccount] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + + describe("Register Validator — Operator Revert Cases", () => { + it("Reverts with OperatorAlreadyExists when registering operator with same pubkey", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const pubkey = makeOperatorKey(1); + await network + .connect(operatorOwner) + .registerOperator(pubkey, MINIMAL_OPERATOR_ETH_FEE, false); + + await expect( + network + .connect(operatorOwner) + .registerOperator(pubkey, MINIMAL_OPERATOR_ETH_FEE, false), + ).to.be.revertedWithCustomError( + network, + Errors.OPERATOR_ALREADY_EXISTS, + ); + }); + + it("Reverts with OperatorDoesNotExist when registering validator with removed operator", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOperators( + network, + operatorOwner, + 4, + ); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(operatorOwner).removeOperator(operatorIds[0]); + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.OPERATOR_DOES_NOT_EXIST, + ); + }); + + it("Reverts with CallerNotWhitelistedWithData when registering on private operator without whitelist", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators( + network, + operatorOwner, + 4, + ); + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.CALLER_NOT_WHITELISTED, + ); + }); + }); + + + describe("Operator Remove Revert Cases", () => { + it("Reverts with OperatorDoesNotExist when removing non-existent operator", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + await expect( + network.connect(operatorOwner).removeOperator(999), + ).to.be.revertedWithCustomError( + network, + Errors.OPERATOR_DOES_NOT_EXIST, + ); + }); + + it("Reverts with CallerNotOwnerWithData when non-owner tries to remove operator", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators( + network, + operatorOwner, + 1, + ); + + await expect( + network.connect(otherAccount).removeOperator(operatorIds[0]), + ).to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER); + }); + + it("Reverts with OperatorDoesNotExist when removing already-removed operator", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators( + network, + operatorOwner, + 1, + ); + + await network.connect(operatorOwner).removeOperator(operatorIds[0]); + + await expect( + network.connect(operatorOwner).removeOperator(operatorIds[0]), + ).to.be.revertedWithCustomError( + network, + Errors.OPERATOR_DOES_NOT_EXIST, + ); + }); + }); +}); diff --git a/test/e2e/smoke.test.ts b/test/e2e/smoke.test.ts new file mode 100644 index 000000000..9a2380501 --- /dev/null +++ b/test/e2e/smoke.test.ts @@ -0,0 +1,85 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + setupTestContext, +} from "../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE_ETH, +} from "../common/constants.ts"; +import { + mineBlocks, + getBlockNumber, + calcClusterBurn, + defaultVUnits, + snapshotContractBalance, +} from "../helpers/index.ts"; + +describe("E2E Smoke Test", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + it("Deploys, registers, mines blocks, and computes fees correctly", async function () { + const { network } = + await networkHelpers.loadFixture(deployFixture); + + const provider = connection.ethers.provider; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkAddress = await network.getAddress(); + const balanceBefore = await snapshotContractBalance(provider, networkAddress); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const balanceAfter = await snapshotContractBalance(provider, networkAddress); + expect(balanceAfter - balanceBefore).to.equal(DEFAULT_ETH_REGISTER_VALUE); + + const blockBefore = await getBlockNumber(provider); + + await mineBlocks(provider, 10); + + const blockAfter = await getBlockNumber(provider); + expect(blockAfter - blockBefore).to.equal(10); + + const vUnits = defaultVUnits(1n); + const expectedBurn = calcClusterBurn({ + blockDiff: 10n, + numOperators: 4n, + ethFee: MINIMAL_OPERATOR_ETH_FEE, + networkFee: NETWORK_FEE_ETH, + effectiveVUnits: vUnits, + }); + + expect(expectedBurn).to.be.a("bigint"); + expect(expectedBurn).to.be.greaterThan(0n); + }); +}); diff --git a/test/e2e/staking/staking-edge-cases.test.ts b/test/e2e/staking/staking-edge-cases.test.ts new file mode 100644 index 000000000..f34945139 --- /dev/null +++ b/test/e2e/staking/staking-edge-cases.test.ts @@ -0,0 +1,612 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + NETWORK_FEE, + BPS_DENOMINATOR, + ETH_DEDUCTED_DIGITS, + DEFAULT_UNSTAKE_COOLDOWN, +} from "../../common/constants.ts"; +import { + mineBlocks, + getTxBlock, + calcAccEthPerShareDelta, + calcStakingReward, + defaultVUnits, +} from "../../helpers/index.ts"; + +const PRECISION = 10n ** 18n; +const PACKED_NETWORK_FEE = NETWORK_FEE / ETH_DEDUCTED_DIGITS; +const MINIMAL_STAKING_AMOUNT = 1_000_000_000n; +const MAX_PENDING_REQUESTS = 2000; + +describe("E2E Staking Edge Cases", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let provider: any; + + let deployer: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let stakerA: HardhatEthersSigner; + let stakerB: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [deployer, operatorOwner, clusterOwner, stakerA, stakerB] } = await setupTestContext()); + provider = connection.ethers.provider; + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Accumulator Edge Cases", () => { + it("Zero cSSV supply — fees are unclaimable", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 100); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 100); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const receipt = await claimTx.wait(); + const claimBlock = receipt!.blockNumber; + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const reward = BigInt(balAfter) - balBefore + gasUsed; + + const postStakeBlocks = BigInt(claimBlock - stakeBlock); + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const expectedFeesPacked = earningsPerBlockPacked * postStakeBlocks; + const expectedFeesWei = expectedFeesPacked * ETH_DEDUCTED_DIGITS; + + const accDelta = calcAccEthPerShareDelta(expectedFeesWei, stakeAmount); + const expectedReward = calcStakingReward(stakeAmount, accDelta, 0n); + const expectedPayout = + expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + expect(reward).to.equal(expectedPayout); + }); + + it("accEthPerShare monotonicity — never decreases", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const accValues: bigint[] = []; + + for (let i = 0; i < 5; i++) { + await mineBlocks(provider, 20); + const tx = await network.connect(stakerA).syncFees(); + const receipt = await tx.wait(); + + const feesSyncedLog = receipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + + if (feesSyncedLog) { + const parsed = network.interface.parseLog(feesSyncedLog); + accValues.push(BigInt(parsed!.args[1])); + } + } + + for (let i = 1; i < accValues.length; i++) { + expect(accValues[i]).to.be.greaterThanOrEqual(accValues[i - 1]); + } + }); + + it("Dust accumulation — dust eventually claimable", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 3n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + let totalClaimed = 0n; + let lastClaimBlock = stakeBlock; + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + let cumulativeAcc = 0n; + + for (let i = 0; i < 3; i++) { + await mineBlocks(provider, 50); + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const receipt = await claimTx.wait(); + const claimBlock = receipt!.blockNumber; + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + const claimed = BigInt(balAfter) - balBefore + gasUsed; + totalClaimed += claimed; + + const blockDiff = BigInt(claimBlock - lastClaimBlock); + const feesWei = earningsPerBlockPacked * blockDiff * ETH_DEDUCTED_DIGITS; + const accDelta = calcAccEthPerShareDelta(feesWei, stakeAmount); + cumulativeAcc += accDelta; + lastClaimBlock = claimBlock; + + expect(claimed % ETH_DEDUCTED_DIGITS).to.equal(0n); + } + + const expectedTotal = calcStakingReward(stakeAmount, cumulativeAcc, 0n); + const expectedPayout = expectedTotal - (expectedTotal % ETH_DEDUCTED_DIGITS); + expect(totalClaimed).to.equal(expectedPayout); + expect(totalClaimed % ETH_DEDUCTED_DIGITS).to.equal(0n); + }); + }); + + describe("MAX_PENDING_REQUESTS (2000)", () => { + it("Should allow exactly 2000 pending requests and revert on 2001", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 2200n * PRECISION; + const deployerBal = await ssvToken.balanceOf(deployer.address); + if (deployerBal < stakeAmount) { + await ssvToken + .connect(deployer) + .mint(deployer.address, stakeAmount - deployerBal); + } + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const unstakeAmount = 1n * PRECISION; + for (let i = 0; i < MAX_PENDING_REQUESTS; i++) { + await network.connect(stakerA).requestUnstake(unstakeAmount); + } + + await expect( + network.connect(stakerA).requestUnstake(unstakeAmount), + ).to.be.revertedWithCustomError(network, Errors.MAX_REQUESTS_AMOUNT_REACHED); + }); + + it("Withdrawing unlocked requests frees slots for new requests", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 2200n * PRECISION; + const deployerBal = await ssvToken.balanceOf(deployer.address); + if (deployerBal < stakeAmount) { + await ssvToken + .connect(deployer) + .mint(deployer.address, stakeAmount - deployerBal); + } + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const unstakeAmount = 1n * PRECISION; + for (let i = 0; i < MAX_PENDING_REQUESTS; i++) { + await network.connect(stakerA).requestUnstake(unstakeAmount); + } + + const cooldownSeconds = Number(DEFAULT_UNSTAKE_COOLDOWN); + await provider.send("evm_increaseTime", [cooldownSeconds + 1]); + await mineBlocks(provider, 1); + + const ssvBefore = await ssvToken.balanceOf(stakerA.address); + await network.connect(stakerA).withdrawUnlocked(); + const ssvAfter = await ssvToken.balanceOf(stakerA.address); + + expect(ssvAfter - ssvBefore).to.equal( + unstakeAmount * BigInt(MAX_PENDING_REQUESTS), + ); + + await network.connect(stakerA).requestUnstake(unstakeAmount); + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + stakeAmount - + unstakeAmount * BigInt(MAX_PENDING_REQUESTS) - + unstakeAmount, + ); + }); + }); + + describe("MINIMAL_STAKING_AMOUNT", () => { + it("Should revert with StakeTooLow for stake(0)", async function () { + const { network } = + await networkHelpers.loadFixture(deployFixture); + + await expect( + network.connect(stakerA).stake(0n), + ).to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + + it("Should revert with StakeTooLow for amount below minimum", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const belowMinimum = MINIMAL_STAKING_AMOUNT - 1n; + await ssvToken.connect(deployer).transfer(stakerA.address, belowMinimum); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), belowMinimum); + + await expect( + network.connect(stakerA).stake(belowMinimum), + ).to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + + it("Should succeed at exactly MINIMAL_STAKING_AMOUNT", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const exact = MINIMAL_STAKING_AMOUNT; + await ssvToken.connect(deployer).transfer(stakerA.address, exact); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), exact); + + await network.connect(stakerA).stake(exact); + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(exact); + }); + }); + + describe("syncFees() Public Function", () => { + it("Should update accEthPerShare without settling any user's rewards", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 100); + + const tx = await network.connect(deployer).syncFees(); + const receipt = await tx.wait(); + const syncBlock = receipt!.blockNumber; + + const feesSyncedLog = receipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + expect(feesSyncedLog).to.not.be.undefined; + + const parsed = network.interface.parseLog(feesSyncedLog); + const newFeesWei = BigInt(parsed!.args[0]); + const accEthPerShare = BigInt(parsed!.args[1]); + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const blockDiff = BigInt(syncBlock - stakeBlock); + const expectedFeesWei = earningsPerBlockPacked * blockDiff * ETH_DEDUCTED_DIGITS; + const expectedAcc = calcAccEthPerShareDelta(expectedFeesWei, stakeAmount); + + expect(newFeesWei).to.equal(expectedFeesWei); + expect(accEthPerShare).to.equal(expectedAcc); + + const settleLog = receipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.REWARDS_SETTLED; + } catch { + return false; + } + }); + expect(settleLog).to.be.undefined; + }); + + it("Anyone can call syncFees (not restricted to stakers)", async function () { + const { network } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 50); + + const tx = await network.connect(stakerB).syncFees(); + const receipt = await tx.wait(); + + const feesSyncedLog = receipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + expect(feesSyncedLog).to.not.be.undefined; + }); + }); + + describe("requestUnstake Followed by Immediate Claim", () => { + it("Both requestUnstake and claimEthRewards can be called in same block context", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 100); + + const unstakeBlock = await getTxBlock( + await network.connect(stakerA).requestUnstake(5n * PRECISION), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + 5n * PRECISION, + ); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const receipt = await claimTx.wait(); + const claimBlock = receipt!.blockNumber; + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const reward = BigInt(balAfter) - balBefore + gasUsed; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const phase1Blocks = BigInt(unstakeBlock - stakeBlock); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, stakeAmount); + const settledReward = calcStakingReward(stakeAmount, acc1, 0n); + + const phase2Blocks = BigInt(claimBlock - unstakeBlock); + const phase2FeesWei = earningsPerBlockPacked * phase2Blocks * ETH_DEDUCTED_DIGITS; + const remainingBalance = 5n * PRECISION; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, remainingBalance); + const postUnstakeReward = calcStakingReward(remainingBalance, acc2, 0n); + const expectedReward = settledReward + postUnstakeReward; + const expectedPayout = expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + expect(reward).to.equal(expectedPayout); + expect(reward % ETH_DEDUCTED_DIGITS).to.equal(0n); + }); + + it("Claiming twice in the same block only pays once", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 100); + + const balBefore = await provider.getBalance(stakerA.address); + const baseNonce = await provider.getTransactionCount( + stakerA.address, + "pending", + ); + const networkAddress = await network.getAddress(); + const claimRewardsData = + network.interface.encodeFunctionData("claimEthRewards"); + + await provider.send("evm_setAutomine", [false]); + + let firstClaimTx; + let secondClaimTx; + let firstReceipt; + let secondReceipt; + + try { + firstClaimTx = await stakerA.sendTransaction({ + to: networkAddress, + data: claimRewardsData, + gasLimit: 1_000_000n, + nonce: baseNonce, + }); + secondClaimTx = await stakerA.sendTransaction({ + to: networkAddress, + data: claimRewardsData, + gasLimit: 1_000_000n, + nonce: baseNonce + 1, + }); + + await provider.send("evm_mine", []); + + firstReceipt = await firstClaimTx.wait(); + secondReceipt = await secondClaimTx.wait().catch((error: any) => { + return error.receipt; + }); + } finally { + await provider.send("evm_setAutomine", [true]); + } + + const balAfter = await provider.getBalance(stakerA.address); + const gasUsedFirst = firstReceipt!.gasUsed * firstReceipt!.gasPrice; + const gasUsedSecond = secondReceipt!.gasUsed * secondReceipt!.gasPrice; + const rewardPaid = + BigInt(balAfter) - balBefore + gasUsedFirst + gasUsedSecond; + + const claimBlock = firstReceipt!.blockNumber; + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = + (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const blockDiff = BigInt(claimBlock - stakeBlock); + const totalEarningsWei = + earningsPerBlockPacked * blockDiff * ETH_DEDUCTED_DIGITS; + const accDelta = calcAccEthPerShareDelta(totalEarningsWei, stakeAmount); + const expectedReward = calcStakingReward(stakeAmount, accDelta, 0n); + const expectedPayout = + expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + expect(firstReceipt!.status).to.equal(1); + expect(secondReceipt!.status).to.equal(0); + expect(firstReceipt!.blockNumber).to.equal(secondReceipt!.blockNumber); + expect(rewardPaid).to.equal(expectedPayout); + }); + }); +}); diff --git a/test/e2e/staking/staking-lifecycle.test.ts b/test/e2e/staking/staking-lifecycle.test.ts new file mode 100644 index 000000000..6fc166229 --- /dev/null +++ b/test/e2e/staking/staking-lifecycle.test.ts @@ -0,0 +1,717 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + NETWORK_FEE, + BPS_DENOMINATOR, + ETH_DEDUCTED_DIGITS, + DEFAULT_UNSTAKE_COOLDOWN, +} from "../../common/constants.ts"; +import { + mineBlocks, + getBlockNumber, + getTxBlock, + calcAccEthPerShareDelta, + calcStakingReward, + defaultVUnits, +} from "../../helpers/index.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; + +const PRECISION = 10n ** 18n; +const PACKED_NETWORK_FEE = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + +describe("E2E Staking Lifecycle", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let provider: any; + + let deployer: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let stakerA: HardhatEthersSigner; + let stakerB: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [deployer, operatorOwner, clusterOwner, stakerA, stakerB] } = await setupTestContext()); + provider = connection.ethers.provider; + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Basic Stake → Earn → Claim Cycle", () => { + it("Should allow a user to stake SSV, earn network fee revenue, and claim ETH rewards", async function () { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 50); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(stakeAmount); + expect(await cssvToken.totalSupply()).to.equal(stakeAmount); + + await mineBlocks(provider, 100); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const claimBlock = claimReceipt!.blockNumber; + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const vUnits = defaultVUnits(1n); + const blockDiff = BigInt(claimBlock - stakeBlock); + + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const totalEarningsPacked = earningsPerBlockPacked * blockDiff; + const totalEarningsWei = totalEarningsPacked * ETH_DEDUCTED_DIGITS; + + const accDelta = calcAccEthPerShareDelta(totalEarningsWei, stakeAmount); + const expectedReward = calcStakingReward(stakeAmount, accDelta, 0n); + const expectedPayout = expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + const ethReceived = BigInt(balAfter) - balBefore + gasUsed; + expect(ethReceived).to.equal(expectedPayout); + }); + + it("Pre-stake fees when cSSV supply is zero are permanently locked", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 50); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 100); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const claimBlock = claimReceipt!.blockNumber; + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const blockDiff = BigInt(claimBlock - stakeBlock); + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const totalEarningsPacked = earningsPerBlockPacked * blockDiff; + const totalEarningsWei = totalEarningsPacked * ETH_DEDUCTED_DIGITS; + + const accDelta = calcAccEthPerShareDelta(totalEarningsWei, stakeAmount); + const expectedReward = calcStakingReward(stakeAmount, accDelta, 0n); + const expectedPayout = expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + const ethReceived = BigInt(balAfter) - balBefore + gasUsed; + expect(ethReceived).to.equal(expectedPayout); + }); + }); + + describe("Multiple Stakers — Pro-Rata Distribution", () => { + it("Should distribute rewards proportionally: A gets 25%, B gets 75% with 10:30 ratio", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const amountA = 10n * PRECISION; + const amountB = 30n * PRECISION; + const totalStaked = amountA + amountB; + + await ssvToken.connect(deployer).transfer(stakerA.address, amountA); + await ssvToken.connect(deployer).transfer(stakerB.address, amountB); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), amountA); + await ssvToken + .connect(stakerB) + .approve(await network.getAddress(), amountB); + + const stakeBlockA = await getTxBlock( + await network.connect(stakerA).stake(amountA), + ); + + const stakeBlockB = await getTxBlock( + await network.connect(stakerB).stake(amountB), + ); + + await mineBlocks(provider, 100); + + const balBeforeA = await provider.getBalance(stakerA.address); + const claimTxA = await network.connect(stakerA).claimEthRewards(); + const claimReceiptA = await claimTxA.wait(); + const gasA = claimReceiptA!.gasUsed * claimReceiptA!.gasPrice; + const balAfterA = await provider.getBalance(stakerA.address); + const rewardA = BigInt(balAfterA) - balBeforeA + gasA; + + const balBeforeB = await provider.getBalance(stakerB.address); + const claimTxB = await network.connect(stakerB).claimEthRewards(); + const claimReceiptB = await claimTxB.wait(); + const gasB = claimReceiptB!.gasUsed * claimReceiptB!.gasPrice; + const balAfterB = await provider.getBalance(stakerB.address); + const rewardB = BigInt(balAfterB) - balBeforeB + gasB; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const phase1Blocks = BigInt(stakeBlockB - stakeBlockA); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, amountA); + + const claimBlockA = claimReceiptA!.blockNumber; + const claimBlockB = claimReceiptB!.blockNumber; + const phase2Blocks = BigInt(claimBlockA - stakeBlockB); + const phase2FeesWei = earningsPerBlockPacked * phase2Blocks * ETH_DEDUCTED_DIGITS; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, totalStaked); + + const expectedRewardA = calcStakingReward(amountA, acc1 + acc2, 0n); + const expectedPayoutA = expectedRewardA - (expectedRewardA % ETH_DEDUCTED_DIGITS); + expect(rewardA).to.equal(expectedPayoutA); + + const phase3Blocks = BigInt(claimBlockB - claimBlockA); + const phase3FeesWei = earningsPerBlockPacked * phase3Blocks * ETH_DEDUCTED_DIGITS; + const acc3 = calcAccEthPerShareDelta(phase3FeesWei, totalStaked); + + const expectedRewardB = calcStakingReward(amountB, acc2 + acc3, 0n); + const expectedPayoutB = expectedRewardB - (expectedRewardB % ETH_DEDUCTED_DIGITS); + expect(rewardB).to.equal(expectedPayoutB); + }); + + it("Three stakers split rewards correctly when one unstakes mid-period", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const allSigners = await connection.ethers.getSigners(); + const stakerC = allSigners[5]; + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const amountA = 10n * PRECISION; + const amountB = 10n * PRECISION; + const amountC = 10n * PRECISION; + const totalStakedPhase1 = amountA + amountB + amountC; + const unstakeAmount = 5n * PRECISION; + const totalStakedPhase2 = totalStakedPhase1 - unstakeAmount; + + await ssvToken.connect(deployer).transfer(stakerA.address, amountA); + await ssvToken.connect(deployer).transfer(stakerB.address, amountB); + await ssvToken.connect(deployer).transfer(stakerC.address, amountC); + await ssvToken.connect(stakerA).approve(await network.getAddress(), amountA); + await ssvToken.connect(stakerB).approve(await network.getAddress(), amountB); + await ssvToken.connect(stakerC).approve(await network.getAddress(), amountC); + + await network.connect(stakerA).stake(amountA); + await network.connect(stakerB).stake(amountB); + await network.connect(stakerC).stake(amountC); + + const regBlock = await getTxBlock( + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ); + + await mineBlocks(provider, 50); + + const unstakeBlock = await getTxBlock( + await network.connect(stakerA).requestUnstake(unstakeAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(amountA - unstakeAmount); + + await mineBlocks(provider, 50); + + const balBeforeA = await provider.getBalance(stakerA.address); + const balBeforeB = await provider.getBalance(stakerB.address); + const balBeforeC = await provider.getBalance(stakerC.address); + + let claimTxA: any; + let claimTxB: any; + let claimTxC: any; + + await provider.send("evm_setAutomine", [false]); + try { + claimTxA = await network.connect(stakerA).claimEthRewards(); + claimTxB = await network.connect(stakerB).claimEthRewards(); + claimTxC = await network.connect(stakerC).claimEthRewards(); + await provider.send("evm_mine", []); + } finally { + await provider.send("evm_setAutomine", [true]); + } + + const claimReceiptA = await claimTxA.wait(); + const claimReceiptB = await claimTxB.wait(); + const claimReceiptC = await claimTxC.wait(); + const claimBlock = claimReceiptA!.blockNumber; + + expect(claimReceiptB!.blockNumber).to.equal(claimBlock); + expect(claimReceiptC!.blockNumber).to.equal(claimBlock); + + const gasA = claimReceiptA!.gasUsed * claimReceiptA!.gasPrice; + const gasB = claimReceiptB!.gasUsed * claimReceiptB!.gasPrice; + const gasC = claimReceiptC!.gasUsed * claimReceiptC!.gasPrice; + + const balAfterA = await provider.getBalance(stakerA.address); + const balAfterB = await provider.getBalance(stakerB.address); + const balAfterC = await provider.getBalance(stakerC.address); + + const rewardA = BigInt(balAfterA) - balBeforeA + gasA; + const rewardB = BigInt(balAfterB) - balBeforeB + gasB; + const rewardC = BigInt(balAfterC) - balBeforeC + gasC; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const phase1Blocks = BigInt(unstakeBlock - regBlock); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, totalStakedPhase1); + + const phase2Blocks = BigInt(claimBlock - unstakeBlock); + const phase2FeesWei = earningsPerBlockPacked * phase2Blocks * ETH_DEDUCTED_DIGITS; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, totalStakedPhase2); + + const expectedRewardA = + calcStakingReward(amountA, acc1, 0n) + + calcStakingReward(amountA - unstakeAmount, acc2, 0n); + const expectedRewardB = + calcStakingReward(amountB, acc1, 0n) + + calcStakingReward(amountB, acc2, 0n); + const expectedRewardC = + calcStakingReward(amountC, acc1, 0n) + + calcStakingReward(amountC, acc2, 0n); + + const expectedPayoutA = expectedRewardA - (expectedRewardA % ETH_DEDUCTED_DIGITS); + const expectedPayoutB = expectedRewardB - (expectedRewardB % ETH_DEDUCTED_DIGITS); + const expectedPayoutC = expectedRewardC - (expectedRewardC % ETH_DEDUCTED_DIGITS); + + expect(rewardA).to.equal(expectedPayoutA); + expect(rewardB).to.equal(expectedPayoutB); + expect(rewardC).to.equal(expectedPayoutC); + expect(rewardB).to.equal(rewardC); + }); + }); + + describe("Stake Timing Matters — Late Joiner", () => { + it("Late joiner B does NOT capture fees from before they staked", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const amountA = 10n * PRECISION; + const amountB = 30n * PRECISION; + + await ssvToken.connect(deployer).transfer(stakerA.address, amountA); + await ssvToken.connect(deployer).transfer(stakerB.address, amountB); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), amountA); + await ssvToken + .connect(stakerB) + .approve(await network.getAddress(), amountB); + + const stakeBlockA = await getTxBlock( + await network.connect(stakerA).stake(amountA), + ); + + await mineBlocks(provider, 50); + + const stakeBlockB = await getTxBlock( + await network.connect(stakerB).stake(amountB), + ); + + await mineBlocks(provider, 50); + + const balBeforeA = await provider.getBalance(stakerA.address); + const claimTxA = await network.connect(stakerA).claimEthRewards(); + const claimReceiptA = await claimTxA.wait(); + const claimBlockA = claimReceiptA!.blockNumber; + const gasA = claimReceiptA!.gasUsed * claimReceiptA!.gasPrice; + const balAfterA = await provider.getBalance(stakerA.address); + const rewardA = BigInt(balAfterA) - balBeforeA + gasA; + + const balBeforeB = await provider.getBalance(stakerB.address); + const claimTxB = await network.connect(stakerB).claimEthRewards(); + const claimReceiptB = await claimTxB.wait(); + const claimBlockB = claimReceiptB!.blockNumber; + const gasB = claimReceiptB!.gasUsed * claimReceiptB!.gasPrice; + const balAfterB = await provider.getBalance(stakerB.address); + const rewardB = BigInt(balAfterB) - balBeforeB + gasB; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const totalSupply = amountA + amountB; + + const phase1Blocks = BigInt(stakeBlockB - stakeBlockA); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, amountA); + + const phase2Blocks = BigInt(claimBlockA - stakeBlockB); + const phase2FeesWei = earningsPerBlockPacked * phase2Blocks * ETH_DEDUCTED_DIGITS; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, totalSupply); + + const expectedRewardA = calcStakingReward(amountA, acc1 + acc2, 0n); + const expectedPayoutA = expectedRewardA - (expectedRewardA % ETH_DEDUCTED_DIGITS); + expect(rewardA).to.equal(expectedPayoutA); + + const phase3Blocks = BigInt(claimBlockB - claimBlockA); + const phase3FeesWei = earningsPerBlockPacked * phase3Blocks * ETH_DEDUCTED_DIGITS; + const acc3 = calcAccEthPerShareDelta(phase3FeesWei, totalSupply); + + const expectedRewardB = calcStakingReward(amountB, acc2 + acc3, 0n); + const expectedPayoutB = expectedRewardB - (expectedRewardB % ETH_DEDUCTED_DIGITS); + expect(rewardB).to.equal(expectedPayoutB); + }); + }); + + describe("Unstake Request → Cooldown → Withdraw", () => { + it("Should lock SSV during cooldown and allow withdrawal after", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await mineBlocks(provider, 50); + + const unstakeAmount = 5n * PRECISION; + const unstakeTx = await network + .connect(stakerA) + .requestUnstake(unstakeAmount); + const unstakeReceipt = await unstakeTx.wait(); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + stakeAmount - unstakeAmount, + ); + + const unstakeEvent = unstakeReceipt!.logs.find((l: any) => { + try { + return network.interface.parseLog(l)?.name === Events.UNSTAKE_REQUESTED; + } catch { + return false; + } + }); + expect(unstakeEvent).to.not.be.undefined; + + await expect( + network.connect(stakerA).withdrawUnlocked(), + ).to.be.revertedWithCustomError(network, Errors.NOTHING_TO_WITHDRAW); + + const cooldownSeconds = Number(DEFAULT_UNSTAKE_COOLDOWN); + await provider.send("evm_increaseTime", [cooldownSeconds + 1]); + await mineBlocks(provider, 1); + + const ssvBefore = await ssvToken.balanceOf(stakerA.address); + await network.connect(stakerA).withdrawUnlocked(); + const ssvAfter = await ssvToken.balanceOf(stakerA.address); + + expect(ssvAfter - ssvBefore).to.equal(unstakeAmount); + }); + + it("Rewards are settled with pre-burn balance during requestUnstake", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 100); + + const unstakeAmount = stakeAmount; + const unstakeBlock = await getTxBlock( + await network.connect(stakerA).requestUnstake(unstakeAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(0n); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const rewardClaimed = BigInt(balAfter) - balBefore + gasUsed; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const blockDiff = BigInt(unstakeBlock - stakeBlock); + const totalFeesWei = earningsPerBlockPacked * blockDiff * ETH_DEDUCTED_DIGITS; + const accDelta = calcAccEthPerShareDelta(totalFeesWei, stakeAmount); + const expectedReward = calcStakingReward(stakeAmount, accDelta, 0n); + const expectedPayout = expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + expect(rewardClaimed).to.equal(expectedPayout); + }); + + it("Cooldown changes do not alter reward accrual before and after requestUnstake", async function () { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + const unstakeAmount = 5n * PRECISION; + const remainingBalance = stakeAmount - unstakeAmount; + + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 50); + + const updatedCooldown = DEFAULT_UNSTAKE_COOLDOWN * 2n; + const cooldownUpdateTx = + await network.updateUnstakeCooldownDuration(updatedCooldown); + + await expect(cooldownUpdateTx) + .to.emit(network, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(updatedCooldown); + expect(await views.cooldownDuration()).to.equal(updatedCooldown); + + await mineBlocks(provider, 50); + + const unstakeBlock = await getTxBlock( + await network.connect(stakerA).requestUnstake(unstakeAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + remainingBalance, + ); + + await mineBlocks(provider, 50); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const claimBlock = claimReceipt!.blockNumber; + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const rewardClaimed = BigInt(balAfter) - balBefore + gasUsed; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const phase1Blocks = BigInt(unstakeBlock - stakeBlock); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, stakeAmount); + const reward1 = calcStakingReward(stakeAmount, acc1, 0n); + + const phase2Blocks = BigInt(claimBlock - unstakeBlock); + const phase2FeesWei = earningsPerBlockPacked * phase2Blocks * ETH_DEDUCTED_DIGITS; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, remainingBalance); + const reward2 = calcStakingReward(remainingBalance, acc2, 0n); + + expect(phase1Blocks).to.equal(102n); + expect(phase2Blocks).to.equal(51n); + + const expectedTotal = reward1 + reward2; + const expectedPayout = + expectedTotal - (expectedTotal % ETH_DEDUCTED_DIGITS); + + expect(rewardClaimed).to.equal(expectedPayout); + }); + + it("Burned cSSV stops earning rewards immediately", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 50); + + const unstakeBlock = await getTxBlock( + await network.connect(stakerA).requestUnstake(5n * PRECISION), + ); + + await mineBlocks(provider, 50); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const claimBlock = claimReceipt!.blockNumber; + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const totalReward = BigInt(balAfter) - balBefore + gasUsed; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const phase1Blocks = BigInt(unstakeBlock - stakeBlock); + const phase1FeesPacked = earningsPerBlockPacked * phase1Blocks; + const phase1FeesWei = phase1FeesPacked * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, stakeAmount); + const reward1 = calcStakingReward(stakeAmount, acc1, 0n); + + const phase2Blocks = BigInt(claimBlock - unstakeBlock); + const phase2FeesPacked = earningsPerBlockPacked * phase2Blocks; + const phase2FeesWei = phase2FeesPacked * ETH_DEDUCTED_DIGITS; + const remainingBalance = 5n * PRECISION; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, remainingBalance); + const reward2 = calcStakingReward(remainingBalance, acc2, 0n); + + const expectedTotal = reward1 + reward2; + const expectedPayout = + expectedTotal - (expectedTotal % ETH_DEDUCTED_DIGITS); + + expect(totalReward).to.equal(expectedPayout); + }); + }); +}); diff --git a/test/e2e/staking/staking-rewards.test.ts b/test/e2e/staking/staking-rewards.test.ts new file mode 100644 index 000000000..2f1dc0f6a --- /dev/null +++ b/test/e2e/staking/staking-rewards.test.ts @@ -0,0 +1,1434 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + getCurrentClusterState, + setupTestContext, +} from "../../common/helpers.ts"; +import { generateMerkleForClusterEB } from "../../helpers/oracle.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + NETWORK_FEE, + BPS_DENOMINATOR, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { + mineBlocks, + getBlockNumber, + getTxBlock, + calcAccEthPerShareDelta, + calcStakingReward, + calcVUnits, + defaultVUnits, +} from "../../helpers/index.ts"; +import { Events } from "../../common/events.ts"; + +const PRECISION = 10n ** 18n; +const PACKED_NETWORK_FEE = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + +describe("E2E Staking Rewards", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let provider: any; + + let deployer: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let stakerA: HardhatEthersSigner; + let stakerB: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [deployer, operatorOwner, clusterOwner, stakerA, stakerB] } = await setupTestContext()); + provider = connection.ethers.provider; + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + function computeClusterId(owner: string, operatorIds: number[]): string { + return connection.ethers.keccak256( + connection.ethers.solidityPacked( + ["address", "uint64[]"], + [owner, operatorIds], + ), + ); + } + + async function commitEBRoot( + network: any, + cssvToken: any, + oracles: HardhatEthersSigner[], + root: string, + blockNum: number, + ) { + for (let i = 0; i < 3; i++) { + await network.connect(oracles[i]).commitRoot(root, blockNum); + } + } + + describe("Network Fee Raise staking Rewards", () => { + it("Staking rewards increase after updateNetworkFee raises the ETH network fee", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerBlock = await getTxBlock(registerTx); + + const initialNetworkFee = await views.getNetworkFee(); + const vUnits = defaultVUnits(1n); + const initialPackedFee = initialNetworkFee / ETH_DEDUCTED_DIGITS; + + const baselineSyncTx = await network.connect(stakerA).syncFees(); + const baselineSyncBlock = await getTxBlock(baselineSyncTx); + const baselineSyncReceipt = await baselineSyncTx.wait(); + const baselineFeesLog = baselineSyncReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const baselineFees = baselineFeesLog + ? BigInt(network.interface.parseLog(baselineFeesLog)!.args[0]) + : 0n; + const baselineExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(baselineSyncBlock - registerBlock) * + ETH_DEDUCTED_DIGITS; + expect(baselineFees).to.equal(baselineExpectedFees); + + const phase1StartBlock = baselineSyncBlock; + await mineBlocks(provider, 100); + + const syncTx1 = await network.connect(stakerA).syncFees(); + const syncBlock1 = await getTxBlock(syncTx1); + const syncReceipt1 = await syncTx1.wait(); + const fees1Log = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase1 = fees1Log + ? BigInt(network.interface.parseLog(fees1Log)!.args[0]) + : 0n; + + const phase1Blocks = BigInt(syncBlock1 - phase1StartBlock); + const phase1ExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + phase1Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase1).to.equal(phase1ExpectedFees); + + const raisedNetworkFee = initialNetworkFee * 2n; + const raisedPackedFee = raisedNetworkFee / ETH_DEDUCTED_DIGITS; + const updateTx1 = await network.updateNetworkFee(raisedNetworkFee); + const updateBlock1 = await getTxBlock(updateTx1); + + // Settle the pre-update window so phase 2 starts at the raised fee only. + const settleSyncTx1 = await network.connect(stakerA).syncFees(); + const settleSyncBlock1 = await getTxBlock(settleSyncTx1); + const settleSyncReceipt1 = await settleSyncTx1.wait(); + const settleFees1Log = settleSyncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const transitionFees1 = settleFees1Log + ? BigInt(network.interface.parseLog(settleFees1Log)!.args[0]) + : 0n; + const transition1ExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(updateBlock1 - syncBlock1) * + ETH_DEDUCTED_DIGITS + + ((raisedPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(settleSyncBlock1 - updateBlock1) * + ETH_DEDUCTED_DIGITS; + expect(transitionFees1).to.equal(transition1ExpectedFees); + + const phase2StartBlock = settleSyncBlock1; + await mineBlocks(provider, 100); + + const syncTx2 = await network.connect(stakerA).syncFees(); + const syncBlock2 = await getTxBlock(syncTx2); + const syncReceipt2 = await syncTx2.wait(); + const fees2Log = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase2 = fees2Log + ? BigInt(network.interface.parseLog(fees2Log)!.args[0]) + : 0n; + + const phase2Blocks = BigInt(syncBlock2 - phase2StartBlock); + const phase2ExpectedFees = + ((raisedPackedFee * vUnits) / BPS_DENOMINATOR) * + phase2Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase2).to.equal(phase2ExpectedFees); + expect(phase2Blocks).to.equal(phase1Blocks); + expect(newFeesPhase2).to.equal(newFeesPhase1 * 2n); + }); + }); + + describe("Network Fee decrease staking Rewards", () => { + it("Staking rewards decrease after updateNetworkFee lowers the ETH network fee", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerBlock = await getTxBlock(registerTx); + + const initialNetworkFee = await views.getNetworkFee(); + const raisedNetworkFee = initialNetworkFee * 2n; + const vUnits = defaultVUnits(1n); + + await network.connect(stakerA).syncFees(); + await network.updateNetworkFee(raisedNetworkFee); + await network.connect(stakerA).syncFees(); + + const phase1StartBlock = await getBlockNumber(provider); + await mineBlocks(provider, 100); + + const syncTx1 = await network.connect(stakerA).syncFees(); + const syncBlock1 = await getTxBlock(syncTx1); + const syncReceipt1 = await syncTx1.wait(); + const fees1Log = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase1 = fees1Log + ? BigInt(network.interface.parseLog(fees1Log)!.args[0]) + : 0n; + + const phase1Blocks = BigInt(syncBlock1 - phase1StartBlock); + const raisedPackedFee = raisedNetworkFee / ETH_DEDUCTED_DIGITS; + const phase1ExpectedFees = + ((raisedPackedFee * vUnits) / BPS_DENOMINATOR) * + phase1Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase1).to.equal(phase1ExpectedFees); + + await network.updateNetworkFee(initialNetworkFee); + + // Settle the pre-update window so phase 2 starts at the reduced fee only. + await network.connect(stakerA).syncFees(); + + const phase2StartBlock = await getBlockNumber(provider); + await mineBlocks(provider, 100); + + const syncTx2 = await network.connect(stakerA).syncFees(); + const syncBlock2 = await getTxBlock(syncTx2); + const syncReceipt2 = await syncTx2.wait(); + const fees2Log = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase2 = fees2Log + ? BigInt(network.interface.parseLog(fees2Log)!.args[0]) + : 0n; + + const phase2Blocks = BigInt(syncBlock2 - phase2StartBlock); + const initialPackedFee = initialNetworkFee / ETH_DEDUCTED_DIGITS; + const phase2ExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + phase2Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase2).to.equal(phase2ExpectedFees); + expect(phase2Blocks).to.equal(phase1Blocks); + expect(newFeesPhase1).to.equal(newFeesPhase2 * 2n); + }); + }); + + describe("Zero Network Fee do no genetrate new staking rewards", () => { + it("Staking rewards stop after updateNetworkFee sets the ETH network fee to zero", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerBlock = await getTxBlock(registerTx); + + const initialNetworkFee = await views.getNetworkFee(); + const vUnits = defaultVUnits(1n); + const initialPackedFee = initialNetworkFee / ETH_DEDUCTED_DIGITS; + + const baselineSyncTx = await network.connect(stakerA).syncFees(); + const baselineSyncBlock = await getTxBlock(baselineSyncTx); + const baselineSyncReceipt = await baselineSyncTx.wait(); + const baselineFeesLog = baselineSyncReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const baselineFees = baselineFeesLog + ? BigInt(network.interface.parseLog(baselineFeesLog)!.args[0]) + : 0n; + const baselineExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(baselineSyncBlock - registerBlock) * + ETH_DEDUCTED_DIGITS; + expect(baselineFees).to.equal(baselineExpectedFees); + + const phase1StartBlock = baselineSyncBlock; + await mineBlocks(provider, 100); + + const syncTx1 = await network.connect(stakerA).syncFees(); + const syncBlock1 = await getTxBlock(syncTx1); + const syncReceipt1 = await syncTx1.wait(); + const fees1Log = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase1 = fees1Log + ? BigInt(network.interface.parseLog(fees1Log)!.args[0]) + : 0n; + + const phase1Blocks = BigInt(syncBlock1 - phase1StartBlock); + const phase1ExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + phase1Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase1).to.equal(phase1ExpectedFees); + expect(newFeesPhase1).to.not.equal(0n); + + await network.updateNetworkFee(0n); + + // Settle any remaining pre-shutdown fees so the next window runs at zero fee only. + await network.connect(stakerA).syncFees(); + + const claimableBeforeZeroFeeWindow = await views.previewClaimableEth( + stakerA.address, + ); + const accBeforeZeroFeeWindow = await views.accEthPerShare(); + + await mineBlocks(provider, 100); + + const previewDuringZeroFeeWindow = await views.previewClaimableEth( + stakerA.address, + ); + expect(previewDuringZeroFeeWindow).to.equal(claimableBeforeZeroFeeWindow); + + const syncTx2 = await network.connect(stakerA).syncFees(); + const syncReceipt2 = await syncTx2.wait(); + const fees2Log = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + expect(fees2Log).to.be.undefined; + + const accAfterZeroFeeWindow = await views.accEthPerShare(); + const claimableAfterZeroFeeSync = await views.previewClaimableEth( + stakerA.address, + ); + + expect(accAfterZeroFeeWindow).to.equal(accBeforeZeroFeeWindow); + expect(claimableAfterZeroFeeSync).to.equal(claimableBeforeZeroFeeWindow); + }); + }); + + describe("Multiple Network Fee Changes staking Rewards", () => { + it("Staking rewards track multiple updateNetworkFee changes across consecutive periods", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerBlock = await getTxBlock(registerTx); + + const initialNetworkFee = await views.getNetworkFee(); + const raisedNetworkFee = initialNetworkFee * 2n; + const vUnits = defaultVUnits(1n); + const initialPackedFee = initialNetworkFee / ETH_DEDUCTED_DIGITS; + const raisedPackedFee = raisedNetworkFee / ETH_DEDUCTED_DIGITS; + + const baselineSyncTx = await network.connect(stakerA).syncFees(); + const baselineSyncBlock = await getTxBlock(baselineSyncTx); + const baselineSyncReceipt = await baselineSyncTx.wait(); + const baselineFeesLog = baselineSyncReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const baselineFees = baselineFeesLog + ? BigInt(network.interface.parseLog(baselineFeesLog)!.args[0]) + : 0n; + const baselineExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(baselineSyncBlock - registerBlock) * + ETH_DEDUCTED_DIGITS; + expect(baselineFees).to.equal(baselineExpectedFees); + + const phase1StartBlock = baselineSyncBlock; + await mineBlocks(provider, 100); + + const syncTx1 = await network.connect(stakerA).syncFees(); + const syncBlock1 = await getTxBlock(syncTx1); + const syncReceipt1 = await syncTx1.wait(); + const fees1Log = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase1 = fees1Log + ? BigInt(network.interface.parseLog(fees1Log)!.args[0]) + : 0n; + + const phase1Blocks = BigInt(syncBlock1 - phase1StartBlock); + const phase1ExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + phase1Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase1).to.equal(phase1ExpectedFees); + + const updateTx1 = await network.updateNetworkFee(raisedNetworkFee); + const updateBlock1 = await getTxBlock(updateTx1); + + // Settle the pre-update window so phase 2 starts at the raised fee only. + const settleSyncTx1 = await network.connect(stakerA).syncFees(); + const settleSyncBlock1 = await getTxBlock(settleSyncTx1); + const settleSyncReceipt1 = await settleSyncTx1.wait(); + const settleFees1Log = settleSyncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const transitionFees1 = settleFees1Log + ? BigInt(network.interface.parseLog(settleFees1Log)!.args[0]) + : 0n; + const transition1ExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(updateBlock1 - syncBlock1) * + ETH_DEDUCTED_DIGITS + + ((raisedPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(settleSyncBlock1 - updateBlock1) * + ETH_DEDUCTED_DIGITS; + expect(transitionFees1).to.equal(transition1ExpectedFees); + + const phase2StartBlock = settleSyncBlock1; + await mineBlocks(provider, 100); + + const syncTx2 = await network.connect(stakerA).syncFees(); + const syncBlock2 = await getTxBlock(syncTx2); + const syncReceipt2 = await syncTx2.wait(); + const fees2Log = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase2 = fees2Log + ? BigInt(network.interface.parseLog(fees2Log)!.args[0]) + : 0n; + + const phase2Blocks = BigInt(syncBlock2 - phase2StartBlock); + const phase2ExpectedFees = + ((raisedPackedFee * vUnits) / BPS_DENOMINATOR) * + phase2Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase2).to.equal(phase2ExpectedFees); + + const updateTx2 = await network.updateNetworkFee(initialNetworkFee); + const updateBlock2 = await getTxBlock(updateTx2); + + // Settle the pre-update window so phase 3 starts back at the initial fee. + const settleSyncTx2 = await network.connect(stakerA).syncFees(); + const settleSyncBlock2 = await getTxBlock(settleSyncTx2); + const settleSyncReceipt2 = await settleSyncTx2.wait(); + const settleFees2Log = settleSyncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const transitionFees2 = settleFees2Log + ? BigInt(network.interface.parseLog(settleFees2Log)!.args[0]) + : 0n; + const transition2ExpectedFees = + ((raisedPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(updateBlock2 - syncBlock2) * + ETH_DEDUCTED_DIGITS + + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(settleSyncBlock2 - updateBlock2) * + ETH_DEDUCTED_DIGITS; + expect(transitionFees2).to.equal(transition2ExpectedFees); + + const phase3StartBlock = settleSyncBlock2; + await mineBlocks(provider, 100); + + const syncTx3 = await network.connect(stakerA).syncFees(); + const syncBlock3 = await getTxBlock(syncTx3); + const syncReceipt3 = await syncTx3.wait(); + const fees3Log = syncReceipt3!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase3 = fees3Log + ? BigInt(network.interface.parseLog(fees3Log)!.args[0]) + : 0n; + + const phase3Blocks = BigInt(syncBlock3 - phase3StartBlock); + const phase3ExpectedFees = + ((initialPackedFee * vUnits) / BPS_DENOMINATOR) * + phase3Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase3).to.equal(phase3ExpectedFees); + + const totalExpectedRewards = + baselineExpectedFees + + phase1ExpectedFees + + transition1ExpectedFees + + phase2ExpectedFees + + transition2ExpectedFees + + phase3ExpectedFees; + const totalExpectedAccEthPerShare = calcAccEthPerShareDelta( + totalExpectedRewards, + stakeAmount, + ); + expect(phase2Blocks).to.equal(phase1Blocks); + expect(newFeesPhase2).to.equal(newFeesPhase1 * 2n); + expect(newFeesPhase3).to.equal(newFeesPhase1); + expect(await views.accEthPerShare()).to.equal(totalExpectedAccEthPerShare); + expect( + calcStakingReward(stakeAmount, totalExpectedAccEthPerShare, 0n), + ).to.equal(totalExpectedRewards); + }); + }); + + describe("Cooldown Increase → Same Staking Rewards", () => { + it("Staking rewards stay unchanged after updateUnstakeCooldownDuration increases cooldown", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerBlock = await getTxBlock(registerTx); + + const initialNetworkFee = await views.getNetworkFee(); + const currentCooldown = await views.cooldownDuration(); + const increasedCooldown = currentCooldown * 2n; + const vUnits = defaultVUnits(1n); + const packedFee = initialNetworkFee / ETH_DEDUCTED_DIGITS; + + const baselineSyncTx = await network.connect(stakerA).syncFees(); + const baselineSyncBlock = await getTxBlock(baselineSyncTx); + const baselineSyncReceipt = await baselineSyncTx.wait(); + const baselineFeesLog = baselineSyncReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const baselineFees = baselineFeesLog + ? BigInt(network.interface.parseLog(baselineFeesLog)!.args[0]) + : 0n; + const baselineExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(baselineSyncBlock - registerBlock) * + ETH_DEDUCTED_DIGITS; + expect(baselineFees).to.equal(baselineExpectedFees); + + const phase1StartBlock = baselineSyncBlock; + await mineBlocks(provider, 100); + + const syncTx1 = await network.connect(stakerA).syncFees(); + const syncBlock1 = await getTxBlock(syncTx1); + const syncReceipt1 = await syncTx1.wait(); + const fees1Log = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase1 = fees1Log + ? BigInt(network.interface.parseLog(fees1Log)!.args[0]) + : 0n; + + const phase1Blocks = BigInt(syncBlock1 - phase1StartBlock); + const phase1ExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + phase1Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase1).to.equal(phase1ExpectedFees); + + const updateTx = await network.updateUnstakeCooldownDuration( + increasedCooldown, + ); + await expect(updateTx) + .to.emit(network, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(increasedCooldown); + + const settleSyncTx = await network.connect(stakerA).syncFees(); + const settleSyncBlock = await getTxBlock(settleSyncTx); + const settleSyncReceipt = await settleSyncTx.wait(); + const settleFeesLog = settleSyncReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const transitionFees = settleFeesLog + ? BigInt(network.interface.parseLog(settleFeesLog)!.args[0]) + : 0n; + const transitionExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(settleSyncBlock - syncBlock1) * + ETH_DEDUCTED_DIGITS; + expect(transitionFees).to.equal(transitionExpectedFees); + expect(await views.cooldownDuration()).to.equal(increasedCooldown); + + const phase2StartBlock = settleSyncBlock; + await mineBlocks(provider, 100); + + const syncTx2 = await network.connect(stakerA).syncFees(); + const syncBlock2 = await getTxBlock(syncTx2); + const syncReceipt2 = await syncTx2.wait(); + const fees2Log = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase2 = fees2Log + ? BigInt(network.interface.parseLog(fees2Log)!.args[0]) + : 0n; + + const phase2Blocks = BigInt(syncBlock2 - phase2StartBlock); + const phase2ExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + phase2Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase2).to.equal(phase2ExpectedFees); + expect(newFeesPhase2).to.equal(newFeesPhase1); + + const totalExpectedRewards = + baselineExpectedFees + + phase1ExpectedFees + + transitionExpectedFees + + phase2ExpectedFees; + const totalExpectedAccEthPerShare = calcAccEthPerShareDelta( + totalExpectedRewards, + stakeAmount, + ); + expect(await views.accEthPerShare()).to.equal(totalExpectedAccEthPerShare); + expect( + calcStakingReward(stakeAmount, totalExpectedAccEthPerShare, 0n), + ).to.equal(totalExpectedRewards); + }); + }); + + describe("Cooldown Decrease → Same Staking Rewards", () => { + it("Staking rewards stay unchanged after updateUnstakeCooldownDuration decreases cooldown", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerBlock = await getTxBlock(registerTx); + + const initialNetworkFee = await views.getNetworkFee(); + const currentCooldown = await views.cooldownDuration(); + const reducedCooldown = currentCooldown / 2n; + const vUnits = defaultVUnits(1n); + const packedFee = initialNetworkFee / ETH_DEDUCTED_DIGITS; + + const baselineSyncTx = await network.connect(stakerA).syncFees(); + const baselineSyncBlock = await getTxBlock(baselineSyncTx); + const baselineSyncReceipt = await baselineSyncTx.wait(); + const baselineFeesLog = baselineSyncReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const baselineFees = baselineFeesLog + ? BigInt(network.interface.parseLog(baselineFeesLog)!.args[0]) + : 0n; + const baselineExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(baselineSyncBlock - registerBlock) * + ETH_DEDUCTED_DIGITS; + expect(baselineFees).to.equal(baselineExpectedFees); + + const phase1StartBlock = baselineSyncBlock; + await mineBlocks(provider, 100); + + const syncTx1 = await network.connect(stakerA).syncFees(); + const syncBlock1 = await getTxBlock(syncTx1); + const syncReceipt1 = await syncTx1.wait(); + const fees1Log = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase1 = fees1Log + ? BigInt(network.interface.parseLog(fees1Log)!.args[0]) + : 0n; + + const phase1Blocks = BigInt(syncBlock1 - phase1StartBlock); + const phase1ExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + phase1Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase1).to.equal(phase1ExpectedFees); + + const updateTx = await network.updateUnstakeCooldownDuration( + reducedCooldown, + ); + await expect(updateTx) + .to.emit(network, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(reducedCooldown); + + const settleSyncTx = await network.connect(stakerA).syncFees(); + const settleSyncBlock = await getTxBlock(settleSyncTx); + const settleSyncReceipt = await settleSyncTx.wait(); + const settleFeesLog = settleSyncReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const transitionFees = settleFeesLog + ? BigInt(network.interface.parseLog(settleFeesLog)!.args[0]) + : 0n; + const transitionExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + BigInt(settleSyncBlock - syncBlock1) * + ETH_DEDUCTED_DIGITS; + expect(transitionFees).to.equal(transitionExpectedFees); + expect(await views.cooldownDuration()).to.equal(reducedCooldown); + + const phase2StartBlock = settleSyncBlock; + await mineBlocks(provider, 100); + + const syncTx2 = await network.connect(stakerA).syncFees(); + const syncBlock2 = await getTxBlock(syncTx2); + const syncReceipt2 = await syncTx2.wait(); + const fees2Log = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase2 = fees2Log + ? BigInt(network.interface.parseLog(fees2Log)!.args[0]) + : 0n; + + const phase2Blocks = BigInt(syncBlock2 - phase2StartBlock); + const phase2ExpectedFees = + ((packedFee * vUnits) / BPS_DENOMINATOR) * + phase2Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase2).to.equal(phase2ExpectedFees); + expect(newFeesPhase2).to.equal(newFeesPhase1); + + const totalExpectedRewards = + baselineExpectedFees + + phase1ExpectedFees + + transitionExpectedFees + + phase2ExpectedFees; + const totalExpectedAccEthPerShare = calcAccEthPerShareDelta( + totalExpectedRewards, + stakeAmount, + ); + expect(await views.accEthPerShare()).to.equal(totalExpectedAccEthPerShare); + expect( + calcStakingReward(stakeAmount, totalExpectedAccEthPerShare, 0n), + ).to.equal(totalExpectedRewards); + }); + }); + + describe("EB Increase → Higher Network Fees → More Staking Rewards", () => { + it("Staking rewards double after EB update doubles vUnits", async function () { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 100); + + const syncTx1 = await network.connect(stakerA).syncFees(); + const syncReceipt1 = await syncTx1.wait(); + + const feesSynced1 = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const accAfterPhase1 = feesSynced1 + ? BigInt(network.interface.parseLog(feesSynced1)!.args[1]) + : 0n; + + const allSigners = await connection.ethers.getSigners(); + const oracles = allSigners.slice(10, 14); + + for (let i = 0; i < 4; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebValue = 64; + + const ebBlock = await getBlockNumber(provider); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: ebValue }, + ]); + + await commitEBRoot(network, cssvToken, oracles, root, ebBlock); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + await network.updateClusterBalance( + ebBlock, + clusterOwner.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(cluster.validatorCount), + networkFeeIndex: BigInt(cluster.networkFeeIndex), + index: BigInt(cluster.index), + active: cluster.active, + balance: BigInt(cluster.balance), + }, + ebValue, + proofs[clusterId], + ); + + await mineBlocks(provider, 100); + + const syncTx2 = await network.connect(stakerA).syncFees(); + const syncReceipt2 = await syncTx2.wait(); + + const feesSynced2 = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const accAfterPhase2 = feesSynced2 + ? BigInt(network.interface.parseLog(feesSynced2)!.args[1]) + : 0n; + + expect(accAfterPhase2).to.be.greaterThan(accAfterPhase1); + }); + }); + + describe("Auto-Liquidation Reduces Active Clusters → Less Staking Revenue", () => { + it("Staking rewards decrease when a cluster is liquidated", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + const allSigners = await connection.ethers.getSigners(); + const clusterOwner2 = allSigners[5]; + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + clusterOwner2.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const tinyDeposit = connection.ethers.parseEther("0.01"); + await network.connect(clusterOwner2).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: tinyDeposit }, + ); + + const cluster2State = await getCurrentClusterState( + connection, + network, + clusterOwner2.address, + operatorIds, + ); + + await network.connect(stakerA).syncFees(); + const phase1StartBlock = await getBlockNumber(provider); + await mineBlocks(provider, 100); + + const sync1 = await network.connect(stakerA).syncFees(); + const sync1Block = await getTxBlock(sync1); + const syncReceipt1 = await sync1.wait(); + const fees1Log = syncReceipt1!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase1 = fees1Log + ? BigInt(network.interface.parseLog(fees1Log)!.args[0]) + : 0n; + + const phase1Blocks = BigInt(sync1Block - phase1StartBlock); + const phase1VUnits = defaultVUnits(2n); + const phase1ExpectedFees = + ((PACKED_NETWORK_FEE * phase1VUnits) / BPS_DENOMINATOR) * + phase1Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase1).to.equal(phase1ExpectedFees); + + await mineBlocks(provider, 5000); + + await network.liquidate( + clusterOwner2.address, + operatorIds.map((id) => BigInt(id)), + cluster2State, + ); + + await network.connect(stakerA).syncFees(); + const phase2StartBlock = await getBlockNumber(provider); + await mineBlocks(provider, 100); + + const sync2 = await network.connect(stakerA).syncFees(); + const sync2Block = await getTxBlock(sync2); + const syncReceipt2 = await sync2.wait(); + const fees2Log = syncReceipt2!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + const newFeesPhase2 = fees2Log + ? BigInt(network.interface.parseLog(fees2Log)!.args[0]) + : 0n; + + const phase2Blocks = BigInt(sync2Block - phase2StartBlock); + const phase2VUnits = defaultVUnits(1n); + const phase2ExpectedFees = + ((PACKED_NETWORK_FEE * phase2VUnits) / BPS_DENOMINATOR) * + phase2Blocks * + ETH_DEDUCTED_DIGITS; + expect(newFeesPhase2).to.equal(phase2ExpectedFees); + + expect(newFeesPhase2).to.be.lessThan(newFeesPhase1); + }); + }); + + describe("Full Staking Reward Math — Worked Example", () => { + it("Exact reward calculation for 1 staker, 1 cluster, 1000 blocks", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 1n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = await getBlockNumber(provider); + + await mineBlocks(provider, 1000); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const claimBlock = claimReceipt!.blockNumber; + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const reward = BigInt(balAfter) - balBefore + gasUsed; + + const activeBlocks = BigInt(claimBlock - regBlock); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * 10_000n) / 10_000n; + const totalEarningsPacked = earningsPerBlockPacked * activeBlocks; + const totalEarningsWei = totalEarningsPacked * ETH_DEDUCTED_DIGITS; + + const accDelta = calcAccEthPerShareDelta(totalEarningsWei, stakeAmount); + const expectedReward = calcStakingReward(stakeAmount, accDelta, 0n); + const expectedPayout = + expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + expect(reward).to.equal(expectedPayout); + expect(reward % ETH_DEDUCTED_DIGITS).to.equal(0n); + }); + }); + + describe("Staking Reward with Multiple Users and Precision", () => { + it("Rewards split correctly with 3:7 ratio and no precision loss for clean division", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const amountA = 3n * PRECISION; + const amountB = 7n * PRECISION; + const totalStaked = amountA + amountB; + + await ssvToken.connect(deployer).transfer(stakerA.address, amountA); + await ssvToken.connect(deployer).transfer(stakerB.address, amountB); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), amountA); + await ssvToken + .connect(stakerB) + .approve(await network.getAddress(), amountB); + await network.connect(stakerA).stake(amountA); + await network.connect(stakerB).stake(amountB); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 100); + + const balBeforeA = await provider.getBalance(stakerA.address); + const claimTxA = await network.connect(stakerA).claimEthRewards(); + const receiptA = await claimTxA.wait(); + const gasA = receiptA!.gasUsed * receiptA!.gasPrice; + const balAfterA = await provider.getBalance(stakerA.address); + const rewardA = BigInt(balAfterA) - balBeforeA + gasA; + + const balBeforeB = await provider.getBalance(stakerB.address); + const claimTxB = await network.connect(stakerB).claimEthRewards(); + const receiptB = await claimTxB.wait(); + const gasB = receiptB!.gasUsed * receiptB!.gasPrice; + const balAfterB = await provider.getBalance(stakerB.address); + const rewardB = BigInt(balAfterB) - balBeforeB + gasB; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const oneBlockFeesWei = earningsPerBlockPacked * ETH_DEDUCTED_DIGITS; + const oneBlockAccDelta = calcAccEthPerShareDelta(oneBlockFeesWei, totalStaked); + const maxOneBlockRewardB = calcStakingReward(amountB, oneBlockAccDelta, 0n); + + const expectedRewardBFromA = (rewardA * amountB) / amountA; + const diff = rewardB > expectedRewardBFromA + ? rewardB - expectedRewardBFromA + : expectedRewardBFromA - rewardB; + expect(diff).to.be.lessThanOrEqual(maxOneBlockRewardB); + }); + + it("Truncation dust is at most 1 wei equivalent per user when using odd supply", async function () { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 3n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 100); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const receipt = await claimTx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const reward = BigInt(balAfter) - balBefore + gasUsed; + + expect(reward % ETH_DEDUCTED_DIGITS).to.equal(0n); + }); + }); + + describe("Staking with Existing Pre-Upgrade DAO Balance", () => { + it("Pre-existing DAO revenue is not distributed to first staker", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 500); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 10); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const receipt = await claimTx.wait(); + const claimBlock = receipt!.blockNumber; + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const reward = BigInt(balAfter) - balBefore + gasUsed; + + const postStakeBlocks = BigInt(claimBlock - stakeBlock); + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const expectedFeesPacked = earningsPerBlockPacked * postStakeBlocks; + const expectedFeesWei = expectedFeesPacked * ETH_DEDUCTED_DIGITS; + + const accDelta = calcAccEthPerShareDelta(expectedFeesWei, stakeAmount); + const expectedReward = calcStakingReward(stakeAmount, accDelta, 0n); + const expectedPayout = + expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + expect(reward).to.equal(expectedPayout); + + const totalFeesAllBlocks = earningsPerBlockPacked * BigInt(claimBlock) * ETH_DEDUCTED_DIGITS; + if (totalFeesAllBlocks > 0n) { + expect(reward).to.be.lessThan(totalFeesAllBlocks); + } + }); + }); + + describe("EB Update Followed by syncFees — Full Chain", () => { + it("Full chain trace: EB update → DAO vUnit change → higher earnings → syncFees → claim", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + { + validatorCount: cluster.validatorCount, + networkFeeIndex: cluster.networkFeeIndex, + index: cluster.index, + active: cluster.active, + balance: cluster.balance, + }, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + await mineBlocks(provider, 100); + + const allSigners = await connection.ethers.getSigners(); + const oracles = allSigners.slice(10, 14); + for (let i = 0; i < 4; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebValue = 96; + const ebBlock = await getBlockNumber(provider); + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: ebValue }, + ]); + + await commitEBRoot(network, cssvToken, oracles, root, ebBlock); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + await network.updateClusterBalance( + ebBlock, + clusterOwner.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(cluster.validatorCount), + networkFeeIndex: BigInt(cluster.networkFeeIndex), + index: BigInt(cluster.index), + active: cluster.active, + balance: BigInt(cluster.balance), + }, + ebValue, + proofs[clusterId], + ); + + await mineBlocks(provider, 100); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + + const totalReward = BigInt(balAfter) - balBefore + gasUsed; + + const feesSynced = claimReceipt!.logs.find((log: any) => { + try { + return network.interface.parseLog(log)?.name === Events.FEES_SYNCED; + } catch { + return false; + } + }); + expect(feesSynced).to.not.be.undefined; + const parsedSync = network.interface.parseLog(feesSynced); + const finalAccEthPerShare = BigInt(parsedSync!.args[1]); + + const expectedReward = calcStakingReward(stakeAmount, finalAccEthPerShare, 0n); + const expectedPayout = expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + expect(totalReward).to.equal(expectedPayout); + expect(totalReward % ETH_DEDUCTED_DIGITS).to.equal(0n); + + const phase1Rate = (PACKED_NETWORK_FEE * defaultVUnits(2n)) / BPS_DENOMINATOR; + const phase2Rate = (PACKED_NETWORK_FEE * calcVUnits(96n)) / BPS_DENOMINATOR; + expect(phase2Rate).to.be.greaterThan(phase1Rate); + }); + }); +}); diff --git a/test/e2e/staking/staking-transfers.test.ts b/test/e2e/staking/staking-transfers.test.ts new file mode 100644 index 000000000..18fa089cc --- /dev/null +++ b/test/e2e/staking/staking-transfers.test.ts @@ -0,0 +1,554 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + makePublicKey, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + NETWORK_FEE, + BPS_DENOMINATOR, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { + mineBlocks, + getTxBlock, + calcAccEthPerShareDelta, + calcStakingReward, + defaultVUnits, +} from "../../helpers/index.ts"; +import { Events } from "../../common/events.ts"; + +const PRECISION = 10n ** 18n; +const PACKED_NETWORK_FEE = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + +describe("E2E Staking Transfers", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let provider: any; + + let deployer: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let stakerA: HardhatEthersSigner; + let stakerB: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [deployer, operatorOwner, clusterOwner, stakerA, stakerB] } = await setupTestContext()); + provider = connection.ethers.provider; + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("cSSV Transfer Settles Rewards", () => { + it("Transfer settles both sender and receiver; pre-transfer revenue goes to sender only", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const amountA = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, amountA); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), amountA); + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(amountA), + ); + + await mineBlocks(provider, 50); + + const transferAmount = 5n * PRECISION; + const transferBlock = await getTxBlock( + await cssvToken.connect(stakerA).transfer(stakerB.address, transferAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + amountA - transferAmount, + ); + expect(await cssvToken.balanceOf(stakerB.address)).to.equal( + transferAmount, + ); + + await mineBlocks(provider, 50); + + const balBeforeA = await provider.getBalance(stakerA.address); + const claimTxA = await network.connect(stakerA).claimEthRewards(); + const claimReceiptA = await claimTxA.wait(); + const claimBlockA = claimReceiptA!.blockNumber; + const gasA = claimReceiptA!.gasUsed * claimReceiptA!.gasPrice; + const balAfterA = await provider.getBalance(stakerA.address); + const rewardA = BigInt(balAfterA) - balBeforeA + gasA; + + const balBeforeB = await provider.getBalance(stakerB.address); + const claimTxB = await network.connect(stakerB).claimEthRewards(); + const claimReceiptB = await claimTxB.wait(); + const claimBlockB = claimReceiptB!.blockNumber; + const gasB = claimReceiptB!.gasUsed * claimReceiptB!.gasPrice; + const balAfterB = await provider.getBalance(stakerB.address); + const rewardB = BigInt(balAfterB) - balBeforeB + gasB; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + const totalSupply = amountA; + + const phase1Blocks = BigInt(transferBlock - stakeBlock); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const accAtTransfer = calcAccEthPerShareDelta(phase1FeesWei, totalSupply); + + const phase2aBlocks = BigInt(claimBlockA - transferBlock); + const phase2aFeesWei = earningsPerBlockPacked * phase2aBlocks * ETH_DEDUCTED_DIGITS; + const accDelta2a = calcAccEthPerShareDelta(phase2aFeesWei, totalSupply); + + const aAccrued = calcStakingReward(amountA, accAtTransfer, 0n); + const aPostTransfer = calcStakingReward(amountA - transferAmount, accDelta2a, 0n); + const expectedRewardA = aAccrued + aPostTransfer; + const expectedPayoutA = expectedRewardA - (expectedRewardA % ETH_DEDUCTED_DIGITS); + expect(rewardA).to.equal(expectedPayoutA); + + const phase2bBlocks = BigInt(claimBlockB - claimBlockA); + const phase2bFeesWei = earningsPerBlockPacked * phase2bBlocks * ETH_DEDUCTED_DIGITS; + const accDelta2b = calcAccEthPerShareDelta(phase2bFeesWei, totalSupply); + + const expectedRewardB = calcStakingReward(transferAmount, accDelta2a + accDelta2b, 0n); + const expectedPayoutB = expectedRewardB - (expectedRewardB % ETH_DEDUCTED_DIGITS); + expect(rewardB).to.equal(expectedPayoutB); + + expect(rewardA).to.be.greaterThan(rewardB); + }); + + it("Stake-transfer-stake cycle preserves reward boundaries across all phases", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const initialStake = 10n * PRECISION; + const transferAmount = 5n * PRECISION; + const restakeAmount = 5n * PRECISION; + const phase2Supply = initialStake; + const phase3Supply = initialStake + restakeAmount; + + await ssvToken + .connect(deployer) + .transfer(stakerA.address, initialStake + restakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), initialStake + restakeAmount); + + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(initialStake), + ); + + await mineBlocks(provider, 50); + + const transferBlock = await getTxBlock( + await cssvToken.connect(stakerA).transfer(stakerB.address, transferAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + initialStake - transferAmount, + ); + expect(await cssvToken.balanceOf(stakerB.address)).to.equal( + transferAmount, + ); + + await mineBlocks(provider, 50); + + const restakeBlock = await getTxBlock( + await network.connect(stakerA).stake(restakeAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(initialStake); + + await mineBlocks(provider, 50); + + const balBeforeA = await provider.getBalance(stakerA.address); + const balBeforeB = await provider.getBalance(stakerB.address); + + let claimTxA: any; + let claimTxB: any; + + await provider.send("evm_setAutomine", [false]); + try { + claimTxA = await network.connect(stakerA).claimEthRewards(); + claimTxB = await network.connect(stakerB).claimEthRewards(); + await provider.send("evm_mine", []); + } finally { + await provider.send("evm_setAutomine", [true]); + } + + const claimReceiptA = await claimTxA.wait(); + const claimReceiptB = await claimTxB.wait(); + const claimBlock = claimReceiptA!.blockNumber; + + expect(claimReceiptB!.blockNumber).to.equal(claimBlock); + + const gasA = claimReceiptA!.gasUsed * claimReceiptA!.gasPrice; + const gasB = claimReceiptB!.gasUsed * claimReceiptB!.gasPrice; + + const balAfterA = await provider.getBalance(stakerA.address); + const balAfterB = await provider.getBalance(stakerB.address); + + const rewardA = BigInt(balAfterA) - balBeforeA + gasA; + const rewardB = BigInt(balAfterB) - balBeforeB + gasB; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const phase1Blocks = BigInt(transferBlock - stakeBlock); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, initialStake); + + const phase2Blocks = BigInt(restakeBlock - transferBlock); + const phase2FeesWei = earningsPerBlockPacked * phase2Blocks * ETH_DEDUCTED_DIGITS; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, phase2Supply); + + const phase3Blocks = BigInt(claimBlock - restakeBlock); + const phase3FeesWei = earningsPerBlockPacked * phase3Blocks * ETH_DEDUCTED_DIGITS; + const acc3 = calcAccEthPerShareDelta(phase3FeesWei, phase3Supply); + + const expectedRewardA = + calcStakingReward(initialStake, acc1, 0n) + + calcStakingReward(initialStake - transferAmount, acc2, 0n) + + calcStakingReward(initialStake, acc3, 0n); + const expectedRewardB = + calcStakingReward(transferAmount, acc2, 0n) + + calcStakingReward(transferAmount, acc3, 0n); + + const expectedPayoutA = + expectedRewardA - (expectedRewardA % ETH_DEDUCTED_DIGITS); + const expectedPayoutB = + expectedRewardB - (expectedRewardB % ETH_DEDUCTED_DIGITS); + + expect(rewardA).to.equal(expectedPayoutA); + expect(rewardB).to.equal(expectedPayoutB); + }); + + it("Receiver B's userIndex is set to accEthPerShare at transfer time", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const amountA = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, amountA); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), amountA); + await network.connect(stakerA).stake(amountA); + + await mineBlocks(provider, 100); + + await cssvToken + .connect(stakerA) + .transfer(stakerB.address, 5n * PRECISION); + + const balBefore = await provider.getBalance(stakerB.address); + const claimTx = await network.connect(stakerB).claimEthRewards(); + const receipt = await claimTx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const balAfter = await provider.getBalance(stakerB.address); + const reward = BigInt(balAfter) - balBefore + gasUsed; + + const vUnits = defaultVUnits(1n); + const maxOneBlockReward = + ((PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + expect(reward).to.be.lessThanOrEqual(maxOneBlockReward); + }); + }); + + describe("cSSV Transfer — Mint/Burn Do NOT Trigger Hook", () => { + it("Mint (via stake) does not trigger onCSSVTransfer — from == address(0)", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + + const tx = await network.connect(stakerA).stake(stakeAmount); + await tx.wait(); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(stakeAmount); + }); + + it("Burn (via requestUnstake) does not trigger onCSSVTransfer — to == address(0)", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await mineBlocks(provider, 50); + + const tx = await network + .connect(stakerA) + .requestUnstake(5n * PRECISION); + const receipt = await tx.wait(); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + 5n * PRECISION, + ); + }); + + it("Self-transfer does not trigger onCSSVTransfer — from == to", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await mineBlocks(provider, 50); + + const tx = await cssvToken + .connect(stakerA) + .transfer(stakerA.address, 5n * PRECISION); + await tx.wait(); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(stakeAmount); + }); + + it("Self-transfer keeps reward accrual equal to uninterrupted staking", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + const selfTransferAmount = 5n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + + const stakeBlock = await getTxBlock( + await network.connect(stakerA).stake(stakeAmount), + ); + + await mineBlocks(provider, 50); + + const selfTransferBlock = await getTxBlock( + await cssvToken + .connect(stakerA) + .transfer(stakerA.address, selfTransferAmount), + ); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(stakeAmount); + + await mineBlocks(provider, 50); + + const balBefore = await provider.getBalance(stakerA.address); + const claimTx = await network.connect(stakerA).claimEthRewards(); + const claimReceipt = await claimTx.wait(); + const claimBlock = claimReceipt!.blockNumber; + const gasUsed = claimReceipt!.gasUsed * claimReceipt!.gasPrice; + const balAfter = await provider.getBalance(stakerA.address); + const reward = BigInt(balAfter) - balBefore + gasUsed; + + const vUnits = defaultVUnits(1n); + const earningsPerBlockPacked = (PACKED_NETWORK_FEE * vUnits) / BPS_DENOMINATOR; + + const phase1Blocks = BigInt(selfTransferBlock - stakeBlock); + const phase1FeesWei = earningsPerBlockPacked * phase1Blocks * ETH_DEDUCTED_DIGITS; + const acc1 = calcAccEthPerShareDelta(phase1FeesWei, stakeAmount); + + const phase2Blocks = BigInt(claimBlock - selfTransferBlock); + const phase2FeesWei = earningsPerBlockPacked * phase2Blocks * ETH_DEDUCTED_DIGITS; + const acc2 = calcAccEthPerShareDelta(phase2FeesWei, stakeAmount); + + const expectedReward = calcStakingReward(stakeAmount, acc1 + acc2, 0n); + const expectedPayout = + expectedReward - (expectedReward % ETH_DEDUCTED_DIGITS); + + expect(reward).to.equal(expectedPayout); + }); + + it("Zero-amount transfer does not trigger onCSSVTransfer — amount == 0", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await mineBlocks(provider, 50); + + const tx = await cssvToken.connect(stakerA).transfer(stakerB.address, 0n); + await tx.wait(); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal(stakeAmount); + expect(await cssvToken.balanceOf(stakerB.address)).to.equal(0n); + }); + + it("Normal user-to-user transfer DOES trigger onCSSVTransfer", async function () { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const stakeAmount = 10n * PRECISION; + await ssvToken.connect(deployer).transfer(stakerA.address, stakeAmount); + await ssvToken + .connect(stakerA) + .approve(await network.getAddress(), stakeAmount); + await network.connect(stakerA).stake(stakeAmount); + + await mineBlocks(provider, 50); + + const transferAmount = 5n * PRECISION; + const tx = await cssvToken + .connect(stakerA) + .transfer(stakerB.address, transferAmount); + const receipt = await tx.wait(); + + const networkAddress = await network.getAddress(); + const settleLogs = receipt!.logs.filter((log: any) => { + try { + const parsed = network.interface.parseLog(log); + return parsed?.name === Events.REWARDS_SETTLED; + } catch { + return false; + } + }); + + expect(settleLogs.length).to.equal(2); + + expect(await cssvToken.balanceOf(stakerA.address)).to.equal( + stakeAmount - transferAmount, + ); + expect(await cssvToken.balanceOf(stakerB.address)).to.equal( + transferAmount, + ); + }); + }); +}); diff --git a/test/e2e/validators/validator-edge-cases.test.ts b/test/e2e/validators/validator-edge-cases.test.ts new file mode 100644 index 000000000..f4f333db0 --- /dev/null +++ b/test/e2e/validators/validator-edge-cases.test.ts @@ -0,0 +1,1027 @@ +import { expect } from "chai"; +import { ethers } from "ethers"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType, Cluster } from "../../common/types.ts"; +import { + makePublicKey, + makePublicKeys, + makeOperatorKey, + whitelistAddresses, + getCurrentClusterState, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE, + ETH_DEDUCTED_DIGITS, + BPS_DENOMINATOR, +} from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { + mineBlocks, + getBlockNumber, + getTxBlock, + calcClusterBurn, + defaultVUnits, + calcOperatorFeeAccrual, +} from "../../helpers/index.ts"; + +describe("Validator Edge Cases", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, otherAccount] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + async function setupDefaultCluster( + network: any, + provider: any, + owner: HardhatEthersSigner, + opOwner: HardhatEthersSigner = operatorOwner, + fee: bigint = MINIMAL_OPERATOR_ETH_FEE, + operatorCount: number = 4, + ): Promise { + const opIds: number[] = []; + for (let i = 0; i < operatorCount; i++) { + const seed = Math.floor(Math.random() * 100000) + i; + await network + .connect(opOwner) + .registerOperator(makeOperatorKey(seed), fee, false); + opIds.push(i + 1); + } + await whitelistAddresses(network, opOwner, opIds, [owner.address]); + return opIds; + } + + + describe("Register Validator — Revert Cases", () => { + it("Reverts with EmptyPublicKeysList on bulk register with empty array", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await expect( + network.connect(clusterOwner).bulkRegisterValidator( + [], + opIds, + [], + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.EMPTY_PUBLIC_KEYS_LIST, + ); + }); + + it("Reverts with PublicKeysSharesLengthMismatch on mismatched key/share arrays", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await expect( + network.connect(clusterOwner).bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + opIds, + [DEFAULT_SHARES], + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.PUBLIC_KEYS_SHARES_LENGTH_MISMATCH, + ); + }); + + it("Reverts with InvalidPublicKeyLength on short public key", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + const shortKey = "0x" + "aa".repeat(32); + + await expect( + network.connect(clusterOwner).registerValidator( + shortKey, + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.INVALID_PUBLIC_KEYS_LENGTH, + ); + }); + + it("Reverts with InvalidOperatorIdsLength for < 4 operators", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds.slice(0, 3), + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.INVALID_OPERATOR_IDS_LENGTH, + ); + }); + + it("Reverts with InvalidOperatorIdsLength for 5 operators (not 4,7,10,13)", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds: number[] = []; + for (let i = 0; i < 5; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i + 1), MINIMAL_OPERATOR_ETH_FEE, false); + opIds.push(i + 1); + } + await whitelistAddresses(network, operatorOwner, opIds, [clusterOwner.address]); + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.INVALID_OPERATOR_IDS_LENGTH, + ); + }); + + it("Reverts with UnsortedOperatorsList for unsorted operators", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + const unsorted = [opIds[2], opIds[0], opIds[1], opIds[3]]; + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(1), + unsorted, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.UNSORTED_OPERATORS_LIST, + ); + }); + + it("Reverts with OperatorsListNotUnique for duplicate operators", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + const dups = [opIds[0], opIds[0], opIds[1], opIds[2]]; + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(1), + dups, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.OPERATORS_LIST_NOT_UNIQUE, + ); + }); + + it("Reverts with ValidatorAlreadyRegistered when registering same validator twice", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + const pk = makePublicKey(1); + + await network.connect(clusterOwner).registerValidator( + pk, + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await expect( + network.connect(clusterOwner).registerValidator( + pk, + opIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.VALIDATOR_ALREADY_REGISTERED, + ); + }); + + it("Reverts with IncorrectClusterState when passing wrong cluster struct", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const wrongCluster = { ...EMPTY_CLUSTER, validatorCount: 99n }; + + await expect( + network.connect(clusterOwner).registerValidator( + makePublicKey(2), + opIds, + DEFAULT_SHARES, + wrongCluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.INCORRECT_CLUSTER_STATE, + ); + }); + }); + + describe("Remove Validator — Revert Cases", () => { + it("Reverts with ValidatorDoesNotExist for non-existent validator", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await expect( + network.connect(clusterOwner).removeValidator( + makePublicKey(999), + opIds, + cluster, + ), + ).to.be.revertedWithCustomError( + network, + Errors.VALIDATOR_DOES_NOT_EXIST, + ); + }); + + it("Reverts with IncorrectValidatorStateWithData when wrong owner removes", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await expect( + network.connect(otherAccount).removeValidator( + makePublicKey(1), + opIds, + cluster, + ), + ).to.be.revertedWithCustomError( + network, + Errors.CLUSTER_DOES_NOT_EXIST, + ); + }); + + it("Reverts with IncorrectClusterState with stale cluster struct", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await expect( + network.connect(clusterOwner).removeValidator( + makePublicKey(1), + opIds, + EMPTY_CLUSTER, + ), + ).to.be.revertedWithCustomError( + network, + Errors.INCORRECT_CLUSTER_STATE, + ); + }); + + it("Reverts with ValidatorDoesNotExist for bulk remove with empty array", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await expect( + network.connect(clusterOwner).bulkRemoveValidator( + [], + opIds, + cluster, + ), + ).to.be.revertedWithCustomError( + network, + Errors.VALIDATOR_DOES_NOT_EXIST, + ); + }); + }); + + describe("Race condition — register and remove in same block", () => { + it("Register then remove in same block — no double-counting", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster1 = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await mineBlocks(provider, 100); + + await provider.send("evm_setAutomine", [false]); + + const regPromise = network.connect(clusterOwner).registerValidator( + makePublicKey(2), + opIds, + DEFAULT_SHARES, + cluster1, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await provider.send("evm_mine", []); + await provider.send("evm_setAutomine", [true]); + + const regTx = await regPromise; + await regTx.wait(); + + const cluster2 = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await provider.send("evm_setAutomine", [false]); + + const removePromise = network.connect(clusterOwner).removeValidator( + makePublicKey(1), + opIds, + cluster2, + ); + + await provider.send("evm_mine", []); + await provider.send("evm_setAutomine", [true]); + + const removeTx = await removePromise; + await removeTx.wait(); + + const cluster3 = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + expect(BigInt(cluster3.validatorCount)).to.equal(1n); + + for (const opId of opIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(op.validatorCount).to.equal(1); + } + }); + }); + + + describe("Cluster balance underflow protection", () => { + it("Cluster balance floors at 0 when fees exceed balance", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await mineBlocks(provider, 1_100_000_000); + const balance = await views.getBalance( + clusterOwner.address, + opIds, + cluster, + ); + + expect(balance).to.equal(0n); + }); + }); + + describe("Exit validator — signal only, no state change", () => { + it("exitValidator emits event but makes no state change", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const clusterBefore = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + const opStateBefore = await views.getOperatorById(BigInt(opIds[0])); + + const exitTx = await network + .connect(clusterOwner) + .exitValidator(makePublicKey(1), opIds); + + await expect(exitTx).to.emit(network, Events.VALIDATOR_EXITED); + + const clusterAfter = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + expect(BigInt(clusterAfter.validatorCount)).to.equal( + BigInt(clusterBefore.validatorCount), + ); + + const opStateAfter = await views.getOperatorById(BigInt(opIds[0])); + expect(opStateAfter.validatorCount).to.equal(opStateBefore.validatorCount); + + const clusterCurrent = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + const removeTx = await network.connect(clusterOwner).removeValidator( + makePublicKey(1), + opIds, + clusterCurrent, + ); + await expect(removeTx).to.emit(network, Events.VALIDATOR_REMOVED); + }); + + it("exitValidator reverts with ValidatorDoesNotExist for non-existent validator", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await expect( + network + .connect(clusterOwner) + .exitValidator(makePublicKey(999), opIds), + ).to.be.revertedWithCustomError( + network, + Errors.VALIDATOR_DOES_NOT_EXIST, + ); + }); + + it("exitValidator reverts with wrong operator IDs", async function () { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + for (let i = 0; i < 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(9000 + i), MINIMAL_OPERATOR_ETH_FEE, false); + } + const wrongOpIds = [5, 6, 7, 8]; + + await expect( + network + .connect(clusterOwner) + .exitValidator(makePublicKey(1), wrongOpIds), + ).to.be.revertedWithCustomError( + network, + Errors.INCORRECT_VALIDATOR_STATE, + ); + }); + }); + + describe("DAO network fee earnings — consistency with cluster accounting", () => { + it("DAO earnings match cluster network fee payments", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + + await mineBlocks(provider, 100); + + const viewBlock = BigInt(await getBlockNumber(provider)); + const daoBlockDiff = viewBlock - regBlock; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + const daoEarningsUnits = (daoBlockDiff * packedNetworkFee * vUnits) / BPS_DENOMINATOR; + const expectedDaoEarnings = daoEarningsUnits * ETH_DEDUCTED_DIGITS; + + const daoEarnings = await views.getNetworkEarnings(); + expect(daoEarnings).to.equal(expectedDaoEarnings); + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + const currentBalance = await views.getBalance( + clusterOwner.address, + opIds, + cluster, + ); + + const totalFeesCharged = DEFAULT_ETH_REGISTER_VALUE - currentBalance; + + let totalOpEarnings = 0n; + for (const opId of opIds) { + totalOpEarnings += await views.getOperatorEarnings(BigInt(opId)); + } + + const sum = totalOpEarnings + daoEarnings; + const diff = totalFeesCharged > sum ? totalFeesCharged - sum : sum - totalFeesCharged; + expect(diff).to.be.lessThanOrEqual(ETH_DEDUCTED_DIGITS * 4n); + }); + }); + + describe("Operator registration then immediate validator registration — same block", () => { + it("Register operators and validator in same block — no error from zero blockDiff", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + await provider.send("evm_setAutomine", [false]); + + const fee = MINIMAL_OPERATOR_ETH_FEE; + for (let i = 1; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(700 + i), fee, false); + } + const opIds = [1, 2, 3, 4]; + + await network + .connect(operatorOwner) + .setOperatorsWhitelists(opIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await provider.send("evm_mine", []); + await provider.send("evm_setAutomine", [true]); + + for (const opId of opIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(op.validatorCount).to.equal(1); + } + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(1n); + expect(BigInt(cluster.balance)).to.equal(DEFAULT_ETH_REGISTER_VALUE); + + await mineBlocks(provider, 1); + const earnings = await views.getOperatorEarnings(1n); + + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const expectedEarnings = calcOperatorFeeAccrual(1n, packedFee, defaultVUnits(1n)) * ETH_DEDUCTED_DIGITS; + expect(earnings).to.equal(expectedEarnings); + }); + }); + + describe("Large number of operators (13) — gas and correctness", () => { + it("Register validator with 13 operators — correct state and reasonable gas", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const opIds: number[] = []; + for (let i = 1; i <= 13; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(800 + i), MINIMAL_OPERATOR_ETH_FEE, false); + opIds.push(i); + } + + await whitelistAddresses(network, operatorOwner, opIds, [ + clusterOwner.address, + ]); + + const bigDeposit = ethers.parseEther("50"); + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: bigDeposit }, + ); + + const receipt = await regTx.wait(); + + expect(receipt!.gasUsed).to.be.greaterThan(0); + + const cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(1n); + expect(BigInt(cluster.balance)).to.equal(bigDeposit); + + for (const opId of opIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(op.validatorCount).to.equal(1); + } + + await mineBlocks(provider, 100); + + const regBlock = BigInt(receipt!.blockNumber); + const currentBlock = BigInt(await getBlockNumber(provider)); + const blockDiff = currentBlock - regBlock; + + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + + const expectedBurn = calcClusterBurn({ + blockDiff, + numOperators: 13n, + ethFee: packedFee, + networkFee: packedNetworkFee, + effectiveVUnits: vUnits, + }); + const expectedBalance = bigDeposit - expectedBurn; + + const currentBalance = await views.getBalance( + clusterOwner.address, + opIds, + cluster, + ); + expect(currentBalance).to.equal(expectedBalance); + + const expectedEarnings = calcOperatorFeeAccrual(blockDiff, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + for (const opId of opIds) { + const earnings = await views.getOperatorEarnings(BigInt(opId)); + expect(earnings).to.equal(expectedEarnings); + } + }); + }); + + describe("Validator registration with explicit EB (post-updateClusterBalance)", () => { + it("Adding validator to explicit-EB cluster adds default vUnits baseline", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + opIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + expect(BigInt(cluster.validatorCount)).to.equal(2n); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(3), + opIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + expect(BigInt(cluster.validatorCount)).to.equal(3n); + + for (const opId of opIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(op.validatorCount).to.equal(3); + } + }); + }); + + describe("Validator removal with implicit/explicit EB — full cluster empty", () => { + it("Remove last validator — cluster persists with remaining balance", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + const regTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + + await mineBlocks(provider, 50); + + let cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + const removeTx = await network.connect(clusterOwner).removeValidator( + makePublicKey(1), + opIds, + cluster, + ); + const removeBlock = BigInt(await getTxBlock(removeTx)); + await expect(removeTx).to.emit(network, Events.VALIDATOR_REMOVED); + + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + const blockDiff = removeBlock - regBlock; + const expectedBurn = calcClusterBurn({ + blockDiff, + numOperators: 4n, + ethFee: packedFee, + networkFee: packedNetworkFee, + effectiveVUnits: vUnits, + }); + const expectedBalance = DEFAULT_ETH_REGISTER_VALUE - expectedBurn; + + const clusterAfter = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + expect(BigInt(clusterAfter.validatorCount)).to.equal(0n); + expect(clusterAfter.active).to.be.true; + expect(BigInt(clusterAfter.balance)).to.equal(expectedBalance); + + for (const opId of opIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(op.validatorCount).to.equal(0); + } + + const withdrawTx = await network.connect(clusterOwner).withdraw( + opIds, + BigInt(clusterAfter.balance), + clusterAfter, + ); + await expect(withdrawTx).to.emit(network, Events.CLUSTER_WITHDRAWN); + }); + }); + + describe("Bulk remove validators — multiple removals in one transaction", () => { + it("Bulk remove 3 of 5 validators — correct state after", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + const keys = makePublicKeys(5, 1); + let cluster = EMPTY_CLUSTER; + + for (let i = 0; i < 5; i++) { + await network.connect(clusterOwner).registerValidator( + keys[i], + opIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + } + + expect(BigInt(cluster.validatorCount)).to.equal(5n); + + await mineBlocks(provider, 100); + + cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + const keysToRemove = [keys[0], keys[1], keys[2]]; + const bulkRemoveTx = await network + .connect(clusterOwner) + .bulkRemoveValidator(keysToRemove, opIds, cluster); + + const receipt = await bulkRemoveTx.wait(); + + const removedEvents = receipt!.logs.filter((log: any) => { + try { + const parsed = network.interface.parseLog(log); + return parsed?.name === Events.VALIDATOR_REMOVED; + } catch { + return false; + } + }); + expect(removedEvents.length).to.equal(3); + + const clusterAfter = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + expect(BigInt(clusterAfter.validatorCount)).to.equal(2n); + + for (const opId of opIds) { + const op = await views.getOperatorById(BigInt(opId)); + expect(op.validatorCount).to.equal(2); + } + + const clusterForRemove = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + await expect( + network.connect(clusterOwner).removeValidator( + keys[3], + opIds, + clusterForRemove, + ), + ).to.emit(network, Events.VALIDATOR_REMOVED); + }); + }); + + describe("Deposit and withdraw — no side effects on operator state", () => { + it("Deposit and withdraw do not change operator validator counts", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + const opIds = await setupDefaultCluster(network, provider, clusterOwner); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + opIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + let cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + opIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + cluster = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + + expect(BigInt(cluster.validatorCount)).to.equal(2n); + + const opsBefore: { validatorCount: number; fee: bigint }[] = []; + for (const opId of opIds) { + const op = await views.getOperatorById(BigInt(opId)); + opsBefore.push({ + validatorCount: Number(op.validatorCount), + fee: op.fee, + }); + } + + const depositAmount = ethers.parseEther("5"); + const depositTx = await network + .connect(clusterOwner) + .deposit(clusterOwner.address, opIds, cluster, { + value: depositAmount, + }); + await expect(depositTx).to.emit(network, Events.CLUSTER_DEPOSITED); + + for (let i = 0; i < opIds.length; i++) { + const op = await views.getOperatorById(BigInt(opIds[i])); + expect(op.validatorCount).to.equal(opsBefore[i].validatorCount); + expect(op.fee).to.equal(opsBefore[i].fee); + } + + const clusterAfterDeposit = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + expect(BigInt(clusterAfterDeposit.balance)).to.equal( + BigInt(cluster.balance) + depositAmount, + ); + + const withdrawAmount = ethers.parseEther("3"); + const withdrawTx = await network + .connect(clusterOwner) + .withdraw(opIds, withdrawAmount, clusterAfterDeposit); + await expect(withdrawTx).to.emit(network, Events.CLUSTER_WITHDRAWN); + + for (let i = 0; i < opIds.length; i++) { + const op = await views.getOperatorById(BigInt(opIds[i])); + expect(op.validatorCount).to.equal(opsBefore[i].validatorCount); + } + + const clusterAfterWithdraw = await getCurrentClusterState( + connection, network, clusterOwner.address, opIds, + ); + expect(BigInt(clusterAfterWithdraw.validatorCount)).to.equal(2n); + }); + }); +}); diff --git a/test/e2e/validators/validator-lifecycle.test.ts b/test/e2e/validators/validator-lifecycle.test.ts new file mode 100644 index 000000000..bfcc5b9de --- /dev/null +++ b/test/e2e/validators/validator-lifecycle.test.ts @@ -0,0 +1,917 @@ +import { expect } from 'chai'; +import type { NetworkConnection } from 'hardhat/types/network'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { ssvNetworkFullFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import { getCurrentClusterState, makeOperatorKey, makePublicKey, whitelistAddresses, setupTestContext } from '../../common/helpers.ts'; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE, +} from '../../common/constants.ts'; +import { + calcClusterBurn, + calcOperatorFeeAccrual, + defaultVUnits, + getBlockNumber, + getTxBlock, + mineBlocks, +} from '../../helpers/index.ts'; +import { ethers } from 'ethers'; +import { Errors } from '../../common/errors.ts'; +import { Events } from '../../common/events.ts'; + +describe("Validator Lifecycle", function () { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + async function registerOps( + network: any, + count: number, + fee: bigint, + isPrivate = false, + ): Promise { + const ids: number[] = []; + for (let i = 1; i <= count; i++) { + const id = await network + .connect(operatorOwner) + .registerOperator.staticCall(makeOperatorKey(i), fee, isPrivate); + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), fee, isPrivate); + ids.push(Number(id)); + } + return ids; + } + + describe("Register Validator — New Cluster with 4 Public Operators", () => { + it("Registers validator, verifies default ETH fee applied, fees accrue correctly", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const regTx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const regReceipt = await regTx.wait(); + const regBlock = BigInt(regReceipt!.blockNumber); + + await expect(regTx).to.emit(network, Events.VALIDATOR_ADDED); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(BigInt(opId)); + expect(opData.validatorCount).to.equal(1n); + } + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(1n); + expect(cluster.active).to.equal(true); + expect(BigInt(cluster.balance)).to.equal(DEFAULT_ETH_REGISTER_VALUE); + + await mineBlocks(provider, 100); + + const viewBlock = BigInt(await getBlockNumber(provider)); + const blockDiff = viewBlock - regBlock; + const vUnits = defaultVUnits(1n); + const expectedEarnings = calcOperatorFeeAccrual(blockDiff, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + const earnings = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earnings).to.equal(expectedEarnings); + + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const expectedBurn = calcClusterBurn({ + blockDiff, + numOperators: BigInt(operatorIds.length), + ethFee: packedFee, + networkFee: packedNetworkFee, + effectiveVUnits: vUnits, + }); + const expectedClusterBalance = DEFAULT_ETH_REGISTER_VALUE - expectedBurn; + const clusterBalance = await views.getBalance( + clusterOwner.address, + operatorIds, + cluster, + ); + expect(clusterBalance).to.equal(expectedClusterBalance); + }); + + it("Register on operators with fee=0 — zero fee accrual", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOps(network, 4, 0n); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + await mineBlocks(provider, 100); + + const earnings = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earnings).to.equal(0n); + }); + }); + + describe("Register Validator — Existing Cluster with Fee Settlement", () => { + it("Adds validator to existing cluster, settles fees from first period", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const reg1Tx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const block1 = BigInt(await getTxBlock(reg1Tx)); + + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(1n); + + await mineBlocks(provider, 50); + + const earningsViewBlock = BigInt(await getBlockNumber(provider)); + const vUnits1 = defaultVUnits(1n); + const expectedEarningsBeforeSecond = calcOperatorFeeAccrual(earningsViewBlock - block1, packedFee, vUnits1) * ETH_DEDUCTED_DIGITS; + const earningsBeforeSecond = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsBeforeSecond).to.equal(expectedEarningsBeforeSecond); + + const deposit2 = ethers.parseEther("5"); + + const reg2Tx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: deposit2 }, + ); + const block2 = BigInt(await getTxBlock(reg2Tx)); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + expect(BigInt(cluster.validatorCount)).to.equal(2n); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(BigInt(opId)); + expect(opData.validatorCount).to.equal(2n); + } + + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const firstPeriodBurn = calcClusterBurn({ + blockDiff: block2 - block1, + numOperators: BigInt(operatorIds.length), + ethFee: packedFee, + networkFee: packedNetworkFee, + effectiveVUnits: vUnits1, + }); + const expectedClusterBalance = DEFAULT_ETH_REGISTER_VALUE + deposit2 - firstPeriodBurn; + expect(BigInt(cluster.balance)).to.be.lessThan(DEFAULT_ETH_REGISTER_VALUE + deposit2); + expect(BigInt(cluster.balance)).to.equal(expectedClusterBalance); + + await mineBlocks(provider, 100); + + const earningsAfterSecondPeriod = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAfterSecondPeriod).to.be.greaterThan( + earningsBeforeSecond, + ); + }); + }); + + describe("Register Validator on Private Operators", () => { + it("Non-whitelisted caller reverts, whitelisted caller succeeds", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const customFee = 5_000_000_000n; + const operatorIds = await registerOps(network, 4, customFee, true); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(BigInt(opId)); + expect(opData.isPrivate).to.equal(true); + } + + await expect( + network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.CALLER_NOT_WHITELISTED, + ); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const regTx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + await regTx.wait(); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(BigInt(opId)); + expect(opData.fee).to.equal(customFee); + expect(opData.validatorCount).to.equal(1n); + } + }); + + it("Mix of public and private operators in same cluster", async () => { + const { network } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + for (let i = 1; i <= 2; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), fee, false); + } + for (let i = 3; i <= 4; i++) { + await network + .connect(operatorOwner) + .registerOperator(makeOperatorKey(i), fee, true); + } + + const operatorIds = [1, 2, 3, 4]; + + await expect( + network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.CALLER_NOT_WHITELISTED, + ); + + await whitelistAddresses(network, operatorOwner, [3, 4], [ + clusterOwner.address, + ]); + + await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(1n); + }); + }); + + describe("Bulk Register Validators", () => { + it("Bulk registers 3 validators, verifies counts and events", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOps(network, 4, MINIMAL_OPERATOR_ETH_FEE); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + const depositEth = ethers.parseEther("30"); + + const pubkeys = [makePublicKey(1), makePublicKey(2), makePublicKey(3)]; + const shares = [DEFAULT_SHARES, DEFAULT_SHARES, DEFAULT_SHARES]; + + const bulkTx = await network + .connect(clusterOwner) + .bulkRegisterValidator(pubkeys, operatorIds, shares, EMPTY_CLUSTER, { + value: depositEth, + }); + const bulkReceipt = await bulkTx.wait(); + + const validatorAddedEvents = bulkReceipt!.logs.filter((log: any) => { + try { + const parsed = network.interface.parseLog(log); + return parsed?.name === Events.VALIDATOR_ADDED; + } catch { + return false; + } + }); + expect(validatorAddedEvents.length).to.equal(3); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(3n); + expect(BigInt(cluster.balance)).to.equal(depositEth); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(BigInt(opId)); + expect(opData.validatorCount).to.equal(3n); + } + + const networkAddress = await network.getAddress(); + const contractBalance = await provider.getBalance(networkAddress); + expect(contractBalance).to.be.greaterThanOrEqual(depositEth); + }); + + it("Bulk register with 0 public keys reverts EmptyPublicKeysList", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOps(network, 4, MINIMAL_OPERATOR_ETH_FEE); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await expect( + network + .connect(clusterOwner) + .bulkRegisterValidator([], operatorIds, [], EMPTY_CLUSTER, { + value: DEFAULT_ETH_REGISTER_VALUE, + }), + ).to.be.revertedWithCustomError(network, Errors.EMPTY_PUBLIC_KEYS_LIST); + }); + + it("Bulk register with mismatched lengths reverts", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const operatorIds = await registerOps(network, 4, MINIMAL_OPERATOR_ETH_FEE); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await expect( + network.connect(clusterOwner).bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES], + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.PUBLIC_KEYS_SHARES_LENGTH_MISMATCH, + ); + }); + + it("Bulk register with duplicate key reverts", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOps(network, 4, MINIMAL_OPERATOR_ETH_FEE); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await expect( + network.connect(clusterOwner).bulkRegisterValidator( + [makePublicKey(1), makePublicKey(1)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError( + network, + Errors.VALIDATOR_ALREADY_REGISTERED, + ); + }); + }); + + describe("Remove Validator — Fee Settlement and Count Adjustment", () => { + it("Removes validator from 2-validator cluster, settles fees correctly", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + const reg1Tx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const blockR1 = BigInt(await getTxBlock(reg1Tx)); + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + const reg2Tx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: ethers.parseEther("5") }, + ); + const blockR2 = BigInt(await getTxBlock(reg2Tx)); + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(2n); + + await mineBlocks(provider, 100); + + const earningsViewBlock = BigInt(await getBlockNumber(provider)); + const expectedEarningsBeforeRemove = ( + calcOperatorFeeAccrual(blockR2 - blockR1, packedFee, defaultVUnits(1n)) + + calcOperatorFeeAccrual(earningsViewBlock - blockR2, packedFee, defaultVUnits(2n)) + ) * ETH_DEDUCTED_DIGITS; + const earningsBeforeRemove = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsBeforeRemove).to.equal(expectedEarningsBeforeRemove); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + const removeTx = await network + .connect(clusterOwner) + .removeValidator(makePublicKey(1), operatorIds, cluster); + await removeTx.wait(); + + await expect(removeTx).to.emit(network, Events.VALIDATOR_REMOVED); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(1n); + expect(cluster.active).to.equal(true); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(BigInt(opId)); + expect(opData.validatorCount).to.equal(1n); + } + + const earningsAfterRemove = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAfterRemove).to.be.greaterThan(earningsBeforeRemove); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + const remove2Tx = await network + .connect(clusterOwner) + .removeValidator(makePublicKey(2), operatorIds, cluster); + await remove2Tx.wait(); + }); + + it("Remove non-existent validator reverts with ValidatorDoesNotExist", async () => { + const { network } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOps(network, 4, MINIMAL_OPERATOR_ETH_FEE); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + + await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + await expect( + network + .connect(clusterOwner) + .removeValidator(makePublicKey(999), operatorIds, cluster), + ).to.be.revertedWithCustomError( + network, + Errors.VALIDATOR_DOES_NOT_EXIST, + ); + }); + }); + + describe("Remove Last Validator — Cluster Balance Preservation", () => { + it("Removes last validator, cluster persists with remaining balance", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = MINIMAL_OPERATOR_ETH_FEE; + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + const depositEth = ethers.parseEther("5"); + + const regTx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositEth }, + ); + const regBlock = BigInt(await getTxBlock(regTx)); + await mineBlocks(provider, 50); + + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + const removeTx = await network + .connect(clusterOwner) + .removeValidator(makePublicKey(1), operatorIds, cluster); + const removeBlock = BigInt(await getTxBlock(removeTx)); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(0n); + expect(cluster.active).to.equal(true); + + const packedFee = fee / ETH_DEDUCTED_DIGITS; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const vUnits = defaultVUnits(1n); + const burn = calcClusterBurn({ + blockDiff: removeBlock - regBlock, + numOperators: BigInt(operatorIds.length), + ethFee: packedFee, + networkFee: packedNetworkFee, + effectiveVUnits: vUnits, + }); + expect(BigInt(cluster.balance)).to.equal(depositEth - burn); + + for (const opId of operatorIds) { + const opData = await views.getOperatorById(BigInt(opId)); + expect(opData.validatorCount).to.equal(0n); + } + + const ownerBalBefore = await provider.getBalance( + clusterOwner.address, + ); + const remainingBalance = BigInt(cluster.balance); + const withdrawTx = await network + .connect(clusterOwner) + .withdraw(operatorIds, remainingBalance, cluster); + const withdrawReceipt = await withdrawTx.wait(); + const gasUsed = + withdrawReceipt!.gasUsed * withdrawReceipt!.gasPrice; + + const ownerBalAfter = await provider.getBalance( + clusterOwner.address, + ); + expect(ownerBalAfter - ownerBalBefore + gasUsed).to.equal( + remainingBalance, + ); + }); + }); + + describe("Full Validator Lifecycle", () => { + it("Register → advance → remove → advance → withdraw — verifies complete lifecycle", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = 2_000_000_000n; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + const depositEth = ethers.parseEther("20"); + + const regTx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositEth }, + ); + const regReceipt = await regTx.wait(); + const regBlock = BigInt(regReceipt!.blockNumber); + + let cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.balance)).to.equal(depositEth); + + await mineBlocks(provider, 100); + + const vUnits = defaultVUnits(1n); + const phase2ViewBlock = BigInt(await getBlockNumber(provider)); + const expectedEarningsPhase2 = calcOperatorFeeAccrual(phase2ViewBlock - regBlock, packedFee, vUnits) * ETH_DEDUCTED_DIGITS; + const earningsPhase2 = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsPhase2).to.equal(expectedEarningsPhase2); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + const removeTx = await network + .connect(clusterOwner) + .removeValidator(makePublicKey(1), operatorIds, cluster); + const removeBlock = BigInt(await getTxBlock(removeTx)); + + cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + expect(BigInt(cluster.validatorCount)).to.equal(0n); + + const expectedBurn = calcClusterBurn({ + blockDiff: removeBlock - regBlock, + numOperators: BigInt(operatorIds.length), + ethFee: packedFee, + networkFee: packedNetworkFee, + effectiveVUnits: vUnits, + }); + const balanceAfterRemove = BigInt(cluster.balance); + expect(balanceAfterRemove).to.be.lessThan(depositEth); + expect(balanceAfterRemove).to.equal(depositEth - expectedBurn); + + await mineBlocks(provider, 50); + + const earningsPhase4 = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + + const earningsPhase4Later = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsPhase4Later).to.equal(earningsPhase4); + + const ownerBalBefore = await provider.getBalance( + operatorOwner.address, + ); + const withdrawTx = await network + .connect(operatorOwner) + .withdrawAllOperatorEarnings(BigInt(operatorIds[0])); + const withdrawReceipt = await withdrawTx.wait(); + const gasUsed = + withdrawReceipt!.gasUsed * withdrawReceipt!.gasPrice; + + await expect(withdrawTx).to.emit(network, Events.OPERATOR_WITHDRAWN); + + const ownerBalAfter = await provider.getBalance( + operatorOwner.address, + ); + const operatorWithdrawal = ownerBalAfter - ownerBalBefore + gasUsed; + expect(operatorWithdrawal).to.equal(earningsPhase4); + + const earningsAfterWithdraw = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(earningsAfterWithdraw).to.equal(0n); + + const networkEarnings = await views.getNetworkEarnings(); + let totalOpEarnings = 0n; + for (const opId of operatorIds) { + totalOpEarnings += await views.getOperatorEarnings(BigInt(opId)); + } + totalOpEarnings += operatorWithdrawal; + + const totalSystem = + balanceAfterRemove + totalOpEarnings + networkEarnings; + + const diff = + totalSystem > depositEth + ? totalSystem - depositEth + : depositEth - totalSystem; + expect(diff).to.be.lessThanOrEqual( + ETH_DEDUCTED_DIGITS * 10n, + ); + }); + + it("Verifies exact fee math with block-precise accounting", async () => { + const { network, views } = + await networkHelpers.loadFixture(deployFixture); + const provider = connection.ethers.provider; + + const fee = 2_000_000_000n; + const packedFee = fee / ETH_DEDUCTED_DIGITS; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + + const operatorIds = await registerOps(network, 4, fee); + + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + ]); + const depositEth = ethers.parseEther("20"); + + const regTx = await network + .connect(clusterOwner) + .registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositEth }, + ); + const regReceipt = await regTx.wait(); + const regBlock = BigInt(regReceipt!.blockNumber); + + await mineBlocks(provider, 100); + + const currentBlock = BigInt(await getBlockNumber(provider)); + const blockDiff = currentBlock - regBlock; + + const vUnits = defaultVUnits(1n); + const expectedAccrualPacked = calcOperatorFeeAccrual( + blockDiff, + packedFee, + vUnits, + ); + const expectedAccrualWei = expectedAccrualPacked * ETH_DEDUCTED_DIGITS; + + const actualEarnings = await views.getOperatorEarnings( + BigInt(operatorIds[0]), + ); + expect(actualEarnings).to.equal(expectedAccrualWei); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + const expectedClusterBurn = calcClusterBurn({ + blockDiff, + numOperators: BigInt(operatorIds.length), + ethFee: packedFee, + networkFee: packedNetworkFee, + effectiveVUnits: vUnits, + }); + const expectedClusterBalance = depositEth - expectedClusterBurn; + const clusterBalance = await views.getBalance( + clusterOwner.address, + operatorIds, + cluster, + ); + + expect(clusterBalance).to.be.lessThan(depositEth); + expect(clusterBalance).to.equal(expectedClusterBalance); + + const networkEarnings = await views.getNetworkEarnings(); + let totalOpEarnings = 0n; + for (const opId of operatorIds) { + totalOpEarnings += await views.getOperatorEarnings(BigInt(opId)); + } + const totalSystem = clusterBalance + totalOpEarnings + networkEarnings; + const conservationDiff = totalSystem > depositEth + ? totalSystem - depositEth + : depositEth - totalSystem; + expect(conservationDiff).to.be.lessThanOrEqual(ETH_DEDUCTED_DIGITS * 100n); + }); + }); +}); diff --git a/test/echidna/CSSVTokenAccessControlEchidna.sol b/test/echidna/CSSVTokenAccessControlEchidna.sol new file mode 100644 index 000000000..674ad0942 --- /dev/null +++ b/test/echidna/CSSVTokenAccessControlEchidna.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/token/CSSVToken.sol"; + +contract UnauthorizedMinter { + CSSVToken public token; + bool public mintSucceeded; + bool public burnSucceeded; + + function setToken(address _token) external { + token = CSSVToken(_token); + } + + function tryMint(address to, uint256 amount) external { + try token.mint(to, amount) { + mintSucceeded = true; + } catch { + mintSucceeded = false; + } + } + + function tryBurn(address from, uint256 amount) external { + try token.burn(from, amount) { + burnSucceeded = true; + } catch { + burnSucceeded = false; + } + } +} + +contract CSSVTokenAccessControlEchidna is CSSVToken { + UnauthorizedMinter public attacker; + + address constant USER1 = address(0x10000); + + constructor() CSSVToken(address(this)) { + attacker = new UnauthorizedMinter(); + attacker.setToken(address(this)); + _mint(USER1, 1000 ether); + } + + function onCSSVTransfer(address, address, uint256) external view { + require(msg.sender == address(this)); + } + + function action_attackerTryMint(uint256 amount) public { + amount = amount % 1_000_000 ether; + attacker.tryMint(address(attacker), amount); + } + + function action_attackerTryBurn(uint256 amount) public { + uint256 balance = balanceOf(USER1); + if (balance == 0) return; + amount = amount % balance; + attacker.tryBurn(USER1, amount); + } + + function echidna_attacker_cannot_mint() public view returns (bool) { + return !attacker.mintSucceeded(); + } + + function echidna_attacker_cannot_burn() public view returns (bool) { + return !attacker.burnSucceeded(); + } + + function echidna_only_self_is_staking() public view returns (bool) { + return ssvStaking == address(this); + } +} diff --git a/test/echidna/CSSVTokenEchidna.sol b/test/echidna/CSSVTokenEchidna.sol new file mode 100644 index 000000000..e34500e6c --- /dev/null +++ b/test/echidna/CSSVTokenEchidna.sol @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/token/CSSVToken.sol"; +import "../../contracts/test/mocks/MockToken.sol"; + +contract CSSVTokenEchidna is CSSVToken { + uint256 private constant SSV_SUPPLY_CAP = 1_000_000_000 ether; + + uint256 public totalMinted; + uint256 public totalBurned; + uint256 public callbackCount; + bool public headroomAccountingViolation; + + MockToken private ssvToken; + + address constant USER1 = address(0x10000); + address constant USER2 = address(0x20000); + address constant USER3 = address(0x30000); + address constant USER4 = address(0x40000); + + constructor() CSSVToken(address(this)) { + ssvToken = new MockToken(); + ssvToken.mint(address(this), SSV_SUPPLY_CAP); + } + + function onCSSVTransfer(address, address, uint256) external { + require(msg.sender == address(this), "Only self"); + callbackCount++; + } + + function _getUser(uint8 seed) internal pure returns (address) { + uint8 idx = seed % 4; + if (idx == 0) return USER1; + if (idx == 1) return USER2; + if (idx == 2) return USER3; + return USER4; + } + + function _boundAmount(uint256 amount) internal pure returns (uint256) { + amount = amount % 1_000_000 ether; + if (amount == 0) amount = 1 ether; + return amount; + } + + function _mintableAmount(uint256 requestedAmount) internal view returns (uint256) { + uint256 cssvSupply = totalSupply(); + uint256 ssvSupply = ssvToken.totalSupply(); + if (cssvSupply >= ssvSupply) return 0; + + uint256 headroom = ssvSupply - cssvSupply; + return requestedAmount > headroom ? headroom : requestedAmount; + } + + function action_mint(uint256 amount, uint8 userSeed) public { + amount = _boundAmount(amount); + amount = _mintableAmount(amount); + if (amount == 0) return; + + address to = _getUser(userSeed); + _mint(to, amount); + totalMinted += amount; + } + + function action_burn(uint256 amount, uint8 userSeed) public { + address from = _getUser(userSeed); + uint256 balance = balanceOf(from); + if (balance == 0) return; + + amount = amount % balance; + if (amount == 0) amount = 1; + + _burn(from, amount); + totalBurned += amount; + } + + function action_mintLarge(uint8 userSeed) public { + address to = _getUser(userSeed); + uint256 currentSupply = totalSupply(); + + if (currentSupply > type(uint256).max - 10000 ether) return; + + uint256 amount = _mintableAmount(10000 ether); + if (amount == 0) return; + + _mint(to, amount); + totalMinted += amount; + } + + function action_rapidMintBurn(uint256 amount, uint8 userSeed, uint8 iterations) public { + address user = _getUser(userSeed); + amount = _boundAmount(amount); + amount = _mintableAmount(amount); + if (amount == 0) return; + + iterations = iterations % 10 + 1; + + for (uint8 i = 0; i < iterations; i++) { + _mint(user, amount); + _burn(user, amount); + } + } + + function action_mintToAll(uint256 amount) public { + amount = _boundAmount(amount); + uint256 headroom = _mintableAmount(type(uint256).max); + if (headroom < 4) return; + if (amount > headroom / 4) amount = headroom / 4; + if (amount == 0) return; + + _mint(USER1, amount); + _mint(USER2, amount); + _mint(USER3, amount); + _mint(USER4, amount); + + totalMinted += amount * 4; + } + + function action_mint_headroom_accounting(uint256 amount, uint8 userSeed) public { + uint256 requested = _boundAmount(amount); + uint256 supplyBefore = totalSupply(); + uint256 headroomBefore = _mintableAmount(type(uint256).max); + uint256 expectedMint = requested > headroomBefore ? headroomBefore : requested; + address to = _getUser(userSeed); + + if (expectedMint != 0) { + _mint(to, expectedMint); + totalMinted += expectedMint; + } + + if (totalSupply() != supplyBefore + expectedMint) { + headroomAccountingViolation = true; + } + if (totalSupply() > ssvToken.totalSupply()) { + headroomAccountingViolation = true; + } + } + + function action_near_cap_roundtrip(uint256 burnSeed, uint8 userSeed) public { + address user = _getUser(userSeed); + uint256 ssvSupply = ssvToken.totalSupply(); + uint256 headroom = _mintableAmount(type(uint256).max); + if (headroom == 0) return; + + _mint(user, headroom); + totalMinted += headroom; + if (totalSupply() != ssvSupply) { + headroomAccountingViolation = true; + } + + uint256 balance = balanceOf(user); + if (balance == 0) return; + uint256 burnAmount = burnSeed % balance; + if (burnAmount == 0) burnAmount = 1; + + _burn(user, burnAmount); + totalBurned += burnAmount; + + uint256 remintRequest = burnAmount + 1; + uint256 remintAmount = _mintableAmount(remintRequest); + if (remintAmount != burnAmount) { + headroomAccountingViolation = true; + } + if (remintAmount != 0) { + _mint(user, remintAmount); + totalMinted += remintAmount; + } + + if (totalSupply() != ssvSupply) { + headroomAccountingViolation = true; + } + if (totalSupply() > ssvSupply) { + headroomAccountingViolation = true; + } + } + + function action_burnFromAll(uint256 amount) public { + uint256 bal1 = balanceOf(USER1); + uint256 bal2 = balanceOf(USER2); + uint256 bal3 = balanceOf(USER3); + uint256 bal4 = balanceOf(USER4); + + uint256 minBal = bal1; + if (bal2 < minBal) minBal = bal2; + if (bal3 < minBal) minBal = bal3; + if (bal4 < minBal) minBal = bal4; + + if (minBal == 0) return; + + amount = amount % minBal; + if (amount == 0) amount = 1; + + _burn(USER1, amount); + _burn(USER2, amount); + _burn(USER3, amount); + _burn(USER4, amount); + + totalBurned += amount * 4; + } + + function action_burnAll(uint8 userSeed) public { + address user = _getUser(userSeed); + uint256 balance = balanceOf(user); + + if (balance == 0) return; + + _burn(user, balance); + totalBurned += balance; + } + + function action_internalTransfer(uint8 fromSeed, uint8 toSeed, uint256 amount) public { + address from = _getUser(fromSeed); + address to = _getUser(toSeed); + if (from == to) return; + + uint256 balance = balanceOf(from); + if (balance == 0) return; + + amount = amount % balance; + if (amount == 0) amount = 1; + + _transfer(from, to, amount); + } + + function echidna_supply_equals_minted_minus_burned() public view returns (bool) { + return totalSupply() == totalMinted - totalBurned; + } + + function echidna_burned_lte_minted() public view returns (bool) { + return totalBurned <= totalMinted; + } + + function echidna_individual_balance_lte_supply() public view returns (bool) { + return balanceOf(USER1) <= totalSupply() && + balanceOf(USER2) <= totalSupply() && + balanceOf(USER3) <= totalSupply() && + balanceOf(USER4) <= totalSupply() && + balanceOf(address(this)) <= totalSupply(); + } + + function echidna_staking_is_self() public view returns (bool) { + return ssvStaking == address(this); + } + + function echidna_name_immutable() public view returns (bool) { + return keccak256(bytes(name())) == keccak256(bytes("cSSV")); + } + + function echidna_symbol_immutable() public view returns (bool) { + return keccak256(bytes(symbol())) == keccak256(bytes("cSSV")); + } + + function echidna_decimals_is_18() public view returns (bool) { + return decimals() == 18; + } + + function echidna_zero_address_has_no_balance() public view returns (bool) { + return balanceOf(address(0)) == 0; + } + + function echidna_cssv_supply_lte_ssv_total_supply() public view returns (bool) { + return totalSupply() <= ssvToken.totalSupply(); + } + + function echidna_headroom_accounting_consistent() public view returns (bool) { + return !headroomAccountingViolation; + } +} diff --git a/test/echidna/README.md b/test/echidna/README.md new file mode 100644 index 000000000..05b104987 --- /dev/null +++ b/test/echidna/README.md @@ -0,0 +1,391 @@ +# Echidna Invariant Testing — SSV Network v2 + +Fuzz testing for SSV Network v2 smart contracts using [Echidna](https://github.com/crytic/echidna). + +## Quick Start (macOS) + +```bash +bash test/echidna/run-echidna.sh +``` + +Both CI and `bash test/echidna/run-echidna.sh` auto-discover every harness matching `test/echidna/*Echidna.sol`. + +## Manual Setup + +```bash +brew install echidna solc-select +solc-select install 0.8.24 && solc-select use 0.8.24 +``` + +## Running Tests + +```bash +echidna test/echidna/CSSVTokenEchidna.sol --contract CSSVTokenEchidna --config test/echidna/echidna.yaml +echidna test/echidna/CSSVTokenAccessControlEchidna.sol --contract CSSVTokenAccessControlEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVOperatorsEchidna.sol --contract SSVOperatorsEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVClustersEchidna.sol --contract SSVClustersEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVAccountingEchidna.sol --contract SSVAccountingEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVEdgeCasesEchidna.sol --contract SSVEdgeCasesEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVValidatorsEchidna.sol --contract SSVValidatorsEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVWhitelistValidatorsEchidna.sol --contract SSVWhitelistValidatorsEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVStakingEchidna.sol --contract SSVStakingEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVDAOEchidna.sol --contract SSVDAOEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVMigrationEchidna.sol --contract SSVMigrationEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVEBProofEchidna.sol --contract SSVEBProofEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVOperatorFeeGovEchidna.sol --contract SSVOperatorFeeGovEchidna --config test/echidna/echidna.yaml +echidna test/echidna/SSVLegacyClustersEchidna.sol --contract SSVLegacyClustersEchidna --config test/echidna/echidna.yaml +``` + +## Files + +``` +test/echidna/ +├── CSSVTokenEchidna.sol # Core invariants (9 tests) +├── CSSVTokenAccessControlEchidna.sol # Access control (3 tests) +├── SSVOperatorsEchidna.sol # Operators invariants (20 tests) +├── SSVClustersEchidna.sol # Clusters invariants (19 tests) +├── SSVAccountingEchidna.sol # System accounting invariants (7 tests) +├── SSVEdgeCasesEchidna.sol # Edge-case invariants (7 tests) +├── SSVValidatorsEchidna.sol # Validators invariants (8 tests) +├── SSVWhitelistValidatorsEchidna.sol # Private-operator registration invariants (5 tests) +├── SSVStakingEchidna.sol # Staking invariants (16 tests) +├── SSVDAOEchidna.sol # DAO invariants (23 tests) +├── SSVMigrationEchidna.sol # Migration invariants (6 tests) [BUG-14] +├── SSVEBProofEchidna.sol # EB proof invariants (3 tests) [FUZZ-3 B6/B7/B8] +├── SSVOperatorFeeGovEchidna.sol # Operator fee governance (1 test) [FUZZ-3 B19] +├── SSVLegacyClustersEchidna.sol # Legacy SSV cluster liquidation (1 test) [FUZZ-3 B15] +├── echidna.yaml +├── run-echidna.sh +└── README.md +``` + +## CSSVTokenEchidna (9 Invariants) + +| Property | Description | +|----------|-------------| +| `echidna_supply_equals_minted_minus_burned` | Supply integrity | +| `echidna_burned_lte_minted` | No underflow | +| `echidna_individual_balance_lte_supply` | No balance > supply | +| `echidna_staking_is_self` | ssvStaking immutable | +| `echidna_name_immutable` | Name is "cSSV" | +| `echidna_symbol_immutable` | Symbol is "cSSV" | +| `echidna_decimals_is_18` | Standard decimals | +| `echidna_zero_address_has_no_balance` | Zero addr check | +| `echidna_supply_non_negative` | No negative supply | + +## CSSVTokenAccessControlEchidna (3 Invariants) + +| Property | Description | +|----------|-------------| +| `echidna_attacker_cannot_mint` | Unauthorized mint blocked | +| `echidna_attacker_cannot_burn` | Unauthorized burn blocked | +| `echidna_only_self_is_staking` | Single authorized address | + +## SSVOperatorsEchidna (20 Invariants) + +| Property | Description | +|----------|-------------| +| `echidna_unique_active_pubkeys` | No duplicate active operator public keys | +| `echidna_id_monotonic` | Operator IDs strictly increase | +| `echidna_registered_owners_non_zero` | Owners are non-zero | +| `echidna_eth_fee_within_max` | ETH fee <= max fee | +| `echidna_eth_fee_minimum` | ETH fee is 0 or >= minimum | +| `echidna_declare_does_not_change_fee` | Declaration does not change fee | +| `echidna_execute_requires_valid_window` | Execute respects approval window | +| `echidna_execute_rejects_invalid_fee` | Execute rejects invalid fee | +| `echidna_reduce_fee_decreases` | Reduce strictly decreases fee | +| `echidna_withdraw_limit_enforced` | Cannot withdraw more than earnings | +| `echidna_withdraw_all_clears_balance` | withdrawAll clears balance | +| `echidna_withdraw_conserves_balance` | Withdrawals conserve balances | +| `echidna_earnings_monotonic` | Earnings never decrease without withdrawals | +| `echidna_fee_change_latency` | Fee change applies only after execution | +| `echidna_eth_withdraw_keeps_ssv` | ETH withdraws do not touch SSV earnings | +| `echidna_ssv_withdraw_keeps_eth` | SSV withdraws do not touch ETH earnings | +| `echidna_owner_only_actions` | Owner-only access enforced | +| `echidna_remove_cleans_state` | Removal zeroes operator state | +| `echidna_remove_pays_out` | Removal pays out and reduces holdings | +| `echidna_declare_fee_from_zero_reverts` | **[FUZZ-3 B17]** Declaring non-zero ETH fee when both fees are 0 reverts | + +## SSVClustersEchidna (19 Invariants) + +This harness also instantiates staking claimants and operator owners so `echidna_eth_balance_accounting` is exercised through `claimEthRewards` and `withdrawOperatorEarnings`, not only cluster flows. + +| Property | Description | +|----------|-------------| +| `echidna_cluster_hash_consistent` | Stored cluster hash matches local view | +| `echidna_inactive_clusters_zeroed` | Inactive clusters are zeroed | +| `echidna_cluster_balance_accounting` | Cluster balance accounting matches totals | +| `echidna_withdraw_limit_enforced` | Cannot withdraw more than balance | +| `echidna_withdraw_conserves_balance` | Withdrawals conserve balances | +| `echidna_owner_withdraw_only` | Only owner can withdraw | +| `echidna_liquidation_cleans_state` | Liquidation zeroes cluster and pays out | +| `echidna_reactivate_requires_inactive` | Reactivation only from inactive | +| `echidna_dust_liquidation_reachable` | Dust balances become liquidatable after burn | +| `echidna_eb_snapshot_block_lte_current` | EB snapshot update block never exceeds current block | +| `echidna_eb_snapshot_root_monotonic` | Cluster EB root block number never decreases | +| `echidna_eb_update_requires_root` | EB update cannot succeed without a committed root | +| `echidna_eb_update_requires_latest_root` | EB update must use the latest committed root | +| `echidna_eb_update_frequency` | EB update frequency limit is enforced | +| `echidna_eb_update_staleness` | EB updates reject stale root block numbers | +| `echidna_inactive_eb_update_skips_accounting` | Inactive/liquidated ETH EB updates only refresh the EB snapshot | +| `echidna_fee_index_current_after_settle` | Cluster fee indices settle to current protocol indices | +| `echidna_fee_uses_old_vunits_on_eb_change` | Fee settlement on EB change uses pre-update vUnits | +| `echidna_eth_balance_accounting` | ETH balance covers cluster, operator, DAO, and staking liabilities | + +## SSVAccountingEchidna (7 Invariants) + +| Property | Description | +|----------|-------------| +| `echidna_eth_conservation` | ETH conservation across clusters/operators/DAO | +| `echidna_ssv_conservation` | SSV conservation across clusters/operators/DAO | +| `echidna_eth_solvency` | ETH solvency for all tracked balances | +| `echidna_operator_vunits_matches_clusters` | Per-operator deviation equals sum of cluster deviations containing that operator (C6) | +| `echidna_migration_one_way` | After migrateClusterToETH: SSV cluster deleted, ETH cluster active (C7) | +| `echidna_ssv_accrual_no_overflow` | SSV operator balance never decreases during max-param accrual (X5) | +| `echidna_vunits_deviation_consistent` | daoTotalEthVUnits equals sum of effective vUnits across all active ETH clusters (C5) | + +## SSVEdgeCasesEchidna (7 Invariants) + +| Property | Description | +|----------|-------------| +| `echidna_yoyo_liquidation_reactivates` | Repeated liquidate/reactivate remains reachable | +| `echidna_reactivation_restores_vunits` | Reactivation restores EB-weighted vUnits | +| `echidna_validator_spam_safe` | High validator counts do not corrupt snapshots | +| `echidna_fee_index_overflow_protected` | Fee index overflow paths revert safely | +| `echidna_eth_accrual_no_overflow` | ETH operator balance never decreases during max-param accrual (X4) | +| `echidna_intermediate_mul_no_overflow` | `fee * effectiveVUnits` product stays within uint128 for max protocol params (X6) | +| `echidna_pack_reverts_on_overflow` | Packing a value exceeding uint64 max reverts, never truncates (X7) | + +## SSVValidatorsEchidna (8 Invariants) + +This harness now drives both single and bulk registration/removal/exit paths. + +| Property | Description | +|----------|-------------| +| `echidna_validator_hash_consistent` | Validator state matches stored operator ids | +| `echidna_cluster_hash_consistent` | Cluster hash matches local view | +| `echidna_cluster_validator_counts` | Cluster validatorCount matches active validators | +| `echidna_operator_validator_counts` | Operator ethValidatorCount matches expectations | +| `echidna_cluster_balance_accounting` | Cluster balances sum to expected total | +| `echidna_no_duplicate_validators` | Duplicate validators cannot be registered | +| `echidna_owner_only_remove` | Only owner can remove validators | +| `echidna_owner_only_exit` | Only owner can exit validators | + +## SSVWhitelistValidatorsEchidna (5 Invariants) + +This harness focuses on private-operator registration coverage across both single and bulk registration: direct whitelist address flow, whitelist-contract flow, mixed public/private clusters with a zero-fee private operator, and a legacy SSV private operator that is initialized for ETH fees before ETH-cluster registration. + +| Property | Description | +|----------|-------------| +| `echidna_private_registration_access_control` | Unauthorized callers cannot single-register or bulk-register clusters that include private operators | +| `echidna_private_authorized_paths_consistent` | Authorized single-register and bulk-register mixed-cluster / whitelist-contract paths keep validator state and operator fee assumptions intact | +| `echidna_legacy_private_eth_init_preserves_whitelist` | A legacy SSV private operator keeps its whitelist semantics after ETH defaults are initialized and it is used in a new ETH cluster through single or bulk registration | +| `echidna_whitelist_operator_counts_consistent` | Operator ETH validator counts and DAO ETH validator totals stay consistent across private/public registration scenarios | +| `echidna_whitelist_cluster_hashes_consistent` | Successful private-operator registrations keep stored ETH cluster hashes consistent, and unauthorized registrations do not create clusters | + +## SSVStakingEchidna (16 Invariants) + +| Property | Description | +|----------|-------------| +| `echidna_sync_fees_handles_decrease` | Sync fees does not fail when earnings decrease | +| `echidna_sync_fees_never_fails` | Sync fees never fails or mismatches | +| `echidna_invalid_stake_reverts` | Invalid stake amounts are rejected | +| `echidna_invalid_unstake_reverts` | Invalid unstake requests are rejected | +| `echidna_invalid_withdraw_reverts` | Withdraw with no unlocked balance is rejected | +| `echidna_cssv_supply_matches_users` | cSSV supply matches tracked user balances | +| `echidna_cssv_supply_lte_ssv_backing` | cSSV supply never exceeds SSV backing | +| `echidna_ssv_balance_matches_staked_plus_pending` | Contract SSV balance equals staked plus pending | +| `echidna_pool_matches_dao_balance` | ETH pool balance matches DAO balance | +| `echidna_claim_twice_same_block_no_second_payout` | A second reward claim in the same block cannot pay out twice | +| `echidna_pending_requests_bounded` | Withdrawal request count stays within bounds | +| `echidna_user_index_leq_acc` | User index never exceeds global accumulator | +| `echidna_accrued_within_pool` | Accrued rewards stay within pool balance | +| `echidna_cssv_transfer_settles_both` | cSSV transfer settles sender and receiver reward indices | +| `echidna_claim_payout_precision` | Claimed ETH payout always respects packing precision | +| `echidna_no_free_rewards_on_transfer` | Transfers cannot move already-accrued rewards between users | + +## SSVDAOEchidna (23 Invariants) + +| Property | Description | +|----------|-------------| +| `echidna_network_fee_matches_expected` | ETH network fee index is consistent with block number | +| `echidna_network_fee_ssv_matches_expected` | SSV network fee index is consistent with block number | +| `echidna_liquidation_thresholds_valid` | Liquidation thresholds respect minimums | +| `echidna_quorum_bps_valid` | Quorum stays within bounds | +| `echidna_dao_balance_matches_expected` | DAO balance matches token holdings | +| `echidna_withdraw_limits_enforced` | Withdrawals cannot exceed balance | +| `echidna_withdraw_conserves_balance` | Withdrawals conserve balances | +| `echidna_commit_root_only_oracle` | Only oracles can commit roots | +| `echidna_commit_root_no_duplicate_votes` | Oracles cannot vote twice on the same key | +| `echidna_commit_root_not_future` | Commit block is not in the future | +| `echidna_commit_root_not_stale` | Commit block is newer than last committed | +| `echidna_committed_block_monotonic` | Latest committed block is monotonic | +| `echidna_commit_root_dust_round_reaches_quorum` | Shared-root dusty round still commits on the third vote at 75% quorum | +| `echidna_commit_root_dust_round_not_before_threshold` | Dusty shared-root round cannot commit before the third unique vote | +| `echidna_commit_root_dust_round_uses_truncated_supply` | Pending dusty rounds store truncated frozen voting supply | +| `echidna_commit_root_below_oracle_count_reverts` | Rounds with supply below oracle count always revert with zero weight | +| `echidna_oracle_mapping_consistent` | Oracle ID mappings remain consistent | +| `echidna_finalized_weight_cleared` | Finalized commitment keys clear accumulated weight | +| `echidna_commitment_weight_lte_supply` | Commitment weight never exceeds the round's frozen voting supply | +| `echidna_finalization_implies_quorum` | Root finalization only happens at/above the quorum threshold for the round's frozen voting supply | +| `echidna_dao_earnings_monotonic` | Gross DAO earnings do not decrease over time | +| `echidna_dao_index_block_lte_current` | DAO index block numbers never exceed current block | +| `echidna_dao_earnings_matches_formula` | **[FUZZ-3 C4]** ETH DAO earnings matches `daoBalance + blockDelta × fee × vUnits / precision` | + +## SSVEBProofEchidna (3 Invariants) — FUZZ-3 B6/B7/B8 + +Tests `updateClusterBalance` Merkle proof correctness and EB bounds enforcement. +Setup: single operator (zero fees), 4-validator ETH cluster, single-leaf Merkle tree built in-harness. + +| Property | Description | +|----------|-------------| +| `echidna_eb_merkle_proof_verified` | **[B6]** A tampered `effectiveBalance` (≠ committed value) is rejected by the proof check | +| `echidna_eb_bounds_enforced` | **[B7]** `effectiveBalance` outside `[validatorCount×32, validatorCount×2048]` is rejected | +| `echidna_eb_snapshot_fields_exact` | **[B8]** After a valid update: `vUnits == ebToVUnits(eb)`, `lastRootBlockNum == blockNum`, `lastUpdateBlock == block.number` | + +## SSVOperatorFeeGovEchidna (1 Invariant) — FUZZ-3 B19 + +Tests that `executeOperatorFee` rejects fee-change requests whose `approvalBeginTime` predates the migration. +Setup: `UPGRADE_TIMESTAMP = 1`; legacy requests are planted directly into storage with `approvalBeginTime = 1`. + +| Property | Description | +|----------|-------------| +| `echidna_execute_rejects_legacy_declarations` | **[B19]** `executeOperatorFee` always reverts when the stored declaration has `approvalBeginTime ≤ UPGRADE_TIMESTAMP` | + +## SSVLegacyClustersEchidna (1 Invariant) — FUZZ-3 B15 + +Tests that `liquidateSSV` correctly resets legacy SSV cluster state and transfers the SSV balance to the liquidator. +Setup: two SSV operators with non-zero fees, one active SSV cluster, liquidator == cluster owner (self-liquidation path). + +| Property | Description | +|----------|-------------| +| `echidna_ssv_liquidation_resets_and_pays` | **[B15]** After `liquidateSSV` succeeds: cluster is inactive with zeroed indexes/balance, and the SSV balance was fully transferred to the liquidator | + +## SSVMigrationEchidna (6 Invariants) — BUG-14 + +Tests SSV→ETH migration accounting when operators were removed before migration and must keep their frozen SSV indices, plus the legacy `updateClusterBalance` snapshot-only path that prepares SSV clusters for future migration. +Setup: one legacy SSV cluster with three operators, with harness actions for operator removal, block advancement, legacy EB updates, self-liquidation, and ETH migration from both active and liquidated states. + +| Property | Description | +|----------|-------------| +| `echidna_migration_removed_refund_exact` | On successful SSV→ETH migration, refunded SSV equals settlement computed with the full cumulative SSV index, including removed operators' frozen `snapshot.index` | +| `echidna_migration_removed_operator_not_eth_initialized` | Operators removed before migration remain excluded from ETH initialization and ETH validator-count updates | +| `echidna_migration_net_zero_validators` | Successful active-cluster migration shifts validator counts from SSV DAO accounting to ETH DAO accounting without changing the total | +| `echidna_removed_operator_state_and_frozen_index_preserved` | Removed operators keep zeroed snapshot blocks while preserving their frozen `snapshot.index` across later actions | +| `echidna_liquidated_migration_branch_correct` | Successful migration of an already-liquidated SSV cluster keeps SSV DAO counts unchanged, initializes the ETH cluster, and does not refund extra SSV | +| `echidna_ssv_eb_update_only_snapshot` | Legacy `updateClusterBalance` updates only `clusterEB` and leaves SSV cluster/accounting state unchanged | + +--- + +## Planned Invariants (Remaining) + +Evaluated from `ssv-review/planning/SSVNetwork — Enrich Invariant Suite.md` against the 119 existing invariants above. Only invariants that are **not already covered** are listed below. Grouped by priority. + +### Strengthen Existing (partial coverage → full) + +These existing invariants should be upgraded to catch more subtle bugs: + +| Existing Property | Upgrade | Ref | +|---|---|---| +| `echidna_network_fee_matches_expected` | Add explicit monotonicity: track `prevEthIndex` / `prevSsvIndex` in harness, assert never decreases | A8 | +| `echidna_cssv_supply_matches_users` | Add per-operation delta: on stake `amount`, assert cSSV supply increased by exactly `amount` | A11 | +| `echidna_user_index_leq_acc` | Strengthen to exact equality: after `_settle(user)`, assert `userIndex[user] == accEthPerShare` | A14 | +| `echidna_pool_matches_dao_balance` | Add per-claim delta: on successful claim of `payout`, assert both `stakingEthPoolBalance` and `ethDaoBalance` decreased by exactly `payout` | A16 | +| `echidna_accrued_within_pool` | Add cumulative tracking: wrap `claimEthRewards` to track `totalEthPaidOut`, assert `totalEthPaidOut <= totalEthCredited` | C2 | + +### High Priority — New Invariants + +Directly testable with current harness patterns. High bug-catching value. + +#### Oracle / EB Governance + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_finalized_weight_cleared` | Always | If `ebRoots[blockNum] == root != 0`, then `rootCommitments[key] == 0` — prevents re-finalization | A4 | +| `echidna_commitment_weight_lte_supply` | Always | For each tracked `commitmentKey`, `rootCommitments[key] <= roundFrozenSupply[key]` while the round is pending — catches quorum overflow | A5 | +| `echidna_finalization_implies_quorum` | Conditional | At finalization time, accumulated weight >= `threshold(roundFrozenSupply[key], quorumBps)` — catches quorum bypass | B1 | + +#### DAO Accounting + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_dao_earnings_monotonic` | Always | `networkTotalEarnings()` (ETH) and `networkTotalEarningsSSV()` never decrease as `block.number` advances — catches settlement regression | A9 | +| `echidna_dao_index_block_lte_current` | Always | `ethDaoIndexBlockNumber <= block.number` and `daoIndexBlockNumber <= block.number` — catches "time-travel" indices | A10 | + +#### Staking Rewards Precision + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_cssv_transfer_settles_both` | Always | After `onCSSVTransfer(from, to, amount)`, both `userIndex[from]` and `userIndex[to]` equal `accEthPerShare` — catches reward smuggling via transfer | A15 | +| `echidna_claim_payout_precision` | Always | Any successful claim `payout` satisfies `payout % ETH_DEDUCTED_DIGITS == 0` — catches precision bypass | A17 | +| `echidna_no_free_rewards_on_transfer` | Candidate | cSSV transfer does not move already-accrued rewards from sender to receiver — catches reward smuggling (needs 2-actor before/after tracking) | C3 | + +#### EB Snapshot Safety + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_eb_snapshot_block_lte_current` | Always | `clusterEB[id].lastUpdateBlock <= block.number` — catches future-dated EB snapshots | A18 | +| `echidna_eb_snapshot_root_monotonic` | Always | `clusterEB[id].lastRootBlockNum` never decreases per cluster — catches stale proof replay | A19 | + +#### EB Update Correctness + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_eb_update_requires_root` | Conditional | `updateClusterBalance(blockNum, ...)` succeeds only if `ebRoots[blockNum] != 0` | B3 | +| `echidna_eb_update_requires_latest_root` | Conditional | `updateClusterBalance(blockNum, ...)` with a valid but non-latest committed root must revert | SSV-17 | +| `echidna_eb_update_frequency` | Conditional | Same cluster cannot update twice within `minBlocksBetweenUpdates` — second update reverts | B4 | +| `echidna_eb_update_staleness` | Conditional | Successful update requires `blockNum > lastRootBlockNum` for that cluster | B5 | + +#### Fee Settlement Correctness + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_fee_index_current_after_settle` | Conditional | After ETH cluster fee settlement, stored fee indices equal protocol "current" indices | B9 | +| `echidna_fee_uses_old_vunits_on_eb_change` | Conditional | When EB update changes vUnits, fees for elapsed period use old vUnits, not new | B11 | + +#### Liquidation Completeness + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_liquidation_pays_exact_balance` | Conditional | ETH paid to liquidator equals cluster balance at liquidation time — catches over/underpayment | B14 | + +### Medium Priority — New Invariants + +Requires more harness bookkeeping or complex setup (Merkle builder, multi-actor tracking). + +> **FUZZ-3 complete**: B6, B7, B8 → `SSVEBProofEchidna.sol`; B17 → `SSVOperatorsEchidna.sol`; B19 → `SSVOperatorFeeGovEchidna.sol`; B15 → `SSVLegacyClustersEchidna.sol`; C4 → `SSVDAOEchidna.sol`. + +### Lower Priority — Heavy Harness Required + +Significant implementation effort. Requires custom delta-block simulators, per-cluster tracking arrays, or boundary-probing helpers. + +#### vUnit Aggregation + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_dao_vunits_equals_sum` | Candidate | `daoTotalEthVUnits == Σ(cluster baseline) ± Σ(cluster deviations)` — catches vUnit drift | C5 | +| `echidna_operator_vunits_matches_clusters` | Candidate | Per-operator vUnits equals sum of their cluster deviations — catches earnings misallocation | C6 | + +#### Overflow / Extreme Value + +| Planned Property | Type | Description | Ref | +|---|---|---|---| +| `echidna_eth_accrual_no_overflow` | Candidate | With max fee, max validators, max EB, simulating 5 years of blocks: all ETH balances + indices remain within type bounds | X4 | +| `echidna_ssv_accrual_no_overflow` | Candidate | Same as above for SSV scaling factor and fee math | X5 | +| `echidna_intermediate_mul_no_overflow` | Candidate | For worst-case params, `fee * vUnits * deltaBlocks` stays `< type(uint256).max` | X6 | +| `echidna_pack_reverts_on_overflow` | Candidate | Packing `uint256 → uint64` reverts (not truncates) when value exceeds range | X7 | + +### Harness Requirements for Planned Invariants + +To make the above invariants exercisable, the following harness features are needed: + +| Harness Feature | Required By | Description | +|---|---|---| +| **Prev-value tracking** | A8, A9, A18, A19 | Store `prevIndex`, `prevEarnings`, `prevBlock` in harness to assert monotonicity | +| **Touched-key arrays** | A4, A5, B1 | Track `bytes32[] touchedCommitmentKeys` since mappings aren't iterable | +| **Per-claim delta tracking** | A16, C2 | Wrap `claimEthRewards` to capture before/after pool balances | +| **2-actor reward tracking** | A15, C3 | Track accrued rewards for both sender/receiver around cSSV transfers | +| **Merkle tree builder** | B6, B7, B8 | Tiny in-harness Merkle builder for valid proof happy paths | +| **Delta-block simulator** | X4, X5, X6 | Test-only function that applies fee accrual math with explicit `deltaBlocks` input | +| **Per-cluster EB tracking** | C5, C6 | Arrays tracking baseline and deviation per cluster for global sum verification | +| **Max-param configurator** | X4, X5, X6, X7 | Helpers to set operator fee = max, validators = max, EB = max bound | diff --git a/test/echidna/SSVAccountingEchidna.sol b/test/echidna/SSVAccountingEchidna.sol new file mode 100644 index 000000000..94b26183b --- /dev/null +++ b/test/echidna/SSVAccountingEchidna.sol @@ -0,0 +1,1527 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/interfaces/ISSVClusters.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/interfaces/ISSVOperators.sol"; +import "../../contracts/interfaces/ISSVValidators.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/libraries/OperatorLib.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/modules/SSVDAO.sol"; +import "../../contracts/modules/SSVOperators.sol"; +import "../../contracts/modules/SSVValidators.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "./SSVStakingEchidna.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETHLib, PackedSSVLib} from "../../contracts/libraries/SSVPackedLib.sol"; +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO, DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract ClusterUser { + ISSVClusters public clusters; + + constructor(ISSVClusters clusters_) { + clusters = clusters_; + } + + receive() external payable {} + + function withdraw( + uint64[] calldata operatorIds, + uint256 amount, + ISSVNetworkCore.Cluster memory cluster + ) external { + clusters.withdraw(operatorIds, amount, cluster); + } + + function liquidate( + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + clusters.liquidate(clusterOwner, operatorIds, cluster); + } + + function liquidateSSV( + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + clusters.liquidateSSV(clusterOwner, operatorIds, cluster); + } + + function reactivate( + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + clusters.reactivate{value: msg.value}(operatorIds, cluster); + } + + function migrate( + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + clusters.migrateClusterToETH{value: msg.value}(operatorIds, cluster); + } +} + +contract OperatorUser { + ISSVOperators public operators; + + constructor(ISSVOperators operators_) { + operators = operators_; + } + + receive() external payable {} + + function withdraw(uint64 operatorId, uint256 amount) external { + operators.withdrawOperatorEarnings(operatorId, amount); + } + + function withdrawAll(uint64 operatorId) external { + operators.withdrawAllOperatorEarnings(operatorId); + } + + function withdrawSSV(uint64 operatorId, uint256 amount) external { + operators.withdrawOperatorEarningsSSV(operatorId, amount); + } + + function withdrawAllSSV(uint64 operatorId) external { + operators.withdrawAllOperatorEarningsSSV(operatorId); + } +} + +contract ValidatorUser { + ISSVValidators public validators; + + constructor(ISSVValidators validators_) { + validators = validators_; + } + + receive() external payable {} + + function register( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + validators.registerValidator{value: msg.value}(publicKey, operatorIds, sharesData, cluster); + } + + function remove( + bytes calldata publicKey, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + validators.removeValidator(publicKey, operatorIds, cluster); + } + + function exit(bytes calldata publicKey, uint64[] calldata operatorIds) external { + validators.exitValidator(publicKey, operatorIds); + } +} + +contract SSVAccountingEchidna is SSVClusters, SSVOperators(0), SSVDAO, SSVValidators { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using ProtocolLib for StorageProtocol; + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; + + uint8 private constant MAX_ETH_CLUSTERS = 6; + uint8 private constant MAX_SSV_CLUSTERS = 6; + uint8 private constant MAX_LIFECYCLE_VALIDATORS = 24; + uint8 private constant LIFECYCLE_OPERATORS_KEY = 2; + uint32 private constant MAX_ADVANCE_BLOCKS = 8; + PackedETH private constant DEFAULT_OPERATOR_ETH_FEE = PackedETH.wrap(1); + PackedSSV private constant DEFAULT_OPERATOR_SSV_FEE = PackedSSV.wrap(1); + PackedETH private constant DEFAULT_NETWORK_ETH_FEE = PackedETH.wrap(1); + PackedSSV private constant DEFAULT_NETWORK_SSV_FEE = PackedSSV.wrap(1); + uint64 private constant MIN_BLOCKS_BEFORE_LIQUIDATION = 2; + uint64 private constant MAX_SSV_MINT_UNITS = 1_000_000; + + MockToken private token; + + ClusterUser private owner1; + ClusterUser private owner2; + ClusterUser private liquidator; + + OperatorUser private opOwner1; + OperatorUser private opOwner2; + OperatorUser private opOwner3; + ValidatorUser private validatorOwner; + ValidatorUser private validatorAttacker; + + uint64 private op1; + uint64 private op2; + uint64 private op3; + + struct ClusterRecord { + ISSVNetworkCore.Cluster cluster; + address owner; + uint8 operatorsKey; + bool exists; + } + + bytes32[] private ethClusterIds; + bytes32[] private ssvClusterIds; + mapping(bytes32 => ClusterRecord) private ethClusters; + mapping(bytes32 => ClusterRecord) private ssvClusters; + + uint64[] private operatorIds; + mapping(uint64 => address) private operatorOwner; + + uint256 private totalEthIn; + uint256 private totalEthOut; + uint256 private totalSsvIn; + uint256 private totalSsvOut; + uint256 private unallocatedEth; + uint256 private unallocatedSsv; + + bytes32[] private migratedClusterIds; + mapping(bytes32 => bool) private migratedSet; + bool private ssvAccrualCorrupted; + bool private daoSsvWithdrawMismatch; + bool private daoSsvOverWithdrawSucceeded; + bytes32 private lifecycleClusterId; + bool private lifecycleClusterInitialized; + bool private lifecycleClusterPathTouched; + bool private lifecycleStateViolation; + bool private lifecycleUnauthorizedSucceeded; + + struct LifecycleValidatorRecord { + bytes publicKey; + bool active; + } + + uint256[] private lifecycleValidatorIds; + mapping(uint256 => LifecycleValidatorRecord) private lifecycleValidators; + mapping(bytes32 => uint256) private lifecycleValidatorKeyToId; + uint256 private nextLifecycleValidatorId; + + constructor() SSVDAO(address(new CSSVTokenMock(address(this)))) { + token = new MockToken(); + _mockSetToken(address(token)); + + ISSVClusters clustersSelf = ISSVClusters(address(this)); + ISSVOperators operatorsSelf = ISSVOperators(address(this)); + ISSVValidators validatorsSelf = ISSVValidators(address(this)); + + owner1 = new ClusterUser(clustersSelf); + owner2 = new ClusterUser(clustersSelf); + liquidator = new ClusterUser(clustersSelf); + + opOwner1 = new OperatorUser(operatorsSelf); + opOwner2 = new OperatorUser(operatorsSelf); + opOwner3 = new OperatorUser(operatorsSelf); + validatorOwner = new ValidatorUser(validatorsSelf); + validatorAttacker = new ValidatorUser(validatorsSelf); + + _initProtocolDefaults(); + _initOperators(); + } + + receive() external payable {} + + function action_fund_eth(uint256 amount) external payable { + amount; + if (msg.value == 0) return; + totalEthIn += msg.value; + unallocatedEth += msg.value; + } + + function action_fund_ssv(uint256 seed) external { + uint64 units = uint64(seed % (uint256(MAX_SSV_MINT_UNITS) + 1)); + if (units == 0) return; + uint256 amount = uint256(units) * DEDUCTED_DIGITS; + token.mint(address(this), amount); + totalSsvIn += amount; + unallocatedSsv += amount; + } + + function action_create_eth_cluster(uint256 seed) external { + _settleTime(); + if (ethClusterIds.length >= MAX_ETH_CLUSTERS) return; + + address owner = (seed % 2 == 0) ? address(owner1) : address(owner2); + uint8 operatorsKey = uint8((seed >> 8) % 3); + uint64[] memory operatorIdsLocal = _operatorIdsForKey(operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(owner, operatorIdsLocal)); + + if (ethClusters[clusterId].exists || ssvClusters[clusterId].exists || migratedSet[clusterId]) return; + + uint32 validatorCount = uint32((seed >> 16) % 6) + 1; + + ISSVNetworkCore.Cluster memory cluster = ISSVNetworkCore.Cluster({ + validatorCount: validatorCount, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + + SSVStorage.load().ethClusters[clusterId] = cluster.hashClusterData(); + + ethClusters[clusterId] = ClusterRecord({ + cluster: cluster, + owner: owner, + operatorsKey: operatorsKey, + exists: true + }); + ethClusterIds.push(clusterId); + } + + function action_create_ssv_cluster(uint256 seed) external { + _settleTime(); + if (ssvClusterIds.length >= MAX_SSV_CLUSTERS) return; + + address owner = (seed % 2 == 0) ? address(owner1) : address(owner2); + uint8 operatorsKey = uint8((seed >> 8) % 3); + uint64[] memory operatorIdsLocal = _operatorIdsForKey(operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(owner, operatorIdsLocal)); + + if (ssvClusters[clusterId].exists || ethClusters[clusterId].exists || migratedSet[clusterId]) return; + + uint32 validatorCount = uint32((seed >> 16) % 6) + 1; + + ISSVNetworkCore.Cluster memory cluster = ISSVNetworkCore.Cluster({ + validatorCount: validatorCount, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + + SSVStorage.load().clusters[clusterId] = cluster.hashClusterData(); + + ssvClusters[clusterId] = ClusterRecord({ + cluster: cluster, + owner: owner, + operatorsKey: operatorsKey, + exists: true + }); + ssvClusterIds.push(clusterId); + } + + function action_register_validator_lifecycle(uint256 seed) external { + _settleTime(); + + if (lifecycleValidatorIds.length >= MAX_LIFECYCLE_VALIDATORS) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(LIFECYCLE_OPERATORS_KEY); + bytes32 clusterId = _lifecycleClusterHash(operatorIdsLocal); + ClusterRecord storage record = ethClusters[clusterId]; + + ISSVNetworkCore.Cluster memory cluster = record.exists + ? record.cluster + : ISSVNetworkCore.Cluster({ + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + active: true, + balance: 0 + }); + + bytes memory publicKey = _makePublicKey(seed); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(validatorOwner))); + bytes memory shares = _makeShares(seed); + uint256 amount = _boundAmount(seed >> 8, unallocatedEth); + + if (lifecycleValidatorKeyToId[validatorKey] != 0) { + try validatorOwner.register{value: amount}(publicKey, operatorIdsLocal, shares, cluster) { + lifecycleStateViolation = true; + } catch {} + return; + } + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint32 daoBefore = sp.ethDaoValidatorCount; + uint32 op1Before = s.operators[op1].ethValidatorCount; + uint32 op2Before = s.operators[op2].ethValidatorCount; + uint32 op3Before = s.operators[op3].ethValidatorCount; + + try validatorOwner.register{value: amount}(publicKey, operatorIdsLocal, shares, cluster) { + ISSVNetworkCore.Cluster memory nextCluster = cluster; + nextCluster.balance += amount; + uint64 clusterIndex = _currentClusterIndexEth(operatorIdsLocal); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + nextCluster.updateClusterData(clusterId, clusterIndex, networkFeeIndex); + nextCluster.validatorCount += 1; + nextCluster.active = true; + + record.cluster = nextCluster; + record.owner = address(validatorOwner); + record.operatorsKey = LIFECYCLE_OPERATORS_KEY; + record.exists = true; + + lifecycleClusterInitialized = true; + lifecycleClusterPathTouched = true; + lifecycleClusterId = clusterId; + + if (amount != 0) { + unallocatedEth -= amount; + } + + nextLifecycleValidatorId += 1; + lifecycleValidators[nextLifecycleValidatorId] = LifecycleValidatorRecord({ + publicKey: publicKey, + active: true + }); + lifecycleValidatorIds.push(nextLifecycleValidatorId); + lifecycleValidatorKeyToId[validatorKey] = nextLifecycleValidatorId; + + if (sp.ethDaoValidatorCount != daoBefore + 1) { + lifecycleStateViolation = true; + } + if ( + s.operators[op1].ethValidatorCount != op1Before + 1 || + s.operators[op2].ethValidatorCount != op2Before + 1 || + s.operators[op3].ethValidatorCount != op3Before + 1 + ) { + lifecycleStateViolation = true; + } + } catch {} + } + + function action_remove_validator_lifecycle(uint256 seed) external { + _settleTime(); + + uint256 validatorId = _pickActiveLifecycleValidatorId(seed); + if (validatorId == 0 || !lifecycleClusterInitialized) return; + + ClusterRecord storage record = ethClusters[lifecycleClusterId]; + if (!record.exists) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(LIFECYCLE_OPERATORS_KEY); + bytes memory publicKey = lifecycleValidators[validatorId].publicKey; + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint32 daoBefore = sp.ethDaoValidatorCount; + uint32 op1Before = s.operators[op1].ethValidatorCount; + uint32 op2Before = s.operators[op2].ethValidatorCount; + uint32 op3Before = s.operators[op3].ethValidatorCount; + + try validatorOwner.remove(publicKey, operatorIdsLocal, cluster) { + ISSVNetworkCore.Cluster memory nextCluster = cluster; + if (nextCluster.active) { + uint64 clusterIndex = _currentClusterIndexEth(operatorIdsLocal); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + nextCluster.updateClusterData(lifecycleClusterId, clusterIndex, networkFeeIndex); + } + if (nextCluster.validatorCount == 0) { + lifecycleStateViolation = true; + return; + } + nextCluster.validatorCount -= 1; + record.cluster = nextCluster; + + lifecycleValidators[validatorId].active = false; + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(validatorOwner))); + if (lifecycleValidatorKeyToId[validatorKey] == validatorId) { + lifecycleValidatorKeyToId[validatorKey] = 0; + } + + if (daoBefore == 0 || sp.ethDaoValidatorCount != daoBefore - 1) { + lifecycleStateViolation = true; + } + if ( + op1Before == 0 || + op2Before == 0 || + op3Before == 0 || + s.operators[op1].ethValidatorCount != op1Before - 1 || + s.operators[op2].ethValidatorCount != op2Before - 1 || + s.operators[op3].ethValidatorCount != op3Before - 1 + ) { + lifecycleStateViolation = true; + } + } catch {} + } + + function action_exit_validator_lifecycle(uint256 seed) external { + _settleTime(); + + uint256 validatorId = _pickActiveLifecycleValidatorId(seed); + if (validatorId == 0) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(LIFECYCLE_OPERATORS_KEY); + bytes memory publicKey = lifecycleValidators[validatorId].publicKey; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint32 daoBefore = sp.ethDaoValidatorCount; + uint32 op1Before = s.operators[op1].ethValidatorCount; + uint32 op2Before = s.operators[op2].ethValidatorCount; + uint32 op3Before = s.operators[op3].ethValidatorCount; + + try validatorOwner.exit(publicKey, operatorIdsLocal) { + if (sp.ethDaoValidatorCount != daoBefore) { + lifecycleStateViolation = true; + } + if ( + s.operators[op1].ethValidatorCount != op1Before || + s.operators[op2].ethValidatorCount != op2Before || + s.operators[op3].ethValidatorCount != op3Before + ) { + lifecycleStateViolation = true; + } + } catch {} + } + + function action_remove_validator_unauthorized(uint256 seed) external { + _settleTime(); + + uint256 validatorId = _pickActiveLifecycleValidatorId(seed); + if (validatorId == 0 || !lifecycleClusterInitialized) return; + + ClusterRecord storage record = ethClusters[lifecycleClusterId]; + if (!record.exists) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(LIFECYCLE_OPERATORS_KEY); + bytes memory publicKey = lifecycleValidators[validatorId].publicKey; + try validatorAttacker.remove(publicKey, operatorIdsLocal, record.cluster) { + lifecycleUnauthorizedSucceeded = true; + } catch {} + } + + function action_exit_validator_unauthorized(uint256 seed) external { + _settleTime(); + + uint256 validatorId = _pickActiveLifecycleValidatorId(seed); + if (validatorId == 0) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(LIFECYCLE_OPERATORS_KEY); + bytes memory publicKey = lifecycleValidators[validatorId].publicKey; + try validatorAttacker.exit(publicKey, operatorIdsLocal) { + lifecycleUnauthorizedSucceeded = true; + } catch {} + } + + function action_reactivate_eth(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickEthClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ethClusters[clusterId]; + if (!record.exists || record.cluster.active) return; + + if (unallocatedEth == 0) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + + PackedETH burnRate; + for (uint256 i; i < operatorIdsLocal.length; ++i) { + burnRate = burnRate.add(s.operators[operatorIdsLocal[i]].ethFee); + } + uint256 minPerBlock = uint256(PackedETH.unwrap(burnRate) + PackedETH.unwrap(sp.ethNetworkFee)) * uint64(record.cluster.validatorCount) * ETH_DEDUCTED_DIGITS; + uint256 minRequired = minPerBlock * (MAX_ADVANCE_BLOCKS + 2); + if (minRequired == 0) minRequired = ETH_DEDUCTED_DIGITS; + + uint256 amount = _boundAmount(seed >> 8, unallocatedEth); + if (amount < minRequired) amount = minRequired; + if (amount > unallocatedEth) return; + + ClusterUser owner = _clusterOwnerUser(record.owner); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + try owner.reactivate{value: amount}(operatorIdsLocal, cluster) { + record.cluster.active = true; + record.cluster.balance += amount; + record.cluster.index = _currentClusterIndexEth(operatorIdsLocal); + record.cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + unallocatedEth -= amount; + + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch {} + } + + function action_activate_ssv(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickSsvClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ssvClusters[clusterId]; + if (!record.exists || record.cluster.active) return; + + if (unallocatedSsv == 0) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + + PackedSSV burnRate; + for (uint256 i; i < operatorIdsLocal.length; ++i) { + burnRate = burnRate.add(s.operators[operatorIdsLocal[i]].fee); + } + uint256 minPerBlock = uint256(PackedSSV.unwrap(burnRate) + PackedSSV.unwrap(sp.networkFee)) * uint64(record.cluster.validatorCount) * DEDUCTED_DIGITS; + uint256 minRequired = minPerBlock * (MAX_ADVANCE_BLOCKS + 2); + if (minRequired == 0) minRequired = DEDUCTED_DIGITS; + + uint256 amount = _boundAmount(seed >> 8, unallocatedSsv); + if (amount < minRequired) amount = minRequired; + if (amount > unallocatedSsv) return; + if (token.balanceOf(address(this)) < amount) return; + + (uint64 clusterIndex, ) = OperatorLib.updateClusterOperatorsSSV( + operatorIdsLocal, + true, + record.cluster.validatorCount, + s, + sp + ); + + record.cluster.balance += amount; + record.cluster.active = true; + record.cluster.index = clusterIndex; + record.cluster.networkFeeIndex = sp.currentNetworkFeeIndexSSV(); + + sp.updateDAOSSV(true, record.cluster.validatorCount); + + s.clusters[clusterId] = record.cluster.hashClusterData(); + unallocatedSsv -= amount; + } + + function action_deposit_eth(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickEthClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ethClusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + + if (unallocatedEth == 0) return; + uint256 amount = _boundAmount(seed >> 8, unallocatedEth); + if (amount == 0) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + try this.deposit{value: amount}(record.owner, operatorIdsLocal, cluster) { + record.cluster.balance += amount; + unallocatedEth -= amount; + } catch {} + } + + function action_deposit_ssv(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickSsvClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ssvClusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + + if (unallocatedSsv == 0) return; + uint256 amount = _boundAmount(seed >> 8, unallocatedSsv); + if (amount == 0) return; + if (token.balanceOf(address(this)) < amount) return; + + record.cluster.balance += amount; + SSVStorage.load().clusters[clusterId] = record.cluster.hashClusterData(); + unallocatedSsv -= amount; + } + + function action_withdraw_eth(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickEthClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ethClusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (record.cluster.balance == 0) return; + + uint256 amount = _boundAmount(seed >> 8, record.cluster.balance); + if (amount == 0) return; + if (amount > address(this).balance) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + ClusterUser owner = _clusterOwnerUser(record.owner); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + uint256 ownerBefore = record.owner.balance; + try owner.withdraw(operatorIdsLocal, amount, cluster) { + _settleEthCluster(clusterId, record, operatorIdsLocal); + if (record.cluster.balance >= amount) { + record.cluster.balance -= amount; + } else { + record.cluster.balance = 0; + } + totalEthOut += record.owner.balance - ownerBefore; + + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch {} + } + + function action_liquidate_eth(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickEthClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ethClusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + uint256 liquidatorBefore = address(liquidator).balance; + try liquidator.liquidate(record.owner, operatorIdsLocal, cluster) { + _settleEthCluster(clusterId, record, operatorIdsLocal); + record.cluster.active = false; + record.cluster.balance = 0; + record.cluster.index = 0; + record.cluster.networkFeeIndex = 0; + totalEthOut += address(liquidator).balance - liquidatorBefore; + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch {} + } + + function action_liquidate_ssv(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickSsvClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ssvClusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + uint256 liquidatorBefore = token.balanceOf(address(liquidator)); + try liquidator.liquidateSSV(record.owner, operatorIdsLocal, cluster) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + _settleSsvCluster(clusterId, record, operatorIdsLocal); + record.cluster.active = false; + record.cluster.balance = 0; + record.cluster.index = 0; + record.cluster.networkFeeIndex = 0; + + totalSsvOut += token.balanceOf(address(liquidator)) - liquidatorBefore; + SSVStorage.load().clusters[clusterId] = record.cluster.hashClusterData(); + } catch {} + } + + function action_withdraw_operator_eth(uint256 seed) external { + _settleTime(); + uint64 operatorId = _pickOperatorId(seed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory operator = SSVStorage.load().operators[operatorId]; + PackedETH balance = operator.ethSnapshot.balance; + if (balance.eq(PACKED_ETH_ZERO)) return; + + PackedETH withdrawShrunk = PackedETH.wrap(uint64(seed % PackedETH.unwrap(balance)) + 1); + uint256 amount = PackedETHLib.unpack(withdrawShrunk); + if (amount > address(this).balance) return; + + uint256 ownerBefore = ownerAddr.balance; + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdraw(operatorId, amount) { + totalEthOut += ownerAddr.balance - ownerBefore; + } catch {} + } + + function action_withdraw_operator_ssv(uint256 seed) external { + _settleTime(); + uint64 operatorId = _pickOperatorId(seed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory operator = SSVStorage.load().operators[operatorId]; + PackedSSV balance = operator.snapshot.balance; + if (balance.eq(PACKED_SSV_ZERO)) return; + + PackedSSV withdrawShrunk = PackedSSV.wrap(uint64(seed % PackedSSV.unwrap(balance)) + 1); + uint256 amount = PackedSSVLib.unpack(withdrawShrunk); + if (amount > token.balanceOf(address(this))) return; + + uint256 ownerBefore = token.balanceOf(ownerAddr); + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdrawSSV(operatorId, amount) { + totalSsvOut += token.balanceOf(ownerAddr) - ownerBefore; + } catch {} + } + + function action_withdraw_dao_ssv(uint256 seed) external { + _settleTime(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 availableUnits = PackedSSV.unwrap(sp.daoBalance); + + bool tryOverWithdraw = (seed % 5 == 0) && availableUnits > 0; + uint64 withdrawUnitsRaw; + if (tryOverWithdraw) { + withdrawUnitsRaw = availableUnits + 1; + } else { + if (availableUnits == 0) return; + withdrawUnitsRaw = uint64(seed % availableUnits) + 1; + } + + uint256 amount = uint256(withdrawUnitsRaw) * DEDUCTED_DIGITS; + if (!tryOverWithdraw && amount > token.balanceOf(address(this))) return; + + uint64 daoBefore = PackedSSV.unwrap(sp.daoBalance); + + // Intentionally self-call the DAO module here: this harness checks accrual-backed + // daoBalance settlement only. Real token outflow is covered in SSVDAOEchidna. + try this.withdrawNetworkSSVEarnings(amount) { + if (tryOverWithdraw) { + daoSsvOverWithdrawSucceeded = true; + return; + } + + uint64 daoAfter = PackedSSV.unwrap(sp.daoBalance); + + // daoBalance must decrease by exactly the withdrawn packed units + if (daoAfter != daoBefore - withdrawUnitsRaw) daoSsvWithdrawMismatch = true; + // checkpoint must be reset to current block + if (sp.daoIndexBlockNumber != uint32(block.number)) daoSsvWithdrawMismatch = true; + } catch { + if (tryOverWithdraw) return; + } + } + + function action_update_network_fee(uint256 seed) external { + _settleTime(); + uint64 units = uint64(seed % 10); + uint256 fee = uint256(units) * ETH_DEDUCTED_DIGITS; + try this.updateNetworkFee(fee) {} catch {} + } + + function action_update_network_fee_ssv(uint256 seed) external { + _settleTime(); + uint64 units = uint64(seed % 10); + uint256 fee = uint256(units) * DEDUCTED_DIGITS; + try this.updateNetworkFeeSSV(fee) {} catch {} + } + + function action_advance_time(uint256 seed) external { + _settleTime(); + uint32 blocks = uint32(seed % MAX_ADVANCE_BLOCKS) + 1; + _fastForward(blocks); + _syncClusters(); + } + + function echidna_eth_conservation() external view returns (bool) { + return address(this).balance + totalEthOut >= totalEthIn; + } + + function echidna_ssv_conservation() external view returns (bool) { + return token.balanceOf(address(this)) <= totalSsvIn; + } + + function echidna_eth_solvency() external view returns (bool) { + return address(this).balance >= totalEthIn - totalEthOut; + } + + function action_migrate_ssv_cluster(uint256 seed) external { + _settleTime(); + bytes32 clusterId = _pickSsvClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = ssvClusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (unallocatedEth == 0) return; + + uint256 amount = _boundAmount(seed >> 8, unallocatedEth); + if (amount == 0) return; + + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + ClusterUser clusterOwner = _clusterOwnerUser(record.owner); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + StorageProtocol storage sp = SSVStorageProtocol.load(); + + uint256 ownerSsvBefore = token.balanceOf(record.owner); + try clusterOwner.migrate{value: amount}(operatorIdsLocal, cluster) { + ISSVNetworkCore.Cluster memory migratedCluster = ISSVNetworkCore.Cluster({ + validatorCount: cluster.validatorCount, + networkFeeIndex: cluster.networkFeeIndex, + index: cluster.index, + active: cluster.active, + balance: cluster.balance + }); + migratedCluster.balance = amount; + migratedCluster.active = true; + migratedCluster.index = _currentClusterIndexEth(operatorIdsLocal); + migratedCluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + + ClusterRecord storage ethRecord = ethClusters[clusterId]; + if (!ethRecord.exists) { + ethClusterIds.push(clusterId); + } + ethRecord.cluster = migratedCluster; + ethRecord.owner = record.owner; + ethRecord.operatorsKey = record.operatorsKey; + ethRecord.exists = true; + + if (!migratedSet[clusterId]) { + migratedSet[clusterId] = true; + migratedClusterIds.push(clusterId); + } + record.exists = false; + unallocatedEth -= amount; + totalSsvOut += token.balanceOf(record.owner) - ownerSsvBefore; + } catch {} + } + + function action_probe_max_ssv_accrual(uint256 seed) external { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + ISSVNetworkCore.Operator storage operator = s.operators[op1]; + if (operator.snapshot.block == 0) return; + + uint64 testFee = uint64(sp.operatorMaxFeeSSV); + uint32 testValidators = sp.validatorsPerOperatorLimit; + + operator.fee = PackedSSV.wrap(testFee); + operator.validatorCount = testValidators; + + PackedSSV balanceBefore = operator.snapshot.balance; + uint32 blocks = uint32(seed % 8) + 1; + uint32 currentBlock = uint32(block.number); + + uint64 blockDiffFee = uint64(blocks) * testFee; + operator.snapshot.index += blockDiffFee; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * uint64(testValidators))); + operator.snapshot.block = currentBlock; + + if (operator.snapshot.balance.lt(balanceBefore)) { + ssvAccrualCorrupted = true; + } + } + + function echidna_operator_vunits_matches_clusters() external view returns (bool) { + StorageEB storage seb = SSVStorageEB.load(); + + for (uint256 i; i < operatorIds.length; ++i) { + uint64 opId = operatorIds[i]; + uint64 opDeviation = seb.operatorEthVUnits[opId]; + + uint64 expectedDeviation; + for (uint256 j; j < ethClusterIds.length; ++j) { + bytes32 cId = ethClusterIds[j]; + ClusterRecord storage record = ethClusters[cId]; + if (!record.exists || !record.cluster.active) continue; + + uint64[] memory ops = _operatorIdsForKey(record.operatorsKey); + bool hasOp = false; + for (uint256 k; k < ops.length; ++k) { + if (ops[k] == opId) { hasOp = true; break; } + } + if (!hasOp) continue; + + uint64 clusterVUnits = seb.clusterEB[cId].vUnits; + if (clusterVUnits > 0) { + uint64 baseline = uint64(record.cluster.validatorCount) * BPS_DENOMINATOR; + if (clusterVUnits > baseline) { + expectedDeviation += clusterVUnits - baseline; + } + } + } + + if (opDeviation != expectedDeviation) return false; + } + return true; + } + + function echidna_dao_validator_count_consistent() external view returns (bool) { + return uint256(SSVStorageProtocol.load().ethDaoValidatorCount) == uint256(_expectedEthDaoValidatorCount()); + } + + function echidna_cluster_version_exclusive() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + + // Explicit lifecycle key coverage: this cluster may not be present in ethClusterIds. + if (lifecycleClusterInitialized) { + if (!lifecycleClusterPathTouched) return false; + if (s.ethClusters[lifecycleClusterId] == 0) return false; + if (s.clusters[lifecycleClusterId] != 0) return false; + } + + for (uint256 i; i < ethClusterIds.length; ++i) { + bytes32 clusterId = ethClusterIds[i]; + ClusterRecord storage record = ethClusters[clusterId]; + if (!record.exists) continue; + if (s.ethClusters[clusterId] == 0) return false; + if (s.clusters[clusterId] != 0) return false; + } + + for (uint256 i; i < ssvClusterIds.length; ++i) { + bytes32 clusterId = ssvClusterIds[i]; + ClusterRecord storage record = ssvClusters[clusterId]; + if (!record.exists) continue; + if (s.clusters[clusterId] == 0) return false; + if (s.ethClusters[clusterId] != 0) return false; + } + + for (uint256 i; i < migratedClusterIds.length; ++i) { + bytes32 clusterId = migratedClusterIds[i]; + if (s.ethClusters[clusterId] == 0) return false; + if (s.clusters[clusterId] != 0) return false; + } + + return true; + } + + function echidna_operator_total_validators_consistent() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + + for (uint256 i; i < operatorIds.length; ++i) { + uint64 operatorId = operatorIds[i]; + (uint32 expectedSsv, uint32 expectedEth) = _expectedOperatorCounts(operatorId); + + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (operator.validatorCount != expectedSsv) return false; + if (operator.ethValidatorCount != expectedEth) return false; + if ( + uint256(operator.validatorCount) + uint256(operator.ethValidatorCount) != + uint256(expectedSsv) + uint256(expectedEth) + ) return false; + } + + return true; + } + + function echidna_validator_lifecycle_consistent() external view returns (bool) { + return !lifecycleStateViolation && !lifecycleUnauthorizedSucceeded; + } + + function echidna_migration_one_way() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + for (uint256 i; i < migratedClusterIds.length; ++i) { + bytes32 cId = migratedClusterIds[i]; + if (s.clusters[cId] != 0) return false; + if (s.ethClusters[cId] == 0) return false; + } + return true; + } + + function echidna_ssv_accrual_no_overflow() external view returns (bool) { + return !ssvAccrualCorrupted; + } + + function echidna_dao_ssv_withdraw_conserves() external view returns (bool) { + return !daoSsvWithdrawMismatch; + } + + function echidna_dao_ssv_over_withdraw_reverts() external view returns (bool) { + return !daoSsvOverWithdrawSucceeded; + } + + function echidna_vunits_deviation_consistent() external view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + uint256 expected; + uint256 count = ethClusterIds.length; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = ethClusterIds[i]; + ClusterRecord storage record = ethClusters[clusterId]; + if (!record.exists || !record.cluster.active) continue; + + uint64 vUnits = seb.clusterEB[clusterId].vUnits; + if (vUnits == 0) { + vUnits = uint64(record.cluster.validatorCount) * BPS_DENOMINATOR; + } + + expected += vUnits; + } + + if (lifecycleClusterInitialized && !_isTrackedEthCluster(lifecycleClusterId)) { + ClusterRecord storage lifecycleRecord = ethClusters[lifecycleClusterId]; + if (lifecycleRecord.exists && lifecycleRecord.cluster.active) { + uint64 vUnits = seb.clusterEB[lifecycleClusterId].vUnits; + if (vUnits == 0) { + vUnits = uint64(lifecycleRecord.cluster.validatorCount) * BPS_DENOMINATOR; + } + expected += vUnits; + } + } + + // Migrated clusters are no longer in ethClusterIds but their validators + // are counted in daoTotalEthVUnits after migrateClusterToETH calls updateDAO. + uint256 migratedCount = migratedClusterIds.length; + for (uint256 i; i < migratedCount; ++i) { + bytes32 cId = migratedClusterIds[i]; + if (_isTrackedEthCluster(cId)) continue; + + uint64 vUnits = seb.clusterEB[cId].vUnits; + if (vUnits == 0) { + ClusterRecord storage record = ethClusters[cId]; + if (record.exists) { + vUnits = uint64(record.cluster.validatorCount) * BPS_DENOMINATOR; + } else { + vUnits = uint64(ssvClusters[cId].cluster.validatorCount) * BPS_DENOMINATOR; + } + } + expected += vUnits; + } + + return uint256(sp.daoTotalEthVUnits) == expected; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 1000; + sp.ethNetworkFee = DEFAULT_NETWORK_ETH_FEE; + sp.networkFee = DEFAULT_NETWORK_SSV_FEE; + sp.ethNetworkFeeIndex = 0; + sp.networkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.networkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.daoIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = MIN_BLOCKS_BEFORE_LIQUIDATION; + sp.minimumBlocksBeforeLiquidationSSV = MIN_BLOCKS_BEFORE_LIQUIDATION; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + sp.minimumLiquidationCollateralSSV = PACKED_SSV_ZERO; + sp.operatorMaxFee = PackedETH.wrap(type(uint64).max); + sp.operatorMaxFeeSSV = type(uint64).max; + } + + function _initOperators() internal { + StorageData storage s = SSVStorage.load(); + + op1 = _createOperator(s, address(opOwner1), bytes32(uint256(0x1))); + op2 = _createOperator(s, address(opOwner2), bytes32(uint256(0x2))); + op3 = _createOperator(s, address(opOwner3), bytes32(uint256(0x3))); + + operatorIds.push(op1); + operatorIds.push(op2); + operatorIds.push(op3); + + operatorOwner[op1] = address(opOwner1); + operatorOwner[op2] = address(opOwner2); + operatorOwner[op3] = address(opOwner3); + } + + function _createOperator(StorageData storage s, address owner, bytes32 pk) internal returns (uint64) { + s.lastOperatorId.increment(); + uint64 id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: DEFAULT_OPERATOR_SSV_FEE, + owner: owner, + snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: PACKED_SSV_ZERO}), + whitelisted: false, + ethValidatorCount: 0, + ethFee: DEFAULT_OPERATOR_ETH_FEE, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: uint32(block.number), index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(pk))] = id; + return id; + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + + function _pickOperatorId(uint256 seed) internal view returns (uint64) { + uint256 count = operatorIds.length; + if (count == 0) return 0; + return operatorIds[seed % count]; + } + + function _operatorIdsForKey(uint8 key) internal view returns (uint64[] memory) { + uint64[] memory ids; + if (key == 0) { + ids = new uint64[](1); + ids[0] = op1; + return ids; + } + if (key == 1) { + ids = new uint64[](2); + ids[0] = op1; + ids[1] = op2; + return ids; + } + ids = new uint64[](3); + ids[0] = op1; + ids[1] = op2; + ids[2] = op3; + return ids; + } + + function _clusterContainsOperator(uint8 operatorsKey, uint64 operatorId) internal view returns (bool) { + uint64[] memory ids = _operatorIdsForKey(operatorsKey); + for (uint256 i; i < ids.length; ++i) { + if (ids[i] == operatorId) return true; + } + return false; + } + + function _isTrackedEthCluster(bytes32 clusterId) internal view returns (bool) { + uint256 count = ethClusterIds.length; + for (uint256 i; i < count; ++i) { + if (ethClusterIds[i] == clusterId) return true; + } + return false; + } + + function _expectedEthDaoValidatorCount() internal view returns (uint32 expected) { + for (uint256 i; i < ethClusterIds.length; ++i) { + ClusterRecord storage record = ethClusters[ethClusterIds[i]]; + if (!record.exists || !record.cluster.active) continue; + expected += record.cluster.validatorCount; + } + + if (lifecycleClusterInitialized && !_isTrackedEthCluster(lifecycleClusterId)) { + ClusterRecord storage lifecycleRecord = ethClusters[lifecycleClusterId]; + if (lifecycleRecord.exists && lifecycleRecord.cluster.active) { + expected += lifecycleRecord.cluster.validatorCount; + } + } + + for (uint256 i; i < migratedClusterIds.length; ++i) { + bytes32 clusterId = migratedClusterIds[i]; + if (_isTrackedEthCluster(clusterId)) continue; + ClusterRecord storage record = ethClusters[clusterId]; + if (record.exists) { + if (record.cluster.active) { + expected += record.cluster.validatorCount; + } + continue; + } + expected += ssvClusters[clusterId].cluster.validatorCount; + } + } + + function _expectedOperatorCounts(uint64 operatorId) internal view returns (uint32 expectedSsv, uint32 expectedEth) { + for (uint256 i; i < ssvClusterIds.length; ++i) { + ClusterRecord storage record = ssvClusters[ssvClusterIds[i]]; + if (!record.exists || !record.cluster.active) continue; + if (!_clusterContainsOperator(record.operatorsKey, operatorId)) continue; + expectedSsv += record.cluster.validatorCount; + } + + for (uint256 i; i < ethClusterIds.length; ++i) { + ClusterRecord storage record = ethClusters[ethClusterIds[i]]; + if (!record.exists || !record.cluster.active) continue; + if (!_clusterContainsOperator(record.operatorsKey, operatorId)) continue; + expectedEth += record.cluster.validatorCount; + } + + if (lifecycleClusterInitialized && !_isTrackedEthCluster(lifecycleClusterId)) { + ClusterRecord storage lifecycleRecord = ethClusters[lifecycleClusterId]; + if ( + lifecycleRecord.exists && + lifecycleRecord.cluster.active && + _clusterContainsOperator(lifecycleRecord.operatorsKey, operatorId) + ) { + expectedEth += lifecycleRecord.cluster.validatorCount; + } + } + + for (uint256 i; i < migratedClusterIds.length; ++i) { + bytes32 clusterId = migratedClusterIds[i]; + if (_isTrackedEthCluster(clusterId)) continue; + ClusterRecord storage ethRecord = ethClusters[clusterId]; + if (ethRecord.exists) { + if (!_clusterContainsOperator(ethRecord.operatorsKey, operatorId)) continue; + expectedEth += ethRecord.cluster.validatorCount; + continue; + } + ClusterRecord storage ssvRecord = ssvClusters[clusterId]; + if (!_clusterContainsOperator(ssvRecord.operatorsKey, operatorId)) continue; + expectedEth += ssvRecord.cluster.validatorCount; + } + } + + function _pickEthClusterId(uint256 seed) internal view returns (bytes32) { + uint256 count = ethClusterIds.length; + if (count == 0) return bytes32(0); + return ethClusterIds[seed % count]; + } + + function _pickSsvClusterId(uint256 seed) internal view returns (bytes32) { + uint256 count = ssvClusterIds.length; + if (count == 0) return bytes32(0); + return ssvClusterIds[seed % count]; + } + + function _clusterOwnerUser(address owner) internal view returns (ClusterUser) { + if (owner == address(owner1)) return owner1; + if (owner == address(owner2)) return owner2; + return liquidator; + } + + function _boundAmount(uint256 seed, uint256 maxValue) internal pure returns (uint256) { + if (maxValue == 0) return 0; + return seed % (maxValue + 1); + } + + function _lifecycleClusterHash(uint64[] memory operatorIdsLocal) internal view returns (bytes32) { + return keccak256(abi.encodePacked(address(validatorOwner), operatorIdsLocal)); + } + + function _pickActiveLifecycleValidatorId(uint256 seed) internal view returns (uint256) { + uint256 count = lifecycleValidatorIds.length; + if (count == 0) return 0; + uint256 start = seed % count; + for (uint256 i; i < count; ++i) { + uint256 id = lifecycleValidatorIds[(start + i) % count]; + if (lifecycleValidators[id].active) return id; + } + return 0; + } + + function _makePublicKey(uint256 seed) internal pure returns (bytes memory) { + bytes32 h1 = keccak256(abi.encodePacked(seed)); + bytes32 h2 = keccak256(abi.encodePacked(seed, h1)); + bytes memory b1 = abi.encodePacked(h1); + bytes memory b2 = abi.encodePacked(h2); + bytes memory pk = new bytes(48); + for (uint256 i; i < 32; ++i) { + pk[i] = b1[i]; + } + for (uint256 i; i < 16; ++i) { + pk[32 + i] = b2[i]; + } + return pk; + } + + function _makeShares(uint256 seed) internal pure returns (bytes memory) { + return abi.encodePacked(uint64(seed)); + } + + function _currentClusterIndexEth(uint64[] memory operatorIdsLocal) internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 clusterIndex; + uint256 count = operatorIdsLocal.length; + for (uint256 i; i < count; ++i) { + clusterIndex += s.operators[operatorIdsLocal[i]].ethSnapshot.index; + } + return clusterIndex; + } + + function _currentClusterIndexSsv(uint64[] memory operatorIdsLocal) internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 clusterIndex; + uint256 count = operatorIdsLocal.length; + for (uint256 i; i < count; ++i) { + clusterIndex += s.operators[operatorIdsLocal[i]].snapshot.index; + } + return clusterIndex; + } + + function _settleEthCluster( + bytes32 clusterId, + ClusterRecord storage record, + uint64[] memory operatorIdsLocal + ) internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _currentClusterIndexEth(operatorIdsLocal); + uint64 networkFeeIndex = sp.ethNetworkFeeIndex; + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + cluster.updateBalanceWithEB(clusterId, clusterIndex, networkFeeIndex); + cluster.index = clusterIndex; + cluster.networkFeeIndex = networkFeeIndex; + record.cluster = cluster; + } + + function _settleSsvCluster( + bytes32 clusterId, + ClusterRecord storage record, + uint64[] memory operatorIdsLocal + ) internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _currentClusterIndexSsv(operatorIdsLocal); + uint64 networkFeeIndex = sp.networkFeeIndex; + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + cluster.updateBalanceWithEB(clusterId, clusterIndex, networkFeeIndex); + cluster.index = clusterIndex; + cluster.networkFeeIndex = networkFeeIndex; + record.cluster = cluster; + } + + function _settleTime() internal { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint32 currentBlock = uint32(block.number); + + uint256 operatorCount = operatorIds.length; + for (uint256 i; i < operatorCount; ++i) { + uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + if (operator.ethSnapshot.block != 0) { + uint32 diff = currentBlock - operator.ethSnapshot.block; + if (diff != 0) { + uint64 blockDiffFee = uint64(diff) * PackedETH.unwrap(operator.ethFee); + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (operator.ethValidatorCount * BPS_DENOMINATOR); + operator.ethSnapshot.index += blockDiffFee; + if (effectiveVUnits != 0 && blockDiffFee != 0) { + uint128 delta = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(uint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + } + + if (operator.snapshot.block != 0) { + uint32 diff = currentBlock - operator.snapshot.block; + if (diff != 0) { + uint64 blockDiffFee = uint64(diff) * PackedSSV.unwrap(operator.fee); + operator.snapshot.index += blockDiffFee; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * operator.validatorCount)); + operator.snapshot.block = currentBlock; + } + } + } + + uint64 ethIndex = sp.currentNetworkFeeIndex(); + uint64 ssvIndex = sp.currentNetworkFeeIndexSSV(); + sp.ethNetworkFeeIndex = ethIndex; + sp.networkFeeIndex = ssvIndex; + sp.ethNetworkFeeIndexBlockNumber = currentBlock; + sp.networkFeeIndexBlockNumber = currentBlock; + + sp.updateDAOEarnings(); + sp.updateDAOEarningsSSV(); + + _syncClusters(); + } + + function _fastForward(uint32 blocks) internal { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint32 currentBlock = uint32(block.number); + + if (blocks == 0) return; + + uint256 operatorCount = operatorIds.length; + for (uint256 i; i < operatorCount; ++i) { + uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + + if (operator.ethSnapshot.block != 0) { + uint64 blockDiffFee = uint64(blocks) * PackedETH.unwrap(operator.ethFee); + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (operator.ethValidatorCount * BPS_DENOMINATOR); + operator.ethSnapshot.index += blockDiffFee; + if (effectiveVUnits != 0 && blockDiffFee != 0) { + uint128 delta = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(uint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + + if (operator.snapshot.block != 0) { + uint64 blockDiffFee = uint64(blocks) * PackedSSV.unwrap(operator.fee); + operator.snapshot.index += blockDiffFee; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * operator.validatorCount)); + operator.snapshot.block = currentBlock; + } + } + + sp.ethNetworkFeeIndex += uint64(blocks) * PackedETH.unwrap(sp.ethNetworkFee); + sp.networkFeeIndex += uint64(blocks) * PackedSSV.unwrap(sp.networkFee); + sp.ethNetworkFeeIndexBlockNumber = currentBlock; + sp.networkFeeIndexBlockNumber = currentBlock; + + if (sp.daoTotalEthVUnits != 0 && sp.ethNetworkFee.neq(PACKED_ETH_ZERO)) { + uint128 earned = (uint128(blocks) * uint128(PackedETH.unwrap(sp.ethNetworkFee)) * uint128(sp.daoTotalEthVUnits)) / + BPS_DENOMINATOR; + sp.ethDaoBalance = sp.ethDaoBalance.add(PackedETH.wrap(uint64(earned))); + } + + if (sp.daoValidatorCount != 0 && sp.networkFee.neq(PACKED_SSV_ZERO)) { + uint64 earned = uint64(blocks) * PackedSSV.unwrap(sp.networkFee) * sp.daoValidatorCount; + sp.daoBalance = sp.daoBalance.add(PackedSSV.wrap(earned)); + } + sp.ethDaoIndexBlockNumber = currentBlock; + sp.daoIndexBlockNumber = currentBlock; + } + + function _syncClusters() internal { + uint256 ethCount = ethClusterIds.length; + for (uint256 i; i < ethCount; ++i) { + bytes32 clusterId = ethClusterIds[i]; + ClusterRecord storage record = ethClusters[clusterId]; + if (!record.exists || !record.cluster.active) continue; + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + _settleEthCluster(clusterId, record, operatorIdsLocal); + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } + + uint256 ssvCount = ssvClusterIds.length; + for (uint256 i; i < ssvCount; ++i) { + bytes32 clusterId = ssvClusterIds[i]; + ClusterRecord storage record = ssvClusters[clusterId]; + if (!record.exists || !record.cluster.active) continue; + uint64[] memory operatorIdsLocal = _operatorIdsForKey(record.operatorsKey); + _settleSsvCluster(clusterId, record, operatorIdsLocal); + SSVStorage.load().clusters[clusterId] = record.cluster.hashClusterData(); + } + } + + function _sumEthClusterBalances() internal view returns (uint256) { + uint256 sum = 0; + uint256 count = ethClusterIds.length; + for (uint256 i; i < count; ++i) { + ClusterRecord storage record = ethClusters[ethClusterIds[i]]; + if (!record.exists) continue; + sum += record.cluster.balance; + } + return sum; + } + + function _sumSsvClusterBalances() internal view returns (uint256) { + uint256 sum = 0; + uint256 count = ssvClusterIds.length; + for (uint256 i; i < count; ++i) { + ClusterRecord storage record = ssvClusters[ssvClusterIds[i]]; + if (!record.exists) continue; + sum += record.cluster.balance; + } + return sum; + } + + function _sumOperatorEthEarnings() internal view returns (uint256) { + StorageData storage s = SSVStorage.load(); + uint256 sum = 0; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + sum += PackedETHLib.unpack(s.operators[operatorIds[i]].ethSnapshot.balance); + } + return sum; + } + + function _sumOperatorSsvEarnings() internal view returns (uint256) { + StorageData storage s = SSVStorage.load(); + uint256 sum = 0; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + sum += PackedSSVLib.unpack(s.operators[operatorIds[i]].snapshot.balance); + } + return sum; + } + + function _daoEthEarnings() internal view returns (uint256) { + return PackedETHLib.unpack(SSVStorageProtocol.load().ethDaoBalance); + } + + function _daoSsvEarnings() internal view returns (uint256) { + return PackedSSVLib.unpack(SSVStorageProtocol.load().daoBalance); + } +} diff --git a/test/echidna/SSVClustersEchidna.sol b/test/echidna/SSVClustersEchidna.sol new file mode 100644 index 000000000..4c5414582 --- /dev/null +++ b/test/echidna/SSVClustersEchidna.sol @@ -0,0 +1,1964 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/modules/SSVOperators.sol"; +import "../../contracts/modules/SSVStaking.sol"; +import {ISSVClusters} from "../../contracts/interfaces/ISSVClusters.sol"; +import "../../contracts/interfaces/ISSVOperators.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/storage/SSVStorageStaking.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/libraries/OperatorLib.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "./SSVStakingEchidna.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETHLib, PackedSSVLib, ETH_DEDUCTED_DIGITS} from "../../contracts/libraries/SSVPackedLib.sol"; +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract ClusterUser { + ISSVClusters public clusters; + + constructor(ISSVClusters clusters_) { + clusters = clusters_; + } + + receive() external payable {} + + function deposit(address clusterOwner, uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) + external + payable + { + clusters.deposit{value: msg.value}(clusterOwner, operatorIds, cluster); + } + + function depositFromBalance( + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint256 amount + ) external { + clusters.deposit{value: amount}(clusterOwner, operatorIds, cluster); + } + + function withdraw(uint64[] calldata operatorIds, uint256 amount, ISSVNetworkCore.Cluster memory cluster) external { + clusters.withdraw(operatorIds, amount, cluster); + } + + function liquidate(address clusterOwner, uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) + external + { + clusters.liquidate(clusterOwner, operatorIds, cluster); + } + + function reactivate(uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) external payable { + clusters.reactivate{value: msg.value}(operatorIds, cluster); + } + + function reactivateFromBalance( + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint256 amount + ) external { + clusters.reactivate{value: amount}(operatorIds, cluster); + } + + function updateClusterBalance( + uint64 blockNum, + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint32 effectiveBalance, + bytes32[] calldata merkleProof + ) external { + clusters.updateClusterBalance(blockNum, clusterOwner, operatorIds, cluster, effectiveBalance, merkleProof); + } +} + +contract OperatorUser { + ISSVOperators public operators; + + constructor(ISSVOperators operators_) { + operators = operators_; + } + + receive() external payable {} + + function remove(uint64 operatorId) external { + operators.removeOperator(operatorId); + } + + function withdraw(uint64 operatorId, uint256 amount) external { + operators.withdrawOperatorEarnings(operatorId, amount); + } +} + +contract SSVClustersEchidna is SSVClusters, SSVOperators(0), SSVStaking(address(new CSSVTokenMock(address(this)))) { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using PackedETHLib for PackedETH; + using ProtocolLib for StorageProtocol; + + uint8 private constant MAX_CLUSTERS = 6; + uint64 private constant MINIMAL_STAKING_AMOUNT = 1_000_000_000; + uint256 private constant MAX_STAKE = 1_000_000 ether; + PackedETH private constant HARNESS_DEFAULT_OPERATOR_ETH_FEE = PackedETH.wrap(1); + PackedETH private constant HARNESS_DEFAULT_NETWORK_ETH_FEE = PackedETH.wrap(1); + uint64 private constant MIN_BLOCKS_BEFORE_LIQUIDATION = 2; + uint32 private constant MAX_ADVANCE_BLOCKS = 8; + uint32 private constant MIN_BLOCKS_BETWEEN_UPDATES = 2; + uint32 private constant SOLVENCY_BLOCK_WINDOW = 5_000_000; + + MockToken private token; + CSSVTokenMock private cssv; + + ClusterUser private owner1; + ClusterUser private owner2; + ClusterUser private attacker; + OperatorUser private opOwner1; + OperatorUser private opOwner2; + OperatorUser private opOwner3; + StakingUser private staker1; + StakingUser private staker2; + + uint64 private op1; + uint64 private op2; + uint64 private op3; + + struct ClusterRecord { + ISSVNetworkCore.Cluster cluster; + address owner; + uint8 operatorsKey; + bool exists; + } + + bytes32[] private clusterIds; + mapping(bytes32 => ClusterRecord) private clusters; + mapping(bytes32 => bool) private liquidatedClusters; + + uint256 private totalExpectedBalance; + + bool private overWithdrawSucceeded; + bool private withdrawPayoutMismatch; + bool private unauthorizedWithdrawSucceeded; + bool private liquidatePayoutMismatch; + bool private reactivateWhileActiveSucceeded; + bool private dustLiquidationFailed; + bool private ebUpdateWithoutRootSucceeded; + bool private ebUpdateNonLatestRootBypassed; + bool private ebUpdateFrequencyBypassed; + bool private ebUpdateStalenessBypassed; + bool private inactiveEbUpdateViolation; + bool private feeIndexNotCurrentAfterSettle; + bool private feeUsedNewVUnitsOnEbChange; + bool private implicitEbDefaultViolation; + bool private ebSnapshotRootDecreased; + bool private ebSnapshotFutureBlock; + bool private clusterBalanceFloorViolation; + bool private depositLiquidatedViolation; + bool private withdrawLiquidatedViolation; + bool private reactivateRemovedOperatorsViolation; + + constructor() { + token = new MockToken(); + cssv = CSSVTokenMock(CSSV_ADDRESS); + _mockSetToken(address(token)); + + ISSVClusters clustersSelf = ISSVClusters(address(this)); + ISSVOperators operatorsSelf = ISSVOperators(address(this)); + IStaking stakingSelf = IStaking(address(this)); + + owner1 = new ClusterUser(clustersSelf); + owner2 = new ClusterUser(clustersSelf); + attacker = new ClusterUser(clustersSelf); + opOwner1 = new OperatorUser(operatorsSelf); + opOwner2 = new OperatorUser(operatorsSelf); + opOwner3 = new OperatorUser(operatorsSelf); + staker1 = new StakingUser(stakingSelf, IERC20(address(token)), IERC20(address(cssv))); + staker2 = new StakingUser(stakingSelf, IERC20(address(token)), IERC20(address(cssv))); + + _initProtocolDefaults(); + _initOperators(); + } + + receive() external payable {} + + function action_fund(uint256 amount) external payable { + amount; + } + + function action_create_cluster(uint256 seed) external { + if (clusterIds.length >= MAX_CLUSTERS) return; + + address owner = (seed % 2 == 0) ? address(owner1) : address(owner2); + uint8 operatorsKey = uint8((seed >> 8) % 3); + uint64[] memory operatorIds = _operatorIdsForKey(operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(owner, operatorIds)); + + if (clusters[clusterId].exists) return; + + uint32 validatorCount = uint32((seed >> 16) % 8) + 1; + bool active = false; + uint256 balance = 0; + uint64 clusterIndex = 0; + uint64 networkFeeIndex = 0; + + uint256 available = _availableBalance(); + if (available != 0) { + uint256 minRequired = _minimumActiveClusterBalance(operatorIds, validatorCount); + StorageData storage s = SSVStorage.load(); + bool allOperatorsActive = true; + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + if (s.operators[operatorIds[i]].ethSnapshot.block == 0) { + allOperatorsActive = false; + break; + } + } + + if (allOperatorsActive && minRequired != 0 && minRequired <= available) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + for (uint256 i; i < operatorsLength; ++i) { + OperatorLib.updateSnapshotSt(s.operators[operatorIds[i]], operatorIds[i]); + s.operators[operatorIds[i]].ethValidatorCount += validatorCount; + } + sp.updateDAO(true, validatorCount); + + active = true; + balance = minRequired; + clusterIndex = _currentClusterIndex(operatorIds); + networkFeeIndex = ProtocolLib.currentNetworkFeeIndex(sp); + } + } + + ISSVNetworkCore.Cluster memory cluster = ISSVNetworkCore.Cluster({ + validatorCount: validatorCount, + networkFeeIndex: networkFeeIndex, + index: clusterIndex, + active: active, + balance: balance + }); + + SSVStorage.load().ethClusters[clusterId] = cluster.hashClusterData(); + + clusters[clusterId] = ClusterRecord({cluster: cluster, owner: owner, operatorsKey: operatorsKey, exists: true}); + liquidatedClusters[clusterId] = false; + clusterIds.push(clusterId); + totalExpectedBalance += balance; + } + + function action_deposit(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint256 available = _availableBalance(); + if (available == 0) return; + + uint256 amount = _boundAmount(seed >> 8, available); + if (amount == 0) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + try this.deposit{value: amount}(record.owner, operatorIds, cluster) { + record.cluster.balance += amount; + totalExpectedBalance += amount; + } catch {} + } + + function action_advance_time(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint32 blocks = uint32((seed >> 16) % MAX_ADVANCE_BLOCKS) + 1; + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + _fastForwardOperators(operatorIds, blocks); + sp.ethNetworkFeeIndex += uint64(blocks) * PackedETH.unwrap(sp.ethNetworkFee); + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + + ISSVNetworkCore.Cluster memory beforeCluster = record.cluster; + (ISSVNetworkCore.Cluster memory expectedSettled, uint256 expectedBurned) = + _expectedSettledCluster(clusterId, beforeCluster, operatorIds); + uint256 burned = _settleCluster(clusterId, record, operatorIds); + if (!_sameCluster(record.cluster, expectedSettled) || burned != expectedBurned) { + clusterBalanceFloorViolation = true; + } + _decreaseExpected(burned); + + s.ethClusters[clusterId] = record.cluster.hashClusterData(); + } + + function action_dust_liquidation(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (record.operatorsKey != 0) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + uint64 burnRate = _burnRate(operatorIds); + if (burnRate == 0) return; + + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 vUnits = ClusterLib.getVUnits(clusterId, record.cluster.validatorCount); + + uint128 perBlockUnits = (uint128(burnRate + PackedETH.unwrap(sp.ethNetworkFee)) * uint128(vUnits)) / BPS_DENOMINATOR; + uint256 perBlock = PackedETHLib.unpack(PackedETH.wrap(uint64(perBlockUnits))); + if (perBlock == 0) return; + + _fastForwardOperators(operatorIds, 2); + sp.ethNetworkFeeIndex += 2 * PackedETH.unwrap(sp.ethNetworkFee); + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + + uint256 burned = _settleCluster(clusterId, record, operatorIds); + _decreaseExpected(burned); + + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + + bool liquidatable = record.cluster + .isLiquidatableWithEB( + clusterId, + burnRate, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral + ); + + if (record.cluster.balance < perBlock && !liquidatable) { + dustLiquidationFailed = true; + return; + } + + if (liquidatable) { + if (record.cluster.balance == 0) return; + if (record.cluster.balance > address(this).balance) return; + try attacker.liquidate(record.owner, operatorIds, record.cluster) { + uint256 payout = record.cluster.balance; + _decreaseExpected(payout); + + record.cluster.active = false; + record.cluster.balance = 0; + record.cluster.index = 0; + record.cluster.networkFeeIndex = 0; + if (SSVStorage.load().ethClusters[clusterId] == record.cluster.hashClusterData()) { + liquidatedClusters[clusterId] = true; + } else { + clusterBalanceFloorViolation = true; + } + + } catch { + dustLiquidationFailed = true; + } + } + } + + function action_stake(uint256 seed, uint8 userSeed) external { + StakingUser user = _staker(userSeed); + uint256 amount = (seed % MAX_STAKE) + MINIMAL_STAKING_AMOUNT; + + if (seed % 8 == 0) { + amount = 0; + } else if (seed % 8 == 1) { + amount = MINIMAL_STAKING_AMOUNT - 1; + } + + token.mint(address(user), amount); + try user.approve(amount) {} catch {} + try user.stake(amount) {} catch {} + } + + function action_claim_rewards(uint8 userSeed) external { + if (_sumProjectedClusterBalances() != 0) return; + + StakingUser user = _staker(userSeed); + address userAddr = address(user); + + if (cssv.balanceOf(userAddr) == 0 && SSVStorageStaking.load().accrued[userAddr] == 0) return; + + try user.claim() {} catch {} + } + + function action_withdraw_operator_eth(uint256 seed) external { + if (_sumProjectedClusterBalances() != 0) return; + + uint64 operatorId = _pickOperatorId(seed); + if (operatorId == 0) return; + + ISSVNetworkCore.Operator memory operator = SSVStorage.load().operators[operatorId]; + if (operator.ethSnapshot.block == 0) return; + + OperatorLib.updateSnapshot(operator, operatorId); + PackedETH balance = operator.ethSnapshot.balance; + if (balance.eq(PACKED_ETH_ZERO)) return; + + uint256 amount = PackedETHLib.unpack(balance); + if (amount > address(this).balance) return; + + try _operatorOwnerUser(operatorId).withdraw(operatorId, 0) {} catch {} + } + + function action_withdraw(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + if (record.cluster.balance == 0) return; + uint256 amount = _boundAmount(seed >> 8, record.cluster.balance); + if (amount == 0) return; + + if (amount > address(this).balance) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + (ISSVNetworkCore.Cluster memory expectedSettled, uint256 expectedBurned) = + _expectedSettledCluster(clusterId, cluster, operatorIds); + ClusterUser owner = _ownerUser(record.owner); + + uint256 ownerBefore = record.owner.balance; + uint256 contractBefore = address(this).balance; + + try owner.withdraw(operatorIds, amount, cluster) { + uint256 burned = _settleCluster(clusterId, record, operatorIds); + if (!_sameCluster(record.cluster, expectedSettled) || burned != expectedBurned) { + clusterBalanceFloorViolation = true; + } + _decreaseExpected(burned); + + if (record.cluster.balance < amount) { + withdrawPayoutMismatch = true; + clusterBalanceFloorViolation = true; + return; + } + + record.cluster.balance -= amount; + if (record.cluster.balance != expectedSettled.balance - amount) { + clusterBalanceFloorViolation = true; + } + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) { + clusterBalanceFloorViolation = true; + } + _decreaseExpected(amount); + + if (record.owner.balance != ownerBefore + amount) { + withdrawPayoutMismatch = true; + } + if (address(this).balance != contractBefore - amount) { + withdrawPayoutMismatch = true; + } + } catch {} + } + + function action_withdraw_over(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (record.cluster.balance == type(uint256).max) return; + + uint256 amount = record.cluster.balance + 1; + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + ClusterUser owner = _ownerUser(record.owner); + + try owner.withdraw(operatorIds, amount, cluster) { + overWithdrawSucceeded = true; + } catch {} + } + + function action_unauthorized_withdraw(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (record.cluster.balance == 0) return; + + uint256 amount = _boundAmount(seed >> 8, record.cluster.balance); + if (amount == 0) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + try attacker.withdraw(operatorIds, amount, cluster) { + unauthorizedWithdrawSucceeded = true; + } catch {} + } + + function action_deposit_liquidated(uint256 seed) external { + bytes32 clusterId = _ensureLiquidatedCluster(seed, false); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || record.cluster.active) return; + if (!liquidatedClusters[clusterId]) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint256 sourceCapacity = record.owner.balance; + if (sourceCapacity == 0) return; + + uint256 amount = (seed >> 8) % sourceCapacity + 1; + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + _depositToLiquidatedCluster(clusterId, record, operatorIds, amount); + } + + function action_withdraw_liquidated(uint256 seed) external { + bytes32 clusterId = _ensureLiquidatedCluster(seed, false); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || record.cluster.active) return; + if (!liquidatedClusters[clusterId]) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + uint256 amount; + + if (record.cluster.balance == 0) { + uint256 sourceCapacity = record.owner.balance; + if (sourceCapacity == 0) return; + amount = (seed >> 8) % sourceCapacity + 1; + if (!_depositToLiquidatedCluster(clusterId, record, operatorIds, amount)) return; + } else { + amount = (seed >> 8) % record.cluster.balance + 1; + } + + ISSVNetworkCore.Cluster memory clusterBefore = record.cluster; + ClusterUser owner = _ownerUser(record.owner); + + uint256 operatorEarningsBefore = _sumTrackedOperatorEarnings(operatorIds); + uint256 daoBefore = _daoEthBalance(); + uint256 ownerBalanceBefore = record.owner.balance; + uint256 contractBalanceBefore = address(this).balance; + + try owner.withdraw(operatorIds, amount, clusterBefore) { + ISSVNetworkCore.Cluster memory expectedCluster = ISSVNetworkCore.Cluster({ + validatorCount: clusterBefore.validatorCount, + networkFeeIndex: clusterBefore.networkFeeIndex, + index: clusterBefore.index, + active: clusterBefore.active, + balance: clusterBefore.balance + }); + expectedCluster.balance -= amount; + + if (clusterBefore.balance < amount) { + withdrawLiquidatedViolation = true; + clusterBalanceFloorViolation = true; + return; + } + if (expectedCluster.balance != clusterBefore.balance - amount) { + clusterBalanceFloorViolation = true; + } + if (expectedCluster.active) { + withdrawLiquidatedViolation = true; + } + if (expectedCluster.index != clusterBefore.index) { + withdrawLiquidatedViolation = true; + clusterBalanceFloorViolation = true; + } + if (expectedCluster.networkFeeIndex != clusterBefore.networkFeeIndex) { + withdrawLiquidatedViolation = true; + clusterBalanceFloorViolation = true; + } + + bytes32 expectedHash = expectedCluster.hashClusterData(); + bool hashMatches = SSVStorage.load().ethClusters[clusterId] == expectedHash; + if (!hashMatches) { + withdrawLiquidatedViolation = true; + clusterBalanceFloorViolation = true; + } + if (record.owner.balance != ownerBalanceBefore + amount) { + withdrawLiquidatedViolation = true; + } + if (address(this).balance != contractBalanceBefore - amount) { + withdrawLiquidatedViolation = true; + } + if (_sumTrackedOperatorEarnings(operatorIds) != operatorEarningsBefore) { + withdrawLiquidatedViolation = true; + } + if (_daoEthBalance() != daoBefore) { + withdrawLiquidatedViolation = true; + } + + if (!hashMatches) { + return; + } + + record.cluster = expectedCluster; + if (record.cluster.balance != clusterBefore.balance - amount) { + clusterBalanceFloorViolation = true; + } + _decreaseExpected(amount); + } catch { + withdrawLiquidatedViolation = true; + } + } + + function action_liquidate(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint256 payout = record.cluster.balance; + if (payout > address(this).balance) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + (ISSVNetworkCore.Cluster memory expectedSettled, uint256 expectedBurned) = + _expectedSettledCluster(clusterId, cluster, operatorIds); + ClusterUser owner = _ownerUser(record.owner); + + uint256 ownerBefore = record.owner.balance; + uint256 contractBefore = address(this).balance; + + try owner.liquidate(record.owner, operatorIds, cluster) { + uint256 burned = _settleCluster(clusterId, record, operatorIds); + if (!_sameCluster(record.cluster, expectedSettled) || burned != expectedBurned) { + clusterBalanceFloorViolation = true; + } + _decreaseExpected(burned); + + payout = record.cluster.balance; + _decreaseExpected(payout); + + record.cluster.active = false; + record.cluster.balance = 0; + record.cluster.index = 0; + record.cluster.networkFeeIndex = 0; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) { + clusterBalanceFloorViolation = true; + } else { + liquidatedClusters[clusterId] = true; + } + + if (record.owner.balance != ownerBefore + payout) { + liquidatePayoutMismatch = true; + } + if (address(this).balance != contractBefore - payout) { + liquidatePayoutMismatch = true; + } + } catch {} + } + + function action_reactivate(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + ClusterUser owner = _ownerUser(record.owner); + + if (record.cluster.active) { + try owner.reactivate(operatorIds, cluster) { + reactivateWhileActiveSucceeded = true; + } catch {} + return; + } + + uint256 available = _availableBalance(); + uint256 amount = _boundAmount(seed >> 8, available); + if (amount == 0) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + uint256 burnRate = 0; + for (uint256 i; i < operatorIds.length; ++i) { + burnRate += PackedETH.unwrap(s.operators[operatorIds[i]].ethFee); + } + + uint256 minPerBlock = (burnRate + PackedETH.unwrap(sp.ethNetworkFee)) * uint256(record.cluster.validatorCount) + * ETH_DEDUCTED_DIGITS; + uint256 minRequired = minPerBlock * SOLVENCY_BLOCK_WINDOW; + if (minRequired == 0) { + minRequired = ETH_DEDUCTED_DIGITS; + } + if (amount < minRequired) { + amount = minRequired; + } + if (amount > available) return; + + try owner.reactivate{value: amount}(operatorIds, cluster) { + record.cluster.active = true; + record.cluster.balance += amount; + record.cluster.index = _currentClusterIndex(operatorIds); + record.cluster.networkFeeIndex = ProtocolLib.currentNetworkFeeIndex(SSVStorageProtocol.load()); + totalExpectedBalance += amount; + liquidatedClusters[clusterId] = false; + } catch {} + } + + function action_reactivate_with_removed_operators(uint256 seed) external { + bytes32 clusterId = _ensureLiquidatedCluster(seed, true); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || record.cluster.active) return; + if (!liquidatedClusters[clusterId]) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + uint64[] memory activeBefore = _activeOperatorIds(operatorIds); + if (activeBefore.length < 2) return; + if (activeBefore.length != operatorIds.length) return; + + uint64 removedOperatorId = activeBefore[activeBefore.length - 1]; + address removedOwner = s.operators[removedOperatorId].owner; + uint64 removedFrozenIndex; + + try _operatorOwnerUser(removedOperatorId).remove(removedOperatorId) { + if (s.operators[removedOperatorId].ethSnapshot.block != 0) { + reactivateRemovedOperatorsViolation = true; + } + if (s.operators[removedOperatorId].ethValidatorCount != 0) { + reactivateRemovedOperatorsViolation = true; + } + if (PackedETH.unwrap(s.operators[removedOperatorId].ethFee) != 0) { + reactivateRemovedOperatorsViolation = true; + } + if (s.operators[removedOperatorId].owner != removedOwner) { + reactivateRemovedOperatorsViolation = true; + } + removedFrozenIndex = s.operators[removedOperatorId].ethSnapshot.index; + } catch { + reactivateRemovedOperatorsViolation = true; + return; + } + + uint64[] memory activeOperatorIds = _activeOperatorIds(operatorIds); + if (activeOperatorIds.length + 1 != activeBefore.length) { + reactivateRemovedOperatorsViolation = true; + return; + } + + uint32[] memory activeCountsBefore = new uint32[](activeOperatorIds.length); + for (uint256 i; i < activeOperatorIds.length; ++i) { + activeCountsBefore[i] = s.operators[activeOperatorIds[i]].ethValidatorCount; + } + + uint32 daoValidatorCountBefore = sp.ethDaoValidatorCount; + uint256 minBalance = _minimumActiveClusterBalance(activeOperatorIds, record.cluster.validatorCount); + uint256 amount = minBalance > record.cluster.balance ? minBalance - record.cluster.balance : 0; + if (record.owner.balance < amount) return; + + ISSVNetworkCore.Cluster memory clusterBefore = record.cluster; + uint64 expectedClusterIndex = _currentClusterIndex(operatorIds); + uint64 expectedNetworkFeeIndex = ProtocolLib.currentNetworkFeeIndex(sp); + ClusterUser owner = _ownerUser(record.owner); + + try owner.reactivateFromBalance(operatorIds, clusterBefore, amount) { + ISSVNetworkCore.Cluster memory expectedCluster = ISSVNetworkCore.Cluster({ + validatorCount: clusterBefore.validatorCount, + networkFeeIndex: expectedNetworkFeeIndex, + index: expectedClusterIndex, + active: true, + balance: clusterBefore.balance + amount + }); + + bytes32 expectedHash = expectedCluster.hashClusterData(); + bool hashMatches = SSVStorage.load().ethClusters[clusterId] == expectedHash; + if (!hashMatches) { + reactivateRemovedOperatorsViolation = true; + } + if (s.operators[removedOperatorId].ethSnapshot.block != 0) { + reactivateRemovedOperatorsViolation = true; + } + if (s.operators[removedOperatorId].ethValidatorCount != 0) { + reactivateRemovedOperatorsViolation = true; + } + if (PackedETH.unwrap(s.operators[removedOperatorId].ethFee) != 0) { + reactivateRemovedOperatorsViolation = true; + } + if (s.operators[removedOperatorId].owner != removedOwner) { + reactivateRemovedOperatorsViolation = true; + } + if (s.operators[removedOperatorId].ethSnapshot.index != removedFrozenIndex) { + reactivateRemovedOperatorsViolation = true; + } + + for (uint256 i; i < activeOperatorIds.length; ++i) { + uint64 operatorId = activeOperatorIds[i]; + if (s.operators[operatorId].ethValidatorCount != activeCountsBefore[i] + clusterBefore.validatorCount) { + reactivateRemovedOperatorsViolation = true; + } + } + if (sp.ethDaoValidatorCount != daoValidatorCountBefore + clusterBefore.validatorCount) { + reactivateRemovedOperatorsViolation = true; + } + + if (hashMatches) { + record.cluster = expectedCluster; + totalExpectedBalance += amount; + liquidatedClusters[clusterId] = false; + } + } catch { + reactivateRemovedOperatorsViolation = true; + } + } + + function action_update_cluster_balance_valid(uint256 seed) external { + bytes32 clusterId = _pickClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + ClusterEBSnapshot memory ebBefore = seb.clusterEB[clusterId]; + if (uint64(block.number) < ebBefore.lastRootBlockNum + 1) return; + + uint64 minBlockNum = ebBefore.lastRootBlockNum + 1; + uint64 blockNum = minBlockNum + uint64((seed >> 8) % (uint64(block.number) - minBlockNum + 1)); + + uint32 minEb = record.cluster.validatorCount * uint32(DEFAULT_EB_PER_VALIDATOR / 1 ether); + uint32 maxEb = minEb + (record.cluster.validatorCount * 16); + uint32 effectiveBalance = minEb; + if (maxEb > minEb) { + effectiveBalance = minEb + uint32((seed >> 24) % (maxEb - minEb + 1)); + } + + bytes32 root = _singleLeafRoot(clusterId, effectiveBalance); + _setCommittedRoot(seb, blockNum, root); + bytes32[] memory proof = new bytes32[](0); + + ISSVNetworkCore.Cluster memory beforeCluster = record.cluster; + uint64 oldVUnits = ebBefore.vUnits; + if (oldVUnits == 0) { + oldVUnits = uint64(beforeCluster.validatorCount) * BPS_DENOMINATOR; + } + uint64 newVUnits = ClusterLib.ebToVUnits(effectiveBalance); + + uint64 clusterIndex = _currentClusterIndex(operatorIds); + uint64 networkFeeIndex = ProtocolLib.currentNetworkFeeIndex(sp); + if (clusterIndex < beforeCluster.index || networkFeeIndex < beforeCluster.networkFeeIndex) return; + + uint128 idxOp = uint128(clusterIndex - beforeCluster.index); + uint128 idxNet = uint128(networkFeeIndex - beforeCluster.networkFeeIndex); + uint128 operatorFeeUnitsOld = (idxOp * uint128(oldVUnits)) / BPS_DENOMINATOR; + uint128 networkFeeUnitsOld = (idxNet * uint128(oldVUnits)) / BPS_DENOMINATOR; + uint256 totalFeesOld = (uint256(operatorFeeUnitsOld) + uint256(networkFeeUnitsOld)) * ETH_DEDUCTED_DIGITS; + + ISSVNetworkCore.Cluster memory expectedCluster = ISSVNetworkCore.Cluster({ + validatorCount: beforeCluster.validatorCount, + networkFeeIndex: networkFeeIndex, + index: clusterIndex, + active: beforeCluster.active, + balance: beforeCluster.balance >= totalFeesOld ? beforeCluster.balance - totalFeesOld : 0 + }); + + uint64 burnRate = _burnRate(operatorIds); + bool shouldLiquidate = expectedCluster.validatorCount != 0 + && expectedCluster.isLiquidatableWithEB( + clusterId, + burnRate, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral + ); + + uint256 expectedPayout = 0; + if (shouldLiquidate) { + expectedPayout = expectedCluster.balance; + expectedCluster.active = false; + expectedCluster.balance = 0; + expectedCluster.index = 0; + expectedCluster.networkFeeIndex = 0; + } + + bool checkImplicitEbDefault = ebBefore.vUnits == 0 && totalFeesOld != 0; + bytes32 wrongImplicitHash = bytes32(0); + bool wrongImplicitHashDistinct = false; + if (checkImplicitEbDefault) { + ISSVNetworkCore.Cluster memory wrongImplicitCluster = ISSVNetworkCore.Cluster({ + validatorCount: beforeCluster.validatorCount, + networkFeeIndex: networkFeeIndex, + index: clusterIndex, + active: beforeCluster.active, + balance: beforeCluster.balance + }); + + bool wrongShouldLiquidate = wrongImplicitCluster.validatorCount != 0 + && wrongImplicitCluster.isLiquidatableWithVUnits( + newVUnits, + burnRate, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral + ); + + if (wrongShouldLiquidate) { + wrongImplicitCluster.active = false; + wrongImplicitCluster.balance = 0; + wrongImplicitCluster.index = 0; + wrongImplicitCluster.networkFeeIndex = 0; + } + + wrongImplicitHash = wrongImplicitCluster.hashClusterData(); + } + + uint256 liquidatorBefore = address(attacker).balance; + try attacker.updateClusterBalance(blockNum, record.owner, operatorIds, beforeCluster, effectiveBalance, proof) { + bytes32 storedHash = SSVStorage.load().ethClusters[clusterId]; + bytes32 expectedHash = expectedCluster.hashClusterData(); + if (checkImplicitEbDefault) { + wrongImplicitHashDistinct = wrongImplicitHash != expectedHash; + } + if (storedHash != expectedHash) { + feeIndexNotCurrentAfterSettle = true; + if (checkImplicitEbDefault) { + implicitEbDefaultViolation = true; + } + } + + if (checkImplicitEbDefault && wrongImplicitHashDistinct && storedHash == wrongImplicitHash) { + implicitEbDefaultViolation = true; + } + + if (!shouldLiquidate && newVUnits != oldVUnits) { + uint128 operatorFeeUnitsNew = (idxOp * uint128(newVUnits)) / BPS_DENOMINATOR; + uint128 networkFeeUnitsNew = (idxNet * uint128(newVUnits)) / BPS_DENOMINATOR; + uint256 totalFeesNew = + (uint256(operatorFeeUnitsNew) + uint256(networkFeeUnitsNew)) * ETH_DEDUCTED_DIGITS; + if (totalFeesNew != totalFeesOld) { + ISSVNetworkCore.Cluster memory altCluster = ISSVNetworkCore.Cluster({ + validatorCount: beforeCluster.validatorCount, + networkFeeIndex: networkFeeIndex, + index: clusterIndex, + active: beforeCluster.active, + balance: beforeCluster.balance >= totalFeesNew ? beforeCluster.balance - totalFeesNew : 0 + }); + if (storedHash == altCluster.hashClusterData()) { + feeUsedNewVUnitsOnEbChange = true; + } + } + } + + if (shouldLiquidate) { + uint256 payout = address(attacker).balance - liquidatorBefore; + if (payout != expectedPayout) { + liquidatePayoutMismatch = true; + } + } + + ClusterEBSnapshot storage ebAfter = seb.clusterEB[clusterId]; + if (ebAfter.lastRootBlockNum < ebBefore.lastRootBlockNum) { + ebSnapshotRootDecreased = true; + } + if (ebAfter.lastUpdateBlock > block.number) { + ebSnapshotFutureBlock = true; + } + + if (storedHash == expectedHash) { + if (beforeCluster.balance > expectedCluster.balance) { + _decreaseExpected(beforeCluster.balance - expectedCluster.balance); + } + record.cluster = expectedCluster; + if (shouldLiquidate) { + liquidatedClusters[clusterId] = true; + } + } + } catch {} + } + + function action_update_cluster_balance_without_root(uint256 seed) external { + bytes32 clusterId = _pickInactiveClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot memory ebBefore = seb.clusterEB[clusterId]; + + if (uint64(block.number) < ebBefore.lastRootBlockNum + 1) return; + uint64 minBlockNum = ebBefore.lastRootBlockNum + 1; + uint64 blockNum = minBlockNum + uint64((seed >> 8) % (uint64(block.number) - minBlockNum + 1)); + uint32 effectiveBalance = record.cluster.validatorCount * uint32(DEFAULT_EB_PER_VALIDATOR / 1 ether); + + _setCommittedRoot(seb, blockNum, bytes32(0)); + bytes32[] memory proof = new bytes32[](0); + try attacker.updateClusterBalance( + blockNum, record.owner, operatorIds, record.cluster, effectiveBalance, proof + ) { + ebUpdateWithoutRootSucceeded = true; + } catch {} + } + + function action_update_cluster_balance_non_latest_root(uint256 seed) external { + bytes32 clusterId = _pickInactiveClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot memory ebBefore = seb.clusterEB[clusterId]; + + uint64 oldBlock = seb.latestCommittedBlock; + uint64 minOldBlock = ebBefore.lastRootBlockNum + 1; + if (oldBlock < minOldBlock) { + oldBlock = minOldBlock; + } + + uint64 newBlock = oldBlock + 1; + if (uint64(block.number) < newBlock) return; + + uint32 effectiveBalance = record.cluster.validatorCount * uint32(DEFAULT_EB_PER_VALIDATOR / 1 ether); + bytes32 oldRoot = _singleLeafRoot(clusterId, effectiveBalance); + bytes32 newRoot = keccak256(abi.encodePacked(clusterId, seed, newBlock)); + bytes32[] memory proof = new bytes32[](0); + + _setCommittedRoot(seb, oldBlock, oldRoot); + _setCommittedRoot(seb, newBlock, newRoot); + + try attacker.updateClusterBalance( + oldBlock, record.owner, operatorIds, record.cluster, effectiveBalance, proof + ) { + ebUpdateNonLatestRootBypassed = true; + } catch {} + } + + function action_update_cluster_balance_too_frequent(uint256 seed) external { + bytes32 clusterId = _pickInactiveClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot memory ebBefore = seb.clusterEB[clusterId]; + + if (uint64(block.number) < ebBefore.lastRootBlockNum + 2) return; + uint64 firstBlock = ebBefore.lastRootBlockNum + 1; + uint64 secondBlock = firstBlock + 1; + uint32 effectiveBalance = record.cluster.validatorCount * uint32(DEFAULT_EB_PER_VALIDATOR / 1 ether); + + bytes32 firstRoot = _singleLeafRoot(clusterId, effectiveBalance); + bytes32 secondRoot = _singleLeafRoot(clusterId, effectiveBalance + 1); + + bytes32[] memory proof = new bytes32[](0); + _setCommittedRoot(seb, firstBlock, firstRoot); + try attacker.updateClusterBalance( + firstBlock, record.owner, operatorIds, record.cluster, effectiveBalance, proof + ) { + _setCommittedRoot(seb, secondBlock, secondRoot); + try attacker.updateClusterBalance( + secondBlock, record.owner, operatorIds, record.cluster, effectiveBalance + 1, proof + ) { + ebUpdateFrequencyBypassed = true; + } catch {} + } catch {} + } + + function action_update_cluster_balance_stale(uint256 seed) external { + bytes32 clusterId = _pickInactiveClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot memory ebBefore = seb.clusterEB[clusterId]; + + if (uint64(block.number) < ebBefore.lastRootBlockNum + 1) return; + uint64 blockNum = ebBefore.lastRootBlockNum + 1; + uint32 effectiveBalance = record.cluster.validatorCount * uint32(DEFAULT_EB_PER_VALIDATOR / 1 ether); + bytes32 root = _singleLeafRoot(clusterId, effectiveBalance); + bytes32[] memory proof = new bytes32[](0); + + _setCommittedRoot(seb, blockNum, root); + try attacker.updateClusterBalance( + blockNum, record.owner, operatorIds, record.cluster, effectiveBalance, proof + ) { + // Isolate stale-check behavior in second call. + seb.clusterEB[clusterId].lastUpdateBlock = 0; + try attacker.updateClusterBalance( + blockNum, record.owner, operatorIds, record.cluster, effectiveBalance, proof + ) { + ebUpdateStalenessBypassed = true; + } catch {} + } catch {} + } + + function action_update_cluster_balance_inactive_valid(uint256 seed) external { + bytes32 clusterId = _pickInactiveClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || record.cluster.active) return; + + StorageData storage s = SSVStorage.load(); + if (s.ethClusters[clusterId] != record.cluster.hashClusterData()) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + ClusterEBSnapshot memory ebBefore = seb.clusterEB[clusterId]; + if (uint64(block.number) < ebBefore.lastRootBlockNum + 1) return; + if (ebBefore.lastUpdateBlock != 0 && uint64(block.number) < ebBefore.lastUpdateBlock + seb.minBlocksBetweenUpdates) + { + return; + } + + uint64 minBlockNum = ebBefore.lastRootBlockNum + 1; + uint64 blockNum = minBlockNum + uint64((seed >> 8) % (uint64(block.number) - minBlockNum + 1)); + + uint32 minEb = record.cluster.validatorCount * uint32(DEFAULT_EB_PER_VALIDATOR / 1 ether); + uint32 maxEb = minEb + (record.cluster.validatorCount * 16); + uint32 effectiveBalance = minEb; + if (maxEb > minEb) { + effectiveBalance = minEb + uint32((seed >> 24) % (maxEb - minEb + 1)); + } + + bytes32 storedHashBefore = s.ethClusters[clusterId]; + uint64 daoTotalEthVUnitsBefore = sp.daoTotalEthVUnits; + bool wasMarkedLiquidated = liquidatedClusters[clusterId]; + uint64[] memory operatorEthVUnitsBefore = new uint64[](operatorIds.length); + for (uint256 i; i < operatorIds.length; ++i) { + operatorEthVUnitsBefore[i] = seb.operatorEthVUnits[operatorIds[i]]; + } + + bytes32 root = _singleLeafRoot(clusterId, effectiveBalance); + _setCommittedRoot(seb, blockNum, root); + bytes32[] memory proof = new bytes32[](0); + + try attacker.updateClusterBalance(blockNum, record.owner, operatorIds, record.cluster, effectiveBalance, proof) { + ClusterEBSnapshot storage ebAfter = seb.clusterEB[clusterId]; + if (s.ethClusters[clusterId] != storedHashBefore) { + inactiveEbUpdateViolation = true; + } + if (sp.daoTotalEthVUnits != daoTotalEthVUnitsBefore) { + inactiveEbUpdateViolation = true; + } + if (liquidatedClusters[clusterId] != wasMarkedLiquidated) { + inactiveEbUpdateViolation = true; + } + for (uint256 i; i < operatorIds.length; ++i) { + if (seb.operatorEthVUnits[operatorIds[i]] != operatorEthVUnitsBefore[i]) { + inactiveEbUpdateViolation = true; + } + } + if (ebAfter.vUnits != ClusterLib.ebToVUnits(effectiveBalance)) { + inactiveEbUpdateViolation = true; + } + if (ebAfter.lastRootBlockNum != blockNum) { + inactiveEbUpdateViolation = true; + } + if (ebAfter.lastUpdateBlock != uint64(block.number)) { + inactiveEbUpdateViolation = true; + } + } catch { + inactiveEbUpdateViolation = true; + } + } + + function echidna_cluster_hash_consistent() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[i]; + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists) return false; + if (s.ethClusters[clusterId] != record.cluster.hashClusterData()) return false; + if (record.owner == address(0)) return false; + } + return true; + } + + function echidna_inactive_clusters_zeroed() external view returns (bool) { + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + ClusterRecord storage record = clusters[clusterIds[i]]; + if (!record.exists) return false; + if (!record.cluster.active) { + if (record.cluster.index != 0) return false; + if (record.cluster.networkFeeIndex != 0) return false; + } + } + return true; + } + + function echidna_cluster_balance_accounting() external view returns (bool) { + uint256 sum = 0; + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + ClusterRecord storage record = clusters[clusterIds[i]]; + if (!record.exists) return false; + sum += record.cluster.balance; + } + return sum == totalExpectedBalance; + } + + function echidna_eth_balance_accounting() external view returns (bool) { + (uint256 liabilities, bool ok) = _currentEthLiabilities(); + if (!ok) return false; + + return address(this).balance >= liabilities; + } + + function echidna_withdraw_limit_enforced() external view returns (bool) { + return !overWithdrawSucceeded; + } + + function echidna_withdraw_conserves_balance() external view returns (bool) { + return !withdrawPayoutMismatch; + } + + function echidna_owner_withdraw_only() external view returns (bool) { + return !unauthorizedWithdrawSucceeded; + } + + function echidna_liquidation_cleans_state() external view returns (bool) { + return !liquidatePayoutMismatch; + } + + function echidna_reactivate_requires_inactive() external view returns (bool) { + return !reactivateWhileActiveSucceeded; + } + + function echidna_dust_liquidation_reachable() external view returns (bool) { + return !dustLiquidationFailed; + } + + function echidna_eb_snapshot_block_lte_current() external view returns (bool) { + if (ebSnapshotFutureBlock) return false; + + StorageEB storage seb = SSVStorageEB.load(); + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + if (seb.clusterEB[clusterIds[i]].lastUpdateBlock > block.number) return false; + } + return true; + } + + function echidna_eb_snapshot_root_monotonic() external view returns (bool) { + return !ebSnapshotRootDecreased; + } + + function echidna_eb_update_requires_root() external view returns (bool) { + return !ebUpdateWithoutRootSucceeded; + } + + function echidna_eb_update_requires_latest_root() external view returns (bool) { + return !ebUpdateNonLatestRootBypassed; + } + + function echidna_eb_update_frequency() external view returns (bool) { + return !ebUpdateFrequencyBypassed; + } + + function echidna_eb_update_staleness() external view returns (bool) { + return !ebUpdateStalenessBypassed; + } + + function echidna_inactive_eb_update_skips_accounting() external view returns (bool) { + return !inactiveEbUpdateViolation; + } + + function echidna_fee_index_current_after_settle() external view returns (bool) { + return !feeIndexNotCurrentAfterSettle; + } + + function echidna_implicit_eb_default_used() external view returns (bool) { + return !implicitEbDefaultViolation; + } + + function echidna_fee_uses_old_vunits_on_eb_change() external view returns (bool) { + return !feeUsedNewVUnitsOnEbChange; + } + + function echidna_cluster_balance_non_negative() external view returns (bool) { + return !clusterBalanceFloorViolation; + } + + function echidna_deposit_liquidated_succeeds() external view returns (bool) { + return !depositLiquidatedViolation; + } + + function echidna_withdraw_liquidated_skips_fees() external view returns (bool) { + return !withdrawLiquidatedViolation; + } + + function echidna_reactivate_with_removed_operators() external view returns (bool) { + return !reactivateRemovedOperatorsViolation; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 1000; + sp.ethNetworkFee = HARNESS_DEFAULT_NETWORK_ETH_FEE; + sp.ethNetworkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = MIN_BLOCKS_BEFORE_LIQUIDATION; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + + SSVStorageEB.load().minBlocksBetweenUpdates = MIN_BLOCKS_BETWEEN_UPDATES; + } + + function _initOperators() internal { + StorageData storage s = SSVStorage.load(); + + op1 = _createOperator(s, address(opOwner1), bytes32(uint256(0x1))); + op2 = _createOperator(s, address(opOwner2), bytes32(uint256(0x2))); + op3 = _createOperator(s, address(opOwner3), bytes32(uint256(0x3))); + } + + function _createOperator(StorageData storage s, address owner, bytes32 pk) internal returns (uint64) { + s.lastOperatorId.increment(); + uint64 id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PACKED_SSV_ZERO, + owner: owner, + snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: PACKED_SSV_ZERO}), + whitelisted: false, + ethValidatorCount: 0, + ethFee: HARNESS_DEFAULT_OPERATOR_ETH_FEE, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: uint32(block.number), index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(pk))] = id; + return id; + } + + function _pickOperatorId(uint256 seed) internal view returns (uint64) { + uint256 key = seed % 3; + if (key == 0) return op1; + if (key == 1) return op2; + return op3; + } + + function _operatorIdsForKey(uint8 key) internal view returns (uint64[] memory) { + if (key == 0) { + uint64[] memory singleOperatorIds = new uint64[](1); + singleOperatorIds[0] = op1; + return singleOperatorIds; + } + if (key == 1) { + uint64[] memory twoOperatorIds = new uint64[](2); + twoOperatorIds[0] = op1; + twoOperatorIds[1] = op2; + return twoOperatorIds; + } + uint64[] memory threeOperatorIds = new uint64[](3); + threeOperatorIds[0] = op1; + threeOperatorIds[1] = op2; + threeOperatorIds[2] = op3; + return threeOperatorIds; + } + + function _pickClusterId(uint256 seed) internal view returns (bytes32) { + uint256 count = clusterIds.length; + if (count == 0) return bytes32(0); + return clusterIds[seed % count]; + } + + function _pickInactiveClusterId(uint256 seed) internal view returns (bytes32) { + uint256 count = clusterIds.length; + if (count == 0) return bytes32(0); + + uint256 start = seed % count; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[(start + i) % count]; + if (clusters[clusterId].exists && !clusters[clusterId].cluster.active) { + return clusterId; + } + } + return bytes32(0); + } + + function _pickLiquidatedClusterId(uint256 seed, bool requireMultiOperator) internal view returns (bytes32) { + uint256 count = clusterIds.length; + if (count == 0) return bytes32(0); + + uint256 start = seed % count; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[(start + i) % count]; + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || record.cluster.active) continue; + if (!liquidatedClusters[clusterId]) continue; + if (requireMultiOperator && record.operatorsKey == 0) continue; + if (requireMultiOperator && !_isCanonicalRemovedOperatorCluster(clusterId, record)) continue; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) continue; + return clusterId; + } + + return bytes32(0); + } + + function _pickActiveClusterId(uint256 seed, bool requireMultiOperator) internal view returns (bytes32) { + uint256 count = clusterIds.length; + if (count == 0) return bytes32(0); + + uint256 start = seed % count; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[(start + i) % count]; + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) continue; + if (requireMultiOperator && record.operatorsKey == 0) continue; + if (requireMultiOperator && !_isCanonicalRemovedOperatorCluster(clusterId, record)) continue; + return clusterId; + } + + return bytes32(0); + } + + function _ensureLiquidatedCluster(uint256 seed, bool requireMultiOperator) internal returns (bytes32) { + bytes32 clusterId = _pickLiquidatedClusterId(seed, requireMultiOperator); + if (clusterId != bytes32(0)) return clusterId; + + clusterId = _pickActiveClusterId(seed, requireMultiOperator); + if (clusterId == bytes32(0)) { + clusterId = _bootstrapActiveCluster(seed, requireMultiOperator); + if (clusterId == bytes32(0)) return bytes32(0); + } + + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists || !record.cluster.active) return bytes32(0); + if (requireMultiOperator && record.operatorsKey == 0) return bytes32(0); + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return bytes32(0); + if (record.cluster.balance > address(this).balance) return bytes32(0); + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + ISSVNetworkCore.Cluster memory clusterBefore = record.cluster; + (ISSVNetworkCore.Cluster memory expectedSettled, uint256 expectedBurned) = + _expectedSettledCluster(clusterId, clusterBefore, operatorIds); + ISSVNetworkCore.Cluster memory expectedLiquidated = ISSVNetworkCore.Cluster({ + validatorCount: expectedSettled.validatorCount, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + ClusterUser owner = _ownerUser(record.owner); + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint64[] memory activeBefore = _activeOperatorIds(operatorIds); + uint32[] memory activeCountsBefore = new uint32[](activeBefore.length); + uint64[] memory operatorDeviationsBefore = new uint64[](activeBefore.length); + for (uint256 i; i < activeBefore.length; ++i) { + activeCountsBefore[i] = s.operators[activeBefore[i]].ethValidatorCount; + operatorDeviationsBefore[i] = seb.operatorEthVUnits[activeBefore[i]]; + } + uint32 daoValidatorCountBefore = sp.ethDaoValidatorCount; + uint64 daoTotalEthVUnitsBefore = sp.daoTotalEthVUnits; + uint64 storedVUnitsBefore = seb.clusterEB[clusterId].vUnits; + + try owner.liquidate(record.owner, operatorIds, clusterBefore) { + if (SSVStorage.load().ethClusters[clusterId] != expectedLiquidated.hashClusterData()) { + clusterBalanceFloorViolation = true; + return bytes32(0); + } + for (uint256 i; i < activeBefore.length; ++i) { + if (s.operators[activeBefore[i]].ethValidatorCount != activeCountsBefore[i] - clusterBefore.validatorCount) { + clusterBalanceFloorViolation = true; + return bytes32(0); + } + } + if (sp.ethDaoValidatorCount != daoValidatorCountBefore - clusterBefore.validatorCount) { + clusterBalanceFloorViolation = true; + return bytes32(0); + } + if (storedVUnitsBefore == 0) { + uint64 baselineDelta = uint64(clusterBefore.validatorCount) * BPS_DENOMINATOR; + if (sp.daoTotalEthVUnits != daoTotalEthVUnitsBefore - baselineDelta) { + clusterBalanceFloorViolation = true; + return bytes32(0); + } + for (uint256 i; i < activeBefore.length; ++i) { + if (seb.operatorEthVUnits[activeBefore[i]] != operatorDeviationsBefore[i]) { + clusterBalanceFloorViolation = true; + return bytes32(0); + } + } + } + _decreaseExpected(expectedBurned); + _decreaseExpected(expectedSettled.balance); + record.cluster = expectedLiquidated; + liquidatedClusters[clusterId] = true; + + return clusterId; + } catch { + return bytes32(0); + } + } + + function _bootstrapActiveCluster(uint256 seed, bool requireMultiOperator) internal returns (bytes32) { + if (clusterIds.length >= MAX_CLUSTERS) return bytes32(0); + + uint8 operatorsKey = requireMultiOperator ? 1 : 0; + uint64[] memory operatorIds = _operatorIdsForKey(operatorsKey); + StorageData storage s = SSVStorage.load(); + uint256 operatorsLength = operatorIds.length; + for (uint256 i; i < operatorsLength; ++i) { + if (s.operators[operatorIds[i]].ethSnapshot.block == 0) { + return bytes32(0); + } + } + + uint32 validatorCount = 1; + uint256 minRequired = _minimumActiveClusterBalance(operatorIds, validatorCount); + if (minRequired == 0 || minRequired > _availableBalance()) return bytes32(0); + + address firstOwner = (seed % 2 == 0) ? address(owner1) : address(owner2); + bytes32 clusterId = _createBootstrapCluster(firstOwner, operatorsKey, operatorIds, validatorCount, minRequired); + if (clusterId != bytes32(0)) return clusterId; + + address secondOwner = firstOwner == address(owner1) ? address(owner2) : address(owner1); + return _createBootstrapCluster(secondOwner, operatorsKey, operatorIds, validatorCount, minRequired); + } + + function _createBootstrapCluster( + address owner, + uint8 operatorsKey, + uint64[] memory operatorIds, + uint32 validatorCount, + uint256 balance + ) internal returns (bytes32 clusterId) { + clusterId = keccak256(abi.encodePacked(owner, operatorIds)); + if (clusters[clusterId].exists) return bytes32(0); + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint256 operatorsLength = operatorIds.length; + + for (uint256 i; i < operatorsLength; ++i) { + OperatorLib.updateSnapshotSt(s.operators[operatorIds[i]], operatorIds[i]); + s.operators[operatorIds[i]].ethValidatorCount += validatorCount; + } + sp.updateDAO(true, validatorCount); + + ISSVNetworkCore.Cluster memory cluster = ISSVNetworkCore.Cluster({ + validatorCount: validatorCount, + networkFeeIndex: ProtocolLib.currentNetworkFeeIndex(sp), + index: _currentClusterIndex(operatorIds), + active: true, + balance: balance + }); + + s.ethClusters[clusterId] = cluster.hashClusterData(); + clusters[clusterId] = ClusterRecord({cluster: cluster, owner: owner, operatorsKey: operatorsKey, exists: true}); + liquidatedClusters[clusterId] = false; + clusterIds.push(clusterId); + totalExpectedBalance += balance; + } + + function _staker(uint8 seed) internal view returns (StakingUser) { + if (seed % 2 == 0) return staker1; + return staker2; + } + + function _ownerUser(address owner) internal view returns (ClusterUser) { + if (owner == address(owner1)) return owner1; + if (owner == address(owner2)) return owner2; + return attacker; + } + + function _operatorOwnerUser(uint64 operatorId) internal view returns (OperatorUser) { + if (operatorId == op1) return opOwner1; + if (operatorId == op2) return opOwner2; + return opOwner3; + } + + function _availableBalance() internal view returns (uint256) { + (uint256 reserved, bool ok) = _currentProjectedEthReservations(); + if (!ok || address(this).balance <= reserved) return 0; + return address(this).balance - reserved; + } + + function _minimumActiveClusterBalance(uint64[] memory operatorIds, uint32 validatorCount) internal view returns (uint256) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint256 burnRate = _burnRate(operatorIds); + uint256 minPerBlock = + (burnRate + PackedETH.unwrap(sp.ethNetworkFee)) * uint256(validatorCount) * ETH_DEDUCTED_DIGITS; + uint256 minRequired = minPerBlock * SOLVENCY_BLOCK_WINDOW; + return minRequired == 0 ? ETH_DEDUCTED_DIGITS : minRequired; + } + + function _sumTrackedOperatorEthEarnings() internal view returns (uint256) { + StorageData storage s = SSVStorage.load(); + uint256 sum = 0; + + uint64[3] memory ids = [op1, op2, op3]; + for (uint256 i; i < ids.length; ++i) { + sum += PackedETHLib.unpack(s.operators[ids[i]].ethSnapshot.balance); + } + + return sum; + } + + function _sumTrackedOperatorEarnings(uint64[] memory operatorIds) internal view returns (uint256 sum) { + StorageData storage s = SSVStorage.load(); + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + sum += PackedETHLib.unpack(s.operators[operatorIds[i]].ethSnapshot.balance); + } + } + + function _sumProjectedOperatorEthEarnings() internal view returns (uint256 sum) { + uint64[3] memory ids = [op1, op2, op3]; + for (uint256 i; i < ids.length; ++i) { + sum += _projectedOperatorEthBalance(ids[i]); + } + } + + function _sumProjectedClusterBalances() internal view returns (uint256 sum) { + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[i]; + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists) continue; + sum += _projectedClusterBalance(clusterId, record); + } + } + + function _projectedClusterBalance(bytes32 clusterId, ClusterRecord storage record) internal view returns (uint256) { + ISSVNetworkCore.Cluster memory cluster = record.cluster; + if (!cluster.active) return cluster.balance; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + uint64 clusterIndex = _currentClusterIndex(operatorIds); + uint64 networkFeeIndex = ProtocolLib.currentNetworkFeeIndex(SSVStorageProtocol.load()); + + if (clusterIndex < cluster.index || networkFeeIndex < cluster.networkFeeIndex) { + return cluster.balance; + } + + cluster.updateBalanceWithEB(clusterId, clusterIndex, networkFeeIndex); + return cluster.balance; + } + + function _daoEthBalance() internal view returns (uint256) { + return PackedETHLib.unpack(SSVStorageProtocol.load().ethDaoBalance); + } + + function _stakingEthPoolBalance() internal view returns (uint256) { + return PackedETHLib.unpack(SSVStorageStaking.load().stakingEthPoolBalance); + } + + function _projectedDaoEthBalance() internal view returns (uint256) { + return PackedETHLib.unpack(ProtocolLib.networkTotalEarnings(SSVStorageProtocol.load())); + } + + function _currentEthLiabilities() internal view returns (uint256 liabilities, bool ok) { + (liabilities, ok) = _addNoOverflow(_sumProjectedClusterBalances(), _sumTrackedOperatorEthEarnings()); + if (!ok) return (0, false); + + uint256 protocolEthLiability = _daoEthBalance(); + uint256 stakingPoolLiability = _stakingEthPoolBalance(); + if (stakingPoolLiability > protocolEthLiability) { + protocolEthLiability = stakingPoolLiability; + } + + return _addNoOverflow(liabilities, protocolEthLiability); + } + + function _currentProjectedEthReservations() internal view returns (uint256 liabilities, bool ok) { + (liabilities, ok) = _addNoOverflow(_sumProjectedClusterBalances(), _sumProjectedOperatorEthEarnings()); + if (!ok) return (0, false); + + uint256 protocolEthLiability = _projectedDaoEthBalance(); + uint256 stakingPoolLiability = _stakingEthPoolBalance(); + if (stakingPoolLiability > protocolEthLiability) { + protocolEthLiability = stakingPoolLiability; + } + + return _addNoOverflow(liabilities, protocolEthLiability); + } + + function _addNoOverflow(uint256 a, uint256 b) internal pure returns (uint256 sum, bool ok) { + unchecked { + sum = a + b; + } + ok = sum >= a; + } + + function _boundAmount(uint256 seed, uint256 maxValue) internal pure returns (uint256) { + if (maxValue == 0) return 0; + return seed % (maxValue + 1); + } + + function _singleLeafRoot(bytes32 clusterId, uint32 effectiveBalance) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(keccak256(abi.encode(clusterId, effectiveBalance)))); + } + + function _setCommittedRoot(StorageEB storage seb, uint64 blockNum, bytes32 root) internal { + seb.ebRoots[blockNum] = root; + seb.latestCommittedBlock = blockNum; + } + + function _settleCluster(bytes32 clusterId, ClusterRecord storage record, uint64[] memory operatorIds) + internal + returns (uint256 burned) + { + uint256 beforeBalance = record.cluster.balance; + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + uint64 clusterIndex = _currentClusterIndex(operatorIds); + uint64 networkFeeIndex = ProtocolLib.currentNetworkFeeIndex(SSVStorageProtocol.load()); + + cluster.updateBalanceWithEB(clusterId, clusterIndex, networkFeeIndex); + cluster.index = clusterIndex; + cluster.networkFeeIndex = networkFeeIndex; + record.cluster = cluster; + + if (beforeBalance > cluster.balance) { + burned = beforeBalance - cluster.balance; + } + } + + function _sameCluster( + ISSVNetworkCore.Cluster memory lhs, + ISSVNetworkCore.Cluster memory rhs + ) internal pure returns (bool) { + return lhs.validatorCount == rhs.validatorCount && lhs.networkFeeIndex == rhs.networkFeeIndex + && lhs.index == rhs.index && lhs.active == rhs.active && lhs.balance == rhs.balance; + } + + function _expectedSettledCluster( + bytes32 clusterId, + ISSVNetworkCore.Cluster memory beforeCluster, + uint64[] memory operatorIds + ) internal view returns (ISSVNetworkCore.Cluster memory expectedCluster, uint256 burned) { + expectedCluster = ISSVNetworkCore.Cluster({ + validatorCount: beforeCluster.validatorCount, + networkFeeIndex: beforeCluster.networkFeeIndex, + index: beforeCluster.index, + active: beforeCluster.active, + balance: beforeCluster.balance + }); + + uint64 clusterIndex = _currentClusterIndex(operatorIds); + uint64 networkFeeIndex = ProtocolLib.currentNetworkFeeIndex(SSVStorageProtocol.load()); + if (clusterIndex < beforeCluster.index || networkFeeIndex < beforeCluster.networkFeeIndex) { + return (expectedCluster, 0); + } + + expectedCluster.updateBalanceWithEB(clusterId, clusterIndex, networkFeeIndex); + expectedCluster.index = clusterIndex; + expectedCluster.networkFeeIndex = networkFeeIndex; + if (beforeCluster.balance > expectedCluster.balance) { + burned = beforeCluster.balance - expectedCluster.balance; + } + } + + function _currentClusterIndex(uint64[] memory operatorIds) internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 currentBlock = uint64(block.number); + uint64 clusterIndex = 0; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + ISSVNetworkCore.Operator storage operator = s.operators[operatorIds[i]]; + uint64 blockDiff = currentBlock - uint64(operator.ethSnapshot.block); + clusterIndex += operator.ethSnapshot.index + blockDiff * PackedETH.unwrap(operator.ethFee); + } + return clusterIndex; + } + + function _fastForwardOperators(uint64[] memory operatorIds, uint32 blocks) internal { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint32 currentBlock = uint32(block.number); + + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + uint64 operatorId = operatorIds[i]; + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (operator.ethSnapshot.block == 0) continue; + + uint64 blockDiffFee = uint64(blocks) * PackedETH.unwrap(operator.ethFee); + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (operator.ethValidatorCount * BPS_DENOMINATOR); + + operator.ethSnapshot.index += blockDiffFee; + if (effectiveVUnits != 0 && blockDiffFee != 0) { + uint128 delta = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(uint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + } + + function _burnRate(uint64[] memory operatorIds) internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 burnRate = 0; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + burnRate += PackedETH.unwrap(s.operators[operatorIds[i]].ethFee); + } + return burnRate; + } + + function _projectedOperatorEthBalance(uint64 operatorId) internal view returns (uint256) { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + uint256 balance = PackedETHLib.unpack(operator.ethSnapshot.balance); + if (operator.ethSnapshot.block == 0) { + return balance; + } + + uint64 blockDiffFee = (uint64(block.number) - uint64(operator.ethSnapshot.block)) * PackedETH.unwrap(operator.ethFee); + if (blockDiffFee == 0) { + return balance; + } + + uint64 storedDeviation = SSVStorageEB.load().operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (operator.ethValidatorCount * BPS_DENOMINATOR); + if (effectiveVUnits == 0) { + return balance; + } + + uint128 deltaUnits = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + return balance + uint256(deltaUnits) * ETH_DEDUCTED_DIGITS; + } + + function _activeOperatorIds(uint64[] memory operatorIds) internal view returns (uint64[] memory activeOperatorIds) { + StorageData storage s = SSVStorage.load(); + uint256 count = operatorIds.length; + uint256 activeCount = 0; + for (uint256 i; i < count; ++i) { + if (s.operators[operatorIds[i]].ethSnapshot.block != 0) { + activeCount++; + } + } + + activeOperatorIds = new uint64[](activeCount); + uint256 next = 0; + for (uint256 i; i < count; ++i) { + uint64 operatorId = operatorIds[i]; + if (s.operators[operatorId].ethSnapshot.block != 0) { + activeOperatorIds[next++] = operatorId; + } + } + } + + function _reactivationMinRequired(uint64[] memory activeOperatorIds, ClusterRecord storage record) + internal + view + returns (uint256) + { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 burnRate = _burnRate(activeOperatorIds); + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(record.owner, operatorIds)); + + uint64 baselineVUnits = uint64(record.cluster.validatorCount) * BPS_DENOMINATOR; + uint64 storedVUnits = SSVStorageEB.load().clusterEB[clusterId].vUnits; + uint64 effectiveVUnits = storedVUnits > 0 ? storedVUnits : baselineVUnits; + uint256 threshold = ( + uint256(sp.minimumBlocksBeforeLiquidation) * uint256(burnRate + PackedETH.unwrap(sp.ethNetworkFee)) + * uint256(effectiveVUnits) + ) / BPS_DENOMINATOR; + threshold *= ETH_DEDUCTED_DIGITS; + + if (threshold <= record.cluster.balance) { + return 0; + } + return threshold - record.cluster.balance; + } + + function _isCanonicalRemovedOperatorCluster(bytes32 clusterId, ClusterRecord storage record) internal view returns (bool) { + return record.cluster.validatorCount == 1 && SSVStorageEB.load().clusterEB[clusterId].vUnits == 0; + } + + function _depositToLiquidatedCluster( + bytes32 clusterId, + ClusterRecord storage record, + uint64[] memory operatorIds, + uint256 amount + ) internal returns (bool) { + if (!record.exists || record.cluster.active || amount == 0) return false; + if (record.owner.balance < amount) return false; + if (SSVStorage.load().ethClusters[clusterId] != record.cluster.hashClusterData()) return false; + + ISSVNetworkCore.Cluster memory clusterBefore = record.cluster; + uint256 contractBalanceBefore = address(this).balance; + ClusterUser owner = _ownerUser(record.owner); + + try owner.depositFromBalance(record.owner, operatorIds, clusterBefore, amount) { + ISSVNetworkCore.Cluster memory expectedCluster = ISSVNetworkCore.Cluster({ + validatorCount: clusterBefore.validatorCount, + networkFeeIndex: clusterBefore.networkFeeIndex, + index: clusterBefore.index, + active: clusterBefore.active, + balance: clusterBefore.balance + amount + }); + + bytes32 expectedHash = expectedCluster.hashClusterData(); + bool hashMatches = SSVStorage.load().ethClusters[clusterId] == expectedHash; + if (!hashMatches) { + depositLiquidatedViolation = true; + return false; + } + if (expectedCluster.active) { + depositLiquidatedViolation = true; + } + if (address(this).balance != contractBalanceBefore + amount) { + depositLiquidatedViolation = true; + } + record.cluster = expectedCluster; + totalExpectedBalance += amount; + return true; + } catch { + depositLiquidatedViolation = true; + return false; + } + } + + function _decreaseExpected(uint256 amount) internal { + if (amount == 0) return; + if (totalExpectedBalance >= amount) { + totalExpectedBalance -= amount; + } else { + totalExpectedBalance = 0; + } + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + function _boundEffectiveBalance(uint256 seed, uint32 validatorCount) internal pure returns (uint32) { + if (validatorCount == 0) return 0; + + uint32 minEb = validatorCount * 32; + uint32 maxEb = validatorCount * 2048; + uint32 range = maxEb - minEb + 1; + + return minEb + uint32(seed % range); + } + +} diff --git a/test/echidna/SSVDAOEchidna.sol b/test/echidna/SSVDAOEchidna.sol new file mode 100644 index 000000000..c3f3157ef --- /dev/null +++ b/test/echidna/SSVDAOEchidna.sol @@ -0,0 +1,992 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/interfaces/ISSVDAO.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/storage/SSVStorageStaking.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/modules/SSVDAO.sol"; +import "../../contracts/interfaces/ICSSVToken.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "./SSVStakingEchidna.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {PackedETH, PackedSSV, DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract DAOUser { + ISSVDAO public dao; + + constructor(ISSVDAO dao_) { + dao = dao_; + } + + function withdraw(uint256 amount) external { + dao.withdrawNetworkSSVEarnings(amount); + } +} + +contract OracleUser { + ISSVDAO public dao; + + constructor(ISSVDAO dao_) { + dao = dao_; + } + + function commitRoot(bytes32 root, uint64 blockNum) external { + dao.commitRoot(root, blockNum); + } +} + +contract SSVDAOEchidna is SSVDAO { + using ProtocolLib for StorageProtocol; + + uint64 private constant MINIMAL_LIQUIDATION_THRESHOLD = 21_480; + uint64 private constant MAX_FEE_UNITS = 1_000_000; + uint64 private constant MAX_PERIOD = 1_000_000; + uint16 private constant MAX_QUORUM_BPS = 10_000; + uint16 private constant BPS_DENOMINATOR = 10_000; + uint256 private constant DUSTY_RAW_SUPPLY = 1_000_000_002; + uint256 private constant DUSTY_TRUNCATED_SUPPLY = 1_000_000_000; + uint16 private constant DUSTY_QUORUM_BPS = 7_500; + + MockToken private token; + + DAOUser private user1; + DAOUser private user2; + + OracleUser private oracle1; + OracleUser private oracle2; + OracleUser private oracle3; + OracleUser private oracle4; + OracleUser private candidate1; + OracleUser private candidate2; + OracleUser private attacker; + + uint64 private lastCommittedBlock; + + bytes32 private lastCommitRoot; + uint64 private lastCommitBlock; + OracleUser private lastCommitOracle; + + bytes32 private dustyRoot; + uint64 private dustyBlock; + uint8 private dustyVoteCount; + bool private dustyRoundSeeded; + bool private dustyPrematureCommit; + uint256 private dustySeedNonce; + bool private belowOracleCountCommitSucceeded; + bytes32 private failedQuorumKey; + uint64 private failedQuorumBlock; + bytes32 private failedQuorumRoot; + uint32 private failedQuorumOracleId; + bool private failedQuorumTracked; + bool private failedQuorumPersistenceViolation; + bool private revoteDifferentRootFailed; + bytes32 private generalizedDustRoot; + uint64 private generalizedDustBlock; + uint256 private generalizedDustSupply; + bool private generalizedDustRoundSeeded; + bool private generalizedDustTruncationViolation; + bytes32[] private generalizedDustCommitmentKeys; + mapping(bytes32 => bool) private generalizedDustCommitmentTracked; + mapping(bytes32 => uint256) private generalizedDustExpectedFrozen; + + mapping(bytes32 => mapping(uint32 => bool)) private localVotes; + + bool private nonOracleCommitSucceeded; + bool private duplicateVoteSucceeded; + bool private staleCommitSucceeded; + bool private futureCommitSucceeded; + bool private overWithdrawSucceeded; + bool private withdrawMismatch; + bool private feeIndexDecreased; + + uint256 private prevEthFeeCurrentIndex; + uint256 private prevSsvFeeCurrentIndex; + bool private feeIndexTrackingInitialized; + + bytes32[] private touchedCommitmentKeys; + mapping(bytes32 => bool) private touchedCommitmentKeyExists; + mapping(bytes32 => uint64) private commitmentBlockByKey; + mapping(bytes32 => bytes32) private commitmentRootByKey; + + bool private finalizedWeightNotCleared; + bool private commitmentWeightOverSupply; + bool private finalizationWithoutQuorum; + + uint256 private prevEthDaoEarningsUnits; + uint256 private prevSsvDaoEarningsUnits; + uint256 private totalDaoSsvMintedUnits; + bool private daoEarningsTrackingInitialized; + bool private daoEarningsDecreased; + bool private daoIndexBlockInFuture; + + modifier trackFeeIndexMonotonicity() { + _checkpointNetworkFeeIndices(); + _checkpointDaoEarningsAndIndices(); + _; + _checkpointNetworkFeeIndices(); + _checkpointDaoEarningsAndIndices(); + } + + constructor() SSVDAO(address(new CSSVTokenMock(address(this)))) { + token = new MockToken(); + + ISSVDAO self = ISSVDAO(address(this)); + user1 = new DAOUser(self); + user2 = new DAOUser(self); + + oracle1 = new OracleUser(self); + oracle2 = new OracleUser(self); + oracle3 = new OracleUser(self); + oracle4 = new OracleUser(self); + candidate1 = new OracleUser(self); + candidate2 = new OracleUser(self); + attacker = new OracleUser(self); + + _mockSetToken(address(token)); + + token.mint(address(user1), 1000 ether); + + _mockSetOracle(1, address(oracle1)); + _mockSetOracle(2, address(oracle2)); + _mockSetOracle(3, address(oracle3)); + _mockSetOracle(4, address(oracle4)); + + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + _checkpointNetworkFeeIndices(); + _checkpointDaoEarningsAndIndices(); + } + + function action_update_network_fee(uint256 seed) external trackFeeIndexMonotonicity { + uint64 feeUnits = _boundShrunk(seed, MAX_FEE_UNITS); + uint256 fee = uint256(feeUnits) * ETH_DEDUCTED_DIGITS; + try this.updateNetworkFee(fee) {} catch {} + } + + function action_update_network_fee_ssv(uint256 seed) external trackFeeIndexMonotonicity { + uint64 feeUnits = _boundShrunk(seed, MAX_FEE_UNITS); + uint256 fee = uint256(feeUnits) * DEDUCTED_DIGITS; + try this.updateNetworkFeeSSV(fee) {} catch {} + } + + function action_update_operator_fee_increase(uint64 percentage) external trackFeeIndexMonotonicity { + uint64 value = percentage % (MAX_FEE_UNITS + 1); + try this.updateOperatorFeeIncreaseLimit(value) {} catch {} + } + + function action_update_declare_period(uint64 secondsPeriod) external trackFeeIndexMonotonicity { + uint64 value = secondsPeriod % (MAX_PERIOD + 1); + try this.updateDeclareOperatorFeePeriod(value) {} catch {} + } + + function action_update_execute_period(uint64 secondsPeriod) external trackFeeIndexMonotonicity { + uint64 value = secondsPeriod % (MAX_PERIOD + 1); + try this.updateExecuteOperatorFeePeriod(value) {} catch {} + } + + function action_update_liquidation_threshold(uint64 blocksPeriod) external trackFeeIndexMonotonicity { + uint64 value = MINIMAL_LIQUIDATION_THRESHOLD + (blocksPeriod % 10_000); + try this.updateLiquidationThresholdPeriod(value) {} catch {} + } + + function action_update_liquidation_threshold_ssv(uint64 blocksPeriod) external trackFeeIndexMonotonicity { + uint64 value = MINIMAL_LIQUIDATION_THRESHOLD + (blocksPeriod % 10_000); + try this.updateLiquidationThresholdPeriodSSV(value) {} catch {} + } + + function action_update_min_liquidation_collateral(uint256 seed) external trackFeeIndexMonotonicity { + uint64 value = _boundShrunk(seed, MAX_FEE_UNITS); + uint256 amount = uint256(value) * ETH_DEDUCTED_DIGITS; + try this.updateMinimumLiquidationCollateral(amount) {} catch {} + } + + function action_update_min_liquidation_collateral_ssv(uint256 seed) external trackFeeIndexMonotonicity { + uint64 value = _boundShrunk(seed, MAX_FEE_UNITS); + uint256 amount = uint256(value) * DEDUCTED_DIGITS; + try this.updateMinimumLiquidationCollateralSSV(amount) {} catch {} + } + + function action_update_max_operator_fee(uint64 maxFee) external trackFeeIndexMonotonicity { + uint64 value = maxFee; + try this.updateMaximumOperatorFee(value) {} catch {} + } + + function action_update_min_operator_eth_fee(uint64 minFee) external trackFeeIndexMonotonicity { + uint64 value = minFee; + try this.updateMinimumOperatorEthFee(value) {} catch {} + } + + function action_set_quorum(uint16 quorum) external trackFeeIndexMonotonicity { + uint16 value = uint16(uint256(quorum) % (MAX_QUORUM_BPS + 1)); + try this.updateQuorumBps(value) {} catch {} + } + + function action_set_cooldown(uint64 duration) external trackFeeIndexMonotonicity { + uint64 value = duration; + try this.updateUnstakeCooldownDuration(value) {} catch {} + } + + function action_replace_oracle(uint8 oracleIdSeed, uint8 newOracleSeed) external trackFeeIndexMonotonicity { + uint32 oracleId = uint32(uint256(oracleIdSeed) % MAX_DELEGATION_SLOTS) + 1; + address newOracle = _oracleAddressBySeed(newOracleSeed); + try this.replaceOracle(oracleId, newOracle) {} catch {} + } + + function action_set_eth_vunits(uint64 vUnitsSeed) external trackFeeIndexMonotonicity { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.updateDAOEarnings(); + sp.daoTotalEthVUnits = vUnitsSeed % 100_001; + } + + function action_add_earnings(uint256 seed) external trackFeeIndexMonotonicity { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 currentBalance = PackedSSV.unwrap(sp.daoBalance); + uint64 maxAdd = type(uint64).max - currentBalance; + uint64 addUnits = _boundShrunk(seed, maxAdd); + if (addUnits == 0) return; + uint256 amount = uint256(addUnits) * DEDUCTED_DIGITS; + + token.mint(address(this), amount); + + sp.daoBalance = PackedSSV.wrap(currentBalance + addUnits); + sp.daoIndexBlockNumber = uint32(block.number); + totalDaoSsvMintedUnits += addUnits; + } + + function action_mint_cssv_supply(uint256 seed, uint8 userSeed) external trackFeeIndexMonotonicity { + uint256 units = (seed % 1_000_000) + 1; + uint256 amount = units * 1 ether; + CSSVTokenMock(CSSV_ADDRESS).mint(_cssvRecipient(userSeed), amount); + } + + function action_withdraw(uint256 seed, uint8 userSeed) external trackFeeIndexMonotonicity { + uint64 available = PackedSSV.unwrap(SSVStorageProtocol.load().daoBalance); + uint64 amountUnits; + + if (seed % 5 == 0) { + amountUnits = available + 1; + } else if (available == 0) { + amountUnits = 0; + } else { + amountUnits = uint64(seed % (available + 1)); + } + + uint256 amount = uint256(amountUnits) * DEDUCTED_DIGITS; + DAOUser caller = _withdrawUser(userSeed); + + uint256 beforeToken = token.balanceOf(address(this)); + uint64 beforeDao = PackedSSV.unwrap(SSVStorageProtocol.load().daoBalance); + + try caller.withdraw(amount) { + if (amountUnits > available) { + overWithdrawSucceeded = true; + return; + } + + uint256 afterToken = token.balanceOf(address(this)); + uint64 afterDao = PackedSSV.unwrap(SSVStorageProtocol.load().daoBalance); + + if (afterDao != beforeDao - amountUnits) withdrawMismatch = true; + if (afterToken != beforeToken - amount) withdrawMismatch = true; + } catch {} + } + + function action_commit_root(uint256 seed, uint8 oracleSeed) external trackFeeIndexMonotonicity { + OracleUser oracle = _oracleUser(oracleSeed); + uint64 blockNum = _validBlock(seed); + bytes32 root = _makeRoot(seed, oracleSeed); + _attemptCommit(oracle, root, blockNum); + } + + function action_commit_root_stale(uint8 oracleSeed) external trackFeeIndexMonotonicity { + OracleUser oracle = _oracleUser(oracleSeed); + uint64 blockNum = SSVStorageEB.load().latestCommittedBlock; + bytes32 root = _makeRoot(uint256(blockNum), oracleSeed); + _attemptCommit(oracle, root, blockNum); + } + + function action_commit_root_future(uint256 seed, uint8 oracleSeed) external trackFeeIndexMonotonicity { + OracleUser oracle = _oracleUser(oracleSeed); + uint64 blockNum = uint64(block.number) + 1 + uint64(seed % 10); + bytes32 root = _makeRoot(seed, oracleSeed); + _attemptCommit(oracle, root, blockNum); + } + + function action_commit_root_non_oracle(uint256 seed) external trackFeeIndexMonotonicity { + uint64 blockNum = _validBlock(seed); + bytes32 root = _makeRoot(seed, 99); + _attemptCommit(attacker, root, blockNum); + } + + function action_commit_root_duplicate(uint8) external trackFeeIndexMonotonicity { + if (lastCommitBlock == 0) return; + if (address(lastCommitOracle) == address(0)) return; + _attemptCommit(lastCommitOracle, lastCommitRoot, lastCommitBlock); + } + + function action_seed_dusty_commit_round(uint256 seed) external trackFeeIndexMonotonicity { + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + _setCssvSupply(DUSTY_RAW_SUPPLY); + + dustySeedNonce++; + dustyRoot = keccak256(abi.encodePacked("dusty-root", seed, dustySeedNonce)); + dustyBlock = _validBlock(seed); + dustyVoteCount = 0; + dustyRoundSeeded = true; + dustyPrematureCommit = false; + } + + function action_commit_root_dusty_shared(uint8 oracleSeed) external trackFeeIndexMonotonicity { + if (!dustyRoundSeeded) return; + + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + _setCssvSupply(DUSTY_RAW_SUPPLY); + + OracleUser oracle = _oracleUser(oracleSeed); + StorageStaking storage s = SSVStorageStaking.load(); + uint32 oracleId = s.oracleIdOf[address(oracle)]; + bytes32 commitmentKey = keccak256(abi.encodePacked(dustyBlock, dustyRoot)); + bool alreadyVoted = localVotes[commitmentKey][oracleId]; + + _attemptCommit(oracle, dustyRoot, dustyBlock); + + if (!alreadyVoted && localVotes[commitmentKey][oracleId]) { + unchecked { + dustyVoteCount += 1; + } + } + + if (SSVStorageEB.load().ebRoots[dustyBlock] == dustyRoot && dustyVoteCount < 3) { + dustyPrematureCommit = true; + } + } + + function action_commit_root_below_oracle_count(uint8 oracleSeed, uint8 rawSupplySeed, uint256 seed) + external + trackFeeIndexMonotonicity + { + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + + uint256 rawSupply = (uint256(rawSupplySeed) % (MAX_DELEGATION_SLOTS - 1)) + 1; + _setCssvSupply(rawSupply); + + OracleUser oracle = _oracleUser(oracleSeed); + uint64 blockNum = _validBlock(seed); + bytes32 root = keccak256(abi.encodePacked("below-oracle-count", seed, rawSupplySeed)); + StorageStaking storage s = SSVStorageStaking.load(); + uint32 oracleId = s.oracleIdOf[address(oracle)]; + bytes32 commitmentKey = keccak256(abi.encodePacked(blockNum, root)); + bool votedBefore = localVotes[commitmentKey][oracleId]; + + _attemptCommit(oracle, root, blockNum); + + if (!votedBefore && localVotes[commitmentKey][oracleId]) { + belowOracleCountCommitSucceeded = true; + } + } + + function action_seed_failed_quorum_round(uint256 seed, uint8 oracleSeed) external trackFeeIndexMonotonicity { + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + _setCssvSupply(DUSTY_RAW_SUPPLY); + + OracleUser oracle = _oracleUser(oracleSeed); + StorageStaking storage s = SSVStorageStaking.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint32 oracleId = s.oracleIdOf[address(oracle)]; + if (oracleId == 0) return; + + dustySeedNonce++; + bytes32 root = keccak256(abi.encodePacked("failed-quorum-root", seed, dustySeedNonce)); + uint64 blockNum = _validBlock(seed); + bytes32 commitmentKey = keccak256(abi.encodePacked(blockNum, root)); + bool votedBefore = localVotes[commitmentKey][oracleId]; + + _attemptCommit(oracle, root, blockNum); + + if (!votedBefore && localVotes[commitmentKey][oracleId]) { + if (seb.ebRoots[blockNum] == root) { + return; + } + + failedQuorumTracked = true; + failedQuorumKey = commitmentKey; + failedQuorumBlock = blockNum; + failedQuorumRoot = root; + failedQuorumOracleId = oracleId; + + if (seb.rootCommitments[commitmentKey] == 0 || seb.roundFrozenSupply[commitmentKey] == 0) { + failedQuorumPersistenceViolation = true; + } + } + } + + function action_revote_different_root_same_block(uint256 seed, uint8 firstOracleSeed, uint8 secondOracleSeed) + external + trackFeeIndexMonotonicity + { + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + _setCssvSupply(DUSTY_RAW_SUPPLY); + + OracleUser firstOracle = _oracleUser(firstOracleSeed); + OracleUser secondOracle = _oracleUser(secondOracleSeed); + if (address(firstOracle) == address(secondOracle)) { + secondOracle = _oracleUser(secondOracleSeed + 1); + } + if (address(firstOracle) == address(secondOracle)) return; + + StorageStaking storage s = SSVStorageStaking.load(); + uint32 firstOracleId = s.oracleIdOf[address(firstOracle)]; + uint32 secondOracleId = s.oracleIdOf[address(secondOracle)]; + if (firstOracleId == 0 || secondOracleId == 0) return; + + dustySeedNonce++; + bytes32 rootA = keccak256(abi.encodePacked("revote-root-a", seed, dustySeedNonce)); + bytes32 rootB = keccak256(abi.encodePacked("revote-root-b", seed, dustySeedNonce)); + uint64 blockNum = _validBlock(seed); + + _attemptCommit(firstOracle, rootA, blockNum); + + bytes32 commitmentKeyB = keccak256(abi.encodePacked(blockNum, rootB)); + bool votedBefore = localVotes[commitmentKeyB][secondOracleId]; + _attemptCommit(secondOracle, rootB, blockNum); + + if (!votedBefore && !localVotes[commitmentKeyB][secondOracleId]) { + revoteDifferentRootFailed = true; + } + } + + function action_seed_general_dust_round(uint256 rawSupplySeed, uint256 seed) external trackFeeIndexMonotonicity { + StorageStaking storage s = SSVStorageStaking.load(); + uint256 oracleCount = s.defaultOracleIds.length; + if (oracleCount == 0) return; + + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + + uint256 rawSupply = (rawSupplySeed % 1_000_000_000) + oracleCount; + _setCssvSupply(rawSupply); + + dustySeedNonce++; + generalizedDustRoot = keccak256(abi.encodePacked("general-dust-root", seed, dustySeedNonce)); + generalizedDustBlock = _validBlock(seed); + generalizedDustSupply = rawSupply; + generalizedDustRoundSeeded = true; + + bytes32 commitmentKey = keccak256(abi.encodePacked(generalizedDustBlock, generalizedDustRoot)); + uint256 expectedFrozen = rawSupply - (rawSupply % oracleCount); + generalizedDustExpectedFrozen[commitmentKey] = expectedFrozen; + + if (!generalizedDustCommitmentTracked[commitmentKey]) { + generalizedDustCommitmentTracked[commitmentKey] = true; + generalizedDustCommitmentKeys.push(commitmentKey); + } + } + + function action_commit_root_general_dust_shared(uint8 oracleSeed) external trackFeeIndexMonotonicity { + if (!generalizedDustRoundSeeded) return; + + _mockupdateQuorumBps(DUSTY_QUORUM_BPS); + _setCssvSupply(generalizedDustSupply); + + bytes32 commitmentKey = keccak256(abi.encodePacked(generalizedDustBlock, generalizedDustRoot)); + OracleUser oracle = _oracleUser(oracleSeed); + _attemptCommit(oracle, generalizedDustRoot, generalizedDustBlock); + + StorageEB storage seb = SSVStorageEB.load(); + if ( + seb.rootCommitments[commitmentKey] != 0 && + seb.roundFrozenSupply[commitmentKey] != generalizedDustExpectedFrozen[commitmentKey] + ) { + generalizedDustTruncationViolation = true; + } + } + + function echidna_network_fee_matches_expected() external view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (feeIndexDecreased) return false; + if (sp.ethNetworkFeeIndexBlockNumber > block.number) return false; + uint256 diff = block.number - sp.ethNetworkFeeIndexBlockNumber; + uint256 currentIndex = uint256(sp.ethNetworkFeeIndex) + diff * uint256(PackedETH.unwrap(sp.ethNetworkFee)); + if (currentIndex < sp.ethNetworkFeeIndex) return false; + return currentIndex >= prevEthFeeCurrentIndex; + } + + function echidna_network_fee_ssv_matches_expected() external view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (feeIndexDecreased) return false; + if (sp.networkFeeIndexBlockNumber > block.number) return false; + uint256 diff = block.number - sp.networkFeeIndexBlockNumber; + uint256 currentIndex = uint256(sp.networkFeeIndex) + diff * uint256(PackedSSV.unwrap(sp.networkFee)); + if (currentIndex < sp.networkFeeIndex) return false; + return currentIndex >= prevSsvFeeCurrentIndex; + } + + function echidna_liquidation_thresholds_valid() external view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (sp.minimumBlocksBeforeLiquidation != 0 && sp.minimumBlocksBeforeLiquidation < MINIMAL_LIQUIDATION_THRESHOLD) { + return false; + } + if ( + sp.minimumBlocksBeforeLiquidationSSV != 0 && + sp.minimumBlocksBeforeLiquidationSSV < MINIMAL_LIQUIDATION_THRESHOLD + ) { + return false; + } + return true; + } + + function echidna_quorum_bps_valid() external view returns (bool) { + return SSVStorageStaking.load().quorumBps <= MAX_QUORUM_BPS; + } + + function echidna_dao_balance_matches_expected() external view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + return token.balanceOf(address(this)) == uint256(PackedSSV.unwrap(sp.daoBalance)) * DEDUCTED_DIGITS; + } + + function echidna_withdraw_limits_enforced() external view returns (bool) { + return !overWithdrawSucceeded; + } + + function echidna_withdraw_conserves_balance() external view returns (bool) { + return !withdrawMismatch; + } + + function echidna_commit_root_only_oracle() external view returns (bool) { + return !nonOracleCommitSucceeded; + } + + function echidna_commit_root_no_duplicate_votes() external view returns (bool) { + return !duplicateVoteSucceeded; + } + + function echidna_commit_root_not_future() external view returns (bool) { + return !futureCommitSucceeded; + } + + function echidna_commit_root_not_stale() external view returns (bool) { + return !staleCommitSucceeded; + } + + function echidna_committed_block_monotonic() external view returns (bool) { + return SSVStorageEB.load().latestCommittedBlock >= lastCommittedBlock && + SSVStorageEB.load().latestCommittedBlock <= block.number; + } + + function echidna_commit_root_dust_round_reaches_quorum() external view returns (bool) { + if (!dustyRoundSeeded || dustyVoteCount < 3) return true; + + StorageEB storage seb = SSVStorageEB.load(); + return seb.ebRoots[dustyBlock] == dustyRoot && seb.latestCommittedBlock >= dustyBlock; + } + + function echidna_commit_root_dust_round_not_before_threshold() external view returns (bool) { + return !dustyPrematureCommit; + } + + function echidna_commit_root_dust_round_uses_truncated_supply() external view returns (bool) { + if (!dustyRoundSeeded) return true; + + bytes32 commitmentKey = keccak256(abi.encodePacked(dustyBlock, dustyRoot)); + StorageEB storage seb = SSVStorageEB.load(); + if (seb.rootCommitments[commitmentKey] == 0) return true; + + return seb.roundFrozenSupply[commitmentKey] == DUSTY_TRUNCATED_SUPPLY; + } + + function echidna_failed_quorum_persists() external view returns (bool) { + if (!failedQuorumTracked) return true; + if (failedQuorumPersistenceViolation) return false; + + StorageEB storage seb = SSVStorageEB.load(); + if (seb.ebRoots[failedQuorumBlock] == failedQuorumRoot) { + return true; + } + + return seb.hasVoted[failedQuorumKey][failedQuorumOracleId] && + seb.rootCommitments[failedQuorumKey] != 0 && + seb.roundFrozenSupply[failedQuorumKey] != 0; + } + + function echidna_revote_different_root_succeeds() external view returns (bool) { + return !revoteDifferentRootFailed; + } + + function echidna_commit_root_dust_round_uses_truncated_supply_generalized() external view returns (bool) { + if (generalizedDustTruncationViolation) return false; + + StorageEB storage seb = SSVStorageEB.load(); + uint256 count = generalizedDustCommitmentKeys.length; + for (uint256 i; i < count; ++i) { + bytes32 commitmentKey = generalizedDustCommitmentKeys[i]; + if (seb.rootCommitments[commitmentKey] == 0) continue; + if (seb.roundFrozenSupply[commitmentKey] != generalizedDustExpectedFrozen[commitmentKey]) { + return false; + } + } + + return true; + } + + function echidna_commit_root_below_oracle_count_reverts() external view returns (bool) { + return !belowOracleCountCommitSucceeded; + } + + function echidna_oracle_mapping_consistent() external view returns (bool) { + StorageStaking storage s = SSVStorageStaking.load(); + address addr1 = s.oracles[1]; + address addr2 = s.oracles[2]; + address addr3 = s.oracles[3]; + address addr4 = s.oracles[4]; + + if (addr1 != address(0) && s.oracleIdOf[addr1] != 1) return false; + if (addr2 != address(0) && s.oracleIdOf[addr2] != 2) return false; + if (addr3 != address(0) && s.oracleIdOf[addr3] != 3) return false; + if (addr4 != address(0) && s.oracleIdOf[addr4] != 4) return false; + + if (addr1 != address(0) && addr1 == addr2) return false; + if (addr1 != address(0) && addr1 == addr3) return false; + if (addr1 != address(0) && addr1 == addr4) return false; + if (addr2 != address(0) && addr2 == addr3) return false; + if (addr2 != address(0) && addr2 == addr4) return false; + if (addr3 != address(0) && addr3 == addr4) return false; + + return true; + } + + function echidna_finalized_weight_cleared() external view returns (bool) { + if (finalizedWeightNotCleared) return false; + + StorageEB storage seb = SSVStorageEB.load(); + uint256 count = touchedCommitmentKeys.length; + for (uint256 i; i < count; ++i) { + bytes32 key = touchedCommitmentKeys[i]; + uint64 blockNum = commitmentBlockByKey[key]; + bytes32 root = commitmentRootByKey[key]; + if (root == bytes32(0)) continue; + if (seb.ebRoots[blockNum] == root && seb.rootCommitments[key] != 0) return false; + } + return true; + } + + function echidna_commitment_weight_lte_supply() external view returns (bool) { + if (commitmentWeightOverSupply) return false; + + StorageEB storage seb = SSVStorageEB.load(); + uint256 count = touchedCommitmentKeys.length; + for (uint256 i; i < count; ++i) { + bytes32 key = touchedCommitmentKeys[i]; + uint256 committedWeight = seb.rootCommitments[key]; + if (committedWeight == 0) continue; + + uint256 frozenSupply = seb.roundFrozenSupply[key]; + if (frozenSupply == 0 || committedWeight > frozenSupply) return false; + } + return true; + } + + function echidna_finalization_implies_quorum() external view returns (bool) { + return !finalizationWithoutQuorum; + } + + function echidna_dao_earnings_monotonic() external view returns (bool) { + return !daoEarningsDecreased; + } + + function echidna_dao_index_block_lte_current() external view returns (bool) { + if (daoIndexBlockInFuture) return false; + + StorageProtocol storage sp = SSVStorageProtocol.load(); + return sp.ethDaoIndexBlockNumber <= block.number && sp.daoIndexBlockNumber <= block.number; + } + + function echidna_dao_earnings_formula_exact_in_range() external view returns (bool) { + (, bool expectRevert, uint64 expectedRaw) = _daoEarningsFormulaExpectation(); + if (expectRevert) return true; + + try this.exposedNetworkTotalEarningsRaw() returns (uint64 libRaw) { + return libRaw == expectedRaw; + } catch { + return false; + } + } + + function echidna_dao_earnings_formula_overflow_path_safe() external view returns (bool) { + (, bool expectRevert, ) = _daoEarningsFormulaExpectation(); + if (!expectRevert) return true; + + try this.exposedNetworkTotalEarningsRaw() returns (uint64) { + return false; + } catch { + return true; + } + } + + function echidna_dao_earnings_matches_formula() external view returns (bool) { + (, bool expectRevert, uint64 expectedRaw) = _daoEarningsFormulaExpectation(); + if (expectRevert) { + try this.exposedNetworkTotalEarningsRaw() returns (uint64) { + return false; + } catch { + return true; + } + } + + try this.exposedNetworkTotalEarningsRaw() returns (uint64 libRaw) { + return libRaw == expectedRaw; + } catch { + return false; + } + } + + function exposedNetworkTotalEarningsRaw() external view returns (uint64) { + return PackedETH.unwrap(ProtocolLib.networkTotalEarnings(SSVStorageProtocol.load())); + } + + function _attemptCommit(OracleUser oracle, bytes32 root, uint64 blockNum) internal { + StorageStaking storage s = SSVStorageStaking.load(); + StorageEB storage seb = SSVStorageEB.load(); + uint32 oracleId = s.oracleIdOf[address(oracle)]; + bytes32 commitmentKey = keccak256(abi.encodePacked(blockNum, root)); + bool alreadyVoted = localVotes[commitmentKey][oracleId]; + + uint64 latestBefore = seb.latestCommittedBlock; + uint256 currentSupply = IERC20(CSSV_ADDRESS).totalSupply(); + uint256 oracleCount = s.defaultOracleIds.length; + uint256 frozenSupply = seb.roundFrozenSupply[commitmentKey]; + if (frozenSupply == 0) { + frozenSupply = currentSupply - (currentSupply % oracleCount); + } + uint256 threshold = (frozenSupply * s.quorumBps) / BPS_DENOMINATOR; + uint256 weight = frozenSupply / oracleCount; + uint256 beforeWeight = seb.rootCommitments[commitmentKey]; + + if (!touchedCommitmentKeyExists[commitmentKey]) { + touchedCommitmentKeyExists[commitmentKey] = true; + touchedCommitmentKeys.push(commitmentKey); + } + commitmentBlockByKey[commitmentKey] = blockNum; + commitmentRootByKey[commitmentKey] = root; + + try oracle.commitRoot(root, blockNum) { + if (oracleId == 0) nonOracleCommitSucceeded = true; + if (blockNum > uint64(block.number)) futureCommitSucceeded = true; + if (blockNum <= latestBefore) staleCommitSucceeded = true; + if (alreadyVoted) duplicateVoteSucceeded = true; + + localVotes[commitmentKey][oracleId] = true; + + uint256 committedWeight = seb.rootCommitments[commitmentKey]; + uint256 frozenSupplyAfter = seb.roundFrozenSupply[commitmentKey]; + + if (committedWeight != 0 && (frozenSupplyAfter == 0 || committedWeight > frozenSupplyAfter)) { + commitmentWeightOverSupply = true; + } + + if (seb.ebRoots[blockNum] == root && root != bytes32(0)) { + if (committedWeight != 0) { + finalizedWeightNotCleared = true; + } + if (beforeWeight + weight < threshold) { + finalizationWithoutQuorum = true; + } + } + + _syncLatestCommittedBlock(); + lastCommitRoot = root; + lastCommitBlock = blockNum; + lastCommitOracle = oracle; + } catch {} + } + + function _syncLatestCommittedBlock() internal { + uint64 current = SSVStorageEB.load().latestCommittedBlock; + if (current >= lastCommittedBlock) { + lastCommittedBlock = current; + } + } + + function _validBlock(uint256 seed) internal view returns (uint64) { + uint64 current = uint64(block.number); + uint64 minBlock = SSVStorageEB.load().latestCommittedBlock + 1; + if (minBlock == 0) minBlock = 1; + if (current < minBlock) return current; + uint64 span = current - minBlock + 1; + return minBlock + uint64(seed % span); + } + + function _makeRoot(uint256 seed, uint8 oracleSeed) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(seed, oracleSeed)); + } + + function _oracleUser(uint8 seed) internal view returns (OracleUser) { + uint8 idx = uint8(uint256(seed) % MAX_DELEGATION_SLOTS); + if (idx == 0) return oracle1; + if (idx == 1) return oracle2; + if (idx == 2) return oracle3; + return oracle4; + } + + function _oracleAddressBySeed(uint8 seed) internal view returns (address) { + uint8 idx = seed % 6; + if (idx == 0) return address(oracle1); + if (idx == 1) return address(oracle2); + if (idx == 2) return address(oracle3); + if (idx == 3) return address(oracle4); + if (idx == 4) return address(candidate1); + return address(candidate2); + } + + function _withdrawUser(uint8 seed) internal view returns (DAOUser) { + if (seed % 2 == 0) return user1; + return user2; + } + + function _boundShrunk(uint256 seed, uint64 maxValue) internal pure returns (uint64) { + if (maxValue == 0) return 0; + return uint64(seed % (uint256(maxValue) + 1)); + } + + function _cssvRecipient(uint8 seed) internal view returns (address) { + uint8 idx = seed % 4; + if (idx == 0) return address(user1); + if (idx == 1) return address(user2); + if (idx == 2) return address(oracle1); + return address(oracle2); + } + + function _setCssvSupply(uint256 targetSupply) internal { + ICSSVToken cssv = ICSSVToken(CSSV_ADDRESS); + uint256 currentSupply = cssv.totalSupply(); + if (currentSupply < targetSupply) { + cssv.mint(address(this), targetSupply - currentSupply); + return; + } + + if (currentSupply > targetSupply) { + uint256 remaining = currentSupply - targetSupply; + remaining = _burnCssv(cssv, address(this), remaining); + remaining = _burnCssv(cssv, address(user1), remaining); + remaining = _burnCssv(cssv, address(user2), remaining); + remaining = _burnCssv(cssv, address(oracle1), remaining); + remaining = _burnCssv(cssv, address(oracle2), remaining); + remaining = _burnCssv(cssv, address(oracle3), remaining); + remaining = _burnCssv(cssv, address(oracle4), remaining); + + if (remaining != 0) { + revert("cssv supply rebalance incomplete"); + } + } + } + + function _burnCssv(ICSSVToken cssv, address holder, uint256 remaining) internal returns (uint256) { + if (remaining == 0) return 0; + + uint256 balance = IERC20(CSSV_ADDRESS).balanceOf(holder); + if (balance == 0) return remaining; + + uint256 burnAmount = balance < remaining ? balance : remaining; + cssv.burn(holder, burnAmount); + return remaining - burnAmount; + } + + function _checkpointNetworkFeeIndices() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (sp.ethNetworkFeeIndexBlockNumber > block.number || sp.networkFeeIndexBlockNumber > block.number) { + feeIndexDecreased = true; + return; + } + + uint256 ethDiff = block.number - sp.ethNetworkFeeIndexBlockNumber; + uint256 ethCurrent = uint256(sp.ethNetworkFeeIndex) + ethDiff * uint256(PackedETH.unwrap(sp.ethNetworkFee)); + + uint256 ssvDiff = block.number - sp.networkFeeIndexBlockNumber; + uint256 ssvCurrent = uint256(sp.networkFeeIndex) + ssvDiff * uint256(PackedSSV.unwrap(sp.networkFee)); + + if (!feeIndexTrackingInitialized) { + prevEthFeeCurrentIndex = ethCurrent; + prevSsvFeeCurrentIndex = ssvCurrent; + feeIndexTrackingInitialized = true; + return; + } + + if (ethCurrent < prevEthFeeCurrentIndex || ssvCurrent < prevSsvFeeCurrentIndex) { + feeIndexDecreased = true; + } + + prevEthFeeCurrentIndex = ethCurrent; + prevSsvFeeCurrentIndex = ssvCurrent; + } + + function _checkpointDaoEarningsAndIndices() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (sp.ethDaoIndexBlockNumber > block.number || sp.daoIndexBlockNumber > block.number) { + daoIndexBlockInFuture = true; + return; + } + + uint256 ethEarningsUnits = PackedETH.unwrap(sp.networkTotalEarnings()); + uint256 daoBalanceUnits = PackedSSV.unwrap(sp.daoBalance); + uint256 withdrawnUnits = totalDaoSsvMintedUnits >= daoBalanceUnits ? totalDaoSsvMintedUnits - daoBalanceUnits : 0; + uint256 ssvEarningsUnits = PackedSSV.unwrap(sp.networkTotalEarningsSSV()) + withdrawnUnits; + + if (!daoEarningsTrackingInitialized) { + prevEthDaoEarningsUnits = ethEarningsUnits; + prevSsvDaoEarningsUnits = ssvEarningsUnits; + daoEarningsTrackingInitialized = true; + return; + } + + if (ethEarningsUnits < prevEthDaoEarningsUnits || ssvEarningsUnits < prevSsvDaoEarningsUnits) { + daoEarningsDecreased = true; + } + + prevEthDaoEarningsUnits = ethEarningsUnits; + prevSsvDaoEarningsUnits = ssvEarningsUnits; + } + + function _daoEarningsFormulaExpectation() internal view returns (bool valid, bool expectRevert, uint64 expectedRaw) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 truncatedBlock = uint64(block.number); + + // ProtocolLib uses uint64(block.number) - ethDaoIndexBlockNumber. + // If this underflows, the library call must revert. + if (sp.ethDaoIndexBlockNumber > truncatedBlock) { + return (true, true, 0); + } + + uint256 blockDelta = uint256(truncatedBlock - sp.ethDaoIndexBlockNumber); + uint256 rawFee = uint256(PackedETH.unwrap(sp.ethNetworkFee)); + uint256 vUnits = uint256(sp.daoTotalEthVUnits); + uint256 rawBalance = uint256(PackedETH.unwrap(sp.ethDaoBalance)); + uint256 earningsUnits = (blockDelta * rawFee * vUnits) / BPS_DENOMINATOR; + + if (earningsUnits > type(uint64).max) { + return (true, true, 0); + } + if (rawBalance > type(uint64).max - earningsUnits) { + return (true, true, 0); + } + + return (true, false, uint64(rawBalance + earningsUnits)); + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + + function _mockSetOracle(uint32 oracleId, address oracle) internal { + StorageStaking storage s = SSVStorageStaking.load(); + s.oracles[oracleId] = oracle; + if (oracle != address(0)) { + s.oracleIdOf[oracle] = oracleId; + } + } + + function _mockupdateQuorumBps(uint16 quorum) internal { + SSVStorageStaking.load().quorumBps = quorum; + } +} diff --git a/test/echidna/SSVEBProofEchidna.sol b/test/echidna/SSVEBProofEchidna.sol new file mode 100644 index 000000000..6afe6c6d2 --- /dev/null +++ b/test/echidna/SSVEBProofEchidna.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/interfaces/ISSVClusters.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO} from "../../contracts/libraries/SSVCoreTypes.sol"; +import {PackedETHLib, PackedSSVLib} from "../../contracts/libraries/SSVPackedLib.sol"; + +contract EBUpdateUser { + ISSVClusters public clusters; + + constructor(ISSVClusters clusters_) { + clusters = clusters_; + } + + function updateBalance( + uint64 blockNum, + address clusterOwner, + uint64[] memory operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint32 effectiveBalance, + bytes32[] memory merkleProof + ) external { + clusters.updateClusterBalance(blockNum, clusterOwner, operatorIds, cluster, effectiveBalance, merkleProof); + } +} + +contract SSVEBProofEchidna is SSVClusters { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using PackedETHLib for PackedETH; + + uint32 private constant VALIDATOR_COUNT = 4; + uint32 private constant MIN_EB_PER_VALIDATOR = 32; + uint32 private constant MAX_EB_PER_VALIDATOR_ETH = 2048; + + EBUpdateUser private updateUser; + + address private clusterOwner; + uint64 private op1; + uint64[] private clusterOperatorIds; + ISSVNetworkCore.Cluster private clusterRecord; + bytes32 private clusterId; + + bool private invalidProofAccepted; + bool private ebOutOfBoundsAccepted; + bool private snapshotFieldsMismatch; + + struct LastUpdate { + uint64 blockNum; + uint32 effectiveBalance; + bool occurred; + } + LastUpdate private lastUpdate; + + constructor() { + _initProtocolDefaults(); + _initOperatorAndCluster(); + updateUser = new EBUpdateUser(ISSVClusters(address(this))); + } + + receive() external payable {} + + function _computeLeaf(bytes32 _clusterId, uint32 effectiveBalance) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(keccak256(abi.encode(_clusterId, effectiveBalance)))); + } + + function _boundEB(uint32 seed) internal pure returns (uint32) { + uint32 minEB = VALIDATOR_COUNT * MIN_EB_PER_VALIDATOR; + uint32 maxEB = VALIDATOR_COUNT * MAX_EB_PER_VALIDATOR_ETH; + uint32 range = maxEB - minEB + 1; + return minEB + (seed % range); + } + + function action_update_with_eb(uint32 effectiveBalance) external { + StorageEB storage seb = SSVStorageEB.load(); + uint64 blockNum = uint64(block.number); + + if ( + seb.clusterEB[clusterId].lastRootBlockNum != 0 && + blockNum <= seb.clusterEB[clusterId].lastRootBlockNum + ) return; + + if ( + seb.clusterEB[clusterId].lastUpdateBlock != 0 && + block.number < seb.clusterEB[clusterId].lastUpdateBlock + seb.minBlocksBetweenUpdates + ) return; + + bytes32 leaf = _computeLeaf(clusterId, effectiveBalance); + seb.ebRoots[blockNum] = leaf; + + bool inBounds = + effectiveBalance >= (VALIDATOR_COUNT * MIN_EB_PER_VALIDATOR) && + effectiveBalance <= (VALIDATOR_COUNT * MAX_EB_PER_VALIDATOR_ETH); + + bytes32[] memory emptyProof = new bytes32[](0); + + try updateUser.updateBalance( + blockNum, + clusterOwner, + clusterOperatorIds, + clusterRecord, + effectiveBalance, + emptyProof + ) { + if (!inBounds) { + ebOutOfBoundsAccepted = true; + } + + ClusterEBSnapshot memory snap = seb.clusterEB[clusterId]; + uint64 expectedVUnits = ClusterLib.ebToVUnits(effectiveBalance); + if (snap.vUnits != expectedVUnits) snapshotFieldsMismatch = true; + if (snap.lastRootBlockNum != blockNum) snapshotFieldsMismatch = true; + if (snap.lastUpdateBlock != uint64(block.number)) snapshotFieldsMismatch = true; + + lastUpdate = LastUpdate(blockNum, effectiveBalance, true); + } catch {} + } + + function action_update_tampered_eb(uint32 correctEB, uint32 tamperedEB) external { + StorageEB storage seb = SSVStorageEB.load(); + uint64 blockNum = uint64(block.number); + + if ( + seb.clusterEB[clusterId].lastRootBlockNum != 0 && + blockNum <= seb.clusterEB[clusterId].lastRootBlockNum + ) return; + + uint32 validEB = _boundEB(correctEB); + + uint32 wrongEB = tamperedEB; + if (wrongEB == validEB) { + wrongEB = (validEB == type(uint32).max) ? validEB - 1 : validEB + 1; + } + if (wrongEB == validEB) return; + + bytes32 leaf = _computeLeaf(clusterId, validEB); + seb.ebRoots[blockNum] = leaf; + + bytes32[] memory emptyProof = new bytes32[](0); + try updateUser.updateBalance( + blockNum, + clusterOwner, + clusterOperatorIds, + clusterRecord, + wrongEB, + emptyProof + ) { + invalidProofAccepted = true; + } catch {} + } + + function echidna_eb_merkle_proof_verified() external view returns (bool) { + return !invalidProofAccepted; + } + + function echidna_eb_bounds_enforced() external view returns (bool) { + return !ebOutOfBoundsAccepted; + } + + function echidna_eb_snapshot_fields_exact() external view returns (bool) { + return !snapshotFieldsMismatch; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFee = PACKED_ETH_ZERO; + sp.ethNetworkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = 0; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + sp.validatorsPerOperatorLimit = 3000; + + SSVStorageEB.load().minBlocksBetweenUpdates = 0; + } + + function _initOperatorAndCluster() internal { + StorageData storage s = SSVStorage.load(); + + s.lastOperatorId.increment(); + op1 = uint64(s.lastOperatorId.current()); + s.operators[op1] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PACKED_SSV_ZERO, + owner: address(this), + snapshot: ISSVNetworkCore.Snapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_SSV_ZERO + }), + whitelisted: false, + ethValidatorCount: VALIDATOR_COUNT, + ethFee: PACKED_ETH_ZERO, + ethSnapshot: ISSVNetworkCore.EthSnapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_ETH_ZERO + }) + }); + s.operatorsPKs[keccak256(abi.encodePacked(bytes32(uint256(0x1))))] = op1; + + clusterOwner = address(this); + uint64[] memory ids = new uint64[](1); + ids[0] = op1; + clusterOperatorIds = ids; + clusterId = keccak256(abi.encodePacked(clusterOwner, ids)); + + clusterRecord = ISSVNetworkCore.Cluster({ + validatorCount: VALIDATOR_COUNT, + networkFeeIndex: 0, + index: 0, + active: true, + balance: 1000 ether + }); + s.ethClusters[clusterId] = clusterRecord.hashClusterData(); + } +} diff --git a/test/echidna/SSVEdgeCasesEchidna.sol b/test/echidna/SSVEdgeCasesEchidna.sol new file mode 100644 index 000000000..03d397350 --- /dev/null +++ b/test/echidna/SSVEdgeCasesEchidna.sol @@ -0,0 +1,589 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/interfaces/ISSVClusters.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO, ETH_DEDUCTED_DIGITS, DEDUCTED_DIGITS} from "../../contracts/libraries/SSVCoreTypes.sol"; +import {PackedETHLib, PackedSSVLib} from "../../contracts/libraries/SSVPackedLib.sol"; + + +contract ClusterUser { + ISSVClusters public clusters; + + constructor(ISSVClusters clusters_) { + clusters = clusters_; + } + + receive() external payable {} + + function liquidate( + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + clusters.liquidate(clusterOwner, operatorIds, cluster); + } + + function reactivate( + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + clusters.reactivate{value: msg.value}(operatorIds, cluster); + } +} + +contract SSVEdgeCasesEchidna is SSVClusters { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using ProtocolLib for StorageProtocol; + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; + + PackedETH private constant DEFAULT_OPERATOR_ETH_FEE = PackedETH.wrap(1); + PackedSSV private constant DEFAULT_OPERATOR_SSV_FEE = PackedSSV.wrap(1); + PackedETH private constant DEFAULT_ETH_NETWORK_FEE = PackedETH.wrap(1); + PackedSSV private constant DEFAULT_SSV_NETWORK_FEE = PackedSSV.wrap(1); + uint64 private constant MIN_BLOCKS_BEFORE_LIQUIDATION = 2; + uint32 private constant MAX_ADVANCE_BLOCKS = 8; + uint32 private constant YOYO_LOOPS = 3; + + ClusterUser private owner; + ClusterUser private liquidator; + + uint64 private op1; + uint64 private op2; + uint64 private opSpam; + + struct ClusterRecord { + ISSVNetworkCore.Cluster cluster; + address owner; + bool exists; + } + + ClusterRecord private record; + bytes32 private clusterId; + + bool private yoyoLiquidationFailed; + bool private reactivationVUnitsMismatch; + bool private validatorSpamFailed; + bool private feeIndexOverflowMissed; + bool private feeIndexOverflowSSVMissed; + bool private packOverflowSucceeded; + bool private ethAccrualCorrupted; + + constructor() { + ISSVClusters self = ISSVClusters(address(this)); + owner = new ClusterUser(self); + liquidator = new ClusterUser(self); + + _initProtocolDefaults(); + _initOperators(); + _initCluster(); + } + + receive() external payable {} + + function action_fund(uint256 amount) external payable { + amount; + } + + function action_yoyo_liquidation(uint256 seed) external { + if (!record.exists) return; + + uint64[] memory operatorIds = _clusterOperatorIds(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (!record.cluster.active) { + if (address(this).balance == 0) return; + uint256 amount = _boundAmount(seed, address(this).balance); + if (amount == 0) amount = 1; + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + try owner.reactivate{value: amount}(operatorIds, cluster) { + record.cluster.active = true; + record.cluster.balance += amount; + record.cluster.index = _currentClusterIndex(operatorIds); + record.cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch { + return; + } + } + + for (uint32 i; i < YOYO_LOOPS; ++i) { + uint64 burnRate = _burnRate(operatorIds); + if (burnRate == 0) return; + + uint64 vUnits = ClusterLib.getVUnits(clusterId, record.cluster.validatorCount); + uint128 perBlockUnits = (uint128(burnRate + PackedETH.unwrap(sp.ethNetworkFee)) * uint128(vUnits)) / BPS_DENOMINATOR; + uint256 perBlock = PackedETHLib.unpack(PackedETH.wrap(uint64(perBlockUnits))); + if (perBlock == 0) return; + + if (record.cluster.balance > perBlock) { + record.cluster.balance = perBlock; + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } + + _fastForwardOperators(operatorIds, 2); + sp.ethNetworkFeeIndex += 2 * PackedETH.unwrap(sp.ethNetworkFee); + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + + _settleCluster(operatorIds); + + bool liquidatable = record.cluster.isLiquidatableWithEB( + clusterId, + burnRate, + PackedETH.unwrap(sp.ethNetworkFee), + sp.minimumBlocksBeforeLiquidation, + sp.minimumLiquidationCollateral + ); + + if (!liquidatable) { + yoyoLiquidationFailed = true; + return; + } + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + try liquidator.liquidate(record.owner, operatorIds, cluster) { + record.cluster.active = false; + record.cluster.balance = 0; + record.cluster.index = 0; + record.cluster.networkFeeIndex = 0; + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch { + yoyoLiquidationFailed = true; + return; + } + + uint256 available = address(this).balance; + if (available == 0) return; + uint256 amount = _boundAmount(seed >> 8, available); + if (amount == 0) amount = 1; + + cluster = record.cluster; + try owner.reactivate{value: amount}(operatorIds, cluster) { + record.cluster.active = true; + record.cluster.balance += amount; + record.cluster.index = _currentClusterIndex(operatorIds); + record.cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch { + yoyoLiquidationFailed = true; + return; + } + } + } + + function action_reactivation_vunits(uint256 seed) external { + if (!record.exists) return; + + uint64[] memory operatorIds = _clusterOperatorIds(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (!record.cluster.active) { + if (address(this).balance == 0) return; + uint256 amount = _boundAmount(seed, address(this).balance); + if (amount == 0) amount = 1; + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + try owner.reactivate{value: amount}(operatorIds, cluster) { + record.cluster.active = true; + record.cluster.balance += amount; + record.cluster.index = _currentClusterIndex(operatorIds); + record.cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch { + return; + } + } + + StorageEB storage seb = SSVStorageEB.load(); + uint64 baseline = uint64(record.cluster.validatorCount) * BPS_DENOMINATOR; + if (baseline == 0) return; + + // Deviation-only model: set up a valid scenario with POSITIVE deviation + // (e.g., 48 ETH per validator = 16 ETH deviation per validator) + // vUnits = baseline + deviation (must be >= baseline due to 32 ETH floor) + uint64 deviation = baseline / 4; // 25% deviation above baseline + uint64 clusterVUnits = baseline + deviation; + + // Set cluster EB snapshot with positive deviation + seb.clusterEB[clusterId].vUnits = clusterVUnits; + + // In deviation-only model, operatorEthVUnits stores ONLY the deviation, not full vUnits + // Record the deviation we're adding to operators + for (uint256 i; i < operatorIds.length; ++i) { + seb.operatorEthVUnits[operatorIds[i]] = deviation; + } + + record.cluster.balance = 0; + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + try liquidator.liquidate(record.owner, operatorIds, cluster) { + record.cluster.active = false; + record.cluster.balance = 0; + record.cluster.index = 0; + record.cluster.networkFeeIndex = 0; + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch { + return; + } + + if (address(this).balance == 0) return; + uint256 reactivateAmount = _boundAmount(seed >> 8, address(this).balance); + if (reactivateAmount == 0) reactivateAmount = 1; + + cluster = record.cluster; + try owner.reactivate{value: reactivateAmount}(operatorIds, cluster) { + record.cluster.active = true; + record.cluster.balance += reactivateAmount; + record.cluster.index = _currentClusterIndex(operatorIds); + record.cluster.networkFeeIndex = sp.currentNetworkFeeIndex(); + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } catch { + return; + } + + // In deviation-only model: + // - Liquidation subtracts the cluster's deviation from operatorEthVUnits + // - clusterEB.vUnits is reset to 0 during liquidation + // - Reactivation with clusterEB.vUnits == 0 means clusterDeviation = 0, so nothing added + // Expected: operatorEthVUnits should be 0 after liquidation removed the deviation + for (uint256 i; i < operatorIds.length; ++i) { + uint64 opVUnits = seb.operatorEthVUnits[operatorIds[i]]; + // After liquidation + reactivation, deviation should be removed (= 0) + // because this was the only cluster contributing deviation + if (opVUnits != 0) { + reactivationVUnitsMismatch = true; + } + } + } + + function action_validator_spam(uint256 seed) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[opSpam]; + if (operator.ethSnapshot.block == 0) return; + + PackedETH fee = sp.operatorMaxFee.eq(PACKED_ETH_ZERO) ? DEFAULT_OPERATOR_ETH_FEE : sp.operatorMaxFee; + operator.ethFee = fee; + operator.ethValidatorCount = sp.validatorsPerOperatorLimit; + + uint32 blocks = uint32(seed % MAX_ADVANCE_BLOCKS) + 1; + uint64 indexBefore = operator.ethSnapshot.index; + PackedETH balanceBefore = operator.ethSnapshot.balance; + + _fastForwardOperator(opSpam, blocks); + + if (operator.ethSnapshot.index < indexBefore) { + validatorSpamFailed = true; + return; + } + if (operator.ethSnapshot.balance.lt(balanceBefore)) { + validatorSpamFailed = true; + return; + } + if (operator.ethSnapshot.index - indexBefore != uint64(blocks) * PackedETH.unwrap(fee)) { + validatorSpamFailed = true; + } + } + + function action_fee_index_overflow() external { + try this.probe_fee_index_overflow_eth() { + feeIndexOverflowMissed = true; + } catch {} + + try this.probe_fee_index_overflow_ssv() { + feeIndexOverflowSSVMissed = true; + } catch {} + } + + function probe_fee_index_overflow_eth() external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint32 currentBlock = uint32(block.number); + if (currentBlock == 0) return; + + uint64 oldIndex = sp.ethNetworkFeeIndex; + PackedETH oldFee = sp.ethNetworkFee; + uint32 oldBlock = sp.ethNetworkFeeIndexBlockNumber; + + sp.ethNetworkFeeIndex = type(uint64).max - 1; + sp.ethNetworkFee = PackedETH.wrap(type(uint64).max); + sp.ethNetworkFeeIndexBlockNumber = currentBlock - 1; + + ProtocolLib.currentNetworkFeeIndex(sp); + + sp.ethNetworkFeeIndex = oldIndex; + sp.ethNetworkFee = oldFee; + sp.ethNetworkFeeIndexBlockNumber = oldBlock; + } + + function probe_fee_index_overflow_ssv() external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint32 currentBlock = uint32(block.number); + if (currentBlock == 0) return; + + uint64 oldIndex = sp.networkFeeIndex; + PackedSSV oldFee = sp.networkFee; + uint32 oldBlock = sp.networkFeeIndexBlockNumber; + + sp.networkFeeIndex = type(uint64).max - 1; + sp.networkFee = PackedSSV.wrap(type(uint64).max); + sp.networkFeeIndexBlockNumber = currentBlock - 1; + + ProtocolLib.currentNetworkFeeIndexSSV(sp); + + sp.networkFeeIndex = oldIndex; + sp.networkFee = oldFee; + sp.networkFeeIndexBlockNumber = oldBlock; + } + + function action_probe_max_eth_accrual(uint256 seed) external { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + ISSVNetworkCore.Operator storage operator = s.operators[opSpam]; + if (operator.ethSnapshot.block == 0) return; + + uint64 testFee = PackedETH.unwrap(sp.operatorMaxFee); + uint32 testValidators = sp.validatorsPerOperatorLimit; + + operator.ethFee = PackedETH.wrap(testFee); + operator.ethValidatorCount = testValidators; + seb.operatorEthVUnits[opSpam] = uint64(testValidators) * (640_000 - BPS_DENOMINATOR); + + PackedETH balanceBefore = operator.ethSnapshot.balance; + uint32 blocks = uint32(seed % MAX_ADVANCE_BLOCKS) + 1; + + _fastForwardOperator(opSpam, blocks); + + if (operator.ethSnapshot.balance.lt(balanceBefore)) { + ethAccrualCorrupted = true; + } + } + + function action_update_operator_max_fee(uint256 seed) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 fee = uint64(seed % (uint256(type(uint64).max) + 1)); + sp.operatorMaxFee = PackedETH.wrap(fee); + } + + function action_update_validators_per_operator_limit(uint256 seed) external { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = uint32(seed % 10_001); + } + + function action_pack_overflow_check() external { + try this.probe_pack_eth_overflow() { + packOverflowSucceeded = true; + } catch {} + + try this.probe_pack_ssv_overflow() { + packOverflowSucceeded = true; + } catch {} + + // Boundary: one above max valid packed value + try this.probe_pack_eth_boundary() { + packOverflowSucceeded = true; + } catch {} + + try this.probe_pack_ssv_boundary() { + packOverflowSucceeded = true; + } catch {} + } + + function probe_pack_eth_overflow() external pure { + PackedETHLib.pack(type(uint256).max); + } + + function probe_pack_ssv_overflow() external pure { + PackedSSVLib.pack(type(uint256).max); + } + + function probe_pack_eth_boundary() external pure { + PackedETHLib.pack(uint256(type(uint64).max) * ETH_DEDUCTED_DIGITS + ETH_DEDUCTED_DIGITS); + } + + function probe_pack_ssv_boundary() external pure { + PackedSSVLib.pack(uint256(type(uint64).max) * DEDUCTED_DIGITS + DEDUCTED_DIGITS); + } + + function echidna_yoyo_liquidation_reactivates() external view returns (bool) { + return !yoyoLiquidationFailed; + } + + function echidna_reactivation_restores_vunits() external view returns (bool) { + return !reactivationVUnitsMismatch; + } + + function echidna_validator_spam_safe() external view returns (bool) { + return !validatorSpamFailed; + } + + function echidna_fee_index_overflow_protected() external view returns (bool) { + return !feeIndexOverflowMissed && !feeIndexOverflowSSVMissed; + } + + function echidna_eth_accrual_no_overflow() external view returns (bool) { + return !ethAccrualCorrupted; + } + + function echidna_intermediate_mul_no_overflow() external view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint256 maxFee = uint256(PackedETH.unwrap(sp.operatorMaxFee)); + uint256 maxValidators = uint256(sp.validatorsPerOperatorLimit); + uint256 maxEffectiveVUnits = maxValidators * 640_000; + uint256 product = maxFee * maxEffectiveVUnits; + return product <= type(uint128).max; + } + + function echidna_pack_reverts_on_overflow() external view returns (bool) { + return !packOverflowSucceeded; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 3000; + sp.ethNetworkFee = DEFAULT_ETH_NETWORK_FEE; + sp.networkFee = DEFAULT_SSV_NETWORK_FEE; + sp.ethNetworkFeeIndex = 0; + sp.networkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.networkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.daoIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = MIN_BLOCKS_BEFORE_LIQUIDATION; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + sp.minimumBlocksBeforeLiquidationSSV = MIN_BLOCKS_BEFORE_LIQUIDATION; + sp.minimumLiquidationCollateralSSV = PACKED_SSV_ZERO; + sp.operatorMaxFee = PackedETH.wrap(type(uint64).max); + sp.operatorMaxFeeSSV = type(uint64).max; + } + + function _initOperators() internal { + StorageData storage s = SSVStorage.load(); + + op1 = _createOperator(s, address(owner), bytes32(uint256(0x1))); + op2 = _createOperator(s, address(owner), bytes32(uint256(0x2))); + opSpam = _createOperator(s, address(this), bytes32(uint256(0x3))); + } + + function _createOperator(StorageData storage s, address ownerAddr, bytes32 pk) internal returns (uint64) { + s.lastOperatorId.increment(); + uint64 id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: DEFAULT_OPERATOR_SSV_FEE, + owner: ownerAddr, + snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: PACKED_SSV_ZERO}), + whitelisted: false, + ethValidatorCount: 0, + ethFee: DEFAULT_OPERATOR_ETH_FEE, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: uint32(block.number), index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(pk))] = id; + return id; + } + + function _initCluster() internal { + uint64[] memory operatorIds = _clusterOperatorIds(); + clusterId = keccak256(abi.encodePacked(address(owner), operatorIds)); + + ISSVNetworkCore.Cluster memory cluster = ISSVNetworkCore.Cluster({ + validatorCount: 4, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + + SSVStorage.load().ethClusters[clusterId] = cluster.hashClusterData(); + record = ClusterRecord({cluster: cluster, owner: address(owner), exists: true}); + } + + function _clusterOperatorIds() internal view returns (uint64[] memory) { + uint64[] memory ids = new uint64[](2); + ids[0] = op1; + ids[1] = op2; + return ids; + } + + function _currentClusterIndex(uint64[] memory operatorIds) internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 currentBlock = uint64(block.number); + uint64 clusterIndex; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + ISSVNetworkCore.Operator storage operator = s.operators[operatorIds[i]]; + uint64 blockDiff = currentBlock - uint64(operator.ethSnapshot.block); + clusterIndex += operator.ethSnapshot.index + blockDiff * PackedETH.unwrap(operator.ethFee); + } + return clusterIndex; + } + + function _burnRate(uint64[] memory operatorIds) internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 burnRate; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + burnRate += PackedETH.unwrap(s.operators[operatorIds[i]].ethFee); + } + return burnRate; + } + + function _fastForwardOperators(uint64[] memory operatorIds, uint32 blocks) internal { + for (uint256 i; i < operatorIds.length; ++i) { + _fastForwardOperator(operatorIds[i], blocks); + } + } + + function _fastForwardOperator(uint64 operatorId, uint32 blocks) internal { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (operator.ethSnapshot.block == 0) return; + + uint32 currentBlock = uint32(block.number); + uint64 blockDiffFee = uint64(blocks) * PackedETH.unwrap(operator.ethFee); + + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (operator.ethValidatorCount * BPS_DENOMINATOR); + + operator.ethSnapshot.index += blockDiffFee; + if (effectiveVUnits != 0 && blockDiffFee != 0) { + uint128 delta = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(uint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + + function _settleCluster(uint64[] memory operatorIds) internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _currentClusterIndex(operatorIds); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + cluster.updateBalanceWithEB(clusterId, clusterIndex, networkFeeIndex); + cluster.index = clusterIndex; + cluster.networkFeeIndex = networkFeeIndex; + record.cluster = cluster; + SSVStorage.load().ethClusters[clusterId] = record.cluster.hashClusterData(); + } + + function _boundAmount(uint256 seed, uint256 maxValue) internal pure returns (uint256) { + if (maxValue == 0) return 0; + return seed % (maxValue + 1); + } +} diff --git a/test/echidna/SSVLegacyClustersEchidna.sol b/test/echidna/SSVLegacyClustersEchidna.sol new file mode 100644 index 000000000..0d64962c0 --- /dev/null +++ b/test/echidna/SSVLegacyClustersEchidna.sol @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/interfaces/ISSVClusters.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {PackedSSV, PackedETH, PACKED_SSV_ZERO, PACKED_ETH_ZERO, VERSION_SSV} from + "../../contracts/libraries/SSVCoreTypes.sol"; +import {PackedSSVLib, PackedETHLib, DEDUCTED_DIGITS} from "../../contracts/libraries/SSVPackedLib.sol"; +import {BPS_DENOMINATOR} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract SSVLiquidatorUser { + ISSVClusters public clusters; + + constructor(ISSVClusters clusters_) { + clusters = clusters_; + } + + receive() external payable {} + + function liquidateSSV( + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + clusters.liquidateSSV(clusterOwner, operatorIds, cluster); + } +} + +contract SSVLegacyClustersEchidna is SSVClusters { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using PackedSSVLib for PackedSSV; + + uint32 private constant MIN_BLOCKS_BEFORE_LIQ_SSV = 2; + uint32 private constant VALIDATOR_COUNT = 2; + PackedSSV private constant OPERATOR_SSV_FEE = PackedSSV.wrap(10); + uint256 private constant INITIAL_CLUSTER_BALANCE_SSV = 1_000 * DEDUCTED_DIGITS; + + MockToken private token; + SSVLiquidatorUser private liquidator; + + uint64 private op1; + uint64 private op2; + uint64[] private clusterOperatorIds; + + struct SSVClusterRecord { + ISSVNetworkCore.Cluster cluster; + address owner; + bool exists; + } + + SSVClusterRecord private record; + bytes32 private clusterId; + + bool private liquidationStateDirty; + bool private liquidationPayoutMismatch; + bool private ssvFeesUsedEbViolation; + + constructor() { + token = new MockToken(); + _mockSetToken(address(token)); + _initProtocolDefaults(); + _initOperators(); + _initSSVCluster(); + } + + receive() external payable {} + + function action_advance_time(uint256 blocksSeed) external { + if (!record.exists || !record.cluster.active) return; + + uint32 blocks = uint32(blocksSeed % 8) + 1; + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + uint32 currentBlock = uint32(block.number); + for (uint256 i; i < clusterOperatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[clusterOperatorIds[i]]; + if (op.snapshot.block == 0) continue; + uint64 blockDiffFee = uint64(blocks) * PackedSSV.unwrap(op.fee); + op.snapshot.index += blockDiffFee; + op.snapshot.balance = op.snapshot.balance.add( + PackedSSV.wrap(blockDiffFee * op.validatorCount) + ); + op.snapshot.block = currentBlock; + } + + sp.networkFeeIndex += uint64(blocks) * PackedSSV.unwrap(sp.networkFee); + sp.networkFeeIndexBlockNumber = currentBlock; + + uint64 clusterIndex = _currentSSVClusterIndex(); + uint64 networkFeeIndex = sp.networkFeeIndex; + ISSVNetworkCore.Cluster memory c = record.cluster; + ClusterLib.updateBalanceSSV(c, clusterIndex, networkFeeIndex); + c.index = clusterIndex; + c.networkFeeIndex = networkFeeIndex; + record.cluster = c; + s.clusters[clusterId] = c.hashClusterData(); + } + + function action_liquidate_ssv() external { + if (!record.exists || !record.cluster.active) return; + + // Settle harness state to current block so record.cluster.balance + // matches what the contract will compute inside liquidateSSV. + _syncToCurrentBlock(); + + uint256 clusterBalance = record.cluster.balance; + uint256 liquidatorTokenBefore = token.balanceOf(address(liquidator)); + uint256 contractTokenBefore = token.balanceOf(address(this)); + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + try liquidator.liquidateSSV(address(liquidator), clusterOperatorIds, cluster) { + StorageData storage s = SSVStorage.load(); + bytes32 storedHash = s.clusters[clusterId]; + + ISSVNetworkCore.Cluster memory expectedAfter = ISSVNetworkCore.Cluster({ + validatorCount: cluster.validatorCount, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + + if (storedHash != expectedAfter.hashClusterData()) { + liquidationStateDirty = true; + } + + uint256 liquidatorTokenAfter = token.balanceOf(address(liquidator)); + uint256 contractTokenAfter = token.balanceOf(address(this)); + uint256 paid = liquidatorTokenAfter - liquidatorTokenBefore; + + if (paid != clusterBalance) { + liquidationPayoutMismatch = true; + } + if (contractTokenBefore - contractTokenAfter != paid) { + liquidationPayoutMismatch = true; + } + + record.cluster = expectedAfter; + } catch {} + } + + function action_liquidate_ssv_with_eb_noise(uint256 seed) external { + if (!record.exists || !record.cluster.active) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + uint32 currentBlock = uint32(block.number); + if (currentBlock <= 1) return; + + uint32 blocksElapsed = uint32(seed % uint256(currentBlock - 1)) + 1; + uint32 staleBlock = currentBlock - blocksElapsed; + if (staleBlock == 0) return; + + for (uint256 i; i < clusterOperatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[clusterOperatorIds[i]]; + if (op.snapshot.block == 0) return; + op.snapshot.block = staleBlock; + } + sp.networkFeeIndexBlockNumber = staleBlock; + + uint64 baselineVUnits = uint64(record.cluster.validatorCount) * BPS_DENOMINATOR; + uint64 noiseVUnits = baselineVUnits + uint64(seed % uint256(baselineVUnits + 1)) + 1; + seb.clusterEB[clusterId].vUnits = noiseVUnits; + + uint64 clusterIndex = _currentSSVClusterIndex(); + uint64 networkFeeIndex = sp.networkFeeIndex + uint64(currentBlock - staleBlock) * PackedSSV.unwrap(sp.networkFee); + + ISSVNetworkCore.Cluster memory expectedCorrect = record.cluster; + ClusterLib.updateBalanceSSV(expectedCorrect, clusterIndex, networkFeeIndex); + expectedCorrect.index = clusterIndex; + expectedCorrect.networkFeeIndex = networkFeeIndex; + + uint128 idxOp = uint128(clusterIndex - record.cluster.index); + uint128 idxNet = uint128(networkFeeIndex - record.cluster.networkFeeIndex); + uint128 operatorFeeUnitsWrong = (idxOp * uint128(noiseVUnits)) / BPS_DENOMINATOR; + uint128 networkFeeUnitsWrong = (idxNet * uint128(noiseVUnits)) / BPS_DENOMINATOR; + uint256 wrongTotalFees = uint256(operatorFeeUnitsWrong + networkFeeUnitsWrong) * DEDUCTED_DIGITS; + + ISSVNetworkCore.Cluster memory wrongExpected = record.cluster; + wrongExpected.index = clusterIndex; + wrongExpected.networkFeeIndex = networkFeeIndex; + wrongExpected.balance = wrongExpected.balance >= wrongTotalFees ? wrongExpected.balance - wrongTotalFees : 0; + + uint256 liquidatorTokenBefore = token.balanceOf(address(liquidator)); + uint256 contractTokenBefore = token.balanceOf(address(this)); + ISSVNetworkCore.Cluster memory cluster = record.cluster; + + try liquidator.liquidateSSV(address(liquidator), clusterOperatorIds, cluster) { + bytes32 storedHash = s.clusters[clusterId]; + ISSVNetworkCore.Cluster memory expectedAfter = ISSVNetworkCore.Cluster({ + validatorCount: cluster.validatorCount, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + + if (storedHash != expectedAfter.hashClusterData()) { + liquidationStateDirty = true; + } + + uint256 liquidatorTokenAfter = token.balanceOf(address(liquidator)); + uint256 contractTokenAfter = token.balanceOf(address(this)); + uint256 paid = liquidatorTokenAfter - liquidatorTokenBefore; + + if (paid != expectedCorrect.balance) { + liquidationPayoutMismatch = true; + ssvFeesUsedEbViolation = true; + } + if (contractTokenBefore - contractTokenAfter != paid) { + liquidationPayoutMismatch = true; + ssvFeesUsedEbViolation = true; + } + if (wrongExpected.balance != expectedCorrect.balance && paid == wrongExpected.balance) { + ssvFeesUsedEbViolation = true; + } + + record.cluster = expectedAfter; + } catch {} + } + + function action_deposit_ssv(uint256 seed) external { + if (!record.exists || !record.cluster.active) return; + + uint256 amount = (seed % 1_000 + 1) * DEDUCTED_DIGITS; + token.mint(address(this), amount); + record.cluster.balance += amount; + SSVStorage.load().clusters[clusterId] = record.cluster.hashClusterData(); + } + + function echidna_ssv_liquidation_resets_and_pays() external view returns (bool) { + return !liquidationStateDirty && !liquidationPayoutMismatch; + } + + function echidna_ssv_fees_ignore_eb() external view returns (bool) { + return !ssvFeesUsedEbViolation; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 3000; + sp.networkFee = PackedSSV.wrap(1); + sp.networkFeeIndex = 0; + sp.networkFeeIndexBlockNumber = uint32(block.number); + sp.daoIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidationSSV = MIN_BLOCKS_BEFORE_LIQ_SSV; + sp.minimumLiquidationCollateralSSV = PACKED_SSV_ZERO; + sp.operatorMaxFeeSSV = type(uint64).max; + } + + function _initOperators() internal { + liquidator = new SSVLiquidatorUser(ISSVClusters(address(this))); + + StorageData storage s = SSVStorage.load(); + + s.lastOperatorId.increment(); + op1 = uint64(s.lastOperatorId.current()); + s.operators[op1] = ISSVNetworkCore.Operator({ + validatorCount: VALIDATOR_COUNT, + fee: OPERATOR_SSV_FEE, + owner: address(liquidator), + snapshot: ISSVNetworkCore.Snapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_SSV_ZERO + }), + whitelisted: false, + ethValidatorCount: 0, + ethFee: PACKED_ETH_ZERO, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: 0, index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(bytes32(uint256(0x10))))] = op1; + + s.lastOperatorId.increment(); + op2 = uint64(s.lastOperatorId.current()); + s.operators[op2] = ISSVNetworkCore.Operator({ + validatorCount: VALIDATOR_COUNT, + fee: OPERATOR_SSV_FEE, + owner: address(liquidator), + snapshot: ISSVNetworkCore.Snapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_SSV_ZERO + }), + whitelisted: false, + ethValidatorCount: 0, + ethFee: PACKED_ETH_ZERO, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: 0, index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(bytes32(uint256(0x11))))] = op2; + + uint64[] memory ids = new uint64[](2); + ids[0] = op1; + ids[1] = op2; + clusterOperatorIds = ids; + } + + function _initSSVCluster() internal { + StorageData storage s = SSVStorage.load(); + + clusterId = keccak256(abi.encodePacked(address(liquidator), clusterOperatorIds)); + + ISSVNetworkCore.Cluster memory cluster = ISSVNetworkCore.Cluster({ + validatorCount: VALIDATOR_COUNT, + networkFeeIndex: 0, + index: 0, + active: true, + balance: INITIAL_CLUSTER_BALANCE_SSV + }); + + s.clusters[clusterId] = cluster.hashClusterData(); + record = SSVClusterRecord({cluster: cluster, owner: address(liquidator), exists: true}); + + token.mint(address(this), INITIAL_CLUSTER_BALANCE_SSV); + + SSVStorageProtocol.load().daoValidatorCount += VALIDATOR_COUNT; + } + + function _syncToCurrentBlock() internal { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint32 currentBlock = uint32(block.number); + + for (uint256 i; i < clusterOperatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[clusterOperatorIds[i]]; + if (op.snapshot.block == 0 || op.snapshot.block >= currentBlock) continue; + uint64 blockDiff = uint64(currentBlock - op.snapshot.block); + uint64 blockDiffFee = blockDiff * PackedSSV.unwrap(op.fee); + op.snapshot.index += blockDiffFee; + op.snapshot.balance = op.snapshot.balance.add( + PackedSSV.wrap(blockDiffFee * op.validatorCount) + ); + op.snapshot.block = currentBlock; + } + + if (sp.networkFeeIndexBlockNumber < currentBlock) { + uint64 netDiff = uint64(currentBlock - sp.networkFeeIndexBlockNumber); + sp.networkFeeIndex += netDiff * PackedSSV.unwrap(sp.networkFee); + sp.networkFeeIndexBlockNumber = currentBlock; + } + + uint64 clusterIndex = _currentSSVClusterIndex(); + uint64 networkFeeIndex = sp.networkFeeIndex; + ISSVNetworkCore.Cluster memory c = record.cluster; + ClusterLib.updateBalanceSSV(c, clusterIndex, networkFeeIndex); + c.index = clusterIndex; + c.networkFeeIndex = networkFeeIndex; + record.cluster = c; + s.clusters[clusterId] = c.hashClusterData(); + } + + function _currentSSVClusterIndex() internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 currentBlock = uint64(block.number); + uint64 clusterIndex; + for (uint256 i; i < clusterOperatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[clusterOperatorIds[i]]; + uint64 blockDiff = currentBlock - uint64(op.snapshot.block); + clusterIndex += op.snapshot.index + blockDiff * PackedSSV.unwrap(op.fee); + } + return clusterIndex; + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } +} diff --git a/test/echidna/SSVLegacyValidatorRemovalEchidna.sol b/test/echidna/SSVLegacyValidatorRemovalEchidna.sol new file mode 100644 index 000000000..1665dcd99 --- /dev/null +++ b/test/echidna/SSVLegacyValidatorRemovalEchidna.sol @@ -0,0 +1,431 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/interfaces/ISSVClusters.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/interfaces/ISSVValidators.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/libraries/ValidatorLib.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/modules/SSVValidators.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO, DEDUCTED_DIGITS} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract LegacySSVValidatorRemovalUser { + ISSVValidators public validators; + + constructor(ISSVValidators validators_) { + validators = validators_; + } + + function remove( + bytes calldata publicKey, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + validators.removeValidator(publicKey, operatorIds, cluster); + } + + function bulkRemove( + bytes[] calldata publicKeys, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + validators.bulkRemoveValidator(publicKeys, operatorIds, cluster); + } +} + +/// @notice Targeted legacy SSV validator-removal harness for active and already-liquidated clusters. +contract SSVLegacyValidatorRemovalEchidna is SSVClusters, SSVValidators { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using ProtocolLib for StorageProtocol; + + uint32 private constant INITIAL_VALIDATOR_COUNT = 2; + uint256 private constant INITIAL_SSV_BALANCE = 1_000_000_000 * DEDUCTED_DIGITS; + uint64 private constant DEFAULT_OPERATOR_SSV_FEE = 1; + uint64 private constant DEFAULT_NETWORK_SSV_FEE = 1; + + MockToken private token; + + LegacySSVValidatorRemovalUser private activeOwner; + LegacySSVValidatorRemovalUser private liquidatedOwner; + + uint64 private op1; + uint64 private op2; + uint64 private op3; + uint64 private op4; + uint64[] private operatorIds; + + bytes32 private activeClusterId; + bytes32 private liquidatedClusterId; + ISSVNetworkCore.Cluster private activeClusterModel; + ISSVNetworkCore.Cluster private liquidatedClusterModel; + + uint32 private expectedCountedValidators; + + bool private settlementViolation; + mapping(uint8 => bool) private activeValidatorPresent; + mapping(uint8 => bool) private liquidatedValidatorPresent; + + constructor() { + token = new MockToken(); + _mockSetToken(address(token)); + + ISSVValidators validatorsSelf = ISSVValidators(address(this)); + activeOwner = new LegacySSVValidatorRemovalUser(validatorsSelf); + liquidatedOwner = new LegacySSVValidatorRemovalUser(validatorsSelf); + + _initProtocolDefaults(); + _initOperators(); + _initActiveLegacySsvCluster(); + _initLiquidatedLegacySsvCluster(); + } + + receive() external payable {} + + /// @notice No-op step so Echidna can build nonzero SSV accrual before active removals. + function action_advance_ssv_fees(uint256 seed) external pure { + seed; + } + + function action_remove_validator_active(uint256 seed) external { + if (!activeClusterModel.active || activeClusterModel.validatorCount == 0) return; + + uint8 tag = _pickPresentActiveValidator(seed); + if (tag == 0) return; + + ISSVNetworkCore.Cluster memory clusterBefore = activeClusterModel; + ISSVNetworkCore.Cluster memory expectedAfter = _settledActiveCluster(clusterBefore); + expectedAfter.validatorCount -= 1; + + try activeOwner.remove(_validatorKey(tag), _operatorIds(), clusterBefore) { + bytes32 firstHash = _validatorHash(tag, address(activeOwner)); + uint32 expectedCountAfter = expectedCountedValidators - 1; + if (!_postRemovalMatches(activeClusterId, expectedAfter, expectedCountAfter, firstHash, bytes32(0))) { + settlementViolation = true; + return; + } + + expectedCountedValidators = expectedCountAfter; + activeClusterModel = expectedAfter; + activeValidatorPresent[tag] = false; + } catch {} + } + + function action_bulk_remove_validator_active() external { + if (!activeClusterModel.active || activeClusterModel.validatorCount < 2) return; + if (!activeValidatorPresent[1] || !activeValidatorPresent[2]) return; + + ISSVNetworkCore.Cluster memory clusterBefore = activeClusterModel; + ISSVNetworkCore.Cluster memory expectedAfter = _settledActiveCluster(clusterBefore); + expectedAfter.validatorCount -= 2; + + bytes[] memory publicKeys = new bytes[](2); + publicKeys[0] = _validatorKey(1); + publicKeys[1] = _validatorKey(2); + + try activeOwner.bulkRemove(publicKeys, _operatorIds(), clusterBefore) { + bytes32 firstHash = _validatorHash(1, address(activeOwner)); + bytes32 secondHash = _validatorHash(2, address(activeOwner)); + uint32 expectedCountAfter = expectedCountedValidators - 2; + if (!_postRemovalMatches(activeClusterId, expectedAfter, expectedCountAfter, firstHash, secondHash)) { + settlementViolation = true; + return; + } + + expectedCountedValidators = expectedCountAfter; + activeClusterModel = expectedAfter; + activeValidatorPresent[1] = false; + activeValidatorPresent[2] = false; + } catch {} + } + + function action_remove_validator_liquidated(uint256 seed) external { + if (liquidatedClusterModel.active || liquidatedClusterModel.validatorCount == 0) return; + + uint8 tag = _pickPresentLiquidatedValidator(seed); + if (tag == 0) return; + + ISSVNetworkCore.Cluster memory clusterBefore = liquidatedClusterModel; + ISSVNetworkCore.Cluster memory expectedAfter = clusterBefore; + expectedAfter.validatorCount -= 1; + + try liquidatedOwner.remove(_validatorKey(tag), _operatorIds(), clusterBefore) { + bytes32 firstHash = _validatorHash(tag, address(liquidatedOwner)); + if (!_postRemovalMatches(liquidatedClusterId, expectedAfter, expectedCountedValidators, firstHash, bytes32(0))) { + settlementViolation = true; + return; + } + + liquidatedClusterModel = expectedAfter; + liquidatedValidatorPresent[tag] = false; + } catch {} + } + + function action_bulk_remove_validator_liquidated() external { + if (liquidatedClusterModel.active || liquidatedClusterModel.validatorCount < 2) return; + if (!liquidatedValidatorPresent[1] || !liquidatedValidatorPresent[2]) return; + + ISSVNetworkCore.Cluster memory clusterBefore = liquidatedClusterModel; + ISSVNetworkCore.Cluster memory expectedAfter = clusterBefore; + expectedAfter.validatorCount -= 2; + + bytes[] memory publicKeys = new bytes[](2); + publicKeys[0] = _validatorKey(1); + publicKeys[1] = _validatorKey(2); + + try liquidatedOwner.bulkRemove(publicKeys, _operatorIds(), clusterBefore) { + bytes32 firstHash = _validatorHash(1, address(liquidatedOwner)); + bytes32 secondHash = _validatorHash(2, address(liquidatedOwner)); + if (!_postRemovalMatches(liquidatedClusterId, expectedAfter, expectedCountedValidators, firstHash, secondHash)) { + settlementViolation = true; + return; + } + + liquidatedClusterModel = expectedAfter; + liquidatedValidatorPresent[1] = false; + liquidatedValidatorPresent[2] = false; + } catch {} + } + + function echidna_legacy_ssv_counts_match_model() external view returns (bool) { + if (settlementViolation) return false; + + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageData storage s = SSVStorage.load(); + + if (sp.daoValidatorCount != expectedCountedValidators) return false; + if (s.operators[op1].validatorCount != expectedCountedValidators) return false; + if (s.operators[op2].validatorCount != expectedCountedValidators) return false; + if (s.operators[op3].validatorCount != expectedCountedValidators) return false; + if (s.operators[op4].validatorCount != expectedCountedValidators) return false; + + return true; + } + + function echidna_legacy_ssv_cluster_hash_matches_model() external view returns (bool) { + if (settlementViolation) return false; + + StorageData storage s = SSVStorage.load(); + if (s.clusters[activeClusterId] != activeClusterModel.hashClusterData()) return false; + if (s.clusters[liquidatedClusterId] != liquidatedClusterModel.hashClusterData()) return false; + return true; + } + + function echidna_legacy_ssv_validator_storage_matches_model() external view returns (bool) { + if (!_validatorStorageMatches(1, address(activeOwner), activeValidatorPresent[1])) return false; + if (!_validatorStorageMatches(2, address(activeOwner), activeValidatorPresent[2])) return false; + if (!_validatorStorageMatches(1, address(liquidatedOwner), liquidatedValidatorPresent[1])) return false; + if (!_validatorStorageMatches(2, address(liquidatedOwner), liquidatedValidatorPresent[2])) return false; + if (_presentActiveValidatorCount() != activeClusterModel.validatorCount) return false; + if (_presentLiquidatedValidatorCount() != liquidatedClusterModel.validatorCount) return false; + return true; + } + + function _postRemovalMatches( + bytes32 targetClusterId, + ISSVNetworkCore.Cluster memory expectedAfter, + uint32 expectedCountAfter, + bytes32 firstHash, + bytes32 secondHash + ) internal view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageData storage s = SSVStorage.load(); + + if (sp.daoValidatorCount != expectedCountAfter) return false; + if (s.operators[op1].validatorCount != expectedCountAfter) return false; + if (s.operators[op2].validatorCount != expectedCountAfter) return false; + if (s.operators[op3].validatorCount != expectedCountAfter) return false; + if (s.operators[op4].validatorCount != expectedCountAfter) return false; + if (s.clusters[targetClusterId] != expectedAfter.hashClusterData()) return false; + if (s.validatorPKs[firstHash] != bytes32(0)) return false; + if (secondHash != bytes32(0) && s.validatorPKs[secondHash] != bytes32(0)) return false; + + return true; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 1000; + sp.networkFee = PackedSSV.wrap(DEFAULT_NETWORK_SSV_FEE); + sp.networkFeeIndex = 0; + sp.networkFeeIndexBlockNumber = uint32(block.number); + sp.daoBalance = PACKED_SSV_ZERO; + sp.daoIndexBlockNumber = uint32(block.number); + } + + function _initOperators() internal { + StorageData storage s = SSVStorage.load(); + op1 = _createOperator(s, bytes32(uint256(0x101))); + op2 = _createOperator(s, bytes32(uint256(0x102))); + op3 = _createOperator(s, bytes32(uint256(0x103))); + op4 = _createOperator(s, bytes32(uint256(0x104))); + + operatorIds.push(op1); + operatorIds.push(op2); + operatorIds.push(op3); + operatorIds.push(op4); + } + + function _initActiveLegacySsvCluster() internal { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + activeClusterId = keccak256(abi.encodePacked(address(activeOwner), operatorIds)); + token.mint(address(this), INITIAL_SSV_BALANCE); + + activeClusterModel = ISSVNetworkCore.Cluster({ + validatorCount: INITIAL_VALIDATOR_COUNT, + networkFeeIndex: sp.currentNetworkFeeIndexSSV(), + index: _currentClusterIndexSsv(), + active: true, + balance: INITIAL_SSV_BALANCE + }); + + s.clusters[activeClusterId] = activeClusterModel.hashClusterData(); + sp.updateDAOSSV(true, INITIAL_VALIDATOR_COUNT); + + s.operators[op1].validatorCount = INITIAL_VALIDATOR_COUNT; + s.operators[op2].validatorCount = INITIAL_VALIDATOR_COUNT; + s.operators[op3].validatorCount = INITIAL_VALIDATOR_COUNT; + s.operators[op4].validatorCount = INITIAL_VALIDATOR_COUNT; + + ValidatorLib.registerPublicKey(_validatorKey(1), _operatorIds(), address(activeOwner), s); + ValidatorLib.registerPublicKey(_validatorKey(2), _operatorIds(), address(activeOwner), s); + activeValidatorPresent[1] = true; + activeValidatorPresent[2] = true; + + expectedCountedValidators = INITIAL_VALIDATOR_COUNT; + } + + function _initLiquidatedLegacySsvCluster() internal { + StorageData storage s = SSVStorage.load(); + + liquidatedClusterId = keccak256(abi.encodePacked(address(liquidatedOwner), operatorIds)); + liquidatedClusterModel = ISSVNetworkCore.Cluster({ + validatorCount: INITIAL_VALIDATOR_COUNT, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + + s.clusters[liquidatedClusterId] = liquidatedClusterModel.hashClusterData(); + + ValidatorLib.registerPublicKey(_validatorKey(1), _operatorIds(), address(liquidatedOwner), s); + ValidatorLib.registerPublicKey(_validatorKey(2), _operatorIds(), address(liquidatedOwner), s); + liquidatedValidatorPresent[1] = true; + liquidatedValidatorPresent[2] = true; + } + + function _createOperator(StorageData storage s, bytes32 pk) internal returns (uint64) { + s.lastOperatorId.increment(); + uint64 id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PackedSSV.wrap(DEFAULT_OPERATOR_SSV_FEE), + owner: address(this), + snapshot: ISSVNetworkCore.Snapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_SSV_ZERO + }), + whitelisted: false, + ethValidatorCount: 0, + ethFee: PackedETH.wrap(0), + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: 0, index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(pk))] = id; + return id; + } + + function _currentClusterIndexSsv() internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 clusterIndex; + + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[operatorIds[i]]; + uint64 index = op.snapshot.index; + if (op.snapshot.block != 0) { + index += uint64(uint32(block.number) - op.snapshot.block) * DEFAULT_OPERATOR_SSV_FEE; + } + clusterIndex += index; + } + return clusterIndex; + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + + function _operatorIds() internal view returns (uint64[] memory ids) { + ids = new uint64[](operatorIds.length); + for (uint256 i; i < operatorIds.length; ++i) { + ids[i] = operatorIds[i]; + } + } + + function _settledActiveCluster(ISSVNetworkCore.Cluster memory clusterBefore) + internal + view + returns (ISSVNetworkCore.Cluster memory expectedAfter) + { + StorageProtocol storage sp = SSVStorageProtocol.load(); + expectedAfter = clusterBefore; + uint64 clusterIndex = _currentClusterIndexSsv(); + uint64 currentNetworkFeeIndexSSV = sp.currentNetworkFeeIndexSSV(); + expectedAfter.updateBalanceSSV(clusterIndex, currentNetworkFeeIndexSSV); + expectedAfter.index = clusterIndex; + expectedAfter.networkFeeIndex = currentNetworkFeeIndexSSV; + } + + function _pickPresentActiveValidator(uint256 seed) internal view returns (uint8) { + return _pickPresentValidator(seed, activeValidatorPresent); + } + + function _pickPresentLiquidatedValidator(uint256 seed) internal view returns (uint8) { + return _pickPresentValidator(seed, liquidatedValidatorPresent); + } + + function _pickPresentValidator(uint256 seed, mapping(uint8 => bool) storage present) internal view returns (uint8) { + uint8 first = uint8((seed % 2) + 1); + if (present[first]) return first; + + uint8 second = first == 1 ? 2 : 1; + if (present[second]) return second; + + return 0; + } + + function _presentActiveValidatorCount() internal view returns (uint32 count) { + if (activeValidatorPresent[1]) count++; + if (activeValidatorPresent[2]) count++; + } + + function _presentLiquidatedValidatorCount() internal view returns (uint32 count) { + if (liquidatedValidatorPresent[1]) count++; + if (liquidatedValidatorPresent[2]) count++; + } + + function _validatorHash(uint8 tag, address owner) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(_validatorKey(tag), owner)); + } + + function _validatorStorageMatches(uint8 tag, address owner, bool present) internal view returns (bool) { + bytes32 stored = SSVStorage.load().validatorPKs[_validatorHash(tag, owner)]; + if (present) return stored != bytes32(0); + return stored == bytes32(0); + } + + function _validatorKey(uint8 tag) internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(uint256(0xA000 + tag)), bytes16(uint128(0xB000 + tag))); + } +} diff --git a/test/echidna/SSVMigrationEchidna.sol b/test/echidna/SSVMigrationEchidna.sol new file mode 100644 index 000000000..1fff2e60f --- /dev/null +++ b/test/echidna/SSVMigrationEchidna.sol @@ -0,0 +1,762 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/interfaces/ISSVClusters.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/interfaces/ISSVOperators.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/modules/SSVDAO.sol"; +import "../../contracts/modules/SSVOperators.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "./SSVStakingEchidna.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETHLib, PackedSSVLib} from "../../contracts/libraries/SSVPackedLib.sol"; +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO, DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract MigrationClusterUser { + ISSVClusters public clusters; + + constructor(ISSVClusters clusters_) { + clusters = clusters_; + } + + receive() external payable {} + + function migrateToETH(uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) external payable { + clusters.migrateClusterToETH{value: msg.value}(operatorIds, cluster); + } + + function liquidateSSV(uint64[] calldata operatorIds, ISSVNetworkCore.Cluster memory cluster) external { + clusters.liquidateSSV(address(this), operatorIds, cluster); + } + + function updateClusterBalance( + uint64 blockNum, + address clusterOwner, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint32 effectiveBalance, + bytes32[] calldata merkleProof + ) external { + clusters.updateClusterBalance(blockNum, clusterOwner, operatorIds, cluster, effectiveBalance, merkleProof); + } +} + +contract MigrationOperatorUser { + ISSVOperators public operators; + + constructor(ISSVOperators operators_) { + operators = operators_; + } + + receive() external payable {} + + function remove(uint64 operatorId) external { + operators.removeOperator(operatorId); + } +} + +/// @notice Targeted migration harness for BUG-14 class: removed operators and frozen SSV index accounting. +contract SSVMigrationEchidna is SSVClusters, SSVOperators(0), SSVDAO { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using ProtocolLib for StorageProtocol; + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; + + uint32 private constant MAX_ADVANCE_BLOCKS = 8; + PackedETH private constant DEFAULT_OPERATOR_ETH_FEE = PackedETH.wrap(1); + PackedSSV private constant DEFAULT_OPERATOR_SSV_FEE = PackedSSV.wrap(1); + PackedETH private constant DEFAULT_NETWORK_ETH_FEE = PackedETH.wrap(1); + PackedSSV private constant DEFAULT_NETWORK_SSV_FEE = PackedSSV.wrap(1); + uint64 private constant MIN_BLOCKS_BEFORE_LIQUIDATION = 2; + uint32 private constant INITIAL_VALIDATOR_COUNT = 2; + uint256 private constant INITIAL_SSV_BALANCE = 1_000 * DEDUCTED_DIGITS; + + MockToken private token; + + MigrationClusterUser private clusterOwner; + MigrationOperatorUser private opOwner1; + MigrationOperatorUser private opOwner2; + MigrationOperatorUser private opOwner3; + + uint64 private op1; + uint64 private op2; + uint64 private op3; + uint64[] private operatorIds; + + struct SSVClusterRecord { + ISSVNetworkCore.Cluster cluster; + address owner; + bool exists; + } + + bytes32 private ssvClusterId; + SSVClusterRecord private ssvRecord; + + uint256 private unallocatedEth; + bool private accountingViolation; + bool private removedEthInitViolation; + bool private removedStateViolation; + bool private migrationObserved; + bool private migrationValidatorShiftViolation; + bool private liquidatedMigrationViolation; + bool private liquidatedMigrationObserved; + bool private ssvEbUpdateViolation; + bool private removedOperatorVUnitsMutationViolation; + uint32 private daoValidatorCountBeforeMigration; + uint32 private ethDaoValidatorCountBeforeMigration; + uint32 private migratedValidatorCount; + + mapping(uint64 => bool) private removedTracked; + mapping(uint64 => bool) private removedBeforeMigration; + mapping(uint64 => uint64) private removedFrozenIndex; + + constructor() SSVDAO(address(new CSSVTokenMock(address(this)))) { + token = new MockToken(); + _mockSetToken(address(token)); + + ISSVClusters clustersSelf = ISSVClusters(address(this)); + ISSVOperators operatorsSelf = ISSVOperators(address(this)); + + clusterOwner = new MigrationClusterUser(clustersSelf); + opOwner1 = new MigrationOperatorUser(operatorsSelf); + opOwner2 = new MigrationOperatorUser(operatorsSelf); + opOwner3 = new MigrationOperatorUser(operatorsSelf); + + _initProtocolDefaults(); + _initOperators(); + _initActiveSSVCluster(); + } + + receive() external payable {} + + function action_fund_eth(uint256 amount) external payable { + amount; + if (msg.value == 0) return; + unallocatedEth += msg.value; + } + + /// @notice Liquidates the legacy SSV cluster through the self-liquidation path. + function action_liquidate_ssv() external { + if (!ssvRecord.exists || !ssvRecord.cluster.active) return; + + _settleSsvCluster(); + + ISSVNetworkCore.Cluster memory clusterBefore = ssvRecord.cluster; + try clusterOwner.liquidateSSV(operatorIds, clusterBefore) { + ISSVNetworkCore.Cluster memory expectedAfter = ISSVNetworkCore.Cluster({ + validatorCount: clusterBefore.validatorCount, + networkFeeIndex: 0, + index: 0, + active: false, + balance: 0 + }); + + if (SSVStorage.load().clusters[ssvClusterId] != expectedAfter.hashClusterData()) { + liquidatedMigrationViolation = true; + return; + } + + ssvRecord.cluster = expectedAfter; + } catch {} + } + + /// @notice Advances SSV operator/network fee indexes without syncing cluster index. + function action_advance_ssv_without_cluster_sync(uint256 seed) external { + if (!ssvRecord.exists || !ssvRecord.cluster.active) return; + uint32 blocks_ = uint32(seed % MAX_ADVANCE_BLOCKS) + 1; + _fastForwardSSV(blocks_); + } + + /// @notice Settles SSV cluster state (index + balance) to current operator/network indexes. + function action_sync_ssv_cluster() external { + _settleSsvCluster(); + } + + /// @notice Removes one cluster operator and tracks frozen index/state. + function action_remove_operator(uint256 seed) external { + if (!ssvRecord.exists) return; + + uint64 operatorId = operatorIds[seed % operatorIds.length]; + address ownerAddr = _operatorOwner(operatorId); + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory before = SSVStorage.load().operators[operatorId]; + if (before.snapshot.block == 0 && before.ethSnapshot.block == 0) return; + + MigrationOperatorUser owner = _operatorUser(ownerAddr); + try owner.remove(operatorId) { + ISSVNetworkCore.Operator memory afterOp = SSVStorage.load().operators[operatorId]; + if (afterOp.snapshot.block != 0 || afterOp.ethSnapshot.block != 0) { + removedStateViolation = true; + return; + } + + removedTracked[operatorId] = true; + removedBeforeMigration[operatorId] = ssvRecord.exists; + removedFrozenIndex[operatorId] = afterOp.snapshot.index; + } catch {} + } + + /// @notice Updates the EB snapshot for a legacy SSV cluster and asserts the path is snapshot-only. + function action_update_ssv_cluster_balance_valid(uint256 seed) external { + if (!ssvRecord.exists) return; + + StorageData storage s = SSVStorage.load(); + if (s.clusters[ssvClusterId] != ssvRecord.cluster.hashClusterData()) return; + + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot memory ebBefore = seb.clusterEB[ssvClusterId]; + + if (uint64(block.number) < ebBefore.lastRootBlockNum + 1) return; + if (ebBefore.lastUpdateBlock != 0 && uint64(block.number) < ebBefore.lastUpdateBlock + seb.minBlocksBetweenUpdates) + { + return; + } + + uint64 minBlockNum = ebBefore.lastRootBlockNum + 1; + uint64 blockNum = minBlockNum + uint64((seed >> 8) % (uint64(block.number) - minBlockNum + 1)); + + uint32 minEb = ssvRecord.cluster.validatorCount * uint32(DEFAULT_EB_PER_VALIDATOR / 1 ether); + uint32 maxEb = minEb + (ssvRecord.cluster.validatorCount * 16); + uint32 effectiveBalance = minEb; + if (maxEb > minEb) { + effectiveBalance = minEb + uint32((seed >> 24) % (maxEb - minEb + 1)); + } + + bytes32 storedSsvHashBefore = s.clusters[ssvClusterId]; + bytes32 storedEthHashBefore = s.ethClusters[ssvClusterId]; + uint32 daoValidatorCountBefore = sp.daoValidatorCount; + uint32 ethDaoValidatorCountBefore = sp.ethDaoValidatorCount; + uint64 daoTotalEthVUnitsBefore = sp.daoTotalEthVUnits; + + uint32[] memory operatorValidatorCountsBefore = new uint32[](operatorIds.length); + uint32[] memory operatorEthValidatorCountsBefore = new uint32[](operatorIds.length); + uint64[] memory operatorEthVUnitsBefore = new uint64[](operatorIds.length); + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[operatorIds[i]]; + operatorValidatorCountsBefore[i] = op.validatorCount; + operatorEthValidatorCountsBefore[i] = op.ethValidatorCount; + operatorEthVUnitsBefore[i] = seb.operatorEthVUnits[operatorIds[i]]; + } + + bytes32 root = _singleLeafRoot(ssvClusterId, effectiveBalance); + _setCommittedRoot(seb, blockNum, root); + bytes32[] memory proof = new bytes32[](0); + + try clusterOwner.updateClusterBalance( + blockNum, ssvRecord.owner, operatorIds, ssvRecord.cluster, effectiveBalance, proof + ) { + ClusterEBSnapshot storage ebAfter = seb.clusterEB[ssvClusterId]; + if (s.clusters[ssvClusterId] != storedSsvHashBefore) { + ssvEbUpdateViolation = true; + } + if (s.ethClusters[ssvClusterId] != storedEthHashBefore) { + ssvEbUpdateViolation = true; + } + if (sp.daoValidatorCount != daoValidatorCountBefore) { + ssvEbUpdateViolation = true; + } + if (sp.ethDaoValidatorCount != ethDaoValidatorCountBefore) { + ssvEbUpdateViolation = true; + } + if (sp.daoTotalEthVUnits != daoTotalEthVUnitsBefore) { + ssvEbUpdateViolation = true; + } + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[operatorIds[i]]; + if (op.validatorCount != operatorValidatorCountsBefore[i]) { + ssvEbUpdateViolation = true; + } + if (op.ethValidatorCount != operatorEthValidatorCountsBefore[i]) { + ssvEbUpdateViolation = true; + } + if (seb.operatorEthVUnits[operatorIds[i]] != operatorEthVUnitsBefore[i]) { + ssvEbUpdateViolation = true; + } + } + if (ebAfter.vUnits != ClusterLib.ebToVUnits(effectiveBalance)) { + ssvEbUpdateViolation = true; + } + if (ebAfter.lastRootBlockNum != blockNum) { + ssvEbUpdateViolation = true; + } + if (ebAfter.lastUpdateBlock != uint64(block.number)) { + ssvEbUpdateViolation = true; + } + } catch { + ssvEbUpdateViolation = true; + } + } + + /// @notice Attempts SSV->ETH migration and checks BUG-14 accounting properties on success. + function action_migrate_ssv_to_eth(uint256 seed) external { + if (!ssvRecord.exists || !ssvRecord.cluster.active) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + uint64 clusterIndexSSV = _currentClusterIndexSsv(); + uint64 currentNfiSSV = sp.currentNetworkFeeIndexSSV(); + + ISSVNetworkCore.Cluster memory clusterBefore = ssvRecord.cluster; + ISSVNetworkCore.Cluster memory expected = ISSVNetworkCore.Cluster({ + validatorCount: clusterBefore.validatorCount, + networkFeeIndex: clusterBefore.networkFeeIndex, + index: clusterBefore.index, + active: clusterBefore.active, + balance: clusterBefore.balance + }); + expected.updateBalanceSSV(clusterIndexSSV, currentNfiSSV); + uint256 expectedRefund = expected.balance; + + uint256 minRequired = _migrationMinRequired(clusterBefore, sp); + + if (unallocatedEth <= minRequired) return; + uint256 amount = seed % (unallocatedEth + 1); + if (amount <= minRequired) amount = minRequired + 1; + if (amount > unallocatedEth) return; + + uint256 ownerTokenBefore = token.balanceOf(ssvRecord.owner); + uint32 daoBefore = sp.daoValidatorCount; + uint32 ethDaoBefore = sp.ethDaoValidatorCount; + uint32 validatorsMigrated = clusterBefore.validatorCount; + uint64[] memory removedOperatorVUnitsBefore = new uint64[](operatorIds.length); + for (uint256 i; i < operatorIds.length; ++i) { + if (!removedBeforeMigration[operatorIds[i]]) continue; + removedOperatorVUnitsBefore[i] = seb.operatorEthVUnits[operatorIds[i]]; + } + MigrationClusterUser owner = clusterOwner; + try owner.migrateToETH{value: amount}(operatorIds, clusterBefore) { + uint256 ownerTokenAfter = token.balanceOf(ssvRecord.owner); + uint256 actualRefund = ownerTokenAfter - ownerTokenBefore; + if (actualRefund != expectedRefund) { + accountingViolation = true; + } + + migrationObserved = true; + daoValidatorCountBeforeMigration = daoBefore; + ethDaoValidatorCountBeforeMigration = ethDaoBefore; + migratedValidatorCount = validatorsMigrated; + + uint32 daoAfter = sp.daoValidatorCount; + uint32 ethDaoAfter = sp.ethDaoValidatorCount; + if (daoAfter != daoBefore - validatorsMigrated) { + migrationValidatorShiftViolation = true; + } + if (ethDaoAfter != ethDaoBefore + validatorsMigrated) { + migrationValidatorShiftViolation = true; + } + if (uint256(daoAfter) + uint256(ethDaoAfter) != uint256(daoBefore) + uint256(ethDaoBefore)) { + migrationValidatorShiftViolation = true; + } + + for (uint256 i; i < operatorIds.length; ++i) { + uint64 operatorId = operatorIds[i]; + if (!removedBeforeMigration[operatorId]) continue; + + ISSVNetworkCore.Operator memory op = s.operators[operatorId]; + if (op.ethSnapshot.block != 0 || op.ethValidatorCount != 0) { + removedEthInitViolation = true; + } + if (seb.operatorEthVUnits[operatorId] != removedOperatorVUnitsBefore[i]) { + removedOperatorVUnitsMutationViolation = true; + } + } + + ssvRecord.exists = false; + unallocatedEth -= amount; + } catch {} + } + + /// @notice Forces the BUG-21 migration shape: explicit EB deviation, removed operator, then SSV->ETH migration. + function action_migrate_removed_operator_explicit_eb(uint256 seed) external payable { + if (msg.value != 0) { + unallocatedEth += msg.value; + } + if (!ssvRecord.exists || !ssvRecord.cluster.active) return; + + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + uint64 baselineVUnits = uint64(ssvRecord.cluster.validatorCount) * BPS_DENOMINATOR; + if (seb.clusterEB[ssvClusterId].vUnits <= baselineVUnits) { + ClusterEBSnapshot memory ebBefore = seb.clusterEB[ssvClusterId]; + if (uint64(block.number) < ebBefore.lastRootBlockNum + 1) return; + if ( + ebBefore.lastUpdateBlock != 0 && + uint64(block.number) < ebBefore.lastUpdateBlock + seb.minBlocksBetweenUpdates + ) { + return; + } + + uint64 blockNum = ebBefore.lastRootBlockNum + 1; + uint32 effectiveBalance = ssvRecord.cluster.validatorCount * 40; + bytes32 root = _singleLeafRoot(ssvClusterId, effectiveBalance); + _setCommittedRoot(seb, blockNum, root); + bytes32[] memory proof = new bytes32[](0); + + try clusterOwner.updateClusterBalance( + blockNum, ssvRecord.owner, operatorIds, ssvRecord.cluster, effectiveBalance, proof + ) { + // snapshot-only path; continue below + } catch { + ssvEbUpdateViolation = true; + return; + } + } + + uint64 operatorId = operatorIds[seed % operatorIds.length]; + if (!removedTracked[operatorId]) { + address ownerAddr = _operatorOwner(operatorId); + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory before = s.operators[operatorId]; + if (before.snapshot.block == 0 && before.ethSnapshot.block == 0) return; + + MigrationOperatorUser owner = _operatorUser(ownerAddr); + try owner.remove(operatorId) { + ISSVNetworkCore.Operator memory afterOp = s.operators[operatorId]; + if (afterOp.snapshot.block != 0 || afterOp.ethSnapshot.block != 0) { + removedStateViolation = true; + return; + } + + removedTracked[operatorId] = true; + removedBeforeMigration[operatorId] = true; + removedFrozenIndex[operatorId] = afterOp.snapshot.index; + } catch { + return; + } + } + + uint256 minRequired = _migrationMinRequired(ssvRecord.cluster, sp); + if (unallocatedEth <= minRequired) return; + + this.action_migrate_ssv_to_eth(seed); + } + + /// @notice Attempts SSV->ETH migration from an already-liquidated legacy cluster. + function action_migrate_liquidated_ssv_to_eth(uint256 seed) external { + if (!ssvRecord.exists || ssvRecord.cluster.active) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + ISSVNetworkCore.Cluster memory clusterBefore = ssvRecord.cluster; + + uint256 minRequired = _migrationMinRequired(clusterBefore, sp); + if (unallocatedEth <= minRequired) return; + + uint256 amount = seed % (unallocatedEth + 1); + if (amount <= minRequired) amount = minRequired + 1; + if (amount > unallocatedEth) return; + + uint256 ownerTokenBefore = token.balanceOf(ssvRecord.owner); + uint32 daoBefore = sp.daoValidatorCount; + uint32 ethDaoBefore = sp.ethDaoValidatorCount; + uint32 validatorsMigrated = clusterBefore.validatorCount; + + try clusterOwner.migrateToETH{value: amount}(operatorIds, clusterBefore) { + liquidatedMigrationObserved = true; + + uint256 actualRefund = token.balanceOf(ssvRecord.owner) - ownerTokenBefore; + if (actualRefund != 0) { + liquidatedMigrationViolation = true; + } + + if (sp.daoValidatorCount != daoBefore) { + liquidatedMigrationViolation = true; + } + if (sp.ethDaoValidatorCount != ethDaoBefore + validatorsMigrated) { + liquidatedMigrationViolation = true; + } + + if (s.clusters[ssvClusterId] != 0) { + liquidatedMigrationViolation = true; + } + if (s.ethClusters[ssvClusterId] == 0) { + liquidatedMigrationViolation = true; + } + + ssvRecord.exists = false; + unallocatedEth -= amount; + } catch {} + } + + /// @notice Ensures migration preconditions are reachable and immediately attempts migration. + function action_prepare_migration_and_attempt(uint256 seed) external payable { + if (!ssvRecord.exists) return; + if (msg.value != 0) { + unallocatedEth += msg.value; + } + + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint256 minRequired = _migrationMinRequired(ssvRecord.cluster, sp); + if (unallocatedEth <= minRequired) { + uint256 required = minRequired + 1 - unallocatedEth; + uint256 freeBalance = address(this).balance > unallocatedEth ? address(this).balance - unallocatedEth : 0; + if (required > freeBalance) return; + unallocatedEth += required; + } + + if (ssvRecord.cluster.active) { + this.action_migrate_ssv_to_eth(seed); + } else { + this.action_migrate_liquidated_ssv_to_eth(seed); + } + } + + function echidna_migration_removed_refund_exact() external view returns (bool) { + return !accountingViolation; + } + + function echidna_migration_removed_operator_not_eth_initialized() external view returns (bool) { + return !removedEthInitViolation; + } + + function echidna_migration_net_zero_validators() external view returns (bool) { + if (!migrationObserved) return true; + + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (sp.daoValidatorCount != daoValidatorCountBeforeMigration - migratedValidatorCount) return false; + if (sp.ethDaoValidatorCount != ethDaoValidatorCountBeforeMigration + migratedValidatorCount) return false; + if ( + uint256(sp.daoValidatorCount) + uint256(sp.ethDaoValidatorCount) != + uint256(daoValidatorCountBeforeMigration) + uint256(ethDaoValidatorCountBeforeMigration) + ) return false; + + return !migrationValidatorShiftViolation; + } + + function echidna_removed_operator_state_and_frozen_index_preserved() external view returns (bool) { + if (removedStateViolation) return false; + + StorageData storage s = SSVStorage.load(); + if (!_checkRemoved(op1, s)) return false; + if (!_checkRemoved(op2, s)) return false; + if (!_checkRemoved(op3, s)) return false; + return true; + } + + function echidna_liquidated_migration_branch_correct() external view returns (bool) { + if (!liquidatedMigrationObserved) return true; + return !liquidatedMigrationViolation; + } + + function echidna_removed_operator_vunits_unchanged_on_migration() external view returns (bool) { + return !removedOperatorVUnitsMutationViolation; + } + + function echidna_ssv_eb_update_only_snapshot() external view returns (bool) { + return !ssvEbUpdateViolation; + } + + function _checkRemoved(uint64 operatorId, StorageData storage s) internal view returns (bool) { + if (!removedTracked[operatorId]) return true; + + ISSVNetworkCore.Operator storage op = s.operators[operatorId]; + if (op.snapshot.block != 0 || op.ethSnapshot.block != 0) return false; + if (op.snapshot.index != removedFrozenIndex[operatorId]) return false; + return true; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 1000; + sp.ethNetworkFee = DEFAULT_NETWORK_ETH_FEE; + sp.networkFee = DEFAULT_NETWORK_SSV_FEE; + sp.ethNetworkFeeIndex = 0; + sp.networkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.networkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.daoIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = MIN_BLOCKS_BEFORE_LIQUIDATION; + sp.minimumBlocksBeforeLiquidationSSV = MIN_BLOCKS_BEFORE_LIQUIDATION; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + sp.minimumLiquidationCollateralSSV = PACKED_SSV_ZERO; + sp.operatorMaxFee = PackedETH.wrap(type(uint64).max); + sp.operatorMaxFeeSSV = type(uint64).max; + } + + function _initOperators() internal { + StorageData storage s = SSVStorage.load(); + op1 = _createOperator(s, address(opOwner1), bytes32(uint256(0x101))); + op2 = _createOperator(s, address(opOwner2), bytes32(uint256(0x102))); + op3 = _createOperator(s, address(opOwner3), bytes32(uint256(0x103))); + + operatorIds.push(op1); + operatorIds.push(op2); + operatorIds.push(op3); + } + + function _initActiveSSVCluster() internal { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + ssvClusterId = keccak256(abi.encodePacked(address(clusterOwner), operatorIds)); + token.mint(address(this), INITIAL_SSV_BALANCE); + + ISSVNetworkCore.Cluster memory cluster = ISSVNetworkCore.Cluster({ + validatorCount: INITIAL_VALIDATOR_COUNT, + networkFeeIndex: sp.currentNetworkFeeIndexSSV(), + index: _currentClusterIndexSsv(), + active: true, + balance: INITIAL_SSV_BALANCE + }); + + s.clusters[ssvClusterId] = cluster.hashClusterData(); + sp.updateDAOSSV(true, cluster.validatorCount); + + for (uint256 i; i < operatorIds.length; ++i) { + s.operators[operatorIds[i]].validatorCount += cluster.validatorCount; + } + + ssvRecord = SSVClusterRecord({ + cluster: cluster, + owner: address(clusterOwner), + exists: true + }); + } + + function _createOperator(StorageData storage s, address owner, bytes32 pk) internal returns (uint64) { + s.lastOperatorId.increment(); + uint64 id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: DEFAULT_OPERATOR_SSV_FEE, + owner: owner, + snapshot: ISSVNetworkCore.Snapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_SSV_ZERO + }), + whitelisted: false, + ethValidatorCount: 0, + ethFee: DEFAULT_OPERATOR_ETH_FEE, + ethSnapshot: ISSVNetworkCore.EthSnapshot({ + block: uint32(block.number), + index: 0, + balance: PACKED_ETH_ZERO + }) + }); + s.operatorsPKs[keccak256(abi.encodePacked(pk))] = id; + return id; + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + + function _currentClusterIndexSsv() internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + uint64 clusterIndex; + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[operatorIds[i]]; + uint64 index = op.snapshot.index; + if (op.snapshot.block != 0) { + index += uint64(uint32(block.number) - op.snapshot.block) * PackedSSV.unwrap(op.fee); + } + clusterIndex += index; + } + return clusterIndex; + } + + function _predictedMigrationBurnRateEth() internal view returns (uint64 burnRateETH) { + StorageData storage s = SSVStorage.load(); + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage operator = s.operators[operatorIds[i]]; + if (operator.snapshot.block == 0 && operator.ethSnapshot.block == 0) continue; + burnRateETH += PackedETH.unwrap(operator.ethFee); + } + } + + function _migrationMinRequired(ISSVNetworkCore.Cluster memory clusterBefore, StorageProtocol storage sp) + internal + view + returns (uint256 minRequired) + { + uint64 burnRateETH = _predictedMigrationBurnRateEth(); + uint64 vUnits = ClusterLib.getVUnits(ssvClusterId, clusterBefore.validatorCount); + uint256 thresholdUnits = ( + uint256(sp.minimumBlocksBeforeLiquidation) * + uint256(burnRateETH + PackedETH.unwrap(sp.ethNetworkFee)) * + uint256(vUnits) + ) / BPS_DENOMINATOR; + minRequired = thresholdUnits * ETH_DEDUCTED_DIGITS; + uint256 collateral = PackedETHLib.unpack(sp.minimumLiquidationCollateral); + if (collateral > minRequired) minRequired = collateral; + } + + function _settleSsvCluster() internal { + if (!ssvRecord.exists || !ssvRecord.cluster.active) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + uint64 clusterIndex = _currentClusterIndexSsv(); + uint64 networkFeeIndex = sp.currentNetworkFeeIndexSSV(); + + ISSVNetworkCore.Cluster memory cluster = ssvRecord.cluster; + cluster.updateBalanceSSV(clusterIndex, networkFeeIndex); + cluster.index = clusterIndex; + cluster.networkFeeIndex = networkFeeIndex; + ssvRecord.cluster = cluster; + s.clusters[ssvClusterId] = cluster.hashClusterData(); + } + + function _fastForwardSSV(uint32 blocks_) internal { + if (blocks_ == 0) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint32 currentBlock = uint32(block.number); + + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage operator = s.operators[operatorIds[i]]; + if (operator.snapshot.block == 0) continue; + + uint64 blockDiffFee = uint64(blocks_) * PackedSSV.unwrap(operator.fee); + operator.snapshot.index += blockDiffFee; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * operator.validatorCount)); + operator.snapshot.block = currentBlock; + } + + sp.networkFeeIndex += uint64(blocks_) * PackedSSV.unwrap(sp.networkFee); + sp.networkFeeIndexBlockNumber = currentBlock; + } + + function _operatorOwner(uint64 operatorId) internal view returns (address) { + if (operatorId == op1) return address(opOwner1); + if (operatorId == op2) return address(opOwner2); + if (operatorId == op3) return address(opOwner3); + return address(0); + } + + function _operatorUser(address owner) internal view returns (MigrationOperatorUser) { + if (owner == address(opOwner1)) return opOwner1; + if (owner == address(opOwner2)) return opOwner2; + return opOwner3; + } + + function _singleLeafRoot(bytes32 clusterId, uint32 effectiveBalance) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(keccak256(abi.encode(clusterId, effectiveBalance)))); + } + + function _setCommittedRoot(StorageEB storage seb, uint64 blockNum, bytes32 root) internal { + seb.ebRoots[blockNum] = root; + seb.latestCommittedBlock = blockNum; + } +} diff --git a/test/echidna/SSVOperatorFeeGovEchidna.sol b/test/echidna/SSVOperatorFeeGovEchidna.sol new file mode 100644 index 000000000..20d345ffd --- /dev/null +++ b/test/echidna/SSVOperatorFeeGovEchidna.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVOperators.sol"; +import "../../contracts/interfaces/ISSVOperators.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETHLib, PackedSSVLib, DEDUCTED_DIGITS} from "../../contracts/libraries/SSVPackedLib.sol"; +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract FeeGovUser { + ISSVOperators public operators; + + constructor(ISSVOperators operators_) { + operators = operators_; + } + + function declareFee(uint64 operatorId, uint256 fee) external { + operators.declareOperatorFee(operatorId, fee); + } + + function executeFee(uint64 operatorId) external { + operators.executeOperatorFee(operatorId); + } +} + +contract SSVOperatorFeeGovEchidna is SSVOperators(1) { + using Counters for Counters.Counter; + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; + + uint256 private constant DEFAULT_MIN_OPERATOR_ETH_FEE = 10_000_000; + + MockToken private token; + FeeGovUser private user1; + FeeGovUser private user2; + + uint64[] private operatorIds; + mapping(uint64 => address) private operatorOwner; + uint64 private lastOperatorId; + uint64 private constant MAX_OPERATORS = 4; + + bool private legacyDeclarationExecutable; + + constructor() { + token = new MockToken(); + _mockSetToken(address(token)); + _mockSetOperatorMaxFee(uint64(10 ether)); + _mockSetFeePeriods(10, 100); + _mockSetOperatorMaxFeeIncrease(10_000); + _initProtocolDefaults(); + + ISSVOperators self = ISSVOperators(address(this)); + user1 = new FeeGovUser(self); + user2 = new FeeGovUser(self); + } + + receive() external payable {} + + function action_register(uint256 pkSeed, uint256 feeSeed, uint8 userSeed) external { + if (operatorIds.length >= MAX_OPERATORS) return; + + FeeGovUser user = userSeed % 2 == 0 ? user1 : user2; + bytes memory publicKey = abi.encodePacked(pkSeed); + bytes32 hashedPk = keccak256(publicKey); + if (SSVStorage.load().operatorsPKs[hashedPk] != 0) return; + + uint256 fee = _boundFee(feeSeed); + + try ISSVOperators(address(this)).registerOperator(publicKey, fee, false) returns (uint64 id) { + operatorIds.push(id); + operatorOwner[id] = address(user); + lastOperatorId = id; + } catch {} + } + + function action_plant_and_execute_legacy(uint256 idSeed, uint256 feeSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory op = SSVStorage.load().operators[operatorId]; + if (op.ethSnapshot.block == 0 && op.snapshot.block == 0) return; + + uint256 fee = _boundFee(feeSeed); + PackedETH shrunkFee = PackedETHLib.pack(fee); + + SSVStorage.load().operatorFeeChangeRequests[operatorId] = ISSVNetworkCore.OperatorFeeChangeRequest({ + fee: PackedETH.unwrap(shrunkFee), + approvalBeginTime: uint64(UPGRADE_TIMESTAMP), + approvalEndTime: uint64(block.timestamp) + 10_000 + }); + + FeeGovUser owner = FeeGovUser(payable(ownerAddr)); + try owner.executeFee(operatorId) { + legacyDeclarationExecutable = true; + } catch {} + } + + function echidna_execute_rejects_legacy_declarations() external view returns (bool) { + return !legacyDeclarationExecutable; + } + + function _pickOperatorId(uint256 seed) internal view returns (uint64) { + uint256 count = operatorIds.length; + if (count == 0) return 0; + return operatorIds[seed % count]; + } + + function _boundFee(uint256 seed) internal view returns (uint256) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint256 maxFeeWei = PackedETHLib.unpack(sp.operatorMaxFee); + uint256 minFeeWei = PackedETHLib.unpack(sp.minimumOperatorEthFee); + if (maxFeeWei == 0) return 0; + uint256 fee = seed % (maxFeeWei + 1); + if (fee != 0 && fee < minFeeWei) fee = minFeeWei; + if (fee > maxFeeWei) fee = maxFeeWei; + return fee; + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + + function _mockSetOperatorMaxFee(uint64 fee) internal { + SSVStorageProtocol.load().operatorMaxFee = PackedETH.wrap(fee); + } + + function _mockSetFeePeriods(uint64 declarePeriod, uint64 executePeriod) internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.declareOperatorFeePeriod = declarePeriod; + sp.executeOperatorFeePeriod = executePeriod; + } + + function _mockSetOperatorMaxFeeIncrease(uint64 increase) internal { + SSVStorageProtocol.load().operatorMaxFeeIncrease = increase; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 3000; + sp.ethNetworkFee = PackedETH.wrap(1); + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.operatorMaxFeeSSV = type(uint64).max; + sp.minimumOperatorEthFee = PackedETHLib.pack(DEFAULT_MIN_OPERATOR_ETH_FEE); + } +} diff --git a/test/echidna/SSVOperatorsEchidna.sol b/test/echidna/SSVOperatorsEchidna.sol new file mode 100644 index 000000000..1741db980 --- /dev/null +++ b/test/echidna/SSVOperatorsEchidna.sol @@ -0,0 +1,1435 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVOperators.sol"; +import "../../contracts/interfaces/ISSVOperators.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {PackedETHLib, PackedSSVLib} from "../../contracts/libraries/SSVPackedLib.sol"; +import { + PackedETH, + PackedSSV, + DEDUCTED_DIGITS, + BPS_DENOMINATOR, + DEFAULT_OPERATOR_ETH_FEE +} from "../../contracts/libraries/SSVCoreTypes.sol"; + + +contract OperatorUser { + ISSVOperators public operators; + + constructor(ISSVOperators operators_) { + operators = operators_; + } + + receive() external payable {} + + function register(bytes calldata publicKey, uint256 fee, bool setPrivate) external returns (uint64) { + return operators.registerOperator(publicKey, fee, setPrivate); + } + + function remove(uint64 operatorId) external { + operators.removeOperator(operatorId); + } + + function declareFee(uint64 operatorId, uint256 fee) external { + operators.declareOperatorFee(operatorId, fee); + } + + function executeFee(uint64 operatorId) external { + operators.executeOperatorFee(operatorId); + } + + function cancelFee(uint64 operatorId) external { + operators.cancelDeclaredOperatorFee(operatorId); + } + + function reduceFee(uint64 operatorId, uint256 fee) external { + operators.reduceOperatorFee(operatorId, fee); + } + + function withdraw(uint64 operatorId, uint256 amount) external { + operators.withdrawOperatorEarnings(operatorId, amount); + } + + function withdrawAll(uint64 operatorId) external { + operators.withdrawAllOperatorEarnings(operatorId); + } + + function withdrawAllVersion(uint64 operatorId) external { + operators.withdrawAllVersionOperatorEarnings(operatorId); + } + + function withdrawSSV(uint64 operatorId, uint256 amount) external { + operators.withdrawOperatorEarningsSSV(operatorId, amount); + } + + function withdrawAllSSV(uint64 operatorId) external { + operators.withdrawAllOperatorEarningsSSV(operatorId); + } +} + +contract SSVOperatorsEchidna is SSVOperators(0) { + using PackedETHLib for PackedETH; + using PackedSSVLib for PackedSSV; + + uint256 private constant DEFAULT_MIN_OPERATOR_ETH_FEE = 10_000_000; + uint64 private constant MAX_OPERATORS = 8; + uint32 private constant MAX_ADVANCE_BLOCKS = 8; + uint64 private constant MAX_SSV_MINT_UNITS = 1_000_000; + + MockToken private token; + + OperatorUser private user1; + OperatorUser private user2; + OperatorUser private user3; + OperatorUser private attacker; + + uint64[] private operatorIds; + mapping(uint64 => bool) private operatorTracked; + mapping(uint64 => address) private operatorOwner; + mapping(uint64 => bytes32) private operatorPk; + mapping(bytes32 => uint64) private pkToId; + mapping(uint64 => PackedETH) private expectedEthBalance; + mapping(uint64 => PackedSSV) private expectedSsvBalance; + + uint64 private lastOperatorId; + + bool private duplicatePkAllowed; + bool private nonMonotonicId; + bool private invalidExecuteSucceeded; + bool private invalidExecuteFeeSucceeded; + bool private invalidReduceSucceeded; + bool private overWithdrawSucceeded; + bool private withdrawAllNotZero; + bool private withdrawConservationBroken; + bool private withdrawPayoutMismatch; + bool private unauthorizedActionSucceeded; + bool private removedStateDirty; + bool private removedOperatorOwnerViolation; + bool private removedOperatorEarningsViolation; + bool private removalPayoutMismatch; + bool private removalContractBalanceMismatch; + bool private declareChangedFee; + bool private nonMonotonicEarnings; + bool private feeLatencyMismatch; + bool private feeSettleBeforeChangeViolation; + bool private ethWithdrawTouchedSSV; + bool private ssvWithdrawTouchedEth; + bool private operatorRegisteredBelowMinFee; + bool private declareFromZeroSucceeded; + bool private ensureEthDefaultsViolation; + + constructor() { + token = new MockToken(); + _mockSetToken(address(token)); + _mockSetOperatorMaxFee(uint64(10 ether)); + _mockSetFeePeriods(1, 10); + _mockSetOperatorMaxFeeIncrease(10_000); + _initProtocolDefaults(); + + ISSVOperators self = ISSVOperators(address(this)); + user1 = new OperatorUser(self); + user2 = new OperatorUser(self); + user3 = new OperatorUser(self); + attacker = new OperatorUser(self); + } + + receive() external payable {} + + function action_fund(uint256 amount) external payable { + amount; + } + + function action_fund_ssv(uint256 seed) external { + uint64 units = uint64(seed % (uint256(MAX_SSV_MINT_UNITS) + 1)); + if (units == 0) return; + uint256 amount = uint256(units) * DEDUCTED_DIGITS; + token.mint(address(this), amount); + } + + function action_set_max_fee(uint256 seed) external { + uint64 minMax = _maxCurrentFeeRaw(); + uint64 newMax = uint64(seed % (uint256(type(uint64).max) + 1)); + if (newMax < minMax) { + newMax = minMax; + } + _mockSetOperatorMaxFee(newMax); + } + + function action_set_min_operator_eth_fee(uint256 seed) external { + uint64 maxFee = PackedETH.unwrap(SSVStorageProtocol.load().operatorMaxFee); + uint64 newMin = uint64(seed % (uint256(maxFee) + 1)); + _mockSetMinimumOperatorEthFee(newMin); + } + + function action_register( + uint256 pkSeed, + uint256 feeSeed, + uint8 userSeed, + bool setPrivate + ) external { + if (operatorIds.length >= MAX_OPERATORS) return; + + bytes memory publicKey = abi.encodePacked(pkSeed); + bytes32 hashedPk = keccak256(publicKey); + OperatorUser user = _pickUser(userSeed); + uint256 fee = _boundFee(feeSeed); + + if (pkToId[hashedPk] != 0) { + try user.register(publicKey, fee, setPrivate) returns (uint64 newId) { + duplicatePkAllowed = true; + _trackNewOperator(newId, hashedPk, address(user)); + // Check if operator was registered with fee below minimum + PackedETH minFee = SSVStorageProtocol.load().minimumOperatorEthFee; + ISSVNetworkCore.Operator memory op = getOperator(newId); + if (op.ethFee.neq(PACKED_ETH_ZERO) && op.ethFee.lt(minFee)) { + operatorRegisteredBelowMinFee = true; + } + } catch {} + return; + } + + try user.register(publicKey, fee, setPrivate) returns (uint64 newId) { + _trackNewOperator(newId, hashedPk, address(user)); + // Check if operator was registered with fee below minimum (should not happen) + PackedETH minFee = SSVStorageProtocol.load().minimumOperatorEthFee; + ISSVNetworkCore.Operator memory op = getOperator(newId); + if (op.ethFee.neq(PACKED_ETH_ZERO) && op.ethFee.lt(minFee)) { + operatorRegisteredBelowMinFee = true; + } + } catch {} + } + + function action_declare_fee(uint256 idSeed, uint256 feeSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory beforeOperator = getOperator(operatorId); + if (beforeOperator.ethSnapshot.block == 0) return; + PackedETH beforeFee = beforeOperator.ethFee; + OperatorUser owner = OperatorUser(payable(ownerAddr)); + + try owner.declareFee(operatorId, _boundFee(feeSeed)) { + PackedETH afterFee = getOperator(operatorId).ethFee; + if (afterFee.neq(beforeFee)) { + declareChangedFee = true; + } + } catch {} + } + + function action_declare_from_zero(uint256 idSeed, uint256 feeSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory op = getOperator(operatorId); + if (!_operatorExists(op)) return; + + if (op.ethFee.raw() != 0 || op.fee.raw() != 0) return; + + uint256 fee = _boundFee(feeSeed); + if (fee == 0) return; + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.declareFee(operatorId, fee) { + declareFromZeroSucceeded = true; + } catch {} + } + + function action_execute_fee(uint256 idSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.OperatorFeeChangeRequest memory request = getOperatorFeeChangeRequest(operatorId); + bool noRequest = request.approvalBeginTime == 0; + bool outsideWindow = + !noRequest && + (block.timestamp < request.approvalBeginTime || block.timestamp > request.approvalEndTime); + bool feeTooHigh = + !noRequest && PackedETH.wrap(request.fee).gt(SSVStorageProtocol.load().operatorMaxFee); + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.executeFee(operatorId) { + if (noRequest || outsideWindow) { + invalidExecuteSucceeded = true; + } + if (feeTooHigh) { + invalidExecuteFeeSucceeded = true; + } + } catch {} + } + + function action_reduce_fee(uint256 idSeed, uint256 feeSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator memory before = getOperator(operatorId); + if (!_operatorExists(before)) return; + if (before.ethSnapshot.block == 0) return; + + uint256 currentFee = PackedETHLib.unpack(before.ethFee); + uint256 newFee = _boundFeeBelow(currentFee, feeSeed); + OperatorUser owner = OperatorUser(payable(ownerAddr)); + + try owner.reduceFee(operatorId, newFee) { + ISSVNetworkCore.Operator memory operatorAfter = getOperator(operatorId); + if (PackedETHLib.unpack(operatorAfter.ethFee) >= currentFee) { + invalidReduceSucceeded = true; + } + PackedETH minFee = SSVStorageProtocol.load().minimumOperatorEthFee; + if (operatorAfter.ethFee.neq(PACKED_ETH_ZERO) && operatorAfter.ethFee.lt(minFee)) { + invalidReduceSucceeded = true; + } + if (getOperatorFeeChangeRequest(operatorId).approvalBeginTime != 0) { + invalidReduceSucceeded = true; + } + } catch {} + } + + function action_reduce_legacy_ensure_eth_defaults(uint256 idSeed) external { + if (PackedETHLib.unpack(SSVStorageProtocol.load().operatorMaxFee) < DEFAULT_OPERATOR_ETH_FEE) return; + + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!_operatorExists(operator)) return; + + _seedLegacySsvOperator(operatorId, PackedSSVLib.pack(DEDUCTED_DIGITS)); + + PackedSSV ssvBalanceBefore = operator.snapshot.balance; + uint32 expectedBlock = uint32(block.number); + OperatorUser owner = OperatorUser(payable(ownerAddr)); + + try owner.reduceFee(operatorId, 0) { + ISSVNetworkCore.Operator memory operatorAfter = getOperator(operatorId); + if (operatorAfter.ethSnapshot.block != expectedBlock) { + ensureEthDefaultsViolation = true; + } + if (operatorAfter.ethSnapshot.balance.neq(PACKED_ETH_ZERO)) { + ensureEthDefaultsViolation = true; + } + if (operatorAfter.ethFee.neq(PACKED_ETH_ZERO)) { + invalidReduceSucceeded = true; + } + if (operatorAfter.snapshot.balance.neq(ssvBalanceBefore)) { + ensureEthDefaultsViolation = true; + } + if (getOperatorFeeChangeRequest(operatorId).approvalBeginTime != 0) { + invalidReduceSucceeded = true; + } + _updateExpectedBalances(operatorId, operatorAfter.ethSnapshot.balance, operatorAfter.snapshot.balance); + } catch { + ensureEthDefaultsViolation = true; + } + } + + function action_set_ssv_fee(uint256 idSeed, uint256 feeSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + if (!_operatorExists(getOperator(operatorId))) return; + + uint256 fee = _boundFeeSSV(feeSeed); + SSVStorage.load().operators[operatorId].fee = PackedSSVLib.pack(fee); + } + + function action_seed_legacy_operator(uint256 idSeed, uint256 feeSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!_operatorExists(operator)) return; + if (operator.owner == address(0)) return; + + uint256 legacyFee = _boundFeeSSV(feeSeed); + if (legacyFee == 0) { + legacyFee = DEDUCTED_DIGITS; + } + _seedLegacySsvOperator(operatorId, PackedSSVLib.pack(legacyFee)); + } + + function action_trigger_ensure_eth_defaults(uint256 idSeed) external { + if (PackedETHLib.unpack(SSVStorageProtocol.load().operatorMaxFee) < DEFAULT_OPERATOR_ETH_FEE) return; + + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!_operatorExists(operator)) return; + + _seedLegacySsvOperator(operatorId, PackedSSVLib.pack(DEDUCTED_DIGITS)); + uint32 expectedBlock = uint32(block.number); + OperatorUser owner = OperatorUser(payable(ownerAddr)); + + try owner.declareFee(operatorId, 0) { + ISSVNetworkCore.Operator memory operatorAfter = getOperator(operatorId); + if (operatorAfter.ethSnapshot.block != expectedBlock) { + ensureEthDefaultsViolation = true; + } + if (operatorAfter.ethSnapshot.balance.neq(PACKED_ETH_ZERO)) { + ensureEthDefaultsViolation = true; + } + if (PackedETHLib.unpack(operatorAfter.ethFee) != DEFAULT_OPERATOR_ETH_FEE) { + ensureEthDefaultsViolation = true; + } + } catch { + ensureEthDefaultsViolation = true; + } + } + + function action_assign_validators(uint256 idSeed, uint256 deltaSeed, bool eth) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!_operatorExists(operator)) return; + + uint32 delta = uint32(deltaSeed % 64) + 1; + if (eth) { + if (operator.ethValidatorCount + delta > sp.validatorsPerOperatorLimit) return; + operator.ethValidatorCount += delta; + } else { + if (operator.validatorCount + delta > sp.validatorsPerOperatorLimit) return; + operator.validatorCount += delta; + } + } + + function action_advance_time(uint256 seed) external { + uint32 blocks = uint32(seed % MAX_ADVANCE_BLOCKS) + 1; + _fastForwardOperators(blocks); + + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethNetworkFeeIndex += uint64(blocks) * PackedETH.unwrap(sp.ethNetworkFee); + sp.networkFeeIndex += uint64(blocks) * PackedSSV.unwrap(sp.networkFee); + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.networkFeeIndexBlockNumber = uint32(block.number); + } + + function action_fee_change_latency(uint256 idSeed, uint256 feeSeed, uint256 blocksSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[operatorId]; + if (!_operatorExists(operator)) return; + + uint256 newFee = _boundFee(feeSeed); + if (newFee == PackedETH.unwrap(operator.ethFee)) return; + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.declareFee(operatorId, newFee) {} catch { + return; + } + + ISSVNetworkCore.OperatorFeeChangeRequest storage request = + SSVStorage.load().operatorFeeChangeRequests[operatorId]; + if (request.approvalBeginTime != 0) { + request.approvalBeginTime = uint64(block.timestamp); + request.approvalEndTime = uint64(block.timestamp) + 1; + } + + uint32 blocks = uint32(blocksSeed % MAX_ADVANCE_BLOCKS) + 1; + uint64 indexBefore = operator.ethSnapshot.index; + uint64 feeBefore = PackedETH.unwrap(operator.ethFee); + + _fastForwardSingle(operatorId, blocks); + uint64 indexAfterOld = operator.ethSnapshot.index; + if (indexAfterOld < indexBefore || indexAfterOld - indexBefore != uint64(blocks) * feeBefore) { + feeLatencyMismatch = true; + return; + } + + try owner.executeFee(operatorId) {} catch { + return; + } + + uint64 feeAfter = PackedETH.unwrap(operator.ethFee); + uint64 indexMid = operator.ethSnapshot.index; + + _fastForwardSingle(operatorId, blocks); + uint64 indexAfterNew = operator.ethSnapshot.index; + if (indexAfterNew < indexMid || indexAfterNew - indexMid != uint64(blocks) * feeAfter) { + feeLatencyMismatch = true; + } + } + + function action_execute_fee_settlement_order( + uint256 idSeed, + uint256 feeSeed, + uint256 blocksSeed, + uint256 deviationSeed + ) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!_operatorExists(operator)) return; + + uint32 currentBlock = uint32(block.number); + if (currentBlock <= 1) return; + + uint64 minFeeRaw = PackedETH.unwrap(sp.minimumOperatorEthFee); + uint64 maxFeeRaw = PackedETH.unwrap(sp.operatorMaxFee); + uint256 minFeeActual = PackedETHLib.unpack(sp.minimumOperatorEthFee); + uint256 maxFeeActual = PackedETHLib.unpack(sp.operatorMaxFee); + if (maxFeeRaw == 0) return; + + if (operator.ethSnapshot.block == 0) { + operator.ethSnapshot.block = currentBlock; + } + if (operator.ethFee.raw() == 0) { + uint64 fallbackFeeRaw = minFeeRaw == 0 ? 1 : minFeeRaw; + if (fallbackFeeRaw > maxFeeRaw) return; + operator.ethFee = PackedETH.wrap(fallbackFeeRaw); + } + if (operator.ethValidatorCount == 0) { + operator.ethValidatorCount = 1; + } + + uint64 baseline = uint64(operator.ethValidatorCount) * BPS_DENOMINATOR; + uint64 deviation = baseline == 0 ? 0 : uint64(deviationSeed % (uint256(baseline) + 1)); + seb.operatorEthVUnits[operatorId] = deviation; + + uint32 blocksElapsed = uint32(blocksSeed % uint256(currentBlock - 1)) + 1; + uint32 staleBlock = currentBlock - blocksElapsed; + if (staleBlock == 0) return; + operator.ethSnapshot.block = staleBlock; + + uint64 feeBeforeRaw = PackedETH.unwrap(operator.ethFee); + uint256 feeBeforeActual = PackedETHLib.unpack(operator.ethFee); + if (feeBeforeRaw == 0 || feeBeforeRaw > type(uint64).max / uint64(blocksElapsed)) return; + uint64 blockDiffFee = uint64(blocksElapsed) * feeBeforeRaw; + + uint64 indexBefore = operator.ethSnapshot.index; + if (indexBefore > type(uint64).max - blockDiffFee) return; + uint64 expectedIndexAfter = indexBefore + blockDiffFee; + + uint64 effectiveVUnits = _effectiveVUnitsForOperator(operatorId); + uint128 deltaUnits = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + uint64 balanceBeforeUnits = PackedETH.unwrap(operator.ethSnapshot.balance); + if (deltaUnits > type(uint64).max || balanceBeforeUnits > type(uint64).max - uint64(deltaUnits)) return; + uint64 expectedBalanceAfterUnits = balanceBeforeUnits + uint64(deltaUnits); + + uint256 maxAllowedFee = (feeBeforeActual * (BPS_DENOMINATOR + sp.operatorMaxFeeIncrease) + BPS_DENOMINATOR - 1) + / BPS_DENOMINATOR; + if (maxAllowedFee > maxFeeActual) { + maxAllowedFee = maxFeeActual; + } + + uint256 newFee; + if (maxAllowedFee > minFeeActual) { + newFee = minFeeActual + (feeSeed % (maxAllowedFee - minFeeActual + 1)); + } else { + newFee = 0; + } + if (newFee == feeBeforeActual) { + newFee = feeBeforeActual == 0 ? minFeeActual : 0; + } + if (newFee == feeBeforeActual) return; + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.declareFee(operatorId, newFee) {} catch { + return; + } + + ISSVNetworkCore.OperatorFeeChangeRequest storage request = s.operatorFeeChangeRequests[operatorId]; + if (request.approvalBeginTime == 0) return; + request.approvalBeginTime = uint64(block.timestamp); + request.approvalEndTime = uint64(block.timestamp) + 1; + + try owner.executeFee(operatorId) { + ISSVNetworkCore.Operator memory operatorAfter = getOperator(operatorId); + if (operatorAfter.ethSnapshot.index != expectedIndexAfter) { + feeSettleBeforeChangeViolation = true; + } + if (PackedETH.unwrap(operatorAfter.ethSnapshot.balance) != expectedBalanceAfterUnits) { + feeSettleBeforeChangeViolation = true; + } + if (PackedETHLib.unpack(operatorAfter.ethFee) != newFee) { + feeSettleBeforeChangeViolation = true; + } + if (getOperatorFeeChangeRequest(operatorId).approvalBeginTime != 0) { + feeSettleBeforeChangeViolation = true; + } + _updateExpectedBalances(operatorId, operatorAfter.ethSnapshot.balance, expectedSsvBalance[operatorId]); + } catch {} + } + + function action_reduce_fee_settlement_order( + uint256 idSeed, + uint256 feeSeed, + uint256 blocksSeed, + uint256 deviationSeed + ) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!_operatorExists(operator)) return; + + uint32 currentBlock = uint32(block.number); + if (currentBlock <= 1) return; + + uint64 minFeeRaw = PackedETH.unwrap(sp.minimumOperatorEthFee); + uint64 maxFeeRaw = PackedETH.unwrap(sp.operatorMaxFee); + uint256 minFeeActual = PackedETHLib.unpack(sp.minimumOperatorEthFee); + if (maxFeeRaw == 0) return; + + if (operator.ethSnapshot.block == 0) { + operator.ethSnapshot.block = currentBlock; + } + if (operator.ethFee.raw() == 0) { + uint64 fallbackFeeRaw = minFeeRaw == 0 ? 1 : minFeeRaw + 1; + if (fallbackFeeRaw > maxFeeRaw) fallbackFeeRaw = maxFeeRaw; + if (fallbackFeeRaw == 0) return; + operator.ethFee = PackedETH.wrap(fallbackFeeRaw); + } + if (operator.ethValidatorCount == 0) { + operator.ethValidatorCount = 1; + } + + uint64 baseline = uint64(operator.ethValidatorCount) * BPS_DENOMINATOR; + uint64 deviation = baseline == 0 ? 0 : uint64(deviationSeed % (uint256(baseline) + 1)); + seb.operatorEthVUnits[operatorId] = deviation; + + uint32 blocksElapsed = uint32(blocksSeed % uint256(currentBlock - 1)) + 1; + uint32 staleBlock = currentBlock - blocksElapsed; + if (staleBlock == 0) return; + operator.ethSnapshot.block = staleBlock; + + uint64 feeBeforeRaw = PackedETH.unwrap(operator.ethFee); + uint256 feeBeforeActual = PackedETHLib.unpack(operator.ethFee); + if (feeBeforeRaw == 0 || feeBeforeRaw > type(uint64).max / uint64(blocksElapsed)) return; + uint64 blockDiffFee = uint64(blocksElapsed) * feeBeforeRaw; + + uint64 indexBefore = operator.ethSnapshot.index; + if (indexBefore > type(uint64).max - blockDiffFee) return; + uint64 expectedIndexAfter = indexBefore + blockDiffFee; + + uint64 effectiveVUnits = _effectiveVUnitsForOperator(operatorId); + uint128 deltaUnits = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + uint64 balanceBeforeUnits = PackedETH.unwrap(operator.ethSnapshot.balance); + if (deltaUnits > type(uint64).max || balanceBeforeUnits > type(uint64).max - uint64(deltaUnits)) return; + uint64 expectedBalanceAfterUnits = balanceBeforeUnits + uint64(deltaUnits); + + uint256 newFee = _boundFeeBelow(feeBeforeActual, feeSeed); + if (newFee >= feeBeforeActual) { + newFee = feeBeforeActual <= minFeeActual ? 0 : minFeeActual; + } + if (newFee >= feeBeforeActual) return; + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.reduceFee(operatorId, newFee) { + ISSVNetworkCore.Operator memory operatorAfter = getOperator(operatorId); + if (operatorAfter.ethSnapshot.index != expectedIndexAfter) { + feeSettleBeforeChangeViolation = true; + } + if (PackedETH.unwrap(operatorAfter.ethSnapshot.balance) != expectedBalanceAfterUnits) { + feeSettleBeforeChangeViolation = true; + } + if (PackedETHLib.unpack(operatorAfter.ethFee) != newFee) { + feeSettleBeforeChangeViolation = true; + } + if (getOperatorFeeChangeRequest(operatorId).approvalBeginTime != 0) { + feeSettleBeforeChangeViolation = true; + } + _updateExpectedBalances(operatorId, operatorAfter.ethSnapshot.balance, expectedSsvBalance[operatorId]); + } catch {} + } + + function action_withdraw(uint256 idSeed, uint256 amountSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + ISSVNetworkCore.Operator memory before = getOperator(operatorId); + PackedETH balance = before.ethSnapshot.balance; + PackedSSV ssvBalanceBefore = before.snapshot.balance; + if (balance.eq(PACKED_ETH_ZERO)) return; + + uint64 withdrawShrunk = _boundWithdrawAmount(PackedETH.unwrap(balance), amountSeed); + uint256 withdrawAmount = PackedETHLib.unpack(PackedETH.wrap(withdrawShrunk)); + if (withdrawAmount > address(this).balance) return; + + uint256 ownerEthBefore = ownerAddr.balance; + uint256 contractEthBefore = address(this).balance; + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdraw(operatorId, withdrawAmount) { + PackedETH afterBalance = getOperator(operatorId).ethSnapshot.balance; + if (afterBalance.neq(balance.sub(PackedETH.wrap(withdrawShrunk)))) { + withdrawConservationBroken = true; + } + if (ownerAddr.balance != ownerEthBefore + withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (address(this).balance != contractEthBefore - withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (getOperator(operatorId).snapshot.balance.neq(ssvBalanceBefore)) { + ethWithdrawTouchedSSV = true; + } + _updateExpectedBalances(operatorId, afterBalance, expectedSsvBalance[operatorId]); + } catch {} + } + + function action_withdraw_all(uint256 idSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + ISSVNetworkCore.Operator memory before = getOperator(operatorId); + PackedETH balance = before.ethSnapshot.balance; + PackedSSV ssvBalanceBefore = before.snapshot.balance; + if (balance.eq(PACKED_ETH_ZERO)) return; + + uint256 withdrawAmount = PackedETHLib.unpack(balance); + if (withdrawAmount > address(this).balance) return; + + uint256 ownerEthBefore = ownerAddr.balance; + uint256 contractEthBefore = address(this).balance; + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdrawAll(operatorId) { + PackedETH afterBalance = getOperator(operatorId).ethSnapshot.balance; + if (afterBalance.neq(PACKED_ETH_ZERO)) { + withdrawAllNotZero = true; + } + if (ownerAddr.balance != ownerEthBefore + withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (address(this).balance != contractEthBefore - withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (getOperator(operatorId).snapshot.balance.neq(ssvBalanceBefore)) { + ethWithdrawTouchedSSV = true; + } + _updateExpectedBalances(operatorId, afterBalance, expectedSsvBalance[operatorId]); + } catch {} + } + + function action_withdraw_over(uint256 idSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + PackedETH balance = getOperator(operatorId).ethSnapshot.balance; + if (balance.eq(PackedETH.wrap(type(uint64).max))) return; + + PackedETH overBalance = balance.add(PackedETH.wrap(1)); + uint256 withdrawAmount = PackedETHLib.unpack(overBalance); + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdraw(operatorId, withdrawAmount) { + overWithdrawSucceeded = true; + } catch {} + } + + function action_withdraw_ssv(uint256 idSeed, uint256 amountSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + ISSVNetworkCore.Operator memory before = getOperator(operatorId); + PackedSSV balance = before.snapshot.balance; + PackedETH ethBalanceBefore = before.ethSnapshot.balance; + if (balance.eq(PACKED_SSV_ZERO)) return; + + uint64 withdrawShrunk = _boundWithdrawAmount(PackedSSV.unwrap(balance), amountSeed); + uint256 withdrawAmount = PackedSSVLib.unpack(PackedSSV.wrap(withdrawShrunk)); + if (withdrawAmount > token.balanceOf(address(this))) return; + + uint256 ownerBefore = token.balanceOf(ownerAddr); + uint256 contractBefore = token.balanceOf(address(this)); + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdrawSSV(operatorId, withdrawAmount) { + PackedSSV afterBalance = getOperator(operatorId).snapshot.balance; + if (afterBalance.neq(balance.sub(PackedSSV.wrap(withdrawShrunk)))) { + withdrawConservationBroken = true; + } + if (token.balanceOf(ownerAddr) != ownerBefore + withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (token.balanceOf(address(this)) != contractBefore - withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (getOperator(operatorId).ethSnapshot.balance.neq(ethBalanceBefore)) { + ssvWithdrawTouchedEth = true; + } + _updateExpectedBalances(operatorId, expectedEthBalance[operatorId], afterBalance); + } catch {} + } + + function action_withdraw_all_ssv(uint256 idSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + ISSVNetworkCore.Operator memory before = getOperator(operatorId); + PackedSSV balance = before.snapshot.balance; + PackedETH ethBalanceBefore = before.ethSnapshot.balance; + if (balance.eq(PACKED_SSV_ZERO)) return; + + uint256 withdrawAmount = PackedSSVLib.unpack(balance); + if (withdrawAmount > token.balanceOf(address(this))) return; + + uint256 ownerBefore = token.balanceOf(ownerAddr); + uint256 contractBefore = token.balanceOf(address(this)); + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdrawAllSSV(operatorId) { + PackedSSV afterBalance = getOperator(operatorId).snapshot.balance; + if (afterBalance.neq(PACKED_SSV_ZERO)) { + withdrawAllNotZero = true; + } + if (token.balanceOf(ownerAddr) != ownerBefore + withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (token.balanceOf(address(this)) != contractBefore - withdrawAmount) { + withdrawPayoutMismatch = true; + } + if (getOperator(operatorId).ethSnapshot.balance.neq(ethBalanceBefore)) { + ssvWithdrawTouchedEth = true; + } + _updateExpectedBalances(operatorId, expectedEthBalance[operatorId], afterBalance); + } catch {} + } + + function action_withdraw_all_version(uint256 idSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + ISSVNetworkCore.Operator memory before = getOperator(operatorId); + PackedETH ethBalance = before.ethSnapshot.balance; + PackedSSV ssvBalance = before.snapshot.balance; + if (ethBalance.eq(PACKED_ETH_ZERO) && ssvBalance.eq(PACKED_SSV_ZERO)) return; + if (!_hasPayoutFunds(ethBalance, ssvBalance)) return; + + uint256 ownerEthBefore = ownerAddr.balance; + uint256 ownerSsvBefore = token.balanceOf(ownerAddr); + uint256 contractEthBefore = address(this).balance; + uint256 contractSsvBefore = token.balanceOf(address(this)); + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdrawAllVersion(operatorId) { + ISSVNetworkCore.Operator memory afterOperator = getOperator(operatorId); + if (afterOperator.ethSnapshot.balance.neq(PACKED_ETH_ZERO)) { + withdrawAllNotZero = true; + } + if (afterOperator.snapshot.balance.neq(PACKED_SSV_ZERO)) { + withdrawAllNotZero = true; + } + + uint256 ethAmount = PackedETHLib.unpack(ethBalance); + uint256 ssvAmount = PackedSSVLib.unpack(ssvBalance); + if (ownerAddr.balance != ownerEthBefore + ethAmount) { + withdrawPayoutMismatch = true; + } + if (token.balanceOf(ownerAddr) != ownerSsvBefore + ssvAmount) { + withdrawPayoutMismatch = true; + } + if (address(this).balance != contractEthBefore - ethAmount) { + withdrawPayoutMismatch = true; + } + if (token.balanceOf(address(this)) != contractSsvBefore - ssvAmount) { + withdrawPayoutMismatch = true; + } + _updateExpectedBalances(operatorId, afterOperator.ethSnapshot.balance, afterOperator.snapshot.balance); + } catch {} + } + + function action_withdraw_over_ssv(uint256 idSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + PackedSSV balance = getOperator(operatorId).snapshot.balance; + if (balance.eq(PackedSSV.wrap(type(uint64).max))) return; + + PackedSSV overBalance = balance.add(PackedSSV.wrap(1)); + uint256 withdrawAmount = PackedSSVLib.unpack(overBalance); + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.withdrawSSV(operatorId, withdrawAmount) { + overWithdrawSucceeded = true; + } catch {} + } + + function action_remove(uint256 idSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + address ownerAddr = operatorOwner[operatorId]; + if (ownerAddr == address(0)) return; + + _syncToCurrentBlock(operatorId); + + ISSVNetworkCore.Operator memory before = getOperator(operatorId); + if (!_operatorExists(before)) return; + + PackedETH ethBalance = before.ethSnapshot.balance; + PackedSSV ssvBalance = before.snapshot.balance; + if (!_hasPayoutFunds(ethBalance, ssvBalance)) return; + + uint256 ownerEthBefore = ownerAddr.balance; + uint256 ownerSsvBefore = token.balanceOf(ownerAddr); + uint256 contractEthBefore = address(this).balance; + uint256 contractSsvBefore = token.balanceOf(address(this)); + + OperatorUser owner = OperatorUser(payable(ownerAddr)); + try owner.remove(operatorId) { + _checkRemovalState(operatorId, before); + _checkPayouts( + ownerAddr, + ethBalance, + ssvBalance, + ownerEthBefore, + ownerSsvBefore, + contractEthBefore, + contractSsvBefore + ); + _updateExpectedBalances(operatorId, PACKED_ETH_ZERO, PACKED_SSV_ZERO); + } catch { + removedStateDirty = true; + removedOperatorOwnerViolation = true; + removedOperatorEarningsViolation = true; + } + } + + function action_unauthorized(uint256 idSeed, uint8 actionSeed, uint256 amountSeed) external { + uint64 operatorId = _pickOperatorId(idSeed); + if (operatorId == 0) return; + if (operatorOwner[operatorId] == address(attacker)) return; + + uint8 choice = actionSeed % 6; + if (choice == 0) { + try attacker.remove(operatorId) { + unauthorizedActionSucceeded = true; + } catch {} + } else if (choice == 1) { + try attacker.declareFee(operatorId, _boundFee(amountSeed)) { + unauthorizedActionSucceeded = true; + } catch {} + } else if (choice == 2) { + try attacker.executeFee(operatorId) { + unauthorizedActionSucceeded = true; + } catch {} + } else if (choice == 3) { + try attacker.reduceFee(operatorId, _boundFee(amountSeed)) { + unauthorizedActionSucceeded = true; + } catch {} + } else if (choice == 4) { + uint64 balance = PackedETH.unwrap(getOperator(operatorId).ethSnapshot.balance); + uint256 withdrawAmount = PackedETHLib.unpack(PackedETH.wrap(_boundWithdrawAmount(balance == 0 ? 1 : balance, amountSeed))); + try attacker.withdraw(operatorId, withdrawAmount) { + unauthorizedActionSucceeded = true; + } catch {} + } else { + uint64 balance = PackedSSV.unwrap(getOperator(operatorId).snapshot.balance); + uint256 withdrawAmount = PackedSSVLib.unpack(PackedSSV.wrap(_boundWithdrawAmount(balance == 0 ? 1 : balance, amountSeed))); + try attacker.withdrawSSV(operatorId, withdrawAmount) { + unauthorizedActionSucceeded = true; + } catch {} + } + } + + function echidna_unique_active_pubkeys() external view returns (bool) { + if (duplicatePkAllowed) return false; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + uint64 id = operatorIds[i]; + ISSVNetworkCore.Operator memory op = getOperator(id); + if (!_operatorExists(op)) continue; + + bytes32 pk = operatorPk[id]; + if (pk == bytes32(0)) return false; + if (SSVStorage.load().operatorsPKs[pk] != id) return false; + + for (uint256 j = i + 1; j < count; ++j) { + uint64 otherId = operatorIds[j]; + ISSVNetworkCore.Operator memory other = getOperator(otherId); + if (!_operatorExists(other)) continue; + if (pk == operatorPk[otherId]) return false; + } + } + return true; + } + + function echidna_id_monotonic() external view returns (bool) { + return !nonMonotonicId; + } + + function echidna_registered_owners_non_zero() external view returns (bool) { + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + uint64 id = operatorIds[i]; + ISSVNetworkCore.Operator memory op = getOperator(id); + if (!_operatorExists(op)) continue; + if (op.owner == address(0)) return false; + } + return true; + } + + function echidna_eth_fee_within_max() external view returns (bool) { + PackedETH maxFee = SSVStorageProtocol.load().operatorMaxFee; + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + uint64 id = operatorIds[i]; + ISSVNetworkCore.Operator memory op = getOperator(id); + if (!_operatorExists(op)) continue; + if (op.ethFee.gt(maxFee)) return false; + } + return true; + } + + // Note: This invariant only checks that operators cannot be registered with a fee + // below the minimum at registration time. Existing operators are grandfathered + // when the DAO increases the minimum fee, so we track violation at registration. + function echidna_eth_fee_minimum() external view returns (bool) { + return !operatorRegisteredBelowMinFee; + } + + function echidna_declare_fee_from_zero_reverts() external view returns (bool) { + return !declareFromZeroSucceeded; + } + + function echidna_declare_does_not_change_fee() external view returns (bool) { + return !declareChangedFee; + } + + function echidna_execute_requires_valid_window() external view returns (bool) { + return !invalidExecuteSucceeded; + } + + function echidna_execute_rejects_invalid_fee() external view returns (bool) { + return !invalidExecuteFeeSucceeded; + } + + function echidna_reduce_fee_decreases() external view returns (bool) { + return !invalidReduceSucceeded; + } + + function echidna_withdraw_limit_enforced() external view returns (bool) { + return !overWithdrawSucceeded; + } + + function echidna_withdraw_all_clears_balance() external view returns (bool) { + return !withdrawAllNotZero; + } + + function echidna_withdraw_conserves_balance() external view returns (bool) { + return !withdrawConservationBroken && !withdrawPayoutMismatch; + } + + function echidna_earnings_monotonic() external view returns (bool) { + return !nonMonotonicEarnings; + } + + function echidna_fee_change_latency() external view returns (bool) { + return !feeLatencyMismatch; + } + + function echidna_fee_settle_before_change() external view returns (bool) { + return !feeSettleBeforeChangeViolation; + } + + function echidna_eth_withdraw_keeps_ssv() external view returns (bool) { + return !ethWithdrawTouchedSSV; + } + + function echidna_ssv_withdraw_keeps_eth() external view returns (bool) { + return !ssvWithdrawTouchedEth; + } + + function echidna_owner_only_actions() external view returns (bool) { + return !unauthorizedActionSucceeded; + } + + function echidna_remove_cleans_state() external view returns (bool) { + return !removedStateDirty; + } + + function echidna_remove_pays_out() external view returns (bool) { + return !removalPayoutMismatch && !removalContractBalanceMismatch; + } + + function echidna_removed_operator_owner_preserved() external view returns (bool) { + return !removedOperatorOwnerViolation; + } + + function echidna_removed_operator_earnings_withdrawable() external view returns (bool) { + return !removedOperatorEarningsViolation; + } + + function echidna_ensure_eth_defaults_correct() external view returns (bool) { + return !ensureEthDefaultsViolation; + } + + function _pickUser(uint8 seed) internal view returns (OperatorUser) { + uint8 idx = seed % 3; + if (idx == 0) return user1; + if (idx == 1) return user2; + return user3; + } + + function _pickOperatorId(uint256 seed) internal view returns (uint64) { + uint256 count = operatorIds.length; + if (count == 0) return 0; + return operatorIds[seed % count]; + } + + function _mockSetOperatorMaxFee(uint64 fee) internal { + SSVStorageProtocol.load().operatorMaxFee = PackedETH.wrap(fee); + } + + function _mockSetFeePeriods(uint64 declarePeriod, uint64 executePeriod) internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.declareOperatorFeePeriod = declarePeriod; + sp.executeOperatorFeePeriod = executePeriod; + } + + function _mockSetOperatorMaxFeeIncrease(uint64 increase) internal { + SSVStorageProtocol.load().operatorMaxFeeIncrease = increase; + } + + function _mockSetMinimumOperatorEthFee(uint64 fee) internal { + SSVStorageProtocol.load().minimumOperatorEthFee = PackedETH.wrap(fee); + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 3000; + sp.ethNetworkFee = PackedETH.wrap(1); + sp.networkFee = PackedSSV.wrap(1); + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.networkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.daoIndexBlockNumber = uint32(block.number); + sp.operatorMaxFeeSSV = type(uint64).max; + sp.minimumOperatorEthFee = PackedETHLib.pack(DEFAULT_MIN_OPERATOR_ETH_FEE); + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + + function getOperator(uint64 operatorId) internal view returns (ISSVNetworkCore.Operator memory) { + return SSVStorage.load().operators[operatorId]; + } + + function getOperatorFeeChangeRequest( + uint64 operatorId + ) internal view returns (ISSVNetworkCore.OperatorFeeChangeRequest memory) { + return SSVStorage.load().operatorFeeChangeRequests[operatorId]; + } + + function _trackNewOperator(uint64 operatorId, bytes32 hashedPk, address ownerAddr) internal { + if (operatorId == 0) return; + if (operatorId <= lastOperatorId) { + nonMonotonicId = true; + } + lastOperatorId = operatorId; + if (!operatorTracked[operatorId]) { + operatorTracked[operatorId] = true; + operatorIds.push(operatorId); + } + operatorOwner[operatorId] = ownerAddr; + operatorPk[operatorId] = hashedPk; + pkToId[hashedPk] = operatorId; + } + + function _operatorExists(ISSVNetworkCore.Operator memory operator) internal pure returns (bool) { + return operator.snapshot.block != 0 || operator.ethSnapshot.block != 0; + } + + function _boundFee(uint256 seed) internal view returns (uint256) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + // Unpack packed values to get actual fee amounts + uint256 maxFeeWei = PackedETHLib.unpack(sp.operatorMaxFee); + uint256 minFeeWei = PackedETHLib.unpack(sp.minimumOperatorEthFee); + + if (maxFeeWei == 0) return 0; + + uint256 units = seed % (maxFeeWei + 1); + uint256 fee = units; + + if (fee != 0 && fee < minFeeWei) { + fee = minFeeWei; + } + + if (fee > maxFeeWei) { + if (maxFeeWei < minFeeWei) return 0; + fee = maxFeeWei; + if (fee < minFeeWei) return 0; + } + + return fee; + } + + function _boundFeeSSV(uint256 seed) internal view returns (uint256) { + uint64 maxFee = SSVStorageProtocol.load().operatorMaxFeeSSV; + if (maxFee == 0) return 0; + + uint256 shrunkFee = seed % (uint256(maxFee) + 1); + return shrunkFee * DEDUCTED_DIGITS; + } + + function _boundFeeBelow(uint256 currentFee, uint256 seed) internal view returns (uint256) { + uint256 minFeeWei = PackedETHLib.unpack(SSVStorageProtocol.load().minimumOperatorEthFee); + if (currentFee == 0) return 0; + if (currentFee <= minFeeWei) return 0; + + uint256 range = currentFee - minFeeWei; + uint256 fee = minFeeWei + (seed % range); + + return fee; + } + + function _boundWithdrawAmount(uint64 balance, uint256 seed) internal pure returns (uint64) { + if (balance == 0) return 0; + return uint64(seed % balance) + 1; + } + + function _effectiveVUnitsForOperator(uint64 operatorId) internal view returns (uint64) { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + return seb.operatorEthVUnits[operatorId] + (uint64(operator.ethValidatorCount) * BPS_DENOMINATOR); + } + + function _fastForwardOperators(uint32 blocks) internal { + uint256 count = operatorIds.length; + for (uint256 i; i < count; ++i) { + _fastForwardSingle(operatorIds[i], blocks); + } + } + + function _syncToCurrentBlock(uint64 operatorId) internal { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (operator.ethSnapshot.block == 0 && operator.snapshot.block == 0) return; + + uint32 currentBlock = uint32(block.number); + + if (operator.ethSnapshot.block != 0 && operator.ethSnapshot.block < currentBlock) { + uint32 blockDiff = currentBlock - operator.ethSnapshot.block; + uint64 blockDiffFee = uint64(blockDiff) * PackedETH.unwrap(operator.ethFee); + + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (operator.ethValidatorCount * BPS_DENOMINATOR); + + operator.ethSnapshot.index += blockDiffFee; + if (effectiveVUnits != 0 && blockDiffFee != 0) { + uint128 delta = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(uint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + + if (operator.snapshot.block != 0 && operator.snapshot.block < currentBlock) { + uint32 blockDiff = currentBlock - operator.snapshot.block; + uint64 blockDiffFee = uint64(blockDiff) * PackedSSV.unwrap(operator.fee); + + operator.snapshot.index += blockDiffFee; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * operator.validatorCount)); + operator.snapshot.block = currentBlock; + } + + expectedEthBalance[operatorId] = operator.ethSnapshot.balance; + expectedSsvBalance[operatorId] = operator.snapshot.balance; + } + + function _fastForwardSingle(uint64 operatorId, uint32 blocks) internal { + StorageData storage s = SSVStorage.load(); + StorageEB storage seb = SSVStorageEB.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (operator.ethSnapshot.block == 0 && operator.snapshot.block == 0) return; + + uint32 currentBlock = uint32(block.number); + if (operator.ethSnapshot.block != 0) { + uint64 blockDiffFee = uint64(blocks) * PackedETH.unwrap(operator.ethFee); + // Deviation-only model: effectiveVUnits = baseline + storedDeviation + uint64 storedDeviation = seb.operatorEthVUnits[operatorId]; + uint64 effectiveVUnits = storedDeviation + (operator.ethValidatorCount * BPS_DENOMINATOR); + + operator.ethSnapshot.index += blockDiffFee; + if (effectiveVUnits != 0 && blockDiffFee != 0) { + uint128 delta = (uint128(blockDiffFee) * uint128(effectiveVUnits)) / BPS_DENOMINATOR; + operator.ethSnapshot.balance = operator.ethSnapshot.balance.add(PackedETH.wrap(uint64(delta))); + } + operator.ethSnapshot.block = currentBlock; + } + + if (operator.snapshot.block != 0) { + uint64 blockDiffFee = uint64(blocks) * PackedSSV.unwrap(operator.fee); + + operator.snapshot.index += blockDiffFee; + operator.snapshot.balance = operator.snapshot.balance.add(PackedSSV.wrap(blockDiffFee * operator.validatorCount)); + operator.snapshot.block = currentBlock; + } + + if (operator.ethSnapshot.balance.lt(expectedEthBalance[operatorId])) { + nonMonotonicEarnings = true; + } + if (operator.snapshot.balance.lt(expectedSsvBalance[operatorId])) { + nonMonotonicEarnings = true; + } + + expectedEthBalance[operatorId] = operator.ethSnapshot.balance; + expectedSsvBalance[operatorId] = operator.snapshot.balance; + } + + function _updateExpectedBalances(uint64 operatorId, PackedETH ethBalance, PackedSSV ssvBalance) internal { + expectedEthBalance[operatorId] = ethBalance; + expectedSsvBalance[operatorId] = ssvBalance; + } + + function _maxCurrentFeeRaw() internal view returns (uint64) { + uint256 count = operatorIds.length; + uint256 maxFee = 0; + for (uint256 i; i < count; ++i) { + uint64 id = operatorIds[i]; + ISSVNetworkCore.Operator memory op = getOperator(id); + if (!_operatorExists(op)) continue; + uint256 fee = PackedETHLib.unpack(op.ethFee); + if (fee > maxFee) { + maxFee = fee; + } + } + if (maxFee > type(uint64).max) { + return type(uint64).max; + } + return uint64(maxFee); + } + + function _hasPayoutFunds(PackedETH ethBalance, PackedSSV ssvBalance) internal view returns (bool) { + if (PackedETHLib.unpack(ethBalance) > address(this).balance) return false; + if (PackedSSVLib.unpack(ssvBalance) > token.balanceOf(address(this))) return false; + return true; + } + + function _checkRemovalState(uint64 operatorId, ISSVNetworkCore.Operator memory before) internal { + ISSVNetworkCore.Operator memory operatorAfter = getOperator(operatorId); + if (operatorAfter.owner != before.owner) { + removedStateDirty = true; + removedOperatorOwnerViolation = true; + } + if (operatorAfter.ethFee.neq(PACKED_ETH_ZERO)) removedStateDirty = true; + if (operatorAfter.ethSnapshot.balance.neq(PACKED_ETH_ZERO) || operatorAfter.ethSnapshot.block != 0) removedStateDirty = true; + if (operatorAfter.snapshot.balance.neq(PACKED_SSV_ZERO) || operatorAfter.snapshot.block != 0) removedStateDirty = true; + if (operatorAfter.validatorCount != 0 || operatorAfter.ethValidatorCount != 0) removedStateDirty = true; + } + + function _checkPayouts( + address ownerAddr, + PackedETH ethBalance, + PackedSSV ssvBalance, + uint256 ownerEthBefore, + uint256 ownerSsvBefore, + uint256 contractEthBefore, + uint256 contractSsvBefore + ) internal { + uint256 ethAmount = PackedETHLib.unpack(ethBalance); + uint256 ssvAmount = PackedSSVLib.unpack(ssvBalance); + + if (ethAmount > 0) { + if (ownerAddr.balance != ownerEthBefore + ethAmount) { + removalPayoutMismatch = true; + removedOperatorEarningsViolation = true; + } + if (address(this).balance != contractEthBefore - ethAmount) { + removalContractBalanceMismatch = true; + removedOperatorEarningsViolation = true; + } + } + + if (ssvAmount > 0) { + if (token.balanceOf(ownerAddr) != ownerSsvBefore + ssvAmount) { + removalPayoutMismatch = true; + removedOperatorEarningsViolation = true; + } + if (token.balanceOf(address(this)) != contractSsvBefore - ssvAmount) { + removalContractBalanceMismatch = true; + removedOperatorEarningsViolation = true; + } + } + } + + function _seedLegacySsvOperator(uint64 operatorId, PackedSSV legacyFee) internal { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!_operatorExists(operator)) return; + + if (operator.snapshot.block == 0) { + operator.snapshot.block = uint32(block.number); + } + if (legacyFee.eq(PACKED_SSV_ZERO)) { + legacyFee = PackedSSVLib.pack(DEDUCTED_DIGITS); + } + + operator.fee = legacyFee; + operator.ethFee = PACKED_ETH_ZERO; + operator.ethSnapshot.block = 0; + operator.ethSnapshot.balance = PACKED_ETH_ZERO; + + _updateExpectedBalances(operatorId, PACKED_ETH_ZERO, operator.snapshot.balance); + } +} diff --git a/test/echidna/SSVRemovedOperatorETHFlowsEchidna.sol b/test/echidna/SSVRemovedOperatorETHFlowsEchidna.sol new file mode 100644 index 000000000..246dd99da --- /dev/null +++ b/test/echidna/SSVRemovedOperatorETHFlowsEchidna.sol @@ -0,0 +1,622 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/libraries/ValidatorLib.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageEB.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/modules/SSVClusters.sol"; +import "../../contracts/modules/SSVOperators.sol"; +import "../../contracts/modules/SSVValidators.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract SSVRemovedOperatorETHFlowsEchidna is SSVClusters, SSVOperators(0), SSVValidators { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using ProtocolLib for StorageProtocol; + + PackedETH private constant DEFAULT_OPERATOR_ETH_FEE = PACKED_ETH_ZERO; + uint32 private constant EXPLICIT_EFFECTIVE_BALANCE = 64; + uint32 private constant BASELINE_EFFECTIVE_BALANCE = 32; + + uint64 private op1; + uint64 private op2; + uint64 private op3; + uint64 private op4; + uint64 private removedOperatorId; + uint64 private removedOperatorId2; + + bool private initialized; + bool private operatorRemoved; + bool private secondOperatorRemoved; + bool private updateDecreaseViolation; + bool private updateIncreaseViolation; + bool private liquidationViolation; + bool private finalRemovalViolation; + bool private finalBulkRemovalViolation; + + bytes private validatorPublicKey; + bytes private validatorShares; + bytes32 private clusterId; + ISSVNetworkCore.Cluster private clusterModel; + + constructor() { + _initProtocolDefaults(); + _initOperators(); + + uint64[] memory operatorIds = _operatorIds(); + clusterId = keccak256(abi.encodePacked(address(this), operatorIds)); + removedOperatorId = op1; + removedOperatorId2 = op2; + validatorPublicKey = abi.encodePacked(bytes32(uint256(0x1111)), bytes16(uint128(0x2222))); + validatorShares = hex"01"; + clusterModel = _emptyCluster(); + } + + receive() external payable {} + + function action_removed_operator_eb_decrease(uint256 seed) external { + // Derive starting EB from seed (range: EXPLICIT_EFFECTIVE_BALANCE+1..2047) to + // stress-test large deviation deltas covering EC-03 / R-11 ranges. + uint32 startEB = EXPLICIT_EFFECTIVE_BALANCE + 1 + uint32(seed % (2047 - EXPLICIT_EFFECTIVE_BALANCE)); + if (initialized && (!clusterModel.active || clusterModel.validatorCount == 0)) return; + if (!_prepareRemovedOperatorExplicitCluster()) { + updateDecreaseViolation = true; + return; + } + if (!clusterModel.active || clusterModel.validatorCount == 0) { + updateDecreaseViolation = true; + return; + } + + StorageEB storage seb = SSVStorageEB.load(); + ClusterEBSnapshot storage ebSnapshot = seb.clusterEB[clusterId]; + uint64 startVUnits = ClusterLib.ebToVUnits(startEB); + if (ebSnapshot.vUnits < startVUnits) { + uint64 prePush_vUnits = ebSnapshot.vUnits > 0 + ? ebSnapshot.vUnits + : uint64(clusterModel.validatorCount) * BPS_DENOMINATOR; + uint64 bn = ebSnapshot.lastRootBlockNum + 1; + _setCommittedRoot(seb, bn, _singleLeafRoot(clusterId, startEB)); + bytes32[] memory p = new bytes32[](0); + uint64[] memory ids = _operatorIds(); + try this.updateClusterBalance(bn, address(this), ids, clusterModel, startEB, p) { + _refreshClusterModel(prePush_vUnits); + } catch { + updateDecreaseViolation = true; + return; + } + } + uint64 baselineVUnits = uint64(clusterModel.validatorCount) * BPS_DENOMINATOR; + if (ebSnapshot.vUnits <= baselineVUnits) { + updateDecreaseViolation = true; + return; + } + + uint64 preDecrease_vUnits = ebSnapshot.vUnits; + uint64 blockNum = ebSnapshot.lastRootBlockNum + 1; + _setCommittedRoot(seb, blockNum, _singleLeafRoot(clusterId, BASELINE_EFFECTIVE_BALANCE)); + bytes32[] memory proof = new bytes32[](0); + uint64[] memory operatorIds = _operatorIds(); + + try this.updateClusterBalance(blockNum, address(this), operatorIds, clusterModel, BASELINE_EFFECTIVE_BALANCE, proof) { + _refreshClusterModel(preDecrease_vUnits); + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (SSVStorage.load().ethClusters[clusterId] != clusterModel.hashClusterData()) { + updateDecreaseViolation = true; + } + if (seb.clusterEB[clusterId].vUnits != baselineVUnits) { + updateDecreaseViolation = true; + } + if (sp.daoTotalEthVUnits != baselineVUnits) { + updateDecreaseViolation = true; + } + if (!_checkRemovedOperatorState()) { + updateDecreaseViolation = true; + } + for (uint256 i; i < operatorIds.length; ++i) { + if (seb.operatorEthVUnits[operatorIds[i]] != 0) { + updateDecreaseViolation = true; + } + } + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[operatorIds[i]]; + if (_isRemovedOperator(operatorIds[i])) { + if (operator.ethValidatorCount != 0) { + updateDecreaseViolation = true; + } + } else if (operator.ethValidatorCount != 1) { + updateDecreaseViolation = true; + } + } + } catch { + updateDecreaseViolation = true; + } + } + + function action_remove_second_operator() external { + if (secondOperatorRemoved) return; + if (initialized && (!clusterModel.active || clusterModel.validatorCount == 0)) return; + if (!_prepareRemovedOperatorExplicitCluster()) return; + if (!clusterModel.active || clusterModel.validatorCount == 0) return; + + StorageEB storage seb = SSVStorageEB.load(); + StorageProtocol storage spBefore = SSVStorageProtocol.load(); + uint64 daoTotalBefore = spBefore.daoTotalEthVUnits; + uint64 clusterVUnitsBefore = seb.clusterEB[clusterId].vUnits; + uint64[] memory operatorIds = _operatorIds(); + uint64[] memory deviationsBefore = new uint64[](operatorIds.length); + for (uint256 i; i < operatorIds.length; ++i) { + deviationsBefore[i] = seb.operatorEthVUnits[operatorIds[i]]; + } + + try this.removeOperator(removedOperatorId2) { + StorageData storage s = SSVStorage.load(); + StorageEB storage sebLocal = SSVStorageEB.load(); + StorageProtocol storage spAfter = SSVStorageProtocol.load(); + if (s.ethClusters[clusterId] != clusterModel.hashClusterData()) return; + if (!_checkRemovedOperatorState()) return; + if (!_checkSingleOperatorRemoved(s, removedOperatorId2)) return; + if (spAfter.daoTotalEthVUnits != daoTotalBefore) return; + if (sebLocal.clusterEB[clusterId].vUnits != clusterVUnitsBefore) return; + if (sebLocal.operatorEthVUnits[removedOperatorId2] != 0) return; + for (uint256 i; i < operatorIds.length; ++i) { + uint64 operatorId = operatorIds[i]; + if (operatorId == removedOperatorId || operatorId == removedOperatorId2) continue; + if (sebLocal.operatorEthVUnits[operatorId] != deviationsBefore[i]) return; + if (s.operators[operatorId].ethValidatorCount != 1) return; + } + secondOperatorRemoved = true; + } catch {} + } + + function action_removed_operator_eb_increase(uint256 seed) external { + // Derive target EB from seed (range: EXPLICIT_EFFECTIVE_BALANCE+1..2047) to cover + // large-deviation increases including EC-03 / R-11 ranges. + uint32 targetEB = EXPLICIT_EFFECTIVE_BALANCE + 1 + uint32(seed % (2047 - EXPLICIT_EFFECTIVE_BALANCE)); + if (initialized && (!clusterModel.active || clusterModel.validatorCount == 0)) return; + if (!_prepareRemovedOperatorExplicitCluster()) { + updateIncreaseViolation = true; + return; + } + if (!clusterModel.active || clusterModel.validatorCount == 0) { + updateIncreaseViolation = true; + return; + } + + StorageEB storage seb = SSVStorageEB.load(); + uint64 expectedVUnits = ClusterLib.ebToVUnits(targetEB); + if (seb.clusterEB[clusterId].vUnits == expectedVUnits) return; + + uint64 preIncrease_vUnits = seb.clusterEB[clusterId].vUnits > 0 + ? seb.clusterEB[clusterId].vUnits + : uint64(clusterModel.validatorCount) * BPS_DENOMINATOR; + uint64 blockNum = seb.clusterEB[clusterId].lastRootBlockNum + 1; + _setCommittedRoot(seb, blockNum, _singleLeafRoot(clusterId, targetEB)); + bytes32[] memory proof = new bytes32[](0); + uint64[] memory operatorIds = _operatorIds(); + uint64 expectedDeviation = expectedVUnits - BPS_DENOMINATOR; + + try this.updateClusterBalance(blockNum, address(this), operatorIds, clusterModel, targetEB, proof) { + _refreshClusterModel(preIncrease_vUnits); + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (SSVStorage.load().ethClusters[clusterId] != clusterModel.hashClusterData()) { + updateIncreaseViolation = true; + } + if (seb.clusterEB[clusterId].vUnits != expectedVUnits) { + updateIncreaseViolation = true; + } + if (sp.daoTotalEthVUnits != expectedVUnits) { + updateIncreaseViolation = true; + } + if (!_checkRemovedOperatorState()) { + updateIncreaseViolation = true; + } + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[operatorIds[i]]; + if (_isRemovedOperator(operatorIds[i])) { + if (seb.operatorEthVUnits[operatorIds[i]] != 0 || operator.ethValidatorCount != 0) { + updateIncreaseViolation = true; + } + } else { + if (seb.operatorEthVUnits[operatorIds[i]] != expectedDeviation || operator.ethValidatorCount != 1) { + updateIncreaseViolation = true; + } + } + } + } catch { + updateIncreaseViolation = true; + } + } + + function action_removed_operator_liquidation(uint256 seed) external { + seed; + if (initialized && (!clusterModel.active || clusterModel.validatorCount == 0)) return; + if (!_prepareRemovedOperatorExplicitCluster()) { + liquidationViolation = true; + return; + } + if (!clusterModel.active || clusterModel.validatorCount == 0) { + liquidationViolation = true; + return; + } + + uint64[] memory operatorIds = _operatorIds(); + try this.liquidate(address(this), operatorIds, clusterModel) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + clusterModel.active = false; + clusterModel.balance = 0; + clusterModel.index = 0; + clusterModel.networkFeeIndex = 0; + + if (s.ethClusters[clusterId] != clusterModel.hashClusterData()) { + liquidationViolation = true; + } + if (sp.daoTotalEthVUnits != 0) { + liquidationViolation = true; + } + if (!_checkRemovedOperatorState()) { + liquidationViolation = true; + } + for (uint256 i; i < operatorIds.length; ++i) { + if (seb.operatorEthVUnits[operatorIds[i]] != 0) { + liquidationViolation = true; + } + if (s.operators[operatorIds[i]].ethValidatorCount != 0) { + liquidationViolation = true; + } + } + } catch { + liquidationViolation = true; + } + } + + function action_removed_operator_final_removal(uint256 seed) external { + seed; + if (initialized && (!clusterModel.active || clusterModel.validatorCount != 1)) return; + if (!_prepareRemovedOperatorExplicitCluster()) { + finalRemovalViolation = true; + return; + } + if (!clusterModel.active || clusterModel.validatorCount != 1) { + finalRemovalViolation = true; + return; + } + + uint64[] memory operatorIds = _operatorIds(); + try this.removeValidator(validatorPublicKey, operatorIds, clusterModel) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + clusterModel.validatorCount = 0; + + if (s.ethClusters[clusterId] != clusterModel.hashClusterData()) { + finalRemovalViolation = true; + } + if (seb.clusterEB[clusterId].vUnits != 0) { + finalRemovalViolation = true; + } + if (sp.daoTotalEthVUnits != 0) { + finalRemovalViolation = true; + } + if (s.validatorPKs[keccak256(abi.encodePacked(validatorPublicKey, address(this)))] != bytes32(0)) { + finalRemovalViolation = true; + } + if (!_checkRemovedOperatorState()) { + finalRemovalViolation = true; + } + for (uint256 i; i < operatorIds.length; ++i) { + if (seb.operatorEthVUnits[operatorIds[i]] != 0) { + finalRemovalViolation = true; + } + if (s.operators[operatorIds[i]].ethValidatorCount != 0) { + finalRemovalViolation = true; + } + } + } catch { + finalRemovalViolation = true; + } + } + + function action_removed_operator_final_bulk_removal(uint256 seed) external { + seed; + if (initialized && (!clusterModel.active || clusterModel.validatorCount != 1)) return; + if (!_prepareRemovedOperatorExplicitCluster()) { + finalBulkRemovalViolation = true; + return; + } + if (!clusterModel.active || clusterModel.validatorCount != 1) { + finalBulkRemovalViolation = true; + return; + } + + uint64[] memory operatorIds = _operatorIds(); + bytes[] memory publicKeys = new bytes[](1); + publicKeys[0] = validatorPublicKey; + + try this.bulkRemoveValidator(publicKeys, operatorIds, clusterModel) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StorageEB storage seb = SSVStorageEB.load(); + + clusterModel.validatorCount = 0; + + if (s.ethClusters[clusterId] != clusterModel.hashClusterData()) { + finalBulkRemovalViolation = true; + } + if (seb.clusterEB[clusterId].vUnits != 0) { + finalBulkRemovalViolation = true; + } + if (sp.daoTotalEthVUnits != 0) { + finalBulkRemovalViolation = true; + } + if (s.validatorPKs[keccak256(abi.encodePacked(validatorPublicKey, address(this)))] != bytes32(0)) { + finalBulkRemovalViolation = true; + } + if (!_checkRemovedOperatorState()) { + finalBulkRemovalViolation = true; + } + for (uint256 i; i < operatorIds.length; ++i) { + if (seb.operatorEthVUnits[operatorIds[i]] != 0) { + finalBulkRemovalViolation = true; + } + if (s.operators[operatorIds[i]].ethValidatorCount != 0) { + finalBulkRemovalViolation = true; + } + } + } catch { + finalBulkRemovalViolation = true; + } + } + + function echidna_removed_operator_eb_decrease_safe() external view returns (bool) { + return !updateDecreaseViolation; + } + + function echidna_removed_operator_eb_increase_safe() external view returns (bool) { + return !updateIncreaseViolation; + } + + function echidna_removed_operator_liquidation_safe() external view returns (bool) { + return !liquidationViolation; + } + + function echidna_removed_operator_final_removal_safe() external view returns (bool) { + return !finalRemovalViolation; + } + + function echidna_removed_operator_final_bulk_removal_safe() external view returns (bool) { + return !finalBulkRemovalViolation; + } + + function _prepareRemovedOperatorExplicitCluster() internal returns (bool) { + if (!initialized) { + uint64[] memory operatorIds = _operatorIds(); + try this.registerValidator(validatorPublicKey, operatorIds, validatorShares, _emptyCluster()) { + initialized = true; + clusterModel.validatorCount = 1; + clusterModel.active = true; + + if (SSVStorage.load().ethClusters[clusterId] != clusterModel.hashClusterData()) { + return false; + } + } catch { + return false; + } + } + + if (!clusterModel.active || clusterModel.validatorCount == 0) { + return false; + } + + StorageEB storage seb = SSVStorageEB.load(); + uint64 explicitVUnits = ClusterLib.ebToVUnits(EXPLICIT_EFFECTIVE_BALANCE); + if (seb.clusterEB[clusterId].vUnits < explicitVUnits) { + uint64 preUpd_vUnits = seb.clusterEB[clusterId].vUnits > 0 + ? seb.clusterEB[clusterId].vUnits + : uint64(clusterModel.validatorCount) * BPS_DENOMINATOR; + uint64 blockNum = seb.clusterEB[clusterId].lastRootBlockNum + 1; + _setCommittedRoot(seb, blockNum, _singleLeafRoot(clusterId, EXPLICIT_EFFECTIVE_BALANCE)); + bytes32[] memory proof = new bytes32[](0); + uint64[] memory operatorIds = _operatorIds(); + + try this.updateClusterBalance(blockNum, address(this), operatorIds, clusterModel, EXPLICIT_EFFECTIVE_BALANCE, proof) { + _refreshClusterModel(preUpd_vUnits); + StorageProtocol storage sp = SSVStorageProtocol.load(); + if (SSVStorage.load().ethClusters[clusterId] != clusterModel.hashClusterData()) { + return false; + } + if (seb.clusterEB[clusterId].vUnits != explicitVUnits) { + return false; + } + if (sp.daoTotalEthVUnits != explicitVUnits) { + return false; + } + for (uint256 i; i < operatorIds.length; ++i) { + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[operatorIds[i]]; + if (_isRemovedOperator(operatorIds[i])) { + if (seb.operatorEthVUnits[operatorIds[i]] != 0) { + return false; + } + if (operator.ethValidatorCount != 0) { + return false; + } + } else { + if (seb.operatorEthVUnits[operatorIds[i]] != explicitVUnits - BPS_DENOMINATOR) { + return false; + } + if (operator.ethValidatorCount != 1) { + return false; + } + } + } + } catch { + return false; + } + } + + uint64[] memory operatorIds = _operatorIds(); + if (!operatorRemoved) { + StorageProtocol storage spBefore = SSVStorageProtocol.load(); + uint64 daoTotalBefore = spBefore.daoTotalEthVUnits; + uint64 clusterVUnitsBefore = seb.clusterEB[clusterId].vUnits; + uint64[] memory deviationsBefore = new uint64[](operatorIds.length); + for (uint256 i; i < operatorIds.length; ++i) { + deviationsBefore[i] = seb.operatorEthVUnits[operatorIds[i]]; + } + try this.removeOperator(removedOperatorId) { + StorageData storage s = SSVStorage.load(); + StorageEB storage sebLocal = SSVStorageEB.load(); + StorageProtocol storage spAfter = SSVStorageProtocol.load(); + if (s.ethClusters[clusterId] != clusterModel.hashClusterData()) { + return false; + } + if (!_checkRemovedOperatorState()) { + return false; + } + if (spAfter.daoTotalEthVUnits != daoTotalBefore) { + return false; + } + if (sebLocal.clusterEB[clusterId].vUnits != clusterVUnitsBefore) { + return false; + } + if (sebLocal.operatorEthVUnits[removedOperatorId] != 0) { + return false; + } + for (uint256 i; i < operatorIds.length; ++i) { + uint64 operatorId = operatorIds[i]; + if (operatorId == removedOperatorId) continue; + if (sebLocal.operatorEthVUnits[operatorId] != deviationsBefore[i]) { + return false; + } + if (s.operators[operatorId].ethValidatorCount != 1) { + return false; + } + } + operatorRemoved = true; + } catch { + return false; + } + } + + return _checkRemovedOperatorState(); + } + + function _checkRemovedOperatorState() internal view returns (bool) { + StorageData storage s = SSVStorage.load(); + if (!_checkSingleOperatorRemoved(s, removedOperatorId)) return false; + if (secondOperatorRemoved && !_checkSingleOperatorRemoved(s, removedOperatorId2)) return false; + return true; + } + + function _checkSingleOperatorRemoved(StorageData storage s, uint64 operatorId) internal view returns (bool) { + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + return operator.ethSnapshot.block == 0 && operator.snapshot.block == 0 + && operator.ethValidatorCount == 0 + && PackedETH.unwrap(operator.ethFee) == 0 && operator.owner == address(this); + } + + function _isRemovedOperator(uint64 opId) internal view returns (bool) { + return (opId == removedOperatorId && operatorRemoved) + || (opId == removedOperatorId2 && secondOperatorRemoved); + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 1000; + sp.ethNetworkFee = PACKED_ETH_ZERO; + sp.ethNetworkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.ethDaoIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = 1; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + + SSVStorageEB.load().minBlocksBetweenUpdates = 0; + } + + function _initOperators() internal { + StorageData storage s = SSVStorage.load(); + op1 = _createOperator(s, bytes32(uint256(0x101))); + op2 = _createOperator(s, bytes32(uint256(0x102))); + op3 = _createOperator(s, bytes32(uint256(0x103))); + op4 = _createOperator(s, bytes32(uint256(0x104))); + } + + function _createOperator(StorageData storage s, bytes32 pk) internal returns (uint64) { + s.lastOperatorId.increment(); + uint64 id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PACKED_SSV_ZERO, + owner: address(this), + snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: PACKED_SSV_ZERO}), + whitelisted: false, + ethValidatorCount: 0, + ethFee: DEFAULT_OPERATOR_ETH_FEE, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: uint32(block.number), index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(pk))] = id; + return id; + } + + function _operatorIds() internal view returns (uint64[] memory operatorIds) { + operatorIds = new uint64[](4); + operatorIds[0] = op1; + operatorIds[1] = op2; + operatorIds[2] = op3; + operatorIds[3] = op4; + } + + function _refreshClusterModel(uint64 oldVUnits) internal { + uint64[] memory ids = _operatorIds(); + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + // Compute the cluster index the same way updateClusterBalance does: + // sum of each active operator's current eth fee index (accrued to block.number). + // Works whether or not updateClusterOperators settled operator snapshots. + uint64 newClusterIndex = 0; + for (uint256 i; i < ids.length; ++i) { + ISSVNetworkCore.Operator storage op = s.operators[ids[i]]; + if (op.ethSnapshot.block == 0) continue; + newClusterIndex += op.ethSnapshot.index + + (uint64(block.number) - op.ethSnapshot.block) * PackedETH.unwrap(op.ethFee); + } + + uint64 newNetworkFeeIndex = sp.currentNetworkFeeIndex(); + + // Replicate ClusterLib.updateBalanceWithEB using OLD vUnits (before EB snapshot was updated). + uint128 idxOp = uint128(newClusterIndex - clusterModel.index); + uint128 idxNet = uint128(newNetworkFeeIndex - clusterModel.networkFeeIndex); + uint128 units = uint128(oldVUnits); + uint128 usageUnits = (idxOp * units) / BPS_DENOMINATOR + (idxNet * units) / BPS_DENOMINATOR; + uint256 usage = uint256(usageUnits) * ETH_DEDUCTED_DIGITS; + + clusterModel.balance = usage > clusterModel.balance ? 0 : clusterModel.balance - usage; + clusterModel.index = newClusterIndex; + clusterModel.networkFeeIndex = newNetworkFeeIndex; + } + + function _emptyCluster() internal pure returns (ISSVNetworkCore.Cluster memory) { + return ISSVNetworkCore.Cluster({validatorCount: 0, networkFeeIndex: 0, index: 0, active: true, balance: 0}); + } + + function _singleLeafRoot(bytes32 targetClusterId, uint32 effectiveBalance) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(keccak256(abi.encode(targetClusterId, effectiveBalance)))); + } + + function _setCommittedRoot(StorageEB storage seb, uint64 blockNum, bytes32 root) internal { + seb.ebRoots[blockNum] = root; + seb.latestCommittedBlock = blockNum; + } +} diff --git a/test/echidna/SSVStakingEchidna.sol b/test/echidna/SSVStakingEchidna.sol new file mode 100644 index 000000000..fb226fbbf --- /dev/null +++ b/test/echidna/SSVStakingEchidna.sol @@ -0,0 +1,1239 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVStaking.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/storage/SSVStorageStaking.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/test/mocks/MockToken.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {PackedETH, ETH_DEDUCTED_DIGITS} from "../../contracts/libraries/SSVCoreTypes.sol"; + +interface IStakingHook { + function onCSSVTransfer(address from, address to, uint256 amount) external; +} + +contract CSSVTokenMock is ERC20 { + error NotSSVStaking(); + error ZeroAddress(); + + address public immutable ssvStaking; + + modifier onlySSVStaking() { + if (msg.sender != ssvStaking) revert NotSSVStaking(); + _; + } + + constructor(address ssvStaking_) ERC20("cSSV", "cSSV") { + if (ssvStaking_ == address(0)) revert ZeroAddress(); + ssvStaking = ssvStaking_; + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal override { + if (from != to && from != address(0) && to != address(0) && msg.sender != ssvStaking && amount > 0) { + IStakingHook(ssvStaking).onCSSVTransfer(from, to, amount); + } + super._beforeTokenTransfer(from, to, amount); + } + + function mint(address to, uint256 amount) external onlySSVStaking { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlySSVStaking { + _burn(from, amount); + } +} + +interface IStaking { + function stake(uint256 amount) external; + function requestUnstake(uint256 amount) external; + function withdrawUnlocked() external; + function claimEthRewards() external; +} + +contract StakingUser { + IStaking public staking; + IERC20 public token; + IERC20 public cssv; + + constructor(IStaking staking_, IERC20 token_, IERC20 cssv_) { + staking = staking_; + token = token_; + cssv = cssv_; + } + + receive() external payable {} + + function approve(uint256 amount) external { + token.approve(address(staking), amount); + } + + function stake(uint256 amount) external { + staking.stake(amount); + } + + function requestUnstake(uint256 amount) external { + staking.requestUnstake(amount); + } + + function withdrawUnlocked() external { + staking.withdrawUnlocked(); + } + + function claim() external { + staking.claimEthRewards(); + } + + function transferCSSV(address to, uint256 amount) external { + cssv.transfer(to, amount); + } +} + +contract SSVStakingEchidna is SSVStaking { + using PackedETHLib for PackedETH; + + uint64 private constant MINIMAL_STAKING_AMOUNT = 1_000_000_000; + uint256 private constant MAX_STAKE = 1_000_000 ether; + uint256 private constant ACCRUAL_PRECISION = 1e18; + // Mirror SSVStaking.MAX_PENDING_REQUESTS to avoid harness-only false negatives. + uint256 private constant MAX_PENDING_REQUESTS = 2000; + uint256 private constant CANONICAL_TEST_STAKE = 1 ether; + uint64 private constant MAX_REWARD_WINDOW_UNITS = 1_000_000_000; + + MockToken private token; + CSSVTokenMock private cssv; + + StakingUser private user1; + StakingUser private user2; + StakingUser private user3; + StakingUser private user4; + + bool private syncFeesFailed; + bool private syncFeesMismatch; + bool private sawDecrease; + bool private invalidStakeSucceeded; + bool private invalidUnstakeSucceeded; + bool private invalidWithdrawSucceeded; + bool private cssvSupplyDeltaMismatch; + bool private userIndexSettleMismatch; + bool private transferSettleMismatch; + bool private claimDeltaMismatch; + bool private secondSameBlockClaimPaid; + bool private claimPayoutPrecisionMismatch; + bool private freeRewardsOnTransferDetected; + bool private payoutAccountingOverflow; + bool private unstakeStopsAccrualViolation; + bool private dustForfeitureViolation; + bool private zeroCssvAccrualViolation; + bool private withdrawUnlockedBatchViolation; + + uint256 private expectedCssvSupply; + uint256 private totalEthCreditedWei; + uint256 private totalEthPaidOutWei; + + struct PayoutVerifyParams { + uint256 accAfterWindow; + uint256 expectedPayout; + uint256 expectedRemainder; + uint256 wrongPayout; + uint256 wrongRemainder; + bool wrongDiffers; + } + + constructor() SSVStaking(address(new CSSVTokenMock(address(this)))) { + token = new MockToken(); + cssv = CSSVTokenMock(CSSV_ADDRESS); + + _mockSetToken(address(token)); + + IStaking self = IStaking(address(this)); + user1 = new StakingUser(self, IERC20(address(token)), IERC20(address(cssv))); + user2 = new StakingUser(self, IERC20(address(token)), IERC20(address(cssv))); + user3 = new StakingUser(self, IERC20(address(token)), IERC20(address(cssv))); + user4 = new StakingUser(self, IERC20(address(token)), IERC20(address(cssv))); + + _mockSetDefaultOracleIds(); + expectedCssvSupply = cssv.totalSupply(); + } + + function action_stake(uint256 seed, uint8 userSeed) external { + StakingUser user = _user(userSeed); + uint256 amount = _boundAmount(seed); + uint64 beforePool = PackedETH.unwrap(SSVStorageStaking.load().stakingEthPoolBalance); + uint256 beforeSupply = cssv.totalSupply(); + + if (seed % 10 == 0) { + amount = 0; + } else if (seed % 10 == 1) { + amount = MINIMAL_STAKING_AMOUNT - 1; + } + + token.mint(address(user), amount); + try user.approve(amount) {} catch {} + + bool invalid = amount == 0 || amount < MINIMAL_STAKING_AMOUNT; + try user.stake(amount) { + if (invalid) invalidStakeSucceeded = true; + if (!invalid) { + uint256 afterSupply = cssv.totalSupply(); + if (afterSupply != beforeSupply + amount) { + cssvSupplyDeltaMismatch = true; + } + if (expectedCssvSupply > type(uint256).max - amount) { + payoutAccountingOverflow = true; + } else { + expectedCssvSupply += amount; + } + _checkSettledUser(address(user)); + } + } catch {} + _trackPoolCredit(beforePool, PackedETH.unwrap(SSVStorageStaking.load().stakingEthPoolBalance)); + } + + function action_request_unstake(uint256 seed, uint8 userSeed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StakingUser user = _user(userSeed); + address userAddr = address(user); + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + uint256 beforeSupply = cssv.totalSupply(); + + uint256 balance = cssv.balanceOf(userAddr); + uint256 amount; + + if (seed % 5 == 0) { + amount = balance + 1; + } else if (seed % 5 == 1 || balance == 0) { + amount = 0; + } else { + amount = seed % (balance + 1); + if (amount == 0) amount = 1; + } + + uint256 requestCount = s.withdrawalRequests[userAddr].length; + bool invalid = amount == 0 || amount > balance || requestCount >= MAX_PENDING_REQUESTS; + + try user.requestUnstake(amount) { + if (invalid) invalidUnstakeSucceeded = true; + if (!invalid) { + uint256 afterSupply = cssv.totalSupply(); + if (beforeSupply < amount || afterSupply != beforeSupply - amount) { + cssvSupplyDeltaMismatch = true; + } + if (expectedCssvSupply < amount) { + cssvSupplyDeltaMismatch = true; + } else { + expectedCssvSupply -= amount; + } + _checkSettledUser(userAddr); + } + } catch {} + _trackPoolCredit(beforePool, PackedETH.unwrap(SSVStorageStaking.load().stakingEthPoolBalance)); + } + + function action_withdraw_unlocked(uint8 userSeed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StakingUser user = _user(userSeed); + address userAddr = address(user); + + uint256 withdrawable = _withdrawableAmount(s, userAddr); + try user.withdrawUnlocked() { + if (withdrawable == 0) invalidWithdrawSucceeded = true; + } catch {} + } + + function action_withdraw_unlocked_batch_processing(uint256 seed, uint8 userSeed) external { + StorageStaking storage s = SSVStorageStaking.load(); + + (StakingUser user, address userAddr, bool found) = _pickUserWithoutPendingRequests(uint256(userSeed) + seed); + if (!found) return; + if (!_ensureBalanceAtLeast(user, CANONICAL_TEST_STAKE)) return; + + uint256[4] memory requestAmounts = [uint256(1), uint256(2), uint256(3), uint256(4)]; + uint256 totalRequested = 0; + for (uint256 i; i < requestAmounts.length; ++i) { + totalRequested += requestAmounts[i]; + } + if (cssv.balanceOf(userAddr) < totalRequested) return; + + for (uint256 i; i < requestAmounts.length; ++i) { + if (!_requestUnstakeExact(user, requestAmounts[i])) { + return; + } + } + + UnstakeRequest[] storage requests = s.withdrawalRequests[userAddr]; + if (requests.length != 4) { + withdrawUnlockedBatchViolation = true; + return; + } + + uint64 nowTs = uint64(block.timestamp); + UnstakeRequest memory req0 = UnstakeRequest({amount: uint192(requestAmounts[0]), unlockTime: nowTs}); + UnstakeRequest memory req1 = UnstakeRequest({amount: uint192(requestAmounts[1]), unlockTime: nowTs + 1}); + UnstakeRequest memory req2 = UnstakeRequest({amount: uint192(requestAmounts[2]), unlockTime: nowTs}); + UnstakeRequest memory req3 = UnstakeRequest({amount: uint192(requestAmounts[3]), unlockTime: nowTs + 2}); + UnstakeRequest[4] memory expectedRequests; + + uint256 scenario = seed % 3; + if (scenario == 0) { + requests[0].unlockTime = req0.unlockTime; + requests[1].unlockTime = req1.unlockTime; + requests[2].unlockTime = req2.unlockTime; + requests[3].unlockTime = req3.unlockTime; + expectedRequests[0] = req0; + expectedRequests[1] = req1; + expectedRequests[2] = req2; + expectedRequests[3] = req3; + } else if (scenario == 1) { + requests[0].unlockTime = nowTs; + requests[1].unlockTime = nowTs; + requests[2].unlockTime = nowTs; + requests[3].unlockTime = nowTs; + expectedRequests[0] = UnstakeRequest({amount: uint192(requestAmounts[0]), unlockTime: nowTs}); + expectedRequests[1] = UnstakeRequest({amount: uint192(requestAmounts[1]), unlockTime: nowTs}); + expectedRequests[2] = UnstakeRequest({amount: uint192(requestAmounts[2]), unlockTime: nowTs}); + expectedRequests[3] = UnstakeRequest({amount: uint192(requestAmounts[3]), unlockTime: nowTs}); + } else { + requests[0].unlockTime = nowTs + 1; + requests[1].unlockTime = nowTs + 2; + requests[2].unlockTime = nowTs + 3; + requests[3].unlockTime = nowTs + 4; + expectedRequests[0] = UnstakeRequest({amount: uint192(requestAmounts[0]), unlockTime: nowTs + 1}); + expectedRequests[1] = UnstakeRequest({amount: uint192(requestAmounts[1]), unlockTime: nowTs + 2}); + expectedRequests[2] = UnstakeRequest({amount: uint192(requestAmounts[2]), unlockTime: nowTs + 3}); + expectedRequests[3] = UnstakeRequest({amount: uint192(requestAmounts[3]), unlockTime: nowTs + 4}); + } + + uint256 userTokenBefore = token.balanceOf(userAddr); + uint256 contractTokenBefore = token.balanceOf(address(this)); + uint256 supplyBefore = cssv.totalSupply(); + + if (scenario == 2) { + try user.withdrawUnlocked() { + invalidWithdrawSucceeded = true; + withdrawUnlockedBatchViolation = true; + } catch { + if (token.balanceOf(userAddr) != userTokenBefore) { + withdrawUnlockedBatchViolation = true; + } + if (token.balanceOf(address(this)) != contractTokenBefore) { + withdrawUnlockedBatchViolation = true; + } + if (cssv.totalSupply() != supplyBefore) { + withdrawUnlockedBatchViolation = true; + } + if (!_requestsMatchFourExact(s.withdrawalRequests[userAddr], expectedRequests)) { + withdrawUnlockedBatchViolation = true; + } + } + return; + } + + uint256 expectedPayout = scenario == 0 + ? requestAmounts[0] + requestAmounts[2] + : requestAmounts[0] + requestAmounts[1] + requestAmounts[2] + requestAmounts[3]; + + try user.withdrawUnlocked() { + if (token.balanceOf(userAddr) != userTokenBefore + expectedPayout) { + withdrawUnlockedBatchViolation = true; + } + if (token.balanceOf(address(this)) != contractTokenBefore - expectedPayout) { + withdrawUnlockedBatchViolation = true; + } + if (cssv.totalSupply() != supplyBefore) { + withdrawUnlockedBatchViolation = true; + } + + if (scenario == 0) { + if (!_requestsMatchTwoAsMultiset(s.withdrawalRequests[userAddr], req1, req3)) { + withdrawUnlockedBatchViolation = true; + } + } else if (s.withdrawalRequests[userAddr].length != 0) { + withdrawUnlockedBatchViolation = true; + } + } catch { + withdrawUnlockedBatchViolation = true; + } + } + + function action_claim_rewards(uint8 userSeed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + StakingUser user = _user(userSeed); + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 beforeDao = PackedETH.unwrap(sp.ethDaoBalance); + uint256 beforeUserBalance = address(user).balance; + try user.claim() { + uint256 payout = _verifyClaimDeltas(user, s, sp, beforePool, beforeDao, beforeUserBalance); + _addPaidOut(payout); + _checkSettledUser(address(user)); + _verifySecondClaimNoPayout(user, s, sp); + } catch {} + } + + function action_transfer_cssv(uint256 seed, uint8 fromSeed, uint8 toSeed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StakingUser fromUser = _user(fromSeed); + StakingUser toUser = _user(toSeed); + if (address(fromUser) == address(toUser)) return; + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + address from = address(fromUser); + address to = address(toUser); + uint256 fromBalanceBefore = cssv.balanceOf(from); + uint256 toBalanceBefore = cssv.balanceOf(to); + uint256 fromIdxBefore = s.userIndex[from]; + uint256 toIdxBefore = s.userIndex[to]; + uint256 fromAccruedBefore = s.accrued[from]; + uint256 toAccruedBefore = s.accrued[to]; + + uint256 balance = fromBalanceBefore; + if (balance == 0) return; + + uint256 amount = (seed % balance) + 1; + try fromUser.transferCSSV(address(toUser), amount) { + uint256 accAfter = s.accEthPerShare; + + if (s.userIndex[from] != accAfter || s.userIndex[to] != accAfter) { + transferSettleMismatch = true; + } + + uint256 fromPending; + if (fromBalanceBefore != 0 && accAfter > fromIdxBefore) { + fromPending = (fromBalanceBefore * (accAfter - fromIdxBefore)) / ACCRUAL_PRECISION; + } + + uint256 toPending; + if (toBalanceBefore != 0 && accAfter > toIdxBefore) { + toPending = (toBalanceBefore * (accAfter - toIdxBefore)) / ACCRUAL_PRECISION; + } + + uint256 expectedFromAccrued = fromAccruedBefore + fromPending; + uint256 expectedToAccrued = toAccruedBefore + toPending; + + if (s.accrued[from] != expectedFromAccrued || s.accrued[to] != expectedToAccrued) { + freeRewardsOnTransferDetected = true; + } + } catch {} + _trackPoolCredit(beforePool, PackedETH.unwrap(SSVStorageStaking.load().stakingEthPoolBalance)); + } + + function action_sync_fees_with_increase(uint256 seed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + uint64 previous = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 add = _boundShrunk(seed, type(uint64).max); + if (add == 0) return; + if (previous > type(uint64).max - add) return; + uint64 current = previous + add; + + uint64 oldDao = PackedETH.unwrap(sp.ethDaoBalance); + uint32 oldIndex = sp.ethDaoIndexBlockNumber; + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + + sp.ethDaoBalance = PackedETH.wrap(current); + sp.ethDaoIndexBlockNumber = uint32(block.number); + + try this.syncFees() { + if (PackedETH.unwrap(s.stakingEthPoolBalance) != current) { + syncFeesMismatch = true; + } + } catch { + syncFeesFailed = true; + sp.ethDaoBalance = PackedETH.wrap(oldDao); + sp.ethDaoIndexBlockNumber = oldIndex; + } + _trackPoolCredit(beforePool, PackedETH.unwrap(s.stakingEthPoolBalance)); + } + + function action_sync_fees_with_decrease(uint256 seed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + uint64 oldPool = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 previous = _boundShrunk(seed, type(uint64).max); + if (previous == 0) previous = 1; + uint64 current = previous - 1; + + uint64 oldDao = PackedETH.unwrap(sp.ethDaoBalance); + uint32 oldIndex = sp.ethDaoIndexBlockNumber; + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + + s.stakingEthPoolBalance = PackedETH.wrap(previous); + _mockSetEthDaoBalance(current); + sawDecrease = true; + + try this.syncFees() { + if (PackedETH.unwrap(s.stakingEthPoolBalance) != current) { + syncFeesMismatch = true; + } + } catch { + syncFeesFailed = true; + s.stakingEthPoolBalance = PackedETH.wrap(oldPool); + sp.ethDaoBalance = PackedETH.wrap(oldDao); + sp.ethDaoIndexBlockNumber = oldIndex; + } + _trackPoolCredit(beforePool, PackedETH.unwrap(s.stakingEthPoolBalance)); + } + + function action_request_unstake_stops_accrual( + uint256 unstakeSeed, + uint256 preWindowSeed, + uint256 postWindowSeed, + uint256 userSeed + ) external { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + StakingUser user = _user(uint8(userSeed)); + if (!_ensureBalanceAtLeast(user, CANONICAL_TEST_STAKE)) return; + + address userAddr = address(user); + uint256 balanceBefore = cssv.balanceOf(userAddr); + if (balanceBefore <= 1) return; + if (s.withdrawalRequests[userAddr].length >= MAX_PENDING_REQUESTS) return; + + _setUserRewardState(userAddr, 0); + _forceCurrentDaoBalance(s, sp); + + (bool preOk, uint256 accBefore, uint256 accAfterPre) = _creditFeeWindow(_boundRewardUnits(preWindowSeed)); + if (!preOk || accAfterPre <= accBefore) return; + + uint256 expectedAccruedAtRequest = _pendingReward(balanceBefore, accAfterPre, accBefore); + if (expectedAccruedAtRequest == 0) return; + + uint256 beforeSupply = cssv.totalSupply(); + uint256 unstakeAmount = (unstakeSeed % (balanceBefore - 1)) + 1; + uint256 poolBeforeRequest = uint256(PackedETH.unwrap(s.stakingEthPoolBalance)) * ETH_DEDUCTED_DIGITS; + + try user.requestUnstake(unstakeAmount) { + uint256 remainingBalance = cssv.balanceOf(userAddr); + if (remainingBalance != balanceBefore - unstakeAmount) { + unstakeStopsAccrualViolation = true; + } + if (s.userIndex[userAddr] != accAfterPre) { + unstakeStopsAccrualViolation = true; + } + if (s.accrued[userAddr] != expectedAccruedAtRequest) { + unstakeStopsAccrualViolation = true; + } + if (cssv.totalSupply() != beforeSupply - unstakeAmount) { + cssvSupplyDeltaMismatch = true; + } + if (expectedCssvSupply < unstakeAmount) { + cssvSupplyDeltaMismatch = true; + } else { + expectedCssvSupply -= unstakeAmount; + } + if (uint256(PackedETH.unwrap(s.stakingEthPoolBalance)) * ETH_DEDUCTED_DIGITS != poolBeforeRequest) { + unstakeStopsAccrualViolation = true; + } + + _verifyPostUnstakeClaim( + user, userAddr, s, sp, + remainingBalance, balanceBefore, + accAfterPre, expectedAccruedAtRequest, + postWindowSeed + ); + } catch {} + } + + function action_claim_dust_zero_balance(uint256 dustSeed, uint256 userSeed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + (StakingUser user, address userAddr, bool found) = _pickZeroCssvUser(userSeed); + if (!found) return; + + uint256 dust = _boundDust(dustSeed); + _setUserRewardState(userAddr, dust); + _forceCurrentDaoBalance(s, sp); + + uint256 accruedBefore = s.accrued[userAddr]; + uint256 indexBefore = s.userIndex[userAddr]; + uint64 poolBefore = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 daoBefore = PackedETH.unwrap(sp.ethDaoBalance); + uint256 ethBefore = userAddr.balance; + + try user.claim() { + if (s.accrued[userAddr] != 0) { + dustForfeitureViolation = true; + } + if (s.userIndex[userAddr] != indexBefore) { + dustForfeitureViolation = true; + } + if (PackedETH.unwrap(s.stakingEthPoolBalance) != poolBefore) { + dustForfeitureViolation = true; + } + if (PackedETH.unwrap(sp.ethDaoBalance) != daoBefore) { + dustForfeitureViolation = true; + } + if (userAddr.balance != ethBefore) { + dustForfeitureViolation = true; + } + if (accruedBefore != dust) { + dustForfeitureViolation = true; + } + } catch { + dustForfeitureViolation = true; + } + } + + function action_claim_dust_positive_balance(uint256 dustSeed, uint256 userSeed) external { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + StakingUser user = _user(uint8(userSeed)); + if (!_ensureBalanceAtLeast(user, CANONICAL_TEST_STAKE)) return; + + address userAddr = address(user); + uint256 dust = _boundDust(dustSeed); + _setUserRewardState(userAddr, dust); + _forceCurrentDaoBalance(s, sp); + + uint256 accruedBefore = s.accrued[userAddr]; + uint256 indexBefore = s.userIndex[userAddr]; + uint64 poolBefore = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 daoBefore = PackedETH.unwrap(sp.ethDaoBalance); + uint256 cssvBefore = cssv.balanceOf(userAddr); + uint256 ethBefore = userAddr.balance; + + try user.claim() { + dustForfeitureViolation = true; + } catch { + if (s.accrued[userAddr] != accruedBefore) { + dustForfeitureViolation = true; + } + if (s.userIndex[userAddr] != indexBefore) { + dustForfeitureViolation = true; + } + if (PackedETH.unwrap(s.stakingEthPoolBalance) != poolBefore) { + dustForfeitureViolation = true; + } + if (PackedETH.unwrap(sp.ethDaoBalance) != daoBefore) { + dustForfeitureViolation = true; + } + if (cssv.balanceOf(userAddr) != cssvBefore) { + dustForfeitureViolation = true; + } + if (userAddr.balance != ethBefore) { + dustForfeitureViolation = true; + } + } + } + + function action_zero_cssv_no_accrual( + uint256 zeroWindowSeed, + uint256 postWindowSeed, + uint256 userSeed + ) external { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + (StakingUser targetUser, address targetAddr, bool found) = _pickZeroCssvUser(userSeed); + if (!found) return; + + (StakingUser supportUser, , bool distinctFound) = _pickDistinctUser(targetAddr, userSeed + 1); + if (!distinctFound) return; + if (!_ensureBalanceAtLeast(supportUser, CANONICAL_TEST_STAKE)) return; + + _setUserRewardState(targetAddr, 0); + _forceCurrentDaoBalance(s, sp); + + uint256 accBeforeZeroWindow = s.accEthPerShare; + (bool zeroOk, , uint256 accAfterZeroWindow) = _creditFeeWindow(_boundRewardUnits(zeroWindowSeed)); + if (!zeroOk || accAfterZeroWindow <= accBeforeZeroWindow) return; + + if (!_stakeExact(targetUser, CANONICAL_TEST_STAKE)) return; + + uint256 targetBalance = cssv.balanceOf(targetAddr); + if (targetBalance == 0) return; + if (s.userIndex[targetAddr] != accAfterZeroWindow) { + zeroCssvAccrualViolation = true; + } + if (s.accrued[targetAddr] != 0) { + zeroCssvAccrualViolation = true; + } + + _verifyZeroCssvClaim( + targetUser, targetAddr, s, sp, + targetBalance, accAfterZeroWindow, accBeforeZeroWindow, + postWindowSeed + ); + } + + function echidna_sync_fees_handles_decrease() external view returns (bool) { + if (!sawDecrease) return true; + return !syncFeesFailed && !syncFeesMismatch; + } + + function echidna_sync_fees_never_fails() external view returns (bool) { + return !syncFeesFailed && !syncFeesMismatch; + } + + function echidna_invalid_stake_reverts() external view returns (bool) { + return !invalidStakeSucceeded; + } + + function echidna_invalid_unstake_reverts() external view returns (bool) { + return !invalidUnstakeSucceeded; + } + + function echidna_invalid_withdraw_reverts() external view returns (bool) { + return !invalidWithdrawSucceeded; + } + + function echidna_cssv_supply_matches_users() external view returns (bool) { + uint256 supply = cssv.totalSupply(); + uint256 sumBalances = cssv.balanceOf(address(user1)) + + cssv.balanceOf(address(user2)) + + cssv.balanceOf(address(user3)) + + cssv.balanceOf(address(user4)); + return !cssvSupplyDeltaMismatch && supply == sumBalances && supply == expectedCssvSupply; + } + + function echidna_cssv_supply_lte_ssv_backing() external view returns (bool) { + return cssv.totalSupply() <= token.balanceOf(address(this)); + } + + function echidna_ssv_balance_matches_staked_plus_pending() external view returns (bool) { + StorageStaking storage s = SSVStorageStaking.load(); + uint256 pending = _totalPendingUnstake(s); + uint256 staked = cssv.totalSupply(); + uint256 contractBalance = token.balanceOf(address(this)); + return contractBalance == staked + pending; + } + + function echidna_pool_matches_dao_balance() external view returns (bool) { + StorageProtocol storage sp = SSVStorageProtocol.load(); + return !claimDeltaMismatch && SSVStorageStaking.load().stakingEthPoolBalance.eq(sp.ethDaoBalance); + } + + function echidna_claim_twice_same_block_no_second_payout() external view returns (bool) { + return !secondSameBlockClaimPaid; + } + + function echidna_pending_requests_bounded() external view returns (bool) { + StorageStaking storage s = SSVStorageStaking.load(); + if (s.withdrawalRequests[address(user1)].length > MAX_PENDING_REQUESTS) return false; + if (s.withdrawalRequests[address(user2)].length > MAX_PENDING_REQUESTS) return false; + if (s.withdrawalRequests[address(user3)].length > MAX_PENDING_REQUESTS) return false; + if (s.withdrawalRequests[address(user4)].length > MAX_PENDING_REQUESTS) return false; + return true; + } + + function echidna_user_index_leq_acc() external view returns (bool) { + if (userIndexSettleMismatch) return false; + StorageStaking storage s = SSVStorageStaking.load(); + uint256 acc = s.accEthPerShare; + if (s.userIndex[address(user1)] > acc) return false; + if (s.userIndex[address(user2)] > acc) return false; + if (s.userIndex[address(user3)] > acc) return false; + if (s.userIndex[address(user4)] > acc) return false; + return true; + } + + function echidna_accrued_within_pool() external view returns (bool) { + if (payoutAccountingOverflow || claimDeltaMismatch) return false; + StorageStaking storage s = SSVStorageStaking.load(); + uint256 accrued = _roundedDownToPayoutPrecision(s.accrued[address(user1)]) + + _roundedDownToPayoutPrecision(s.accrued[address(user2)]) + + _roundedDownToPayoutPrecision(s.accrued[address(user3)]) + + _roundedDownToPayoutPrecision(s.accrued[address(user4)]); + uint256 poolWei = uint256(PackedETH.unwrap(SSVStorageProtocol.load().ethDaoBalance)) * ETH_DEDUCTED_DIGITS; + if (totalEthPaidOutWei > totalEthCreditedWei) return false; + if (accrued <= poolWei) return true; + if (accrued > type(uint256).max - totalEthPaidOutWei) return false; + return accrued + totalEthPaidOutWei <= totalEthCreditedWei; + } + + function echidna_cssv_transfer_settles_both() external view returns (bool) { + return !transferSettleMismatch; + } + + function echidna_claim_payout_precision() external view returns (bool) { + return !claimPayoutPrecisionMismatch; + } + + function echidna_no_free_rewards_on_transfer() external view returns (bool) { + return !freeRewardsOnTransferDetected; + } + + function echidna_unstake_stops_accrual() external view returns (bool) { + return !unstakeStopsAccrualViolation; + } + + function echidna_dust_forfeiture_correct() external view returns (bool) { + return !dustForfeitureViolation; + } + + function echidna_zero_cssv_no_accrual() external view returns (bool) { + return !zeroCssvAccrualViolation; + } + + function echidna_withdraw_unlocked_batch_correct() external view returns (bool) { + return !withdrawUnlockedBatchViolation; + } + + function _verifyClaimDeltas( + StakingUser user, + StorageStaking storage s, + StorageProtocol storage sp, + uint64 beforePool, + uint64 beforeDao, + uint256 beforeUserBalance + ) internal returns (uint256 payout) { + uint64 afterPool = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 afterDao = PackedETH.unwrap(sp.ethDaoBalance); + payout = address(user).balance - beforeUserBalance; + if (payout % ETH_DEDUCTED_DIGITS != 0) { + claimPayoutPrecisionMismatch = true; + } + if (afterPool > beforePool || afterDao > beforeDao) { + claimDeltaMismatch = true; + } else { + uint256 poolDeltaWei = uint256(beforePool - afterPool) * ETH_DEDUCTED_DIGITS; + uint256 daoDeltaWei = uint256(beforeDao - afterDao) * ETH_DEDUCTED_DIGITS; + if (poolDeltaWei != payout || daoDeltaWei != payout) { + claimDeltaMismatch = true; + } + } + } + + function _verifySecondClaimNoPayout( + StakingUser user, + StorageStaking storage s, + StorageProtocol storage sp + ) internal { + uint64 midPool = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 midDao = PackedETH.unwrap(sp.ethDaoBalance); + uint256 midUserBalance = address(user).balance; + try user.claim() { + if ( + PackedETH.unwrap(s.stakingEthPoolBalance) != midPool || + PackedETH.unwrap(sp.ethDaoBalance) != midDao || + address(user).balance != midUserBalance + ) { + secondSameBlockClaimPaid = true; + } + } catch {} + } + + function _verifyPostUnstakeClaim( + StakingUser user, + address userAddr, + StorageStaking storage s, + StorageProtocol storage sp, + uint256 remainingBalance, + uint256 balanceBefore, + uint256 accAfterPre, + uint256 expectedAccruedAtRequest, + uint256 postWindowSeed + ) internal { + (bool postOk, , uint256 accAfterPost) = _creditFeeWindow(_boundRewardUnits(postWindowSeed)); + if (!postOk || accAfterPost <= accAfterPre) return; + + PayoutVerifyParams memory p; + { + uint256 expectedTotal = expectedAccruedAtRequest + _pendingReward(remainingBalance, accAfterPost, accAfterPre); + uint256 wrongTotal = expectedAccruedAtRequest + _pendingReward(balanceBefore, accAfterPost, accAfterPre); + uint256 ePayout = _roundedDownToPayoutPrecision(expectedTotal); + uint256 wPayout = _roundedDownToPayoutPrecision(wrongTotal); + p = PayoutVerifyParams({ + accAfterWindow: accAfterPost, + expectedPayout: ePayout, + expectedRemainder: expectedTotal - ePayout, + wrongPayout: wPayout, + wrongRemainder: wrongTotal - wPayout, + wrongDiffers: wrongTotal != expectedTotal + }); + } + if (p.expectedPayout == 0) return; + + if (_claimAndVerifyPayout(user, userAddr, s, sp, p)) { + unstakeStopsAccrualViolation = true; + } + } + + function _verifyZeroCssvClaim( + StakingUser targetUser, + address targetAddr, + StorageStaking storage s, + StorageProtocol storage sp, + uint256 targetBalance, + uint256 accAfterZeroWindow, + uint256 accBeforeZeroWindow, + uint256 postWindowSeed + ) internal { + (bool postOk, , uint256 accAfterPostWindow) = _creditFeeWindow(_boundRewardUnits(postWindowSeed)); + if (!postOk || accAfterPostWindow <= accAfterZeroWindow) return; + + PayoutVerifyParams memory p; + { + uint256 expectedAccrued = _pendingReward(targetBalance, accAfterPostWindow, accAfterZeroWindow); + uint256 wrongAccrued = _pendingReward(targetBalance, accAfterPostWindow, accBeforeZeroWindow); + uint256 ePayout = _roundedDownToPayoutPrecision(expectedAccrued); + uint256 wPayout = _roundedDownToPayoutPrecision(wrongAccrued); + p = PayoutVerifyParams({ + accAfterWindow: accAfterPostWindow, + expectedPayout: ePayout, + expectedRemainder: expectedAccrued - ePayout, + wrongPayout: wPayout, + wrongRemainder: wrongAccrued - wPayout, + wrongDiffers: wrongAccrued != expectedAccrued + }); + } + if (p.expectedPayout == 0) return; + + if (_claimAndVerifyPayout(targetUser, targetAddr, s, sp, p)) { + zeroCssvAccrualViolation = true; + } + } + + function _claimAndVerifyPayout( + StakingUser user, + address userAddr, + StorageStaking storage s, + StorageProtocol storage sp, + PayoutVerifyParams memory p + ) internal returns (bool violated) { + uint64 poolBefore = PackedETH.unwrap(s.stakingEthPoolBalance); + uint64 daoBefore = PackedETH.unwrap(sp.ethDaoBalance); + uint64 payoutUnits = uint64(p.expectedPayout / ETH_DEDUCTED_DIGITS); + if ( + payoutUnits > poolBefore || + payoutUnits > daoBefore || + p.expectedPayout > address(this).balance + ) { + return false; + } + + uint256 ethBefore = userAddr.balance; + try user.claim() { + uint256 actualPayout = userAddr.balance - ethBefore; + if (actualPayout != p.expectedPayout) violated = true; + if (s.accrued[userAddr] != p.expectedRemainder) violated = true; + if (s.userIndex[userAddr] != p.accAfterWindow) violated = true; + if (PackedETH.unwrap(s.stakingEthPoolBalance) != poolBefore - payoutUnits) violated = true; + if (PackedETH.unwrap(sp.ethDaoBalance) != daoBefore - payoutUnits) violated = true; + if (p.wrongDiffers && actualPayout == p.wrongPayout && s.accrued[userAddr] == p.wrongRemainder) violated = true; + _addPaidOut(actualPayout); + } catch { + violated = true; + } + } + + function _boundShrunk(uint256 seed, uint64 maxValue) internal pure returns (uint64) { + if (maxValue == 0) return 0; + return uint64(seed % (uint256(maxValue) + 1)); + } + + function _boundAmount(uint256 seed) internal pure returns (uint256) { + uint256 amount = seed % MAX_STAKE; + if (amount == 0) amount = 1; + return amount; + } + + function _boundRewardUnits(uint256 seed) internal pure returns (uint64) { + return uint64(seed % MAX_REWARD_WINDOW_UNITS) + 1; + } + + function _boundDust(uint256 seed) internal pure returns (uint256) { + return (seed % (ETH_DEDUCTED_DIGITS - 1)) + 1; + } + + function _user(uint8 seed) internal view returns (StakingUser) { + uint8 idx = seed % 4; + if (idx == 0) return user1; + if (idx == 1) return user2; + if (idx == 2) return user3; + return user4; + } + + function _withdrawableAmount(StorageStaking storage s, address user) internal view returns (uint256) { + UnstakeRequest[] storage requests = s.withdrawalRequests[user]; + uint256 total; + for (uint256 i; i < requests.length; ++i) { + if (requests[i].unlockTime <= block.timestamp) { + total += requests[i].amount; + } + } + return total; + } + + function _totalPendingUnstake(StorageStaking storage s) internal view returns (uint256) { + return _pendingForUser(s, address(user1)) + + _pendingForUser(s, address(user2)) + + _pendingForUser(s, address(user3)) + + _pendingForUser(s, address(user4)); + } + + function _pendingForUser(StorageStaking storage s, address user) internal view returns (uint256) { + UnstakeRequest[] storage requests = s.withdrawalRequests[user]; + uint256 total; + for (uint256 i; i < requests.length; ++i) { + total += requests[i].amount; + } + return total; + } + + function _mockSetToken(address tokenAddress) internal { + SSVStorage.load().token = IERC20(tokenAddress); + } + + function _mockSetDefaultOracleIds() internal { + StorageStaking storage s = SSVStorageStaking.load(); + uint32[4] memory ids = [uint32(1), uint32(2), uint32(3), uint32(4)]; + s.defaultOracleIds = ids; + } + + // Override to add access control check (simulating SSVNetwork.sol behavior) + function onCSSVTransfer(address from, address to, uint256) external override { + if (msg.sender != CSSV_ADDRESS) revert NotCSSV(); + StorageStaking storage s = SSVStorageStaking.load(); + + _syncFees(s); + _settle(from, s); + _settle(to, s); + _checkSettledWithStorage(s, from); + _checkSettledWithStorage(s, to); + } + + function _mockSetEthDaoBalance(uint64 balance) internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.ethDaoBalance = PackedETH.wrap(balance); + sp.ethDaoIndexBlockNumber = uint32(block.number); + } + + function _forceCurrentDaoBalance(StorageStaking storage s, StorageProtocol storage sp) internal { + sp.ethDaoBalance = s.stakingEthPoolBalance; + sp.ethDaoIndexBlockNumber = uint32(block.number); + } + + function _setUserRewardState(address user, uint256 accruedAmount) internal { + StorageStaking storage s = SSVStorageStaking.load(); + s.userIndex[user] = s.accEthPerShare; + s.accrued[user] = accruedAmount; + } + + function _pendingReward(uint256 balance, uint256 idxAfter, uint256 idxBefore) internal pure returns (uint256) { + if (balance == 0 || idxAfter <= idxBefore) return 0; + return (balance * (idxAfter - idxBefore)) / ACCRUAL_PRECISION; + } + + function _creditFeeWindow(uint64 addUnits) internal returns (bool ok, uint256 accBefore, uint256 accAfter) { + StorageStaking storage s = SSVStorageStaking.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (addUnits == 0) return (false, 0, 0); + + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + if (beforePool > type(uint64).max - addUnits) return (false, 0, 0); + + uint64 oldDao = PackedETH.unwrap(sp.ethDaoBalance); + uint32 oldIndex = sp.ethDaoIndexBlockNumber; + uint64 targetPool = beforePool + addUnits; + accBefore = s.accEthPerShare; + + sp.ethDaoBalance = PackedETH.wrap(targetPool); + sp.ethDaoIndexBlockNumber = uint32(block.number); + + try this.syncFees() { + uint64 afterPool = PackedETH.unwrap(s.stakingEthPoolBalance); + if (afterPool != targetPool) { + syncFeesMismatch = true; + return (false, accBefore, s.accEthPerShare); + } + _trackPoolCredit(beforePool, afterPool); + return (true, accBefore, s.accEthPerShare); + } catch { + syncFeesFailed = true; + sp.ethDaoBalance = PackedETH.wrap(oldDao); + sp.ethDaoIndexBlockNumber = oldIndex; + return (false, accBefore, accBefore); + } + } + + function _stakeExact(StakingUser user, uint256 amount) internal returns (bool) { + if (amount < MINIMAL_STAKING_AMOUNT) return false; + + StorageStaking storage s = SSVStorageStaking.load(); + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + uint256 beforeSupply = cssv.totalSupply(); + + token.mint(address(user), amount); + try user.approve(amount) {} catch { + return false; + } + + try user.stake(amount) { + uint256 afterSupply = cssv.totalSupply(); + if (afterSupply != beforeSupply + amount) { + cssvSupplyDeltaMismatch = true; + } + if (expectedCssvSupply > type(uint256).max - amount) { + payoutAccountingOverflow = true; + } else { + expectedCssvSupply += amount; + } + _checkSettledUser(address(user)); + _trackPoolCredit(beforePool, PackedETH.unwrap(s.stakingEthPoolBalance)); + return true; + } catch { + return false; + } + } + + function _requestUnstakeExact(StakingUser user, uint256 amount) internal returns (bool) { + if (amount == 0) return false; + + StorageStaking storage s = SSVStorageStaking.load(); + address userAddr = address(user); + if (cssv.balanceOf(userAddr) < amount) return false; + if (s.withdrawalRequests[userAddr].length >= MAX_PENDING_REQUESTS) return false; + + uint64 beforePool = PackedETH.unwrap(s.stakingEthPoolBalance); + uint256 beforeSupply = cssv.totalSupply(); + + try user.requestUnstake(amount) { + uint256 afterSupply = cssv.totalSupply(); + if (beforeSupply < amount || afterSupply != beforeSupply - amount) { + cssvSupplyDeltaMismatch = true; + } + if (expectedCssvSupply < amount) { + cssvSupplyDeltaMismatch = true; + } else { + expectedCssvSupply -= amount; + } + _checkSettledUser(userAddr); + _trackPoolCredit(beforePool, PackedETH.unwrap(s.stakingEthPoolBalance)); + return true; + } catch { + return false; + } + } + + function _ensureBalanceAtLeast(StakingUser user, uint256 targetBalance) internal returns (bool) { + uint256 balance = cssv.balanceOf(address(user)); + if (balance >= targetBalance) return true; + + uint256 deficit = targetBalance - balance; + if (deficit < MINIMAL_STAKING_AMOUNT) { + deficit = MINIMAL_STAKING_AMOUNT; + } + return _stakeExact(user, deficit); + } + + function _pickZeroCssvUser( + uint256 seed + ) internal view returns (StakingUser user, address userAddr, bool found) { + for (uint256 i; i < 4; ++i) { + user = _user(uint8((seed + i) % 4)); + userAddr = address(user); + if (cssv.balanceOf(userAddr) == 0) { + return (user, userAddr, true); + } + } + + return (user1, address(user1), false); + } + + function _pickDistinctUser( + address excluded, + uint256 seed + ) internal view returns (StakingUser user, address userAddr, bool found) { + for (uint256 i; i < 4; ++i) { + user = _user(uint8((seed + i) % 4)); + userAddr = address(user); + if (userAddr != excluded) { + return (user, userAddr, true); + } + } + + return (user1, address(user1), false); + } + + function _pickUserWithoutPendingRequests( + uint256 seed + ) internal view returns (StakingUser user, address userAddr, bool found) { + StorageStaking storage s = SSVStorageStaking.load(); + for (uint256 i; i < 4; ++i) { + user = _user(uint8((seed + i) % 4)); + userAddr = address(user); + if (s.withdrawalRequests[userAddr].length == 0) { + return (user, userAddr, true); + } + } + + return (user1, address(user1), false); + } + + function _roundedDownToPayoutPrecision(uint256 amount) internal pure returns (uint256) { + return amount - (amount % ETH_DEDUCTED_DIGITS); + } + + function _requestsMatchTwoAsMultiset( + UnstakeRequest[] storage requests, + UnstakeRequest memory expectedA, + UnstakeRequest memory expectedB + ) internal view returns (bool) { + if (requests.length != 2) return false; + + bool direct = requests[0].amount == expectedA.amount && requests[0].unlockTime == expectedA.unlockTime + && requests[1].amount == expectedB.amount && requests[1].unlockTime == expectedB.unlockTime; + bool swapped = requests[0].amount == expectedB.amount && requests[0].unlockTime == expectedB.unlockTime + && requests[1].amount == expectedA.amount && requests[1].unlockTime == expectedA.unlockTime; + + return direct || swapped; + } + + function _requestsMatchFourExact( + UnstakeRequest[] storage storedRequests, + UnstakeRequest[4] memory expectedRequests + ) internal view returns (bool) { + if (storedRequests.length != expectedRequests.length) return false; + + uint256 count = storedRequests.length; + for (uint256 i; i < count; ++i) { + if (storedRequests[i].amount != expectedRequests[i].amount) return false; + if (storedRequests[i].unlockTime != expectedRequests[i].unlockTime) return false; + } + + return true; + } + + function _checkSettledUser(address user) internal { + _checkSettledWithStorage(SSVStorageStaking.load(), user); + } + + function _checkSettledWithStorage(StorageStaking storage s, address user) internal { + if (s.userIndex[user] != s.accEthPerShare) { + userIndexSettleMismatch = true; + } + } + + function _trackPoolCredit(uint64 beforePoolUnits, uint64 afterPoolUnits) internal { + if (afterPoolUnits <= beforePoolUnits) return; + uint256 deltaWei = uint256(afterPoolUnits - beforePoolUnits) * ETH_DEDUCTED_DIGITS; + _addCredited(deltaWei); + } + + function _addCredited(uint256 amountWei) internal { + if (totalEthCreditedWei > type(uint256).max - amountWei) { + payoutAccountingOverflow = true; + return; + } + totalEthCreditedWei += amountWei; + } + + function _addPaidOut(uint256 amountWei) internal { + if (totalEthPaidOutWei > type(uint256).max - amountWei) { + payoutAccountingOverflow = true; + return; + } + totalEthPaidOutWei += amountWei; + } +} diff --git a/test/echidna/SSVValidatorsEchidna.sol b/test/echidna/SSVValidatorsEchidna.sol new file mode 100644 index 000000000..dda313836 --- /dev/null +++ b/test/echidna/SSVValidatorsEchidna.sol @@ -0,0 +1,861 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVValidators.sol"; +import "../../contracts/interfaces/ISSVValidators.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/libraries/ValidatorLib.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import {PackedETH, PackedSSV, PACKED_ETH_ZERO, PACKED_SSV_ZERO} from "../../contracts/libraries/SSVCoreTypes.sol"; + + +contract ValidatorUser { + ISSVValidators public validators; + + constructor(ISSVValidators validators_) { + validators = validators_; + } + + receive() external payable {} + + function register( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + validators.registerValidator{value: msg.value}(publicKey, operatorIds, sharesData, cluster); + } + + function bulkRegister( + bytes[] calldata publicKeys, + uint64[] calldata operatorIds, + bytes[] calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + validators.bulkRegisterValidator{value: msg.value}(publicKeys, operatorIds, sharesData, cluster); + } + + function remove( + bytes calldata publicKey, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + validators.removeValidator(publicKey, operatorIds, cluster); + } + + function bulkRemove( + bytes[] calldata publicKeys, + uint64[] calldata operatorIds, + ISSVNetworkCore.Cluster memory cluster + ) external { + validators.bulkRemoveValidator(publicKeys, operatorIds, cluster); + } + + function exit(bytes calldata publicKey, uint64[] calldata operatorIds) external { + validators.exitValidator(publicKey, operatorIds); + } + + function bulkExit(bytes[] calldata publicKeys, uint64[] calldata operatorIds) external { + validators.bulkExitValidator(publicKeys, operatorIds); + } +} + +contract SSVValidatorsEchidna is SSVValidators { + using ClusterLib for ISSVNetworkCore.Cluster; + using Counters for Counters.Counter; + using ProtocolLib for StorageProtocol; + + uint8 private constant MAX_VALIDATORS = 16; + + ValidatorUser private owner1; + ValidatorUser private owner2; + ValidatorUser private attacker; + + uint64 private op1; + uint64 private op2; + uint64 private op3; + uint64 private op4; + uint64 private op5; + uint64 private op6; + uint64 private op7; + + struct ClusterRecord { + ISSVNetworkCore.Cluster cluster; + address owner; + uint8 operatorsKey; + bool exists; + } + + struct ValidatorRecord { + bytes publicKey; + address owner; + uint8 operatorsKey; + bool active; + } + + bytes32[] private clusterIds; + mapping(bytes32 => ClusterRecord) private clusters; + + uint256[] private validatorIds; + mapping(uint256 => ValidatorRecord) private validators; + mapping(bytes32 => uint256) private validatorKeyToId; + uint256 private nextValidatorId; + + mapping(uint64 => uint32) private expectedOperatorEthValidators; + + uint256 private totalExpectedBalance; + + bool private duplicateValidatorRegistered; + bool private unauthorizedRemoveSucceeded; + bool private unauthorizedExitSucceeded; + + constructor() { + ISSVValidators self = ISSVValidators(address(this)); + owner1 = new ValidatorUser(self); + owner2 = new ValidatorUser(self); + attacker = new ValidatorUser(self); + + _initProtocolDefaults(); + _initOperators(); + } + + receive() external payable {} + + function action_fund(uint256 amount) external payable { + amount; + } + + function action_register(uint256 seed, uint8 ownerSeed, uint8 operatorsSeed) external { + if (validatorIds.length >= MAX_VALIDATORS) return; + + address owner = (ownerSeed % 2 == 0) ? address(owner1) : address(owner2); + uint8 operatorsKey = operatorsSeed % 2; + uint64[] memory operatorIds = _operatorIdsForKey(operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(owner, operatorIds)); + + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + + bytes memory publicKey = _makePublicKey(seed); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, owner)); + bytes memory shares = _makeShares(seed); + + uint256 amount = _boundAmount(seed >> 8, _availableBalance()); + ValidatorUser ownerUser = _ownerUser(owner); + + if (validatorKeyToId[validatorKey] != 0) { + try ownerUser.register{value: amount}(publicKey, operatorIds, shares, cluster) { + duplicateValidatorRegistered = true; + } catch {} + return; + } + + try ownerUser.register{value: amount}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, owner, operatorsKey, cluster, publicKey, validatorKey, amount, operatorIds); + } catch {} + } + + function action_bulk_register(uint256 seed, uint8 ownerSeed, uint8 operatorsSeed) external { + uint256 remainingCapacity = MAX_VALIDATORS - validatorIds.length; + if (remainingCapacity < 2) return; + + address owner = (ownerSeed % 2 == 0) ? address(owner1) : address(owner2); + uint8 operatorsKey = operatorsSeed % 2; + uint64[] memory operatorIds = _operatorIdsForKey(operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(owner, operatorIds)); + + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + uint256 batchSize = 2 + ((seed >> 16) % 2); + if (batchSize > remainingCapacity) { + batchSize = remainingCapacity; + } + if (batchSize < 2) return; + + bytes[] memory publicKeys = new bytes[](batchSize); + bytes[] memory sharesData = new bytes[](batchSize); + bytes32[] memory validatorKeys = new bytes32[](batchSize); + + for (uint256 i; i < batchSize; ++i) { + uint256 validatorSeed = uint256(keccak256(abi.encodePacked(seed, i, owner, operatorsKey))); + bytes memory publicKey = _makePublicKey(validatorSeed); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, owner)); + if (validatorKeyToId[validatorKey] != 0) return; + + publicKeys[i] = publicKey; + sharesData[i] = _makeShares(validatorSeed); + validatorKeys[i] = validatorKey; + } + + uint256 amount = _boundAmount(seed >> 8, _availableBalance()); + ValidatorUser ownerUser = _ownerUser(owner); + + try ownerUser.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, cluster) { + _recordBulkRegistration(clusterId, owner, operatorsKey, cluster, publicKeys, validatorKeys, amount, operatorIds); + } catch {} + } + + function action_bulk_register_duplicate(uint256 seed) external { + uint256 validatorId = _pickValidatorId(seed); + if (validatorId == 0) return; + + ValidatorRecord storage duplicateRecord = validators[validatorId]; + if (!duplicateRecord.active) return; + + uint8 operatorsKey = duplicateRecord.operatorsKey; + uint64[] memory operatorIds = _operatorIdsForKey(operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(duplicateRecord.owner, operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + + bytes[] memory publicKeys = new bytes[](2); + bytes[] memory sharesData = new bytes[](2); + publicKeys[0] = duplicateRecord.publicKey; + sharesData[0] = _makeShares(seed); + + uint256 freshSeed = uint256(keccak256(abi.encodePacked(seed, duplicateRecord.owner, operatorsKey, uint256(0xB001)))); + bytes memory freshKey = _makePublicKey(freshSeed); + bytes32 freshValidatorKey = keccak256(abi.encodePacked(freshKey, duplicateRecord.owner)); + if (validatorKeyToId[freshValidatorKey] != 0) return; + + publicKeys[1] = freshKey; + sharesData[1] = _makeShares(freshSeed); + + uint256 amount = _boundAmount(seed >> 8, _availableBalance()); + ValidatorUser ownerUser = _ownerUser(duplicateRecord.owner); + + try ownerUser.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, cluster) { + duplicateValidatorRegistered = true; + } catch {} + } + + function action_remove(uint256 seed) external { + uint256 validatorId = _pickValidatorId(seed); + if (validatorId == 0) return; + + ValidatorRecord storage record = validators[validatorId]; + if (!record.active) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(record.owner, operatorIds)); + ClusterRecord storage clusterRecord = clusters[clusterId]; + if (!clusterRecord.exists) return; + + ISSVNetworkCore.Cluster memory cluster = clusterRecord.cluster; + ValidatorUser ownerUser = _ownerUser(record.owner); + + try ownerUser.remove(record.publicKey, operatorIds, cluster) { + record.active = false; + bytes32 validatorKey = keccak256(abi.encodePacked(record.publicKey, record.owner)); + if (validatorKeyToId[validatorKey] == validatorId) { + validatorKeyToId[validatorKey] = 0; + } + _recordRemoval(clusterRecord, clusterId, operatorIds); + _updateExpectedOperatorCounts(operatorIds, false); + } catch {} + } + + function action_bulk_remove(uint256 seed) external { + bytes32 clusterId = _pickBulkRemovableClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage clusterRecord = clusters[clusterId]; + if (!clusterRecord.exists) return; + + uint256[] memory activeValidatorIds = _activeValidatorIdsForCluster(clusterId); + uint256 activeCount = activeValidatorIds.length; + if (activeCount < 2) return; + + uint256 batchSize = 2 + ((seed >> 8) % 2); + if (batchSize > activeCount) { + batchSize = activeCount; + } + if (batchSize < 2) return; + + uint256 start = (seed >> 16) % activeCount; + bytes[] memory publicKeys = new bytes[](batchSize); + uint256[] memory selectedValidatorIds = new uint256[](batchSize); + + for (uint256 i; i < batchSize; ++i) { + uint256 validatorId = activeValidatorIds[(start + i) % activeCount]; + selectedValidatorIds[i] = validatorId; + publicKeys[i] = validators[validatorId].publicKey; + } + + uint64[] memory operatorIds = _operatorIdsForKey(clusterRecord.operatorsKey); + ValidatorUser ownerUser = _ownerUser(clusterRecord.owner); + + try ownerUser.bulkRemove(publicKeys, operatorIds, clusterRecord.cluster) { + for (uint256 i; i < batchSize; ++i) { + uint256 validatorId = selectedValidatorIds[i]; + ValidatorRecord storage record = validators[validatorId]; + record.active = false; + bytes32 validatorKey = keccak256(abi.encodePacked(record.publicKey, record.owner)); + if (validatorKeyToId[validatorKey] == validatorId) { + validatorKeyToId[validatorKey] = 0; + } + } + + _recordBulkRemoval(clusterRecord, clusterId, operatorIds, uint32(batchSize)); + for (uint256 i; i < batchSize; ++i) { + _updateExpectedOperatorCounts(operatorIds, false); + } + } catch {} + } + + function action_bulk_exit(uint256 seed) external { + bytes32 clusterId = _pickBulkRemovableClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage clusterRecord = clusters[clusterId]; + if (!clusterRecord.exists) return; + + uint256[] memory activeValidatorIds = _activeValidatorIdsForCluster(clusterId); + uint256 activeCount = activeValidatorIds.length; + if (activeCount < 2) return; + + uint256 batchSize = 2 + ((seed >> 8) % 2); + if (batchSize > activeCount) { + batchSize = activeCount; + } + if (batchSize < 2) return; + + uint256 start = (seed >> 16) % activeCount; + bytes[] memory publicKeys = new bytes[](batchSize); + for (uint256 i; i < batchSize; ++i) { + uint256 validatorId = activeValidatorIds[(start + i) % activeCount]; + publicKeys[i] = validators[validatorId].publicKey; + } + + uint64[] memory operatorIds = _operatorIdsForKey(clusterRecord.operatorsKey); + ValidatorUser ownerUser = _ownerUser(clusterRecord.owner); + try ownerUser.bulkExit(publicKeys, operatorIds) {} catch {} + } + + function action_exit_unauthorized(uint256 seed) external { + uint256 validatorId = _pickValidatorId(seed); + if (validatorId == 0) return; + + ValidatorRecord storage record = validators[validatorId]; + if (!record.active) return; + if (record.owner == address(attacker)) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + try attacker.exit(record.publicKey, operatorIds) { + unauthorizedExitSucceeded = true; + } catch {} + } + + function action_bulk_exit_unauthorized(uint256 seed) external { + bytes32 clusterId = _pickBulkRemovableClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage clusterRecord = clusters[clusterId]; + if (!clusterRecord.exists || clusterRecord.owner == address(attacker)) return; + + uint256[] memory activeValidatorIds = _activeValidatorIdsForCluster(clusterId); + uint256 activeCount = activeValidatorIds.length; + if (activeCount < 2) return; + + uint256 batchSize = 2 + ((seed >> 8) % 2); + if (batchSize > activeCount) { + batchSize = activeCount; + } + if (batchSize < 2) return; + + uint256 start = (seed >> 16) % activeCount; + bytes[] memory publicKeys = new bytes[](batchSize); + for (uint256 i; i < batchSize; ++i) { + uint256 validatorId = activeValidatorIds[(start + i) % activeCount]; + publicKeys[i] = validators[validatorId].publicKey; + } + + uint64[] memory operatorIds = _operatorIdsForKey(clusterRecord.operatorsKey); + try attacker.bulkExit(publicKeys, operatorIds) { + unauthorizedExitSucceeded = true; + } catch {} + } + + function action_remove_unauthorized(uint256 seed) external { + uint256 validatorId = _pickValidatorId(seed); + if (validatorId == 0) return; + + ValidatorRecord storage record = validators[validatorId]; + if (!record.active) return; + if (record.owner == address(attacker)) return; + + uint64[] memory operatorIds = _operatorIdsForKey(record.operatorsKey); + bytes32 clusterId = keccak256(abi.encodePacked(record.owner, operatorIds)); + ClusterRecord storage clusterRecord = clusters[clusterId]; + if (!clusterRecord.exists) return; + + try attacker.remove(record.publicKey, operatorIds, clusterRecord.cluster) { + unauthorizedRemoveSucceeded = true; + } catch {} + } + + function action_bulk_remove_unauthorized(uint256 seed) external { + bytes32 clusterId = _pickBulkRemovableClusterId(seed); + if (clusterId == bytes32(0)) return; + + ClusterRecord storage clusterRecord = clusters[clusterId]; + if (!clusterRecord.exists || clusterRecord.owner == address(attacker)) return; + + uint256[] memory activeValidatorIds = _activeValidatorIdsForCluster(clusterId); + uint256 activeCount = activeValidatorIds.length; + if (activeCount < 2) return; + + uint256 batchSize = 2 + ((seed >> 8) % 2); + if (batchSize > activeCount) { + batchSize = activeCount; + } + if (batchSize < 2) return; + + uint256 start = (seed >> 16) % activeCount; + bytes[] memory publicKeys = new bytes[](batchSize); + for (uint256 i; i < batchSize; ++i) { + uint256 validatorId = activeValidatorIds[(start + i) % activeCount]; + publicKeys[i] = validators[validatorId].publicKey; + } + + uint64[] memory operatorIds = _operatorIdsForKey(clusterRecord.operatorsKey); + try attacker.bulkRemove(publicKeys, operatorIds, clusterRecord.cluster) { + unauthorizedRemoveSucceeded = true; + } catch {} + } + + function echidna_validator_hash_consistent() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + uint256 count = validatorIds.length; + for (uint256 i; i < count; ++i) { + ValidatorRecord storage record = validators[validatorIds[i]]; + bytes32 validatorKey = keccak256(abi.encodePacked(record.publicKey, record.owner)); + bytes32 stored = s.validatorPKs[validatorKey]; + if (record.active) { + if (stored == bytes32(0)) return false; + bytes32 hashedOperatorIds = ValidatorLib.hashOperatorIds(_operatorIdsForKey(record.operatorsKey)); + if (!ValidatorLib.validateCorrectState(stored, hashedOperatorIds)) return false; + } else { + if (stored != bytes32(0)) { + uint256 activeId = validatorKeyToId[validatorKey]; + if (activeId == 0) return false; + if (activeId == validatorIds[i]) return false; + ValidatorRecord storage activeRecord = validators[activeId]; + if (!activeRecord.active) return false; + bytes32 activeKey = keccak256(abi.encodePacked(activeRecord.publicKey, activeRecord.owner)); + if (activeKey != validatorKey) return false; + bytes32 hashedOperatorIds = ValidatorLib.hashOperatorIds(_operatorIdsForKey(activeRecord.operatorsKey)); + if (!ValidatorLib.validateCorrectState(stored, hashedOperatorIds)) return false; + } + } + } + return true; + } + + function echidna_cluster_hash_consistent() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[i]; + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists) return false; + if (record.owner == address(0)) return false; + if (s.ethClusters[clusterId] != record.cluster.hashClusterData()) return false; + } + return true; + } + + function echidna_cluster_validator_counts() external view returns (bool) { + uint256 clustersCount = clusterIds.length; + for (uint256 i; i < clustersCount; ++i) { + bytes32 clusterId = clusterIds[i]; + ClusterRecord storage clusterRecord = clusters[clusterId]; + if (!clusterRecord.exists) return false; + uint32 count = 0; + uint256 validatorsCount = validatorIds.length; + for (uint256 j; j < validatorsCount; ++j) { + ValidatorRecord storage record = validators[validatorIds[j]]; + if (!record.active) continue; + bytes32 recordCluster = keccak256( + abi.encodePacked(record.owner, _operatorIdsForKey(record.operatorsKey)) + ); + if (recordCluster == clusterId) { + count += 1; + } + } + if (clusterRecord.cluster.validatorCount != count) return false; + } + return true; + } + + function echidna_operator_validator_counts() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + return s.operators[op1].ethValidatorCount == expectedOperatorEthValidators[op1] && + s.operators[op2].ethValidatorCount == expectedOperatorEthValidators[op2] && + s.operators[op3].ethValidatorCount == expectedOperatorEthValidators[op3] && + s.operators[op4].ethValidatorCount == expectedOperatorEthValidators[op4] && + s.operators[op5].ethValidatorCount == expectedOperatorEthValidators[op5] && + s.operators[op6].ethValidatorCount == expectedOperatorEthValidators[op6] && + s.operators[op7].ethValidatorCount == expectedOperatorEthValidators[op7]; + } + + function echidna_cluster_balance_accounting() external view returns (bool) { + if (address(this).balance < totalExpectedBalance) return false; + uint256 sum = 0; + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + ClusterRecord storage record = clusters[clusterIds[i]]; + if (!record.exists) return false; + sum += record.cluster.balance; + } + return sum == totalExpectedBalance; + } + + function echidna_no_duplicate_validators() external view returns (bool) { + return !duplicateValidatorRegistered; + } + + function echidna_owner_only_remove() external view returns (bool) { + return !unauthorizedRemoveSucceeded; + } + + function echidna_owner_only_exit() external view returns (bool) { + return !unauthorizedExitSucceeded; + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 5000; + sp.ethNetworkFee = PACKED_ETH_ZERO; + sp.ethNetworkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = 0; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + } + + function _initOperators() internal { + StorageData storage s = SSVStorage.load(); + op1 = _createOperator(s, bytes32(uint256(0x1))); + op2 = _createOperator(s, bytes32(uint256(0x2))); + op3 = _createOperator(s, bytes32(uint256(0x3))); + op4 = _createOperator(s, bytes32(uint256(0x4))); + op5 = _createOperator(s, bytes32(uint256(0x5))); + op6 = _createOperator(s, bytes32(uint256(0x6))); + op7 = _createOperator(s, bytes32(uint256(0x7))); + } + + function _createOperator(StorageData storage s, bytes32 pk) internal returns (uint64) { + s.lastOperatorId.increment(); + uint64 id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PACKED_SSV_ZERO, + owner: address(this), + snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: PACKED_SSV_ZERO}), + whitelisted: false, + ethValidatorCount: 0, + ethFee: PACKED_ETH_ZERO, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: uint32(block.number), index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(abi.encodePacked(pk))] = id; + return id; + } + + function _getClusterForRegistration(bytes32 clusterId) internal view returns (ISSVNetworkCore.Cluster memory cluster) { + ClusterRecord storage record = clusters[clusterId]; + if (record.exists) { + return record.cluster; + } + return ISSVNetworkCore.Cluster({ + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + active: true, + balance: 0 + }); + } + + function _recordRegistration( + bytes32 clusterId, + address owner, + uint8 operatorsKey, + ISSVNetworkCore.Cluster memory cluster, + bytes memory publicKey, + bytes32 validatorKey, + uint256 amount, + uint64[] memory operatorIds + ) internal { + ClusterRecord storage record = clusters[clusterId]; + bool existed = record.exists; + uint256 previousBalance = existed ? record.cluster.balance : 0; + + cluster.balance += amount; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _clusterIndexFromStorage(operatorIds, s); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + + cluster.updateClusterData(clusterId, clusterIndex, networkFeeIndex); + cluster.validatorCount += 1; + cluster.active = true; + + totalExpectedBalance = totalExpectedBalance - previousBalance + cluster.balance; + _updateExpectedOperatorCounts(operatorIds, true); + + if (!existed) { + record.owner = owner; + record.operatorsKey = operatorsKey; + record.exists = true; + clusterIds.push(clusterId); + } + record.cluster = cluster; + + nextValidatorId += 1; + validators[nextValidatorId] = ValidatorRecord({ + publicKey: publicKey, + owner: owner, + operatorsKey: operatorsKey, + active: true + }); + validatorIds.push(nextValidatorId); + validatorKeyToId[validatorKey] = nextValidatorId; + } + + function _recordBulkRegistration( + bytes32 clusterId, + address owner, + uint8 operatorsKey, + ISSVNetworkCore.Cluster memory cluster, + bytes[] memory publicKeys, + bytes32[] memory validatorKeys, + uint256 amount, + uint64[] memory operatorIds + ) internal { + ClusterRecord storage record = clusters[clusterId]; + bool existed = record.exists; + uint256 previousBalance = existed ? record.cluster.balance : 0; + uint32 validatorsAdded = uint32(publicKeys.length); + + cluster.balance += amount; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _clusterIndexFromStorage(operatorIds, s); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + + cluster.updateClusterData(clusterId, clusterIndex, networkFeeIndex); + cluster.validatorCount += validatorsAdded; + cluster.active = true; + + totalExpectedBalance = totalExpectedBalance - previousBalance + cluster.balance; + for (uint256 i; i < validatorsAdded; ++i) { + _updateExpectedOperatorCounts(operatorIds, true); + } + + if (!existed) { + record.owner = owner; + record.operatorsKey = operatorsKey; + record.exists = true; + clusterIds.push(clusterId); + } + record.cluster = cluster; + + for (uint256 i; i < validatorsAdded; ++i) { + nextValidatorId += 1; + validators[nextValidatorId] = ValidatorRecord({ + publicKey: publicKeys[i], + owner: owner, + operatorsKey: operatorsKey, + active: true + }); + validatorIds.push(nextValidatorId); + validatorKeyToId[validatorKeys[i]] = nextValidatorId; + } + } + + function _recordRemoval(ClusterRecord storage record, bytes32 clusterId, uint64[] memory operatorIds) internal { + if (!record.exists) return; + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + uint256 previousBalance = cluster.balance; + + if (cluster.active) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _clusterIndexFromStorage(operatorIds, s); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + cluster.updateClusterData(clusterId, clusterIndex, networkFeeIndex); + } + + if (cluster.validatorCount > 0) { + cluster.validatorCount -= 1; + } + + record.cluster = cluster; + totalExpectedBalance = totalExpectedBalance - previousBalance + cluster.balance; + } + + function _recordBulkRemoval( + ClusterRecord storage record, + bytes32 clusterId, + uint64[] memory operatorIds, + uint32 validatorsRemoved + ) internal { + if (!record.exists) return; + + ISSVNetworkCore.Cluster memory cluster = record.cluster; + uint256 previousBalance = cluster.balance; + + if (cluster.active) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _clusterIndexFromStorage(operatorIds, s); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + cluster.updateClusterData(clusterId, clusterIndex, networkFeeIndex); + } + + if (cluster.validatorCount >= validatorsRemoved) { + cluster.validatorCount -= validatorsRemoved; + } else { + cluster.validatorCount = 0; + } + + record.cluster = cluster; + totalExpectedBalance = totalExpectedBalance - previousBalance + cluster.balance; + } + + function _updateExpectedOperatorCounts(uint64[] memory operatorIds, bool increase) internal { + uint256 len = operatorIds.length; + for (uint256 i; i < len; ++i) { + uint64 operatorId = operatorIds[i]; + if (increase) { + expectedOperatorEthValidators[operatorId] += 1; + } else if (expectedOperatorEthValidators[operatorId] > 0) { + expectedOperatorEthValidators[operatorId] -= 1; + } + } + } + + function _operatorIdsForKey(uint8 key) internal view returns (uint64[] memory) { + if (key == 0) { + uint64[] memory ids = new uint64[](4); + ids[0] = op1; + ids[1] = op2; + ids[2] = op3; + ids[3] = op4; + return ids; + } + uint64[] memory ids = new uint64[](7); + ids[0] = op1; + ids[1] = op2; + ids[2] = op3; + ids[3] = op4; + ids[4] = op5; + ids[5] = op6; + ids[6] = op7; + return ids; + } + + function _clusterIndexFromStorage( + uint64[] memory operatorIds, + StorageData storage s + ) internal view returns (uint64) { + uint256 len = operatorIds.length; + uint64 clusterIndex = 0; + for (uint256 i; i < len; ++i) { + clusterIndex += s.operators[operatorIds[i]].ethSnapshot.index; + } + return clusterIndex; + } + + function _pickValidatorId(uint256 seed) internal view returns (uint256) { + uint256 count = validatorIds.length; + if (count == 0) return 0; + return validatorIds[seed % count]; + } + + function _pickBulkRemovableClusterId(uint256 seed) internal view returns (bytes32) { + uint256 count = clusterIds.length; + if (count == 0) return bytes32(0); + + uint256 start = seed % count; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[(start + i) % count]; + if (_activeValidatorCountForCluster(clusterId) >= 2) { + return clusterId; + } + } + return bytes32(0); + } + + function _activeValidatorCountForCluster(bytes32 clusterId) internal view returns (uint256 count) { + uint256 validatorsCount = validatorIds.length; + for (uint256 i; i < validatorsCount; ++i) { + ValidatorRecord storage record = validators[validatorIds[i]]; + if (!record.active) continue; + bytes32 recordCluster = keccak256(abi.encodePacked(record.owner, _operatorIdsForKey(record.operatorsKey))); + if (recordCluster == clusterId) { + count += 1; + } + } + } + + function _activeValidatorIdsForCluster(bytes32 clusterId) internal view returns (uint256[] memory activeIds) { + uint256 count = _activeValidatorCountForCluster(clusterId); + activeIds = new uint256[](count); + if (count == 0) return activeIds; + + uint256 next; + uint256 validatorsCount = validatorIds.length; + for (uint256 i; i < validatorsCount; ++i) { + uint256 validatorId = validatorIds[i]; + ValidatorRecord storage record = validators[validatorId]; + if (!record.active) continue; + bytes32 recordCluster = keccak256(abi.encodePacked(record.owner, _operatorIdsForKey(record.operatorsKey))); + if (recordCluster == clusterId) { + activeIds[next] = validatorId; + next += 1; + } + } + } + + function _ownerUser(address owner) internal view returns (ValidatorUser) { + if (owner == address(owner1)) return owner1; + if (owner == address(owner2)) return owner2; + return attacker; + } + + function _makePublicKey(uint256 seed) internal pure returns (bytes memory) { + bytes32 h1 = keccak256(abi.encodePacked(seed)); + bytes32 h2 = keccak256(abi.encodePacked(seed, h1)); + bytes memory b1 = abi.encodePacked(h1); + bytes memory b2 = abi.encodePacked(h2); + bytes memory pk = new bytes(48); + for (uint256 i; i < 32; ++i) { + pk[i] = b1[i]; + } + for (uint256 i; i < 16; ++i) { + pk[32 + i] = b2[i]; + } + return pk; + } + + function _makeShares(uint256 seed) internal pure returns (bytes memory) { + return abi.encodePacked(uint64(seed)); + } + + function _availableBalance() internal view returns (uint256) { + if (address(this).balance <= totalExpectedBalance) return 0; + return address(this).balance - totalExpectedBalance; + } + + function _boundAmount(uint256 seed, uint256 maxValue) internal pure returns (uint256) { + if (maxValue == 0) return 0; + return seed % (maxValue + 1); + } +} diff --git a/test/echidna/SSVWhitelistValidatorsEchidna.sol b/test/echidna/SSVWhitelistValidatorsEchidna.sol new file mode 100644 index 000000000..bcf4db0e1 --- /dev/null +++ b/test/echidna/SSVWhitelistValidatorsEchidna.sol @@ -0,0 +1,1122 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +import "../../contracts/modules/SSVValidators.sol"; +import "../../contracts/modules/SSVOperators.sol"; +import "../../contracts/modules/SSVOperatorsWhitelist.sol"; +import "../../contracts/interfaces/ISSVValidators.sol"; +import "../../contracts/interfaces/ISSVOperators.sol"; +import "../../contracts/interfaces/ISSVOperatorsWhitelist.sol"; +import "../../contracts/interfaces/ISSVNetworkCore.sol"; +import "../../contracts/interfaces/external/ISSVWhitelistingContract.sol"; +import "../../contracts/test/mocks/MockWhitelistingContract.sol"; +import "../../contracts/libraries/ClusterLib.sol"; +import "../../contracts/libraries/ProtocolLib.sol"; +import "../../contracts/libraries/ValidatorLib.sol"; +import "../../contracts/libraries/storage/SSVStorage.sol"; +import "../../contracts/libraries/storage/SSVStorageProtocol.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import { + PackedETH, + PackedSSV, + PACKED_ETH_ZERO, + PACKED_SSV_ZERO, + ETH_DEDUCTED_DIGITS, + BPS_DENOMINATOR, + DEFAULT_OPERATOR_ETH_FEE +} from "../../contracts/libraries/SSVCoreTypes.sol"; + +contract WhitelistClusterUser { + ISSVValidators public validators; + + constructor(ISSVValidators validators_) { + validators = validators_; + } + + receive() external payable {} + + function register( + bytes calldata publicKey, + uint64[] calldata operatorIds, + bytes calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + validators.registerValidator{value: msg.value}(publicKey, operatorIds, sharesData, cluster); + } + + function bulkRegister( + bytes[] calldata publicKeys, + uint64[] calldata operatorIds, + bytes[] calldata sharesData, + ISSVNetworkCore.Cluster memory cluster + ) external payable { + validators.bulkRegisterValidator{value: msg.value}(publicKeys, operatorIds, sharesData, cluster); + } +} + +contract WhitelistOperatorOwner { + ISSVOperators public operators; + ISSVOperatorsWhitelist public operatorsWhitelist; + + constructor(address target) { + operators = ISSVOperators(target); + operatorsWhitelist = ISSVOperatorsWhitelist(target); + } + + function register(bytes calldata publicKey, uint256 fee, bool setPrivate) external returns (uint64) { + return operators.registerOperator(publicKey, fee, setPrivate); + } + + function whitelist(uint64 operatorId, address whitelistAddress) external { + uint64[] memory operatorIds = new uint64[](1); + operatorIds[0] = operatorId; + address[] memory whitelistAddresses = new address[](1); + whitelistAddresses[0] = whitelistAddress; + operatorsWhitelist.setOperatorsWhitelists(operatorIds, whitelistAddresses); + } + + function removeWhitelist(uint64 operatorId, address whitelistAddress) external { + uint64[] memory operatorIds = new uint64[](1); + operatorIds[0] = operatorId; + address[] memory whitelistAddresses = new address[](1); + whitelistAddresses[0] = whitelistAddress; + operatorsWhitelist.removeOperatorsWhitelists(operatorIds, whitelistAddresses); + } + + function whitelistContract(uint64 operatorId, ISSVWhitelistingContract whitelistingContract) external { + uint64[] memory operatorIds = new uint64[](1); + operatorIds[0] = operatorId; + operatorsWhitelist.setOperatorsWhitelistingContract(operatorIds, whitelistingContract); + } + + function removeWhitelistContract(uint64 operatorId) external { + uint64[] memory operatorIds = new uint64[](1); + operatorIds[0] = operatorId; + operatorsWhitelist.removeOperatorsWhitelistingContract(operatorIds); + } + + function reduceFee(uint64 operatorId, uint256 fee) external { + operators.reduceOperatorFee(operatorId, fee); + } + + function setPrivate(uint64 operatorId) external { + uint64[] memory operatorIds = new uint64[](1); + operatorIds[0] = operatorId; + operators.setOperatorsPrivateUnchecked(operatorIds); + } + + function setPublic(uint64 operatorId) external { + uint64[] memory operatorIds = new uint64[](1); + operatorIds[0] = operatorId; + operators.setOperatorsPublicUnchecked(operatorIds); + } +} + +contract SSVWhitelistValidatorsEchidna is SSVValidators, SSVOperators(0), SSVOperatorsWhitelist { + using ClusterLib for ISSVNetworkCore.Cluster; + using ProtocolLib for StorageProtocol; + using Counters for Counters.Counter; + + uint256 private constant REGISTRATION_AMOUNT = 1 ether; + uint256 private constant PUBLIC_FEE_1 = 10_000_000; + uint256 private constant PUBLIC_FEE_2 = 20_000_000; + uint256 private constant PUBLIC_FEE_3 = 30_000_000; + uint256 private constant PRIVATE_FEE = 15_000_000; + + WhitelistClusterUser private eoaWhitelistedUser; + WhitelistClusterUser private eoaWhitelistedBulkUser; + WhitelistClusterUser private contractWhitelistedUser; + WhitelistClusterUser private contractWhitelistedBulkUser; + WhitelistClusterUser private attacker; + + WhitelistOperatorOwner private publicOwner1; + WhitelistOperatorOwner private publicOwner2; + WhitelistOperatorOwner private publicOwner3; + WhitelistOperatorOwner private privateZeroOwner; + WhitelistOperatorOwner private privateFeeOwner; + WhitelistOperatorOwner private privateToggleOwner; + WhitelistOperatorOwner private privateMutationOwner; + WhitelistOperatorOwner private legacyOwner; + + MockWhitelistingContract private mockWhitelistContract; + + uint64 private publicOp1; + uint64 private publicOp2; + uint64 private publicOp3; + uint64 private privateZeroOp; + uint64 private privateFeeOp; + uint64 private privateToggleOp; + uint64 private privateMutationOp; + uint64 private legacyPrivateOp; + + struct ClusterRecord { + ISSVNetworkCore.Cluster cluster; + address owner; + uint8 scenario; + bool exists; + } + + bytes32[] private clusterIds; + mapping(bytes32 => ClusterRecord) private clusters; + mapping(uint64 => uint32) private expectedOperatorEthValidators; + + uint256 private totalExpectedBalance; + uint32 private expectedTotalValidators; + bool private unauthorizedPrivateRegistrationSucceeded; + bool private mixedZeroFeeViolation; + bool private contractWhitelistViolation; + bool private legacyWhitelistViolation; + bool private whitelistMutationFeeViolation; + bool private privacyToggleViolation; + bool private legacyFeePrepared; + bool private mixedZeroScenarioDone; + bool private contractWhitelistScenarioDone; + bool private legacyScenarioDone; + bool private mixedZeroBulkScenarioDone; + bool private contractWhitelistBulkScenarioDone; + bool private legacyBulkScenarioDone; + bool private whitelistMutationFeeScenarioDone; + bool private privacyToggleScenarioDone; + uint256 private nextPkNonce; + + constructor() { + ISSVValidators validatorsSelf = ISSVValidators(address(this)); + + eoaWhitelistedUser = new WhitelistClusterUser(validatorsSelf); + eoaWhitelistedBulkUser = new WhitelistClusterUser(validatorsSelf); + contractWhitelistedUser = new WhitelistClusterUser(validatorsSelf); + contractWhitelistedBulkUser = new WhitelistClusterUser(validatorsSelf); + attacker = new WhitelistClusterUser(validatorsSelf); + + publicOwner1 = new WhitelistOperatorOwner(address(this)); + publicOwner2 = new WhitelistOperatorOwner(address(this)); + publicOwner3 = new WhitelistOperatorOwner(address(this)); + privateZeroOwner = new WhitelistOperatorOwner(address(this)); + privateFeeOwner = new WhitelistOperatorOwner(address(this)); + privateToggleOwner = new WhitelistOperatorOwner(address(this)); + privateMutationOwner = new WhitelistOperatorOwner(address(this)); + legacyOwner = new WhitelistOperatorOwner(address(this)); + + address[] memory initialWhitelisted = new address[](1); + initialWhitelisted[0] = address(contractWhitelistedUser); + mockWhitelistContract = new MockWhitelistingContract(initialWhitelisted); + mockWhitelistContract.setWhitelistedAddress(address(contractWhitelistedBulkUser)); + + _initProtocolDefaults(); + _initOperators(); + } + + receive() external payable {} + + function action_fund(uint256 amount) external payable { + amount; + } + + function action_register_mixed_zero_fee_authorized(uint256 seed) external { + seed; + if (mixedZeroScenarioDone) return; + uint256 available = _availableBalance(); + if (available < REGISTRATION_AMOUNT) return; + + uint64[] memory operatorIds = _mixedZeroFeeOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(eoaWhitelistedUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(eoaWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try eoaWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(eoaWhitelistedUser), 0, cluster, REGISTRATION_AMOUNT, operatorIds); + mixedZeroScenarioDone = true; + if (!_validatorStoredActive(validatorKey, operatorIds)) mixedZeroFeeViolation = true; + if (PackedETH.unwrap(SSVStorage.load().operators[privateZeroOp].ethFee) != 0) mixedZeroFeeViolation = true; + } catch { + mixedZeroFeeViolation = true; + } + } + + function action_register_mixed_zero_fee_unauthorized(uint256 seed) external { + seed; + uint256 available = _availableBalance(); + if (available < REGISTRATION_AMOUNT) return; + + uint64[] memory operatorIds = _mixedZeroFeeOperatorIds(); + bytes memory publicKey = _newPublicKey(); + bytes memory shares = _makeShares(nextPkNonce); + ISSVNetworkCore.Cluster memory cluster = _defaultCluster(); + + try contractWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + unauthorizedPrivateRegistrationSucceeded = true; + } catch {} + } + + function action_bulk_register_mixed_zero_fee_authorized(uint256 seed) external { + if (mixedZeroBulkScenarioDone) return; + + uint256 batchSize = _bulkBatchSize(seed); + uint256 amount = batchSize * REGISTRATION_AMOUNT; + uint256 available = _availableBalance(); + if (available < amount) return; + + uint64[] memory operatorIds = _mixedZeroFeeOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(eoaWhitelistedBulkUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + (bytes[] memory publicKeys, bytes[] memory sharesData, bytes32[] memory validatorKeys) = + _newBulkPayload(batchSize, address(eoaWhitelistedBulkUser)); + + try eoaWhitelistedBulkUser.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, cluster) { + _recordBulkRegistration(clusterId, address(eoaWhitelistedBulkUser), 0, cluster, amount, operatorIds, uint32(batchSize)); + mixedZeroBulkScenarioDone = true; + if (!_validatorsStoredActive(validatorKeys, operatorIds)) mixedZeroFeeViolation = true; + if (PackedETH.unwrap(SSVStorage.load().operators[privateZeroOp].ethFee) != 0) mixedZeroFeeViolation = true; + } catch { + mixedZeroFeeViolation = true; + } + } + + function action_bulk_register_mixed_zero_fee_unauthorized(uint256 seed) external { + uint256 batchSize = _bulkBatchSize(seed); + uint256 amount = batchSize * REGISTRATION_AMOUNT; + uint256 available = _availableBalance(); + if (available < amount) return; + + uint64[] memory operatorIds = _mixedZeroFeeOperatorIds(); + (bytes[] memory publicKeys, bytes[] memory sharesData,) = _newBulkPayload(batchSize, address(attacker)); + + try attacker.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, _defaultCluster()) { + unauthorizedPrivateRegistrationSucceeded = true; + } catch {} + } + + function action_register_contract_whitelist_authorized(uint256 seed) external { + seed; + if (contractWhitelistScenarioDone) return; + uint256 available = _availableBalance(); + if (available < REGISTRATION_AMOUNT) return; + + uint64[] memory operatorIds = _contractWhitelistOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(contractWhitelistedUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(contractWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try contractWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(contractWhitelistedUser), 1, cluster, REGISTRATION_AMOUNT, operatorIds); + contractWhitelistScenarioDone = true; + if (!_validatorStoredActive(validatorKey, operatorIds)) contractWhitelistViolation = true; + if (PackedETH.unwrap(SSVStorage.load().operators[privateFeeOp].ethFee) == 0) contractWhitelistViolation = true; + } catch { + contractWhitelistViolation = true; + } + } + + function action_register_contract_whitelist_unauthorized(uint256 seed) external { + seed; + uint256 available = _availableBalance(); + if (available < REGISTRATION_AMOUNT) return; + + uint64[] memory operatorIds = _contractWhitelistOperatorIds(); + bytes memory publicKey = _newPublicKey(); + bytes memory shares = _makeShares(nextPkNonce); + ISSVNetworkCore.Cluster memory cluster = _defaultCluster(); + + try eoaWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + unauthorizedPrivateRegistrationSucceeded = true; + } catch {} + } + + function action_bulk_register_contract_whitelist_authorized(uint256 seed) external { + if (contractWhitelistBulkScenarioDone) return; + + uint256 batchSize = _bulkBatchSize(seed); + uint256 amount = batchSize * REGISTRATION_AMOUNT; + uint256 available = _availableBalance(); + if (available < amount) return; + + uint64[] memory operatorIds = _contractWhitelistOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(contractWhitelistedBulkUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + (bytes[] memory publicKeys, bytes[] memory sharesData, bytes32[] memory validatorKeys) = + _newBulkPayload(batchSize, address(contractWhitelistedBulkUser)); + + try contractWhitelistedBulkUser.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, cluster) { + _recordBulkRegistration(clusterId, address(contractWhitelistedBulkUser), 1, cluster, amount, operatorIds, uint32(batchSize)); + contractWhitelistBulkScenarioDone = true; + if (!_validatorsStoredActive(validatorKeys, operatorIds)) contractWhitelistViolation = true; + if (PackedETH.unwrap(SSVStorage.load().operators[privateFeeOp].ethFee) == 0) contractWhitelistViolation = true; + } catch { + contractWhitelistViolation = true; + } + } + + function action_bulk_register_contract_whitelist_unauthorized(uint256 seed) external { + uint256 batchSize = _bulkBatchSize(seed); + uint256 amount = batchSize * REGISTRATION_AMOUNT; + uint256 available = _availableBalance(); + if (available < amount) return; + + uint64[] memory operatorIds = _contractWhitelistOperatorIds(); + (bytes[] memory publicKeys, bytes[] memory sharesData,) = _newBulkPayload(batchSize, address(attacker)); + + try attacker.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, _defaultCluster()) { + unauthorizedPrivateRegistrationSucceeded = true; + } catch {} + } + + function action_register_legacy_private_authorized(uint256 seed) external { + seed; + if (legacyScenarioDone) return; + uint256 available = _availableBalance(); + if (available < REGISTRATION_AMOUNT) return; + if (!_prepareLegacyPrivateZeroFee()) return; + + uint64[] memory operatorIds = _legacyPrivateOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(eoaWhitelistedUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(eoaWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try eoaWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(eoaWhitelistedUser), 2, cluster, REGISTRATION_AMOUNT, operatorIds); + legacyScenarioDone = true; + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[legacyPrivateOp]; + if (!_validatorStoredActive(validatorKey, operatorIds)) legacyWhitelistViolation = true; + if (operator.ethSnapshot.block == 0) legacyWhitelistViolation = true; + if (PackedETH.unwrap(operator.ethFee) != 0) legacyWhitelistViolation = true; + } catch { + legacyWhitelistViolation = true; + } + } + + function action_register_legacy_private_unauthorized(uint256 seed) external { + seed; + uint256 available = _availableBalance(); + if (available < REGISTRATION_AMOUNT) return; + if (!_prepareLegacyPrivateZeroFee()) return; + + uint64[] memory operatorIds = _legacyPrivateOperatorIds(); + bytes memory publicKey = _newPublicKey(); + bytes memory shares = _makeShares(nextPkNonce); + ISSVNetworkCore.Cluster memory cluster = _defaultCluster(); + + try contractWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + unauthorizedPrivateRegistrationSucceeded = true; + } catch {} + } + + function action_bulk_register_legacy_private_authorized(uint256 seed) external { + if (legacyBulkScenarioDone) return; + + uint256 batchSize = _bulkBatchSize(seed); + uint256 amount = batchSize * REGISTRATION_AMOUNT; + uint256 available = _availableBalance(); + if (available < amount) return; + if (!_prepareLegacyPrivateZeroFee()) return; + + uint64[] memory operatorIds = _legacyPrivateOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(eoaWhitelistedBulkUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + (bytes[] memory publicKeys, bytes[] memory sharesData, bytes32[] memory validatorKeys) = + _newBulkPayload(batchSize, address(eoaWhitelistedBulkUser)); + + try eoaWhitelistedBulkUser.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, cluster) { + _recordBulkRegistration(clusterId, address(eoaWhitelistedBulkUser), 2, cluster, amount, operatorIds, uint32(batchSize)); + legacyBulkScenarioDone = true; + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[legacyPrivateOp]; + if (!_validatorsStoredActive(validatorKeys, operatorIds)) legacyWhitelistViolation = true; + if (operator.ethSnapshot.block == 0) legacyWhitelistViolation = true; + if (PackedETH.unwrap(operator.ethFee) != 0) legacyWhitelistViolation = true; + } catch { + legacyWhitelistViolation = true; + } + } + + function action_bulk_register_legacy_private_unauthorized(uint256 seed) external { + uint256 batchSize = _bulkBatchSize(seed); + uint256 amount = batchSize * REGISTRATION_AMOUNT; + uint256 available = _availableBalance(); + if (available < amount) return; + if (!_prepareLegacyPrivateZeroFee()) return; + + uint64[] memory operatorIds = _legacyPrivateOperatorIds(); + (bytes[] memory publicKeys, bytes[] memory sharesData,) = _newBulkPayload(batchSize, address(attacker)); + + try attacker.bulkRegister{value: amount}(publicKeys, operatorIds, sharesData, _defaultCluster()) { + unauthorizedPrivateRegistrationSucceeded = true; + } catch {} + } + + function action_mutate_whitelist_and_reduce_fee_after_use(uint256 seed) external { + seed; + if (whitelistMutationFeeScenarioDone) return; + uint256 available = _availableBalance(); + if (available < 2 * REGISTRATION_AMOUNT) return; + if (!_ensureMutationOperatorUsed()) return; + + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[privateMutationOp]; + if (operator.ethValidatorCount == 0) { + whitelistMutationFeeViolation = true; + return; + } + + try privateMutationOwner.removeWhitelistContract(privateMutationOp) {} catch { + whitelistMutationFeeViolation = true; + return; + } + try privateMutationOwner.whitelist(privateMutationOp, address(eoaWhitelistedUser)) {} catch { + whitelistMutationFeeViolation = true; + return; + } + try privateMutationOwner.reduceFee(privateMutationOp, 0) { + if (PackedETH.unwrap(SSVStorage.load().operators[privateMutationOp].ethFee) != 0) { + whitelistMutationFeeViolation = true; + return; + } + } catch { + whitelistMutationFeeViolation = true; + return; + } + + uint64[] memory operatorIds = _mutationOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(eoaWhitelistedUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(eoaWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try eoaWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(eoaWhitelistedUser), 3, cluster, REGISTRATION_AMOUNT, operatorIds); + if (!_validatorStoredActive(validatorKey, operatorIds)) { + whitelistMutationFeeViolation = true; + return; + } + } catch { + whitelistMutationFeeViolation = true; + return; + } + + bytes memory unauthorizedPk = _newPublicKey(); + bytes memory unauthorizedShares = _makeShares(nextPkNonce); + try contractWhitelistedBulkUser.register{value: REGISTRATION_AMOUNT}( + unauthorizedPk, operatorIds, unauthorizedShares, _defaultCluster() + ) { + unauthorizedPrivateRegistrationSucceeded = true; + return; + } catch {} + + whitelistMutationFeeScenarioDone = true; + } + + function action_toggle_privacy_after_use(uint256 seed) external { + seed; + if (privacyToggleScenarioDone) return; + uint256 available = _availableBalance(); + if (available < 3 * REGISTRATION_AMOUNT) return; + if (!_ensureToggleOperatorUsed()) return; + + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[privateToggleOp]; + if (operator.ethValidatorCount == 0) { + privacyToggleViolation = true; + return; + } + + try privateToggleOwner.setPublic(privateToggleOp) { + if (SSVStorage.load().operators[privateToggleOp].whitelisted) { + privacyToggleViolation = true; + return; + } + } catch { + privacyToggleViolation = true; + return; + } + + uint64[] memory operatorIds = _toggleOperatorIds(); + bytes32 publicClusterId = keccak256(abi.encodePacked(address(attacker), operatorIds)); + ISSVNetworkCore.Cluster memory publicCluster = _getClusterForRegistration(publicClusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 publicValidatorKey = keccak256(abi.encodePacked(publicKey, address(attacker))); + bytes memory publicShares = _makeShares(nextPkNonce); + + try attacker.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, publicShares, publicCluster) { + _recordRegistration(publicClusterId, address(attacker), 4, publicCluster, REGISTRATION_AMOUNT, operatorIds); + if (!_validatorStoredActive(publicValidatorKey, operatorIds)) { + privacyToggleViolation = true; + return; + } + } catch { + privacyToggleViolation = true; + return; + } + + try privateToggleOwner.setPrivate(privateToggleOp) { + if (!SSVStorage.load().operators[privateToggleOp].whitelisted) { + privacyToggleViolation = true; + return; + } + } catch { + privacyToggleViolation = true; + return; + } + + bytes memory unauthorizedPk = _newPublicKey(); + bytes memory unauthorizedShares = _makeShares(nextPkNonce); + try contractWhitelistedUser.register{value: REGISTRATION_AMOUNT}( + unauthorizedPk, operatorIds, unauthorizedShares, _defaultCluster() + ) { + unauthorizedPrivateRegistrationSucceeded = true; + return; + } catch {} + + bytes32 rePrivateClusterId = keccak256(abi.encodePacked(address(eoaWhitelistedBulkUser), operatorIds)); + ISSVNetworkCore.Cluster memory rePrivateCluster = _getClusterForRegistration(rePrivateClusterId); + bytes memory rePrivatePk = _newPublicKey(); + bytes32 rePrivateValidatorKey = keccak256(abi.encodePacked(rePrivatePk, address(eoaWhitelistedBulkUser))); + bytes memory rePrivateShares = _makeShares(nextPkNonce); + + try eoaWhitelistedBulkUser.register{value: REGISTRATION_AMOUNT}( + rePrivatePk, operatorIds, rePrivateShares, rePrivateCluster + ) { + _recordRegistration(rePrivateClusterId, address(eoaWhitelistedBulkUser), 4, rePrivateCluster, REGISTRATION_AMOUNT, operatorIds); + if (!_validatorStoredActive(rePrivateValidatorKey, operatorIds)) { + privacyToggleViolation = true; + return; + } + } catch { + privacyToggleViolation = true; + return; + } + + privacyToggleScenarioDone = true; + } + + function echidna_private_registration_access_control() external view returns (bool) { + return !unauthorizedPrivateRegistrationSucceeded; + } + + function echidna_private_authorized_paths_consistent() external view returns (bool) { + return !mixedZeroFeeViolation && !contractWhitelistViolation && !whitelistMutationFeeViolation && !privacyToggleViolation; + } + + function echidna_legacy_private_eth_init_preserves_whitelist() external view returns (bool) { + return !legacyWhitelistViolation; + } + + function echidna_whitelist_operator_counts_consistent() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + + if (s.operators[publicOp1].ethValidatorCount != expectedOperatorEthValidators[publicOp1]) return false; + if (s.operators[publicOp2].ethValidatorCount != expectedOperatorEthValidators[publicOp2]) return false; + if (s.operators[publicOp3].ethValidatorCount != expectedOperatorEthValidators[publicOp3]) return false; + if (s.operators[privateZeroOp].ethValidatorCount != expectedOperatorEthValidators[privateZeroOp]) return false; + if (s.operators[privateFeeOp].ethValidatorCount != expectedOperatorEthValidators[privateFeeOp]) return false; + if (s.operators[privateToggleOp].ethValidatorCount != expectedOperatorEthValidators[privateToggleOp]) return false; + if (s.operators[privateMutationOp].ethValidatorCount != expectedOperatorEthValidators[privateMutationOp]) return false; + if (s.operators[legacyPrivateOp].ethValidatorCount != expectedOperatorEthValidators[legacyPrivateOp]) return false; + + if (sp.ethDaoValidatorCount != expectedTotalValidators) return false; + if (sp.daoTotalEthVUnits != uint64(expectedTotalValidators) * BPS_DENOMINATOR) return false; + return true; + } + + function echidna_whitelist_cluster_hashes_consistent() external view returns (bool) { + StorageData storage s = SSVStorage.load(); + uint256 count = clusterIds.length; + for (uint256 i; i < count; ++i) { + bytes32 clusterId = clusterIds[i]; + ClusterRecord storage record = clusters[clusterId]; + if (!record.exists) return false; + if (record.owner == address(0)) return false; + if (s.ethClusters[clusterId] != record.cluster.hashClusterData()) return false; + } + + uint64[] memory mixedZeroOperatorIds = _mixedZeroFeeOperatorIds(); + bytes32 eoaMixedClusterId = keccak256(abi.encodePacked(address(contractWhitelistedUser), mixedZeroOperatorIds)); + if (s.ethClusters[eoaMixedClusterId] != 0) return false; + + uint64[] memory contractOperatorIds = _contractWhitelistOperatorIds(); + bytes32 contractUnauthorizedClusterId = keccak256(abi.encodePacked(address(eoaWhitelistedUser), contractOperatorIds)); + if (!_isCurrentlyAuthorizedForOperator(address(eoaWhitelistedUser), privateFeeOp)) { + if (s.ethClusters[contractUnauthorizedClusterId] != 0) return false; + } + + uint64[] memory legacyOperatorIds = _legacyPrivateOperatorIds(); + bytes32 legacyUnauthorizedClusterId = keccak256(abi.encodePacked(address(contractWhitelistedUser), legacyOperatorIds)); + if (s.ethClusters[legacyUnauthorizedClusterId] != 0) return false; + + return address(this).balance >= totalExpectedBalance; + } + + function _isCurrentlyAuthorizedForOperator(address account, uint64 operatorId) internal view returns (bool) { + StorageData storage s = SSVStorage.load(); + ISSVNetworkCore.Operator storage operator = s.operators[operatorId]; + if (!operator.whitelisted) return true; + + uint256 blockIndex = operatorId >> 8; + uint256 bitPosition = operatorId & 0xFF; + if (s.addressWhitelistedForOperators[account][blockIndex] & (1 << bitPosition) != 0) { + return true; + } + + address whitelistedAddress = s.operatorsWhitelist[operatorId]; + if (whitelistedAddress == address(0)) return false; + if (whitelistedAddress == account) return true; + if (!OperatorLib.isWhitelistingContract(whitelistedAddress)) return false; + + return ISSVWhitelistingContract(whitelistedAddress).isWhitelisted(account, operatorId); + } + + function _initProtocolDefaults() internal { + StorageProtocol storage sp = SSVStorageProtocol.load(); + sp.validatorsPerOperatorLimit = 5000; + sp.ethNetworkFee = PACKED_ETH_ZERO; + sp.ethNetworkFeeIndex = 0; + sp.ethNetworkFeeIndexBlockNumber = uint32(block.number); + sp.minimumBlocksBeforeLiquidation = 1; + sp.minimumLiquidationCollateral = PACKED_ETH_ZERO; + sp.operatorMaxFee = PackedETH.wrap(type(uint64).max); + sp.minimumOperatorEthFee = PackedETH.wrap(uint64(PUBLIC_FEE_1)); + sp.operatorMaxFeeIncrease = 10_000; + sp.declareOperatorFeePeriod = 1; + sp.executeOperatorFeePeriod = 10; + } + + function _initOperators() internal { + publicOp1 = _createEthOperator(address(publicOwner1), _operatorPk(1), uint64(PUBLIC_FEE_1), false); + publicOp2 = _createEthOperator(address(publicOwner2), _operatorPk(2), uint64(PUBLIC_FEE_2), false); + publicOp3 = _createEthOperator(address(publicOwner3), _operatorPk(3), uint64(PUBLIC_FEE_3), false); + + privateZeroOp = _createEthOperator(address(privateZeroOwner), _operatorPk(4), 0, true); + _whitelistAddress(privateZeroOp, address(eoaWhitelistedUser)); + _whitelistAddress(privateZeroOp, address(eoaWhitelistedBulkUser)); + + privateFeeOp = _createEthOperator(address(privateFeeOwner), _operatorPk(5), uint64(PRIVATE_FEE), true); + _setWhitelistingContract(privateFeeOp, address(mockWhitelistContract)); + + privateToggleOp = _createEthOperator(address(privateToggleOwner), _operatorPk(6), 0, true); + _whitelistAddress(privateToggleOp, address(eoaWhitelistedUser)); + _whitelistAddress(privateToggleOp, address(eoaWhitelistedBulkUser)); + + privateMutationOp = _createEthOperator(address(privateMutationOwner), _operatorPk(7), uint64(PRIVATE_FEE), true); + _setWhitelistingContract(privateMutationOp, address(mockWhitelistContract)); + + legacyPrivateOp = _createLegacyPrivateOperator(address(legacyOwner), _operatorPk(8), 1, true); + _setLegacyWhitelistAddress(legacyPrivateOp, address(eoaWhitelistedUser)); + _whitelistAddress(legacyPrivateOp, address(eoaWhitelistedBulkUser)); + } + + function _createEthOperator(address owner, bytes memory publicKey, uint64 ethFeeRaw, bool setPrivate) + internal + returns (uint64 id) + { + StorageData storage s = SSVStorage.load(); + s.lastOperatorId.increment(); + id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PACKED_SSV_ZERO, + owner: owner, + snapshot: ISSVNetworkCore.Snapshot({block: 0, index: 0, balance: PACKED_SSV_ZERO}), + whitelisted: setPrivate, + ethValidatorCount: 0, + ethFee: PackedETH.wrap(ethFeeRaw), + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: uint32(block.number), index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(publicKey)] = id; + } + + function _createLegacyPrivateOperator(address owner, bytes memory publicKey, uint64 ssvFeeRaw, bool setPrivate) + internal + returns (uint64 id) + { + StorageData storage s = SSVStorage.load(); + s.lastOperatorId.increment(); + id = uint64(s.lastOperatorId.current()); + + s.operators[id] = ISSVNetworkCore.Operator({ + validatorCount: 0, + fee: PackedSSV.wrap(ssvFeeRaw), + owner: owner, + snapshot: ISSVNetworkCore.Snapshot({block: uint32(block.number), index: 0, balance: PACKED_SSV_ZERO}), + whitelisted: setPrivate, + ethValidatorCount: 0, + ethFee: PACKED_ETH_ZERO, + ethSnapshot: ISSVNetworkCore.EthSnapshot({block: 0, index: 0, balance: PACKED_ETH_ZERO}) + }); + s.operatorsPKs[keccak256(publicKey)] = id; + } + + function _whitelistAddress(uint64 operatorId, address whitelistAddress) internal { + StorageData storage s = SSVStorage.load(); + uint256 blockIndex = operatorId >> 8; + uint256 bitPosition = operatorId & 0xFF; + s.addressWhitelistedForOperators[whitelistAddress][blockIndex] |= (1 << bitPosition); + } + + function _setWhitelistingContract(uint64 operatorId, address whitelistingContract) internal { + SSVStorage.load().operatorsWhitelist[operatorId] = whitelistingContract; + } + + function _setLegacyWhitelistAddress(uint64 operatorId, address whitelistAddress) internal { + SSVStorage.load().operatorsWhitelist[operatorId] = whitelistAddress; + } + + function _recordRegistration( + bytes32 clusterId, + address owner, + uint8 scenario, + ISSVNetworkCore.Cluster memory cluster, + uint256 amount, + uint64[] memory operatorIds + ) internal { + ClusterRecord storage record = clusters[clusterId]; + bool existed = record.exists; + uint256 previousBalance = existed ? record.cluster.balance : 0; + + cluster.balance += amount; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _clusterIndexFromStorage(operatorIds, s); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + + cluster.updateClusterData(clusterId, clusterIndex, networkFeeIndex); + cluster.validatorCount += 1; + cluster.active = true; + + totalExpectedBalance = totalExpectedBalance - previousBalance + cluster.balance; + _updateExpectedOperatorCounts(operatorIds); + expectedTotalValidators += 1; + + if (!existed) { + record.owner = owner; + record.scenario = scenario; + record.exists = true; + clusterIds.push(clusterId); + } + + record.cluster = cluster; + } + + function _recordBulkRegistration( + bytes32 clusterId, + address owner, + uint8 scenario, + ISSVNetworkCore.Cluster memory cluster, + uint256 amount, + uint64[] memory operatorIds, + uint32 validatorsAdded + ) internal { + ClusterRecord storage record = clusters[clusterId]; + bool existed = record.exists; + uint256 previousBalance = existed ? record.cluster.balance : 0; + + cluster.balance += amount; + + StorageData storage s = SSVStorage.load(); + StorageProtocol storage sp = SSVStorageProtocol.load(); + uint64 clusterIndex = _clusterIndexFromStorage(operatorIds, s); + uint64 networkFeeIndex = sp.currentNetworkFeeIndex(); + + cluster.updateClusterData(clusterId, clusterIndex, networkFeeIndex); + cluster.validatorCount += validatorsAdded; + cluster.active = true; + + totalExpectedBalance = totalExpectedBalance - previousBalance + cluster.balance; + for (uint256 i; i < validatorsAdded; ++i) { + _updateExpectedOperatorCounts(operatorIds); + } + expectedTotalValidators += validatorsAdded; + + if (!existed) { + record.owner = owner; + record.scenario = scenario; + record.exists = true; + clusterIds.push(clusterId); + } + + record.cluster = cluster; + } + + function _updateExpectedOperatorCounts(uint64[] memory operatorIds) internal { + uint256 len = operatorIds.length; + for (uint256 i; i < len; ++i) { + expectedOperatorEthValidators[operatorIds[i]] += 1; + } + } + + function _prepareLegacyPrivateZeroFee() internal returns (bool) { + if (legacyFeePrepared) return true; + + ISSVNetworkCore.Operator storage operator = SSVStorage.load().operators[legacyPrivateOp]; + if (operator.ethSnapshot.block != 0 && PackedETH.unwrap(operator.ethFee) == 0) { + legacyFeePrepared = true; + return true; + } + + try legacyOwner.reduceFee(legacyPrivateOp, 0) { + if (operator.ethSnapshot.block == 0) { + legacyWhitelistViolation = true; + return false; + } + if (PackedETH.unwrap(operator.ethFee) != 0) { + legacyWhitelistViolation = true; + return false; + } + legacyFeePrepared = true; + return true; + } catch { + legacyWhitelistViolation = true; + return false; + } + } + + function _ensureContractWhitelistUsed() internal returns (bool) { + if (contractWhitelistScenarioDone) return true; + + uint64[] memory operatorIds = _contractWhitelistOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(contractWhitelistedUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(contractWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try contractWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(contractWhitelistedUser), 1, cluster, REGISTRATION_AMOUNT, operatorIds); + contractWhitelistScenarioDone = true; + if (!_validatorStoredActive(validatorKey, operatorIds)) { + contractWhitelistViolation = true; + return false; + } + if (PackedETH.unwrap(SSVStorage.load().operators[privateFeeOp].ethFee) == 0) { + contractWhitelistViolation = true; + return false; + } + return true; + } catch { + contractWhitelistViolation = true; + return false; + } + } + + function _ensureMixedZeroUsed() internal returns (bool) { + if (mixedZeroScenarioDone) return true; + + uint64[] memory operatorIds = _mixedZeroFeeOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(eoaWhitelistedUser), operatorIds)); + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(eoaWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try eoaWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(eoaWhitelistedUser), 0, cluster, REGISTRATION_AMOUNT, operatorIds); + mixedZeroScenarioDone = true; + if (!_validatorStoredActive(validatorKey, operatorIds)) { + mixedZeroFeeViolation = true; + return false; + } + if (PackedETH.unwrap(SSVStorage.load().operators[privateZeroOp].ethFee) != 0) { + mixedZeroFeeViolation = true; + return false; + } + return true; + } catch { + mixedZeroFeeViolation = true; + return false; + } + } + + function _ensureMutationOperatorUsed() internal returns (bool) { + uint64[] memory operatorIds = _mutationOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(contractWhitelistedUser), operatorIds)); + if (clusters[clusterId].exists) return true; + + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(contractWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try contractWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(contractWhitelistedUser), 3, cluster, REGISTRATION_AMOUNT, operatorIds); + if (!_validatorStoredActive(validatorKey, operatorIds)) { + whitelistMutationFeeViolation = true; + return false; + } + if (PackedETH.unwrap(SSVStorage.load().operators[privateMutationOp].ethFee) == 0) { + whitelistMutationFeeViolation = true; + return false; + } + return true; + } catch { + whitelistMutationFeeViolation = true; + return false; + } + } + + function _ensureToggleOperatorUsed() internal returns (bool) { + uint64[] memory operatorIds = _toggleOperatorIds(); + bytes32 clusterId = keccak256(abi.encodePacked(address(eoaWhitelistedUser), operatorIds)); + if (clusters[clusterId].exists) return true; + + ISSVNetworkCore.Cluster memory cluster = _getClusterForRegistration(clusterId); + bytes memory publicKey = _newPublicKey(); + bytes32 validatorKey = keccak256(abi.encodePacked(publicKey, address(eoaWhitelistedUser))); + bytes memory shares = _makeShares(nextPkNonce); + + try eoaWhitelistedUser.register{value: REGISTRATION_AMOUNT}(publicKey, operatorIds, shares, cluster) { + _recordRegistration(clusterId, address(eoaWhitelistedUser), 4, cluster, REGISTRATION_AMOUNT, operatorIds); + if (!_validatorStoredActive(validatorKey, operatorIds)) { + privacyToggleViolation = true; + return false; + } + if (PackedETH.unwrap(SSVStorage.load().operators[privateToggleOp].ethFee) != 0) { + privacyToggleViolation = true; + return false; + } + return true; + } catch { + privacyToggleViolation = true; + return false; + } + } + + function _validatorStoredActive(bytes32 validatorKey, uint64[] memory operatorIds) internal view returns (bool) { + bytes32 stored = SSVStorage.load().validatorPKs[validatorKey]; + if (stored == bytes32(0)) return false; + return ValidatorLib.validateCorrectState(stored, ValidatorLib.hashOperatorIds(operatorIds)); + } + + function _validatorsStoredActive(bytes32[] memory validatorKeys, uint64[] memory operatorIds) internal view returns (bool) { + uint256 len = validatorKeys.length; + for (uint256 i; i < len; ++i) { + if (!_validatorStoredActive(validatorKeys[i], operatorIds)) return false; + } + return true; + } + + function _getClusterForRegistration(bytes32 clusterId) internal view returns (ISSVNetworkCore.Cluster memory cluster) { + ClusterRecord storage record = clusters[clusterId]; + if (record.exists) { + return record.cluster; + } + return _defaultCluster(); + } + + function _defaultCluster() internal pure returns (ISSVNetworkCore.Cluster memory cluster) { + return ISSVNetworkCore.Cluster({ + validatorCount: 0, + networkFeeIndex: 0, + index: 0, + active: true, + balance: 0 + }); + } + + function _mixedZeroFeeOperatorIds() internal view returns (uint64[] memory operatorIds) { + operatorIds = new uint64[](4); + operatorIds[0] = publicOp1; + operatorIds[1] = publicOp2; + operatorIds[2] = publicOp3; + operatorIds[3] = privateZeroOp; + } + + function _toggleOperatorIds() internal view returns (uint64[] memory operatorIds) { + operatorIds = new uint64[](4); + operatorIds[0] = publicOp1; + operatorIds[1] = publicOp2; + operatorIds[2] = publicOp3; + operatorIds[3] = privateToggleOp; + } + + function _contractWhitelistOperatorIds() internal view returns (uint64[] memory operatorIds) { + operatorIds = new uint64[](4); + operatorIds[0] = publicOp1; + operatorIds[1] = publicOp2; + operatorIds[2] = publicOp3; + operatorIds[3] = privateFeeOp; + } + + function _mutationOperatorIds() internal view returns (uint64[] memory operatorIds) { + operatorIds = new uint64[](4); + operatorIds[0] = publicOp1; + operatorIds[1] = publicOp2; + operatorIds[2] = publicOp3; + operatorIds[3] = privateMutationOp; + } + + function _legacyPrivateOperatorIds() internal view returns (uint64[] memory operatorIds) { + operatorIds = new uint64[](4); + operatorIds[0] = publicOp1; + operatorIds[1] = publicOp2; + operatorIds[2] = publicOp3; + operatorIds[3] = legacyPrivateOp; + } + + function _clusterIndexFromStorage(uint64[] memory operatorIds, StorageData storage s) internal view returns (uint64) { + uint256 len = operatorIds.length; + uint64 clusterIndex; + for (uint256 i; i < len; ++i) { + ISSVNetworkCore.Operator storage operator = s.operators[operatorIds[i]]; + clusterIndex += operator.ethSnapshot.index + (uint64(block.number) - uint64(operator.ethSnapshot.block)) * PackedETH.unwrap(operator.ethFee); + } + return clusterIndex; + } + + function _availableBalance() internal view returns (uint256) { + if (address(this).balance <= totalExpectedBalance) return 0; + return address(this).balance - totalExpectedBalance; + } + + function _bulkBatchSize(uint256 seed) internal pure returns (uint256) { + return 2 + (seed % 2); + } + + function _newPublicKey() internal returns (bytes memory) { + nextPkNonce += 1; + return _makePublicKey(nextPkNonce); + } + + function _newBulkPayload(uint256 batchSize, address owner) + internal + returns (bytes[] memory publicKeys, bytes[] memory sharesData, bytes32[] memory validatorKeys) + { + publicKeys = new bytes[](batchSize); + sharesData = new bytes[](batchSize); + validatorKeys = new bytes32[](batchSize); + + for (uint256 i; i < batchSize; ++i) { + bytes memory publicKey = _newPublicKey(); + publicKeys[i] = publicKey; + sharesData[i] = _makeShares(nextPkNonce); + validatorKeys[i] = keccak256(abi.encodePacked(publicKey, owner)); + } + } + + function _makePublicKey(uint256 seed) internal pure returns (bytes memory) { + bytes32 h1 = keccak256(abi.encodePacked(seed)); + bytes32 h2 = keccak256(abi.encodePacked(seed, h1)); + bytes memory b1 = abi.encodePacked(h1); + bytes memory b2 = abi.encodePacked(h2); + bytes memory pk = new bytes(48); + for (uint256 i; i < 32; ++i) { + pk[i] = b1[i]; + } + for (uint256 i; i < 16; ++i) { + pk[32 + i] = b2[i]; + } + return pk; + } + + function _makeShares(uint256 seed) internal pure returns (bytes memory) { + return abi.encodePacked(uint64(seed)); + } + + function _operatorPk(uint256 seed) internal pure returns (bytes memory) { + return abi.encodePacked(seed); + } +} diff --git a/test/echidna/echidna-ci.yaml b/test/echidna/echidna-ci.yaml new file mode 100644 index 000000000..109c21a7e --- /dev/null +++ b/test/echidna/echidna-ci.yaml @@ -0,0 +1,204 @@ +cryticArgs: ["--compile-force-framework", "foundry"] + +testMode: property +prefix: "echidna_" + +testLimit: 50000 +shrinkLimit: 5000 +seqLen: 200 + +workers: 4 + +filterBlacklist: false +filterFunctions: + - "CSSVTokenAccessControlEchidna.action_attackerTryBurn(uint256)" + - "CSSVTokenAccessControlEchidna.action_attackerTryMint(uint256)" + - "CSSVTokenEchidna.action_burn(uint256,uint8)" + - "CSSVTokenEchidna.action_burnAll(uint8)" + - "CSSVTokenEchidna.action_burnFromAll(uint256)" + - "CSSVTokenEchidna.action_internalTransfer(uint8,uint8,uint256)" + - "CSSVTokenEchidna.action_mint(uint256,uint8)" + - "CSSVTokenEchidna.action_mint_headroom_accounting(uint256,uint8)" + - "CSSVTokenEchidna.action_mintLarge(uint8)" + - "CSSVTokenEchidna.action_mintToAll(uint256)" + - "CSSVTokenEchidna.action_near_cap_roundtrip(uint256,uint8)" + - "CSSVTokenEchidna.action_rapidMintBurn(uint256,uint8,uint8)" + - "SSVAccountingEchidna.action_activate_ssv(uint256)" + - "SSVAccountingEchidna.action_advance_time(uint256)" + - "SSVAccountingEchidna.action_create_eth_cluster(uint256)" + - "SSVAccountingEchidna.action_create_ssv_cluster(uint256)" + - "SSVAccountingEchidna.action_deposit_eth(uint256)" + - "SSVAccountingEchidna.action_deposit_ssv(uint256)" + - "SSVAccountingEchidna.action_fund_eth(uint256)" + - "SSVAccountingEchidna.action_fund_ssv(uint256)" + - "SSVAccountingEchidna.action_liquidate_eth(uint256)" + - "SSVAccountingEchidna.action_liquidate_ssv(uint256)" + - "SSVAccountingEchidna.action_migrate_ssv_cluster(uint256)" + - "SSVAccountingEchidna.action_register_validator_lifecycle(uint256)" + - "SSVAccountingEchidna.action_probe_max_ssv_accrual(uint256)" + - "SSVAccountingEchidna.action_reactivate_eth(uint256)" + - "SSVAccountingEchidna.action_remove_validator_lifecycle(uint256)" + - "SSVAccountingEchidna.action_remove_validator_unauthorized(uint256)" + - "SSVAccountingEchidna.action_exit_validator_lifecycle(uint256)" + - "SSVAccountingEchidna.action_exit_validator_unauthorized(uint256)" + - "SSVAccountingEchidna.action_update_network_fee(uint256)" + - "SSVAccountingEchidna.action_update_network_fee_ssv(uint256)" + - "SSVAccountingEchidna.action_withdraw_dao_ssv(uint256)" + - "SSVAccountingEchidna.action_withdraw_eth(uint256)" + - "SSVAccountingEchidna.action_withdraw_operator_eth(uint256)" + - "SSVAccountingEchidna.action_withdraw_operator_ssv(uint256)" + - "SSVClustersEchidna.action_advance_time(uint256)" + - "SSVClustersEchidna.action_create_cluster(uint256)" + - "SSVClustersEchidna.action_deposit(uint256)" + - "SSVClustersEchidna.action_deposit_liquidated(uint256)" + - "SSVClustersEchidna.action_dust_liquidation(uint256)" + - "SSVClustersEchidna.action_fund(uint256)" + - "SSVClustersEchidna.action_claim_rewards(uint8)" + - "SSVClustersEchidna.action_liquidate(uint256)" + - "SSVClustersEchidna.action_reactivate(uint256)" + - "SSVClustersEchidna.action_reactivate_with_removed_operators(uint256)" + - "SSVClustersEchidna.action_stake(uint256,uint8)" + - "SSVClustersEchidna.action_unauthorized_withdraw(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_stale(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_too_frequent(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_inactive_valid(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_valid(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_non_latest_root(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_without_root(uint256)" + - "SSVClustersEchidna.action_withdraw(uint256)" + - "SSVClustersEchidna.action_withdraw_liquidated(uint256)" + - "SSVClustersEchidna.action_withdraw_operator_eth(uint256)" + - "SSVClustersEchidna.action_withdraw_over(uint256)" + - "SSVDAOEchidna.action_add_earnings(uint256)" + - "SSVDAOEchidna.action_commit_root(uint256,uint8)" + - "SSVDAOEchidna.action_commit_root_below_oracle_count(uint8,uint8,uint256)" + - "SSVDAOEchidna.action_commit_root_duplicate(uint8)" + - "SSVDAOEchidna.action_commit_root_general_dust_shared(uint8)" + - "SSVDAOEchidna.action_commit_root_dusty_shared(uint8)" + - "SSVDAOEchidna.action_commit_root_future(uint256,uint8)" + - "SSVDAOEchidna.action_commit_root_non_oracle(uint256)" + - "SSVDAOEchidna.action_commit_root_stale(uint8)" + - "SSVDAOEchidna.action_mint_cssv_supply(uint256,uint8)" + - "SSVDAOEchidna.action_revote_different_root_same_block(uint256,uint8,uint8)" + - "SSVDAOEchidna.action_replace_oracle(uint8,uint8)" + - "SSVDAOEchidna.action_seed_failed_quorum_round(uint256,uint8)" + - "SSVDAOEchidna.action_seed_general_dust_round(uint256,uint256)" + - "SSVDAOEchidna.action_seed_dusty_commit_round(uint256)" + - "SSVDAOEchidna.action_set_cooldown(uint64)" + - "SSVDAOEchidna.action_set_eth_vunits(uint64)" + - "SSVDAOEchidna.action_set_quorum(uint16)" + - "SSVDAOEchidna.action_update_declare_period(uint64)" + - "SSVDAOEchidna.action_update_execute_period(uint64)" + - "SSVDAOEchidna.action_update_liquidation_threshold(uint64)" + - "SSVDAOEchidna.action_update_liquidation_threshold_ssv(uint64)" + - "SSVDAOEchidna.action_update_max_operator_fee(uint64)" + - "SSVDAOEchidna.action_update_min_liquidation_collateral(uint256)" + - "SSVDAOEchidna.action_update_min_liquidation_collateral_ssv(uint256)" + - "SSVDAOEchidna.action_update_min_operator_eth_fee(uint64)" + - "SSVDAOEchidna.action_update_network_fee(uint256)" + - "SSVDAOEchidna.action_update_network_fee_ssv(uint256)" + - "SSVDAOEchidna.action_update_operator_fee_increase(uint64)" + - "SSVDAOEchidna.action_withdraw(uint256,uint8)" + - "SSVEBProofEchidna.action_update_tampered_eb(uint32,uint32)" + - "SSVEBProofEchidna.action_update_with_eb(uint32)" + - "SSVEdgeCasesEchidna.action_fee_index_overflow()" + - "SSVEdgeCasesEchidna.action_fund(uint256)" + - "SSVEdgeCasesEchidna.action_pack_overflow_check()" + - "SSVEdgeCasesEchidna.action_probe_max_eth_accrual(uint256)" + - "SSVEdgeCasesEchidna.action_reactivation_vunits(uint256)" + - "SSVEdgeCasesEchidna.action_update_operator_max_fee(uint256)" + - "SSVEdgeCasesEchidna.action_update_validators_per_operator_limit(uint256)" + - "SSVEdgeCasesEchidna.action_validator_spam(uint256)" + - "SSVEdgeCasesEchidna.action_yoyo_liquidation(uint256)" + - "SSVLegacyClustersEchidna.action_advance_time(uint256)" + - "SSVLegacyClustersEchidna.action_deposit_ssv(uint256)" + - "SSVLegacyClustersEchidna.action_liquidate_ssv()" + - "SSVLegacyClustersEchidna.action_liquidate_ssv_with_eb_noise(uint256)" + - "SSVLegacyValidatorRemovalEchidna.action_advance_ssv_fees(uint256)" + - "SSVLegacyValidatorRemovalEchidna.action_bulk_remove_validator_active()" + - "SSVLegacyValidatorRemovalEchidna.action_bulk_remove_validator_liquidated()" + - "SSVLegacyValidatorRemovalEchidna.action_remove_validator_active(uint256)" + - "SSVLegacyValidatorRemovalEchidna.action_remove_validator_liquidated(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_eb_decrease(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_eb_increase(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_liquidation(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_final_removal(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_final_bulk_removal(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_remove_second_operator()" + - "SSVMigrationEchidna.action_advance_ssv_without_cluster_sync(uint256)" + - "SSVMigrationEchidna.action_fund_eth(uint256)" + - "SSVMigrationEchidna.action_liquidate_ssv()" + - "SSVMigrationEchidna.action_migrate_removed_operator_explicit_eb(uint256)" + - "SSVMigrationEchidna.action_migrate_liquidated_ssv_to_eth(uint256)" + - "SSVMigrationEchidna.action_migrate_ssv_to_eth(uint256)" + - "SSVMigrationEchidna.action_prepare_migration_and_attempt(uint256)" + - "SSVMigrationEchidna.action_remove_operator(uint256)" + - "SSVMigrationEchidna.action_update_ssv_cluster_balance_valid(uint256)" + - "SSVMigrationEchidna.action_sync_ssv_cluster()" + - "SSVOperatorFeeGovEchidna.action_plant_and_execute_legacy(uint256,uint256)" + - "SSVOperatorFeeGovEchidna.action_register(uint256,uint256,uint8)" + - "SSVOperatorsEchidna.action_advance_time(uint256)" + - "SSVOperatorsEchidna.action_assign_validators(uint256,uint256,bool)" + - "SSVOperatorsEchidna.action_declare_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_declare_from_zero(uint256,uint256)" + - "SSVOperatorsEchidna.action_execute_fee(uint256)" + - "SSVOperatorsEchidna.action_execute_fee_settlement_order(uint256,uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_fee_change_latency(uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_fund(uint256)" + - "SSVOperatorsEchidna.action_fund_ssv(uint256)" + - "SSVOperatorsEchidna.action_reduce_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_reduce_legacy_ensure_eth_defaults(uint256)" + - "SSVOperatorsEchidna.action_reduce_fee_settlement_order(uint256,uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_register(uint256,uint256,uint8,bool)" + - "SSVOperatorsEchidna.action_remove(uint256)" + - "SSVOperatorsEchidna.action_seed_legacy_operator(uint256,uint256)" + - "SSVOperatorsEchidna.action_set_max_fee(uint256)" + - "SSVOperatorsEchidna.action_set_min_operator_eth_fee(uint256)" + - "SSVOperatorsEchidna.action_set_ssv_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_trigger_ensure_eth_defaults(uint256)" + - "SSVOperatorsEchidna.action_unauthorized(uint256,uint8,uint256)" + - "SSVOperatorsEchidna.action_withdraw(uint256,uint256)" + - "SSVOperatorsEchidna.action_withdraw_all(uint256)" + - "SSVOperatorsEchidna.action_withdraw_all_version(uint256)" + - "SSVOperatorsEchidna.action_withdraw_all_ssv(uint256)" + - "SSVOperatorsEchidna.action_withdraw_over(uint256)" + - "SSVOperatorsEchidna.action_withdraw_over_ssv(uint256)" + - "SSVOperatorsEchidna.action_withdraw_ssv(uint256,uint256)" + - "SSVStakingEchidna.action_claim_dust_positive_balance(uint256,uint256)" + - "SSVStakingEchidna.action_claim_dust_zero_balance(uint256,uint256)" + - "SSVStakingEchidna.action_claim_rewards(uint8)" + - "SSVStakingEchidna.action_request_unstake(uint256,uint8)" + - "SSVStakingEchidna.action_request_unstake_stops_accrual(uint256,uint256,uint256,uint256)" + - "SSVStakingEchidna.action_stake(uint256,uint8)" + - "SSVStakingEchidna.action_sync_fees_with_decrease(uint256)" + - "SSVStakingEchidna.action_sync_fees_with_increase(uint256)" + - "SSVStakingEchidna.action_transfer_cssv(uint256,uint8,uint8)" + - "SSVStakingEchidna.action_withdraw_unlocked(uint8)" + - "SSVStakingEchidna.action_withdraw_unlocked_batch_processing(uint256,uint8)" + - "SSVStakingEchidna.action_zero_cssv_no_accrual(uint256,uint256,uint256)" + - "SSVWhitelistValidatorsEchidna.action_fund(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_mixed_zero_fee_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_mixed_zero_fee_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_contract_whitelist_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_contract_whitelist_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_legacy_private_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_legacy_private_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_mutate_whitelist_and_reduce_fee_after_use(uint256)" + - "SSVWhitelistValidatorsEchidna.action_toggle_privacy_after_use(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_mixed_zero_fee_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_mixed_zero_fee_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_contract_whitelist_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_contract_whitelist_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_legacy_private_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_legacy_private_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_exit_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_bulk_register(uint256,uint8,uint8)" + - "SSVValidatorsEchidna.action_bulk_register_duplicate(uint256)" + - "SSVValidatorsEchidna.action_bulk_exit(uint256)" + - "SSVValidatorsEchidna.action_bulk_exit_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_bulk_remove(uint256)" + - "SSVValidatorsEchidna.action_bulk_remove_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_fund(uint256)" + - "SSVValidatorsEchidna.action_register(uint256,uint8,uint8)" + - "SSVValidatorsEchidna.action_remove(uint256)" + - "SSVValidatorsEchidna.action_remove_unauthorized(uint256)" diff --git a/test/echidna/echidna.clusters.lifecycle.yaml b/test/echidna/echidna.clusters.lifecycle.yaml new file mode 100644 index 000000000..a32bd8efe --- /dev/null +++ b/test/echidna/echidna.clusters.lifecycle.yaml @@ -0,0 +1,34 @@ +cryticArgs: ["--compile-force-framework", "foundry"] + +testMode: property +prefix: "echidna_" + +testLimit: 200000 +shrinkLimit: 5000 +seqLen: 140 + +workers: 7 + +corpusDir: "test/echidna/corpus/clusters-lifecycle" +coverageDir: "test/echidna/coverage/clusters-lifecycle" + +filterBlacklist: false +filterFunctions: + - "SSVClustersEchidna.action_advance_time(uint256)" + - "SSVClustersEchidna.action_create_cluster(uint256)" + - "SSVClustersEchidna.action_deposit(uint256)" + - "SSVClustersEchidna.action_deposit_liquidated(uint256)" + - "SSVClustersEchidna.action_dust_liquidation(uint256)" + - "SSVClustersEchidna.action_fund(uint256)" + - "SSVClustersEchidna.action_liquidate(uint256)" + - "SSVClustersEchidna.action_reactivate(uint256)" + - "SSVClustersEchidna.action_reactivate_with_removed_operators(uint256)" + - "SSVClustersEchidna.action_unauthorized_withdraw(uint256)" + - "SSVClustersEchidna.action_withdraw(uint256)" + - "SSVClustersEchidna.action_withdraw_liquidated(uint256)" + - "SSVClustersEchidna.action_withdraw_over(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_eb_decrease(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_eb_increase(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_liquidation(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_final_removal(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_remove_second_operator()" diff --git a/test/echidna/echidna.operators.activation.yaml b/test/echidna/echidna.operators.activation.yaml new file mode 100644 index 000000000..31dbd072a --- /dev/null +++ b/test/echidna/echidna.operators.activation.yaml @@ -0,0 +1,41 @@ +cryticArgs: ["--compile-force-framework", "foundry"] + +testMode: property +prefix: "echidna_" + +testLimit: 200000 +shrinkLimit: 5000 +seqLen: 160 + +workers: 7 + +corpusDir: "test/echidna/corpus/operators-activation" +coverageDir: "test/echidna/coverage/operators-activation" + +filterBlacklist: false +filterFunctions: + - "SSVOperatorsEchidna.action_advance_time(uint256)" + - "SSVOperatorsEchidna.action_assign_validators(uint256,uint256,bool)" + - "SSVOperatorsEchidna.action_declare_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_execute_fee(uint256)" + - "SSVOperatorsEchidna.action_execute_fee_settlement_order(uint256,uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_fee_change_latency(uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_fund(uint256)" + - "SSVOperatorsEchidna.action_fund_ssv(uint256)" + - "SSVOperatorsEchidna.action_reduce_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_reduce_legacy_ensure_eth_defaults(uint256)" + - "SSVOperatorsEchidna.action_reduce_fee_settlement_order(uint256,uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_register(uint256,uint256,uint8,bool)" + - "SSVOperatorsEchidna.action_remove(uint256)" + - "SSVOperatorsEchidna.action_seed_legacy_operator(uint256,uint256)" + - "SSVOperatorsEchidna.action_set_max_fee(uint256)" + - "SSVOperatorsEchidna.action_set_min_operator_eth_fee(uint256)" + - "SSVOperatorsEchidna.action_set_ssv_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_trigger_ensure_eth_defaults(uint256)" + - "SSVOperatorsEchidna.action_withdraw(uint256,uint256)" + - "SSVOperatorsEchidna.action_withdraw_all(uint256)" + - "SSVOperatorsEchidna.action_withdraw_all_ssv(uint256)" + - "SSVOperatorsEchidna.action_withdraw_all_version(uint256)" + - "SSVOperatorsEchidna.action_withdraw_over(uint256)" + - "SSVOperatorsEchidna.action_withdraw_over_ssv(uint256)" + - "SSVOperatorsEchidna.action_withdraw_ssv(uint256,uint256)" diff --git a/test/echidna/echidna.staking.lifecycle.yaml b/test/echidna/echidna.staking.lifecycle.yaml new file mode 100644 index 000000000..3cd86d200 --- /dev/null +++ b/test/echidna/echidna.staking.lifecycle.yaml @@ -0,0 +1,28 @@ +cryticArgs: ["--compile-force-framework", "foundry"] + +testMode: property +prefix: "echidna_" + +testLimit: 200000 +shrinkLimit: 5000 +seqLen: 140 + +workers: 7 + +corpusDir: "test/echidna/corpus/staking-lifecycle" +coverageDir: "test/echidna/coverage/staking-lifecycle" + +filterBlacklist: false +filterFunctions: + - "SSVStakingEchidna.action_claim_dust_positive_balance(uint256,uint256)" + - "SSVStakingEchidna.action_claim_dust_zero_balance(uint256,uint256)" + - "SSVStakingEchidna.action_claim_rewards(uint8)" + - "SSVStakingEchidna.action_request_unstake(uint256,uint8)" + - "SSVStakingEchidna.action_request_unstake_stops_accrual(uint256,uint256,uint256,uint256)" + - "SSVStakingEchidna.action_stake(uint256,uint8)" + - "SSVStakingEchidna.action_sync_fees_with_decrease(uint256)" + - "SSVStakingEchidna.action_sync_fees_with_increase(uint256)" + - "SSVStakingEchidna.action_transfer_cssv(uint256,uint8,uint8)" + - "SSVStakingEchidna.action_withdraw_unlocked(uint8)" + - "SSVStakingEchidna.action_withdraw_unlocked_batch_processing(uint256,uint8)" + - "SSVStakingEchidna.action_zero_cssv_no_accrual(uint256,uint256,uint256)" diff --git a/test/echidna/echidna.yaml b/test/echidna/echidna.yaml new file mode 100644 index 000000000..23fa0e519 --- /dev/null +++ b/test/echidna/echidna.yaml @@ -0,0 +1,207 @@ +cryticArgs: ["--compile-force-framework", "foundry"] + +testMode: property +prefix: "echidna_" + +testLimit: 500000 +shrinkLimit: 5000 +seqLen: 250 + +workers: 7 + +corpusDir: "test/echidna/corpus" +coverageDir: "test/echidna/coverage" + +filterBlacklist: false +filterFunctions: + - "CSSVTokenAccessControlEchidna.action_attackerTryBurn(uint256)" + - "CSSVTokenAccessControlEchidna.action_attackerTryMint(uint256)" + - "CSSVTokenEchidna.action_burn(uint256,uint8)" + - "CSSVTokenEchidna.action_burnAll(uint8)" + - "CSSVTokenEchidna.action_burnFromAll(uint256)" + - "CSSVTokenEchidna.action_internalTransfer(uint8,uint8,uint256)" + - "CSSVTokenEchidna.action_mint(uint256,uint8)" + - "CSSVTokenEchidna.action_mint_headroom_accounting(uint256,uint8)" + - "CSSVTokenEchidna.action_mintLarge(uint8)" + - "CSSVTokenEchidna.action_mintToAll(uint256)" + - "CSSVTokenEchidna.action_near_cap_roundtrip(uint256,uint8)" + - "CSSVTokenEchidna.action_rapidMintBurn(uint256,uint8,uint8)" + - "SSVAccountingEchidna.action_activate_ssv(uint256)" + - "SSVAccountingEchidna.action_advance_time(uint256)" + - "SSVAccountingEchidna.action_create_eth_cluster(uint256)" + - "SSVAccountingEchidna.action_create_ssv_cluster(uint256)" + - "SSVAccountingEchidna.action_deposit_eth(uint256)" + - "SSVAccountingEchidna.action_deposit_ssv(uint256)" + - "SSVAccountingEchidna.action_fund_eth(uint256)" + - "SSVAccountingEchidna.action_fund_ssv(uint256)" + - "SSVAccountingEchidna.action_liquidate_eth(uint256)" + - "SSVAccountingEchidna.action_liquidate_ssv(uint256)" + - "SSVAccountingEchidna.action_migrate_ssv_cluster(uint256)" + - "SSVAccountingEchidna.action_register_validator_lifecycle(uint256)" + - "SSVAccountingEchidna.action_probe_max_ssv_accrual(uint256)" + - "SSVAccountingEchidna.action_reactivate_eth(uint256)" + - "SSVAccountingEchidna.action_remove_validator_lifecycle(uint256)" + - "SSVAccountingEchidna.action_remove_validator_unauthorized(uint256)" + - "SSVAccountingEchidna.action_exit_validator_lifecycle(uint256)" + - "SSVAccountingEchidna.action_exit_validator_unauthorized(uint256)" + - "SSVAccountingEchidna.action_update_network_fee(uint256)" + - "SSVAccountingEchidna.action_update_network_fee_ssv(uint256)" + - "SSVAccountingEchidna.action_withdraw_dao_ssv(uint256)" + - "SSVAccountingEchidna.action_withdraw_eth(uint256)" + - "SSVAccountingEchidna.action_withdraw_operator_eth(uint256)" + - "SSVAccountingEchidna.action_withdraw_operator_ssv(uint256)" + - "SSVClustersEchidna.action_advance_time(uint256)" + - "SSVClustersEchidna.action_create_cluster(uint256)" + - "SSVClustersEchidna.action_deposit(uint256)" + - "SSVClustersEchidna.action_deposit_liquidated(uint256)" + - "SSVClustersEchidna.action_dust_liquidation(uint256)" + - "SSVClustersEchidna.action_fund(uint256)" + - "SSVClustersEchidna.action_claim_rewards(uint8)" + - "SSVClustersEchidna.action_liquidate(uint256)" + - "SSVClustersEchidna.action_reactivate(uint256)" + - "SSVClustersEchidna.action_reactivate_with_removed_operators(uint256)" + - "SSVClustersEchidna.action_stake(uint256,uint8)" + - "SSVClustersEchidna.action_unauthorized_withdraw(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_stale(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_too_frequent(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_inactive_valid(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_valid(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_non_latest_root(uint256)" + - "SSVClustersEchidna.action_update_cluster_balance_without_root(uint256)" + - "SSVClustersEchidna.action_withdraw(uint256)" + - "SSVClustersEchidna.action_withdraw_liquidated(uint256)" + - "SSVClustersEchidna.action_withdraw_operator_eth(uint256)" + - "SSVClustersEchidna.action_withdraw_over(uint256)" + - "SSVDAOEchidna.action_add_earnings(uint256)" + - "SSVDAOEchidna.action_commit_root(uint256,uint8)" + - "SSVDAOEchidna.action_commit_root_below_oracle_count(uint8,uint8,uint256)" + - "SSVDAOEchidna.action_commit_root_duplicate(uint8)" + - "SSVDAOEchidna.action_commit_root_general_dust_shared(uint8)" + - "SSVDAOEchidna.action_commit_root_dusty_shared(uint8)" + - "SSVDAOEchidna.action_commit_root_future(uint256,uint8)" + - "SSVDAOEchidna.action_commit_root_non_oracle(uint256)" + - "SSVDAOEchidna.action_commit_root_stale(uint8)" + - "SSVDAOEchidna.action_mint_cssv_supply(uint256,uint8)" + - "SSVDAOEchidna.action_revote_different_root_same_block(uint256,uint8,uint8)" + - "SSVDAOEchidna.action_replace_oracle(uint8,uint8)" + - "SSVDAOEchidna.action_seed_failed_quorum_round(uint256,uint8)" + - "SSVDAOEchidna.action_seed_general_dust_round(uint256,uint256)" + - "SSVDAOEchidna.action_seed_dusty_commit_round(uint256)" + - "SSVDAOEchidna.action_set_cooldown(uint64)" + - "SSVDAOEchidna.action_set_eth_vunits(uint64)" + - "SSVDAOEchidna.action_set_quorum(uint16)" + - "SSVDAOEchidna.action_update_declare_period(uint64)" + - "SSVDAOEchidna.action_update_execute_period(uint64)" + - "SSVDAOEchidna.action_update_liquidation_threshold(uint64)" + - "SSVDAOEchidna.action_update_liquidation_threshold_ssv(uint64)" + - "SSVDAOEchidna.action_update_max_operator_fee(uint64)" + - "SSVDAOEchidna.action_update_min_liquidation_collateral(uint256)" + - "SSVDAOEchidna.action_update_min_liquidation_collateral_ssv(uint256)" + - "SSVDAOEchidna.action_update_min_operator_eth_fee(uint64)" + - "SSVDAOEchidna.action_update_network_fee(uint256)" + - "SSVDAOEchidna.action_update_network_fee_ssv(uint256)" + - "SSVDAOEchidna.action_update_operator_fee_increase(uint64)" + - "SSVDAOEchidna.action_withdraw(uint256,uint8)" + - "SSVEBProofEchidna.action_update_tampered_eb(uint32,uint32)" + - "SSVEBProofEchidna.action_update_with_eb(uint32)" + - "SSVEdgeCasesEchidna.action_fee_index_overflow()" + - "SSVEdgeCasesEchidna.action_fund(uint256)" + - "SSVEdgeCasesEchidna.action_pack_overflow_check()" + - "SSVEdgeCasesEchidna.action_probe_max_eth_accrual(uint256)" + - "SSVEdgeCasesEchidna.action_reactivation_vunits(uint256)" + - "SSVEdgeCasesEchidna.action_update_operator_max_fee(uint256)" + - "SSVEdgeCasesEchidna.action_update_validators_per_operator_limit(uint256)" + - "SSVEdgeCasesEchidna.action_validator_spam(uint256)" + - "SSVEdgeCasesEchidna.action_yoyo_liquidation(uint256)" + - "SSVLegacyClustersEchidna.action_advance_time(uint256)" + - "SSVLegacyClustersEchidna.action_deposit_ssv(uint256)" + - "SSVLegacyClustersEchidna.action_liquidate_ssv()" + - "SSVLegacyClustersEchidna.action_liquidate_ssv_with_eb_noise(uint256)" + - "SSVLegacyValidatorRemovalEchidna.action_advance_ssv_fees(uint256)" + - "SSVLegacyValidatorRemovalEchidna.action_bulk_remove_validator_active()" + - "SSVLegacyValidatorRemovalEchidna.action_bulk_remove_validator_liquidated()" + - "SSVLegacyValidatorRemovalEchidna.action_remove_validator_active(uint256)" + - "SSVLegacyValidatorRemovalEchidna.action_remove_validator_liquidated(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_eb_decrease(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_eb_increase(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_liquidation(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_final_removal(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_removed_operator_final_bulk_removal(uint256)" + - "SSVRemovedOperatorETHFlowsEchidna.action_remove_second_operator()" + - "SSVMigrationEchidna.action_advance_ssv_without_cluster_sync(uint256)" + - "SSVMigrationEchidna.action_fund_eth(uint256)" + - "SSVMigrationEchidna.action_liquidate_ssv()" + - "SSVMigrationEchidna.action_migrate_removed_operator_explicit_eb(uint256)" + - "SSVMigrationEchidna.action_migrate_liquidated_ssv_to_eth(uint256)" + - "SSVMigrationEchidna.action_migrate_ssv_to_eth(uint256)" + - "SSVMigrationEchidna.action_prepare_migration_and_attempt(uint256)" + - "SSVMigrationEchidna.action_remove_operator(uint256)" + - "SSVMigrationEchidna.action_update_ssv_cluster_balance_valid(uint256)" + - "SSVMigrationEchidna.action_sync_ssv_cluster()" + - "SSVOperatorFeeGovEchidna.action_plant_and_execute_legacy(uint256,uint256)" + - "SSVOperatorFeeGovEchidna.action_register(uint256,uint256,uint8)" + - "SSVOperatorsEchidna.action_advance_time(uint256)" + - "SSVOperatorsEchidna.action_assign_validators(uint256,uint256,bool)" + - "SSVOperatorsEchidna.action_declare_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_declare_from_zero(uint256,uint256)" + - "SSVOperatorsEchidna.action_execute_fee(uint256)" + - "SSVOperatorsEchidna.action_execute_fee_settlement_order(uint256,uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_fee_change_latency(uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_fund(uint256)" + - "SSVOperatorsEchidna.action_fund_ssv(uint256)" + - "SSVOperatorsEchidna.action_reduce_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_reduce_legacy_ensure_eth_defaults(uint256)" + - "SSVOperatorsEchidna.action_reduce_fee_settlement_order(uint256,uint256,uint256,uint256)" + - "SSVOperatorsEchidna.action_register(uint256,uint256,uint8,bool)" + - "SSVOperatorsEchidna.action_remove(uint256)" + - "SSVOperatorsEchidna.action_seed_legacy_operator(uint256,uint256)" + - "SSVOperatorsEchidna.action_set_max_fee(uint256)" + - "SSVOperatorsEchidna.action_set_min_operator_eth_fee(uint256)" + - "SSVOperatorsEchidna.action_set_ssv_fee(uint256,uint256)" + - "SSVOperatorsEchidna.action_trigger_ensure_eth_defaults(uint256)" + - "SSVOperatorsEchidna.action_unauthorized(uint256,uint8,uint256)" + - "SSVOperatorsEchidna.action_withdraw(uint256,uint256)" + - "SSVOperatorsEchidna.action_withdraw_all(uint256)" + - "SSVOperatorsEchidna.action_withdraw_all_version(uint256)" + - "SSVOperatorsEchidna.action_withdraw_all_ssv(uint256)" + - "SSVOperatorsEchidna.action_withdraw_over(uint256)" + - "SSVOperatorsEchidna.action_withdraw_over_ssv(uint256)" + - "SSVOperatorsEchidna.action_withdraw_ssv(uint256,uint256)" + - "SSVStakingEchidna.action_claim_dust_positive_balance(uint256,uint256)" + - "SSVStakingEchidna.action_claim_dust_zero_balance(uint256,uint256)" + - "SSVStakingEchidna.action_claim_rewards(uint8)" + - "SSVStakingEchidna.action_request_unstake(uint256,uint8)" + - "SSVStakingEchidna.action_request_unstake_stops_accrual(uint256,uint256,uint256,uint256)" + - "SSVStakingEchidna.action_stake(uint256,uint8)" + - "SSVStakingEchidna.action_sync_fees_with_decrease(uint256)" + - "SSVStakingEchidna.action_sync_fees_with_increase(uint256)" + - "SSVStakingEchidna.action_transfer_cssv(uint256,uint8,uint8)" + - "SSVStakingEchidna.action_withdraw_unlocked(uint8)" + - "SSVStakingEchidna.action_withdraw_unlocked_batch_processing(uint256,uint8)" + - "SSVStakingEchidna.action_zero_cssv_no_accrual(uint256,uint256,uint256)" + - "SSVWhitelistValidatorsEchidna.action_fund(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_mixed_zero_fee_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_mixed_zero_fee_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_contract_whitelist_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_contract_whitelist_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_legacy_private_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_bulk_register_legacy_private_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_mutate_whitelist_and_reduce_fee_after_use(uint256)" + - "SSVWhitelistValidatorsEchidna.action_toggle_privacy_after_use(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_mixed_zero_fee_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_mixed_zero_fee_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_contract_whitelist_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_contract_whitelist_unauthorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_legacy_private_authorized(uint256)" + - "SSVWhitelistValidatorsEchidna.action_register_legacy_private_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_exit_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_bulk_register(uint256,uint8,uint8)" + - "SSVValidatorsEchidna.action_bulk_register_duplicate(uint256)" + - "SSVValidatorsEchidna.action_bulk_exit(uint256)" + - "SSVValidatorsEchidna.action_bulk_exit_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_bulk_remove(uint256)" + - "SSVValidatorsEchidna.action_bulk_remove_unauthorized(uint256)" + - "SSVValidatorsEchidna.action_fund(uint256)" + - "SSVValidatorsEchidna.action_register(uint256,uint8,uint8)" + - "SSVValidatorsEchidna.action_remove(uint256)" + - "SSVValidatorsEchidna.action_remove_unauthorized(uint256)" diff --git a/test/echidna/run-echidna.sh b/test/echidna/run-echidna.sh new file mode 100755 index 000000000..826a5d753 --- /dev/null +++ b/test/echidna/run-echidna.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo "==========================================" +echo " SSV Network Echidna Fuzz Testing" +echo "==========================================" +echo "" + +if [[ ! -f "test/echidna/echidna.yaml" ]]; then + echo -e "${RED}Error: Run this script from the project root directory${NC}" + echo "Usage: bash test/echidna/run-echidna.sh" + exit 1 +fi + +HARNESS_FILES=() +while IFS= read -r file; do + HARNESS_FILES+=("$file") +done < <(find test/echidna -maxdepth 1 -type f -name '*Echidna.sol' | sort) + +if [[ ${#HARNESS_FILES[@]} -eq 0 ]]; then + echo -e "${RED}Error: No Echidna harnesses found in test/echidna${NC}" + exit 1 +fi + +echo "Checking dependencies..." +if ! command -v brew &> /dev/null; then + echo -e "${RED}Homebrew not found. Install from https://brew.sh${NC}" + exit 1 +fi +echo -e " ${GREEN}✓${NC} Homebrew" + +if ! command -v echidna &> /dev/null; then + echo -e "${YELLOW}Echidna not found. Installing...${NC}" + brew install echidna +fi +echo -e " ${GREEN}✓${NC} Echidna $(echidna --version 2>/dev/null | head -1 || echo 'installed')" + +if ! command -v solc-select &> /dev/null; then + echo -e "${YELLOW}solc-select not found. Installing...${NC}" + brew install solc-select +fi +echo -e " ${GREEN}✓${NC} solc-select" + +REQUIRED_SOLC="0.8.24" +if ! solc-select versions 2>/dev/null | grep -q "$REQUIRED_SOLC"; then + echo -e "${YELLOW}solc $REQUIRED_SOLC not found. Installing...${NC}" + solc-select install $REQUIRED_SOLC +fi + +CURRENT_SOLC=$(solc --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "none") +if [[ "$CURRENT_SOLC" != "$REQUIRED_SOLC" ]]; then + echo -e "${YELLOW}Switching to solc $REQUIRED_SOLC...${NC}" + solc-select use $REQUIRED_SOLC +fi +echo -e " ${GREEN}✓${NC} solc $REQUIRED_SOLC" + +TOTAL_HARNESSES=${#HARNESS_FILES[@]} +for index in "${!HARNESS_FILES[@]}"; do + file="${HARNESS_FILES[$index]}" + contract="$(basename "$file" .sol)" + + echo "" + echo "==========================================" + printf " [%d/%d] %s\n" "$((index + 1))" "$TOTAL_HARNESSES" "$contract" + echo "==========================================" + echo "" + + echidna "$file" \ + --contract "$contract" \ + --config test/echidna/echidna.yaml +done + +echo "" +echo -e "${GREEN}All tests completed!${NC}" diff --git a/test/forked/v2.0.0/config.ts b/test/forked/v2.0.0/config.ts new file mode 100644 index 000000000..3c9c6cca4 --- /dev/null +++ b/test/forked/v2.0.0/config.ts @@ -0,0 +1,45 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { ForkConfigFile } from "../../../scripts/common/fork-test.ts"; + +const DEFAULT_FORK_CONFIG = { + SSV_NETWORK_ADDRESS: "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + SSV_NETWORK_VIEWS: "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + SSV_TOKEN: "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + DAO_ADDRESS: "0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6", +} as const; + +function loadForkConfigFile(): ForkConfigFile { + const configPathFromEnv = process.env.FORK_CONFIG_PATH; + if (!configPathFromEnv) { + return {}; + } + const resolvedPath = path.resolve(configPathFromEnv); + if (!fs.existsSync(resolvedPath)) { + throw new Error(`FORK_CONFIG_PATH does not exist: ${resolvedPath}`); + } + const raw = fs.readFileSync(resolvedPath, "utf8"); + return JSON.parse(raw) as ForkConfigFile; +} + +const fileConfig = loadForkConfigFile(); + +export const ForkConfig = { + SSV_NETWORK_ADDRESS: process.env.FORK_SSV_NETWORK_ADDRESS ?? + fileConfig.ssvNetworkProxy ?? + fileConfig.ssvNetworkAddress ?? + DEFAULT_FORK_CONFIG.SSV_NETWORK_ADDRESS, + SSV_NETWORK_VIEWS: process.env.FORK_SSV_NETWORK_VIEWS ?? + fileConfig.ssvNetworkViews ?? + DEFAULT_FORK_CONFIG.SSV_NETWORK_VIEWS, + SSV_TOKEN: process.env.FORK_SSV_TOKEN ?? + fileConfig.ssvToken ?? + DEFAULT_FORK_CONFIG.SSV_TOKEN, + CSSV_TOKEN: process.env.FORK_CSSV_TOKEN ?? + fileConfig.cssvToken, + DAO_ADDRESS: process.env.FORK_DAO_ADDRESS ?? + fileConfig.daoAddress ?? + fileConfig.owner ?? + DEFAULT_FORK_CONFIG.DAO_ADDRESS, + MODULES: fileConfig.modules ?? {}, +} as const; diff --git a/test/forked/v2.0.0/fullIntegrationForked.test.ts b/test/forked/v2.0.0/fullIntegrationForked.test.ts new file mode 100644 index 000000000..b6f863ddb --- /dev/null +++ b/test/forked/v2.0.0/fullIntegrationForked.test.ts @@ -0,0 +1,1863 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullForkedFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType, OperatorTuple, UnstakeRequest } from '../../common/types.ts'; +import { buildEBMerkleForDefaultClusters, calculateInitialBurnRate, getCurrentClusterState, makeArrayOfKeysAndShares, getFeeAboveIncreaseLimit, getValidOperatorFeeIncrease, makeOperatorKey, makePublicKey, registerDefaultCluster, registerDefaultClusters, registerOperators, setAccountBalance, updateClusterBalancesForDefaultClusters, whitelistAddresses } from '../../helpers/index.ts'; +import { CLUSTER_VERSION_ETH, DECLARE_OPERATOR_FEE_PERIOD, DEFAULT_ETH_EB_PER_VALIDATOR, DEFAULT_ETH_REGISTER_VALUE, DEFAULT_ORACLES_IDS, DEFAULT_SHARES, DEFAULT_UNSTAKE_COOLDOWN, EMPTY_CLUSTER, EXECUTE_OPERATOR_FEE_PERIOD, MAXIMUM_OPERATORS_FEE, MINIMAL_LIQUIDATION_THRESHOLD, MINIMAL_OPERATOR_ETH_FEE, MINIMUM_BLOCKS_BEFORE_LIQUIDATION, MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, NETWORK_FEE, OPERATOR_MAX_FEE_INCREASE, OPERATOR_FEE_PRECISION, STAKE_AMOUNT, VALIDATORS_PER_OPERATOR_LIMIT, } from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../../common/errors.ts'; +import { deployContract } from '../../../scripts/common/helpers.ts'; +import { ContractTransactionResponse } from 'ethers'; +import { trackGasFromReceipt, GasGroup } from '../../helpers/gas.ts'; +import { getForkedConnection } from '../../setup/connection.ts'; +import { ForkConfig } from './config.ts'; + +const RUN_FORK = process.env.RUN_FORK === 'true'; +const suite = RUN_FORK ? describe : describe.skip; + +suite("SSVNetwork full integration tests made on forked contract", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let randomUser: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getForkedConnection()); + [operatorOwner, clusterOwner, randomUser] = await connection.ethers.getSigners(); + + for (const signer of [operatorOwner, clusterOwner, randomUser]) { + await connection.ethers.provider.send("hardhat_impersonateAccount", [signer.address]); + await setAccountBalance(connection.ethers.provider, signer.address, BigInt("0x56bc75e2d63100000")); + } + + operatorOwner = await connection.ethers.getSigner(operatorOwner.address); + clusterOwner = await connection.ethers.getSigner(clusterOwner.address); + randomUser = await connection.ethers.getSigner(randomUser.address); + }); + + const deployFullSSVNetworkForkFixture = async () => { + return ssvNetworkFullForkedFixture(connection); + }; + + describe("Constructor, initializer and upgrades", async function () { + it("Configures SSVNetwork correctly", async function () { + const { network, views, cssvToken, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(await network.getAddress()).to.be.equal(ForkConfig.SSV_NETWORK_ADDRESS); + await expect(await views.getAddress()).to.be.equal(ForkConfig.SSV_NETWORK_VIEWS); + await expect(await ssvToken.getAddress()).to.be.equal(ForkConfig.SSV_TOKEN); + + const version = await network.getVersion(); + + await expect(version).to.be.a("string").and.not.empty; + await expect(await views.getMinimumLiquidationCollateralSSV()).to.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL); + await expect(await views.getValidatorsPerOperatorLimit()).to.equal(VALIDATORS_PER_OPERATOR_LIMIT); + await expect(await views.getOperatorFeePeriods()).to.deep.equal([DECLARE_OPERATOR_FEE_PERIOD, EXECUTE_OPERATOR_FEE_PERIOD]); + await expect(await views.getOperatorFeeIncreaseLimit()).to.equal(OPERATOR_MAX_FEE_INCREASE); + await expect(await views.getActiveOracleIds()).to.deep.equal(DEFAULT_ORACLES_IDS); + await expect(await views.getNetworkFeeSSV()).to.equal(NETWORK_FEE); + await expect(await views.cooldownDuration()).to.equal(DEFAULT_UNSTAKE_COOLDOWN); + await expect(await views.getNetworkEarnings()).to.equal(0n); + await expect(await views.totalStaked()).to.equal(0n); + }); + }); + + describe("Function 'registerOperator()'", async function () { + it("Creates new operator and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + const tx = await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + const receipt = await tx.wait(); + + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_OPERATOR]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_ADDED).withArgs(expectedId, operatorOwner.address, operatorKey, MINIMAL_OPERATOR_ETH_FEE) + .and.to.emit(network, Events.OPERATOR_PRIVACY_STATUS_UPDATED).withArgs([expectedId], true); + + await expect(await views.getOperatorFee(expectedId)).to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + await expect(await views.getOperatorFeeSSV(expectedId)).to.be.equal(0); + await expect(await views.getOperatorDeclaredFee(expectedId)).to.be.deep.equal([false, 0n, 0n, 0n]); + await expect(await views.getOperatorById(expectedId)).to.be.deep.equal([ + operatorOwner.address, + MINIMAL_OPERATOR_ETH_FEE, + 0, + connection.ethers.ZeroAddress, + true, + true + ]); + + await expect(await views.getOperatorByIdSSV(expectedId)).to.be.deep.equal([ + operatorOwner.address, + 0, + 0, + connection.ethers.ZeroAddress, + true, + true + ]); + }); + + it("Is reverted with 'FeeTooLow' if the provided fee is less than minimal allowed", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE - 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeTooHigh' if the provided fee is higher than maximum allowed", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + await expect(network.registerOperator(operatorKey, MAXIMUM_OPERATORS_FEE + 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + + it("Is reverted with 'OperatorAlreadyExists' if the public key is already registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_ALREADY_EXISTS); + }); + }); + + describe("Function 'removeOperator()'", async function () { + it("Deactivates the operator and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + const tx = await network.removeOperator(expectedId); + + await expect(tx) + .to.emit(network, Events.OPERATOR_REMOVED) + .withArgs(expectedId); + + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR]); + const operator: OperatorTuple = await views.getOperatorById(expectedId); + + await expect(operator[5]).to.be.equal(false); + await expect(await views.getOperatorFee(expectedId)).to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator with passed id is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.removeOperator(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await expect(network.connect(randomUser).removeOperator(expectedId)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setOperatorsWhitelists()'", async function () { + it("Whitelists addresses and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(await network.setOperatorsWhitelists([expectedId], [clusterOwner])) + .to.emit(network, Events.OPERATOR_MULTIPLE_WHITELIST_UPDATED) + .withArgs([expectedId], [clusterOwner]); + await expect(await views.getWhitelistedOperators([expectedId], clusterOwner)).to.be.deep.equal([expectedId]); + }); + + it("Whitelists multiple operators for multiple addresses", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 10); + const whitelistAddresses = Array(10).fill(clusterOwner.address); + + const tx = await network.setOperatorsWhitelists(operatorIds, whitelistAddresses); + const receipt = await tx.wait(); + + await trackGasFromReceipt(receipt, [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.setOperatorsWhitelists([], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidWhitelistAddressesLength' if the array of addresses is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.setOperatorsWhitelists([123], [])) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELIST_ADDRESSES_LENGTH); + }); + + it("Is reverted with 'ZeroAddressNotAllowed' if one of addresses is zero address", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await expect(network.setOperatorsWhitelists([expectedId], [connection.ethers.ZeroAddress])) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS_NOT_ALLOWED); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.setOperatorsWhitelists([123456789], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.connect(randomUser).setOperatorsWhitelists([expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicate", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await expect(network.setOperatorsWhitelists([expectedId, expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 3); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + await expect(network.setOperatorsWhitelists(operatorIds, [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + }); + + describe("Function 'removeOperatorsWhitelists()'", async function () { + it("Removes addresses from the whitelist and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.setOperatorsWhitelists([expectedId], [clusterOwner]); + await expect(await network.removeOperatorsWhitelists([expectedId], [clusterOwner])) + .to.emit(network, Events.OPERATOR_MULTIPLE_WHITELIST_REMOVED) + .withArgs([expectedId], [clusterOwner]); + await expect(await views.getWhitelistedOperators([expectedId], clusterOwner)).to.be.deep.equal([]); + }); + + it("Removes multiple operators for multiple addresses", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 10); + const whitelistAddresses = Array(10).fill(clusterOwner.address); + await network.setOperatorsWhitelists(operatorIds, whitelistAddresses); + const tx = await network.removeOperatorsWhitelists(operatorIds, whitelistAddresses); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.removeOperatorsWhitelists([], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidWhitelistAddressesLength' if the array of addresses is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.removeOperatorsWhitelists([123], [])) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELIST_ADDRESSES_LENGTH); + }); + + it("Is reverted with 'ZeroAddressNotAllowed' if one of addresses is zero address", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await expect(network.removeOperatorsWhitelists([expectedId], [connection.ethers.ZeroAddress])) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS_NOT_ALLOWED); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.removeOperatorsWhitelists([123456789], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await expect(network.connect(randomUser).removeOperatorsWhitelists([expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicate", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await expect(network.removeOperatorsWhitelists([expectedId, expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 3); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + await expect(network.removeOperatorsWhitelists(operatorIds, [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + }); + + describe("Function 'setOperatorsWhitelistingContract()'", async function () { + it("Registers whitelisting contract, emits correct event and allows to whitelist addresses via contract", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 3); + const { contract: whiteListingContract, address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + const tx = await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT]); + await expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, contractAddress); + await expect(await views.isWhitelistingContract(contractAddress)).to.be.equal(true); + await whiteListingContract.addWhitelistedAddress(clusterOwner); + await expect(await views.isAddressWhitelistedInWhitelistingContract(clusterOwner, operatorIds[0], contractAddress)) + .to.be.equal(true); + }); + + it("Updates whitelisting contract for operators", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 3); + const { contract: firstContract, address: firstAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + const { contract: secondContract, address: secondAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + await network.setOperatorsWhitelistingContract(operatorIds, firstContract); + const tx = await network.setOperatorsWhitelistingContract(operatorIds, secondContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.UPDATE_OPERATOR_WHITELISTING_CONTRACT]); + await expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, secondAddress); + await expect(firstAddress).to.not.equal(secondAddress); + }); + + it("Registers whitelisting contract for 10 operators", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 10); + const { contract: whiteListingContract } = await deployContract(connection.ethers, "BasicWhitelisting"); + const tx = await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10]); + }); + + it("Is reverted with 'InvalidWhitelistingContract' if the contract does not support required interface", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { address: contractAddress } = await deployContract(connection.ethers, "SSVOperatorsWhitelist"); + const operatorIds = await registerOperators(network, operatorOwner, 3); + await expect(network.setOperatorsWhitelistingContract(operatorIds, contractAddress)) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELISTING_CONTRACT) + .withArgs(contractAddress); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' is the array of operators is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + await expect(network.setOperatorsWhitelistingContract([], contractAddress)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + await expect(network.setOperatorsWhitelistingContract([12345n], contractAddress)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + const operatorIds = await registerOperators(network, operatorOwner, 3); + await expect(network.connect(randomUser).setOperatorsWhitelistingContract(operatorIds, contractAddress)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'removeOperatorsWhitelistingContract()'", async function () { + it("Removes whitelisting address and emits correct event", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 3); + const { contract: whiteListingContract } = await deployContract(connection.ethers, "BasicWhitelisting"); + await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const tx = await network.removeOperatorsWhitelistingContract(operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT]); + await expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, connection.ethers.ZeroAddress); + }); + + it("Removes whitelisting contract for 10 operators", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 10); + const { contract: whiteListingContract } = await deployContract(connection.ethers, "BasicWhitelisting"); + await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const tx = await network.removeOperatorsWhitelistingContract(operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.removeOperatorsWhitelistingContract([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.removeOperatorsWhitelistingContract([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if one of operators is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.connect(randomUser).removeOperatorsWhitelistingContract(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setOperatorsPrivateUnchecked()'", async function () { + it("Changes privacy status and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(await network.setOperatorsPrivateUnchecked(operatorIds)) + .to.emit(network, Events.OPERATORS_PRIVACY_STATUS_UPDATED) + .withArgs(operatorIds, true); + const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); + await expect(operator[4]).to.be.equal(true); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.setOperatorsPrivateUnchecked([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.setOperatorsPrivateUnchecked([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(network.connect(randomUser).setOperatorsPrivateUnchecked(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setOperatorsPublicUnchecked()'", async function () { + it("Changes privacy status and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(await network.setOperatorsPublicUnchecked(operatorIds)) + .to.emit(network, Events.OPERATORS_PRIVACY_STATUS_UPDATED) + .withArgs(operatorIds, false); + const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); + await expect(operator[4]).to.be.equal(false); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.setOperatorsPublicUnchecked([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.setOperatorsPublicUnchecked([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(network.connect(randomUser).setOperatorsPublicUnchecked(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'declareOperatorFee()'", async function () { + it("Declares new fee and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const [declarePeriod, executePeriod] = await views.getOperatorFeePeriods(); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + const tx: ContractTransactionResponse = await network.declareOperatorFee(operatorIds[0], newFee); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DECLARE_OPERATOR_FEE]); + const block = await tx.getBlock(); + const expectedBegin = BigInt(block!.timestamp) + declarePeriod; + const expectedEnd = expectedBegin + executePeriod; + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_DECLARED) + .withArgs(operatorOwner.address, operatorIds[0], tx.blockNumber, newFee); + await expect(await views.getOperatorDeclaredFee(operatorIds[0])) + .to.be.deep.equal([ + true, + newFee, + expectedBegin, + expectedEnd + ]); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + await expect(network.declareOperatorFee(12345n, newFee)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await expect(network.connect(randomUser).declareOperatorFee(operatorIds[0], newFee)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'FeeTooLow' is the passed fee is less than minimal", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE - 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'SameFeeChangeNotAllowed' is the passed value is the same as current one", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.SAME_FEE_CHANGE_NOW_ALLOWED); + }); + + it("Is reverted with 'FeeTooHigh' if the new fee is higher than allowed", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(network.declareOperatorFee(operatorIds[0], MAXIMUM_OPERATORS_FEE + 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + + it("Is reverted with 'FeeExceedsIncreaseLimit' if the new fee exceeds the allowed limit", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const exceedingFee = await getFeeAboveIncreaseLimit(views, operatorIds[0]); + await expect(network.declareOperatorFee(operatorIds[0], exceedingFee)) + .to.be.revertedWithCustomError(network, Errors.FEE_EXCEEDS_INCREASE_LIMIT); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' if operators current fee is zero", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, 0, true); + await expect(network.declareOperatorFee(expectedId, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + }); + + describe("Function 'cancelDeclaredOperatorFee()'", async function () { + it("Cancels declared fee and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee); + const tx = await network.cancelDeclaredOperatorFee(operatorIds[0]); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CANCEL_OPERATOR_FEE]); + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_DECLARATION_CANCELLED) + .withArgs(operatorOwner, operatorIds[0]); + await expect(await views.getOperatorDeclaredFee(operatorIds[0])) + .to.be.deep.equal([ + false, + 0n, + 0n, + 0n + ]); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.cancelDeclaredOperatorFee(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee); + await expect(network.connect(randomUser).cancelDeclaredOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'NoFeeDeclared' if no declarations were done before", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(network.cancelDeclaredOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + }); + + describe("Function 'executeOperatorFee()'", async function () { + it("Updates operator fee according to a declared one and emits the correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee); + const [isActive, declaredFee, begin, end] = await views.getOperatorDeclaredFee(operatorIds[0]); + await connection.networkHelpers.time.increaseTo(begin + 1n); + await connection.networkHelpers.mine(); + const tx = await network.executeOperatorFee(operatorIds[0]); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.EXECUTE_OPERATOR_FEE]); + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_EXECUTED); + await expect(await views.getOperatorFee(operatorIds[0])).to.be.equal(newFee); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.executeOperatorFee(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee); + await expect(network.connect(randomUser).executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'NoFeeDeclared' if no declarations were done before", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + + it("Is reverted with 'ApprovalNotWithinTimeframe' if execution period is not started or ended", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee); + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + const [declarePeriod, executePeriod] = await views.getOperatorFeePeriods(); + const [isActive, declaredFee, begin, end] = await views.getOperatorDeclaredFee(operatorIds[0]); + await connection.networkHelpers.time.increaseTo(end + 1n); + await connection.networkHelpers.mine(); + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + }); + + it("Is reverted with 'FeeTooHigh' if the maximum fee changed during the execution period", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.connect(operatorOwner).declareOperatorFee(operatorIds[0], newFee); + const [isActive, declaredFee, begin, end] = await views.getOperatorDeclaredFee(operatorIds[0]); + await network.connect(daoSigner).updateMaximumOperatorFee(newFee - OPERATOR_FEE_PRECISION); + await connection.networkHelpers.time.increaseTo(begin + 1n); + await connection.networkHelpers.mine(); + await expect(network.connect(operatorOwner).executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + }); + + describe("Function 'updateMaximumOperatorFee()'", async function () { + it("Updates maximum fee and emits correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const tx = await network.connect(daoSigner) + .updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE * 2n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]); + await expect(tx) + .to.emit(network, Events.OPERATOR_MAXIMUM_FEE_UPDATED); + await expect(await views.getMaximumOperatorFee()) + .to.be.equal(MAXIMUM_OPERATORS_FEE * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if the caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(randomUser).updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'reduceOperatorFee()'", async function () { + it("Decreases fee and emits the correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + const tx = await network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REDUCE_OPERATOR_FEE]); + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_EXECUTED); + await expect(await views.getOperatorFee(operatorId)) + .to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + }); + + it("Is reverted with 'OperatorDoesNotExist' if the operator is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.reduceOperatorFee(12345n, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + await expect(network.connect(randomUser).reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'FeeTooLow' if the passed fee is less than minimum allowed", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + await expect(network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE - 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' if caller is trying to increase the fee", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + await expect(network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE * 3n)) + .to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + }); + + describe("Function 'withdrawOperatorEarnings()'", async function () { + it("Withdraws operators earnings, update balances and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + const registrationDeposit = requiredDeposit + DEFAULT_ETH_REGISTER_VALUE; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (registrationDeposit + 10n ** 18n)); + const earningsPeriod = 100n; + await network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: registrationDeposit }); + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + await expect(expectedEarnings).to.be.equal(earnings); + const withdrawAmount = earnings + MINIMAL_OPERATOR_ETH_FEE; + const tx = await network.connect(operatorOwner).withdrawOperatorEarnings(operatorIds[0], withdrawAmount); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.WITHDRAW_OPERATOR_BALANCE]); + await expect(tx) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], withdrawAmount); + await expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0n); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.withdrawOperatorEarnings(12345n, 9999n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.connect(randomUser).withdrawOperatorEarnings(operatorIds[0], 9999n)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'InsufficientBalance' if the amount is less than operator earnings", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.withdrawOperatorEarnings(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function 'withdrawAllOperatorEarnings()'", async function () { + it("Withdraws all operators earnings, update balances and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + const registrationDeposit = requiredDeposit + DEFAULT_ETH_REGISTER_VALUE; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (registrationDeposit + 10n ** 18n)); + const earningsPeriod = 100n; + await network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: registrationDeposit }); + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + await expect(expectedEarnings).to.be.equal(earnings); + await expect(network.connect(operatorOwner).withdrawAllOperatorEarnings(operatorIds[0])) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], earnings + MINIMAL_OPERATOR_ETH_FEE); + await expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.withdrawAllOperatorEarnings(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.connect(randomUser).withdrawAllOperatorEarnings(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'withdrawAllVersionOperatorEarnings()'", async function () { + it("Withdraws all operators earnings and emits correct events", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + const registrationDeposit = requiredDeposit + DEFAULT_ETH_REGISTER_VALUE; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (registrationDeposit + 10n ** 18n)); + const earningsPeriod = 100n; + await network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: registrationDeposit }); + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + await expect(expectedEarnings).to.be.equal(earnings); + await expect(network.connect(operatorOwner).withdrawAllVersionOperatorEarnings(operatorIds[0])) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], earnings + MINIMAL_OPERATOR_ETH_FEE); + await expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.withdrawAllVersionOperatorEarnings(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.connect(randomUser).withdrawAllVersionOperatorEarnings(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setFeeRecipientAddress()'", async function () { + it("Emits the correct event with the correct input data", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(randomUser).setFeeRecipientAddress(clusterOwner.address)) + .to.emit(network, Events.FEE_RECIPIENT_ADDRESS_UPDATED) + .withArgs(randomUser.address, clusterOwner.address); + }); + }); + + describe("Function 'updateOperatorFeeIncreaseLimit()'", async function () { + it("Changes fee increase limit and emits the correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newLimit = OPERATOR_MAX_FEE_INCREASE - 1n; + const tx = await network.connect(daoSigner) + .updateOperatorFeeIncreaseLimit(newLimit); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]); + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_INCREASE_LIMIT_UPDATED) + .withArgs(newLimit); + await expect(await views.getOperatorFeeIncreaseLimit()).to.be.equal(newLimit); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(randomUser).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateDeclareOperatorFeePeriod()'", async function () { + it("Changes the fee declare period and emits correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const tx = await network.connect(daoSigner) + .updateDeclareOperatorFeePeriod(DECLARE_OPERATOR_FEE_PERIOD + 1n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]); + await expect(tx) + .to.emit(network, Events.DECLARE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(DECLARE_OPERATOR_FEE_PERIOD + 1n); + await expect(await views.getOperatorFeePeriods()) + .to.be.deep.equal([DECLARE_OPERATOR_FEE_PERIOD + 1n, EXECUTE_OPERATOR_FEE_PERIOD]); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(randomUser).updateDeclareOperatorFeePeriod(DECLARE_OPERATOR_FEE_PERIOD + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateExecuteOperatorFeePeriod()'", async function () { + it("Changes the fee execute period and emits correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const [initialDeclarePeriod, initialExecutePeriod] = await views.getOperatorFeePeriods(); + const newExecutePeriod = initialExecutePeriod + 1n; + const tx = await network.connect(daoSigner) + .updateExecuteOperatorFeePeriod(newExecutePeriod); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]); + await expect(tx) + .to.emit(network, Events.EXECUTE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(newExecutePeriod); + await expect(await views.getOperatorFeePeriods()) + .to.be.deep.equal([initialDeclarePeriod, newExecutePeriod]); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(randomUser).updateExecuteOperatorFeePeriod(EXECUTE_OPERATOR_FEE_PERIOD + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateLiquidationThresholdPeriod()'", async function () { + it("Changes the period and emits correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + const tx = await network.connect(daoSigner) + .updateLiquidationThresholdPeriod(newThreshold); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]); + await expect(tx) + .to.emit(network, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED) + .withArgs(newThreshold); + await expect(await views.getLiquidationThresholdPeriod()) + .to.be.equal(newThreshold); + }); + + it("Is reverted 'NewBlockPeriodIsBelowMinimum' if the passed threshold is less than minimum allowed", async function () { + const { network, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(daoSigner).updateLiquidationThresholdPeriod(MINIMAL_LIQUIDATION_THRESHOLD - 1n)) + .to.be.revertedWithCustomError(network, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + await expect(network.connect(randomUser).updateLiquidationThresholdPeriod(newThreshold)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateLiquidationThresholdPeriodSSV()'", async function () { + it("Changes the period and emits correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + await expect(network.connect(daoSigner).updateLiquidationThresholdPeriodSSV(newThreshold)) + .to.emit(network, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED_SSV) + .withArgs(newThreshold); + await expect(await views.getLiquidationThresholdPeriodSSV()) + .to.be.equal(newThreshold); + }); + + it("Is reverted 'NewBlockPeriodIsBelowMinimum' if the passed threshold is less than minimum allowed", async function () { + const { network, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(daoSigner).updateLiquidationThresholdPeriodSSV(MINIMAL_LIQUIDATION_THRESHOLD - 1n)) + .to.be.revertedWithCustomError(network, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + await expect(network.connect(randomUser).updateLiquidationThresholdPeriodSSV(newThreshold)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateMinimumLiquidationCollateral()'", async function () { + it("Changes collateral and emits correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const tx = await network.connect(daoSigner) + .updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_MINIMUM_COLLATERAL]); + await expect(tx) + .to.emit(network, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED) + .withArgs(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + await expect(await views.getMinimumLiquidationCollateral()) + .to.be.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(randomUser).updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateMinimumLiquidationCollateralSSV()'", async function () { + it("Changes collateral and emits correct event", async function () { + const { network, views, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(daoSigner).updateMinimumLiquidationCollateralSSV(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.emit(network, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED_SSV) + .withArgs(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + await expect(await views.getMinimumLiquidationCollateralSSV()) + .to.be.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.connect(randomUser).updateMinimumLiquidationCollateralSSV(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'registerValidator()'", async function () { + it("For a new cluster, creates it with a passed validator and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredDeposit + 10n ** 18n)); + const tx = await network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE]); + await expect(tx).to.emit(network, Events.VALIDATOR_ADDED); + const expectedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await expect(await views.getValidator(clusterOwner, validatorKey)).to.equal(true); + await expect(await views.isLiquidatable(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.isLiquidated(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.getBurnRate(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(await calculateInitialBurnRate(views, operatorIds, expectedCluster)); + await expect(await views.getBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(requiredDeposit); + await expect(await views.getEffectiveBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_EB_PER_VALIDATOR); + await expect(await views.getClusterAssetType(clusterOwner, operatorIds)) + .to.be.equal(CLUSTER_VERSION_ETH); + await expect(views.isLiquidatableSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBalanceSSV(clusterOwner, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Registers a validator for a new ETH cluster using whitelisting contract", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + const { contract: whitelistingContract, address: whitelistingContractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + await whitelistingContract.addWhitelistedAddress(clusterOwner.address); + await network.setOperatorsWhitelistingContract(operatorIds, whitelistingContractAddress); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredDeposit + 10n ** 18n)); + const tx = await network.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTING_CONTRACT_4]); + }); + + it("Registers a validator for a new ETH cluster with one whitelisted operator", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsPublicUnchecked([operatorIds[1], operatorIds[2], operatorIds[3]]); + await network.setOperatorsWhitelists([operatorIds[0]], [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredDeposit + 10n ** 18n)); + const tx = await network.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]); + }); + + it("Registers a validator for a new ETH cluster with four whitelisted operators", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredDeposit + 10n ** 18n)); + const tx = await network.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4]); + }); + + it("Registers a validator into an existing ETH cluster with four whitelisted operators", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForOne = perValidatorBurn * 1n * minBlocks; + const requiredForOne = thresholdForOne > minCollateral ? thresholdForOne : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredForOne + 10n ** 18n)); + await network.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredForOne }); + const existingCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, existingCluster); + const newValidatorCount = BigInt(existingCluster.validatorCount) + 1n; + const newThreshold = perValidatorBurn * newValidatorCount * minBlocks; + const newRequired = newThreshold > minCollateral ? newThreshold : minCollateral; + let additionalDeposit = newRequired > currentBalance ? newRequired - currentBalance : 0n; + additionalDeposit += perValidatorBurn * 2n; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (additionalDeposit + 10n ** 18n)); + const tx = await network.connect(clusterOwner).registerValidator(makePublicKey(2), operatorIds, DEFAULT_SHARES, existingCluster, { value: additionalDeposit }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4]); + }); + + it("Registers a validator into an existing ETH cluster", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForOne = perValidatorBurn * 1n * minBlocks; + const requiredForOne = thresholdForOne > minCollateral ? thresholdForOne : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredForOne + 10n ** 18n)); + await network.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredForOne }); + const existingCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, existingCluster); + const newValidatorCount = BigInt(existingCluster.validatorCount) + 1n; + const newThreshold = perValidatorBurn * newValidatorCount * minBlocks; + const newRequired = newThreshold > minCollateral ? newThreshold : minCollateral; + let additionalDeposit = newRequired > currentBalance ? newRequired - currentBalance : 0n; + additionalDeposit += perValidatorBurn * 2n; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (additionalDeposit + 10n ** 18n)); + const tx = await network.connect(clusterOwner).registerValidator(makePublicKey(2), operatorIds, DEFAULT_SHARES, existingCluster, { value: additionalDeposit }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_ETH_CLUSTER]); + }); + + it("Registers a validator into a prefunded ETH cluster with zero additional deposit", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForTwo = perValidatorBurn * 2n * minBlocks; + const requiredForTwo = thresholdForTwo > minCollateral ? thresholdForTwo : minCollateral; + const initialDeposit = requiredForTwo + perValidatorBurn * 2n; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (initialDeposit + 10n ** 18n)); + await network.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: initialDeposit }); + const existingCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const tx = await network.connect(clusterOwner).registerValidator(makePublicKey(2), operatorIds, DEFAULT_SHARES, existingCluster, { value: 0 }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the amount of operators is not the allowed one", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 5); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidPublicKeyLength' if the public key is not 48 bytes", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const invalidLengthPublicKey = makePublicKey(1) + "11"; + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await expect(network.connect(clusterOwner).registerValidator(invalidLengthPublicKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'ValidatorAlreadyExistsWithData' if the public key is already registered", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredDeposit * 2n + 10n ** 18n)); + await network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit }); + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit })) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_ALREADY_REGISTERED) + .withArgs(validatorKey, clusterOwner.address); + }); + + it("Is reverted with 'IncorrectClusterState' for the new cluster is the cluster data is not consisting from zeroes", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const invalidCluster = { ...EMPTY_CLUSTER, validatorCount: 123n }; + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, invalidCluster, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicates", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + operatorIds.pop(); + operatorIds.unshift(operatorIds[0]); + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'CallerNotWhitelistedWithData' if one of operators did not whitelist the caller", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_WHITELISTED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'ExceedValidatorLimitWithData' if one of operators will exceed the network limit", async function () { + const { network, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const { address: upgradeImplAddr } = await deployContract(connection.ethers, "SSVNetworkValidatorsPerOperatorUpgrade"); + const factory = await connection.ethers.getContractFactory("SSVNetworkValidatorsPerOperatorUpgrade"); + const initData = factory.interface.encodeFunctionData("initializev2", [0]); + await network.connect(daoSigner).upgradeToAndCall(upgradeImplAddr, initData); + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_VALIDATORS_LIMIT_EXCEEDED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'InsufficientBalance' if msg value is not enough to cover the validator", async function () { + const { network, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(daoSigner).updateMinimumLiquidationCollateral(100000n); + await expect(network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: 0 })) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function bulkRegisterValidator()", async function () { + it("Registers bulk of validators, creates a new cluster with the expected data and emits correct events", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const numValidators = BigInt(keys.length); + const threshold = burnRate * numValidators * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredDeposit + 10n ** 18n)); + const tx = await network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: requiredDeposit }); + await tx.wait(); + const expectedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + for (let i = 0; i < keys.length; i++) { + await expect(await views.getValidator(clusterOwner, keys[i])).to.equal(true); + } + await expect(await views.isLiquidatable(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.isLiquidated(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.getBurnRate(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(await calculateInitialBurnRate(views, operatorIds, expectedCluster)); + await expect(await views.getBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(requiredDeposit); + await expect(await views.getEffectiveBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_EB_PER_VALIDATOR * numValidators); + await expect(await views.getClusterAssetType(clusterOwner, operatorIds)) + .to.be.equal(CLUSTER_VERSION_ETH); + await expect(views.isLiquidatableSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBalanceSSV(clusterOwner, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Registers bulk of validators into an existing cluster with one whitelisting contract operator", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsPublicUnchecked([operatorIds[1], operatorIds[2], operatorIds[3]]); + const { contract: whitelistingContract } = await deployContract(connection.ethers, "BasicWhitelisting"); + await whitelistingContract.addWhitelistedAddress(clusterOwner.address); + await network.setOperatorsWhitelistingContract([operatorIds[0]], whitelistingContract); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForOne = perValidatorBurn * 1n * minBlocks; + const requiredForOne = thresholdForOne > minCollateral ? thresholdForOne : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredForOne + 10n ** 18n)); + await network.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredForOne }); + const existingCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, existingCluster); + const newValidatorCount = BigInt(existingCluster.validatorCount) + 10n; + const newThreshold = perValidatorBurn * newValidatorCount * minBlocks; + const newRequired = newThreshold > minCollateral ? newThreshold : minCollateral; + let additionalDeposit = newRequired > currentBalance ? newRequired - currentBalance : 0n; + additionalDeposit += perValidatorBurn * 2n; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (additionalDeposit + 10n ** 18n)); + const keys = Array.from({ length: 10 }, (_, i) => makePublicKey(i + 2)); + const shares = Array(10).fill(DEFAULT_SHARES); + const tx = await network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, existingCluster, { value: additionalDeposit }); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the amount of operators is not the allowed one", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 5); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidPublicKeyLength' if one of public keys is not 48 bytes", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const invalidLengthPublicKey = makePublicKey(1) + "11"; + keys.shift(); + keys.unshift(invalidLengthPublicKey); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'ValidatorAlreadyExistsWithData' if one of public keys is already registered", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (requiredDeposit + 10n ** 18n)); + await network.connect(clusterOwner).registerValidator(keys[7], operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit }); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: requiredDeposit })) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_ALREADY_REGISTERED) + .withArgs(keys[7], clusterOwner.address); + }); + + it("Is reverted with 'IncorrectClusterState' for the new cluster is the cluster data is not consisting from zeroes", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const invalidCluster = { ...EMPTY_CLUSTER, validatorCount: 123n }; + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, invalidCluster, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicates", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + operatorIds.pop(); + operatorIds.unshift(operatorIds[0]); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'CallerNotWhitelistedWithData' if one of operators did not whitelist the caller", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_WHITELISTED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'ExceedValidatorLimitWithData' if one of operators will exceed the network limit", async function () { + const { network, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const { address: upgradeImplAddr } = await deployContract(connection.ethers, "SSVNetworkValidatorsPerOperatorUpgrade"); + const factory = await connection.ethers.getContractFactory("SSVNetworkValidatorsPerOperatorUpgrade"); + const initData = factory.interface.encodeFunctionData("initializev2", [0]); + await network.connect(daoSigner).upgradeToAndCall(upgradeImplAddr, initData); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_VALIDATORS_LIMIT_EXCEEDED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'InsufficientBalance' if msg value is not enough to cover new validators", async function () { + const { network, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(daoSigner).updateMinimumLiquidationCollateral(100000n); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, EMPTY_CLUSTER, { value: 0 })) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Is reverted with 'EmptyPublicKeysList' if the array of public keys is empty", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await expect(network.connect(clusterOwner).bulkRegisterValidator([], operatorIds, shares, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.EMPTY_PUBLIC_KEYS_LIST); + }); + + it("Is reverted with 'PublicKeysSharesLengthMismatch' if the array of keys and array of shares have different length", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { keys, shares } = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const sharesWithMismatch = shares.slice(0, shares.length - 1); + await expect(network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, sharesWithMismatch, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE })) + .to.be.revertedWithCustomError(network, Errors.PUBLIC_KEYS_SHARES_LENGTH_MISMATCH); + }); + }); + + describe("Function 'removeValidator()'", async function () { + it("Removes validator and emits correct event", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + const tx = await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_VALIDATOR]); + await expect(tx) + .to.emit(network, Events.VALIDATOR_REMOVED); + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await expect(clusterAfter.validatorCount).to.equal(0n); + await expect(clusterAfter.active).to.equal(true); + await expect(await views.getValidator(clusterOwner.address, validatorKey)).to.be.equal(false); + }); + + it("Is reverted with 'ClusterDoesNotExists' if the cluster with this owner and operators does not exist", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await expect(network.connect(randomUser).removeValidator(validatorKey, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the cluster data is incorrect", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + cluster.validatorCount += 1n; + await expect(network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ValidatorDoesNotExist' if the validator was never registered", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + const incorrectValidator: string = validatorKey + "11"; + await expect(network.connect(clusterOwner).removeValidator(incorrectValidator, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reveted with 'ValidatorDoesNotExist' if validator is already removed", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const updatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await expect(network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, updatedCluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function 'bulkRemoveValidator()'", async function () { + it("Is reverted with 'ClusterDoesNotExists' if the cluster with this owner and operators does not exist", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await expect(network.connect(randomUser).bulkRemoveValidator([validatorKey], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the cluster data is incorrect", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + cluster.validatorCount += 1n; + await expect(network.connect(clusterOwner).bulkRemoveValidator([validatorKey], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ValidatorDoesNotExist' if the validator was never registered", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + const incorrectValidator: string = validatorKey + "11"; + await expect(network.connect(clusterOwner).bulkRemoveValidator([incorrectValidator], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reveted with 'ValidatorDoesNotExist' if validator is already removed", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const { cluster, validatorKey, operatorIds } = await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const updatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await expect(network.connect(clusterOwner).bulkRemoveValidator([validatorKey], operatorIds, updatedCluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function stake()", async function () { + it("Stakes SSV, mints CSSV to the staker and creates delegation weight", async function () { + const { network, views, ssvToken, cssvToken, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + const tx = await network.connect(randomUser).stake(STAKE_AMOUNT); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.STAKE_SSV]); + await expect(tx) + .to.emit(network, Events.STAKED) + .withArgs(randomUser.address, STAKE_AMOUNT); + await expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + await expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + }); + + it("Is reverted with 'StakeTooLow' if the amount to stake is smaller than minimum allowed", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.stake(1)) + .to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + + it("Is reverted with 'StakeTooLow' when caller is trying to stake 0 SSV", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.stake(0)) + .to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + }); + + describe("Function requestUnstake()", async function () { + it("For full amount, creates unstake request, burns CSSV and removes delegation", async function () { + const { network, views, ssvToken, cssvToken, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + const tx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REQUEST_UNSTAKE]); + const block = await tx.getBlock(); + await expect(tx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT, BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + const requests: UnstakeRequest[] = await views.pendingUnstake(randomUser.address); + await expect(requests.length).to.be.equal(1); + await expect(requests[0].amount).to.be.equal(STAKE_AMOUNT); + await expect(requests[0].unlockTime).to.be.equal(BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + await expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(0); + await expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(0); + }); + + it("For partial amount, creates unstake request, burns CSSV and removes delegation", async function () { + const { network, views, ssvToken, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + const tx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT / 2n); + await tx.wait(); + const block = await tx.getBlock(); + await expect(tx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT / 2n, BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + let requests: UnstakeRequest[] = await views.pendingUnstake(randomUser.address); + await expect(requests.length).to.be.equal(1); + await expect(requests[0].amount).to.be.equal(STAKE_AMOUNT / 2n); + await expect(requests[0].unlockTime).to.be.equal(BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + const secondTx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT / 2n); + await secondTx.wait(); + const secondBlock = await secondTx.getBlock(); + await expect(secondTx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT / 2n, BigInt(secondBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + requests = await views.pendingUnstake(randomUser.address); + await expect(requests.length).to.be.equal(2); + await expect(requests[0].amount).to.be.equal(STAKE_AMOUNT / 2n); + await expect(requests[0].unlockTime).to.be.equal(BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + await expect(requests[1].amount).to.be.equal(STAKE_AMOUNT / 2n); + await expect(requests[1].unlockTime).to.be.equal(BigInt(secondBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + }); + + it("Is reverted with 'MaxRequestsAmountReached' if more than 2000 pending requests", async function () { + const { network, ssvToken, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + const smallAmount = STAKE_AMOUNT / 2001n; + for (let i = 0; i < 2000; i++) { + await network.connect(randomUser).requestUnstake(smallAmount); + } + await expect(network.connect(randomUser).requestUnstake(smallAmount)) + .to.be.revertedWithCustomError(network, Errors.MAX_REQUESTS_AMOUNT_REACHED); + }); + + it("Is reverted with 'ZeroAmount' if caller is trying to request 0 SSV", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await expect(network.requestUnstake(0)) + .to.be.revertedWithCustomError(network, Errors.ZERO_AMOUNT); + }); + + it("Is reverted with 'UnstakeAmountExceedsBalance' if caller is trying to request more SSV than they staked", async function () { + const { network, ssvToken, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + await expect(network.connect(randomUser).requestUnstake(STAKE_AMOUNT + 1n)) + .to.be.revertedWithCustomError(network, Errors.UNSTAKE_AMOUNT_EXCEEDS_BALANCE); + }); + }); + + describe("Function 'withdrawUnlocked()'", async function () { + it("Withdraws SSV and emits correct event", async function () { + const { network, views, ssvToken, cssvToken, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + await network.connect(randomUser).requestUnstake(STAKE_AMOUNT); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await networkHelpers.mine(); + const tx = await network.connect(randomUser).withdrawUnlocked(); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.WITHDRAW_UNSTAKE]); + await expect(tx) + .to.emit(network, Events.UNSTAKE_WITHDRAWN) + .withArgs(randomUser.address, STAKE_AMOUNT); + await expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(0); + await expect(await ssvToken.balanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + await expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(0); + }); + }); + + describe("Function 'claimEthRewards()'", async function () { + it("Processes only the first claim when two claims are mined in the same block", async function () { + const { network, views, ssvToken, daoSigner } = await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + + const oracles = (await connection.ethers.getSigners()).slice(10, 14); + await network.connect(daoSigner).replaceOracle(1, oracles[0].address); + await network.connect(daoSigner).replaceOracle(2, oracles[1].address); + await network.connect(daoSigner).replaceOracle(3, oracles[2].address); + await network.connect(daoSigner).replaceOracle(4, oracles[3].address); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + const clusters = await registerDefaultClusters(connection, network, operatorIds, operatorOwner, 8); + const merkleData = buildEBMerkleForDefaultClusters(connection, clusters, 33); + const block = await connection.ethers.provider.getBlock("latest"); + const blockNum = block!.number; + + for (let i = 0; i < 3; i++) { + await network.connect(oracles[i]).commitRoot(merkleData.root, blockNum); + } + + await updateClusterBalancesForDefaultClusters(network, clusters, merkleData, blockNum, 33); + + const claimableBefore = await views.previewClaimableEth(randomUser.address); + expect(claimableBefore).to.be.greaterThan(0n); + + const provider = connection.ethers.provider; + const startingBalance = await provider.getBalance(randomUser.address); + const pendingNonce = await provider.getTransactionCount(randomUser.address, "pending"); + const claimData = network.interface.encodeFunctionData("claimEthRewards"); + const networkAddress = await network.getAddress(); + + await provider.send("evm_setAutomine", [false]); + + try { + const firstClaim = await randomUser.sendTransaction({ + to: networkAddress, + data: claimData, + nonce: pendingNonce, + gasLimit: 1_000_000n, + }); + const secondClaim = await randomUser.sendTransaction({ + to: networkAddress, + data: claimData, + nonce: pendingNonce + 1, + gasLimit: 1_000_000n, + }); + + await provider.send("evm_mine", []); + + const firstReceipt = await firstClaim.wait(); + const secondReceipt = await secondClaim.wait(); + + expect(firstReceipt!.status).to.equal(1); + expect(secondReceipt!.status).to.equal(0); + + const firstGasCost = BigInt(firstReceipt!.gasUsed) * BigInt(firstReceipt!.gasPrice); + const secondGasCost = BigInt(secondReceipt!.gasUsed) * BigInt(secondReceipt!.gasPrice); + const balanceAfter = await provider.getBalance(randomUser.address); + + expect(balanceAfter + firstGasCost + secondGasCost - startingBalance).to.equal(claimableBefore); + expect(await views.previewClaimableEth(randomUser.address)).to.equal(0n); + } finally { + await provider.send("evm_setAutomine", [true]); + } + }); + }); +}); diff --git a/test/helpers/balance.ts b/test/helpers/balance.ts new file mode 100644 index 000000000..f7a176b8c --- /dev/null +++ b/test/helpers/balance.ts @@ -0,0 +1,106 @@ +import { expect } from "chai"; + +export interface BalanceSnapshot { + eth: bigint; + ssv: bigint; + blockNumber: number; +} + +export async function snapshotBalance(provider: any, ssvToken: any, address: string): Promise { + const [eth, ssv, blockNumber] = await Promise.all([ + provider.getBalance(address), + ssvToken.balanceOf(address), + provider.getBlockNumber(), + ]); + return { + eth: BigInt(eth), + ssv: BigInt(ssv), + blockNumber, + }; +} + +export function assertBalanceDelta(before: BalanceSnapshot, after: BalanceSnapshot, expectedEthDelta: bigint, expectedSsvDelta: bigint, tolerance: bigint = 0n): void { + const ethDelta = after.eth - before.eth; + const ssvDelta = after.ssv - before.ssv; + if (tolerance === 0n) { + expect(ethDelta).to.equal(expectedEthDelta); + expect(ssvDelta).to.equal(expectedSsvDelta); + } else { + const ethDiff = ethDelta - expectedEthDelta; + expect(ethDiff >= -tolerance && ethDiff <= tolerance).to.be.true; + const ssvDiff = ssvDelta - expectedSsvDelta; + expect(ssvDiff >= -tolerance && ssvDiff <= tolerance).to.be.true; + } +} + +export async function snapshotContractBalance(provider: any, contractAddress: string): Promise { + return BigInt(await provider.getBalance(contractAddress)); +} + +export interface ETHDeltaCheck { + address: string; + expectedDelta: bigint; + accountForGas?: boolean; +} + +export interface ETHDeltaResult { + receipt: any; + deltas: Map; +} + +export async function expectETHDelta( + provider: any, + address: string, + action: () => Promise, + expectedDelta: bigint, + options?: { accountForGas?: boolean }, +): Promise { + const result = await expectETHDeltas(provider, action, [ + { address, expectedDelta, accountForGas: options?.accountForGas }, + ]); + return result.receipt; +} + +export async function expectContractETHDelta( + provider: any, + contractAddress: string, + action: () => Promise, + expectedDelta: bigint, +): Promise { + return expectETHDelta(provider, contractAddress, action, expectedDelta); +} + +export async function expectETHDeltas( + provider: any, + action: () => Promise, + checks: ETHDeltaCheck[], +): Promise { + const balancesBefore = await Promise.all( + checks.map(c => provider.getBalance(c.address).then((b: any) => BigInt(b))), + ); + + const result = await action(); + const receipt = result?.wait ? await result.wait() : result; + + const balancesAfter = await Promise.all( + checks.map(c => provider.getBalance(c.address).then((b: any) => BigInt(b))), + ); + + let gasCost = 0n; + if (receipt) { + const gasPrice = BigInt(receipt.effectiveGasPrice ?? receipt.gasPrice); + gasCost = BigInt(receipt.gasUsed) * gasPrice; + } + + const deltas = new Map(); + for (let i = 0; i < checks.length; i++) { + let actual = balancesAfter[i] - balancesBefore[i]; + if (checks[i].accountForGas) { + actual += gasCost; + } + deltas.set(checks[i].address, actual); + expect(actual).to.equal(checks[i].expectedDelta); + } + + return { receipt, deltas }; +} diff --git a/test/helpers/blocks.ts b/test/helpers/blocks.ts new file mode 100644 index 000000000..3d40413b6 --- /dev/null +++ b/test/helpers/blocks.ts @@ -0,0 +1,23 @@ +export async function mineBlocks(provider: any, n: number): Promise { + await provider.send("hardhat_mine", ["0x" + n.toString(16)]); +} + +export async function getBlockNumber(provider: any): Promise { + return provider.getBlockNumber(); +} + +export async function mineToBlock(provider: any, target: number): Promise { + const current = await getBlockNumber(provider); + if (current < target) { + await mineBlocks(provider, target - current); + } +} + +export async function getTxBlock(tx: any): Promise { + const receipt = await tx.wait(); + return receipt.blockNumber; +} + +export async function setAccountBalance(provider: any, address: string, amount: bigint): Promise { + await provider.send("hardhat_setBalance", [address, "0x" + amount.toString(16)]); +} diff --git a/test/helpers/cluster.ts b/test/helpers/cluster.ts new file mode 100644 index 000000000..da2712467 --- /dev/null +++ b/test/helpers/cluster.ts @@ -0,0 +1,196 @@ +import type { NetworkConnection } from 'hardhat/types/network'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import type { SSVNetwork } from '../../types/ethers-contracts/index.js'; +import type { Cluster, ClusterTuple, SSVModules } from '../common/types.ts'; +import { EMPTY_CLUSTER, DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, SSV_MODULE_CONTRACTS } from '../common/constants.ts'; +import { Events } from '../common/events.ts'; +import { makePublicKey } from './keys.ts'; +import { setAccountBalance } from './blocks.ts'; + +export function getHarnessName(module: SSVModules): `${string}Harness` { + return `${SSV_MODULE_CONTRACTS[module]}Harness`; +} + +export function createCluster(overrides: Partial = {}): Cluster { + return { + ...EMPTY_CLUSTER, + active: true, + ...overrides, + }; +} + +export function createLegacySSVCluster(overrides: Partial = {}): Cluster { + return { + ...EMPTY_CLUSTER, + validatorCount: 1n, + active: true, + balance: 10_000_000_000_000_000_000n, + ...overrides, + }; +} + +export const clusterToTuple = (cluster: Cluster): ClusterTuple => [ + cluster.validatorCount, + cluster.networkFeeIndex, + cluster.index, + cluster.active, + cluster.balance, +] as const; + +const EVENT_ABI = [ + 'event ClusterDeposited(address indexed owner, uint64[] operatorIds, uint256 value, tuple(uint32, uint64, uint64, bool, uint256) cluster)', + 'event ClusterWithdrawn(address indexed owner, uint64[] operatorIds, uint256 value, tuple(uint32, uint64, uint64, bool, uint256) cluster)', + 'event ClusterLiquidated(address indexed owner, uint64[] operatorIds, tuple(uint32, uint64, uint64, bool, uint256) cluster)', + 'event ClusterReactivated(address indexed owner, uint64[] operatorIds, tuple(uint32, uint64, uint64, bool, uint256) cluster)', + 'event ValidatorAdded(address indexed owner, uint64[] operatorIds, bytes publicKey, bytes shares, tuple(uint32, uint64, uint64, bool, uint256) cluster)', + 'event ValidatorRemoved(address indexed owner, uint64[] operatorIds, bytes publicKey, tuple(uint32, uint64, uint64, bool, uint256) cluster)', + 'event ClusterMigratedToETH(address indexed owner, uint64[] operatorIds, uint256 ethDeposited, uint256 ssvRefunded, uint32 effectiveBalance, tuple(uint32, uint64, uint64, bool, uint256) cluster)', + 'event ClusterBalanceUpdated(address indexed owner, uint64[] operatorIds, uint64 indexed blockNum, uint32 effectiveBalance, tuple(uint32, uint64, uint64, bool, uint256) cluster)', +] as const; + +export function extractEventArgs(contract: any, receipt: any, eventName: string | string[]): any { + const names = Array.isArray(eventName) ? eventName : [eventName]; + for (const log of receipt.logs ?? []) { + let parsed; + try { + parsed = contract.interface.parseLog(log); + } catch { + continue; + } + if (parsed && names.includes(parsed.name)) { + return parsed.args; + } + } + throw new Error(`${names.join(' | ')} event not found in receipt`); +} + +export function parseClusterFromEvent(contract: any, receipt: any, eventName: string): Cluster { + if (receipt.eventsByName?.[eventName]?.length > 0) { + const parsed = receipt.eventsByName[eventName][0]; + const clusterTuple = parsed.args[parsed.args.length - 1]; + const [validatorCount, networkFeeIndex, index, active, balance] = clusterTuple; + return { + validatorCount: BigInt(validatorCount), + networkFeeIndex: BigInt(networkFeeIndex), + index: BigInt(index), + active, + balance: BigInt(balance), + }; + } + for (const log of receipt.logs ?? []) { + let parsed; + try { + parsed = contract.interface.parseLog(log); + } catch { + continue; + } + if (parsed?.name === eventName) { + const clusterTuple = parsed.args[parsed.args.length - 1]; + const [validatorCount, networkFeeIndex, index, active, balance] = clusterTuple; + return { + validatorCount: BigInt(validatorCount), + networkFeeIndex: BigInt(networkFeeIndex), + index: BigInt(index), + active, + balance: BigInt(balance), + }; + } + } + throw new Error(`Event ${eventName} not found`); +} + +export async function getCurrentClusterState(connection: NetworkConnection<"generic">, networkContract: SSVNetwork, ownerAddress: string, operatorIds: bigint[] | number[]): Promise { + const provider = connection.ethers.provider; + const owner = connection.ethers.getAddress(ownerAddress).toLowerCase(); + const ownerTopic = connection.ethers.zeroPadValue(owner, 32); + const opsExpected = [...operatorIds] + .map(id => BigInt(id).toString()) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + + const latestBlock = await provider.getBlockNumber(); + const minFromBlock = Math.max(0, latestBlock - 199); + let allLogs: any[] = []; + let currentTo = latestBlock; + + while (currentTo >= minFromBlock) { + const fromBlock = Math.max(currentTo - 9, minFromBlock); + const logs = await provider.getLogs({ + address: networkContract.target as string, + fromBlock, + toBlock: currentTo, + topics: [null, ownerTopic], + }); + allLogs = allLogs.concat(logs); + currentTo = fromBlock - 1; + } + + allLogs.sort((a, b) => { + if (a.blockNumber !== b.blockNumber) { + return a.blockNumber - b.blockNumber; + } + return a.transactionIndex - b.transactionIndex; + }); + + const iface = new connection.ethers.Interface(EVENT_ABI); + let latestClusterTuple: any = [0n, 0n, 0n, true, 0n]; + + for (const log of allLogs) { + let decoded; + try { + decoded = iface.parseLog(log); + } catch { + continue; + } + if (!decoded) continue; + const operatorIdsFromEvent = decoded.args[1]; + if (!Array.isArray(operatorIdsFromEvent)) continue; + const idsFromEvent = operatorIdsFromEvent + .map(b => b.toString()) + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + if (JSON.stringify(idsFromEvent) !== JSON.stringify(opsExpected)) continue; + latestClusterTuple = decoded.args[decoded.args.length - 1]; + } + + return { + validatorCount: latestClusterTuple[0].toString(), + networkFeeIndex: latestClusterTuple[1].toString(), + index: latestClusterTuple[2].toString(), + active: latestClusterTuple[3], + balance: latestClusterTuple[4].toString(), + }; +} + +export async function registerAndParseCluster( + clusters: any, + operatorIds: bigint[], + pubkeyIndex = 1, + depositValue = DEFAULT_ETH_REGISTER_VALUE, +): Promise { + const tx = await clusters.registerValidator( + makePublicKey(pubkeyIndex), operatorIds, DEFAULT_SHARES, createCluster(), + { value: depositValue }, + ); + const receipt = await tx.wait(); + return parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); +} + +export async function registerAndLiquidate( + clusters: any, + ownerAddress: string, + operatorIds: bigint[], + pubkeyIndex = 1, +): Promise<{ clusterAfterRegister: Cluster; clusterAfterLiquidation: Cluster }> { + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds, pubkeyIndex); + const liquidateTx = await clusters.liquidate(ownerAddress, operatorIds, clusterAfterRegister); + const liquidateReceipt = await liquidateTx.wait(); + const clusterAfterLiquidation = parseClusterFromEvent( + clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED, + ); + return { clusterAfterRegister, clusterAfterLiquidation }; +} + +export async function addValidatorsToCluster(connection: any, network: SSVNetwork, keys: string[], shares: string[], clusterOwner: HardhatEthersSigner, operatorIds: number[], cluster: Cluster): Promise { + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (1000n * 10n ** 18n)); + await network.connect(clusterOwner).bulkRegisterValidator(keys, operatorIds, shares, cluster, { value: DEFAULT_ETH_REGISTER_VALUE }); + return await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); +} diff --git a/test/helpers/context.ts b/test/helpers/context.ts new file mode 100644 index 000000000..00ce7867d --- /dev/null +++ b/test/helpers/context.ts @@ -0,0 +1,16 @@ +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { getTestConnection } from "../setup/connection.ts"; + +export interface TestContext { + connection: NetworkConnection<"generic">; + networkHelpers: NetworkHelpersType; + signers: HardhatEthersSigner[]; +} + +export async function setupTestContext(): Promise { + const { connection, networkHelpers } = await getTestConnection(); + const signers = await connection.ethers.getSigners(); + return { connection, networkHelpers, signers }; +} diff --git a/test/helpers/contract-helpers.ts b/test/helpers/contract-helpers.ts deleted file mode 100644 index 2a68ec5dd..000000000 --- a/test/helpers/contract-helpers.ts +++ /dev/null @@ -1,347 +0,0 @@ -import hre from 'hardhat'; -import { ethers, upgrades } from 'hardhat'; -import { Address, keccak256, toBytes } from 'viem'; -import { trackGas, GasGroup } from './gas-usage'; - -import { SSVKeys, KeyShares, EncryptShare } from 'ssv-keys'; -import { Validator, Operator, SSVConfig, Cluster } from './types'; -import validatorKeys from './json/validatorKeys.json'; -import operatorKeys from './json/operatorKeys.json'; - -const nonces = new Map(); -let lastValidatorId: number = 0; -let lastOperatorId: number = 0; -const mockedValidators = validatorKeys as Validator[]; -const mockedOperators = operatorKeys as Operator[]; -let ssvToken: any; - -export let ssvNetwork: any; -export let owners: any[]; - -export let publicClient: any; - -export const CONFIG: SSVConfig = { - initialVersion: 'v1.1.0', - operatorMaxFeeIncrease: 1000, - declareOperatorFeePeriod: 3600, // HOUR - executeOperatorFeePeriod: 86400, // DAY - minimalOperatorFee: 1000000000n, - minimalBlocksBeforeLiquidation: 100800, - minimumLiquidationCollateral: 200000000, - validatorsPerOperatorLimit: 500, - maximumOperatorFee: BigInt(76528650000000), -}; - -export const DEFAULT_OPERATOR_IDS = { - 4: [1, 2, 3, 4], - 7: [1, 2, 3, 4, 5, 6, 7], - 10: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - 13: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], -}; - -export const MOCK_SHARES = - '0x8f5f07aac10b2113fc18c178080e34665931f4bcefb743a888acdee7799fbf70c2db26c7dd9d102161aad3751b1db06c177659c3fe9af62ddae8e70dd1461267e97fe751974e09d4a8e1176e73567e29b2329566dd759ba464eab259111d3cb7b2843fe292c8b8d295c3b30b98ee77cd5d4aaf14dbfa3f63fb092b44d002a851fa9c16f4c1f1356fbff61757fff5d1c493826db56c37b7487bb50e0cd5cbb3f4698797f31f2c22373e6b601cae505233b5fa4e1dc583f0b7203372a807684990a7dfd9c775f6f3105f4d0200b63779b575bc709b4a55461f6597e506c9be2d253c8732a4acacc8292983218a516eced8a176064fad697ecd82b17adcee4d10a57f3fe9b539b467cb66fdf36e3f0b0591f1f161fcaf7c4bbb1ed000d50c8cc4e1960cc20b17c3c55c7b7a1f6b3c530f40f7b74225d38aff51cfdbfcacb78a9bdf6f31a5cf21b99c4d1055416935280b9b8f38850139a31263d625b7d4098f521225ae8941131c54fcf5a1b2589a01b96759ec92e869ad6c80ba73085a37034c291cd2716b6b1e9e8633b6037cf9312a7de72c9d04db5c52801e84b636f6c51762b63e48b454dba198fc604d326a3249370de6851a3f7ed4cbe2d3ff92de780ea7c9708df9012be9c115849c44533d574a35bce635e98e8835818052155bedd8aade6d27e480a6c497c804815b740489bd0790be851f97947fbbaf2526d84a05c9aebd0ad2161e117a2878a24e49932c7a2dff4b20725d20c1600c2103fcbadae04f5cb95ec413923e710b332be42ebc0264128b8f250063fa346be2f55108e917a9dd0c64a5159411ff4f99801f546c77891c88e4f6db6dddb8f18bd87be91a9ba0eaa076994448f97b4f3273ccc5aa51c0c6cbf6cf69f446862a0a5bbc0ca590a961871f8ef8f38f446db9cadd76675f53df8f41350d76a4015fa5b700b3caf07f9b2014778bf3400d9a43962c3ace37f1dadaa7afb546825acbf6081f2168e8496468f25fefac1b884f434f3884eb12a3e9536914a9c87b9a3dee92e83be856dc29718ae387f0066ca04ff0c8f07855a295b568358c4cf7de4964ecc9d69e0efd7102f915d0a343c85f3519e4ac0ba32b4912b2016389e3ebb9d1412b1118a428fc036b8a599614d033d89f764446b6486103bf98f782dcbdda5cfb415ff18a1fbe16d2cd448227b63bb6e2989cd54170cd4ca4400802910d77adf0bc670eba3c8c8a1571fbb73040b2f5fb391f5bbfa4f3e8b62cf8f516ce9ed6726516c19a956df1b7a20ab4fb82a0a3b0235518cc0c52fff7a59ca52a49c4b15f652466048933f6651a66abfa8eb4c8836ccc1db2a5fa7cba133923fc7ebbdda2f3db26c9e1a194dcb51543df4e06d401ec24d17bc42db2d822abad9e6a775ef6c33bfb54760839adb02cf5bd9740a59aca9a6dd20b0b90a68a940626094df638fb0a3405b1324508492a9549a316d0c8c7ab27303668fe6d61f3c75a27fc4a6008cbf948084881e9b34cdbfe2773d595d637b2ab1444219a9aad51e8f6d4a4905a5845e58cbc8ef743f30a9c17869dd5ce8adef13fe4e43fce4f380fe5a3e502e7699868ad8baa5aeb52d8c9f498a665365fac845fe6df6949a653af825e20e1e9966363ffbb0c3babe48165f72643cfded8cc451553edc1c2fd6e513533c9c51cc3ce6c12930f17fc27cec2ead93b095dc452dc3f988bb72e730e1f4c67b4852c4d20f9d8bc198d2b09962de51518d2c93f3f33a49b5d64a3ab20a4f1bbc0a972d075ab3f482c060f46d3b31c124ed8f8d89e056e3f40853f15cf92a34796c5435f2e44a1a3a941aa3afe9333b83f2a23617b715442ea13a256f7575cea9cce1c07e485'; - -const getSecretSharedPayload = async function (validator: Validator, operatorIds: number[], ownerId: number) { - const numberIds = operatorIds.map(id => Number(id)); - - const selOperators = mockedOperators.filter((item: Operator) => item.id !== undefined && numberIds.includes(item.id)); - const operators = selOperators.map((item: Operator) => ({ id: item.id, operatorKey: item.operatorKey })); - - const ssvKeys = new SSVKeys(); - const keyShares = new KeyShares(); - - const publicKey = validator.publicKey; - const privateKey = validator.privateKey; - - const threshold = await ssvKeys.createThreshold(privateKey, operators); - const encryptedShares: EncryptShare[] = await ssvKeys.encryptShares(operators, threshold.shares); - - let ownerNonce = 0; - - if (nonces.has(owners[ownerId].address)) { - ownerNonce = nonces.get(owners[ownerId].address); - } - nonces.set(owners[ownerId].address, ownerNonce + 1); - - const payload = await keyShares.buildPayload( - { - publicKey, - operators, - encryptedShares, - }, - { - ownerAddress: owners[ownerId].address, - ownerNonce, - privateKey, - }, - ); - return payload; -}; - -export const DataGenerator = { - publicKey: (id: number) => { - const validators = mockedValidators.filter((item: Validator) => item.id === id); - if (validators.length > 0) { - return validators[0].publicKey; - } - return `0x${id.toString(16).padStart(48, '0')}`; - }, - shares: async (ownerId: number, validatorId: number, operatorIds: number[]) => { - let shared: any; - const validators = mockedValidators.filter((item: Validator) => item.id === validatorId); - if (validators.length > 0) { - const validator = validators[0]; - const payload = await getSecretSharedPayload(validator, operatorIds, ownerId); - shared = payload.sharesData; - } else { - shared = `0x${validatorId.toString(16).padStart(48, '0')}`; - } - return shared; - }, -}; - -export const initializeContract = async function () { - owners = await hre.viem.getWalletClients(); - - lastValidatorId = 1; - lastOperatorId = 0; - - ssvToken = await hre.viem.deployContract('SSVToken'); - const ssvOperatorsMod = await hre.viem.deployContract('SSVOperators'); - const ssvClustersMod = await hre.viem.deployContract('SSVClusters'); - const ssvDAOMod = await hre.viem.deployContract('SSVDAO'); - const ssvViewsMod = await hre.viem.deployContract('contracts/modules/SSVViews.sol:SSVViews'); - const ssvWhitelistMod = await hre.viem.deployContract('SSVOperatorsWhitelist'); - - const ssvNetworkFactory = await ethers.getContractFactory('SSVNetwork'); - const ssvNetworkProxy = await await upgrades.deployProxy( - ssvNetworkFactory, - [ - ssvToken.address, - ssvOperatorsMod.address, - ssvClustersMod.address, - ssvDAOMod.address, - ssvViewsMod.address, - CONFIG.minimalBlocksBeforeLiquidation, - CONFIG.minimumLiquidationCollateral, - CONFIG.validatorsPerOperatorLimit, - CONFIG.declareOperatorFeePeriod, - CONFIG.executeOperatorFeePeriod, - CONFIG.operatorMaxFeeIncrease, - ], - { - kind: 'uups', - unsafeAllow: ['delegatecall'], - }, - ); - await ssvNetworkProxy.waitForDeployment(); - const ssvNetworkAddress = await ssvNetworkProxy.getAddress(); - ssvNetwork = await hre.viem.getContractAt('SSVNetwork', ssvNetworkAddress as Address); - - const ssvNetworkViewsFactory = await ethers.getContractFactory('SSVNetworkViews'); - const ssvNetworkViewsProxy = await await upgrades.deployProxy(ssvNetworkViewsFactory, [ssvNetworkAddress], { - kind: 'uups', - unsafeAllow: ['delegatecall'], - }); - await ssvNetworkViewsProxy.waitForDeployment(); - const ssvNetworkViewsAddress = await ssvNetworkViewsProxy.getAddress(); - const ssvNetworkViews = await hre.viem.getContractAt('SSVNetworkViews', ssvNetworkViewsAddress as Address); - - await ssvNetwork.write.updateMaximumOperatorFee([CONFIG.maximumOperatorFee as bigint]); - - ssvNetwork.write.updateModule([4, await ssvWhitelistMod.address]); - - for (let i = 1; i < 7; i++) { - await ssvToken.write.mint([owners[i].account.address, 10000000000000000000n]); - } - - return { - ssvContractsOwner: owners[0].account, - ssvNetwork, - ssvNetworkViews, - ssvToken, - }; -}; - -export const registerOperators = async function ( - ownerId: number, - numberOfOperators: number, - fee: BigInt, - gasGroups: GasGroup[] = [GasGroup.REGISTER_OPERATOR], -) { - const newOperatorIds = []; - const targetOperatorId = lastOperatorId + numberOfOperators; - for (let i = lastOperatorId; i < lastOperatorId + numberOfOperators && i < mockedOperators.length; i++) { - const operator = mockedOperators[i]; - operator.publicKey = keccak256(toBytes(operator.operatorKey)); - - const { eventsByName } = await trackGas( - ssvNetwork.write.registerOperator([operator.publicKey, fee, false], { - account: owners[ownerId].account, - }), - gasGroups, - ); - - const event = eventsByName.OperatorAdded[0]; - operator.id = Number(event.args.operatorId); - mockedOperators[i] = operator; - newOperatorIds.push(operator.id); - } - lastOperatorId = targetOperatorId; - return newOperatorIds; -}; - -export const coldRegisterValidator = async function () { - const ssvKeys = new SSVKeys(); - const keyShares = new KeyShares(); - - const validator = mockedValidators[0]; - const operators = mockedOperators - .slice(0, 4) - .map((item: Operator) => ({ id: item.id, operatorKey: item.operatorKey })); - - const publicKey = validator.publicKey; - const privateKey = validator.privateKey; - const threshold = await ssvKeys.createThreshold(privateKey, operators); - const encryptedShares: EncryptShare[] = await ssvKeys.encryptShares(operators, threshold.shares); - - let ownerNonce = 0; - - if (nonces.has(owners[0].address)) { - ownerNonce = nonces.get(owners[0].address); - } - nonces.set(owners[0].address, ownerNonce + 1); - - const payload = await keyShares.buildPayload( - { - publicKey, - operators, - encryptedShares, - }, - { - ownerAddress: owners[0].address, - ownerNonce, - privateKey, - }, - ); - - const amount = 1000000000000000n; - await ssvToken.write.approve([ssvNetwork.address, amount]); - await ssvNetwork.write.registerValidator([ - payload.publicKey, - payload.operatorIds, - payload.sharesData, - amount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]); - lastValidatorId = validator.id; -}; - -export const bulkRegisterValidators = async function ( - ownerId: number, - numberOfValidators: number, - operatorIds: number[], - minDepositAmount: BigInt, - cluster: Cluster, - gasGroups?: GasGroup[], -) { - const validatorIndex = lastValidatorId; - const pks = Array.from({ length: numberOfValidators }, (_, index) => DataGenerator.publicKey(index + validatorIndex)); - const shares = await Promise.all( - Array.from({ length: numberOfValidators }, (_, index) => - DataGenerator.shares(ownerId, index + validatorIndex, operatorIds), - ), - ); - const depositAmount = minDepositAmount * BigInt(numberOfValidators); - - await ssvToken.write.approve([ssvNetwork.address, depositAmount], { - account: owners[ownerId].account, - }); - - const result = await trackGas( - ssvNetwork.write.bulkRegisterValidator([pks, operatorIds, shares, depositAmount, cluster], { - account: owners[ownerId].account, - }), - gasGroups, - ); - - lastValidatorId += numberOfValidators; - - return { - args: result.eventsByName.ValidatorAdded[0].args, - pks, - }; -}; - -export const deposit = async function ( - ownerId: number, - ownerAddress: Address, - operatorIds: number[], - depositAmount: BigInt, - cluster: Cluster, -) { - await ssvToken.write.approve([ssvNetwork.address, depositAmount], { - account: owners[ownerId].account, - }); - - const depositedCluster = await trackGas( - ssvNetwork.write.deposit([ownerAddress, operatorIds, depositAmount, cluster], { - account: owners[ownerId].account, - }), - ); - return depositedCluster.eventsByName.ClusterDeposited[0].args; -}; - -export const withdraw = async function (ownerId: number, operatorIds: number[], amount: BigInt, cluster: Cluster) { - const withdrawnCluster = await trackGas( - ssvNetwork.write.withdraw([operatorIds, amount, cluster], { - account: owners[ownerId].account, - }), - ); - - return withdrawnCluster.eventsByName.ClusterWithdrawn[0].args; -}; - -export const removeValidator = async function (ownerId: number, pk: string, operatorIds: number[], cluster: Cluster) { - const removedValidator = await trackGas( - ssvNetwork.write.removeValidator([pk, operatorIds, cluster], { - account: owners[ownerId].account, - }), - ); - return removedValidator.eventsByName.ValidatorRemoved[0].args; -}; - -export const liquidate = async function (ownerAddress: Address, operatorIds: number[], cluster: Cluster) { - const liquidatedCluster = await trackGas(ssvNetwork.write.liquidate([ownerAddress, operatorIds, cluster])); - return liquidatedCluster.eventsByName.ClusterLiquidated[0].args; -}; - -export const reactivate = async function (ownerId: number, operatorIds: number[], amount: BigInt, cluster: Cluster) { - await ssvToken.write.approve([ssvNetwork.address, amount], { account: owners[ownerId].account }); - const reactivatedCluster = await trackGas( - ssvNetwork.write.reactivate([operatorIds, amount, cluster], { account: owners[ownerId].account }), - ); - return reactivatedCluster.eventsByName.ClusterReactivated[0].args; -}; - -export const getTransactionReceipt = async function (tx: Promise) { - const hash = await tx; - - const receipt = await publicClient.waitForTransactionReceipt({ - hash, - }); - - return receipt; -}; - -async function initialize() { - publicClient = await hre.viem.getPublicClient(); -} -initialize(); diff --git a/test/helpers/fee.ts b/test/helpers/fee.ts new file mode 100644 index 000000000..aec7a30cf --- /dev/null +++ b/test/helpers/fee.ts @@ -0,0 +1,100 @@ +import { BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS, DEDUCTED_DIGITS, } from "../common/constants.ts"; + +const DEFAULT_EB_PER_VALIDATOR = 32n; +const VUNITS_PRECISION = BPS_DENOMINATOR; + +export function calcOperatorFeeAccrual(blockDiff: bigint, ethFee: bigint, effectiveVUnits: bigint): bigint { + return (blockDiff * ethFee * effectiveVUnits) / BPS_DENOMINATOR; +} + +export function calcNetworkFeeAccrual(networkFeeIndexDelta: bigint, effectiveVUnits: bigint): bigint { + return ((networkFeeIndexDelta * effectiveVUnits) / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; +} + +export function calcClusterBurn(params: { + blockDiff: bigint; + numOperators: bigint; + ethFee: bigint; + networkFee: bigint; + effectiveVUnits: bigint; +}): bigint { + const { blockDiff, numOperators, ethFee, networkFee, effectiveVUnits } = params; + const operatorIndexDelta = numOperators * blockDiff * ethFee; + const networkFeeIndexDelta = blockDiff * networkFee; + const operatorFeeUnits = (operatorIndexDelta * effectiveVUnits) / VUNITS_PRECISION; + const networkFeeUnits = (networkFeeIndexDelta * effectiveVUnits) / VUNITS_PRECISION; + return (operatorFeeUnits + networkFeeUnits) * ETH_DEDUCTED_DIGITS; +} + +export function calcVUnits(effectiveBalanceETH: bigint): bigint { + return (effectiveBalanceETH * BPS_DENOMINATOR + DEFAULT_EB_PER_VALIDATOR - 1n) / DEFAULT_EB_PER_VALIDATOR; +} + +export function defaultVUnits(validatorCount: bigint): bigint { + return validatorCount * BPS_DENOMINATOR; +} + +export function calcLiquidationThreshold(params: { + minimumBlocksBeforeLiquidation: bigint; + numOperators: bigint; + ethFee: bigint; + networkFee: bigint; + effectiveVUnits: bigint; +}): bigint { + const { minimumBlocksBeforeLiquidation, numOperators, ethFee, networkFee, effectiveVUnits } = params; + const burnRate = numOperators * ethFee; + const thresholdUnits = (minimumBlocksBeforeLiquidation * (burnRate + networkFee) * effectiveVUnits) / BPS_DENOMINATOR; + return thresholdUnits * ETH_DEDUCTED_DIGITS; +} + +export function calcAccEthPerShareDelta(newFeesWei: bigint, totalCSSVSupply: bigint): bigint { + return (newFeesWei * 10n ** 18n) / totalCSSVSupply; +} + +export function calcStakingReward(cSSVBalance: bigint, accEthPerShare: bigint, userIndex: bigint): bigint { + return (cSSVBalance * (accEthPerShare - userIndex)) / 10n ** 18n; +} + +export function calcSSVClusterFees(params: { + currentBlock: bigint; + opSnapshots: { + block: bigint; + index: bigint; + }[]; + opFeeRaw: bigint; + netFeeBlock: bigint; + netFeeRaw: bigint; + storedNetFeeIndex: bigint; + validatorCount: bigint; + clusterIndex: bigint; + clusterNetworkFeeIndex: bigint; +}): bigint { + const { currentBlock, opSnapshots, opFeeRaw, netFeeBlock, netFeeRaw, storedNetFeeIndex, validatorCount, clusterIndex, clusterNetworkFeeIndex, } = params; + let cumulativeOpIndex = 0n; + for (const snap of opSnapshots) { + const blockDiff = currentBlock - snap.block; + const currentIndex = snap.index + blockDiff * opFeeRaw; + cumulativeOpIndex += currentIndex; + } + const opFeePacked = (cumulativeOpIndex - clusterIndex) * validatorCount; + const currentNetFeeIndex = storedNetFeeIndex + (currentBlock - netFeeBlock) * netFeeRaw; + const netFeePacked = (currentNetFeeIndex - clusterNetworkFeeIndex) * validatorCount; + return (opFeePacked + netFeePacked) * DEDUCTED_DIGITS; +} + +export async function setupMockProtocol(contract: any, opts: { + ethNetworkFee?: bigint; + networkFeeIndex?: bigint; + minBlocks?: bigint; + minCollateral?: bigint; +} = {}): Promise { + const { ethNetworkFee, networkFeeIndex, minBlocks, minCollateral } = opts; + if (ethNetworkFee !== undefined) + await contract.mockEthNetworkFee(ethNetworkFee); + if (networkFeeIndex !== undefined) + await contract.mockCurrentNetworkFeeIndex(networkFeeIndex); + if (minBlocks !== undefined) + await contract.mockMinimumBlocksBeforeLiquidation(minBlocks); + if (minCollateral !== undefined) + await contract.mockMinimumLiquidationCollateral(minCollateral); +} diff --git a/test/helpers/fixture-presets.ts b/test/helpers/fixture-presets.ts new file mode 100644 index 000000000..06ffa1b4d --- /dev/null +++ b/test/helpers/fixture-presets.ts @@ -0,0 +1,35 @@ +import type { NetworkConnection } from "hardhat/types/network"; +import { + ssvOperatorsHarnessFixture, + ssvClustersHarnessFixture, + ssvValidatorsHarnessFixture, + ssvDAOHarnessFixture, + ssvStakingHarnessFixture, +} from "../setup/fixtures.ts"; +import { + MAXIMUM_OPERATORS_FEE, + DECLARE_OPERATOR_FEE_PERIOD, + EXECUTE_OPERATOR_FEE_PERIOD, + OPERATOR_MAX_FEE_INCREASE, +} from "../common/constants.ts"; + +export const defaultOperatorsFixture = (connection: NetworkConnection<"generic">) => + ssvOperatorsHarnessFixture( + connection, + MAXIMUM_OPERATORS_FEE, + DECLARE_OPERATOR_FEE_PERIOD, + EXECUTE_OPERATOR_FEE_PERIOD, + OPERATOR_MAX_FEE_INCREASE, + ); + +export const defaultClustersFixture = (connection: NetworkConnection<"generic">, operatorCount = 4, operatorFee = 0n) => + ssvClustersHarnessFixture(connection, operatorCount, operatorFee); + +export const defaultValidatorsFixture = (connection: NetworkConnection<"generic">, operatorCount = 4, operatorFee = 0n) => + ssvValidatorsHarnessFixture(connection, operatorCount, operatorFee); + +export const defaultDAOFixture = (connection: NetworkConnection<"generic">) => + ssvDAOHarnessFixture(connection); + +export const defaultStakingFixture = (connection: NetworkConnection<"generic">) => + ssvStakingHarnessFixture(connection); diff --git a/test/helpers/gas-usage.ts b/test/helpers/gas-usage.ts index ddbbfcdb0..0b06ffb9b 100644 --- a/test/helpers/gas-usage.ts +++ b/test/helpers/gas-usage.ts @@ -1,6 +1,14 @@ -import { parseEventLogs } from 'viem'; import { expect } from 'chai'; -import { ssvNetwork, getTransactionReceipt } from '../helpers/contract-helpers'; +import { Interface } from 'ethers'; +import { createRequire } from 'node:module'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const require = createRequire(import.meta.url); +const ssvNetworkAbi = require('../../abis/SSVNetwork.json'); + +const GAS_REPORT_OUTPUT_DIR = process.env.GAS_REPORT_DIR || '.'; +const GAS_REPORT_JSON_FILE = 'gas-report.json'; export enum GasGroup { REGISTER_OPERATOR, @@ -22,7 +30,7 @@ export enum GasGroup { EXECUTE_OPERATOR_FEE, REDUCE_OPERATOR_FEE, - REGISTER_VALIDATOR_EXISTING_CLUSTER, + REGISTER_VALIDATOR_EXISTING_ETH_CLUSTER, REGISTER_VALIDATOR_NEW_STATE, REGISTER_VALIDATOR_WITHOUT_DEPOSIT, @@ -70,6 +78,7 @@ export enum GasGroup { DEPOSIT, WITHDRAW_CLUSTER_BALANCE, WITHDRAW_OPERATOR_BALANCE, + WITHDRAW_OPERATOR_BALANCE_ALL_VERSIONS, VALIDATOR_EXIT, BULK_EXIT_10_VALIDATOR_4, BULK_EXIT_10_VALIDATOR_7, @@ -80,6 +89,10 @@ export enum GasGroup { LIQUIDATE_CLUSTER_7, LIQUIDATE_CLUSTER_10, LIQUIDATE_CLUSTER_13, + LIQUIDATE_CLUSTER_SSV_4, + LIQUIDATE_CLUSTER_SSV_7, + LIQUIDATE_CLUSTER_SSV_10, + LIQUIDATE_CLUSTER_SSV_13, REACTIVATE_CLUSTER, NETWORK_FEE_CHANGE, @@ -91,99 +104,142 @@ export enum GasGroup { CHANGE_LIQUIDATION_THRESHOLD_PERIOD, CHANGE_MINIMUM_COLLATERAL, + + MIGRATE_CLUSTER_TO_ETH, + UPDATE_CLUSTER_BALANCE, + + SET_UNSTAKE_COOLDOWN, + SET_QUORUM, + REPLACE_ORACLE, + COMMIT_ROOT, + NETWORK_FEE_CHANGE_SSV, + WITHDRAW_NETWORK_SSV_EARNINGS, + + STAKE_SSV, + INITIAL_STAKE_SSV, + POST_INITIAL_STAKE_SSV, + REQUEST_UNSTAKE, + WITHDRAW_UNSTAKE, + CLAIM_ETH_REWARDS, + SYNC_FEES, + RESCUE_ERC20, } const MAX_GAS_PER_GROUP: any = { - /* REAL GAS LIMITS */ - [GasGroup.REGISTER_OPERATOR]: 137000, - [GasGroup.REMOVE_OPERATOR]: 70500, - [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]: 70500, - [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT]: 70000, - [GasGroup.UPDATE_OPERATOR_WHITELISTING_CONTRACT]: 70000, - [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10]: 375000, - [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT]: 43000, - [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10]: 130000, - [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10]: 381000, - [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10]: 168000, - [GasGroup.SET_OPERATORS_PRIVATE_10]: 313000, - [GasGroup.SET_OPERATORS_PUBLIC_10]: 114000, - - [GasGroup.DECLARE_OPERATOR_FEE]: 70000, - [GasGroup.CANCEL_OPERATOR_FEE]: 41900, - [GasGroup.EXECUTE_OPERATOR_FEE]: 52000, - [GasGroup.REDUCE_OPERATOR_FEE]: 51900, - - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER]: 202000, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE]: 236000, - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT]: 180600, - - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]: 221000, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4]: 221500, - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4]: 204500, - - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4]: 231000, - - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_4]: 835500, - [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4]: 818700, - [GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4]: 830000, - - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_7]: 272500, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7]: 289000, - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_7]: 251600, - - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_7]: 1143000, - [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_7]: 1126500, - - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_10]: 342700, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10]: 359500, - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_10]: 322200, - - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_10]: 1447000, - [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_10]: 1430500, - - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_13]: 413700, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13]: 430500, - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_13]: 393300, - - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_13]: 1757000, - [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_13]: 1740000, - - [GasGroup.REMOVE_VALIDATOR]: 114000, - [GasGroup.BULK_REMOVE_10_VALIDATOR_4]: 191500, + [GasGroup.REGISTER_OPERATOR]: 200000, + [GasGroup.REMOVE_OPERATOR]: 85000, + [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]: 84000, + [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT]: 135000, + [GasGroup.UPDATE_OPERATOR_WHITELISTING_CONTRACT]: 90000, + [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10]: 354000, + [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT]: 60000, + [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10]: 133000, + [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10]: 166000, + [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10]: 135000, + [GasGroup.SET_OPERATORS_PRIVATE_10]: 56000, + [GasGroup.SET_OPERATORS_PUBLIC_10]: 33000, + + [GasGroup.DECLARE_OPERATOR_FEE]: 76000, + [GasGroup.CANCEL_OPERATOR_FEE]: 42000, + [GasGroup.EXECUTE_OPERATOR_FEE]: 61000, + [GasGroup.REDUCE_OPERATOR_FEE]: 62000, + + [GasGroup.REGISTER_VALIDATOR_EXISTING_ETH_CLUSTER]: 209500, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE]: 225000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT]: 209500, + + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]: 225000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4]: 225000, + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4]: 209500, + + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4]: 234000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTING_CONTRACT_4]: 251500, + + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_4]: 626000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4]: 489000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4]: 515000, + + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_7]: 273000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7]: 460000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_7]: 273000, + + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_7]: 767000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_7]: 580000, + + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_10]: 352000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10]: 590000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_10]: 352000, + + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_10]: 908000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_10]: 670000, + + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_13]: 431000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13]: 760000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_13]: 431000, + + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_13]: 1049000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_13]: 760000, + + [GasGroup.REMOVE_VALIDATOR]: 140000, + [GasGroup.BULK_REMOVE_10_VALIDATOR_4]: 203000, [GasGroup.REMOVE_VALIDATOR_7]: 155500, - [GasGroup.BULK_REMOVE_10_VALIDATOR_7]: 241700, + [GasGroup.BULK_REMOVE_10_VALIDATOR_7]: 254500, [GasGroup.REMOVE_VALIDATOR_10]: 197000, - [GasGroup.BULK_REMOVE_10_VALIDATOR_10]: 292500, + [GasGroup.BULK_REMOVE_10_VALIDATOR_10]: 306000, - [GasGroup.REMOVE_VALIDATOR_13]: 238500, - [GasGroup.BULK_REMOVE_10_VALIDATOR_13]: 343000, + [GasGroup.REMOVE_VALIDATOR_13]: 241000, + [GasGroup.BULK_REMOVE_10_VALIDATOR_13]: 357500, - [GasGroup.DEPOSIT]: 77500, - [GasGroup.WITHDRAW_CLUSTER_BALANCE]: 95000, - [GasGroup.WITHDRAW_OPERATOR_BALANCE]: 64900, - [GasGroup.VALIDATOR_EXIT]: 43000, + [GasGroup.DEPOSIT]: 400000, + [GasGroup.WITHDRAW_CLUSTER_BALANCE]: 120000, + [GasGroup.WITHDRAW_OPERATOR_BALANCE]: 120000, + [GasGroup.WITHDRAW_OPERATOR_BALANCE_ALL_VERSIONS]: 140000, + [GasGroup.VALIDATOR_EXIT]: 80000, [GasGroup.BULK_EXIT_10_VALIDATOR_4]: 126200, [GasGroup.BULK_EXIT_10_VALIDATOR_7]: 139500, [GasGroup.BULK_EXIT_10_VALIDATOR_10]: 152500, [GasGroup.BULK_EXIT_10_VALIDATOR_13]: 165500, - [GasGroup.LIQUIDATE_CLUSTER_4]: 130500, - [GasGroup.LIQUIDATE_CLUSTER_7]: 171000, - [GasGroup.LIQUIDATE_CLUSTER_10]: 212000, - [GasGroup.LIQUIDATE_CLUSTER_13]: 253000, - [GasGroup.REACTIVATE_CLUSTER]: 121500, - - [GasGroup.NETWORK_FEE_CHANGE]: 45800, + [GasGroup.LIQUIDATE_CLUSTER_4]: 155000, + [GasGroup.LIQUIDATE_CLUSTER_7]: 173000, + [GasGroup.LIQUIDATE_CLUSTER_10]: 218000, + [GasGroup.LIQUIDATE_CLUSTER_13]: 265000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_4]: 175000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_7]: 220000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_10]: 270000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_13]: 320000, + [GasGroup.REACTIVATE_CLUSTER]: 310000, + + [GasGroup.NETWORK_FEE_CHANGE]: 72000, [GasGroup.WITHDRAW_NETWORK_EARNINGS]: 62500, - [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]: 38200, - [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]: 40900, - [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]: 41000, - [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]: 40300, - - [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]: 41000, - [GasGroup.CHANGE_MINIMUM_COLLATERAL]: 41200, + [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]: 50000, + [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]: 50000, + [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]: 50000, + [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]: 50000, + + [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]: 50000, + [GasGroup.CHANGE_MINIMUM_COLLATERAL]: 50000, + + [GasGroup.MIGRATE_CLUSTER_TO_ETH]: 600000, + [GasGroup.UPDATE_CLUSTER_BALANCE]: 300000, + + [GasGroup.SET_UNSTAKE_COOLDOWN]: 80000, + [GasGroup.SET_QUORUM]: 80000, + [GasGroup.REPLACE_ORACLE]: 120000, + [GasGroup.COMMIT_ROOT]: 150000, + [GasGroup.NETWORK_FEE_CHANGE_SSV]: 52000, + [GasGroup.WITHDRAW_NETWORK_SSV_EARNINGS]: 95000, + + [GasGroup.STAKE_SSV]: 400000, + [GasGroup.INITIAL_STAKE_SSV]: 400000, + [GasGroup.POST_INITIAL_STAKE_SSV]: 140000, + [GasGroup.REQUEST_UNSTAKE]: 300000, + [GasGroup.WITHDRAW_UNSTAKE]: 250000, + [GasGroup.CLAIM_ETH_REWARDS]: 200000, + [GasGroup.SYNC_FEES]: 180000, + [GasGroup.RESCUE_ERC20]: 120000, }; class GasStats { @@ -211,15 +267,22 @@ for (const group in MAX_GAS_PER_GROUP) { } export const trackGas = async function (tx: Promise, groups?: Array): Promise { - const receipt = await getTransactionReceipt(tx); + const response = await tx; + const receipt = await response.wait(); return await trackGasFromReceipt(receipt, groups); }; export const trackGasFromReceipt = async function (receipt: any, groups?: Array): Promise { - const logs = parseEventLogs({ - abi: ssvNetwork.abi, - logs: receipt.logs, - }); + const iface = new Interface(ssvNetworkAbi); + const logs = (receipt.logs ?? []) + .map((log: any) => { + try { + return iface.parseLog(log); + } catch { + return null; + } + }) + .filter(Boolean); groups && [...new Set(groups)].forEach(group => { @@ -237,8 +300,9 @@ export const trackGasFromReceipt = async function (receipt: any, groups?: Array< ...receipt, gasUsed: receipt.gasUsed, eventsByName: logs.reduce((aggr: any, item: any) => { - aggr[item.eventName] = aggr[item.eventName] || []; - aggr[item.eventName].push(item); + const eventName = item.name; + aggr[eventName] = aggr[eventName] || []; + aggr[eventName].push(item); return aggr; }, {}), }; @@ -247,3 +311,173 @@ export const trackGasFromReceipt = async function (receipt: any, groups?: Array< export const getGasStats = (group: string) => { return gasUsageStats.get(group) || new GasStats(); }; + +export const getGasGroupName = (group: GasGroup | string): string => { + const groupNum = typeof group === 'string' ? parseInt(group, 10) : group; + return GasGroup[groupNum] || `UNKNOWN_${group}`; +}; + +export const getAllMaxGasLimits = (): Record => { + const result: Record = {}; + for (const group in MAX_GAS_PER_GROUP) { + const name = getGasGroupName(group); + result[name] = MAX_GAS_PER_GROUP[group]; + } + return result; +}; + +export interface GasReportEntry { + name: string; + maxLimit: number; + min: number | null; + max: number | null; + average: number | null; + txCount: number; + withinLimit: boolean; +} + +export interface GasReport { + timestamp: string; + commit?: string; + branch?: string; + entries: GasReportEntry[]; + summary: { + totalOperations: number; + operationsWithData: number; + allWithinLimits: boolean; + }; +} + +export const generateGasReport = (): GasReport => { + const entries: GasReportEntry[] = []; + let operationsWithData = 0; + let allWithinLimits = true; + + for (const group in MAX_GAS_PER_GROUP) { + const groupNum = parseInt(group, 10); + const name = getGasGroupName(groupNum); + const maxLimit = MAX_GAS_PER_GROUP[groupNum] || 0; + const gasStats = getGasStats(group); + + const withinLimit = gasStats.max === null || gasStats.max <= maxLimit; + if (!withinLimit) allWithinLimits = false; + if (gasStats.txCount > 0) operationsWithData++; + + entries.push({ + name, + maxLimit, + min: gasStats.min, + max: gasStats.max, + average: gasStats.txCount > 0 ? Math.round(gasStats.average) : null, + txCount: gasStats.txCount, + withinLimit, + }); + } + + entries.sort((a, b) => a.name.localeCompare(b.name)); + + return { + timestamp: new Date().toISOString(), + entries, + summary: { + totalOperations: entries.length, + operationsWithData, + allWithinLimits, + }, + }; +}; + +export const printGasReport = (report?: GasReport): void => { + const gasReport = report || generateGasReport(); + + console.log('\n'); + console.log('='.repeat(100)); + console.log(' GAS USAGE REPORT'); + console.log('='.repeat(100)); + console.log(`Generated: ${gasReport.timestamp}`); + console.log('-'.repeat(100)); + + console.log( + padRight('Operation', 55) + + padLeft('Max Limit', 12) + + padLeft('Avg Gas', 12) + + padLeft('Min', 10) + + padLeft('Max', 10) + ); + console.log('-'.repeat(100)); + + const entriesWithData = gasReport.entries.filter(e => e.txCount > 0); + + for (const entry of entriesWithData) { + console.log( + padRight(entry.name, 55) + + padLeft(entry.maxLimit.toLocaleString(), 12) + + padLeft(entry.average?.toLocaleString() || '-', 12) + + padLeft(entry.min?.toLocaleString() || '-', 10) + + padLeft(entry.max?.toLocaleString() || '-', 10) + ); + } + + console.log('-'.repeat(100)); + console.log(`Total operations tracked: ${gasReport.summary.operationsWithData}`); + console.log(`All within limits: ${gasReport.summary.allWithinLimits ? 'YES' : 'NO'}`); + console.log('='.repeat(100)); + console.log('\n'); +}; + +export const saveGasReport = (outputPath?: string): GasReport => { + const report = generateGasReport(); + + try { + const { execSync } = require('child_process'); + report.commit = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim(); + report.branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); + } catch { + } + + const filePath = outputPath || path.join(GAS_REPORT_OUTPUT_DIR, GAS_REPORT_JSON_FILE); + + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(filePath, JSON.stringify(report, null, 2)); + console.log(`Gas report saved to: ${filePath}`); + + return report; +}; + +export const resetGasStats = (): void => { + for (const group in MAX_GAS_PER_GROUP) { + gasUsageStats.set(group, new GasStats()); + } +}; + +function padRight(str: string, len: number): string { + return str.length >= len ? str.substring(0, len) : str + ' '.repeat(len - str.length); +} + +function padLeft(str: string, len: number): string { + return str.length >= len ? str : ' '.repeat(len - str.length) + str; +} + +let reportRegistered = false; + +export const registerGasReportOnExit = (): void => { + if (reportRegistered) return; + if (process.env.REPORT_GAS !== 'true') return; + + reportRegistered = true; + + process.on('beforeExit', () => { + const report = generateGasReport(); + + if (report.summary.operationsWithData > 0) { + printGasReport(report); + saveGasReport(); + } + }); +}; + +registerGasReportOnExit(); diff --git a/test/helpers/gas.ts b/test/helpers/gas.ts new file mode 100644 index 000000000..ecf5368e2 --- /dev/null +++ b/test/helpers/gas.ts @@ -0,0 +1,408 @@ +import { expect } from 'chai'; +import { Interface } from 'ethers'; +import { createRequire } from 'node:module'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const require = createRequire(import.meta.url); +const ssvNetworkAbi = require('../../abis/SSVNetwork.json'); +const GAS_REPORT_OUTPUT_DIR = process.env.GAS_REPORT_DIR || '.'; +const GAS_REPORT_JSON_FILE = 'gas-report.json'; + +export enum GasGroup { + REGISTER_OPERATOR, + REMOVE_OPERATOR, + REMOVE_OPERATOR_WITH_WITHDRAW, + SET_OPERATOR_WHITELISTING_CONTRACT, + UPDATE_OPERATOR_WHITELISTING_CONTRACT, + SET_OPERATOR_WHITELISTING_CONTRACT_10, + REMOVE_OPERATOR_WHITELISTING_CONTRACT, + REMOVE_OPERATOR_WHITELISTING_CONTRACT_10, + SET_MULTIPLE_OPERATOR_WHITELIST_10_10, + REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10, + SET_OPERATORS_PRIVATE_10, + SET_OPERATORS_PUBLIC_10, + DECLARE_OPERATOR_FEE, + CANCEL_OPERATOR_FEE, + EXECUTE_OPERATOR_FEE, + REDUCE_OPERATOR_FEE, + REGISTER_VALIDATOR_EXISTING_ETH_CLUSTER, + REGISTER_VALIDATOR_NEW_STATE, + REGISTER_VALIDATOR_WITHOUT_DEPOSIT, + REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4, + REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4, + REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4, + REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4, + REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTING_CONTRACT_4, + BULK_REGISTER_10_VALIDATOR_NEW_STATE_4, + BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4, + BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4, + REGISTER_VALIDATOR_EXISTING_CLUSTER_7, + REGISTER_VALIDATOR_NEW_STATE_7, + REGISTER_VALIDATOR_WITHOUT_DEPOSIT_7, + BULK_REGISTER_10_VALIDATOR_NEW_STATE_7, + BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_7, + REGISTER_VALIDATOR_EXISTING_CLUSTER_10, + REGISTER_VALIDATOR_NEW_STATE_10, + REGISTER_VALIDATOR_WITHOUT_DEPOSIT_10, + BULK_REGISTER_10_VALIDATOR_NEW_STATE_10, + BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_10, + REGISTER_VALIDATOR_EXISTING_CLUSTER_13, + REGISTER_VALIDATOR_NEW_STATE_13, + REGISTER_VALIDATOR_WITHOUT_DEPOSIT_13, + BULK_REGISTER_10_VALIDATOR_NEW_STATE_13, + BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_13, + REMOVE_VALIDATOR, + BULK_REMOVE_10_VALIDATOR_4, + REMOVE_VALIDATOR_7, + BULK_REMOVE_10_VALIDATOR_7, + REMOVE_VALIDATOR_10, + BULK_REMOVE_10_VALIDATOR_10, + REMOVE_VALIDATOR_13, + BULK_REMOVE_10_VALIDATOR_13, + DEPOSIT, + WITHDRAW_CLUSTER_BALANCE, + WITHDRAW_OPERATOR_BALANCE, + WITHDRAW_OPERATOR_BALANCE_ALL_VERSIONS, + VALIDATOR_EXIT, + BULK_EXIT_10_VALIDATOR_4, + BULK_EXIT_10_VALIDATOR_7, + BULK_EXIT_10_VALIDATOR_10, + BULK_EXIT_10_VALIDATOR_13, + LIQUIDATE_CLUSTER_4, + LIQUIDATE_CLUSTER_7, + LIQUIDATE_CLUSTER_10, + LIQUIDATE_CLUSTER_13, + LIQUIDATE_CLUSTER_SSV_4, + LIQUIDATE_CLUSTER_SSV_7, + LIQUIDATE_CLUSTER_SSV_10, + LIQUIDATE_CLUSTER_SSV_13, + REACTIVATE_CLUSTER, + NETWORK_FEE_CHANGE, + WITHDRAW_NETWORK_EARNINGS, + DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT, + DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD, + DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD, + DAO_UPDATE_OPERATOR_MAX_FEE, + CHANGE_LIQUIDATION_THRESHOLD_PERIOD, + CHANGE_MINIMUM_COLLATERAL, + MIGRATE_CLUSTER_TO_ETH, + UPDATE_CLUSTER_BALANCE, + SET_UNSTAKE_COOLDOWN, + SET_QUORUM, + REPLACE_ORACLE, + COMMIT_ROOT, + NETWORK_FEE_CHANGE_SSV, + WITHDRAW_NETWORK_SSV_EARNINGS, + STAKE_SSV, + INITIAL_STAKE_SSV, + POST_INITIAL_STAKE_SSV, + REQUEST_UNSTAKE, + WITHDRAW_UNSTAKE, + CLAIM_ETH_REWARDS, + SYNC_FEES, + RESCUE_ERC20 +} + +const MAX_GAS_PER_GROUP: any = { + [GasGroup.REGISTER_OPERATOR]: 200000, + [GasGroup.REMOVE_OPERATOR]: 85000, + [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]: 84000, + [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT]: 135000, + [GasGroup.UPDATE_OPERATOR_WHITELISTING_CONTRACT]: 90000, + [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10]: 354000, + [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT]: 60000, + [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10]: 133000, + [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10]: 166000, + [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10]: 135000, + [GasGroup.SET_OPERATORS_PRIVATE_10]: 56000, + [GasGroup.SET_OPERATORS_PUBLIC_10]: 33000, + [GasGroup.DECLARE_OPERATOR_FEE]: 76000, + [GasGroup.CANCEL_OPERATOR_FEE]: 42000, + [GasGroup.EXECUTE_OPERATOR_FEE]: 61000, + [GasGroup.REDUCE_OPERATOR_FEE]: 62000, + [GasGroup.REGISTER_VALIDATOR_EXISTING_ETH_CLUSTER]: 209500, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE]: 225000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT]: 209500, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]: 225000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4]: 225000, + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4]: 209500, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4]: 234000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTING_CONTRACT_4]: 251500, + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_4]: 626000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4]: 489000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4]: 515000, + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_7]: 273000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7]: 460000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_7]: 273000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_7]: 767000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_7]: 580000, + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_10]: 352000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10]: 590000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_10]: 352000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_10]: 908000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_10]: 670000, + [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_13]: 431000, + [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13]: 760000, + [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_13]: 431000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_13]: 1049000, + [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_13]: 760000, + [GasGroup.REMOVE_VALIDATOR]: 140000, + [GasGroup.BULK_REMOVE_10_VALIDATOR_4]: 203000, + [GasGroup.REMOVE_VALIDATOR_7]: 155500, + [GasGroup.BULK_REMOVE_10_VALIDATOR_7]: 254500, + [GasGroup.REMOVE_VALIDATOR_10]: 197000, + [GasGroup.BULK_REMOVE_10_VALIDATOR_10]: 306000, + [GasGroup.REMOVE_VALIDATOR_13]: 241000, + [GasGroup.BULK_REMOVE_10_VALIDATOR_13]: 357500, + [GasGroup.DEPOSIT]: 400000, + [GasGroup.WITHDRAW_CLUSTER_BALANCE]: 120000, + [GasGroup.WITHDRAW_OPERATOR_BALANCE]: 120000, + [GasGroup.WITHDRAW_OPERATOR_BALANCE_ALL_VERSIONS]: 140000, + [GasGroup.VALIDATOR_EXIT]: 80000, + [GasGroup.BULK_EXIT_10_VALIDATOR_4]: 126200, + [GasGroup.BULK_EXIT_10_VALIDATOR_7]: 139500, + [GasGroup.BULK_EXIT_10_VALIDATOR_10]: 152500, + [GasGroup.BULK_EXIT_10_VALIDATOR_13]: 165500, + [GasGroup.LIQUIDATE_CLUSTER_4]: 155000, + [GasGroup.LIQUIDATE_CLUSTER_7]: 173000, + [GasGroup.LIQUIDATE_CLUSTER_10]: 218000, + [GasGroup.LIQUIDATE_CLUSTER_13]: 265000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_4]: 175000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_7]: 220000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_10]: 270000, + [GasGroup.LIQUIDATE_CLUSTER_SSV_13]: 320000, + [GasGroup.REACTIVATE_CLUSTER]: 310000, + [GasGroup.NETWORK_FEE_CHANGE]: 72000, + [GasGroup.WITHDRAW_NETWORK_EARNINGS]: 62500, + [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]: 50000, + [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]: 50000, + [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]: 50000, + [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]: 50000, + [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]: 50000, + [GasGroup.CHANGE_MINIMUM_COLLATERAL]: 50000, + [GasGroup.MIGRATE_CLUSTER_TO_ETH]: 600000, + [GasGroup.UPDATE_CLUSTER_BALANCE]: 300000, + [GasGroup.SET_UNSTAKE_COOLDOWN]: 80000, + [GasGroup.SET_QUORUM]: 80000, + [GasGroup.REPLACE_ORACLE]: 120000, + [GasGroup.COMMIT_ROOT]: 150000, + [GasGroup.NETWORK_FEE_CHANGE_SSV]: 52000, + [GasGroup.WITHDRAW_NETWORK_SSV_EARNINGS]: 95000, + [GasGroup.STAKE_SSV]: 400000, + [GasGroup.INITIAL_STAKE_SSV]: 400000, + [GasGroup.POST_INITIAL_STAKE_SSV]: 140000, + [GasGroup.REQUEST_UNSTAKE]: 300000, + [GasGroup.WITHDRAW_UNSTAKE]: 250000, + [GasGroup.CLAIM_ETH_REWARDS]: 200000, + [GasGroup.SYNC_FEES]: 180000, + [GasGroup.RESCUE_ERC20]: 120000, +}; + +class GasStats { + max: number | null = null; + min: number | null = null; + totalGas = 0; + txCount = 0; + + addStat(gas: number) { + this.totalGas += gas; + ++this.txCount; + this.max = Math.max(gas, this.max === null ? -Infinity : this.max); + this.min = Math.min(gas, this.min === null ? Infinity : this.min); + } + + get average(): number { + return this.totalGas / this.txCount; + } +} + +const gasUsageStats = new Map(); +for (const group in MAX_GAS_PER_GROUP) { + gasUsageStats.set(group, new GasStats()); +} + +export const trackGas = async function(tx: Promise, groups?: Array): Promise { + const response = await tx; + const receipt = await response.wait(); + return await trackGasFromReceipt(receipt, groups); +}; +export const trackGasFromReceipt = async function(receipt: any, groups?: Array): Promise { + const iface = new Interface(ssvNetworkAbi); + const logs = (receipt.logs ?? []) + .map((log: any) => { + try { + return iface.parseLog(log); + } catch { + return null; + } + }) + .filter(Boolean); + groups && + [...new Set(groups)].forEach(group => { + const gasUsed = Number(receipt.gasUsed); + if (!process.env.NO_GAS_ENFORCE) { + const maxGas = MAX_GAS_PER_GROUP[group]; + expect(gasUsed).to.be.lessThanOrEqual(maxGas, 'gasUsed higher than max allowed gas'); + } + gasUsageStats.get(group.toString()).addStat(gasUsed); + }); + return { + ...receipt, + gasUsed: receipt.gasUsed, + eventsByName: logs.reduce((aggr: any, item: any) => { + const eventName = item.name; + aggr[eventName] = aggr[eventName] || []; + aggr[eventName].push(item); + return aggr; + }, {}), + }; +}; +export const getGasStats = (group: string) => { + return gasUsageStats.get(group) || new GasStats(); +}; +export const getGasGroupName = (group: GasGroup | string): string => { + const groupNum = typeof group === 'string' ? parseInt(group, 10) : group; + return GasGroup[groupNum] || `UNKNOWN_${group}`; +}; +export const getAllMaxGasLimits = (): Record => { + const result: Record = {}; + for (const group in MAX_GAS_PER_GROUP) { + const name = getGasGroupName(group); + result[name] = MAX_GAS_PER_GROUP[group]; + } + return result; +}; + +export interface GasReportEntry { + name: string; + maxLimit: number; + min: number | null; + max: number | null; + average: number | null; + txCount: number; + withinLimit: boolean; +} + +export interface GasReport { + timestamp: string; + commit?: string; + branch?: string; + entries: GasReportEntry[]; + summary: { + totalOperations: number; + operationsWithData: number; + allWithinLimits: boolean; + }; +} + +export const generateGasReport = (): GasReport => { + const entries: GasReportEntry[] = []; + let operationsWithData = 0; + let allWithinLimits = true; + for (const group in MAX_GAS_PER_GROUP) { + const groupNum = parseInt(group, 10); + const name = getGasGroupName(groupNum); + const maxLimit = MAX_GAS_PER_GROUP[groupNum] || 0; + const gasStats = getGasStats(group); + const withinLimit = gasStats.max === null || gasStats.max <= maxLimit; + if (!withinLimit) + allWithinLimits = false; + if (gasStats.txCount > 0) + operationsWithData++; + entries.push({ + name, + maxLimit, + min: gasStats.min, + max: gasStats.max, + average: gasStats.txCount > 0 ? Math.round(gasStats.average) : null, + txCount: gasStats.txCount, + withinLimit, + }); + } + entries.sort((a, b) => a.name.localeCompare(b.name)); + return { + timestamp: new Date().toISOString(), + entries, + summary: { + totalOperations: entries.length, + operationsWithData, + allWithinLimits, + }, + }; +}; +export const printGasReport = (report?: GasReport): void => { + const gasReport = report || generateGasReport(); + console.log('\n'); + console.log('='.repeat(100)); + console.log(' GAS USAGE REPORT'); + console.log('='.repeat(100)); + console.log(`Generated: ${gasReport.timestamp}`); + console.log('-'.repeat(100)); + console.log(padRight('Operation', 55) + + padLeft('Max Limit', 12) + + padLeft('Avg Gas', 12) + + padLeft('Min', 10) + + padLeft('Max', 10)); + console.log('-'.repeat(100)); + const entriesWithData = gasReport.entries.filter(e => e.txCount > 0); + for (const entry of entriesWithData) { + console.log(padRight(entry.name, 55) + + padLeft(entry.maxLimit.toLocaleString(), 12) + + padLeft(entry.average?.toLocaleString() || '-', 12) + + padLeft(entry.min?.toLocaleString() || '-', 10) + + padLeft(entry.max?.toLocaleString() || '-', 10)); + } + console.log('-'.repeat(100)); + console.log(`Total operations tracked: ${gasReport.summary.operationsWithData}`); + console.log(`All within limits: ${gasReport.summary.allWithinLimits ? 'YES' : 'NO'}`); + console.log('='.repeat(100)); + console.log('\n'); +}; +export const saveGasReport = (outputPath?: string): GasReport => { + const report = generateGasReport(); + try { + const { execSync } = require('child_process'); + report.commit = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim(); + report.branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); + } catch { + } + const filePath = outputPath || path.join(GAS_REPORT_OUTPUT_DIR, GAS_REPORT_JSON_FILE); + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(filePath, JSON.stringify(report, null, 2)); + console.log(`Gas report saved to: ${filePath}`); + return report; +}; +export const resetGasStats = (): void => { + for (const group in MAX_GAS_PER_GROUP) { + gasUsageStats.set(group, new GasStats()); + } +}; + +function padRight(str: string, len: number): string { + return str.length >= len ? str.substring(0, len) : str + ' '.repeat(len - str.length); +} + +function padLeft(str: string, len: number): string { + return str.length >= len ? str : ' '.repeat(len - str.length) + str; +} + +let reportRegistered = false; +export const registerGasReportOnExit = (): void => { + if (reportRegistered) + return; + if (process.env.REPORT_GAS !== 'true') + return; + reportRegistered = true; + process.on('beforeExit', () => { + const report = generateGasReport(); + if (report.summary.operationsWithData > 0) { + printGasReport(report); + saveGasReport(); + } + }); +}; +registerGasReportOnExit(); diff --git a/test/helpers/index.ts b/test/helpers/index.ts new file mode 100644 index 000000000..b2f68e76f --- /dev/null +++ b/test/helpers/index.ts @@ -0,0 +1,18 @@ +export { makePublicKey, makePublicKeys, makeOperatorKey, makeArrayOfKeysAndShares, } from "./keys.ts"; +export { getHarnessName, createCluster, createLegacySSVCluster, clusterToTuple, extractEventArgs, parseClusterFromEvent, getCurrentClusterState, addValidatorsToCluster, registerAndParseCluster, registerAndLiquidate, } from "./cluster.ts"; +export { registerOperators, registerOperatorsSSV, whitelistAddresses, getOperatorFeeBounds, getValidOperatorFeeIncrease, getFeeAboveIncreaseLimit, calculateInitialBurnRate, registerDefaultCluster, registerDefaultClusters, seedOperatorWithETHBalance, } from "./operator.ts"; +export { computeClusterId, computeEBRoot, setupOracles, commitEBRoot, generateMerkleForClusterEB, buildEBMerkleForDefaultClusters, updateClusterBalancesForDefaultClusters, mockEBAndUpdate, } from "./oracle.ts"; +export { calcOperatorFeeAccrual, calcNetworkFeeAccrual, calcClusterBurn, calcVUnits, defaultVUnits, calcLiquidationThreshold, calcAccEthPerShareDelta, calcStakingReward, calcSSVClusterFees, setupMockProtocol, } from "./fee.ts"; +export type { BalanceSnapshot } from "./balance.ts"; +export type { ETHDeltaCheck, ETHDeltaResult } from "./balance.ts"; +export { snapshotBalance, assertBalanceDelta, snapshotContractBalance, expectETHDelta, expectContractETHDelta, expectETHDeltas, } from "./balance.ts"; +export { mineBlocks, getBlockNumber, mineToBlock, getTxBlock, setAccountBalance, } from "./blocks.ts"; +export type { TrackedCluster } from "./invariants.ts"; +export { checkETHConservation, checkValidatorCountConsistency, checkCSSVSupplyConsistency, checkAccumulatorMonotonicity, checkOracleBlockMonotonicity, assertOperatorVUnits, } from "./invariants.ts"; +export { approveAndStake } from "./staking.ts"; +export { setupLegacyClusterAndUpgrade } from "./migration.ts"; +export { defaultOperatorsFixture, defaultClustersFixture, defaultValidatorsFixture, defaultDAOFixture, defaultStakingFixture } from "./fixture-presets.ts"; +export type { TestContext } from "./context.ts"; +export { setupTestContext } from "./context.ts"; +export type { GasReportEntry, GasReport } from "./gas.ts"; +export { GasGroup, trackGas, trackGasFromReceipt, getGasStats, getGasGroupName, getAllMaxGasLimits, generateGasReport, printGasReport, saveGasReport, resetGasStats, registerGasReportOnExit, } from "./gas.ts"; diff --git a/test/helpers/invariants.ts b/test/helpers/invariants.ts new file mode 100644 index 000000000..c78c28d88 --- /dev/null +++ b/test/helpers/invariants.ts @@ -0,0 +1,49 @@ +import { expect } from "chai"; + +export interface TrackedCluster { + owner: string; + operatorIds: bigint[]; + validatorCount: bigint; + active: boolean; +} + +export async function checkETHConservation(contractAddress: string, provider: any, clusterBalances: bigint[], operatorEarnings: bigint[], networkTotalEarnings: bigint): Promise { + const contractBalance = await provider.getBalance(contractAddress); + const totalClusters = clusterBalances.reduce((sum, b) => sum + b, 0n); + const totalOperators = operatorEarnings.reduce((sum, b) => sum + b, 0n); + const totalAccounted = totalClusters + totalOperators + networkTotalEarnings; + expect(contractBalance).to.be.greaterThanOrEqual(totalAccounted); +} + +export async function checkValidatorCountConsistency(views: any, trackedClusters: TrackedCluster[]): Promise { + let expectedValidatorCount = 0n; + for (const cluster of trackedClusters) { + if (cluster.active) { + expectedValidatorCount += cluster.validatorCount; + } + } + const daoValidatorCount = await views.getNetworkValidatorsCount(); + expect(BigInt(daoValidatorCount)).to.equal(expectedValidatorCount, "ethDaoValidatorCount must equal sum of active cluster validator counts"); +} + +export async function checkCSSVSupplyConsistency(cssvToken: any, expectedTotalStaked: bigint): Promise { + const totalSupply = await cssvToken.totalSupply(); + expect(BigInt(totalSupply)).to.equal(expectedTotalStaked); +} + +export function checkAccumulatorMonotonicity(previous: bigint, current: bigint): void { + expect(current).to.be.greaterThanOrEqual(previous); +} + +export function checkOracleBlockMonotonicity(previous: bigint, current: bigint): void { + expect(current).to.be.greaterThan(previous); +} + +export async function assertOperatorVUnits(contract: any, operatorIds: bigint[], deviation: bigint, effective?: bigint): Promise { + for (const operatorId of operatorIds) { + expect(await contract.getOperatorEthVUnits(operatorId)).to.equal(deviation); + if (effective !== undefined) { + expect(await contract.getEffectiveOperatorVUnits(operatorId)).to.equal(effective); + } + } +} diff --git a/test/helpers/json/operatorKeys.json b/test/helpers/json/operatorKeys.json deleted file mode 100644 index 5afe266f8..000000000 --- a/test/helpers/json/operatorKeys.json +++ /dev/null @@ -1,9344 +0,0 @@ -[ - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFQ3ZG9FdWFTUDBGcHRxTEZEMWQKV3o3SFYzU1JMVlVKejJQYWhkRWpiSm02WHk5eEFVOTJwV0owOEo3MHNYN2gySngrL08wQVVkMmlSTms3OFdRWQpqRjRKbFgxc3RoK3oxVzBMcWQ5MXIxYXkxcjk1RXFuR1BLK0x4NkptOWFSUGFFMDZqVUE5QTNSY1JEY0srby8xCmVMNzNsMmFlY0JxeXZnaW12L2VhOGg0aHZSb0RzWko0bVgvY1h0VzA0RTlxM2lYUGZPN1FqdERaME9GcVdrMDgKNVFlQmk3TmtpYWZ4QnN4ZVJyclVmU2lFNFI2WGV3T21mMzZvQloyRnZGbWdFNzBQaUdMZ28zOEtJRWNDbGRMWApYb2M3VC84R0t4UEVUVUFyampHZzBMdmpBNnpjMFY3cFBGam1jQW5PUU10Z3AyY0F4NE05YXlJOUlrVjZNYWxRClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUgwejZsSEIwL3I0K04wWFF5ZG8KRFN1MHNUTElmcmZLMnpGWjJlM1dOQTBTWFRUVmhOMEt5S29zdlN0bWFFek51Y0x0eTVXbVZ3Rkp2MXZubm9RdgpnVTdNYUFsdmZHUVB6cGJOcHFxV2FmOVIzNm5ZWTdUN3hORjBrZEg2emVXWmFLT0RJd0NMdDRxWmxIbTh1RGJPCjcyR3RsZUVNN2JCUllWMnFycHZtdnJxck5kbmVhT2Q1TURyYjlBd2g4c29UUGlja3RxZjFwc2I1ZGhZNlppdjAKeVo5WG5TS2lCanlCQWVqYWI0T2h3bnhvVURPcUw0QXFOanUreWY2UGJuakw5bkhKTlhIWFEwVmRSYTZZbDRuVQpSNGdseTBDT1dkRkJpU2VYYlJxSXVGY0dyeGZ5anppQkQ0RnBKbEo4Q1ZxbWE3cll0MTVKU1A3eUNCSXl4dlA1ClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeERiT1g3TlFOSDdXNnoxTGxLMkkKbGs2bXlCc3VObzRWek5SVnd6ejlLQ0tKcC9HUVI0cCtOZWI4aC9VWjFzSHFtN2dOU25USVZWUWlqSmVzbzJ3egoxMFd0SCtJOGhrRVBvTmZwY1FUMjd0NDZxejlWK3NVUnpQb1BRakZRL0Zlb1h0eGYyQ2gzcWZqVWN1QmtSUS96CkQxUzZtS29qekNyZHM0SU92eGZUa2YwTUJXNkpSYXp1Y05iemtSdi9sMVprRFBFSzMxSmJiZlArb1ZrSmlkSE0KVVFJZzc2WHZGYitaOENublRaeHhBNVp2ei9uWVRTWUJoRElzQ2g2eGo0dklhNUlhY3JCTWVDMTJiY2l4K3VKdQpiU1U0c2s1RlRPcGhZT25GY3dFWFlwM0FDV0h4d1IzQjdTWkN4YXd1YUU2aE01eHRxWTBNc0RFSkFBbFVkMDZvCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1pmS0FoSnlHckwzVWREYmZFMnUKMWlTOVFzQWpNWGMxeWFNK2tReUpOdzRlODF1cUhPdUVYcWNLdHJsNUZUbkIvYWVhRVdBUE44OHlmOGd2ejV1RQpQQWNuVi9KYUNQTW9TL3ZOcmpOQ0pOMDNEVmd5bEJMc0VybjY5VlNXbUZoM0xCRmpHeXlRWHpvb1ZUTzY2TDJhCk5MUHdvVzI5Qm81QlR5Y0tBb2FXRGpHWWYxN1VoTXdWbi9NWDludFM4TGVNMU1nRmlJZmZLN2RERmpoSkUrYVgKQ0dla1RJb1pUcmdxMlJwcXlIS2FZc25CcEhXc2JtVEVFbWR6S0lJNmNiaitXNmx1UGNZV2hybnVvRHp1Vll2ZgpvM1dLejVxUzJKYkZCMGhQY1hjSG8zM1hWSFczS0J0M2llYzRWMEZPRGd3RnJNejNOeEY0eU0wWVVicnFwcTBoCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVVVcVNKdFcxeU15WVE3QnVqYkQKZ0dXVUtSNlJpOWwvSkZnc01aTjJLeGV0MU9VTENqdE9TcTRhUDJqK0c4c0hneUQ1RHZFcm9DelQzZ2ZJbEh2VgpaZ0hIUXkvUWQvbUNrNytXTEZmMk4wQnBiWjJVcUpsQUhaWjhUNVRMSjJJby9HOWNVNEMzRkY0VndiOVdtM0R6CmMzdjIvUzZqSFhUd1E0bkM4SzRwTUxITjdWQ3YvVEcwdngvU2JMRGhwSXlOUmlBVTE4akgvbjI3ckNHbGFkOEYKb05jc0lqTzZjdk5kNWp4bFVJeWNRQnBzeGRBOHNFaER2YmpPaUp2T05nVTZ4K0ZyRHZ5WnRJODlJbzdnbHdSVQprczNFZS9jZzk4YjJoSzFSZCtxWG81K1duNUVXcEN2ZlAzQnZraHJGWjNNV3hiOGhxY2ZRUHBZc3ZZZ3l0TWtSCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVhQeDJpZnQ0SWVOMDgxb0VGSGYKbzBua3FtRDRtSzFEWFZDR2pwcy9Nb1k5QW9BQ3ExSXNiZ2NxZVVNbjBQTmRmZXlNblRUYXFYY0U5OVc3YUFZQwpLYjM5QTNsRDNuYTNvb2VFdkR4L1pvcHR2UkNaMDU4eFdwcU44bkh1RUh2MFQ1WTBua0hwTzV6RlFXenlwcGJ1CjNwaGdoY3VUdStvek1HR3Y4cTMxUnNUdFZ1SnBXS0ZLR1Y3d1lRWEQwNkR3UUVQeG1JQTRRRnFmdDZycHpvRGIKaG9USDY0V2NwaUwyZ3Nkdi95N3AwS29OOXVvMmoxTkNpMUluV2RSOVRDRWs4NEowYVRLVWdEZjFJSklPOUlIOAptcmFLcWFNaHYyK2MweGtBT0ZPWXV3ZGRkQi9wTmFQVXM2Zm9seURyQVpjU1RXU3c3TDREM2pYWTljZ1VxWUExCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0N0dnl0cVBuUkJtZ3Z1OUk5bnQKOVk2NHlsenVZaEhwYVNuZjU3NjVHNVhWVWl2SlVDeGhyUDIvWnMvZ3lnT085a1JsVlZvS1VKMW9YSENsRE5MYwpJTHZaelFERnllLzllOTVDMmJjVE1qeDV2U2FqdEJ3eXFjN0dqbzhFWC9EQW9IWG9aVEg0UnBvK3d6Y0NRWmVxCjYxQXQzaFp6YmgyK0hhNTNQcld0UnV4NzdDOW9laWJFaG5KY0c3MFI5MktMVEJ5aXJTV1BITXFSTlhSZENJZloKbTFwTUpYYTVMVGlwanJVZHI0SXQzNTVObUxENnhJaW9jVDVEbXVnQ3ErNmVvbGhTMnc5bkxqVjF0NC8rTEZqNQpDemlkcVNKaUN2ZzlSV01mZGJLakZ3NlpwOUhuc0FkMlJLN2xxbENzeVdJR2E2NjNodlRYVEc5K2pWTkVWSEpKCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGN5U0dUcDR4aHZrMGN2VUNHTnQKNnlIaWF6SHNLdkQ2czNoL2c3aUthdTIwVWg0YlhRUVYxQ3VZMUpsZVdIeXlkSm0yUW5nd3dnaE9TdnBUY3hBTQo5UWwvMTYxNm5LbWsxY0J1OS9lbm1XRE1JRFZHSEFOSXpJclo1QzJieUJaSy9TcjNjK1JMeTk5NEZpY1J3eklsCjRkeElPRmN6NWw3SHFEOEl2WmttS2JDemtiVlZqRElHQTFkSFBkd1VrMlhVSHhmMjUrdVpCQlFLRzRBY2k4RmsKME84TWNGMzVWakVxeThzUUZzejJxVmgxbXZXWU9WOHROZmNNWnlXN3VIVnAvYk5WK1FKOUpvNkxOZkhkbEtHVApTVDVpTVhXTmlxMkIxY01lS1ErRzN6M3lObjZ1RmdxWFlrMGpwaUdFb1FObm90T2twV1kra2hoL0NIWHVVTmxrClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE5GOGwwcEdseGxSUVhZUm1jTlMKemZWTnQ4dnhMam5LaHpSSEgrbmFUWjNFdHlMSEdTN1NHVTZCTzVLdXRPWDMzWm1UZndqSEtma3BhMHBWd0hXWgpqWFVWN2pFS1gwMEhMU093NEYrNC9GRjJHVHVLd3IzQ1ZZWjh3VmU3UEg2VEM3ZnQzRkpyVnZ5MGRPa2ZBMHNHCmRXajZlSXpMbEdJZzEyeFpONjVnT0xyaXN2K1BxNkdqci8vMytHOFRKVitaWlFkREErc3RKKzNxeXhTa0hoT28KMmlNWGJCdlUrTyttdW9DZ2xKQUk1NmNxV0E1cUd4WHUvczFhN3E5c3NUOWF5bWJQM2hLVVNkamk5YVBkeVd6NApzSmd0dnc5WFl5ZmRkNDZvb1hNTEtvYllHVXNiMFdHcTdyb2lYMGNXQXFSR05WYXRNRzNlcjdFSjlwYzBTcEVlCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmN1QmxyTDYxU0JOQUtzZXlYU0oKUWhVUHhVRnE1K0RQQk9DNVc3S0ZkeG1TcjMvUXpDYm9HdzE1bm5LR0dSclV6SlR2dUc1Qk0xaktvTk12MTlSYwpJRGhYdWNJNWw1bTZPbzFkbWg1MUxnKzhtRkkzMTRMWTRoRlZXb2RiOHlnTGd2UGlxQnArd2pHTmVLSzB5ZFZ5CjZONmhYY1pIR2FHb0FVV0V6WnlkMW1aSnFiMFJvUnVzRlgwdW11blFMVUxLcjdCYzREMFR3ZUxSUWl3Y0M1K2QKOFJEVzJtakJSY3BLdUlsT0Z6MzNMQ3BGZE1kbGZwdTZFOVI2UFArVjBSTHhGYUN6OHgvMHZOdkt6dU1RdUVtZgpvNUVuTVFDVTlabFZIMlI2aFFZd3pEU3ZhQ1VCR3JTbk9paGlzazN2ZG5nQnhmVlpVOEplck16dTdRUTEvMVQ0ClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWUxb09ZbXJZVmgxQWhxRWpFRHIKMjNFeFJ5YWxFZTNLTmdkNkl2SDZUWG5kQndUeG12VmZUckVPYklCT3EzQ3N5cVdBREQralNkT1NtbHAyejFiTgpJVkp3TEFoK0w1YnBCejRaZC9yazkrMFd2WFVVSTVCMkJOZUFDTm41Q25aWndRaVllWEQ5NTV1RDREbW8vTHNECmh4bUVpU3U5Q3NXa0IzMTN0SURQRFZINEw5QjdDcDJDOXRPcnZOSjloZEd4SzROMkRoVTZpa0FGRjIzYXJKblcKeUovSEVBMUtEd0tjSmRvbm5hUXkvOFdsck5nZ1BqUEFyNm9WN2g0bHBIRndDcGNoV093QkhGQ1BFVFRDQ2lKNQpadTlvbFpEbThGRlFta3JCREpDWWdHdS9FdnNIUURQS0VRM2dFQkJaVDVhcytnWFN3NXZZTE9CWktNdno1VC9RCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHc3cklZVE9SQUdpL3Z3ZWI3MTUKS05PSnMxVTZLZjQ5YkQzeTZWZUsyOUt3Y1JCU2NnaTV6NjI0VVdUSGUzTUNTWnEyOTFGQkR1Rm9lNnhoTndyOQpybzNPaCtyNDBDV0lwdWZNdnZlWFEwKzFiMVVjTldvaXpZYUdUZlJCRmxBN3lLazluc0k4Y2lRZEtnQ0lKZTN5ClkybmhsTzB0dStVS09MYzB1RWJtbU5QUW5nNzNOTW0rV0NBU1FHbXVYb1k3NXFqRjZCbGl1Qjl0WWhMYmtWejEKeHJCaHBqTk1rVjBxcmFFZVN6MXhDdDdzTGJGeUd0d0xmN1JUYUpkbUVMbzMrSU4zR0R3am9WalNYUFhxa0hUWQpjREZ0dGNXcHh3bXVEK1JnNHhyYW95V1dhTEZBZFNXa3puMnc4ckdqUEQ5d0ZwOEU2a2NkVEVjSytJM1dYTGdWCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDh1NFBWL0liSzJTd3BseG9uaWMKUG1QQXJLZFF1MGpYT3piY3lyNXlnSE5DcHkxREVYb295ZTBrVWtTTmE5MHpBM1FKMEhhWkNXNVRwaGVzMmYrcAp3QnZ3ZzhJMEQzcW1IdTIzM2kyNTBoWnF4bE1yVFprbnlQWkZnOW12Ny92U3RTQzRsTEdscVZQSlJRTzRrK0dUCm9pbnhMMmlrR013dXhEcHUwTENya3creEZ3bXZPSXFZS0xjUUlpbjU0Wmd4ZXN6UFVOVGdWaW8ra3Zrci83aUQKRitUa1J1US9UNzVqOUdIUjFtdk42V0dxWG90dWpQcFMwT0ljd3BESXVHTmJiMVhqYXhyRmZqNE1vZjRmMm5WdQp3NVVtdzA1Q2NBRWMzY0Q2M0pkbmwraERUMXNyYTc1Y2ZsbXdQRnBUZzRoaURPVnVIakVZVmMyS1VlS1dVbXBSCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVFvM3lFTk9KU3NseTViWXY3VngKdW9pUno1U0ZiVk5YdDRCTCtsd1ZqRFdVSm5GWkJCOGs1d3YvRmNicXhGY04yYThGUi9QNDdmOE5ndFY5ZU5EWQpqSmkvMG1qSGNKeEFSaVNTTDB6SHZWTVhxb3didUxUMnRrVGdKT3FjdXRibXFzZElnQTNLR3ZOK2QxYUNhQjMyCm9qQVV0ZDJjeHVpaCtMTmVEblJXMCtJUllRbWpLT01pNDJUdzY4a0VmTXlLSDRrUUNKcENGL3pXalp0anBZWWkKaEhtbUdYcVBaMURMcy9Wb1N2SjNyMk52VjhSVXM0cWpQUlVxQk5MM0ZlTWdRZTZmWTFJd3FwM3FFMU82K2JONgpMRmtYeCtKbUw5amk3SnNNRDJwd3lkQ0FZVlZydUUzVGgvdVJSVzV0WG5KQU1VSWVGMXFFTDBwdDRQb2lZVFdICjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd284VG56R3ltSk1ILzJrV09YblIKK0pwcENOZ2V4STY5dXJQWlVnbGpwU3lPcGNnaGtMbFJZWmd4c3ArY3g2U3dlTFlWbEJiSXlqVnRoVGZlZ3p6TApFM0IwelpLYVdFUVBQVHRZd2xCakg1eDRkbGJJZWMydG02R3dTL2RtZ2VlbXh3S1I3MEVnajBaWHdBZnpQU1JJCnNPaTJGSGtGRlI5UlNZQkVkNG1SMTBKek9uL3NESTNRYW1lbFFtNGRwWDBIWXR0ZnBab0tpSnl0WGNjYjQ4VkQKSXF2N0R6VE5PcFFLQTNRRXJ4aitLcXU3Z3E4a3crMndzUnl6aWtsUjNTZVVhUHdhb0taaDhudzRxYnJMekZNYQo1Vlc2QzVIMTV4NGxVMzQ2RE9WL1haWnM4YmVwdGRGRVphZC85UnVZVm1JcEhNSVE1UHRObnFmR2pldVdFbFgyCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBek54WWxLc29ZOW1iOWFvZ3JQOEwKV3oyUXJDTnRobG9XYU54OWYyMGJEbWg5MG82VTBmaktOekdESldrTWJFN2cxNktVQURzUjdaWStMTncvTkdyUQpBcis4OXhSa2pwLzBrbHJ4dFduQmh3RUNrRGdXRWhZQ0ZjZDhxYnBxM2xqOXZLbFNKTHBPeUhIQUVkVXpXRk5XClVTNExHRkpXREhYb1FOZ282RHp3QVFyOEVQUWNpU1NuVDY4aDltM05rM0RVKzlMelNpN1VtOWFyd1BzVjdKR0sKU3hpRjJnVHhJbm1OVzdYUURzc25FOVV4ckx6VXc3R2F6djhuNzJjR3FXV3ZTcmpTV2dIQ3R1S0tIaWovVkZWegpYYkZVb0VaakZQM2xZVytQTHVmNG8zbTExUFVWMGhwS0xqem9hQ3dkZzB2a2NMdTBiUHZtTVBMUUJKV3dKekcrClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnUxVnJqdXZMOEp2OU01RFozLzMKb0JHdytab3Q5Vk5hZ0doNWhGRlVZanJZY3NGcnNsL0Z1UXAzdWwvZGJ3TlBVV1kxU2QrMm5tbTZtWklVTjVQYgpxWHpxRjZvcU5nZWZPQ2swMlNOdjYzRk1sbW0reFZmaWFJN1hLdSt2eElCOG5HeUZLaDM3UG9YMEZTRks4SzY2ClpRMjA2ak56ekVSUTJ4QnVGZDJ3N1hHc2dKVHV4Q0p0ZnJtUnNNNGl0aFRncllvZjdYNEdyUjZBMmtDcno3dE4KTjVQVEliODY3NGtnN2xVcW5wdDlvL3M1dUZhYWhMRnd3WEJwN0hOMFExbk0xdDlhRDZ6NVdDTnlKTEUvckJERApmNk4wVVVxL2lFVFcrYkt3YUprbGNqZFVIRDBEUVBpdnF5RDRrYzRSQ1J2L1M3SStrYVNFRnByRTRCaG5Sdkc1CklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmtuN3BvZCt3bDhuUHlxRmIvZmkKOUxiOVpHT2NXOE9LV3JLVFFiaklDU0lXejRIWU1tYVd4T2ZZcmVScEJjelIzWms1RG9YaXVZLzk0TE5mZllsRwoySXhCOWJtc2ZpWCtoYUZCUUJIbmdQMitxWldkbXdBU2lCbnRuaUFad1QxRHZRakFPN3hmRHpCTFFXNU9KYlVGCnJGMDUybUk4R1ZoUzJlOTVXNmt2VnZrZzdSd3lYSGV5ay95eFpWWGFzZEhiaTVObjY2WXRaaVRCLy9vWVVadVMKSnZzQTFIUHZNTmhBaUlOL3AwQ3NkTTJOZ2dXdVM0SU0rUzBZQUxsL0hjVExuSWF3UHgxSFpGL2kwekh0YmFDZwpvVW0zZklwZGdudE1henVZZTVyNkxxc2w2eVpsQy9udjFySStveWFpTE9NN0VmaytuZnNINVVhc0JOdjN1YktKCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnZ5dmpLM2tsZGtkT0UzMUJNdW8KOTIrRlI1S2FLcHFXUmtwWkM3L3pZaGJnSGRTYVFKYVhkL2VWT1doWVJKdGNrUFJscWllYzNQbyt0bURURk5mYwpHbUtmdnlGUG5taUJJS2lyWndiODlVMkFaZVhBRUVTc1RDNVdaRlp4T2RReUNpVGFUK3R1NmhOQk9kSFowVGlQCmc0VVhLaDFsT0lNbzcyM1VJSWFtZnVTS0ViaTNEMkdGdjBabHY3NG5jN20rcVdPR0F5T1Zndnd6cTVPbTJSNHMKeFlTTFN0bUVEUzlycDhjSEJsY25oU1cyTllWaWtXM1BTOURoMk1XdmUwVTRGcnpvOE8rSThxRVFCcHR5TlFGRwp5dkxBdVFPZEdnQnhYWnVCSk85WFJTcU5VbEdzZktUb0YvZ3RIUFhSYytvWEhoaElxN0Q3eEl4N1RSRmZ3aHpHCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFkybjk4Y1Q4Z0NlZjN6WWhRNEgKdFIzTDkxT0dBbDVQUkFJVkpPTzRrUWExZFd0RENNek42MWZBNDFZK1JFOThYV20vVlNtd3FaVGxBTEtXQUVPYgpBbWw5S1Y1QlNlM2RQV2JCajZWczJSVEZKLzFqR0Z4L0dYb3kzd1daaFRWUHJJeEZ4MFlWZUlVVWlxc2h2VGxSCkxTaEhxUzczRGFFejh3bmtPS1lVZmlGdDNrRTB6MzlyWjduM1Bsd0YxWE9LbE8yYzUwL21iTjZTUzN6L0hMemIKdlYvRlhHV3M1NHkzcDJWSUJ6Y0EySmRLR01CR1RiVDA0b051L1kyK2JLTjU0eFJvUjIvQTNKQ3g4dVVuVVROUAp2NE5RQjB6dFBRcVQ0d0tKcjJCZUcrOHJERmkvSFNxRjY2TEVBZWFvMHBJMVBHaGlOWkkwbEdySzRKdWE0aG1YCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3ovblpDZFRUbnBsRk1oVWpHVTAKUEgyRkhJUnJvM0pQcVQvN0hKbTIwejJmOTFYWVpSaXo2OEIrZU02Slh2MnZYVnJhTkRqbTc2VU9lREZ4N2VzMwo5TGNqUU9xQlNPR2JtUFo2bkMvNitpbHJMYU5oMyt1WmVyMk1kTk4zUC9qQVcyK1pKVUVsQ1pGemU4SHJQakZ4ClBPQTNpMHlUeWR6bUxldnJpVjZjWWV6eHI0bHVzYjd2Sk15dy9rRWNGYzhlQTZ1TE1hOWVmZnF3YXVKZUFRY0YKTXVnT0VXUFFub3phZDRFa1VJek1oSVNHMzBwVEdjNDVLcEVFdllYYXRvdGlXYkZubFg3L3hkcTZXU0xuZHB2cQphdVVZeXJtblQxS2NIaHJTL2hXREhlZE1RVUcvcTFmaTdncmtVWkNGQ2g4eEZYZ1lvcmNQUkdQeUpOL3Yzc2xRCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGthTS9kcDR6MTcraEh5ZzlqdGoKL3J2K05sdVVDcWp5bmczZGFKaWl6WlRLQ3dYRWhYTWxjMDFKblRSRngvSCsvQzRtaEdsRDEzQnlaL0Y3a2M2ZgpSRWN2M1Z1dFd5MldaNkhZSWczNURNYTdSR0JZZXo0Z3JYNmJxN2JFKysvZncxVXREN0Nid0k3UlVSMGZmRE12CnRlS0JzeEVwUzFlL1JvVDk4R241WkZDTHNyOGRNdENjNnAvQmpybXE1YytqdUFDT2wzaWQ4UE5nRG9aRTdLYVUKbm01TEx5VkpSMTQ2bTQralVRYlFuYzNEMW1MMUhqcmlDVi9SSnVDU01pemJIUWYxSU5Da05kOVZHTkFmQXZsQwpkWU9JZHVBdGNZY1dLS0VCYkwrWmtiNVd3TTVleTd0UkJrQ0FFZ2N1OXpOclJhb2IyQ2dpMWMyekZNUzZSaXlQCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEo2WndrVUJvcnZWKzlrTGpsWk8KdzJDaGlxUFAybytkQzhra2w4NVF4VXpMNWhURVdXVHF0c2F0ZW43US9JdzMxL2crREo5MUNnTmRoZG9kM0p4RQpIcVNxWm83THdhZEFvWXhiSHUzUDRvZlJ6Mk4wZ1QzeUIzK1lCVmVjSFdFVVJHRGcyekNZMFlPNkliOEhxek5ICjR4RW5JUW1mTWtQT0QyQWZsU2Z3OTQ3eXlKQ1pZTTZMeis1ZW9qd1Fad1hsdkFUV1RaR0JWNzdhMUtSMkpPc2QKTElkTjBvcmF2aXYrb0h2aXJYV3lWWVkxSUxXVGRRYWcybFIraCtzSnQyM2lMZlBLaTlUQklPcWhzODdJbS95bwphcVh4MHdnNkl1VExDa0dwaUk2U1BPdUYwRUFKSlgvdVZpc1JxQmxHdFpQbXJMeHJvSGw4eTZ6ek02cmNnaEN5ClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclJKdCtYcnEzMEszelZJUWlRNkYKQ3VCNENyRjhoL3UxK2syNmVVME1oMmk0WlRpK3djTzBqdnhVU2ZtUjZrUUl4ZTR3K2pTdlBISk8zamNJTWt1MQp0UEpEYk9vQit5aHR0WWZMYkRwdXpoVkRkamFzVTlBZlRXZWl3b1VvdTdtb0lMRUtSOEJWbGMyUDl2aFpicTV4CnFzYTQrUzZUdUpBTjFJdHhPSzNkWS9xblhoKzdJcFEzT0x3K202cERDQytJS2pLY1FXb04yaDcvM2hsb3EyYXIKR0ZEZ0JmdXl2RTE3bXQ1aHlMT3JDTXRiZE5iT216WWJWbGhKTGVrMk1XSmhaREtROWJoUXBSTDNldjVnMXJGawovZ2tMY1ljRS9WQWFrT25PWFk2alpVWVRoYmhBR1cvaUp4UDYrV1EwK3ROTncxd1MwbEVQYm9vYkpOakJZVmpPCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHQrOEdrMHJ4RC9yKzV6akNyblMKR1ZPYW9BMzVBeFVSWlVieTByOU1vdThaYWYzK1IzVTVyc3VaNGcycFppalBrbk5CQ2lsSlFEZWdWRlhpdVlxegp1Z3V5NzhJemx2Y2RBeUJtTFo0UmFMczVvMmRvMzYzVklmSzN3NWYwMTMzb09kUUxuSDBpbWdaamdpdmhsUE5DCm1jSDM1VGh6Wk1PeXM5U1VlYUthRmMwazJwK20xQnB4Z1A0Q2Ryc1NLVndLQklvTEdDbW5BSUJhM2hydTFPbCsKMTVMWXBHSVZtSW5SMWswRkhTY3ZsWHdpOXdhbjBHRXF4N0hqTkhjakhEZFFnaU5XSi9kRjFTQjRuT0ZtVDhIKwppMjhmazhzRllFVkRaa1BMcG13TmttRFlMVy9OekdsUVppTWMzNEN0QzA5bHhpc1RDT0IreWVvRGZNY0p0ZkltCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMi9KOTZzaGRYTVdYOG9VejB6ZnkKMjhjVmUyQ3BKK0NXL1pLNkRjT0JNQXMyZjQvU3lJK2J3eU5JNERESXBWUlNXNU5kUGl6K2gwRlAwK2NsTzJqdAoxc2JlRFdvRnRZV0xaNThURGhiVVdlbExaY0R2QmxxWStvUEQvUXpLa2J5QU5wR0ExZHViZmpSQVpBUkI2K2llCnNQSEllclVtWmFWZlpyVlNRaS9uUkptS05heUMxSjRjSDdxYnNVa2NvSU5aelFNVWdEc1VvRUVqN091UDZ6UXcKWjB4SHRQYlN1Y0h5cmVYVUdJTjI3MllybWJudnM0dlpqREd3NlBaVXg3RlhaUi9Odk0wcFJNN0tkcE5qay9IZAo0Z2VZTlBSZWZ4M04xTnVkZElYWUx4c3I0YjZmQnpFc0E1d0RYUFp0cmRuRU9yT2pkN1pSTE9JajNBdXU1NnpTCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFB0bTgwMVllc1pRMGZTWmQ0TnQKcWNlQTBQV1dya05rdVlmeXcwemVLUm5oSXRHbEpVQWNnc2ZsLzdOSXlIeGRqWDZhSmUzSzlOODA3cHNBbDVxVgplRjJha0p1UHRMRUpiSWFxSzBRUVg2WXhJQm56VDNGYTlJWE1ielBjSjlncHZoUVlwSS9MMnBzUURkRHBIcmxNCkVZazd2d3BBZ3JUWFBvT29yeVhFMGcyNzJlYkdSY2pVQVlrRTdNWWtGRjhOVW93WFZHVVRyc1Q0bEhPWGdSUmIKTk54Y3F1NVJDQUlVd1o1T1EyOElnZEJ3WC8zMDRtKzNETWw1dmNqTDNKK0JydjVTb1lJdDRWSDRld1hpeGxBSQpXWkNnVEVtQjEwNDg2S3JUOWpzc3BGZ2t0UkNVRFRlRURmZzZ4dDVwN1hsVHN5K3U0ejRzeW5WNmpMVFIvenV2CnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMENBRURyZFhORlZ5Uld4U0gyeVUKZjBJWi82UU5vaFlKVjVIRlFWQVFqVTdVdThHbmVZNWhMYTIzd3dzRlZYSU51Tk9hTGFmNURDZ3A0SGg2Nkt4ZgpGajkyZSsxbkR0SEYvVEMvclhGMGxmN1FTK0JnTGkzWEk3RCtvNWsxUEhzemUydnZqNjg1Sm9nSWtRbVl0VFhICjJzalJEVm4zeUpKY2R1UWlzbEVtTllmaUU0R0o4NXJRVDllOHJRWnNUU3pIZDFvTy9xMGhGN0tpckdSYUtGTk8KUlVLazBjekFMUTVzZjVFSGdMT29vc2gyQnJmcm1EelgrazluMFNoTDgzdlVRa24zT20xR1B5OEkwMTBPN1p0cQpwT3ZQMWVrUEVsQkNENzE3U040QTVRanVPcWVsQVVrbDNUR2owaXF0T281aW13YVRHVzVRcnpyU0pxU1FEZHhvCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXFLcWtZbXB3a1g2TDA5NmkyNGEKbFBuRWVtcWM3K0gyT09yNEkrQzl0TVNsSlJ6SzlHME4yMHVpa2lIVVc4Zis2S3ZvSFoxU1Fzb2Q0anVLZlhqWQpJYi81QmhjUnMxMkZ6Z0prZlBJQ3JCcWtjaXo3OEpuaWx4WTgwWkt0T2l4RzFiV3IrSlh4cUljRDJITCtSbDlmCkw4UjVHSWdjeVRnUC9XNjZkZ2R6SGJtYzkrbDBHcHhCSGtXN21mTGIydFo3V1lNdnAyQjhqU1ozcGExZXdzWU8KYUI5NXVzMkQ0a2pPQUlEUGo0MnovelllZEh5UGNkeDdsZjhXWVc1YXRvaDdNcDdmcVBHdDNTMGFNbHVZeDVFMgpkcGZqVDhXYzdHZzFxekZLdkd3S1hiU1pYN2lWMW93UzNLMjRTWWIyYTRDa2RtOUx3dEhCbzFuVVJIVXVHdlhpCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWFueHowbEVPbkhiYjFMcU9RVUsKVDdpL3Q0RmNsaHhWZXNVNjRIVHREQ0tDbGxFa1hKeXBlRjBWWlF6OXNGdDExMjhubmdHeXFIQy9Za2RZNE5oZQpGb3NvaWExWmdNdUY4MGhEdnR4bTFRV3hJdHRSMzNtNG14Mml3S0l3NUtZUEwwbnRPU0dwM09Na0Zoby9iRysxCjFCTTNqcEIvZ0JoS2ZqRlFjeGxiQ0o1NXJNOGhCZWw0UUZrM1JvK2gvRjBMdzdFeHEvZ01XdXJHc0poaWhoT2sKb2lobVpaaGRObzZMeS9DVjMwYjBYSDRxUE1xNlE1Mk91b3VVZDFqOEc0NkxEVy9YUHZNdXVEVDF4ZWlHc2JlQQprbGxKQUt3d0hKOS9NbUlhWnI4RndiZTdaOGVPL004WWhNKzJyeHl0MkxiSEN1U29RdHFyV09heStnNHZuSStRCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWFNREF3bUFxV2ZkMUpaTTlFczAKTTJ1bG52YkZ3MWtLTUV4UjZKS1Q2dmluWHlscnlRUkpYbDFwYkhsaW0reFRSS05VYjVJOUxZL3lDbW5nOEQyKwpiTGZDRXM5bDVuMEZhNmVVSVk3Kzk3U0Q4QWRrcVY0R1RmekVwYmFHRmU0VHpKcExlYkwzcWw1MUlEdFFjSGxQCmVVWkttQ0hJTHh3cEo2V0RsSkJOait1Uy8yRll5dWJrby90Tk9xRkRVdCthZXRVNnA2dHYzMFpVbDg0alBLWVgKelQwUVNPbUJSZDZoT1RjaThZTTQxOCtMMVBBNGFjQmV3cnB5ZVBSZ0ZMUVNzZHRGSUdKQ005UGNOeTZZS0RJWQo1a3l3VktiOEVFelBFaS8vTWpIZy82RG9BcDdWSXFOMUJyNEQyWE56dEcvRTIrNDZxNnk0clMzcmhPOFNxTktECnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHBHWXNkZHZlZUZpT3hVb291YW4KSEFERjl2ZFRvVE5SbjlETnl2bGNEbEw4TjRvZzNKTE40RExPdno0aXh5OUpRN1FKOXJrOEZFOUJvMUJNN1VHegpoVmxTQVFpS1hkWU55bVpJc21naWU1Ykp5UThrZ3RCMTJlb1hIUm1YWFNSMWpPdmI5cTI1UFlzdGdYNmswOHFFCjB3M1R3Ykk4blZUY01BQ2xWaHVFa1lyVm5Fb3IwSzVNWldtU2FBcXFZWEVETW1BSDRIcXlZdnB0SHIrem9aV0YKTVdSOE83ZU9WRWF2azJGR29sdXdYVWJnZ25XOTlmVCtPaDQxcTR0RllSMmZBbGM3NGg5OEFSek9hZmJrRUp4WgpSVEZKZTJQSGZWMnNKU0JrRTRxSG5vRHpNaytCVWJkb2FrRy9oTFU5bTJ0bDhJRkVWZEJzeVZ5U0VzUytnaGsyCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTZMbUcreEp5UmFBcW8xOGRBNzAKZXBoN21lUlJBZlNRK21wZFFzQ09vNy8vOHZrQkFWaGdxYW1FQ1F2N2dPU3lrb3lRYXpDVEsvZEU1SHhQL1N5Tgp0Um9xc3hQSXBXMlk3Y0xaMTRpZjZsaGpXY3gzaFN1MjNlbU1YOGg4ckx0emo5eXdaM1k1ZDluWkYrZGNUTkNuCmZHWFlIVkU1eG54MEZBMDB4a1lqc2h1YUtPTjhGblBjSWxxMi90MUw5bkR6TWpXbTVxcFY4Wkh6c1BKT0oyNXYKbzlmN01PT0d1WlRqZE4zSkxVT1JIY1BtUlJ4dFJrcFI4VXppL2dsSkY3MHRtdXZZenVUaFpac1VJMkRKMmlSZQp3V21pUVBWZ1k5Z0lVUVFZRUtadnFqajhUR0hpajE5YjVHU1J4dHN1RkdLKzhsVnk0S3QwR01QeXFndVpkZTJhClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd21ObmEzYU9NYlM1eUwvR1AwaDIKK0w5Z0lyaTVZNXp0UlRLMU9iZkJOMUdKQjRvdExoWDVvb2VYZ3p2MjVTM3N5M01pdDY1NmlwRk9ETWdaT2JYdAp2K1R0Wkg3QkNPR1lFVmtUSEJKQ1prM0VMd2grSE96cENkYWdYWkt6Mjdva2w1eXc4OWFiZUJaeGlKUEQwdlQrClBsaWtPaEF3MGNSSFJoTGRyRFBCWmxEUkZSSjBQQmt3YW0wZlVGcm9WNm8yZFZPc1BMZC9YampuNGkzTldaUm8KbkhLZWRBSE5lR3VqRFgyVlVhc2FuWjhhTkUrNDc0LytheUhidjdQQVo2ZGpXL0NVcFU2L2o5QnNaa2xaRFJreQpWczFHZS9yUFd6QVdGSnNJNWRZQXBmZjFpYkkySTZwRUMyWFhFdE80WVdnWXJycGgwRG5pRi9penlsZyt4SElmCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOVV6VUhOLzZVeWZuYzFDR21zVjEKaVVBVk1VK3ZNaDVvK0pZYS8xWGMrMU1pMU1vRXlGeDhhQXdINmtMbWI4cTJnaUloUlRHZWFIOFA4V0NsNFNzMgpZQkRtT1VIWk1ZTjIzWkYySDFEcCtNYnFsdDc0VnptcVhjZ2hrbUpSZlVhYnFoTnlaOEFZNld0RFhFRzFCUXdrCkpXbHBkaXZoSXg0YldwUU9GZDl1dkd1Wms5SFhVYkMrNG4xRUgwTkZFVVllQ1FmcnJQcW9CZjhYeUJzQ3ZJcEIKQ2s3UnE4YWdQejNuV05Hb2x6UE1JdkxoUlUxeXRoakVMSTZjSElMdmlDZFp6bVdpTlRhbmJwMkdDR3F3VW1nNwphTDBvZUxIbXBZVnFXQlFqVXlJUWc1TnpIK3FPdUp0QjhrR0x4alBGK2ZpSWtqdmR3NWlQTXdqa1ZObkx6MXNECnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEFMWVgra2hlSGRadTFScVE2QzgKUzBQclVvRStMUklqUm9MUjlhR3BISSt6b3pnT2NER1N5ZjFsUzFJOXo3emlKbnlQSHNjZEFFaU1uL0ZQNHdOTAoyR3JYTHBTT20wQnNQZzMrcmZVcVU3OHdDelNDSW8yQ1NqM1RaQ1NtNXAreXk3MUdaNmdSMTAvcTBMeUl3MkdZCklFS0dGYWcxVTMwNFJXZmllZWljN1N6SUttZzVzZ0xUTGJYeE4wbDFqZmZsY3kzbjVWc3ZDSUVvcWhHK0xxeHQKUXloenpqTlFIY0hNQzkySjZ3cEE3WDZ6OFY3aXBEKzRwbkJqRlpaRkw2MzVJMGlGZXQrZ08rWWR0RVEvdFlaWgpqSE5Kc1AvWndUZzkrUm1Yak8rMUdnNDZTaUFISTNKV3IzVy94Q1JrYzZsaWJNUUJWcEthdFYvUjk2VkU0S3NKCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0xGU0pESzVENWlHZlp2Vm96WjgKVFF0TzJmRUFSbVYwcVRYbS9XZnBCcFkzekVJZ2Z1c2JUQ2hZUXZLcFRlZzF6UCsyQThWcXd2dWc5b2RzQ2RDeQoxRmExUDROVmlha09EejlNY3ZuZHIrait1R0h4M3BCQnJOQThlSG83OWVNUFpnTUlnVEhvSUxTOGlDUUV6WnBvCnpDcUthUE5tTjQ4MzMvTHovSWpRM0lZY2xzR0diUHBydSszVnRqSlNtaVN1dmx5SExPWVZWaUNGOUZHb2FOdkYKUERLZmdjSVZWa0JOQmJ5MlBjdVBCSjRrRG5reXo3WVl6YmxrcUFKTFI4YXAxb3hYcXFVeUh4OENtdG5Scm9HWAo3dTNSWHc3eEtGZzEzRFFwVVBBbHVpK3p3b20yY2Vpc0tiWjdiUERQSVBiUlErNi9ocFFsSVZ5SzB6OHdKeG1wCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdU9nNVVVNnBWQjhZSTd4NFMra2sKOFRtUVJLcUFiMGxoc0g2ZDFOS2pobkJIY291Sk53Um5uTXF5Q2o4dHBCdVprZ0JZSElGUFJhUkZZVHcvYWNMQQo5SzA1akxBS0FjWGNmbXJsa2IzQVU0dVBLWWZCTEtlU3hDOFR6a2w4dGRFMUpUb2YvdE1naEJOUXpqWFBOS0dOCjFleS9NV09SbjRPQjFPN0hCZWZJSSt4b0syMkFsSG5TR1dtcEtaQ1ZtSDgvMUpUNk1WblVKbzNUNGhaTitzT3MKK1V5dHNhSmw2R29SRFl0c1E1S1RzQWErWFJMTHplK0k4M3FjNzFoTWtNTDRFYk1mdHBqTXFrNHV2QkhjWVpvWgpuYWN6TmhOWTlYQ21nYVNOYTRpQ2RGMUIrZG5vNE9zQ21pOWFWUWNySmNoSld1RVZxdXNNZi9mYnFvdzdUNk5hCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFR5enk5U1FsN3VlWmlYVDZsWEkKQ3N1aG5zREp5VlluQWdCc0FoVDFOQTZnTG95T1N0cU9QSTFyTVlnMStKOXRlalZoc3NkSXdJNFY0YmZIcUJTNworQVU1aTIvalFOdXZoKzhlaWY4ZS9lT1ovQmJXS1FtYmJYL3lBM2k3RWJJR2RscHZwT0VtUjRiOEhyZ1pjREEvCnU1QlRzd1JBNmtkYXAzZE9adC90c0twWDN5VW40cWhPM0VtaUJVcExRQUsyd3doWWtjUWJaNWdINWlHV3c2eXEKekdTakg4N09KYk1jckg0Zjk1MHBFVnVNRlBpYkRnTi9zT2FTWG1OMWVldHo2Njc3UktDL2FJaGRzd1BzUTRaYwozS0tDUS9sTDhwN3VaRU53b0Jla0FmbDZPV0tqQjN6VDJadEcwR2ZoSmk5UGVWaXYwSjVhckV6NnVPKy9ENzM1CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUlQSzl1NGR0bS85MnFIeHRQdmgKTkZNNWVzV3ozS1g3LzAxSEYvcy80eStBM1lIY3dZMU9uY09HTjVSYkNCaE5RUFUvcml5RjRQditXd3VkeVNUbQpWS1JHSFVtZ25kNU05dnRJdEh4eTlGc3Y4MzZGMHFZcEJqUnc2ODR4MmRQeVJBYU5veFpPOUx1ZVZUU1RwZ2NlCk16cEpCdm94czhNK1BmczEzdHJ4bHlxaGVMTkhjY25zM2pwSHptWTFIUW9ERkFlOXVHWDNXM2x3Rng3WERzVm0KZFhmUURPakZEaEVLMWNITkwwWUxWb1VVU1VGMzlJRGdXTHNKelVhaTlGb3RBTitLeFhhTkRibVhkbmRnSmYwYwprZXZaVklUU2UxdUxOOXJkY2ZpN0tzakMzbENpL3NWVkh3MW1PdHlpWlE2V3pHdTMyci9kZEgzdTJLblBvNm9SCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmh2SVhVMFIzVXJhSU1jNzNpZUEKMWd2L1VNZVJKQnluUEZqYXFRK0tHbkdGTTZlYTdwV3REVVNiT3RvaThGV1ZGbjZ4UCtNbjlnejBoSUhHNnpmZAplOVRmVXE2NDRzSkFieE9pQmdkK0U4T09FWjZpU084aEdmODc0R0RlR3M2RUxncE8xRi9FbGYrQWorK2F0MVg3Ck9pb2lVN1JiN3g1SzlRaE5zLzZhWVdJcW9oRWZaYXYrMVE3U3lESzFSUHJqNXgwZGQ2NWk3a2dhOTdGWFVpaU4KZThudzlZNGxiYTFPS2IxdGN4N2VmZkFvVzRYVUJHRFF0TVpySDQrQncrRWFqTkJRQUNNbHBUekpaZnlscnFQdApyVEc4Q2xWZm5oQk9Nd2NoRjRXWEl3UXN4WS9RQ1FLQnZLOFlZWGdIVUVBZUlXZC83NTR2TUZaTE02YTloc2paCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkptUlBIdkFkL1lxUmFQTjBhWSsKblRrK0RqcnA2L1ZYNE5paDFnRHJZQ1IwMFhkMG4za0g5NmUvYmg4K3FjMEpEZU52NUJHVnpObGE4M1BTYXk1QQp2Tk9zSXhURUgrbVFMcllXRmVkaE1nb0kydjV0SDdOZXdJeDFUZ2x4YXVNcHhzVFhBUXYrNGxJSmlQRm0wd0luClRsNFF2MXM4NjVpRWJ2cnNTNWVFT25qTm5FMnNvTUt3UHlFdzFCQ1ZIallOeGkwb2pQUjJBVDdjRXo3NVlCcXAKa09rZ3FnbVBQeUJTU0o5cnJRL2YxUjE5UmVFM1BQcHF4UlZGNWwvSldMcGpieUx2ZlFHdW5maU9jaE1PdFBXbQpKRjZrWGtrbGVUNjUxSm05ZmMxODRSSTdnL2oxMExOZHc5L1B3QUZhMTVUTVdZOEpPRDRrazBTVVJzZE1rUktJClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk0vV2svZDVPNzF2ZkdXR0hEZUcKbHRIQUUweEUyRVZDVUpmekgwTHFLV2RuTDdJd2h3YlcyTUpRTEdWUzdESUZvejZEcE9NeFQ0WHlHbnpEVkxCTQpQOUFRNzlLalZ5ay9vTHFqeEhCbE14RlhXYUNURnZHWEp5WFZlV051SU8wM2dPWVpqRVQwcDNJUkhGR2Q1WE1wCmw2U1B0SUVocmladnM2OUIvSG01R2JaUThsUGlhN21YR2l5MDA3MndIZDRxd0xqZDhGRmU0b00rWUdEbTZ2RVQKUW96cWJaNjBIcHE2anVRZ285enRMM3M5UjNEeHdLOUEvWGNXZnM1a0dkb2F3WGR0bExkbkZkUW4wZTZNODYxagpseEZUY2YrVTZGUlhMeHNrUnVDa3RwZUd5dXNGbm1VdWVSVTlmazZhSnNYcU1ubmJTS0ptVEhpeXNxZkJ0MDI4Ckd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0I0N3JNaVdSb3ZlMEZodDc0OGIKczNuMUNVNHBCM0pxSEhQNStQN3pxWEZwSDBlVFpmRmpHZFA1MTJWV1JsL2FycVluUXE3RGlMNDhnc2ZJSG5UVwpRMmtYUytHeFdNZ3A5TXo0emJ4Y0NSc0M3Rlp4K3Foa1ZQOUdxYUIvLzVFbTIyTkplZXkxN0hES3NsT0xrb2hoCmtmUXhmMVVKLzlaM0ZDZFlHZkdhcmY3MVJkbUNUVGFYbEx0MFlzbFRZOHVNR2xLbWcxU21OR21IOWRWR1F0ZkUKN3k3cWVueWtTZGg5RW14UXFSWjFTU21oUDRheWIvdWhPMTlUaW9lUnp4MHBBcmNnQmpwNjVlaHJVRWIwcWFrRAp1bXgwcHp6Q1VnUzVQTFYvQnVnR0JOVnVZcjNiQ3d5MjJuWFJNMjRHbnVUUU0xWGlGbHRRMVpvYXZpSzIwc1JFCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDdRM2VQcE5XOU04M3ZZeXhpamEKaTNteWhpL0pGbXFmSVk1TkkzNGsra21PMVFqb2U3RmVlQTV5cUhac2J6clpLc2x5RFczUVRLaXgxdk1qQmhnOAptQ0lZMVMySDY5VWYvYVQvM29yZkJvdlk1T0RRWmYxTnZJaFA4bWthc1RCRFZocC92dU1QeDNhUWY5Z0NyZWJrCm1JTDVJOHlpbjZqTW1FdVluU0svOUlkV0x4dFhyeGJkQVcxQlQxTzkzaEZFcHpFdkNLTDNibWRUSTVHOFBEQTUKM1FiUXBubnNxd2dPT3FPU2NRZnVSaGJua1FLcHd4b01GZkVLMldGc0xtbUJEWWh1K1hOV2drK1V2b0R2Mk5qSwpyZitBMGg4MXVTUnQzNjRhMUFRc3NvZHN0TnFpZElCbHZsUnFDYlpHSldqVE1PM3Z6RHNlSmxDem4vY2xUMktwCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDZ4SGNLYitPTTB4QllkN09vZWQKZnk5MjE4WHBFbmVDN3JOR21qOWhnamJpRU9XM21IaDN2VnJ0ZWdkZjg0a1k3aU5GN3BYeXoveHNUclBvVkZMYQpZY2U0RzVvbkZ1cUZkUTExUHFrbE9aelBLeGljbXZJYVB2MENOb1pteHJhc2t0K3BaTXFHeURhaHhFS2VDaC83CmVleUlXZWFGN005UFVaSi85MW1XWVpENGFPZmZXZVF2cmNCbmlaQnp6RG5NRS9TbzRCZEsxUTk1TU9sdnNvN1YKbWF4aVRoWEgwaVFIdWhQV3Jhc3g2NmNSR0liRDJNcENZaWVJVkpvVmhmYVNLVjBLZ3RtQVUwaDVKVHllWnlTLwpLbmR3a2pZZXd1NkkvWE1nQnZzWDdQNktjRHBDM1FWZDBnWWpsbm96M0J0dWhRVEhyU3ZYWjZRbGtwRXFQYUgxCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHluN3JzanIvblVQU20wY2F3amsKMStxVlVKbG9LNFE4OGlYb3ZPNkxCMEpQN1pQZzhvK0FXdmNZZElYK1MwaXlNdTZGdEhLS3ZONEFTMkx2OWdoQwpKYklCd2laQUt6cklYYnJ4NlY2VEN5cEpEUzd4dVRhVkJuTXdudlhtc1pBblJlRWNnT25pM0RPc21xWjNNczZ6CkVSb0tHOCtYM1RzSFZOcUxpT2xkV3E1MCs0Y0lYNktiaFpKZnJnNWIzQ3pPa1V6UkkrQUJqbVpsdjZRRGFVa0wKQ09NdmV1Ulgwa1hJK1FQZTBmQ0VaQjZWZTQvV09FWFdkS0RHWkZPeHpXa25maUszRitwVFZwTjJwdVlmRE1oKwpKUndTWTgzK01HU09oemlwU21nbjc0K3lOZ3R0NnlyajQrZW9Wd3IvUVNmaitXSDFSL2RMZDRkUVFVL3h5a1czCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGJTRFF1OE8yS3hZSDd6ejZtQjgKRnk1dWNqT3l6TzVzYVpsdTZQeEw0YS9iYnE0N0Y5cE5NVG4wSkFOZFFDOEx0WHNxTzUxS3VtVng4dHIyRnBtKwpma1d5MFBWNWpvTXdCMXZqZzZZRjd4K01hWnNwVDV3KzZmQStSajg0MWcxYWIzbysyWVdhdkQ1MnNKbGlHOExvCldOUlNqUnQ2dE5ndjk1SGx1am16MDNiMVZhVGNmU3R6UmpmL09nKzFRb2VJRDN5bDNFT0M5bWdjZTRPbE9zemsKdHF1REE0ZGtvdit5ckxpNTNHY05mM2drekQrNmlPUk1ndHYwaVFPdzNzUXdVNXlzK3pkU0NwR1YwYVBvK0ZuVApFWGVYcFFEdVlObzJTZzBWSFZ2QUNkMmxDcVI5Q21YN2tyZW1kNFBtUkxtQm5zVndveUsybSswTnA5RStSY0ZXCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmcrYndzOWFmVkRpWFh6Z3dqbGcKRlpMWFpOWmR5ditlaTNnc1lTbUtLei9NdFVrRXF4aTlON2VqL3B6bWU4UHBTWGl6NDlEaW53SU41M2x4dEd2QwpHZFZXTC9ESG5ZNGFKV21KRlFHYUhIUWJRbkVjZHMxK256eXlveUgwYXFxanN6WUY5dUYyZys4YXd3d2dzRXJ5CnhEdjkwYVh4cU5kNmw1MFduL3VQNlpCZVJEbWg3MmZaYmxOQlpaWDAvTmhybTVYSG9nekFqeXBQei9VY0V0Z0IKNGo5dkdLYnlEYTZGcmtRZFpLNkppN0dDd0dSb2RscnVtd1l3K2dNSUg5TXlXYjVjV3B2dGJUaGE5ajdpNGRyNQptd3pIUzA1dUdLYmN2QjlWM0g0TWgvQXJMYWdvakxydFFGZjh5VXl0eHVVYllUWUlzNkhFVEE0ejEybmxXaWViCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1NXU0NtUHdhajM4cTJuUjVtNDQKZXFza1A5Mm4xa2h5T1pJWncwYSsySUgxYTVvQkt5R1JacVlwclVncWx5RGRSU2l6Y0V1WWY4VFVlc1paWVZzYgpLYzFBNlM1UUluT2s2M2d4K2hOQ3lBN3B4WjVUc3Vqc0VkNSszNkF1R25XY216S2VXVlpVNlVIUk1sNTBBVDcyCkFURisyekVkMWR6ZCtwQytHMUhTMldWVU9TbXVGclNMS2JxMk4vMmthSytOMXZYMDJGL3pyemlSWVI3amxZOWoKSlpvN0FxdmhjU21PckJEQnRSZVo5TlJ3T0JxTUFTNUtQVXRpM3hGRkhjSEdtbWJLYW8yM3pKS2pUNUY4R1R5UAo0a0tOazUrNHlwTWwraURwaExyQ09SUGlVdlJUNDh5ekpnMnQ0R1JpcFozWFVnSVVzeWZ3enlsTE02NmRRNjB0CmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWgxTTdCQjFLVStVMEVRZWgrYSsKUFZOQjQvKzlVemJsY241UjVEc2owbWQrcy9uMjhmclRqOG9xWS8xQk9vZUNHekwybHI3akoybU4yVTdrWHk3MApKaEZRanozcGtNMlEvYzVHMGszeGxzT21TTDRGWGtQS2xKcW5wNzg5eXcxRU1lTjRWTnlSTmVEWXIyVkczOWZJCmllSDV2MjN5UG5MU0dMUEdjOW5wOWI2a0JWeEpFRzNCdngxU2ltTGlDcVViekgrcktsRVZ6WndCbGliT2Q5VWgKdTluQ0JpdjZ2Q1FodlcyeUlORFNOQjdVenZzOGFsdmcvOXlnRkFCeE5rbkVPTzA2WVpGZllkWEpjZW9sTXdJMwo4MkZMcGZHSHp2aVdCeXJPZjZkOXBWNCtLMVM2c0M1dUxVdFp1OVoxa0RjbUthcTV0MURMSWhOZFRQaFJWYXA1CllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGd5VmJvVmdsWTYxU2Y3VUNnS2gKMUlubGpyYnBwdFJFTE9oNDQ2NFNkM2dVb3NCZkxpZXlyU0hDNS93Ri9NNmpQVGJJTFo2V0swOUtuK3FzTmtBaApCSjRVcFU5Wk4wNkVzMExidXNnN0FZWlVoR2RIT2hyNEFMaE9Ua1NhNmpJODZkWFhFUTZYVERSbGNSZmZZdnc3CjlESWxEMXR1RXZZZWFDS3NOdzZrOEg4S0g5aVlwRTA3bTNaVEN5VkJ1dmVIR2tjL1ZsK3ZCSi9LYzVETkRQMC8KeExaSGtLRWxlejRESkJmN2t5eVJtaFZpRjV6VXB5d3p5YTY0Y1E4K0JUaUxRL09ZenMxMmFVSWtDNEdPY0tRWQplYVg3ZlJleE80UXpBS0drM0ZxclBNTnFzVERPZVdiQlJ1bkxmb0ZOYlllUUlPNGJldjRMREg4VE5MS012cDZhCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjRZWXYwb2c3MmZONDk3eWw1K1kKVUZwNjFkN3hWeHJOM0ViVDdWZWQwa3JkbFJMMm4zMnZUQlRXWlVxVmozd1NLTzZ5ejhuNDNHZmlTQXI3MDRBQQpobExtam9OU0JNV1RSYS9vR3RKMG0rMlZ1Ty94RDVQZDEzZWovSGVMNi9rTXc1czhQQVNuQWdpeDlCSjBwdFNjCjFHOElFM005WXNMNFFBQU9TQXJpRHc5ZFFvejltRDNjdVdvMmRLZlU2TlhJL3F2cjdISW1GRW1DWThVcFBVb2gKTVpoSnIycEp4TE5FVkxuc0dEUXJqNUR3OUdqZ21ONkpldkZEYTRibGUxd2ZHbnI5WDVRVjQxZ3B5NGUxd3hjVApld09aRjhmeUx2N1ZpcXhqQ2RzcWVqaHM2aTRMY2R2TzlCQ0ZEZjNmMEtpS3YzMElldThxMGMvdS9KRmRHTEQxCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlZzeE5YQXozaTdSb1ZPSjB5MGcKREJMeGVabGNFanZNTS91WWtISEYvS0lMKy8zS2tKVmxJaWpQdlRXLzBzSkNxMkNBam8ydVFaaVMyeDBKUDlYOQo4bng5MGFnMHFTUlg2dUZSSFlscExycTlPV1VXTi85WUhPMWtUZ0dMUGw5dk1NQVJCTFBZUUlheFoxS1NudHFNCll3bWdlcjBmS0FsRnAvY1RpbTVtdWtCVDRGM20ySlZ3RnB6U1NEdytMZmU0S2l2ckNRakxrQ21sUGdOUG1seVgKZnBMZFhFR1dhNHhqdlNyNjRxaXFub3JwNUxuRHFzeWF2MU84WWsxQjVRM2F4R1RGSmRuVG1jeE1ZUExZZFR5aQpmUXozaFlqRlZqbndoVS9YOVFNN21kZ2FSdXZrNk5DQVExTWhLMTl3MnFUa2VkdjFIcHF1NEZnZ3N3R2pSemhCCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlBwc1Y5YXkwVExPeHpxUVZxQjUKWFA5bVk1UEVzeG5TY0tjUFY0NjJUbjJHSlQ2U01hVjF5b2Y0NVpNZm9uMFM5T3BkYnNVa05NVjFXUGduN3RvcwpicXdUMFkrZExpaG5vOTZsY1NvRS9rMExqTzdBZ2MyVXZGL2paTE42Q09LaHozMzBGanZrNUVBZ2lNOVBGYjZaCkUwMHJVMXRpdjB2UXJqWnloVUlYWllJQ0czZEs4V0Q3TDNpMGREMU01NVhoVFRrMmxyL3dzYU5Cc3cxSDNxOFcKNDFrUFRuNkQ4VG5wSWp1dGI5bGI4Q3JGL2pvYjJ5em5WekNFb1lFNEFlK3Z1MEErc3NlY0JzandCRHg0NzJsZApFVTlaN1o3dFRlV1VvMkxQWUtnWUJWbkVxb3VDS3BTSHRzakdjWXNBVFhtZ3dYTmhwOTMyVTJxU1RuenJTMlZUCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmJVMTFNUlRHSWNzM1BKSlNrZFcKTFFWc2lib3NTWlNLUlEwZnBuR0hEZDNkdnNMVUVQOHBXWEFmcDhyK0wxQzdqdVB2NU1YM05TcTByVmtCeEs2NgplTGppRTlXYVloUHRwMGxSaU5JU1pNL1BacGphYk1HSFZuTmxxNitVN3FhdkVxZkxVWU92ZkROT256dVNaZTg0Ckx5M01MVmxtMTJUT1F0azc4MVhPVEJ5MEtaOFpNUGxvLzJTeXE0OVlEU0kzWlBBd2VJNmVtbnI0cG9aZ2FkbW4KS0hqM0RJTytGR3BFMGZsRVo1NjFQS0JjRThVTlM0OUovUWJ5dGFIcERXeG9UUTFkSVNGcFVJWGZUSldMR21OcQpyenhqbjUraGNpVkFwNDBNTEpNWDBjUnZPV1JncGZnaXYxeFQ1K2kxejgwaVk2U3FtYUg4N0NMVnREZlIvME1YCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjl1ZjIyNWwrTWxXZStyV01mT0IKVmlMNytNYjlrekxhT1VTcGtuWk1CWnBrMUFXZHluanFzVzZLRSs5ZzZLTUN1dy9VUS9sRy9NMFlyK2t1TlkxUwpPTmNqakJmWFZERkE3Z1BJem82WTdDa0tBZ3VzVUM5VVltbStMZTVVc3BSRmNoTWtMeFlOdmExNlNiMWtZelN2Cno0cXJZd0ZQbG10K25XbnAyMVdzRUNaNFNKajJidXJYeEJ2bGVCa1hBOWhBZFpjMmxmRW9meHhnbk5CTmczNXoKSDVuUi9mdE9ZT29Xa0NJMkEvNTZyU0t1Z3JjaXQvdS93bE5KdDliRWFCNXBIejNQUmhXR2d6SFMrYWJjYkdlUwpYTWNKeStZYVRzL1Z6WER2dmRMRUFicUNyTm53a1Z0SFVBa3pFTGNoY0N0QWFGYjZiblpUeEJ5V2dDNkt2ZWRCCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0JrZ3E3SHFuTVNEMWN3ZG41UUIKSUNvekRPamp6eTRmRkYrTVlMSVFjeTA1OFc4S1AwSjFkd3VXMmpqdDRaZkhBMS9wUTRmUDhnaElDa2M5cUZScgpYeTNPRlJrbklFUWQwRmNvenBQWmRCaFNWdGJsank2dzM3ejIrZFE4eEMyT0h1WHZOakRZKzgrYXdKOCtMbWl0Ck1KNjlKM3lyRUtad1BkYkd2cmo1enF4RVROb1RlbzdGZElMSm9ucW5FeVltSTVVTFBMSXlHSlVieDZFRGR3MFcKN2pDSEtkeC9qN0FFVU1sdDdtbXdMOXZvT0RtLy9qQkgyYSs2QVZ5QXZvNVZuVVBuajJoOWRKblVqcUppK1VjbQpRZXkxNlZmM0FhWHpiRkp6NjNQQlJNa1NUYzluZWJjdmhYOG5uOHNWNFdJdnNPbzFhTkRxeEZMYW1Lb0IxaW9vCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGVXZjRUT1Y5QThhOUVxbVNXSHgKOU9QSmY0MFEwR2RsS0hJRjU2QnBPSmUvWnIwMDRSZmc3em9PSTFGRHVVQ2NnZHIxcCtVOVpKN2VKSVZlVDZQYgp6V0RaaVdlcENaakZNeG9BRGNoNFRib1ZhVERsMmpzQ1VYTERpZjFrdHU0VVZWM3BsZUFCME1RaEVZeitQRy9tCmxzUmM1Rlg0WnJzQW1jSFo4MS9DN21pdDBqYlRxWnFhQnZSTDZIL09iOVlFWldIaVFCdWtVdWI5Nlk1anViMUwKQXR3aWR4ank0c3pOOUIzK2ozaTN6cUl4VGlJZUxUN3BtbGlhNWt3OGlzZFNaVHdtUEcrbmhhV0gzcENYenQ1egpIa0l5VUVMTXl0Q2dtT3RwQkdEdGxLeitCQlBzdDhDak9tQ2o2Q0VWd1NFZENGR09DVEtyNHF6Z24xN3kxbXNnCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczQ3UGM1UE9zQjllZWNrNEVBUnAKbFlhMytYUGJUOUdWU0NROXVvZ09rczBHSjRWNXhDQWl4Z0tacnRsais1UGpKbjBhTWJKTXJYNFB0VEFPei9nTApnTmZpQ0ZuTEllTElUMGE3a1pFdXdVbGY2eklCaDBJREd2NTNFSXYvZVVpc252NUgwWGxmSE12dEJFK3pMV3E5CjlrZ0RHZTNLQ0lLQ0pmRFV2RzZ2ajExODVybTZDaThRcmVUZnMzN3pMbFgycEtscitwR2hNR2dyc25aSnZiangKcVdIc1l6dmRRZnRHS1R2MmE5QVczSWRmaTBjM2VLcFJuTk5QeEFUQXc2NjNteHZpOVdRZGVRTVdXeGFMd1k0cQpQVjEyOUZleGNHbUMwbnk5U20yeGxKZFU4eGUwaDFBZkx0dzR1RDdQRzZWT2xJamZaMU9NVkVmc204RTZSSnBSCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0QvUHNSUEwvTUhRT3ZuT2NJc3oKRjZDZy9DWXI3WmpNMjRwS3hmK3lJZVZqdmtwVkw2MklQK0pmdVZnTGF3MGIxTzBVL05lQlpJQlp1c09qb3BlaQpDQnJVS3h3aXNmc3g0d3UvZVlJeERDVEFqdGc3NGRib2xqOEFwL0dGdkRKcTljYTdpUEUyaE1SOXpxU3M4emowCkowUjdsSlF5dUtNUFViSDdoUlRuajhDajFXa0ZwcXdzK0dCL0J2eTV3MmdNYXlqNjhqcVI3Nk84YXY0WU5Na2YKTWtmMG9yeWRzZjlvNVk0MTVVUDUxZ2s0bnZrUktpS0Z4UzNxdlNxaWhzVkg2OUdLTW45K2xaMTRWT3dIdnVLdwpLNG05L1BMNklzeVoyL0dDd0lWNXBCUXl5ZlFWeXVVOTBJZHk5MlcrM3M1TzZrQ3BrL1hsekVwaHd1ZURUdjVQCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2YvRlFNYnpwWXIrU1hnNHZYT2cKL3dNckFPNVFQOGdWMXlkalJ3K0Y4ZGFzK2MxbENWNE55ak15QkVKOVo2YUloOUlsT0RadngrMFFSNElVQlR4dwpRTXRXVkpaQW9rNEI0bDdIbWp2MUxneis1cTN6YW9aUHcwVWhhL04zOHBiMW1WNWhvYTgvOEhaZ2UzdDJHZmZ1CjZGZHB5OWlUYzk1cXFhNDBLMEY1OFgrMFVUaEI5U1FBRXRhUy9qZS9JMExCM05SZU9XcjdiaWxMc2RPY3Fvd2YKUVJyd3BWSEhQajZOei9LSkpuUE0rV3hiUWFHL3UrN2VJcXJyNEdWTGNKVXlBZ2UxaThUakNaUkV4V2s5YlRVYQpDazlzRUgwR3JhMU52ZWRrZTh6Y3MyY2tFU2tBNUN3RzJZRTFnVkNORFlVYlN1NHorL3doUEhqMnhQVWd2c2ZrCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUJCN09LcFVhNTB0VXhhVXlhRlgKZFZoMHQ5NXBNUldwOGdGeWJqcEIwZ01vS0xjMDJiRXVta1hGaHZaZE90aWNxTkUzTWtTQ25aSUNpeWNmQmxKTApXOTFlQ2RZMUFHQitScWRxRHlTbEhYY3ZLV3VjRWQrR3l6ZVdLRzY4RHF4VDBSZ01vNWxLbEVYUDJzRGdvZlR3CnVJVEtmQUt3ZmdzeWRUcWFYeTJRNVIwYVFNR0dtUEE2eldCSVRBc3oyV3orVjBGdGpqZHdJOXFXTHRieCsyVTQKc1ZUSzZpTWVaT0I5TU9nMlJwaCtnYTAvaVViN3NkMHhXSXA1MVNqZVBQZjJ1aXdRYjZZaUhPVS9jR1ZFM2pmeApJV240M0k0UjRML3QxZ2Z2VUp0UG80Znd0RzNmdEF6KzZQdFFVTDA3a3h3M24xaEFRUURrNUJQZTZRYjBnVFduCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMThNeFgrbmc0YVpCVkR0TnN2eVkKNEVTdmVLajdkcmdpeWJiYTZacGVVdGFkUjhZbHh5aEVCQnJQVHJiVnhFZThvMVZaRTJpN1g4MXdxR0ROWTgwcQovd2l3c2tyQWtrUkVlc3B2ZjNwZkoyVUhhbm1iQk13cGF1aHRtaEpSM2FDMWgwZVQrSDFTZzVpSG9aNGM3eHMvClVnNVBsbngwZ1A5czlndE90SUFNcFhERXZFNUFNS29XVzg2Wm10Mm05V2ova3RIT0owb2psaXNyRGVsT1BPUGMKazhlajVWaEV1b0wwSTFwUmFCQVBCRmFVM2hCVFdxRkkzUTlham51WjltbWdYaTFBQkV4K1pjVTJ5ZFRBYklnUgo5akRZbHFiMjNrSGg3dFJROUpjYWpHL09xRHcwSndKRlBTTnVQNnIzYTd6bDJiN3c2NnR4K3pFb3JNZXZaZVdnCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1k1RlRLMjZaOUZUVVlwNXRvbUQKd083OXpPN1UxV3ROY1ViNmx0WXlyOTRUTVlITnc3akNYOUVBcVJmZ3N4K1JwUmZCZUZZLzNYNGtzY0JHcnNQbApQNGdWaWg4dDhoSkpxQVZPMXJ1OWhnQ0ZHeUlmY2VpUVhUb1lZUWVXTDhQVVFxdTNiR3ZOTmovNnJNWDRoOGVGCks1cWJoZVpOeGRGekRwc3ZUdTBqWkpzK1orZDhia3RWVWpaZHl1c2dmVHN4VFdEbkpyTXZ1WmJOSmZoSlR2RHcKQ0pvdkNTaXNCMlpMMEhkZ0pPbjZmU2dJZENLbXdCd1A4TnhGTTJieFRMRWJnejlyWjFWbFkvandjWnlPMkJVNQovWnd6Z2VCdGdIbUxOTDExa0dUWFRSbE1sendqUm9ZbnMxYndTL0RMS3hBNE96ay9hVlJEL3JKbFhpdmJCVUZwCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBblNsOUJZY1BDWm5Pa28wR2F6V1oKRlhQdEpDc3BHUGRTSktTOVp3YitUa0FJajdZNmx0OEJoTGZOOVErTHpGMjNZRGdOVnNkN2wxdFgxM09lWWlpcApYdEJYeC93bUpxb2xPcG9GM2JpNk50ay9pWWxraGRocFFRWW9TQzlITlYrSVJCSmVKdFBWQ2V6Q09wWlYvM0swCnc5QWJwdzhaOWYwOVE5Mjl4RUZqTGlSbEkvcTVWMnAxQkc1VC8rS1lhNE9kNzVQMGt3WE5sdWpQMUFMODc4encKbkdVREFCZ29zSmFZRTlOK3F0R0x3Q0U1Vi9NZjdESDlvSXE0dStCUWVnMUVURmEzZEQyb29odXNlR2NPc2k5TAo5S2s0bVkzdmE0Ny9ldkxrbzNGUzBYUitHTlQwdEVNTTNqQlZIOVRLRXZEUVdtRHBXOHZkMlFTZmNRSWJLT0cvCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclRpMmNzQ2xrSUdwMEdiMmRPQUoKZDNYRDB3MGdmVjdrNHN4YnhRdGZGU0lVVmxuK3ZVNVVPeVhPWkpWcnNRcVJXaUR2cjI0RzJqRlBhOFpWNVlRUgp0UW4rUlJjUGhiUWpCZEkrTmExZy9FeFN0Q2xaUXVoaVdPLy91RXZMKzhMUW1sRXQxdHd2WHVqeXN1cE15L2dRCml4UVMrNG9GM2l0aFEwUEdBaCsyMUtXT3ZHZmNEN2xtTUdKekJyMmVwMzhIK1pxak1uMks3N3RhVmk5a0Z1VUUKdWpWeWVkOHVZbjExTEpHNU1ZVG1tR1Z6T0RQU3pmWG9XSHBqR3pJbFoxa2pFNTFMRkdEcmg2UUN0OFd1dWtMZApNWXVINis2L0JIV3duMW5pT29zcnl4TWgyenN4RG1UdEoyVzAvWUxHNGlhVHQwYzhRYWdXdFRMOStxdDBjNVN3CmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNm44QUxaZ2FqUDNvam1pQXdsQ08KY25XbFIwaVUzb3l5OWVVODRib05VcjVtd2FrYTJtbmVKYXBsVk1zTDZPNlorQTY0VHFPcVZwOWpWNncxSDAzMAp4bHRPYmVBd3NWZlRJMkNTUHYxTmN0ekFDRXhVM3pVTFZGTnZNd3U4ZVgzeUV4c3kzNW9ZU1lwbVFVcjUra2VyCjFZeE9ZV0xlU0V5VlEzTlBpKzg3T3g5Mi92a0hOeVNPQ2JBWVZwcEhxd2gvN090c3JFMWV3eDM2REZ2a2lrVjgKNWtyWFk5S1dCeFhnTk1CczNNSEt2bWhrQ0ErZG0vd0d0SWtXcjRZZk1wenFLQ0F1ZFJxY3RsSk12S3JjQjNCdApKb0daVGNmMlZZRFNUSDFka215ZTg3T1dzdlI4UGF0OGRTMVNsQ2prc0FLRHdBNERDTVJ4d29XaXowTEJOTHhHCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFU1eGs0cjdaYnZ4N0JaYUZsdmcKMit0QnF5Z0xBNUdHVmtMYzZvcDc5a1FSNEdlNnVSZjZ6azgvVW9nWUdReVFiSUZhb3lSdlB3MHZuSzgrUHNocgpMcUZLbW9EeldNaXZBaUlERGJ1NmpKYXgzTjU4akhEejlMTzVDOXlQSUtFQTZGYzJab2FzNG9oajlFbDVhQnNyCnVuZXFybi81QkRvcTNrSHg1WGNTNm9kRWJkWTJhdTVRbVp2aWlRd0piV285ZUwzOVp5OHhUY3QzODQ4ZTVCWUEKaTNUYVQyNW1oQmdsdGc2OWZONUlvdVE5MWNabSt3a28wTmY4YXo2a1RlblVQNkVTWFZqR1cvRnlJazBiblVybAppenBBc2pmYUxYTG1mRlNwVnNERHZ3REgxbzRvK0lpTnZzTUJyWEVGa2hEYTVrNnRSNDc2aDZuVWhSeFlxTkRjCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejZyalJKelV6RG0wMVExTzNoWlUKUExTazlORkpKQWdDNVVkS2RmSzdUaWhZS2RDM1AzTmdicWpDaHhWdUxyN243MkxMTllqdmVnblc4SWR5cUpJaQpCQ2I5ejRZTUJDMmpMQTUzdHU0Rzk0S3NqVGRlU1JOUDhkWDlVZ2VZQ0hLejlJZExwK0JJQ2h2OEFMSFU3VUhRCm85UXk5STlzMSs1dTVqSiswQXppNUw3dnZtV2ZQOUhIUlNoSGt0T0pydE84dVpJT3ZITk1nT3BLcUN1WlNVY3MKeWgveHprZjkrYTN1bEVtdVp3VTZIS0pPUXdYdC9KdHd1ZEVYTHMycFUxQS9DbFZGeHZlRk9HN2E1ZisrUUVmeQpnQTlWSHhrVFM1MXRBd05xYVJBRi9mTzc3RlQzM1JuUTFGRlViYlZpK0s1MDRBd3JkNmZEUC9kUUZYYmNWSzArCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeCtnM0JZeEpLY2YwbXRsMU80U2sKb0J6d3lyVVFXZjd3bnBFQmlLTjVZRXhOOXJkMjBkS0lyYmQzU2NoNGdWaCs2UllqTG02N0dYWHh0Y0xiZFZISgo3U1RqSGtpcUJ1Njg3cUlJdjI5OWJ1SnV6MXhGcHd3N1FhWk5Va0M2S3BjU01wNER4WTd0Z3BkM2JRemZUeklDClpQaEliSWhYVjdvN3pVR3g2Mk84ZitPbnBnVThTakNLSVZxRFZiRDA5YnZ4SHNDRXFoRlJScDhCWUlOQmh5ZEYKdjhuSU1jWWFWS09ZaG4vZ3NPM0VHcWJHYTRQejhWMXBrOFk3VnRYKzRXSGQ5bklidEpXWjlSbXNURXNkL2d3ZQpqSjFZbDlqcUJNWUFYK09mWnBsWGpkMmVqSWNoNTFmcFRqWEZjaXZCTjVtdEhkdytHd3FpMkxoTGl1VXcwWk01ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc01UaW9HeUVGUnpEZDFpNWMxVWIKbGdRV0w1aFpkRVR3VlBCV3dtL2pUOW1NR3gyYmh4cHBvTWx0SjZXZHowWmYzZTRHM2F4UXEvNzI2bkNwMnJkUgppVXVtVFIwbVpocmphVlFPWnRtczJyUDVZSFBzNWUrVDc4QlJNTXRuV2VVNC9lSzUwVDRORlVjdEtoY0ZROTdkCjhBTVJHb29Mb1U3S2ZmZk00cG9qVXN1ZzN4bEdIRGU3Mm8yVU5zUUR1QyttbEJZSkQ2KzRJWjl3SWlUWXpteHoKTU9seWhwUmd3MUFPb2pVVFQ4NGdHYkM1d0tISkJnQm5KS2N3M09KejMrcUVwdGNUMTFobFdidGpEUHZuRC9KRApaa2JoUkRzVTlyYUtYV0tFbzhEU0VFZ3VPbnAxRmxYWG5waHErZVFBOERuSHhsaGIvNytWVGR5amF1SHFaZm5FCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk5ITXFrTFVUcm9CcU03WDdkT2cKNFY4Ykc1K1JrcVBibEpYc0xXN1pBdVBINTVUSmEzLzJHZUE5bi9wOVJJbG9KTVkvM3BUUjZHZ0ZkVEdZQjF4OAphQkdmVkJLdC9DYUtCVzhUejRKaUZHMlR2am5VNHR0TFp3UEI2QnZqQ1NQaHFBVUQ3Uzg2OFhMZ1dUaEhFVFJjCnpFUG1Wc2pNTUo3Nm9BOUJNcVZaTHJvWUh6TFk5dnhaZDlwd0tSQzhsSlNxM0dNWmExUDI5WFkvV3NRZ2lteWwKaXFCYnJmNGlGMCtVZnJUd3p6dDBCODdWdjFPYXVVcVBoc09DcmdGYlRZVzhVN3ZkN3RiSEZsZXg3Mkcxbm51Vgo3R0s2MEw4UnRMVVYxUDlidkkyRHF4ZHNheTB3dmVhWEVscVdpdDFFaXJoS09MK3pzV1ZyNVZtcVZKenZnOWhLClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGpjdFpHdjZGT25pdG5aakZ0bUwKMFNJbVlNYWpzN0N1b3RHcUZjL0p4UEluRDI5MnlDWEcyVm5ESUc1b3o0cXNrY3lnbDVkaEhjUnpsR2NuWXlrdQozclVNc1BzNFhOdHp1M3pFNjd6NENMUnpmb3hOayt3eHE3Z2NibEFHeHR1c1k0ZFVEamd4b1dQSUNtV01KV1VpCkpOdEVNRHdJZVZhT09rN3YwcUdWMDEvVHZsWEhlS3NUdDZJMzVmWnZDVHhwa1VBZ0IrMlM4Nkw3b0xMQUQvSjIKcU9mUEtlRW5yeXRTbmcwZE1uZnAzVXNDK2V0QXdWOXQ5ZEFvUzRnNnhLbHFMcndWQ082ck1aemdBay9oZ2Z5YQpJRWVMdEp0cS9NK1ZFcVlWNW1PNU1iMVZRbWlvQ0ROWXU4Z3NCdm9YaGkzMXV4SlA1d2tHSktJYTVVUFBqOTVlCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeExSdXB2c0hKdThyNDFJWGhZZnIKejhoaGt0dXNtNGUzWnZPMTI4QnBUMmR4ZnpjZlVKaDloL0ZSZi9RUzJla25PY3VOaFIzNFhad2NoakxkTzdDZgpLZ2xtU01sd1lGUmlCaGZ0NmRDamh1Z2EreUwzZjFMcWRQZ0J2dUtGV2FEaHZRUGZWN0g3NjYwU0tIdkt1S21mCjcvVnlNeHQ5M0wweXNpVzM5cWprenJkR0xLWGpNUWhpY3FPSzdaTUw3TzhrUVhqeWhvUTE1MEVTMEdaRGpqTjQKek5tcHJTc01zSG82RzU2OHFwOTBBK1pXbEpoTktyZWZic2JPK3ZCTDM0dnpOSU5tWDEwT3ZrT1E5aU1sa1QvbwphSE9JSjAzWWJhQnRXVjNyYTJBRWZybjBZWGdmSzNDY3pXUjcxMnNVbFRQMjNpOVAySjRXWENyVENtUnI3REwyCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWpnQ3RMNWdyK1JNQU5ETUw4V2wKa2ZpclJBK2xYMmx2RWNJZWdCQ1cxQmZmQWk5bjZwNHRaVHZiTXc5aUtYYU8xaVo0L1ZPc1U3ekRYZm1wTDl5agpCcFpCdjR4VXk4V1phcHY2c0RuNWhFdUxhTmhwbGt3WFdubzlablhuZ3lVdm0ycVlnRjY2UUMyRTQ3b0VhZjBWCmZJVGRFdytDaWt4OUlSQm1KOEJBUUY0TVRNeDBHUE9Tb1hJRFNuY3NSUm85Zzdlb1VsWWZ4Vzd0ZTZtdHJFbGoKWE9SUVBTbFdQV2VGVE9aMUNjQ0ppcVFlRWRNVCtzTzY3bGdITCs2TktKSVYrOVErUk9SL3FpYmU0ZDlUR21aSApNY3UyWExpVXpoY0Q1ZEZyWFhXaEhFK3MzNjJHYkhuLys5M0thc0pYMGNrNW8wNU9FMUtIUmY4OWZSNWhrR0JxCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclpJTThJbk1QZmpLYXltRFRvK0oKd1BZMFpZejY3bW10S1RPYWUwNmZsdWhmNDFzSVUxSVNQdGJzSEoyYmM5cFROWWEyQnAzdXJ3Z2pXdlN5eWxNZAozWThlMDBFVVFsRU84bnF0aktRSTloM2M5RG12SWQ0WnRBY0RYRGNobGVpclJibWp4eUJwYzh4ZnJPejc3RldVCjlDeFhobld4SUFPSDhHZ3o0UHE5NjBUSE9QSFJEVjJ3dUhkYTg2eTRzNkhOS0VIbU14NWRJcXdOZEZiQVBqejkKWWpTOWVyMHVsb2FKQjNPSmJiQloyV0hWVFRHdjZDaGwwaGhvakNlclhvUHZFY0YrVHRuYWZGSlAvL2tCNm12UAptNGVkVTNQQ29tN1VhdnZrSGNYd1Q1eG9uL0RidFFxMDRPbnlrS240S3h2bHU1b0IwcXRML3FYVWhHcStuaHptCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUE5c3hZU1JvOExSS0dJV3hlZk4KWjJyZitYcFBBeDJiWENIQ3grNW9tQ04ySkJETDQzdXp3cXNYOWM1b0Z3ZWdyZGNOdHYwNnlGQ0E1dWdYd3NBWApVbzN4cHFiUGhpZC9mVjdpRXdjTjZyV213bVdjOFNXWDZ1TXZmL1JyeEFLV0U5blNvOVp6RlVYaFRDeTJxSWVRCm1rVHJrUGtsQ0VZR2NLbmxVWmptRSttQlQrMXFFd2hnT09nSE5zMVV0YU1HejZBU0RqQ2w2anlOaURSNDJvanMKVEx2Q1BuMjd0ekovRSswdDVlTnQ4alRzVzNsSjZ2bWhwRURCRnZBTlBzdEd6QVFVMFNZRWRaL1RGblpzb3p0SApoTDF4dU5QenpnM2s5KzVvdENVeEt6b2hFKytHdVZ2cGFnckl1a054V2pCekhEMGkrRXdXMFlkMDVYZFNZNlF5Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUNSNE4rcVpaMzZ2b3hpbUhEcVoKRUVBQTIzRk1sRm43bEZCbExmVExGVU8zVFJkT3NlVW9BWkNqMGYvc0h5RDJPOWwxQlF0U2J1SW42R3FINDlJMgpMS0s5MDJTMHhZYWlWaVhQbkNKWlBsY1RlT1dEUTBPWmtGTHJjNmRHTjFIS29uRkU5VWZ4QlVoRHJzM0tLZ0RCClRPYlg0K3VLcG0zNjhhQUo2dlAveTVTeDhJOVNBYTF5am53QWtRQ3Jyd2R6NUwyODVtSUZJbkc5ZGdheEJ1U0sKVmJrNGF1d3pZd2hZUVNoWlViSEYvQzhvVTNwS1BQTWlpYzk5N3IzWlBydVNsVEdJckxOQXJ5TzFGNmlLV3NvNgoxb21PVlN6czVPUS9ObzdzTTh4L3VFQkQvcnNFN2ZoekdYTU85c0VMZ2V3bXZIdTdkaXRyTVRNcGtJNTJVTkd0CmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2k0L2E4Yk9Idlh3RmhUUURHUjkKYTY4L1hoTStZclR5aUgxS2dsTVdMTktuRDg5QjM4bThGR0xvblpIejN3N0lOR05TQTNGa2xUUENoaW1vMGNodQpxZnFqVUxpbU1FWi9PM1NwR3ZSU0ZVdllVSlVwK2FDMldhZGxxZkVITWVkYzl6NFp4WFZYazUzYTlvRWp2cUZTClp3Q0kxamU2K3dUajZ1Y0NtOUt6MnQwRWVKLytOSE95Vzk4Q2J6MnNWRDBURTI4RnVwbDdydW04eXZ1eVRKNDkKYVVtYkNmZHJMY0MwUUdMRzlMajRBTkZ2eVlBL3pVUDY2S2M5WGJQUDNMV0I1QjZCcDZXcXZ5MVZyUWpHcDloUApRMUE3ZG9uOGpXc3lhbXJXU3F2dEFXMWN6Qm91VW5Gd0RBbjMxeHlNcFNQWStNMSt1REp5NEJhc1JHT0NKOW1pClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVhUVmlTMjhreVJKaGw2NkcrbFEKRHJZYnBqMy9WdVFFUkp6WkJxcGhaNFgrWVhIUldYWGM1ajBFZ3EyS2t1SDE3bGJ6WU9CdmFWYjFWOUhjZ2VvWgpMcXgxVlBpYjBVSW5TcUc3QW9hVmsyeWtKZ3R0dHZ2NjRMakxaR3ord1V1aWNWSmU5Mmhmcm9BZ3ZLc3hoRk1SCktERE4yRlpoYXp0ci93NDBIV1hveGJSYnFIbVNuUHByRWtkVjJpY0NLTjNxajFDd2dZcXVkR3VGWkFzUFhTQzUKNmxsdE1qYXNoUHcyTUpJSWVsTU1UQjFEWHBEaHN4bE43NzBscSs3aWFMRkQzMjg2VEFiQklQMjZCRVZPK1oxZQozQ0RnTDlwVXRlK1UwSWZpb29UbS9DUW9ja25SVjlXUm1rK2oxRE9xOGtEbitwL1h5c3Y1Qnl1S1JEYUlEMnljCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUliR3ZZMXpZeFZvREF4djJJZDkKMkN3WXVDdFhnSzBmV1dKaWFFY05YbWtyR0VDVkMwTkYrZTIyeld3WDlnWk1SKzFjQkFKOCtmN2FVVEgzU0hLdgpDVnp0QlZ5RFpBaVhLenNtaXlzWWFOMEg2dFBXZ1NpRTVOUUpNMlRRR0d4OFF1ZUVGejlmc3BzOUZMSnFmZmE4CkZaeGRJdFh0UlZaY0NwTUZ1SU9HaGl1UXNqNUkvWHVsZnNsdkc2cm44bXdITEhZZUlTc3RsZ1lwSW5lZTZLS1EKUDdRT3hXZjdBZnVEY2s5ZU85cXNmSHJ3S0JRbzFiWkZvZFI1VlZjb3M2WFplYms5bmZ1OW1XSXRNdXV1bHRoNQpDZXIxRjhhYlF5R0UzM0hGcC9QdkNLNFZVZXIxOTlxKzgvSm5hUW1yRFhTejVheGoyc1o5Zis5RHhGalB6eis5Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdS9hRDI4YlVDUlJvVjU3c0tzNXIKVWxmbnJUNWpLeGdSQWlCYnpCZktNUjRqNm9Xa082djVTQnUxWVJNcmxxUEM0NTRZQ203Q3IwTStXWjUwWnBQZApQbzhsTFhpWEVMWkZ6UVhZUWxOem5qeWtEOHBqQnBLR2FKWVB3Y3c1UEo3QWxRZHFMQjZxa2MxNk00Z2RqZ081CllJYnAvMjM1VGhoakM5cnMvUVRZUUNyWEhyUDlqVEhBdGg5ZGRXTUtwblBxd0xweGJBMG5yVjdGSmVjeVNLVFgKck5ueW5iTnVCcWM2OHZKUnZjRFpMWjhYSkYyRnR1UDRxdmhNeXFLMGNrVVdNVmtaUEtQbUZ3RnJrNUE4ZWFQQwpDWkxZMmNKeTVOZE1pY3dkOHZNUmJYZjlQMTVwVXpRMmViTWw1QURRaklLWGZoVXdETDVSUThKNVloNkxxYXFJClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekd1cjF4S1VwbDRKM1NIV0xqczQKSk9wQjVGYTQydkRqRmhjNm9LUUVpbmptdDJqOVVkUDhYL0VTYjJUbnZmZitCZndiQXlsWWhqL3Y2ZldTbjJQSwowekd4d1hEZDR4ZzFMVGxBaGFyOEFNT2FSZzY1RXQ4TEdiV01qdDVVR1FLUCszS01mUm1jc1VDdHFQcGpoUkVrCjFqT0RKSlY0VStrQ3czc2xMOHB6bDcwSVN0Qm8zWEFYOVRDV0xzYzlOS1QyVWxxNlFTUVNHbHFGSTZ4azZkV0EKNkdtZmlYLzFCTnJnZ2s4aEdvWEVldjZSMktJYU9yVnRmYmJBQmgzUDcrcnlCVnlBYVlER0dXdDFYWXVBTTNCOQo2eVVZSmo2K0NMSHZxZDc0UFJuVVV1MzdhVW43aEJlazNSM1BaTThnTkZvczFsUWtBWDhDMjhFRWxVYitHWmxSCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWkwOElqb0k1bjdiUHdaRXVKdTIKYTFsVkQ5cXJKUEdwbENFMmgyQ3RTK0xoVk1Bc2h5amU1eDN5L1VJQ3htNHRlbnRrSGY5T2oxMFN3TmpYVkdtMAp6OUNLV1A3SnNadThGcFhOSUtkakdrb0JhYzlJTEZkbk1vZU9EU2pIYkxES3BmZFdDdU1rV2pYeHJhQWlmOGN3CjZEWkh1NHpGUVJMcHM1Q0ora0piakRsRXdOS1lRV0FoQmE5Y1BGekV1ZDhNYmpNL1VzNzE1OHR3d3ZlMW1qdDkKODU3WmRSb0w2OEM5KzRocmduMDFLS0wxUlkxZEdjYWhYaitPYUtwQ085aDVzaDhCT0N1N0YwS2N5M1c0bC84MQpaeWlSeUVNd3dKTXA3NkpCcThPWFNjNVh5di9HRVhSN2xoWE8wYmUzYWh6REljaG9CMS9HT1RBWFI2WkdZamx6Cit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmlqeVVub2drcWlwa1MvZE1QdHUKMDlwQ2k5WUYyVndZMmFYVmkrKzcyYklXeWlDNnZvd2d3MDREdS9wWHV2VlMyMmM2TVdOZGdPS2tSMzVVRVA4QQpVZ1FNdnFPalNtS0t5N3NBRllhQlY3OTdUaFZmNEJBbEtqemFwRGhxNWFjb0tpbEs0QTl4Rm1scTdWYnoyUldzCjg1SnNHZ1g1SWJVQ1BnMHRJL0krVkFQTldOM1p3akdWUXEvNnA3Wmd3L2JDY3lZei9UUnljamt5Qm9CbXhzd0cKM3FlY2VhNkNUY0t6ZjhmdVc2K0Q0KzRjREcrenp5MnN5SjU0NGlrTHJESFBvN0M4TGk4SFZyNUVneVJSc01SbApjZjlOT2pRay9SeU1DTFBJTEp5ZWszTFBnMlh1dmdGeUNQVnNnaU16Mjk0dDZHRVRNZlByTVM3OVo0ek9NWWVOCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzJpNStoYS94aXVUaWhVeG8wVWsKeDEzL1h5akJsYS9kSnBBQTN2ZzNnUWFUVHorbGVvNWVIcnlZRStwemtJZllvc3FCTmtvbC9iMjZURXFyTmFqdQpTM2dqTWpWUVVDR3plVWJsMkZEdkE4eElJNTJYa1R4d3VkNGVtWDA2Q2Z2eThVZTFiWG5RVEw0QkkxTUMyR1JuCnNOOGRKLzVzQWVOeUVQd3ZVOUxScENSM2E1WjZxRUdPZ0p0bnNxRWJ1bUpQNkwraUFRSFpuSXhtd003V0llRlAKeDFCa204Y2dEcHAzZStGQ29jUlpDcTRJQzhrK3ArQllrQUNYbkk2UmZMMUw1MHk1L29ITTRldGkyYmlpK2IxWQo1OGt0d0tGUjZTUVI4NzgzdHdmWXFXN2FtaEU2ckF3WVhDNmxXdHhqSmJaMTk1Qy9Ba1JRNHIyNnd0T0dubnpSCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUR5UFZQVkVEYkJhNUFTWVVEN0UKOHNYS1VXelRkR1dDcEExVmQxNS9rRXVBMTllSXQrTzAyYmlkcUFHa3BrMExRVEhkYlJhNVEzcmc5TlFkWnR3UAo4cEhqa25FZ3NqRXNITURlWTBHaDdyaUtPNWJNTkhXUVYvNmNHWENPOXRRR1pkTXpvMUN5Z1YyME5TeldnUzd1Cm1iMVVPSTMvclJxRytnNFNaUUdvSit1QldJM01IZEFBWVZBVDRVNk91NzhvbVhyM0t6QXlNVmhzNHFySXlKNkEKeHZqRFMwUFVUTUg0OFR3dGR2ZmxKMFJLODRWNUpvdlAvVEpXMTlDRmJKeGovRmRjWml4bXcrNHJzRC9acG1pUgp2eThQTG5BS0daRVJSTUlEenZZYm1uRXpNNFdUYllmcXRKTFM2c3ZWTXFRT3dzQzVXM3lVODJsWlBGWThvR2tPCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNnNENEp0aWg2dnBOekdlbW1FNXgKY2Y3UUJ4cW5HZk9LQnRJZld6QTN5ZlZFOVcyV0NZSWttcExaQ3dZQkV1MmRFUS9wbW93bEdRZExyNy9OQVpJZQpFUmFzbjJ5OW0wZjcvbVI4bnQxbjJ5TXIvK3RseDNoNmZ3WWIrV3U4M2MwL2ZOcmw0cnFEdVZIV3JJZm5IaFg5ClJtajlnOE5DMkM2YnRteW1HaGt2czBzTy9tL2M3Y1QrR3RWdUFySW9pTGNlSGpKUkpPWW50NWN3V1lzY3VMRTMKOUtDUTJUbUV4dWtsdkluWVB0Yy9XRkFqYi9FcDY4M3hib244UjlHTjM5dmd2aC9wMmMyaHQyVXVCSlV1WjRpOApraEFGRDRIVlQwM2x3N3lud01JTTM3Q1dMc3M2clZBRTUrdmF3S1I3eksvbkptWjVwZ3U1UGtuL0tvRWQ4TS9TCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNnorRXRjTFlwRzB5Z0ZmK3M2UUsKdGY0MmpOUG5vSG5rTWhsVDV0TzAxa2tPb282NkE3WE9ZWXV1NjBwOE85UWViOWdZMERTT1UwcnFNeDRieTZmMgpzek8rdWNwRDUvc1VaK0Q0cUhOL2FZZVJOdmFvZ3NlYmpFQlZaUy8yRXpaUTIzV0FTVjBwQk5jNHVpb1VRK0NOCkkyeW9NTzE0SVdyREhrN0J6aVJ3NTdBb0M4eDlZamEraHlDL29ldFJrWTlXNWlCT1Q5Vll4QnlNQ3RMRVZvaFYKK1RsZnNYNkZXQkkrUVZWRFJFTWRHZk9ubTJ0Z1RLTHozaGVSMWp2K0toNTFMK2NJZDd6b2VtR1RodFpEWW0yMQpBeEY1aE9IaGNYdTFLYkU0NVcrUll4UTV6OTlYdExhUUZ3V1JJWVlyL1JQcG5kdWpaUlhBSVVCSU5oRlRKcTdaCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3ZSRlFVTjNUQXhKTFlxMG9uQWsKalpOZlVuMTBuYzgraTNkQzB1U3ZNQ09pSWhlQW5IYjBLcWl1QlVoaU9mdzhlbEV1QSt3dVNGb1kxR2xBNVBZQQpxNE00QzVRQkk0ZmdEQjdnL1c5QW02VnQvWnhmZ1FiOGdLUkJNWHBvaXZ0RzlNczR6WTJwVVpUWDFYczJZY2FpCmg3OGlxajdMak41aXF0T0JoM1J4ZnpiMGYwU2R1bW9uY3pOMkZWaHExdTFnRzFEcE1qZGFMTG5lc01aV29KQ2wKV2doNm5MVUVvd01YQ2NYd0ZjR2xzdU5JaE00eE4xV3Z5Q0FVRGNEaWlOYzJaUDEzZ3o4VkZzQnNHRTl5eXovOQpGekdGQzVhc2xGWGdtdXA0QUNXQVUxeXZWVmZiWDRPdHAraGxBR3VCazhCNkFGU2VZZnd0a0tzR2VWcWZJN2wvCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkQrcGFFK2VaS1phSGlOdEtLQU4KNmtxTUU5M2lEUk1WcGoyVWdBaGw5VCt1Z3JMbzJsZHgzMXRRTVM4TldHR2hkdTBjMjJlLzJQNC8xZ2w4TzU1VgpnbFN2NnpPYlM4Z0lqaEVyTmY5RnVPaC9yR01jeWQyZUVBVmVXNU1lYnZXdDk5Uy9RR2daL0Z3RzhyekN0dkFjCndrOG5KUTNkM1BTOWdkclBUWGlnY2NEWnNDL0FTTjVyTXl0SC9CaHowUmNIdUZOa3hyZFM0SndDNTVOWlE1bVoKUFBaaUFUM2Q4ZCtaTDIwcmRtV1ovajVnRVUzNUJFWk1jdFF4SWg4NmlGckF5U2VWSjRkWGsyTisxOVQrK05mOQp5cjBhUTdGQXlIeGY4NTZOWFBja2hrU1I4RzUwUFNQWm43cGUrNFpwczZYTjNCZFlubDRjSHk3cWZYWUxMRnNtCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHd0TzNmVDk2cHRXTkFOOHUvMUQKZ1BhU0FmOG56T0s0S2dpd01odXk0WUNseU9ETHhQRGMzaTQ1N1VrdEFTdVhObzIxSUJ5ZCtneUdkNU5XTUcwYgp0dStOM2d3NlhxNitXbFRaV3JxSjEvalE0K083U05zbnhpTXBsckhTQXM0OUUvY2JEWkRLZnI2VHNDRkNYREFQClZETmcxeUd4VFRtSlFOcWxjcEVQWGxjUkl1YTdueVR6UmJ0cFV4bmEzSURjMVFHUHVDUSs2c2VBNnM1MzE3NkQKM2dJcVlPQnYxdkk5b1pPTVVOcHJ2RGZqN0ZwaFZYU0hGcmg1UmlpOTBYalhjR1ZPY1dhMzJLRWxYL0dqRTY5eQp0MDhUa3I4VDZELzV0UlljTlA0NEp5QjBjMFZETzI3cmNTS3A1R3l4b1JvQTFNV3ZFaVdOY0tpU1JZNU1lU25WCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemtWUUhVVkRCVjRFU0xCWlVCYm4KYUFxdThpUDdYcHhUaFV6ZDZXcHRqbnJvQ293RkhCK3UxdkZBTnJ2b0kwSU9LNC9hNDJ2M3o1SUM0czBlVlVkUwpMdVpQUUNHMHZJZUxIVkkxS0J2R2p1cWhaVXJyUUtCQVFmK0JpeFNyWlJEdWc0RDc0TUJEcWl4blc0V2FQYzYxCjJqTGNFM0VzM0hIMXV6THdVWDdHbTZFUlVCazY3amtSVi8zY1ErQ096Mk1ScGxST0J6RTZZNSt3ZDNsK3BleFIKNXBPejl3eU9zU1N2MTdSalNQT0lHRkREb2dwVUVjajhtL0JkcG1XVXF2d0Z4TFF4OXA1THRzMnBSZHRxNXJaUgpwQTNkSW4zTk5PSDlML2RQbUQ0ajduNVB4UHJaSkw0ekhqNkU2ZVBCQVc0N1AwcXpqa0NwNUJMV2hvMHl3S3I0CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3BvTVo0SFZyRlZQSE5HSVA5dnEKdXlaS3czZitkSm9FU01mVHpBQ3BUdlNlQzEyQ1Z4OEFpaVlqc09xQ3dWWWN0cHhJaGNTWWphbmh6Wm41Tk5PbgozTWt3SzNscGd4TmZOYmxWYS80Q3RDa05SbTBIRTMra3RTZ0ZqSkFoUmN1WVcyL1lmQUhKUnM0aTMzVEhNdlAzCmdhZzViZElaV3c4SFpuSDhQWnh1eFEwbVdjSnhNWGlCM0VhL0dKcFcyU3VTdEJwaHRKQXEvNTJLVzhiVTRLQ2IKU1E0Si80WlVxdHpZSDl2dlcxRC91WHNmVDBjUldWZlovWXdveFRjclVqR01oZ0tOenp3STlIaVcxZnc1MmJPOApqMXY0RUtmT05FUlZBNWFaMEFQdW1xbzNxZHhrc1hSSHpEWDBFS0k3enB0dHhrWmhPNHRyd0tidHlFRTl1bWR2Cmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXp4NHlzeVJoV0UzVzIzbmhFQzUKeUluYTExeklGMGMwYTZ2WjdiL0REdmpsYWJHZEhaUXE4UEx1eDBqR3Nza3hOcjRMNEdZQkRyTTlwMFh4eGE2WApNcXZxQS92WWk5RDZnNFJLaGF0U2VKb2NSMWQrSE9LK0U2VWdKYlU0RCs5VmY5TytZb1J2czBuNktMZ2dmTEFPCkoyUU94SWR4dVlmQlFOUHRndWE5LzlBNWtqaFZPM2gwT1JtWVZ1eUNVZG9BNUFHNVdnbnZzQyt4ZWducGZzQ1YKR2tqVDFOTldDMkpGbG9ac1doTGlaWnExMnlHTE10bWN1US85cUhER3pvTVpZT1lrU2xCaEpYUUU4VjZueHdLNwpmWGhxZ2JZL0tLRk96ODd1QVgzVzFwNER0YVdoOGpIT0tHMk52d3BlQlRDZDFpK0VnYTkyODFvRzUydHZvVUt1Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnI0K3NZTnJnQlB1MEc4WktPK3YKUk00eDJRVEx1akVja1JOTkVNeEowWTNZOUh1MlRTbVRWR0hkOHp1bXVWR2xyRkt5eXhWLzRITElDL0NoVlFVZgoxYmtTWGwwVmRiTDM5eXdXWkplVnp6cm1BNFpIb2tSSFJGUFFIdm9HWXhBK1c1SWRWMk55YTlkSUs1ekdwRk1sCmluWDdvRTh5NmV5VGY2NTJsbW1rWGtobmdyamF3ak5weVBPTnVWWmhjS3JoZWpsYWFTVmwzOUMzdFIxTDZGcWcKeVFoajc5NFlqSHdmcVRBNXFEL2ZJLzN4Tk1lY3Y1MndLTTduOTNNT2grd2NiT1JzYmJKU2ZZVEhvL2RJWlZaMgoyaDF0Q3BVWUJ6TUV2MHZEZE1LajdDbXFEejBtdXdJbHFhTG9ZOVFtTmV4T0pFY3c3eUZJYjFPWGlZM0Z5S1Z6Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGoxYnYrTnBzdDU5WmpmT1B6SFQKRkdSem10UUE5a2grTnl3S2JDV1VKQ3dPVXp6S0ZMWXlCVm1SSXZFRmNyaGdrNm9YWm9TdXlTNU1GcU1kZFgwRgpqaFVxU3pldmFqZS9ZNUYvWEQzWVI2OXlCa1UzczU0Z0NDVkdERG9KQXZ4dWhmR1pISXd0amh2Z0U2VGVCcXlZClNoaEhvckRuWVlnVW1VeWo1bXF5cnJab1EvdTdYaEplL2w5UUVlOElLTDN2d3lMNzJnbGxkd1Y1T3ZGQXMrcUMKeU1LZWNYb0hHR2JtRjYzdXVPN1h4c2NkMkVCOVd2dmRnWk41emdyN2dFYzFwdGpDeUg4M0RCdlRkQXhISVArTQptbXNGN1RwY0ZVUzFTWk4xZGQyRFhJRDJLbEZ0ZkpFQTdRbklJWmt2N3pWMUx0RkhkTTRFSjZ2c3ovNTc2WlBDCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXd3UVdZWGJQUFNMMTJ5TnhiVXgKYmNhWHpLdzVOS05idlNYREROYkU1aXcwWk9Ra2N4WW1ZZURubythSjBGS2oyRGVHZ2hvU2NacDdEcDd5b1FCcwpHTVQ4bFlWZ3NsK3NQQjZqaHoxT1RiVjFXTDgzS3AweHE4VzJ5aTJDV3NmR0I4ZWU2SkxEMlpqNkt0SXBVNU8vCmQ5a1g3ektPZmF5ZllBckFxNE1DSm1tcHZQeUlnWVUzVVZjajA3eG5zeDZzUUVkV2NBcHNHUUlIZEtPSHpWVVcKRDdFNFpxNGE1cjUxY2JDZjdaYnhsTjBYY0VoZFRVaExrUVB4bHp1VWtZRERrbDdnbVVsaDc1SWxrUzRhR3FEZApSVDJWbkhBM21jQ3lKZXcrSHJ2T20ybWxkaDBISUMxS3ZqTmxjbkdSMjJZRmlpS2tBdWUrbG1EUUZmaE9GV0VwCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWJmNjNCYXRPanNzR3BXeWk4bWMKY1NYU0ZKd1dYQ0tnQ0dIR1cyM3U3dGgvU2ZsNmxBalVXUEVHSkxFbzhTdTBVY29hZ1o4TEJHRmVmK3FNZVFaQQpDTFl0VGZ1RXM2eTB4YUxYd2ZjcnNldjhERi9KeGtTMU9EdHFwUVVCcTBSNG54TmNDMHZZRHdLbjd0amZ1Vkd3CkFSV0RNSXNRbU4zcVZNbUEwV3ZhTy8xQ21hK01qN1lySHVDVnpZaWlEdTZncXVWUlVjeFhPRDhlSkZYQmVqd0cKL0JEL2QyQStkdGlNSHpkRUd4Z3lxSi9WY0hSNlBhbXVEdTk3RW5BellxdnFxdkNvSUFHa1duOExjZi9QVGxKVAoya3pnVHJLNEVIZTM3L1JIeTZ5VU9XOFVNZkxleWpsS3BJWEI1eFd3d3pLS1A0OEwzSzdyQ3RxQUxWcFJXQXVVCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmlJV2U5aDlpSlFnY0NhTjJnN2wKM2NOU212c01jMEdKaTJOT2xCVVN5eFZmN1BnSUtQZnNvZ0hlVVRzL2haN1M5SXhYVEk4cDdXQWczTVAyU2laSApiS3h6OTNhdDVQUm9NTW5JWm9TdnJVM0FUaVpmM2tXTGh6SWdLWGVJTnZPZzN6ZnM1azlqRHF0SWllSjhSMmxOCmV4cEhzWitTQm91RFZ3cXExdlM1TFFCdTBPUFlQODl2K2Y2VFozVXdHRjhsczJESTh0VVRFT1F4TjFGRTBIbDkKNXhpb1JHSU54cHh4VmIydDZYOWozekNvSzh4SVNwVThrcTVxa0VoaHA5Q2lsVXlLQVBLSW0vVmpKQVMzbEg4bwo4blk0UmhlU0RlVTFNVWpub2lFbGhMKzlmQVhhWTlRdldVZ09STENKSVZiT1Q5VTZKZVd6VE1wMWlSc3lHSDFRCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFdTL2ZSRmFSSXpIUXVGbnRyMWUKUmRGbFYrVG1mZUVuaXpnMTFhMnBPekNkakJlT2N1bHNRcTJ3bmozRm82dmxiTThoeEdSQk95MVJ1b2k3V1JkbQpuMUxtQVpvUzhUUEtVUjdmYUQxOG9rSFVXZlY3bkVoVzBJcHlUQUs2RkdMVVFFTmJUaEFBOVpmbG55ZENPZldHCllwUXgvUVgvZDJIKytOMC8rSVlFaDlrQW1iRlBpc29SZ0FUdkxsSk9JNHpZUjBWMjVYSngrbzh0NldzYks0ODQKOVNTeHNCc0pUTjBLTGUvbnNXSXV5Um5DWFpWY1JrdjBwYnFBT0taR1J2Um03WS9TMENoZUs2WTlXVnhJbStNUQpaY2J6Z3AwUWdrUDVYTkRuV1lPR2lXQjM3dWlvU0lNQ3BqZldSTXV3bGZ6bUZWZUZXUGQ1aGlGRnpMY2dycU1RCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2QyU3RtWjROOS80QzhPNzZTMEMKekJCZVN3blZMK3YyRURsOU05TUZTUGxWSVBLWVcxT2JOVndxTUZtZVBGSEY4bjhieE5STENOMndCbWhMQ3RTNQo1R3JsSHNKRXE1Z3dNL3ZxSkZtRDJwRm1vKzhvdzQzM1RSaUh4eEU0MW5Kd095QStsZHV5VlRWSjhvR2ZVUHhHClowL0xqVlZCc2NPQ3ZGNTY1dXl0ZlJlOFlVOEtiQldLY0pydnhNT09qU3hEUjBRZllxWFVOL0lsWXYyS1F3SjQKWVpleXpXU0RzdU5SUHBVcUNxRGNkLzArQWc0cUpoRlFJelRGS2tOYzlJZzVUYmovSDUvaDlReWdiUXU3bnc5SApMc2M1eEZ1N21aY1NsTklucGFpNzBvM0d3aWJhUGpHYitScVJma2djNVNkZmJhSFdUUkJGRFFQV1J0TDhuQkI0CkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFFDRXpmYlJzQWlXQmRYUmlrNkEKckJWWlVrVS9iRjFpZGR5UHpjT2JJd1l4eDlLVVA5V3FSL1VNL0g4Q3dMVXM5WmNOQ3daNUVsUjU5em5WQk1xbQo0YjhVZ1JmYWZmQ2FwU2JWejNxUkRhOEhvWHBHWHZQeHFDazVLQXUwSCtLZ0l2cVBTUkZWNkUwNWhXYkR3dEJVCktqNVM5U2tjT2s4RkZNUjNBK0s2d1ZOMHFmTkhOSjM0bHFuaWMyeU1qTVdMN3Q2a0s0VGJEbjhjdlNxdCt1QVIKWUoxSW9Ud1dXRTVsRVRjYXNhaGpIV0RUbDFuSnEwVzc3Wml4NmR1Q3U4eHhVUVdYeGtkMzJVa0ZxSmZQOUZkegpQL1Y3aER0RlNid2o1K0Nkb1ZVMExrSHQrSW1rVXlhelZaS3JKVnljYmNpd052a04vM29wMnBjakNQU3BNaEUwCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUV0UkdCN3RBdVg1cG9JUTcvajAKZUpKR00vblNSWlpjdzIrUlpqTWVwRWkwQVlEeDV1YUJSS1Vnb2oweGxnU1dnclZVbHZDMzFjeTA5Ykl4ZHFCeQp6S1NxNjYvckdWeFBIcTFvN29lOStsNjRUdXJKRExDTWdOdGFQUkpmNlhKVUhCRFRUeE5WL2RGTi9IU0dIbVVrClR6NzlBNnhvc0drM3ZVRVN5RnpHZXRhaFUrL3RnWTAzYUVvTnNEVEl2ZGF6WldLUzhpczNlbUpaQTE3U3FLc3oKbFU2U0Rqa3hNYjZTVlpHb21XbW50VFVYNmZ1cXk2dlB5c2MyMzhrcG1udm5FRTkzaWorWEFMVzhXY2tvY09MdwphQS9HK1RKeW9mSTh6US9xMmF5em82M0FlOUdFOUc5MnVjTThSTTVhOE5GMFM3R21MeVZveTVsV3JMcWhSWUxuCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHUzTmhnZERrNXNlMExHUUxLekoKOGoxbnJYNUNyUDRwdjdnN1F6T1hXTzNZNDl5WDB1aUFqcnJaR29LcW0rTTRocXFoTUo3Z2EvUWEvRXRCTVFyTApmdmR1aTVjWUYwdURLV3h1Q3dNYTNZRmhIamhHam8xRHF3ak9vUkE5RFZEWEJKSmY4dzZCdXZFTkZLMWRtRTlGCk5RMDlObEtaYlVldEQvSnB5NmhRV25TMVVrZ2hXZjVoV3IwY29aNEowYk43SVNlWmVaeGZjUHVNU3BPOGI0TUIKMVlHdE1maEF0RjJldWd4Z0lRUnd1d1l6YnhFNHJLMlhiSFhDdkZiMitSdFZTWFI0MThaUWZhM2YzZC9jcnYxbwpnTzJvRW9oZ21lYzhCelhtejZndFI4K05QWGsrSUZ5ZktXYW1VZU82eW1waFV4aG9JQ0VrclJzekpBbGxjYkY4CjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOW5YZHdNSDlHM1d3ZWlhMlVTSHMKNmxJbXVyZVR2R1BRTEpXUmVheXJMS2dMSWVRN0ROVlQ4TXNsbE1DOWFsekZWakRnbnlYMlN2R2crWGk3NDc5SApBTFdIRklHUVE1VnBjZGZOeEZHMEtmTE1oTG8rbkpiZW9vT2laajlZWDcvWThPN3E1WFNVQk5pbCtXbjV1NWE0Cm1LV29HZ3BhekpGVGR0T3JPKzZoYjFtVkd1THd4akJlOExEbkZoMHF3ZldIOWd4Mm02U1QyZ1RhZnpyK2ZQT00KU3JxUUNJVTQ4NVJKV2xkU0tDd1lodS9YVks0OGdqN3poNmpXSVEwQUJQN291OEdDSHlzM0ZXbjJlQVF0aktFbwpDNzBCdURQSGJMVmZuRzdwdkROdzcyRW90MHNzWUpWbm1haXN2N3RST3FCRUlVZzUySm5ZendVcUI5bWJmTzN2Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFVwV2IwWjNTcHVEV3VlUGpXSEMKaThkbCt2WXpjTGl0QlFGQWd4dThRUnFNTE5XR2Y1aVRwNkFLZTA5L0ZoaUVuTm52R3l2djd2elpZa2l3N3dCYwphQkJ2YkN0VVA3Y3J6cXBjeTdvTW0xckFlN2NzT0JST1pCYzJXdjgvcEdSUU9iVldHUmdWY1JCeG44cjRoRTBpClZXL25tL3VseU5vZmdsVW1GS2FKenlDUmdzdWpMYlVPeDRRYnFnd3VDOTdBWVErVkN4cEtLTjdlV1pLa3dlQTUKbFlBRTBYNWhrRHpBNmFVNzBtT2Zacy9NOG1EbHhrSmpIUXpPK1EvMTUyV0Q1S0svMUFobVdRd25WeGNSb0c1RQpPa3IvN2pLRXBZeUxSVjF5NXpRM01pSlcweXVjZm1naS9TNjU0OExBZEF6QVp2RytmSlJBUHJOVWhBcDdVUVhYClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG9ZWjBKRG9CUVZjRTNHYVd1RFUKTjV6ZWRzUWdyb1YwcFd6Wm9JL0VBcWY1S0k5N1hCV2RpdlhZb3JhOVhSc25aWXc3ZVB3Y1dMRUREOVppTDJyTgpQVGRMNElCQjQ2MkpTRTZvZDJHWUg3TTlHampkZ1VDQ3Q0cDhYR25NdFZQdzd5TVFGdDIvSVdwZDBwTUZ3UnVuCm80akhqeXdqd1pmMHJqcFVlZHVncVVXYlN0Y2xmNHRDRkxNWWNZTkRLK1g3ZHU1U0dvQk9SMUdJeGlBN1BwTHoKZjlEMjN4TTc0MkhNR1Q3MDJaU1ZHZ2dsaGM5Z2UvTUN6TVN2d0FaU3BNMloyZ0VVQXlnSUJsM0lTdTh1KzUrYwpMcmhnVGFtVCtuaUNhRUllQi9MWkl3cDVGaHZ0dngvdWVpRkhoQTBmckxHNUtwSmpiblFmNG00dnNRcHBXMVVLCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOG9jK1hCR2Y1MGYxZnBaMThkSG4KOFpFa29MQXF4OXhVQTNwMkRBaCtHbFR5cmVlYjExQkM4SW8rZGtkREVpTEJVcVgwdU5iZXdIUzJzNzVoQXJ2agpnUUVXbndhZ3VPaC95NW91MUFxVGtDVWVKc2hxczcrbDRYanhCcnpnZ3dVYnlEcVhYbDB3NUpmYlVORzRzdW53CmxBMjVFUzBOMVg2Q0xwZHh5aWtmeGg4M1BzTytSdUg5aXBiRVJWZk5CakNkeWtFVytlSGEwQkJINVIya01yVHQKY1lRUnVRYkJkZVFOeHNEODlZMU5JT1BJTUtCVFNyK29zTktwenJJb0s3bG10djkyRjRMdnZ0S256K0JtWVAvRApaWnFWdnAyUEJxa3hEZHNTV1hoYS9TM0EyK3ZHVmlxdFZ6K2NzVVNtQWpIUW1vUnY5K0lkRmI5UnBIckx3bUtGClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTNIN05NVENJejFsMDlYUEU2WGgKRE1BZ3Zud3BZZDg1bEN4V0lnT2h0THlLSzU5QmxhYWtTaTRzL0lGeUlPcmpYWEpZcUp3K1pKNWpCYXljNTJDbQpHQko0M0MwRCtNYzBkSnBham1CTkYzcnYwWUdFSWhzTWJGaGRpWEFjUllVYWd0YWQvNEJHT1hsSlhYZXJESjNyCm01a1Zja0lPdi83MDJTc3VOYXBLb0tZTUlUUS81L2VLeWtNZDM4Tllld2NkSCtDdDlNSGFSejR4Tk5IdzlRNkEKTG95WEwzZTRTODdvSUo4ZXk4cGRQbzRxcjdMaU9hd2Y3c0tZcldPRnJkMzlIcjdpOXk4L0E5dityY0c1ZERTWgp0TTh3VUhlT0VpODdGeis3cC9OYUZPYllDYy9zaDYvbzBmY3poTnpQRFpxMElDc3NKb084WFNvR2hTU2dhR1dkClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWZlZVBReVVySktMM2VCK1V5RGIKOUpDelE4Nk1FdGZXZFhXMXV1YXJMK3pWMENmaldBeDNOb1BHYURIcFlRZGcvRzdYVjBwaGhIVkVOdmtLTnpleQptTDRCRHlnL1lkVzVGbmtCOXF0ZTJodVNCWHkzQVBMZ05vbGR0aGVjcldEOFkvYXo5Q3F1OE1iMll6aDFpWjI1Cjh3V2lXVFhaK2NPR1BRYWc5ZEhNZEMzdEkwT05ocjB1ZVhoaVVCeUZBKzVneW1ZeTVaaDd0T2dXdUdhNzRwVmQKaGN5V3FiRXVSNmc3a3c2WmMvWStBV0ZGWHQzQnZWa0RiemlWak8xb1lDV2RqQXR6MmhqcnNnRG94V2U2M0NRcApCTWszczNYT21wZUwxR21mc0ZRaEJzOEVMVnRSV200cnRXMlJ6c0FzNktwSXVLZVQ5S1MrQzhWQ2lJalc5cEdiCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVdqU3VQMTVnbGd1TXcvTVFpV0QKWkpLakNjNHZRSFF3cTBjcWM2Q0lweFJDSnBUMElpcHBWOGdYeTJ2bkpYcERCRjVTM0RGdFBDcS94cmd5TWJUegpqd0xyczVSQTN3VkVNWG45ZWUyaE1WSkxKNkNCUnBJWXRLaWZyaTVmTkV2QkZFbWRkSTFOMzJEVFRKVlFjVFJxCnJqQ2JCYjF4Uy8wMCszQkdKZEhzVERncEo5d1JhS1c3Qk9MVEx5dGlwblBZQ0s3VzhrZXV2bXUybmdzeU5MY0wKREVybWttOU0yUmhYQ0pxRXg5TEsyTUNtUVBvRThOUHFJaE9xNVBSTEZlTVAzeVhJWHJjbk5pTTBqSkpEZWVHNApMdEwwbTRkTHV3UVhRZmoraUVwd2R3UFhjYUk2dm5pb0RBcDBsdFFvYWEyUWtuUUlDcjY3b0g5TjVRZ2dhcTBYCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBck1mbTZKRE95N1BHSXArTFVvSFUKeXNqakpZeUtnWURFalplYzJhWUFTMzJRS2k3d2JiTWxXb0xsaEpLd2kwckl5WmlDZUtPN3N4N09oQ2tWT0h1WgpOWTAycEdIb3hvbXlvT0FjUTA4QStJVEdXUXU0Zm9ZdUk2dXNrMC9IN0ZLK0pvZkxodFozSGhVeHcxRXFIeWIvCmJvdGNmaUNFSExxMkVLc2JuZzRJVXNraWFOQUdFOXdQSzVIdmhKRlRXYWRoOVA2czdmbEQ1REJ0WjVMeWR3YTgKRmwyRnROZGJGMHpKVTFMSzRDVzQyY0NnUUoyakppK1BOc3o5aFNNZ3U2QzU1Q1VneDcyMTcyR3gzRTBlK2lRUgpTaXEvVnE3MjBFbndqc29HNjJIdk9YVjU3OGNKN1E2RlJLa2Y3YmQvT0ZJR2R4NjRWT1oxaDlmMm1iSUhTbzRHCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmkwa0NrelVNbVFucjlvL0NDMmMKM3Nmckdla1NYNGRrekQyOE5WTGo4YldsSis5KzFZU0pqRjZlK2VUdkRHbEN1Z0xMZy9YMzg5RFk0TmdaamRvWgpFOXd4NnhmVFBMdDdXeWp0b3pBdkdoVGVVT1pBZzdEeEx3SGsxT2d3ZDY1MmxhT3VVQ2pISHZ2RUt0eHRPcGpqCmhUbTV6Rm1WWU5DWnFOU2UwVjBLNW9SNzlmbmFlN2h3dmJFT1JoNkY4aVpMMUYzQ3k0SjhBK3VSVkpkQjFKYloKamlEZ1hrYzFSbjJMc2ZqRW9jRng0TDcwdnRaYTM0VGU3THpGekVXeC9vSnY3YmU5Vk5kWlJVZldxbTI0NWVCcgp5Vm5iVG9DRjYrL1FHZG9aS0puSk9TSDVOZlhYTUpzbFgrRWdDbFFZVE9nNWxCdXBzRFpIUUtaMHIwaG1VWENnCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBby81Y3FyYllDeUQ4L3ptcHNKRjYKM3ZFbmFNMFM4UktkZ0lERm9lVm1VSWh2VEhDdlI3L1JOOWQ5cVZWNlJDelRJWk9Lb0d0eUdWNG5oOTArbDNaKwpCcFNXZStFL29SN1lqUlh6ME9ZWWlJL1o2K0RkRlBvVTNHV0ZSeGFIcXNXTkh4cUNZZzNkL1RzVHhGR2F4M2YyCjlmY3IzRENkcU1qdFp5TGI1UW9zNUFUazZTc1R5VXNBM004cjJ5QnliUlk4dmRiT1c0dnpFanVPNThnYkdyTjAKK0dieWY1Sm9tV3RqSld5RFpJMmRnb0FBYm1mREl0d096WThGVCtmMUZHaG1WSmVvbnZwTVhxMDZFTVR4TEtWVQpqWFNPenZQb3ZUdFpvWjNTOXhFUVRuV1dMVThXUUJiZ2t6TmF4SHdmcy9NQUV0ZzlmcVZpMm5kQmJlemJqZDcxCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1AvRzVKTFNncHdwT0prL3pwVmsKRDNHNnBTUmhkRG95ZEtFek5nODYzZWVwOUNUcFhnRStLb294NU1GWUtmWEhCRm9hNzlST2M5S3FsOEE1elVjcgoxY1FuQU5idTZ6N0NwL2xxVTRpU1VzYURPS2xVOVorK01XbnJ1TTVTSWJ2aWJYWmhZanlpY3BrQTR4S1ptYlM0CnNIaFJ2WXdkajVLbHpwS0szNjVzakduS2d3OTErbkVUKysvVU9ZeCtXa21zRmd3NU80NXFyNXhnZ0xZdFpWRk4KTE9FdUxIVGRFd3NsdXRoWHZQbWpwSVIvLy9IVlBnR24rNllJT0tub01zOEgwZUZ6RStZUW1wamlWUFM3L1pjUApYRHpML1dVcXlYY0FlTU9IUm9MbVl2djNpRW9MNmJTeEI5YkFxVmpaNEtVaDJBS21JZHZ6eGZsMmJtK01GUU12Ck53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVpPYVRQTkdJNVFtZUFEVUFwbFIKMjFQaDBIUjB0aHU0cGFQVEY0TVBWZlpQelVZd2VFblBDZUZ3UFpuZGlDV3VoaldCRERRY3VXV3BHZ1JKSVk4bQpndExURG1jNFhDWVZGRWtScXdIdG5iQWVES2p0L05SVXpvK0hsKytmZ2JNSFpvV0NOemlna2dCZjBHektoT2UxCk9HS0lhSTc3Nk5ZS2M0Zm5UUzlCMmlGUWd5WHlFRDc0cndwQ2FSSjBXRndOSlZWWFhKcUhjMWxtaC9rVVFRZVYKY0hnL0hqQmhZWDZkNWl2ZktMWTR0Zk11V2luM29GYzFiRnRqRFhqZjJjWTJrNUpJOUtIM2xVVHl4ZXhUR2RhSgpqTXpiVWtCZWo3c1U1enIzN2d2YTVOSWJpMlh4a3pJK0JyN3kybGlxMlJPNk5RcE84M21vRXlRSkIrRUJURDZiCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXl2SkVzcEYrTVo0cWFOUXlZL1kKQUxCdXpKaHJHaWhRYlhUeHBWN0hpeko0ZzhEMmtzZFpYc2MrK1ViTERPL3pPcXNScG91TjBUTlIzZm04aGRveQo4aTZLeFhlM0Jzc0J6TWQwN3BXSFJETnFvWUNnTWp0RzJYbExPaDBsODRNQkhDeWZaRU9jditHOWhGSTFZRnNzCkVEaW9QelJhNk1PMlhNYnV4UnRzL015UXdOQ0E4RHlFRUR3YmI2eGFCcHhwSWVDV0VITGYrZUVVclcwTE5LZWoKZ0NyM0FzbE5GYTRhODVvS0VGa0FRMnoxZXUxUzZDOUJ0NUEzUG5ncFA4WWpkRmhZZzljZ3RXQ0xROW9GU2o0SgpCUnExRUlXUWVhMXp1cytGZzhYM1N2V1ZJMGVBcUFUTHF5TnU2NTJOckVrQi9uUzhXK1Y3SzBVUzhtQjhMTVpXClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenRSeHhuTElUc3p0MCtOS2xlcHMKU1dxM0lnbldzSDVFZ1p5Z1JRV0w5SmdkQ2ltWmpsZFJGU2xScG9KNFVkM1dONDI1aTR5MFY2c3NBcVlubkExcgpBZXF0aHB2bXVsdmgra2NUOUJaU1YrczlSTkhlcXRwbkpxZ080U2xQUlQ0UFNFT09VVzQySGE2NStTY0hrdWJ1CmFoVE1NK01yZVB0VFRieTJJNEhZVVgvNUpadS9TZy9FZ2xKSWdCcmFaTURvQVlKYU56K0Eway9PMExGUVNzU1kKSFByVnp6MnlsazM4K1VNNEhKeW9JbktQK0VRMit5MGR2ZXN4UnFuK3NDdWJkMng0TkdlWVFPc3JURVAwYWl4bgpBQUZVbEppZzV6NDBld1QwaEczd0JpYVhHV0luQjB1SmZoY08xcjVqaHkvaU9MV3pjdW9HM3JSbnZHR25TNzVKClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0puaWdHbDk1NXRhYmoxTmk5VnoKTGxwSkQ4REsycEtHWm9UQWFxa3h4U2lRSHg1b1NBclNOSG5MWU9WVGFQSTRRQlp2YjR6VUlNMDQwQ3d2b3hSbgpCdUlxOEtZaGdRa0Q1Zjh6V096K2dYdVV4WGYrQ2hsU2l6VzBtaEJnazJiOE1tQjZ4aG1BdjRQVG9oa3BXVFZzCjcvUHBPMWVrZzBIYlVLd2VuRFRHUmlmbldCOTdlZkIyeUtKQjVFKzl3MkV4QTBVT1FPQTRnTDN6WHlQeGtid2cKZDVueWc4WWNiSFNkamhqSzRWOWsrMFhqSWM4RFl2Uk1TRUZ6N3ZTUDBCRlkxUjFCSElmZkNYcCtxSWZpcnFRKwp4VXpJbk9rSU9id2JZdVI0TTZRY3B5d000N0E2c0N5TDljUC90Z1dEOTZUTUg2UndZL3c1VkI1ZVJOK1JNem1ICk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWhDckhUK2JORzB4bExaYVJ4UlkKRlVnZnhFT0tYTzRIMDJ4aWpNQnJHeGpnRitZQnhwSGg5ckNsRTFUTFhEZm1UR1RRK2VuMnh3N1htTW5tRzFLZQpKVUZwUXY0YkRHRnVPNVZVdEc3RGM5aG95SERVaEhNTFg4QlUwMmhYT1RORGZpQzZFSk5MRWoydU5FVTd0akcxCmxhaWR4aEdwRUYyUTNYN1hGejFVZGFOOGlPMmFIS1JoNnRQOGdyR1FYdVRLWkpPSFF1MEtrMU5icjZjS0JtQ1QKbStCL3ZEd0FoT25ITE10U1BLSnZjZld2aU8rWEo1dlFhOHdSRVQrdGpJTWZXMnlLK0JHNUh5dHh3dmJJVEcragpvNVVkUlM2YTZpSnpDRHgrSk5CMEFlU3g5WnJOSlJjWFBEZXBVREQ5dXpnWSt0K1hmV280dVJwS0tkMHo3UkgxClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclVoUmxiOXB2U3pvL3h1NmdUK1QKUTdGNnhyUjNZY1RUdzJJYnpwYVloRHEyMXRhR2J5WlY0clVJQUVzemc2cnB5VGEwZ2cxRlk2Q0dMNE9VRjk4RApGR3RGNzZ3ei9PWXZ5N0RSS05PTlM2UE5XSHprTW9qWmswRmR6K3lGYjVyWGl4WTBjNVFiY1A1ZU9ySjJUcnpYCi9zbHBOaGxCRXBISTU4dmJUZmJiYWk4K3BhWGNDbFZmeEtUdUdiTmQ2Y1ZVWU84K2dxMEw2enFBNHYrWlc3OUsKbHoyejZwcXRIMFhTS2ZoMEJ2YURQbXZyWEFDcTdDb2lVeWlzN0F0L21mblFqZlVpSkVtQ0g1SUpzTHVIRUttdwp4cXRlUGEreTBZUDlJNk5oakJxSTVWUWQxdlphR3ZlZ2lJZlBudzRkNXFmOVhNSEh1QksyajZwSTVETjBNVkhXCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXBGZEkzUkZjTnNJZW5ubFF4eXkKOFhIb2JhQWEraHg0OEJ3TjhMcXc3WWlyb01nSkRkQ1E1M3Q3Sk1YM2VwcEl2MFpraVVTZktMMHkwd3UxQnZZQwpEby9IUHU1VlFDN09KTDFnV2ROMjZDaGdkbGZwYzRVNGE1Z2xzdGNBQXhDYVlBa2lBTFoxdG85ZWtOckRZbmFuCmd6R0lLWXN2ZVlRbWFjTnN5ZGdDOFk0UkZ5RWw5RmFWbUlHSVJhUkNSY2ZKTW1vZm41bHI1S0NlT0dmTENCcGcKRjR1cCt2YUhJYXVISWRMV3FETExvVXc1Z0ZzOWhRbDZTSWVZSkFSWVBPcEJGTW96Sm1GYzgzV3kwTUsxYUhyZQpxTWR0UFhiKy95SjQyZjFrZFoxazNLMDkzbTlZaEpFczEwSGtRRkVSSUJrQkhHVEZzYnA0SW1LK3dvYVVCaEsxCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE9UY2FFY0wybVdOUlg4cHc0T2YKQ0JEUDg4TDAxclhJT3NjWDdqc3dxYi9aNThFbkhtZk1mTjc0c3BPL3pHVFNubitsR1lLY205RlByalBuVWd6SwpnTFcwRUlQRXFnZm9VNGYwOVNCNFcrTm1NOG00bGZPZ0xUaXNjZlVFZzZBblhvYk5uMUNxbndqWGhjREs4WWthCjVUSVFkbmF6UTRCLzJldmMzS29CVTFsYkNIU1hhWUN5NGhwZkdHNnphYkxpRy9MZHkraXZHc3lkVXYwZ1lWcmEKbktpWndaWG15Q1JUUS8wSWhPQkJ1SGE4NmE5MlJjYkh4Wk96dFdOVEJTR0l4VXhkajUwcnVxVWdUcDRyaVNGUwp0cFlvZjdKYUEwckEzNzg1K0tDL21LZ3BhK2Y2V1h5eWlvVlRONXhjK3pjZ2NySVl2bnJUSU94NE9rMWNLVER0CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUZLdGpNeldWbERQME1Db2hhcDcKOWRIVnZGaFc2Umt5N2hrMFNFM0h3QzU4ZGl6cHgyMWtub1JhSVMycW4xL2lzZEVxZTk3VzRaUzZaVm8yd2pEYQpWMWhDTG1QdXoyWFkyYXUxTXBjS1BHTzAyOUU0ZkExdGNsQzBBdlFpVTIyT1ZEMllta3BPdmlxc094LzM4Vm9CClNYckJDQ2ZHM1JMM2dSRDBiTG5ubnk2WVdNdTlBT1RlVjdKOWdFUmNyRDR5MlBCS1RVT2pPbXJwSnBCTWZseHMKTmhJb1N0ZjIvczcwMXhPTlFpclZrZjhHNGpyZGkyUjhtYS9uQ3pwdndIUmg0K1FLcVo0ZnlnUkk4SUFOZXF4SQpnTFZlck50RloxV3ptUG11bE01bzEvNjlFKzM0cXBLWGRjOGtiaER6c0sxdWtLY0hLMjZNanRDWDJ5WUlabjBYCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1RlQWpVUWJsdUVjZXN4V2toNjUKSHFrc1ZQU1NRUWV1Z3hBT2xNcjlUSkV4bVNjYk1wOFU4Wm5rZlYweE8rdzZQK2NmN1pLbnovZWF2R25OWFp1Rgp6ZS9kZE5JWXU1Z2lLeDZ1WTNDdEo5QXk0Sk5ZZzRRZDNHVGlVKzRFTDJFaHl2clVPQUVOTUdXdkIxRERZQkVsCjRXYllIQUpvMmVqTzNzSUFHa0daa3VOSEY0WTdLSlBvcEdFRnNTTk9XNzdvNGl6dzRGZld4Szg2K1hMQitneVAKT1BQSnpDYkdneTFqSkhvZlFJcDhLMnhVUE4ya3lhNXJ4VWF0cExnYWhkNXV1MTNZWk9CM3NRRWZjTVVvcThIbQpObzhLa05ud3FRZ0oxZDZMYmt5OEQzdUxwV29iS255NkNDQ2VXZFdCMWZsYlY1U093dG9aUzhtNkN2UFhpTXh1Cm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzJPbklDWGVpQytLVWVkWENEQ1QKYkpqbDFRV2I5bGpuU0ZKTU9tQS9DZlVmdmdWdytzN0lrQUp3WlV3RDkyVVU2bkV1S1pxQWFzNzdTVFd5aXFtZApjRUljRmFmeWNZcE5zOXNQSnpCbGJxd1R5Qm9yMU9jd09pL3dYZVRTRXBld0Z3eGFJWmxZQlRSUE1sdWhWWlEyCm52L0dKTVRvK0lmWXRLaXpFN0huTFRmRnkxSEJ0djYzUEVEK2Z6dlEwT2gzWFJmWXdJM2ZBcHk3RmlVVWh6OHMKY0V0dGh2akVmeFdPNEM2Z0lMa3BWTk4xZnQwQ1lQMGsvMUVKcnQwMUFxYzdESU1ockEyejFCQUlNaUdnVHRuYwp0QXI4VGNYVldrallrZEtGMG14aFdmWUZSWVAyNE9QUU9qNVZZMEJieitkS3RFanladUVyZ2hWMVRPSlA3Tmg5CnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVNEQnBpTUp0ZjNOdEhkOUs0b0sKZzNaa1U0eEVKTXJYajBGMjdSMkh5VmJ0czNSaGFVTXpONmw0NnNIaWhlTUlmdGsxZWZFOFVFaUhVMmpncnFLdwpkNzRKZXI3a3hwcTBKQ0JDd3FBbi9OMVN3NDIxTU5qdmNYY2p3OG1IcFZPaFZLTTlzRWUwQzBOdFhjbG1uVURWCmJOVVhFSWFIbno1WXVkVnRyY1U5RTNSbmMwTG9kUzQ1NUw3Q1ppUTRrWGVSaWdyVEZNMG5PTDJtTHN3YlBGYTMKaUhaQjZMMUp5ZjM5Mm4zcyt0RjgwMk81bnpqaXdsSktaWlhKYVV0dWZsdTdQMTB4S2J3azY1ZzdCUDRZNEhKYgpjcFNBU1hmNk1QNUpKWnNJSERFRURGQXhwMUV3ZU5lNnZnaTRmb2VtQS9RcjI2bjliaEViVDlOVldoRG0vejFQCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEVsUEtiMWNpMGtrcU9uNW4rR2MKeDF2TlRBajViczNYMFE2d21mL01qbXBDUGZkcXhCVTY1ZG5jNUhJZ2c4TzYyMFBZaURJUG54RHVaQ0dneDBDYgpRSGFORlArbmpHM0xDbnlFOVhwSnNlbmIySHQ5MXpzb1prQzg2c3V1T292UGo2bDJjQWRZRXhPZmpHMi9ndGxECmpXMStsdVFNYWFFVzVNWVNrbXV3bWkvTi8zZEVnQ0F2WlRvUHNOWVZabSsyeFVYVi9ldlVCOHREdU5lQk8xTnAKV2ZBOEl2Ym5id2RpelRzd2lKYkZWM20yV3VsUU9EOWZobytJWlF3RlBNYTl6aTNiR0hGOEdPUy9oVmJNMlNLRQpzYmN6T2RyU1F2RkI1cXZqaGRCeWh2L2ErZTBnSUVyUTEzUnFlb2o1azF6L2h5RWRBOXpCNGZxTGE1cEpOQWRWCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmo4OUtlRHNkNjJybzUzd0NEZW0KNE9qMjMzdVBBbkFnRHMyTEx1MkhzUG9zSVRzUFJZMWo1UTg2S2VUU3pCRXFJdVVlTTZpK1AwRiszZjIxdDRnVgp4RVNGa3NSS3FUVk1zUlU4L1h3Y0dPdE1ncUFsNGdvemVSVGsrUFRlVUxUZkNxbjlSS25PMkVYZDNIZGpkMGRmCmVZdCtmckRjSU1iV0lnRzBDaEFEN0ZERkx6VngvdTVUVHJrck1XSFllY0taN2RmRUVDc1JzR3dMcE45bCs5Q0IKQlpRa2kzVDkzZGZQNEFSMkFXdytvT0xqRDQ1blp5WlQ3VWxWSVREem1vV09PVkxod0JadkxyQVk2WVVpQlVnNQpYWE10L0NiY2ZyeUorYyttdlBIL3NYckx0aHdJNXMzNE5tbUVjQmt3UFFuMThiLzVLaHRscndQZFVYQnN3RG53CnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlNtY1ErOUFrK0VoOEw1NXZYdFgKK0tpSjJ6dURLM1I0L3dpQ1VYdHhmTXBsWlVxNmtuRk5BT1Y0aWRmRWR4QWpqTzVUSEUyTHJMek9hbEVRaU9ndwowb0QrSkVZeEVVNGlBNUkrT01MT0hDNVZoTTdDWS94ZERUOFZJRDNla2h2VDRkY0FZOFZsTFMwMzJKNk56MHhECnJmZW5OcVd1Tnd4OHpKQjVrN3FHODUvOURjbmZxdFJ5bCtleGh2UWpjdE5Ia21CTkpBQ25BT3pkSHlQTlY1c3gKTFQ2NGpDTm9RSzhTMk9yL0ZkVEZ4bUg3Zy9jblpKZElaOWMwSVNtWWtQV1h1OWhaTzBud1BvblA2TG9DWDd3OAo4OHVKaHRWNU1RNHNiRXFkcW8rNHJkZmllNGxTaG45cmtBcXpkNlhHUkY5ODV1OWZ4MU05YUlHUytWR1BXbzZjCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlVwbmJzWG1sMmZ4cWlDTE1PMjgKbGNuL3Zva05SdXUrRmRwRFpTSFBwbHlJeWxWK3k2a2ZkNE9yVEY3TTgyREZOOWwxNFowdFZrdWVnK3dSUHhNMgpEUUlFT3hJdTIxOXArTmQ0azBzR3R2UmYrZEpBVFFPd1Q3ZkdlWWxwTllrd0RYaENyY2lIaUQ1WGFQcjdWUGluCjlyR1k2TjZ0TmJGN01HU3VsVDQ3MTJiSFZ6MU9TOVlDL0xQYm54MHk4N1FxNTkvZnFhb0p4RkVESTR2aSsweDAKZVBqMTdKRmdzdUIvYkNMQUZiUlFmSU0xM0k4Nm9CcEZpTjRkcGpIOWVXaW1UQVNabXE4STd5ZU90TDhnelh6bAo4TTltNDRhMlJ1YUxrWHdSZHpBZHgrd3Y0OTJwRERQSTBhb0lmUXhmdVVJQjVhc3hVZVY2TGZkOVdBbEFSVVBLClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnluQTJaeXFuaXBJMTRaWWZwNUUKSkFEZFJtMFg4MWJhK3lYdGo3dUZZYlVuOWhlT1RGS2gzd0g5SDJZWlRrZ2RGNFhwWXMycE1YcFRHU2NJaXg2RgppN3BJWmtheE9IYWVNSDltY04xM0N6aEFIQnVKMm9kUHRQNm05U1RteWw4SGV5VzlmSTZiQkg5NWxmd2pNak5XCldmZERHOWhKblYrcjF1dWtobTA5UlFsU1BVRjg5MEt1ZEU3Qytqa3l0Q2oraVZldTkwR2gvaVJ2NmQ3U1NIdmwKY1VDbDJlSjN0MS9WaFZjMU9oY3YyTFczUko1YlRBa1ViVW9ZMGVESEJmS3BmM05SbHV4TmpYdFVmandrcUdaOQpPMzlQSkxCanVQdUw3eVlJMXh2dzRIdXJpc1hxSkRMMHhLOVhWRVNzZEM0dHNpcDJyK3QyNEd3cStWeHNybnN1CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclRiVExYSjM0d2p6WFZ0eE5zUDMKc0tROFJVQ2FhYm51M2ZFODlxaEt4Qy9VcTkvNnZ2aVRBWDV0Y29Hd0pMR2VzNEorNTVIaU0vbWFSZ0lQeDB1eQpWZVZoejVOZU5DaHRpdTBQTjJQSER3VkxtWHBRUmlmT3NHSG5xSWljd1psSHZWQXpiekdHOEhJNEhRbWFoS2tGCkQ2SE5BRHBFUStEYTF4V2o2S0hFMnBkZXlKelhXUTZVUlBKclFXQ1dRSGJSQWIzWVlRZ3dIakROMjR0NmNNQ2sKODlpSFJVOGx3OUJuV1dEdkR1NlhFVTR1Vk5oaFVlRlJsTXhJOUIxRGl3WEpsTHgwMFgxeVB6M1FicjlIdWVJNwpsYndFRXM1UG5VbmQyUHBZMU54UC8zZXY5MTZUMkpsbnZmVEo5NWVJZXRyblNpeXIrRk5Tc3lac1Y4K1EvenVGClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEY2NTJpTTViVm5QNVFYZmJTRlkKWUxmUUtWcW9Xalo2M1c0N2VDNzlqeWczL0o2dGJPRWpVU3M0YklGamVvKzc4Zy9nYzJsQ2dLSlVBaGZMb0lrTgpTU25rM24zQ0dzdDFhemhpMFVLR0dQcGVNOEtWOUN2SXhnK1pveG9sYk0ydnhTWEcxNG54SGREanhlQlNLbFVYCkdWNnk5bWtXcTFlL0dJeUNQSnkxeS9jbVUyWFZUNy9NeDQvRE02VTZ3cUEzcjFyVEI2RTRwWlU0M2xDRzZWbDQKQXVWR1NMY2hwcU1QdE1JdjVoS05GY2o4emRwc2sybFV0TnpDN2JBTGJYQmxGZE1sU1JTODhCUWwvZVlhR2pKYgoxNnlnRkZxNE4yNnFCQThINklhKzY5STh5UUY3czEzWkxjeWE4V28raGdnSkt2S0U2Yy9udmNkZk56RWY0SHdSCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0ZqRUNkdTBHVmFQUG8rcUxrWnIKVFBXdzk5bnNGdmhvVllyYVhhL2VvUHdTS1U2Ny8raVhrdW8rRkl2cmp0WldzSWVBVUd3QnV6ZjRKUG14MnpxZgpKbUEwRS8yUzM2VWxsNGM5WWZZQXhrcjdwZVd1OEFoMWd3dnowQk5xLzM3bk9rNFRHOHhid1FsMThxVk5KRHR1CkZVL0RGTUtiV0wxMHp6bERjb29TTFM0S05uYk1GQUN5NlRZZEY4UWxNSWwrSGE0aWVKTmtYcUlPS05BZjFyRFAKM2tLcy9RWm5iYzl3RnZ2ZnNmbkx2SWNmWHo4bkIyUHVXWTNVbUNEQW1JRG0vQTVLdFhDN3gra0h3L3hCd1BNagpmR3JiRVBWL2U5dG1TTFc5VzZGUFhqcHZTVU5OdmpKWldOanBSaUtteE04YTVYWkk5UWxEbEx1aXhERkZBR1FMCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbS9rMFlNMWltbW5FbktCTXovMEIKODNQSVM3cEE0bHFGaTVLajV4TCs3L01sWjF2UzhxYXhiNDVqQ1NEbDlsRWtJbHpmanc1RzFkOUlESWRDclRiZgpaa1FMMlluUTJYeEtqQ0F2WEM1VU0zdW5ZY2VqeEl2SEJ0TTRXK1FoT3NTOWRJR2ZYZHBwSjV3T1Y0VkpRS3hFCk12eVozK3VhWVlEVGg4c2dFNFg3Z0w3MEFYQTRLNnNhamJZZi9FN2pJa1JsOUZKVS9taHBheXphK2dDVWo3RnUKOC9wVmNwZHQraFVUdHdUS0ZFeWFwU2xqYS90UGVlamJEL0pnNEJhZytkTWFHemVxa3Uwbzhhc3dEaGZFVFFYYgpLWnVGNE4rMElVTUR4b2wxNW9JaFZEZHViZVhYYTgyaWJhY1dtVjdFNXRXU2hscStEMmlZVFpiQWRCZGtlTHhyCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEo3Q0dnZlpoM1dPdTh2OXdKdDYKNFFISjAvN3pjaHphbHlrbGdXSzNpcURnYXpRcGdZbk9nZEVoSkIwT2IrSnB0eUVHdWxzbXZ3Mlg0bGp0K3NTOQo3UVVxeEhERHdEeElJdkpSNS9odEdxVUM4cC9SQndod3JFWlFZUkt3aktYMXR1SmVnVjVKSjUyZDFDcTA2aGxCCnJjNjlxRGxvOHN5RjFycUpjT1dSVjhoVGo5aC83Rnl1NXNZN2xEN2o0eHNiZmswYkxxd1JvYlhhWlNEMkNuNSsKM1Ewb1Z0WkNJT1RSenlOUXpubXpQbi9BTXB4TFhoR3A1RW9qZHJwUFVLNmRjMGFKNFJXMktQU1dQanltQnJVeAo3aVZLMjltaEtXUDRtQjRid0s2WFd6TitiVjA1UkV0NFdUQnB6N1oxNnd1UGMweTlzalhEb2lyaGZNaElyOWttClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzhHNXEybkN3OE1lK1JkWDQyS08KeVZkQVR2WHBuUzkvYjFOU000cnkwY1U5ZW1GN0lKOWFxeWJIdDNBcVpWWENFVUQxOUVQK3RUakxhbkwvWnl2Lwo1bzVyVFFHcXJCK1huaDdjOFVvUkNaaFoxNktUM0JaQmhnSlpXTUpPaGY5d1RVZDdOSlpBUDUxTXZjYjRTWFpyCk1iYk5LYTJrTjV6UURhaW1xckNqZThUUlk1TEJ2MEtCNGN2enNCRWM2bTlFbFdyOHA5cmxqeXh3ZFUwNEtEWDIKUmREU0ZERnhvU1pDOUVnMEZENXJyYzBNQVh6Z29kL1hhNWFOWVI0M0hac3o5aXlEK0xOaW04Qy81eHdUSS9mcAo2Qk5OQmFEaWh3a1VpSXFLeCtNQXFDRTB3VzFwb1JubldkT2NRS3ZSQlFMNUJzU2dLbVI5OE9pSEhMSXJvYUV1CkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXVPd2dRSGJyUTllWjBaeVdUTjkKS0huWEJndVMwb1BJWDVXaFB4bTdPMkFSZWNEUExVbmFPWmsvbnRYZ0ROem05aVNjdmMyM1JVeU5ITUFua0xBZgo3Q0l0MUFmR3N6b1ZNM3dPV0tXL2lyOE9IbzEwejhOaVBveTBXdDFQaVNsL1RvMzU4by9sNHZ4NmVpSWM1U1dKCjRoVWJCbzl3UTE4aUtScFhtNnVPMDFNVVdReFFYeEdxZEJUZ0FXK2NDL0pVR09XY0NycllvQjVwMUEyeXB2eWsKYWFVcEtkT0dIOUE5ODArVlNpWjB5VHhPT0hGZzdETSsrTWczN3hCSzNtU1c4TlJHOFM5cm5tdGxlSjl0RkFzdQpFdkVDRmFUOGREaGNHRDFRQjVxSU0vVmtEa3QvQ0UvcXhYN0xYV1gxOGZnWjhnUlZHV0wvQi9CM1YySU5xUTBvCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBci9CUkhvcWg5dGVHMmlpWDBhSTIKMFBwQmE2MG5xa2FubEdmZXdTNkJ5a1R3ODVhNmg5U2t3YTVpNWtDdVovTTI0T2FjaHBza2ZUUG5icUZqUjE1bwp3OEREM3J2djc3NFlKMDRaMFN2SEF2M3djYVhYaFFYNStHQkZsNmZ3NnRhSG5GYk5pOE4wdGU4emxEblUrdXo5Cmk2LzdRTzFkcGw2b0swU1BlVWk4bTRRM3dmeStDcngwVC91cjRaLzh6ZC9zNnBTY3daWTFpZUVmYXRFOTlUZW0KWlBKNVRDY2tETGJVSGVzUU9DOXA4cW4vYVA5Rlp0SU5GM3Bya0dKOEVXNFByQ3JnUnVGdm9ZaTVkWXYzV2NiKwpqMzdWVXMvOXFBemNoQmZuOXJHVEJDeFZ5ZWtMZnBKZUZZRGNBYVZPYzZmV2R3THpQWDNMdlM5OG04Z01MblFZCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEZBWmw0NVJrNzFtSENubmlMWDgKQlk1em1lZTdMWXFmdzBXTUJSeWNlcEtxN0ZUZ2lENEZVZ1FJQXU0TEVtanFYcDBtMVpvN0FSREhQUTFCeEZJZgorajFKcUIwdnJVSkFISytEOGRoa283TU9XK0ZEOGpVcjkxc3dOdWNuNWJFbFlyK3ZUeTdKeEJoRHV6eHhaNnJ1Ck5wUWh1eiszdnluL3lFb3ZYR1p2eDlUVFRvbGZyaDhQNUpjTTVGcm90THNDNWxCZXdDSWNGQVgyNzQ1aUJvamUKR0VCc3BMclh3ZjNiTk13b1ZLVkpyc1FkVzVUMWdhUk9ZMHphZmRhMHhtNFQ3SEUyQlFscytDd2ZDQ2NGTUQvVApHZUJKbFZPT3BJajM4anlHamVCeG9POWVsYjhMMWdncXJoOXI4TVhxVkZ6YVFYTUhxWUhOanNpY2dwcnBhUzVPCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd09WVFVKTWYxQlV1a3RpNmpxUEQKV0tvSFpsMmZzTEFhQjF0UTBjbml5dkcrTkVtWTZXZUN0OEQxWVpTSUlWUTJIbVdYZHJZNzFWM0lnUUJ1bDNiOAp6Uk14MnhodkVJei84VzdmY1h1Y1M4OVB3dzA4WmladHdUd0xuUE41WFp0RmxseGE4YUdpclVwelRBcWtwa3R1CmhhLzRVMkViSXR6SSs0Tys3L3k5NzNaRk1zT2cxbXdOZ3V0Nm4vY2h6eVpiNnJzMEN3R1NidUloZG5mcVFlTEwKV3gyVWF5enA4MXBVZmd6STNNcnM5dlMrR2JEWllZY3dxdnA1aVNHOHVNRnpwOWxra3BRd3A3aGdkR1dhRzhadwpxWjJSODBTSnJBUks2ZDNwN3NxRVAyc1FVd05lVDZZSkY4VEloL1crQXVUZUhZNTgra09UK2ZQMXZSVDJ4VWlJCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc29oRE5jRVo4eEFNcHV6aUlqZkkKNUZZUFhocnN1OGtIODB5VkxOSFlOK3ZrUTNwYi9EcTBBT012NW05OUZQK2xHelY1empmMEh1QzVvVEx4V0VGUwp2QW82RlJaMGY2b0VENUhTVnVjQktYaEtoQ3FMcWFEL0xBMTR2cEJUbFovM2N4VXBrSHFoOVcwOTlIOFk0cGEwCnIzVys4M0QrVHd3R3VsM0U1WFgrcm0vNnJuMWtSeFREUDRQekFJd3BIWTg1WEdURE1xR1BaUnM2UVFjTGtlVlUKU0FNWjhJczRpY2ZTUjVsOXpCL0VnUXFWVGpnYWZHbU1XRVRhOE1lV1ppcGRUWUd0NDF6UXk5MFlEWmx2ODVaMgo4RjdoQytrSWEvaklibmZGUzBUTE1BdmJCbENhQnNpcjdVMjhrN00xUzhZNy9vcTljTFY0eWUyU3Jpd012U0RWCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdC9lR21WdnJCQUd0WHdEeE4wd3EKcFlmWWZ5L2ZOa1NNeGJCd3JzRldlcWNycTBZNVNneWRXTFI0Tnd5aVcrbExIenRDYUhQNTQzUFlSSXk5alpYRgpqSWFMdVk0aVAxRlpndWxkWVpLaW5OS0lVTlFMMTlWdlpQQ2xGKzdzSy9QYS9yMFRIQnFzNXdVbVBHRTJ1QlZQCjJQeGp3TmZwREhxMDIvbmtLb3NoaGlTQktpRFhvMUtEZEFpVjQ4UEhzcU93MGlkVGRITkJWSFVsYThHbVFwQlMKODc2OVZmVFI5QmJoRUpNOHpvcGZmZ1VkMkVRTWdHNUhKL0ZTajlXWkhUM3NqMVlSRXhuaTYxQWp0WjRjQTZWZwpNNjlkMm14RG5wdFNkVXNxN3N2SEtINjV2UWdHUEpLeGNaSGNkclZhODgxRnlPVWpIVWlnT0pNMk4wVjlKeTVHCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVA5YnJKSEphRS9wczVWMDYwM2sKQVJUbC8wenVvcXJMZjA1aVdod25YdW45aUUrOTFyRERWTno5MzNubDUydGtUNzZRZ1k5UVRaZFhSb0JVODhNdgpRTHZneUt2WTBscjg3ckJCMHFXMjF5SUo5VlI5aDRuMTlHQnoxeTRTTlByanh3R1EzanViQXMvQlpoTkloVVMrCnVwNEhYWUw4ZS9TcUFjQUQwbVdIdHN5MHpEN21WNGtXa3IxVStyai9WSWtaQlpQVmI2MFRWcVdxT282eXZSTG8KY1IycldzTjluZlpxVkxFZDZIcmdpR0p0MEFIUWxzZUtWelFpTWtndVFiVGNLUkQ3YTB4SmcyVkRXK01UR3ZINgpIRU9JaEdUVWpDdDNwQ2lUajFtYm40TEtrL3hNdDlNRi9HbE5HWTEvSzJ0V2NFc1FhMG41Z2J4M1RaQ1lmTUgvCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWxKTFdvRUVkWjF5bUVyTHBqY2YKUjFoQlEvaWlScGxDbEtGSENSY1hlR2NYNGRqamNJaFlxL3M4NEgrcXBxak1oVVcrWndSbEZBZjkrZko5cVM0RQo5SGVyenhUdEpQSlBIbkdDZXl0alJyMVdScmV0Q1VDTGtUdGhIUjR6aC9hd2pRWndKUjNlbkJ2NmRQYWtNcm16CmoxSG5Sem81MXBzNy9GZzMrdjAvYkJENURzM2hkWGpXLzlpM2Q3VTRoWFFxUjdXdGRFckorc0FPTHJVQ0QvUnYKSWhzNkxpcmRCditkclhFd0VqcXl1MUJKcEJjK1RGZCtVUU1ZbU5WUHI0aFcwaTQyWnhGMG5yU2Y2NHNOVlZuVwovZW9KdlZXMU8wd3dMbkZ6c0V4NUZVR2dJNUc4VGN2STZaZlR1Yk5MNG5rWXk0NGd3NnhnL3oyL2wrMjhzUFBtCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeStCaDFSekdJbEk1dHgxUVNrR1kKRGlGQU9LZjB0MTc2OUdLbndTek9pYm5uL2VLUk52cU5qb1VJdVlpOHJtNTduWkIrZG94VGU4LzZiMy9DakVGagpGNkdMWFQzSElIUjBQTThYcGdmYVA1TXVOZ3Fwa3RLSjNNOGhQV0l0QnBFNTcyY0pVekI3Y1p0VjhYWGxUcFJZCmw2YTdrSU51V2NtZk1NdTg4Q0lKWWd2WkIzQU5EZW1udktEeHZkVEhzRk9xb2k4YTQvVVMzNUtTM0tESmhPczIKUWQ4WDNlNHZ6OEw3YTdqcksvTUtGTzJFNklwZzJ0RUtUTU1BS1cvRjVnZHdhTmluUDdmYjAxa3JQVGlZRWRCdApudDdpR0tEKzVNNWo3d08xdmgxTGRyK2tUK3lqZ0Nub3JhOFFibURLK0xqcGZ1K0R5K1pQenhTcWVYOVhYQXlhCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVQ5MFo5V3grdXVoM0x2U0JITW8KU092TVBjaE5zZGppTjEvVDZLQjZUM3p4eDFPM3VpUE1wditVZCtTendtVEZFaFFQK2JVU1BpbjlHaWFIaGUrQgpGdTI4VmN0a3BMb1lLdUZKZkdCQkFhbTVZOCt6bCtLcnhhbGZLL2drTVp0M2gyM01QWDdyalB1dVVmRGs5WkV4Ck12MllMRTBmU0R6Q21NTmQrUzl3MWJWc01YZ09aaDFPNFV0NFFRRjJrLy91VVU1YmFhUFFUK2ZrbEFVcFFnNHEKOWJDbVAxMUw5UVE2a205MTN0c29UbnlUU1Z2Uk90MC9zSUtYaldPa0NLNEQxWXFncUxhQzhhRUs2SVpYMTJoRgpLeHlCVUJDMm9qVDh4NzJCM2JQZzQyalRZcE5tWTdQVGtVa0JhN2YvUGZ1SFRnUllHOXpzZ0Job2JPYU5RbjhCCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBei8wK3lyYnFtVFRaWlc5WWRvZVoKVkVTb0ZWM2IzWU0wOUthd0FONnBYMnNwUFVLQTVTRHlxVm8rU2E4cmZIWnFMKy9zTUFYM0JvdmZUY1AyeDlnSgp3bStqTEppUEcxWUFpS0pPVzkzWHE3a2hTUDhZNmpxQlErcG8zdk14b0drV2RCaHVMTU9pRWJTbm0yYVJWZmljCnA3WTY0SW5MRUhzTzhkc21QclhGWlRiK3hGTXA0YVpYeCtMd3FsMm01U1l5d2ZSTXZ3Uncyb2Y2SUV2MGIzTWMKb3E1NkNialpkOXl4N2lvSXplZHorZVZaaWtURkR2aXo1T3M3UGhHa0Z2MUVQM0dtWHdOWUkrU1VUM3VYZmhBbwo3NXIxbVFoTytBU1A4NFdWOHZIQnNQTmtLc25ZM1Z3QUgxUFovQkVCbVZTendyK2dYdGdtUkpkUXRwejhGZjhUCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWVJZ2JYZHE5bTdDY2w5ZjQxZFIKcTc0UldQUnNianFLQmZBQVJDN2dhaXBFWDdsUkhWZU5La1JUcEtxN0lBaHhaN3I4eFBOaHZQYXVJcEgwd1hwQQprb1NkN0IvRlVUQ203anM1OXphOXBxUmk4djBtZVZIMnV1Y0d2MUhUNzI3KzJMMy93Wit5ajF6Wlc0UkVyWWFOClBVTlZYOGpacXFFdzFNcEp4dHVFVW5qenNKWEhqOTFkaTdELzhLVkhsTXpMSmdGRXE4dGEzbWg3RG5zK2RYKzAKQytDTDR4dk9wQjl5eWdsT2lvWURUNTNMVEVYbWN2WjRoc0hVeElCZWpCS1orWnNtUGJHaDJMRDRsOWlHMWdscgpOTWovOHpLK2xpSW1RRHQvOVJLV1lhTFBqMTAxdXNNRnBVbSswYkdLbTRMdE1pQk1Pb2lJTStXOXZpMGJTMWNDCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXhZUTZVODg3RXoybVB1QmhGSDgKQzNhNHZsN1RtRjI2SjI4czZRWjMxL2dNVkkvdnNhc1JVeGlrWE41dFhNL0VXZzJzUVg5Z1NEakFRa3pvaEU3ZApDQzQwMVN0aUJQY1dYTktZYkRlRTVqV0VQaVR3NjRLSWJMU0lNWFlOYS9jV005bzVncHhYVVo2WS9pK0x5MjhqClZ4MTVuRTBIeVV5K09JZzdvK2s2NE5rVkx0d1hKTTR0WERlS1U0U0VmM25rTmcwZ0ZCcGxsR3NqRXpVTWJkQUYKUml0TUpPRFR1WUNQWTBBZXVQWmlEYndQUVR3MlFESHVzalYzbitsbDRkT012a3RzeXczRUV1WkJjZTQ2dm5hMgpjZS9wNllqTXU2QUZHdGVETXVFU0NlL0pWQ0orbk90R0Y4R3ZpVnVxM01CejI2TmovTUJ0VTB4YjdGWC9jdHg1ClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmZTaWJhR2ErMWw1TFlERFUrek8KdlZSZm0yV0M1SkJEdzhTNHMxVC9BQmhxZ21iTTBPK0IzM1V4K2xvcS82ckhsdnNGMkFVZEZoWUFBcUJKZDlVQgpvaDU4U3NyYjl0aUVmSDNHcmVkaDhNY0cwblRIOG8rNTVQZDR2RG8wVmRSMFg3V3BLWStkK0F0c0JhZWNmQWlOCnNoU2lSNVdkU2tOcC9wWmVQR29vZWIwVVhtRW5kNkhsMUlYZFFKZ1pRdjBWdWJibnRZOTdMY2xNU0pBQ0R3bCsKWjFhb3Z3aFdHbFRCYXpTRElwTHhXUFpVTFZCcGdWakFXZjVJdmgvTEdQeHZ6SmRnTlRRaDE2VnZIa1BTUmJZOAp4V05TdXU0cmxqeU5PZlQ3ck5UR0lFY2p5Y0NZYzE0MVpBNzlpa2tLMjFJN0o5Sk9pY3NUSG0yc3NHTGZoUDYyCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUNOMyt2T0ZtTEhXQlBVNkxRcG4KNHluT0tjN3FCekxxWmw5ejEwdytZZjNhVHpqMVdKMDM0YkJuZ0xCekVuaW9wQmhnSkNUaTV6dENVY01RWWdtRwo2VS9EWElVTnNEZFhTMWgzUlBLZWMrcWJvTlZISWpqS3U4dzJHUDRuQjhKSHQ5ZlVOZGlUcjV2TmN3d09vbkhiCjk4MEpoRE1KWGw0Z3hMaFg5NzZUQkdrejJzenE1V0xrR2hsaUN2RlRhZHNYZ3ZscDhxUGRHSDBwZUdSeFl3Q1kKZFpsK3pDNjd3ZDZSMitob2NqYmsrNmVRcWg0YTBCdzBKNS94dkRRWks2dDdKUDZqMzBvLzI1ZFF5dUdqOXUxMQpkeU5iK3hTVlNmTUtxWDZ0Qm1FZlBlcXBNUHlTSTc2cUY4YWora2cvUUZib2tLT1M2WUw5ZTZ3NXhSMWI4ZEwzCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVZIbklmaXBSUHBZa2F0VWpXbDAKOVJieDJ5K005Z3NZZ3NDeGUzVGJGMWV4U1JkeXZKc25OVENHazlzUk1LZitFS05pR2FTQmQ2SUJ2akhIZG52eQp1RDhoeGw3RXJFQUkvTnI5dmxySkp0WFV2RXBxeFZ1Q0crZURXcEU4MHRtQ0JSbEk1Uk1lSjBibytsVUp1YTlLClUrQ2FCMHN3UTJ1T3B6bTF3dHplS2NhY2VJRlpWRjdXa2dQeUR6K2RlaFlMdnNtRmV1ZXBXSVF1SzREL002UTQKN0s1RmFLaFhFa3BTVjgwK29PSjNONE14amZoUU5WRUdPbmh6YkxkZmNRSXAwVkMzMHM4Tjdqdkh4QjY2T3ZwNQp1SXlFQUxUNjB3OTROTU9FT1FEQzh0Y2ZjSEgxenplczdxUDAvVXdvYlpUMFUxQWJtMXp1Uk8vOHVPMXI3WlE2CklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWYwRjJYcHdZUTdUdERXNXFkeDQKZHlTSVYyaDZ4MnZXTWY5YUhhbzA2ZUtjZ3BFaTJKTTFUMmI2QlpXdCtPUUxSdStaQWxiUzVQUnNpZVo5bkh0RwpzTWgyQ25ya0xTZW9BMVY5bVcwNHpCeUJlRHgxcWtkZzJ6QnNNN3ByMTZaNG9wbEN4SmM0dlNkSmZ4RGw2SlMxCmVGMkxhS1BTZHZ1TlNHYUZpZXE2ZlVoamlyRTdnVlNGTzczM2svOXZSL3RuY2hRbW8xbVZBK0p1ekNVMWM0NzYKeHMwR09QbDh3T3h2OFhxSlJCV0pNcWZlQlVzbFRrQnEwdTBkVWlJS1RydVFMd1J1S0ZLUUprTURaUHIvcHhFWQpmR2ZJdUJlbWh4Ymo0aGpUSTFjdXNRMGZqRnprRGhvTHc2M3pMSkwwcnErYXBnU3h5Z2lCSWVPRkFYYkNRUGt1CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdS91Q283ektpRjg2SWZyWkZOMnYKUk53STFzaHhHdWlEb21kQkl0Z09YSW4wN3ovZXNsWXNQcHEwbDR6dmhRYkN0TjcwRVdOWnlWQjVtdFhLVHNRbwppOUFOVm1YaUczb3RxMUVGZ1gzMkZCcDFtRjJBeW1EMzRGK3cxenc2QjZwakVveERSTlZ1L3Q1NnN0NVpZSjhUCjBlVm1FTCs2dFh5eGFRaFZBeUtUMUNCZ040VmFNbEZHSGVNU2Q3bkNFblFsci9sb0JhNnRnbG96aGJINW54YkcKSmdCVjNhLy83Zk5QaWZMQzB4NHRoeWgxVHlEQXQ1N0FCQkhBMm1hSzRtK2ZjZUFza1ZINlBhSDZ4ZFRvMWRFdwpnKzNST3ZjenhWRCs4ODJwM3JxVURNempKazE0QlFIYXZwa0pyM3JTMjZMSVZ0bjVBa1VUd3Z2SUNUMXBSbldQCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclM0SkN5cEhVbVBnck80SFo0NmsKTm5UdlFaWHlGYjRSSC9tY3cvT2tOcnVVWDY0NjZWeXRGZ3E5a2NHMFNVOExHcTBsVUR0Sml4YUNKQWs0am5xVQplVVRyVVRvSy81aFdtWWtIWitIQVIvanhFK1YreEwyTXhPdGtMYnhlU096d0JXWnpwZllldzE0YzkvaHpXWEtpCjFJT1dMY2RjdjdvV3p1Qzd5NlZoVG5wZXFPV2kwZWtXNFNSWStVRU9QdGM2TWxaQldGT21sVUM5aTE5cnZJZWQKTENpc25sQ1FVMWZMYjlNNFg0MWVjdnloSmZESGNCeHc2RXgzaHhCanFvN2ZJN1ZYVDRvWHVLMVRNYkl2L1ErdQoxa3BpUXRuT3d5blhMTDN6YUFxTkw4VUZkQlZuTjYrWXpiQkVnWGlnWUJzcFRMRTJYUTVDVVVkSGxTTmdDNHhtCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnhmUW5VYmZRNlZKNTBSVTdUbTQKRVdCZEJlajBJZjVMdmVPb1FlRlRGay9PUlpVYmRreGRUamlhTHZVT25ZVFEyN3JoYVJETUkzWkRMNlRwc0Y3TAoyNFk2RC9uK1NuanRhU1RTRldQaXViK24zSnpLT2dBSm84RVlBeFN3NkFoeDBKcjlaNEtPNlJQeXBLeU9zNkdDCkgxaC9zM2pDd1M0N2FqNjNhZHBic05XVFNSZldxQ01ob01Sa29ZQlRRSXJ4dGhwZXJ0a05FeFhZTFUrWDFhVjEKeHRiVXIxWkFrem41SHc0VVBzSWJnVkxjS0RUNlBmSG1Lbk1HRkJKQnQyYTAxUWpvSGkrd3lENlQvbVRNUVNPNwo4VUdlYzYyaHY2K3BKbGpEaERwV2RNRHpDcXdCdEtvRkpBRjZkVlR0V0Y4VUdneVl6aGl0MktZWWdUYkpPeGRyCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVFkRnhWRWlONUM5NUZtaC9HQ2sKWkpGM1lqVDQxdm1MeU04cWNoUUJFcmNOa3BEWGlXeWFZWFVyUHJKYzNqanoyK2hpSEJiVC9CSkM5YmtGcjhOSApFUjhKcnVtUTl6N29NSU96clBKVDdlNFZBVm1xcjVOSUxzeDF4WnpDRGdaWXgxSmVLTDk4eHBLN1Eyc2c3OEtCCnEwRGJJbzZZQmQvOW1ySFBlcWQvRTVZQmd5aDh5ajRya1NtOS9KMGFpOW9PLzlmb0x3MEVjTGdXWGdkejNZbk4KVmFDQkxQRGc4NVlkamcyUFpFc3BqN0dRTFJBVHY5RzRQRDdIcVY1U2FYQTNISWZtSHZudGlZNVBMTXdVbmhBegpEa3hNamNhMGJQUGEwclJ4b0I3TzhYMkNDM3NQMFg0R0cxZ0dVUDg2eTdSdEhreWVDbzZSS2NjbEJjVG1yUVdJCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXZUdVk2Lys0Z2xFN0FCSWVxY3gKSjk1U05TenlkTHRLL052NVpSQUJwUUlNeXZidWtMRUU3ODd0MnhHMEprdzFLSXQwS3gyelhOTVRCemJOMGJsTwpNUEtJYnRpdlRFeXZFZ0RtVDBwQmY2NWIxcWsyOFhQejFrMnBvVFRWc0Y4cXdDVU5hQzFMejAxTnl3eXF2M25HClRwSjZzYTA1aURKdGlJQ2s0czNaNFp2UjJRbitkWXB0QnZnMG1vdVlmNlRsN081WUhFSEN5aWl2UExsNFZwRHoKN2dvV3ZKcnhnYUd4alFmWGt3V2F5TDlvVTZSY241aEVuQzBTdDhKN0pDVFpET3p5WWV4ZitRNXRTTFlXZFIvVgovdkd6ZlNVYkxBemZINWQ1MEVqeEJCaVVjUEVCd0dCSndsbFNQWjE4K0ZQVG8vWW45enF2WURxVHUwbk4rSlV4ClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDcyb3B3MGJBNjEyd0Vjdkc5dUgKcDB6aGt5NCtjQkNyVnZ4bWtDZFpCd29jQ01zRXBzKzlkekpETndnVnVZRTVMSndNeU43UEp1ZVJKRmd4bDJIZApLOTIzcDFMd0pNdmVSQTI1U09rT2lLMWpoVGlIandmTjVFcXNFazB3TXF2QzBOZnBUWElWbURNVjNkVHd0ME80ClpSUmJjWXN1eHZFS3I2UHMydnBJN1J6Zm83V1Y1MTBkaEhTVHBRdWFCODZxKyttaG5hdjA4WW9FNzJFekNoc3EKOUJUSDJLL1lJWk5qRnlMYjZaKzA5RWM4alh0QXI3bUNBZWdqLytpcWtIL0FvRDk0NG5oVEVicDhGRXQ1ankxTwpJN3R2MjFnWXY1WUVQekZiVW00Rk5PMWhsZnRqUVdvaytSVm5xUFVmQ0J2WUVGY1VoRnlqRDJuWkQ2MzlrdzZjCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXNVMWh3OEYxYTR6NThOZlhEbE4KeUlZVFJuWnUyWWcxSFNsUVRLMFRpV2NhbndkL1ovWjl1L2p5Q0V3WDZ6aGlVdkRocFp1ek5rRmJUZUVqMWZxbgpJcGo4S2NDMllvMi9NOWlUUGhTTEhDOTBJelJCWi9zbCtuZlNaN2VYcVhNMTYrNy9aQTFhMlJWM1RHdzJJTmpFCkgxRjlFWnAzVkRsT0tud0FsS0I2U2JNeVhSUFVRV1pZTUZSZVhDRVowcDFYNWVnWi9leXpFZUE4cU9ldjY5UngKTXQvMUdRTXlCTFNFZzFXT0R2a3A0cjZLcmh1Z0dRRGFmK2ZFbnF3YnNYQmdubHpxYkZ5eEwyQ085NzdJTUtnbwo0R0VtQm42NXk2SzJhMG85QVUwbmpQTG5Ya2RWc2pnaGs5ZUJBNk1VcFQwakNUZnZReEFuQjRKNExhZ2dJNG9XCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejZnY1ArdVllZFNUS0w0OTJNcG0KTXE4TUhZOTdJREU3d00xOEs2VzhDWXpiWWlzODBlMHVIbHUwY1JTRTNRazVMektLeEJnMnY4ODVYNW5PVjZFeQpHNXVtWjc3MWZJN0FSOURhV083RU54c0hhSVlYQlgwQWFmaCsxY0JQMTBYUlFEMjVQYlpZYjZsZGJuY3M4Y0JSCmJlMmJCUEJLR2dzc1cvT3FyemxBbFpGbUpLQWhKaFoxT08xVUFsWFdVSTlocUtaYkNiMWpSWk9PQ3ZzdTNWVlcKQU84L2FnV1dQaEwwajN2Y3hjdzRvR1M1aWNJT01tMjhibGVhMUgvRHRvaldMbmtSOWF2S2xsVEdMOVVYbnVtOApwU3JQUW95Y05CMHBXd0dBcGZOdHhtYzR0eWNkQS9lNWhmVG83azdoTzhqMk5mNHk3SVpKbUVJcEVrVTQ5SkQzCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnNiZnpLT0ZEUzJCNi9TVldLQ00KWFg3TE8vNml3ZkdxS0ZwZWgrbFpHdFpYbUF3K3VSVWxUOGJESWhYcUxGM0VIZENScVhTdGp5bzBVaElDTjlqSQoyTGM2U1FhV0JBb2hyaVJocXhwNFhDV3ZHNWswVjRLVUMrQzdncWJZem5EcDZmYlExVXM3ak0raHBTZEQ3bElJCmdIamcwTEU0U2U1bG9RN3VGZU1yOVBobTVEc21vUENJVXBic21ZemF0VmZoNmhrZGlnMGQ2Ky9QeCtidVF5MGcKS1B6c2E1TWlRT0xoOFNmci9jU2o2NEpPdkpVQ0ZYVTVteWFCM1phL21ZS1VsRGNIRnBXRVVuL1FINGlsUVVDVgprYy8wc1NPNXdKYXlXZnNWTGlaVlhaeDBjS090eU94ZFNSOXM0SkdoNklobjc2cVNId2J2MmZ1QnVMTVNEZXdiCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbW5GUlhyYkRTaTl6TEJkVm5UcUkKTWlHTER6RXFzVnNnME5qcGRhUmVnNGgrTEFUTDVxOWlha2Y5YktDSVdSampkcDY2YWN5VDZtYyt3V1JPeC9FWgpoZzJ0cS9mOWJpYVNnYWRZM0pjaDJ0bnd2dTJjcFBPWkprU0x3S3ZiQWVBVURoMVhTV0pBY0NROFBKbmc3UFEzCnpKZUM3NjlZK1U5U1JrZ1c5VGpsT2J2OWp5UmlZRE9IYm00NGJjempRQldUMHJnVmVROVpUR205KzQvQXZHZzYKRE5sdVlTMWo3NWt6d1ZUMWU1cXRsYy9Oc0cxNnNiYTgvWExtK3BrMTJqT2YxWFcwS3dMalFCWWRhSitSK0VGLwpLMHN1UDFzMHAyc1d5aG5lc3BCTDU0V3pVU2xHMFBhNmlrNUJ5NzRYTEZGQm1zS2FpQUpxMUpSbXFBMmpjQXNCCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2pMempLYTdEcDhoYUZ1RjRRZk8KMk1oZTVnQ21lQ1EzS2d5ZWpUT1doQW1yS2VjUFBKOXh6enNlYlNRTTZObkVXSmlieGRxZDhpdUN4N3g3TkhrYwpYQUhuVEQzY2hyYUNJMnkxdWlrMHovUWd2ZHB2ZENKU09qQ2ZUdzBvTC9BSFpCaFgzOVRkRVR0Mkx4aHFnamllCmxpWFArQll3RFhZMW9ORkxDSDhmYitqTVQ3dWp5V0dtMnBVRUtNOU9jWUdIZ29OaGYzNnhPY3UwblRBTzFiUVQKQVY0a1Z4QmRibVdQMEtSY0k2UFdHcU1GRmRNeWR2TE5wWFRJUmluR21LVlk5MUFibVBhV2t1NCtidFZzQ1RFVgp4ZWwyazhyUi9IUUtqRjJ6cGNTUlhaUUJ6czhzT3Q1M3FWcWVBTUQraVJCVGtqSktRSUtEeklxTDRMMW5Qcmp3CnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTJpVFpPRTVBN2Z3N2Z1aTM4V0kKeXRIL1M4ODdkOTJPN2VEMndnRUFaQ3Y2MzZBN2JoemF0MUljTllXWk82eGMvUmMxVHpHRUZJbGExZEpnMTdhTApicHRkeWExUXVvdUJpbVRTUzA4aERDOHZ1Y09mZkFiR0UwN2RPY0MyS0tHNXhKZktLVWZiWmE0YUJtOFVudWRxCmJqd1pLZ2s3UEF1aWs5Wndzc1VqR09IaXpJUUh1OGtudnFCbmZlNGtaTUZrLzdoQWRVRlllU2NJVGVDR0NjZEMKVHZvSE9YWndubWlmYUdIbC9sRkhtUytaak4vaEVRc1J2MjBoWjRLYzZhdGtrRXdBdVhsQ1A1eGM3S29ydk1KSQo3QkE1NGxwMVgrZDJWbW9EWjMrTFY2Q08xbTV5djlwOWVrQWdQR0dyZlZ0enpEVTNnRTY1aUx6OXVndkg4cHMwCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUVYQzQwRTRXVjgzaXNQYWs2akYKYkdEUE8yNWRrekpTYWhRNndBYW9paVIwYkZOSTZPd3JqaldXcnp1cnpHc3hwNk10Tlp2Vmg0dnR1djM4VlFzNgpPQnhOcjNyVGtqV2hLYzJIdHZjbEtwVjNJSzZDSWt2dVRZMkhaSDFVR1A2c0pBNFJJTklnL2R5UTI0aWhPcnovCkRJeDhwd3VlcnY3WmxrcUVmLytFY1VZMXBRRzZlNHQrQ3Zya1hkR3NjMWNzbmtRK2wxKzBrUmVtaytDTjFJNHgKdXFkcGVmRVV3RlRoS043cGFzTVJxMkYvY0JBSEpQWFBJaXlrclZ4aU15Z2ZyL0dzNnlGTG5PcnpKdm14dzJSSQpXLzJjbEU1Y0pvbnRFcFpFdGg1UFFIVThmRVZmWUlEUFZZeDFJU2grbVhhODB3NVArT1ovSDVPVmxxTzNDVVFHCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDdMaVVGTHU5cS8zd0JSOGpUcC8KWi80Q3loUkFvaEllMzRTT0xZK1paSzRkSzRtc2g3WFliU3hCaE5zS3l6RzNwMi8vUTVreGo5MXNQMU5XbkREcgpIVjlxS1piYzJsei9qREVra1hBcWFpMWJlZ0JKZlVGelVkekZiSlRmbmFsS1VnL2hoeXlUajNjelNqRVpQdTJrCmFvM3hCbDZ4amRQSWt0cXNOcDBSRzNPRW9LUzJrdUFaMXRtZUp5OGdzQ2NicjdmTENXWURiTnk0cFlPR1g4WUUKWDlYQzk5NGFtUExKNUxBTmI3bmJBZGVENnowOTAxZkJxanArSWFIUXptSW0zRCtqOVJoeDJ5eWozYmRRT3lSagpQZFFUb3JUTHpRV2RmQWdiczhlMFNCTER0akRKbHJFaS95a3Z0enlVSWlISkk5WWF4aVlYRHhOWEtEdWFrOElkCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNU5MOVRxV3FIYTh0U3dyekJOcWEKeDI4aDZkUUE0RGtCYkNKajR0U2o1UjBwcjF2K0p4Y1pZZG1NVnU1dnZiMWtRZHVJTno4a2pJNytNbzN1RWZoeAppUFdtVUtnOXFlU3pveWp1cFRnMkJBeUtKWWNscVVhTGFjTDlXbGQ1VU9OQ0VQNG1Ka3NIOEp0MTFFVW51eW5hCmFYMWtZbW9DS3pBdkJ0RzlDZkNJYTFGMWEzUld5WGhUUUxYd3pQUW9DaGYvRzRwbTN0c3VNMmw4MHVsbEZEeEgKeFg0aFltOGpvQXlzdjVKWlEvUlR6RnMvS1BleWRFNGJ6MW5Oc1lUZ21WQUNrUzFPY2ZEVXBEdDR6Z09POS8rZQpxZGVEQU9tdjNtbFJaeWdoSXlURE9zNHVVOG51WldYVG84Mlpra0s3OHhocWhzbGxuNW9yekZWOWY0TEgveUVnCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBazFjZVpLZ0NkcXRrSHBtUWFUcUwKOGdtTEk5WGJFZWhrQ0ppZFhGNXYxZzJxR3U3bVVZVi91bVdXTkxaNVUveDJrU1FrRXVsMmJzNUVLY2w4SDdycgpoK0hmMWR2VnBWSE9XMHozYTZ0RjV3ZXd4bjFjbWhlQ2p3Q2VSMmRhdFRJaHIxY3BkWlMwYkJPSzRzWTJOTWdyCmhScjBycldtT3d3MnBmakFNTUNucGJxclQyQTBxYzRSMStoZXg1b291S05HeXpZWDRGVmJEa3dTN2Q0ZmdtOUsKMGYxb3BmTnhCN1ZpK1pnMlZsbXhmNHY3UUduSmU5ckRPUG5mNmhVWWRyMUFvdEtQZGcyUDYvVGNYTlFJU0s0bgpBS1V2amNEaEFRektib2o2UFRwZTFpdUt6NjBhSmZERTgvc3ZFUEJVRFFQVjN3Vmk2WDlUWlRxYnlLbFpyWng4ClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3FNMUFGbWliWGd1V0htR0VBaFgKRHZnM1FEUHlKVlA0aFoxS1JOMDFjcG14RUZLOXk5eTc4Sm1ZaFpCMDVkQnBxUEhmMHJVVjN3NWpFdVpSaWpGSQpjanhEcnhlM2duREpxQmtMVVppWktCazJhUW96TXlkTXdnYlRudkZoa0ZRT2hzcG5iN2ZjNW9WMmF3VHlZU08rCldjaC9YUldMTTM4ZEtrR2lEM1c5OU5GUWJmaEt1cFNBSktHUUJSbG0xS0NvL2x1N3o4T253YlA3OTZjeW81dDMKVWJYQUdCbEVtVmp4SS94SHhtengwTmwxVDVjU3VXY1NrdkFUekxabHYzRDFrdGgzU0Q4aGtuYVlxMVlRRWhNKwo5SVNPVm9zajlBYzZHQldtbVpCcDJSckd3QXV6aitiMzFwdmdXRlNUMnhpN2dieFc3RnJIZVJWUXVGb2hzamtCCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGlHQ1paRGZCT05RNXVRWlJoRHQKZHhjdkpVSWZVUjI5SzJ0MHdPOUZKZFVNMzQ1K1dBcUx4S3NoOHB6di9GeVZyL0NBL09FRmtDajNFTlVyWDBrbQpOY1gwT3VrY3I5OCtpajF3RmxPR1U3Qmw1SXowRFhCQitUMXZiY2liTDE1RXorM3lQaTFsbDc5Q09wL0drVnNjCkdjTUc3Zi9ZelNDdHdhTXFidjZBSGtHUWI3WWNiTmhJdC9BNDViTnQxWFVveW92ZXIrejVWc2hvVlIwNmpZV3kKRW81M2NjeC9Yd2QwaHk5MGxIcWZsOU04NDlncENmOWpCekZnU1ZVU3dJcys5QUNoUG1GTGp2NVRHZW9NVHdaSApHNWZWSGxoZ3Q2SGpSNXd5SE1PcVZoSkg3RUxyMlY1cVE5cWo5UDhSSDQyc3BqS0hWNXRQSGQxWForb2dFR0EyCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDdubzRRREZjRjlKNHc0WjlkTTQKRmFldGtkWVhZUkgyVWpDOWVZa0g3K3RFSVplcHp0UXcwSVNVK2JWR29pakhrS1RwL0IzQTZnUHJqMWhQNlBlSQpJd00rcE90YVBPWlliOGpKRW9GT1JGQWxhYklHR29LYStYcmltQlQ5UFVxdmR6cnJQRCtxR3BTQStKMmw4VWthCndDdXNNVlBqekZMb04reVVsUEEzVFNhOTZ6Z1QzN1paZnZidlE5dGtmeTBla05RSE15ejg0cWk3VzBvUUpxeFAKWk1ndzMwU1dDT00xcTNmVWdiUmMzb0hUclhQT2JHUDRMTnJoempCL2M5OUMrUGRldHBoNXQwRUprYW5oajU3TQpBRGxxdTRBZHZKRFZHcVIwRDFVZjFiSDVncGUrWmt1bUNJSWVoMTZMZUoyNkMrbmY0Q0tKK2g2R3QrMytwWE0zCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE5NRURxU1RsVitROUZnNVNadmEKSE80Mkt1UnZCL3hqeW9Qa1BydnVOcXhUTDY5OHR0UER2QzMwTlE5cXBXZHBFcWlKK0RBQzlpai9zemRtdFFLdgpIb3kyRWlqdkxmUDE2aXJTM3JxVkdkdU1YT3BpdWVVQnNqaEdEUEdxcGRPOXhxTGxQNHZZbytrc24zM0tkQ0twCjZNVGd2TkdCNG1BNzNVNFRYMk5MdVF3dVhQRkFkNUxsUEJ4RmpqUWNQNVR4SVFMOUZyTEhEemRUTlV6TlhNZWYKRmFUTHcwM1hjZEsrWWlhU0h2cmxPQTNpRlFRTllpOWFWc0FsNG1RK044OHNHV0E0T0JVMkFlLzJnV2NVQnF0NwpLTnppS3NmSUtvUU9GN0VWQWkzR2ZURHV2NFg4SU1vRlNnQm9lVTRXajEyNlVNcjNvSkNibWxVSHZZdnJsUlg3Ckt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXQ1YjF0Vi9VT1NoZlFKTmViL3MKL0t1WS9lQ0d0eXJ2ZjhlVXZuNVp3c1loMXpZay81RTlqOGFyL3JxWjhqVS9nSy9ocEJadXZqc1NJaFZseEpxagpoaGRJcW9NKzlzNE5OVm9ESHJlR3IxYTFZTFBzcG5Gc0FnblBnSUkwbklqTnplMXRTUEI1ekR4aFk0cmg2bHZrCjk0WG5ONUtNbHZMRGtpOStnckhnLzVlUTVTaVlSTXBEM1JZWnRmeU5JY013NlR0bGluZE9VQlR4dWRwYldIeDAKWmQ1Yjd4cXFYOEdyTkVueTE0UlNUc09DZUVtSnNOcGtNeXJnT2pLcStscnBheXE4bDNocURRY1NDWTlkMkdDdgpqYnpDaDQ0T0I3MlV0RXMyRklLVHpEYmptNzUrMEhvamZtUE1MUzYxQjlSQnp5ai9VRC9Ob1hPVmpRNmxPT3FGCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWdoZk9CVjNqcDFMZmZXak85VW4KaTJNYWNxUjhJcUVkVmx3NXQ1cExiMkpvU1kxUnk5UDFtYUFPQjNOOVliaG14WmFLREdzNW1GWVV1MSsyS0VUbApuakQwNHIrQ0NIOG1zbEVtSmNRRkVORXhCak9VdE52Q0cvb1YrbGw5Y0w4Mlp2TXlVaHMyR1ZzUFc3WERDY1czCllsSDYxL3FsWDUyZWJxU3AyNG01dnozTzN4VkNPQ256NlZVN2NUbHAvU3lzdFRQM3Q2UlowMmdNay9LaEFUUGIKMXVRYm4zZGJGVnlBK3VWeFJVUi9hSUJ3OVhMS1FiWmZLK3A0WitNRm5oU0t6cVBlZG0xN1JRWFNMOW96S3dTOAozTDFWc0cvSFUzV05BblNzdEZMMU5oeWVwejBFRkE5UWNPbDFPdkJNTjI2enVkS0Z0TzZzclpJUDBwZWtjVnBxCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVFacnl2U2tEbVM3b3RSSFRidmYKOVBZa1lac1hoNGpWdGxaNmJBemI0czZJcFpjWTY5djZGUjRuTDUzVzVvRzVtRWx0RkE4eGNsVjNLbFByQW01OQpaVHdyMnNvdUcwRnhqUEpqQU1qSEtaK04xYUNwTjJpUlk5d2NwYWRUU01vb21Zem5EdnZZTlhGbzhBVlY5VFQ0CkVzd0RLeGlTNVJZY1N6Z1hGSWlLTDc1djl3ajJQZ0pEaytKYjF1cUZxUkFoeHVOc21aaWFBRzNra09rMEJTWnoKYUVTUUNvbW1QOHgwc21hYVg5bU14bkFnTlhxUXFER092ay92WWRvbGVPYmw2dVJaOEZIUDNhbkgveVgzNWxwRAorNEhkeFlvZkt2a0h1cUVIVUo5UUtZOHAvK21Nci92MzlxaWtOVTk5NGVXQXk5M0tyYy9MNVc2eEQxMUgzdXZCCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeExiTUJSY2VPS1JNYTFkRTZyNlAKQ2FLY3M5M3QxV1UzbzhoalpkYWRaNUU0UForZUt3TWl5c0x0eTZ4Ky8rcUt5ajhCS3BTUGE0TEh3QUV5bUtVOQp2My9TaDYzZ1BHZzR4Y0JDYkFrWVpxMk5RenA5QXYxNUorWjJLWGM1elo3akoyeVpFRE9iNTZaYXdlQWpzMllYCi9XR1A2R25SSWE3M01YaVFORWp3QU5XMFBQNUpPZTM5dCtKQkNmN3IvZVhsWWQvUXRJdXlMWnovOE9naUEyTFgKeTJBeTIzbHI0cjV6UnFWNlJoSUNLRUFpN0pVZER2Nzg3dnlVMnhiUTY5dC9HRzBRSjg3V3oyNmNNU2xEQXNVWgpMaGd4eDViVjVmeFZIVzFiay9Jam9kc1Rla3gyajZ0Q295bU1CanpaeDgzSzFCTkdzUXVmeWp4bkluRGkvcEFlCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFN1WnRSQVo1bGY3S3NoL2xseFQKWHdRTjIxWjhwa1pnSEVjc1YrQzhDWG1FckNqdk45SEFDN1MwMUdrSmZjc2xZQVdhSHZDRjJMcjN0QTJhNHMrTgpHMTh1S0JBM21PM25KSVloVjRrVHNHWThiWnZ4TFhHZXVmT2JTazVzRjRaL2pNMSs4dmwvRDBqTXFiMEFwNnZKClo3ZzgrQnpzcEJxS1FOM0xQSjFna1NrVjEvOWo4MkxRZEV6Uy9xNHo0RktEaXZreld6UmpGdnNhRzV0NmxnYnYKWkNBbFdJd3hIVE9mb2ZJbnE1NTc0VjlHMFhpNFZaVllGWnpIZzdYZktrUG90aFNGWG13YVBqV2lvdXVJVDF3cQpHRmpjeTA0bU03Sm90TTdJeEdpQkk2Z29tWUYzem4zdFhUbEQ3dTkrWkcxY0tUaTc0b0ZYOHk3RWNHNGp6RHo2CkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEJCU2lPVkNpV2s0WEcrS0N3bWwKS01RZVRUV2R2TEt3WXVTTzQzQW4zOHVBUVptN1YvUXBJTUJqMUovbkVIZFNSK2N0NjlmUzFzTW03Rk5nYUliYQpyWFpBaktxVTNybytBQ0pjOXhmN2xsT292cnlTWnpuOFZwQ0UzSVQ4bW1neFYzQ21kUDg3cUd2T2tzbUViNzRHClVPZzFjdy9aRUxBbEtCR3VmYmUvQkpGQTZPS3pIdFZyVGlrSWJBU2ZLaHhkK25oUDB3L0twc0htREt1bmNqU1kKTk03ZGcrbFhwUlBUU3NrSm9LbkVCL1BJWloxWkp3TUcrMWJoZU5mZ0lia0xXTU9WVDJrbmtWd0IvUDhjMm1hRAozclNOQWw0WkVnYXF3TTVxRTJ0dWF0bXA1SlRCMjNQUkVRZkxJeGFzV29oMndlTlVlVkFTZ28veDhJUXByVGtICjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcm5rVVNkTGd0R2lmcHBFYXNIbDUKR2hmYW96TGk4QUd6b0tLd0xORnZMWVcxemRmTFF2Y1dXd2d3c2R6MXdMdmFWUGhiTnVEUFoxZmlnNFVCeFlvdwpaSHpaenJQMzkzM3QxazYwRm8wR0RSN1hQTDVzdmNjcFFxcFVXWVVwK3JpT3ZWU2NHS3FqNkdvaTlsTHcydUFqCndsdVpCZzZvdjFlbW02NkozQUpUSG5MVGE0azZkVzdIYnpyRTlONEk5RnNpYnhMUEM2dHZKRmVMVVJEd051MWgKbG5VRnQ1amEveDhOSDJvL2l6RUpvTE5pS21WeFlsOGhEdGd4ZjFTOG5obklWdTByNVg0LzBaNnlNS2ZrV0FiTQpWNGpIb0FneDJnUmJxOFY5U0w4MEJmZXEyc3YwMHh0QzJXYmFqYzQwcklIdGgwNmh1bHZ1MFlrNXZFQ0t6RXBWClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMC95dHpvdDZUNXRRc2JvWFlMaC8KemxmUlZ6K0p2dUdrS0dZeDF5c2E0MmlaVGtLbGxXeGtDL21iVjZQVldBM2pBQ1VhTGRLcUtMejF3cHo1OVN6OAo1azlBWkFTUHYrMmpxNHBsVThrS0t4Qm9vZmtlSVhJdWFsdnh2MlFDLzl1b01YWGJVNXVUbm9TVDQreXN5eDhWCkJ2MDBNNjhoMVk5Y1Y2bFBPVXpFeVZXNmt4ZFJnT3U0dkpRSHZVb2NPVHh2b0JuNjJHTENaaDNZSER0TklGb1oKdHJNdHh4aWQ0dFBTNVhEbi9pVGVLMzFvTHFHK0RFOEV6aDdiRUp1RWR2dTI1dVVidkhBWUdoTUhyc3FLeE9zcAo1S0UrWWVOcVJpUUJmbGtJd0tEMHpHYTZLVkZ1QUNTclFucG5xQ1pqOGJscUxrMU96RnZvd0JhK0FIQkpSQzRDClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDdqRlVvb0tsa05ad2N1UkFYYXIKUG9lUkJJUnd6RjNxWUUvbDVVUS8zQmJWb2FHTDkxOWlIczRMZElLSnZ2dkpQT0g3QzJHSWRqYnRoc1JrSmNLMQpuWkI0VHdraVdLOEQ3MTdwOVl3NTI0cjl4dzR4cFVObnVuRUQvcmx5c041cThtdTFrV0R0bWNhUW8wVXdkdS9sClNFb3hvS3JQTlIxM2tkbWE5a3BSb09VZXhRcm9rdkhSNldnZVNZOUcwZ3FvbUVVRHZRcUFxbk5TQkFzbzZoeTgKY1FuWjh5NUl2ZFVKeG5BanlTZUpjL1BBdy9VZTdGR2QwSkp2a0NkWVZkMFZLTlFDSTVaaGJBc0Z5TEd5aDhuNwpNa0dPRmdnUGx3c243Zzhuemx5RzA0YnJkZzRPcUxybjQ3c3kwcG9kK0NORHltOTljaS95Y0lRSkFPYzdtWlNtCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkNIdjN2SGJzOTByZ3laZXJJdkQKRUxOK2grcmV3bGo2NXVxYmdQZUV4aU56WWhsSkNoNDVWODN6NVBsMWQxREt1ZnB1QzFqcG1QUkZSRWJaYlQwaQpwMUdvTjZSYjM1TVBMUmR5ODJDY3pDRHlkU2RMdmtHK2EzejROYlQxOUNNR09qc3pjR0V6a1dLeVJCYk13OGx4CkFLZ1JGNlkzTHRWbWpudldUeFMzTzY0UGV2OENMS3FCVDduQW1QVHQ2eFNrcjVVdjlVemsxYmdCVmkyUzdtUU0KQzJaeG44MmdtQ3RnMFZReGNUa21BRmluNlVnSUlPTkQvcmthamtjaVhKNExGWWwrS2F3RHB5RmJmam0wbWtlSApzbHRDanpIMW14OTcydnpHMVpEbXBVMXBkTjAyb3NndlVWZFZveUtQbW1hb3BIc3ZmK0g3Z3d4TTZTV0pQVmluCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2hQYzlMb0szUXVWblEyQ0xkem8KWVkwTVA3MkFqUFZjQWlJMWIyamx2Sk1zVlZHRUpUZUpmQmF0OHFWZnNrcWZXYnFQaVJnY3NmcEVUUHB5emdoRgpnaFVtVVJlUm9IYk96OHhRVXg1YklBaDlyZ0JGdjhpRWErRGZXaTFpRzI2QThBL3U2c0tCN1ZDaXRqNXlQVmpGCjM0aHI0cHpVVG9vRVB4c1BEeHlzQ1JvU0FpSVp5UitSUmdOM05xTjhCYU43cCszN0d6ZDBXVDdGR2dsSVdWckkKRjRISnJ3bDVYMTJuNjVLaGgwOU5VNDdSbnFHK1VwMm80R2JKVWxpUW1WNGk1KzVuSHhuUFFaZ2d0RDJLRVJvaQo5amNrZVo0dGtTV0hzYzFGb2x6SmsxK0xiNVM1Vm5KYW9XMFlYM0EwRTk5KzNMNW5tblhhcjRtc29ONytYU1BlCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUR2ODJzRTBxeGh0UTBYdHdwS0gKUFZsM2c4bE4xT2YyR1Z0c3luam1CTTZWSlFmaElSQ202YzgxRXI0Nkc5cUxQNzJaOFVzQnhhVDhTZm5CTkxDVQozT0VYTDA4U1RlL0E0bkxzbDNXNlRFSjVyNzlOazNZd3hZMmNJU3FieU5NNXhMT3RSNndOZlRlV0FqTGZNMHhRCmxHUUxnQVptbDl5U2VYdzEwL1duM3BxYSt4UGovTFdxbUUzd1FuQzZHWDhLY0EvZFVwMlNvR0ZTZHZzbDM2a1QKaHNTTnExUyt4OExlQTZSRCt1cXVTYWhXeXA4SUJldVl6QVFKVHV4TUxhbkZud3laTDRacjdSWjcya1JYMUlWRgprQzEzSGlQYTltWVFDWlBCdTJrTDdYQkZ6MVJNck9tWm8zK09lR0hiUE1GdU9xMlJaSnl1aVBtQWk0M2llWmp4CmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTY3elpwMnExOXZRZlowUS9kN2sKcldxeVpBNml6MUhTZWFsbERGdlBEdFhnQUhvVnJiZUg2cWd4UUF6UlpEL3lvS3VOTzBDWVI3Qk8rTHhzVUw2Ywpacy9icnFwMWRjNi9GQytSU0JsWTNOdXFNTW1xanhFV2R4WHUvQXdFUDYyOHhBL2NFSFpXdmg5MDUvUVlGcXVrCmxMbkpWWkpGT20vdmIxWTdoVmM4RFRIRDNRTm5MN2d3ME1xNmQ0SjQzNHJPVUNUN1A5MFJyUDRXeElJMWNCdkcKOHlCamxjU2VNOVcxcUpiZWxGQkxvNnBrdHQ2SGdjOXEyY29zMDJiVlU3TWZuYzk2eHdFVW5Nb05RSTFSSjR6SQpCRHF3YW50dllXSmJsMzluVGthWlAzYUNGMDA5M0wxMUNQT20zcDB3aDNzNmF2bFdkYVMvTFFyUHNWVmxBbDd2Cml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTdiaDBvejhUWno5dWpjS0hSelAKcmdaVmUrNWYyb0tocUpUblBNK09NK3QvcGlpREhqRm1aYUVBNkJwcXdqS2J4TzVsRXFwaUFTeFRQNENNZy84ZApZbTFMS2srdUJjbk9lNTMydGVEeE1JSVBTOEJKL1BDQkRxRWFldDQ1UERkTC91UktIckVaVlU2N0gzcmExanFHCm8wSE5hS0ZCN1RLQW5nSzRUWTNsa1d2YXM1VHhyMnZ6WEROTXhpc095WFFIZEp6eWk5NmUvUnZaNkZYd3RPZU8KL1JoSmtzN0JIYVdkVnRucU1ZRnlpaHM1cHVNOE8wRmQ0Q0h0S3A5bWZRVnNQdWRoKzBMVUJ1dzBGK3U2QlV6bgo5L3kwSnlBSjF5cWhjaWpZYmFvN0VOeWJ0eWRVNUZwdVB1eUFzT2ZOS01rb2pubHQvSWc4Q1o5N25ZditvalAyCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdU92RkJKSWtrOEhaY0VvaTNEU3IKVGJ2OTM1NDUrQVVQSmJLRmhrK3FwdHJEUnUvS1dOQ2lnckI5Zzg2ZHRMbDJoeWk1bWxMcWdKYVU2UGNhVWJ1VwpUNkF5aXZTWTFXa21QcWVoR0NIaGJXQW1TaVZhNGtSdkZzN0xoUU91M0FSeFdSWXh5eUREV3liclZsTW8xNVhaCnZxZXJXOTV6WVJCUVoxRWRPWUp3emd3UUIvSCtMUFBhdm5pYW9BUi8yRDN5L2IySk13YTFMczB0c2Nwa2FlU0EKRnhkbWF4MExQNW0vSEIva015c29ZeUMybkdWU01MTWlydHBwWVg4d1JFOFdjeW45WEk5cDA0RjdLa2p5MSt2VgpQQVI1YjBUTUhqSitEclhHSnVaNnJ2R2dPTWpUd1NDS3hmOTNRTnFaT2NndUVnUHRLU2ZZbVNWOGZhVXNsbi9XCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkVWdDFtblNBak9sL1RwTGJNU3QKc0FXcHR3MGV0NFY1MXNWZG04MU9OZjN1Mmk0QW9PbVJvQmZZT3k2Wm85eks1THFXSHlTY054d3M0WDFsMUJVMApIK1QvYVhSV2VRN01YVHp2empPejBNMzNQTEFzcVptczNjc0JwdjVBaTRHRzlZaHI3VzNjbTl2ZlBYWXk0RXVlCkNnaEs1dFhsOUNERlliYnpPaTFDeHQxSUZ6ZSs2RThzSUlncUtKZzFERGtNOWZXY2RQZlFCeHhGcStiL2wwQzQKVC9HR3Vpd0NHaE1NMW5RZUQ0WSt4NzEzMGNSQU5hWkpBNVJQZ1I3OFdNdndPUDk0OHE3QlJ4ZGZLZlQ4YjNBMgpQWjFaY3RrdnpLb3kwYU1RM1dMWitvdTRDT0lEanNlSENhb3d2MyszMnNEaXpQbUcrbmMyZzd6MGh5UTZwRVRYCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkdxRUZFcFo4TEFmcWFlaEhXaHQKY1d5clpBVmJrMEpoWSt1bWlGdkpFZjJGaG8zTVhBdVp6Q0Z2WjhVOE40Um9HYWVsYURDcnZJdW9icmE0bHBNUwpnbGtPVzRVYkpkY3IzbEtFaHZTbkhIUDlwU25mVW9LM1dSVVhLVUZTZGk0Q0FiMjlka2hNdU05ZGtJSmI0TCtBCkNZa0pWRmtkNGd5TWRUbGFrczVUcW11QlRWSm9iU1pzQmp2eTFwOEVWd0V5bTRZME1tT3FLNGtscmRPeGtXb2UKUkVjM3lHNmtETEdkckxGRnBIeDFPVlVPVlV3THhyd3U2RUpyZjFCTUxSMHJBa0ptY0F1NEtUUXorbElBVTcrZQpLcnZKemxPcDRldjhvOXdhRk44cmpDSnpxVmtZVDhtOGd2VlIzVGE0SE5rZzBsdmN6ckpLcC9wWkN6Z0Z3b2FQCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2M3RmZ4cnR5aDZXN3htUE1NaTMKbGdwVDYvSXhrd0FwOVdsL1hxWjdjT09ub1VFRmFreWU3Vm05Wmd5U095N002bEdBcisyVzd6MnUzTFBYUTVRbwovYjdkcEZYUnNhbjdrT056NDZ0QzR2OW9yMzNMRTkzN2lKbVVDc3pzR2hMRHFCRW1QajVzc0hvdXVqYitRaXFHCnd6RklFQU93RlhqcTVOLzBMV25hSmFCRDNMT1dIdURDSmNQazc1S0lqRXFsZytvVEQxaGxnSGJNRDNleWY1SW4KTU9zdjZaUWpqQjZHRDdWdUt0bWJXMjdBTE5OOXNsWHFEY2hkRTB0ZTh1VHVtajJpNU05eFZUbWNSNzJNYkFGYwpqVFRFc0h1L1JmYkVWU0FhY1VJdEJtbjRpL09oUjlaRnRXbWZzVlUvUUxQbjRjSmtCYWNQTzFZYjZyMjdIQjVjCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc25wVmZjcTVjWFNLWllXemhPdU0KQUF0Q1BOMFR2bnpHVzRkNjlZSGJKVC9oZGJoemU4U3lBOGlZUHhocXAvK2RvTElvb1ZoVkFqZG93bzZkSHVkUApmZjF6cG5tazNVeEQzNkM4REFJYWdDazZseTFQUDJQMS8va0hDVkR1V25qM05uREh4UGM3RjRiOFBrNmgwY3lUClBMSXZJZmJ4SVBTZFlyN1BiNzNYdWZWV0RzU0hjUjBZcTc1UStQWitTNjFhQmJKZUoxUFJ0Mkp6TURPZTV3d3AKbHdDK1RUVC83QXFVcnB2NDJiYW1FZWQrNVgxclNKVk9lYVEwYU5sTkJFZ1JmTmU5Wkp2cE1iYSt0WWIrQ3lqRQozV2J3T09XR0pHb3ZCU0Fwa2crVVhwT05ydTArV2pxcG9NM0hTbk1BQVJZL1FyM1hQWVlvWGxTSVRzQzZDb3NiCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczZ4enNyYmRDZkJvcXZNMmh0VS8KbkVheEczd3lvd3RWcnA0L1RZaXdnaHJyczR6VkxUQXdTdTE1TWJ1UU5ZdldVbFdtOUNBaWFJa2JCbGlDbjVMUwoxMGhiWXJ0MVRoUjVLVlBXcVBSbkIrVzM5VkljYWdrWjRpK0dxdzMyMllpOW9zUmh6ZHpSelo1cXpEYzNpc3JFCktWeVFucHJVMTU0QzRFa1MyUzVPYk1RTjM1d3p4Q1JPVVd4b2lnbm1kN2ZrYmtuNGdyaTRtdi85alh1NjVKTTUKbkREeU9HamV4VVlTOFhLRG1sYXdtRTdTVG91RkR4Y3R6WmVwajE3YXVQaEtJSnlsRjg3OE12dVE5NXpxM1MydgpPTUFnMlowRTBIT29PQ29PeEdablErbzY5bkpqMlZWVm1JaDI1MGl6RUVJeDgxRHJ0aVQ3c2tjMXZ6RlpzOVFEClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUpjVDYvV0pCWjY5MUt6YVZZbGUKODl2WmptVWhvNmJFckxPV05BWktTckMvb2Q3aGFOeFduaWJMVnRMbldzWGd1MHM4bzlRVXczU1pBM09zZWhLYgphZjd4dm9BNmMvSXNKWFNmZmJFSFZLWHlvM1d3NUVrR3BBRlloUG15Z1VMc1Npdk5UYWVsQWZvd3JHSEFneUtWCmFTbVZnZHlqOVoreW53NzVnbXJIZUlWRE02b0c3dzNiQW8rL0hIS0gxSGlENW1keGRFalFVQUZzY1pxZVNpbWkKYjJZL1BhMWh0RGU2WmNkNkx6UlIvOWtJdWcrY0ZhQXVwYkZPbWdBNVJ2bG50RHRVeVNpTzloUUp4V1JkeXFzRQp1d2tXRXQ4bEM2NS9MWXNBcWJwczBRanE1bmpRZzRjaStMbFQrdUYrYTVkWHhIb1ErcldPK2RUM0tIc2I3eFVZCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2RCVFg4VjljRHMzL21YMG1UWlQKWkQxS2NhTmIvUHN3MTdQbmFSMGZyZHdzT1RLdGg2K2tWWW9CQkJpa2RtOFZnazAvK28wN2ZlbmhtYXRZY3R2WQp5ekoxMDhIc1lBcnhoaUZtQzNCb1dHcS8zN055YWxyZldueHlNRXZNYWhpall6K0FmdC8zZWV4cWx6R2VZL0dOCmNtd3BzdURlNEFNNS95cm9YRmZacStqZzFIMWxLaDUrZzFESzEvTDhkRXZPQVBUaVliYzBRb2pacnNFUUswcisKdDRWYXJKN0RuUGNyaXhqWFRMV3owTzc2THhpZ2JkNnNYMzVrdnRvWkhsdmdnQUcxVFZuNGVJakhYcG5oNmVpbQpIRFVCajM0QmdNZ0NWaTJKblVJdHVUbUphd2tRZU15OEJ0TXQ3V0I4STN4SnJhM3FFQURROVN4bFZOTzZvcTBiCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFdtcmUrYzZRUytOZzJBSmxvTHQKV0swZjZILzFuVHBiYTFEcjFRWERDaFdUM28rRW5JM05xdzdNaFVHOVBXUkdLNHIvOWp6dXBrcURtQy9ablFNeQpDMlkvendOWlFxVjdsOHRFRExKYkc3c0xoamZLNEthd2ZWd1pMcE92NVVSMTdFYVBlYmVBdkg2c21UVXVPYXAzClV6Qi9QeG1nb0JDM05kMGZGdVVhVzNHbEErVU1xYUJidVJGRS9jM0RGaEZjUVVyc3lYTkxRcnZLcW4yNkUxS2cKQ3RSaGxpWVZNVjRJTjJtYmhncU5oM2RSeXNHNGpkVGd2UjVJaFk5WEJQL0wrUngrSzBuRmlNdG5NWExEOFBPcwp5Q05xSVg3ek50Ym1tVit2elNWVDZ1MGh5MllQOFRHTkltdkZsdXVzQ3kxYzIrdFBYOEtNdE0xQXFRWEdtdjBRCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0xRa1hwcE0xd3U4Si90M2pHU1UKSjFoa25ldG5USXVNOVYyNnFkSzBFcS9oblVESU9DZk4zQWxxaWxrV2RQeGV1aGJvUUJpSDhhQnBaRzRkamN6awpaamx1WjRyRFoybWs3My9IS1ZxTWRVM3QxYXR3Y1hoZkdhK0hmUU03UEJrMDY5cmJrckh1U3k1aHlNeEtpL0hiCnhSS2FvQUNvVXdxbUV4YVdmcFI1VmgrZ1NaNTdPRk5iRTBObnJ1TjJhYTVDUExnWk9yTGFtV3JxekJ1ME91c04KdzlBQzVZbWF4Y3haeEd1bFJoZVVISjZjTDd2VXVEWjZNaGNwb0dpcFF2U0JKeFJHZ1NnOVVMQjV2Z3ZpbzBOZApCR2VCaGZLZWV1dkUyaE1oMkplNHQ4REJxZDFmdkY5Z0pBUzJlaDMrVXFyT0tPSDRGOTFwOWI1LytEWkpPQ1AxCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFBhTExVQ002YWpyclhyRDhxVkQKZmtMWDJObkFaK1JrVE8xdW1EMWNQYmh4bnZRL3JHOWRwWmxBeHJtYmVzbEY4cjE3UzNUUXdJeVpycjlTaXA2QgozRGlrNkZrSGwyNjFBVXN6ZExHVUpjYzNCMllINmcrRk0wdUw3dnp0ZEgrSzdaTFgxKzJqa2x1T08yQWJDWW9SCk9iZ3plaWc4c20wTEdKOWd1Rks3dDMxTVpoVDJXdUd6L25GMmh5K0pGakJnbzBxMGpvNGpSZFNrdEh3T21wMWoKMjh4WmJKcVdpODZzL2JUT2paY2J4VG8weTQ4NmkwK0tJYzlwNnU5UGRzUjBoSmM2TGFaWWtqUkpnTWk4NVN6Kwp5UW1RckpxVUo1NWtnOGNwQnJkOE5aTUVWYllWaFhidW9yT1ZVUUU0enpUSkN1TUc5d096cm1vMzQvVTZiT3cxCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK3B3U2JDaGtNTXM5OXY3Q2M2cVEKL1VVQ0ZUbGNzbXFUOGtqTnI4eGZqbVJ5dVByMnJWclJTTGViSVc3YTlCQUU2bUZMejliK0ptN2VLTXRyeUtPZwpGOGR3eVJWK1FWdWZObzFhUU5pd2ZOaXIxeGZBYUJRc0JsRHJBVE5ERnR5UkY5Z0R2MHM2UThUMCtIUzMySnNtCjN2SlN0WWNwNGNqZkRuV095aWZoSVF6c1ExR1Y2MmlwVy9RZ3hOMEJvUXQ1ck1RS3VXOW1zOHFhL2NWWUdyaXcKeXJhUmNzMXRFN09jcDE0MEVLbUFvSldNTkR0VGc5Nmo0R2ZMSWMxYjE4WGQ1SGcrdnY0UUF5N2NDZGtrZDhOdgpWN1NwUUxpT1h6VWVvZGRjV0lkOEdZcnEvM0k2TTVCMzY4UTlWYkt1eDl1eWlTMHV5OE0zMHZVeDZDcVpadEQ5CnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2RHeFBSMDlqM3l3ZXNSVk9hK0gKQWQ4Ri9NQXlDRUVyc2s3VWxsWTNaQjUzNXd0N2xZSTB5Y3B6cERWNjR3aVRNUmlIT1MwdU9TTXN5Sm9CUURuWgpnM1VXNmZNNkZNeEVHWE9xTC9EWE9VaS95ZjM2NGhsaWt2VS9rU0VITUFVMUpndStsNWQ2UFVtdjBQd0JHOTJNCitEYTMrdmR3MEJIM2psdktYbHNPRDZYb2pOSjM3UUxEdnIxcVpVNXRJNFVVSUZieVdmUGNyR2dXaHN6RWdISHUKanJrNFY4ckpFOCtFb1N1WkFwNS9rTi9kdSthY2pBVUhIQ1lXS3JIMWJoYnpTVDVXWUpjckZWSWEwT3dzQW1LLwp1YlhMY0dON2cybWpnK1pRT3huUkxrQ0xQOWZoZ016aWdvMjk4N3pzSS9XeW5KRE9EbVNIZDZnemR6SDdjOHVkCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlpXbmZBM3JZZ0x6RXMvdDg0WEYKY1hyRTJ0REM2aTZIcUxyY0J5TmdpWnJKZlhlSHlUSnRSRHhaNW45ZlVpbnVPUEpTdHcrZTdhS3NIbllWNmpwSwpaNHROTzZBY3FBNG13VVZsSGtLRTNLaWxQQUZPMlpnT0UzdzBCNHhPTTdIQmprQXBLWmpua0xkT0NTbkphbDFLCkh1RGJVLy9sblMyMXRRdWF1QXdNbjM0WFVNYWV2OFV0RGI0enpCTGFJQWtBdVRyT0ZBSTJleTVkaWpaREJibXEKWGxVMW51aENKWld5Qkh1MUZnQTcyRzM5NHhXYWIvTkpUUnFobWV3K2txeFp3WHhXckFpanhQbVJOckMrdUtneApYNng5K0hkRHAxMHViMGNUcGZlZUlJaDVUQjVhTkNUMnN3eGlyTXRHZklTWkVnOW1EMUgvMmo4VWpTazUxTnh1Cjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHd5L3l5RXJBbkd6TW5jYm9Dd2EKcDlJUk4zalZGWGFkM25Jdlg1eFE1U1RvUDlLYzAvUzYxcGZTYnpQc0dxdHBuWVM2bXkrZmIyRmU4ZyszM2lEQgp4eWZDVzVVRXZmYzUvRU1MQ2ZLaXhFWFl2NmVjeDBkdzRvVGRyaXJHWGx0RXBWOXcweDQvRW9OOXc5TjBnUGhwCjRyZWZFbEs1ZkVxN3ZJajVWbldTaHNYbWV1NDVTVWJuMkFvd24zaEF1dlgwaklVbmZJaExPWitKa3g1TTRtMDQKYkNCdzI5K0ZpVzR0MXVjSDd2dkwxRC9JWVFISGZvY2Y5YlQvMVpzT2dFU0tzaVJ6akZ0ZnNnU2cvbENzeHYxRQpONVM3L1FNenloNmkrR0JKTzU3MmFPMStIZU5oUE9pYUd0anFkZTBHUGxmWExKV3p6enU2c3ZOOEtLM2lvaVVKCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWtzamc4d2QwakxkSGdweFloemQKOUk3OFFhRllZaFNBamc1Ukk0M0JWNHQvd1ZBOW4yYjViNWx4U2ZLVndpOHlKaytqaEZGQ0NqVkgwWExGaFh3Kwpremp2YTgvK0FuWi8wcjlHZDBtT2tpWnZqL0N0c0VyUVBaOXpJTjdoSnB0VE9HVUw3cS82NTI5VnlIL0tqVjkzCjFPK2ZTOC9XbTZoMHdpYnRNVVBiQVJ1OHV6Z2dUQ29HMXhNRkVxK2oxQnUxT0RJRnNZcG5ubVplOXZqVmlJcHcKTjhXUnVOcmFaeXMxdC9DQ0NpNVpMMEhEOVJqb2l5SnlpUlRjMWt4MmRHd3hTZ0NoNGVnV2pZemhkWllnSTBHRQpnVS80U29Hb0FlUGJoUFE0WS9wRDNuZWNQeko3R2NmN0xaT1FkYVhHNC9MbHBLWGtVWkNCRGFQbmhjTUxwcUZVClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFZHV0FpU0d1YTEzYlQ5MXBaU2UKL1NQZm1xLzFOWjJ0N1FNYktLV21BT1kxY0hoTlVZbWRGTUNVZEtXWVhxcWhlMmErQURpVFhHVk5lNmxsVmkxaApNT2VXa3I5Y3BJZU1qMDczMElwRmNIWm5QNW5oS3NkUWdQZkg0ckhxRmw1SjJoR3MwWEFZOW91UGJYamJHV2FoCi9RekQrV3JIQ2pkbGRMY3kvdHJYQ0YzTmVRVStOcFpMdmJSUVl6OEVxU1QvUFlMWkltTVJLd25Qdmw5OWVocU8KTjU5a1FNbE1YT0wvcUMreWNweEdSSUZSTDFGMGdZeEpremR3djdBc0pMS0hBYnFuNlp2RHJaVDR3RklDV2xzMgo2dEc2WTh5WWVBdk5lWFhHMGtKWTZOUGUyWDZSMWVZQ3BWYmlaTXZJYkRkNyt0cWlIaTQvTHovU042Qzh3cE1lCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd25CaGxXRFZHL1gxZ3MxbVgyekEKN0h6bmhKRHAxOEtkZ1o3djlia1FBVi93SGFERmQxdEhvbFQ5SVNuRlNDZmk0ajk4RGQ3cWdJekI0WmRzN040QwpQM2VrZlFjZmgyeWFNM3VMYzVNUEhlQjVrc3c5bS9VT3pmQlcrdFhtSXZBeHZlTGVVYnl2SkFHWnVQM2EzZ1RyCm5Ic1VudUg3d3pDVG5JdkpFendtMUxUNVpLd3VZT3JDbkJQNzRSeTBodm1ZdWYvMmlEMVpDSlJKandxQTRNL1YKd0lvbG8xOHhRWVRaUW04MWRjYloyM3JCMElUS1N1enBHbWNHRjBaV2UwbVpoOEQ2aWhhUFAvWjJ5cm0wMDhyMworcTVlc2pqYVh6MjZQbFFGNHVmN25UUzJ3VTFIU01SeXBXV1RYcVF1S214RGdZcXlXVHlPY2xJVUs5bWthS25XCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXJuREpobUZGK21ZeGlXT2lLQ1AKU29vQ3ZneWZxODYxQjFmdWhWRjMzODEzc1BXMGtxdzdTMU1XVTNJa0srcytZOGRTM1ZKdHR0eG10dTUwMEpxNApoejRzbWxyUlFsWXE1RDl6RFNJL012RzhPdUdYREVzVSs0RVhjYzFoT284Z2I3ZExTRlJ0cG9pMDRrOUJKRGtKCkpEMnRrUjA5aGRkK09mNHVPQlQ1ZlR1VXVQazh4emhubG5XUHhXV1ZzUWlpMkd1Y1E5L2taVXhtZWN6TTd1M08KVkc4cG8yNTJFbGgwZWJMQUxuZWJOSXBWYllkMmxoTHZKY0gvbmtMbnhVOWtaR1dnTFVBSHZEOGFEa3pWUTJrKwozR0U2WldWNjN2VDV5aUdPeVNwNlVXMTFWdXozc1pmdkRkWUR5S0pxOVRGdWFHbFJxR3Zpd2RuaSsxWVh6ZWJmCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFluTlZtVHpoSGhsZUQzYzUzeVMKVExNOTVZWVAwWUxMbVRQVVNtYmdWNUI3R1E0RjJqSzBvZHhYQkNkVktjSkdpVHppMk9yOHdocENBOFB1bkJYZApUT3EwYVpJMWhKWnRYb1JtdzlZcXZESFdGUHE0VkMzaWlVaWg5YmdIWE9FSCtNSFAzUGZVcnZ5eWpFODdjTXh3ClZ0L1hGYTQ5S0tZWkk2TE9pRDZrSXhXeFZMTnZwc204WS92TEZ2Q2lEZStiZ09IS1Z6aGVkTFgzeFo1OER1Q0YKTTIwaVlmL0Z5UkdYZ1ZZZmxac0J4dENYVzFLVHMrbGpIV3RvM25Zd0lDckswUTVrV0llUGp1eEpNSzZwMlJPZwpiWE9tOHZUYmIzdGdVRXdldTg3em1mbmRyWjRqNmRzaUkyVUVkL1puNXBObzRqd2Rhbm1QREo5R3hCTEEvdEljCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0ZMNSt2cnFjdnpGT0I4ek0vc3YKS1Y1ZkgyWlA2Z1pVVzBHVklFN2hiNS9jR0l2ZG0zWStSYmkzRzgwamR0Mko4bXRkR0FWeDNGWEtTQnl5d043KwpCWGlIc08vTkthZFRyR1ptOElwaVZhYU5NcVpxcWVoa1RKUWV1Z1ZsZ0JNRUpWbUlSZEdpbzRLVGxKbURVSmRBClg0bFVwOWhyVmVsTXMvSkloeEE2RVpMKzlFeklNSFQ4QkxaeTMwZ1doUlR2TFlvNlpOeXV0Zk93aGRGNExoN1IKcUtEdUQwRSt5VGh1bDU3bjVpMU1oZXZub2x2TmpTeG9PWTNxOXpGQ3BQV3hRbkEzM0xiVjZTeVVXVGVWU29RNwp3MXROby9ZTktvR093RHVXTFVxV29GOTRJUzFiV0JIakRIcXhmV2hkTWUyUnNTbkNLdjR6emlKTzkzWkI4WFBTCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkVIOE1Ua0hrSWdRYm16RHJxWDgKL0s2REs1OG9OcXJMT0MzcTlzL2NnbFpSWlBTZ1FiajN4c21GY2RCbkRwaER2Vzl5WDB1TVZzbXJQNUU5THM5MApKek9KZXJ1cGtySnR2TzBBSm1pNWM2VUpvNnVBWE0ySzFOM0ZNbStndlJzTHNMVUVoODNWMVVHTk05OHBQelpwCm82OWc5V3UzZyt6MTRnZXFLdmJsMXlyN2JLRlZha0ZkNkZsN2gyMjZTSFNLMkRXQkkzWEdwOHBGY0d2K3M3UFoKUlZGVkc2R1FLY1ZOcHl0T1ZqcTBrMXhPSVBPSDJkK3drNHhTMHFwcG4xNHdWMXliaExpYkNmNE1qTzFYaUU0bwpHUC9nTXduVlJ3M2JSaEIrUEFzT3B3UXJETzJMZUtYMDJnWlVZb0o5WHozakJGK082V0VEbmR4cXlPV3FoTVVoCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0doNzRFZkdVRHh0d3Vxckx4WU4KaHp0bmVCaHE1b3JjdllDWU4wWXdGb25ObWxxMmx3N2t2Nis0T28wK1BvMDdZaUFuSlJGUGRUdE9lRXJaK3VlZAp2OXBHZ0Q4Ync0L0xDNmRiUnQ0LzhldHVIelRkWFdZZUcxMFAxWVBqY1RzREtPeHFkRWlzK1pSbEw4RDJJYk9TCmZHMW4vUVh4OVZNYUtkRHgrNmcyYm5OQjVYVXFWTElhMThGUGVNMXllMm1QbE11RnhoZy9iczMxR3NYdDk1T0UKTlYxbUNyN3lCMUFETCtOZG9wdDAvamxKV0haWEVpL3JsVWgwTERaM0JUZGdsSTdPd3dSeXMwSlczRUFhR2VIbwoxNnpySDZDQ3NMa1JWSUdEeDZGM3Nzc0x0dGtqcEg1eWkrRkdvclIwRmVCTG8xQUlsWkF6ZnZ3SitRMCtFUTdPCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnRHcnhZOVN6NVdsY0pBQ1EzSUkKb25DeGYyQkJzaW5wbkpkaENIWTMyVWRPdklRbUZTNURHMGVzZ1c4Q251RHM2cXY1NWx4bDdDNzY0NXdnL2R0VgpRUkdMaU9hd0RhanZZck9KamttSjhTcW53TlpodXloc1JiY2lrMkFJZHJNZjZCbmhFSERHL0h3QzJVSEpNYXF6CnFxVTdjdnA4TjV0WlRKTjNZMG14R0dtVWRuT1VNWEFwVmlsRHVYeXpDWFpLaTMzOTNNZnJmcEZTTUgvZlRTUEgKdFhLQy8xZmIrTENPZkFFUHJNc2U4ZjRLQW96OE1Na0lzKzBRSnpHRUpGVW5Nb0ozMU1oZEVzYWFhanhiWjBzZQpiMWxSczB0clhoQk5FYkJpNktLbit3WXB1UERCUFNYbVMzb29vcFE3Y0tDVkpnSDl4bnBuQ1pReXdDemxXQ0VyCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFM4TEozNlNCcWREWXU4aUJmNFcKdnphK1VYb21FNG5SOFAwRTk4VTB4K2xyeWVDajNYeDdQWHN5allCZUsvVEloMlFBU3pPQlEvYWV5eVc4dVZ1awpuU0dSMEhVMlR2MmpTNUNmY2JHVzR3WU9YTGhuSS9TclVVc2lEYkFta3hhTnMwNW5xT2N1SXgxM2pRc3N5bXN0ClpBbFAwMmtvSnlDcFNXN2h5NUZDNWJYazVTdmFtcGFRT0ROS0VSVDlDTzhHOVRTNm5aVlBqaDlHcHh3RnNEUDQKR2x3eHU3NWxSRTRKY1NKclhoZ2pDWUhkWVUrdGJyQ3hmbHc3RGlna0c2czVra3dhZGpHV1FGdjM2M0Q2VEdZMQpLQ0hhZzZZQWpPV21vYW9EZU9PVURyL2l0bmxRejhSUk9aQTRLVmtDMnk3VXAxTHk5V0pybmQ1bzgzSmR3VG1oCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNnJpVzJoSFc5VTR0YlRXcjFrZ0EKVDNwZTlMcEdPNU9KM0dLa1NjeUFqQVhqWHV1OWJqdWtPSndRM2wzZktSMWZMT05zY3dpYmJkZzVUOTZ5dEtZagpuRVNhQitTOWlEbXlSWnMxOGJ3RlBSajN1ZTBrNzhuWXdMeXIzM1ZkRFZrd3AyZjlSSWhzdmVLSEdSaUw5cFVWClZBZDZhOUV5M3lKY2tUcWRpT0VKMkN3N0lFemI4N1BULzRTR3NkVDIzWVMrdDkzYVA5aXhSZ3ArUXhjNXFGbTMKRmlIelBSYjlBWU5aVGRPYkNIRDJkU2l0ZTV3eUhMWEwzdG90OGlpMERaOWMxMWRmbTJoYUpUS2tUdXlxRktRNQpVb00wcFFkUXlGaVhZY01RcHd0L2I3dE15UWZwZEwwZ3VETVdTT0dIRU16RXZJZ1grN21JU0h0dHJ2dXVXekdSClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFB2NHF6UG1YdllWOHp3SWR1ZysKU1h2QTdSSDZiZHFWUTJaQW5LbllRQUlrb3c3OTBPdzVtcGRacUt2WmM4MEE3SkUrWEF2ZnFPZ2R5N1kxWEdWVwpyWFl5Ulg3SkxqM0NXVkRsQzBjY043ODhnZUgyWFJ2aTVub0grc1kzZ3dLMEZJWnYxMVp1c3ArSS9DYTdySkV1Cm5QcFlsMTQzYWJHZ0VDYWVoeVlqZTBWYXBZZHoyUFZKeC91UjhjS0xIZ2htSTBHSWtTN0lJYWtRRWNiL0JCSEQKVno0ZkF2RzhETDZRcEIwRzBRVDVPYVFlQ2V6TnJHWUtMTWszaFFnMk1UNld2eWdjd3hRcndzVzJtanpaUUljWQpVbk9VNnEwYzVYVVNXUkIzN1NiOG9yQmlMWG5ySmFmYm1IdFZ2ZVJkMXFaNUNsS3pia2hCblozOXFJYmlyVUFZCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmVTcEwrakNSWVZHM1FHSFlzd24KUTZmQ0cvUHo5WTBFZ0IxWmxZLzAvcWgzVWhJMlRlMUcrSldpZ0hRR0R5Zm9TQmF0REllZ0ZYYlg3UHZnMmZRYQpCTUtkUGF1ZitUREpJeENNWlpJeHRtT2lsOWxTSHc5VkVUK1llVGZ1QlVjVlFCUjJ1QTBOS1BjRXpyVWEwWkZNCjdqUEgzTEVIbXNkWlJ4TTBnNkQrb29WOVB5a2h1SzBiVHpjOHd2RHRkOFNocEZGdUpvem9DSGdLTis1dkRxcVEKZ1A5Q3VVcFNjUTZkejNza0p6eE01cXhUSWk0UjI3MG91SktlVWFFVktzYWhyTXg4NTk2TEV0ZzdPNzRjZkx3LwpZUUw1RmhweksvNnNjSE9jOHlaNUIrUlc3aVl0cDd5R2ZJN1dEbEcvNUwzcUVHdjJWK2tMSWtmN2RSc2xGU3B2CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE9QdGlPR3ZhZ2xoQ3FndjV3QWYKa3BQbGt1QzBJVDIvcWZtd2RqTGNZSUx4b0xVV2lTMTViZkcyajZNL1pBNnZPZUpXM3BGSGkyZVJ4R2hpNktXTQp1czFFTUNqdzlnUzdUUlloekVmT0xkSmphV0h6bDB4KzJnMlNXa2tJL1ljZHRFNkZUN0xVdW5RS3o5MTVVbUVDClpZT1owcDN1N1dDUUxzUTZia1ZQSXNnNnNaSHA0cFpacDgvQm94Q1ZDU3BENnQzbFRia2JtUmpDUE1sMmp3WTYKWk16M241NFJKV2x2Z1pYb2NuY1pZUU5vdEh6NzJSRFR4ZEtZZWsxVTJSbTBPbFVyc1Y5QXI4MUVpbEc5d2VPYgpJcHhCMDFSN1lGT0FWWDcyZTFHTURIM0dIRGlLNHhneHNOQUJYUEIxR1IweFFXS0tMeHdENkxlRW5FVDVpYVZGClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeW1ZNGk2MkVxbHdrSWs5bFpMQUQKbG1UcEJIWUd6cjhEamN0N041MFdWb2Q2cmJTb3U1NzIvSTBQcXFHSEZwdGFISGJOdUNIUXZWTHBmUmJwbmZORQp5SkVXYlliOTE0Ny8yS1QxNlIzR3M0WlVVa3FJWkNVTlVSMzdVczRYeHppazA2VFF2QmtQaXY4RDh2NTlvNjNSCmhWSnVKUCtCeU9OREhOSWlCN1h3eE5rbURvOWt5NFV2V0pTc3RJdUVmV3dYUHpvcnB6TnNGS1ZRMTNnTGNkWjIKNk9odlBSdkVTR3h1Q055UFRxTHFWQVZjRHVpcmY5eDA2TzI2TCtvdHZXR3VRb1JyaUk0TEZIOHhJUGwwNUFKQgp0dzNKc3hYSlQ4cDVwZzZGa3JpKzFJSTBhN3k4UHpKVEdOSVVzcFhGWVF3M2FEVzJXQUJEMVR0VDdsUFY2NHM3Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2NZaVFITnd6SnkrRFZYc00wU0cKVko3S2VRNW1BZGZ5MzNBcWNZSmxFOVorbVpWTHY2K0VXbFFhWlE1TUQzSVljY3NCZEFNVHE2ckZXamphQnI0WAovcmdmSTVReHQ0SEIxK1RnY1JZTjBTbUVmdFJRaFBoZ3JmZnFyNE13Sk1PWGVnQks1YndmNWJ4Nk9PcjB5allzClhvQ0I1NE4zUkZzVnhKUHNWNHp4M0t4TmlTL1VLS1JreFowRUNJYmQxaVZ5NGFsZzNaMmxiaVVGQWtiZ2FtUDEKd1RlYnBvcmU1c056YlgzbUFmTmduOTFuMGV5L2EvL2lremQ3M0J4UHhNc3FPc09jK0orbkNhTHJqMDcyZzdhKwo3UVZ1TlY0N3F2b2xldGpjSDFId2x1T1BnNHJJejB2N2RuNGo1c2FTeWhscEdpNlY4SjlIN0VnRUlwV2RqVUhqCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTNZc09BdmZEM2xvNEVsRXNwemkKU1V3WXVaMXROUUZWWHBiM0k3eDR3NUdReUZQOG5lSzhJZ1h4QTh2QzBYbXZ6ZkovRXVOSWtUQUVlMTB1a05hKwpXUysya2o2dEFHeFdLQU5JRm9mOFVoaVMwME5Leno0b2U5YndtcDd1eWN5UnJ5bTMxMUJTRnpqQ1BYT2xoMUQzCitPd2w3eGNMckw1ZkRtN2VsUkFDNGtoSHQ4NWd4SUp3UkhWaFJLeXpOR1FZaWVpbGJXMUJISHVyaGhQMUVuQWMKQ2w0K09sVmJDd04vUlo3S09DcVM3NzVISGNPWkwrYk5keHAyOGtYeEhFV0JnR0xjNnJKZ1pPbG9TRHdwOWJadgp0RzdnM0hOYmw2YWlQL0lIREhrTG1XdGhMaUhyVUw5WTdhaHU1ajh1RG1lVHVQMmZIVGVkbG9YYlpNejY1Vkp4CkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGxEQ1NNekV5VlFxZUJKSjFmQXQKbXFWM0ZCTUdIZ2U4aHhDRkk4TGFXVFR3U1pBMk80c2xRaDNCSzRub3JDdkFHUkdVTjBHUVpCZE5SK1R0QXRaZQpBQzJqbUlMM1lMcWNhTFRzbG56YmxrRE1tajNMS3F1QU9ZZE9LcXdSU1lVNVRZSDVGRStRQkN6R3UwNXV5bE5kCmEwN3V5bzNDRXFKdFkyelBNNFZwb3I0WkwxeWtmZ0U3dk5YL2FYRlUvMjFxSGNPR2dUWUlMcHJhQ3g2eklHVDIKdlJSc1c0NzE1OUFxRkFzdjVnOE1GWTk1dCtyQzhsY1BLYW5yK1RQU2l1cnRUakx3OFMxQi9aYjI1Q2lTWDdybwpIVmcxR2VWRk1iNFFEK2M4eVc2V2FUdE1RZzFlaWgrMVlEbThhRmo3RUdzTXBPYngyV29sM0NpVUN5YWFjaUxmClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOVg0OEtzbHUxci8zOTEyYzNVZHYKVklPQTY2VlFYN1YzQmRQb3BYeWE4U3ZHNDBqMTYrVU9pZG1GUUhYQTVqcmJ0T3Z5ODkzWUNFdU9oUWlxY2d6ZQptQnRscU9GM3cyK05jZlhFZVZzRXR0bGRCQ3ovTk1qYUlqRjJSNStiNUx0Qi9VM3YzMGlGaytlTVVDaGNyVE0xCnoxb3dMYU1jUHpLaHZtTW1lUUhnVUhGQ0h5eE1XNEVqSGg5MWprVGgwK2hSdVZhdERrNXZjTXhEbkU5QXl3SncKK2lNZnFqeEs3M1ordURweDlQNGpKUndjQXN2N2V1YjhZU3I1Uk54eUhieW9ZSFMzY1orUU5PdklhMWM2RnRpTgp5bnpwYU1idkdhVTZuNUN0dXV5UURUWGdMbWsxcDU5ZU82WmpTUWR5aFQ2VUNVandYNjI0ek5mWjdVTG83d2lkCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1YvdFNDbUVTK2NYR3NENi83eUEKbndnRVdGcWJWYlJ0c2UrMUpIN2lKNFI5WGNSQVRVSEFTaDhOaVJ3aUFDbTUwcnNIK0xMRkhvWlBwQnZydDNndApSN2FjRFZESU5wVkFHQ280UmZkMjhLdVFGc3I4c2luZXRSb3h2REkzSFFaMGdCbmMrWWNMWFdaUjM5eWZhZ3JRCnZmTHVFSUtwaldrb3J1N3d4WFB1RStnSXY1b0ZXVzdEWG9qaGUvNHNPYzhidU9ENmZkUnhmMWJzaXNtN0t0QysKZ05QUTg4Z3pGZDA1SmxPdW9MazZJcS9reDJmdm9HWXpIMWFSdFZVNytFdjZqaFZYY0Y2S2JTUVA3ZTlIUGpHSAo4RFJiREJjZ21qaTlWMzFhV2JvMXU5bjBPdm45dExKVVFhTERnTUNmUVhjcDFlVWlDdXRhZVBUcDZFVG9FUjN1CmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdSswNW5WS2pzZndTRkZ5bWlrekQKZC9vZGlvZVBwV2k4bmZCUnBsaU81OXh0NlFQdzhyMFY4R2QwYTJnOUlmNWtNSWJEMmNoa21sRVhOZnJuYkpQQwphQ3gxSzd5cXN4KzlEd1g1dWhyRlJyenN4SS9neFR3U1BqN3U3N2dvVVFVOHZjVXF3SzIxSG5CQ3U4ZmtqNCs3CkY4NzYvQkdURFhwZGZ4MU1hOHdvMS9XTGRvd3VuNWlTbmREVWh2SC9KeUFLVFdicHVLZStPTitLWUpoWkgzSG0KNDIzY3c4VFZIMEdnU3liM29pY01rNUxuTDMxS083aU50bUVBYmhFSHlXRXhJS3dxYW9LWkJMOFA0MVlzc0JWMQp2Tms5b3FGQWo5NDI3WWhIYzRQNGt2THJ2S0gzODJqN285R2hxck5EVmYyUE40NkpoS0k3c3N3dlhQMkZ0KzNNCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUdnUFk0UXVzWFhpOGdnOXZpVXQKQXg0STdCNmZzM0htOWVManAvRlhWaWl0OGd3R0h2RFNIb1NLaWppOTFhcDV4bEZGRlB6Mjd1dWJid3BuVzQ0Wgp0OXZXOUVFNlU3SnllR1VCY29nc2ZtSUpSSDdFcnA5aE9uZ0pnKzNtYnYrYWRscjRjdFNtdkFIQWV0SlVVQWxICi92OEVQTm9veVdCUW16blRGMk1XZzdFMDl6eHVnYVAvUEhONVZraXkxeXFKb01KOEtBQTQ0MkRJM0VEVGs0NzcKMGd4MDZUdTN6NFY2ODJHUHFaTlM1akpHZnV1cjRvQXZFYnJTZ0VSNXYvb0JHY1pNWXBaSlI5YlRJcHFFN2cwQQpMQlJiUjJTZGIxbjBlelVnQ2owenIzSTZVd3BKVjE5QkdDbnVzRndzZGpOa0ZTQWJwVmlCRVN2b0YzclFOc214ClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3VDVi9Dd2NKS3p3d1dFL3RjNEoKRUZkUmx2dUhtYmpESEc5Q1BZcUpnVE1hZy9RNy9xM2hacmRiNlFJbjRVZ0thVlBjUXlUUFEwSC9EMFFuV2JvZApXblVUbUlLODRFaEF6d3ZoRHgwb0FqQWpHUEJBaEJKN1Vwb2diTXZEMmxGN2ViTndvUndRZzYyemFmQWRHU2xPCmhhZ3NRdjNlTFpPWit3b2dIYk9rZUxsSU93enAwZy9lTlNJbDNLOEdVRUFJSExTWU5Ob09jVXFLSVZlUS83dGMKK1N1aEdvTDU4UnRkYk9sYVVIMkhRR1M2a25HMFR2YmdKdk5mL2J4VFp1N3hyVWtlS2YwRWZhdHdmWjU4SEVYVQpUK2JSbjNucUFLOGhRTWIvM2NwVk95Q05aLy9QU1FUL0pjVnVzOFV6ZUpVZ0p6NnFqT3A4cjZRaCt6cWFJLzV3CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnh5OXdLeURqajNBa1hoV20rRkgKL3JOMU9JZXZCNEtoM0tTeVR1YnBJZ29IT25tYmpISVBJblRVclAvcVdsN0FCcDhhdVVraHZXdnkzU0JweFhaUgppTGlEWUtMcW9JQzBaMVVpMXNvb3FiWjF2QTB5Q2tQQVQvUkcrc2lUV0JnNU1rQU01Yk1ETHRyeTVJYTBvU1dSCkxGWUJaYVppRzRBUkFKS200Vmg2aTFwZUJHaUplcWV4bHNMVi9VV0JjZ3Nrci9iUmFDazlVNnY2OFJuTWh2KysKdXZKUnNWMVBOU3MzSXZWek93TW5ZUU9GN2tscUd6MUJuMnNOcU9yR1ExWkJtMVBZTjUvQ2VsbFJPSnBUdnFZWgpGelNrWjZyK2xCRVlkaU9sb053YUs4YW9XM2x2N2MyRmJXZ1Vud2Q0azRueE90a0F3UlRvaEpYMksrWnRMQkk5Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjdkK0tlQUVSNDFEOXNnTjVOYk0KbHJlVDk3M3AyYWdUVVE1dE15aHRSdzJGMm9vU29CMjE0blUwWWU2b3ZsMTZyT1N3dVRzd2h5RHpZTk4rM2piNwpUWUlMeVpVeVN6SmVTSDVGeFY2cmllNzZiZklvWEF2ZTE4ekh3Rzk0blJJRHZlYkIvb2t5YXlkMjRSVmJ5NWZhCkVsRnh3YitTejZwajErSDZTb0pIWFV4QnRtNHdTd1Z3a0ZMa2ZiYUlTL2l0NTQxUCtuL0RYZnMraEExSGlKQS8Kbkp4eFVOaGxwUVdnK2FnNmNidnlpRVlBWlVTeHA2cnN3Y1RoV3RQRE1zRG1vdFBKbW96VS9aMXFlaVRQTHZ1dAo2WnFvM084L2ZNMlpyb1lhakZhSUdkOHByOE9lVmxGUmRpUXVHUXozWXM2Vm5uNnEzSE5hRmQydVVkVjdmOGp3Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem9xVnlXcm5VQ1crT2k4MDBOeG8KZjNPd0ZVa25xeE1WNklpdDVWYzBFRGNpVGJzSDlQUk9xdjdvM2IrVjhCZjFMbFFrY0tkdERhM3BGdkRvcW1sQQprQnJwOGFwNC92SkdWMEVSRFhNbUgwWi9RZGIvRVN5NTBta1Q4c0dKM0RUbWF2WGN3NEszcTNMWFBFK0laUTRnCmpVaFJMNkZ2U1drOWIzWnVaN0R2TkF2dytBVTZqVXRmR0dXUXBoeWc4NGNHc3FHUDZWM1pGeURnc0NIMWt2SGoKZWFIMGZMWHQ4cW5VSWdGMmlpR3VQOVFvK3BDcHM0TEFUVWVneWlrbnF3MGpGYkx5cERqdSs0UUFXcjBWYXRGcgpBNC8rM01qSSttTk9PQjlQRXJvb1NtUytNYVcxaGQrNWVCUWtsaytrRmlqL0o0VnN0cDRxdk1BWmhsTk54QzdWCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUZrZ1EyTkdlWGkwSUkwN29LdUwKbm1BZFlUMHlFai9ZOGRKNTBxQzN3SlB0eHpTY3U0UVY3R3RIQXc4N0MvSUlsNlhCSmtsMks5SEJjRlZ3bGl0OAoxMHlkQXc1TUFsWjVkZlVaY0NFUk1uek5mU21TZitMMndnNFErditNdVFFdlpFVm8xSmxMblU2bUpabzNZT2F0CmFqa1pzZjZ2ZkxzbG9YMzF3SFhGeVl1L2VTUTc1VXV2L1RIWTBKSThvbWsySkw1dGRKNVczeHBoNFIzUTd4VkMKb1JlZ1NWbEtnYVFjN3VFTUk4WTNNL0haQ0VvZFczNU53T0NaT0w0UzcxbFgxQkVYQkZuc3BsRnowcTA5Qm00TgpxckdGdm5jYm4wTnNnSGVMYzE4V0JEaHlDaTJyTHBzMUluUW9mK0RjeHorT29qZHJIbitOQjZLWFIxWnI1RlNRCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDhDZ1NJVTFtVEp2RzhWVEtkYUEKK0FJeFZBdm11aUE4L3cvUlBxSUZ0TklkRm9wQTFILzJlYWZCRWNvU1RsQXhhU2ljcDVlaEt3Z0tVZzFoVXhqSgpBNGNCcy9mTjhKRVZTUm9hNTBnNjRUMHFHNllJNkc5Z25RZnUyRi9tWXNodmJxdEZMSGx0cDVpMWdFVjNwWlAvCnp0a3ozbEZ5Ni84dkczSVZhdHRWUjJzT3cxbkhsY3FRakk2dXl2MjJkVm5VTnNiS1BvRjArRlcybmI4aE81WlYKQzNHeGtmZm53dmU0dy8zY2VJUXBqdGtDUXlBRXRqUFdrMnN0azV5QlU3dE1KdVNqdk1EL1J3WmJhTDNGby80OQp3SHpwc1RXRDhFYkFSTmk1ZnJvWWpEdE9DNkhqb0RpdUV0L1UzRk9iOHhFamxqR3RDTkpiaUlZSjlMSFJiTTk4Cm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmp1ZlRVME9ES04rbWxVeElPZGcKbTFTczdSdStsZ3R1QXh3SFFad3Vyd2MrNGtIYUxvZzFMbEpNRm5aUmhOZ2NTK294K1pscmlIZ0plRGtSbVlxRwpiWkFGbFpwMFJxMERlekhOV3FvcFhmUnQ0QlM0cnlSVjN6MThqWmRFQ21heE1IaDNmWDFIQXVqL1ZqWktDc1g3ClV5S1dOWDc1SXZEa3VmQmpsazE1NDl4ZFJyYnJrRkNpRFdHRElyL0ZXSlhzcDJReGhmRWRTaUlwSi9FMFYzTnAKK0ZTTVFmanN6ZUZSNnpGUkpaMW1BU3hvWFpva1ZHaWNLVEgxYUFCN3hZNjBabm1ZWENkaGx4TkpQU0dZeGRmKwo1VFR6RVh0TlliQmM2cWlkOWZ4YUEvS3pRMnNNMmJJNVhGU2JMZ05QZzRmWk15QU43OENZOU03VXJVLzJZWFJZCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2d3c0w4TUM2c29VUkEwQ3ZwTmsKYjluNzJXSjVQLzFKZ05ob0xUMG5QcWd3cnhtNWI0d0xLSnJCMlVBa3RSYmVOKzRpT0Fic3AzM1U2Nm5JcktHdgorSkpDQTkrT3o1VkFHQ3lYMUloaGhTLzExQ0pPUVFPdC9NVU8zcy9YS0pBeFdwSGYwaFVPZEM5N0hvMDY0U0dICi9ZU1FKVUI0NndidGJLUUtPVzRXSklXOFJMYWVTYng2MVorZEFMc1QvcUwzN24xWHdZdjdDdkZGQ0tSZVlDU0oKMUZCMzRmS2N1NGhIVTIrbkdVeXBsMlRsaE1ScGN2MEREaGh0UXJMbDBnR2o0OThxdkZNTE9QZFhHZExEaDdUdgpnbnAvcEs1OGZTcTdvdjFjVXBlSU56eXZHWGVMb1Mrc29yamtTU1FpeHAvcjRiaks4WmNDNllhSUhmR2JCTDJWCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlFXWWlmUjlzK3htRm5XbkxjNmQKRnhkVjlXNGpBdzJXZGlWUTUwQStYVVBNaEx6eGJhSTlEdXBMNklyRGRodSsrNWFFbEhXdHZoMitBell6UDl6eApHckRMdUJrQ1VDWE1WbFNBVmMvY21uZlBTeXFlZWFTWElTeWlTaGxIUGM3QnZkQU9RQzdKTnI1SEVwMklmSFBqCllKcEx2cFJlVnVlZkY5VlYrQmViSzYrcjdySjdGNEI5M0h2VHVCKzB1MjdOU1k2OUhZZnBMb2c5RUlHNDVjUEsKVXV3aktYWjhxUk1ib2U4eHBEc0NIYVZxNDlPcSs2R3djamdlazlRaGY3K1pGR2hORVBWU2ljNTRlbmRvTW1xQwpyRHprejZkeS9MT2xEcVlmUWxncTZFMWlPcmxFTGxtZHNEc3VTVmFTM3lGc3NKRHp1SEpPek9iU0l6bFJTdHVzCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2ErOURFYnNYRExXV0J4Rk1ZZlkKRnRPNC9kVm94cmcySVRDTDU0MjRoeXozNUFNS3lVRGtxdG9UUUdVMk1pUFdBOXdMK0NUUmJNSmVXQWtjVnZ5TApNMC9FZDIwbmxXQldhVXdIUmNhTUk5b2pES0FKVjVFR1ZKSy9ucGJtSmd2UElDck1rMjVhbWxSS2xlRU54bm1aCkVnT0ZwZzl6N0RmTWxjUnBGWjZ0MzFCL2NvRjJ3d0hieGtkbXdsaEpzR0Q2b2llUTV3eXVqZ0hFdVVqcjc5aXQKYVpGQlVWVlE0Vk5OVHFPQ0xNRFZPQy92Zk9yWUxNMWJIZXZOZEhWKzJzSFpHQi9TNDJJQUJPbm5PMjRDY0VWRwpQWTJCUFhWeUdZZEI4Y2paVHB3VWVzSi93T0h6VDh3NGNvN2RsUnJudVJlNkQvUG9HcEMvR0dXSDhzVDdhNFhRCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1E5cHB2aTc3YkNZalBlejNIUGMKUXhOdDBiNDN3dEVWdDBGaThjSXhPc0ZzeWxxMXRCMDAwSWZMQWw3TFQ2VHBsdHhvSVBPMmlIUFBBTUxUNTd6NApMcHdaS1lwallzeDdoOXZDZUl2L3ZkYzQ0dVl4ZHZBV0QyTVN2NzFlOVNMdnd1eEQ4MmtoTWVUQmNlLzZMRHBJCm5rN1ZrMmx5NVFpVlFxbHdaZnZDamJMODJKQUtXWkxqaytjZHlPd1h3bk5yOU53ejlrWmxQOThBQ1hHcytCajAKTHh5b2tjdXFmcWs1ZnY3NHNvOE16eC9NOXlFTk5VNVZJYkZ5bmRBNWdNelJUaGxoaXI0d2RUK2wwNS9PYmptQgpkbmpuajFKWFBUVEF3Y3pQaElBRjlVb1NPMnVtdFl0R3dkWDhyNWV1OUNyd2tTTVRiMzRlamZNU21PaitQWWVyCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeW8vZW52YTA1OWNVL1I5cjFIRVUKVmdQTk1SdmNueVVMZGhjRDZJUXlxNDA2VjQvNUIyVTRJUGJlK2U3UWNhN1I3cjRqUHNzNHV0R2FzRWt3OTFmMAowaDk0WmlpN09pYitkelJESkkxbHBaZWgvRk96WnE5cDRPM084ZmJxaGRsREdTYTZTN2V3Vzc1N3BDM0tVQzlJClE2MGc4cVY4ZkkxYXZqY2wrMklwR0lYamhWMFh5aGJRTmtXQnVYQkQ0VnhvUVoyM0VmN09URy9zeEtwZXN4Z1EKT2pmQnU4bWlNNDB0YnVMeHNaRTNEdko4QlQ3YitQamtxajBaL1F2QTNCS1VFQnR3ZURnZVJpcjZUanhPM2lYego4SVR1S1BSaENTMHhudlZzTC9rZVNteGtJQ3MxRUg1d2Yrc1QvVXpGM1h1WWV0MXNlZjgwb0NwazVFRjMxc1ZUClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVVFcC9xcnJSaGR0blZ0akIzbUMKeURuTlhjcDhhTFA4L0lRcEtZWE0zVDBuZFFmZkxCT1ZBT1JmMkRPR2t2N2ZaY05HRlJiOGY4Z3IvNTdHOVh4SQpOMWlYakJlNDl5RlJNV3J4QTdWZUpUM3I5TUhVcVJQZTVsZ0pxSmQ1ZjJ5Z25ucmtCWkRadHdKZUgyOG11SjJBCkpJRjNKSldUS0JlRk9tc3A3ZWdBN2NOekZGTnNwOVVmVEoxQytJNEZnT3o2dno2a3JDczUvWXZZRFZpOVVBRDgKd29yckFEdEFEc3htWC9INDU2U1l4enNzUHh5TDRHY2EreS9WbXRtdVFHdUFaUXE5b1VrV1ZFMkNBY1Vjc0ZwWQpPWlIyZThYVzFYYXV6UC92eGdwT3E5TldLZUMxdzEvVHpQTVJoZ2djM0I5MnppelNCdEhRQ3JEZmRnMGFIUk1OCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHpPdG5FQ3hDOHAwR1hUUWN3M1QKNmR3YytyN3NiQmsyRkdHN2pSb1Nrb3ZSc1NqdUtZUkJlY2wwY1l5K3A1YzV1L2xHYnBVS0JpTXB3U0o4eEJDTQpzTGljYiszb2R3Vnhlb2hQUHZDY1BFYVZGN0tiWWU5T0VlTnRvekZuQ2F0V3BIM0FBMWs1dUx2THYra3N2WjlLCjVuRUZjMk5reWFhU0RsTjk0eFNkajBjZStmM2lkV2hWRnJjMGIzcmJjdkZKczgxR1hpTDd2cDBrVW5rb0Z6WlEKNzV5Y0JKd21TbHZoMElDUFlOR2tETXIrV3FUeVZNTG9ZdVg4QXhIUkhyWm9iMjBDb3YrRHZqWEk5RS9vV0U0UwpNWEd5TmkrM1N3cm9MWVZwb1Zpd2JwdDdoSWFEeTE0dGpVaU5ocFRRa3ducy96NkU0RWRZbk03Y1F2ZWlWd2hTCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdktLbkk4U2JWejA2NEluSHRrQ1MKSmRra0VZMFc1dWVld3o1Nm1qU2pxZExKcmtZYTJLSnVJbHlRU3I4Z1NaNTNMS1RlZVQzNUxLY1ROZHZaK0EvSApFSXgyVXZmeStQOUtmWFJnZHFvMWZ4Z0s0NHZEWUVSbk1sK21xRTZ0ckgyWTIwLzZqUG9wSDdsdnV0TXVNNW1vCnJMemxlQytLOEljNUw3ODRvaGVCNFprZ3crM2RvT3ZvRCtIczhYL25EL3RGMEQ4ZngyaWJXYXNqT2hhSnR4dksKWXVsczI2dHUvZENma3lLK3lyUG1IS3V2UThzcnR6dFQrQW9GcnRDdUM2Qk5jUExMWFdiQnNwZ09FUTNISHVyVAppUmY1MGZGMmpBVVVzWnRDSlMzUzQ4dTJGQWFJTWdFaWlwcU1vTjAwWjREYzB4SWk3cEUrb2s2U3hVOG94aHdLCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXM0MXZMdGl2MjYvWkNPOHErb1oKOFdxWDV0SXRWK1Q1S0haemR3MmpGcGtDSndnS3RMWHh6aWU0b21MSGovTjNIa0h1bEgyYWJ3TlIwTytpdHRBOApKYncycFl2UWxkdUVTdFVKVFNKRVdvc3hrQXU0Z3V4Y1p6VlRPZkc1WW5MRnNzRXZjaFV1dlgwSER5SG4wbENjCm0va01EeStQTmkyQUVLRTdaNVZLTmp1RDhnL3VGSzhveEY0K3JqSnFZT0VTNllrR2N1Mzd5MmZYNnI1dFppbEsKQllyNnFKcUlwNUd0dnIxMSt3ZW1sZ0hLeWtzRUJTa09uU3cwMGEyLzU1eURIbnZXUDF3VmR1VE5MMHRmaGR3ZApVcTYvM2lMMXlzU2dpT3oyYmtiYVU5ZjRKSDhnTmNYTE1HRjNXejBWaVFTR0M2ZjQ5QjdGSkZYSi9xdkY3SVBFCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkFCaERkd1VRU2wzY3JDN0EyL0sKK2ZqVVY2dWdWWFpEaGxEU1BCOTEzOC9QZ0V5UTlBVjFTVEc4blpKS0hWYU8xK0pCaC8vYkZJZHVqZzVoekR2bgp1YmtxMmtBY08rV2RZUE5OalBhQ01EL0NYYW1ITnhLZ3NTUlU5RWV0RkVSaTBjWXRXUTlZa3c4dUpEZDcrUHNXCnM0OHJEMDR3UVVIUGxOYVAzd0xseDF2cHk2UHM3VXpyeFNHUlI0NGlEVG8xVDBaS1AzaWFjSjZvN3J4aGREK3gKZGQrdDBmZnc2VTZTcEJVd2NVRHRRSWJrZmh0LzFpQXJuTlg4VVZkL0xFTVNDUGhManZYa093Z0ZPUHdnSUpXdQpucFhuNmJRYlQxVWV5eEsyT1hZK082WWlLK1BGM3k2YklaK1Q1Q1hGZkU1eStYeURFRStWdkVmdzh4YjdlVnRXCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGJnaDNIancrOWdFcElFRHNGZ0YKekhPT3pZeWlhb3Njb1NSakxaZnAzZGU1U3dUUllLR1N4bkh2SDd0VkJtVSsyK01mRkVkMmlPMmVrQ2h6SzJsaQppUGpYUXhYTkpJQXYwbXdkZ2dvWVdFNE93RG5CbDB1cE9GaHNlRXMwaWQxcEdYSHl4TE95Qzl6ZVdBNGd5bWhxCnhHREZSTDJGWTFxVjF4UmZ4VWhSOVQwbWoyaTVLMVVLWTJKSXkwbE1qTGs5a09xTm5XK2pxUzJqNVlPZ29HcmwKOUZzN2JtYWlhbDB1UHJ2Sjl5ZGVyRlNRQ3FIa0paU1VHRDR4aVExSHpBSlVwcFBRNHhINXZtdEx2Qk9oamljMgprZDk4bEM2aDFpbk1HMVF0emNWYXEwWlZ5VlMxYnJ0QklkbVM5Y1Z6T1JHcmtGdzlrUW1WM21XRUgrVE5aTHdDCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVFkcWJBNU1kL0xRbGowWm4yWk4KTEk5UHBWWjVPSU50U2I2MlpUc3ZSNXVieTNMWXFqcWRqQytMUmVyUGpON0FmOEh6dFNHK3daRTBDTWdXMzhuZQpOUXgzOTBQNmNacWc1cnRSTlpZdS93M1NKdTg2dFBKdGhBRXdQRjBlenBHajhFYUp3YXMxSUIrWVQ5eWhTL3hQCjE0SEZwcnZBNVFtc2tzZkp4WWdkNFdTa2dhQUEzZUhUOHZPQzJRVVd1dmNad2toQkFZcHlQbHZGaGU5U2dWKzMKOWZhcm9iMHhEWGFLdk1xNHZ3Yk1pNGtqajM0YW9zdExlQ0tUQW1xbUR4c3dXQnMzVWgyenJlRzlOVTBqNmNBagpIWlVrNmp6aTAxTlltRkJwRHhWd0JISDNJcUtYZjAzNVhRcVliOTJsNmVOYXlnUlFNU1JRdStob3R5RmxDNndaCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc05wZmQ0UjBBd3BzdkhqSmwxNU4KZHhHcjFXeFRiWmpFcTlKMHpSLyt3c2VaSmZBc3VjYUszUlZ4U0FtSnhWWjA1dkl0VTE4Ti91c3d0bnZ1YVlVMgpSYWJnRFppdXVKcWNoMnVQdXFsWnNRWVFtdGp2bzVSOFdndHB2Y3NIRERlYlhHMElHYTd1WUFLNHBlMlRBTjFqClJEazdCWVczcTFDYnc0anpTTk14WGxISGdkd1NYZk9HKytYc2lxRElIZGp2d3c1Rk90dTQwRnFMTUxVTkpvNDAKQUI2cVhSY3lPUWlwUkphQnYrQ0JnK2F4T01lc1FxV0JZMW1nQVppY2k2dDlvdzJLWlV1enRwa0U1V0dkMVZRaQprSjVaekpPZ3FlMng5LzVGSGtmRnpzQmczdTJhb0NLSUQvdVFCRmQ1Q0F1LzhPbkFBd0h1VmNRV2JhbXQwOG9tCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWNia3VNTTFuOW5vSWRCTEhwOUEKaDBQZkVVN1ZHZkpZSFF3NGQxK2REOUpVWCttYVVHLzRnenVTVVRRZ2FqWS9aTFVHS0w5ZDdEMDduL3d4bkhFNgozUlVabTZ6RVQ4YkYrL1E5Wm5qMElPWXd4WmxHcGJVVW5NNStMNWdZK3BXc1ZoRG9FNHA1amlnaVdtK3R4MnpjCkppdkpQNXBXQVVSQUVoV1lvZlAxeVdxcWZjclBKdWJ4TFVNNzQ0dXZucmhYbFN2NURZWFJyOVVsSmJrbmxYOVkKN1FVYjlBdGNva08zODJPVTh0NEg4aDZ4emYwL0lLaFk0NFBHZk1GNFJSalJHeTVKdHpkbW5HejlDdGJlV0hlSAoxR0lPS1lBd1hNSjUzcUIyNXRMNElvd1BoVzYvOW12dU04dGVXaFJveWtUbnJyTWZpTXdmVEtnMjFLaFJhTGNECkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckJ2VEMvZzdoY0ZUditWa0FJV00KSGlGc3ZLc3dtbjNtWDFYZUJsWENocjlFRWduRWpLSEpTaWxWT1F5Tlpiam1YY1A1Vlg4SG5EWUd3YWxpdjdyVwpxQ1ovVFUyd3hkc1k2dVdJNVBYVjBvTXFMbjRmUXB6a05NNmU0Rzg2RTVtWENFVTRJUWdOY2tWQWFHbW1NV1FnClBEM1g5Rk92NTBSS2FoN2czOFZhNHlNRWRZSTgzaXZXRVptU1hCOGxYdGRtSm9GNGN3WDV4d0ZYa1Q2RXhEL24KNEh3VFBqUlZHdmE5a1dqdXFBWUJqdzdhSDlxTk0xKzc1T0VUYlV3WWxmSUV2QVpyVldTdjlOa0NyOFQxaENWTAp4a1QzZURWV3NyZTZoekFtZ3ZVUjI5a0pkUmg1akcxYUVEOGhscHY4REIyMk0zbkdVQ20rd1FJYitGb2c3Mlg3CmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWRTb2tTZHk4S0JDWWhUR3AwaWIKZEtIQ1NEdU9hc1g3NVQvYlVYZ3p0WnE2MnpQRk90ZWFyMFFKTUR2VDc0eUU5bkpuWWdkVlJ4NHY5RGJ5THEvMQpORkh0S2pVbEpBenkwdE9sTmkrS1U1dFNHU01UbDIwdHlGd0MzS2l4WXVXSERtSTQwczZ1VktRUDI4dWFoS3ZQCkNQSFNQaUpXMjYwa0tkOENIYTNveE5GL0J4OWU0V0h5VytlYjRCSXZsekUzTUdXMDg5MGtaTFUzelV0dG9GMUwKenpTYVdGVVY0WVVVbDBWdjNLdVdBaDVEKzh4aXpCSTJvbWxESjd1Y0JoeVNYdlp2MXI0eWJnOWVBcHpQeDFNTApPejRYeFQwSzJtOVE0Sm0wbDB0MHFHNFhHcy91aEdiUjUyTHNUeWxpZEdlaXdVWlIyZGQxU2ZxbkFPQzdYUDA4CndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUxzTHhVeWRYR0wybmhaMG5XVlcKd3U4ZDZPWEd0NmhhNEsvYUNMLy9RSHQySGJZRHZqcm5aMDBHRjRuTmhYTjNuYVZwMEZSWEhYbERwM2hHWWVaQQoxaVhXbHlvZ0VwZnZlbk9jVHJFbmRKUkR2WmFoQjhHUGRJS0paTW9vYW5QNTJzejVEL1lJaWxzb2cxeGpCbDZUClhGbU9vbHRYZmxVaEFKV2FxWm9mVWlna1lzcDNNak5kL1NGSldnOWY1bTl6UFByN01xWXIybVU0dHdVbjlIcjIKOFZ5Y01nZGFNL01Pa0YrL2tiYU1ERDdUb0Z3VklCWmtPNG9lelRua3JOMkQ1dmlvMU9xVlBhdE41WW9SNjRuVgpiV2xsQnIxbjdFaW83WFFkV3cwNy95K3FoUnVzQVlDVzVJSndzR0dudjVaZUc2aGNNUE5DVU85d3dWaVFpdFVMCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXdkYUN4QTRJaDRvS2x6c3I5VE4KUUVlT1VjMUVUYkg5dEQzSUFVMVJ5S0oySTNac0c2NVl2T0taQnR4Y1lRbERlQWFycmQvelIwVHBEYWFNUHhvMgptVzlxZnRVWE1FTlE2a1oxbWxQR1pvckxkNkNQVFNnSzFpbzlVSWZrS3JpWEFIcGthR0ZkcGcyZTF1V2NDRTZVCjdJaER5WWZWRDlJL0tkYk9zbVU0ejZxRkx0NDdlaXVXQnVYekgveUpFbDl1akZQNzhEOFBaSUdYa0tkMlJYNEUKZUlWY1FKT1JnR1pXbURWSldlbzNzVHFMSFBZZS9COVNkcFVybVMvYlRoWFd5ZWM3Z1JwRnFTSzZON2lwbng5TgpMKzBuREhscEhPL2p3Yis0WmpFVzkzeDU2Y0VtRWxSZGpNZXh6cU1iOW1NNTZBN1Q4V1p3L2RsenZ4VE5qK3lzCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNENOVC9aOUdadlYxNUg1UHI0VlgKazBkU2lYK1FQNzdkcnBRRTd6Q2hUd25hVWVQTUxVcXJBMEkxOW9COE5zaTg2Y3dWeEgwc3Nnb1RGOEJoZVpNcAorNmxtVjlOT0k1VTc0NUxNYUlETHRmMkJZNWw0dHlkamZEam1NeDFKcGphZy9kanh3ZTFtVzFsa043STl3eW1sCnQ2YmpwY29iVUJBdFllOXR3QUNuekFqNVljR3duME5tU2xrWmhuYWFqVjBibXBtYWZta0c3MGlZeitDWEE5ekoKUDlCTVpMWWZ1aUt6TkUybEJrRnZJYTM4dzZqVEoyRndyMWZ1WEtTaGk1cjhCSS9YVU5aa3ljYWJ1d3RlVkhNSwozV3ViK2NIdXZZWWFWMmlKUEdiZXNydzdPR1F0ZjIyTExMcHMrbGdlanluUEpaTWk5YWc3OEpKNjN5U2dydmo0ClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWJSalpIY253aUNRL25ubGFFcmIKbVJFUmFUUll1WGZob05BVitMQlZZZjVEa1VrclEvdHoxWEoxam9vSkwrTEZ2bFh6U1NQYWRSNkJWM3BJK1NTSgpOSkRQZ0haM1VDeTluU05CSEtoOElMaUJqd0FDUFhPcDFnNmdtQ1NoQ2JPaHd3NDRGQ1oyTE1kTWtZVjI0dEdNCkFVeXdVOE9GVE9XdUx6L3RDYkdHVC9makkwKzFXdEo1OTdDV1RPdWEyUkZ6bS91RlpNeE9uSThUY0RIamxFd1YKVjd4emhsVWlGWkUyRnZEWkpRNDJYeUZFTmhqRVJDeTdnZW9aSjlvVGcwMkE1OUdBTVMzKy9nVGpPU2QzdHBmZQpsckdCT01Ic0kwelU3b2sxc25SR0lXRTVON0JrbnVUV1hxSU9aQjBad1IrS3dxV0R5eURuK0NkaUlUbGI0QUlxCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWFWdFZpOFVyTVpGRTBXbVNVNjYKOGNtUDBlU1JNcGZGZWtCdFZnQ0todE1VdWtnSWhpekQ4U2ZINnhOVlEwb2d6SDRCZWQ5T3RUYkYzUGxYRGE1ZAo2RWdFNUlOQklSbkwwZy94OEZVOVljRlRpOGxuMVFPN3RrbkYvNHJOWFpNdXk1T043T2pCRjB5UDFreFpDaEo3CjR5R3d0RFZ0clVHZUg4RDQrY3M3KzU1Ym9GZStBcVhhWnY0OEFhK3BRMEYzRVRDVkFKTmJ5U0Y1OTAyUk1PRjcKY0k2d1A3V01pUEh1UEJibnNZZkE4dFJSUlFSMzJ2SVFWMGRFdmFURjQralBSbjRSUFFGczFETnVIdktYTWJNWApsaVVoYnpURThZME9oL2NtbnJzTnhMOEpNTGlYeDJVM2dTTXRuekNXY1hsZmdtZ2xmUlgyWDg0YUVENk5XL2JvCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFB4RTdyOElteXRXVXJNQTVyZFUKQkNSbkx3RENjVlhpdmxzSmJpZ0VFbitnbXVQSkx1Zi9ZaXNWcHFabzdkbmJkZnZkNW4vQ0xUdWI5ZzhicFlpVApmbjZRcFQ0b2tvTnYvbVFNNEZuakxrb0RzVzMzaVI0Y21kayt1YThRUVJ5QWI5NHE3NEhhTzRsbW91ZVBycm1TClR6OStOdXpDcC9EM2laQlcyTVZhTHNBeG85bHpNVkdWTmFNR2lpd2h3b3NGTHNKOHBFV0M0d1pGTDVUb0xxSlcKdWtiSWVGY1lac2l0YUNycURLMEo3YjJjTEUwOU8wL3lOMEp5T2hEQmpOamdFeCtkR1ROZy8zM1A5ZTViSU16dwo5R2g2a2pianJNV2JUYlpIbnZNdG1NNlk3eGtRcWZpeXRTc0tIZG5ya3JqSTdQelVZUXVZZXZ3ZDUxS3RsWVQ2CkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0V2QWFTRzNWTVZGcU5qTXpROEgKSTkxTjZEVjVNTG0yaXQzMHJrS0s0SHFYTDFrMThSOU51UmVzV3pabmFKNUttVlJ6aGZGNko4WXBxWkJicEg1Uwp0TFRBUlh3ZVhqMG9NNWxzcEY3SEd1U3IwOGp0YXgrVE1uR05ickduVjBudFlNTGlPNXV5Y1hkV3VieUovUXh5ClMrdEJFUCtkSk9VYkkwcGp6K05oTk1ITXFwdG1MdjNiNmJ5aTM5eSszT0NvcHpJbWYvV3dWNzR3VEt6bWNNRmUKNXJEdkwzT1lNOERwMmdtc1FMQktUZk12K1RDd2tkeDlVdm9PZkxKRzNZU1dMcXdDc2N0dEJGQXluakU1RnlBSwpOb1d5SFhnak9RMThBNlRNTzJQejRCN1RXRGg5d0ViejNuOEozUmh3SnNESy8xZklhd2trRm44N2cwamk1SStyClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTJuU0JIblNuektkM01CVWd6VWIKWEdIVU5CNUxUczFQK2xockc1QUFyMitONS9kMjdVMmxPaUFmKzJZM25QUjNqdFdEb0FxZVZFQUlYYnpiVGRtTwoyMTFQVnY5clFsSjhuK2NzbWNVZENmRTZsdC9vdytIN2VrUThFeXA0OEpkUy9xQ1dDWEl6b3lSdHlQWEgxbE1BCmRaaE4xZFR0TWN6Y2RhWUVSbmtBMnFPbzh0cVY0MHovcG5nWnl3YnQ5M2tSMWYzTUFHZm1PMS9xcmRJNmE4OWIKM05Uc0pFR1BDTFRWTC92RHJmbVdRR0xkcUd1ZzFITE5ycG5JM3hNK1NKNUE2bFh2dEdFSEpOaXBwK2kvY1o4ZgpteURPbUZrZEpPNjJyOUU0UGhER0FZdkdkdlI5YjIwUDM4WnFEQmErTjJLOStaN1pCMmJkdGdwemhVazZHSVVECmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWdNL2ViMkR5dmhJNDg5MmlCc1oKSmpOQ0xtb0dtSzZNYXhBb3I2bXBVWk91Y1JOdEV2TExkcGg1QTAwYVlvb2FwdTk5KzlDVFBWYThNQmd1SGJRVApTT2VhM0VXLzlnYmVWVkpFUG5mN09aYkJ0K01yRUhldXBCZDBOT0R5emZpeWZRbWxHS2xibjBEMEFPa1lVeVF5Cm0xTWJoczFqQXpNMXErK1daZTVrcVlTSlJmcnpYS2R2MTd6M0NvaytDRVdqbm5TVWNhT2tKeFBWZnpmSHhKVzAKVEltdHh2bGlLdEJoQk5oYkFiOENMZ05GZnErWXovUFU5Uks4UEVCTVVkWCtaMzI5SzRWcy90by9lQXU1L3ZNdQpwQ0VUcllsQ3BxVGh6bEtOOXl3eWtrS3FlcVpnUUFIM3h2eEVCdTZyZ29jWGlkSE1kZkg1bER4cnQyanF2OE1jCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDlnQTNmTmc5c0YzZFE3T09WVFAKeXI3TmpXZWRpV0ZldVhvaDcvN3BPQi82ZXk4TFJNMkpGVEJYby9HaTYwbjFtR3F6TjF0WUtYeUVCR044dGRIQgpBaXlQSWRyQkRiUDlNbDJuWTBiYndXU0p2QXF0bGtpazZOVzdxOEQycE9sQVRhVm5pY21aR09VYkEyWWY4NVdNCk9iby9rZnlVOTBJZzh2YUhoNHRIcFVkM1A1dHhUc2dzV25wemlneDU0dE03enZGRlA3YUdGa1JkOVdGZmkxcFMKRmQ5ZnpMeTUyNGRuWTJBL0NXSDBBL1lNekhCQWZydkZDbkVYSXVkWlVZQnF6aW52SmVISXRXNVd3VzFIaVdjegpkN0MxU0ZwbzN3STNBcXlDUCt2a2Fzb3NKQ3pqMVlMcXR6VDQ1ZS9YdjJ2eDNabkU1R0FPbjBWdTM1UXh2b0xOCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHBjamRhdURwcklKN1k5dEI2U2gKbTRDK0NUeEt3U2E4SFcvVjRxNUlHT296K1MxNDJJMDRtR2d2cGNuN0VtS0R2aktZWXBkRVNIVW56RTRtMHZHTAp6Wi8yTWQ4ell2V2s2ei9ERHN3SmsrRzN5VFNVbGRSdDhUemRJNDVqM24rbEFIMy93K2xHWTdFUksyNlJTdnZYCmVrWkRhL0JlZ0V5eEJEYmNZMFZjVzZtVHNGZERqOHNNckU5aGdjSW5IMUFTSmJoelhjNTBtUndFWkwvL2IzODcKRTFPbHhjb3U0V0FWWVZ6VXpsY1kzb25abVBqeE0rWGMrY1pXZkpWdisyMWV6dUhoNjdxeU5YY2Y0blQ0Q2p1bQpnejlzclgvd3FHSHBvS292ZVdpenNUQUN4Z3ZEMzRQMFpBaER0ZEp3S0pUTCtLdkZ0V1N2Yk5rdExjTGV4QWErCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEtmNVNJSmpudTBna2tqUGtHNGkKMDlFV25IMzN6cklMTHlmN2lUTmsvbGVha1dPK2FlR1dsSTIra0FHcWY0MXBuWUFDeHVlc3NIU3VRbk8vNzNOawpJL1lQaEQwNWlEZ2xwd2JUL1hycWl1NUdpN3BGS2dheDZYMXFmN1V0YlVNWTE2cmhiV0hVaEJqeGlzdkp1RUJLCmIzZWNGQjliWnc1c0MzckRDVG1JTGo0clQyUHM0djhDSXdmTUNYdWJXYldxODlvSkxzeUFoNThoM1luMlFmOFEKTzhONUNsQnJaTEE4aG1UZjJxeDZGZ25MU1FMNU9GNDdFOVpJS2JGY3NQWkE2Q2lrdjZyV01GbGJLV1k5a1NPLwpQVXNZQ2QralNJQnl1THY2MVVMWjhTTHJJWkZOOHNFTFVseVBiWWRDUlNrYnBHMGkxSCtwTkFOdG5WdTJ5a3R5CmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFpPODhJQmY5S0NSSXRIY1R5d1kKdUVaMVBaZ2FXTTNXZEc2Mk5pZ3gvWlJpOG5QZldZQXk5VkF2TlIvMytrYk1DaVJpNkY1S0FYdVJtZG5kOTAxNQpKTHVlQm81YzlIL2ZUTkxia0JCVCt1NG03RHJyU3F0WGNUSnljUEpyc0JleDdpTjNibi96RW1DdUtlZG1UR1ZsClRKOHhmU2c0UnNoNXREazh4eGZ2Ry9LN2ZkSGx6bzlBZkF0RkhXZjRjdE8rbzhsb3ZtR2o3ZzgvKy9acHNpT1kKS2ZQa3NuZmh1OEthbXFielNmcThVL2lCdzFBTFJhcytGMW0vb3kvaHFKMjlVWlFEVDdPd3NReGFLL204N2w4ZwpiR0psL3pUQkFIcy9tUjlYWlBCSDhwY0JnV3ZNdmV1Qzd0OVV1K3lqbG03ZXRJQnVUdk9KY1hvWUtzamViMmJkCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHRzR1B1ZUQ3ZjVkeFJGSTJzUloKRGFVRWhrVTZveEp1RmM0SGRpMHJvR3p1REw3d1dwTVk4VjZTdDZrcDNjM2tQeVJUa1Fjbm1xT0s2STJNMEJvWgpROUFEVW52UWZtWFFCd2dwYU40RWcrN2NXbHdTV2JZZVo1WDlRNlhPdFBVZHhtMkNob1F2R3VoTGRoVStpbmUzCk84R2V3bkRHMGJCcGw5a0RxVVpURXFHNk5DWStxMWFCZWh4ZWdsMTY1dWduZ2w1TVFHTkZkbmRHeTU4K2xBajgKaVVpdGFrNzlBRngyRmh3Znh3K2lIUWJXRUU0Y1EwSWMrbmhIcmU0RVZqL1MzVVd1UXpPdGRYR1pJM0VHQTcyQQphSTdsME9pbEJQUGREUDFiU1B5cTVJcTRvV0dmVHJabzNUTGEwZEpDaE9rTXRZcVNHOTZrOHFXY21SaXBub0k2CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2d1MHg0R204emFmZnBLQiszVkIKdkZkWk1YT2ZIV0k0cEpxUHhPYkp5Wk1GZ0xBazlBeVhjL2prSm9oRzVSdTYvd2JDZkVzZXBkVklBQkNSektzRQpJU2FGZmxVTzhBN2E0eFZpdkxaYkVSa0VWN3FwSDhSaVNLTW9HdEhvR1hWUzQ5Sk1LaitpUzROa1VSY2l4SktjCjVNaHRlVUs1dW1tc0tza2NCb1AxZkVmR3NLWUJUZjZuc2FMZDY0ZnQwTGhRZGRpQWNkZnNJbVNtK0V3dnlhTmUKd1MvRzZ3QVQ2a0N4Tlh4UWtBeFl6SnkzTW80UmRFSE56aFZEMnRXa2U2MzNjQ1JWUDNxQlpYb0Y0a2tqV3VteApzeGc5K0lGQ0xlaE05WlZHMVh2eWRNV3BVY0xQbElsemxTN2NVVmlJRXhGa0NyS0J0dGJubUpWS1ZYWjFNYW9tClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG5vcmx2V2tKQmYrUW1ra1NGd2MKY2FKbXRPRS93aHA0b2xTcytxQkRBUGY5dUpjVllUV0sxRHk5MElxNVJFZkhtQkdVQks4dktmS3Z6M2tBbzJWdQpMaDQzc1k5UGZQR2lLTVpWT2gzZjJGNGxRbHNJMG9kcWlOYnhBTUpWZGh4TndUWUx4djhEWVlkQm96Z3c0by9WCjVST0t5b2h5Zlo1ckZ3ZVhrcnVpQzlER1ZsVzM5SEk3REJpQjZDM1BEMkV5bVRUNmRpUHpJWmdneFE2b1huWXMKTDVsNVNiSFowYVd2U2o1bjRQZVZyZm1XcEd2VVgxRG85VlBFbkQ0aGhSTDFhZTFFclVzYlEydGZ3VmdJeHRlOQpZcThHVlFPL1pGOTNDaFRWRytmcnpWdVFTUmtERzRPSlhHYUw4ZnFWeG5BcCt3cVVJWkRmU0ZwaDZQZU56TkthCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW5XUG9NSXlTL0RZdlZkTG8yTVEKaEg0OUI3NGZRVlgrRDB3OHU3dlo4TngyYituOUFlQk1SQUNkVTl0bVJUTUVPcVFmQjNJQVVIbXptS3JIV3VabgovU3lDc3ZvUjVKQytkOEt5dGllN3c2OSswUFlYWmxOWGc5bmJnNzMyemk2OExjUTFyWTFrSlpHam1hdFFpNE16CkFwajlUZWpldk5oU1lPZE5NWnM0Ti9IdDloZlZCNWVDamJ2dDBEWWlLbXpnQkRpQllIWDViS2E5Rk5BM01GL2IKME90endQa25TejE0eXQ2Y2hLMDBuWDNLS0k3bFB1M1hLNGprd1V2TnJ3ZE5XRjlLeGE4S2RmdkdzRng4cHB0TQpJWkpJK1ZGTFZDL1JVRnl4QkJMcTA1em5lamVTVmFaRW5VdzlDb0xkZk1kZThvWm93UG01YzhhdXlyek02VzNoCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnFqc0hNcGJyMys2bk5aZlRTcjQKVHdnbEIzcloyTFowRDBwcnNqQlFsaWc3d25EaHhPeUZKMk9HQ0dPOTZyKytUcmd6N0RiYkZVcTBmRW1TNWtZegpFYncvNFlqd29JN2pUUHhreFlxZUZVd3hwSmd6VHBEblBWNEI2dkN2YnI2K0hwcnl3U2RSelpuaDZod3NVNnc5Cng1S0dKYlUxYVNBT3IzcjYzY0hIdWE1REhUUlVHYWM5Y00xbERVc1ROcnRTN1FMbSsxVHVsRlJBTTJpU2E2WDIKQzFoR2F1dVlKb3B3RkhRZk9OanYwa2orUnp5QlBIdjNaQ1dMdDBEK1BIcFZvK205a1Y3cE5OV1RyR2FuY094UAoyZDY5TGNDWmh5L05UV1lpakVZVExJOFdYbHh3eWx5d0I2SllPUG05RlJ0QytaeDJqWjVkSGFrajBwTUZWOWxjClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG56Vkc3dnduODZhYmxieTNlbDQKeTBQN0w3YWZCblpZbUljblhtNVU4QUpqeFg1Um5SWjIvQ09sZE5PVUhPRzRXUFF2ZWdobmFHck5peDVKM3NlSgo4S2hFbkpwSmRTR3c0UzZSZjRqdm9LYU9RR2xtZkM4dkp2U3VDcWhOYVZlSXkxSFVMV2FhN3BieWlNd3E5Q25ZCk15WUtBVWh3L25LYlRvY3JGbUdHOXNpSTIxbldXNnlDSXhOTGFiemhvR2RIcE16UCthMkh5dXlKTmkzaDBvMC8KQU1jQjhQNGtXVDhtZ2Y1U2RreGFmSUp0UzY4YmI1Y1QraXZmalJiUmdFSVZrajYyd3d6K01LM3puRFhCajJJawpLdHYweWZtWDV1bTVzZGszeGRnQWlOb1JkTDhVdGtqMmdmUlJNVEpFdHUzYTI5TkkybVhSUEwyMzlFelVkbHE5Cld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbVJMNExCZU96Zm9TQlczVnRLelIKZVNpN29BZGZjcUVZeXVFd3h1Wm9MaUh0UmJMNkUwOTlISVJscGIzN2NsRVlCc2poM1cvT1p6M3kvaFpqWi9zeQpuU2JqaHdia2Y0RHhOTjkrZldkNDFMZHVwQWVoQ0JXUVZOODd4VWhoVnZUWUdaRTBFZW5CQ1Ewbmx1UG5lSG4zCjNWellsaVN2cFpzWjRmbFh0aVVydmVPQWxRb1hSbnhSVDBDZ0FoMTc0dnBYdmd6YUtlTmVwdkdKY0I3SlhoWnMKMUVENkxFd2dHWWlYcTdkRUMxaDEzcVNPQjNNbUFMQnFIQk5FcXVTdC92Njlib25VemJ6Z2Fia01xQ2U0QkNjUwp4TE44Y21MRHZtM1UwMFRlMjVPYThaakJlb3BWT1ZXWmNwSW80SDJpaDR6V1VrL2sra0hvQ2NzQ0lWL3hocm5KCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmF2MDZ4ejNsWnBzSG1DUDhzcnUKajd4OG9kdHp4Yy93NUJueVR6bmNBbUtGTi9UOHlJYk56ZjN5eGZrM3RxcWhDMG1tdkl4aWM0dytvRUZKNUlyMgp2bTVJUnlzd25SZzVxRjZwUEw5V0lyVUlTZWlRT0pWeC95L2dLNkswVnl4aW1GZDExamk5cGpEdFYvNkRUQWtxCmlhM2YyOVlnVzVnTFFQZW9sdGp1OUkyQXVvOFZSMmpHdlVCK1RWd3JpRk1jVWxPd0JSUzFlTnpNajVQKzd4Y1MKN1JoTzR4QWgxNWNXajRBbXNYY1dYVWpUaUJpSHplYU1sM0VRaWJJTDFRU0Y4QWRjYXFhQ2lhdklyVVNRZERKawpkcjYvMFBrZDNCVld2SjF5RmU2NUs1RjhYNmZyU0NuMC9NV0NnVUI4OHhpWUh3NllGbkhzL3hmY0YzWW5adWp6CjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDJNUGdrK0Q2UFYzTnJKY0Z6Q1MKcFRxQVNNdmV3YWNzam5WVUxFYmtzYUJzS2pOSFpsWkh1TFI2Y0x1S016a1M2UU9KbVRzL2VDa0N6dEh2MDlvZwpOeE9kVDNKZXcrRG16T3NXWVZ0WXErMWh1SzFjc0dtaml0aC9VbXowVjZDelZuRW5rM1c5NkpST2pPUzN6d1dWCi9idTdmTmhTcUFEZ2xRdXZ4SDdsdzZsSFlJVHFvSWtiNGNRalRXK2RRejBpQWZmcTkwRkNDcW5SbmczVnNHNEwKWFJkS2FPcWFiaXJja0lNbHVtM2FDSGRDNkd0YU5Gb01laG9uZVdVRTdyOXJRNlRmblhXdTVWMHNlT2dlalpaUgo5a1F3VkF0QUU0T2FWdzQ1T2ozc1VFZWo0OGVQVmZjY3VPYUFlYXc4eHE1T3FvOUZRM1RscVZKWDIxY2RhYndjCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBblNWZWJ1REFNTURjK05oVUxpUlcKZUI1dUlsWXY5ZVlxRGp5OVhvTmlSWEEvZHdRcHpDNnM3cExrbnR0d3licUhZakpsdnh0bEpidSt5emd1WVBBUQovYXF0L1YzYzc5eGJFc3kzTmVaZjR6ZGt2NXNkRjFYMmIxdVZUMFBaR3Ewa1NVMXg3aXZiNHZ5aGREYlVtN0hFCllmbm9rU2N6L0RhK1c1QVNVT3E1NmZvaVg2K0ZnTHFXeWV0WDhROEN5SVVuaDl1VUd3bEtLeHZGWmZpeXEwK1UKdmZ5Ykd4N0lhSVFETHl4Ylo1ZGR6ekI4L3ZSREF5Y1VPSTM2TTlGaVlsem9mSy9NZ2JJaktnemhBRHFXT3pBSQp4T3hKUkM4eG92VzVVNm9jdHdGMlNmZWpFdG1xMEFMenZNY3U4eXE3TnB1YWxxYXQ4eEYvVW91ZmJVUVBGS1J0CkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTA4NU5JMldjTjFGd3lwU1pjVTgKT0x2dFhUQ1U5N3JkOVBNN2V5ZVRhYjU5YUFyeVA5UXdiN1pMNmlLYnN6R2RTb2Ria2RmTm5TZ1RCVzI5WHZBWQpwVVFabE8yallKdHFPckJrM3I2bzNEbGUvbFVGQWhmZWdaU2t6V21TTTV2bkVla20rOHFiUTBSMlNSWGRrY3ViClFkRmMxQ0huVHBEZEgvQnpCTzRMd3NmVXhEU0t2bEpDaXMvbEtBaEhQKzd0YzlZd2NtblpKc2I4bkNXOWVNNXkKaWJSeFNqR09MUHRFdWtDdjJ3cHZSU3RtRHZmcC9MVkRHdWJIYnFtMDRMbCs5ZWpxUXpaK292MnEzb2RrZ29BTQpiU3VKQXRWQnR3T2g2aE9LeS9ZcUhlOC9NN2ZhQXlNRzFRd092WUIwVFdoaiszOW1YRE1UeUhveTl0bWQxMzh0ClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHhDTEl5Y0hhZjlxVDl3UVdWUEwKOFd1VE9XRmtEV1NrdmY1eXQzVlUzcm9odCtYTWhwNlhIekJhZUwvcURuNHBiMXBsUHNQTXJPYXpCU2xTRUs3egp1ZUJ5VloyQkRuaDh1VWJadi9WM1VhQTJmcXJXK21VdE5RR0YzZ0R2bEtoc09EUnhtZlJhajRiWnNyNHB6NWYzCnJ2Tk5DaCtIQjcwSlovVWRFWkVTV1I5b0pBZUhmaEg2aityaGp0cFdMRGlZQUFQZnYxV0tHZjRnR0lNQm92dC8KczhZR25KYndWdVk2Y2R6eUZidlRnR2RUM3Z3KzdYc29xQVJpUTRyZHNoSlZSY3pocUQ1NDVwSy8xRllURFc5ZgprUUFkRG1jM0taS3NscDhjMEFMVWNROExSdi81SkNwMWNJYjdxaEtTcnI1b01IWnZwRkI4anhkV2x0eU9QOENWCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHhneU9sc2V2N1UrajFOZmZYSmoKdXI3MllmQnhsdDYwNUMxNVJyNW1YNU8ybUFmTEZRb0Q1Q1dVWG0yMnNlZjJsMjJKS3E3dmkzd1ZKZVpPSWRQTgovY2ZESC9zSExLVzR5Q2kwcFVkcGZFRzE2cVJmRnQvVVpxUUN5Y1o2Skk3eG1neUJRUSt5TXp1aEgzRDJQREE3CnVnVUw5dm9RRHArcmEwMWxwb0NLaHFlQ3ZCeTdlUmRGSzBLTDl6c3E4U3FuTXNub1hVY0p5THNFV3hGSEZFUlIKVDhTNVVqdDI3QUpaa0ZHS3cyZndtSDdtWHU3V2xyajNTOVpUbm13V1NWaSs3bTI3TXQzSGJjVzZVTGpXY1hnTgp2VXJpZjJVQkk2NGdjRUpVcE5VcDFtK0FhMmdZTW04ZmNpMDA3a0x3dHFxQ1QxUGN0VlI5WFdxaC9JRXZxbFJtCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEZ5d3ZaUStkR1MrME9PTW5WUUkKTzFCQkVzVFZta21uUm5XVWI0TGxiZDNJK0VqeUFKNHNOdnFpM0ZIVmFMWDZ4TWN1bEV2SEF1Q2twSVZFcGw3VQo5b1FPdTN2WkZvMzBGQ0pBT2lZbFg5WGNyL1EvanlzcFFJQlZrbnRhaE0rWkNmS1FaL0hnUENqUkpkU0pEODdoCnJWZ0R3WStTYUdrM2RjajlhdCtlYmdsZ25RUlB2M2I2UVJGam9xNjE0S3RWNmh3WVplWDRSSy9zL0ErNGF5M24KOXhnaXEzUEF3cWNFWUFoeEc5UTJ1aDBESmFXWGVRWldNdmhZOHhkVlNOcFFzYUpUeHkxQi81Qmh2aXR4b0t1ZwpXSDBWY0ZBUzk0UVVGSHZLb2JoQXFlOU5zb2ZuWW9BcUI3ODVSWG81YjdoOGJzOEdhSWJia2NGSy8vN3FKM2h4Cmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1QvdGxzMENIZGxFdVFOdTBqMS8KVkJHMFRQVjAxMUhFc3h0bnVYSFVqYmJSdjFrNjk0UzNiQVhpQ2owdkJWbnZpUDJ3ckI3WGFyanZxZzNkWHByNQovWkZtV3Y4TWRzaXRneVc1WXBHTVR6eng1MVhNeHh4SDRBaFZZMG1SR1c0czN4THRZZ0FsajhvaXBSNExVVkpnCmhlcGZoKzVBQzliRXl3YnByamFlRy9TT0s2d1NGSUdPU0dLcXpYTlErdXdKTnFoY2x4ZUkybHBIOHgydTNPL1MKQThWaGhENzJJZDNIUXJLVHlra1lvRkZWdEtQdDdKekQyOXI3b21mS2F3bUUxY2gvYWRnT3czdy9HWlpRcFBLTAp1QWM2UTNiWFBBdERFUWphdFd0VlgvK1BOYjdIbExVcjVNR1BwTjFwN3JwbVZqdEtoS1IxWjBjTUVZQlNIUVI2ClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnBUemJ6MlpBOXBrNmFsWWN4OXYKNTBubkFna0c4VlpEaXhlSndjOHhxMUZsUjM4THRjMHFkTkJ0VTdodkZmRzBRZzFtcmM1aGJUNGw4ZmNBdmtuQgpFWDF4dkpNSFdpUUhwdUFQK0hvVnhES2dLbHI1Qnh4WUlzRXFuNzc0ZVlmL1RtclZVSTNVekN6Z01UMTViWFBaCjVpbGhTUWFFL2hhMy9QbFo0dXl1dFR0TVpESlNxaHI3cmsza0lXcExWZVdLbnFnK0tQbHE1UHY5eUxqU3NEQ3YKUlRQOHBZd2UyRzFScmgvNXR3ZjlkYjFqVmpYOFpFaDN6Z3lvanpNbERnNWpNaTJmS3Y1eURoelhwN2hGamEvMApCZ0o2S1BYK2FNV0pSQWdaYzA4NWluQVZOek4rTzNrUWEvOUpSK2VhbXVabjA2Tk51Z1g5aHMzYmNLTG9qUTM0CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcy8vWXRHRXRiV1A4b1dpZS9QbmcKQTJlb2JYb0VpTWltbC9HbTdlSVExekhiUEsyL0E2SDNKMUF0dExoZ0cvbHdWOTdkUHJmNGduak13dzlZR3J0UQpRazBMRVF2UER5OEtFUlhiclk1QU15Q0xsdjZjMk9NVUFTNVN6Tk16SFhibTBwalJDd3lPOVRrWXljMnF5Z0QxCjdnd0JjeE8rWCtkcGdVb3pnQkhMNS9EZUN5MzVpTVNCNDcxcXUzTXBDZm1XMDM0SzhhdGhybWV6akJBekYrMWMKdW5BeXErc3RmZFlBYzE0RVFhVnZmNjgyeUJYeTl0ZklJa3JSNDFkdHh4OHZaM0dZUTE0VU54cm5OSzE3SGJleQpFWVJidFpNZGE5WGJBSkpoRlFDdGlPZ1BveG43STRpQk43bDRjekQzTkJadlJuKzRYb1BuQ0w3TkswOE41WmNCClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3g1eTFtTHRxcFVXM1RILzdoUGQKSEJVZlZLZVBQK2lSQnhxSWE3VkVwMlNFSlluWjhGNGlBQVFvNzFrU1FNN2xzRmZXYlRSR0FxMFJwRE5xWDBESQpmdUFCU1dRTGtPMllwbWZYQ2Zla242TlprUDVpZi9OazBLRzIyR2Y3RjVMQko3NEVweUZaVE9GdkhuOEU0VzVKCmFlM1RCTHlLeVg5OURpNmNDek9oSzJYcXdUaGV3ZHVWMlZKMFJRbUhiWDZSZkdRVXE1emxGUTFrTWZDZ29tNkEKQjlPL2xTQlpSeUJPcG1rSm5iQUNNN2paNDhKTFVjcGpMRlpTY2FwRU5CdU1kVWR2aU5rWTdSQWZTNDRicHdHVApPZTFJOUJsZ0lSSXNJdlNubFk2ZmNCTlBsbnNVOXZwRmwzM3pHcmNUa2ZKSzZFaFlTSWdlb1plek1PWnl2eURnCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc01la0FacUlRenYxVzMrZ2JQM1UKWlIxbmtvdDRFWlo5OWZEZWpGNFNiNEpZODliQjA1d1dQa1IvSmwwRDByWmUva2pjV0xmZFNkcUtuelc2MkZySgpvenI3TUNQcjQ0Q1VJQlcyVmVMY280UTMwWTNUS1d2amVBNmZwL0k0bzlCOGVBMStDUXhIc3prd0cvaVBYZGNZCmVQQ0kwMDJOUDQ2bXdjVVg5S0NkbXA5dk8yMDdXWW5BMnJQQTgweCt2MHNJVXc5c2IybXI4dGEvM2ZsQkxPWlUKczJDemEzblkzamJzRE94Q2FXNmZxKytxdUVKWC9YcndXK0YrbjA1bGx6WC9XOSsvcWk3YjlWTTg5V3AzbE1pdQpVa2tWTVI3VGdOcEtxajc1VnhRekxSTXpnRmFqY09HNDdaZDNBY0d6WG1SRGdweUxHREVMNjVvWVlWNm8vWC94ClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3VmbldlTHp5M3JUdVNQSGM2V0cKVmkyUHMwUUxvZG5QT1dxQkpNLzdITlBQMnMrVDlMNlliMXFkdy9nYWgwTTVydDY4M3NmU1lETjdGa3F1bmNxMwowTWJxN0F0VEIxWVdUMXlDODlpOCtyeUpycHYzOUl4NHJZeEJVcmRNNlpBQ1o3MmVETEZEc2d2czhkVzlkTG50Ck5yZkJDMmtqWW9PcytqVUdibGN1cFhnWkxVL01wWFpWSXg5bVhhRlgvOXRtQ1UvbGZxWHVOMVptNFhGVi9ZTm4KenlhNUNYTXMvRUVvVnZ2VnN3a2tZclVvRWNvOHoxTmlPRGJ6bmdGWkd0WkxpaThYek42TmVycWZKeTVmV1Y1UQorRHk1bTBzenNybmhIVHJNNXZDYkE3UElSSC8xMmN5WWFTdnUrQTlxSmJ4Nk9ldm1nUk1EbXdJS0pCVG9HZllNCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzFLcXZHaHJ5YlVIZ2l6UWx3aVcKYTZNSWNqMGl1UkRhbk9ON0ovc1ZLdm9zc0tyZzErQU14elNvbk14VU8wT1JwU3I4QWxPTmprMGFGN1hjTGsrMgpXZ0Z3WVlqbVBoQ2Vvcm15R244SEErOUg3aXNOeEpPbHdvWDJGL0RCdU1kQlB5UkRJVTZZRlZPbFJ2SVFDZVQ2ClMzYzFmU0ZaUlBGMTI5V1N4cjNLcGdiZk1iKy94WjNxWE5GamVaOGpkVUJPSDdJK0hpWmgxRXdiVy82YlYyNkUKUmR2WU1DUjZoZWRtTjlZWW9zUE1kUGJqUTNsZ1lwbTlRY1NyOXAzZUZtc0NhS2J1QzVTMEtSTjIzZU1UU0puVgpkOGRPYk02cHNKREt5cFE4NDAzekp4Zzl3QTZkK2FjSEJCSzNIcnNpOWVRVnlQTTByS1R0cWovT0pXRENKOHRqCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGEvbXRuVFdHVlc3aktqTXNsKzIKZ1ZjMkEyRVEwVEI2ckI0NHFnam5hRG0rRmR5WnRCREJLMHF2N2lpc2Z2eTJMa0FOUnVMdHJZaXlIUUsrTzF3OApBQUxKU3dHZ1FQdDQrQ3hxTWl5WUcwQ2x6Q2tpRDlkRFBqTGlZclZYc3pCQ3RyVTVYMEdrVWlCdWV3Z3Bkb2hvCkNYL08yRzNUVDk2Rm1wVWQ2NGgyMWtrOWZXbUdRN3A0YklQcjh3L2xqb212SDZRUkpFNWhYcVVDV08zaHdUUmsKQTdXVElxQ2lYeGhZRml3bzNiSU9VWHhQMWJ0MzRPUFp2Sk1xUkoyd1BYZjE0WFZNeHhBL3IxRU1XWkZIZjEwRgpXS2UxYUZsR1B1Q2czU2pBeCs3cHFIcER3cDNFMmdFeEtQS0NndVoyWFQ2djAzaEFEbHZJMUo2YWVkR2htanVuClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUwydllUVEtJeXpueG5nQVJ4VUYKOG03MVJ3Ukg2c3hvMWxWNEw0ejd5QndjK0hPbDY0NkhEMHhkR05tbndDWnp1QVVjY0JvYm0yRVJ0YUhHTW9uNworUlpvVlRtNmVaUXllVWtaeEgxSkNlUUx6bHFDdHh3bjBzUVhkMDJzVWJxSVFkRERlc3JQc254dDRWSk9vQWtCCkxhZC8rV3FMUURSanhrb3cvQ1o4NlJiL1RqeGFLdjNUNUNSTlhsMmZHN2I5RlArb0hmQnpGZUNOUC90VUtra2UKZVZBeGNmMGlwLzJIYkFsOWlzSnExZUt5M25zUEpMQXphdW9DeXE0clB5ZGo1UEtHYnhRaFdXNWw3cGI0K1hVSwo0RmVyQXFjS0MxaThuMVZGS1B1ako5NkcxYWRSbFJqNmptci9SSHVLd0EwdTdsNTEzc3FMV1hCSW92L1lORnhtCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekxwaUo4WUZObmxIcmdXUkhCcWYKM1FNa3Z4UHIxbzZRUzZtc0NlWmxXcUVLSXRObllLM0NiVW92U0xDS1pzS0tFcWQwMHNpcjVIQ0c3eW1vMkg4UwpoL2Z6aW5zU0NlU0lGUEhnZzQwbFpMUG5rTlhtTjIvQjFnSUluaGE5TW1RM0VyYno2UjM2V0RZZHYxUDNaQTZUCnl2NWpqMktxZHRVQy82UytCTFhXR0xsTUVBSzZkanJ1NllidldMVkhrdWlTYkw4bTFDU21ldUNJVG9uOURMVEcKN0Q3YUZxTytiNEFVbjBYR1Y0ZGtHaDBNRzB5WWw1M1lVRTRoc3dmRVlXQXdBdmJmclRGbDZ2bzF6c3d2Z211NQpPRnZPckVPSm81dElkaTNEdlVKSzRRLzdHMEZCODN6bzBFK0N5eHpmSm55Mm5peFBHVGZJaDBIVXZpOUtnMjl0Cm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWY4OWJUM1phQWRMbEFzaWxvUUgKTHoyL0VuOHd0N0JHUGl6WWtEWk56S0Rhemx5Wm96YzJ2cDFuVE1SWHM4QzZnOVpNTmZMRWhFQTd3MitUaVFGSgprZXRsQ0NNcFR1K3JvZU05Mk0wOFdxMSt2amN3VGJrR3NiMU8xbGNpT091Tk9LRVhTR0xNRTVXZ3NWUFZzM0V1ClhsOXNLQUtiOUdlVWJvWWw4WmIzbHJaWUNSeWJlUTFmK21TUmtHbXVjSmhLSTRZVVFxRXo4VEF4RDcyRkhCMEQKTnAxT0Zicm5kWUEya25kazBCcVNieUhoRnQva2lUdHNrNkFhZVcwdXhQOWNlQjlLbjN3emVFTXlYMFhYQ0RMZwpNbkJhY284Nnh0dGNoVWVNYkZOdnNMSkhNb24vV0d4WmJCWitKMDdkSUhpbjlwZnVISlFOSDVjR0JDRm5ocFdmCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzVSNFdhS2h3UUl0L2w1cDd3d0MKWVdTOXV2UmNFbG83eU9GZnIyT0VZb3VRTTRBdUtWY1dNV2xRTlpzbEhoT1RYdjRUc2JvL0N2ekZqMktwMFBhegpFazRib2lRazNmcWZsZFF3S3d4N1VHM3BjNUFlZ1UrL2hmR0pKdDhEaUtxZU9BMnFxWmE5UHM2NWlKSG1PWU9UCkIwWTdiTXhLTFVaS2VkQWFGdTFtY0U3MVgrMzFnUWw3VzlZblppRWVHWm1MaGR5ZXRqZHRuNk5NdG5jSlNGdGkKZDNxTnZieXcyVHo4NG9LUExyalphaStqSlR6ZU9kSHoreS9vTUNabGJGU2Q2TW83akJwUENGaWVOMFA0c1c3SwpBMjY0M3kxeTNPNExFbjFsWmRqRm83MEJqRmNETWpjS1ZqY05pYnAvU1BaNWMyZ1cxWmRhUmE1dUgwclcxaEFECkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME4yYWpLMk9WTHZHYk1yaURRWlEKYTVnUXZFRlArMUpPdTlvT2w1eFAxT01mZ2hRU0NjVlcxWDRTNXVPVDVpUThoZG1hQ3lDZnd4U2RxY25rU2Y0SQp3OGRraTNMT213UFlrWFlBQTEzWUZpYWQ1ZGJmTzRYSkEwejNBS3JJNGV0dUp6YXNndUZGSWRDKzNsYVJpS29jCkNrN2d6REtnN0k2R3JoTjg1RTZrRVFIeW9GQUFSSlcyTW11eTZ3SzBiTUFzcWI1WUcrT0RwQUV0LzUwTi94RlYKRnFqV01KaEdaY1pTazVmMjhTbzhXSnd5UjBqckpKYU9oczk0b1FMZTJETGRNNHd6Y0crQ01sR0hyR2FYaGIxWgpTTUk0ZDBkZ1lCY1dscDByMGxnVGZmc2Z4YTlpU2xjRytHSkpsaUh4N2h2VDJhK29POWJzUVZWOTJxMUc3NWtCCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzFZbTJrSUVzbFJNdTlvQU01N1EKTUNJR0lGUm56Q0Ywei8xQU1iWHczK3ExK0E0TzRRMkMrMjVITlJtZ2dYa2J2MUlXUHVWVTdwT3NIclRIUWthZApJMk9qNlVZRVg5R1NYUDhENjZFM2xKQUxTTUQvdEp0QTU1NW1EOW4yZlhOS21jQjdCMzcrUWJTaXBZVTZSaTdoCkFiYVpGMnlWVzlrT2RGSFVYRitzRUtJYjJ1bGNNUHdIT2xWbkFyK2RpQXhxWFk4VEdxeXRBZE94R3FoK1ltdTMKNWZVRVlMTmxnck5aNGxSNkRWdk5vUVhxTzhYeGFBTmo3Qnl6TG1udlVTb0JYOEJsdHhSNnRDZGRxM0U5bmxzZwpUQ3VLMWJtaVUxbXFpOVRud0YvdUR0b0VpNWV0dUthcGZqdDR0eHhsUlFXN3lGRmtyRHM1WE9rT28zWWlsd3lsCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0NCRTBVNnNQK1BicU9QeTF6YzIKc3FGUC9KYlZHOUZBZjVuZ1hMMW10YW1XM2lCUkcwODBCUEorWFE2Mi9CSlo4V3BqM0FhUThyVXdlVVBmVGpiKwp6U1pGOEIyWEVTZ1VvQ1JSdzdpSnIyOG9IeG1rTjk1cUsyNm9XYzZURVJRbGoxaDNHYlZkRXB1M0FhMkVpNEVNCm5MWTE5K2xnc2tHa0tWcXdDYmhEQ21lZ0lhM1h4Z3JDTkw4ak5YMDZEU2tWVGxrOW0rTHYwUnpCWVZNTUFjWWUKRnJQL0lYSkt4c1FqdTFWN0VwZW1qYkdnVWN0WWdHQUlxQXY0ZGhaRnViS1poMVNPOE9DR3dBOWZCcjhURzNsUgozRmM2K0VXTGpreVB3STVveVZ5aDk3OEZMK0p1WG40c0hFWG43ZVhZRnZkZ3B2WnI5R0gzbjZ4c2tFMkp6eUVtCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnBWMDZZcmYySFpSY1V3NmRUNFMKVmI4VVJPOS8veTQvM045RUo5TlZEemJ5OTd3SEU1NGQwUVdGQzNwdG4wQkRMWlBEWGUxMFdnTWJoOEF2bEpRUgpldnBDR2x0VDZaUFVRanVzaWI2eXpzUjRsNHM1aG9sTWVXaG1nSkxQbEVuNDVpRDdCRnZPdzBhRzJEYi93dWFjCkN1SGhiRjJEcnAvTERuR0hROWhwSVEyOWQvKy9rU3FOdXFVZjRaaFB0Wi9DNWNxL3dseEZzTFVzR2ZOUFh1K3QKRkwvWVZIU295WW5jK09aRUIydy9QVUxRbFBJckVrYi84bmx2LzZvRzJHSG9BWnRyd1Y4ZU1wSnpDN3BVMHRwaApvYStzOWlIeWIzWkMzaURlWDdrWTlJUldhb2cyVGJSVEdtZEl0SlBmZ25GeTJWWU0yaGJvM3VuMlJmWUhBZVZDCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0FGMWdnZTdlMHlLbEVOTWd2YzkKQVZ2R1VMakxDcG4wOC9sNzVGVDgrdGxnQit2cC9zN0kyenp1a2JmRmRSVjREemRZN2lEcEVBSHRlRXhaeEIyMQpwaHUzUzYzdHZ6bTVzV3Frai9VcXFLclVTRzB2ZmpMVE9JTVV6MndUNitBZ1FrVEVrbWlxWWQxbUVWRHV5cEQzCkR3M1RWWnNEeWxDbFNJSlZhbkF4VklBM2FjVTdDRjdmeVBkUkdzdTBOSVVySXJBVTJUdFNnb2Z5M2EwMWltUGoKT09SZy9EWU91aXhFZWZQYjJrMlo2UDUrWVp4N2dFVnVNVHZXRWY5MUk5SEFkYlJoM2EzQUZJdG1acElnUHBOdQo3MEEzQzNCNnkzNXNnVGM4dlNWWmE0RVB3VzFJclcyMStUQnc4ZWRmUGk5dStvSjUwSnF3SDhVYmxibkdsN0V0CnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2xjMmRVWHoxdEg0eHdoTkIzM1oKY0lKTnZJaFdUbjMrNUp3eEdVazhKeUhEcUlyYWRsNW93SFo3Yno0TUZNSHB2ZHdJeXZtVTUvLzdjQXJvaDFSaQpIcDBtTmRNTTQvbmt0cUhlUGFKYTJIdHFQNk9pQVlkMTFnZDlKS3pFYjFGK0V6aDVDTW5WcGlRVHI1cC9LQU5mCnZXc0tQUng2UHV4SzB5RnRNZC9SVGMzWlBPR0VZZFlnS0RsTGVXVGw1dmR1UWtwVkc4djBqc1h2UmhSeGRHU20KemxReHZYNXBIUDUrbWxVeEF6eWtzS1Fob0lHcHY2K2hEZHNkQ2gxSEtwWUc4eGtuUE1pVlpWVE1lR25IRStSNAo0SG50VkRlenRXZWJLWnNDWUhzL1lqMDBUNDZnQ1l5Zk00TnJySnZJZnNoMzErdHhwQXlHWlgvYXBnejgrNFpQCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGVGOXpTQm93RXBZbmd5WHFWaW8KVzFDaEJ0V09mR3RzcHJVMFlxRWYrNk03Mjk2aVd3Smw1UFhGVmxqV2pUUG1vMzk1bzE1WjI0QVR0UEtWeTU4QgpFOUJtTWluY0Vpc0pxY1RsS1ZWSkpTQWhjQTgxWGNBMzZybXQ1cXl6REJmQWRwWGdUOGFEQ2dvSlZhck80VnFtClZ5V0Z3K3JpbEZZbUV2MC9aajIxTEtBWlJUS2wwL1NrRVpOK1ZzRFZnRVJ1UVVxM3pzNHUwYlVic3pYaVdMdncKL1A1bVU3M3JvMFdtb1QxTHUxTE9NeFZTMXZCbGxVT1Q0aStuV1ozUENQZUVvcFVOTUdQR2NiUTg2aTgycEdoWApMdm5RSDlHbVZKdUpKVHpDSzcxWm1BeXU2K3cyZk9zeURyeDJoWXp2eUViZ0dvaGo0ZmswcHZJK0VpcmJ1ajQ4CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1BCTkdxLzVUTHdFczUvZXJubjAKRmd3OCt6TjRPUE5hb0VnWmFjQlhEcnJjQWphTHFEb3FBUG5CaXg3dG82WXF2TzlOSzA3Uk1XNTJlT2N2Wml6VApNakc3bkhLYjNvOGpTM0xQZVFrODliZkVaYkN3SnFuQzdQMUt4VnlHSW9mZzZtVnpYQkxadGZhVklrNzFLL2JhCmFXUXBjL0JjSE56K0VyRVZiVitZeExpRS9DU25HNFBoditHc1E2MENaU0NMcUh5bnRSSk5OWW0xY1pHb1J1Q00KaXZaSmlYRW9vU05xeDdoZVBoZ3JtQTY3MlMrOU95anU0Umw0M1lLc3k0dmtKYk1WTE5XMEJZVVFOcXFhQlNOeApuN2RBS2J3MWk3Uy9XNDVNdWptWlRqRUhqU1pjdGpRQk1tUkRFTHJ3UUcxUW1jWkM3WWVXNVFmdmplanVaNlZlCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEpId2loU09zMk9LU3NPS3RYVnIKYWt1akVrZTRuTkJiSkl5QzM3MkpmaGd5ZzJrRDRNUklyemtrT0U5NGlFUkU3ZWk5amdPeUJlMWg3c1J1K0ZZcwoxeEIrVEx4UnRzV3lvcVFDKzU1cHlxRWd2SU1VMTdkUXVnVGNZcVhRM3dCMk54Ync1KzZBZ05CeExMOHJTdVBiCnNwUkhsYXdBaWVaVG1sWmV1NEw5NVk4RjkxMHQ0QnRLVEE4TDAwOVBsZU4yTFBhMmNWazAwcHR4M01TWGlXUnoKd2dRNWU1anNIZmROZm1Fc01uU1psSy9SSUdOaW85eDZmTURaUEZDWFdnSmw2MUVNWTNLY2FyeVgvemE3OTUzNApOUEpFV3J1U1FWUXNHUWZrcWU2TS9tQXVuRmF2YXJpMHVKL1VBNzgxNnpkZUN3OTU1bFliSWNzRllpR0dHM1d6CkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGd3a29nNmQ4OVJZb0tKc1JTemUKWnB5dHRTbzdGd3V6a1o4Z2ZYTW50bElhYnpVbFpMKzdsODlZb0tDdkp5Rmdxd1V5eDNqcHV3YVFGK3ZFdTN1ZAppRDUrSmF5Wm0rSXBsMmdiOXpoZWRlVCs1UmVUWVdGTkJYaGtrNjdEejVZRlljcW1KRnlGaHlXUjBLWkMxcVdoCi8xd0ZrWXhoZXRPa3dzVmZBSFRpa2g2dUtsdWFrT0ZPZm1WTGhnN0dzWE9tM0o0Um9GT1lyVndneXEvSDhRUlkKcWcxYkFoT1pXbFRpbWY3NmczdWFJYzROVzgxL0VOL3JnQzJLZGtxVVJZSDF2UnJuMm5Qc1doVnA3NjMvV3Q4NQpGVnpTb1oxbFpaenh2QVp1SkhIbmUrbzFqWUZJWVM5ajkwN3VXUmZ3dnVBRnlKUlNSQzJuSXlVK2NDL2JFNk5jClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU84cXAvSzZEZm92OC9ITHBDSVEKMTY2WlkwaGUzL05tZ1lCd3B3TVFPelZXTmpIWXBrL1NPQ2ZTNmlOVlAxajlSL3FmVXNERTJLNk9xTlV1ZGZ4RwpQZmk5T3RUUXc3VUFZRElFd0luNWRpTGxkeDRIZXRLMzdkczR3eVRLYTd3VkE2YS9rMHd6T1Fmd0R2Zml5c25OClMydTh1MmlnWWlzaGp6bTM3SkJUQlN6Z0dpWlR1bXkvSHhhZ2FOVytZUm9Ka0MyYTBvS2lrbnZqZUVGVXl5SXUKMURTa2EvRWVqeGhBOG5OMTE5L3pqMktTeVpTMzRuZ0o0WTFrczZTeWZXOEw0dDRwUFVQcEZNdTVKMEluTnBiaQpFMWxlbmhuVlFjN2FjZytRdTRpM3AxVlNPTExGQmR5K2hxQVpSSVRaNjJHa1JqRG9xNkh3bWNSeS9hcURrTGRiCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWNURlo4NWNuclpCOFZYQU45RlMKK0I3L3VQUmRZbFQ0d3Zzc0kvb24yU05kak0vNEtNcGNCaWU2WjNZYzBMVmowcmVFZ3dFS1RJWFNFUXNDYWJnZApON1BnNHBFZS9CYlc0a2M2VGZJUk9hY0trN2t2UERNaEJoQU45azQzZXhDMzl4SWNkZkZNVFQ0UWhzSVlWRFkrCnNaWkJ0SzRnUGJ3STFSNDZJamd3cFRCbk4wSUVQOVJzeHM5WmhMdnUzWlQxUzgxT2RvNThWSVBXaWhucjlrRkkKRTRCVEZYRmYvTlBDdTZqaSthQTFINDVXY0hheUVZYmtGRkR0NkxRbFZSZGgvT0dNNG9WNy9oOUdhcnF6aFhxegpva2h3M2tmZVpGT3NPS1RyM0JhcGh1azlTNjM4ZXhoZlk0R1ZEWHYyUTdaTGJxVmdTa2RSbUdWbzdCSzlYMnB3CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDB2MDlkMzB6SkhlTEQwNXgwUGwKeUVmV1lCeVBuc1JzblZnZnQwam5kQUROZXJLM1lMT2JOY25EaXFndjg1ZWJaSHQ4ZzR0cUFQdGRDNUhnSHliRQpGOGJiVnBIRHhwbGFNU0ovQTlRWk9EQ3ZrZ0JOald4Q2pJR1VFaFNWeGlKd2RZMHZ6RC9YeUNwOHNwaWh0Z2toClBGWDRDeThCN3BySytkTlR1TGNPdXNDMGRPUDQva1c3eDg5emhDaVJSeHRMTkFsOEl6SXh4UVBWdGticHYvOWgKYlBLZHY3YUN0S0wyc0pEZjNCUU1iT3JGZUxRN2xRWVBRaUNIMmtoOW8xajJidnZVVjYxRW00eXJXWmVQUHM5MQorMlBIMlBvc240TC9QSG9jR2FSVlNiOEFtVG9NbnpMM29vZGdkc2laM1ZJbjhjeHNiN3J3SDk0R2ZCejFCVFp2Cnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWpCTHpROEs2SU9zZWRJbnVsWmQKZU1mN25TVUlVeHY2TjYwcXlXLy9tOFkrQS9DM080eWMyeFhUdU1yRWdyVUdhZDJtSnlpYjUvOWt5YzNNWnpESApYR2NmbEFtYXJoRklVVHZLOXZmYXhzZjRRUlVteCtZdlVqd0FmQXVyRHJQYk9WdmZEWitDNTgyZEN3aTlKQ0ZLClV3V1Z6cTkzZDNTVG5hMmxIS0czU2VoSEUydlA2cWFYaTllZ2RiUTJaKzZBNlJBbG9BODJnTDV5NDBJdVBmOU0KT1lSd2VFSThtUkxGUWExTFNUYVFMVkJWbFpjVnVoSW5ZSWhodE9lY1U3bG1laGhvVjlUTkxxU2sxTmhhWlJSRApNVWczRDU2dE9keGY1Y1F4QXUybFhyY3J2cGR0Y1FjdE0zM0ZtdzNNMmdqUU9ZMFJpM2hkaWJvQ1BMcDZzaUxJCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUlralgzT2t4aUVHWFZLdi9wU2MKd3FuWXNlUFltWlpuV2JBamh1VVcvdDNBT1FJeXdhY0FLT0dWaGZOekEzbnJmQVRuU1E3dVZkbTM1UzRuZWEySwp6ekJ0TnRRenpkLzBkR1daNFlDTGtTcFJkTit4QXBjVnhlTDlUL2IrcXBDMi9EV1dFcEZBS2crd2xTNDkrMjhTCmZWcmtJSFhQQXAweWcxdzRvRldMZ0NsbE5jUFZqMmxhd3U2ZGQ5SW5POWN2djFNNnZpQ3NsVjdUN0l5dFVwN1YKL2ZQcmNrU3pxQTIvSTZJb2lralAvcHRDbWRDNmY3cXdLREFaK25vRTI0bmtZYVhBVTdvMTlCeisxaU10eEllSwozTHgzc0RLZXBLMTVyYXlENzJFUitrb2MvVGprSE00b2F2V3B3MGVua0VvY001VFJTMVJuYWZxc2ZTekpKNXEzCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBblY2UVp3QUthNDlNM3dOSlFuK3kKb0ozdEh3Mit6TDhRdGRBYzRubnNqM2VsQ3FQQzhoTmRjVnkwbGJxSmxGcW1wTXUwTE5OZkdMSHVlRDFNTmpqNQpnM2JudHhHbjU3b3A2UW9tMVJSZWJWMFd0Y3o4VVhJVGpKLys4MlgvYndWdDVTTVR1MTFXZ3g0YVpDK0dSaEJjCnBDbytqL2hFcXhveGFaWXlsTTg2OEhaRktLaDVMYWw2OGJJLzF6cnM3R2o2R2FWSFdTMDZiOGZ2MzNMSGt1SWsKS3h5bkI0SkwvQmdRQkYzMzFkR2lBY2FzeEVUeHlUYnlpNS9hd2VoaC84TlR5ZTYxZ3ZEaTZkWGFMWnltMnRtbwpJMlhoVSt6ZG80cjEycEpydWtjeEhDYldoMlgrbVZ0VGk4RHo0NjQwbTltbTF5NE00M2dZeUtHcXFxREVERlZMCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemN0SGtLMDZDemlXeTJ2OFl3SVAKTFE2anU4UUlrQmsyZUlDSTF3ek5YR05jcnBTNDZPUkRpaDRvczRrdDZqT0lmdCtGYUVlNmxVWlJkdGRuaklhSAp6MUJsUTB1cHJtZ3pxNHBvWFM1MUNJUVI2Sm4zMXpycFk1bWdrM2dCcWZ2YzFUbThBam1DV2p2eDFvZzFGSThtCnVUUHQ2bzFVSVVpcWtPMUJVMjlacWdwczA2cC9nRkVGTkV6Ulo2Q1I0ak5vZ3Nja3dwdDI2T01BeUVMOU1sa1EKMGMya25naHNUeVRWMHMySVoyWW1yVFhFSDJSZ2VESmhkd3dGOVRpODA0dmJLK3hvcVRwTVJBVTQyWENSUFkvMQpCWlZ2c1A1VzRVR1JlZXF5MHRVRnczQVRQdVVMd2FVYmd5ZmtLeGF1UVhFcU9ObDdGTDJ3ZnY3QkFTMmMydWVwCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBa3lVNlJqZHA1OWJLa203TTEyYUsKemV6NG9uR0tIaklreTNWSzdhbUtwMXIyekpxTjV6ckp6S2VObGtUM1czcWxUSGxtN1BKSnVrZHA0QmtRRi9ragpwajhrWjQ0Lzc2OUtpTnV6UFlZbWVMWFNSNHZ6MlI3Z29sRmdXalVtU3NrMUNBQmQxTGQzeGNZY0YrdW1NRE1CCmlWNlVpWDZiMzVqd3hVdmVyZzJsSWFVMTRZd0gzVDRMdGp2ajNvSjFwYlg1ODduZEpmUG1sWng5SDVuSXNtZFkKek4vd05rZ1BQOHNla2JocXljVU5SekE1ME4zS0ttYWVvYTNCUWxjRGI1T21oS0NueG5laXR0NVM0Vk82OXdsYwpZc3dPSk5UZkxGb1dORFFtUk1vaytmUkRCaC9USXFVQUh1VTdyQy9ra1pmaFhsWG1WSXRYU1RadjZaVysrQmJiCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDVZelhmbE5xTEwwc29GMEEvMm0KcXZ4ZGJ2clRwNWZNc1JZWlZjU0N3MGE2WGUvUWt6QTFPQ0VUOUxvaVdvQS9XYlVVVTA0dC9WemJuRzhkY2sxZQovZ3RiYzMwNlNrMnY4NVZiMEgwemZTTHVIbmZEVFMvME5jcTdVSlh6bCtGMmtOT21MbU9GOXNjdXhLUlZlR0JQCjVkRHlQU25kbE13OGMrN05kNXZIbmFORE1iVk5JQmdDWTFJeStBaG8zVWttVXBhdDFlQjNMWmNsTzMySE15TnUKK0lQVGtNOUxhUGFLZmU5RkRUZUVzTjRjMzVSYnF6WnJub3IrREZDdmRZc2hYbUxCMHpwZkdsNUFYRVZvUi9XNwpaekRhZWJqTkZOWHJLdmRPdVRuWjcrS3NTUFdaU2VLVlBiK2hZV0hPUC9LbmtmTS9OanJWblZkOXhBV2RYc0xFCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkE4ZFNaVVVGaEF1a0VSWGJaaUkKZDRpVmRNZkh5ZGRtSmZOSVNIMi9wc3NZUlJ6cnZFbTA4c3lHdjFScUlIZi9CdllQRGhSdDl5VmR2ZkRqdzRwSgovcmx5L0FaZi9VeXYyMzQrRy8xZmhKdjN1L3l6M2VINit2eTBiZ2UxTVI1SDVGRnFlYm1URCs2ZHVzc1V1cXNJCkdrYytCRkZVanR1MU14eG9aRStOZU1FSmpBaWpkbVpNRnJCcENEWWFhUGI0V2lsQkcwbC9BYTA4UTBIVE5TM3YKa2VQNitySTdqRm0zRDVJbU54VlBLLzFqd29uOHl5UlprNW50cEtvS0h2bDNFY244aXIrUk8vcnNEQVJ6V1dBVApsUnhKSThwT2crUHFIMjM5V21OT2w2cU5yMmFCMGd5aVJGQWhTbmFIa3JWWmRyeFowd1JpZTdrODBvYjMyQWtRCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXFXNjMybTVnbzlsMk9GR1lZRHkKRnF4REhTcGxOTGlSNHBwamVmMmhqK1lUNEV4RHdSMWExRThrTFFxV0txRkFPcWl3ZHpJdmZqWHhvdjdydHc5Swowbm9NcUUzcnBwcXVaY3BWNWdDT2Z6ZUUzOVRTM0NuT1ZzU1lwZG14Q2I5WmJLY2dDVkV4WE5KZ1dMY01xL0dwCmo3QndwZEhld0V2a1pKWjdjQktJdW16Y0VxUHB1bWMwbFFCQThsZGY2Y08xZHVFVVB1bGM4Z1dFenp5cG1QaVQKdGZzU3lCQjFnbC9xZlY0cDRoelVURlhmdUJES0paeTNZRmVpUlltOXJkRUk1VS9jbG94Ui82c3AyR0ZUZm4xbApIMjhxUU5kRDFSSVc3YXhqQStzU3c3Wm9Sa2d1ZmpwVkQ2ZDFCdnRjSGlMaHNnWmdHTzczMWdVZnFoUnFtWURPCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkl2RmkzWlVxcGpDSGRsZW9Od0cKSlA1Q3VyeHFGQWc5OVRTQzdiOWJFYS9SdFNMeXo0UTlVVFViSU1IK1ZlWlNxUW9ibTNsOGFJdWdLVExnUk40YQpoWjRkSXNnbnA1cXJPQXBEYStrcmZSeldxa2tVMmdDejNNYnZXY2V0c21FcTIyZ1F6Q3hSNUZLV2RLWXZtOVkxCk4vcXljYnVtSjgwOEp1ZFZBcU5pczIzVnNRVURod3hwNVhJZHhMRVRnRnJRWHNvQmJRblhTM0FZYSt4Rlh1TjMKUmFGTmhvcDFPQTZoUjN1NE9NK2lKR2l1cXJQeG5pdmdZMjNnSEF4bjROMThGeDZvUmdJWlpTdUM4SE5TNDVpVgo5S3A5b3phdzNDU1dxRkZBbDNWVXhMeWM2QVB6aUFOQkd1R1VJN1FNczc3WkNtd1VKNHZ2OXc3cm55eDRxZjZkCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME9ycFBFbFlyZHQxb3FsOWlNNE4KT1RjQVRZVUVoRzJpbWxmZnhkYkMxVlozZFVqenRaQmpYeGJ0TlcyREZkM0k1TllEOUJJSjlJYlVqRkJQUmZTdApiNmZ5QTFadDRHZW84Ulg0cTZhK2JLS1dSUlpZbGNBRWt4ekp5QmhMYjllRW83WVpWV0ZyQWRsTWdZdzhPVWtkCjdQdDQ3Z1RWbXpnbmtrTFo0enBCOXlGRjFYcjFtdmNpc2pYREFjTFplYThJc2JDMTZoajZvMkQ4RGVoRnpWR1YKcG05eUVqUjRDcUVLeE1qOHdJaVQ4OG8rZitjZGYwK2ZDTi9iS29Zb2VjUzh6UEVMc1FPVzl6Y2NJWWxJTm9zYgpvbUxvdEQ5aFU1empRNkNZSVJyNk93YytrR0JDTmJIbnlOYVBDRzZBUk9KTGJueDhFWE04MUNRZHdUSmRsNkFGCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG14RTRvN05lWDRLOWhyb1NTU1cKSlhBY1U5bmNWSlFnSFh6VHNDT21EU2VnRFJUSFJGTkRCd3h4bmRCeHU4TUdBSWNpcm9sM3cxUm1jNjFNc3BtUQpIT01xQkVHK2dJelkzZm5KR080eTQzbVphYWVWOXFOZ0JvamdrTEdScXVmV3BBUFJreVdkRndmYzY0TTVTWVloCm1GNklqVjJjcEFpK1g0TmFoenl0RFE1ZSttK2NOdDRnTTIzdkdPU2I1VTNCbnNTUzFVRU1KR3dibHhQdWZxMmsKbnp2ZERvZlFqTFRJR3d2VjRCZHFwM0IzL0xhUkxtdHhlL2FTOTNZaXgyOTVYdzAyMkRzT2hiUjZpVHVuZE1jMApUNitKZ2ZMMU9jdTZDMFpGOTlLWjNEOWRaYTJYZXNTQmwrdk1ESU13V0R4eEV1WXcxaUo2eUlWb3ZWVkMxUWg1Cit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDJZU0ZMRkJxS09UVUpRWTkzK0YKb3VtR2hLbndkU3pZMElqVllSYTQ5dW9ENktTL3RtblRHeFVJMzlmSm1lazh5ajZDNGdPVmt0N2MxMFhvMmNtRwpNV1NYOE4rUlI3Sno2UXBGamdNTHhFaHoxNHBzK1VrUS9JOWtCS3JteHY3U25sbVprZlFIb2w5djliNVloRmdWCnhMd0ptQ1paUXgyQUozTlBaYjVncGZBRmFEeWFBNnBzb3VnOG5ORkxQRzZiTFlKcW5HZU55cG5VakQ1NkU4L2EKeVVSVlN4UzlJMUliZUFwemg0enpVczNaMnVsODlwY0dyTGF6b0lWRVdqQkpja2FFVDgyVk5scVpWYXY3QzNQVwpDNGhzaTA4aGEzMjJRZXpJcUlmYXk3ZXZ4V2M4Nk5taklUaWpiZXZ5RGdyaXlPUGdDR0FEQXZxTzJxZGd6Ukp0ClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHY0WUVkZWRWaGVIbE5yekc0WnoKVnJjVHgxN2pjTm9ValpFTHdJZ2F5VG5velhXTGlLN05SU1FhanBnejJsREkxbUxCdlNMK1Q0UTM0ZWhOL2ZBMwpCV3VQa1V0UkhSWTNrVkRwZlRvcFkrMUlkNmRHb2daaWNWcld3SGsrWVJrc3VMTFBidWZaQ201Y3ltUEE3TTlvCldKeEUvWEVGZnllRGczQ2VXemZpS2tYUjFLdlczTWRxQ1JML2tKSE16dXlYVHpSc3M1aSszb1pHMFpBN20rWnMKYk5tY2p2NGtKUnhFMjQ1K0FpYlVYcXE1V3pKZTNteVVQRElKa00wYi9USlFobzJoeGFjeWV3dHg3YUhPL2xIawp2aWtxR3JibHRaaDB6NC9NamNLTzBuY0srYmlITkJzRUZHYXQ4TmluekpramRNU3YxYjk5bnEyWTZXMGdrUGJPCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFZxdHpwS1JVUHMwNy9XRmsvWHAKR0xDbTFHQVRBYzJtaTBLVHFWbEMrdVRTSmpXTk5rR3E0aEdlMklnSHVOREtldDhQdkN2VWc4Skl5alY2RlFHWAozNEsvTEk0QmZxa08yRDRBSkVMMWlFVE5SdUk4THh3dDkvTDMva0x2RXlWdmx1blZBRkFWbm5hVFArckZNN1dtCnZKQ3ZoZ1BORjdoVlozRWFLaFowenFSRGlBUjJkaTJ4R0FhZm4zSk04ZFJ5WU9nTVBWU2ZCL1pZZi9uNyt1RDMKam96OFZIYm9KUjN3OXFPSWg3ejg5U1Q3ck1RakNDVWd2eEZtSTNnbW9XVnhzTEJTM1VoRHNMTEF2WENXQTZKZQppYWFkSXFuMTlyWDk4QkgxWEtvUGl2ellhQnpjTStYZUhoSGpPaUNqV0diOTlOSEFNUjg2ZlZCazIweEdkVHdkCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2d6YXg4SVFxOWFObWc3RkdjVGoKTkV5ZXFNbVBMUHJ6TEJ1MmJSNlhVdFg2UUNEK0wrRjVjRFFaK2NVczdjV2JURWpvWmlleHROZmU3eVA3K2w3WApyZkVsT2prbGRFRGJJRlNCZDg5NFByN0R6ZnRkQjNRdGo1MGlkWEtEYkFwN2RXNlhqeTJFcW1vYlZXMWtob0hhCmM4Yyt0NENhaDFHVHc4QkNKWEZOTSttcU1KR0Z4R0tRaEFtR21HN3dZVlNGN0xqcmo0b0FMRXdHM0tjZkplb3cKcllHUGY3anl6V0RBZk5mZlNLdUxtK2NsZm5KZ2IrWGhIY1llc213eVFuY0FBdW10SFpzWlBJSkpWZWY4QXduUwpHWGpIQ2VJUGxCTzRSMkViS1d6YmhHaHhkZ3JnKytveUhFMSs4bDZGYVc1dzJRV3RzcFlrVW14YWhQMkdBdlhICnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVErNExrYVAyOGFYbXVuZGQ4c2EKWmhUaUtmQ05RbTZDazd2cHNlWnJKdExNc050a00vdVk3c3BTT2Zqc2NwR3RMK2YrU1JBYUxmbCtUcklVSjJ4Wgo1dVdSc2tWNXdDWGlESUdoOE9INmZDRWtqekhrSEVEWXoyb3g1WTIxU2VHZ0NxVEFHTWEvNWwrWWJLQm9nWXlxClR1cVRLVUkxT2pBWnFXWDBNd0F3R1dONmR3Q0dqYlF2ejN6dFBkYUd0S1BzcU1NZm01a2FDUEY1OW9GNHZTdVQKQ0dZVnRWWFBUYWVoNzE5bkxyL09uNGg1bkx4Y1JCZ05obTY1MDgwMnNqUXBSaGdiTVV5MG5TOEhJaTF6aTNrYQpML0VKT2VJdERjK1ZtTjlwUENwRW14S3pJNkJacjl5N1JuVkIwL1FiNXc3NWJWdTdoUjB5SnpZT0xzSHB0d09uCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVdnUFB2eWFXTnB0ZlNRbUNqaTkKUWhKMERGRkFhL3hpY1JtS3YxUzFnSEVRK3JyeWJrYUVvVm9PQnVJaThFdHprQnVaWVBnUFN5QlZINld3c2lhKwpZc1hidlZmZTBpeHZYZWxHd050d0ZEZnArWDRQc09mTEtZZnpxWW1EdU1wUUtUbHhXQzgzT05zNi9tbjlSRHFaCi9MUXRJVlhLR2ZDb0oxUWVaSmxkemdTUG0wb0lwOTZWci81RHVrWVRaNnZadFE5Q3htTUxNdW45V3VsbkZVMDYKdXNWd0RUdTBNa0RyT2EzVldCRnFOdXJva3o4WFFua1g0eUJLaUE4ZWV3cElHT3NGbDIydlZ4cXhiMzhHZC83MAoydTQzZFVoZ1prVjZuMy80ellwYVBTL3YrZzBSSm5JeVhLNzRHaDlncEFnSWhWR3F4SUFiV1hsYzBpdlBGNGs5Ckx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUN4a0VnZUgxQkorUzBMWHI0UEsKNVp5d0NxWUt4TjJ0cDlOOWJEdVdDeHExNWpMeEh2bi9zdllLNWg3RU1EY3BodWZyVWNkMFNTRVpjNVFSd1BoZQpZelZxRFJhMlRaMHJOaU96dlVUWVhqMnp3RVZ4T1hQWWpOdk55VmtpanNVNk44Sk9vZmdLVXBJVFdwcXM3NnFZCmNJRWlRaW1IM0xhS0RzcDk3cWlSSTlNRElUZ0tWam83bVc5VXpzRE0vaHluM1ZoQzJpS1pUcU5WUkR2SUt2OGgKdEk2aEZ3K1RUbXByNEMvQ3p2R2ZudWwyY1RjdVg2WTBTYm9TYmczNktQTGM5MmlPaDI0NnB0aXBteXlyM1Nlbwp2aGFSYTZ2RERQeCtvSTdTNnJYeWs1bEFOL1BzZ0FPbVRyWUs5S2g4SkpobkpWYy9tbS93SlpiVjBWNDRhdzUvCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUtnQVlNREswakNLVjlBbHczN2gKNURUUnpIRlVCdHZ0NXQ4S2pqLy9JcTVER2FFZEl3dm1HbGJ5RDI3VU4zNFB6bHNkM2ROVU9MbzViNG95RlZhNQpCRThmeUFNcEk2UTFpQjA3d2NOU0wxeXBpdXNxWFpHekFZVG1PL00yNDA5dXlrUnpUaDc0T2Q1VDAwTUZ2YWFDCi9KTy9FV3dyVVk4SWUrU29TaHpKM0ZCVlgxY2pDSWRpd0VkbU1uZDdMcGl1bHVSRzdXQnc2eG5ydG5lYWdTb0oKRkJRbHdhVjBiRTFrQm5iMm95TmZ6Z25HRERnV3k2aENVWFFTbC9kZFNqc3NHQWl1RThQTVBDWUNnWUk1d3ZoWApDQzNoblo2YTFucTc5QWlNbWROZEYvdXBQRi9CdEhBUi9FSjY5NzBRa3VBVG9hNC9sWmtpcDFwajFyOEZrZ2tLCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFJjcmVSdWlueXNudjJOaUNjT28KNDltRFBNMGJIYmV4Y0tTUzNrOEh4NXQvS0pNcEN5Q3pjYzZxSzBIQ3pISW9tdWJ3T1NpcXhyVmJzRkh5T2w2YQpoVFI1YVJNaUZRcGU3Zlcwc2xZckx6TXdlSkNPNk5aZUJuTE93eVNyWENuYTByZXBJc1BCMSt1bWExam91TXl1CmpZSGhRc1dCMG95R3kvOUlBalg2L2lMWCtOc1E1WDIva0FwTVROY2dyUkZxT1NkblBMS01DalIyK2FaT2twY0UKdk81VUVPRzNlUWIxWEtubS9neEU0TTRSTllicDBWdFcwWVo0L0Z4WjFkclEwTytuaU5Sd0xWdThYQVphOWVZUgpzdzY2R3JmUW9wQnRvajBJNThpOWY5NWkxZnVIODdZYUt6eHl5RlpDUzZrZ1V0a2pBMVRaS3ZSU0Fod29zMkNRClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnlad3p0N2ZGLzhrN3dBQ1hUWTYKeGdwa3VMajhjakJqa0Vma0xWVWVZSlZYRzRPeXNjQkVRK1pRSjZDQVRmUVdHMnJyQmorTnpRZFhQMTRXZ1JDeQpDdUlaWXRhUDFVcEF6azFONHdoZlYwZ3JOT3RnREVxRXJCSS9rY2dUZ1F0NEExTm4xbEdUZHduQUF3ZlQwTGUxCjBsTG05bEZ6ZTBnZWl1dW1uTXd3T3dFaUNyWVp1NnY0Slo3bDI0d0pReHN1ZU1ZSVN0SnpaQlV0YU9RTXdFaGQKRG5NQUlxNGtoTnJQTUczbFpaWTVRK3JPMGxVU1Iremt6TUhDVEw2UHFBbk5sYVJZUEcycG9mMlVqaUQ2YVZXMgpPeTZCQTMzSlBXZlJTdFlvejdEaDVpNUtjR1NQNTY5S1RvYTNlTzRleXZhRUZvNWt0UTd1SGVMRnkyR2dFSVpHCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzc5TVd2aVRUSi8yMktzVjc4b2kKb2hGYkdpU1ZzWGVuZHBjbFEwK0ZCZ3AxR3paRGF1MmVtSUJnTjAxejRPaHRzTWRGL2dZdGRnbzFkTERsbXExeAo3WTJ0aHlzTHk4VENLOVhQazhNOUcxSmtLMjVjTUkyYmJyeGRBRDNWWlZ0WkxSOWJpWFIvcE0wRFhsbXNEb1ljCkQ5bWowN3BGM01KZ3hQdmg3NTFvYkJUTW9seGNiN0p1SzFaNzZpeG1ZaVFHU3diRUE5cDVXQStLL1k3V0ZvMGQKaVZ0Y3pxTmIyaDJzV2RuN3M5VWZSTkVySnlkVFVIU0FEVktpeHgvQTN6b0U4cGFRS1JmSmJIbVJzTHBSYWczMQozVFB6aXgzVFM1SFFNUHN0UXQxZ2FBbWN3cS9RUTc1V2xZb1NXcEpQMGlyc05RQk5HbWRvZS9rclBvbEFzOHNyCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWhLRkxYTXhsMTdNYndxc2tpc1EKQjFWbjlhVzdmdUt3UHVadGtvWGV5QWFuQ042VEc5a3d4TkJIcW43ajJ3eUtLTlJOWXhBUmJGUHB3UWVUVnFjQQpLdUhiSTNqaWFOWk1HTFovRFFzK2dtWE1iTm9Bd0FiNkpVaUJBRzhlekdDVFpGcDRSOTRKemVMQUMwbkhHTnlJCk5haE4raE5tYTNUV01NR09OOVlqa3ZKM3MwaU5FbFhpNXptMGdGYzdrekdtalo4VUlTUWRRZDZNQ1NaT1cvYnoKYTJJenNxRlpyaDNWTE1JckZNNlRZMnBJZTMwNTc2clJrbmtvOXpsd3BvTW5HRjVYWDB6d2tTRVdQL1QvalNMaQpKZStET0RVRXhDR3JXTmlPTzFEd3hTQmtLayswZHpQcVVNclpJUEo3MG85THBSYnIxRXVUbGVxTGE3ZmJ6T0hDClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmlhT0NyNE5jc0k3UDlaVE9yWkEKYWF0SzlSS0RXNjVnbW9hYU51ZXo2NE0rVDdWSnJ3TDZkWWJKd2V1bm1waUdPOVBtQVhYV0FBakQ2UE5uZjNudgp4WExob1lsYktwTTJOZDVoKzlNenFLWERROS84S1BicEpHOGh3WFJRdDIvT1BxMzRzTWt2aEFvSkhzZG5hTmZyCkRBbDhFeEVDcmkybkYza3htb0U1MTIwajdGWGVYSkxxTDZJZnlacnM1cHZuTlJQV0ZpbVMyRm8rY09TeGRPU1AKTlFUMDNORmp1ZXJDaTRJQ1M5WVYyKzRxSUlJR1paR2JOY0RTMVBXWTVTZGhwZXRVcEhtOW1hMklIK2FvMFlCVwpydnNVSXBBc2JIcSszVUkvKzJ3V1h5YnR6Y0loNEZXdDVraWtub1VUSkkvZVljSENycWFiam5XeWZoYmZHdUd3Cmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkhnZVdRSVY0MHE4M3FMcEFmQjkKQU5qVXF6MXA4ZHl4N3FvTmNKQWk1bEZSOUNrcXI2ZFFyeVJpbzVCeVNGSjJNQzF0S1k0dGdxc09nWW9rOTAvOAp3cllkemQ1KzYwbFBwYmdrVnlablpLMks2RnFndC9MMU9ZMnQ4WGdPdEVvdTFKOFNQQWcvR3hLN3JkZDlWNWpCCmloVUlzV3U5YlhtVmVLYmJnZUdpNURtSDQxR3puQjBERFl2LzNBQ3pLZVk4VGl2d2VXd0o1enZxRi9MUU9tWmwKbjBNT0JQL29XSDFXNlBwZWlYL1FEdUNuR1lPZUJ4RzRsVUlaNUNRZXZubHRVQlR1cGN5T1RNWUdEbWFVeTArZwpZQlVaNlJ1ZVhOeDVMUlJscUVqeHl1dDZ3WmQrTXM2RmlzckwyQ0l2ZEhEd3JGNUIyejRPdTRZUm9iVnNZMFJ3ClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0pjRjc2KzNNU0FHWmpmQ3c5ekUKMEdZTkhjTERZdC9VZ1NIa084ZWdGYzRMZVFWV3MzT1dyajJiL3BTMkVuOXF4cldraldUNXJnclZhOEhjM1BBTwp1aTZBWjhKVjNsY3U2VU00UGViSVdST0pETnA1K0dBZ1ErUG8xcmlkR09IZEZpTndEbEx5R25NK2NVOXBLc0hEClc5TzhFbmE3ZXZrbjRPUU9YbVhDT3BBa2xia3JhWisxMVFpdzhWRnFudXRudUtZdkpTNkl4SEhzT1ZtSkFINW4KUExPVkxhWlhZcnA3RW84WFp0YkR3MDRHaFRTNUZFN084NVk5MkhyN1JFMXkyVEJkMDRJZzZkbDQxei9rTXlVTApZSk5JOHBQZWtRNk9PZU1IQyt2MFNaS2F3Wll5NGlka0tRS1FVMXhpTDN2SUkrNlN6VmNTMWlwd3pyWHRHQjdmCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEZ6SGpqOVFicE9VYmVZTXhCNnkKbU0xV3h5S2VkcnhiY2RQSmNUN1BWSk9xV3U5NkViODdNZjdEc3c1THBGN2M4Slc3WWFmYTM4K1JndlZmWjZBdwpXRzUrMkg0TVhQcTA1R244VFNyQVZMQ2pRYlVteGw4NUFQeFVQVjBOZXhLWDRJQjBGVkFmVUpIelpsTElIYXQ0ClR0cTNrczQvL2ZKZmIvb2k1V0YrdDNoZWZCZS9CWThCc0F4MEhIRDVvTjVIM3JIZUEvbVFuTWdScXdERkxsVloKZUdFVnJ2MndYVzRjVElhVlRpQWc5VnFocVF1d3JLZzc5WnA1bVhLb0JveWxTMmFhNFk0S2dPem8vMDQ2Yi90VAo2RXdXRC9ITkJ3ejNhSWlXQnp2ckJlRmMwRTNNM1AzZGFUS01HemRiMDdFNlZ5MkdsZjY1ME1CZzd2Z1VNNFhICjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkRJbm8xR1k4M25qVzZQcTBwTnoKTjJlc0NjZEN0Um5HejhBaUFMaUdNN2RyQi90dXcwOURuNVlDRWp4bzVkSnM4RVhjRW5jRDJrUWdSNk9xRndRUgpjbG1FN2JML041RE1uUjlHbWU0RXlrNTFhRHZsQzV2c3dEMHpTb3VPZ0NoTllCMHhlREIxNTJjQ1RHQWYwWm1sCjYvMWlHTWFtUjZ4ekg3RTdPMStFVXVJN3ZWbTd3ZzVMaElpNDNiMW5WSWNiZkw0NUtpSzFHUGxHMldGNnFmcFkKYlJRTjlaR1FkRFJ1ZDJMcEY2ZzVZNGFDY2ZhOFozVWhrUGY1aGFCMEh6TUNIVmp3NUZuOEdDN1k5U2s1ZzJNQQp0dnJOM3gra05yWTdXYkU2TklEQW5hTmJuY0NXZ2ZVd2FJMVlEU01ZU1hhOGVFSkdVN09yenN4N3FrYlNMRlZpCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3Q0OWpEMzkrcDN5OGhpUkdrL1EKTCtEWkRLZjhySFBjZWU0MW1iR0h6QU5QdnF5V1AwdDc2ZlR1QkR4UmwwQy8xSjlXYmxQVFBENk92TzEwUzJSTwpsampEdUptZ2VseHFNS012VUVILzFERld2Y1VYazNDKytQNVg3ZXJ5cFhrazJ1azlxMVJ6U3JLYVc1OW00WWdrCmxkSkswMlNkTDh4d29JTmxScnNSelRpMGZ2WHZzSnlmNjlBalp3cEJqeUpJNEIrZlhJRHgrMWtUWTRLamM4aVMKdTk2SzVNbURYK05FeUVKUGVLTVFJdDI4ZTlUWFovNy9TbHdDRU12SWZPR0N1MnlkSFJUSWFqN2lsWEc5MHFQUgpQRExvbFhIK1FlWGs4WFNpWmVtTlF6NEtWK3pJV0ZwVXlxQ3VYZlVVb1dLaUcrUjFrMU1TbFU1bW0wb1M4b3o5CnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTFrR1M5U295MDdSVEszNXZFZTgKcXVUbml6ZkQrbTVVZlNNMUxRUVhOVmdWeUNvaFpoZEEvcE54RlFndU8vQlZDSEF5YWgyeUFndlk3aEZXcEVjZgpKTERrVEJ3RkpUZ2RhdXVYaXlSK05nb2VjV2xGd2wzWjRSM0FqYlQrVHpCQm9sL3N1R2MvL1V1VmtpR0dLbkgzCkFnRmpqdjZnYTNSRE5WeUR0MWJWWHlDU2xJODkvTUdMRFRHZjl0WmJLemQydVhHZ1czTXhtTzEwelg5ZXkvOUsKVHFVRXVHSE5vMjlEaDM1OXQybjM3SDBuOE9FaXhrTWZNRldOSkZqY1RDSE40MW12LzVBbWpqeVhhSnB1ZFRxZAo4dUJudGZLcitDa2g4R2xGYzJPWGVOREVoVkZqekZkSkp3R3pXNUlId0RNNUw3OGpJcmltSWExQ3Y3L0RPbitxCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0xmS2ttVHRXTk95UlJDL2E5a1gKeEQ5V2ZWaVlZRklOM0pRemRVbVRaRFZ2b0UyVnhPWXY0VUFtSU9FMUdiVDRJRERVamFrWTg4V2hWVXBIL3RVeQpZZXdTWVZONzVVaitrVlJEL3NZOGRtRTlSNGg4cTd4UVFqblFmbEpEdHZqT2d5VnQ3SHE1UWR0VnFJR1JKWGh3ClNGdFZoZWgzVWhXdnlHZlF0aXdXZ3ZhTFZuMTBCY1duYmpEQm95SHR6UGZCWGZSY3Y1WXRzNlNyYVhpWC9meVAKVkE0UGErbTY0clpERkd1RVJoT0JPUjRYWlI0UnZ3Y0l2NUdES3lFOUI2alA0RG9HV01WSXFVOS9iZTZzSDFwVApNOHBrOCtuSnV5VG1nbmNzWjdQMXVGdHFWcUwzZ1B2SllBQWRxWG1iSUJ4bU1uZmlldVRJYlQrRm9acTVCaVpsCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd012NXBkc0p1Ty9SUnlnU0dGbEsKenhSTGlCVTdlbEVjdkJEeHBsYlczaENkWVF4RFBQZlVqUFd6dWhKaXlSMk8rZW1FZzdJVnBLdnNPcXEyN2ZVUApCSTZTa05Iem9WcXZDSURhemtwU0dtdE1yY0lqSzRNZnJMakN3YnBhQmEreUZGRC84WXdNZGlONFBOODFKVDVECm52blFOZmxHSjZPVVkxU2RBYVFlTFdhenFBQ2VHdTNlSEo5WThkREliaGJBZ1o3a01EaUMxc3JLeUdMNkdaaDAKTlJRNmpZNE5FRURwVzd2S0FVb1RzM05vZXErdHliQnJ0NGUwWTNEQkI4a1hOS3dlcFpINmxzTGZLeFkzYzJDYQpscHhJQ0dmMGxKNzRidzM2KzRacUJNVko3dVFmQmtYV3M1Ky9GNjRSQXV2R1Vqd0RUemZWQzI4RkZZdjZlN1pzCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmU5K05BcnQ5YUlFMFFPN2ZBWmQKZUVqdWhPbDlueHB1Wm5kMXNjY05KN3ZTMHNCV05Id2Z6dG5nYzNZVXhPWjdITEt4bFcycTRMOG9aUXg5bDB0YQpYTXY0MHowc2lxblpHL2FJTlQvVy9QWUpRUSthY1RBeG8wK3JTYWxUNnVqOTM1USs2TmhQWVk3UG9Vb3QzUEFKClZEcHJWeDlDZEtMMUMrc09BSkhENlhZcUVWL2Jqelg2Ums1ZFBkb3h2c3RzSXhOUlJkaExSODRkYWsrckIrMUEKVUtQTWlKOXpIalhCaG04bFRCalFOVVlqTlR0UmlxS0x6R3A4YmFROVRxVVNKY1NLRGJVdVZRbDJqL0txN0JWUQpPdmFqWmM2Q2FndjNJYm0rYmR3U0sxajRoSC9TU1I4YXlackRjWnJJL0VKMXhEczl5WmxTMVFURkU1dzJJWE03CmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1ljd1I2S1NSMVNGWlhrdXR6ckYKTUREbDIwOFUxd0ZXZWkxbGRESzZ4QzhhY0tzQmVpVEpLWnJIWDNwd1hFbWpnRGdwU1BjLzhzTXRwSlAxV3RkYQp2dm8rTUxjT3gvNCtIdGF2MHN6Umd5dEdjdWVuL0w1VkJTcDIyZDQ1UCtsMDlkZTFYbnQ5anV4bG4vdE1aMHBTCnRLK3lWQk5mVjNya014UzhiOFdyN1BESHV4K3BuZDR1L1grMWxHYkYybnprVWorSWptYmlQT0RzeWtqVXYwWHcKbjRUdnEwN0FkQW1nUkUxRzJvTUE0MmJmZ0piZzZ0V1JEd25TNE5Edklhdkl1c3hjQTN2QnNDSEtJRDRWWFFjNQowK3g4MXF0QUZoaEtYc1JSQmVDU3VWU2NFblc4dG9RZVIxUjFqMytZZUJzS0Y1L0Nkb0pTbGR5NWN0VGJ5RVZICjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXc4ZTduVFY2dUtPQWNKY0NmZzkKaDZLN3JucGhsV1QzeDg5QjNMaGVaLzdUYzV1T1RSbUR5Y1BmUzFsckliWE1iZEdiRFBwUzI3SFZKSTd6R1h2bQpYUm1SNUU5NWhlcGxzOHJXRzJ5SldoMUtIbk9SSlVDTkdESGRSeWIxSXhKMHlCYkY5NEFuTlVCNGpNRXQ1VEl6CmpqbVZoTWdEV1NuUXdaRXJGZ0Y0V3FGVnRLRnh1SXZldERiWCs5UG9WMElCb2xGcG10eDUxZVRPRVhpS2xsVXkKOG8xSjZHUnd3SDN2QUFOTmVHY2dtWXprRWRiVXo3VHE0Sm9lZkZSeHozdGc2QWZrTWxuMjVmM1dHZWZwSkR5VAptU0Rzb1pHdml6eUc3bTBzVEoxYm1qTUJURjFJUmxXemR0TVJ3U0NHQ3RjT1Nrd3hQQWtjQXhtMGw3WTkzYm5uCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGhkcm9kZDgwNS9helUzS3p4bDkKSFZnWUZuN0ErRS9hYmJ3UVVvMTlYdWk0WUp5bjVwZmovbG56M05hNkRxNjQ1bkYyd2V3THZidWhidzFBeXgwcAorelJZZHlKTVRGS0l1TDhMRVRaSVVBYXU3bVhDZlhoWmF2WEZ1TXBZRzd0R0RRZkRxY0V0aC95SVRlUU56L1VICjZQOEhUUWM5Z3hhZ2d2cHRLbnBjWEc0REZIalJkZTc2L0J5Q2lySW1EMkVPek9uZE1kaHc0MVlHVTc3N2ZQV3QKbXp6R0JHTGMxSXpaS2FUVURBZkFkendxU2NlR28rdkl0RW5CbG5YRTV6SWxMQTd4Wi9qbGVnOWVvZFFhN210RwpKTG1raThlWUtWZ2pnd2Y3UmVsYUswZmZWSjIvajJBMHpMdG10OHRNS1A2VEZKNTNHbUt6TmhpMVN2QTh2TE9CCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXBENzd0OHdXSzhhS2lOY1VkMVAKNTlWam1jL25Kc1V5S0toRVZZcGZhUzZFSWlPaDJXMVRvcjN6SEFPQm5XRmNybk1OTVVtYjBHNG1vak0rQW5MaQpGeW1IY1Zqa2piTzBiQTdoeHV4TTdQU2tLNGVWMmQwdnBlMnI4dEFaZUlaSFQvWFhaSlZYaDRjdDVVVHZmdTV5CkRrZnZvbmxDMmthWHdyaDM0d1lEcEtuSE84emlHSWc2eWxmWFVSbVRCNXQvUjhyR2Vmd3NvNE9Ub3V3L01rTFUKZVJmMkhrS0szeVFwMFlxYWhDUXlSZEwwc0hXR2JWMEZLZWJyMTVjd1hFZTg5SzQ4dCtJYk9ld09CbXg5bkx6VQpGZFYybHV3dGtEcjY2QWc3UFQyRno5Vkg4a3E0MGlqT2ZYVG0vaHBrYW9TQTgxVjc3bFlKSTU5Zzh3TWxJbnZEClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzFha1RMZTQ5VFlXcnM1QTBRSHQKWTlpZ0dYUXN1WWxnTGdtQmFNNm4ycEZVR1JKM0szbW0zVU4rMzYxdmd4NWx0RDJTOUdoZGhCRWlVa05lKzV2dgpreUVUdTROVmdBZVFXYmM4SUsxYmxxVlBoRzhuZjZmWXJ6ZjV5a29vL2Fuc3ZIWSsySmZMNDEwdXR6NHB0bGpYClFCcTg3WFN5OTdvVWJrWjhHWkRzamlFRlg5R2RHdTVFb1lWVWlsbEJvS05WejFsQmc0ZHhSNUVNWGI5clhzUmoKN2NEREFkQTdiVmNUb2hBRXNGYS9ZMnl3MGVDVTQvVllORlAyMUptMksxVVEyc0N5WDVYQklGa0xJdXcrMlREQwpUeVNOeTJFQ2M5Um5oeHUzb2h5U3pRSUVlbnpnZC9ONm9ObTE2RTdLcGQ1emJ2V2RIK3RlZFZMbDVxNWQrVTdwCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb21BQ1JQTk9BUkE1S2NVV0dMb3UKNC9MM2twNWxYSEljU0FXeGJ1MzZrdlUybjduWGFWVm9YbUl4K0JWcFRSTDNZTmVhbkdHeHh5RjIvOEc1YWRQNwpJUTJyamFhLzFhUFZzc2hZVUVDSHVYd21wVzJCZitkUlhDajlJZFRJcDM4dmJZNWphYlFqRXczenZYeFBxbGE2CjNNelcrd0g2ZnBKM0h1N1RyVllXbXY0YnZzSVRkVUpuWm12bmRycjRrbXZwMk4zcjJreVJsQlhQZy9objJESjYKeWo4WVNnNmdZZGhCUDlqM3ArdnhGVXdmYnVCWFVBa1ZuTW11NHBUdzlTOXBoanUzOTBScG9rSzU0d0RFQzlhWgpBc0FqYnNVY0k0endXTXE3Z1ZFM0JwOGI5WnVuSmhmTHFHenhsR3hpQk9qMFI1VFNpSitLVmxseFpGNlVocnU5CndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDRIb2RNc2VwMGhVTmpoN2xLWnYKVmxYYXlkRjFOUFduMTU5WTRjMHZaajRXZG5yY0xJQkhDcVg4ODBMZG5TL29ETks2L3Z3ckpvNjFMZXlxZFZnQwprNk82VnowSVBjYWdRMFU5bXE1akg1Z1loWVZZY1NOTE1ySEsvdXdOdk1uWWc5V1d5RFk5SHdJay9pc1lHRlFvCnc3elZVMDJXZGVNMW1hVDVBbWFILzRiTXlZQ1Y0MmJURTJ0M2JnT0thMWRzbzJSdnBCRXBOOG5nQXEyRXcvRGcKejQ1MkdPdG9DRERzbnN5elVOR3ByQW1oQ0tscU5jaUZRNVg5SWtsOUd3THp1eXlobkdNMUdDREMvVkRwWVBzegowdFBrU2RCbDZXSWRENHhIVmJaZmZTbkZlK01zQXc5eTk3cFphUVJYZFlMVThUZzJsZ3J0cVc0Z0VOMFVtckpkClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTl5dVpzQVlQZEJIVUxmTHErazQKc2NGVnpLakFSZmZmRnVqd2RGQTIyeWxWUFBLaTJXNmZNY2l6UVpicC9hdldiRTJsZEh3Ymlia0ZtcGFQY3FWNgpEUVFyU0hTejZxRG5FY3lmZmJjeFpZay8xaUZPRks4ank3SmZVZkE5ajRhM01lYXNlWHVUdWdWRXZsd1V5TzIwCndUb2sxdE5yR2V1TlJtWUxlL1hsc1pqbTFRV0RYQkpWNzEwZmd6UklGRUMyZzh5aUkyaE1XOThFSWQrWS9uVFAKZlNlUFIrR1pRbDFPTjh1eW50S2hWbVlnNmxSeklncUEyQXJFcDY0d2dITlltQ3RzRTRYQVBXckszRlB1dnRRcAo3eW45c0lJRnA3ZVlqSkRiN0YrTjhQcWdvTHl4RG9KdEhmUmR6OFJMdEJlQ2YxYTJUWDdmb2dLaGFhS2dmYWsrCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2tzNEJ3QmZZZko2Sm5JRlM1Q28KOXhNeDZjTjZQL0dlTW5kcHpHTlZBQTJuWTh1V2JHMUxQclpYUGQ1Y211Y0JKWXNxSXJvY1JxV1F6b1Izckw3UApjK2pZUGRaNENKaXFyU3dZL2NpWFdyUndZV1ZWRDhGQ2xDV2tzd3gvdkVQSFlWQUZ6a2V2aVRxeWh5Q1JKMG5ICnlWY1Z5RkVNQ3UrcVRFczhHWGl0T1UzYnpnOVV3UDljdTNQT0Z5dzlmaGNycldYQ0hhNkM0czh1QUZ3cGY5Z0cKS01pMUE3RmpRMFQ1cnNBNS9Lb2V4eFJkeW53bm9HR0huMHVCRXBpTE1LM0FDRGdxaGVEL3dyQVJiWERIT0txbApPaHgwVEVXU0kzdDJYVG5CdmlKQUI0cWtjUTBYL3RpbVNpS2w4azhUN3U3WUp4QXVmRDJjYkNOLzl4eXA3S0U0Cm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUlIUWdTNjBPSU1zdHpuMkQ5aDQKTzlJMDlMK1RBNzg3eVNpZ2h0d1Z6SGZEdzNPUDlFLzc2cjR3WGxBWjR0Vm84aDUzWjkwNmFrckpPcU9Pc3ovWApYL2VpZFBnRDUzVTlTSDdVb20xemFPbk1LMk1rODNRRy9hbi9FVTM0S0Zna2JrTjhZbTNMeHQ5dFYrWUY0Nm9rClFYYXFiK2VONlZVakl3NkhSNDlOZGJiWU9pZ3RXUHdaam5GUnJmUXYzdUFUTy9Za0ZFY21uY3FSUVVZNkh2KzUKR3BtcnM1aVFnOEt0MmpiRVZRTHBUU3dHWE16SThwL29yK09kTUJSWXFhb2JxczhUcWswYWc5UGM2TVJuQmRTTAoxWXJZeXlkNHpHTmg2cVFVcGZPUkEvTGxYVmRsVGcvTmNxZ0RYRi9RcXRJT3Y1MzltSlVIQlNuMmF1Z29WZjVQCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBa3l1SUcvaHNYT0pxMWNrYklGa2gKR0pJcjAvUmlSc3VNY0lYUEJWU0ZvYUpMbFFvSHhlRXRkYUkxa0RnenZIeFRyY0xFVDdjdm05Z2JOWms2MkJmNQpVeGRlTTdCWU5mVkNISTFTMC9VeFQzNzJNeGxPK2pwRlh1KzdPL2czVnZnNHFIQVNiTitIejB1N1VkR093MFZBCmFTVnFTNnR2SjNJN2NSbURZMW1ZalpyQ1NDdFZWQm9SRXAwa2txWUZtNm5QNnBSdndRYXN2U0NXRlVkWTdPei8KMEhEclo5Y2RIT2NyVGhYZnF4cElJQk1ZaVZZQXI3TkJQS3R4Rys2dWN0QWJIcVh2MWRDb3J1eWQ1d1gzaGNmRgo4M2VhTFQ2UjlPYWo5dGVyZVFXenhVK29EVis1Q254eFJLV1JvY1dUMHpOdW44YTJMU284clYyaXdacU80aDBkCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXduU09UYTJLV2gvYTUvbTQvOFUKcFFMT0dzTlNRWU5ROFJITUcyQktSd2dnUnU5WHlxbzNnZ0U1UlJwWjMxbEx6N2Z5dmlPL2FhQkMraHduTTdEQwpoQ1hnUHNjdnhrSFMxcHg4bHZ5WC95WjVST05KdVlyR0pnZmFWM3FRVHR5eDRCZk9uUDZkUlhiZXJCNDAyQ0UwCnFRUHVlUlJHUjdTbmhFbE94UElWVTRNWTRRN1FnaVdkNjUwSWdNNVhqekpjRzNlQ1BEdVl3ekZBYUhnR1ZheGYKcjFhaWcrNGhBYjFVaytmVE1vWVRMK3MxSERIMDhDWXF4RHF2a2hES2FFYVVzTWNEVytvNlJoWnlpeXM1aHhkWgozay94by9ud29aQ3JBR0xZNkV1TmxVeXllbDAyQi9DbW16ZG54VmQ3UElqcEFJTHB4aThHdk5uY1NJNjdrZnN6Cjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK1l2NjlqTnJ0OU1lanBPS0dyWlUKMU5NR3ZMOUgydFFRKzZIczcrMWVTMWtabEtwM1NkOU4zdkxJbjVjWWN6QnV0NWRpV2FveGtyb3BoZzZlSmF2RQovVUo2WDR1VWlYZ1pMTkh0c212aENzUG9QOHUyYk5yT1lubEEvd3Rxcy9IVUx2aU5pWTByNHVnSjJjaS9kamhsCksxYjJJTjJpWStTSkprdzNLZzA1VzFuK1VaOHN5V3Z1djZvT2ZCTTRma1lPZTB3ZkJ0OTk0aG4vSXhBaVhOTWoKamRKYWxCbXlldDNlVGhnTWVwbldYR3hubFNWckdtbm5zMmdoNVJXRzh3RXBWZUJJZTdCSE1sSVdJZFZHbWlxLwpuNmJ1VklBOXcrRldPSW0vODE3TzZQelJCNy8xZm9KL2sxN1RzVE9XNUJjRE9IL01yeENmK0NzN2ludFdyZW5rCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGxsbTJ6VFd1clVud3RLOUNwbXMKZy92bS9TRTl4ZkhJUm1JaGhEUHpBanFOSDI1OXhLS1FabXNma2NtQ25nL2RpZEtUNUFsem92SDRwakE3VlZrUQpLeXRSL1ZDL2FySWlWT29BNFppMitQaEpxMkI3ejU2OFkvSGoycjNBM0pXNXhXOFk2MS9ZdU93bkFVaFZ3elFSCjNFZE5tUERRMEFXN0lyTGtEdzBOK0dZdUw5cGttTmIvV1lvYmR2NytMMmRyb1JLSC9PUG9vSXNWOFdPQ0ZTODAKK1lJdkVqMFNDYWFBenEzTHFyaitnTE1idU9Bd0lERzFmd3hPVnFieFQvYkpDMGZuRjNwYnBkZDZhYjZQNmd0VwpjY2VuSFEyblBNZE1nYW9ZdHcvZ0xqU21aRnRuYlIzcW1ybmo3MUhUMy9MOW1tZzlYbUtpYks5TDhyaFBQZUVkCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVdUUThsSitHaGZlTVFoemFHSU0KZEtwM1c3a3FLUVBjMVpBVzY0YUNDSFl2WGh0bFYrM0pMVzJvNHlqSVVaUFI0S0FpQXd0N2JLUkRVSThVV3VOVwppUE83TUx0VnhheTZ3YjJPTUljRHZMczBtR1MrdmF4a0J5UHNFNmpTdjlzWEhUci9KRmUyaEJmZE8wN2JKc21TCnpTbGNyWjhFTlF3TVkybngwYXRyNE9UNXg0Z3UvNVloMmF4VEY4T2owM0JMc0FkWE1nV2o4U2dqUnRoVno4M0oKUUZLU3Y5Zm5tV3ZlVFlqQmI5b0k5MjRxY3EyNklLYUZLd1B6ZmFPMnJkRlFKdlZTV0xDYVc4ck1VYWVPYmJLZQpBblBQdkxaZGQvSXBYU1NrUHZSS2diVTdvb3F1Q1pCeTFWNEM2OHgwWGNiNEtSVUhuek0rbzloUVJ3RFQxN2JDClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlpMcFUzelNuemtzRVlVY0djZXoKNVBrNVVLMlNFKzYyUjlESE1DMDlMYWZWL1p2WktlUml5QzlSMk40aUdZNWp1VDFtM2lxR0VEZldScGFKa1VmOApLRXNVc3NrYVRRM0luNkwvekM3S1dOdE1MYlQ1RXFBMSttVXljWGl3QWdPL0hQRnh4TnVOYXlPWmdyNHpvNGZECk5wZ2JYWERxeDlOSW8xUUtuc0RCdjJMYmtac1VPT2ZxV2IvSWhMMUppekFtbE5RbGMrQmZTUSsxSTUvTGM3NnMKTnBJQUhmekJ3QUhnYW1CUEh6WnpUc0REa2lrYWdYUHVYamw5NWwxVlMvaG1ROU9tdzlzaUpsK3lqek4zL1ZiRQprQmtKdm9NOSsvUERBSjNJdkFEeVlCWVlWSUl3V1ZXSVNhSVRLanJPTnk4b2YzVmRTSklid1Q2RExORVQ0VjJyCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbm56dXNwLy94NXZyNk5FbTJoOC8KRDc5eVN0MW5pbGFCeVFONTNTdzhUajFicGE0VWU3cURlcDRQUHh5TTVRSGJLbVEyU2Z2aUorU29WZWRFcFpxUwpkbTQ3ZVk4S0paRmNQMVpSbHhiODFFbmxkaFpXSkUwa2xFU0NTc0xXWFphZ2xCbkkweW9yRXVHdCt1NWxUMFRlCjlqVlQxWGpTZmE0blNWbDVkaEhxQ1ZBRVVmVUZEeVFZeWNWZFlqZkFLaTAyNmhHSE9tRVFmVEV1Vmd1ZXNTRlcKbVhIU3Q2RXJiVWVZL2dIeDZiNStCL05IaDRXQ2lYL0dzZmFFeGdpcjlEMHVyTVlrci95NG5zU0JUaW0xYW8yZApISkhpc2F4R0hSSmJCUXBueXc2UVVhZTc5bmV5TG9zL0toM0ZobVJCRUR2NUlENEdnY0dtOFdUa1lrK0k5WGlECml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnpLeUhjdldVMlhQWEhrcDd0OUEKUnp1aVJGWTVzZFRPQ295WUY1ZVlYdDdCck9vT1F3dEZyUTZtdDMxejVKa2JxLzhpR0RMOFlhd3FTVFFtdHpoOQpwdFhRMFlMbE1JRGR1OTZ4eGJyMGdZT1l6L0MzL2FTU0paZlVRMi9Ld3ptMG02Nk5jT2YyQllXdVo3U0UxYmkyCjY0bDFEK3k1WGt0REZDcFdkT1pJQWZTS1pnSnVLcjdKNHdodUROZDBmcS9ibWVFbXEyUmJpQjM0OWxFUGFFY3IKOEtQMTZrS1hRZDVFa2VhTkgzQnlRNU1YVGtQSTdoSURseXo5OVovaU5QZ1VsNlAvTWhtSXdNL0VUSlBHUEdjZApuamF2dG04cmRPZHBScXZ4NVdENzRDR1ZGTkNvdUZneExCUGJrV1BFd2kyWDZHTStPbElLUXRDQnhrTGZmNUNFCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0x0UEhvQVF1VkkxNWQzTkJNbW8KVTRsVHdBR3loSlVMVGlnaVZMdHBpK3dvajJTd0tkdW1rUjl4QkFTYmZqeGpLSS95bXlXSGl2Sjk2SkVobCtSNQpaaEpwMWJ4RzBwVFhzazB0S2JqSk8rcTcrUWZIemx2Z3orcjJWZHB1U3kxSFpCOEF1TTdYcXJoQ3gwbFo3dk5NClY5aG4rcEVVYytSbElkc0VoOXBNWXA1Vnlmclp6L0kwaGdJaW9GUmtmQkk3U0d6RXVGZHk1L1VpVStGRlhObzcKVmxEcE9nRmVISFZwbys2dCtqbWpjYUpnWTJ3RS8ydWRlQitBZDRVSEN2NUJBNVFvUG9kRXkvNmN1aUhsdUpzTwpQUG0zTSs4ZzVnRytHeU5rZWE2d2h4cnlacXQvT3I4cDlONHh1VUJYZFRLZUlscEQ2djd1cllERWVkQzhHdEJMCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjRjNHhodlFyd2RIWGJHOUErSlgKV0U1endSbllFUnZoRnF1Nng2V2FSeVRIS01vUlNOV0N4Tkx4SUwrZlNqN213VnZ0eDZyTUtaVkdiWEJMdUtqbgpLcnBJSFFIODh3cVJMSGlEU3QrUU1aVlM3aVEyNmVWMVlqYkRSaW8xMkdOM3Z2MEhSeVY1bnNUSWM0L3JlZGt6Cm5SN2xIM296K0g0Y0krc3JSZ0VPeXRzMmZwQmNTRzhNeGlUUkI3aHVvYWNja3lkTWZrMHJJTEdEdk8vQjlOS2UKUllXOTZmNERDSG5DVDZKdUFlNDVoOVF1UzgyZWRJQmJJRkJRWldoRFRuaUFBNWp5aGdqWWhnY3NwWFN1QWFiZQpUK3dQaVRjTVNwNGM0V1VkKzMyWTlIOXM5blZPVzhQc05PcW1VMjVDQ3NRZHZxWTdkbmw3OUE3ZFVwRzZiYWtICnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjNkQUc5Z1RBTm5GelRmNUMvZUIKcCsxTTJSMUxHUlE4YTlzQjFKb1FVUnc3TzZCL1h6T1VQVlpxN084emdwTGhyVXNqS2tya2tBeGhiZ0V4VS8zWQpSNHkxeUhGbWZYRnk0UkpKdmF3UnNOYWUyQTZvS1JIUEV1cjYyUEFQbHpJZzAyeTkvT01oeUg3MFFKMTA0eFhQCnRmREtTMm9Wd2pJajN4N2RYWlI2ejBwajlrN3BCemZKTE9OSTJwUEJXSFhqL01Jd0NkUk9RQ3RGNXVLbjVITlUKQ25UR2lLVkdIS0x3TWNWUkJjYi93ejFBUldzQmFEWmVCUDlWU1NYY0gwSlVNdkpCOUZJR1IzQnJsQmJ4blpVbwpkYUZjR3FkNUxNbmcrQlpjZWN5QTlmZTFYRE5QM1RGdHFsZm1yWUxXVm5weWJ3azdmOXplTkpKNE5pRjBBcTdhCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTlBMnJ5Q1BMYUxhY3NKYnV5YVQKbDBhckJ6MC9CRUI2aXorYjhpdDNxY2dxK2t2bXpsenExT1BVbHBKQXh2Y0djYW01aWlnejJDRVlDbVl4WnNrUwpaYmtUQ2JYTzR6MG91Z1lJcTJXbHB4S0dOMkNmdCs3SzJoKzF2d0p4QmVrZWREZlIrcVBUd05nT0ZPNGlmenl6CjlCMWVwbDhVdktueUpjbGdZOGduVVFvRGdwbFhMUjdpTEc2Mlora3AzazNrVHd2dzRLT3h2V2t3QWIxakluNTcKWHNSaTBYYmw5Rm5oOXpCYStwQ2dNb2xBWEx2U000bk5Yd0F0L2VpdHByRWlVVjZMT2g3REZlYThWdjJjTTd6WQpLYUtVTlBnMTVBWmljcVhKcS8wVnYxUUo5ZHRwYjFQUUJqeGkzUmw3MkViZ0ZhemJmaDBCTVpIcFB4eWMxM2xSCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUk5T0VoWG1IdlFwWW96c0doTnkKWU9NTjBBZlYzOGxjd09BNkc3bERkSCtiUnFCbXJ3bzRuVnZSdjlMUnVkYWVJVUluVmRuanhwRnBuMWk1ZXVFegplUzgySWVoOE5uYzVvT3dObXoycjhUbng1enFRbmo1U0dwTGpnT3FJSTIrMnBYU0ZIMkpWM0ExeFNOYmFiazNzCmdYSnJOaElnNjI1dlhyTVNkM0dEbEUyUTNGSUpJd2w3UjFQaDJkNDUwbW9aa2xZdGp3ZmFEMWcvOVNEREkwU1UKMUdzZXNuZkVjRUNrQ0l0amJJSTJSM09kbURmMEZqRTJQNFptanV3NDIvT2tZRG9uMk5yQXdUNk5YYUZOV2YzYgpQSnBiMENKZGRQR0Y4VEVCd0xPbWdZRDR2WEtFZktXN0pVQVduNHBLbmRtaHNaTFdpMFFPeTVFYnFGcnBVVVZNCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVZmVDlQUzNOZ054ZTZPMTYyaDkKRmJnY0lBek1mNDRWOWYxT2hhR1Zkblo5WHBZUlNmUHFUTldWY1FUb2wrRTFUWkxRUGZYWFR2WVZMRzFmK0lQVApVSERIZEdUV2IzRWpZZldPcC9PU0FVaERwRWtVZUdSbGZhSEJickF4ZytCcHVRMkJkU2YvNUlnWHUrNG5aU0VpCmtXSXY3MjFaN1kxQUNSaXNlVUoyUi91NXJucEdtd0pLcEkxbGZURVpJR21Sejd0TGs3d1VNTlFYWmcyZE1GeG0KZjQrZHpHSUM0WWdLdXlXYTZaazB4b1NZMnlqV1l5MjlwSjBMWXVkTElYc0VoT005RGtiVjcxRWlqNnZXQlFPUgo0UXVVUmZraWZyUHlVcFR5RWQ4T2t2YWxTYnRRUXNzOHk5YjR1VFBtKzJRTFJEeStpc1pyOGphYjNFTXBpZ0xLCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDN4UWRjdk1MWXhDSEFCSVdkTGsKYjladmhZd0NzUXhCMDBObEUzeXFValZyNzBFUkx6VjgxQlNFUUVlTVN6clVDT29vTHpvV1NVOGNZQjJ2K0E1UApFUEZVaElaRmtaOGp6cXViOTVYY29RM1RWQndDQ0xWSTlZcHV6amNWNTUwWVl1SEdKbDgxUmcyU0JnQjluR1owCk5BSEhrU3JxRlp4MGlra1p1QmtXSWRjV2tvMi9HL2FZOEIvM1NkV2RRMVR1YXJEb2JDd0xwOUhOMGdac3JCTXEKNlppVWdXSVNpNzAzRHZxRllUT1VxVGF0Qys0dnVtWGtWRU1lam9kSFMwVFo2TVJnRGxKd2tMS1ZyWEFYR0JUNApMV2ljdGp0TlJvMHI0Rjk0TVRDRUdVaTdFa3RsQ1JVdjhDRVF4ck9tUGREVml4WWp6ZmwrQndCbnkyZ0FQMnBXClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWMzUmo4SlBrSlNtQ0xRUWJ3ZTYKZERWc2hOVWtidFVlMXdXejlZMTlqdWc0RnRLckxIS25kbmZZU2hFaGs5ZDZEYjdQSmordjhpUW5HakR6NVdTNQpCL3phZVp1VGZzUmxkRTlPczFxQlhINTAvMU93SGFEc05mL0NIUmdtcFhxOWQ2MWVJcmlIb05IM3J1b0NSZHg1CjI3VHN0YkZ4Q3czNDhGQkJGRUw1RWhzVkRxTHI1Njc5ZnZwMnROb2MzaDRpc1ZnbHB4d1ZBdjkyUjJOd3FsL1AKK2JEcklZbmxyT29SK1dVS1BBM3d1NUt4b2xPZ3RvUVNKRVJramFnRWxML3lKKzNsbmdiN2ZXRHJzdHZpZ0FBdQo0UTUxUmtGdENkWlNLVmQzcXNFbzg2QjhTM01rVlBweWNvUnRTeVJOV1NId3FVQ0xuK1hVVHB0MUJ5YzBuWWE1Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlYxTHZwbXdveTdreFlJbkZYcmYKQ3FlbllRV2FTcWYyMkt1N1VaMWxreGJUR28wVlVpZzlnUHNSdHpiQlVTM2JiUmg1M0tYeDRtay8yc1VrOTZBMApJQlVqQnZrT1BZWVdDeTk4MFp6bC9zVGVzMlBPam4wM1RQWHcxekVGa3lGd1BJSm8rQmwrWi9mM1JXcGlJUzk5CnFvaFd1L0JhMFpVekd1NlBFM0Y5WS9ocUkvNWw0QWZ6YTJOcFZISG90cGl6eE9DRDlwaEZUZ2NhS3A0dUUvczIKSjYxSUJQa3pFQ0xsU2VGeGd1NUMzZFoxYTNJU3pVS21kT2hYWkg3LzlMbXRxTWJZaHBmWnZtRVNaR3FaTnZhQwp2c1ZrZ0l5WW9YVzY1ZHJoSlQvOGcvd3c2b0k2VXRnZGRzNjF4OTF3YjI3WGFybXRVVHdCMTlWalN0YXJMbE1oCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkxCd2ZZTFZiU3FPaENSQkFvT1EKMW9GVy9IN0oyUWhPNUp0YmtUVWxKdG5XQXBQdkxhZjJWa2xmWk0xUk5sMjhSbHQ1Y3VHcU1hN0RWUUZYOFpjcQpsZDRpc3JtYWd3bVljMWZmVHh0VnF4QmRGbG5mekdpaDdNNkpVZUxhUGxWWnE3YmlsWURMYlNxOHBwVkJmaklPCjY2eDB1aGhtSnVQV2lVbEliMk9lZ0wwWkZaSm5lRXZLNHJvNVJzUHlNRUVCZWZrYlcyblJ5cVkwSFZpcmxqaHYKOFZyVktDSTRvdkNYczNyenJEWTdpTkVhQU5OcE4rc2YzUVJUdUNMbUtwaUtJQUlOeUw0ck5sYjFJdHRHdXVOVQo0cm9SZXZFcHlRVlNQSkZyMk5iSG4yTFUyTFBTaUlaZzBUT0kwNi9tVC9RcE9xYlNOV1RvVzN6VU1vVVo5QUVmCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1BFRGdqQjFTOWJaQ2ZKMkNlRmoKM21heEJ4MDl1bUFOanVjaERXWTRTdnA0aFBobmJ1bnZDOHd6ZGlIck9OZUNPYUtRaXdnQytBeFJVdE5lSU1WeQpPOVFlckdvMk8rUFBDME5nWVVubXdVSG5DM1VteElKWHZGcU9ZV2NIM3VhSXd0c2FvVVhEbGp2RWMxL3R3YklQCks2VDd1bzZSY2tMUmRwUUdFbVhwZmtXZ0pSU3N6T0VNL0FNSStZVE0vTU9pVUdBSEVWMHhXWHBnUU1DQ3B1bkcKN05CNUtsSzJwRVI1dlhpWEN5ZmZvQmsreVZBZzI2Q0ZKOG5FZHhHRzh2ZXd4S2hMWlZoSENhdmJvSVBmeGp4TQpiR2xsY3RNeVJZakpWRjBxNURNakhKTVQyVHMvNnN3L21CbDl4Z1NraVlWc2pjaWt5TkNGZEZnTSszbXNXNVg1Cmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckNJaHQzNXVOaStMbFpBeHY5SVEKUy9ab29zc2dSQ0FrNkNzcnB3Yis1Rkx6cW5pUXI0UkxkbmpVSk9jdytjcWtyZjV3L2ZWV2owMndseU9ueTI3NQpVTWV2cS9NNUdMN1BnZm1TeVF5YUVMdGorWkI2VkQvMHY0ajJ4cnpDM1h3ekM1WEFadTlzN2VpeUVLUmpkVERaCkhaNFgzbUNzNHRHS2NxSVY2ZHREc011Q09EZ3pPMU1jZ0U3cjZSK1FIN2NicE1pNjZlQW5VQ05rTGI5c1ZuTVMKKzI0eXh4bjlLaVMxQ3RXK3FlZ0Q4Znp0UndVWWJEWjRkenhHbFBTcVVvcVBtQk0yV3ZyMVdNMkRiSDBWOUZRSwp1NHBqc0hJSlVjbWsrR1RlWDlGYm53andJeU9RMFFCSzF5T0dDdVJ1NmlRWVVzQm5ObGVUS2RublN6VzNDOEV5ClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2tWWjRtQzcxazhRZTFaNzZLZG0KMldOZFlDM2tzRFFZMFduUlJXWGRGaU5GYlY1RWgvbkxDMitsTmtacVZVUklXTzJFSTQyajF4L2xwZ1BPTDhuRwpmemU5WXlDWE9obE9CWlNZZ3Q0MG9FUk5zalZQejB6MGF0Qmhvc0ZXRXFtL1ZzQTcxTU9DZjNBUm0xendUeElaCmw2WjU0eDVtRVRIZStvblo0YTZVSHpKYmEvcTRPWU9DM25ray9PL0xxRUsrM3ZKMUZEdGtqN2ZKQUhvV1RsZWwKUnAwcE83NERtVnNvNHZpQWVyemxYOHRQOTZRNkFpcVRGNytHUkVPQllPR2E1dUtNYlhaenY2QUljV045MzRlRwpHZENPdHBmVkhUeU1FRVNnbVI0R2dzaHFFdHAwTDJ2Yi8vMUg1SjhsMTdnMjErNlNoMnhjSWdQRWFNZGxVa3B3Ck9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFQyU3Vmd2hPSTFIT1hVTlNRcFYKZHA4L2xlaGo3aGQxUEVLdlc2VnFYamV3bGYzcDZnQ3NobVA3Zy8vUzZxZzJWVlVKY3ZVZGJkRWJkRktjSVNtcwoxZ3A4WTNPRWpQV3RHTDNNTXR5NTN0eUZrNXh2UXZRaWsxR0hEazRmMC9CZ1Uzci9RaHl3aUJPSk11ZmNDT0VQCjhnTXRwNTBJczllbHhGVDh6akFBTEt1dDllRitmVDc3a2xuaGRPejVEem9QcVVJWmxDNGlmbW5GOXJKd2k4SVMKcTR3aGxBZ05EQldxd0pPVUY4NWZHVG5BZjh4aXNVcElnWm5hdVJtTFBGZk1ObDVhMEFid1pxZGZVclI0am43VQo5Zy9JYnVCN3MvK0xXa01zb3U5Y3FJRWZiSmN5bDBWZzUrWi90QTk3RzZJekI0VStJSVY2VVNLYmE5Q04rL1RnCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBci9BZWxZQVdBb0VlY254M3M4NGsKeTRoWE82SThnUmhpeXRuZFI4Y2orOFZCSG02VGJkT1Nad1oxOUw2ZUM5UGdUVDQ2dmVabHlkMnpTMDJvb0hRagpYY2Zsd21sSFZRbnJpQmFGaG5PLzcyaFVxT2JveHpPbWNQQ2c4R21DTFJWbC9MWVZlSHZjOE96MFZYa2F6MGMyCkR6cHJvZHl4WFNvUXdDV0N0MHduVWFIem5zTGlmcjBVQXVEWm5vdFJSdHg2UXl4VEYwb1VtWVJXejR5OGh4S3AKdGU3cUtuZHBtVEZQRFpmaVNoOFl2TGVWVjl2WjZBcjJHb004YVJqYk9YUjBhbUR1bTNUcnc2R0x4eTJhamlFMwpac1pSdWJ4ZHhSU0JuL2Q1OUN6ekh2b2wxTVhEZWx0aGJleHFJK1FKdEJhdmJYUlpPRXBSSWdjOTVTZnJ0c25tCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbDFOZmxrLzlWdjFsOUIxR240elkKUURWOWRwMkdwUmIxSFR2UVUrUHhzR1dGUXNlbzM5WUtpdTNQLzZ4M0lGZE40c0htNzRsVXk4OG1kSTBoaEM2bQpwUGNXemJtM1BZRjkxMkMzTEc5M1VzcXk3ek5xMEtMbUJsNEJ0UzVHb0JHdjlGRXBHZlFYOGhkd0txZ2RZazFhClNvTFd5eGZjNzNHRXZtYWlPWFIxam45akxEK0hpNEtMcjFCVmVpdlV2L3lhalBON3hrcnFESW1yRG5FZ1RveDIKMGxlVkdrcFRhZkVYa1RXNjdZaVd0RzRhRGd5WmNNUHdlVTFXSTNDRHRqaUNXMUpSTlRqNm1ZdDNGbTZmMkxvSApRS2pXNTNreXRPMGRIVHJmMHZiSzFOU29LRHVmcXBIQUhoUlpqMWVjQWxWUHdhKzNqWEZGWGhvZU1CK3ZEcjMzCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcktCL1BkQXErRkZyTnlIeDd1WlYKa0g3MXpualdKT2Q1bjlIQ3pFbVkzZDJLN2FORVFTS2c4TzVKei9Deno3UVJkb0pmc2tNN29ZZG9Ccm4wRFlCdApBVVo1U2JIWUtZcXQwVHM1N01qN2tNQjFHVlJ0YWdyQUFFR1hRWnBIby9BeFNQeXExT3hYZmo0SnlHRjJ1dlQ4CmhmamRRbE9iNlg3bFNSemJLQzd5ZDhaemNFeW04OEF5MWRrVVhMaGl0eUtvWjhDalFkaEdMdVE1MU5jaDhwREIKeDdBNm96VUR0YUx5cUlHQ2hWb1RGZ3JJeE9PYjNJdW9hSmhMV01HcGw0SXRVOE5JYjNtbTArL2tpQjFBcDdPWgpqdEFFclNYOUxoWEdKVHVjK0VaT2gzdG9hSUJlOUFGK3FaL1J1MlJEOVgrSjRFYkwwMm5BOXVDTmJnQjdnVkxkCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjZkMXMvUnFmUnJYWnJzeDh0MGcKNTVCQTg4S2Vya0pOakNXTzBmTTYxcklpTWtndVROYUZBSnJVM0tCZUhvcEFIRkRXM2RGSDVFekREOXBIaURGQQpCcndQaWErUUJMVldDVk1oWnRPbFBGR05CR21GZzh1MU9KQ2w0SCtyajVwWDlJYzNENDJyaXpBWG9uZ01jZzVPCmI5d1FrZzRJQ2dlQUtyZG1wekhsdDNxY1BxNTVSdHlRNnVhWFU3TXFlc3N6L1RnMGlBWjlaeUloT3ZUalVsc3oKWVVrZ201NCtSSUEwM2ppdFNQbTUzMDhDMHFqejZ5aGt4czI5L0VabW1IS1pKTzZIWTQ0ZklOT3VRZ21sd1ZtSgppTXFrbkJjNDJkNTFnUkUxeCt3aHo2eVdXL3FKb1dkNk1LYkcwN2xMTTQzb1JZUTlXSWUzaG54OXRZRGdQUTU5Ck1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBblVLMFN0bHlJTisySWd1bFFpRnMKeXNyZkNYOEgwVlM5VmhPcTczMGZlZmN6UE9aUWtGMGRWd0RBODdIYlFmTFNJTlNQSG1TRWgwUDdMKytJTVA3UgpQR2ZrenpEaW8zQ1E3S3ZuU3M0cnZLbG1TTVY4OGg4ekZ1aHVvY0poQnJyWStMcHJyeFYxemtLM0Z5VDRnbytuCmpFSWlCZ1F4WmkvSHArdzEvSUttQmZ2VFNYL2MxM2diQlRUMHFndURBU2hlU0lmNzg2TnhpUFZmZ01RSkprNDgKcU9WRmVETTQ5NGl4bjBXYVpBQmhhamJuZ0dNKzVSZ2pPQ1EyQU9CLzBJV3RpemY1blRTK3MrYlN4SldpZ0NpSQpVU0J0TmZYVGZ6SkVPUk5La2RCWnN1bFh5a1dsNTJFbnlKQlNEeDFCQks1VVJXZHpzMFZNR21YSG1sZTIvSFl4CmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnczTlZKajNtWEZtQndKYzh2THcKeE9zT1NQc2Eyc2gzTVRMMTBwb1h3SGd4K2R1M0w1Z1BVWFJjeVQ1dzYxN3lnMzI0M25kOURSUzBVYWUvVmRmNQpDWnp4QjlYbHdNRmlwTGt6Q20xVGg2QlBUZkg3WW5IV1Q4cFcxVVNHZFBZOTNuTHVYU2NQNExsVlBlOUZXU1ZrCnFpbjBndmdMcEx5QlNXaVlEak9FdExDVEF5QlQwbWw0cGZuRFBCclFuUkEvRTBVd01kelZCY25tYnZWVGNDNWwKbUlxWXRoVGdaQ0kzSldSOVNQTVVyZlBFOE5ZUGRuU25xVS9WamxFbFJyemsrdzkwcnp2bHI4dEczWW9tN1NNZwpuc1UwN3ducnQ3L2MwanFGek9SY3d1RE5RRUl3Zk9Yeld1dk91ajB2TXR5S21Hb3crK3RWZWdHWVhNZmt4WngxCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjZNOU5Ca01FRUkxQ3MzRWkySkMKdDJCT1dScDc0TGJsekZlTkJNeFpLeGtrSzhxc0ZBWnpaWGZKQ3VncEFIMDV1emRzRmRTNlNoUzQ1ZitkbkxUdgpJV1lMeWdWb0IwVlBRR0RLT1pkajJvRzZvNGlWMFBad0hob0NhVmpDVmxoelVZemQ0c05wbU1CVGgzOFRvRzBZCnl2SDVSd1J6bHp0N1RobFpFWUQ4S2hQSU9JWFVTVHJLSXhXMXJLZ2wwdmhFMFc0a3d4RlpTZEM5OWFxWFpDbkEKbnhXUHRPMU1KZFhIV2NPd2d4Z05oWWw4eVJQOHVjcjR3SENIVlo4ckNLVEliQzI5S2lVcURsaitqK2dCdUlOKwo3VXFXQzdBNHc4eDhCWEdIa0cwWXk4czZxK25sbU5rL2VlKzArNk1ZVUM0c3dJdUpqbHhEcGpOR0RTaTZ6WUxqCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHlEYUhON2JCRHAvTkx0R2wvWnkKSjhQendUdU5FQmYxVDR3YkduWlNEUzZLclhJREUwcm1tUENsWmY0Q1duaHB1WDFROStmQWR4UWIxY2EvVnk3bQpzZXVHVTl5aVcvb1UxRmczSzZCVGZTeFIyRnduOGRTZzM5ZURxMWVzMEx4dENOYkNUczVQWjd1aFY0TXZMSWdCCi9Yd3FwdkZJZ1I4R3ZDQ2dYRldpY0pVZitRUWRBSEg4M25UVm03SjdMcDBOLzBBOE9BTC9xUVRKRFNLMzBNSGMKUE5mZ1ZUT0E4cjFYb1dMaGU1YXhHUzFNUUVhRTIvek1GNCtuMHQxNkNwNFBhZzIvbVRkcHZxY1ZJZERmYkN3egpQUkVSbE1jaUJjL1ZvUEh2M0FYOEM5UjNUYUZYZTBLNnVhUDZPRmdPRGRkaXdVREpkeXRvK2hHZUJMMzJMNEhTCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkhZSmp0anZ2U3dmYklTK3pwMUMKS3dmVytvaW94UDFid0h1d3huNnpkb1A2YnNvVDFoaWMwWTBabzgvQkNmN1lPUnQrbDhUMlJUSzNrZHgyNDRCOQppZEk3dngvMmZ0allZeVhaTVAzWm1wNkR6WEc0VEkrc0JrMWR0czhqU2YzTG9yN1JIL05Nbys1ZitJRERTZkVTCnE2UWpoNE95Zm9seUZOT3RUa213M01vVnlEdTBHV1JjeDNWT1g2L2ZEaytyY0RxV1U2QWhpVU0xeWNiaEhLUXEKK2QzaFNLbFZTUEJCSFZBV2d3a1ZzeEJJUEdKLzdKdXl2UmJSaytrNHlWck14M01XMitCUzVJWkZPYjJOWjVaSgpMOWcxVkRYUGVZWThySnlZVFU5YnlIQUNROXR6VHRzNWFZcnVxa0pQWGVYTnc1UXU5b21ITjJWbjgyN2NBcWgrCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUt5NXlNdGFaK1FRVk1TdGxzYjgKYlNXU3hnQnpId24rSmZabU1IOG55bFJtdzh3dXpCZHFQUTZ5UTFReW5NbDhRazY4OCtEenZCUzA1U05oc2ZZUgpMQWtmSXl0ek9BV3hGNlp4L2NmUk4zL0NzTXZMemVtZzlJWW83NjVVdmN5Y2RCZ0NxQVpnZmZLTjFGRUl5anN2CjVTdU1JLzJScHJVaVVGVzBERG8xYWJ0OHVBbURHSkZiOXJwQTB1SklhcitmNDYxenAya3NNSEFpelNCdVVwWUwKa3Z3S2VvbkVIcHJnZDRlcjQ0WEdIMDFCbWVLMUNGUVlhbFpPbThXMHk3Ky9mWEVyTjN1UjVlaGRHMjZNK1FSbQpoanNJRHpJNFFzVjZDdzM5aUJ1L2tWVzRDazJlTVNYL2psTHpzU2gxQkYzOHg3WitsbWh0WlVqSG1PKzJYTHNoCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm5XZ2V6VE1XT0NURjVJajJ1ZlUKZGVNbjZLVHJoTnRxbUdtTTlFaDYvR3N4S2lwQmlvMlEyeVJKbjdxTWZsTGFaR0ZpVEFIa2xEaWw4emhUa09XbApxTC9mdXZkMFF3VzBBTGF6Q1FwNnRsd1ViYUpsczQ0a1p5MFpEZ3ZTYnJ2UDdQT0VYNExvNi9BaUlsQ0xGdXZVClJTQ2NRaXhZTnpuUHNLTzB2TUlyOWlianA3SnZDR1FZaERPVGZNaU5oZDBXNVZXRkZCRUlnR1VqSHNyYlhLak8KYjIwcFhuSXhJWmZjb0NmL0E1YjViWkQwOEFTZmJWVUh6Zm5FWlc1ZUlkRVZ2TDR2ZkdkSmZHZ2lBeHlRSUhBaQo2SHhaV0UrL3JMRW5OT0lpUjZ2dThwS25LQXJsVTVVem5wTWo3UDhUME1sa3RMNTI1YjJ5VGVZc0xJT0VjSXN1Cm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2hxdllGb0paNG9FWGtOcWtYbnkKcjRqUURyZWVDYkN2aXYxSEFlNkJ1RlVFeHh2ZDBiQ25OSFRqa0lybWZUK216TThCeDBValhDWGt2akhpaU1xTApXWTloSDlsSkVRclVJazMwVGxHMUg1VlU4dGtOT21Ddk5vM0RQR1BaSzVuV0tmYStUZkswUVozV1BZd0VLUjBKCml3a2lOS0Y3RXhUSjhNQ0w4RFdkWFJ1RFNoa1pWYmhkVXptM0t5T3MxcHZHa2hZbG9FK2ZzcEFvelB1dklFTGcKb2RHRndRc21MNWxsc2xyWlMyMUVXSzVONFN4a0JKR3hJRzNaVnJZUmp3TW81NGI3WmhzUEhyWi9YVHNVT25sVgpwNmFLcTRQanlxdE5Pa3BuZFFDTkJFcE9QbUYyU3hjcTJSaWZLQ09Gdno5TmdscHJ2YUlkKzdQNHpUakhDMUVUCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOTUrc05tM1pCN1l6ajUxV09CRjYKOTZiYklvVHpDZU9mQWVaWWtUSHNTOXBTU1NoMGp1ajJQc09JaWJQRElVdE5GRWlmbjI0ZXk5ZFZEMVJEN3NybAo4RmdJTWZYdk1WU2JJNjVsRDZDbS95VXp5Mk45U3lldXJHQXNlS1BPaHpWZWdJdDNyZHhQT1kxcXgvK05yY3U3ClR0aFExNjVjbG5RcUp1NHBtT2tqdExnaHdsSU5mNW1kYU14NGE1Q1pZc05WZjdzY3hJUm5adUVsV0l6dk1aNG4KYVV4TDR1UytodHFPa0ZVVmhsK0VmNVRjMUlOQ0JyRzFqQXFkNlN3T2Y0RFRscE9JNDN1STQycTI2RnRXZWRpLwpzQmFSWDdBaVpMN0YyY1dGTTJJR1pYVjVpZ3hKcEdTUDAwVFpiblVCQ3d4WjhEMi90WGhtUnZoa0hLakhtRWhFCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemVBbXJQckxJdmxzSzcwSDRlTXkKR3h5b05IcWhXOG9vMW5TTHNjakRkUFdhelpLY05DSUFBVEFkdlk5d2xGVTBwSkpXT2NsOFhPbGlqMWJBQkw5QgpKYmM3WXNRY2hGcWE3UXIwQ0VUL1luaUdUMW5DbjlKTGpwL04wY29wSXBrSURsNEVWWUw3SVliS0U2ZW1mNS9SCk1KNzlrOEk1OW4zUGIyWkxveVNyaGdMVy9kNndPUHlvMkFHMnZVTVpRMlFHbUhxcStWRTRZZ3JXR3FpMHRDa2EKdHNJTnhHZE84MVI5cnFaSEdmbVR4cVQvRkR2VVByWmcySnIxYkNmRkNkU3pOOGw0YXlhSTFESWEyb3pWSTlMRQpRWnFMMHJkRzZGNkZiNVNkbDVLaE5VM1pBSExpQnU4SmU1TU56dVExMGR5aGhNOWZPbE9ha1ZuSDZ3c0VkRmZJCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGdBd2FPTjBvQ1VSbDdvMFZQS24KWCtGajh5T0N1TGVXWllSQVc1OCtmVGxSNWMxZXBJSDd5SGRpams3YXlic1VKMWdUckIrNlBxM2pXRnF0YXIxNApTY3ZzRUxCVkJQWW1vSGhOM3ZiaHJPNmFVVWNJaFoxa3NKWEY1cUNwWlFiTFN6M0dVMitqQ0JydHBFaVA5c0xHCmRrYVhTVTIzZkNCTjZhZ3grVmtNRVMvRmtZVEpWUHdGVUhzclgyMXhhaVdxOG1nSlpwZFA0a2NWVmMxVGQ2VmIKc2dMSW0zV1g1MlU1NkdtWjdwbjA2UWt0VnNnOW0wcGs3cWswKzR3NEJkcXk4bGZFZkR5UE9WdytTNFhoRThGbQpXL1k2R011VnRRek8xck9lQ1c0Nms4dEVET1ZsR2c3OGp4cUZvV0hzMG5RZFFzM28wcDlTTVdhbWZISFpKVllUCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFlSN2lBVXlLNnhFZkZhOElUUDQKMVZ5UndnS2c5NkgvQjh5M2ZLaFNZZE1KMHdvTkorVmYxNEVhOWJTQWl6dUdpU2pxeEVaVjhHNEhHVE5KMlNpNApQM2syL1EydmJDTFBEUFhHajRGbnFvajFaYUhFVXhJTTRoN1dBUE9ZOHV2cENFbTYrQkt2OWFEYTkrTnErV1ovCjJKeHNqcytuWm44elVVaVUzU2dBNlFXSndnZDZpdWUvaXpVZzBmMU92V0NNUWNyZG5tQTN4eUVQZzRZMHFkY2MKUnJlTlFETjYzQVpZRitidzRmUEFVU3RJOE9PWjBLK3BWRFBZQ0Jlb3p5NGdnY21zZkl0WVZna1JBSVh1K3RKSwptNWJWWGV1N0dGUVRVVkZMWkwwMThVN3c4Z29MdEdHWER0dHYxcUVra2E1YTFER25lTDNzcGVPSzhnWW80MlFJCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmhVeHl3eTZqc2RaZHNBeURqZUQKd2RDK1F4NlREUnM3UlVYVVNRWEp0U21aL20wSlZvSVc0M200YnZDZ0I2UG1NR1VBTGFRRnZ0MmJ6a1V3VUU5WgpyS3Y2MmNNTjVnZlFqQkk4djJ1VFREaWhaWkhTT1pXSW4wQUhsQUIwR0l3WlU4VTM2TkRMYjFUczk0MmlXVGp3CjNTUnJoaU1aWmxZVmdCeWxtaU9OVFU2RGN0UHNRZktDWjdZZDFGRy8xb1hGQkNMak8zWjhGL2NjTDdlZnFGemQKd1FmU0xZU0RFK1lORzF0NDM5aWdXVExnajMrcGhyYStLWkxkeEdCZ2F5VmFrTXEwRXNjcTlyenZndnUwdzZPVwo3WXY3MHpOb3JSbkptQlhJRTB4eElyZWR4aTN5aWNIcGxvL1AwVTZuNm9oSmJhZDRQRXlGV1liSFRRZUpoQTQ3CnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3kwcmU1Um14Wk05REUxVW5KR2sKbGswS2dWZStWUXFvL1FVTTNDNHhQamIveHJYQ1NaVWx5NC9zY0Jsd2xFalRERTFab1JtZ2VqS050dXFSdC81UgpxY2lVekR0VXEwUlhLYmdBZFgwTElwYVFBcjFXcDgzN3JZelJOVi9SeDVLTmhlRjhOWUlsalp2VG5EaXpTcitWClh5VmJLVXJqVVRvdEE4eSt2WXp1ZFIrVDRTRzhKNlNZN3RqOVBnU2c5OGh1UnhkanFJbmRUVVk5UUYvWmlTcXEKWVVVd0xqNVUrYjZHcWZnTHg3SS9Ibk5pZ0ZuU2crQUVrNTlqak1HNnpvZ2oycXdJU2tadGZjS3laYkExMGtubAo3TTdic2U5bW5IRTlmTnMwUGtOY0pTeExpREgreUl4bTdhQnAxaUx5NUMyU0VpRm5iU3FJY0JlWmgvdVZQbllpClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0Rma3ZBTS9GQytMSjMxS1BydnUKa0VtUjNTNkx6Mnc4YVp6eFlwbERwdk5IZlhnUXY1djhLOUN2ZW0veGNKUGY1V3o5QnFwSkY3OEZNWnBGcGc5cApRV2JFRjNwcTRadi90OUkydjFpVTFjQTVycGl3ZzhqZFp2RUtSSlg1YjNVUk14cmxCWVdoZzFIc0pYYzVSVXczCm43Nkl4cU5lWUR3V2ozMGp2czVvdUsxNWloaHVlOFBINElhRW9tbGppb0ZGU3lPU2RXNnk4NmN0bWZMUVlKcUgKQi9veWZ5bVAzZHN0U0tFWCtOTnZLa2V3Nm0yUTQyWlprcjhaV3cwcjdqcVJCRWZYelVRVTVFVlh0d1F5OEk5dQpSSlV5QjJWb3pvMDF3TFdDYVdWR3RFb3RzdVBzUXhOOUtzOHJWTlN4ZHpJdmV0elFtZUFHTGE5aUErREpTM3JGCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkp6dXFYM0RhZFFSdThaQURidm4KSUh2M0NKVnE0M1l2ZzBKQ3NtQUN0MjUrZ3o1UWlwSHZFT2JrZFlvSEV4bzdwaUxYaUdicW9EbXFlWEtCNnd6ZQpUNlN2dHc0VFZBN2Zib3BWYkRJci9sU2U3Y1RWdjZCZDVuQm9VYXgvNmZ4dHhlOE1VM0dLRTl1dm5pT0lzWW40Cmh5MStSZkFyMVBTU05tQ1A3ODFiUHBKOHl1V1pIOVVDQncyc3ltTURMS2x5WEdTYnRrSHd6QW1LUXdQY3Bmc3kKZkdGd01hcmdKbzhLSUNEVnJpQVVZMll6V3o5WUdqb3ZCcEl1cWNka1VHS2VNWkcyZEhwdVh1UHE4U0pxdzZDSQpHVEVUbndqV0Rtd2t0cW5oWEd5U0l3Sk92R1lJUWtWWUN1N2lDelcyYUpvTWhKM3h5KzVVQXljeWpKVnIzQUdrClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckc2Nmxic2w2blVTSUJCd1JBd2IKcmg4NGdjU1F5MVVWZjBqMDZ4eTBINWlvY0hSUVRLQUJmK0JqNHB1NEJDcmZhQ2I2NDJ1V3dhaUJld1gyWG9TVQpOVkxnQmpuRERFRmFPSEREbnlSeGQvcU9ZdTgvdWNwQ3pwN25aVWh4dXBMeU5zczJCWHhkdGI0STVPRXF6VkR6CmFMVXlQSFE5a1AyaDZSZnB0MGtrZzAvc0ZSdTVuWUdOOTdKMzBLQjE5T1d5alJxRTZGNGVId093TGhqWk5PbFoKUjVaNERld0RuRzNmc0dDRmlNZW1RaTZMZEVteFlVV2JBbEZ4WHdGTHloZmRvQ2ZrTWIxTmU3QW1wU3dERzBNMwpIZDl0SXVyTFo0ZkJ6eElCemZuWGdKZENxd2kzU21HUDZhOHRVUzQ4T3FaSnNhM3MyTmxjV0QrT2k1cDYwMHlsCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2NYZWFiOFAzbWVabVM1LzgwL3YKbTAyOUJEWVF6S0NCcmYwN3hiQ3RQb0tkTWF2T1dXT2tGaG9STkNuS1RybjhUVE53enFmWUQyNi9SQ2dkaFZqdwowMzNVcG1IQVZuSU42aSsvUDNzdEFOR1VoUnRQMHNUTDBGa1ovM05YTFNZREFiZmdEdElCbVRCdU5lSDc2VEFmCmI3WHNLQmphM3IrSkgyR01ybTVjUGk4RzBZMGVCU0oyUmRIY0ZEK0s4c3luZVNDN3p4T0ZQQmFBKzNsUkp1ekMKRGh5NTRrSXlXSW1WK0JJNHdxbkFzcXgyR3E5WTJMYUZITEFpTGFsWGwwNnpnN0F4NDUxQ0o4bVEyZFVaOEFYbApkeXlEbXlrZkNqRVIxbU5wa1BrSnVobC9KeEFnVHFnM21VM2t1SVJ3bmgxYnphWHJMNy9GOHk0emJHSHdvcmEvCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcElxcjkrd25mVlBCWEF6UGtFM3EKaXE4aUNJb1oxNThwSzVNdXBsVlVXbGNyTUh4QmdpZGtUMWRLbnI1eStGRnNsd05hKzhMbVJ3R1Z3b0RxRHJNNgoxcDNTcWpORFB5dU1wRW1OeDlwdk92MlhBOE96cjVjeTQ2RW1rYmIxMlZBMVpIeFlvTHgxOXpCZC8yVDhoM3NmCnl0OVJCUjJrYVFqcVZlTStlSnpsTVg0RTV5WENKZ0plOThPeUtUZDlWa0s1dTJUbW5PYUM3cFZkaWJ3NnAzbkUKbkhnTFJ2cEpRK2s1a0RyZEJwQnpPL0hIOU1zdUl3dGM4RXNWeWhkc00vMUdCYXBselhrYXJ4Qjl3M0swL0VKNQoxdStmZUUzeTdOZFhIRHRpUmdqS1NiejBLbEwrYm55N2JPNVdxc3Bvbm1pd2JnUzg4QlF2NGRnenBNMkIvdVRRClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWJXcnNSSjlwLzhxRGlkRFRxYnAKd1l3WXJkRzlCK25IWVFQWTVkbUhzZmNCZWlkOUFYbG5QRkVYd1RsdS9EWWMvRHBSakRvTnM0SE9rc081ZWkyVApoMzhzeDlNN3JBNUg0THFYVFR3RVpsaDRYdUkyTFE4WFVXOENSQlVNMlFoR3NkOVVyWldaRExPKzlCTmtJNVpQCm5NalZxb29vb1o4UFcxbHlwVTZxVVdhaDQ5YnQ3dXBVMk1JMk9NaHRIT0xQWEhpMHVzWDRYbTZGUEdwdGlzelMKdVkyREdJQ1lKKysyb2h3ZjIxVi9yNEF6QzB0d3QvU3AxN0pWZEJ1Ti9ra05iN2ZkM21zU25jNVN2eURLRjZkLwo1aVBhYnhpVk94K0dQbVR3MEd5VzUzUFlHOS8vVjJSWVVacFhaTnp6TXNQNDBLakFTSXVCS2VjTkNZT1RXZDk1CkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXpKeXFUWWhiNU01VUF3cWcvMFQKZ0IzWCtXSjJmRWMvSE8vbU1OWlVobkR4RFNNWHFBekQ2Tmx5YnJTNHB3Y2pqSm5GSnViVWE4SGJRaTBndXI2eQpPUzZUQk40bnZ1UlRRK1RCZVRxcHA0RkxrWTg1aU1UbTFWbmFpdFk3SG5QTksrVTRzWlZ1cm5qNzVKcS9LcnlICjY1RzJ1NUpLMmZPR0tVVyttbEtJTU1HS3dFU0s1UlRrY2JtdExoS21yLzdQcmx6b0VVRG9XZGxaNFZqVkFqTnEKeHBtb0hHeDNYeWgvZ1RRR0VkUklsb01IRHFoc2lxR2p1SkE3L1o0N25pRWRkaGQ1WElydExaaDBucjNRYW5jRwpheTRNVXNMZDhLMVFsUU1qZlFTUjRENkVySU8ycHZnYXVOd2I0dzlFRG9RSWd3TnpocEJzbWFzTHNKWHFSYWVJCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckdKSmxlRDJBWXg1Z3VJRmtsWUEKbkJlSi9qdTI2SUlmaEZqZ2Fma0FmVU42REVuYXVmWXBXRlFTa05oQ1I2eHVvVk15QVhpR0xUVG5MVm5VYnBMMQpFVWtIS2JxMmFPUmp4NzA3WUw5L3U0L2laMW12WHJjVGUrUFNSYXZGNWtoWm5ZNTNIaTQzZFJhelRJbVVvY0xCCno5L3pkTEU4RTFkT0hBZ0wyMkNMRWdFNHJ2K09hTmtaUnR6YTBhcTkxNk5JTVJ6NndVQmE4WDg1QmhFRGV1eXEKcndhcENhcEVYNUpid3JYSFBNTDBhdmFyR1RCWG55bHhDNnBpN1lDRU01UHNGY1d6ck5WR0NBbUlFb0g2bGlRcwpwUjNNWDRKbFBSZEVBcnNVNlNpVUxXL0tXZVdSWnZmRUJuUHBSWldMaitBK29QcUYyZ0pFeHIvakwrZXZMMnNaClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFRCQ1ZwRGVMZUFuVHZtTnpOajkKdHZ2QkZseHY3SXVvMVRWK0NWU3dNWTdEQjAxRG5vL0JBYjZneWxGTlRyVFVyVjNiOU16eWFFbGpRNnBxc3UrZgpHOEVIOEsyTG00alVUSDVVRkxhZjlxc0ZsVmRSL0RSNVVEbUhuZi9tK1pVKzBLODNNMFdSbFQ3Mzh4REdBMjhlCmlsYjNsMWZaaGF0WC9hYkxQSGQ0V3VpZHhteGkxQ1JxZFRwOThKdlJ3MTR5MHIvOWZwZ0pxY0wxLzVHWW8yNmIKa1VnZm9aTnc4aXIrNVdiRm1HVVFSb0NmSFF2ZU1kOGREbWRiMW1CS0dPbTR1YmdlWjU5cXQrd2wrd1diQ1pYNAorQzY1aUtPRlp2RUIxYWNVRTErOWl6N3NZRjJoNGZ6QzJDdEs4VEZlWFkvOTFRY0lMSEFvM0Z4aVZCT0dlYVZKClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1JxSHFRMkxPcXF3Ym1YbXM0RjcKVnllK0lqWkp0M0VtVDVOMnUydk9Cc1JFU0xXT1dtVlZSTzB2ZEMrQ05YU2xQTWhxMFZvMHU3YVppb3p2a0FURgpVbXEzK25pSzZhVVZxaWRTcEJTdStNTDNKUm9jdkc1NXV4V1A5YWpVTUVtcjViRTR1VnFpd0dZRW1OVUc3cmpzCjlBTklJZ0F4bXVVWTF3bmhQTmdXVVdhb0VEZU1WTE12Z1psTkZ3WHc1K0daVXM4U281Wko3ejRlVG02d2JQVDUKOWFsekZSbDk3SXJmN05iZ2RUcUZPNEZlV09RRzROYmpKZVNyMjZKYWtqdEdWWHhFbGNWQU82T3RtbDdGQUpHZQpyaHNjYlhCazZHRWc1ZERMQnErVlNjdjdpU0lMNzJsblFLMjNYc0haUGNONnk5ZVk1M2VFRFdzcjVUblVNVmozCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnV3VVZhTFBYWFFCZjJlY1pJYjkKQldtZkNUT29vZGxvai9LRFZrTjlNblgzNmVQWHZhWWQyaHJ1T0J5ZGd6MjNjNjE3dHA2QU5udVU2Kys3OHJHRQpHQ09JWDJuaEVHTEMxdnJMTGh6OWpSNE1oenBmdGFpOGdJNEI4NkRkbzJsK2xwZ0YrZ2x3c0Q5bGtNWGhzbFp4CjJrL2ZMQ25wZW51eEdFdkluUnhmbnVRNHptK0p5Z2MwRVhVdzR5b2lYQUpXL0VxdDY0L1FYZjhNTlp6NHRiSzcKOHlnRTg0LzJqZlE1c3FRaWdpODJ0d0NSTGxyV3BjTEFDd2NWMnBoZlJPQS9iQnJMdlFOR2MwdzA3ZkxmWmd1QwplbVFyQ2RrN2MxSmRHaEJrMzA4eVZuVzRiK2J4TjR2UTFucVY1R21Qb2hZU2FOWnpjeHRLc2Y2cG9XM0lWY0pRCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXVwTVN4aWkrM0F6d2Vic1U2ZkkKNVQwMWg1d1ZhSVpRV1BVQWd3OWp1MEtSS2M0UHJLYVBRRUdhN0lES1cyYUY1UWJRdkFpMlkyekpEekFqaFlrQQp1QkN6cGtsZ2w5NGtQL0dYODRScUx6WE9jR2tEOXRYM0oxdWlMRXVueGw3Y3JJYXMxZ0dpNUtNaUg3NGhmOGtECk9MTkpDTVpERkZNWG1WTGp2b2tHV0ExeWk5Vk5xY0J2cjBjWUJWelpNdjdlY1JCbUJROTJYcFladEJuYStZZTUKZDMybjBqdktRVmpOeWNJR3RPYmdBbzVHajBuMWYzNnhaendZR3JkWDRMZEoyWWxiVGJiQ2FMNTZwOVdJS3ZzSgpGOFFKWDJhSFl1MXRkaTJRMkJ2eGFwaWtrK2VkWHVteDlNVHFpOU4rY0V2UmpIVVVvelErZEw2U1V2V2dYSmFmCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTlhS0ttdnlGSW9aM1hrSGh6M1IKUHdyTGdzRytxRlc0YmxHbHIrZVYzYnlZUnh3R2NxUlZDOHd1a3NibHVXTEhRUHFzZlBQREwrOFJEMHNuRVM5UwpLNXRhUUpOZUVOZklERnhTYmNpWVBucnJFdzRIZ0UwK0N5QUM1RjdJM2o1azhqZWtxZjVJdUc3V1NiTzdVT3BCCkFSVzBoTHpLb2g0WnpTc0VDcitBNlVOa2JLYW1ieUxxYlNIdUlJcklEMzFBK29ScTlMWkx2RjJvaHRwQUlWUS8Kb1ZvbUlId1hHOHJjbC92U01ueStHTnl5cU0xQ1NISGNQdENCbnZhZms5OEJZZmswU2RKNnZNZjZLVHJPOXdCUgpERjFkc0tWTWVjeG43M2dIcWY5NmtMTVNJSFMwSHVWb2s1S0gwMXBZL0ZYYmFlQkxEd08rL0pGYlZ2Uis2enAvCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDY3OGNnSGxnVlNGdFIwVzFrb3IKNGFFck1Nb3F0OVpWVVJjWU5aMkIzTC9aamNtZkNZdHlvR3JRQzNuMEJEdFk5WC96QkVDVnJjMTZneEJmTzJZYgppN0xoUXVnOVlySzJkcWRXcXp6WFNaR1kwRlI4R21VbWpyWGRLQ1libUUybUYxY2p5MmFxUkJIVC85RlpiR3d2CmVVMEd4R1FyaUQ3SlZWVG5McVlDaWdVK2kzcS9sWlc5UkdmaEFTMUxkZUl4dUhvc2JWVUhIU2gwMjNSYk4yZzkKWndoNHRaSnVkRkhDd29UbEF6UnhZbkdOSGxkNndTbHMyaEo1enRHaCs4elk5VTlvN0ZYeU9zR3NtU0w3VTNOTQorc1VkWUhicC9NemowN1hKbDUwUzViMXJYVDQzSTVML0lDZDRlQ29KRVMwaXlsYnJ1SVNFVGIwb0JYVGNmRWxMCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFFYSmRSVVpRQ0FTZFVXeUpEMnAKbkxyMTJkZnY1TTFpVTdOZFdLaytIYzhzU3dQNGxValI4eGFTVU1lTW9abTgvOHhHeHJKS0psK2s5RXBpVGptRApzY21KeWVIclJCbVdNcjFRNS9XcDIzQWkrQ3pFQ29lTm1DN2ZBaVQxbk5NR3VJa1pOZlU4YnNkSUVyYW9iOEUzCjlMSGtzV09PWW5CaGRaMXlWSG5OZ1RCWHFPeFNGZU9MMElwNGVyQVdGTEgzZHQ4YWxLUmVtRE5KTUNYazAyRk8KK3VSTkV1YUYxaFdVMzNqM1o2YkdiWG1LUGpDWjlLTW1IbHhEVXJlc2NadHR1b1BkZWQ4cCtBbW1jcEVqT1VRWApqQTZqQmJTK2YzRnEyRWNiMlFqSk04U2VGMTJrZ052ZFVjK29URTBlZU9JS1NVL0VMUWpuVHE1cXl1TVBjRDRwCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkxxNU02SHpiN0lMVjVaMXN5aGwKOGpBZXN0TDdzQWxsR3NSdVdxM0hWV0o0dDFYcjFRUmplQzlNTUE5QXhGKzJOUE10SzU5UCtycVRLMUN5aDB5QwplSzlKQlhzZ2RKT3ZmUDYwdldaZHNkSjBPekphZmJLSXdsZjkvN1cxTVliano1VmFCVlZvNXJLRGQ1ZEtNa0tRCkZRbExXN2psdVE4K1UwYUMwOGdlamloL3FWYWRzTmRpWXhUbkNrYjZoNWtSVW1JNnhuYmVLOWtuN3Jta0pPOGsKSm82My9tYjc4ZVk4T0NnUWp6VGZWYnZIYzl6MzNWWm9yVUFidU9ZOFBoTGtIaDhocDk2QnhNMVpXMXR1bCsragp2eTFPQ0Y5Yk9OZDMxT2xnL1RxTFpmOVRZbE1NUWJWcmNaME1LY1pOcEovWHpRNWlPb1lqYnVDTUZ1OHQ1djUwCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXVFOU43LzY3Ly9vZ2xyUG9uc0kKUHQvMDlsM0x1Y1c4Rmx5OEtZL3FQOWQ2Yk1zeXpyUW5KMlU4TUFZQU94bUZGZDdUb2pnM056Q2YrNklkM2duTAp2NzZSanpwMDl1YjlKUzJZZlhQdnQvT0tUYzRTZ0tzeFdtUlQzaGhYVjkrYUdzbG1YWlorWnVWQVc0MnJWdTZBCjl3cUJOMVVWMU0yWG94dEtGalRBU2lrSEZCYmZ6ZmcyN2xwbi9ITG5oMCt5ZkJFN1FVOFVjRjZiWTZUb1hPMkMKNHJrUnhyYmtITkk3R2FiVmZuR1M4NTl1TS92K3hRczMrTTZmcGhnalV2SDI3WmNCdUlrNzdCdXdqNDNpSVdibApSMUNaZlJjTGp6T2JuS0ZreWtyRElMUC9ycGZEVzFoTkY4S2cyVWtOVnljN2ZoNEJVR0dpQkJySnpjb1VNTzlXCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNm1ua0xWWE5HUXYwV0wzTFp5YUYKUEF3Y3NMc3BjU0JDaDdiNmF4b0twUE5OS2syVEhwSmxzL3RtQkM0RWhsWmJvV0UvNXRWNkM2OHMvZTcyTUJQZwo0aGlSTjVZYVA0SGdMbk5YaEw0MzVHZFcxTXh4aGlYS0JvbThvc3JiTjBDaFJRRFlMbjZCQXdNZ2xKcXhhYVdsCjhqaThybzhzMHViTUdKRHBlMDFwUVorR3ZycWl4N2ZSUU4wbjZuc3BNY1dwSElqNWNyMmN5TmhzNWM5dWZsczAKMGtGYlAwaGhGWjdlb3hHZFBtZ0wrVU45ejBGOWR0NVN1Yjg5RktBNktWUEFKRXNRUXZXZ09ZY1NwMlgyTU9POQpRcWQ5aFNjYUp5SHVoK3NRcTBwREdvZ0RBTmtEWEFRUlMzWm9NWHM2WmRkMUltTG1TWjBqbVMxcFBBMk1aYk4vCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOE5VaHZtL1dzWHpKUktXN2tpZXEKckhROENuVmdkTDlrWmFMYldRWGFmNEEvbm1oRTZGS3pLbjU5NmRpejQyaHJuK0VNN25zQ1RpOVNkaFBPWENOYgpRUTA2WjVteHdFcG85UFZad3QvU0VyUDFXWGNuODFBNmVPZkFHWWZRQXNyQ3B3Vk9EZEIvMFpCZDJUaEE0d2toCk4wYUVmck5mWW00OFg4cVNUUDVrUUZ0WHpza2pIRXB1cUw1N2ZuVjNoUVVjUFdOVnk3UWJwV08yamM0ZkN1Tm0KdTF0Z2xFSlJtMnMxeURKOVJzcVlFQlVCRXZwQVV0N1NNbUt3K1QxTFBmN0I4YmMvTmpwRkpWUk01RzFVZDVCdApKeU5LbERpZWV4alVTdzZMSWlaMTBzc25hYlZWRHBFQitGMHBBM2M5RFlmRHpTanV0MzBIYnhST1NUcU5ab1doClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFRWbUV0cGVYdzc0a3ZDZmlXQTYKc0o3SDVUbnU5Qkg1cnVBRmRNNStURmFCQUNYdnZLZ0REUWhsamFmUnpWeG85dmFRc2wyeVQxWDQzRGl1eEtqMApZMTJITVlLYUVmYzd6Vnp4N2JncDl1UTBydWVtQlphaCtKT1BBRmxkVGRPMGRzUUYybXZsR2tlYlJkQ0p4WXB4CmxpdE95Q0dDa200eHBxZE14N1EyUmc5T29LWU9MWU9nckFrNkxTZkFpRjQwU2tnWncyQnkvQzNYQi9XQjZYSXgKREdqcWZBZUxId1BCdXArQTdVTFcrVFRNbUpwdit4OHQ4SVlUN2NFcXZ4eHI3Rm10RVoxUnpYZ0FNMUVST0ZDSgo0TnVrZUw5Rm1mSlRsVDVlMHVzVXZGY2FXQTNGbzBOOElLTDl5c0NSQVluWDNtci9tUmJEU0RsdmVaZXBrcFB5Ck1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3BOY0VzMm10Z3ErUWwwNllFR08KSlRNcEdxdzhycnd4M1o3Ulc4OWxIanVHYnNrVmtDdWxtL1hHTm9tRVJBUVd4bTVzazBTMDN4SlVramNERUx2RAovQkZtT1Y0eXUyN0x5QkNER1E4MzIrbXhVK3ZmOEJNTnlQODZkTEpIK2pEd0h5aEpUdktWaFBOUk9ZNHZwMHdEClA2VWYrNzNadUt1czhhTUxxejhZeUxNZUpKait1MG9IdW03Zy8zVjJsTnRCMXEveCsyeit6VUVOYysyc2NDdlcKa3p6WlpVN2JQME5jUXhlZXRwN01lNGw3aStDWEl1Zi9RWDBQWGczbCtjaUpsTjlJNm51RGFndG9qNFBSV0JLRQp5Q0YvR1NUUHNYVDhVajJSMkxRVWM0cGlHMjRERGp5SlBmKy8xQXVtSURjZG1kS0pRNE9Zd0pxcGxLbWZGWnQwCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdldzR2VuSnVFQXdOTnBHVzlISDIKMmkrWWtxM3NFS0pxeTFjaU15Y01GWGZGUVdEMUN2em9aODFtNGpRUHY1alhiRlp6VDNUckJHajdvQ3ZZV01jVgo1QnNmNW9xeXhzSUJ3aGJYVXc5TkNwZ0lCUnQ4WmQzeXBibitacTdMYlkxTSsyc1AwbjkwaTZNWHJDbzljRU1XCnRyWk9PWnlKbWZLY1JKcG9ZNS94WEp5NkJ2MmFPMWQvdnp3ZjZMdGxCeDdWbVFHV2IyUURxVFFJS055Nmp0MDkKWGQxK05JSlBTeWpFeE5tQThXTWV5OE1ma1pHR2h6NmQrY1UzeEZicWF2RXYrd3JaK3R2YWJiLzVFeExxV2k1egp4MlF3QmVyWTJNcHh2bXFxWGl0U1EwVzZ4bXRjeTJXenJRdFZlYUlCWnhXS0MwYndXc0xmU2xTNTF6aEpNNFhkCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDljZkVMcHl0ZUlwTDdHUnQ2alMKVUJud2llTnZrS21qd2tKd0xCV2ZpRC9saXhBc1FWTzN6czdFZEZzNFNFM0hJMnpaQTJobmxXaFkxcFRITUVkZwoyaUVjZ3FjL3hORGlxdlN3VTVmMklIM2REazFrOGVuQzdNN3VqcmVhVG90K2tYNVR2Q3RTazh3aXJCNHpQTytRCllwTlRObXF5TGZPMHo4RlAvejVwZGl1RWdkRHdHUVgxWlBHZ0dnamE5MW1JQjcwTjZ0S2pOTnZVSEFMOWxybXEKcDhreDBPbndmWitvWEwwSm1qQ1pRbEs1SHJoTHFHbEFOM2cxLzlvSTVkUEhQbDhwUWtsZTBOMVdlNjUxYVZIRwpQalFMM21pUDRBNlh5S3A0QTBHeFdHd1FzSjM2OWEwcDFhb0taQ1d5a09YT3MzN1lMUXRlSkFKdlBZQnNxRE5pCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3E0VEhTRmc5ZWdhZ3hhdkE0bnIKQnU2SmJMQVRwTGRpbmxEVUVNMEMwWWErd2x0UHVaMTl1SGh5c1ZJMlZuY0IyeERIczlvanA0NUFEMlowTGYzQwplVjBqcU52a2NUU1c1R09USFJNQWNpNURjV3MweW54YlRxK0NrcTFrUThhRXZzRG92WHNTWHh4SUZETk0xdXN1CmlLMkhVUFFBUjFrZG1XN3JGbFFkdHd3N0FEMTJkZ01sTEtlQThnL1diUnlQWUdKRzYyakFBdDdoTkpTUlNVSUcKa1YzeXRyWklHUE1TMGVIRzdZWlRCTWpYcjhJQ2hPY09IdnRsRXBuS1ArZkJuUW1zVytZRm9teDdDSWp0S1dxLwp2b1NDODRGbmYvcXFwRmUzNFhjMzJjYmRkMUFNUFhWbVFqbEhWMEFaNlpoMnVGV25RMVZwU3ltbHhxUGkrK1o2Ckp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjFYSUlsMklaMUxhNGxrWHhHRXAKamJaY3dvS2tnNytpOFFscmlVNW9tdi9XVnVPdXBTSktIVWVzR25jN1FVUnlDNXhRNFdsYm9EUFhBRmIycy9FTgpBTWhsaVV0VUtaTGtvV3dQTXFKNVY1ZXZuTHIrUHRXR3h1dFNoL25zKzVVUjJnblpxMk1jQ29WN2lmc3hMOGJ1Clordm1KczVVUXBPVW1TN0xMK3FvUHVWcjc0ZDNta2NyT0pSZHIxUk5CTnVKYzFJdmdzSm44TGZ0V1MreFBYdVMKSE1mZ0laT3R2YzZlYUJ3TlJaMG12aGlsa0Rpc1ZtMVF6ZTRJZkF1TFdoL1NRNWYzRkpFNGRJdm9sM000eGpBTgozUWxJbXRLdEZOWDhsNkdveStYWjI5NVJOQnBjV0JOWmpjZVR4VEFXNUVNL2hOQTNCYVJiVnVQKzRPRXNTT3hRCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbk9pMk9WWnMvY3lRek9wMy9zYVUKVm1sdStFZkkyOFBOa3pIVG56YzdoY1NRQ052cDBwVU5HczhuU3Yyd2tUeTVWcUc5MUU2dnZUdFRsQjNjRUhpZAoyRzFhSVM1amtEVm05M0Y4Qnp5RFVldFB4VWhrcGN3K1NTYlBrdlJ5SWtLZ2VQT2p2VVJKSlI3TUpuQTlKSnFrCnNsSnQ3QTRTVEVHMkdWYnIwaGVaK3ozTnNyUzdnYWVOTUh5M2FzK0N1d3VtbDZiU0VmZ3lhN09qeVA4cktjRGkKN3NVTWlQNWhrOUNKYXVvb2VkRVJlRnU1MVlFbmVKbnliQjVra1BhNUZwdG4rSWFBN0orVTBMKzF3d3QxNlExMgpNTjQzYndpZGpLT2ptRTNlMU44VExzOXAxNE5QdGE4MnVMaUpxWjRBR2RQMVc2V1JIMVFiOUZtM0JjTXFFMGJsCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTZJbm1tL1VlS3piTFV2VW1TdjUKS0VuZWJUOFN4c3hxUnBlZFZWd2lwZEVUd0oxMkEweE9YV2MxMTBZbVRIQTdGUW5oVnhOZFRmZ1dsUnRaWlA4VwpBTStLYmduMlpxaWZNY3V0eHlDWmxROXVxMFkrSUFuVTRoYW11V1A4TjlZZGV5V1NPRzI4d2s4T2g1aXVNQkpUCkIzdHBLamF5b3NEWU8zTmFEVzJVbjdqUW1rdEdlZ1pSSFFMdGF6STRqWHY1eWVyYlJCZmdiYXpkMEltb0RrSmoKdGFxcUZBR0U1QmV0VnJqMHFidVE4dDhEOC9zdmhwaitiL1dmcTQ1MmJOSzNaWkFmT3J1OG9DcmFLeDBta1Y1QgptSCsvV1BBMHEzQXM1SThNemdMc202Z0lyb2NxMlhKRjI3eXFqaitkY1psRFQzSlZ1cVY3aUJjRkg4UVpsdnhjCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMysrakZvNm81cFlLZit3NFlyeS8KVSt3THlHd21pYUdRcTU2NE5sVDJEM0hab0U0b3JNY2VDRFFsTHppVEYxT1JXS0pROXZ4blFjUlF3aXJWL3RWWgp2aWpsajVVT2NVa3E4ejJNWVdKeGIxTGdBRHNYVGo1WisxZlphMzRNMkQwa21NUTE3NVdRTmNmNGM4UHRVTndGClpwM0Y1U1VOVTdJM2V3RnFJaTN2NUlFVTMxdHJ6TkNyeUlERHFUekZuQWJsSWRXQ2YzcjBoSFlMdFluVkRqcGsKemU1cE1uSmZSN2FpMnBOQjVJclQxNHQvSFh4SVJMYWtNMEttd2hmS0ZjNjZRdzJ4TGMzZFlXcjlDMWpSY3NMUwpTZlIwR3c3TlJRT3UyZ2dnMXpmbnVBNXhQNTlMMDYzVXNtUkg5bGUyMFhkcTM1UDFFZk9XOGQxckdQdFZLemcvClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBci9yc0JHczRyNnR5VVhkVVFibm0KOG1mazZ6czNrZ0gzczdzTnNxdDBqbzRYS1BIQU9lY2JaMFVPYXV0L3FBMXd4bWt2Q1JhNWxJdjhyWE9lMlRZRQpOa2tNVFdsOGUwdmhVZGYzOVB3MW84a2Z1VFlEU3hXODNaU1dIVWRIQ2VndHhrV2pXK1A5OEhyTW4vWGV4R2NhCmF2eUhhczYwckZKc28zRHVkSHBIZ28wODNOand4K2EzYjBkUXNIMHZTdGlQVXhzRFFpM0lQaUJZUkNvNlZPeFIKek5ZN09GUUtoK3RKNTdVK0lVLzBnRFo4aGI5OE02SHQyeElXa0lVVjhFeGZmY2NIS0ZsNDJsVW1qK1hwb0ljRApoejRaV3QyS3pOUmdCMG03NzRXK0tGNkoyK2oySlNKaUR4djVpQnBrSWhGY1ZOazhGdWhNUUI5UUp1OUVRTlcxCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVpzUTByNzFXN2RTZXBPalBNdm8KZ2liTDRDU2J4RlpTQVdkZ2FsWWJDWlV0VjBuaEsvSEZlMHNhQVNVWTZ5cjRISjU4WSs2M1R4YzVseGFzZEcrcQpodzJGWVY2Z2VRU0FROEFaZmhWb3I0eVFuS1lWY21DN1NGYVBONU5QYjVtM0hRaGwvMDEvcEFzOU1jT21YRXJXCmNWdnZIKzBkRW5OV2xGbTFlRy9ySG1pZXhuU09vRnQ2QmF4TVgzZTJ4Y25zMGlHVDI0a2pUYWI1dzJkcTcyMGQKaXNWRm4yM0RuNkVISUp3TDA1dUo2Tmt5SWpxUTBOTlBManhReFN6cXJNWGpGaGlLL2xiVnpjOTZLdFcvSUVSRgpoaUZwbUR5eUcxMWpoMWoyNnFWU2JBNm0wQzQrb1F6KzlzUWdzWlpLOVdOTDhYRmlMazZPc0JZRVpLMm5rMjJaCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTBsSVY5OW9XRExqYmtqbkx5VHkKd3VFUHVJYVg5K3VZa21ucG1sVWdDM1NwaUVMdGVrTlp6ZVYxMVBuQ0U5TUcyR1BZNTNjN1kyTHQreE5CVWQ2ZApZcG1PMThWUFY1WFBxcXAyU005b080OCtqd2FkaUtqRjRFcGhvU1gzaXJTQW9wdXY5a3NIUnNtb08rMFdaNkNxCm5IYm5PZm1tS1h0a1NzZ1FpKytVVjY0bnJtQlFkcHZDVkpkR0dEL0UzZmJPUlZXZktKTFRUblp1UWxzZjZmNHIKZjNZOXV1cUNtME42aGppckkvekFOdVBUc0JHMDFBYkZqRmNjTXNLVjlIZ0VhWGZLdGRISWJiOUdsb3dQUUI5SQptbmVrODhSQkVEZnl6YWt5TjVDNmlvSTcxaEVueVhqQ0t2UnNnVk9JeXRNOElJQjEvelcrZ3FHQjRHc2RQbjRyCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVlZdXkxYndTSUYzL1NLeSt5OVkKbitRRUNQNEljaGUvWjJhZmlxNlQvQ3NhOXhxcTNpckwvTTVlNlBoNkNyaG1zWEhZSjR6V2tFLzM5YnhiNGZtZAorMjFvMTJQS3FNSnRhUnBBTmZZajJvaXhpZmRoemJ0RTdyYzcrMitPSFNITkJlWWtoOTlmeGlsL3d0UkczOGRYCk1TdEpHejJ1SEd6MWE3V3g0WE1Ib1UxWExmUURHbmZPdU1GUGZ3SXZxSHdsdGYvU1V6N3Bid3RwRmxxeGpkNGIKVkVaYkZsSkwzdndmMEJmZWVHQkxhbCs4Z3ExbHZoSWE5Z3V0UGRDclNGZi8zVlljNUpaM2xQT1NZUjk1V3k0TApkYnVVRzdXZnlkTldPM2dYV2JReFBGODdiNXVrTWRZYTFwMk02aTVrZXFFbTZjR1doMnJOQ2NXT2tVQ05WOElECjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUF1bjlSRXUwcklxZHpINEszQkoKUE11TXBGZ1VEQTBQcE9hTE1tRWQvTFJpbHE0OFpRR0tLa0tjREttMEgyMU45cjcrWXNtMEhQNEJKNVIxYjZGMwpkZ2lUay9aaGE4OUc2YVlhNFl4VHJ1VXZlZXRyc1E0UlJFVDBEU1pJUDJrU0h5MTRJQldwYWNsYUx5OWNCcTVsCmdVay9QcjFOOVBjNWo5VEFLcWIwci85c2Z0S3hWblN2TGMvSi9CVk5vOEdINnQ4UmhJTWcxU0VEbWY1QURzZmMKOVVpd1ZIZnkrZUVkR2xyMEQ5L2h4cCsxbThzaVhYcVJ5b1RHa240UkVxOC92cEFrRGZkeGcyalM0ZURnd0h2VgpCOVQ5K1dxL3JVOUg1ZjUrK0UrNEIza0hlUU04bCtEbHFrN1FnN2VXa1F0N2tvWEdXZjEzQnlOT3crazh2ZEE4ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnVXY3RyZGZ5RGFCcm0yclh5eXoKeXFER21Cays1WDdPakIwb05Ud3ZlYy9vY0FWNUpZM29wVERFL3RGUmREM1RpRlZleGVQSUJ5VG50d21kS2I2bApvaVM0M3RwdGhvSnptV0VQOFNhaTBsNHBYd2tFdDZoRmxPUzA3NzhIbEFUVXFhU25Ta2lSUUs4VTVBRU1nYUJDClFZT3Vpd1JlWm5YbVh4b0NXV0VtejVpdk4vdW9OVUV2M3AwbDJVNTZnMGZNVGJ1ZHY5WGR0TWxycGdWY3RaMkYKZkdndG44RXNtdWRwT0o1L1NDTzUwUXlqdzN5K2pIYXBpQVM3VUpGc3VrWTRNbFdpbjdiZ0FwcmFWQU5EdkdBNApUMkVIM3JOMlJZMm43Z0F3K21pc1BKaG1XWjg1b21ISEgycWg5NlJNL1l1UDU3aWJKSEtxcnBUajZaMU9adE9uCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN05mdHB0YTBwUzFMb3VBMFczTGgKTWI4cG8wSVp3c3haT1pTMEE5SjgwZmpnSUl3ZCtIWnI2U3JnOVN6WjZKV2RyTGIvNjl0UDBzRStoSGR6aHhjZgpvS2oxekVhZURheW9wV2FjVGsySUpGOVAxRGJQMEtRdVdSeGE1WHZvdjIxNXA0T3k4Y1htMmdPamxMUHI4eEJUCmpsVDVRd1c1c2VWd3dXRDNaeHY2N3R6OEZUY0ZNOTFxMDU0N29NSGZYRXdWcVFMZ2NoRVhEWWxMd0ltWjUvN1cKTDhxV1p6b09PSnIwdFNabkdaYTRXYnVwR3JIZ3ZhM1ErZnBLZUpPQTE3VDJVeU1JQXJVVkVpSnZtRG01SVdVKwo5K3JySnN5WDRzci96QXJiazBROVhlY1VaWUlmSWFXUnV1RmRJV3VLb0hvNTNTRUJKalAvS2FTN1Z5cW1UdStzClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc05lS21MOGcyZmpLaVMzTXJwcWoKMEluZEJrR1dOTzNDV0U3c1hLQTF6VGZjbFRDZGN0bTZpbXpFQjI5QnRUWWFaOU91a2gwQWJLVnF1QnZtR0R2cQpNQnJUNElreW5jL2owL0M4VHNzYzN6dlBLRURzcm9DMm9JbUZQcC9TeUx5L0N6WTJ5ekJ4ekVWMThzOE5UaGdxCkExTUo5Q0ZvdzhvNWNqNTJPeEx2MGFTTVNkdjVrTEtSMm14b1pGZ1RYSUs5RlRhZXBkS2FvZ2ppTHZhQVh5OGUKYlRZRHVaSHJaUGs0MDBSVzhtT01rVWFFdXpWZ3NxRklRa2xyNmdvSXdxaGRYcHBBUFJoSlNFT0ljQllIRExDbgpRMzZkaXJhclRwYzUvbE5qdGUzS0RZZWlLcnQySWZDajBsT1o4aGFDR28vTUNiUnhRcXNrOCtvdm5mc3hkWko4Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUdNZ2I4bWJOUkRiYW1qbm9UNWgKYnFvb3ZaSDhDc1V0OFJZRm9aVVdRK2MvczlSWDlZY1NQa2gybTc3VUVvL21CQ2J1MVJPVVZWUTZBQnJGQmN5MApnbE1DN3hJaHRUWVFWSHNXY1VLUm93S0ZsQ1FOK2Q0bG8wZm44dzFMbVorTkhGamRBbk1ZVmFnQVhNb2ROSTlsCllkUW9idkdPdzNSMFE0UlJTVkdLcEdSU2RvbzlpNFRDaEkxRmJLUSsyNTZ3M3BsYmFuNHFPdHFabjFDdUdoOWEKQlRXNzZPTW53LzVWS1BmMjVrV0NzZzN6emxkTWxWME9iaXZ2dFZTMENLMXBlWXlQNUFwZTBjamdjTzRDbUExUgp2T1pMdktMRFcyK3hjV3pCWGhhcmpoeitrbHJ6UHNnclRVQVA1aGpWZTU1d0NUMGVXT2lzb3lNVGM2V1o1SFdGClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXdKaHRKc281TU1IWTlDaDNrbUYKRlJoZ1lIY1NxNmNVTWJxcmt6ZkQrVlcwRlBtdDM3UUlnUFdiMWNjbkFGdXQzUlRSMERPUGtCNUprRlFYY0pJWAplOXF6VStzdFY3a3BFQzN0ajRTdmNsN0g3bjYrUENBSVgxZ0NTeW5ETC9YY0VYbWkyUkt2SXd3ckM4ZTVldzhYCmZHNXRxbStkS1dVVW5PMVZ4amFqRWNUcVdITDVFZW9YS0lxL1VFYjhzUWtUbVNyRDRZc1AvZnI1L2dVOXhURXQKZS9CU1p5aWViZnlIU1JlODF6aFJMQVhaMXlFMXM4S2RmYWJTTUZNU29MdzJXZnNBTlhGM2pjN1dpOWRuMlE0SQphUHlULzdZNzFoNWhlUzBjdGRsSE9tdWYvSWFDZmJJRllyUktzOExKTXRtSG5SZ2RUa0JyUThUUXFQWDR2VnJuCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3Z6T3A1cnBlQTVRTTI1N3pCc0kKWSt2ZFVleTYxTS9oRyt1UjRZWjFRWE1tZzdGMm1vSmY2ZkhjRFFXaTE4S2UwOHduQS9BZE4vekVsUVNFeXVNVwp0SjVkc3FPUkdyd0xDeEg0K3RBdlVpYVZTNU5BWmJKVnRoRnZOaFdJbmpZbUhpQjNmc3BHalVLSDZ2ZXBQU0JECmpVa0prT0J2ODZMWWhXT05BZlRuT1N5V0FnWVZ4Q3U2Zjg0b3VuZjVuZ042aGxWK2l2WUMzdWxKd0NJeVRqRk0KOHExejQ5REd6WWc2NFNieHlCNEpIY3NPeXhyU2hodk1mb092ak55TGNMK2trTjBmd3dHSDQ3djdUZUExbjJmaApPZERKd0dsdzEzblR5V2lTQkpudm5jdFdUWjBPTGw3cjRiTjhBZlo5YzI5anRBSmo1WlYxZWw2RU1oR0d4N2ViCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG54Rkt5L01FTUh6TkVSZ0VRYTQKa0FpUTIzVEdVbjFMYVlrNFpLYzQrN0M1dlJVRXpGR0pUdC9zNXFsWlRGZFBhV2g1dXlnQ2p1K1VHOHl4MlpFTwo3YjNiejRZajBRS1NSUTU3RWJFalhvVUlKckxqOHhUODNTTStNNEY5S1RFU0xVdVVNbmtIcm1GdzZzaGp1REhLCnR6UTBPbTl5M3VCeVVueXM2NXFXalZRcW5UWEdnSE9mZndYWTBtSE90cGdRWjM2cXB1OG54SjZ5bFdSZ0pxT24KUWdDRjlFR0dxVGhKMUJQc1JyOE1VQ2gwb0VSQ2RYUUlIWWVJNDFESXdSYXUyYTdwajM2THJFcFAzUEYzL3VuLwpuaGtDdXk5Kyt3REZ4K2dyUXp2emFuY0dDcFp4V29nVkljY2Z1TXRyZ0RnMmZ3YUZUQ3JHa0VQSGpEeWtSY21sCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBL1VnWXMxVnJXK2I5cHlQeTM1REkKaUFJc3h6K0M2Um5RRStJR0lMN1VtZ2t4QTZuc0RjLzZUWnRic0VSWXRFcEpBd3BXc2tPc2tSdjlvOGluRG5ZQgpuTXdxL25pL08wdUxSZ1cvcmpRc3RLc1hKL0lUSUoxbWM2cTFRd2JBekI5Q0ZXZFRyNlJRclkrM3JJS29HL0ZYCm5YUWhlK0dsSnF6VnNubDR6UU9wWnA3S2ROd0tFNWdqK1k4dUNybU5XVllhWGNaL1U4N3hwSUZkUXY1dTJ5YmMKQ09kTXlZREYzTXgvWlp3WW1XS2J6a0E2Qm93VnlBL0d0ZTRqK29HNnFpVVNTQk9peEhpOUEvRGtNN3drNFlvRgpOeTFHQ2kyVGlYYjQzTk1MYWt2MkJ4REg0U25qME5lY05EMmdGaHd5eHhrSUdpajUvakFwczlETGZjUWEybDBZCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2ZLU1BhLzBrKzduM1J3c2kvYWkKNHFBdksxblV2djlsTG8zS2JJVk1JRFlZdjFhSVdpTFBQUzVVNDM0U3VPK0VaOFIzMnR2eXRvZitkWW1YeTJFMgpHcXg2U0FVYlZuZnV5OHB0SjhBcEcxZnBCcHpCcERwc0NDNEJHNUYzYTFybk9lZ1EwdjlIaCtnT2tBV2RCZ2owCkpQdGNucS92SWVPcmFqajVFbXlDcmQreWVLRVBYemtMUktndHBWeEtEWk0wZWlzZE1yOUNiYlJpeFRyOEpKdGMKeUZwVUFmdDVVR3g5bnpNaTlsSCtFQ2pxOFNlQmhHdnY1ZUliV1VVeElvS3ZoYTMvQXdJTGJ5dVdOdktONnFEQgpyQVVoRjRpWHVHV09CYnlIdnB1ZUV2YlZiT2x4TG8rdjVzQWtDNGRmRWozNEN4K3dYRGhpWTk2clVPZ0RESDJMCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemtGc1NVc0U3THJqNDhQTzJBRXMKdm1CZ1BaVktxT3RZVHVFeDhreXpDNUpIcXFqVGFEWTl6aVlMSXkxYnJYTTJaNm1YN1h5RGNWU0F6ZVFjSFBPSgpZVzVpcmV2b2xWb1g0bFIzWCsrcndPRTNLYm12Vm5YODBLVXA0K0dOc1RsUkV3b2IzanZQM0xZbWRvVW1KdFhGCmFZWmxUSGcvVWZHa2pOekF6Z3FiN3BsUkhSV2poL0prSkhiaUl6K25qa2oyVGlGMmhPeGJ6bTVJM0NLbW9ZS2cKMnJGQURBVGs0MXVHUklkMDFMNEc3WUxDU0w5VFRVL1hiS2xscUtKeWh5eUhFZ2YveUZiZVZ6QnFuZm5lZTVVOApkNXZwdTZoRDI5K3VsUmZNRFR6V3YxamxCYkV0YmFmd0NsakFNY0I3dUM4ZldNRVlNV05qb3ppcFY4dW1ON3dhClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmRNbDlrWnpNUXcyaHRKUTJkTDMKUUxjejNEajliUFhQQmhSaXpYdnNKU0lpdEkrT2JoY2Q4bUtmZE1ZbW9HN3dEbG55ak15UTJuUzFVNCtqN3g2bwoycE5zd2RodXQrWGNTYlEzNURUYXNrVzhsT2pDTUlXVHpLa2t3QWpsTXJ2c2p3WXNwWWg5OCtWcWl1ZURhWGVNCm1aQzdUTkgyYkVva1dhcXdDbmEzR2Vjb1pEUTEvbXozNU5jNENWaEh6cXpKOUFDTU10aEJjcE85cUFxZkdKT0EKeTJlTWF5NUJaVGUxRC9LMENTRDY0bUMyY2t4NW5SNTI0d1RNRFdCRC9Cd1lUczRKNURDcVRHRXRQcmZkanN1dQpUTkxoTTJXNVZpNmgrazE3QW1pQk52dzJhaGRoTHM5L3cyZVdESEJkSkFIempuNzNYNW1wMkMza202aW45S3dqCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVBwZERRMjgwQXREL3Q1LzRWU3YKOVRGbEhNU1JvalkyOWFlN042a09YUXZjejhNOFVoNUhDKzg5cXRjTzU5c2paWVJGT1h1MWNtREpOb0IveHdVOQp2Ym9PdlcyR0hJTEJBTzIwVFEvODMrVVQvSnNjdGV0dFRCeHhnSS9tWlRFWUF3d2ovS3hFWlRucEx4K0sxOHpxCnI4dTZwa2R1YlFid2s3Sy93TmNJL21hdDdJZkkxbExLQTdrbXpxMGhZQXhiZnpsZTNoQlhqUTZTMUpVTUxmZ2gKYldoYmpVVFRWckhiYWxEcGg0SDVDeDhVZG9ORWlpWkswc1BVZGFQNitkNkprOVRVK2h0U25wS1Mrd0VwS1RHUApTdUYvWEJ3QzI2WUxzeVp6RWdTeXlDMlYvS2x2VXhsUWxTTW80YlNSTWhWSVkvVVkvekVxWi9jbU04d2FrcDdhCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0lUL3FqeER0VVIyRG9UVE9HclEKY08rend5VWlZZ1dWVndnRmFJSURaaTNFRDMvdnh2b0YxOThzSzRvZVB6OFpNSCtpbm1HL0p4YThIVXVxeVNZSQpDK1AyOFQrWUlYd3IwalY0bThqSmJCV0lSR2VpY1lKcnRjMDRZbHlKcXVtRitHa3RhRS9WWXZkdDdaYjFHWHZoCmh0Q3FWbFZVUTBVU0k1SWJlNlVWT2hOTXRDUDdHLy90K0hTWEVGSldPY05xMGZWMXpsMi83ZldDSkJUaFQ0WWkKNEdJclM4T21Zd2k3VFBZYWdIRFZCZit2Y1dmSUhKNHVaYXhqSEtONnFRdVVOdFRra1hYbU9qb1JySGRCbEtvWQpVVkVYNWJtTlBmQ2ZyTy85OEluZ1dvL0FuOWduUFlHcGVycm5McnROMG1pbnNCdFREME5GeDk4OXZKQ2NvaG03CnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWZxclBjK2Y5N3pCNXNKOGE2ZFQKQUt1ZzVtZXlPNFl6MndLM0JxUGVWbUtkMDdhZ3l6OGhkb2pBeURObEhDWi9QeXNLMWhtTjN3RmE1anN2d1ppWQpTdDBKbzNJczFNMElYaDVhclhGdzY4SFRaNFdiWnUwUUE1Uzc0c3VSa3lFZzVGdnUxSHZJRWJ5MlRPdm0vTUNlCmFDQlJQem1nSWh6bjBNVkU0SmkxU3JvK2lZdkhDVVpHSFQrSkFJTFAwM1JhMFRydGF6bmU4M2M3a045SGpSbU8KdHNOWXRteklQK2FvUTRxa2lCdERRR2RRWW05Vy9zYW5WNzhIbXFGV1piOWtKeDdUakMrUDI1NzFaampRbHA3OApYRi84SUlPWGlMQnBZV1I3SGNVampDM3hWVDFKYWN4UzlKczJEU1FIUWlEMWRMS1ZYVDZtbUtnbktTdkhtYzlGCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkRWdlRwbzZ2UjdENjZYazFHaDUKNGxVS3Ftdkt5Q3Y3SldIYUZKc1dVaEFjNkpmRXJweXBuRHl3WXlsZzk2N1hxWVdPYVE3N0Y3NFkzNW9DUFRhQQo2UWxJdXk1ZkVpeVhQaHhSbWhhM3NUTkpKUVJBdFo2RmNtK3N0T0lVTGROUENHUjhpQ0RtQU0yT0p5UlYxdmxLCmxVTUg0Q0Z2elR4cDFjVkZDc3Y4VG9HQkFyRWFhMzdPYiswanJabVRWbGVFdytaamRhR2oyQzBlOHI5T3BPeSsKdzB0QWMxL0Z5ZXR6NjBzdzVGNTBZaXBqamlvelViWWtiS3hqZUE2a3phTGFMT05nZUVMWXFjYXBTREFHWFZ2WAphV0hkQXNSVzE5SlVaZC81aVFQZTgvNlA3bFlNdENYK0RqaVZHc3pvTTRSU2NqZFJuTEg1UDlyZnJpVXBqRS9sCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDZoU2xCVHFNUGJRY0pWVHgzbzcKK1Y4c3IxZlpzMFlUYTMxRzdvVUJCK0NPN2ZYWmRwbHJSVmlDRlhhWm10M252VVI3L0Q5QnFocjNsblFWL2JSTQorN3pXbmlqMUZnVG9TYWJNWkV0VHJXZlRnNStrU3g3YVlpb21qVDA0dng1QjRtaU1kUmFzZkhpYjlsVFQxeGtTCjFkclIyZVMrOVd1a2ZrOWh3Zy84UHdqbGMwZ0hvU3R1UlQrMms5ZE1vNjAvVzlLZzVrMW5zOWU3Q2k5dXVyQ2QKSU5pcEJJcnFhSHMydXlHdXVnZkZhNUtPWVA3TjJkejdPTSs4UXp0NWUvVjk3YXZUY0w4WUdlR1ZzVGx5dUYyRApRQ3hVM1Q4aHVITDQ2Q0paK3hxSU9xenFnVTk2YlpxTWNvbzVOWjJkZXU5bmcyZjZOQ2NqVStyZGRoTkdvWVV0CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFdoVVI4Q3R2Ym01UnBIbnhrY0kKbThHbUU5MHUzRnNOQTVvWGxkWGxEbEtBeWZUVVVhVWNNa0NDNVFrTTM2U1ZUV0tCblNsYTloWXZFR21xUXVneQpUKzVKT0I4UytTeG90ZW1KS3h5TEZ6SXdkdmRETE05RXgrN2tBZkM5UEt2bURpclBWU2dkTW1aZHFMWFJibGp5CjNmRkRRKzVHaFJtN1E3LzlLelBZMWwrNXgrZmkydHltL1MzbHk0c0w5QWhub0ErZ0laMkUwTjBTcWJDUTlsamIKMjB5amhpMCtiL3A2SCtXcUdmV2c1Q0l4YXlESGxrdVVJSi9qbVRpdDJYWi9GMlJIQ0xJb2NQeitaWjFkV1RqTQpxbnVqcVpRdkNsTXpjNEtkNUZYbTJhb21RWHc1aVQwNmxsbUpUYXQ2SWhycmpkTzFuMFBtLzV2emU5VCtBQThGCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnZyZlpsTFpMNnpBMEFiaHZtWXEKMmFUd3lxWWxiQUQyNGRONEtEekpJamtvMmpLQzEzN1lNanJZbC90cndnWmo1aS9NNFlTcExhYWxhcldFS2ZwcgovV1p2R1pjUkhLclczRk5BQ29YeXpnSTZjYjJZc3h0dXg0REVGdFRUamxWa3RvTk96TWxQYnBRd0lsQlJkN05sCm9qcjMvRmxsMmpiOUtramJnUE0zRUFRbWxadjcyY1picVpoWnRiVEVFNnNxTHczaERKa005L1VrU2hSMGRzbWwKKzR6bFBreENoa3hLenBucm1ja1pROWdCS3hSQ05wTjRFTEswbjhIYnFadkVtbmNhMGdyNkhlRk5lR2pnRU1tQgpRYjBwZW96Q2hlNHR0SnhuZTNyeGp2UFJzbitmRWhtODhZZ2tDOTIrbDM4UUhTUUw5b3FtTGU0aG1YaEdBSlBICjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGpGL3JFSFpIUFpCMTlOalZtR2UKN1JqZk9pNFpLZFpPTmlnejNPOWwrUVdPeXFHY0xUdmxRT3hpdHBTREU0VE4wSHZYMTJoSU1TZzk1eXdheXZPTgpaUjZvYzkyQzVJejkvWlZ6OVNFaXNickgwK0x5em04b0p3OGhscmlTczNBTldWb3RMb3lJdThNOU0rNG1aODAyCm9jR2MvamRnTlQ2ZUhZNE93cTdBTHlyNFpVditxeWlXMGJTdUMwK1d3T2xRYjlObUJ1bUYxL3BpMEJ0UUczT3kKeXdWaG4ydTVCenQ1Mk1YUWtyQUsyRHZRdnRYNFprWE5iZ2hOV1lMNHhWc1RWUjl1YWV3bFc5T0VhaEZwYkVFTQpSSDMxYm1Cd0c0STk1MWRWeFlpY04wL3UrV1VicEU1QTdwUHByaVdIaEdzZUZxL0IveGZ5VFp4Z1pHNGFXalBDCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEIzR0dKcEhrT1FrVXFaVDhWdnIKcG10UGN6RGxKN092TVhGZFNaQ0lZeDNrWTRteldZb0ZLbk1JQUFYZ3NCNXZMM0RFdzhMdEJGQW5EWHgrWU4weApkMEpuOVNLZU9icUV5eHlIOU9yN1ZVSElmazNjK1FaeWNLQXJ2MWphVWFGcS90L0xlcDZjNjlEd1JhM3BJdys4Ck0wYWt2bEFGRXUweldFK0VrMXNvMThHdFhqbGdQejlSSVc2eEw1dkozZWhpczkrRGV1c1ZPUzRoVitRRTNSRHgKMHNHVGd5OGtIU0RVdDFrSnFuWFV4dkFrR3ZXTm1GOGVOQVdVMmZHQmt1VlowTXFKa084eEd1NjVPeFNzSzZkZgpjcnU2Vjh5RTFuWjd0NXBBVGpwNDhwVlI3aThrWkJ1VXNZdE1WS0xvOHlxVE54UkI4ODJ0R3hzblJQVW9CUU1iCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW5Tb0dQbFlHQUVzcnNENTM2TloKMmEyVTNJUXBJU1l4eURjU3h2c3A1aFBnZDJYVXl2eDlMbTZIc3pPdTNScGt1b0FNQlpHd2JGRDBna21vYWhMVgpQK3BmajZnWVZTNGhKdUIrdm9qTFoyQ1U1UWFjM0k0WXhMWDZXREIrNUFCTXREU2ExTk81Y2tKTnc1ZWZKL1NLCmJ5aEwzVjN5SjRWcTJ4QlE0L3ZsczdmT2wvakEzODlPYXVEVWRUOE03VVpvbjN3bTZiRW1NMHdnKzRlTWlCa00KVlZzVkVVQ1NIbkdNK1M4cnZBWDJxY1VEQ1JDdXZnWXIxQU9FbVdBRktoVWtSQWVhNk9VTHlvTGp5bzVybjdVTgpLTGxEYUtnWEVaRzZVQ3pFMHRwV1dnVGI1Yk1EZEI4NE9BWjVMT3FoYndIa0dkbUwvejVrT2FCZ3NCT1BoczlECk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmhaQTUrc1NMd2J5NFEzS2RZL1gKQS92S2ticENZRUY4RmJQbnRsU3RNRkc0L1JtMnhMQi8zZThQb0JpeFFZbEZqL3BxV0FhS2wzVnJESUVZVkxBMQpoNG10eU9XSlZML3FFTnk2VXN1QTAxV1NZaGlFSERBcHJxblNRMVBMcFpaUXNQUU9kRlN1YWYzV0tPbVVBTXJWCjQyeW14aUJWR2FhUzJwbithSWFyUXZMbWZ4eitnQ2xWQTQwNTdESTY0QklsRmRlSS9xd0hTRWw5aHBRMm14eFoKbEc0RW1aWXRYaGtMS0JvSFc1NUQzeGxtT3lwbGc2dHdiM0kxWS9aTzFaYkZQTXZTeEp0OCtlZmQwYUtsTUVRTgprVGU5RklCdVk5UWRRbWlVd3JtbkFTeW9NMmpWWUlVRWhMc09lL2xpMHJpdjBVNWVoQmlKSDIxdGlwZzJtcktSCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWoxYXhSOFNMNGhXRXNiT1N6Ym4KbVpzYzdLUXhUM3Rxcmg3bHptZit4QkFjWDEwcnJJdkFDOXpnZDk0MVpBb0x5NzFmNXh0NW1nSnR0UHNUYUd5MwprUER5VGx4WWNranQ0K1cxRkxWam9KRExuNlVZdGI3OVRKcDZXSzQ0d3FTNHhCVm1KV21QS210cXF3WFJJaHBjCnlLTVpSODF1MktMdmtyODltSEt4SFZ5U3NXQ2lxL25uWlVmMzhvYnAzRGxSWkdiNjlVaU5Hdk9rVDd6cVVweU4KWGdrNjdmLzlJVjRjVGZqVGJESUUvSmFJbDRvUC8wclpUVEdHUjBlb0duM2l4TzNQcXBKbHRnVUtlSnlHbno1SQptZHhEYUFSTlV1Y0lnd3NoSW5DWWxIS1ZMTzNiTlMzTTFWZFJ0d1d0QnlicUN5d2VQWGxtKzY0Z3M5SWhrZHBsCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEkxa1pYS1Q3cVR5YlZUWXc3aUYKc0FudVRyWFZySENwWnRTVUs1NTFNTlpsL2w4REN3anF4Z3QzaWJmUDg3ODlpRy9hcVBiS2pRRnZ4OTA5Nzc1cQprZSt0R1JvRWFKNThlR3N2bUxKSlRuSldhT1pSbmNRK1VmUkVscUk1NmJjZTF1dERNVVlQWXB3VjlFK2ZUclB2Ck5wTXJkbEhNbWpGOVFpN0J2QXRkcFFDbjFQVS9ISjFnWDJITko3dWFsaWZpSVpGRHB0amNPTzZMZ3FvWWU0MlMKOHVpWW9ZbXhINkJOSFJaYytKTFVXVkd6ZDRkMTJKcldlOGkrcm9hczUzV3ZYWkNTRzZiQSsxNjlaZ1RjSzF6Zgp0WW5NUmw2WmxrWDdTQjVJWUpZbVIrRG5FNGlPaUJpckVYZ3RQbWdReTVoU2d2YThrdVBjSzBUc2JzK09hb2F4CjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0FEM0w3SzVSN1dLZ1R6M21hTXQKbm5KY0pRc1o2Nm0vNmNHNW1uR0N3cGwwaDJXRXVLeXQ0Q0JDNUloQUszU0dxY1ErWFp2OFRock44WE05WERFNQpyWmxWbTE0SnorMkxFSEcyS3c0YlhROGJEK21JcURVcS9QYlJhV3RWK2lGeTcvWFYvMUNNdXhBL3NvQ3RNRHJwCjB6SzR5T2NySHBnbFQ0VWRCSnhRL3IxNFhXYi9iUTFNUG9QWk4xa3BiK2JQY1dNWFQ5Y1FEL2NvczUxK0x3Z2MKOHJ3NlhZeEVYVzZyR1BaT3g3SWcxVEtXSDVNa1dNYW5zNmxNSXdnUTBNMWpWOVJNaEFiMGRBL1ZUd0pXY1VCawpyNHN1bldNOEdHM3dBcGVMdnltZDhhT1o3Q0xOaDhvR1k0TDJLMkwyZmhaNnRUSTE1bFd1QVRXWDNHUitUSHBGCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXlCcTdGU1Y5UVBDZ0ZINUJnSTIKYXNwaEZRMFo5R2txTVExbnNMeFpMbURicjd0c05mazNNSzArS2RUZ3Y5Yi9yVFdPSjVmaHVRalNBVWszUmlPVwp2TitkWnlqcFJYUDQxWjF6ZHpYUEFUMkM4NVdCRHpaeHpHakRLMFFraGNDVHRVTGJJN2tBS2d4SlVNeUJyeTZZCmNWZUhkSWg3eGhEaTBuMnp0cUNlVWM3M0dKakZ0OHBXZVJhQkQ1R0ZmbzB4WmxxYXdMTTFlQkZRYk1mODFwcWcKWmU3YjlWRWpiV0p0d21qZktWdDViajVqWmdZaUZqRE5mLzVFTnJPMHZKd0dmV3E2RFQ0cEdaa3ZReHo4K0xMSQp6aXNCcUlSa1UxenArWU9yblc2NVpENllNNU4yVjNMQXJBNlpaYVllZVpRUkUzR3NBM0xsc1FCdjlvTzlIVjF6CmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDJZSTdOUm9kdFJPQlZOOGZuU1MKR1A2SUttVUEvNlJXWk05WksrQU1VSkVLOEhjQ21iZGU0S0pXQUROVlZIYUU3bmFnQUtrOURWSU1OL2pVYi9VagpwOHFCOVNGNnQxSGxPZnQ4bWZmdTN1cXF0Wm95Q05lU3VvR1NGbGQ2WUZMZkVOd1NuUDFGWXZCMVgwMUQ2WDkrCmZJNDRsU3pRUzhhQ2FXUHlFcnY1WVlCQVhRbmM3ek5MY3Z4VDRPS3A3Wk1qOGhhcWpwRmVzRG93bTNzTmpybTcKMnJWbjR0NnVlbjkwTlE5WjlRcldEU09oVzBKanlDZnNtUWt3OFcxRFh4RzFkWDNRRmhDbi9ac1RjdXJQWHZMNgpKcXVnZzU0YmEzbElyVkZ1Q01KcExmb20zZ3c5M0Y1a0RTU28vRFM5aFphTlgyaGxHdEM0a3BLeWhmQ1dtTHpqCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFZaYTYxNHBiV0Ird3FSWEZqSnEKY1JPelhabHRJTFBUOWplOHhhamJDTGkyZExZRFY2NC9TODFnWDM0UjM1cG5Rb2JuQlU5UXlZbkxDOU5nSHBWagp2RXVFUTNoem9hazNKemxwV2dkOG9SQmdNUG93WnVrSVFXTXNLMGJPRzJOcGMvZjdSWEt5eHZubnNOSTVBa2FZCnROYTZuK1R0Rjh1QVpTUHVBMFFqTXZ5bFdOd0FTcjE5dEdyOENZSXB5Qm1oSSt1a2dBNms2UEI2dDMyMXdLM2YKL1NmemFSV213Z2FaK08vVDM5LytFRGRqb3lOSnhuYldnbXdQSTVRSU05b3lVNUxKMGpsWU1oeGZWN0tGVHVDMAoxNUhIUUxtNzVadjJDSXV1SEtVdHBnOUxMWDJPa3VDd3FuSFNnYzJNOHNDQ0Y5RDJ3MkptekRGUitVTmFkODlXCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbDd3aUtZZTBDRDV4THRhK05RZEkKNzlNZ2d3VkdrL3ZiUUFEMVVrZmJIbFhCMEt0TWpiUkN3Nmc4cDVWOWpTQTlUcHlLbjNGT2t2d2VCVldWYWE4KwpTRWNZMGh5WkpZRlFGSCtRSnRpUGxnRE5qYjNITW9Cb1VJZWZhKzYxV0ZwZkZSdlNvc1Job0lEcmZncVVndm84CjB1VXRiVjNjMG52ZDFYSWZGTmtQOWJjdG0zbU00TkpBMzRYWU5NVjViQm1QVDlLcnRZbVdET0xyckV6bDhEQ2oKWGpXbUxYMmZEVzZQb3l4QWd1S003QUJBZ05pSWdDUXBYL2FDRFV0ZGdpdEFMQ2NUa2NBNklEejVKeEJUSUtDMgo2b0FlYnk0RjFieUhtUUh2TVo1Y3hQQzRhTFpVZmFSdnppV29ucVBTQTFuc1hGeDhYaGVwVWgxaE0vUmtUdDUxCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlZLS2tJb3FCZGxwRjgzQjg5THQKVG5ERWM0cnB5L0ROVkdEb2M3UUIwZ1FKV2czdzVGUS9wOVFJak9COWhPNk96R3hpWEVzMmhFbnM0UjNRZ0w5SgpGM2RNYnNRVGdNYmRLQTdvbHVQZHpQMDJmL1RjaVNId1h3ajlsYitndEp6dDlWOXB3K1hPV3hUUlNEY0IzSlhPCjZDTzkzeGxCNXJlT0ZpUUVvdkZ2bkFXdjN0VXllMVVFWUZmOWZsbTFDbXF2Vkk0UzR1Ujg3YTQxQS85b2pkK2EKUnFwZVp2dSs3R3hxNStVZnVuOFBWaHdKZlVVWXNSS1RRMHZQTFBGeWVZWVY3UVAzZHZhRzZkUmRsWGJUZkxWeQp2VVJ4Y1JpK29Rc1BlSXRUQitzWlBwamt5VzV3T21TTEx2Q1VPaTFLbkN4Y0hOMkJaZVE5MDlYNVJNVUJyTXEvCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK1NDVGl0YkRxbUYwY3FtaFlubTEKWGo3K0dTVlJoWmh0RVRPTUVxUk1ueDRFckE1dTJYM2k1aXh2OXBxemtpaFFLdVQ4ZjAwZ1F5Y0E5a3gvZjl2MQpNdGIrYVBpRUNGb2NFL3laSXB6NCtWZXRQdFRJbGllWWY1cEtDLzR1N1hFMG14YVgvVTFEcWU4SDJhYllUd1kyCjdreGtYQ1RGN09yNXlsSFByQ1BGQlVkVFhEMXd2c1NVbG5tYW54MDNrTjZsRGJhK2xEazI3NkpLem9oalpickMKcDVtTjEwcVpGbTNYUjZ3S094dFlxRTJFcU1uK2k4enBQa3JESi9EenoxSjM3YjY4VFg3T2xjVFJoMWFmUzZCeAoyUkJ1QllIZitBY1MydXdoQ29iTi91dm1iZHl3TWtac1I5TWNoUWVVd2xIOVpIeVhXZkpoTlJQU0t1eUNyUVJCCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlkwNllxaHR4SmdYelZ2eG1vbEkKUVJ0OXkwc21tSS9qMk51dnMyK2h2bDZaTHU0Wm1nazBzVHZGQ2hWWVE4UE5Dbm0yR3lLWnR1aHZXY25XVjF1WgpxcTJaWWM3ZER2SmFmQy9UQzBkdnlEQ3dPNzArUUcrQWNGZFJXWFRrTWk2RTZzMW5XbFJHUHJSRG80ZXpKb2U3CjBRSGt6OVV3Y1d2S2M5eXh4QmZFbVZCTnhZUWtNUEFqdmpHamFyRmpsbUFHUG82bDZPbWUwR3FSWVRnWFJ3SFQKM1VhcGlVamVCOWdubEpoaGlJcDNVYTYrY0tpMlFXLzNZV052RzRUdTBHQTU3N2FLSyt4b256Q1BOSkVoSlBoOApPeXpiSXozV2dGdTZKYkQ3RXkzL1hKQUtaS0w1NUVKa25aKzlZdmJpdzQxeC9lQmRCUnlsNDBjeXltNWlGR2VrCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcCtsdDJ0OUpRSXgvdG03SnJsRHIKaVJNUGd2VDFiaFFlMld5UVFXNDc0TEE0UTFYcWxQeGd2aEthTFJMVDBDbTA2ZTl4NmxPOXc5SXRGaVdzVUFiawoxcDZOeDI4Q3FTTUxkMmV2UXVGNVlnSFloWVFER0F0ZitsNzhTQ3NHcElod1VqUVZKdDJrZGpFMnlnSmk1N2VGCm0rZGRodlRBVjNodWxQM1VITjR2K2V4RmN6ZHhYbUd6QUVTcExxRFc5cVpEd1RXaHZSOHRwa0xQV1grb29LRmcKQ2RTNmFRSUh6Qk1XM2hEcWdYV043K0hqaHlpWUpYUnlLcWZkdmVHZytlOTF6QkxLWTVhc21nRVJEaEVMaEJOQQpRWFdLVTZNMkNYVWFETXpGc21qK2NlVG9ldWt1Y2hLVFdlR3hUSG9URk5pRkE2Q0w5Y1UxM01YLzBLN1BvTU9PCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckswL0lEZmJMUXEyTGdDYUpqc2QKZFBRZit1bnBGcTcxeklMZlFVWmRmOGtQSEwzYklpYmtCSGtjYWxPQzFpN0FjZW5VNUlWMkIvN1dnLzhpWVlnagpVZWJLbkw3QnljRkdLVnc0Tm1kMWc0YkR0MnRJSWEyWFhlQjNEOHhpZTd3NDBDR3ZKeU5YTlFIUnpjZEpyZHB4CjhPdXUxVXkrRzRSTEdXMTdQWmZzSWI1WFNENHhCN3E2SnM5NEd0ZXNtWDhaVGg4cG9LT1c4RlhxRmwrQlBjZHAKbDltMHhPbEYrdk1SZzE2US9ZaytOTnNmVWNTa2ZZVkdxZFYyZVFoTXBNOXBjUFlXSnN0UWNqbCs0S01teU1GRwo1elBmN09EZ2lnSE9CNTB4WlErMlUyZXhkMnJHSG4yblQ3K01iaGVUZjJiYWlPL2duS3NXNytKZXg0bXRCM2FlClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzU0T0VUMyswc3BGWEwxMm1UdEoKRHdwS2pORnVXQzI5OEZERlV5YzlBRlhyWTRsS25GUW5qUzdSOWZ5c09rd1BxMGwrWEdobnF3SjJtQnRNYnFOWApDa0VwcjJDcXRsbWU2QTBrMElqVEtOcVU1ZlVqUStTTEIxaG01bHVSL0xLb2kzVWRMK3pTMEx2T3ZMMjNSYkZ5CnJZQk51SDdFeUt6bjlHbmJtZ0s2UEsxOHNtdStodUN5Wlo2eXNLcnIwbzcybDRGUHFMVHRScjlGZ3pqeUpha0oKaG8rVzdXWHRtbGFEbXZPcGtuOE1JZjlXdlNFRXpwT1oyRllTQlBVdlJ2eVZmMFVFdFg5cGJBMmd6QnJjNE5vRgp3emxVbk82c05hWkNsRVdKLy8yYUliM1BUcThPakVOTTY5dkd3MUYxejFYRW52cjRod08xNVhHbWpDbnV5MjZFCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVNITldROUE5NEd2cFl1MjhhbW8KUThucVBQamcrZGZtTmVCaXg3K3U0ZnpvdHloTFM4OXRsL29RTWhONG56SnprVWZ5VFNBRTBwT0pLVTJmRGJMbQpEbFUzOEFGVWttNy91UFdYSGU3aGovOHdwc3YzY3RxQWFtcjgvanVHU2Q5SlVvZVVUVWhDV1RHZXBtc0FWaHpIClB5aUtnTEo4M1hQQm9PSlRmblhWS0JsRGdLdFVUWlRJbGZGbm5SOEV1d0xGbG5JTC9UMG4xYThpaEk0WXNRQkUKSUtVdVNyVVRyRDNGU25IVjh1L3pVUXFVOHYvZ0JSVVRQMnIraC91VWMwRGk2MFMyaHF3ZmlTZ0VyVDdlRzZ0VQppN3hWZzFQV3o0TDQrN0hlMjQyTHlFekxtZzFGZkdndDFDWjhKcWFiWWFpSCs5ak9rN3hxRzFxbXNGbHJrK0VpCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTQxR0JoaFBUYmM1Y29DbUs5eUcKNm4vaU9kb2xYNWFBcmtobFdFOVlRd05CUWl5QmlQTHFIMUpEREdWVzFFc3p1U0lwUnplZXhiRGgyYThhWEIyUAo1b0ZIT3FleHBNN0RRb1hoV3QxQWkvZjhYV1ZtUGhSYTN4alBHRE0waEJzUno2eXh6MUJBTTRURUdmOFRweERpCnhaODQwcW1WQ09SRTB0V0hnZWwxT0Q1MDE0OVVTczB4ZEkxMUdvQnlYdHF5UHJLSmpXSXM4SC9WNUVJd1pvVFEKY1B2cTh5NkE5N3Z2VmUzSGM2djFtbGkvN2dKM2xOdTlxeVkzL0xXaWVtZVpUWVZOUlE4Ylg2eGgwR3orcHUyWgpYdFJtY3BZV01LTjBNREVBN2RwSUorWmpvQmhFaXc3Ynhmc2VrTzJOV2pjYjkyM2poSnBGdlo3V0NNZE1yODBJCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFcwdU95cjZ4RWdtUzdvM0VhaWkKak50VUVpZDVKZ0RKTlZOekRTNlA3L09mQncxcmcxNXEwSlFJSVROc1daRGtURTlackw0TDU4alJYUlVPYlJ6egpRNExtU2NWay9Qei8xMkIvVm0zTENocXlFZkxDUjdMb1VYRytzZmJIa3IxTjN1WWNMMHo3blJqYWNUQkJ3T2FPCmNVM01FNWJHYmJrM0laUEl4MUdZTkszaTN6c0RRZ051WkRtckc1QjFaVTFGTkx4QTlXd3R0R0FsUzZ2aVJVVWMKQ080eVV2Rlk3WTRFblNlVGNPTGQ5dExWSng5bzdoRDZCS3ozaW5GbVd1VGZxcDUrYnE2a3lWamg2bnY3VTF2QgptdDd3YXM5cjhTTEpNcExkNFIwVURmb2cybmZtdm1wc1l3Sk43Mm1rczBmaExWTlZJUzRqbkQxc3BjdXBESmk1CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk5LNyt4TkJ1a2szNm9yN0hDcjAKZ3hvNkdkbzJXdjNhMGhqbVIvamE5OXpMZmtXNVBDWnQzNU9hLzJXVi9XNS9OSkUxTm5oREdPektUQ3RrV2tHTApYSllVSGx4ZmVkdVl3dlBpclVvQVlJRG1VUTBWaFNGUllNaUIvRlJXS0g0NmJMc1RoQTRlTGdwRnJTdTJmL1k1CklocGFyR2tLZFNPcWdWQ3g0dDhjZHZDUTQ0eVMxbURKckxSNUIxRzBjcHNyc3o0RVZjdjZ0R2VXN0pRakw0WUsKanVYZi82QlZMdExsbjdGUjd6eVRmSzM4U2tBV21Wb3o4U3VCQTh4ajBZOTdNakg4N1pZU3BNT0F3eS8yck5SZApFTE91M25rbTliemxOeXNKaHhFaUlZUmNQQUtrMm1tQmIyV2Yvb25vY3VVSjFXMko2NmRESU1VRE96VnUrMklWCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMW5jWHF0VG1BeFpmamcyZkZPZW4KZ3RSQ3Y5dU5pb2FzNVNrUnd3VXFGVEVRTnJ2Z2lGVk1uQTNvWFBsOXRHb3o3SklxYTdwOFpUc1pxdkZycjlHZwp6ZTBoRmVRUWU2Y1VHUzhOMWpOc1ZMa25SK2wvZTh2dWJLZWVrRUJPQ21tak0xZlVzOERkVFhmQ1hBamVQSFdSCmhGZ1NBeVBVQTlIOU5lVGhiQS9VNlhueWtub1ZjOGsrWkRYeG52d0tNNWs4M05idXVKNW1INFFzeE1QM3FIRzIKbXBzRCsxWG9EYm5KQ0c1d0tXTFNZdmdWMWhEY2xMQ01hQUlLR3hzSGNtb2liWXhsYWRqZ21MWTJGNFBGdGFFUApnTWJjK2tzRXBQdU1oRXdIUUxmditNZE1UTS9PcmNNRzB5RHJ3VE5BZ0JJZFJkWHBSSlNORkc1Wndkb1p4STByCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGpEd1QwdzB0Nmt4cSsvSWJvdlEKL1AvcU16aHl4a29qL1d2VTh1UTRYd1RPb2hlQWRaRjJZRWx2L0NYZnpMYUtrbUE2TEZqWXBMODdwaGU4eTJLLwpudXpZTnRxL2JjVllHMlJhMFRKSTBnTEJKY0VmQXJUZy9Ld1hzNkRYaXZBWldoekZUWU8yZUFZN21Zb0haK0xNClVoTm1xWjNDOXFpc3BseUtDTlJ6RWxhVXZLVHNkMGNaRjhMdXNiZTNVd01wUUpESGVMQjFYYkdMU2hGS2ZTdTgKRnRacVk5V3VUaXRTT2NSb2tNMnVrMVBhbkc1Z3YrZk9ZWFVTTlZxNVc5azVrYTE4cE5EcEh1TTREeXVTWm51bQozdEhnOUZ4Q29NaTdFNDB1R20yQnRVYnZORDFpbEtycVNCWmxhZG1hN3hSVVNkQnNIQkR1RnJoeUZCSDdwZ2hjCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG5JVDlxa3dxazBIL1BrZU5OVmEKN1NJZFArR2ZnRThudjZndytMS1NzU3l4UjNqNGd5dXVHQlIxb3M0UFVJRWxnNHRBTjdDZ2NZY0N4MEZHRVovcgpmRTJKSGFaYW1mSnJOcDlJVnd3SldaQTI3OU00ckJsZnlyN25nc1hNaXlSWVlrSWVVblV6RnIrMXZvNWJJTnFSCmVZQUJiMUF1ZzZOazNqZ09Wc0V6N0VHSzd4R3d4MWxwajRGTDJ3djlqb1F2WENLeW5PWFdmdDJyblBKYkdKeEIKZ3R1QmtlWUdzcDM2Y2FyUkRKMko3TUt2UEx5S29KNllRR000YVRxYlF3cERuQlYyTmpuMWVqbzZDWVM2RGxibwpqL1BQbTFYNWxQekNYODRhSHhDSmJaOVk1VkRMVTBmOUVOMlFDa2FkdkNSMDF3dGxqTWNWV1hISkJGeitGVDVaClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXZCYzVCaTRaZW41VnRKQUNVaUwKQU1FZkUyK2pRNHRjNTVrS3F4MGFVTE1JVXQ1TjVuNFM5L2hqRHhjTlE2REtjSTRNcFZYYVp0RHc0UUc0UkQ2ZQpUblhkWHJ5T3hKTVY4Szd5NVViNnVhWVdsczRIWXY2NHFibkYwNGV0VUhpVktzZ1ZSVC84eG9zS2wyd091NnpGCkNQVzQ4U1pXMXI3NFVvMm4yc2NoVlpNVW5oSXVUVXRxS0tXSmV3TzNCSkVjdjNveUd2TXhtcG91RjNiT1BZaWEKT2FFd01XRW9yakpRU2hyNjVncmh6bnF4MGlZaFNzaW0vN1lnSzZYcm5oSkM1eTdFRkdMQnc4dmxiZHhRVUpvWQpLcjdQZ0gxNHFFZ3UyMGZhc3RraGs3eU5DUDlybGpiempWZlp5OWs1L2ZyaG55SVA3ODRGMit4cmNBaWV6d3RPCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVU1S3VTenVkaURZcVNKT0FmUzkKK010UUk1emM0RmtDNGJkRGNJanVIekY4cGs5Y3F1SHpjUWNkVTBrR2RXRmxDZnY0OEZ0cjhhVUhlQm1jaHdUTwpTRTBRUXMvTzNiM3dOQkhoSys3OWlpQyt5UGRLRDlHOGFoa0JRRjcwR1F4MWN3U2l4c2g2TTlVL2RZTWY2WWg4ClI0enBwTkV4UkhaT3FjNkNud3JUV2hueVl6cE1QL1pQaGFRaTR1aXdtY245SW1ORFRvU3dncCtUcmhvY05Tdm4KWnZkeEhxdndpZkU4RGlrajRZbHhHVzZscGV3ZElKYVBBR1p3a3pGZU0yOWZFYU1sMk1SZkk2SklOSmtQWUYrKwp6SFhpVzhkQ0dld2hoK0IwUEVHY2NSM3I4bkUzenVCZHkrcTVpZ21wNlJ2NmNQQk15US9BNVNrQVhwckR6MnpZCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTZhLzJDSFEvbG0rSlBVNXltN0sKTXdxSWZMcFZWMk5TRWJOaDBWb241dU9NNVhvYy93b3dFVmMrcmtQQnpnUndRdDdMeTEvdTJqTDV6dVp2ditSWApyYVFJVGYzblhmdXRuQXE1azFVSk5pU2Vnc0ZtZCtqOGNpMzN4RmE5MWFCWkpVakIwSEdUWXBRajNzOVM3Y0RqCjRCektIaTZVS1lOOW54L3I0VmxGbExzK0FabzZRRWRkRDIvMWVyOEFPaFVSY3gvYkhmWHVkTzBqeldsSHpyN0EKaDFRN1V2VytFdlVmMFNiVmlLU1BlNnFxQ2R4a2Yxdm9HTE5zeUhEakxET3pqTmxMUzMzWThqSE11S2ZsWG02eQp6ZU52M1ppVU9qK2RNUmd5UHdvclY1dDhTcE9XRjFvMFU1eUtBOW9jaythcW5IQUdRQjFwWlRGVVdNQTRyN2hIClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3FVRWxVRXhHZ2NFNHl4Q3JnaHYKYVpKL204RlFpcWRtT2ZPN0p5ejNWUFVkTkg2ODIyd0llTHpYT2lrQWtBNFVpNnFnUVBiM09sSHlyQnNMTGw3VgprWEJuazhTR3Y2T0xmT1ozWldUREhVMTVBbVVVVVN3ZmxLbFRJbzc3QlJQL1AxTzVLVHBBM2hxeThjdjErSnBqCk9ub1NWU29lRGxHa1N3U0VOVkZPV0VxYWpNVlVFZnpWa0RhMkV6MFAwVFlybG4wUzhJSnhiMlJyVE5FMDhMTFIKWSt0ZFZON2V3SUdLVW02Y1d0b3czeU5HMWhsR2JDQnltQUpKSTJFUFQ0a2FERGcwRkxBd0MvWG5Za0p5a0lKeQpJYlVNNUlkYlloVWVBKzU4VkwvZTdXRWNHeThQb3IycXFib21ycXhRalc2YUZ5bERweitTbEttQnFJZTdXT1JJCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2hCRVZCaGdEZFRCTVV6SVJJQjYKQmd3VENPZmU2TG5DcnpNVWszS0RCMnhpeW53MUdDVHJJeC9aUzNwVnVxclJ2Rzd1MFpRUUh1MGYwNVpMa2orcgpvSndNVW5aWUdJcEcvUVNxTFdqdXZMUnBHWnlod2FsZmxEYmcxS1NUbTk3ZUdLQ0JaRUFIc0VxczFMcHQ4NXBZCitXdVRlOVByVm13TTI0N0lLaUdQR3NiR0FqMm83VFZneHdpbFdqUEFJMW5NUkcxem45ZHpCR0NzZWkrRUpRZVMKK0JZODRXV2V3di93cEVVVWQvSURGeno1TmQvRGZpSFV2VGxEZWRKRGhxTkh4OEJRVzk2eXVyaHBjZVBhNit4awpsMVI1bnR3NzNBQ1IzWTZaKzBkTzIxTXAwaFpMV2NBMDFQS2NrQ2FLNFUxQWpQSTViRCs1Zmx5RU4zMnF3NjA1CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGZ6T3VIdE96OWU4d0lPUTZzVzIKRmFOVG50K0JHYjc3NERGUzU3bkM2ZHBTeG1qTXlsa2FDRk1Ra0RDNC9OWjF4eXlBZmlVSUdvSWZ4RTNNRjUvdwo4SVZ6ckZPV1ZYdzdISFBTdHpPY1ZSNWVBTTdaZjVwRmM3QWFRM3JienNRUDBvNXpBN2hVa0ZERWVTWWN2R2FiCmZEcVpJQWZZeDhZYlkyZm5sc1dHR0dMTHV0WnpjTkFSdk1jenlHdTJvR0dPVnRIU0xnektiSVpoRTVCRkZZQlgKQ3BKRWQ0NnlhOVg4R2N0cC8xdklRZURwOXVkaGxBR3NNZVNNWTkvV1BLSmV5L3AxMHZETThkMWhOdUt1cFJTbQpoc0UyU3ZGeGRiVnpveXNGVG1JUENRU0xYZ0ZRMlhqVTZhWVBjOExoOGx6V3BqWE5vaWdvaVJ3V01mS2FOY1RaCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkRnUVJSVHJNQUR2RFkzcTNJWmEKTDdiM1dNdkJ5Z3lzcDhadXJxWU1ZNnF3R2VtZ3piV1Y2ZE5SUytzTGdQZ1V6cGp4VndNVmVIOWNjQkNhdE83eApJR2U3Nlh0aFgvRlE3N3ZPekpOa1R5MlUwWHVPOG01RnFtb0hmdEdQWGExTkhwTnFSTWRHbk9hUTJiSjlKWUcwCnE3bDBOQnNwZ1NFZk93T3IzbG9mT1oyYVhmQVFxdCs3TUFjQUJTK1ZsVkxKQjNvRVVsYU1QZTIrTjlOOHdEMUgKQ2txTkNNV05BZDZZWDFhZFhHZlVPNUNaSEFLNDdoMGZoKzhOYWVUSm9LRVJWWmkybUtvdEw2bWlXaTJnV2JDLwpOUUdDUlhxNFB6WTh5NGk5SzlJQkMrbXI2NnlnNXl4UmNORkxuVnl4bkRaSEFOS1NTY0R4RUhELzlZVUFlNEhyCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWFmSWVoYnBKTE8wWVpjYXAyRU8KcjdyL21HR0E5dTAxRVNwdktOT1dyZnM3QXJmNzBkUW1aUHNxbml3YmVIUk1HVmh2L3ZaenVOYVA5V2VJRXc4RwpDUHVuck9LQkFESHBQeXFzNmhQWWVMeUhBZFFIZDJuK3lkNTloL09OMDFFMFBQdGlrOFpvcTc2THlnQ3Z5M0o3CmprQlhFWUNDV0tQR2dHclY5dnVuNm03Wk0xN3V3VnlvMFlNZmRPS255N3ZTcW5TbUxVTzFMQUJHeE0rYkkxS1MKNWtTZXpubTM3N2RjdTlIemt0NGVrcjQxSEJoVy9VYVBHby9RSnZNYlVsL1lPNGN2OG90QjYyNkJWOWIySEM4Ygp1NWxaL0FVVXRUNUl0eXp1cHJabXhYeGtOTVg5dnByZUI2azZwZndBelo1a25IMm55akE1eUJJWGJNV2p5bkZiClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWNKNU40QmxpS2wxSEwxSGlkcEoKaUVHUHh0Nis5OXk2a1ovdWl3RWZFNzhDQmRQVjNwQ0hDTHJMRlRLMWFZcFhKMUpZRnk1TUNaL0drd3hYYlZtbwoxY2FKWnR2QW5sODFHRWkwWUhwNFhUdHpnV2N4UDk3blZ1MStBcjhKMXIvakEvaXJ1L2d3aEFBTFpqbEQ5d1I0CktPZTFFN1dvMWRkZ3ZVellpVDd6SXNyOVVxOEpqQmwvTkZPVXIvdHNoZWp0THhkSk90dUZnVmlGSG9MaThrNlAKLzlJczM4Z25BLy9LbDFFTkNGQk4xSWdSeHpSWWlCTWdBdXVpaWRLbktrSzhWV3hjNmNBZ21OeHJOb2VpcEd5awpsSHpRSkRWSS8vNUZTOTJuaUtqRjRaRmFMOGQrSmE4Q3F3ZVNoUGt5Tk82V2h3Ry8wVUwvb2dhM3RhbHVXV2llClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcktpTTFzVkErMXoyUHJEUFNUcDkKOG1SamVjUDJqVGYwbE1LMWZmRTJXN0RwcmNDTXZyamFBWVZ4MGJiNWI0TjJiUG4xakJUWnl1TlExOThrdFRpMQp4dnVsckNsa2lDa29jVnROVUdjL1JHZkJnamt2Y2tWQzZuRjRpeGNCOWxOYm84R1VEM0ttcThvd3NSU2dLSWVXCkQ2ME5uMWFyZmduZG9sNCtDdU1abDM2bzFjMzZlODZMcS9kVVFmRVdUVExJajZrQTR1Y0NxdHpaTWt0RW5ZSVcKNThaajlZSng0RHdOWkFHS1BVSURLTzhMQThPR1RoTHltMDdtaHhlWGtuWkNCdm9RR21TQ3JiNkQ0NWgxcEFJOQplWndlV1pZdmhsSVNYWWZtZU5EZnQ5RmtybUY1bVdNdFFFTE1TWjlLN0hITW0xdEo5dTFHQ25lR0lzMHlwQkdFCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkI2RTZadTVaTVRFSFlzTEIyTkcKNUlxVjFEOHFHeHNkTGxBSnhtemVET1JYbUs1R25QUEwxR01NNmlHaG1YbVNIRGdlRG9sV3AyTHVhcjNCdWxZaApmN1c3TElUenQvVG9qdDE3NVQ0MFd2ZkNGeFpqbzd2YnZ4aElqK1RzZFF5TTFnaWhjTzFiZTVhQmFWSlJSUzYvCmZKZTRrY3dkNEQvOWloMkFLVWg3Y3NvaGZEbENzdURVc1ZDd0JjUjZmNStqUDdrWGFjay90aUs3RmExVEZyc2oKU3pwNWRzZEpJYkdmcDM4TkUvTmVjeWhZTW41T2w3eHZmTkhRYU9FSUdkTkNiRStnV1pPZHdLM0htVnNvY3dMZApZYzdxOFd6UlJNWjQrS3R0LzVVVklIME8yWFlYTStmTFFUdUZYN2NFMTd0STZ0emxja2J3Y2xwc1docjBjM29MCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdStZMkE5MHBqMWJoMmxlaDI3aWQKeHNnc0YrcnlYRW4zQmFkTGt4UElCRFZPUjRwUDV3M25LM0xvZ2RtL0hKTnRnSEY1b3R4SkhBbFlBSjdzQUpZdApPTnhIMXZWNHlMUktYMHp3WXM3YnR3b0pTckZERitMQVZVUkRXeitXbVZnUGIrTkJJY0c2V1E0aGN5RFF3ZlF4CjJON3hGUDBGMlhjc1ZOQW9jZlBtdDlLdHBoU2w2bW90TENHd1VCa1BNb1I0dWpRaTU5RFgydC8zY1p0eTliTCsKdWduTVBEYm5HU2hrUThtakR4bkFldStSTHAwaWY2VFc3MjhHMXVST3Rxa0pBNWE1ZmMzWkVVRnRFZTBtakJKMQpid0VWNk16WjdGNHJHM0pUK0pGMTJEY0NFSG5XaktOcGExNnV1Z0dRUG9YNWJJTDdTV2QzZ1Jnb2xaU2VYcUpPCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkRXQ25hQm9TVG9JTHVZV2h1LzYKc21lbUc3aDVZLzhwUGxTOGxFa21UT3hxU0UvZ0pDd3JFRDNIbG43TVA0bkUwdGR4ZHZNY05kL243RktjbUp1ZgpaN1dHVE9PdzNNV25IRnNMVDZzaVlVN3hHRVBrM1NuUHFvUWViNTlCYXQ0bjFBRVYwaElYNHh2YzBVTXpiei85Cjh5QVIwRTNWNkdMYjN5NnRDR0xRQUZPZm95bEprL1UwbWk2ZzFuYThqcGJGeGxXVVNxb01HcHc5RGlSRThjeXIKRDkwOG9TMXU2VjY1UHdYZzFTbjIzUUJvR2pLOGE3dE1lNVVzNnptUnJMRWtMTEdaTDQvRWUya2VJRnRFaFN0cAo2SmNzMW9rL2cramVpbkNzUnZ1Nnh3a3V6S2R4aDFGYjlkcGhDVkJRaVdEWjNINzU2VnY0aTlLeTFwL2NwS091CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1hpd3p4NVNxRWd6VUJudm1DTnUKRVlvZ253OUhLVkxpeEdMbm16OU1WVXFmL3g2bGhhbmMvbzl2Rk1XanBqRWtWYmVYRlpoYWVLNWtYNjJrWjZTSgo2dE5LblZtWE5jVDdDVXFlVGxsdjhQSUVSUThPdDRIWlhTaVpXaEtKMkNYZDUxOEU3UFIzNmdNUWVnc2NleS9FClhpMnlYcm4xbEJWSG1TenMvVmFRVGZndnNuVXhOekNKNSs5bUU0NmdLRHdoYTYxcmtIOFFVbEJoNFpNSTJCTFEKODFNZ2JJVFlwZnUvV08wUmZydzZtT0VUQW04U1ZkYnBDVUdHdFQ0ZC8rRWwxWU5KTWxBRnQ5RmZNVlUwcGx6YgpteUxGUFplcG1QTmpxRitraCtJU3VYbk9zWERlU3p2blNqbnlGUVRucUhzZzE0MUdTOTVwaE5EcGRZWkhYMnNzCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdngzMDJSZEFncnNDMXphV1d1Y0EKMmRnbzZOS1Vtc0dqc3B0MjRmWjFOMU1nYlJmMjkrVmY5aW5sSFg3RFpJM0hNOFRaYTBFRXJWUmV5RWNlTUo5WgpoK1FGWXBpQ3RLQUpaNy82eG54YWljdmU0U0RnT1FyN3g5dUJWemw0OU1IREdQTERrMlM3RUlLdk9hZlZadVRwCm1zdTVzcmNBdEVNMGhtVTdnZFVRNjBxZkZmVTBqVm94UlM3NHZ3Y3lZdDZ4TlJTVzVnRlhwbDAyaUYxYzJxNTYKNDR6SlNXWnVodThWQW5qeW8yYUVDZWVCd004WC95L3NTTDBKZ0E5TXozMXprYnpSRlM2WEhEZEpoSlV6cVlxeApHelUvRTY5UkJwTDkzemJ3ZlFIeEpiSDhjbWRMRk00SHYwdHFPSjJmN3lpeURiRWNWQ3YzbUsrYmJGZlo0eWdqCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2g0R0NpUmV4MFhSa1luUWxWNjkKanhkZDd2RWc2am1yalBNRGhhTm9Sc3REQnFGTEFCdmpWVDlzWkpGYk91ZGhmUzlkNmh0NWRScEE4ZzAwRWw3Ygp6S0NaVjNvMzdwMmtxeDVpanM5K3A1emR1c0hYcnhqcWRabUtDc2ZDcHdkWVVlL2phdEVqM0ZxeXlpUjVmZnFxCm4wYWFFWjNSSTJVWW9renhlRGRLSGdmMnlTdjRyZFlwcFV5UERVcFNBRU93ZURaS0o0aGZnVjhVTmZQNkpDaEMKNExSUHpHY3FUOWJhMmo2YWNVcFdxSUNVYkhWNlgvVEV5VnFPcm9Rd28yMXhkSGRmeDFHUUlwTGdUYW5KbWVCcwpvcWZIK285a3FhOG5oaVBjRDE2RjhOUVNnMmVoTmxNbURzQU83QTFxVDNodXRxN0tXdDZQRnI4Mmx0aWgrS1AwCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEo3TWRZbWRmN3VHZ0lrdjEwOU8KQ0FoOVk4MDlRWlRuRU1lRGQ0ZzFHTFUwRlFpSEd3TU5iaGQ3eFk1VEErczNpMU9kc3MrWGYyWWtwNzd1VjRxdApoQS9sMGU3NXdEZkZLMDF0MW1MbTFKVUhRTElOY2pXMTJNekhTdlg4MkpIb3EzQlFvZ3FFUmFKcENIc2l1bFZZCjFCVTBxemlLRSswNysyK1c3UUZ4WDhZQzZLdW5xdlMvYWxORlRSVmFJYlN1eHJ3M1lWUzVHT25tQytzOWNUa2sKT0RiL3RzQWF3dEs5TnpSSkRITFZsUkV1dEpwNi9KUXEzUXhxUW51ZzEwYVlqbStBQTRSZVlUQWt1YVNYTFZJYwoxOVNxTUtUS2dZaTl0eFBMMnlWZHh2RWFZK0dHdFVqQnArYjF2dzB0VVJRVXBSa2c3S1prMGhKUjVwcEpyRDNHCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjV4UHRXNzNZeFIxVlFTM0lTREsKWkllMnV4dHNuQlAwcG84akZOOXg2S1lUbEFmRFlOa2hoYldTV2pRaXE2YkhsWk5ScTB6eSs5V1NISkdXeTgzaApCTGlqbmlYM1l0RXJYL093MjB4VllCVmJIUmNOaXVKaHV0QzZIN3R1WVdzaUN6cFRJK0VkZlo5MWJZQ3FLZ2lSCmtKVUN6ZGxSY3h4bXNUcUhsek15cEdNQkdPWjR3akVjRWtYL25zMEoreGRVTEdxSUNjVTMvVXk2QVd3VGNoOTkKTXVoVXphdVU5djczVTRUNHpFcVpoMlh5Q2duVzAzUWEvK2QwNjlVTEhaYXErKzcvbzV4UFFvcCtvRzczMkMvWgpvMUhuSzBlamJ1WmJMSGExYWxsWkhhcktZNWVMb0FhSHE4R28wTjdBOTlMZkhEZUdPYWZTdnZpSWl0U3NEOTk0CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHUyaUM2azgwc25zdHRJb2xtMEsKUTdwdXVOc2dCV2xWS3p3eElRV1NicHdXSW14aDJEODhoNDBrV3FMckFQNkJPYnhUTkVibEx1TkUrQm82cXhyVgpOUVVFYmtiZmtDUm1HYTVtM2I1TG1oc3FkNGJPSVZIaWM3UFlTOWpuOEljT3cranZzNTlGcTVqdUdsZXJFWTl1CjIrTGs1dXdkSGJ3YkNYK1diTVdYNkJ1SmYySVBIdVdQVWxRdkF4SjhkTzF0dXkwVDRrVFJueEQydzhUdGVxSVMKMllnVmRDSFVsR0R3WjFmOENyMmYxdjF6amZMNUpuR0Y5Mnl6cTdUYmdzSWU4cFZ4QXhNODMxYlQ4V2xLTGpWdQptUDZ0TzdYTngyRHJZV0IyWVdmZlJ5ano5ZkxTckkrMzhobzduaGx4dDBpRDVsR3Vzd3BDN3djWGN2b1BQNUhuCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckdaYStJdVRTNWZyUTdpM3BnTzkKbkdrQ1N3eEhNaVI1SncxMUhoLzJWMTdBc0ZWaTMzWWdiN29yajJ1YlYvSkw3WTVlREphYmpmVURZaHFveGNiNwpOSkptNFhTekR3WjhUZHJzNXpRdlBja1VMajk1UExZekIxZWtUWXFlYjNaSnozQWJraytBbWE3VS8weGhheVBtCnp3QlJWdXVpTlUyZ3VCL2xOOWNXSVlTTWpSc2VqOTNGYWZ1R1ZyRHhxSm9WRjdMTkFwcjlCNkk3c21VYUVrcWoKQi84M0xsZWtjcTdZOFRYSHlqdURmeXM4LytwT2Z4N1UxRjhDTjVsUk53aFpkMWJPR2hGR2p1MnBQQjgza3pUZQp2ZHRGS3NKK3ZEaVZHbXdIMDNDZU9VV1luUnZOT29xNGNTdTB0dzNvMzRKMDVxWWhIUDJqMUovMmdCY01SbmowCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVJQQUJ4K0tMYTR1R3ptc1JZdWcKeU9hMkk2em9nWVJCK3VFT1dxUG0vODFrMWljN3BtM3F6Nk93eklOTEszNGR3UWhWQlZFNk5adjQvbjhzSGZEZwpPR3J2MEdLRFdUTHBMTG90d2VkSVZOZk5TeXI5OVRYNU8yVnhrZndSWDdpVVk3Q0tKMGdoNGE5UUpjTWRjNEt5CkMwdUdPdnRreDN0dVBzRHpQcnZaQkNLSnZFdW9MK0gwYVQ3cEdsWWc2bk5yNjhoMTV1dnZHL2l1ODExQWRjZWcKbU9EYXphWXlpb0doOS9oTVk0WFZremJ4NE1aOUxKOWd4cC9vZ0M0WDZseFhYb2NKb2JDNmdpQzZNL1c0c25vTwpXUXNjUEdja0VPS0MrZmgwNDlNby9veE5obVhYbGZHRzJjaDNCRGVnN1BKYWJRZkJORlFpYVlhdUtsUGpkSWxkCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmtBVDcyMnJMT1BEUHV2R2xKdTIKWGJFRnNYMUs4VW54S2ovcVFyWWRHOUNneVIxZWNjTWxhUlNJak1xNFVGbGcyWFRpbmx2WUhYaE5zakVvdTBMNAphbEg3UysvMkU2c0NjWFZIYkJMNjJnU2R6cGhoU1JJNkhXSHRtVHNQUmtSVFB6elY2MHNwT0VLV0p1OStwU05WCmJPdDNHdFNDdmZJSUF0YnUvSitnQUJreW1jbEwvbnVENDBzNk9TVWFqaERSaHhBa2lvQ2RYUVp6L3lTQURxdisKRWYyR0JLaXFaMHpQMkNFUTFWVDZpc2dRdmVXRk9zcVF1bU5jNVA4Unh6dUdKZlVOQmxydEV1bFNTSkM3US9NUgpndmtHZXYrZ3B0MGVoeGl6TmZPSkFuNmxxaG00Y3VDZVEycEZDekx0RkhSOWpPVjNHcEN2ckVyV2pGWEhLa1laCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjV0OTRrVFdrdFBWdU9xWlJWMWIKM0gwUG5WVjYxbXJHL29iMVRBd1ZtVDdQOVNjcFRZV24xN2oxMDlhQkI3N2h4aUM3SG9GTUEzVGdIRUFxNS9hZwp2THVBVW1DRmFRNklXa3hNb1A5NGJ6ekpwQ3pzNG1ZUGF4b0hqc3ZoaW8yL2xRZG5rN2xmZ2djVTRlM0FWa1J4CllrYmJJeGVZajNaRkZnVUEzSjh1ck5HcVhtUzBpa3cyRWR2ZnpEdGFRSWRJNVc4RXNhRXFJdy9TeTJsVU4wVWEKNTYrRWZiM1ZtT3lleEJLK3JEYjlzeXhldk9oQUk5VVVEMm5nWDdqb0V2RHErL3ZjL3Vha3h1aURCcVlXdzdFVQpROGQ3eWluaFpEaE9NSGozb2ZETHp5eUd4M3NLUXN2SHBCcEpUcXRGMk14VEhCeU9XNm1yRUt2WHQ5RTIwNWNXCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHpOVnh6N2c0ajd2K2ZjeU52NkwKRElhWE9LcWhQdlBDYVd6N2lOUFFsTytMVXo4QzY4UG1hTCtER0R6SFhtaDJHQlIwdVQrVTZoSk5hczJLZEF0Mgp4OU5aSDlNMndkN0N0NG1FT21wR05ZUmVORmVFVHlPNU5pcEk3bUlacVdyNjdETk83L1RkamFESHI5NWF0NHpXCjlQb3lyZzhwdkRXeWpKZWZSbElCT21IeWhmd2NHSGJPZnlUWGlYNzJHb3JjZjZwYUhMbFJFcDRMWlpXMlc1bmcKRkJvNlpvK2tUSHRmZXcxZWxvZ0N6UnRRSlBFL2hQNEhWUytYSUJua1JpYkNIQmEyR1UraVowaGlsV2pDTTN4Rwp6SzBDWnMwWGFsTHc3YzhLblRuZXI3SXo4dmlDQVVkL3NSRWs4TUZxSzJFOXpGMmJ4WWV3bDk3WU5PZmNDU0tZClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2RwS2pEUnBaRFp3Zis2MXc4OUMKK2dxSmtkbkxtZFluM0dJRGk3d3JTUzhOcmgwUTd2R1FJY2hjbThwOW45RVpvWisreE5nUWhTQVNralFWWk4wcApxTUt6SjZ3OEkyYnZOVU1ZUUhRQTN1cFJlV0tRY0I5U1JyWkxPclVuNFk0b3NKdngzSFdoRmdZZ0x5QTB5MEtaCjUrblI2ejFFWGRCYWF4LzNNdnQxSitCVTZJVjV6aC9kaVczb0F0V1JDd3JyejMxVTRqK3pPakN3Uml1eDlUbC8KR3Y0WVlwNTVIV0QrK2owUm9lNVAzNjdFcTJUNXFIdnNvREtKcGJxQXZIWit2ZEZZN3FtcGFSc0IxWURjUUpwYwo5cjhwWWxScFpZTXU2OWZRMmg1QzdiSUtRT3lZYkZnQlRYc2lxR1BpMThZbEdvWSsvZWhrWFoyTDEyRWVYSU1KCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbC9WcTVUMHgvcWpXdlNpRXJ1R1EKY2RWK00vT1NFMkJ1TGlLZjRQNFhuWHVUaks4OHRZZ0xWaFZySWQraEZjRnAyeFl0ZTVsTHVDd0Q2aDJMRGdRSwpBLy9uOUhNU1FTbDJlTnUrZURpUjVDc1BkQjJrRkU4Wmlnamlobk15eTVyYjJnejBud0taSXM4RTRnbDdFYTRGClRNRWQ3aEVkejRVTm1VK1NRL3RTZ2V0dk1kZXRqVy9JSWQxcUR0dzRKdHRDM0pUZkdpTGdQNlV2L2VUK08wRDcKeFQrZEk5MDhiRGRKcE9UVTNPRFhsSHBhRXNCTWVhekFDQ2Q1V2JGbzExa0M4dThmZU1VQWR6L1ZvQ0RyZk0rRAp0L3NSTFlsTkRWSkpXOHhrdVdtR3pKL1E0VDlCQUY0aWZuRUhadnpmWXdOb3lqZ1ZGU2puRkJMbWlWeTBwVFJ5CmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdm4xK1JJbE1SQUIzU2EwNFcvNHkKczRQa3Nvd0ZWTTAwTW9nUzBBUnlCWmljeldxZ08xaEV1S3IvWVByenZHUGhWZUxiTDRzMHRNaVBSN1VzSnhONQo4K0pnRGlaSXQrQkRwYnk5c21ZdStCbzRHZVYwQUJpZWd0WkpJRXJNMmZWMWRQRU9qMnN2M0xETUdWVjRLMW90CkR2VkRkZngrWXRndmxuV3I5bTl6ZlI2ZHNQeW9wd2E3TnFXU0l1ZDdrSklBcEVwRlgyOGszTWVZeUovVFROODEKR29jUCtIbUN3Nzc0UW5qOTkyamIya29MSXJSTmROanZ5K0Z3b25FZUZEWW8rMmRWa21OeTMrYU5RTTB1R1BnVApTVkovZ2lBNDAzY0toMXhvcFROekN2S3RrdkFZcnZUVGQvVzhTV21CakpYbEZuNTRybnA4b3ZmNlROZHVYMlBKCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjZmc1NaOThvNk1EK1JMY2lVSmgKdE9leWwzL0dWeS9xZGE5cE1wazRNeURYTUVCVDRUZ0dHS3gxWXVoWSs0TVB4QXZSS3pzcVg4dUdZWFpnY21XRApENlIxaUJqMGZBYjl5QTI4N2EwaTF3RUJ1Rkt4b05iN2I2OGlwRzVnUC9xdU5hNTJLL1VBSGdFTnBwdVhCTTU5CmZER1J1aE1icmhHMmo3QUx1QmFzRlEvVzI0NEU3NGdESU5MdFFHUWlIazRHNnZyUlVIZjhaNGcxOFVOTTlXVkEKY0tOMk8yWlVacjlhSmtKWXY2S2JFQyt5TUYvU29kMmdBRC83RGo1V1ZjM3luMk15a3BvN01pVTdBbU9NODR0RgpkdDgzM1dtcmQyQ3N3OXFIMFY5WEdxWnhLVDI0WTJNejB5bFhBVTh0T3h4SHlQeGIrcm42MjQxbDdOb2tGNGllCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDJGNVlGenNUSEd4c0lmeVo4aTMKZmxxcDhnd29IbUxDRXpTc21rV2lTOWUrQTRGOVVwWVdsWUVLNC8xWXIvNzFjdThlMUxlVmxET1Eva0dkUEVMZwpBUlRwSEZZSXJsOUE2c1l2WWhsb1dJZDhwNUxaK3M0anNqQzVJZ2dpZVhFK2ZzdlR1MWZlNFhyNFVxKzY5YUZuCk1yREpsZndtbXEydnVYanpMYUVzZXhaMU4wWVZkOGJyR1VTRnZTR2FNeDUzQ1BENllNMGZvRC9IVWdrNllpUHQKZFpkR3dEM0ovV1hlNlJ4SStNamdhem1wUGszeFJnT0R6b0tkZDIyWnliNlFNSHJwQjY4NzY4bnM0a1FwZmJPNQp0K2FVc0xPdDFrNEZaNE54N3E1bmFIMXhZQU11ck9DMUl1azdVVTBtVzVjVklrakRkUFY5ZDRvT2JTTUNNQWMyCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWxWWmNTY3VOT2JWSjR4Nm5VeTkKdG5hUE5LbFQyekcyRzNGY01td0dsa2p1UDZrcW56TkNrSnVnbHFyM3ZDSFdwcUdGN2FZRjBMdXhYaC9jbFVxTwpYM3JibWhHUk5EWS9WdENGbTRGNzZTSjNmMzI1WGMyY25CazJ2NXFkaUd0QXVWdzhtMEJ0N1dkbXpxeU5tSHBYCk9oTWFSZGFCVW90QUhQRlM4ZFViRWYzbnZQd0JGb003OVhhMEFmRmhHWXRQVG01UDI0aXJubUZXNWdqWGtGTVUKUTY3c25yZklRbGE1c20rYStvV0JjNzk2RXZ3bktLSTZDcFh4NTdvNlI2KzMwdVpKVzFPc3ZJdGpHdkVIdjlPWApJY0p3OVA0MUN6dEdPM2lQU29XaGZDTlRacmF0LzFjdzU3N1NlY0pBbmlhTGswdjdQRG92ZVpvaUVEb1Y1UkFJCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOC9hMk5oQmdVK3Q5aVhHbGExTFEKUEZreTF2YkZmOTZacjR6Yks2M0lrTml6Q1RtYzZoR2hwL0E2Q0h6SmdNSTZDcEpkZmIxcnVQYkRRTjVGTmV4cgpEd2NNV2ovVVVvVmdHYVR3MjZCWFRHdmJvWUh3cmhPdVp3VWFkaHJNV0lsY2RLRGJIazZuelljMjZLakUyY3h4CmxleXZ1ZUdMbFB4ZzZpSHdTbzV6ZFhSWjJYb0lzYVAvTE1NV1pKeUNlZTY2WFJoUzhBNDRUWlEvUmlETHJmcWMKc0o5L3FLZ04yMmhBeWQ1VTFDSHlEV2RqTUNtOFRQbW96UUhXRXg0dDlKd3YrM2F5Rko4S3NhbFRuVUNhWWx2dQozaDRwaC9ab24zVnczSC9RVlpjamM5V25jbWdaU2dhdjlIUVFzRGZjdFFaOVhRSWtiQmV0OVE1aWFFakJ3a2tkCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkFFK1dkakhUY29WYUNWeElGU28KZzUxY21RVWYwaWRhcWZ2anZwalpIY3JXQUtCMGtOT2xCek1ZU0FaYjBabHV4WUM2TS8vOVNlZklGSEdTQ2dVcgpMeHF1QThraVJzK2tqaEpLMSttcEM0c1lyZVpxczVFdlFOdE4xZGszcHlZZ1J2UlRXWFJZeUF0aEh6dzV6eU5PCjlTeUF3TWplR3RxZ0k4QmVKVmZrUzVDM0NHWWtmWWxtZGlNc0NIVnJGa2pQdlNaTWFpdnRIcW8xNWNpZS9TWkIKaDFPb1BkaFkwT0ZUd2lGZVRhTi9wV1FsTFBISVFwYUNQcmtUUW45ZnozQkhaQzFSWFZocHYxR2NkRnFjbk1FbQpUSFRRVSszT1FXT01TVVNMSE9VaXo2RVN1aFc4MzlNNE92dXhMUjZFaWlRcldablFsYVBwWnFxa0lUWHlMRDJKCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkd6UkZIQ204eEtTbFhsOGJZSmcKVVZNcWFwOXhOMUlpUDJ5aFg1MEhzYkZZOWVTNFY0VTBjOGdEVkQ1My9mTnZaeVJ1QjlIa0p3aVorSWRnQ0MvQQpCNnh1N3IvVERDVk13VHpBdXBTa0o3TjkzM3dDY056L2FJSmxpVy9ZbzVhMDY1NENhSkxSTzlDL2FWQ3lRN2Z4CitEUm9VSUxBR1lpV3RtcFAybU5NUHJIL29UNFBpL0VvTzdwNjYwTW1RazJYempXV3MrYVlrSXliZGVmNFh4VGIKWkxmTW5wOTJIRVl1N1pLeFcrRytRL3NreWZlZkUya1VQMXJPd2t1bzZLZS90Q21sQTg3OEN6TlIyV2g1eEJMMwphWW16cXVGWFRsSjNYQUJtWGRKME5MQWkxQ2ZxMHl2UWdDTmoxempOWDhmVVo4c1lKSnpDak53WUlNUTNrV3d1CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbE5BTVJmMkpXNGltZSs3dWIwWC8KZ2NxODdrYlZqeGFIRHg3aFp1Qmd5VnVLQUFzTlBid0pKZzJPK0tQdCtiNWE3amxPNkZpajMrbXAvVnlOSjQ3UgpwdFo1V1lPSmF6d2F4c1pPaEhnbUREMm1zMklZLyt6b0xwVFN4TUxNTlh3czZ1UWxGdFpaemtxbVpLSGdaQWU5CjdxbEQzWXgxRkJac29DWVEyaytSMm5RcHBhWFlldWptT25TYmRKSlBza0RrNlB0M251UHZ5TjRMNVN4dFl2UWUKK0gyQzMwTS8renVaKzBFaXBRY0R6NjlTby9TeXNUeGV0NmxiVWJHc1JqbThvWUZJWlBlSGZyOG9WbWI5ZDFEUAo1MTBSVC93QTU3SUNjc2hqb09OUHdiYldSa3ZkcnFiRU5aVEJRYzNlWjFsSkdJcEhDUWdrbXp2VVc3N1RHR3dVClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXl0amxMOUNiS3REb05mT3VxR1cKcTdSZmF0Z0hsdTMvdk9tYXN6b0RMN3lFSS9xMWlKMnl3MnVGT05Ca2hwNkdCbkRTdXRoYTUyLzB1SittMnkreQpSQXpoMURBSGQ0UmpOVTRYU25RdGF1MmhYL1RHazlKdms2NXZnejhicGZ3U2hmeTQ0R2NyOEV4Q05kTDJhUFJPCkdBSXZzbURKUjNPWlNXYW9xUnN4Z2Z5ZGpDL0RuclE5UEZXSjlVMkhGbFZxOXpNOWY1T2tmUllaU21hbTRaN1IKb2hlaVptNWpZNm12UEMvYW44aDQ5ZzR5MFRiNEZudW5Oc3NzYzhDMEM2Q3ZnZE56OS9QMlp6Nyt1cjMwNnNHVgo4UzBoZG1henhBaTRmZEJhck0xRkt0MDF0bGVwb1RxelF1Z1JpSWNuZ2trOWdTclFaZHJ6SVhZMDZtM0x5RUs4ClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME5TNkZsOHUwYkkxNlBHNWdyL2EKL3Y2RUhlY2JUUzdCTExyRlpETU43Slp3RUtUdndKbXlhaTM3b0ZRY2pRMnpENU1kZTF4bExrazZFUDAwS2VPMgo0Szh3SkFJU1U4SkRxbys0bjhhYzQwdWJwZkpkc0QvdkcxWG1iMlJTZklsTk1KOUVEMXV2c2h6YU9oc1piZHhlCm9FUnFBaXJuYjNiQ2J2OXdmc2YvVlJvc0FrWlRIVDl2QnFxUElPVlJ3U0luN0NTblJ1eFRNbmI5VFhnZjF5VFQKc3pENXliWVl6WXYzK1VCNzFQS2xrZ1poNEhZS1Z0UVZpcXI0ZnBYclA3L2V3U21HVWpnQ0ZXSVBjWGtCUDFMZQpxSGMyRTV6QzNiTS9NYzcvRUtiMllFL0lvOXVZbC9KR3pPeThsemVteGtpU3kxWTFvdUtGVGM0YmhERjl1UXUxCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGZtdkpqazExblR5UHg1TXpza2gKQ0YwdTlrUmtsbHRUWGdLbllVMW5QdVVWUlRHRVRDRm5tU3Mwbm1WVFpnUHdyWTBwWmNpMk5hYkQvWXFHM1NzdwpEdG9uc1p5bXN2TGxidWNpdkw4NGtKV01BYzVaTC93bDZBV1RvS0ZlbDd6TDVuM3M5RDNrcS9yc0R0aEc2SHVnCmEwck9IZTNhdlhkUGxydDdWTjA0cVdDR25jV3djd0Y4YnVNaHJ6VmNCSUFBSjFUd1VqVFhWWHc2dUR4MlpwcHkKbWp0VHptWlJ1TFBEY0Y0MFdEbk9EbjUzNGROQ1pmb1AxQ3VqcnE2UnVsZENrVFNZQUhFVElmOW5pY3pEZkMwSQp5bDZrbG12R3Y3RUd2Q0c0UHBMSWpDL2J4b1MxOFdmK3pyaHRKdUJBY3FJVVplUjZPZk1oYTB1SlYreCtHWDkyCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdy8xYWZVWldIdUsySTlKMzdYb1gKSUpJRWUvb0h6NzgrSCtlOHZER1pXQnJwNDZwaHRrdnZ0czdnNm9kV043eXJWK2ZiaVBPdkR4dXpWUWZoZ1NBVwo0RGM2Q0h2R2cwdXB5RUtOaEtBZjRVZ2JuSmpFamtUc3Z3U2lHd2xydURrWGNaN2F2aVEvc0dnc0pQK3Y1WGk2CjJkUy9vUG1BVzBIaTd0bVBzMVRSWXNTYlp3MEgrMkhqcDJKZDg1WDB4NGgvcHB5TStqdUk3Z0ZWQ25mMm9xZUsKZnVqS0czRXNRRm1mUkIzZ1VCUlRhb1JhQkhEaVhwY1hHWGloejV1TTlqRXduSWxCa0JXc2R4ZmZQUjRmRWdVTApJZFFmdjArcE1Mc3hDTWg2Z1pUY0VidmpBVERVZjZSRHJhWXlscWU3Ull0TVBXZUwxUUZrdUhMMTZORU9sbkdtCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEFJK3lyVFpqeFFyVm5mWFN4RFoKVVZ0S2l5cnQ1TjRobS9RMFhvZTQ4ZDlQU01FZVlLYVZUNXN1RUlpK2pBV2hsUWNnQ1ZOV0I0SDVNMHJzMkpaegpmcU1KWHJxU0NjZU1PNHpFbGF6Yk9VS0JzQytXSFFSRWluUnc5My9KWkVtNnRRWW1uL0tVMlRINlAxSStaMFlBCjhQdWFwMGlKV01WNmpmM0tMWElhaTR6cWh4OU5SOTJMZ2c3bXAzbjdlcVd3SjZLcWllcUNiVmZ3UXJGS3lsenYKd1BPcXFEQXZTSnMzaVFJMC9qb05jRnNQUDNWKzgyQ1oyZ2ZCMFEyU1dlY1RCVmJzMWU3anlGam9wbFJhYjhHTQpQMklaQzRoVjJxam02bUZtWnUzL0dVdGlVS0ZBMFg4U1R6cnJoaDJMTDlSY2kzV2pxbUtXdE54Z2QwWDYyamdYCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXYzdy9BZVBpQ212Rm1PWDZoODIKL0NCdGt2djUwc2Y2OVNMbFpCRmxDNlNscFh2U2xwZXNrZVN4d1NjZG43NTRuRjZ4UkdQTUt3Z3h1QW5FK3pvawpSN1hDSnJadjV2LzlmRUFuVlQ1Y2ZWdC9kUnpHNHZwaXY2N3RWRFVSc283RXpWTEF6UXp4dlBOdXlOMktvN1JsClhDMzRHR1AzbFhTR0ZYSDRPOEJJTnIxZVN2UU1WZVArejNYRzBaNFpjMVNieER6WE43SVk0L0ZFUk4xRHFPRTgKRDNHQS9oMzhHdDNWbWFralVFbC9QdFdCV0RqOFhSWTBmay9sRnA4TEtXQWRNV3doY29yUEsyTllqU2NwZ1JBbQp6WkhJV2xXaVllaVlFS2p6a1F0eEQ1cmZNSHdQbll2emdueDRaeHZncmxPRFBhSkZIMnRnc08yWFhXd3BUQ1YrCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUlIenBQOVFmS3dDa0lnM2NhVEEKdEJDSmg5Z3BMT0xHa0NTNURjSUlmTnNxRnJyR2diRGZxR0hPbkhETnFUcU1ya1Rjem44TFBucUZCQ1k4MHA2awpWR2VkUmhHcWk1d3pWM3JmVURPcmd5OXorTzZGUTJVWkhqdkttYzBmRTIrNWRxNmFCRG0xUWR2RUM1TUIvTHJsCktWRC82NURHNUM0Ri9JWXhGU1ZYTkVKUkpxT0pxQlpxOFE5dWxXTmdQdmFHelk2N2Vkb1pmUnJPb2lVSFo2Wk4KaVA2eEV3cXNoQWUzb2dOZFE5bGhQTTFPQURjNWtsV0toeWNzeTZRaDVNUytQVjVSNk42K1llVXZRekRodEd2dQpTTW4yMFdQbHZZbEduejBaWk1OOUpYV0pBNHl0U2U2enBtL2g4bnhVaDV4R1psMmtnd1pHSXUrRVVaU0tNUnJaCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzdXVGRBSldnc3FzWHBxb0VUVksKVi9sSmU3ZXVQMlM3ckdUNTJ1eVlaaWNwd3Z6RE9rVG5oMi94SERobUtpclpKbzgrdXhWTmM5a3lKdjZERmJOawpKYUdtTis4T3BjU3VhL1ZDaHh5Ymk2UFNwN2kycFAyVThTYTNLaTR2OHQzRTAzRWM5dTBvU2ZxNWdPaWNvd1hBCmcwVWVCcTUxdXZmbFM1dXZtV29zTGhyZDlsT3UybHlwUzkrOGRGQXg3TG5EWVN5Y3Uvam9oRlZsV0RJTFFrSHEKOXJaR1hyWnovOTZrVlg1czhEdFJlcmloVXlMR2UxMWJVRjUxQjYzUFhuMXgxeWErYktBdURzNlErMEdzZjZOTAo2OE1WUkF0a3AyVVVDTTQ1N2p4M0NISGhuTTkzS1V6Q3kwdXg5dXpNVTVSZng4czdQa1djQWN2NnhZZmdhdmNhCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkExTmVxMXNTWVJtL2psQXZvRUkKK1AvK1ZFbDE1ZWs0QXc5blhlRkNTZjRVSGFZQ1NTenZxOVA3OG5GN0dOcHFTek1TZEFnM3ZzWGJBUTFndmg5YQpLM00yU3hJR3hIcHpPOFdQQmE2cEN2NGowMkd3d203Z0FOczZWcERoNkRHRFBZSmtkdmdESkxnRTg4cmhvWmx6CkJxcmY5eFE4MWgyYkpRQ05TRXRJY1o2S1NnUHgvQXNBcDBpenh6NWFUUGg4aGlIUGxnK2tXT0JhZEk1eWRRMFMKdW5aUDVwQzlxSXdObG5MRVdPcktQelk4UkxWYlhhN0R1RzJZZlpVNmJwQW1DVlpqSXo3eFBSQVoyUFRibkoyZAoxelRmY2VZRmhtZnJKOGtSczRtNnNtU1JSSHQwQStjNmwxN0FTTnI3aHR0NG81dHJIRDZ0azNWck9BZkJDYmdiCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmhsM1Zjd0tOU1ArWTI5Y1RuQm4KREl0OWJJdXYwRGtqYzlSWkZ1S3JWWURURjB3MmVSZHg5QnZDVE85OEFPcWtQYUxDbStXUWt3TUV3d1RpVWkzaAp4MHZVcUFNZVRSTnQrY0RJb25QRXMxcmRrNWtMR1JPWG9ka3hyMXYxWjJ5M3hEdlVxSGtLT29VcUxucDBQSTVTCnNsU0pkdDlxR3pyc1JYMGZTTTNSa3lxd2RvZjVWdno1TjJBZmo2YUl4eDU3YStHNGZ2K01hU3poSkFyZysyRXgKTWFYRlFBSVlRZG1qeklVNkhIanNxR1ltbmhIK0tBelpxdDhLdmRqN1lTRnJjTm5CM2dNb0R0cHNyNDVEZ3lNMwpEdXJiTEdFS0E5aGwvY29lOThYS3hjU0JyS3NHZmc4TjBwUDJKVTRrdGhxRVBXUGVTYUdkU0tDOWhaMHN1VmF1Cjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1RNOWRtOUdSdnRHak9NcTF2YlcKTU1mdkYxME5DeElGNGRiakpDNFJzUVZIQU0rSTdscjFacjg3b2txWmNpb2NtcGppVzBVN3ZISjBzeWxUZk16SQpZV3JIaWI1Ykp0Q1ErRHBycG03anpwUzA5UnlXSVg0WHVRODhhQzREaDZXc2RkTFlsemJsZTl5dWxoMlN5eSt6CnlCc29kWGxxYjZ4eU5oODVXd1I1RktPcC9GS3dSN3VzOG42MCtWSGFDRnBpTDNZeWhndFVjaXl6RGMyRklVZmsKSUJscWowQ1hwVXpBanF0eko5Z1NtclhybjZXc1pYbDRWUzMvSWlQejA4MUJBbzIvWEtpbjBqS3crR0EwaklhOAo5MkpKS2prbEkrQlJNbEhtaEd5WnVzanZVQXFJdGk4bERFUEViYzl2L0llaVUrZkJLN1hudnZQZXhEb2ZJbGpBCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2p0ZGFKZ1FuL3JBVmxPL05vQm4KeXJrTlg3akZ2K3NtYWJkeEpqL1IvMWdpWlRjZmpXSzVrSy9vbUY2bzUxWkl2QTFhNUdkejdFTmIvTVRpUTQ2RgpIeXFiMXpjOVRkNlQ4aVhwVk1nUFZwc2FhSWhJdnR4ZVBpMnEzSHh5OTV0YisxUm8yVEdTNmFPZHBVRXdOYmZzCk93ZmtUMDErWHByb0kzcThJR0JWRUlmbnJmN1orZURrR2Q4WW96eWMraFFsN1dvMVNvK3k3cVE1Rm5CQzlxa24KdTlWVHFrSFRGeWMzM2hkL3RzTW5PNm1TTm5ldSt6RnZJVTdZR0ZCOURqdGxTY0hOcUVUSWp4d1FyL0FNMmZkWgpUbllCTVFlVVZnVE8yVzFBbU9VbFI2WnQ0NnJ1REE3VkxlZU44YVM2S0tQU1JsRXJYRGxRODM4YnV5MGxqeXNYCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXBleDF6OHlaOGwvblFkcVVvOFQKMkNHWmNDUjk0TVFXa3BrM0FkTjFaNzdHYUJPbGxTS2M4bGRlcUtDMGt2dFUwN3JSWFdUaVhoby9QSnlVbEZiYgpHN1J3ZW82dGZRc0JZOTYvTHlVdWJhcGdZR2llUVBQQ000Y21jZXhFRGpKTEw0WlVaSzhYRDk0S1RqelQ0cVpICjFTSk5vbThEZVo0dklUTFJRUmI5bjM4clo3MGZ4NFc3U3AxYTl1dWZ3cm1uLzkwUE1YNmRrNlJSc24yZDRUbFAKYyt1bENTb3VJZHlIM0oyWXRyRnJIQVloL0I1bGtFUElTQ1I5TDNVbjVRdUkxVnFxVWxhTXpnVXM5Q1grcHpJUAplZWZVVFdaMWFtZWxJUnBFY0o4MGIxYUgzcWlWQ1dpTEZXdzNXcEIwYXhhRkZJR3NKTWxsdzZ5N0g3cjJLQzdWCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUVWUDQ3RzJRMUZYRWc2ZHhraTQKT0N3K2N2NEp0YkRMc0VwMjA2d2xXdVBXQW1RcGM0d3A2OFZCZSt2WkhTRW1aQ3cxVk1EWklMNUg2RlZtWGNSLwpLWEp3d0pVV1RsOHhJMUhFU3phRVFUMGoyVmltNVoyV0VGeWRlRmhUSEJWMmFBYmpDTWtKS1lmYWFHRGdVWFMvClVnUmk4TGZuWWo0WjE1Wld0SzczSlBGSHdKOEFjdjcxcnZOMGVDWWxYeDBlcTNNSm4wa3UvTGhvdGh1QlpKbDQKZ0Yxdm5VK2VEQWlSVDdFR04xS01Bb0lweWsrSjZkUnhpYi84L2lZQU91bTlpUnY3Yi9QSWJCODFCR1hmVm13VgpTNDhxQzJxZWE3Umc2djZXMGJuOURPY3hPRVd6ZXhRRnV3UjRSUmxQVi9vMkV1MEpnVFVKeTJvWkFDNlFSbmpxCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHJ2TGlXYVZCS1BQM0hwTU9lbE0KODRqVDdhU0ZrQ3R0L2xXT2ZvUS92TER3ZGlrc1Y1SGNOTjVZVWx6R3NZZmF3Sk1aVk85S2gzVmEvVFpnZ0VJNgpDRURGUi9BQTNHQmhGZG5NU3RBaE9FTlJVMllnUjExSHc2RUJHRkNKM3ptRUYvcWpBeTlRbjVYWFFJK2w4YTRoCnBtcm1DQ0loQW5zM0hYS093K2pSTEtaRjA4a1hIRmVqeTZFMVhBMGMwL2ZHUWF6OFU0blF2RnlHTGZycnpiaEYKc2FTVHRTZDZ6ancrMklXZ0pkMWVPckJWL2F3eStEQlNuWnJEU2E4VmNwMGRFL0VWYlQxUGo5L3JFb0ZJaVV4ZAphOElNZmp0eWJCME9wN29ZSE1FZGN5bkFOVWhFcW9mV0pqTFY2UUNYQlVENEJPYkVTSVdEWWRyc0lrdHp4aUFPCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelpXQ2tHNkpGYlpxMytOL2p1T04KM21od3J5bEd0N3lTNEp6b3U0Y25zV0dFV0ZzUjhrKzNUaWpIWnljN1ZJeC94ZGFxZzJDRkFsS3RhVHBpMjF4cAppM3FnR0RydGJPNGVTTHgwSjlGdGdNMmFCMnZzbVRNTDdFTU5pV0hER09yMjZqSDZFOXB1enl2SkJRTmxydDdaCm02L1JpWEdudXFsSjRyT2R4UXJqaW8wOEVTbjFZMnlSVDN1SW02NUlzclNpbUZwU2twaVltM2h6QTZkVmF3Q3EKeis1UVNjR0xHQnpOY3hEYnB4eWh2MTVQdjQwWE5BL2RHcGxmTExaNWI3T08rbXRYU1BQZWlFQlZEU3I3QmpEMApZOEVFZEZuQ0RmY3hJVThYZG9Rb2hKMzFvRDVqQXBxa0VTZjN6UmZma0hqNzcySnZNUGZzRlRnenhJMUhsQkh0CmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjNKN1AxekZjLzh0THlJRXR0VEMKSUNKZG9vTHVjREpkSU80UTlpR2MrS3liZi81alN6U2FnVmUvRUo1bUhiVDBIa1c5ekF6YjQwZTNnSTRLUFdTaAp6V3l3aTFXWW9Cc0dJTy9pRVlYTE5vUzU1R050VU4zVXpzVzIra09PWUx0ZmtCVlZlT0k3QW5oZGtieHg2SDFLCnRjZFFERXFsTFkwQXVYMEcrZ0JiQ05Nb2RrOWducERxdko1SW1rQVlGSWVJVXdrQXJTWXZGZlZ6dUgva3ZUTkwKRENWR29BWnJPSEwzQ1BlUFFnemhOSXUvdVNrYzBvVTUrclBoeit6WUFxMVVDYlYvUFRmQ0Q2Z2o3dDh0QnlqMApJM0VJV3hmZjd3ZmVsS2VHd2d6M2tNcG1iM1FodHNrNldsTnd0M0REVEtNdHpMa2pSZTNHZHk5M3ZxNWdZbkwwCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0tvZkZqSGFHZjZ0djQzMk1UNk4KZi9RMFdSNkIwTmZYaVorZHkvWkVOTDNnTGVGSVJ3NHRob2Vlbi9VYW0xNWVDOVBUUHYzcmV0bTJLSytnTkVMNgpiWU56QUZIdzY3UEZJaFAyR2dacERmZFBnTit4ZFE4OW9MWTdjUWNmZytWTDNxUFFkSXIxeWFnK01rQVdUMkcrCnZJT0NzM0RncWJyRnBpaDJMQkJRL0N4UjlXZktlK1ZSVDNNNktDRktHSlZVVXlCRHNjZy9QZTA1R0FmN1lTNnQKd2hRQnJyTGgzR0VvbEZPczhPSzZ5Z1RBK2xlcGFCL2ppbXVoSUJaUVpUV1dYNERCalhlQ1pIbXdWckhra25XNQpPRjI2R1oxbVVENlZORHRNemdEVnd0YmYwWU53aXgyODNTTmRFd0k5MUJtTUhQZXRFYTd2S085bHV4N3RGQXZKCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK2VTK3Mrei8zc3NMZkRxSXN3eUYKSFA4aU5acmRhcDA1eXMrZnZINWNGVklPRVdTVVJmRHpCZlJjU2xvdk4wT2N4aUZRRVNDbGRPWEpEaTVCc3krYQpTWVdUdkk0Y2xYQnRmYmM2b01iaERsQXZWdlJKTjdPTHgzeVpPa0JpWDhLQVdCMHBYVGV2R3M5MmdVYXNNV0FMCnNnU09LVnlzMlZBTWwveVFka1NITnovL0x4ZkdkY0RvMXVTVmhId0hvd0dHOEtkUWdDSWw3OVBPL3daTE5pSkEKNGFwTjNvYldHMm5DNXJxbzJXODBBMDAzcWhwbFFtRlg2Zk1VWDd1dmV4UjZmNlRzNmw5Ti9KcXNEaVhmQmk0VQo2bGw5dW1QaHM0TVpOSldkaHY5ek5Kd3hlZExtOS9NOC9JNlZLMFRMUXp1ZHZ5MW1lYVh0bEc4dWNKRGs5V1J5Cml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzZLOTNmZzJRMmk2S1ErT0NNTysKcmVUQTRSY1N3bzV6QVpTUkw3TGRxVmxWTEJKeCtIaGNCUlJNL1FJOFE1UUovNzNOOXdIQ2JQT09iUnRyL2tBWApZc2R0UENtQlVVUzl0S28wSVc5RG15OUlHa0RMRVk2c3lhZ05uQjYrczVtL09udldib2pYWC9EelB6bHZMc3dmCndFRldvNTRkWVc5MXE2S2tWOXhEb3lDSHA0SGFJd1lvL2l5M0RVZFpYdjd5U0cydlRwS1Y5WHJJYnBqd0lnVFkKUzRzQWtJd0N4NmJra0hDWGdEYm04aGpTSWVpdzhZWnZrN0F4VlJEYms2bm5aT0pDaWRHK25mL3JpcGFEZFp0NwpkYWdtWENJRVNtbXJ0YVpZQ2RqSllhWnNQZGNmSlRMeFM0MEVRdkhIanBSaGdybUdKMHRnODVTVVVaYlZZOEJpCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUNrOGtveEcrcU9nZDAxN1JFblYKdmFHRkNyb3pjUXRLOWhVWDNKUFBQeVlRS1RHeUIzQ0lNL0ZZcmczRTcrZ1BHZll4Q04xaFBkYTByMkZtMzZpZAorOTNxUjhBQWdOMkV0SGlsbW9mbWJNc0VJSkhvOEpYWVNZSjltMTM5dk5QdDJDMnJSTm02V3R2MnVOaFhuQWcrCnl1U2d2ekMrRmFGemdVWVFEN0o4T0VmM3RhZjVVTnF4Z0Z0bkhPb3hwUUQ0SHBqYjRLbDNQVWFqK0FaRXFZTmUKWmkxSTh3SFBPdzQxbTVDMEFwSmdjWG1mQTRnZENyU05jQlF0RGdFM0xPQjdPVkhrTUhHS01FSFBrZ3pIc1FYcwp2eWp2MUxjYUkvWHVFU1EzMjAxWnZKSkprZy96SCtKZ0EwQ2hjNjlLZnFLUCswYitWRVNnMEk0RzE0blpNcldPClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlZpRnNKMTYvZEx3VXFsRDZLRHgKOHk3K1kyUzN5UnFOWkQrTC9xNlJEa0lya05YeVdTMmtaQWFXc1Zpc3U2NFo0c29zbXpPYkcvVi8ycmVYR2JVZwpadHpHSWM5VWFJR3JsVDNwRitIWEp1eDhmOWVHTElHTExVeHNPNjdjaEo2RVJsVkZBRnRIa2x3RytiYmxyQ3drCmF3MjB2enFKWVNzcmVJN0V3cTBFaXgzSlFOSjZJemJGNVBYdkN4SWcyc3JuellKQzNqK3pzU2ZQMEpxTFVnU0sKWjBEanZ6Q0RtamNVWjlNTm11RWhFdDkxVmxjbDBJSDRKNjF2NnJ2eU1zMUFlYUMyL2d2ZmhINWdsNGFCQjM3ZwpPNHZQSUt6bkk3d1ZIOStlcWFkQm8xc3BTSVRBak9pNzJmTXB0L1FZSTBBUWx4ZDB4QTNRNnlUYUp3dUVBS0pOCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHpmdk05OGdMMVF2R25aelFIOUkKcmpqVHl1SXA2ajlHc3lWU21hUGV5K0dGS01sdVQ4cmlvZngxT0wxbmE0MUU2dnZVS3I0Q2c2Y1ppREVQOE1DVwpSNXZGUXFyNmcvZmlvaklNT3Z1TGVGS0czN3ptbDdkdXBPcFAxWnIySjlkVW9HUlBXYXZ4WFFpdXhNM2NHb3RsCnlGNDE0L3czaW9xVnJlbW8zL1pEY0kwUVhLRTFibmxiU1lESEp6V0t4ZjgvcmVyNEswNy94VVUxWWUreFUwcFEKeUhuZ2tsR2x2V3poV1FHQzRsaXVMQVVTOVVLb3JYb2xBampib3pVU0g5UWQ2alRIQnJqcmcrWmp3bnJqMjJRaQowb2dQUVBRaTZKdWIzOGhCcmxHODBpRFAvMWdBSHpMK2ZBOUh5QlFkS1ZxZUJoMVRDQy8vdmhRQ3BzZlQvMElQCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUJOeFFZTkRKc0syOEhVSVpjSnIKek9mYi9sT2oxSTRlaEZKSDlBQVk2ZUx1cEw4bFRIZlV2L2lNeS9RSzFWbmt1ODlHRnRxY1RZcm1uUjlIUVpsNgorK3JEbndCMDBTZE1qdmNpRmdyellmdlZUVlNoRjFtdE83RG5TM0dNZ21ZRGZGeExwU0tVYXNoZDZWbW1iUEpGCjZYLzlOOFEvdFd6Wk44bGF3a0VjSmYrcW5KQVM3bFdzU3dGUURaOE1hcncvUjFVQzloRHAzcUlmcmVtVEhuWisKZU9oTGR6OG5HNDJIU0NUYWQ1ak5CNFhmb0RCMWVESEpwTWxzZlpGc0pxUzIxZnlWcytoTENYOXNibEl6bE56VgpMbUFUSCtyRHBmVGxwN284eWNjc3VKdTcwQXpGZTR6VUxaSVFYdm9iSjJPOUd4dk01OE1tdERkalVwRUNpNGhjCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVhLQktEN251UnRPM0twMkVyUEsKOWh0RkVVZ1EzTnRseER3QWlsRkM5MEJ6R0tkcUw1bTNkeGpDT2k5MVkwOFk5ODhzSldqV25xanRvQ3VqVWFxcApQWHRUZzM1Zm9jTmUvZFJnNXBjTXU5VExRbFkyS3E1ZlVpTjZJVjFNejZ4R3VQNGh3VEtRQ0dxK0IrL3h4VDd3ClRlMmhGRHczdVN5QmdpcFpEWk5kM3RMTkgya3BDV0JLaGFzeU5PSXlHTE5lVnNMUmpmVC95Tk1vdjZ6UWdXODUKc0dVV2lVL3BWL1hFZXUwL01YcWxjMHQ4dlFhSXVVUnhkRitqOWczMmZXcUNBbFFqZTVIOHM1WEpsazlzWkNKMQppTDVhNkdvUGppeVZyb0xvUmdvOERIWVUySXM1Mm8vOVl6T0VidElPay9wVXB3Z1YvQmxzcUUvVy9ITWpadjhWCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd29WOS9uMmhVbXJ6Vnp5UDBlTHYKY3BGM3ZsTVlNWC85cElhRHFBQnI2VU5oZmhKQWZ1SGhLNmVlcXY0eFRoVUhSbEtDdXV2VmVNVVpoWUFEQ293dgowd2JuWHhjS0k4VkUwQmU0eDN5T2xXOWVFekRwMGxIekkvenU0Lzk0UFpVcFpYY3RPUkdkVmxuWHhhR0JqTXl1CjIvYis2YXhkejJkaXk2enFWc3dBZ2h3SHE3U0Q1SGhSLzc2cVZkUHlraHBiQWZTUkZVcy9STk1PdDVFa3M0bHEKM2ZicitqVndSejhDblAwazlCZmhYSzR4Z2p1TS9tNnBRUkFlRXNIM3N5MHh2N1pkdmt0cDFwUEZjeWd0bFp2TgphVGlCdDhMb1BORGVqTVVkaUM0MWRQZjB2VTUrVEMvRUhlSHlYR1gxck9RQmZpdHlnOVl6YWJHT1hQMlhBZ1JOCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGlOYXpGZlJQeE1zWm5CbU96T0gKT3IvM0s3ay9NSzNEb2JzWmNKdzlCTXAyb2swNHp5OWtDMlNvUlprYXVWV3dXdFJOUDZFQ0ZWQW90NVpSUFBDRgo5OW9QZlBCaU5iWkNyU3ZuS0xJTVk5blJiYzd5YXYzUHlwbVUzeTNuQk91eCtlbm1jOUh5dDhjRW5rSVoxdXArCjVUM1BvYW5XQndLYzkxS0t5MGxiZ0lIWUJ5VmtYWFA4UU56dkpUM2R1S2xUNlA4eURzYlpJOGFqL1ZTcXQ3QjYKcm1aZjZ2UGdjeDQ0UU5SZmEzNGRBOGJEanlQN2RkOGU5Vk5QbFY4L01UOE1QZ2oxNXc2MkpidWlXS1l2Tncvawp4UFBNSU9OeVhMeVlzMktxZTFBZ1ZlMVFhbUpBS2dteitSaFFnZkNjQWJsNlBRR0ZHVXVpaEgzY2Rkc3FvVkNuCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHZ4R2pMWWtTUFk2K2h3OTJKbGIKSkw4RzhXUkVVK2VGc2hXWGJLMi9WTGFWMlZWTjVySmR5TDR3UE91UUVRSGViMFNJK0NCSjZPVjBudW9NKys4Ngo2Ums2WXlkanBXa01aVUxZZXJveWhNVExWMklvNVgyRUpPYWlKSGRVTy9YOVBzM3lxZnBLa2lpOFd4a1ZGRExxCnFwVDZSV0EvQ2g5Wkh4UXZqUlU4dUdaVWhFcG92MzBLdS9qM21haWdQemM2cCs0d3lHZzNuaEtLYnJLOHIybzUKcy9sV3dtMnRRZlFBZWtqR0hOWWhGVEI2OHBGcG9GbkZPZzVVNkJSK2YrWFFqS2ZXS2dvNjgyc3E5OGtqOE5LUwo1cXR2Z3Q1QmFWa1FZZGxOKzFTdjFlRTdKdlRITDg5WFJHak0vb3ZCRXVHbUIwamNHdHVzb1NBTS8rVVhIODdICjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOStrNWswd3lTeE9lU2tPWHpTM1IKTk5oVlErY3BuLy83U1VHTVovVEIrNXhFY0JubVQxbTBrY3dNa3RNTXYwc0RwaUlkNUJGMitmMk0wRHFvd0xNaAp1TThaMnpodVY3SWpXTlpKZFVwcXpWaE9oeG4rOFBHSlUrdWV3QmVoUVRKQ1lDZmJxd3dWeXhOZ3VsSHJtRUw2CjdMcHNqZVFYOG4rbFBiQnlpRUNjdmlIRG5tYjJ0TE1aWjlBSUQ4VTM0NFlGM0lsd1BJSkNXazFZQ3lZTFc5NmMKVXdoMHdFUEpCL1JxTHRuMUdxOHp1YmdtM1pQY3laRlZMUnY1UW1vQkg2WU5qZU96S3pnOW1Gb3pjejJaMzdlNwozR3ViTTV1ZXZIYXl1dFJhc1REZnNrbTVpUEFQR2pNUW96Y1B3SG5jRHpHZGgzTy9QSHVkRDBUUC9rSWt6bnNjCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFJ4aHhYU3ZzM0pnMXVLbTJmVlUKSS9kUTQwVTVFL1NzdzkraCtDb3dta2l5SjhwZGErWnhmVzBLQ3ZDTkhBd3pRM1AreVJmSVZGZ2ZVTlBtSWdCUApvK3hxUk9rYXhIY1dSTngrTWI2TXpoT3plaUx2TWNIWmprSDNISGNLclhvWFdsRENOSXJrelR2OFY0MlRtYmtBCkh1ZFJFM2l6ZllBSGRmWktBcFYrZmtiellQQW5SSzUvaXo5b1gxN1ZLalpQQlZSd3lzQlppYlRtRm5DZUJjR2oKd2M0Vk1ka215K01pQzFYejNVSzJoVXpadkJyTll6WFFjejZzZ1QvUEJtWU90VmV5OGt4Uk5iNlBlVys0T3daRwpqQ0M1VXp4bmh5RVBaQzBwNXJPT1VmMHlKMzNEQWt2YUREcjlNdWZES24vTTg4eHRRYVVtU3RGMklwbk9ScW9sCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1BxcWtCd2Z6YmhNeHFPeUVETkkKMjhwTW1lY3hpL0dCam5KWmdRWjJ1NTl6RTdDQnB0OXBNZDhsaDVISzVBbjFENHRvRGlRZEFzdzVmQlRPZnhQaQp3L3h0MXVBeTYwdTBMeFhIbndyZHdzNG8zMUE3SUNvckY2a1NwbUU5cEdhOHZjTXZtZzVzU0xwK05zZ3lvdjhmCjJxRy91b1J5M1RXYkFRRmdJcXNWSXIxNzh0NTIyeklxQ05TbENjOEtVM2IvSk1lRjZRWk5yaTR5a2FYWGpSSmQKbjlyNzlxTVlnUTVsdDlxK2E0clNybjg1K2hLYkZ2cHVLU3hJcU4yMEM2eGxEeXZaR3FGdGZoVlBnN3RWRTVPYgppTUR0R0ZLYlIwM3VlMHlVbDdOaGlHWUV6RTNYTVhKZ21GTkZibnlhWkxPRnltK1hOUkVVNnlFa0x2TElEVFJ6CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnRyVjdxZHd5R3o2NTJ2VXg5VlkKcENlWGFJeUF4OHBtVTBZVkE1M0xCMmVoSWdWRHh3eXg4R3N6ZUdQeW92VWVRQ3BUcTI5MlJiOFZsU0FuWDN2MQowUjFKd3htV2J3VlFKclE0QzBLYzRESDdxOGtMbWxTWkFYWG5HdHhPRlY1QW5za1REN1dETHN5bzM3d2x4UlRECnhidmNFR0FHTzA5MVNBVTJZRnc3Q0FmOTVxbk4xblJGSDc5VEl0Wm9mSUgrVnZUSjFINm9jeFljRkoybVg5RzUKVTVFVW13clNOaEFuRWhxaU93T2pkdHZud1pqblRXblRoaGxhbWQrampTTGQ2RnMzVklrWnViUktQVzBiQXYxeQpkVFM4Z3NKY1B1NGxGV3djNXF3UXVCelhYaU1aNHdYcmZodHJJZmc2QUx0M3RxdXJRSVZkVzJBOVNBaWNjazFSCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHMyU1RXcmVMNTY3MXdjRC9Dc0cKd0pxQSt2UjVPbW0zTmxUQzZPcnpKVUZqaU80dVkxd0JJY2EvaFF3bnBJRFUxQVNXQ3pQRGcyVk5iWCtSTkFJbgpiY3ppcGU1elcwajBHdkkvMmVlb3ViaURxN01PenBPQTdQVDZQbmZCaFQ5ck9mRDVqVHhlVndQWjVoZDVvWllQCkVBTXlSTDBlRDRLejV0TEdUdjVkWnA3aWhGT2pHNXJ2NkFrV1A5T3FUanJ5MHlwVFFIemRvMXh2NU5RcGtUdm4KUEo3UlI2SFlZNlFISnFvNEJpNW9SY0wweGNjdktzZW5NU1BvR3NvcS9BWXgzL0FZU2RyU0htRVFDcjFEMFk2MgppM1FTbmlLQ1cwRmE4NFl6UTV4cWxQb1oxRUYzUjEvTVZwdmJMOGxSUzlMOVY5WVdvM3VyWjZRa0FtMkd3UWVlCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUNidFpEQlZISlkyaHNTdmNSeDUKUzFwVWkwdnc0Ym9uaXRTME5scU9zWkRiZ1ZNNHdOY0w0SG9MWHRoemQyaDl1WDFvSC9SVWNxOFU5MVJBRElWMgpkQVFKb09YM0dlL2ZlMHlVbm1XVFNtekxFNnVtOSt4cUJuckpJK3lxQmg2V0NXZ1p3SzZnbjQ1WnlZZjlNdWRSCmtMbjVjSWpUUENXY2ltWU9jQ0Z3R3hzMmVPNStlR1hmQ28vODZkMUxZdWNUQ0E2NUluSVpGcWo0N3VvQzlrSlEKcmQyMDIyVmQ5QTcybGRaUXRHMkdxb3F5MWlReng3NFZsNjlOaGdaQU81a0pTZmloNGdGV2xJeWs2blY2TnNiZAp6RTZJQk56VnczZE5QZThST3p6Q2E4T0I0NkdHKzhQTm1GWHloVUxub2h5UmZHaU5aZUd0Y0RHQVRndkNoSVVUCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcE15V2x5MjhtWU5pZHBCc0RPczYKV3ZVaTdsSGVvNGllbnh0Tm9wU2QzWGh1OVhsK2RHdllzK0ZBRkMvRUhNRklKYWVWN1UxSXVReUY4V3hqckJjVwo1RVJmU3l2UmRoWXp5a1I2RTNoRkxwL2FQT0VDRVUrQTNsdTJYVDgrWmZQUTZxRDJFbmRiUy9zUlpkSEVOMG82CmJESXNRRFV4dmtrY09iWkJ6Q3FpNDU5bGs0S0hNZzNkcHYxNDZmTXZNU0FPNXNEVUphNkk3Yk43Q0d6YWpmQk4KVjRGbEZ0TWNKVnVPaGM1TUNjaEI2RU1kR3drb3RrRkFtUjVFaHdxNWNHOXJCdFVoeU5xYlh3emEwcGNVZGh5NApRaEVzVlRVTzY0QmE1ZEYxOGpNd0s0MnFMNC9CMDNPU2c0dlZHMDl0eHlZbVBRSG1DWDkzYXVBWTdJL3ExZXYzCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclUvQmNWUVRBV3kvVHRzeU1za0gKWFFMWEJvOHl3QlRnMnZXVTI1ekhKNHI4UTgzTHBnWG4weENFdElOeDNHRExVV1lGSWloRlFBbkFhSkpSZUhqVApqNngxc1grVUYvK2diNUZPMFY0MW1zdlpVbEZNaFlnUzA2VHE3c1pZVzMxTFIxWkpLdUxVUXRYRjZoRDF0dUdGCmF1WVhqZ1Q0RVNYVWFsUit6N0ViLzJHVU1qOHJzaEJodGZKblM2TUVaazJ6NHgzbFp6SDhVWGxWYkhQbWpLb1MKWVh2SFBVbmVvVVJtbnZCTk9kcEJEZkc5OWlaZ0FsUDdwM2w3ZHdtd2tBYWNidmVUSlptU3dqWGJUNEY3ZUxrcwprMzUwZmxMVm0zNHI0dmhabncyb3ZQbHRiVm1Rbm5WaWk1SHIwT0RBeXkvOVNtY0I1dkhpZXkvd2NOSzBZMGFUCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOXNTd2h3cFBOa2ZteENNSlkwQUcKV0FlQzhDQTEyM3JkRVVGRWJ6a2lzSGMvaXlNWEozY0hSdWRtbVc5S0R3UWVsVk00czB0LzRabmNmbU5hSENDLwpjeHJqRjgrV3VENU00K09DS2FDVk4yNVJWcHY2QTdpMkR6YmJYNXVLMHZGWmgwMFpkanVyUUVEUU90ZWw2T0dUCnhUR3dDMEc0U3dNUG9vNDZzQUxPZjY0NmpnbFhIcUJ6cVc5MVpNRjhSK3BYbXY1UTJNUGxzN0RBSTFkeHRoVFUKc05ESDVjNDBFbEdiRktkaHRXZUM4VEJrSlNJc1hZUERnNnpjNmc2Y0h4M3I4Wk5nTkd1YXZHdy9RSEpITllNbAoveVViSFpJczdxTDNDY2p6RzVDNHhKVkhNUldMSnJta20vdXNGNU1PL1VwNFJ5TjZhMjYwK01uK3Q3QytpcC9NCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkFIYmZORjhjcHlJdnVsTDFiY0UKL2hHRFBBYlhid3NYak5OYnZWZnpYVE8welh2UGg0dE4zNm1VaTRJOGx0WnV5TXRTWUtVb3JxSkZJQzY0MzIxUQp0SW1GVGhCY1FIQm1OekFGWFJFNklsdDVDQWVKQU95ZEpNUTR0OEQwK2lJK1poOS82U1V6WHBDaVFFUjZBQzVECnhlQ3BOS1Q5Z2NQOWwyUDgwOFBHbi94Mlg1eWt4NytvV2JyamZSV2Q0ZE9KZ1JqUWJMNkZZbHJ5NWhGM2NFeVMKdzRPV09aSGF2SWNTcXdtUW5OcWhGS21rVUQ5MFI1cHRDL3Fnazh2bDd3SEk2YXlwaDk1NWtidFRDek5lSlVBVApIYk1td3pGYVJNV0ZTaUk2R3JDa09Sd21uc1ordEZ2UUgvV2pacWxIallRTGpUdnEwNUxDVkdsWTN5bWpHSmNOCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUJUK1JLNDNYam4wSHczNU5VUUsKS3ZSUGdYbzlLd3gwRTR3VkNDVUlJQ25tNXlRMFhkTWtNNmRKZnl4MVBUMTd5MU5TWGxqSmd2UzZXU1JoWG5jRQpnQUYxZTkrMDcvS0UzcUtIeVNLRUVHUW5tM3NQSjZrQnV5Q3pnVWxnSFFkRFNFckk0STFQUjRBK0FtaFpHVFdtCkh0dWJRTlRZeTNCWDZWd2wyUWkrNGVjcDFqdWdrQU5IRE84bUliNlpSRHdPY2VlbExKSm55YzFROFVxampSbWkKajhUTmU5ekh3N010dW1rekxaMHRVLzBnMVZXemp4Wm00TXArY2tiTU5sSzFkaXRiT0JiQUdRZDcyZ2E2eDdtSQpiOTJCbjE5ME1ETFh5b0FGeVNrb3pTN1h2NWN2bWlrZWhFRGhXekdqZUdXZUxmbWlTSGRVcDlSM0hFK051bk9NCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemowdVE2NTVTRDJQUnpFZWIwTG0Kdnk0NXliNXhOR1Y4YU1wWHNzN3paZVlSeHFnRWVOL3VpVWJ4MzJmWEw2TlZvdEhmNElMUGY3dHdXdUFYM2tPdgpUM3pqTldwTWZWazZjbDBmdEcwQTByMU5Kc2NsMlMyam1PR2tHVVhPdUIvZ1FNeDU2N3ZxcHArTmpOMWNXcTZCCjY2VVRadTNRMlV1TnducXRTNVZzRTFoZVFLSk5MWGFUd20vMjlmSnRHaDdwNHo3S3NTTkQzRUs1QlhLcGRqck8KUmEwRjVBMTFOYnp0cWJZYmVuUDdNZUMvTVlUZW1XV3V0elphMklYNUtPcGlPQ3dHK0hINUpOc0ZoYWhUNGtLbgowWUFVRGZ6SmY0eVhrdWFjQVA4MlQwcEU2UjQwMnJRL1YzdjVMU2l4dkNaRW9VRVNYZmt0dXpNdFVDT2p4blNzCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmdVc0k0TmtCRjU4TjJDTHpyRisKSXp1SmowbGZXUERCMTJORmJ3MTUyOHRiajljNGVEOEIzUHp4UVdidURSMGRJenpVVHAzYTN4TGM2UHY0R0ZrLwpRTVRVMmFuRW5PM3BpWlVRQm9ESkhqeGpML21mSDhhRWpvejFOR1UwQjRKSE56eE1DN2tzS003V0R5QjlHMWFLCmtqSm1TWVBCZHVwSXZWYXJxTXNhTGp4NHhJVE9wbGVHaWdEakNKd2dJcjg5QUFsdDRCb0NmcG1pZGVVclZzUnYKNTFOd3RyWlhhakUxajBGdFlPbjl1ZHdnMDJzL2R6aHBDc0RKQXBSL0dmQ3dEY2ZOU1ZFN051U3ArZzFrMG85bQpUUUhsR1NwaTU4Wk92TzFaZmV0WEZMOWYrcG1hOEFYZTEwWUlkTFQveHlLanpnTWw2eE1WMkVzcldWUTdZdjdZCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemtKL0h3RVE0TmlhSFFuQlVNNVAKT1JrQ2dWakYxYW9xdnhRY3pmZGdLaHNlNU5BdWdZMXhkME8zdmx3VWVQNmZ5V1hkRWw3cUEyeTZJSW5wSTBQTgpIMEYxNzdtUW5JMHhSbzJpS09nVDZBTWVwY3BOcGc0NmIvOVBaNVFmWFQzWjVadjB2SUx3UEcrVUl0S1NFZHQrCjVyeUVsVHoyL2FFS3BkSitleSt5N1JCeGNmRC9YT3ZObG11ak4yZXBtWm1uUlVzTHJEUzE5WEo3K2NDS0JWeWUKNHRNN21na0dQM3NTd1dtTkI4T0lESWVkOXliLzlOT0dBby9saXd3c0Z1c28zYUZlb1NpSEU1dHdLWVVRSGRlYgpQMjVIQ21BWkdaSUtEaCtpeUR4VTNhaWx6dXVDSGVEOG5BbXYrU3FlWDVYV2VSS3J3NWdBM3NwRTNXTFZaWFdhCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGpGTmdmN0FmQUUxVG13cUJDcnAKU3RuMzlJdUY3TG94OW1hOGRHS2N3Z1hVOGVBZFNXRklQWE5WZkd5TVpUS2JKemszSGxPQWdQVEI5WnlBZExpaAordVprOXhXb1kxNlRWOHdvM2tzU1J1dkxJRWVsQmd2b2J5bkRiR0VJREJ6RVVQVjFGcHJzVENKQkVDM1VSeVNhClozakFTMUd4a1kxdkhiRlpQMEJRUS8zSzU4bTQxM0dBdndjQVdEcGhhRVk5aW02NjdvQUxabGNPU2JIZlUwV20KOTVVU3Zyb1JSdThzTWlvK21LTGZuUzFaQlo5eDZTUGFCYnkxMVJHZWExRkNDdWlHYWNOWC9SZTVPWFV5c1BwVQphMDUzWk9TUXJyaFJhVEV4RGhMcnZ5ZzBUL05GK2xVclUvWG1JVS9PcEE2cDhLTTFUWUJWMWRkUDBRbXY3TEpqCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWhyZ2k3cHNVZjkzazNRb0RwMHIKbHhVcTEzRnFRSzA3WnhURE1PWHNHcWIyY25VWG1PR2hMaTUwQk85WmFZTXJGWWxwVUFXMlNjU012ZEcrZG0xTQozR3hvdVp6YW5OTVVOTGQwUlNQUDJObEpaNjZIbUJRZmlhYmlXTjVpM1dSRG42YzgxbGxob2IxTVpwcGpWeitOCkdSVGM4YVVzVUVwQ3dPdE1RZGp2U3RyTC9FQnlLL1lHc2ZIOEpzcVpMdmY3S05YT2RsY2szTG5ER1liWlRyRTgKU3RIOG5paEQ3THA5dmxEZlRlcUE4MXRUdTN1MEJCWWlhVGZ2ZmdRNXE2NUgwWmlwM3ZuKzJ0dTk2N2FnY25UMwp5TU9tVzFSLzVPd3k0d1pKaE0wWVBlRkFaaW9yTGtWdFBXNXVIOWxUbmY1aVMzV2ltV3FVRUhEeTN5Z3llL1VsCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXJ4V3hLUCtSMkNFUWQxZkpVWkIKalV1clk5ZjhINEhpK3QzeG1vNVhEYmgyRWp2Z1hBU0lVM0NyM1NaZ0VxRU9zZXBCSU50Y0F6eldTdkxMbG5TZgpham9TOHlKTFFIQmZzWmFJYkVCbkFnTHVBbitCUXFITDVyd3ozaGtkWWNLTEFnQ1prWEUwTm4zNE1OdkVrN29HCk8rUWtHb3hGOWZBYW0xaVFQaFlSR0ZWRW9lMmZKbm4vYlJsZlNURzdKK1M1VEg1WStBTHlzMmRuVlJuaU4rekQKMFpKb3VaYkhQdldxU2hRdE8raDU0K2tHaTBZYitXT05MWExaR3A4TTEvZU1saEtPYWFYNk81MEcxU1UyQWlDdgpiK3NvOUhkYysyU0V5aHgvUDFteEpxSTFKeWUyb3NMbUN2MkFTYndONEtJOFF6NXVVMHZPWjNWb0NtcXZ1dWd0CkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcE9aOWVpTHBaWDgxTnBKUzVWNlkKeUtUZzBOajgxMUlVb0l1b2FFNFFvbmZnYVZvOHJ4SElnbU45NDNMZnJjaGcxMXNlTU9TbmNvcDlQTnhWekV4eQptb2Ryc2g0R3dGdHNTS1hvYmdrQmFqMS9zMXJFblp2Z3dCVDJGRFoyYVhKS0tUSklsdkNEdmVJaWh0WlJ1UU5MCnc3cEcvbUpIQ0JRTUUyYklPQzMvdUVSNXh2YWFyWUJVWncvNm83RFpvUjJhQUhndUQ0VjlaVHYzNHpaVGtUSTMKcys3YUNUcWlUY21WaVVZaG5SSjNYN0FkdWg0SEErbUdtQXFQY1VrR2YyY2lRWVpQR0o0bDZ0akdubU5vOEprNgpocTFVYVZ4SURpYUtQeXdzT3UrNjArSXFseWMvYnV0a2Z4VDRydGpuUDhRZGZQcitpL05vdXpGUHBsV2VickpuCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWVJdXhJQ2c3ZzlmLzdncFhTTWEKS2ZpVjJjWGVHOERpQ0FxSGp5eWlYenExTWN0TWhkbUJSUkoxSVpNdGxwQlpjZXZXeklPWjU4aWIvVVgzbzBCSwprSmpUakRHNXlMNXRGdXBrSU52YUh4SGRMaGQvVlhjZk1Dc0RBNFl3ZXFtOEhGZEorWmxNQXNmWjFUaWFmbjdXClRvYXZiSVkyUUlTVU5ERm10M0FqOUVITUhRRERuMmt3a2lBVnZXZTllTW1rRlFUVFRJUkJmeDYwdGJNUS8zeEgKcncrcDNHcStVckNiQjVEeFhyZVFQdVFCMW1mZnVYaGdNOXJQNjlNeTUzZllucnJPYVpRa3pEeUpVUk81VUZnVQpLaS82YXd5TCtBWEErMzk1UXdxbk1sc2o4dzh4cS9QdjVKb0MveWhwcUZTcGtkS3lQYTd0Tkt6MGtDdGpsUWk5CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM09reWlpdkVxd3YrcUJWRkVQWUoKQU5FWVpzZEVMbHV5QzFycGppQmZwQmU1MzEwNStIV3MzUHJUTmJsbVNyRmpURnNYTWhpc1E0bWRxVVFzdFJrOQpCM3pNemZxeGdMd1R2SWQwaFZCVE9Cc1BGc0MxYkxBb3JqU3ZvREROM3owTndtV3M1T0dPOWdoUzVLdVVkOVRRClZDelpqSW41NjUrUkJ1ZzgvRUtsQmQ1amxmWWpqK3F6cDJ5dXZyakxxNHh0VmRqRXVVL3FrQ25kLzV2NmQzR0YKaXRhT3U5bWpVdStHNENrSmVZTmNRV2xSeFJHL3N4TDcwa2ZHc0Y0MVpPTGsrc3Uvc3lGc1R0Y2N1V21ueUJIUgpkLzFIaFBlYnc3aUprZGNmMGFSc29QaUtlZGpIOTZyeDltVUp1VkNNT2pIL0xmdGZXSC9HQStMQW9hNTNXOW1kCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME9UNjRVQ1prS2RQMXlTWWVpZTIKSmQ5Z0FwdVpQU1ZlUzBCQUVsUkpibVhOOVJDSnczVXlnVW1NaXdxcXRkYU0wUHdhY2UzWFMxVnVHOEsyYlVweQphQXI0LzNSeVYwNGFNcjhtOUtJR1M0MjNhRmZwbjM3cDdtVEM5Q2hrUWFnYThTK3Z2c1U0WE9seHJzb1Z2eGhFCkQ2VnRwZW90SjZIMUh6Ukp1bjg0eGV3czVXaGVQdnhmZnczR1R2cmc4Nm0yVHNhY1ltUVNZek9TTUxISzZJVHUKYStGTXN2dmxJajlRMnpiMWdpR0g3RG5mWVZ5QitnWVY2MTVHUkM3a0xTOTZ0K3VWaURZOVNMMjBLcm51aGV2UwpSOEtuUW9PVGIwRXlPalhOMVNFRjFMK2dYMm5IQ2h0cGNxaWxCbzA2d2QrTUt5NW1zaVAwQmtmR1JoeFdaWVg3Cld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1QyeGtaRWhkK3hCcW5rd2Q3bW8KM2dQcEZiTG9ucWFpY1diR0JZcllueVFuU1RhZWhzbW4yblNmamhONXZ3R2FlSVhmdWVkM3NNK0hsdTFNVDhuZwprQmhwTHBzUFlId2tWS0NDTy82WEtHZHFZZEN6eGVnYmdCOTB0UG9UY2ZFT25yRzJNc0xpQTAzUGNpZU9yK2pnClcvcnhMYkdvTGY3dmJaY3l6TU9aQ0tjbjU0QnJ1NmdjZCtuaURrZ09ydy8zRTQ0ZGQwMWQrQ1gveFRKNnpQbk8KNnFaY2srT01SSENodm1Bejc4azV4d1JYWmJjSkQxVEpWUVlzOTRRZU5jdU5HQTRoMkNlQ3VxL05CRVRoTGRvSgoxZlFPaVdCR3p5SHZIQ01lTXEyUzE5bnE3MkZhaVZGWUcxMTRSclRRajJRbXBSZVpZRWpaS21pR2F6QlpTUm5HClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbG04WCs1OVc5d0RrUFJpNDB6dVEKLzJKanRjQ1BHT3dPdGZIajcySnhITk9heWszR1NOd29QNUovSVdJV1JqUTZ0V0VFMUsvTHFsdGc0QmlRVE9GcwpEc2FWdEIwckRJRjJOWEw5SFlaSUc1b05BT3FJVDlqcGNMT1lvdDR1V05pWEFLWVhYTkJJUi9XV3ZjZWZXOXF4CnpXY3MrMzJSMVRYOVZ5ZXE1cjBpS3dZZlduVys0ZG5PMFRxK08wOGxxSGlIS25LM1NYYWRzdmZ6Z3daUStFaGIKaVpjQlpUclQyZU8vY1lrS2JxaWtFMjJjTEthdEJxVkY5T0JacTlNaGlyc214cEtWOVZaOGM1VnNwWmRDS3duRQpaelFGS282ZjBmeDlhLzlvWUxoMDg0WFhGMGRQMWxzSTdldGNBa3hWU3NDM3B0RXVvYWc0V3U2UzFNU3VJS2IvCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdm04RHcyQ1pQOC9scjNsYS8rc28KNllIczk5Rk9OVHhIa3pPQmlFeVFqM1FNZVVZVU14OTJKdzZyU0RCa3c3ZGxiVUttWWg2cnR4MzlRTmdaeElOVgpSL0R4U0VySjJBNUd3VHVUZFo0L0JuL0JuYTMrRkoxdEtLTzJwMkFtM1ZXUHRLNjhGTXRYdUxGZi9FMGZtRE1UCnExRmRXWlRuaXdybDR3WDl5TlU0ZSsvcVZYSmRkbkRldGZsMmd3cmgxS3NYRGZ1dDFFOFlldW1mem1NSEUxUXoKOGt6WUNQamNWRThrOFhUc2VoMEJ6NVgzT1pmamNvVmZucExGU2pHTjYxSktscHVkOUtnNW9NMVdUVnExdDBuMApUWEExem56cCs4c2hCSjJSMno0dDVraUZUMlNPemZSMWNvM0R2OVhVcllEZW5JVCswSDE5TzU3L3NkZGEyVXFiCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHZES3N3SXQ1em5ERDlCdVhWYVAKSlVEeVZ4RlFwQVhveWFqYndLcVdjdDdhRkN2K0NGMHVpTTJMVjFaMGRVd0lQRDg5MGVua0QzancySTJzaXphZQpRRm0xcWVsMWhwVEs2RFhJQUNxQ2l3cGJqQzE0ZmxXdHlTazh0cEZnb1o1Y1V4L2ttVnNtRjlVZnBVN1paTHEyCjFuU3FDUURMOVExV21BYmgrUy9ya1NUdmZscVVwOFloUVhIM0NBOWFiRDB3Q0l1SHRabmNRcFU2bXFTVy9iamUKaFJQQkxMSnpXUXBlRHhCa3lWYVlJNmJCWWNXMkFVYysxL0RoRk82c1g2dW41eHl2NWYzRnVDaVFJMFk5cHAxWgpsVlN6SW54RXJJcFo4cGJua1dOQ29yemNML2Y3K2x4dFpKUW42QXUyQWFMYWQxTkx4Z0hZaTdCQkdtZ3RlZE54CnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbG4rOThGT1JrdHF4NTNHenpFRGkKT0VOREdGaU9UZVlHS2FrSnExdVFuOHN3Y0N0VVZ3VGdrNDFMcFNIZVlnaTY3N3dhT1BnbDgvQmQ2alQxS2JYWgpqbzR0bmpqWXNtUDdIZHZ0ZnFKUmwwcmJrR3dNS1hlYUxNenlTRVhnckV2eGpCM0NuZ0dCUU1paDJ2ZXRiTUxVCkVvc3lmZ0NWQy9ueTR6UUdjZEY1b3gxNlMwMDVzanU1TWhGNUhmQkY3NkVJMitMY1ZXem8xVFN1REQ4bDJ4angKME54WS9YNUtBTlJCT0ltWm44ZGVoYzgvZmRpakh1UzVpcGpRcHdsMmQrVUxaZFZNaGNIOGZPOUwxMkdVa091MgowWlh1aTFFWFlKUlJHdWNlL3d0RUozdDJQOUpNVFFzdDh3MFArT3M3dlR5Sit5MitmZnFKdEJ3TUxDaWdVd3VmCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbERQeHpNUFpqcGdjamEzRTdDN0UKRVFCZFNZTmNBMytiM1RwRy9pWlVIbjdaNkszaVIrYWR3REFLbFo0c2tNVG0yTVl3UUVLMms1QUxaRC9OQVhRQwpyVndtaXMrU1VscTJ4L0RJN2RBdVNwOE9jRXZqeXpBR21vSXVQUXErazgxVkp1NjJFSHAxaXpSNjdYRm1YeU1VCktMWmxBbTBwd2FHM3FRQmRyZkxRaGxOSk4vbWQ0dTZJanMxckxIeXV5aUZ2MDJla3NrVTJia1lBTkNpT1JHTnoKQjQwclJrRHdPVmowY3BQYmo4QzFGdXJKZmp6ZUlJajc2a05yNzg0a2k5ZXR1blFJcDZzVGVxYXdidzVmem1DTgpweTFhNDVuc0d4S1hWdWVZVDhqUlF5ZTI1eStCUHJ2OVVvbENjY05RWmlndngxSWs2YWxFUm1SQksya0s2Z1JHCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHdkK3d2UEZ0U0JBR09nQ3REMmoKZVNhaUNWU1hYQnBmUDJKeEpRdXQveWNDQ0hxNVRpQWZoWHVjUVdkMmpsK2hUMHkyVFVhUU1zS01wT2FZdFdUeApwVm5LTXpoZjVxcUVtaEUra3p2NFMwSG9CMDlqQ3dRZHpTd0QwL3hCUUNvSjZKRmRNVjdTTVdwZUx2RWRzQ1k3CmVGMzFOV1BXZ0FWNkRwSmo2c082dG1LYmUzaDIvbnZwL0JYcFFpNW5LR2dVb2VCN1RMWENBdklrMEdRWGlsTTAKY1JZU1QvQ0FlOFN2SS9DSmVaYy9MYnMyU0xNTHFmNUhDWGdNYURSMnROcnZ5N3h4VXhKeGFHdFRrbGUxY2VQbgpQTDcvNWJ3TitHZG5qeW52YUJOeW4zalUxdnZuMjVTaHp0N1NRanpCMXI0bmZLcXVrWGRQMG5NczUyUVF4QTFSCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3UxMlUzV2I4S2c4Mnp5ZDRrYysKOE02aVZzaUxaRktsMkhoWGhkUjZEQ2Zsa2NZaHV3QzFkT1p3UVkvK1BRSEd2RVJCWnV5VGZ3YzhnTnNwVmk2YQo3NVVCRDB5eDVWeXRadjBLbmZtTU5NeUlkTWdNeUgvUmVGMVYwOEhNTTEydnIwU3Iyazc1SHN6aXJPdHFiWDRBCk9qeEQrcnREeDJaTjUwSWl6MXBzUGxESHVoTzkrSHNHNGlwNjZPenVicnVEaVM3blJMazNyUlJDV0wxQ0NSWDkKa3BBUStDVTRhckVpazFpN3A2UlNPL1pEZldGRHdZWFRwWGJCa0gzMHZxRTE3VFBxSFh6Rm9NbWk3ekxNdTNueQorNldtUy9JazRndjNWR0ZBYlhCTzZqNEsreHFBbklqNVc3T2pzS25SSy9uZ0I4RmU0dG8zVnNEQzlzRHc0bWRpCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK3RaYXJsMTE4N25LRHRUbzNkY1oKTzZlTWRMb21qNkpYK1YxNkxVOEUzaVpVdHo2MjNQR0VHSDFPdldNeEtzU2c0aWVSZHlwcmtEWFh0bHd6OXl4RgpUMmwxbzV0ajM2TmlVeUg1eWtFbjN5ZjdlQ0RNemlnMWVrMy9UQWw5VWw5WTBBWE9na3JOZnlKSDQ4Z0NXMnVyCkpmdWdLYjNDdHRXWTlDbWNDdG8rSUxOTHFqUE9iLzVpS015UkVKWXY2d3hBR3NrckU2UVlmRzlMYnFnSkJ2MmgKSVFSbnBuTG44dkNMTlhiTHJqVmZaMmZNZlB0NDBsOUFFYk9DN05RRnVCWnRXQUFrWUVkeS9NT2xDN0tjd2VFWApMbldvRHYvRXBnaTZpNXc2VzZseGpneXZUbmpDdFN4Ny9EQUdwSm83S0JQU2RYa1lQM0crY0NJWlg1bHAzMXo1ClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFFKZ0ovMi9veUJla0Z4eXd2a1gKK0NWZzRFdlhWeGdxVUhhU3BITGNJNzdIVWE2STJNQVdiTUVzM0FMZVZqNXphNHMyWnY4Q3BKVVBPa2pJL3o3TgorR2JFQmphM2QwK2h4eU5YdHlBdkNJNkJYR1p0KzJxVHdxR0NOOVg3MXdrUVI1cjMvTjFEWG9yY0lSL25uTnRvClZEU3NheGFZV05oMURKNXRVeGpPbGNLRUN5YmZ1WkNaL2t1QkhvSTAzVjM0NVdMRlY2cnBHLzBsRVRGdnhCTVAKd3d1YlNCekoybENWdU1ONGJwNkg3bUl0RFVwaDFYb0FTa2k0d3d4WWRCWEdWS0V1WHhqRXExbHhldTdaUEZLeQp4V2szMFV0VnlpRnQ4VXpqeHhsMGdhbUdjbzUvOGdnVzVMWTk4ckg2cVRoNGJlUDdkKzQvd1NEVEhKMktCc0ZCCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnJld3N4S3BRMGpwcmFPK2lLSmQKa0dCTCtjUU1Ocmowc2Jobm5WL0IySS9WQkNaSjlWODJMbm43SkVCR1lOcUUvVHN3VGRYWEhKYnJGbGh4UVhaTQpDaHh5dlMyMGN0TXFrTGNjS2tRV1BnRFZ5ZURTY1J0a0xsaHdFVTl5Mk54UFgwU1VhZ0hlTXFteVJVUmhHZStLCnkwV3JsSEZ0WVJMdVVDZDN2cUVHaVBXWDhTR1paaVk4ekMxT3IzTjFNb0loV3Q1TDhuS25xL2FvdHUyNU54WXEKVnpKMlRVdUphV2hCTUtLUEpQMmFOc0IwaU1NNlFqeUhqa2p0V2xreEdqdkpDU09OVmNETG51aGk5NDZNSThEagpzUFpmcjNzd1lvdFBXQ1czVWZNSm9PcFFNbFZaYkF5SXl4L3ZRWEFScnRsYUhhbE9VeDdwSmd2WG5MRTF1RjRxCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFZaaGtjQ2U1WmRTdVdWaXZPMEMKUkZCejE5Szhyb3FlYWp3czRIVkwveU5aemJNUnhSRXlrU2xoOFZIcm9pRnNTSXcrQ3pqNTNjcklCMGJET3p0eApRRHNDcWpobEdQd2VJckpHRWhSYjBUU01pN1FqV05QOXhWQ0Yza2U0SzNYelVOMURtdElabzdPS29jS29jejlNCmRoOUN5WmhKVEhsdExDSlZsbEVWNVY3T2hRUjNPbG80aGg3aHdlWGtFVjNaRWdzUENMamx3blhvUlJyazEvcm0KV3JtN2F5L2tqcFpDZU9YbmFJL0h3SXJ1dlREdTcxQUhrRTloZzVGNkIvaGI4MDA3Z2V0NFVFa2NkRzhIN01zNwpsUUMzQlNUMXM1aUFhQmFrVzFBeE56V21XUE1iQ0h0NTVUaVVwMm9WVFdaeVhBa2labk1MNHByaktiV28wQjNTClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbVMyMHpCNHAzdXVlaTZucGtSemMKOEtLVUFpZVl2WSt2N1lGUG05SnNSQVF6aEFoYjQvVHcxeWF0alhJQUNDSEpBU2pTbXdOSU5HQnlZOE1kOW03UQp1ajZHaUpLblVKOG0vblhZQXBFUFkwZHlhMkFFU3ptdkUyVlN2M1J2ZVRIbkw1akdlZCtvTmZVTjZzejZRSUVuCnpzZmozWURUdytuYVlqVGpSbVFmbE9OamZGaTVHRWdNQ2xVNTN1VGlEYzlZZzNCanhycmUzbDlKd1JpTEkwMEsKQW93L2YrdkthMGFwYTQ1L2J4bSt4S2NROWFqMTNTNnhvM1MyR0ozc3J0alNYL043NTRhOGxGeGxHUEpEUkJWNAppNHEwMm1jSnk2dEdKYkRYTlBtaHJGOHpMTW84eHNYSm5KQ1ZveDhBbVhLM0FjdDJiSXNiUWR5Q1h6VnpNSkNEClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemExN3dxd3JjUUdkblI3RjZqYUoKK05wMU93YmMxSGg4TDFKejN4bmRFWTRNQzUrQWQ4M1Z5b0QzLzluUmFHN0VxQXMrbUdVcW1XSjRhZC9aNXd2aApFaGVvV29yZml4VE5zN2x5NGdNb0ZCSWVOUUpxL3NWdVdYdUxMSG1TcmxRMlRNUlhGRmwvenp0Z0E3VStlRFVrCm85VExMaEZyWW9rL2RueUlrTDdPR0J6aml1L1JlNUVUemVYM2ZTZ3FVUDlmbGlJbHhSZDN0cUY5bmdDc1JRZnEKUCtQVWlsdTVicDEyNXJ2V0s4WWtwOE43ZlpURFZlUzQxUmRzRDRNeEdUMEc2bDNUN3ZmMFoxcWs4eDJwOGZEVwpnaThsRnpYVSt0OEtYaGRXeGRiZVRmdG9Cdnk2ZEdBR004cWZXc251WnRROWRmeWFCZ0t6NmZWU3hwYzNXVHlRCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdStqSWV3Z0Y3UWVaTUoxTFdsK3cKeTUyMG5oYStEZlVrMzJld0grdEI1Y3ErRnQ4cXBnNkk5VVpWSDhkUW5FOFcya08zQmFlbTFkUFN3NmdvQkpkdApDNU15RjFseDRSL3Y3SUdBRU0xS0QrNUhtYmdBdkZURGErN2cxbEZvT3Nza1pJay9uc3BBQ3pDS1FTeFZSWjF3ClBIK20zWnEzeitFU3J4cmhxNkRiQmQ0NDBBTGpSYkVDZmpPbDF1Y1B3V0M1OE43bHljcFRPSlp0YWZrbnNWYkMKdzZRaVhGMXRDZXJlQVJzZFpzYm9ralJOb3RnclF3c2hLU3dWOFgva09zdG5aS3I4STlLY1dGeE5FOTJmbjdXVQo2V3V4cXA3UkpqTkpJMGN5MkhhMFR4VzhubEplRldWVHpqUlNZQ3BEVEh5RVhSQTZjcmpxL09TNWxlQTRMdDF5ClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejh2bDVZVGZhZHRHY09OdXlQNTkKdmd5c3o4b2Z5cnFWYWE2djl3SFJPQkVVNURsSW5BQmVObDZwWUJDQVR4SWNyRDEvVDhjR0UxSDZNTVdMbFJqUwo3eXpkUDZXMGwxcWRvc2FtemhHcFN2ZEdXV3B4bmpLUXZQVHJIVkJGZmxjUGkvZVhiT2NyTERvOVJnZUdSdGZlClFha3N0S1dIRkJGWVQ4bEZWRzh1aURaeHo4MTJHMEtIcERtSDI0bzY2T2U3aytCMEFCK3FvVjBuc2MyaVhQKzQKemNrQ1JkZ21zMnRqY29ubzE3Y0V3eGhJTmZ2dkpBM0diTnoya2lTcEtheGMyam5XTm9ncDhHeWpBNU1ZYWppVApWMEtROXI2c21ZSktOcXBESEFwVXlUeVdXZVJmU2tTYlBQUU5rUmllazB5RjBZN0JSN0ZqVFFES3BYY1poalFGCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWNpSlFhV0xsUGtjU1M5ekhWRGIKcVViWHpXNk00MFpJSFZVVHdkK2ZySFVvZlpaa3lPRjRHV2t3cEg0SldKNmJXZW9KcWMySlZMb1F6T3dWWnBFNQpFN2FVNFdBUWlLcXRobTZEM0NwYlN6ZXZBUWJmaUkycTMyMG80K01zaFpWUEhRemliczF1ZDgvTTB0RVUvZDc1CnZPVDN1d29qZmV5SENvK0xWVkNFS2s4eWk5OWo0MHhJUmtUcms4dmpEbFBrZ2FUbVFrcEQxazY5TDdXMm5HYW8KUkVkNVVIS2xJSlRIamVEeGRqUmNldk80dVd0cG40dEdob1ZxWDJFWStQK3NDVkxMRkxPeHZlb215QWRKZjRKWgpuNCtnMXhkRVQ2MjB1bHoyOWRGS3UxWlZpeXUvN1BybjFJK3FDYWNZM05lbUdEcldCU2lrOXlibm9la0RSa1I0CkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUc1cUtDWWl1THU3M1NyUE9zVkwKSVlCZ1AzOVMzYzY4YVA5R3NjWmhVemxodjQ4QzlRbmYxb2JTckdOWGdmdG5Cek9DQkFheTkvaWMybnV4bUN4SAoyN2pyUjhPRC94dW9aUjVJYjZTMUZyaHVibFVwUUtiMVpvOUN2SmFVZkpSU2swR0R4bE5QbVdUSit4cGtnazhQCnlBbmtHVlk3djVobmp3QTJNRjUxVWRjNGdRMGE3eU1vU1pTZ29oaU00LzRRVU1wTk5EQ3g1dEJJUUhUVy9RUGwKeHNheUF0TEduYnRIK1RFOExzVlY0b1JKcFpBTUkzanJiS2pJRFk3blFIVytzVGJscXNlWGJlUm1FVVNOL1AvRApjZzFWZDZxaklmT0NVQ29SNlorM3pWN3ZsckswTW44MGlKM1VHTTg2T3h5MDJaTXg0OE1kMDBWOG9OdnRKYnhRCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd05oa25hOStLSUt3Y2lIRCtRMkoKZ3RrTUhHdFNqQmFncnFVRzNKd0JPVTAwWVJFa3VBSldLWXJuMWhzaSt6enRoeldadXIzd3haSjB3bXV0SGE3SgpteGdIZUZYOFFCTXkreUU3OTZKSWJUbUt6WWxiaVljQVVEQ2tsKzRrMW5Idlp0SGhlVFMyV2RZZWlZVnBxSjlxCkJTazBqQ2Z0TnpJRmZhbFdlZFY3TVFBL2thY1pzTlhvYzNCUDJuYkRNUmY4RVBVUktWQTVhQ1FwVGwza1Y5RnAKTm5OQnJoazBYZ0FmMTg5WS93anR0dEo0NGwvSnc4QUZqR3NkVnNCVk45QWhJd05UbXVMbnllb1N0WmVjbUYxUwpZWGV4OXBHL3FqSGk4TzZVNG1VL2hKRFpFTzlpNDN3dVllaks1dEMwbFpKeTNHR0lrRGdjR1FwZ2g0VlpMNDZhCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHNVN0RvMHo1K0FrOFZ6Z2l4bk0KcU1HWkowRGFHYTRHemc5ZGlFcmxsb1VBQ014NVhjUnk3WUFKTnFDVy9HOE8yaDQxaExnSnNuSzhtbDZ6WllGQwpHZkI4NlBvOUt3V0h2U2lxbnc1N2Fua3d3Ky9QQlVxQzBrVjhBWDVHZWlFSXhrNE82MXMzOXBtVEZxbkI5c1BTCmYweERUeXo3c1ZvRXZGc1ZuS1ZVT3paeE9sUHdleXZsY2xoRU51U1oxS0NUKzZVUjNKTXhpdTV1ZUNEUXJxaTEKM1BvRkkwNC9DdjFmOG9ZZG9YaHpYUmdibWlqSDhTMFRCRndwejBvMUl3MnRtWEtrOE02Um1sbmdXRGltUVl5aApManJwS21Da1Mzc2VYOGQrWGU1UHFTc2dhdG0vd09VNWM1dFR2S1N0dkhIU0RNUFYySnRYaW1XR3RCQWhOb3VFCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXRoaUVlY3pTSEpFU0EwaEQrUUYKTDJYNDBZVjJVMUZ0RGZiZlRoREZoVjdsaXlybmRZWk5OZEhxamNQZEVyeG5DR3Rkc2pnN3U3WHM2ajdqUGJWNgpWSVBWR2dFNDZrQU0yZWQ5dGV3cTFlNXpLWmthRlJQbkFhWjBJVTFmVUxkMC9kdEhWNEc5S1Y0c1ZJamxvL05aCkV4d1hlY1d4RXJqSHczMlU3eEo5RER6S0pZbkFQaGpRWThtVXY3T1ZwS0huVHZHeDNrUEVzWTZUanovK1R2aGIKQ3BHc2VvVTMwU3U4Z0NOWGpna0w3K2pNWllRbElBbUszSVl3UzQ2d1RwclRaNVhRMFdDZUJUTWZ1WENBcjRjMApaenkzdDNRVndFSHRRclErVlhOc3M4OUJkT1N3RjZIOFhJMWhobmFTa2h3NTBzZkJjQmlNY3NNV0N1aEZvaTRNCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3o5QjR6b0F4RW1TQU1MWWpzTlEKYXJrQ2ZQT3ppTGdWczNIUjZlNUx6WDZZWWRPdzdnTDYxYkFZSHpVNjYybHZuYlc2anNOby9VTWRXSVFQWENacwpkTlZHd2oxeW9kSGFRYVJwR3FuVmZieVlBbFptaHIrc2NZSmlxNHA3aHZzNXBaREN6RUpObHplQzBEbmNYNlRxCm0wMmxyUEZpOHlJMzNmajVsVDhhak5nZm1zR3NrenE2TFI5NVdObEdrUDhpZzgxU053TVoyaDhYRlRkMzdPQ0cKcUozeGVlekRBblAwYldsQk9NRm80aDBtZXN1VjdNend6SkxjL0hhYTVNc2xDMFM1WXZaQnJISGt5QWVwM0tHdApwYzhNWUxza25Ud1dROGEvbUx4VlhoR25FTGdqMFRLNXJxKy8zVmQ5elpsbnJWaER2VU4yMHkyQnpQNTFGbzBxCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm9weEtHWTVpYk1tOXdIN2lvU2kKNE5zZzlWZGRub3RUUVIxSXBuMVc0RERUMTdFS0V5SW91Q1hRekYrMm1JaXIzOW9JVFBRdGVWbm11aWFlcEI4RwpENHBrMGdGZktHNzdXVWcyMTNiRE9jTzlUUlkwNGRJUWx1QVNMSDVFQ1YvMXBaSERTbXBEUXV4RTZnMzJTWTdqCnRNV1pOeDNzMG52Uk5KVVdBdllleUp0R2ZqVi9vWW9EbVJuVzEvbmFGWTFDQTE4VzRiOE01bnBQQ01YcWdNOFgKUm1ibmMyZmwrQTJZbTNOSGN4UmFKaTNGRW9yTU9kZTF4T25od1JMS253eW93TDhGd0xwZ0lRSVJKTEllaGR6cwpCNFFGRDJHeWVmSTdYSlQzalpIanN1YTByOXhZaWx0dytjT2NhSjMraDI1em8rV0dUMENQS2dCbGNmTFNEWlZvCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUJybEIvYndWTXNKUmlZN3JKQlAKR3BrcVVoS2EvUmg1SE94cGpTKy9WYjQ1MHhuQTRKWi9CbmQwRG1Ga2JIQ0o0SzFTTlcvMnFuR1B4NGNFakluUQpjTVZwK2l6NEV4OEhqcEg5ak9sOVoyQlpaWWhVRy9yWlVhbnlYdmduSEFUMExpYmlVWHY5Rmt1SU9zY3pvSW5lCnlyRFoyVUdreEx0Q0h6MldycUkvY0M4ZnljWTMwcmFXa1poZCtYYWRtVVlod00rSmhEdGRSOEg3S3piZmdKYysKYWtVQTFCaXoza2RLOGVpdHVGRWRpdVRzeEQ2aVNNM09Pcnc2MEJZa21YcWU1a3NOaThKKzZlODBtZE9SUjFYdAo3YzdjQjk5K0xTQkRyVWg5V3k5WGkyb1pGRUVkUXhTTUVjTjhheitlOGxTNDNpZmJRTjZoYlVBT21TeG1UQ3RFCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEU2WmxvdkhlVlVtaGRXeVFtUmQKYkhkWC80ZGszMVdHc0ZkS0hmUE1nK0k5SStVTjRZSzRTc3VmQ1Vrb0NhRzNXa2FRVnZLYlVvUHFsbUNDSjR4Ngo3d2E1TkJDdXVYdDRFcHZkVXp1MGFBTGRKQ240aklPOEtlTy9jTFArbmh4TWRVaE5BMnhHancwTWxzeExKRHE2Clg0b05lTGlUTFVpSE9CSVNHRXgzTFpWbWdYUGx1NTEweDUvSy94MFBwa2R1N0JtS1cwTk45MTBYcElJalQyaXYKZys0Z1BTQ3BTTXhWN084bzc0SmJZTXpMVEJWN1QxLy9VR2h5MGtVdGlGSlUyWGJKeVQyM3lHdjNnaEkwcmtWeAp2b25uSkJwM2swTGZSZTYzVE1tUmEzcHBQL1JwamJsSWtHbWt0TDd0Z2VwNDk1b2NqejdNZVcwSTRkNGVzbG5YCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemJYbFZDOVpPVVJtY2NQZk9SS3MKTHpkb1RVYW5GeXhHQ2F4KzQrd3ArRmY1UVY1QjAwM1kzZ2t3UkRxY2U0ZExtN2NBOGtabUZGR2JQaGJCYUZvQwpveFhJWklpZWlybjhLMWlLN1hKbmhOL056VTFtZzNLRUttUjRQc0M0ZUJpMTVvSnA3dDRrU1lNRU9tZkp2enRqCkt2cVdRdmg0dHRDK1NnUnVjci9BeTdvbzNLY0xQQ2V1b1NKRy9RSkMzZG5VNkRCVWJlV1U2blRsNlNpZzhyN2UKbU9HNXVPMWNGUk9scmVVY0pUUU1yT1cwd0xKRTlFaXRmK010bzFvZUZMUjhpTTUxenJjTzFTUWhrTW9PbHFVMgpoeUpQTVBTSnBFVTdxMzFtcVEyaW5PZkhYR2k1OGQrRUtXUllMUGpXWnNNYnhSMndmZ3VycndZTEZOVzBRVWYvCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFRJaTN3UERZajhQdE9Lc2NIWGYKM1VmWTVqQ2lIeTRZMThtTEMyQm40SjZPSkFZb2tBYVNVWWcvcjcrSk5pOSs3eG4waStlSTNOVll1Q1RjNEZGRQoyWlhZckZXUEFUNFNVdjJQK2Zwczlna1pWQ0JHdWViWlpUVXJyaWI3TjFIQnowTmVVTlY5ZTY0bGdDVjNyRGxqCnZyVGk4bTZaWkJKVTV2cFJVRW1PcDVUZTZvZnJweXNTUmJTUGNjYVhlTHVBY0FKVmNNdEtzZ2wxMDdYTWhjWGkKK0I3U3lLTGlWR2VzTUlvUSt3NWFIRGRzMm8rN3JJQm45U1lkVjdxOHdSUVZXL1VkcmF5RVNaRXhUcFdmRzZUbgo3TTR1RnFJRXZPdjhDc1gyeFQwaVY1TWI5VDA2Tys4ZFZmZXZoRm1RSElvd3QxeTc0VG5ucXpqYk5PSWdXcitSCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEROdnhTNlk3SE9WSEpoMTNwclYKRmdoMlZPeWlSK1RKYXhOcmk3Y3N4V0xLc0F6cjY3S0p6QXlXOUhkZ3RBemFGcVd4TDhIZGpFNitiNVdwbXhvSQp4cTdFbGg4eDhiY0ZYMTBNM1RpZzRHUW1CY3ZBd2cxL0pMU1RRM1hDYXE0bHlqREhLdXIxL1hpMTJkT1E0VGpkCnUyeGNKcnkzZjhGcEJ2NUNBUVdodWZBakdvWnM5a1E0dWJXNnBkNDVZTkFLOXNFQ2hva2VHUTRlUzhwaWl6N0QKRTJ2Qnl3eTc2dFVoNVJsYTYrRFRzcFY5SmdBSklReUZnNjVFUHhvbHRKSDk4bXRLMVlHU1pTUnlZeC9LUytiWApzS21hSmc4QW1meWo0T0ZXUC9ZR0REUWd3VWEzWWR5N0F2OURVV1p1UFdHT0tCOSthSzV0c3VsWlNLcUpDRXZHCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1pzMXhFcEpnMDNwV0wzTElSelAKM1hCbjVTdTJsMVFQTWtiNG5YRTFmYzlvQkNEUmVLejMxYkVZY1NHeUdZUVpMV2ExUFBodkpBZno4bDNFdElIcQpJMFlJNncrWVBodTlkZ0ZzclY1VTZUd1lZa0RZUEJSdXlVZlowaUtwV09qakc0TDd6azZlMUJrZzdNaytRQytqCnFIcktnVUx1em5XR3JSRXFDK0d5aDdFN2tXRDhzc1JYNmNLVUY3ZmVOalcvZjg5R2NmSldpSzlUdnNGcTRnSDIKd3VydHhMUC83bFlhRlptaEJrOWpodmtsNzBOYm9wNkg4MGRIbWJrdUdOVHNRWFpQM24vdTJ1MDdYSDZ5Q1JadQo4aFJib2p5UGI0anRGcjhBdGZhQU9ReGdnaDh3bDdCQzZsMHdlYnFTMlRCQWp4dC9TbXJPR0w0MFN4ZGNFSFdqCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0owRDMwd21TQ21YcnpqYVRWMUYKTE0vbFZQYUhGNEc1U0JkTjFvaVl3cXBUNG5TZ1UzbHMvMTV0RGVBdEN1RVZwQ0xiRVBKUk1YaXg3SEkrTlpMLwpzN1RrN2xFM2NxdXM4K1pjd0tRVGUxRmJmMEdBbkg4bmNPNmtrN0hLakxXbnV0TVVCU0Y3ZktzeFJCNUMvOVk3ClFxODFSU1ZFWjVXVitzZHpFZFlONllVck1ucnEyNG9NY0RNbFhzYnRoSldYRUVTdUJiTjZPWVVUWnFuSmFDV3IKUjhRcDBFT0w0Y0c0cVZqdHR6WG1OaXorb0t6UDFWZytOekRWM2FENHJzeXJqeVRZNnViV29TUE50ZEp3VHlNeAp0UE1CbDVIeERjOExqcFAxL2JmQmtJUzFGNkF4aC9IQSthei9mV1ZDank0VlB1WEtPMFhxNy8yWXlRTk1iTkI0Cjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDBmSFF4YXJab0JHRzhHbXpzN0QKSElxdFB5VzdRdDNzWkg4cVpIYVB2R0ZkTTNHTy9FWkM4OWkxWkdyVVVYQUp5UjFVTFYyS1dxdmFQTXoxZDVrZApHS0xIcDBUQ0xpcUZjc0kweCtYUGVqSG50d1VvWHA4VFZCS0lTWmkrSEV5TnRZaHVzYkpieGhxNjN1aEljOUZKCjMrUEhlQktMbWl1MEFHM1lKV0JPb09KNytuTFZldzhVWEFDVGIrOWlER29PUTlueDliRzZLWVZRcnV2ZXgzZmUKOFNLMGVod1JoTk9zeHFtTGt4ZUNaU3RXd1dnZWR1ZmJIQXdtNTdxMjFsY2RnKy9KT0dFRXhGb2VXb0NWQXFXcQpNcEhzNS95cEJtUEdVSm4zTkhGMmtSMDE4cmFIVlZwOFplN1NrZFFJaTFvRXlqYzF5UnhCcU5ZVDJHNE1DaDBRCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0lPREdvbVk5MUtEUlQ4T3hoVzAKamlEMlRFMWFrRHMrUmdJTHkrV0tVbTAvTCtCTmJOUVhNdUxzQTJ5RmpGN1IzUTZPSHJuQWFBZFkrbjNXelBRQgowcmZCYzZmK1E0MFpoT1JBdCtRV0Y5ZGlYRXNmRm1XbTZwbW8yMFM0TUVGeEdvWFRGR2JRL0FCRG15VVVseUF2CnF0VHE2bjdGZDloZnl6d0xxZkMyZUlGT20xTm11cXU4TW1tamJNS2hlaS9oMU1JMVdqbFFTU3RGd1hzUTl2NWwKYTd3R05jeDJtcURsb253MFBpOUFnNVYxTmxHc1ZEeFlWL1dpc2xNcmNodVhoN2l4ZEhyU0JJZzFDVHhwKzdOSwowL1VKTWQzUnBhL0FXWS9rMllRUkI1RDlPVlRsTnNpZWxnVm5Kd05ENjdPZFZLSUpNSjk2dHpkZHRzVTdLT2J1CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbGJ1SGhBYWFhQy9rU3U2dDcwMUEKdkNGWGd4Q3dyc1pRK05TaDlOQzdML1AvUnJhUXhZRUpxM3RzSy9BSXE2WHZzejVid3F0dUJIbmZ2WW9XWDZTUQpFR2pnVU15emJBeDZzVUR5ZWtjUDF4VjR5T1l1bnpMMXNvWTN2Z3k5Zy9keEpXVkgyMG1VU1RORDV3eEtWZ2xiCllENDdCbXA3c3ozZWFYV3duaHBOL2xqSXplSkh3WlBkTW04amdPc3kyZ1Z1K0NsSXdaQU1hbGxiSzNsOGtLRFYKNndaTGR1VmF2WGFMU0xKTU5vM1R3NTduRmgzTU4xSWtKMGtURjRoOEs5UnVQVmtUVzY2cnVwWHVNQm81OGh1bwpiSitUenJBeGtYZDZtVTRZdkZ0TnJKYUt0enJ6eUkva2prV2htaXdrMnUzazBXb3BFZ29iUkc1YXdIa0xKZGNXCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTlZUTN3dXZlOGRVQlY0UGJZeUMKWkUwUGZRbThnZFlNVVRIMXRmQ1pOOWhZSjVGeGdUUFQ1Yk9VNkhTL1FGdEUwZituZ2gwNkRUNkpCNm1LOUlqbQpqNnhEazJWUHkzdWNoVFVtS3JEa2c3bXFKN09EWUJpeU9EQjEvbDMrVmNGdjI5ZnNuM1RCTS84VHFMTWk3NVZXCmxzNGdhSFh3ZG85WW1VZ3dHWis4YUwwUTBhbFZ3aEhpL3ZUeE9Ja2FENVVHR3ltYkJITjdYbWFTUGNkTmlWQ1gKVUphMkMrYnk0YTlWMFdScE5jUmxlQWZRZENxK2tEMFk0OStOWCt6UW9VdUl6WjZxWExhNTFhOHJRdDIxYzRHZApVQ3ZYR243Q21IMHUrYUt1WFhjS0VIZURvaGJnTDA0VHBOclEyVjVMY216ZkNjVzFYQmVPTmt6bUtHTUNSRDNPCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjBMTUJUdzMrR0M1T0F6WnMycVcKaDFWbHRKQk84bXlQRWtHNjVra2NXOEYrYUtGWmFTS2RobWlJOFBNZGEyRnFXTkJBZE5kZVJNU28rUGttV0ZPcwpRR21xNXl3a2ZEZm8vUW1NSFRyNnFiM0ZEa1N6ejZ5a2RwODQ5dzkyU1doanVmTzBQUG5JOUdCd2NZMklBOFo1CmJ3bVI0WURpWmVQUnRtS0JvVlNKd25mSmcxdlhHQ241dE93LzQ2Q2lHbzNacjFodG9SWXpRcy92L0E5aDFxY3QKKzZGSUtHaXNkd2JQaDc1RE1CVFRpTjRXSGN6aEZRbnlHcTUxNDZzSGpzdG5nSnBmQzYxK0RHN2NTS1dBaXBlZQp3bm80dG9yM3B6dFJJQzJFd2ozUXUrQStVVXE4VVNhN29BamNvZ2VoVzhxSEJKUjBCUS9lTkJDQU1HanI1Sm5pCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTJ2TjdXd3EwSWFWNXl3T0I4T3IKWmR5QlRXTFJlSlpqKzJUN3RISDJHYzU1UEY5RmZ3T1FzWjVSNDV1dmFkc0I0MWdoc1pHY3RWcDF3dnRmM2QyQgpiNEVXS200cE0yenhyZ21NWThSNVZIWWQzVTF5TXBUWk1lZXMyUEZuYjh1OFhqT1N3WjVQN0pVZ1ppTG05RkhTCm5pNW1aZVJ1emtOemd1S001R3NIS2ZvUzFLV1hDWW40S3hxMXhGWi9ScjVCdVgxL3FKS3V5cVR4Wi9XRTMzdm0KNkpSMWU2UkVGRTdvdm13NGVla3FrU0x2eVZlcVhiQnFFa2l0YjNUWCszbmlaNHEzdGc0SnA0WXh1Z2JDWlYrSwpnaVZkeEszelhuYkRzUXZJQkNmUmZWV1FmbWkzL1hza25JSk9jMmU0ZlZ5aElpVmJMTjFDQWRVNE5LbnRuMWxECk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHg2UE9sVkVoYVR5L1htRnMwWnQKdGx2elhuQVNuWkhGNG1LRHBsd3MxOHFmbVpzSDMyNFVvSUVZZjVadVJmcVprMFEzQjJNb2xadGU0bEFvdStTVwpPKzVHODhPUlA0L2pLdjBIUjBsOEdXTU1GM2duOEtINDlwdU5hNHZmaE8wR2VJaVlXQzh2aUNGS0lNeXB4V2lDCkZ6Y21hUU1NS0JFbEdhUnVwTjJ1dlJnRDFwdC9RTzJnVFo1SXd6ZkY0UTYzZ0xIS0tPTU1SSWFna1RnazQxNUYKUnVmUVg5SjAzR0hHSUovWnpzUkVBOFZSaTkzY2VyWkZsZHN4YmFaTzhTbTI4eEdYYUhIMG9VNks1UzlSVUYwaQpGS1RwU3Z3VmNpVE9lWEpmMTZGVW9BclBNaFdWUmw4MXplYjl0c3Fveko1MmxwVFBkRko3bFRSd1hJdnZ5Y1lHCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFMvdWFQVnBDQWFBYlZsVEJCUUQKdUJiMmJJbnBxcGV4N3ZyWUJHZXA5MzlRRzNzM3M4TElINkRTS2dpN0p2NlUwZGJib2pGMVVMbnF1WVF6c3EyQgpVSXRiZ3RpN0pZemhhaFVhMFQ4ZmtDbmttMmVWUjN2VkxFbVpFbU93YzY4SEttRWNqQVY2OG5LQkJqUjFVOTVKCnh5a0NJOWc1OWJjd21BVUVTTzhzK1g5bUQ0eE5qeWRhVHg4MHZUdGZaYmhMMnE5eGRkdmI5emd3cDZQOFlZTGsKaFNEZnhtYittMkp3YWlGQVVIWW1UT3lIUTZhL2dvczc5NWhHY3dJSnhPeUdxT1pwdWVuRWlibm04M2cvSkpKRQplZVlJUGNVa0VYLzZONFhXTmd5VkNMb3VVVnA4UTJrUE1GUWtYSmFaV01PcllIVE9GRFRjaktzMHdwaUlhVWsrCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2lDWlFoTWt1aDV4a2lkU0RGQUUKSHIzK1VLakZVbStCMFd5c3BXTEsrSVlqcHBaOTBUNEFvaWVGc1FLdnhUZnZmaHRIWERmZVJoU1dteUhjeWUycgpieUpmMVdQd2JFRmdFV2NhMDB4WGtSSSs2RnlFdnpiaVZEbXpZUFFmOEJLT0dsak1vUXpCSVp0MGIwVERBZ0hUCmszRnZ4N1hTbDh4K2dER1RzZ21JbHNHbjV1U0Y3OXl1SjF1dzc2RmFEZ1pIUFp6Q3FGTlVkSll2MHZienJqdEcKdlVUVjZXdUZDNGU3MnBJeDJaWjYyR1BCVmZqcjhQUEE5V2I2ZlQvcGtveitKcFlTZFNaTCtKdTRpM2FDWWMrYQpUdnZLUW9pUWQzQW5ZL2FtWHhMbWtsblduZEdGTHplcklHWHRISUgzendzMDQ3Yzd1VXpMMzJOQVJyaS9UWFZZCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejljYUNIZVA3bUFKTk50QURWT1cKcnpaMjlvbDBDQUllNEw4YlpDUzlJSUhzRkNzc0JMalJwbkFCRzBSTXpnbVlxaDdNNmVqM25GQ1pOa1RZSzQ3VApaQUYyWEhRa0NvUWNBMFhTbjVjNW1pOElteTgrMi9mSEl0M1MxV2NUOXdDSEtyeUFqTGpGZE5adWRyL0dhWE1PCmtFT3RIZkdIejBDd0tTM3FEYmczYVlqUDRCRTArZDBzM0g1amJrMVFPTUVMdEhsVmt5VU9ValJtL0paMmtwY3EKSjZRckhEZnY3VkZWMFZ0QmxMSVdteHB5c3RJcE5CUUVlUE5nRzZFVUJJY3d4a1lleXJhek54OEdXWGlxZ2M3bgpwZ2h4WlZ1djJvYWRTSHM5cUZteHlReFhwVDFSSUZzNTFzVFVPT281MGM0TTZsYUJPajY4b0JpNllBeGpWOHQrCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3RkU0F6NlVrM1lGT2NWeFBSdXoKRUNlajJKUS9icXh5SE5PdVIyR0F0T0VlSW41T25zcGtTdlNtRkYwN3NiY0l1bnNGVTJOejFnc05jUHM0a1FwdQpsM1JiTUp2QnhDeVdwcUZTQmdGTzVyWmRsRWVacFBxVDIwM3pMcmlScEcveVZWNkxoa3R2SmUzcmJDd1d6ZUtqCmE5OGU1aHAxKzQ5TE5HdkhXaHU2M0FrMTBPYUlYZDJiWW5neWxvMkxFYW5FcU5Tbi8yQm52SFRYRm0wejFTcnMKRm56ZmVpSzZUS0h0VFVaNjhFcnovVkYxWVVscUdRSFJuNEhvQitZZWVnVGlQZS9qM3lCUStubngvbFBYelFXaQprSVRkVEpRUStVZEJtQnA5dHJmRTd4UFB0MnQ2WUdFMmRoNi9MZXoxUVpxU25wZnQ0eXFjTjJ3aDhpT1lOeGhDCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXF5d0xvQWNyWDJRejNwbFNjcmcKNGhIVXByMERWRlE4c2hKaGF0cE9DekFqT3haRkNhcENQSTlsUW9kdk43T1BERkkvdzRqRnR1U2ZNeElDZ3ZGNwpYYkkrTSsxTUhMWFVranpxZ1BmRVJtVllCNnRhK3FzSFdnek52Mk1rVy9CV3ZEVzlIZVRXV2oyRGtudTI4eDZZCkpzeGk4aEpDSWpaa1RRZDVqSWZ1bVN4MHl3WlNCR3VsbFRCTUQxYXROZ1hDWG4yQW4rTjJwSUltZk9TYUhrOHoKSFh4cS85OGV4MDk3U3BKRXhPVzNyakM4cytpeXlrQStiamxiRDRCaWNtVXhhU2ozRjhsaUo3ajJpM24vQ3VWegpYL3M4ZzR5d0lvRjFwZGtmam9sMkloWjNLdVdKRmFWdlBOU01jdmcvK05RNE5URXcxZUNZS3hzK25OWitTUHhrCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWpMNlhRN2dGWVZ6eDcwV1l3WDMKQ2ZtbVhiWVlXblFqemVHaDB3aEoza1JxN0E2YklsZXFOR0ZmcjJERGw0N0ozNTkvV3c0d3BscHZoQWNTTDhFcwpSNkFmQUZJWDBrOVR4ZVdhRmRnbEo4RHF3K0pQcWhRUUQ3eXZMamsrT1MrR25LNys3WWhsZ3RXWktnZit4aEErCisyd09zMU5Sc0gzSngxLzNjaHcvbmRYL2M2b0g3VzBIaTBlNmZoS0Y4Uk8xNUpnZ28xTWZHMUJHSlRra3B3MXAKUVdvbnpzcVFvM09EWUtTNUsxWGFudWdrc0tlai94aEo1dXZiblc1bDdZeWpVdjhybnRFSUYzZGJEWmlBWC92dApmcjYyYzJaSVpmai9LdUxpTncrUWkyQ3BXK0ppS2ZtWEJyRWxpdXRoQWRmSi9GQmdlL2Q1MURpa0tRUURZTm5kCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUE3WUdLWitzdjltVVMrb013SUQKeUpvdWwzWkx3d0c0SEpJeGROTm4vZnNISzY1NUUxR3hNZGxRbWF1c2ZjZ0FMWlJhVFhLcCtWbm1rOWVVcmVkWAp2TWRFbmp0SFkyR0pCTENvczVYQjZvUE82NUJQSm10bThjWU9CUFd4NENSSEphK3ovT20vN29rQ29GMm56SnYwCm5WMWhodW1LenErRmxlMUJzZE9nbnZXRVNNbStlbzZpZXpmWS85VzVObUU0UmgxS1o2eUN5eE1GbjcvV1NNaSsKUnJZdVJaT1FsQXNlaEN1UXFzOWR4Tng2SGgvNnFLVEpMT0FFMzRUbG5KL09XUVFoODdxTHhZQ0taL3I4QUl4RQphYXRBVW5Kc0NOT2hhMFlUanlNUFdhblZHalRpTFNrN3BWN2o2NnNxWXFwT1hqeHNvYXhFSStDclZYb09WUzU3Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHFFV0w0OVBEVHEwKzlFQ2pXOW4KQmZUbzdOYXh0SWJSK3YrOHdrekZoUm1jRGNIeUo3ekNyNDNKcENjWEZudVdyZWxIZmNMY3RWVmZETkp1bWMwRQowSkJMeldSNDc2RmxNRTZHbCsvZmlLOUV6enY5Wnk1a0k4REZwUmhRTEs0MTczTUp0MkR2VHNuMVRVU3o4V2krCmlxQmppSnd0RFhIM3BoR2xCaTkxWFg4Q092ZlIzeURNdFgySE96L0ZaUEEwbFJnZ1JRbndIZ3hUQ0ZmVFZzSzEKYmNGSytLZ1NNcVUvL2UvN3d5eGhrNzhzaE5qS2k0d2RWaEwzVUw1QXUyenVGWUlQV3RoRE1MdUVjREpXanRvdQowcm4xWitWbkpWOXdaajFRV1hyYTJKWkVmcDUvYnVPeXdYVHhCUWNXaytabWVheHlmaFhXeDVMY2t6SGlwM0RpCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbW9YZ04xUm9QTWxhakRDUVR1QmUKS0lhMUg4V05NYnMzMWc5V2hjdGtHU0VQL3hOeGVaMkNqc0gycklLZXFOVWdvd212YkM0RUllMHNveDdCS2VGWApKdUJBRVNzVnVCUDdVUnUrWS9UWG1IdlFvK1dJQVhScHRTYVBpa2tibFE2bGhEdkpPUWU5dXhZdmI4MVBaNFhiCndrZk5qKzhVaVBkeXEyekZIN1FvS1F0cllkbkVNR3lVUWlFZW40M2t3UnhCSFZjcFdYZWdHTThKczU5RUpkUFAKUmtZdHJuQ3IxMmVkSjlWbW44RjA2YTQ0eHg3b3pNTS90WDZnN0FWUC9XbUtaVTFtbkpQcGZjc2RtUWUzZjBIVQo4bmJmclczbGhaQ04xWnpoeXpUb3NidTdMS1hYSC83WUx2RDZIUXNmdHBZTE14SWZBOUpOcnhpQmJDQzNEMXQyCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEFhRG1VdE1KRTFITUtScm9ob2UKNTdTUS9qUzY0WmczSTJjNnlEZ1dSN1A1NitrZlBUUkxiYk9abmYrWGEyNWV5ZUg0RS91RTVUd0ErVWd1ZkF0dgpEWWhSaHJsNjduUnVtM3B4aW5abUdzQnZqQk8vMlJ6QW1PZDZKUGpIbkUwaEZ6VjBIWWl4M2ZnMjFsM2EydkxMClI1VHBrK1greENOWXhVU0laUDFjNU11bVYyMWgrbTliVHdCVFdkUUh6VEtBNG1WZlJwOC9WYnBNRE9aUWdoZ2sKdVc0UzRDMXZwM2tDT2hOUWJIbUY4bWl5WlI5Sk1ScFVNN2ZyT2xkaHltU1BOZ2thdDZ0VlhRWWpWTkRJblEwRQpYaGRNMzdsSENtUlZTNy9XUEcrLzNhZkttVU9UalJQZ3ZuNXVRbUU1WDJib0RRNEIxWkVwdDRVWUJ6VkUrK1oxCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHJHK3NOblRzeVlPbkhodGx0QkYKSk11MHVNUGg3OGpkRVA5Zzdic2dGU3FDUkdFZEpVdklKMUQ0dWZMUDk2UE9sM1FnMXdWeVlYS1dMRktacjFiVQpiTmxuYWhaM2I5YVBMWFdXS1NxNG92dHBteDlUZE5OVEdXUG50MWx4VnpJM1pDbU5tckRNZ1RQNFhGSENnNHJ2Cm82di9uQWFnd01BaFZOcFowaWVIajZuZmpoQWYzM3dVMis4blEzT1duKzdSY3JsZzNyOFRxOWxtV2pudmdOdEkKMHVJcWhIOVVvdkpCd294NitCNk1xR3BzT2hwTklaaG44ckdXTG5TckxndEhlZUt5Ty9pVVRLeDFZSDZVTnNqUQplM1JVRFF1eURUY1hGcUhJWlY3aW1vaVIrZmNncDkrRENFUmZZTDNpc0ZSZ2FUMDdLMWs1UTU1UnpUeEZuaFBlCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnUwM1lNUlVkcGNUTG0xZXlVaUcKQmN2ZSt3WDdTSkRFbnY1RkhQaXJRSUk3a05jdldaeDBlRjdwclJQa1FGV3pTc25ZWGY1S2R5RklieTVmV3ozMgo5OWs5clRneW5VQkxpWjZiaDRrWlU5TFRtMDZXdy85bUFjT0V5SWlldVc5N0J0MmFkcDZydS9UcUF0akg2cmxqCno1SWE2QS9JUmk1MDEydjlIVDAram0xZVVzUExFYTRFNUdHVWtCVXhRSXd4ZUhtZGZDb0YxdXlqWEpSeWtsWkUKejBOZXJEN015RjA1dHVkVWt2bU41b0lMUGFoOFJFTmJXa1RzVnN4YUNsZ1Q1VVdJYmVqRFFCTityU3ZYYzY2cgowQXNyVkFXTzk5TnBzQmZwOWtCbHRDVC91cjZLRzdIWEZqMjA5NVY4anJRaHpqZ2tHamlCN0d6R0IyK3laUWM0CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnlNNXVjeGMzdXk2dEd4TGc5b3UKK2Rhc1hDaUNwU2IzMG82SU15Z2hoU1cxQVBlRCt5Wm1aWmhlQkZOdWhPKzRTN0Vyb2tZdS9icnlxbUEyd0N2RApDekdRTnpuRmU3bEd5c1dNaGxrWk1TTGFsVGxJSUk1Zml0TjJVSTBnTkJER3Q3REhtR0wvc09WdzdEQ3RnWjI3CmhwOTBtZnp2OGhCQW9KK2g3Y3VTOFZFOTI0alJKVTJpL3RRU0xReFJjd2h4ZXM2UzRHTmpyYlhxYVd0NktMeDYKSnc3NTVDRzA4RlZUWm1kTnNGVHltYVV2SmY0alZTZDRJTU9Uei8rc1ZDelIvUDM4c2FMZWtJdXMrb0dPN29OYwpKVUlQZjJBaGNEZXMrYVZ6VDRjQi9NRk9ZTkNUdjlNZUUrRS9VNkFubk42aWdqSXJPMTBiYjFOQUNDdEpwSkx0Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0lUci9VQ2h5Qlo4V0JDcU10UTAKd3lGRzhNRXBYaDVMMitJc2RnS0dDT3YxR3V6MzVpMVlPR1d0MnlMNmFCMHBzSnA1K1o0WHowNGxPZ0FCRTFTYwp3cEdQUW1CVjQwdU1oY3NLa2ZkQ1JqVkhXVVU3VVpzNmJPK25VcUdsY3g3UnJLK2dUTXhmT0FaVFI3U3dxb1paCnkzdHdvb0RhMjBrN1VyL0VxUXlUVXZhVzNydVYvR0NCbDUzUGJDblpGVDFUejlxbXd2WG5Sb3kyQzRjZm5meS8KSFBBWlh1QUE2VS9ISHZKWkF3QjRZbkwzakovS3QyRmtEbWNhRlhtNWVZVEFMdWZkc0lZdXZjbVhaRFJMaENWSwpBbEtlMCs5aU03M29xblhjZzlNL2NjakwrT1UxR3dZN1VKWFlxNUN1NjE1SmpVd1U2OTRweUYrQVpJSGJMTjdsCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUUxVGkzMnZrWEhxd3FoeUhVdm4Kakxmcm04azZQV1FLaTBrNUNWempwbHVJbEJjOVMzbCtlbXg0ekhNYlExT2JWVzNMKy9oTVZBOTJDSWhaNVE2Vwp6S1BFc3VmSUV3anNjaGc0elFTVDVKMlREN3FXUWZtZDdwbHg3R3ZYUjJ4UFBJaVh2bVoyNzdVTCtNaFVyeWtyCkdtNGJpdG85azhqSEtWV1p6UXRLZktQUlpaWlVTM0dPdXU4VTlUQkhvN2lqL3MyaWZGMUk0WnJNY3F6NlJyQVIKaGVjMmJQODh2aG1uaTZlajVuT29yMjFjSEtsNzhNTnZTemZiaHhSWWtrOVpMV1cxckFBT0lNdVNqL0V2elh1SgpjdVltcGhIazg4Umd0RjRzZlVFUThWbHJIdkZWbXg2Z3BvUHBhRXNqZDJIZ2lISmhxVDV3aDVOS3pEaGNPMnNHCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTRXajZkQlA5SjM4dGdHRnNIRVkKdFErdkZ6RDFkbWZoeTRYMGZtY2ZlZU9aQ3lsZEtWY0R0cEhEMGVJc1dITnkvcTc2Qnp5M05qOW5TMXBBbFQvQQozSDQ2ODVtaDZWV2o0YkVTVER1OEw0Qm9zVllzaHVUODNuZkRqZm9GNTczUjFTZGJlUTNocVlVZjAxTFBLdUJIClpXTjQ5c0xDbVBtbnZjeUdkUTd0VmJ2VWhxMVk0VDhNNDRBZGp6Q3hMbXBIY2hzeHhPQmIvV00zaE8vY2xLRVEKaU5oMTlaZTlWbWM3U0w1cjliRFUyM0hBclJxL002VzlzcGc1TnVoUnNDMXRoZjRXbVpkcDZqbVMyUzhqK2MzTwpsRGE1bEFibnJ0WmFWT0c5UHZrZ2FZN1ZlQTI4dFhtbG1XTEN3QTdwb3Z0MGorblBGRkRrckpuN2lWeThlNHkzClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHZZMnVIQ2tlT2FET0FOdGdPOHEKdFQ0RnFrYUw2M2VtRVVxdjl4bTg4OWZWYnZvSk05S1c1Z2RzWGd1RXVhcXpxTTVQVHh6TlV2dWIxSHU0QzVUTQplNVEvb0Z2aDEvejVPdG9zbThwamJmTENUZzV6bUV5QXcxQ0FWKy8yTmpvdU13T0s5ZE1mbkw4TUtKczJMdjJDCnNqbXBmUnFiZ1FSdDNKUzFrOGxEUkJxNXNCVmp5L25XNXpqdEJySU9CUU01a3VpNWpnblJEdkEweGRjbXVFL3cKTmpLSzhLQUFxSFB2S081UzMxWkJsRW9OYUxON1NJaFJVUkswMGM4cG9uZjlKajFUdlZodmZYS3N4ZnU5cERIOQo4a3NHVkhzV0RDOEdHYVcrSFl3d2xvU1pzZURmL0VER3IwWllVdTBRemRGQlpqU0RhN05xR3JDenhWUUJSNHdjCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNCtOYVFDdkl2VWtFQnJ1SDJ4by8KbEVlVHRGTzFzSGxHeWo1T0IyQXB6SVM5MzlYU05TTEtrV0M4VXptc0VuaVdUdVB5RHdJWExEVnJrRzVSKzJ6cApDazNWdnFQWkE4WkpYa1crVEoveUNBSm9sdTg4bmtRODF6N3BBUk5RMjhYZjlBVWlYQy83U0NXcVBIV2dRNUMyCjdTS2VyVkVURmsycEFJVmV0SFRoUUtjb3lTRmhRd3ZtekdIajFDc1FwZWYrTjRBOGJDZjRwM1NtakJSeWFCcWcKU1BCd05NNTAvVHJvcXEwbXJESTd4b2hVNzdRTzI5aTNldWc2cDFXMGJGK1JESWNOWWtzcUR6bTlDbW5SWkFRSQpaZjlsMFhueS9lQW85OEYvdW1ubXppOXZ1amlwSUdWZVpPYytpUXlMK3lYeElQMnRXSWlCbU53YzVZYXQrTllnCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWlCVmFaR0RyV1lTVjNSdGdtUHIKbWkzSm1uYjkxdzJYNTV2d3ErSFpKN0crVW9aLy9rQ1dxK3gza0NRNlF4N0ZBZ0dRUDJOU0t3L0VGOWdLOXBYbApwOG1XeE5oL0tTNUt6Ry9ZM2tRTXdmamx4NVdRdnNVVnR0V0NvUE11cm5TcXVrQWhXNHd3SGdqYVFWKzI0Nm0xCnhuOEVXN1k3amJGa2lhWVZ6dHN6bU1JcFVNUTVWaktDMzNRTVpXZXFmSEN0cXAvY1cyeGVMOEFOL1l3bVlpR2cKTEVmb3cwa01hNUY2NURaSCtMQTl4cWptV1JQbmZWUDNtYUk1K3lWUzJIMWYwbTZ2T1F6c3pBazRuY1p5N3pQNgorSTN1UEx1am5yVjdpYU53V1dUSnJ6MFRiK0ZBb0VCd3VaTXZESzd1K2xDV1hFUjYrNHRNT0xHV3NzT2lPNW84Ci9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2grWHd5WXNJQjZCVnJyOXhDL20KejlzK1l3ZFRvTS94RVFyNE5FQlhOZllORmR4TUQzbW9TZTJRbTllWk9LWWNKU09uYzlXOU9MamlTWkNpQUJLeQpJUWlpVWxSaFltMUplVk8zVml6WHZaNGFHKzZNeVFRM3hNUUNiQnlCNGhHZHEwTS9OMmQ3QnFnS1dlZ1Fpc2QvClQ5aUdiRjk1bEJjOXpmSFNLRkVaRjBlUzg0aHh6MkhNbjlWd0NZSkQ4czRKR3RsZXkvTDNzazVKN2R1dTNaeVIKT2ttK1BaQ2ZMM0lVQ01Ha0NML0tDbjV0OUYvTG0zVG1sVDhDZTZPaHFFWmQ5QjYraWlZZitlU1RLd3VhcXVpdgppM01ocy95a3hGa0hQc29sbEpkRWhPdVptTE1zZUkwUUZqWXJHSXRPWjg1VktkTDF5bzVta3VvL1IxczYrQ3R3ClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3hqMndoOTRkYXE3U3pwSHNHM1YKL01zbnpPdEduV2hIa0lCVG5qVXFBU3ZGdWkwbUc3RDVNcHZZQUM1NnhGdTdxeFd4VHp3V0FycHRYTkc2YkhRNAptUDNQb0ZRWlNZRnJncnFYTHRZQ2xsRkc5S1RiS3lHTG84QjJ0eGVWMXFCSVJtYXdVbFNtdlZhR2FpSDNTelp2ClpTSkM3RUpJc0gxUWhCNnR5b0lVcCsyYlpmQ1R3Z3orOVdwdXlKRklQQjZMOW5DSS9vektESGJ4RzJzUFMvc0QKczFLTUI0SG9VL1dYeXhnbE82TGd0U1pkczNjamRWY2FYWThmd09uNDhraUdxdGo4ZEJBZXYvbUhDdHdsUzlJLwpNMitOOGhuWTM1dXBRdlNmWFd0dEFDRmdWdDFkcjhyVTFDNVJ3Snk3bnVCZW4vcTIyUFRTdUNRdzFsMUxndnBICnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDZqRktDTVdDOUJodm5XMXp6WmYKOS9LdFUwQVV1d1FaWFduVVlac1lBYUpHOFFVV3Z0VUpNZjllbVl4WGE3bnorSG1wWkNMV2RHTGNWWmIxL1QzMgpGZ0RlclZtaU90TnNQVHFobVVXblRkY1dLMHlicGpLZXlDbWZZUmxuYnJvUjBjQ0FMaEU5TmJhTldIR3orNGhmClIxVnFDZ3V1Q2xpdTdkcGJBU1ljbmlTOE8vWTg5YVo1RjJ1b3ozcmF6N2RZRERtdDJFU0lyR3dNQzRXeWQ3U1YKRW5kOHFQOE55ODY3cVNrNVd0KzhSQVBta2VYYVFIYUcrNlNBU1p4RWJpOGx3UHlXQ01RdktQQVUvRWRqZG13YQo5YVpmOHpQYnBwTjlRM1Y1MmpWWWdqVkJURkgyS2NUNGVHMEE5SEpCeER0RW9EV25mUjJETDdEeE1aTExOMzRPClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekdYd0ZEck1NTUdtaXV6SHVzdmgKVjllanlqbzRPV1ZiREVWVDdrQlVqZG9XRll3L0Q1K0ZycFJnVHo1UkJ3UWtRTXc4cDJRZ0oybk10ckdGRS9iUQpsN2o5R0RlWW9sSnNnNGtWNnpxbmRodjBCa2M2TnZOeGxzNUZhSVlydXJNY3VZVjUvTkZPTGtiM0FNMWViLzgrCjdBN0NkY0lyM1I1WWJ6a0I3RzBXWVhGQ09SUXF4WE85RUl4ZEFwNW1PdTM5TCsxMjFoZElPUVkxS2hSU3JOWXIKSW1NbmZiaU1pNkxzWnVtSzZMNmo2Qy9jbVU1OTIwYkwxUnNIRG9YSHVocUFNSFhYSVhNQVM5REZnY1FDOUVrRgpQOW03WnFLV2pVMnlPd3hNb1MxQ3ZiLzVwTjdUQklNZzZhL0pJcUFXdkdVMWFGc0xRMkErVUM5NUozd1Y2d0QxCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnVhMTRCTUNTNGFzVVYvc1lQWW0KUTBBb1V6MkdJblRmdzNQeEVMUElxMUZiVXlRcXgyUm53UlFiSFg4ZVlYUTY1dVB3UmtaSTREYnBNSXFNdG1DbwpER1FBYXlOcVczeTJobTN2c0R2djNodUxsSVEwa0VOMmNVekJHRHo5MFZ1WXQzNXNWR2czeFFzUnkwZ05GYUZRCkFnU0Q2UXJ3cWxJd3NzN25KY2N2dE92SmtnOWxnRjRmTjhkOUo2TTFMUlZSWUl5TFVod0MrU1NwYzVDeEpKY2wKcWYyKzlJblM1bXVoZ1BHY21wWFdKdVM0MDhCZHF0T3pONURBKzliZC9BSDFXUEl4QXU5VncxNjh4VTRoL0pXaQpOc201b0lISmdIZUpKUG5wQ3FyZElCOHJhck5DQTltRWNka2xlcXoxVVBybG9NenpOOGM0d29mdmdVelpHOW5kCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFJYemh6NC9hbVJydk43WHFyY0kKVFppRTRPTXNNQ0pCQ01ZdUFDWHJtYkpYbU1OYXpLUk5aMG5PRis0ZHk1anQwOHJLZStuQ2o0TVFSNGdid0RaMApVL0FNcHBsTXRTTEd4NS8xOEF0T2grR0VyRXBVZE12cXNNU0RQeTNreGlWN2hIWVFudDVwQU9EbXF3NWcxUVJ4CnVySVVNRDBkd1phbDBuK2tObTBwd1pCenMxbzNQMHpSSHNOWXA4cGRzTjNnWFNlVmhYSHB0dDEwSHpVa3ZWYUUKcEFRbGZYcDRFbXJmQ1RyOWNFZXFoT3ZUb2trKzVVN240U2twcEZsWEwybFhHVFFIRjVIVUl2bDhZb2g2YzhBTQpacXlRWWZGc0dnd1ZFL0tmOVhjbWtBeko0enlPUmNxVG84a082amhpL1ZVVjF6cjlqVWsrZVQ1WUlsWkRsOXZNCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzdXdUJxU1huRFhlWFljNXVaMSsKcFlEMFlPQ1pBZlNKcnJmaTNHYVVFUGgvKzVITy9ucnRRa0QydXBTazNJdHU2MWZQam1la3pLdHhreHc1MjZ4Wgpycm5KaTRqSDlvVy95YlNTZmNvR1lmdytSTjZ0Kzd4THhISlJBcTQzWDZoQ3JJb0FhTEsrU1dwTE5HRFp2V2NtCnNDRi9LeEMyaUxMRlA2WTNPOTk2ZHIvbGdJOG9XR2R6Y3BnUkxkdnM1aTRhVW5iNm9xeUxLTFI1eU96NUJ6QUIKSndrM2Jha0czZTMzTkY0NjN2eFlzaXlML0pESzVYK0JteWtSUVNDRUZZSkp6ckVFVlZiTnd3ZkdPbm8zMGFXVwo1Q1hsTkwyWHV4MGJYWGdyQ1lKbHZ0Y21ZTVpHVVU3QjRiRit4WUd2aU9NU1dvNjRocTJOTmNCV0lVZ3JIdHhDCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEZ2a21VODZvNXRDbmRpcUFuQSsKTngyOHpuaVc1T3Z0TUt0VTNyT1ZRWU5jamhQUHVFWUFMVlUzMjZWR2pValozaW9pekRvYVZzVWppVHExeEVxZwowOHd6M1lUc2tBcE5yZy90TUc0TTYyR2ltSVBNWHdmcE50QVZJMXZUSXRWcHE3S0ZEZXdwbEFmUFVrSkwvR29vCndYN2NWTkxUVnR6WSs0Z3YvQVhKbXFHSzBnSXBhK0RGTWFadHhmRC9iVHk2UnE3dnpSSVBFY1N6NjFETXJxcmEKcXNrR0VsS1ZtdzVrZy91U0JqRWxiNHBnMkRjWG0xRWpRY0dZbnZYemp2UlR5Z3dqRGNoakI1YUVwMXJBZjIxSwpRZTM3WHBzdmIzZE5uem95QVlUQ2VuV2tYMC9BY21tU3FvbWU0RDROWExaMnN0M1pWaDVPL0ZlTU1KNG9kclZUCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelRCSlBQTmVvZm5xallOcEswYnoKVHFrbFhEL3ZRSkJ5OWJ6a21iQzRRdUZRUFB0RVJhYzJNQzdEcDlIZk9NaC9RQnRoSUlSRFc1TStkdUJBQURkLwpIMHVIRHdQZEo4dnlLMlBsVTNDOEhKSytaQiswcC9pV2QwQXptR3NlSWJIUE05M2NDRGExanFmT1lMZGtxbWs4CktrWGxqdDJnbElnSlZSSWNYbFpXZkQxTWt2aUxTYjFSaUg0dG1GbFBGOVhEVmRSNWxZakpaenZQUE1DMmRPRG0KWUtDR0NIVkUzLy9HcGdxSGdDYXp6NFNld0J3TVJrSTRYRTYyclVsQnBQS1UyUDczMDRySitaVUZFUy9UeXFpVwpiYi9OaWR5Sk1OMWl6UEZPU0xsekhTdzJwRExUNXg0djBMODkvRXRQZlZZNXRaUGJhcDFnL2d5d3FzcktwUXFxClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0w2dEY2OVhOQTdCaWw3NGE3cDEKRGtpbStOcXBxZ0dQZmZCa3NXa1NhTzdBUzB5YXVodDA2UlBDRXh5a3hyYkhFS25NNXl1S3NrNEEzYmZHV21KUgpGcHFGMXg1QnhYckg4MUFMb0hyU0htSmN5NUFxU1JJNkVYd1hlM0o1Q0xqMnI1L3lDY1V5NzRWc0s3blBGZ0s5ClBmaGkzWmN5cGhMVUg2SWU2UktDV1RqR1U2Y0I1dC9CY1pBUmZSeFI1Rmc1MzVYOUxEd0F1bVYxL3NuRFhvVWQKSVBzMkRycTFrdTlsL3VmdE5iNHNXMzUvUzl3eWtUaWJ6U1k4Q0V4ZThpMHBKK0tBajcrQnVPbzZhdnN1UzgvUgpGVW1FQk1jRm9GUm9MTS93SGUyS1lBRlRSSDV1Um5kVFpJMDQ1bVRyUlExQnpqaEpBTEJSUzZwWm5QWWthQU9OCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnZmNFYwM1FvZWRzMXNRVllCWSsKaTROUHlBZkltQXNRMGtlcm80M1BhQllWZXZNQXFhNENET1RGbHBJbWhTTXZXSjFSazFzZExHYVY5SzVlVDNZTQpaOUxVNzg5RzNkV0VqWi8vSUZGd1kyYTFzU0REVDJHZWhjK2M2bkRYeDU2QWFHeWZwbUJvMWhzSTV0TXFUYUxVCm1SSksxZ05Sd2M2U2srRk1uczYvMm1KZUVocVRlQnBvY1BRZTZJd3VIa3JWdEErWEt5aTBITHRvS3B2d2VQTVEKMU9aNXRSbjFRS3ZtMW9SWjJ6UUpuN2d3b2I1eU01QmJrdVpYeUtkWm4vWXE0UjcyTEpvUmxMNWNKSUp1UWVtRQp1VDliVCtOcDNyUi9xM0FvRGZ0TWxFanVFSm5xWnJKK21rd21EZFVOcUlpazNvRk9jcllBclplQUU2WHZySCs2CjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjZYdVdSdFBYdXcvQ0RhbEJ1YkgKOXpVZ2xvOExXMmY4OTFGRmlHeGlzRTFPZUtRUHp1OVl5TVdEZmZjUTN5OGRzSHZUMjlRZ0Z6WjBBZzBGazJ2Zwo3aGdFUnRJenNvd043YUUzT0cvVERJSm12ZEd3WmVDdXhkVjJod3NVY1NMWW5DdWJjRDRwdGtPUWhuQTBZNFN6ClYxZVhINkNjOUZnYmF4VnJFUUZodVhSNmNGeFRhUFdITzd5eWRDa0tEenE0OVVVZ1lsOTQ5V3ZsMmw0cndWSDUKSDFoRi9ZM0R5bmtiR2I0Vk0vQmhIbkFxVzNPdGZFcGZrK1JIbCs3T0pRVDJGSDFxVFpLY3hERGRaZkZGTnk4cApEY2t1Z2YrSllNRG0rTHlna0VhbE5ERW1FOW5MNEFBY0hSc1h6WEEydHlYNkthSVg5RXQ3bzBoRmkyOFQzWmxOCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHUrWDVGbUZ3WHpwRDdIazR2R2YKejhuT3JNMHJVSzJCZGVpZmx0Kzk2VE9EMk5IRDNleUYvRTJ1cnJRRFEvMTdvT2hpc05iTFlhZ21mWDhHeGpHbgorRk9WTVB3V3hYais0MDZNNjFHZkpaQlBHUE9CMlhxNzVLTjVzVG9rRjN2N1FsOFFiVnpiUmh0TG9hbGxqODhyCmt1UE1ybHk0TkRmdXgxaGZZaDF4dE9iUDFyY3RzVUVMbUZxYnF0L3dGSDN3ZmpNbkJkQWgwVkFUOUNoQ3piWlUKUlhrb2FoYXFsNE9pMmNkMy9QRDdERXZVMmFIUUNXcjgvLytyaTBrUmd6MlFuYWFWSzRkcFp1NVdXby9tWTJWSgpNYW9aeUVyZWZBRXFrZWRTcEhPMlQydWR3emxrbkEwU3k1d0wwdWFnQW92NzllZzJnV0xVSEpnNG83MDY3MW9FCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnZWek9iRzlQVHhBQzg4MlBDVXEKKzUyN1ZuTjN0K25kVE5BZXRSQ3N1VmUxZTVjajRGVS9JOVFxNkFXOUY3eEFTS2lpZGg0dEpkMkViQUozWTZXYQpIeWNYWHhjemZXNDk2ZUhudVA1TGhTZ1hZNEpTazZwYk1wUGVoakVmU0ZhbTIrVFRjZnhTSTlQS2J6cFhzaTBICkFFQWFSNGxMKzhyR1JwTmJldUxRZnJzVURLcGdrQjMvMFBKUy96RHR5dVhEZWt6R2ZSTVlOQVhEeU9UMGNvK3cKczdtYUxaTjF4SjBjaVZockxzVWVWS2dwY3dXRklmdTgreWNkVnY4T3BkVVJ0SDlTZW1VRXRtblFxMFB6NWU4WApUcC9wMTAzNitKT0tjM0lYaDhpR1c5eW5meUVjVjZUZnJJbUQ4VDJVM0s2ZlpVRSsxS3h0WDhSYm1wblY5UW9OClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK1JSWnIzNVdMQTRqcUxSYTFpeWMKVkwrNVRtNWxCcENqTHUwL3ROU29lVTRSbS9yM28rZ2ZvdW16dW1JWklCSzdRY01kWjJwY1VwcmR1cG0ydHl4MQpteTAxMzJEaU1wbkVYYm5wTm94Zmx5djBaR05wRUtUNlozM01zWERCa1VBeDlnVktrSENwWjhUWEFUMm1LMWFZCk5JYWhzdUVGQ0VwMkhXQW5xVmp1dUxWaGpLZ05XaUtkOGgwUEw3cEN5ZCtvaVpMYnkvQUFKNUV2S1QxVFFGRjAKaUQ2SnM3UXRpWExsYnBJdzhWV1dMVWp5dWt5b1AzMDJFS1hWQi9LVnc3WGdXNGV4YXpqNVlqY3p6UVRSZXFaZQoveDV5U0lxNHBDR2NZQ0VJeVZXL0JNNm5JVnpjYjRmTDZDc1VGTElvVEFSMG1jSkJrNzdJaWYrUkFOeUZQdUwzClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUYzYXJodXpGTE5yZ2lLZmdhRloKVVE4aHNqSjRNY0gxMS9tWHFvMWUyRUx6cVpyMlpRZGN0TWZpemY0eDJ6TS9GQ2FGM0puckhZKzlBL1R4dGx6bgpZSW0yUGw2STRwckdreVUxU0d0ZVUwVGpZdjZrMFJxQ0gwT0h6YStwWkpnb1ZaQlpwQzJPK0dRRzRYOHVqK2xDCjVGVVVQcnZVSk1PRXRCb25qVWEyL3VDcHJLMXY0cVlWSm5VaEh0MjFmMWhGUXdxQzFscEJpMTFpVjBKWjQ5T08KVUExdmsyN1Q4WTJkcWNzTjFpL0FxK3A0NDZldzlGUEZVYUFMT3BBOSticE1HL0FHdXV1RmhpdnB4QmNQYjFuTQpCK1FvOFovVkpIaWowNXBwUHJXS3l4Q3R4UE9UMk5hU29ZNEphTlY4TFBmckd6Uys3QXdzejdFVndQQnpwYlVSCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGYrY2NrOVFocWp5Mmw5NlRvUUsKMzRMWWlVRUxGSkhjRTlDTW1RRWh1b005d1ZaelJjV1lLTzZreSsyTFQ0Rzg3d2JONWkyZnFWK1dxUklTR3U5NApHVGZkNE9WZCtabHVzTUNOU09vRUFOYXBuck1xQzQvWHY1UVhoSWxNRk1jVUJGSVFVdDYrUmNCYkhLWnZLbUxhCnZZSWRBa3ZrNDJOZDBGSCtZVWN6dXhnMzBKNWpHaE44dDYzbWNLYmtlTkhqYjgzMUtNeldWanVkdlJDNVNlTG4KK01hVmRGNTdMQ2pKRFNlZ21TTUtBNytLMzA1VkdiaU9UT1NTWFFHVGhoQnZvcHI2OVozZTdDbWFtZG1VMU5hUgpuRmc4aFMyelJwT2pVM2xUc20zaWUzMERXYnIxQ3BNV2hZYkgzOVZvOS9mRVJVUjRhZnd2dWFOYldNWC9NaXEzCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXlhVUZMRHhGNnZBOFRiRG9ETXEKY3FtSEdZR0xUNTVYQVRDZmgxNElOZ2hTdDhjMHp6Y1hJTUNiSlc3OHNiV3F1M2RQN3lwSG1LTnY1ejNpTGZEZApLSmg2Q2J4ZDhJQythaW9wZ1FxQWh5MVh2b2JLTFB6bVUvOENCcndoRmtlZWFMWjlVT3ArY3p5WVExSlB2RmkrCm1waEZEQ0FDNld5TCtqeVNKYVlyWUxOVUJham1XK3l3V0RLeVM4T1d5Y2hSeE4xTHBkN0FhVVFreWRYVlAvZDMKbnNuRFo1QkhXTyt5MGl6bDhmMkJkcnVKZmZONFRGT1c2N3I1ZVd2bUcvRmVFM0RMSFhiNDJBTVZTODczNXFDTwpVUE9YOEZ2aklQYnVDQ01IcGRtdkRWK0xURzV2ekdRNW1weXc2WlJqSVdTUytreTJZc0FKeGVUYTJRa2VXLy9RCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcks3RTg0a29jQ01nTGRjMkFyaloKdS9IQzdPM0lJOVJGT1ZUckRYVFBXbCtjYm9OQW8ySGtraEdTMGxyYXQxaTJCREZHUHY4WENBWGlpa2ZtRUhoMQpOVlFGMCtXT1dsVzBMWE53VERqQnNpblBoOWM2SnJ1RGVQSzBBcWxKNStDMEFLMHI1dlpvdGFLZWt5VjdrZ1U1CldkTkw3dEo4bnFhdGxNL3B5RnAvL1plblVwS3llT1g3TU96TlJlRUdVeWlJNThMYTEzS1JyS3p3bDBmd0NTOVoKVWREbmltcVVUeWliK3B4QWc1RXFEVEJKTEZYNUJsQk1kTXJxR1JZUFNQMkpsOGxDdHgrL1NkVStNbGVpNEtMcApUbnV1ZmJ5WGRzRklUSUlTMzdrZERQM2s2WExlTlIxNkI2TVNTQWtZMjlHdlZnV3ZCYmx6UXBMYXZoTXdSUlRpCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjNXU2o4dDNTS05pOXRDZWZBRmEKcGs5WWE5aGt1UisxNDkvb1Mxa3BPRzYzcmVlYURsb1QrczJEcjU3Y1FFZjFadGdVYng1ZW1DbHNIbW5BbXEyWgpRRzdlZkN6OVFrbDhJajZldFlCT0lIUjNXMDduSDhQMDFHSWJ0dXo0cG9UTDBqejA3dTB0SWQ0UlpMSVI0dHg1CjhTRTRoc2RCU3lFbmY5bjdRS0hzbHNGeC9IMmJUZWxuQWkxejlPNG1BWjZzOGc2RmdIMFh1bXZOTE8yMjYyZUYKcDFDMGdSK1gycWhxRzVkQ1ptRDl0M3JjSVluYVdCMXJEdHVvMUFPZ3MxMzZhTGlNZ2Z4Ly9pU3FCWWhDT0k4bAoxQ1BOdVRTS0VKOG8vZUFseWFzWDFvenY0Vm4wSHY4WnhSbjhlSFRnZ1hJRm56N1MwZlZWMzdOdloybUwxL25sCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenNMT1pyN3FaNWlnV1huQlpJdFIKRmRyZDdzOTl0M3hCaEhnS2RueDY5bnBwekhWa2pSa0U4T250VlU0R3Y3Q2FyeDNCVUEvU0JYMStXVWJURUZldwpHdGhuWm5UNHc2OXBHYWtwVnNoWHgyYnBVYll6Mmg0OWZ6VklXZEg1VisyUm0yY3lpM1hUeklOVlVMZVBMd0VoCldEbTEvM0U0MEtFZmpWbHM5cWtaSmF1SEhseDlIc0FQY1FuMU5rTXdLK2Y5SVZzRXVRQlE1QUhKR2J6N3dsRnEKRkhwbW0wTUE5ZWdVbnNpWk5YQjFDT1c5M1dVbXIyWCtHTHFreklMLzkvOXFrcFY3TmxRVm5FRlBQWnhmcXVTcgp3UVdKTzZxc2thSDVVN2xWZWNaME9XOG5TU1NPMEY1SzNlcDBLVlBlc1ExK1pHMjVhTU5UaDN1djh5ZkFEREV4Cmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0EzZmdrRTAvY091dUdmL0ttQy8KanZEVW53NWptTk9FZlA2TlhUOTJZM2ZubFZNaFQxL2lvUElYeTlleHR5MTRwNnBtYUpLQzU3MGlqTHNrSHJtYgo4SnEyY1orWVZmOW0yM2lBUnJjUUlhK0hoT1AzUHpyZFZpZytla0NKd1JLVDhLbXZvaFVaOFM4THNzZlpETzBVCmZJUjFDcEpvTTcxS0haRlV6dlFDZ05UeWg0MGxMbFdKcUlZc3hEb2NpeGxGNURBWnBUclhrUmNXK3hNVXJPWnMKKzVNQjBJYWo0c0F4b1J1dWN5dEw5WnZLb2ZUbGMwclJJZnB1djNXNTlzZEJXcnFmc2grRUpSdWJLakJmdXU3WgoySGgreXBJL2p2U2pNMFp3eTJSaU5rNWErL1M3ZjByUGt0TFNaaVNBMkVqS3FNTUZsNGpFYVVHSjRaUmM5V29lCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnorMGlxWmNDOGlYT05rUjlqcTgKOExHOWV2UFpiTjNlVk80ZlJlQ2JMZldTWUdMc2d3NDR0blFqa2IrVHc3dkhmN2dxbjdCaXhTa0c1YjZGY3N2UgpIZkVhT0xEalhJWjY2MkY2MndhWkRrSjdqMk1YMzhtYXU0S2ZDOCtDZ0YySnlwVXJEU0JKNnlNMjcwV1NweUZJCkhtUzFzRENLbXpmd1NUejBVWWQzOTZPbC9aVFRtU1Jxa3V2NnF1Mm1uSlpGL0thdDFrWDh5Z0ZOdXhQeitBUmkKWk55WlNKRW91SmpoT29YT2tqcVlLV2tpU2JmbG1Eajd6T2FNQSs1dFlFL2ZKbFE0b2I4WDZ3c09BN2tCMEwweAo3ZHJ6T0U3SnlaSnNYNHRJVEkwcHM2Qmh1OE12NHdwRmp2NkZ1dGtGdFQwb0Q5UjVackw1YW9Fb3NPZDU0S2Y4CnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTE5SG5PVXBYSXMzeG5pNlBRcXcKaVpoaTVBOWNKWTNSd1RjVjFuSEpZV0pTMWVLZ3pmMXN4ZTQyREc1MlRSK2hVb052bVIzNmpTdjhETjMxYng0RQpZK2lQaHB1UlhuaFpUQzVjK1FwN2Ird2Y0cjZNS0NBWWYrSjVGTlZOVFVOa3ExcWNWYWIrMStveEliOVVnSlIwClQxdVlhWC9kVTRDT2xEaFdwOG51TnFCczE5a1hpQklKbll4bVdybCtPenZYRkhXYzgvc3dhSSswTTZJWDBia3cKSy90a2NMRjRsekJhK3htbHNGZDAzWGc5TTh6RDRLQ0lBUitTRmdsaENDTlV4YW40Vkl3NERzbGM5dGpEWlBaZgp3TWdaQ2tEb3J4SWJ2T1dhWDErWEM0OTZPRjd3TmJTNWpKZ3daNitscFlIVkI2ZVltdUdoem1vZUdNNnlWMllHCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXZPUTV3cVptVWNWRmI2Si9ZMGUKdlpRN2RrejRDc2g4alUzclNWQVA5TlYvcmpoRTFLOEhrVTFGd0IzaHdmOVA0ZEF5MVNRYnFQa09zZXRQYk5SSApldUdndEpZSDJ6WnJxdlVHTVBTL1BWSmNPOVZhelFnaWJLSEYrQVJBcEd3S3gvK1kxSG56TkVMR2ZGREhLejlUCmpMcjJRZ09ucXUzRVVZZ3lNanRrR2JuU1hJNXBxM2R5Y3pOOXArUWg0Y2lQUUFGZmI1c2dYeWZRK0xUUHI1UWkKdFNVUllLT2V6SldIdWdFWTgvSTA2eE1qSkFCeFJ5MTdJaTlxWFc4Mkx4WXJSeEhIcW8rRVAyNDBVQjM0SjN1RgplQUFabXVHcWJaek5oejlOeVBlV1Jaa1JVU292czBVeTgvTk4zWVFtOXFHQ0REMUFLOGpEWDFkME9GNzBrRElDCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnU2ZVVqTTlZUDVYb2w3WEF3M3UKYmJqS04zdUVLV3BOcmVtdzJGUXFEd2lJTEFVeVM3UkF5RUFnSGJLdkhqZG15bkRhUkpnb1hUSFBVaVh2ZkpTTgpJa2ViSHJJYysrdjd0Z3J4dGtvQXFoUm0xT05WcGtlcUdCa29LT3ArNlhMWjF0azJlck5VOGRnZmcwK3pibmI2CmNpeUo2Mk16eEpCaXJXdUI4RXdIeWwzMzZEZWx0YkozdmNYS1Nvd2ZwRDlQVEx1RWlFalJlSVNKQWdKV0Q5RkIKaHhhbVZ5eEQ4dmZmejkzbExheWdrbjJpTVY4S3o1aVlhYUVabVZiUXhVWlpvR2V6SlFPMURBSWhMSjI2RThncQpTTG9PVitSUGdhOFM0eUltamxURndFSGg3Q3huakVxemFWUW9jbzlxY1RJNzBHdjFma05LM3NQV2hHQkxmRHJoCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbk8xY09CVHU3My9UZ2FQbzRIdE4KL1VVS2lVWXh6cTFUWUJPYkdiSmgyeCtvNVlqcmw3T1lZSmdzNVM0QTQ5eXRvL3JWbG0yWERhYjZ3MjRWci9RNwpEVDkzSitnOEl3OVdGSWFWSWZpRGxOWVdiYXoyTnU1bFhRbGJlWlFvWDdvejNRdW9wKzR2UERTRElvNlBFSjRvCjdKQktDaEJiVHVGaVplaVdqbmpLWEM4Y25WeS9zY3JYOXFVb0JaWFNRUE1vRjQrUlA2OTF0WDhRTWJTV1RCTTQKUEdpeUg3Ny9nRmJuMGRKdVdQYmE1Snc4aUUxSHA2STJoMUxIODVXN1pzbURzVkU4UmVleVVnSU45V3RsZGpjTQpiOVowRTlPOWowZkhvS3RtYzRUUG1vS0VzOEJNR2tSbkpVVlBhM29lOGNDbHhEQnF1SGhxVXRQN3Boc0pIQjdDCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEhwclVKdXR2RHVVc2ZWVDhVdjkKVzcxV2RDMkYvOEt4TzRzUyt0b21Cem41QXhPVmpXajBhT2tUN3g0b0FWL2FueGJudjhuaGFIRzE3eUp5KzVCbApST3dqMFFEUFBKSVkxYTllbXlReDh2Q29IRnZyT1dLVnJRSHYrZEdWK0VMb3ppQTFXVU1UOTBHZHZvbGhHdmF6CnhpVC9OWC9ocllMM0tYSGR3SVo5SWNpcVoxaGhmNGRlWm1zY2c3YWtBR2dyVGQyNFZVMktwbUVGdjNKbTF4UWMKV2plL2JRNDJuTWpTUEdTbWZSUDdXaVY3QnR1TmpmMWtYSXVRU0ZYakJwS1VyQU1LQy9ZWjR4c1lHbzhFcG90QgpBWkc0UWR6dWFudXBZZHNyamJWaE9TQndXMi9MS1ZKYS9UNXBqaUJyMng2d2U1Njh3aEs2UHhqQzVSM2pCa2JRCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGRWK1N3L3FoZ28xNGp6aVBOZEEKaVBnSU5JVWUrRTZwWkhvNDFKTkNaNzQ3VFlsSVA1OS9ReExJVmx2Uk9tVVdnQi9tVkNwcU1PZGJrWXNIMFdmeAp0OGNJbm1LR2FzQW1nZlJSa2xmZERGeDUyMFZneFdvbkZhOG5QWDJaSmE3bnlqUVVkLzRoUEhtWUoxdjRIeS9HCkhGV2xpVzdtbGtoK2E1OVFrTnJCS0NkZmNacjJOZjc4S1lzUlpzVjd1RElRWU1qYXVnbktSTm5FMkdMR0I0Y00KbVJMaUU0TXU2aS8xS1YvQlRMYllOWXlVSjJqd1dubTMxWllCSHZDRnBCZ2c0SHp5dTNqUjdYZzBRVGJlVXo3Ygp0YzdhOVN3SE5wT24xWmFqMG9PWDg5dXAyeVNmN2N0NDk2MjJ2VFg4akZkTzZJTDBmYnhyamlDNldDWlNwd0FqCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXZoOEJyeXBIek41WVdCLzRsdU4KMkt1dTAxSHVRMTBCKzRLb1lDZHlqWkU2UXV5ekIzQUV0ejdEYUxPbFluMTlPNTYzY3NtSGpSYWI1eHJPbnZieApGUVBINlVQY1RITE56bndDNXpWa3J6Z0tRNTN5eFpXSHpqa0thQUFDNjFpNW1yRzdDS2U1NEV2UmhuWnc4aXVuCmFkNHJiSGNLSnRnN2Qzd3B3bUV2YnQremZpUnFMUjF3eUdBdmwrWFJkZHl3cnJDanI1Y01LVVlVOVg4a3JNaTgKelZlMFc1ZWdvQTRsQUpINFdRQS95QzBqYVVIY1k0aDEvS1BWK0EzMDBMRTBtRi9YeFNiM2hRK3NRL2FHRTRETwoyK2ZOUnFyZnlWeE5rVGJod2pHMmxnWWpNdUJRR1V6dWtjaWJTMnN2TmhuWXlMdjNPanlkMXE2WDhDeU9WS2JRCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjRhWURyUktSeXBINEczUFhKU1YKUjBLZmFtT0gxOUpRTkVoR3RTN0ZSdUxQeTBJL3p1Tk13cHkxWUFUZjlabk1GcnY3eGdsRUM1WGtveGpYSGJQVwp5RXoxSWJzcWZCN0hqM0R6dkRRL2V1SXJlVG9CcnppVi91dWt0ZE9WTGo5WEc1NUF2UmYxOTczSjUxV1drcUtvCkI2UG5XN2NqWHM0OXVjQVZFVEgrZmFvS0NmVU83RklLdFBIcWh4elRnaG5JdWFKbWVPOEtoUkJkWG5lL1ZkRFEKU3NzRm5KUUQ2VEJSZ21vTnZCa21lL1VJYkszYUYzSUhGVDFRQlRTZ0NkeW56dnVBV0hiTmFuSXZNY3dUeU5pMApXbllIYnh3YXBRWUpSZEIrTnNqazBwelZ1aXNLRTJIcVNFaVRxWTJKdnFWcFdDc1lFRFdZVmRiVU1haGJiZkZlCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMU12dzRlOGc4WkhkVkQwMkNlQ28KcFpMcnJZbHhDWWNTWHdKcllwakFqVEd1Y0pQdWNvTklOZXhoTHlmTm5mazYrZmRxSXdOZXFGUzZXbFMweXVMMwp0Z29PYkdPMVRIazhUeWUwRHkzY0E5M3Y5dUtrS1NlMHErQ1pCOVRkbWgxT3lZNUZoZlBnMEJWMGdUUTJOajVlCjF6a0s3allZeXVDaFBKUCtidnUrK3dTZTYrNlZsMUVrNHdiWnBET01MZ0hKOEJVZkZ0RmRwUHl0d1oxY0NzeEYKa3JFWUw3c0xhOXFzL1lpV1gzQmVPT0kwWExaMzFMZTFQQ1liWmlqWW1ZWUNpQkZ2ZmptOGNlbWRVVHVWc3VQYwpKSHUvb3ErK3hoSFZSZkFCMjZ4Z2cvaEh2NzMwRkRuWTFQWHh5aHFGalY1N3VKNXozSm04c2pvcUhzRCtVZ0dQCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejZUQ1hXNkgyWS9XQjcxL1hSMksKNTZ2emZvenVQdDVYOTZBSDc2QjFvWEZGZkpBS2gwTzQ0Y3JOZlRvalpxTERIQTF0TXJqbTVodlNYNTFrQ21ubQorUHE5RTVaMjdDQnppQWpvS0h3RmdZeC92dWJwUlA0MExScDhVSHRUbHE3K0t0NWllVVdOK2JtWGJkVnpjVkpDCk1hemE1di94aGY1VnFwR2VIR0oyNlM3QTBlV1o5ZW9xYlhwSUF0U3FQdjRNQ1RBZW1pUzZSSDhyalZKU0I0YkMKUXordzk0M3lWSG0vVWltcnpXZXhuVXZCbVcxdERsQ0sxU2VRdEl4VUk4Sm1jTXV0cTlWdU9JOFdhVHJjOHBBOApucExPKzhjK0dCRHBmZDFxcnEwYkJBdndiRU44d0pqYjVBSy9KMzBNV0x4MzR5UUZmTmhFUnVOdnp4emhZckQ5Cit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmNwNTV1dXJ1cEFEZ3RmZGJLRDgKUlFnMFBlcDgrakVhbTlSdVRvS1NxbnFTKzV1VDYwMUFNTCtyVDRFUW12SmFFVW1FQ2ZuNDR1MmlUbTJ1dVJVQwo4Ym04U01jaWd6S1o3UUpxVzBnMmdpdTQ3a21ZTWpYZXJFQjN6T3ZBUXE4WGlSSEdvbDlzbXcvWVNWalg3eldBCkRJajlPQ0FxeEJzSUdDcDZYTUhZaFFkU2ZJMGZ0dlR2b1RsdjFMMDJEZEhVQXQwOXFmVXlQbFdML1lmVnFGai8KYUNwbmwzWUZkRGNjV3pGeU5qdTJ4NitiTVBaWG5sdnVVM3NSRTBGenpLOXY5SEZjL2dFMUV6V1dKcE9EbzNvYwpPaTNVdlJKZmFRa3VUTHJYY0VvTDY4QzFJS21Zckx1N0pRZHYxYXRXOTRwbXh6TlU2WERSZ0NHSUx1WjdIb3lWCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckJvRU14ckZjQUppM1RmdThZb3MKQ2dVSFhLNnpPOHprMWtYM0Jwd1hKczBxcVV1Qms0RzlvUlU2OW1uUVAyeHNFbkc2NmNOb0hIRERKWkgxbGMrVQpXSGVJSk1ma1AyZUN5RFB2cmlvNVU4M1pKM0VyM2xVQzRQNzNoSVZaM0crQXhmM3FBdS9oK3U3TFBac1Z6R2lNCncyMnZ4YlIycXA2S0xRdHpYUG92MnVpbXZxNnJiNkdYTE53c2tmUjMwY0diZkUwNHR4UkdMY3RhaTlQTElLblgKT081Zm55TGVDYjdIVm0zU0xRMUV2S3FMRUJiVnhqN2RqUmF1RHgvOVZGTkp2bWNRa3RleEtmV2FFZDVDYjVxVwpMVGY5RzE3UloyME1QTkliY0FBTHpySjdmRU5LblBhSVk3SHZZQkFWUmJaVE5QaHovVEpGUVN5alNKWGhTVVAwCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDkzMksrWDRvMFZYSGszUUdNeTEKN21qQzVDTHN2NXhKL1RTWWx2d3pDTGN5dm9OamQ2T2d0aWEzZGZMc0daaitQYmlEK08zV1J1OTZSNG9ZVHN5ZAova0M1Vi96bExmR3RnRWE5cUFxU2s4RkN5K1I2UWUzRHR5UW1SalNkYVBKV1ZEcWovS0JYTkJYcG9lUk9WRm9oCnV2ekFTUzU3ejlTeEdOL2hWRTZLU0VJV0MzUUpCdzB0aU1WTlB1MFFYb3RqSzI4Q3crcHhGcDY1VEswK3F1QWYKZ0pqMlo4by9tc0gvT3dkZEozMTRqbjRqL0VxYkRJLzZqd2Y3bGtCTit2eWNLT0RYMzFnalVyYU5iSEVWaWMvMQpvZUV5bkxHTGs2anRBdmFOZURYT1N4b3QrOVlQbmE4NllWUlFYTkQ2diswbE53ZVY3N25qeTB0TS9mazNIcUtkCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTBVajlFVG5wZnJMZ2x4Q0UrQ0wKV1ZoMEhXQWtUOHpFeVV4bGtQOW9KZnd1SWNFRjkzaVBQWHl3SmtFZll1dFF2NHdNOHBvenY3RUNmYThKeTJiNApucW1rZmx6MVY4QmF4NFFZV05yWGhBVlRqT2lqd0ovaDA1d0ZRdHhBakhKMm1zTzhOYkViUmQ0VHUrN0lKbldJCitTS3ZpbCtpTHZyNTNMamxtMHJyUTlVcUpuUWNDMlF2dldJaEdqaDNvdytJN0pWQXk3b2hUMCtuWTh4S0hCN0gKVG5WOFAvOXhYVXp4RXNTUGZEaWdScU9tWGxTSkh2aEhtRjNDaG5DVStDdU95c2o0b2RLSFk1NVYydFRyV1FUcgpUUTJPTExGaHU3VjZKVXJ1YzRQdVdVNXVXMHNRMXZuRzIwMmRuY2FSdVEwUkFNcHhFcUZmMkwzbDNNSnI5cnJkCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0x0YU5MVmdlVEE2KzB6dk81alIKMUNWUUxqcncyY0xNeXkvalgyQTNIYTk4WHEyb1FHdHhvOFc1aEkvUkZrRDRYUGFRNHAxMXdmRERpR1VpWWNvaQpDaHJ4ZlgrWW1waGxOejYyUmVUOEZQb0QwSHhxekZUOThWb1RoelRvWHlTZGU5V24vUy9hcFpSd0c2Z0w2dmVaCktKQnA2VzJKVW9xaXZtSkNUWlNodU1DV2xuN2lDT1JTU3dJTXZjZjNvaXEzd3pGdnlQc29sWlZ3MWx1WElKc0IKZUdlZVo5M1dZU0FFT0JqeGU2VFdnYXNSMW1YNWQvbGd3TXZINnoydS9IWFBjbGl0R0k3KzA0UnVuTDdCakt0YwpZVlhwS01vN1NWMWVFS3F1NHJ6ejFPaDNnVk8yODdqVUNhTWFSOVNBaytHMkFCdHY1RXFSVmg4QVZqZnd1d0MwCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem4xeDhha2ZvZHdjV2tBSWxXcngKeksxdm1MdjlScjlDTnduT2VBZGdVcU9FN1pkaHBLN2pNZXVPMTRQcndaUld4eUhOb3U2ZTRUYmlNL3VEMEZ4dQpPMmdkMWJhV0ZuSVYzWkpWYm5zd2dtMTNRS3lUWnBCdXUxdmJkNXhISHdXbFpRMDVJdmNpNzdyTXdmSHFLYU51CktFcERpaGpZVVZuVU1TeklMYlFpVXAxUHl5bldHSUY3Z0VIT1ZPQlliNlVIZ0tyWVAzenk1QWVhNlFUNlR1c1EKbXQ5Z0o1aUxCdHRZaTRkcSthbWlDaXVreSsvSmtmOEt2cGl3YzN0OStmMnMzanZMVnFmWWJkT2lyWWVHS1pudwpaVnI3dHBMUDRBTmE1YVZONnlmVXFYTHJoKzdoenRySzdvd2NHZVlRQ1pDZkkzZnFwWXNQd0lJVEhyNk8zY003CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0Nvc1JHeWtxbUFXazg5UkVNaEoKcUtWYmtVRzZwV2wyTHJzcWcwRTNnZXFjbm1Uem9DYVFCaTBEczJPMk1CS1pkbG9IVDVPR3ludHBmdzNuTklVbQpFYWszVm5Wa1JHbEppajdYWFhlYzFYZlZ3MDNPTE43UWMvTG1zN1hmM3dodERBNUljSmhHNUc3S3RPc0YzMWdoCjQxVDk4b05KS2VjOWVPQ1hiSDRCV1RvdUNvN0lXQUFDL3orUlVQYXpra3R4bGFpT0hlMG9KVEx4YlU0dUpVNnoKa21sNmpRS25zZUtSZjFyeHk3YWFiZk1oakNRRHphVmNMM3BlMkF6S0xyZ3AyQ0tOTjZuK3A0Umd4WmxBK08rNQpyMXVYNGtlVTh3V0pKZXVXT1ZyMUI4ZFBGZ2VDVkpST1owVm15VGtPZHlPZlNFWjAxL1lLMzFhVlJaTjB6S2tVCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1BYY0ozZ3RNVUppQnB2VFJhSkgKUy9yaTNvVTdUVnNuT2dreXhHQTJsK2puaFEva3FjV0lkdmIyWUpialhPOFltNW52bUd6Y3NuZU1HYVA0TVA1OQo5K1p3TXc4K3M0aDNFc3JMQWYrTGhCQmlzeGs4TmFKbGFneHhHODhwdVNuL1dCRy9WdVZhYU9RVGFGbEZQN2ZlCk44TVp5dUp2dDNhZVJuanhTMUFPU1ZsMnJSUkpMOUQ1eFhLeUdza1d0aDFHMWs2V280QXdxU2xDdU1MT1R5RjMKdkxFZ2Ftdy9OQnZLdzEwcjVURFlHQVhIUWFKMCtENElpZVZ2ZWo5aFZYZ1RVVnoyUERiVlpZTDlFZjdTbk9wagpwdmZZMXhwNUlVRCtkTHplWTJtMzRvSDJWdXo3Sm5ERDAvVXptVEp5SXdlTXFETmlDbS95T05qQVhCMUtFQ1Z3CmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWhZWlFiNjk3UGgzaFppNGJmcVIKbFlaMC9pNXpOL2NaWFZodjVpR05rWHZKMkxBOUs1c2FIcnpmVHdzby8raVA5Y3lqcmlLVElhWXQ5UGkyTXdKdApZdkZYYWFSSVNQZHZCdTcvR01FUWUrSTZSUHZock9ON2lVYkNkSnMzV3Z0bmw3V3RvVUhXZjZHVW1FVEpsT2JHCjBJQzM2cWlqUWtuWGZYeHgrbmtLK1o3dCtHOW1temUvc2ducENZWlVXS3BHQnBqczNXajN6VU54WTlLRjJYNi8KQTYvNW5ac3lXVFh6a2ZDVVg5V3Q1cloyNC9JNXVvRmJRalhHT0FaQVZ4UjB0UUtabkt5UVlSYU0xV1dCMVJucgpRUnpzZ25UbyttSHNTazQ4Q1pzYk5KbytLcTF1NitKemM2U2dxRzk4ZU03ZzhjcnErWFFNT3pwU1FNYmR5aDRvCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0xNT09KV1VIM0hZMFJGQTNWMlMKUnhrRUJsb0grVk1HMmc5UzdkaFVCVkQ3ZGxLT1Z5L0lXVlhhOENSNkJndnRhdjV5ZWNreDQ4UmlUVDJadVB2cApjR3VGZmpLaW45cmZiMkZNSmxjRi96Y2NKY1U1ZzFaZjcyM2EvejNLa2VmUnZ6SXF5NnVKeHF3Mjc0TGYwWE03CmdGbTFNVkl5alMxMXNKc3JpZ2E0M0Z5UlZ4aWFtWllOOCtROVpUWFA5NWNmeWFqSEkxeHphYjR6ZzdaaW5WUFUKbDFoTXRibVZNams0czU5aTJWQnVyRWdkVXV0YXNSSldKZURlVWdlQ0s4UkVjdmpDcE5uckdFVnkrNWIrc2NTWQpYN3lwVEpERUpsb0pKMjErOVJ2N2w2LzlkcTVpajNmUWdxbnBkY3FuU2ErK29DRUhycnRVWUhQOFgvSXVpRXhtCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmJOVWdPTjFPbGE3WkUycGlDbjIKYWhiVzZsaU1uNkEycGt2YVVmdkJjNWdIWVA0a2JRYzNRck9FK2RSbjFzY0QrUkZ1YW9vTmlPUGFnSzEvbnp4MQpwcjFsb2ZCTnVXTFRhR29neW9RQ0JhODhiOHJMTnh1SEIyVXdaVkQ3WVc5Z0VKV0IyUDM4Ni9MOVQzcnN0NE82Cm1DeHptU25aWWtHbnp0VG9JeCsrVVkzSWswaE5tUTMvQzRmVW04R3huWlNEbW9OaWdDcURheEtSWjhRNzlidUkKY0hDTGZPM3lSVGFsVHBjSDBnVGlMMkhsZVczbnZoZHdpenRhem9xYVB4d0crUE1hcVMwWGFJWjdTUjRzaVVVKwp6QWl3ZHgveG9mc0N0K1pZbldLempQUk1KRlU5Y3hHbGo2anRWKytjdzlTdzdkd1pKUDlTL2s3QlZPV0NEdE1GClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbHJ1Tm5QU0ZUTW9hNDlNZXUvc0gKd2RscFJXdGpXNE5NcTI5YVl2cUlkV01PSDNJeS82c2tuKzVIUEVVS2MwRm50RVN5Wm55eTlxaEtKZkhNVGJEZgo3WjJoVzJ3Tk1IWE9VWUFRWHBJaFhJcjUzVU4vNWp0andjRFRnUWZ4a3VvSDREeTVqbkhZTmRMaDBIM09NcjRDCnZyZktwY3BFRXZWdEFWSFpmRUE0ZGxyTW4rYmR1Q1dCTk80K04zblpqbDFhaUxqMjV4WUdUME9QcktiOVBTSW8KSmpZZmllNjVqb2VXWUJ6THF2MFdsUnVIeVZYT3RTS2RISjlFWWZld2YwWXA2U1VwN1RzWVJuT042U3B6cExDMgo2bnJIUzVSaHlETndYTlVneXd1dXVCVnN0OVJtWkVtc2NOREMvcWZoUnNNNDVWeTA0bnVmMHhSRVg3UFh6aC8zClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2tKYURtRlF4VFg2RVVlaHBlUFQKblpyWitaK3FEd1prRXJRV3MvTy82UjBOWGhDaFh3UlR2dlY3c2NzaGQ3KzBFNXEwUXFmUnU2SXFDV2RQbllwSAovcHQxbXpBWk1aQmtMZkgyaWtTWHNwS3hDS0hjYy9Mclh2Z0JxQjZiNXlmRVcrOG9tVTlrV1ovMlowSFhYRVpzCkRQZ21EU3U2R0dBODNzUVl4VEtHNlJyRzA4d0JDUWVEdEV4ZzhqZStTV3VNaTh4MC9IVFJ6Wk1xZnNoa1hyUnIKbFhxNjJYdkZNTUdURFVJQ25NQ0tKSU5WQzEzS3N1Zk5VQ3pBNEp6UE0wa2xRZEQ3OEtMOWx3dnhZRDV1Um45ZQpBaFdmeERuOWdiQVhHZkx3eXAyZFhmME5yaDZCU2IybzNoK1Jma0F1aVRZNHZhenFaSWMxbHFKSk1WZWZTM0k1Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3F2K3JvK0ZSUVJqNHFyMW5zTXUKVkgwYy9VcVpmQ3B5RXp4TlZCQ2QrZW4xRjVHVlNTU3ZxQ2ZrVCs2UnduZUo0YS9xc29YSXZUZDRLeVRNdHlzagpaZTdSeDZJUWRMQWtqc0R3OUt3Qk4rMW54NCtueUF4T240cUtheGR1N3BzaUpDNkplaENTTkNpU1JwY1NyUHh5Cnl3cUFLRldWNHB2andnVFNGdlRxS3lWWEIwbXY0bW5WNzhlY0FhRHZGbjRLOGQ3MlUwV0hiQWRrUFE1dFdEVFAKWUZvNmNLT0dYVW5rdnpBY0dSQjlPQ3I4azI5eG1wcUNtT25SNkV5bG9FZGorSW5kMWJ5MlBvakNWZkpJdStlVQpHMlMrS2xGb2swVnFwTkVLVXZ6NjFSTFRyQ2cvOHVyRU0yUjQxZTFhUjRXdy9iQk14aGhCdVNPMWRJUDdxNG0wCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEdNYkdSWFdvY1BoL0JLS3Y1eVMKSXFSWDFIbUo4VTdlbWE5N0pKMXVIcGJtczJKV1cxakJaNEhqTkVUUDRyY2FaSFJzckpHVlZndTJQcno3aUxOMgpMTk0wVjFad3hTWFVHNXI2ZHovY0JvbkZ4RFRIUVNPOHM2RUhoSm1lMzNiV3AyWEtSTzNINXNZSkFmcFUyRXBPCk1QdjVwMHoxUGMyY21PaDlYZWxVQVhyWTJLb1AxQ01Ob3htVHNJOHk5bFNNVjFHYVoydXp0ZUhHaXk2N1M4ZlIKSUZjVEgxME1TRmtUU3lTNURvUUQ4QzVIU01OSTlwSi9iS24xbWNIemZqZURtc3FLZ3RHZkhVY3pMT2VnTVE1NAppT0tSaUVBVXdLL3VOTkRjRkxKR2gxWmNsbkpjZTR4SjFWT1hwWFlMZzlleHppYjdUQmhhaHZ1OUowNXo3QlYzClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcno3MUtKRnljZ1FLZ2NmRTNERWwKYWRRNk4rNFhkcmVJL0FkMU1QTjJFdGJ0RW1VcU9mcU5qaGdWZ1NoOHNNM2k5NVVtUWMwSXloKzRmS1A0ZDdXZwo5RW9pK09zdlE2Q0ZGSnpCNmRZQkJ3V2xTMFE3YTFpZTlNTjBJN0hUZGc3WlhhZzhRS3VrMzROcVMzeU85RTVXCnlwS1lrckdRS25JUW5XKzdZV0IzdUJkYUFublFWNlR3anRmYTRaaUlqRFIwblFaRW1BTjlHZWhqUFl1U3VDVDcKQmtHdlZ5Z25mdzNhNVNGblAyNmdNc2tPTWFNNXBnRGNadUZsQ1RBWnFzOVhiL2Q3MmFIaHN6c092NVl4ZHl0bQp6aUZDM0RFeWRWMUthYldVL1g2RTI4d2drUlFNK3pjREdtRDRGaXBTanZ1ZVhkQW9NTWEybkxnc1pYa0J2ZjBpCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeThVVHBQUmY4N0gyWEFzS2RSREQKUDk1czA1c2Y4RVJVU1V6bDJ3Tlk0eG5QZzdrbnNUeWJFekVYL09jYm94b1VhRVRLZW0zMGtVY1A2bGJqY0FaRgpEV1R2ajZlVHFQeU9BQzc2TUZHbjAyK0liMzFpOVFlcWxzR2sxTjZnZzM1Q2lGaVlOTytPUVRUS3hqc0lxN0o5CjdyS2Z4SFZhUXZWZ0tWZlg0VzZQYmJXbjZBemM3dHFnRTRwVktKODlDUGNaajFPTUJPYTExM1BWNlVLZ3dIK1MKTFY5SFdrNXlQdWRsT1JXSjR3T2R5OXBTcmtSVHRoRlBtOXlldXFrZDRGdXcyU1hqUXpXbFRTeUptbmFudkxwegorbkU5UFQ5MVNqc1pRdGlrWE8wc3NVemRGN09MUGMwS0MzUFJ0SHdHYVI1cDBXWEZENUFzb1NzREZaMWNnOEQrCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMCtadUYxWVptT1VMa0d5VGNpTTgKWnVUeXVhM25UZzBVTWJ5akNibjVtcTR4RXUzSytCaVpzN2lUUXgrRVNOcW96MitWTitzSDR4UTRhRkp0UmY1YgpKSFdXS0ZHRUw2VFY4Ni9BYVZNTE9lZzAvTkFrTmxieCtscGNBcURSSGd2bGxUbUFUUWxkUzhBb2t5TXRYOTJzCklOVG05RVVkWlBocTBlQmFnVit1b3dFeldRMDZ2Ym9TSmRibVgvVklaTzcvL1FiN3BqUHNCL2dUUDE5dmtwZUkKRVUxcGdzYXFvSFAwUEFGLzRsM256WExmN2p1dEhuaHVvMGc4S0wwR0pWQitZK1E2N0hOZjVLaUJpTUQzdnJKTQpUdVMyRGkrOEIvMmZ4UlRSdUFpb2FxK1BqTGhzM21kTlNPNzZVM3N4cVVmRDUvbEhwMjF6WDhMRUdyYmMwWFdlCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWxVM2RYSDFkazQ2WlpsUC9TeUcKVm12M2UxdjU3eElKMW9Hek9qcFpyZGRNbXVtSlg2Q21uZGNvNll4NFVGVThZQk5mbnlMa2dOaERROXhCdzZObgpmNCtDZE1RcEFqaGlXU1RDV3I5clJQTmZBQzdoMUpFYi9kQWdqY2padUt5UXVMUnVMRGh0NEthdVVHcElWdEpkCmVPY3o3ay9RT1dpckV0MWR0NnJsWUZaNlhkcXVTQWlwb2J2M013QXJqd3V3UktyNzFtM1YxTk1SdytSVW5KcE0KemJid1pyQUhOM3Rmei9ucnJpamljUkRKQndMYVdOdExXeW81a3F6eEh5Ymh6eUtRNjJqSUdlWFZPSjZoMUh1bAozbGJtdFpTbmc5SFZXaGQ5TzhxaFViVUZPU1NiVENkOGtGVTNoVWRrRVl1dWx1VHl5UzQ0UzVQMFhnNm5INDZUCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE4vVG10TTd6cTErclhFUGhkR1QKMmNoemdRY1BqSjltUGZRNmdnU0pzdmNMY2F0eDJkdWtFVnVoMnV1SXh0cEVBSlVJT0xCV0hGMG1QMm03REFwcAp5ZitiSUppZVgxdTZPalNEQjlnSXNOOWJwT01Ld2tVZEJ4Qk1aUnZGSFBIb3ZycFJibm01bldNaUZQMjgrK1VpCncvKzdSTUdaWitUYXRDbUg4ZmRLenZXY0lyekh1T0FlQlppTFZpYnVxdDhackZlOGpFcjkxU2VrblR1SlNTRzQKbnlPTmpxcEVYRSs3dE1mUzMwT1Y5VGUrSlliNWVkRmVhZTdWUjdMaDI3azZ1eWtwdExkU1A3bGtWYzUvMVlOeApNMFdzZU9naVg1b0dobWFoeWJiWkk1eHJmbklqeFJwTEdocm9ML05WU01HTnVVeFAxdzdoVE8wZGlQUlo3TUh1CmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVgwVUV5ZmdvaWNTV3Nab1YvbUUKNTBTNHgybityQ25RVXc0YStBUUNQUURRMFl5cmxCOUErUjB2QnVwMHdHR1l1ZlZLWnhZc1haNFYxWnhOb1NvYQpNSUt5Vkl3bWs0SnJxNjk2NlVHWExGbWF3SzFqeVVDTVJoVmZadU5CNjVYZHRPUlZNbGNVRGhyN204UDhlT2dnCkhIMmkwR0RJdnpNc0dqd2JaNEFsTkxSK3ltMjFHZU1EY0htY2hZVmhHNGtKTlQxMzdtMlBkMmxTdnVWUXBabjgKTVlFUklWRHR0cHozTTlGVGg2a1dnT2NzMVVzVVk1YUwrc3RpTkdYNWFmTDg1dWhUSmlENlMzaEV5emtacXQzcwpOL2pzak9TT2psMGxXcThsY0gzM3U1RjhOQXdLT2VSdC9MdUhEVkhlaUVvdFhKT0pOOTNIZVRsZjZTTXlSQjQvCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFlicUgyekdpaFBqZUJLbDBOeTUKN2p5aDlCaTk2Y2N4ZlFnY3Q2Z01xUGczVk9KQTl5SXV6NFhjWmxRdnJLbjY4SlVKOFVlQmF3a2s0cm9YaFJ2RAppU3BUSCtBeHJUTVNYV3N5L1lRK1ErL0RVL1F2cW9rWENUbFJzWE02eFp1SlJiWW16Z1FzVHJCY0dEQkxWMU9mCmNKelRKRnlETHZYS3pKRFRSdFFnR093Yy9RajlaWmNxeDRUMGxVdTNFL2lJRG5IbTNOeEsyWTZpamVQMmtzdjcKSXc4aWlmUVE1UFh1S1l1d1hEZjVyaWtjNUpKVmsydjMzYjFNN2tsSnQ4amRSMy9JaVVIT1Ryc0dVeUVhSTdxTgpsSC9wN1gwMFFKcUFFTyt0S2NtUHV3UXVEUG5WbkpxTitIV0Q2Z3h5eEltNnhJZ05ON3BRNHJjNXRkYnFmSDdXCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVRSNGdvcVlLaVY4RHFhY09qYlIKMURsWHBjOXptWTAyNXpMN1JhNkdVV0JJdHl6dHFPaW5nSXFoakVGU2hNV1VDMElyU29jU2g1U3hNcmN2dm1MUgo1bVpYYVQzZmRvUkJIeTRuZmQveVduWFN0amdPa3M1UENEMGtDU0pSTXhibzRKSFBsbWhLQ244UTk1ZXBSOWwxCmphTVA4NEt0Q29kOUdHVnZPSzJwNXpSM2l1aWtRYUlFTE5XVHJSaEhudFRxZTNELytNa0pLTlBSU0gzTEpHalIKYjVld1JhWTVIdUdQWHZrYlY1N0lpT3EyTnJ2QzdBdDZ5SmtOcGEyTUFHYTB0RjlXRFJYN2l1SmZBUmRYK293eApsTzlKTGhSN0ZJd2hXYWdvTklOUldONUJQNEF6dWx6U1AxVC90Y2cyUFZ1VWdpYkZOZGFFb3JaUGZyZ29VTHNMCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekhIYUVxMGpFbUVSVC95TnhqenEKelJTeFpaSXVNMlNTeVhYdWtwWGRROHNpbEE0ZmNLVzlSbGJnR1FoOWRzYkR2QWhZV3dubkd5Yy8vWE5DY2htTApKYktZOGpmajZhVzNNQW9QczRKLzhkdEJnallHWWZTN1RSNjJuMWY2eFdQUFIvbDhYZ0FGdS9JckpydmlOSmNsCkxDRCtHdnNlbHY0czZweHpkTzYyM2pybjlwWENYOVZvSW5Dc2ZXYVNqVW13UEF0bUtxeXEya1luY2d1b1BsRzgKZzhOdWlXVGEvNElDdVkvbzlKM2h3UTllOEF6TWJqM1JoMnl6Q0NwcFN1bmFQTHpPbzdsVTJ3Y3JJVnA2Q21hQgpPZWl0NHNqdUtsOEc1ZVZoWGlRcEU4Rmd3ZG5FaHR0b0FjVW5ESURPSzM4RWNNRU1ZMDVteWRQWlFFamN4dUVZCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeExEY29jcFFpY1RiMVlRWkxDS0UKeDdWSUtIU1FmaDVwbG5DcmZBVTRWTWxQb3ZUZVdqSlJaQytzbzB1V2Jhb3RBUEJwaldXbFd4UjVJdkZkS1RDVQorTWdCSFJFd0lvbGZzeC8vWFIzNjRhVC9VSkcyNHJMdXVVbHgvREJac2gwSzlzWE80ZXh2ajBYZ1FkV0xlckdjCnRHYTNEWVlqUTZUU21maStJcEhuWUhBb29wMDR6cFMxQi9wR0Z1NWk2eXhnOHdyc2F6T0sxTys1ei8yanNHWGYKcTdDYzl1NW9oa0tqN3VQVXc1V1BqVFkzckpyOWZERzYwbVp3a2lLNHE3MEQwQjZLbzFRZ3RQeHZpNzlVVW5NRAoxZ0NzUVNZWVRBZmRBS3FFaVhzeDNveDNnaVZsUTFxQ1lMcHRHL1JzbGZvb0lXaXF2SFdZMkUvb1hVdmV5Q1ZhCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUw3eDVBN1lyNHk4Mzdva0pZdm4KM2p6RVdQVHBwZlhjWlU4eC82amVOQXJicVBPNkk4SGdWb0V4ZTdZSlhzSE1Dd1ZiaElYazN0WXpSVVV0Zkp5aQpqSExhVmI0Y3RWYW5GVlB6Ym0wT1JYbGdna1pDOFNIOVpsUEJNa1plL1RSdVFVTXdXME90RmFrbkxXUlo1Z0dUCnMvS3BSSTl5QWRUWmJQeUJVWUhQelpMZjRhdG92aXJ6bXNNNXlZMjVlSFJ1eUxFcGJ1ZVJQY3FlQURLckErbkgKQXp3RW9wZWZWL0liZWl3S3J4aVlxMUdlWksvSkY5a09ob2tKdmxDelZSOVJQaWR3Vml6Sk40d05KSzY5Y1g2SgpCdUVSQVFxUk9qbnEzMW5RYWJSRGpEdlJsOVVUaklPNHNPZjB3SGhxV01PVk1PM2I1RDlSQmRPTXZUWkFHckJtCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGpLb3AzNW4zQ0RmL08xMjIwcGkKT0FnNnkrRG56TGxvVzFNcWtQbVc4N3pqa1FiT1BuT0Z2dkE4QmRMY0cvTWxJNmtnUXBDc245UWl6VlRoV0kzSgp0OTZJcnQ4aUFGSDV0TnhwRGxDeG9VVXBlSEhXTFBGb09pSmpaZTlpMGxWWmlPTDRNR0RvQjZIWUJEVzFnakRiCjJNTFZKeUhGUnY1MHBqQm82a3didUEvTTBOWSt5VkZqT0hxRFJwS1B0bTVSMGpJZlQ3RVRpWEFOMGhCb2NuTTgKWWltQ3ZpODJHN0owTS9EYUl5MzlXZFMzVC9aZW02TkJqc24rSFpmVXV4YjdDSDlWZkwwQ0RobVZIVnpMbjBURgpVanJENHQ5OFhRck1ZanVubWJTZnRrdXl1aUwwWmhMV212Nis4QisxVHJScitKVXczYWY3RSt3UUpBcmlldTV0CkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0FlVDhOWGxVeFFlVEI5dXRNNmUKT3pFRytwRWZvMEREVjkvMzg5a1NaaElMeG9VWG1YUElySXZkd092MGJrNzFTSjdaMmlrRmR2Rk5mUDFGSWtzYgpTZER3MVM3R3JiZW83bnU5c3BtNTkrVkdrMThyVUFzWmpwblFjU2NGeWVsNTVlSTAzWGQyMHFSdDJDV1VqSWRqCjR0OWhvMUk0cHhaSjQxOTNIaENPUWttSTAzQkRVanJELzFXQ1hJMVdyZG85ZXp0TXdqWWFLWkRaNjh3Rm5EbGkKMkRLT0diS0JKcm1SMXE4SzJxZ2NtS0FWNkdGOW4wUUwvQnJiUDVQaVNocWoxSTdtNjVEUXVTQThkeURkRnpFaQpMRVhCTWorSzl6NzI5VzFCNVdNaURRMEtLR01xckJvTHVTNE1oWWtFTEVra0VPdHdkbm5QZW1xWndBSTZPSGJWCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXZxRk9SaHU4U3RPU0l6MUhRWGsKdHlYUkl3WjF3VjFrQm0zSHJuZVJaYTduRTZDZHJXQ2xxTndWQ0dlZzdtc245eXpoN3Q5aEdXVEpNYXhucEFPYQpCcDcyRG1pTTBsaTRUUFZtOGJKb0lmdGlqdXZBdjU1SENET3hqZ3J0cjhHSlgwbFRpYlFjckRoc0dFSGY1V3ROCmhhZUw5cTR2dTZUTzBzZ2R4S1ROdjc5RmwyREFBMlhxQ3I3R21aOVhYNUk1dzFoOHFxTUdCVFlMQVc1Qmg2b2sKeFNZSkcwd0VIODZNMHprV2NzOEE1bE5mMko1a1o4QjRMV3VVRnFzYkx6UWpMM1lnazMrTmN6c01GOHVCWUdkdQozODF1cmZuWHNuQ2U0ck5NSGVLWmNmTkdEd0J1SjFObFBzQW9acFk2SS9ITDNQc0FNK0x3OHpXSGc2eks4akFSCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVcyZ3NlcUVMOGpzdldnZTdSSlAKSlBjWCtWVGRtQU1UWXVybTJXd0h5a2xqc2NPSUN6SFRYVEQ0K2FWaWlvdTJqbytBK2pRRVJYQlNReWlQdFdreQp5aHQ0YllqUmFpYlBmT3M4OEVkTnZOK21PQStNWWY3OGp4dHVXVkJBYk16cG5XQ0xQcnllZTc4S0lpM1Ixc1ZlCmZreStSY0srSUU0eWo1RmhONUg4SDE0UHhMd25pWTFtYnZtMGJFeDZPZytpeWpEZnB1dDdhQVN0VWtLR01RcDgKMGllbm9OWTFzN0VyNnRnay9HbVl0Tk5rZFpxK0sraUV3bzB0QzIzNUtKaEZZU09VVWhYdnJvVXZFc1d4ZFZLaAoxNEhEQjdiVFZhWk01cEZPRGZ5N2ZmcnVUdElVOTdxc2hRVzlCK1YwZE9iK20vNlNoQ2p6SkU5ZGI0V1dRQkZECmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3E0RVBpWWtxWkZTdWdWWFM3dTAKN2VwMU5pK214TjhqQlVYTHpNSEMxWTdhZEx4cC9nV1gwcDBBNWFCSkZTT202VXd6eVQvV2puRm9oaWV3eTdaUgpXMTNpSU9MangrWTYwUXFWNklTVDgyOVNFejdWalM5OFB0dFNITDRkelhmYis1QVRJWEdtVlp5MSs2dmJ0cVh6CmVxSUkzNFluZm1zTWR3M1BneHhJWW1aSmEvRXNGUmtqSERKRnV5KzhtTEx0bkJNS093c3F5cFdKMmduL1RmYysKOCtoemY1Y3hJME54N2l4bUExaUlzNGdWU2N2ZUtGSXN5bW9ycDNCTXIydEllL0wzM29OektNZU5VNTI4c0MzVwpvR2doSVFMSytycmFtc25MU3MzQ081ZVlDNTJZSXZDWjhsQUtBOW1Mc1l5S2l6MzVoNjl4Yk5ZeDlZSkNjU3U0CkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzJMTUV5eFhSS1RtdWpNR2ZGMHoKUy9FMXBZeU5Wb1pxenk0ZmRUYWUya3dRcU9BTUFWWnBPaUxkY2pGRURsNzJwWGdhcHJyQmtSSjNLK3ZCeWtDTAp6UUk2SE10bWNHZFdqNm1vVW5NcHlsenZLVjc0WFBEbyszNkVPYWpZR05mZGcraFBYR2FDTjQwNUhHc2EwWlU4CkZVYm1jdG8zQWYvbUdsSHczdHNXVTJXUGJTOWxFbnFtUjVNTVF5QmFmaUF1M2pKcktLdGI2UStVWEwwbUgrOW0KUHZ3bXFUS3ovYW1KZkQ0SlMxaEFKTnlISWVlREh4Q0QyVEFiMVhDcWVLQzhsYTA5REpEcG1Ed25nSWY5UVdKagpKREdnT1lzbExaRXpmUC8vU0dOdzVxRi85TzdaWE13VEZkQVh3Z3k0OWI5V0ZTeW9QdUFXK2U4SWxIem1raWo5CkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmkwbWQwUTF6bm1PS3lYQzFvWjIKSUUwR1R2a2RLaUdadjU3WEJrbUNTUWd0WXN1ME5JUkhGVWZLRi9XRngxdnZxUXZaMElTQlVnYmZHNHBINHZTawptZk4xdnB0cjZ5bzV2K1pOYkdheEdZazhMQzRQN2g2Ym5hU0QyMWFqeTJKNUxuaGZ4SThMWC9oUk8zN3Zmb0JaCmt6K2dVWTByeUFLT0NCWlVFMzRRd2hNYmVUaUVlaGZ5VklKSWF1bG9BNmlVWkU0dmMzKzBET0lRamFnbDhzSXgKN1RBcXl2UzF0Qi8yZnB2d3BaR0gvUVJFakJoSkZRaGFTaWMxMFVIVm1raXRzK0NUYnJibEJXTzM3VHdMb2FBSApyNysvNGhSLzFqRTFsYk5MRnA0d1ltcmVmeXducnRrMXJ0QWYreFdBenF3OGNFSU5nWGR3MHpvTkVNclhsUVVQCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeERseFBtM3IzNVZkckQ3STdqOTcKcm9WODZ1cTk3dmVjeVNFbG9YcGNaRGV1elpkTGV5eGJxZTJybTUycWFnVEM4YWcrdUtWNjd1MTBvR2FrZjVyZQpKM0pTam00WWFmZG02M2h6c3VDOHR6UmMreDlTOGk1dnFEbVBnL0hSbGlsT0lPSEE4ZDhBdzVqZXUwNkluQXo4Cnp6N1NmY0svQ1FUS0p0R0FRNDVBYmNJZUxYZnVBZ0FhYVVacVltbHdnaVU1d0tvQ3VJVjNpeFIxeldBZVR4bUsKelRhTGF0Y2FxQm1kaERoc2g5YjRjaW5xSG0waGdaOERlQjVuYzgrT0Q3bFhKU2twNmtOSVBxbmQ0anA3cVZKRQoyZkJjUVdDZEhMMXZ1VzFZZGZ2Vk5IWTcyN0Q4cXU2Z09sMjBzYXplemN5dDdIQXdWMlhiTE5HZDUxVnZ5cithCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUxjYklxdVJBYWNLZnZwZGVLOFUKMkVodmRKRkFhTkRRaXh1cVpIOVF0WWJMemFYZVdMOS8zZjVrQ1hLL2cwZ05RcnFYc1Q0aDk4ek5tOTdEMkFGaQpQcGwyMk9vNzF2NEh6ZGpMaVZJMkkzbStmMDhieGdTSmRHUTR0WjVQK1o5dXFiRFlIaEF1M0czOHNtaFFrMmtMClU0ODBDRGlkQUhOc2VXMUZUaUM2d0hqZ1lrQTFQRk1KQ2VxRFdIcno0OWtKUW5rK0R6Q1FxTGZnQi9YVTBoM0gKSTQyQnNzQTNDeERrZ29oMENtazhUZUxmRGY2ZkV1U2xsRWM4UDZFS0c0NXBQQ1ZEb3lsNHpTdjdxelRjMWllbAp0ZkFYaUZPMjFyaXhIcmtFUjJvanlXeU53YmE2R1JPTWhYMHZhYTBnbkVZVmpaMU1iSkxHTmhvRW9OSHlOR1RtCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTNreTgydWxQdXo3UWpLQ2ZuV2MKQ3V4UEtCYjFzNHRQQXNJMFEzN3R3Q0hIV2dFdjRMcWJsYmEyeWZYamUzdTVoNnVMVjJsYUFlN3ZKTmk1b3NZVgpRQ2M5Z2cwNHJBYVVlc3FMaW95YWNBQXFqRUFtc3B6anZIQUVLeVFSZVAxNXQzZ1pYNndUWDVmMlVMMXdhOW5CCmFYVTFHMmFqU29XbStBaVpXQmJjRFVZZDZEbG50NVZxcE5IWWNRSUVzaVRvZ2xUNHJ1T2FSNExJaS83bUFDL1oKT3YrVXh4VHR3SmozdWpIaVQ4S1F4OHh2eFhrMlh6SFpKR05nQ0tka2U0Qko1RGNLRkpMd1dtd2l6Uk8rNlhNTQpPMXZiV09aa3FnbzUwa0YveU1ub2kyOVNDTGhxdjRrSE1RY0J1ZGlZSm4rR3JKOTI0SU0ydUcyVG1vR3VkU2hSCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2N3NWMxQWVaUC9CUjg0ZTlTaG4KVnFMZC8yQkFxZnVnWnYrRXZZcGQ5dVRQMW1sUjBpZ0lRM1k1a2NCQXBWZjAwRy96RG1Bb2lZU1lrbkJqYmZRLwoxR1ovTEV4K0V1ZTZtNmllbWxKdnZNeU9BcWt3dzVsdU1jZzFNUkJzMVFNNzdHL295RHJIY1dtQ1BHRi90UUViClBrbWc1UzNJSGpla2FqTzlYNHgrdkNDU25KMWl1YWZycFN1Q2tHaHhKczExdlFmdjk0TDNwSUY5RHR3OFVHaTgKNUZKb2ExbjMrWkZYMTFESTBneEp5NU9LZ2J4Zi9qSXFPRzd0a0VYUTJ2UTZraG4xU2taSk9pWUVNSGswamp2dgpsbzVnRldMV1BvQXJ5TUxnU2RhVzh5TSsveG4zZ3VSQ1NKN1J0SWhZNFhrb2RvMVlMWkhramt5N2JMZERaZ1ZvCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemY1bHdvR2xLZElyeFlQTityVkYKaHNBSm5IZ2NXTzlyZXEzd3NYTVppWEozUGFJVDdSVWNNNEw1UlYrR01nVnNFWnVvMThhOVFiVjlRL054SHdYMQo5MisvRTRRUjVUSUJnNDR5SGNXSXc3cUYwOG13YnpQeXJtWjlBdXErZHRSaUNhL1ZMMUlpQ2RpWVNhQnpFVE8wCm9NQ1NyOFlEOUVFd1grcHBFdlY3L1lZTXBGa1A1TW54T0tsR1MvNTBDWkVFNXJmNFJTbXVIaGRhYlRMQVhxWXEKbTk1a3JWdHVxSFE0aHR2WWxZZ2lvWXg4VGJWL2xEbmlBSmpDcWdpejE3MmxsY1piNkxGempHWGxBTWQ2ZFBLMQptYWV3OC9mYWl0RG1jSVMxdVdOZXNDQjh1d3k4V250Z0s0bEJVcngvQmQxcUl4NDRVZ2UzOTJHVTZ5Q0xPeUhTCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmcyMlI0RmdaSUpjZzBGSHBHNUUKTmFkMjVpU1RnMnVPQnJiOHd3eFVydVR0SW9FNFZFUTB0L2ZQMGpwRU10N0gvQjRzclFRNmE3MzJRcm1QanJPYwpjdEtHUVdUclozRkZXTDJRbW9lMVdnbEpDQ3Jwb21BVU1iNGlPMDdST0tRWjlZUVNjV2tyU0NCaE5GQmFHaEN5CnZ6L25xS1VzL2JsZmNDT2E0RWxablJwYmVtWS9WZWh0cTJpVTRoeS82ZTFxYmNIN0JEdkIvamtXUGtpcm1Ob3UKSUxvQy9jckx1SDBjSnFreWVxVVZzV2J1TVFQU2hrSm12K3lZMDVjMEpNaVcxU0xrbEkxRVZEVE1wV3h2bTlqYwpwZlVnLzRMY29yc1dORVNoV21kT3dPeUwyWldGRUJNU24renM0WG1UK0tFMGJPYVJ5MTVVazhERXpKWEdHaU90CklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTlmVXgzZkhvT3h1dGlTNjYxY1cKUlJmd2lYWGpWM285SkY5NHFUS080OU5BZDhXSGR6TDFFeDYrVFpaTk00ZjJieisrdVdNMGw4WCtkNUcwbFkxSgpkTVN6bGx1S1J1dzEvZGxtVEhCWHd4OE1rMWlEUXc5ZE5mUEFEVlQ3VnNld1RWdnhQU3U3TjNHY00vTEJzWnl4CktQL0JPRk9xUkcxVjlDcHZ2NWpkQWY1N2dWWUp3djBXZi9TRXBCUkU5bldicGowSklqczFSZWxaSkwwelYxVGkKdnJneGx2UXZmcHFHM2RBcDZ0OG80Q3ZsVVVoSlNnVUNJYmdEY3dJZitudUMwa3pydmFWMjkrdFhVTkJ0WDVpegpEVjdBWUR5OG1KenF0bWpub05GNkJwUkIxSTk0QWQ3RklmNWU1RndFUXhjeVJ0YWxpdHQvazlLV1c3VE1ZNldLCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0F3cWNyVlowZnBwVk4yRWhEZEIKdDUwSnhJZkVNcHA4Wmk1L3NpOWNvTnBUWG5xajRubUw1WnFwanpHZFdySXQrSWViSVIrU2JLdHJ1NzNJdzdOVgpzcWRpWTE3aVB2L3NEaTdyU2NNcEs5MmZYZVB5VENQenZyc3BoNkZ4TVhYeGo3ZkxhSzl4K3dUQ2YwRjlsS2dlCjhZdDhWYlNUSFpRRGtZR3d0aFJCcUp6dUpLdDFnbFRrYXZ0MDRqMFZMSW5YYkZCaU5kTHB3bDBacHBZQVowZE8KUzM1R2ZhcG1ieXdza013cDU2cklDdDUwTi81ZzlHWVRqR2JIalRSL3NiVUlRS2QzVTV1RHpWVDQzWEVTRy8vOQprbnZTM0dHY1hRM0FxS3JRQU1HTFFpZW01MGdlTE12QUNWbGpsUkhmRGF6WVZDRUpJS2tVVkV0bjhwV3N6SG9aCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0MxY21pc3pQOXFDdTlKQjh3RlEKVERSaElkV21udUl0cTdTVGVuQ25FVkpoVGM5L1FxaXY1Y1FhT1V0MEp2cENxK3dRWGJHV01DQlY4Qk5jQ3RYQwpSWEpNQTYxMXFDSTNDTjN5d2NoZW45UHFlTlZ0VjhoVlhRUVZXZ2dNUE85UGRMeXREK3RWMytNcDRyN0JBZXFrCnJReGV1TDhqNGVrV1VDT2U0dDVXVlhLNFJJd0gwL0VvMFlVSkh0bmZkanhmeGxFc1dTVzM3Rk5na1VPUGVFNC8KRFdpai9JeXQrRjF6eVZ6aVF3YlBxbXg2dEtiS0l5YkZWMmZvUHNVMVN4dVpsdTJqT2t0TnJmVVp1L01oK2g1agpDTXd6NlppUEh4Wi9QVXN5aEdKVDczOWNJUGpobUNmbC8wRlhzUm1McWNHLzhYYlEvU3gxUTRzRWJsdldUQUVPCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFpnWnNJRkVEYUg5Y2hhcFJsL3EKOFgrRUZJQkdld2cyY2hPbEdrd0phUWJaVlh1dXVMbTZvanIrR0MvMFpiZ1JmcEtNY0tpQS9Ia2Y0UHZiVEdPaQpZSFVVRyt0ZGROMW43NkpuQTJFUWZhSHNHbW9JN1pnR3IyQTNCTTErd1VybjY3bEVyc2E5YUx5RlRCMWVIUFgyCjNYdnN4NWwvZW5tYWZRYWpDTHYyekdsUEpoczREbmR6TDVSSDVtcktXOGZnRkI2NUUyU3d2VlhwTDQ1bW12TGEKb0JIRHdqTCtSR3dlNlFnbkh3eXg0R2FxM3RaMW1nYUhWUlV2cGllRW9sbGFIQXBGZlBKMGxqZ2psTHhmcldJWgpzNjdlaDFoQ1cyQkhjb2xKOGNPRXY1VWhCdm1NZ081UHQydzVYWVhCb2ZoSXh2MDhYL0N0emtkR0t4VUMyUnBBCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXAvSWxaWVM3RGE4TGxnRkd3L2UKZmlZSHNMZ3U5NllnSkZVajdaUjVPRnE4Y2Z3eEY5ajN1bzltazEzdXUvdTcwWlBYaWJVampySzgyRGtOS1lRbwpDWTBZS0xSVDhYV21pbG41MFptb2hHTEoxRmZQdkFpRTd3ZENiWHF3WWdIUHBFZlpXWnVTODNwQmxNRm1EMk02Cm1iSlh5Y2ZjS2dzSXp4SEgweDl5L0g3QS9WeUhCbUxIVmROQVZIbVE2L1BIcEs2V09sbXRyT0RDemRWSFY2Q1YKVHl3a0UwZ3ZsazhtQlN6NUs2cWo0aUJpRE5BTmNlNU9UV01YZU5ycnhBNk5ndDFxVzltS0pZeWIvYnRwQVZGdQp6SjVyU1MwbTUrYURCODdxcUVJUXp5M2l5QW1YdU93Tml0OTB5ZU9tbXVLOUlPRUVUNTBwM3p5ZG9oRTFFd3c0CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2VmRG5GMi96cWRYSnB1VFhuMHIKVUNEQncyb0ZZSGpNVzFEc3dIRU1NMTArYzZPMGVrelZIZ2NVVFBTaVprb1NSNzFIMEZCNnc3SzkzQ3ljd2pqYwpRNHRrdHZ2SUtPV0NibzM0ZktGakFpS05UR29OZTEra1h2SzB4Z3N3a3BSTlp6V1VBVEtPeG5sSXlxejQ4bmdFCmhhNkcxZkVDekNaeXUrM2JWbEVVQ3VMZjZ1STNxNHRtRzNocUQwdzhvWVF6RW9SaGJCcThiMmhZSXFCTFBPSzIKbExuZ0VxOEw2VFhHcS9DallQZVg3MFQyT3JFa3VUMXRpalIzVE5GRldGb1Y0SjBDREdoQ25EMFZZV01iaUszMQp4cEtRNFZwOFphdzg1dmVYZzhzMHY4dU1sK0hqQ2dQOVJSclJYeVhIRENCd29QY3FCMDBRTExEaEpxaG15VWNiCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmRhelROVCtRdGhDL0owYzJDcUsKMVdUc1FtUlZmSTE1RUhITnlQWVhMQUgrNm9keXNrQVEzY1M1cGlVV1RmTThxYmFGYVRmZjY1YzhhdkJWYVY3ZQp0UEFCYjU5dUhEVXlJWWgwTlhsY1NlNnRQVE1IUjR6SGFQSG5weXVaQTRxU1BpRHVyN3psbTk4YjhHTnRncXJiClUzU2IrQk1IeHc3b24yYTE2ZGZEcWoydmRjNUVONkl1NEZaZmR0dzN4NHZqaXlEWUN6b0hBUGt2cHVYb21HMVMKRTFxUnN4N0o5QUNVWXRHeW80aWJVR0hEUzJyNnlIRW8xUEwweEJsUFZLSDJzRXkzR0JXUm9qR2xQcTZDYlhwRAo0VWpzT0MzV0J0VzNqYStnOE9GQzBzY2tmUENlTHhiaW5tM1ZHaFcvOVlJVlU5MGZJRitUT0VNb20wREw0WXc2Ckl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDdMVlNoZWVKR3I1bG8vNFB2MXAKN3cxajlDOWJsK0NpY0FXQnRJM0F4ZmpWOGNvTFF6M1dZNDNwOEZuaW9hbnY3TjFVRDQ0cDVGVmNnMlBJdnlpLwo0SFJNUWdLM3B6cEoxWGJ2VEZIaE5kVUJMcXd6Q2U0dTZnYnJFMWM0RWZ0MldjSlJsM0J5QVcxcVczeGpFZ3hOCjNqK3UybS9QMDdEU1J0bWdlNFlCQ3RDUlExTWZUVFlNVmZ2Q293VWVlRWRzVTF2VEFaUGNYd0pHWktjYUEyLzYKUEFGbVdFQmkvemVsSkRwVml3RWRVem1jb21aWnR5cVF4V0Z0LzBpbkdTMzhNR0dtcitmdmJFenVMSjhwN1BYSwpUZnRCZ2U5WnplSEdGazRpT283RmJ5Ym4zRkFnc3loOEpKM2RWSmphN0JhR3UyNEs1bTNob29ZOEdrWG5RdEZtCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdThGSVFCSGxOZVc4SVVoOFNwOXoKeUFqOENsUHhpV0lhZTRialpRVkgxWjJCMjhRN0RWZkpIMFBiL243Uks0Wmd3bEhNTjBuVWdBbjlFOTdrTVFjaApjZzB2K21tOHpZT0V5M09OL2VTY28yRmVwa2xyNVdvVlI0NUd4eHIwcVpsQUtkY2MyTHIvQnhoWHQ3b2ZXaFBlCmxPSkc4TUVZVXhmNnc4T3dxVW5obFJUM3VPSXlRYkNIT3Vkbk9yeU94K1J5YnpPdmN4LzM0RHd5bjJ3K1lyVVAKKzBYSFdnSldEZlVxUUxZWWJoUEJXNnAyMzFZYW1QMmFrc3ZwRHIrS0hkYXFmK2hmN3IrOEhJS0tSaUR6VmQvNgpYVWpuVUc3cElGU1FFK212bXpDaHcxZUpmODdQOFg4WUpiQXc4WFhTVE51Y29mcCszcXlGS2oxTWtJSTlDaWc3CkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFovYkhtZzU4enJvUzh1WmsxK2IKMG1aeHU2YWg3UUNleGJDZWRtNVNIcUVBbFlINlR2eElBRzQrUzdSbHpmcU1yTTIrSTcrVGZhLzZQNlRmVlkrRwpxSDdJQXVWdVRsVkphS2tqbm0yOFpaKzRPTDZmSlhJVzRlSTA5Wm1GYzc2Q09TZmVmOHpZRk9ZM1hKeUV5dEV2ClhrRmtzd1Y2K2ZyNGhZZlUrRC9ybStvOVhOcWFmcFJaZG9RZE1VKzBWOTZyTVlxKzNCYlQ5UFNxdk01SlJsNlkKM09NbGVXWkI4eFlIRFNZWmhiTUFjRzJUQytNNVR6WVJSZ3VHRWdZLzJqNTRIVlU0UkhXWFppTDRCWXYyalp6SwpybDlnU3ZoRTNkSE5pVmhSa2x1eUxwRXAzRDN6VXhkcmtudU01SUhBZXVrSmc4VnJlbkxTMXJZbDZ4TmNQZkg4CmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3Z1aCs3VndzSnBLVFl3SzBYTy8KQ0JBYXg3Vm5YcWhydHFQS1dUdU9ZaG5WcUd1VHBYdXdZMDlxUmVxdFp5akx4Q0hXQmpsdDlFZjdRanVaZWtMbQpsaGdBN3U1Tmpudk43T0J6cHZMcjFZbmJhWjBvc3poQW1nZndlck1sRkJBcS93VDVWdzRWd2xXY1g4eGE1UW5nCnM2Qnp3WkQ4WXRWanpxOGp0MEw5blZiOUdTNkdSSCtyc1gvb1RXd2tSTmZoc1RnZWJpRXJaZGU5N0Z4RlM2ODgKVWJkdDdIaGEyQTNkcEVzWU9HeGJPZ3BydE5kTmZmMyt0b3FsR0p3eTg2OU1uRUVvTzdydjhOSEJmdlpvdDJVMwpveFAzL1IxREZwQkdmT0xKQWZ6ZElGSUZpZnJtTHlPNUhMWXh6OXFSOUgyT0tsYnBQSjhzeU9mb1Y3bGtXUGVKCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEpwOVBVcDZEeFNKRTRqSXZMZk4KaGZXUUlDQmRmVmlENHhpcXdiVXVHWFB6T2ZKS2xNSFVwMUJFN0RBTFZqQXRTRGw2Y2l6bXF2MERsNXBodWFkLwpLOTZYSGlsOUJtajBzVGthd053N3daWHZyLytwY0xoV1NObWkyaWNWTnJrdkVMdStsdFhDSGF1WmpFZkNyZFgvCkcrMXFXcWZXd0E2a2FvelpTRm5pN1BNMzJZT1BsYnkxUGFYQk5ZSkdhSk9wekk0TzVlUHN2SS9ibXFBVHB5czcKaFV3WDlpL2lBWnh0SEUxVUxWWjJVcm9IY0hFUlRsbyt2WGVqb1hrQlNmN3ZtSFEwaDdCL1FNZFhETTNGejR2MgpBemNtY3BIWU5aK1pLYm13eTBCelVkOWx0eHJ4WVUvRW80cTBoczV0VXdudGNYLzBaTy9NRzFBSytsOXdNNlpzCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK2xrZm1qYy9qQW5FYUhMK2dVb04KanZqdVprd1VhTlkxWWlneDh4NU5zOHVuYVhUWmlNTm56M3VkS3dzNDMwNnAyekUwc0xpcVE2VCtzZnBKT29tUApLR3RvWENGaDh5UVBoTUdYZndUN1BRSEo4QnVuN0F3SVY0aDBjYUFmdGliS0FrZ0k0VGVWTEptRVhXY0djeE5HClA3NXFCQitFbERxS1hSeWFHWGN2eXFENFU3TU5RZEtsNGdTbGtlSklsbTJCZitoOFI5aFdNNytQNThXVkIwNm4KYkQ3NG5uTkh1MVJlMS9QWXdvS0U3QnVvUWV0VTRMaGVwOUZpZFRILzlselhZZ3lHd0ErSjJ4KzFmbzBjOUdIZApmYTUxdWlXMHFDN0pTSHFLd2RocDFWNWJ1Y2pQWDBGQmJpRDhabml3UEZwNXEybGw5VzQzdlRWbnNVMk5razVlCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2Z6a2J0UThoVDBrK2l4U0h3WlkKRkRHQWJpYUdHQXhOdVdhaFFCVFRXdmxHK21taVZrbnVUeGp4MTlnTkVHWmdYcHlxVmhVZ1hpR05HNHRuZHRvTQpxV1FvZlBLaXV4TG5GVVEyZ2dpSW11QWRCVXB3aGdGZFVyYmk0Q3c4dTNSZU1sUWdJM01ZREl0VDIxanpWOC9YCkFyOFRNMXp3RHFpNmlNTmNSNVFoN25MT1hqd25DMlRNTnNMVTAzcGFzem1uQi9WK3FRb29tR0VPQUQySUd1ZWkKT09qbEJkOUMyeFNmYzJrWjF5UnR5VHU0a3AyOEZZS2xBcko1c3Aydzh1WnZSbmN5NGF6aW1CSzBQaHc2S3d3eApER2xZckMwcE10UFlKWUtlZE4zUHQrWFRzOEFzOHlsRHNXaE0zMUs5cGdEOFlUQ2V6Nkp2N29mcEhSby9LYlN6CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk5wcmlUSVlLZ0VJRkxrTm9zbUMKTWhnb0xUOFphdVV4NHZxM3JhRitSN3c5YW1mTHRjVGVldlU0ZWdGaUNoTVc0ZWl5UlhCeUdSajlHOFRDYUdvTwptaDBsQm1haTQyV1NYVWJuVHNpQ0tWOFlyS1RhUy9RRFBBSXJmc3Q2YW1MaGdFS0VyYnRXYnZxdEJDVXV5eTVrCnFtYmJlUGFPQ3FIV0UyWVljT3RJT3hlL3grcEdyUjM5T3liZ2kreDFtdFg3VzdLTXJscmN0a25EbzFpa0NyNEYKV1B0NTdTSFRnM0VMS3BZMEdsZ05hWDJ6bmQ0amVPVUdoRUt4c3N4c1BjdjdKU2VkdG9RQzcwdFl5UVRJamFUUQpaM1ZLckZwdFhnaWhWOHdYaWhlMTNDNHh2d1BObXU0K3dmaWI5NnI5WENQb1czb3ltcHRkcisrRnJtQWpIQjh0CjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTBSNC9tcEo4Z2ZvcUFrZE44MVYKa1p0ZmkzbzNyQTk4THE4Z1ZTaUxuUXJyOTZ1dFB3MmVOdmx6bHNrSWVJM0ozRk5pZkpVSG11bitTeWNyVVRzcgpOZzluZnpsTC9UdVNpbFkrOWlnQ0JYN1RmNDdRcnlqdUc3M1B0VzhkR2ZuTjRUblkvYS8xUkk5RHk5YTY1UWpuCjU5N1VBUUZXdWtFVUtCMys2Ny9ENkNZQXBOck1uQWpBOHdQYS96MmZ0ZzUvL3FaUHlHd25pS0VLZG1ucGhmd28KY0xvZEx0VFdQWmROb2Qvc1pRRmZpc0hPY0VLSmxxQTg0UStlaDJQblBGU2R5b3BwQ0tvMHgrcjJqNHdTcVd6RwpXZjIwaDc4eW5VY0w2Y1ZzU0k1QkZvY2sweWN3MlBkQmI5L2w5Y2djT2FSMFhEbG9ta2EwdnpWM0RmOTNmR2lYCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjBIYlF1QXBEMXYzcjVXcWNOWDMKSE5DV3FtWlZpY3JUUFFISnR2V0N2V3g0Ykd3VCt4VlNEd2hoOTNNWnc3eUNWTjRjdzRhRU5RMFZiS3p6aVpYaApydFI0emVIMVQybU5rRmUycHpYZk5hQTh6YmtIeUcwS2tsRTJoaS9kQ3RWMzBxTklpV1NUd2R4NW0rNmNsRjJPCjRlMUdYbjhwREJBWkhCZnNyVHBlcWxPS3ZZNXR6Q1RhQ0JMOGV2WFdWTnJXcWt1U2VDOHV1QXlQVk92Wk5uZ1AKaHA1ZFU5cDBMNHJ0MDV4OTNJYit6cXF2OGtVSEwwakZZWSsrZkZwOGEydUs3b0N5WEUwamR6NitGenA2M2t0MgpjZy9HcE04RVJrcnpJbGxpNFd6Tkk2SHVyb05KZzBhdkZoV0plMExPMnJvdEJRb2ZTV0dmdEZZL0VIYUs2dGlPCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOU1EaWdLU3ptcmQ4U1NaK3RiRHgKUm9HTXJrZnBRTU1zZGxvYUlpUUdQN1hQTDNlR2xDKzYvUDY0U09DanlRd0NwcXpPZVlIRzdnRUJYRkE1dGZaMwpFSy9VV0VqeW5GTGEzS2Z5Y3Q4aC9kNU96MzFudHRHRDBxcnRlY2VNbGtlLzF5Z1VLODQ5L2VFZVJDdlBieUwxCk1vVVBWS2l2N2VSQi9rZWhJMTFxbFE1Ym9FemwzUFphSVJXTm04OTV2eGJKM2NOTk1uRDdySG4vK0tIdkxZb2gKaTNUYzQrWnhHMmlVeC9lV1lONWk4OGUrN2t2ckFCdFM0OThVSWhaUjZDNFNKaFJoazdnTFZiOE9LTmtFNUJDZQp0Y0ZiSHRLL3gzRHFhTFdaYlkxWUo5UmFlUTFhbkRFY0tWcCtpNU9jYlcrRzlsMzRpWVZURVREdEdvODl2ZTlrCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDVUNDNHbmx1SjJzdEV3QzZMcHcKbFJBYmIxTDU0Y3BOSnEyNnBlanhFNFo0REdVQmNZWTFkUXpQSE0xeW0vUmZTMVl2Zll0dlE4bFJzeHNObTVFagplM0FJeklPTFVCSE10VFAyN1JLdlIvZlZ4V2lsTlYreG4wL2tRMEtHbCtoeFkydHE5b0VRaElybHNqSkpIdjVNCjVLRGxtVVFqaUVYdHJhVVRRbUhNWE1LTHVYckNmVDlnV2lNWDc4NllnVU81a1NHTUthTVlqc25YUzZsN1ZMb3QKNjJEK1J0TWZxdERWY0MyN2pGUHc4bW91UzlvUzBiUGhpcURmeldISElLa0kydnZzcTI5M2NsckxSWGZiTjU2VQoxMjJvRWJ2cHJZVitEbGhIT1ZwZlBGUEJKMENjdGw5ZUhxMkdMaGdqSjRpNEhaQnY3TGtERFhtN1pvcW9lZUVYCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb05LcFhwc2hjYjJYVG5mbTVXME8KWTRWaXZZS21lRVdUcG5nWHRZTmpLTjZiU2J6RUExYnRjczArYnlTQ1ZnRjFzZU04WXZ0SVVpeDdCcnhub2paaQpuY0krakROMXlpcVErVGtQUnI5RlFEOVhlNlRqSkY3MEQ1aFNEMmdLc2pqdUsrUEZyZ21RK1FhdXc1MEUwTExZCkgwN3JyTWlRQll5Z1A4eXlEODR2TkNCU0ROWHhVY1Uyd3RpdVBuRytKUUwzTlNtbnZmNUNCYWtsQ1EzTGp0cncKdXY5UXNrbXZiTkRndWNlQjdQcEVRbGh5RGI1Zkd3RWhKakdWS0lTYXo4L1ZzM3QrR0RhbGROajhRMzJKR1Y2SApCa01QREl2bzl1SDVDK1NOb2F2MTZJMElUOVM2MUJ2WXZUTkdTSTB3d09PMWRoMmhIMDE5TW10eTRIaytuejVmCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUlSUXlxQlJOTGdBRUpuQjJFaVgKNzF3Q0hBamR4bHlGQVMxR0RkWEhtaGtVY1VDVURHOFRLQkZQbWdPejVZNk9TK00wZHBxcXFlcFZkSXJaakVJWQpnbFJjNlJtd3RmeHhEVWpJN3BGQW9KVWdmd3VMZFg3amMwL3Y2RDc3bVdidDNBUGFRdFdFYjFFVDBGc3AyeGJNCisxcm8vdGtVc21lRVpUeGlHaG15U09nTnhDSDVlL1lvMno1V21LYjhUTXd6bFViRVBOaUZiQjlPOHpvQ2ZweEYKVXNlWURFdGJhSWdldXI1SGVjQWtNeVFFRU9FUHRuS0owZC9wUXp6NkFBbnJLUVhVSUI5eVI2V242Qk93QjFwbwpYVGVYUGlpUDg0UE5ZU0I5aTJCOU8va202bGZvQ2dGMWs5QXJPQnlZbGNiYm9ManFkS2xRZGd5QStibUJVRExaCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHZmV21Wc3VGV3RMOGZWVDZLbEoKWnl4V2lEUlVFQXZBUTVWZXFHRUFxbUpnQjdiSDA1TEY5bFR5S0hhalZLZzlIWUV2cmpmSVE5UjFJNE9iOENNcAprOFYwd1lKR0lybWNtRkxhMHQwdW5sdXlsdVBwaldNaW15WVpKT3c3a0hydk1jVWJ3WE5QdmFsMGRXenpLRmxsCkFNTHFCcCt2aFV2UmY2eVNDZGJjRFRlSXFMN3FZdWdzK2l1WTkyd3NTVW5CUDMrRjVEZ0Y5Z21PeG5xbnR2Z0YKV0g5czh0eHprN2JHRTk2WmpwRDdZRFAyc2t5RGpoejFrQnF1MURTNlVyZmk2ck1SZXY2KzBGK0todjM4OG5CRQorOUhzd241RVIvZjZtbXRJejg3V21LeHhHbHRtaUVha1MweWl0aFdXNmFzbFBQMVhJRFVNMU5kVExhelRNcVo0Cnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0lycjlhalRsZWdXZTVkUHZ4akYKZGxVTlNwaGFJc1hRb242MHMyV1ExTHM5ZlZpTWtpTnhGTnVucDVYWkRqdDhLWE5WUlRaUzI2a3ZUSnQvWFVMaApDbm1kZ0RRNFQ5SkxpbjI3RCtUK0tNVXhVdjZnRDhpVDBWRjV5NllwSTJLcXpTQ1ZiaStkRVJ6VVZkWi9ZT3pGCi96Vzdqay9lUzdUSy9saUdWWUxnaFc4eFlhV21PRWdJSHZqNW1XdW01NDdSNVZzTWplUnhvM1AyUk4wWTl3K3EKN29objJzVFFXb0VyaU5GMFZZc2VjS2lNSUxYMGZVdWN1STdCWnRjVWN4WGk2OUpTMjI4QXh2elIrclFicXNadwpnUldrR2lFRUsrSEZjTEN5ZkV6Uk51SkV2U3dnZ2xoblozbFJUcjhWM1o2ZTBHNzlwK0hDM0lHdHhYWk5PUEhECnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzgyS0dRQWo0M3lqSlF1eUdJTEcKUGEyS3pWd2RVSUJJRWRYNDRWeTNCbGJqT2tiNW51aTBzdXF6aE5oQVFTenAxQm9pZVM3cTd2eXUrd2x0WlkvYgpqWDFyOFU3amtCWGMzU3I3SkVQVldXODVyVzVIL0xyUlFIMWNacVBTelgvdVI3OXlBb0RTT1Bua2hqT3Y0cWRhCk1OQms1U252VE9aZC80QjB4MWtVQXNwOUVZOWZBazIxLzZmRUhScVJzbnRKV3EvcEtqNFFtbzIyejdjWUM2ZGYKc29KVlV5LzNQUFFSMnVFTi9tU2lZeXRjTVVHMzljNGFwR0w5RnBMOElhc3hrM1BUUUFFWWNtM0ZvYy94MzhmTApwVFB0TDNNbm8zMVVHL0ltSTVQdXV5UllqMWdMOWZ6czZ2T2cyVDJ0S3NiVGdlU2E2bE1mZHFjTWZoMHBxODh5CkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2I3Z000a0x4Rmk5OGp4d1EramUKNklocFM1Z1pZZTBSdVY1Vzk3MmNpRTFtWkFRNFltcHNjMXhCZkZhdTVmQUxnOVNFUVZnV05CSW9rdlZrWkNhWgpxY2N0Tmo3T2NKdDFmVG01VGxpanhMUzhBOHFLcnZnTGZxSW9TL1ovaEJTRTJRSGZhcFNyaFpiZzBadms4ZEMxCmE5UDNWV0VSbGtEd3Y1Ylg1Z1N6SExwSVFSOGN6SzNmekU1clNWTytqRlRjK0phd1hEWDNCd2N6ZWNPandjdzIKNFlwalpVUGJWQXNrUG1KNThYYzVzZ2xuRTl1VFJXZGhHOWxGMHF0ZEZLTkZMRUNVN1M5SjdtUk94QzJvUXo0cApZUDluRWt0WUw4VkJXTituUGpRSmxLbXdXa1dZaEVGdHlWNFROQ1JwMjEyMEZGNXQ3c3c4clFlOVIxSG1hV1pVCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUt6S2dkQWM2VC9HeHg0L3VoOFgKSjNvemU1ZEhkN1VKUEJxZ1FnWElEVDQvbVNGejY5UzRFYi9jMEFlWDA3dFh2OWVjcVdVVjU2M0dVN3JaNHpFQQpGUEFWRkh4SXFNbUs2bTFzRDIvWWUxVmJ0OVl2aVNBdTJEckhKRTBzY3BlRXJLdEF3bWdtUEN5MGxnM1RvOGpUClZxdmpzclA5bTRDb2NjU0dBWUhIN2hVRjU1QWpnT1Z6bHBWWDU3a3lRcjdrL1RuMkxYcDZnU0k4MkloUVZKc2UKWXpiem5kcnNsamJHNmJTYUJaTUE2YmdiWHU5NWhDRVpBdDJjdmlsU1lSWHplZWUxajVueDRVVnVObTRLU0F3OQpnNWxJWDZxOHZweEt4TktJd3pzOTRZWS95dFVPOG96MG5zdkMyZmUrRHZKSTd5OU9NcXh4UlpDeUJVSEdLdXVCClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczE4cVZEakhFTXMyeDRjM0tjUXEKT1gvSVZGM0NLcE4xL2FjZFRkREppVnZYSjJ6Wkx3dmVpdzNuRFhtSXlpaDNQQnBpYVowUHlxdVpGb2xSOTV5Rgo0eWh6dzlQUmdhZ0hVQU56dXRvTWpIaEZuTWl5ME1Ddnppb2U5Z0NGTFRXNGJURlBpU3FOVTJuUEpxMWU2Nnp5CmFwMU9HbkhiYlNjQWdScm83c252bUpUdkhnZEphSU10MXg2eExhQW5wTmpLekRuVWtVeXMrUDd2VzBCd3I5UlQKUkVlaUVNZGEvQzU2RU0zekFLM1BRbG0vT1pYYjk5TFdhQUE2amdScy95Q1BjYUpnQ3BaTWwzdjdqQTFyRmM5Rgp2V2g4T0lUeE1FWWkrMlAzYmFxN2I0SG9QcHZ5REI2THRCTHhNMGMwZGZBTmRMbGRDVU5yTGZGc3dZU3lYOXpSCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0NBVmt1am9rVzE3ZDBYSEd3K0sKZUJjOWxNbG9GMTRwemJ0YS9DUFRtcXM3TGJPQit4K3VrRk9pQytDVCtWSURjb1JYTmtIMVVMelV6MmhsUUR6RwpjWk9GUHVJUkdDWmFMSXA4bThhTEs2d3QxVG40K3lmU243L1ZVT0UwbmFNdm5Vd0txcy8wSUI5WHljWkNZQjFJCmRCOGkvVUxQbnNoZUxVNE1qa3JJVU1wcjFyOHZqMFhHVWs3YUhSMmtjVGk1Yy9XUzNlVFFuTHl1bkp6ZzB3ZFkKTFZHVHFnNGR4alFHVFN6cThFUFZxVSsxdlk5VFkrSHBCMG5UaHFGR2M1M0JOUk80aVpiOEIzWlVJL3VCWXdKawprWWhXVHZvZng4aTlIQjU1S3o3cFdHcDVUTGY5V3FZV2lDdFdFaW1iV3lTQmg2MUxIdDg5TEtKUzNPcnlRQjVCCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOHYzTHA5Ny9lc3kzRCt6Qy9lZ1EKS0N4VktUVmVLR0VtMWk3SUdWQ3VhRXVObjJUQ3oxaHNkZVA1S25vTllGOTB4QkhWY1N3U1VaaEFLdHd6dUNNTQpQUW56WTByTjFDWHd0MVJuWFMwSnRMZTRNTElueXErRldscUttYnpYY0gwcE90NlNneC83UUxlY1ozMXRRWUh3CnBaL1M0UUNGSzNYVU9PdmdHQjlUSy82TDdvVEsyWTA2elgyakZ0TEZ1UzBtREFDNWxxL3N6ZnJiRzhnNFo0SEwKM2pDc0ZUcE9FeXNMZFRGL2RsS1FZWHY4ZVczcXZzVnQxUGNBRjk4bXlTajRSV0F2Q0xRM0xOa29OZ0FXaHVKZwo1ck5DT0NpRDBjUWhSZm1EYkJQSHNualQxU0lmN2s1bUNtN0s5aGQxaEt6R1U5di9ZeVBlQWRxSVJpcVdqT2ZpCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFlLOGd2Y3NaZ3RoQVB2NkpvTTgKQmpUYXJHOGxOUitZOEE2TndRaDF3QjlTaTljRzJqUWJrYlJLRzRYU25QcHZhc3p5U1BqbnhaWDBjei9Kc05EeAowdmtadnY1b3BqblFFWk91dTZaYXFvYjJhTVp2YWNpZktXeG5EQkVrRW5ONE9adERyY2k2ekdPbTU2OVJwYkZDCjN6eHZ0QS8yRlRpS0ZtSTNBL29MYzZoOWdHU0p4Z1N3Q1l3MTZ6NUw0eU1zWVJtMit0c0NQTkVIZTM3cGJSYzEKK2ZuZTRsY01ERmhsRVEyeU5UUm9LVHQ3L0VmdDZkQkIyY1loTkdDc0dNSmZiQVdWQU1GQU9QMHVuYjdSaUsxaApBc3c0UE9XUG1WQUFlMGN6WldERWw1K2RvRVI2dUlDYVRhY3BKWWRFMjlvWHMzUHF4ZWFGckVTWGJjWHVmSUVJCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcG90OE9NTy9NMk5IdytUWFVmcTkKQno5VysxTFZNMUs0VmJYWlIzaGtLTEw2K2Z4T2JzRlhoRVNIbkVLRzVUbnI2K3lzRmhVa1pSeHBYaVlXVEVoSwp5SXMxOEt5SVZPVG9RK3ZJMC8xTXBIbEhrVjlDam1pY1NUS3hodHFUOWxRbTBjU3QxQk0vaWplUVg2VUVBZEkxCkhudmpmYlNtbHdtQ29qMzlQWDZsK09id014eitxSmFuOUNxMG1pQ2RQNTZJcmZDSnNZZ1pIS2lOSVJwQUlHc3AKZlBHNEk2dnIyNDBHYStlRVIvWDQvejhORDhQbHhxVkR0NjZHRE44bE5teDVQWmdmL3VxanZVQTNqaUJ2ZURHYQp1ZmJkOWl6RDZ2dGVIeVc0cytsOGxvWGJ5bEVZL1d2T1dJMnNuK002b3hUVERmNHlMcTQ3eWZhNmJTc2wrWUdkCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTN0cWpuQU0wUVFzNzhtRy9SbGIKMkRUODNYbDBzUVhkTnpheXArS0I4Ty9YT2NSWE5JcGowSEJsdUFNQm1tMWNyRjVSc1o5QUhjMkYyRitsQ1VBSAp5SndEY0lJWG90UGxyMmpGdFFBWlhVWFMrSGNyOFJ0ZWRRWlg3UlNiYlBYanFaQjgzM2xiRU9zT3JTSFJGSEE3CjBKcmtyU2xpbnRPdDBYelR1UGE2MjRyckhvT05QRFRRRnhEWE1xNjNaaGVoakhuVUZHU3FaU3VONUNzKzNtMWMKUGZSQUFvemJuN25DTVV5YXlHL3NzZEpTenZrUm9paXN0dW1HcWdGMlRiZVVCVExkQmlXdHBRWVZRaFRuelJQTAprRmRoOG1wRmhNQnlEMFBGa1ZVSmlVb1J5L1EvanYxVkVsa2Fzc2lFZXIzQ25hcEc4RTNyM3V1RkhjejdxczJkCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWFmcHVERXRaS1BaZWMxbUpqYUQKdWVPcUhlT0dSZlZteTNhbDZZWXp4cFVQS0s1b3poV2c4Ym1oQUZqMjFSTHlIRmJTOU95cHNBS3lRckhEckhWcgpvcHlSb2U2Q25JSlJuWWVmN01udTZVckdrNnlTSEtLVS9EVmNkWElneXk2dElxR2FQNFNEcjgzT0dPaFpKMHJaCjhFNmVUZWhXVnp5c2FsaGhyaE1uME10NUN2MVU4ZXkyRG44ZEZLNU1PWGZJWXZNdHlJVElIaXRGQWJ3VG4vREkKMnd3TEFSbWtOeUhUNWNSdVVNY0FOcW95RCtPQ0FSTWRGa3NrYURtSG9Vb01oUE1nNTRidFBDTlNKVW9IK25uLwpGQ1B5TzU2THVxcmpkUXdLakxqdEhPc3RIU3p1OW01S2pVOExJaC9GS1Npdk50THlnYkJBaG1uZEduVnJ3RXg3Ckl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0FCcVZLQSs1eFRJVG13T2huSDkKVG1ENUhFZEpXMEcvK1pQTDVGMkFXRGE2d3gwbENkdHN0cDB4anBiVkU2anFYMitJdVlDbFB2QmRpOFpETW0xbAo1ZVZuSC9uTk9sbWJXNm1XNlRDd2xVUkRRMWIxQXBUTWwyZDI0N3ZzQnRQTlFYYStFZmxMR2V0dXFEODRoYjl6Ckg2VGFsL0hqaFRtTDVHMk9mMkQzTktuKytkK2hHTFVuTTZqT1h2Sm5YSVFPSElGblNKeHUvNi8rUnRVK08yQjkKWGI0WnF1d3BrbHh4WmZiWjJ4Y21na0U0NVBMKzQ4K3NRZEdpK2V6anVyRkY4am5rL2o3UU9xU3Q4ZWNIWVRPVwpqcFRHaDRTSTBlYlF1VkZscXk1MnI2WGxSUnErdzhmWEpSV2dEeWFFcjB2cUZ5VVNGSUpMVjNOUlJBTmFXVDk5Cmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0tNN1Q2WXQrYlUvdEt4eHVzK2gKNkhEV0I2bG1JaEJKbk1BMlRvUUx2VmpmSHNYaEtvMnRHUHQvZ2plT1o1K2RxM0kzWENqUlREdnFGV1RuUnNUZwphcWJ3TWd5NkYzWnR0anhURjdBQ3luK0hTWHE1RVBtSitHQ3JxM29sZTN4bXpHN3N1L1BiaHVSVGpmeVprSC9ECnZ0djg0Z1BhTlBPNDQvNHZHWXVTQXBBZ0FzdnBad3hHRVJRUXBKcjFaNi9BK3FkMU9iWDNTQVVoZXFGbmJPSWIKZGM5V1c0ZHdtMFlDRk90ZkU1MlFHcGxBM1hCQnMyUVdqK0puRlkrcnR3amNSRG1hVEM5cFVVNVZEZExlNGozOApOYThDaXkyWnRYSFk3K3cxcUN4akZueityVkFKTHlvK3dMdW9mWHpaQ3pNMmJ5YUdCZ2U1SEd2a0FoRUg0d1NkClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm5uc1FoNEZySHFGK0daY1pKYmsKclZZOFB6UUlhVXhZVFM0WHJBS294cHJ6US9kVEV1ZC9CNko4MmFoRVhlUVFCdFVUVkxUTU1Zc2VwOWNrWElUOApPeTFGQ2VCL3lVa0NNTDVvMmtJOGo2KzJuU1pvU0tDaVJBS0g0Ym1MMUpmMkVrRWNFNXBrNzNWbm94MkRFR081Cm9zaUs4eWR5elFuTkJmMUpxSFRiZm9nQUZseUovR01qYmh3Y3dhcVlMK0pWZk9sdFExSk9xS00yMTJKekcrcSsKS2FYYVpYeUYzampUVnI3L0lYMGQ5U004cE92eTFRd0ZFZHJlSzB2V1MybVM5dUxBbk9ES0dIQTFwMFdzYjRFSAowM3FYclVaRzc0RDU1WTRrTWJrOGlTZWcybzd2TVdoZmxNUm1hZ1l1WnZZWE0yMDFHZkpyaVNyYjFEUzBsUmpJClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGJ1R1NnQTdRTU9JdVNCMWpyaksKZGg2cEJDZFcyTW1lZWk0dVlNZW0yYlZ2SVBFcWtMajNPVzNDQVBhTkI3S0VPS090Mno4TDY5V2o5RDRhN2NObQpVeVZxN0tIUVplVUNJU044VDVRazN4dWttMWR4NUtkYWg5STJIUHNZWnJscE9VcklzR0JQK2J3YzgxMFlWeW8rCkc0a0dKL1dCMlI3NkdOb09NcnI5TmVDZXBhd1EyUnhPaDdvelVXMTNROENIb3pLS20vMHdPUU5OR0MwNUprOTYKYi9IYVpoNVNubUt1V0EyZHZtOW5XSmNCajVRVHBhQkszTkNOWXl2MCtpQ2tFS2hGVEtISDk2Z2Z6Nk5EVmFCRQpEaVg0dFYwV05QU2I5Q0lpQ1dHUEUzcHUrenpsRFFva0V1NkhPZHA1Q2xXUXJGUHJ3ZlBSY0lJSktSS0VVNjZSCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVcwNWpIbGpKWFNiK1U2ZUNOekoKa21oQTlVU2tlK2dSOStzQWdscldpTXFrbmhwNE1lci9HRlQyOWl3ZWcvemhySE1KZUlOVDc4dTVubTJmYjA5ZQpOY09NMk5XQ29xMFROT1hFZDQvc09Hd0F3aVFsZGtQSWtPUTRjUVpxNnZSQ2tvN1ZTMHVLV0VRQ1pyUG9ZVk02CktZN2szbmlKbEJPY0pqZU1zNDJ2V1J2TElzWmRPMk1sc2pKUC9Sc3pXakhDejAvdm03L2JvakdjQlFVK3JLM3IKd0hYcVpqbW9tOFcwVUVNQUNaaGZWVENnUlptWk5wTU1sWDNab0VBZXFGQkI4UTRIV2trSXFMbHdKNnRnaFFCMQpFaG9vQ3V1ckNKL25CMkQ4R3R5VUZpV2RLN0MwMXBCbE1TQXNjdFJlQ3RuNDlPa0xVU1pROUw0clROOUszQzJnCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1lkOVBnYnY0SWg1WHZ1cDJWMEEKRFFyTHNuT3AyMnNDOWRXMVF2dGo3a1Z3a1ZwOUw5WVF0L1ZmN21vTzFPLzhUbU04Z2tCTjB2dGZIQ0QyRkhhUgp0SHRzOEdiMThxZFY0UTBPYmlBeFdUdnVJekRUSHJiKzN1M0tkQ0FiODhhWGNVVmtTRjVBNWpJNCtnMjEzRXpaClJxRFh2Wm1nUUhqRTU0TlhUYnBocEJFT0xwMExIM2s4eTZEY0xGNmZzR1F1alNWYWRNdWtUbXlVTHFRUnQwYlcKMDJjQ3BkYkovVk5lMGFUNnV3ZVhJSmgwRElCTVFDOGZZWmFBVlpQYnc4dmRFbmxRRmxwNUJPOTNUVXlaNDMycgpNbktQNEM4UUZqK01aWHRjVGpoUE1FY3d5T0wxSVhMY0ZQRHBoM0thMkRNaER4cjdRenZGc3JXWVdBSUFkOG4vCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEtPNVlkWFgzbVUwbjNsRkVPVSsKOTdjSzdSWEdSWXgwcDdwWkJnNGZnOW5wdmloMUpDM000L3hrYWlWTHJmNXhHYlVOV2g1WE1ESmRTamd1bHhwWQpQWjVhSVF2b3VMd2Vrc2FrSDdkVW1UTGRsdHZycUNnWGN1SEtlUDZHbFVNUGR3RHNONDFlM2VJb3B0Ry9hVGovCmFQV1BvakVmU3pEMXJ4ZmN0Y2ovN0N6NnYyZEoxSG5JY0NpQU81eUtzby9UWENtVWZ5bno1bzhjaXVDRVhrRGEKUDZwektYNnIzL25WSWlVK2JtYU8vc0tCTFdhRVl3akdTWUVRTVk2NzVKaDhRNSsxbnRneFMxZnNhRWcyV3dZNgovelRVUHlvcUJiOHY0T1BIUHlPNkdEMUVLSzRVdThUSlRlL0srTGJaOTd2eUdjbnE3dG9INDAwYmtBYzFQVTI2CkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbU9icUt4RlA4bnBoMlNXRTk3VFIKNkNhZXNxMGVWUjhrdkxFeUhKR1V3VFVYSnZHNGRlcUdsbmZBM1dCY01hNllwV2dOL3dTci9HekFEUHRMWEVacgo3UHkvcnk1eGVrZ1AvT2ZKOVFQQ3Q3YUtpZ29wQ1h3d1pEcVIySlJGN1ZXem1tU3VlU2xmSGJuVmtmMC9IUC9lCkp5M1BqN2hrdVI3SkdEQ3N4L2lIdEVQbElveEFxTVFsU1BvNEl4cUF4NGpJbTRVenlrc3FsdktpcDdkdFlLMjkKZkhGL1JSTWloYUhoRER3aWpNR0pOUGJYZlNQdzVidzdwYjRZWDZxYXk0d2VUNTIxWnZhMDNWZVFXdHRwbUVMMAphRXV0VnBDSWdyQnRMOVFWOVJtUUUxLzZiM0dDY2o5aStFZUJRWnM4ajZmanhsRk9PV0xqY2Y4a0hOWmFOK0VWCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFR6TFpBN0R2ZDRoM0VhZVkvcnAKZ0NNRzBXeXJOYTZUYVBDY21DWUZYQkVVYjBkZWxKM3U0OXdEVG00LzZZSkxCL0N1OS9UTHdVQnp3SUpFSTFRQQpnenJrNGZwRG1jaUF5cjBaMVkxTndmaEp3T3Q3d2liaFdkSkpZODNmU2NZR0hjL0xqOGpCeVBKeTFWS3dOdkJ1ClByZkZHZUpHRXZIKy83RHFmNHkraWU5Ujg3NFQzMG4xMENWTVJJa2R3em9maVBVL295TWJUcWFBRjdIb0duYSsKWnJ6NERZOTFPblo4M3c4QmYzR1EwWTA0a0R6aG1tbk4vWnNIeGZOZmVLQ0VjK3VZZmF6MTRqbHYxSEVvK3A1cAo3b09zM2kvQjlKWCtBN29XdG1MS3gzTTJlcHhkeGlLc255SkR1UldpbktqSG9obWZ5QWpRMk5VNG1zSUZSWXZ1Cnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFBFcHlWaE10dXViSFg0WVJCbS8KNjdubWt1UEtXYTdHSGFXTTdFM1lvVGRvSWpLMjF6QmdmZitCb0tYUFNkZDRldjVhb0FYQTFhQWkxdHM2cXhwMgpaMVlobHlqY2xWeGxKZ3A5ZXM3Z1MwcnpvVWx1TFFlWTI4d0NPUzBGSTJ4L21IdXBDTTZjZURVT3RjN0E3M3JUCmZnZWlmbXU5MkF5Y1l0ZU1sS2dZSE1yQy9vaitkem9QNHlDMVVRUDBPT2pxSktuWnFYM0FCZ2MzUkI4enV0S20KV2F1aUhFTGJEdjhHbERvQVdTVkdYYzZOellCWWREQlF6cGxPcWlDZ1RUVkVvS2RtOFZKTUIraFpxbnZLejFnZApTcTVwUVNFVTRtUXI2RzdOQ25NTDBYQzh1TTh5bmRVd1ZjWnNzdWJkRDhhOWl2WWxYZ3doeHRHNytoSXNMaWN5CjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGFYTVRraUtGQXFDS01KRGRUeHAKaXRidlV0YUtEUHd3ME9KUmpvd1c2M3RXMjZsK3IrOTlsVmZSK3VVZzU2U0J4TGtEV1pJWHY3OWJPWENNNWt5VApCMkNxWFkyeHJWeEZQbTBjT05zQkQ4am92Sk5yMFo4VjdWZXArVXFtQmRnTloxaDlTUnZNTmI5cUlrNU53RzhlCmRkelRsbW03S3oycGQxdDlsZnprcVIwejdFRWlwc05DRGJNT3JZd1VHUVhMeHhjb3VidmdtdHIrOHJXQks4bksKVlk0TVdWb2tJUmdxNjJSM3RJTWlrSFVabE15ZVNDd0FVelRLV1RqVW1BUzFWK3UzMndiM0VLaWR5Ykd0QWwrVwozZnFuQ0lwZ0VILzNKT3ZBL2I4VmZrb05nZCszMHN6Y2hWOXVWOXF3Ukg4N1hVV2RobnRPd3ZoMFR2Rll4WGF1CkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDgyWS9OMDFubDR5bVM2UDQwTGsKM1NlZFNtU1c4VzFCb1FYRzd3NEsxYUJDSzY3d2lyek92UDIxdWpVc2NnY1BVeHphUk1ubUJIZklBWDdPSzBTKwpkRmt3MThBeFVCbUdIMXduRzhidkVtc1B0UHcwMFU0aFlNWk1YRWkxWlRwYklQd0ZpOER4RlhsZjQyeWRrWnlGCnZDWDJTT3B5bGs0RUlaUEwvNjc4MG1uSkpTSWt1QmFXSUx4VmMyVWlVTzM1MTEwLzZwbm43YVdjNGlBOVRNZ1MKZXNlQ2tkUHlHakd4b1pubTg0c3JrOE4zOExzV1VPa3orQzhNRUhiRTJTTmlWSHFWL1lUbk1kTUU4VUN5KzhqegpFcFNGWVdEYlBKK3BQQlpNVTZKdkMvNmJib2FxNjNKNjJPcExRUllhZysvUkJnZVlGRVVxUDM0VkN0RnRJWTJVCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXRxRE4zdEFDNENXZ1hpUWpUT0wKQ3BacFRnamdQMnp6MDhMREN5UkR2akFuM3R2UkE1OEk2NWdsaWVkN2g3STE3NklTUjdsQndLK0dadTB1NG1QNQpISHZTZVRVS3RqK2RXcy9TQkpjSjhNTzc2aVBpQ0RSWExYR3cvY05Kc2VHMExPZDNiaFloV2ZNT2wzNGQ2TnVCCmNWdlZDclg3MW1RSkM4K3k0R0g0T25rbkxUdlEvSndFeG5TeXh2NGhsMFRYZC9pVnhUdmlRK3dPR2JteWhxdXEKbFB1cEI5VUR4bkJLaFoxR1FWUGMzUWNONUs0cU5YUGpZcWd2dGtlekwvMDZNelRJS01VbU5acTlWOXlYdURZdQpxWWM2b0JUc1l3dTg5aGlGcmNFVUdBSnZHRmtxc1VoSUNqcnRoNG9ZVlhLQzZ1QnNpR3pjR2R0NUloMG5rcklNCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0hWcnp6K3BNWnhydy9WRWRYL1cKTTNLMGlYMmY3UXNCb3FFRGw3TEZvYW9vcW5VRWxwdTkxTlJIR2xyUHV6cHU1UGdiRkdndGg0NkptSVlVU081WAo0eFdSQ0dKR0FhWkQwTjFBNlRJcWk0YVBsdlJqVDhNVnRGNElFdlhMdjByYUZaZGZZdVZneDFMNmlqNnFScmh2CmNTRGlMaUlMbVErbVZGUExZcll3a1VrYldpVGpDZFJPdXBrRWdkZDRLd0FEUXcwRm43eWVKeklWVTN4aW02RDcKb0wxK3FyaU5pZy83Z0h4cUhmN1N3VTVicGowQjhZV3Q5enI1MXFYdGJya2RFb2JLVy9ubk0rWjRIUmxjc28yUApHSWtIZ29Rblh3WTBaWTB6L0RBREowcTFEZ3hFTDRqcDdmd0wyVmhXdFdaM3VXWHU5WmtSVnY4R1NKSENyaDVUCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnExcWl0c1BKTW1NM29MbGxEVGQKalFKZTFSYjMraG04bmcyWUhJVVpjZlpKUnVTcW5LU3RCVHVUaWZNVFRFUmN1eHQvdThIVkYyUEFwMWk2ZWZSQgpwZ2pyWVo2OVg1M1BielpLVjVaYTRTaWozaldlakJ6SFpiMkg1azhzamVGTFYraERSTTNYSU90N1JnVjJ6TjdVCldZazg4QUI5bHFEU2xjbVFlZWlqbmF4YWdxdGdtcVVwNzc3SkR5bTl1dFBVM1VqdWpjTUxlLzAvV3lZb0ZDbTYKSnFhV2JrQ3V5WTZrWFJwMm9ZQ1hhL2lEWkRrd244VnE4Mzh4QkJYNlYyUzNJTGp4MEN5UUNOajFMMnRSYVpzTQozbkp4eGc4Uk95a25EYU9qanBEeE1wOFhvTzBhUTJLMnFNUkRia2xJV3VoS0hpZXB1dUZ6ZHMzY0tNYXVZODJjCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGxyTDh6OTAyRVlOOW0wdHB3R00KYVlvRDJ5ZFZFcmlGSkRqbEcycVhtZ1VKdTE1SnU2bXNhYmpmYTdjaFM2UERWRWVsWjBuSHdDdnV1YjE0U2RjQQp6Nm1McGNvcFl3NERucldnRHdEdFV5dzZBQ3IwVXpYV1JCQ1RFM1hzamRIdUxKS004QUlBa3EvUERXSThMZUsyCnRqb0x4ZDkzaWs1dmN1YjF4ckdKZnY1V1JxakxZT3huR04ydVZrRk04Q1laZkxTNmVQS0hvdTh4N0FONlZ2NksKdVBYcUhkazhHM2llZmFNNFV1MmlJejhhd1ZjVEJHL0thV3prOGhlbnBSQWdMVEs5Sm1EL2g4dE1GcEFkR3F6MApiVHVrTnFLdXB4VUxpKzFVYnRZcGtuU0VYcjF5WTRieW5hQlFRWjdxK2hMTkplOUVwdU9nM0tpZU8yVksxbWNQCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzlDY29kYUcvZklLU0NWUGZzRDcKR2xYQmtVZ2VFNGR0V0FOZVRjYkhUNWsvYTBxSEFVdjNieFQrMllQOWZ5cDY2Nk91bXE3YmxWWHpQb1Q0eFQzYgpHdklKdXdLVitSRitvS0R6empKL285clpqSGtwSU0rZTlpT0JBN2JweUlFMFV5TWJOa1c5eUxBLzV1M3FaMXY4CkZJTW5tNitndTV0Sy9qRVpleEkxYkdNT2oycldjYXdPaW9MNFpBcElPT2xyODlvNlJhdll3dW5TcUNsQ0ozdFcKM1doOEIrVlhLT2FJZFhudXdnMHd6ekpkemI0K2lCcWsrUk42ZCszV0FSVGRDWGdQdlBGbDZDUjJyMDYvMkJOVQp2bU94bllaZzQyR0tPTHJRc0l0Q3dHais5ODdMMWFsVGVwQmIxWTFRRmVFdzkybWMwZEFCd1ExSHhsdWxabHRXCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmtWZElLK29yT0NBSnhDQklJaEYKcjFDVXZBU0ZnRElqRCtWMkx2RE5YT3o3bVR6QzVlZXVhTjB5bFBEM1ZmbmxoNS9KR1FxbGx2NFhYdlEvY0tGYwpOME9Ka1d6aktwTUczQWNCSnN4ZDNLL2xqcElKRXRxaldLcFZhZTdhRW5mVzdZUzJ0aXpQaU1KMk1xckRNUUtzCkNQZnUvSWFXM2UzUVBKUG1mZDV5S09YM2V6QU1XdkJ0NG5xZ3RhSTZZWVBkNGsvMUtqOHZBWmp5aStTbGFYTm0KNkhhK29oWWFZbnE4Uis0V01keVI0cEJ5U09sNHVZOWxEOUFOS0xqSW5aQWQ2VFRNRU02OVFyblg3cmtMSWpjUgp3dDlveXRvRDZTbC8vamlOTkZLbFA5THVGMUlPbHdEU0lTMGJYOFRjc1FxVm4zS2NWK2ZXZ3Y0ZVhRR0NmNnhWCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFpORWI4dTgzK3ZSUUNlMTQwQ3gKRHpoWHVIOVNIc2JIb2w2SXVTbWdTWVJjMDh6M1J3cEJKdGFRcG9IM29uU25GazJhWGpkR29aOG9zd0M1UG5ERApBcm1kU0NtdFl6SmtKUzZoT1QrNTY3ZlBUaU1FdTBVaFlpZllFVHFocWFvUk91T3NRRTh3OVovb3AxK0NncTh4CkdKN0VVc01SZTZhQkc4MnYrNWFsV0RZRmltTXBPampYQkJOS2IxQmo1QTRMNkdyTk9yak9jbThnNEw0aUYyOUIKVVlEWUZxc0UxQkZQZDZGSU9YYVJ3bXFES0R3eVFMRXhKbC9lNmZienQwSXZEWmRFWWx2WEwzcG14RUdVeUxxZwpidmgzK1NmQlY3aFY2UUUxOEpwWjg1dkNEaUZsSm5qdlEyNmkxcFpobW9VaDNlaXQ5cUZURjZVVmpQU3MvS2ZVClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGlXa1E5Z2JtQlExYmNkc3U1UWEKcW1paHZtVnBRR21Ua3JmV1BtVnFCQVU4SGlsQzRXVy9MYjVUYW50UGE1eU1ubzhuNG9jRDdWdGMxSEZqMit2Rwpzalg1S2VtM1RxRmhaRC9OMzF1cHdheGNnck5yYUdzQ0FvdlZvbVZUZjBzeVBrS3YrbUdLcXQ3bE9YVldMOGhxCjdzOG5UNy9KOUJhMTBudWZSU2RMSkllb29jejBJZ2NEVExTNXFHWGp1dTlGVU9YaFREZUUzWGF6Qysva1F1bDcKL2hPRUZ5aCsvTWVjUlRpdlNxRVVpQzliNFpEenpFa0J3TVFNb0xXajFYYnl3RHVub25lQlppYjVnaG1jNDNsYgoxaDdEVndFOFgzYkluUXpDOWV6OU9UdWRyMW9aV1JMZVY5aHVTa1d0OExveXJwVWxMVkhobmc3bDR1MlE5SXdZCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTQxYUY0STdhako2U0llc3pscEkKb2QzT3JQV1FpZXUvZk5nb0ZjS0Jrc1E5NTNkS0Mxak0yVEs2YlZBejJ6enVIejFzMFNWMUxlZDU3V08wZHZCNgpjVk5nd3lLdENVZ3NEYUZnRFBCcDFCckJlMUluTUtTWWxMZm9XYWxTOGZMVEdkbmdyN09obUxuN1JlWVh4YXB0CjlKODdPb0Z4bjJWbElKcU1hRWY2UE5USVQzZXVScTJ4bnBKRXY2SlJTbUZGajlJWXpFZEVNbnhLMmk1Q21JV1QKcVBpQmttVXBSSGNYRWVmREJxTENKSmRuNjIyR3pyQmlhWVNYTkwveW1vdlQvdTNMcGJMUkdGL0JhVHkwVEYyRgowd0p4MU1PNlZxL3ZrTmhQcHNLSk9vSy82djZLTXlTbnJOVExSSVU5bFVsVlk1TzNiUW0reUV2WXhKbEo3MDVTCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW5nM1E2bXhZdGNZSTFQa29USW0KVmkvanh2bFNuOUFMSUJVekF0cG5QZ2Y2ei81dEhJMTFKZVlQclZFajA0SGVkSmprU3IwdXRQQWsvRWF2Ky95UgorWm5oalcrblpmYmpYeGI0SWVxQ2tBeFdpUTAvbDlNdU1MejY2MGxYSEVaOXBTZWttajRQTWhrM2owcy8waXZvClNHYmZaSUFhZ3JKaWhINndmdE5GdG50UDZ3SFFkV3VhNWViTW5JMCthRWNPTUxpc1pNY0JOTjZoMDhCZ0xiSC8KOG1JaTduV1h3SzZZT1oycmJoRjd6bit5ZE5pOUdrZ2pKRjVPZmNibGpndTJ2R3A1b2QvZVRxRkFGTTVHZEZnQQphRHp2bnlvSGU2WEVYK1dnS3RIaGx5b1paTU5hUEVOdnUrY1RiV2Y5RW8yd0R3VFk0akttcGFLc3l0QnRZSFlsCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWUwaU9HclczaklUZHpMZlJlQTYKbVpZMTZpejIydmhvQTZQY3YwOEtpK0dKRlRMdnNnTXFkL0NiTnpESXQ5ZHd0bXQ0MUFuNElZOUFUcDk0RjNGTgpwSkdqLzNNcFRCRzBXcWk5ZXRJVkdCanZmaWFBN2FaM1BGWjVVMTAwKzZYLzhqb0N5b2w1NzBNa1FUb2t1WFBKClorL1pVSkpWckdIc28wL3pCZVJENzRQZzQxcmFjOUJIQlBxNjlVNUw2ODhoOHVRQktRNEdIRTZFbE02UmNHVUYKNmExTnRhZys3bHlIOW9NSE9EVUk2WVFYQlNwMHI2N0pMbDF1YXdSaTQxVlpHQ1pmSEp5T0VaUXFsZFdEMEEzZQpuZkc3MFBBTnI5Rm5QZHJMTVkzS1E3eEhZLzdOd3BOZ1NVV2J6bHIxRUtOOGkyRzlScnVTTXFoYm5CZy9aUVkxCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMCttMWdNOUpJV2duU01zM3RxamsKaUNqYTdtb2NTdEhoU3ZKckVLZkVBeG1iOFRaMSs5azdBeDVyZUw0aXlpMzFJM2p0Qm1XWi9LUzRIeWgwY2JLYwpzSjRkdjhabTAwV3BmQlN3U3VpYkRXZ3JKTTFkQ0xjZEdPcjUwWW1HaUFxVXR5VFpRSFkvaDF5NHZ3SUEzUjJ5CjVZMlhIWjBPV2IxZUxoSXBEczkwZjZzR0lPTktjK05UUEtUcVRENGxaZWM5djRoazlYL1VtTVBrOWJJa290dmMKVUppSnVZSDhPR0VKWjBaV1BZQkl5eEg0bE5Ib1NqbXlWcy8zOU5vc2JaUU9mZkEzWkRDT2lib3ZHSlRQK003SgppdWhxOGNXVXoxS1NveUhkeWtYZ2o1U1NzLzJYeVFQYy9ybFlOd1pjbHRMVzQ0eVhyTGlOQ0pBM2NIRVVMTVAxCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1IzbjdTWlU4VmFmQzR1dHQyM28KWFBtV2wvKzBCOHU1dnovSnpzdWJvSXhDcU5nQkFzZi9lZXI2TGZDTU1kNTQxN1phWGt6LzRMNHlVMFZoS0FhVwpOaVFQWk54azJydUNGZW9TcEt3aWZDSTM2cHFNWFcwd0crTko3MGdXZnk3ajl5ZVhLbVJ0UlBtSmxCVW1EVEhOCkJIck9zQ20wZ3lSZy9yWWExa2pWaUMvRTNETHhCa004MHBUbHVvalMwTjYvTTI4eUpLVlN1Yk16R0xhdUtFbncKMVo3SnRKaUw5Njg5Q2tFUWxTUnVJRXg5ak5GSk1IdjV0OW1LUHZWa3VDSmhadTJqSEl0YnJraE5DSExxM2N1SQpzdHFlU3FPRklldWxUUWlvLzQrSWVNUkRVcTBSOW1yTWlSVGlHTVNNTHdybnJWeHFRZmx1TXNNdlBTYzgxTDhhCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGdYUUJEaVdSWk8ra0g0SHhQUGQKNlZEc3huNEpRR0VDL3lOeXFJaVI1b2ZmckdLOVE4Zy9FcHJ2Um8yTEhhZ2sySjlKVnZkTEIwVWJuU0k4elVQZApRSmNmQ1dTZURyNUFyNGFUaXh5RVpRQk85OEt0Y1pnbzZmUDd5SVlxK1AwYWZxMFk4UDlYd29WcXZnWjZ2RUEzCmtrWlFpRlhBVlZHZm04a3FVai9FeUM2OUpqWE9BL1d0UWRWYzlPck85S0dsTmh0UHgvK3Q2dWZXbi8wd2pDVlYKalBUcmFTbDBPUzlMQjl0eC90Q1pIZklWdkhMSVhIYkNBYlMvV3diWkN6Smw0eXE0SG1UT2ZEcWx5YWx3eDZ5RwpNZDZyQzlpeGRKOWVCRnNJOWhZc3Q0Z0E3T2Q3OW1PY1RBcUpsM2tXMmJ0aTEwaUdNQ1d2Wld5U0JPbjhoWnVlCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME5lZDVVTEZUa3dNRjZrYU0weEoKOWdmT2x3R0pWWmhuZ0FHTGRxcngvN0JOdHpkajZOZHZwUGJ1YjVNcVQycmpSRTZYZXBzamRnM0d4bWNvaFpwcApHK0srbjRGNjM4SHJlMWFJWjgzT1JxU29nUkRldXdnTndjZHlQTlNnaDNjWE1lNk9YMmZWWGNRc1A5Y1hLWkxOCmE0RmRicll5WkpEYnFSUDJLY0QxaFVmb05nYXdYTmRqVEpEWkxJZEYvYlU2NE1VRFFGdWsyWWRPL3creG5KV08KbjJ0UFhabzBPSTBBQmFNK0dibFU1ck9hd0RQV2NLdjZ4TFozWm1lYVlPQUlFK2c2cVN4Y3RPK2ZsSkZkZVdPOQp5SDVSSmRjaStlN1c0clF5TWJMOENSNlpGYldpRlJjemQwK05RUi84WVo2SExUajhGaHpMOG9EZzhoQktHU1lBClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2xlcVA1UXg3Q00vUnhuOEVqWFAKTTJ2MU9Jd3FKTlhiRUtLaHAySGtBVHRCRktLZzNlS1FTdEhEdE5GUi9PUlI3MXlUekxPVlNGRFUvUjRWRStWTQoyNkNLcElPSzBucXE5d0hwTUhEdUF4VGNUZDZLRVNvQlhsNlpvT1pQZHBqUmhwRDlRbHFZaGttTk5lNEhNRHlmCnVRTGRiQU9YUCtBOXZXRkkwRk9Ic0VzSVh4cUQzK1o1VkI5T0JzOHUwU0g2SHhqWUVld0xNN3lpMU1GMTRxYTQKUnJDMWhyMWhMZnpDMVBxY0I4RG1GcFJid0JPMFB0eFZxOUJpcm5INmJaaElTb2tmcUh6YUlxeTloaGEyTFNDdwppbUE3eXhsMHJHeHkyWFZ2N2xSYzY4bWt5SG9uTWtnQ2QvMDRVeUUrdTVWbDVDM3lTNjdBOTlzSjUwL2RhY1FWClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXk4Q2hwRDJqbHU5QndEUWEzdFoKdkRhZW90VFFSYThlZ0s5YlFLcG1FK29EaUFjRllDdU9IdnRmU1JNU0FPYnkzYzlDWnlobUt5cWY3MWhtMFlVQgpHbHVucFdvbXJaYmhrSHVuWGFkeDZ1WHNjVk5DNnVJeWN6VForZ3l5T1Frak9Fdkxwa1hOamFqVDVyTGdJZWJNCklTcVIyMU1Ya2xmY2ZCeEhkU1pVUUVkNmk3WW94NEowU0ZMSDZyUmdRUER3ZVBYUk92ekNQcE5nQnBjWFJlR1cKMjlhR1FrRExsMzdBbE5idTZtN1diRTVvWkdyRndtdVh6QjkxVndhZE4yZG1kZTZCL0VzTEgwOWFXdzM5c2VOOApNRE9sZndoZGFtSWJqd3Z1eDJaMUZvcXBUWUJ0MFk4UTBYRnFZSVZHNTB3M0xiVHZYSnhxVzRxT1cxRDlCaU9nClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBL3dXdFp3NGRvYmpYUVQvTVZNTGkKY0NuMlFvckZTT3FzcTV0UUMvNHg3WGRFUUJtcGdoQ0NZQUVWYnFWOUtWbS93OEVtRVFnZTZZZ2I1NEhZS0x2WgpmRHpRTVZKYjM0Qld3MmoxMElwdEhqV0p0UWlITm9KZDBXRDRXdDd5OFMzWGRZQ1NpTmhpVUpaTDkzWmZySFZmCk1GZnoyWU5ZZXMvZDFqSlAxb2lraWNuSGRSL015SVFPRk8rS0c4ZGtSUHVIbHNzNjR6MkJ4NGU1NjJVVHlpNW4KWG5FMkRsY3JyVTl1blpveSs3Y3VlSzV2Kzcza3UzeHNyZUtSSGNBZU5iWmdlNU9sY3VsMzNSWGJDTDc3cEVscwo3MjZGT2ZuWDdQZGlKbDBweU5wcEs4NlJWcDVnQmxpV0tOUUhsV1ZSNllmNEp1cXNoNWYwSEZmeWN6WlZtQ2x0CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUF1bXJhdnpralZWZzMvdWJMY00KaDc2MHk0bEVweDhlVXRIZlJmOFdlZ2t3WGlNbzRKMHgwQWJOb1hPaGRSdWZKQ2srV3lsODdNdGpsZWdMMjBvRwpUZGNWUnVlSGtFWFVJQ3NkUW5sVXhTTHJaNlQyaVU2QWg1N3Y1VUxmbWc0S09oY1BwMlFneEU1akZ4RDN5QlJ3CmI4VUF6dHIva2hMd2hSamsxNFduSUV5bm1aSlFKekxPUTZHUFBTQkRvUEtPb3BkVVIyK0x3WW1Bd2tWZTh3OUMKOHVGS0hRNHUxWXM1L21KUkJlaktONnZIMUtFRzJrczFDeTZiWkZQWVYrSk9ycWZ4eFUrUElYMEtvZTJ6dDJDbgptcmhlejZESzhCMHlzVjJubW1wWThCeHJJaStFdXBjQjAzMnB1Ky95SU8rVlVEQURDaGZ5MUxDb3FlMTkwUXZLCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEl3ekNSNTRmUDNMK1BZMC9ld3cKUjJFL2VJRWp6a2VqMkV5aFVXSUU1Y3JydHlzd05yYks4NzBjZFI4WXE2bDZ0NUNiLzNtcmFNdlNCakRYalJMeApualREZkMvMnlvOGJqUUZ0bTIxcERUdy82bkQ5OEFlQmd6Qk5LZnRnNmZMTm5JOW5WSDJia1hVTzhBL3JKbkFqCnFROWF2NzhrdnVQREw1UmJiTXNhNTZBU2w3aFloclMyeWt5TExFbDB4c0h1dktzVUpWenJqenBMMGJrLzU4MnkKZVd5bGpIV3RiTUxNSGFlYVFSbjUraTFPOFc2WFhmMjBEc0c2THVGelRBVWVvdExGc09QU0YvUGxvWkk2WkRrSQpqNkt2eDJlQ2JXeHdSNGIraVVXTkVScmlFSmx5eGFsaHRtanlheHdDMUFVdjRKRDlTNS9IV3RaRjZpQ3RYZFJUCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0o5dHI4bXIxcDBLNll5VUdzTDkKamtWZmZQSzVZQWpKNkdPWGxmQTdNNXE5dVFIQTU5T2JoNy9hTFV0bmlrZkZKS293UjdiVS9ZUzdMNmRadHN0MApSejlESFNsNy9ML1hNRkZHMExGV0JMcmtpSW9sVFZNZTZwYnVWWEFwY0wxVkkrRDFHeEdBVlNpWGZRVk1Vd3RECnl1WXUvaW5YZ0tJYmdWYyt1T1lDNlVSSHhOQnlFRXRYbUVLU1VOM1lOb2JCcDVoTTZ0bzJGSXh4djhFRVFQb2gKcHNjY05HS2NqbWdCbUdZejFJY2dxRkgzZVlDay9uR2RHQmJZajNNYVkxYWxtTUNaaStWdjQyM2srM1drK01MeApNNE93ajhvRnBHdi9COUM4RG1zaXVWL1BjaTMvZnR2M29LR2ZvTENnTGJHUU1Bb3Ric2ppRGlCcjM2aGtrbkhNCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcG44SWpPMllsZVFIbGNaQnpxcm4KYWhKTWxtS1NEV25LMXorT213bndlMytUSlhtbDY3MCtWOXN3L2dxRXRLYWJVbCtER1BZMitJbUtaeFM0RklNNwptV2wwWGVURGNjNmRMbWNicEVrUWFtbm40TTIrZGw2cjcrVExmMzl2aW1vVWVDektSbFFaak5EeHVQcjY3Vk81CmNwZ3R1SUREMzFFUTNBTUxNNUpQMnNqQ2ZMZlhJMmlOY1gySCtzUWIyVnpmc09NZkFGNmhLZXFvMFJ2YW1GRjYKdlBGSWl2czFkTWczaEZlT0FHUmczQTFXRlZjaGRLM2w1SThGOTRERW9yV2Q0Z3loM1QxSFlqNERYYWFmWGNHYwpVcXp5M0Fod1RUTlBpOTJ0ZlJZYmdjMWV3eEZHM2JxNzJza20yWDQ2VHM1VWFBTU9yRzVHdTZpeVhSalQxU0FiCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUtTbnBoa2RWczNNTVAwQnlIYi8KZUxZRThaOUlHcjZ2WlNDeXlZU3puclFtWCs4akVoRnU0UExMSkJza284UFB1NmhEeXlnM2dJR2pzWEJRQVpIMgpEQVlBMGFOQW85Smg3SWpMeVk1SFh3eU96OGZsZnNwZ3QwRVlObGtrTXA5UFhDcjdLNTMvY0JiYVkxTXJaRFBNCmhaRmpPTVJwSHptT25oSGtBVzNmVjQyM1pPTzZ3RXJzN3RUVFZRbENaNllpQ0h2bjRhanF3WlgweVJWaGhZT0QKa0t1clRaUlBQUmNyK2JnTm14ZDQ4Ni91SjUyeXFOQnZldjA3d2svVEExeFR3UzB1TENoMk1rZFQ2di92Y3FUagoraGZhaW84ZVNmUXlORmpoeldSOEJEMEUzYzNsMHorVnBWZWluUnNZbU1nZEJnbzFXZjB3Ly9qb2tEZWttZ28rCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMER0RWxQYktuN2NMdGgyNko3ZUMKVTZUbHpMaWUwb084d0c4TWROOU1oVTRhZGxuTXA2SHVLNExGU2JHVGRwR0pwT2JIcG1zbGcxOVQ4N2VpeUlPUgpZYzNCdHdLZnpwbno1L08wRGlIekdVZ1RyNjNZZnBORHFYQ1diMTJTL0hQVExXN3BQb1VrMzE4aldCODlZZ001CmF6QUhpaWtYTTM1SE1FSnppSU9xQ1JyUjg4c1Q3aFJyTHhmSzd5NUJvanpSc0xNaitwSEEvNm5XTURQSjBlQ24KdUxIczBaVG5XbzAwdDl2K1JvNi9URVJqNWFwbFcrRWNDOXFOSXIzb1Y2RGw1dGZIcXhPWjdtbHE4T21obUdZRgpzYWdiSXhyZTRTNHgzYU1tSmVwY1RXREZWWEFQK1Bsc2F3UG45cFBsd0ovNFd6YklLYVJVVkwzV0VwelQvamlQClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG9qOFVIMzlOYVpXQWExYUttQ0kKNkZwWUZkNGlZU3A1YWF1Kzh2ekh0WDJJOTNpWit4Y3ZWdURYemNaOVNjWHdyMkZjZnhoQWZ1bWZTTEJvS2tVdApWaEhxdXhaZUlZSG9IUU10akJhZUozL1BlczM5KzdLdllDaldITTg5d1VSVzNVdHg0OW9FejBBd3VWeUsrL1Z0CmJLWkNuUVNFYmkydWp2Y1FvNDloNnlwV3g1Z3UrSk9PcGk5enJRbGRwL0RmZXZJZGk2OFgyMFFRYjhlRHB5SFQKcURFRU4zNFdzaEFjMGVHbjFrelBadWpsZ09ISUppeEl4MXhQWVA1MDh3MmtTTm9SZWV2ZW1Cc2NOZ202RFEzdwpLeSt2Sk03SEdQRlNIZXRXZG1DVjVNbVBUK3VKTkV2MXVDWld2cWxCRzNwL2pjSm8wZUs4WkFXbm1KRWMwTWRMCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBKzROQWtNa0JDOElCTGxubmFBNnQKOHFuaVB1QXpYU0FhTnFLYUtkanZHMll2ZzdGOGtwTGJJNEtTMDQ5Ry9vT2Z4K3NoTHFjSGRsaXNubEsyNCtIRwozdkt5bWNIak1Fa1VoUnArdk9WSlNEcWZmcDBWaDNkeU9ydHpBYmFUOVpKbmhuYnVYd2J5b3g0Z1ArSWFuYXJwCjlaTlVib1RzU0pHa01Qa3dsWHVLK0lTZGhYdE4xL1QyTC9kdnJNMEdheFVSM2tkY1ZrbkE2OTdXeTh0anFPWkUKSE1GbGRvN2wxQXBWM0x5bGcwNTFNVkpsclgxSGNHbkpTKzkvVEVqL1VIdnVkWWltUk5JMElyZyszcWRKWmR1ZgppSGcxaDZ3NlQvZ3RsUmZEeFNKR1NWZlB1L01sd3NsM2V3V2d2UmxUNDZmUHV1Y3p5eE52Z0gxT2VtaFlVdmE4Cmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnp6ckIrSW02L2xNT2dJMkNvK0YKT1BBRnVHNFpjSHN4Z3lmdkJZVUwyTk1haisxSVlOQm10ZElyZ2ZnK1EvLzJyWVFNREZnaUlvUVd6L1dXYSszUApCVGRpRG01L1ZvSmZTSG5WeGx4dnFnN0VNYW4xSWhHSTVWZlFDTzd6MmFJZWFLbHdHVm4xZGEzWTZULytDVGkxClVERElTVlZocGh6aGhIWmFDTCtxaDZWMXFOeFN6SEZQQnl6Wm5vL2xxYnYzNE9jN3ozV0FFS0VhYnUyb1FBbTkKeVVvTTJvd0R6TytWWXJ3OFZHN2NVWnhJZGllMXFtMjNQNkdJQS9lZm9WN2JBOVlYTHBnbFRZbmdKdE1jMGxRVApxTUF3YUtjL0JTK1dyR0dlUTFXdjFjeTlKaDZhVm1Vd1VPTVM5VEdmdWlSVStmbXBzUC9Jc3h2NnZpcGlXdENoCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcE5YVVI0R2lzUk03Vi9SK1BQRXgKcXRWdldlb0tUVG90L2FTMnVaN0NFK2xQZFFFZitPdlordWJwcVhXdzVSR210dXhVaW5ldDZqbXhvZGxnc0t6VwpSVG9OT2cybDA5eFRmc1NONGRWTkNPVDdMbGxVYU82Mk5LS2JydWN2NmFMZ1ZyamFuYnBaYjAzR3FnWkkxVmtUCmE3MEMxMnk3UFlpNGNTLzlKSE5jQ2dmRk1iWUhxUmdWaExwc2pjekhlZm1Hcy96ZTMrVnFhZEl6TWE3ZTlmK0UKdWRtOWpobjVKb3lwNXJQL2lrZnVGcHFWd0srWEV4cHN5c1lmQ0JGSnZ3NTJDZml6LzBnL1p1ZmNQODQ0b0dIZQowRzdqbUVDZ0RPYlpVMFRTZWF0RFBiYmVqbGk5Yk9jR04vdndNNzA0dU5FUHYwTnR0VU5UN2kvMFRXalRYZkhaClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdi9EdU9VbzlCYVJ1alVtMWM3UGUKL2tIUXFnbldGSHllYmMvUUs5OXhNUk9YVG9NSVVlV2FYZEVNODNyWTh4RG1hN1Uwb0tNQkM4ZHJNL09tcUUybgpYb0NXMk45UFRUQlg2eTEvS3NnZWxBRkI5NFF6bWVSeWtVbk1CdW1yejVzTmZySDNQOWZzVFpNbDlOT2tkelE2ClRidXB0SWVJa2llWENKaVhIM2FaeHpsYnNwMTdxQWdCMEpWalJtNlBaMWIzWkViOFZoeXk2MDd6Zm8zSVhNc00KVUxseGhUK29KVEZsY29uMms0cE5DLy9CUGtRNlM5dWdEakprWnF5OGlGTEVPY1NKZ0ZCMEc0R1lXdis1a1pJZgpSdkpoZjQ1OGVJbTNXSmpKdmNMYitDYU1YRmtCUkdOV3NrSURnRnFKVVpKY1NaNmk1WUNpTUxKbFVPU2FWMWNTCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXVTQUxSQm9sa3Rpakx5Z3QvaEUKZW1ycXFkODNiYnc1U1IxVmUrUGtJckhTRzdRZ3h5YVF6UzVza2VsalhGVlJ2SEpnWDlFL3ZUN0NjSFM1c3RERAplYk1HU3l0OGdrNUZSbXVvaGoyNUhUNTlzaTgydHBSSHc2bzFZV3IxclBBQk5UdzhKcGE4Q0U5a0YrM2RCa3NoCjJKaXVsRldRTWJiSEczanBCamk1cGhOQ0Npb1lnMm1XMWszU1ZPQSsrc3NkM2pWT2piS1dJTGVNL1MvcDlnZngKUVFrc0hNNy9OaDRndGZ1aW0yNjN4WkxNUHpWenc1VG05NDZwOGtOcGc3U3B0dHdsS1lTOTNHUm4rNm01cDdDMApqMWFSMXJzT3FweUhvUkhYUTlDQ3BURWtFdFo3Y0RRTk1FeTBXZzU2Ny81MVlHZFVUU0J0ZGFmU25SZHczMHBUCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0ozTXNvY0xSZDNhY3BoejlGNVIKdjA5ZzlpZWdSa1JGMjg1b1lLdy8vMDNRUEZlZC9LREFycWc0dlhPVHE4cm9MMVZGeFYyS2xrT1R6RXJQWDdObApmb3RIZ1ZFd0g3QWpCSkgvQmJ5WEpEU0JhQ1N0Wk56WGZNR1pERkRhVnN3amJXdEg0S2hDV21XaEI2Tk0reUpXCkpSakdBdTFkM2xaL1dlUmtoUW1KYnFnRE5uRHR6eGdZbGlycFREVzhCN0RjNm1iR3pUL0JKckhib3Yrd0IxZjQKTnV3VVdxWFpZZGtxMjByczdMKzk3N2lvaG43cWU5VXVxVWFQV0NZNDhjQ0VWc3BwNzN1MW04dFZydW4xazVpRQpOemVQQWZXS2V2L0lYMk1PMEpPTlNiK2VaM012MTdOa2dmOVNUK3BBRkU2TlRpNTM2RnltRENvK0VKazlLUDRHCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjRaRWVMZ1lmYTI1aE9zUWhlVHcKbWFWWUF2VUVVaTkzZE4wa2pvUDNYTG5DTlZwUGF2NHczRjlBMmNQdmNhSHhEay9CSS9qQ0FJT0lFd0ZmTG5OMApKRmhteVNZTkRyRXRiT042NjF5eU51N2xuZHF4TlVJOXZFM2x2dDlzZ0daZGRQcDZpYkUwRUVLWFNKU0RrVjNDCm8yT3p3M0M3eGl6aEQ0MjF4OXAwdE1wTGpnbzF4Qm40em1FRFBDc0dHbE93NTNpQ25PcEd6emV2VWpkMS9vNUcKVGpadUptMWdEZU8yS0VNb1pZQnBHNkxDY1BYamcwQTI2cmQzN1B6QnJsa3IvVFVMZ2NiUXNRRzROSG1zYmxhUwpibTl2aTVubzdCZmNsOHJBT3JYWkVkNWdqNnRTRGhBcWYxTmZBSmsvclIxU3hmdDlXRm5RL0pOYkdna0ZLeXAyCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzRoL21sMDVhM2hSbzJLRmYyVUgKZ2s4M2NMMWZBOEdUU0xXUkRtSGttZnBLeForVWlPcnR5RXVzT3lBMHRQMmRJM1I0WnlxQm8wM08zN3VLSG5IUwo1Y29jSEduL2s4bzV0QTRHM3JGamVZTndLTzBacFdGSFd2Z3Rab0dyY21rNzYrM0JFaGoxdndJeW5tK3ZaK3pjCjh4eGtHQXVGWE9XNFlSdEpUdDZQdkRVdlJUZGxyVEtFTXZEaENBMGJXR3BjMUZVQmZGZDhDWTJOVDRCaXQxZU0KVDArWE9GSTlvVzhpVkVMbk5IQWFOVTdMM2RGQk96b0duUnN3QzJsZFd1V3c1RUUxRzRuVStFT2VlektMSDYzTApBQlAzbmNUMms0Um5HVnVmc05CY250cUhUamJaUFhGYWJqV0c4bjUvcjU3NFBpeXB5bU1CRUNFdktGMEdMMHlBCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTVqRmQ4d3pSNkE4NlhkRVFSb00KR1Y4dDhIRnluczVCam53Rzd6bUFIYkd0SU5nWG1IeHdqUURyRk1IelJZSTJLbW9TNG9VZWltNTYvWHdRQVBQdwpkNjNrZW9aSkVNZG5kZ25DTjNRY0N4d20rak1SckJMMmVBZXUwSFVac1lIS3JWMjE4d2kwYmFndDVkVDlPMk5mCnlRUWFKelJIY2ZqeW9WanduaXpvRndGUlltRUp2eDgwZlF1Kzlpekp0djk3Z0JtMUJ3VjQ0cmVJUzNJYk9HcTAKcUxvUDlQVU5rYjZpZjFkN1ZWVGxxZ2Y5RzJyV3pmTmR1R2ZNcGdXZU51ZjdrODF1elRZclExYnI3OEdwU3BKSgpFanFBb0YxQTJGTGRPZDNyT2tSNXdTSWR4Z1g3UGtVOHlnc0ZYNkM1WEJDYmN2NXlaNkJCRWFRSStVVjdmb1BoCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFdHVER1QUpZZldDVG15NFNYemwKSy9pVTZiSjdGMUlVTUZtRG9vWUhtOVpKTmRDYVJCK0h0RzdXdVRXdGZMMjZ0Rjd2SW04TDVjMFhZVzJjdS9hTApJcGdnWUpWcWFCa0ZlZlc5YUg0RDhxWG1yUTB4dEdDMUtJNmM0bXVYb0UvZnVmMldLU3F4ayt1UEFNanNkRFhzCjBqS1A0VEtUdW1zdUNDb0FQUkx0QS81NmJHVDljRXcwY0UrRld3eElScE5qaUsvOWdpZTIrWFZLSVF0WStMOEQKQnN3TUFWUERwQjZCRDMzVXFDWWE0ak9ZWWFlTzdaZjRjbFFHKzRpeCtPdkxvRnlCa2V0eHB1UjVGOWdxOVR2SQo0RHYyUDRGQlZ6ZnVZdUUxb0dDaU1JMHo0Z2g3R0hDaGhHWFk5cEtFd0JRQVhJUS9Zb3ozeStQT3FzQ3EwdzJyCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelNZaDd6cm1QcXprem4xZ1RmZkYKMHAwNVdQQVppckc3SjcrdjdLM2ovdzc5Vit6NldXTnNZZEQ1VUJzUHNQY25mOXJyQ05BSHNXSnNHdjZ4dUJSeAowdFVVcnRpSEVCM1FrVDZ1R0k2azdkNEI1SEVUYk5ieDVJRnFJRHQ4eDNLTnpuNHQ5L3dvZ1hnRXdtOHJDVVNYCjUwbmo5NDE3VDcwcVgrdnBNaWNlR2VrNFgyM2pBdFQxUU1ZMjhxODdSd1RzNTlZdVRJWmhXRHZ3OGh1NnVXbGwKRjhnYlA0ZEVTellwMEZvYVM4MGRnYlQ4dEtPVkEzUWVDUDRYSWdnd1NvcTZ4OVRRQTBGVThyZkxlRXl6UE1vcQovQVRZWkF3UUtkUEI4TFpyek51MlJCaGhSRStyb0hKTTQwK21hZDVlbVo5M2NDS1U2TnUxWTJLVi9YVDdHQ2s5CkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGR3cHJuUC9Xc0l0MCtGQUkrS2MKaXpjVkdzeE1WdVVUZEg1c1JPMXB2QU5GWkYvYjN0RGsrVkhzZ2RTL0VTRytkcVNzb1phTTdIakZ0K3grZHUvcgoyc2hCeUxEeUpxYmczbzVad1VuL0YwVUJNWUhUVXluR1czRlJYVm8wYnY2RkY0dm42cjJtWHFhSTNzRmkzLzBICmdnWHJvbFBHY29iZnBaT3hSSjVKYUZRb2R0TlU4MkV5MFdmRVBrNmIycDM3SXF4RVFWSTlXNkNuelZQbkcxakIKVU9nZjNIZ0Y2R0gwR1Q3TUFnVkNHVHUyNjhhR2JVaEIwcHZaM1NaUzRrd3Z3RjJ6MklBNVdGWFo1SVNUQ3JDSgpOd2ppbFRXRjRaQ0tzRFdTRVJlWjZ4ZjJNNXBreTNVQyszbkxKd2RCd1o5Q2MyMmRIVmxEaHJ0M2RvYUI5b013Cld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVg0bXhPV3NNbVNDNllJU3MxU24KOG51NGxaSFZhRkNPYkloZXNEeXVCcGRJT2NSUGg4dEdCcjZSU0duUktMbnc0bWtubHd4UlBUK2pCclVucVZELwptSHkyeFgxNFhQV0RDYW8wWU9IK2VRRklzUWhrRHRGVHR3QmNFbVNjcEZaWDhLakdESjhXQzkrRVFZKzBmV0NmCjhnK2xXNFVDTEszbERtWkE5RTM3aTU0QTBUTERQb2psZjJiMitETVg3VldzOVlEdVozeG5Fb0g2RmlHZmQrTU0KOWloZ094MjB2YmNUVHN4Zk8yM3YwaERVL0VLaUpZZndHcXIzTWFDdHc5eEpuTkMxMWZzRThRbk5GS3lPQmpTNQpWZC9jc09YUXBHU2laanF1TGI2dXczUEhVR0hoTW5ieVBNNlZRbFhOa2gwUjdQTmRmK0FEQTlDTG9RQUxtQU5wCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBazZQbTQ3cU5KenRsVldtUmRSRzIKbXFDNktMWG9GMWszTFFLdC95Zk43YUovbm9BdlpHNlhQNmVwTEV5b2hacldJcTlnVitrcjIvdGhFbXhhWUQvNwovNURCY1RRY2FBcjdSUXVUd1FmYm5ONW9xNDY2aFg0dnVaNWxRQ3JIMU5VTDRVQmJ2Vm5jamdZUnJPSWxiTVRHCjJaUE1oaTV5V1kzUDFLTlJDaGJKVG0wNkkzeEZXK0I2UDRPRlc4czQ3Ulc2cDJQZTdVSFRRTGJIYzQ5eFZVVVIKVXdUVzZuV3NQWllZVzJPZU5qMTVzVmJubW9ydm4zM1RrdWZzdWxMQnFGVmd3SVh6SWFBbXpma1BuUHpEMHFVVQpUYnEyRU5vUmx6bm04RTJHTERHZGwvcnlScFl1Vnh1VG5RV2xZek1jbXR3YVJHT3lJNjByZDBjbVBsRk9MTjkwCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHNqL2ZDcHRPZUdXTzAzSDM5LzgKdHphTVJqcEwyTmpwM3k4MWJVdGZBbGp2dDVPdTZGcnRpTWpjdC9SN0FGUjV1WlFFcHN4anJzemFUdjV3dEZXNgpydC8vdFhkWTBUckx1RE1vUWIrTEllUVJzNjlRY1JhenRzUU9xKy80N0NnbmdaSUFLY3hFcFExcEpHRjUwQ1Y3CnFSbFBCaithQTA2d0tEWGZsQm4wNTk0ZWlxdHZmRVJtcEIxcHlXRHVIT01uM1d2Q2dqTVV0aTN5Sk1ScDU4NlEKbjBtaGdyVktVT0RxaWxlc2NLQWVtN0I0QjVsUzhRYWVhbTg1M3p0NUt3aEFOVzl2NllweUpKU08xcWtIb3UwUAp1cUdHRVRCa1pnUDVvZUZQZUFWVUxpOWtiZkJKQkJybm9VR0lwcXRVMVFrYkRKSS9hb3pvSXJocFFGVnFKZGMzCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVNvbHhURHlWVVhpSU9QTWpkN0EKYUx0VHExdXVnTXFrQmhWOE8wNmRTa2J4L2ZURmJLeDRpVkVlL2JXQVhVYmk5bzhlV25EMlhNOHV5empIVWFENQpGaWFBQmFmNG1qSlduQnJWMXRzZjE1bkU4bHZmSUQ5YzRxWlRUMEVTbXM0aEJDVEViaFlqdStqNDdzZ1JhNnhiCmR4QnJBNzNHT0VUTVB4NFZHd3dDUzl5SVZjaXN3SEl0b05ZYk5rOHNDaGZXcXQvenk2aW03Y0NTMU5OcTFEbGQKcVVyNFFzeXR0QnVTRHlLTHBuV1BKVW9TU1BEL0ticjVGTzZia0tVeEJmN3dHU09UNndHMW5yR3ozM3VvdkhIaApZMzBOQzVjUkkwT3FnMDJIcEhrYU5kSWFNMnBxSTMwQU04YlIvdVNiWkExdkc2TUxvNEQ1VGM0K0FPVi8wYVRvCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUpFa3NUUlBtSmlxVFFLQTk0RnkKWDRBemY4ZU9SWjNUZzYrcHRlMVgzRlkycWxRY1VpQTdsVFRkdk9tM1VrbWljT0NET213czdCYXNFMURBbThqQgpUZm54VHN3TEJuYml6N3U1ZTMxRVNSRWVaZ2Q2Y2hnSHcvVEF4b1hpSVFpUlVFSmQ0TElzRFRoVitJdGhwZHRMCjdLbWxyaTNqU0QxSjhIK0FkRUl1NXc1RXNqbHZDUDdzb200Vy9TckVGbUhwTUNXT2M4TXFUcVhkakhCMWY0c3AKUlJGZ0oweTV5TDl1N2kzZmE3WlExTkYxV2ZoOW5FTSt6RzYweGIrTnFrYkRFQTlSaW5HaGhtN1d1aCtmRWxGWAppUytqSU5HeGwwZ3dCK3lpWEtRaHN3eThVK2g0MG16SW1aSVE5cEVTem9VRFdSZWM2aUNqQ2xZOG90dU5WYVhiClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEZCUGpyM0UyWnZLR2dDV3V1NEYKVE40M1pHUjdrYXQwVjVPQ2VQSmlra1hLa1NINWphbEdrb0FEd3Z3KzBwUk9tNmR0Y2JpSSsxblFMRTFjeklJcwpjOGthTlZSc3Q5R01MaG0zcjduZkZRVVhzYlgxYjV3VWFZcXF6MDQwKzljTS9jL2tWa29YMXFJQmh4NUxTeFVICkVJdEo1WDJSdWdmbHJvaXhUaXVKbHpEM1dxY2dJZUdtWERWRDYwS1d6ck1CUHd3Z1h0eWEzaWRYTnpJbVI0RDkKRVp3UjVGMmRmM3ZTSkV6WEFxWHQwYUF6Uk1seU1keWJvUW5vNFlzZG5IVGxKYXFHYVVWMStLZ2JxS1p2cUlSNApWU0VWUmhXV1J1dk9laHJROGdpVVA2OE45eE5wQ20yNXpJblNBQ3Bkc0QrOW1SaXZJT1czY1ZSZ0VoTG9sMnRVClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzk5S3F5UWFZNUprZm9kbnhpYmkKUWpXSlYxRVlXWFEzdERyV2xsYUptS0J5TXdseDJIZG1GQTAreHova2xMVjdDNTBmUDVXcjZnOEZHckxVZHZNdQp2OG9mL2N3K3hsS2dOYjFwSGZobWRESkx1eVRGdzlOYnZjUFZMZEFyRUZOeFN6Sm9GZGs5QlgxeDVyZ1FHTWRRCm1BdGQvOFdsdkxLV2ZXRzlrVFBnT1hHV0FXbGVLQUQzYnM1M3lpVHkydWJTSkFGeU12bWlmbzJMUmhHYjZueTMKNmdmNW5YTCt3TGRCUnFPcFhlSy8xbkU1U2IyZ1dkVWpDYjB5QWxhNWhCZDRGR2ExT0IrbkVQcEx1aHFUV3lXZApTTW5yWUdCakMwcVNPMWd1K1JuVFJqYWg0N2pUKzBzWkZHUU90Y1E4d29QTE5DTmp1UXdhNTB5NTc4RFhVVnhWCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXdKUXcrYVpyOFdVVXRpYk5FNEIKSDhUYTl5a1FqZlhDVVFhNHBCYmVHZ0FPdFllRVVhM3hkSTlSVzlWczlEK0x1NzV3ckR1T1hqOTZBbWJlbWFURApKTS9vVmx2STBVWGF6UFpyY3JNZE95UkRsR3NRcUwwNEdQT3hoRzNSRzVaL0xCSU5iQkxxcmthRGVDcW02VXRJCjc0czVLcEpHNlhpTEpWc2lmSE9iMEU3c3VrQ09hQzZLYjVNQTJrR0QzSXIwNzhXbFB4NDFIWC9TK01sSkdHYmMKOU11dUViNm1vS2JIMmkzYWI1ZnlOZDZJUmgxVUN1SFlRMTluYlVWZDZ3bjcvL0d3WmFZeHVxdEZCa1Z2MjAxSgpGQXAzK1Fnc01sVGptaFQwTmlQdlQ4VXpYZVpGV3dyRVRkTWVOOFZWTjQ0d0pCbEtFUEpudy85a2l5WCsvTDN5CmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMW56dUtJdU1ubVlnRk5XQnNLelcKUmZKNU9lQjM5YllFSlJ1V3hhZnhETjRJMm9lZUdBOXhTSUNwVzE5eGx0TUJJZ2d6c3ZnWlRDY1ErWTVBTG5oZgpINFdHc2R2RUM5WHpTcEtFbGgvVE9zZjVBUkJGNVI0TVQvcnM2cVNaVzgxc2swSHlGR2xYVFM3cU5EU1hYUXhVCm1oekY4bjFlUU5FMXRLNHBzclladXJ6Y2NNcjJEenFsQ1RVY05GUGo0S1lvRXRpVFNMejBzNk5iaG1jYzVBa2gKWUh6RWhIclRGUTU4eENCVTUrcElMT3FaVk9jY0h3UzJQU0IyUkNNNXNaNTluWnlHeWg4dTJheXV1WG5VcUJnVgpBR0FMYjlGSGRkb0cvZEtHcFJoVDNWMk5NSU12Qk15enk0a3B0dXBJSWFsaHVKdlp2MWdkK2RkSmVhSmVYbWYwClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkM2Y1BGWm8xUlpvQVJveWs3UG8KN2NkMEpielQzK3JUdFA3TVBuOXVWY1RPUXljaUQvdlp6QjUzTW5YNXRMRG4xLzZxNzZmeVdZTFJtSHY3VjdiOApKdEdUZGFsdmhvbzJEOTFpMDBvNEhWNHlPSy83ZW1aVHVtOHZSVyswaUE2MmNtNUdiNHo5MFVhV0c1OXdLTGhBCkdYQWdrRi8rcVF1V1laS2tpdkRiblcxV09vQlhKTEYzZ1FKS3RTR1h4eWxlZkdVQThneUtKSC96MFhCek1XSkEKbkJVd0tyOVZzZ3p5L2JtODd3REF4Z2w2d2pSSXpBWFdCYUZ4b0tFUWJ1cjlKNk05SXQ5NXcyUEdqL3BNWEFrTgpraWJnd21VZVQ3NHFEdm80cGt4b1hJMlpXWG56UVNvTHAxQkVVeDBlaS9GVWRnVFlzT3dKRUhLMVZpS0dlOGN5CmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUJucWE2QUtyQkZZQndtWVBmWEcKM3NzVHNLbHdhODA1R21wY3l2cVZOUGsyV0lhbVJRTFBMbWVPbWVkYm5kZ0lKdzUxWlRpQ0laZDU3cVZ1dGVBMApLMHZoNkpzSGk4QnJmM2w2d1pJdkY4b2ZQV003dzJEemFBaWVjbmg3MjNkV0tzNEMzeWV5RTdJNWVuNjFoNmlXCjZEbkdUV2lCZWlhU0hlR1NMMVlVTlovd1kxNkxXQm95eVZQOTRzQk1VS0NuU1NJMURVZHprZW9UOTdBTDBoZ0sKMDhRUjNuMFdpQm0zV0E0N1hCdW5XOUhYNGxQTzM2RG9sUCtIZXBueDhSbC96RzdoRFFnc1plNGRZNmphUVhGdwoxQlYvbTRLMUUzUTRsSU5UZjdYbkF2Vkd1NElYdTdMSHZZUzFQZzV2eGIwS1hDZVlHcVZJakp0eHlyWkloWDNmClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUM0WXIya2VGeFZaR3pXRjZ1M2sKWHkwd1pxNzZIZ0xMVm9iSDV6OWRjYkpvV1V0NXFzNlA0b3dlS0c0YTVHbU9aU1RXQ25iSUNGcVRkRElRMHBYZAowdVJwRG9pNVFtYUk2QUhJL3NJY2tpUDFPeXJUam1LV0tObzlVTC9Sek4xckkyMkxjWXM1cEwvV0FLMEhVeFZrCkIwR0lXQmtIMXRudGdleEpkU0kzdGFyVlc4RWhsNUJWTWRZOXcrWkNxYmY0ZlpmenVWcURRSHVVeTVDSnNFOXoKeWsvQjdEUWplTjNyRjZkdjRoV3ZPc29TWXBtSHc2SmVaT3BITmFjRXZTdEZrV2lFbzJycW9pb3BnSHkvcEV1SwpvVVpkemczejJ0SGtpaDVSU1RwcnBnbjRGdFkzbWVoQ1dwTjRPeGoyWXRvaWR5dDMveHkydU5GZ3lOM0h2YVRyCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeENXUG1kSmVNbFJjSzBsQXZTOWIKS3MwdGh6WmxXNk5ZZ2J2bExSR2h1SlBDdVNObFJINmJiSUZoOUh1OEYxRy9QZDE5SjZVWmhFK3hBWDBucnA0NwozZ2pnWGR5V0Jad1A5MmFMaEdqQ3Y1TkNjQmM4TVROUnVlbWZ5YUdkdkEweDlXQlZpY1VUNUpLakdRRWhuWnRTCmJMdTFjQTZnQUNsZmNVQTFSWnQ2RW9aUFBxenFBQ1VEUldyazBqQnUxeW5NSlR4b3RZTlNWM1YrdFFUSVhFRisKVnA3MVZUSkdpdVVvMzdSUHFmSzdBd3p0NGZzMVhGQk90N1BtMjJGQWxsaFd2Q3gycllvcTU3Qmk0ZVdUdDFPZQpxdTVBTmtnMXhVeGhLcUJBdmEwSXMzaXN6RnFIcEx3TUMvM2ZyVFZnWmh1SnNjbmZjSzI4aXNkR2E0emRNVTZaCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHRDQ0J3aVdlTmxUeUFZcWtXK0EKcjJjQ0pWczNOSU9neGRab0lYRmN4QVI2ZmF6cVJHS0FkdUFpbWtyYTd4c0JoRnFEWVVRL2RPajNyTnREdFBtRApoOGFJYTVhYWRKZVFRSUV2UExxYU5pcFVUNzBpV1BmNzMvbTZCb3hUdk0xSWdlUy9IWTdBeldjYXE5UDkvVVc1CnBQdzY0RWdWNGJNVXNlQ2ladzFLekFlNUxENGFpdENmSWxmNzVxRGwxdzd6STdqTVRuZkppU0VuM2VReDg2ZkoKcW1HeXJFSzZ5NzQyRCtRYUtwVVBIYXo4ZWNrd1dKaVhzUFVHUy9sOXpHL2lxbmhad3gycWlwMW1uTVNVQTJidAp6WnlRN01ETlpqdzIrMEY3MWcrS2JNM2ludjQ1RlE2VjNlRGtsMWptNzlSekZrNFdRQW5iSW9oYUZ2YUh5aENsCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDJscHVXS0IxVy93NVBUS0c5ZlgKclhvVktXZzBxZ25HYjB0QkVvL0svQ01tR3FaTlRmSDRKWkRUUm10NytLcGtlOWVINGkyc1hWYTRnajdQU1dmeQo1Vm5CaHVScldsT1NHVmN1eWhzL2RvMVdjRHRwNThkbEYvZWowWnBmOUlzd0FJdXpYbHRwYXAzUXprL0tIcTk2CjJFK3hMVG1xaDJiUnhMMThKWmM2MWtReUloR1FzNXVZb21IZmVvZWFmTy9ibGovbDIxQkVDVXNRWWJJNnZBRlQKUzRybTZucFlJYXdkQXZySTUvSXdrS1gwUFJ3R3BWWmVlVTF3SDNlcWZ3VHRxMWFNUnhYdUtFRjJWWXZhQUY3WQpBdVprUDVacTB3eVA2b1lRMDIrU1c3cTFFZVJkaUlsU0JEak1vTyswTnBvcG41QnFLMjc0WiswU2Y2aDQ1eUpWCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdm9lcG9Wdis2SzBWMWxCdGhDaXAKQlNCcUdTMTd1dmgvRzd1SWUybEpEQTIzVVBmVnBnc1Q4VlJEcnp0Mnl5SXI4Sjk1dUFzTDkzWkR2T2JURVlqRApsQUJZb2MxWFNwditvSTAySEcxbmVKN0FiR0NNMThybStkcDgxUC9IMXUwaVFla00xUWI4SGk5cFVzU0JpUkNWCll2WmY3UVZIb0hJQXZkblJwYlpDb3o3aUFFNDFodGFaQXRjblcxT2F3QmdRRmgrMTVXZFVMU21YMlJkMllYVXoKcXAreGk0dE91bFVMMktyTlRvU0FqN1pHUkJRcHBxdi9NTnIraVJuRVdrUThHcUJvTG9tSGI3SXVDaGpTMm14Qwp5VUwxNVhCUUpMdTNuQUY0Z2R5c0QwMjd5bzhPUExoQTVuS0xiZUNrMjMyc002cVhrdFZDK1dZVnQ2c1R4UTFsCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEdMTDdtR3c0SzB1dFg3UG93UzUKOUZXZi9yakc1bEpKa3dwK3RoSExIV05wVUxBRkE4SGhHTzgrMEN0OVE1Z0xKNFNVY2pWNVNqaUpPcnQ2NU9RSQpDR0ZPRG81UmV2NWFWUkozbTNYTUNld1ljNXhkUGN6bkFDdkMrUEJTTHN0c29odTg5T3NwSCtCdG9ZRFIrVjBzCnlpd0trRlArWU1Pei9TYWxrU3l6cS9DS1dhcDVRbVZYZGtINEppK1QxOG50d0thRFFERzA0eC9JVTFSTHBtVHEKNXVyYnhhaUxvaWZ4U0k0ejB2UTlZeTVLeDJnb2U3QlNvWU5IRzFxQlhvT1UxMVQzaUw4eXAvMExCZllHODlwTwpuSHNqcUF5WnNhV2xzR1hFVWtrME9ZWVNuU0NEQ1NUOTR5bEgvMm9sdjFFV2lGRWlzUnlzbi9kZ2FyMFdZK3J3Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclluVTRqL2kxWmlwRUEwVHlVa0UKbTJ1c0N2ZS9kRmwyS3RZUi85OW9aVWQzalJXVGcyN2JITC9yc1RxL0U1cDJsU2RUdGJuVHpBVWJueWlRYUxjSwpDYnZxSnlxZjJoYUNkTEdDMFc2a0srVmNZZ1gwN1psdkhXVDh4dTV6ajdiNXlHRkY4OStRdE5jeHRrYVdhU2xuCjQ4Wmp6VmJGRXVPcFY5aHQrR0h2YmFHRkVhbDhNMk02NGVKUDNWUFFqSFhzNjh5ak1VODJTNzZOQUd0M1orUlcKajlQQnd5bWxSYTZNMzdLZUUrM1lwWlMxcXFlM2RvNlRsZnRnL0tkeWJ4Y3JnUkY0anFHQmd6dVVYNElnZE04SApLNmhnSXVFV05MMThhVnhHaGZrTmdBY293WVEzbXdmRGlLQzVPcjg5aDlzakZHRS9GMUhlVXJjbUdRYzhJYWkvCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW1SNjloQ29XNWRPbDVyYmlOQWcKMjlBbEh0cDRkbGJ3aXBkaDJWaVIxQ2dOVnhDLzRpOG9nemZNL29KOHg0bTMxSVdKVkpJWVZWN3h5MVE4VUFXTApwSm1FWTBqdHJGaEZsS0E3YnFSSmF4cUpEQ0VCRmNhSVZ4dHk0R3ZybDdLcFR5N2tOOWFPN1Q5K0czUXBnUWtICkF6VGV2ZTEra0hKeWxGbHp3VHo2L2JHenF0NXFUakI0Tmo4SFJQZTNkU0dhUFVsNzlmRHRWVkUwVjJpWHZleFoKNFVkSUUyOGFRUFZva212K1pDNlVRL0FPTzRMM1VuWS9rZTZ4UmtuTit1Q1dlczhGczdKdC8yVG1pSFYzd3d0SgpJb2ZmM0I3K0NlYTdsOVVpeS9lZURQd3FVZEpNS1lhUTdYbTBRMnl4UzJmRjNZOTRPK09rK2kybGtsU1JjR0xaCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUpHcUUyVXhhaVJRRWc1RnhkVHUKU0cxMkMzaHhQekF6bHBMTmZzNFpJOTJIeUtGWldyK0dHSzkwSzZSaFhaU04wZE96V1U0ajNNQUprZ21CRXhHYwpNQVdEbEFRYzZiU0UrU1psWU1tdzYzMHo5cnh5VXBEMjZmZzdaZEIrSERIcEdMQlNvVE5PUzVudDlNZEZSbjgrCnRUTmdKeXptSEx3ZWRhaFVkRXRDZzZ1alFPOHdBOEhna1gxTS9hbmdaWmZTQmFFeWw1TzFxMGlRT3lQZVZIa0gKY0VYMjJzUjZ0ZnBVWXYvSzlSWk9IZTZMUWNmSCsyTllzQ0pmZW1zNmkxek5tN0NQZFpvQ0pQbm9BODZpVVRjRApHS3FYdzJlRS9yN2JxRGNRZ1ZCNzcwT1FDZWVQaWpCbHZPOVZZV0ZiMkI4di9CUkx5d2dBRUE1Vzd0SXhBWVR0CnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUVEdEkvVXEvcWlxenhzTEFZdlAKNmx3c0pTUzR1c0F0L3JQcVVwM2J2UW9NbkNVN05JeTZVckN5TnBKdUZpWjRTUXloVnFCSVFhR0xYd0txNmZzSQprajZ5YUc1TXpab0cxYWU0ZUR5L3IrS2s2TUZwNEh3TzV0aTNHUE1aL2dwSHJlSVJpOGVSL3VqU0hBRUZudVdWCmNkdkdtODFGc1J3R3hla1pJSHdsd09wc1V6b2RIUTkxNWdOS0hiQkwwWGZsSmNtSXNmaWNvaEpuTXh1RWNidHYKSkExendjNWlWVURoU0xGT3ViclhsTU0wd1hwMXJLczV3b1BNSmpRMmhTZW1rM2t0MXRwWkphalo1OGd0ZnVoYgo1VmkxT2hKNnJ3ai91TzVTbThBM0NtZGNCZVhqS3Blb2NCTlFMOXNOVWVrWTViTW41K20xMVkwaCtqWm5TZzMyCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmVLVENaNXlmRU1EUVcxUzVWdlkKaGZOVnlWbmVvU1pCSzFjRnZNaThSb3FYUTRkOXdpUkE3MDhGWFp0OW1oZHRoWm5VK0gyN25Vem12Q1VMbXBxTApwdXVmMWllWitFWkdjMy9tTlJtRHppK2F5akNVVS93b0NlNWJFRm9zbmlyZ2dMYnNuT1I5YjhhNS83VWNreWpjCnRvU2VUWXBoeFBMeFZoQVQrYVNSUmI0OTBVZllLREtLU3VUMEFMYzRDaDQ5Zkd2WDRidFR5VmpjdjBMTTl2bEEKMGp6eGVEaElOYXlucUF4RGM2NnVqSHpaUFAwY0g2d2xWVDdsZ3NnTFNaQmVaS3BsRjdEL2dSeHdpWEl5WE1SbQpGQWZEVWJBaXRMNEtVSG9Od0J4SkNWS2o0UWd4WThkNGl1UGNYZUhxSWNYSTk3b1U0eC96SDNyVjBzSWFnWjVuCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbVdKT1RyV2VocTRVREdsYitNSUwKTk5aemU5T1Q3WVpROUtON0RtYmQvalp4bU85QVJKM3hLVWdhSmRrc2kzZEszOVQxOFJ0M0JNR0dHdTVIY293awpsSWZhM2t6YlhSaVlxQ0d3ZXVuMEIya0t5blhjZXJCeUYwZjdzQUtYVHdvQ1IwTUFxWGpoTUxyYXRLRDQzWlV5Ck5RcmMyWlh0aGhOZFNiR3JlVXo0MEJaSEVUQnM5U0Z5MDg0WGRFUzh1WWpQU1piV05SQWJSNm43ek9GMXY4cWkKS1hqZzB6SVpFNnRlZ3JBTXdkVWVzNDlTVm5maW9hSDdJZUh3cWNYQmc0WVJ2dG9OamhWank1Z1ROR1g0aTZMRQpXVmJhczczbVZlK0crYjZWMllGcUJaSnYwaXdrTVpvYzB2YnJHV01JT3NRdXB1TVYxNHNHd0NNZFFjSSt4QmpYCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1FIUWNnbkJiMnU5SHNtOVdQL3IKSHZnL3p5V2Jsa3M3SU9yMGxPYUhJWlZoVEszWEo2aXpWT1c5YWdVbUdDcW15VDhQNkRoblB5MWZxZ3BwSjRwegpnczBNdnpXZFdObmRZRWh6K1VmWHhSL2U0eVRJUHJFWVBkbVIweGxHdXpjcFU2SXdMYVBjU0IrNit1VW10dHdtCm1kc3Jocmd1Z2lHSG1FU0NiNjFOK2MrSURJNVdramI1Vi9vZ1hGNEVnQjdPdW80SU12dHBUU0VjWXJuS0JZL0sKN2VaMTA2eWVxUGF4amJlQTg3QlZ0ZkVJU242Uys2U0dJY0pKUUl3TXErMk1jOXQ2eEEwNkxjWkZJQi9kWVJSTQpQcVZ1eVZ1QW9oTlhvTFF0c0t6MzZReDV0TjNURUcvejdGdHk1WVcyZ2VQT0lFYXBqRS9xZ3dMaHpyR2xxVlRTCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUZBa09OSnVZbWcrSmI1cEdlWkwKMDY2UlpFZnZITkl0YVBJQm8rbnFiSnhxT0MvSFBvOWN1cCtXdE1yVU4yUjl2UjJJTFpZa0R4UEZrRTMyM3h3UApNNWdMK2FQalZUYU4vNGVyWis3YThHME5lcnJWMHh3bGVtWjdKUnZaWEt6SzVQYTRqcEszUVg0dlJBSFNtZkVpCkc4ZHJLcm1uUWJMcGIxTk4wWGk3YjVwbnZSdEdnQ01EaVhtV2tkbDJHV1hmTmxOYTNaaUxnZkt1TTNYYm5KV1IKelRXT0prOHgxWUdIRDltbWw5QXI4ampWWk41bjUxblNESFF6ZnF6b1B3ZzBjblhnYXlwbVV1d1IweVpyKzZ1NQpnQU1COXJoZm5EZnZxb1RhZE1iSmFpNWxFTS9xSFdyc2Jqc25adGRBRS9CbCtPd2p3Z0RReEdmeTMxYVFzdk9UCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlVYc0NzOUcxeHdQM1Q1OG4zU2kKMTBLcGV6ZEdvT0lwWEtsY0plMUxscm9JNEZ5cFhZcVVmMWZVRmR0U0dHNGVNYlJIQTFHWkYyRTI0LzIvSW13aAo1eFIwcGJnT3BQRUtBU3k0Tks2am9wSkx6OEJzTnVCa2pUMTJFR1RiQUxQTVYxcWxvbzhEempyVnpqRDAyUVFkCm5RdjEzRGZMY3FQWXk3YXd2V1dwNHBXUndValdGK1NyRGRuSmRwZXlvcmQzNDMvd1piYWZoSWxubHNzSUw2eHYKVUlYM3BCUG1lLzJoeDJIR2ZUalBVbzNlbHBoWW93cE9PYXJOYVhHVEhhT1BKQ01WL3RwNEdOL1doZmRHQzhLcQpVUWVoQlU5UlhLOVpMV1NXTWc2eVpGUVc0RkUyUUlkV2lCcXJzOWRqY1ZGY014dThiLy8xUDRqUy92ZERBZmJXCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHl0SHkyNjVzY2dhTndjQURUK3EKSmM1NHFxeDUxc0E0VlRJd1BoMEt6OFhqazN5MlkreFhwNnB4dXE3WTh0NXdIQW5FVHF0dWtlZVo1ZHcwTUUyQwpESW5sVUFOeVJmZGFFRXV3aHB1eDdEdGg1WHdwdit0S3AxV25uNUhwZS9SaEZxSmlkRjdOb2EySWsza05KQmVCCmtTMFpwRXMwbHhhcmdzZlRKRVAzdE1kelhqb2ZhRkc1U3RoMG8xR3Y4MU05a0lWZnBzQXVQUzdLRmtpOVZPcUIKRFVubEpMN0ZIcWpvbFdNMjBxck8vM2lIVmNSUm16anM4NFRxayt1enZmV0ViUVdiRDZDb3RLZytTSEp0RHdoRQpUcFNjd0VFZStkRTNMVjVvQWZKVWhRTjJQWHRqRVdoSXkzVHkwQlUwS0ptb2kvYzh6Vm9xV2Rsc2NtRjFqVyt4CnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzFZNDk1dHQ1OGY5dWlVNmJtL3MKRlgwNC9EcVc4MlBKMmhtMlRxRlJMM09TeTlMcGJCd2ViTFM3V3ZIaGhIeEtGVFhHd3FadXMyZFl3ck5lMllwOQp3SklobUovQnFmemdtZU5iam9yaU1KU25TQkRpck9FSnhrZlpURFBuMHdvVEJTN0c0c2h4Z25kaWRQd2hFaTZyCk5nL0VvblQxNzFEV1FxT01XYTJ1VXdlRmRNVkdaaDZ4YjluMDJmTENXMmc3aUJ6RDYvVVluWE8rMWVMK3VkTXIKL2FmejZ1NXcwMTlOZTJWdjVqMEtaZENKRk9hLy9VYnlEOGN4RTU1V1lUNHJ2Z0ttUTZCRi80ZzhOYzZYb0MvbApJRy9WTm1ja2ZjVmRKWDYxODFUTk5xT0dsY3JoYmEwWVdxVlY0bjgwRjRUNlZSeXBwU2FCKys5Z1Y4Nk5yT3FrCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFg4OWZIelNwSmpqSFJqOUN0bVEKbmN6UTc0VkZXaHZsRk9SRWFxcnFiL0FTMXYzTUUzNUZtWnZ5NXdmelRzUzNaWGw4dTFyb1pFTmIrbysydFBzaApLcXdFalFLNHFFY0JvVXZvaytzclhNK1QxSU4rMWdaUEYzOFR5My9wdkpUeGFZbFRqMFg3WEdpVTAyYm9RcWdrCmRJUE1lOHg3MHhNeDEyTUJuYU5DK2llUGo2bDFiYUlER1FHRklleFVqOGhKT0RWMEJuN1hMNXZJY2VsUnZNRFQKQ1lWZUM1aU1JNzkvSHJEQ2VRL0M2WUZGRklkSmQwbzYwcldENU5ESEdpRlNtczR4TzNHcDVqMVd1T0tLVThtdApObVA3bFk0Wm1BTzZZdG16M3FVcngvWDBlcTRWdWFoUXBwK1p5R0lUWTdDZ1BldU1xekdSTEpETUFWZEtQYnN0CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0poTEZaNkk1a0oxOGpCbmdaUmEKWVo5ZXkyenEzbDF2OEVQRkFtQ0NFTFVTNk5rdmF5QkNxVUlKdUJMc092eW84b2NBY2pzVlhWQW11bXpzTlJSbgprRnIvZVQrT3Y2OHcwcktsWjZYcW5DWDlLTHRWYUgzUkU4M1ZFdlZsZ1YwVEdIcWtZcjRvWTZIaVhiT1NtVVpTCmx4MzdNcHNaN1lkUWhYVDcxanM4V1FBUnpHQ2kzSDhIaXRuUGxrcEQ2Z1MzY2ZUVlk2b0hUcUc4MjB0ZVRITm8KRHBqYTRhMFV3Rk9ZOU9ZRk1aUEtIZ2M4czR3VlpEeThFa1YrRUdack5IaVd4dmQvVld5M2RZVWtMNUNHWTByMwowenJpeEh2RGkyamd0Y1BKSXdMV3VNOUpoc2hHV2ZDemV3S3JMSU9XdTg2MjFhYjdkK0NiaEl5dkovVlhUdHFGClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEFjbjIvMDc1dklibElSRENQSHEKU2paMlg1Z2QrZ0prRFJveDNjRVVPVlhScWd3WFNpdng4M0hDVytzOVNmTlY2Z1RaUEdnMXlOQi9icFdhY0lOZQozbTlNamp2Wkl3V3FKUjI4alVoMVhrNmZDSGRHSURreXprQXU2UkdCZXA4S1dZK1MyempwTldqa3RQZk84dmxRClVqUi9XRDBBVDVFMU5mclpaSDE5NXhwUHdZL0hCRzJhZmpaOXV3OFMzc1dydzQxdVNnUmo4VEtXbEdPOGcyMWYKdStCVURvKzM3VjJSL1l5K2VQRElJT082WXQrcCt3eWJpWHQ0VzNVdlVqZ1NDZ1UvNEVoNmZPbGIwL09WaklhQwpOTXZacHpQZ0IwL0Y5ZXplV1ZTL3JYeFVsVjl2dU9kMDA1b3FyT2FYVGRQNGgwVlZmQWduQ2RaT0g1cmNSeXdoCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1dDalhuZVUvTDRqUmxOeUYvL24KdkJUSks0TXNZZkFDY2hLT1YwZ1Z4MjJLclU3NFFVVTdsbk8vdTVIMk9CcU1qV2VyblhuVkdBZUR1cE9hd0RyUwpxMmlWUUMwZm1QbzU5b0N2RFozYUhYRjZZUzFoNVU2WGpIU3pLa09Kd1JIRURsbktSZUV1NGxOVTV1bzdkY0ZOCmlnNTY1dTFzQy9ETEsrYkxzb01mUzNoTktkNC96RmRZbkR1UzJ5QUxDTHRBU3p6czVIN3ZsWHYwRWF2bER0dXYKWCtIOFVrLzUxRGdTUDlRYlkwZTVpTmF6ZndKdWU3WUlKMlpaaTk3TUk5NU9zZ3hSNjgyQVNieGFlNXpQVVhkZgpJQWEwalYwcUZPSmxnRlU1U2d4QlhHZDlmdU8xUWN3RTlUNkNQNDZzVFptcFluTmxjendBVEErSVpaNUthUjVXCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0lELzAxMEhUUWh3TUZ5L29nb2gKZUZsQkw3UHU5bWwxOWk3dVB3cXVPTG8rc1o3TUNSZUwwNHo1QkFwWlBzYkhlSWtSUmZZcXlyNjJNQTRjOTRRTQpxdmxqWk8wYUxvSUFYWTVXSkhUY2Y2L3gwNFIxUFZGT2ZKU1RkR0pMVVorYkVja1FOcDU5ZjZoeXEyTG5VQlRmCkljb3g4RTlDSGtqSG1NVDVVN2FxN25SdjBDTmhOWFR6TXpEQzVZd0laNDA4cDU5OHFBUlRveG1zMUJhRGx1WWYKOHpkaGw3cENSVFlCeHJwaXZlNENXdGFSU2FCNXkwUVcvT3dnTHZDNHJoTVgwUjhQYjNtRjB6cWduVkFkY2tFSAo3VXpZakIzOFp1WFF1UnlMRUhPdTFPTjFMM0hWRU5FV1JGekhiMkd2VnIxT25TTm1DWm5oRGVOMFA0Qlo1T3lqCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjQwa1EwRDZESEIvdFp0RG9zQ0gKMXZrVFFpWGVYOWllOXF3N05wQ1ZyZGVpS1ZBSUVGcy96L29tSSt5clJPTFhOMlEwVmozckkwa0ZGNlhtd2xxZApuNFgvcWJ5NEhZaysrTktYVHFuN2NSR3c4cEY4RThORU14dGNDbnFOWWJxbzRYTlhIcFV0ODNqZEdnV01xZzJrCmJ2WlVnTFUwV1cxRGlweGpiMitDN1NmMEpsZVBYVE1id01pR2J2Z1pTUjE5bUdSZlpzZk0ydFlXYi80TGJRMnMKc3U2UzRRSktmZ0JUNk1yRU9RSDQ2QjNGVU0zZlorMlkxbUk0UlpETFRFeE02UEp0QXhOOGJMdTRIVFgrNkU1ZworOUk2RkpPcGZxVFlJenV3NzlaUXQ1YzlWSVVQeVVqNzdrZ3JtVjFiS2RGWEo4aDVCNjI1ZU5DNGJvejhNdW81CjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFlTUGlsYkZESHZKQXZFTnhVNXAKUENybC9EZ1N5NFFPUkIreEdwTCs1UGpyMGdJNmo1NHRMSFR4Rms0QzhCRml0NzBSVlloT3FKN2c5UWtSZGgrbQoxNXROYS83TUl5N0I1bW9pMWR1ZENLNkhVQ3g0TGlwTXQ4eno5N0FvR1pwSjdPVG5BZjBJUHh6VEVXN3FRVW5aCmJSZXh0ZzY3S1JGcHdRUldwU09Razd0eU9DTFdMZ0oyOW5mNVN6ZjB3VmlIQXdSVUxKZENrS3NxR2hsWmZsdHUKdng4V2VTczRBVVNJQWt2MTl4dGd6Y0tQdVlyUU9WSXlpSHdnS2VpOXgyMWptcW1TMDYvVFNDaGZiMXliMUpLVQp2eWlMM0U5aXV2SHZEQ2pjTDBSRmMwTnRsc3lLVnVDRjJ3SFVHMHlKOUhpTmdJN1Z1cll0WndoRDdHeGNmK0RsCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTZxWm90UlhjY2JGb3lENzBZc2QKTHNVNnlKbiswa2NrMXlnTWZyWUtRT3pNNEMxNXNSZm1tbkVYdTBWY095aFRNZEE3anI0TlQzd3JPcWF4b3o3RApoeEQxbE9VVW1KaHIrSDMxNzNIdkhjRWtIRnNxWnlsN0Y3MFBwbzh1YVRNWnpaZS95Q0IzUURBUnUyZzFhc2NKCmhuQjNSM25aSTA0amhaSk8vVmEvZ043dlNJM2Q5ZHNlZnVUZmZveDkwWXdLMmQvOElCQ2NGTHRmMHZxbXdkQVkKUkZUQ25jMXppVm5UYkRXekpsOVJzWjcybmRoODRvMFlQZjJsOFM4cHdQSFlyaEZoczNoUFlZa0VFKzVOcVZISQpSYXBUdFRFK082V2VsOHFhOFM0MGs3Wng1c1VPbkF1bndHN0h0OXJvNDU1QjV5eVlORllTUUQyTjU3eGFHdklCCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWtJL3VPS3NvcmJTMENXOWtTY3gKRnhkZ3ErSm5KUnVYUnY4QlVCQkRVd2JNZ2JnVDUxUVlPV1F2K2NZSVRMdFRyUSt2M0J4cmdleU5HaHAwWW40SgpOemlRV0xMZHI1K2ZuSS8yZVZrNGtmUWZ6TWM1TExEcG5qeVZkZDdsMlhLVit5dFhUVUhvekJXajQwQTdYUkpTCjNneEp4dTc0KzRGeHlOV1psTUJzMjF3bFM1ZDhVVDF4VWJ2TVZKY2lGME1wbTRwVnFYRndmb3Zwd1Z5Mkp6Qm4KU0NPWUJTOU1ZeGQvK09ZR1dGVk1Tc212WDFRSWNHTjd3bUVyUy9JV3dHMlc4dFIxZFAvakp6K2dIK2dVeVZ6YQprM00wRnZNYlBzRW9HZGhGS0VYM0crZkMxQXB6TS9acTVFNGlDUlo0WEMrZXIxQzFJQjBmUTVERU42U0dyT01nCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE4xQUg1RlZxbm94bzRnRnY0cW8KTWRSVmF2YmJGQlQ5bVhMTVdhNU5MOWFzYVBkUkh0bXViZXVCVDR3R0FBR3UzRnpIemNoVDJmYk1yME1DeHJEUApLZm1McVNIZFE4d21KeGlxSjk1ZHRGZzZXeVlrY1NuTWM1bDlncFU4Vy8wZU1JTTZwTTRjdzRXS0JtRndUMVFhCkI0c1RpMmhhZkJlQ2o1cEVWQWdobTlaZVRQVDRzZXFIaXMrSGR1ZDNLdk9GK3ZHbE14cTFFazErZU84QTFTT2UKNTF2WDV5T0Q3YXEvbW9Ub3g2SDlVQ2xad29pdndPZ2FpaCtLNlBEbDJuL2JlMzZVQ3dqNk1LelNXUkdjVnl2SwpBK2I2RzhNYVRUQUowTXFvMVV1N1hkYitNTFRzbU9zZGhWaHB6aE52U2RJVXI1aXRxR3YwdkRLcHNKb1RhZjBNCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDdFMzNCWEFOUlpjYWtHc09uQUsKOU4zVjJGV2crWm9hZllTVmJubmx4T3A5bzYzWmZWK2RwZWFuOWRrZDhaMEVhZ2gzVGt0YjZFSFo3bzFFd2VMOQpndkhMVTZPYmRmL1Jkcks5TVBrakR5UHFrd2duYkVSMTdhMmtzRWEwSy9sdEg0bDhoajU1bDVBeUFmWGkwTGM0CkxYTUUwMXpsYytTZ3daOTkyaURvTUNEaHJRSkFVdElEUnFjU1FxbUk2SlRrYzhnTHJpYTZLL2VHdlcvM0dZRy8KUFM0WFdyNzgvcERpczNRZExhK0I4SGtqK3ZnYWVtOWJpWUZXNlJkUDhiZG1vT253bnJBVkY0NkhrRS8yM0w4WgpnMlV5TVlFSmp5OVFEd1hLc0hhSVJIY3FnY0VXTCtQclZDOFdWL0pabXJBZFZNdzFvZVVjNWdmR1piRzJIbnhkCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWJFMU4rMkpxcUdrdmVBeWE3K3MKMjNmY2hDZnozbG0wVHZ3bWk4NVlNMXVVK0R4OEZ1Y2xFb2lvck5MY1UwcXp3cWppRE5YVE5JS2hVZHQ5bGEvRwpNN25VWm12aHZ5WVIvN2g2czRNZDVFTGJJRXRzOGxjUUl2OTVlV1IzNDU3alNSQ0h0UXJGVzZZMzRVd0tRV0pKClIyOXFvVm5Ea0VJbUh1c0FJNXNhMW9ZM1F4L1cyV281cms5dGRYQURzeG5oaUJCNGJwWTFVYnFLaEMxYnRWd3EKMXFvdnNRT20yZHIrNlkyVFBrSXlRTjRabWgwdXcxU1p0UWluNWpLNy9mRkNuYnA4NDI5ajYwT2NNZnlBVWxyaAorSnJTbjdkZzh0SUxUNFpNdUExM1JNdU5UR1N5UXErRTYzc29ONCs4VmxrNXJuUisxdHlRbUZyRHRoRkhjSkw1CjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdisxYWlLaUJBb1BpNG5Tby8yWE0KQStoTzVBQUplVW9HOHVtbzV6NmZqM2xwWUs5Mlo2VG45K1VOZEpNK0JIdTBrNnYxUHVtOFYxcytWZnJZbW9NKwphZ3RFT0I0a1ZrVGN4a25HcFVlRTNCdWkxdnBKaENkNnpiSnp5eDdLUTFEOFpBZUFxZFgvT3pUeGRnNE9MTjhMCkhzTWt3YzM2b1M4Z25iZlBvQXNValR4QVBiaG1TZzJGTmU4WTZycFZsd0Q4VG5iaGN0bnpMZTBNUjFWTzRERWsKR2UwaDI1aSt1UmRHaHVBT1d0ZG9FYlI0T2s3SFlOU0x3RnBLNGRtSzVkWGZCUm11cUE4WEhWSFR1Zk5ScG1tUwoyV0o5WE8ySUpZUHB1dzlvNndKNXJBM2FtejZTd05sTlJaU2lKbDF1am9mVjMxaUd5a1dFN1k5RElyRVl4bndRCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUNiY3V4R0NoUGluUmlaVFcxbEMKRzZXRmRiQ2c1Qkh1NERTWFJZclRHbmNUdGpSb2dJWWtSQjNLMzZ5dW43a3RObWx4Ulo2S1lVNzVzR2NYSDhQTgp0M3dCVS8vN3hMUDZHbEw3b2xiaTZoYXpvZURlRzVaUEVwUlp5NjF5NFVrVU5pMzBRUXlzZlUzWVJFZm96dzNuClg0Q0RlSlpuYlVHbnhibHRlS3hqUUFZTXVCSEcwZkpHVmJYUHpYMUE2L2tkT2dJRTRxNmVSQUFmSjliZHlDZVAKK2pmOHYzQ3p5Rys4UzFiakMxS0VBbXlxZTVMTmFSbWhlME1KamVuc1RtMGdkMTc0OEFFanB6RFNkSi9tYWh2UAo4R3dSL0JJN1B3dlVyRUZQenJSZ29vQlNXR0tTSm9NaFg0bUtVdXlQYmFwY3N6eHNHRDVLYmVOeEthb3NmYkdCClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUlrRzRvVTFWNnVKRzBwUEp4YnUKRlRaTERFei9IeGtRT3pyT2oydUE1MEFKa1RFZEJETFJjZ2JVMm50eVFUN29ySDlqak1USkQyT2NoSWdYQWZRNgpDNGlaM1lXTWtnejZmbzA2RnJET0pSak5TVzVBdlJCQmd0SFEwV1BzU2lqNEZOejJqejl4ZGFkVG1heGxlN1JQCmZTMjJCK2RxVTBwaXlpRTE0Q2NucGhlancwd0ZENHNlcWJsY1JYU1ROcnk0b1lkSisvd1dqbWsyaXU4b0JxQWMKa2dyeXJCTXd3OW9FNWZVWVQ1NW13WDdOTDVLMGIxVUxuK3lFazZWTVRlMnhHN1RJaVZ0Ym51TllFVkIxQ0FpTgppUzBqVHBJbTdpOW9WYXIzejd0WUFUU09YcHJPbTVaT1VQSk1QdGJORE8rRVNIeUNicTNuRkFVbzF0b3pickxECmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBei9WR0NuUWMrWkpod0h5RmFNd0oKMnFWMjNTT3VtNTNJcHZ5dEVyWTAwQnJJMEZDR0sySEc2alE0RUVFT1p6OTVTcVZHbFE1dFZVM252b2VxSlVzegpHQ0Z2cUhlZXJHQUx5WWM1dTVHN1J2Y0lzTVpEa1JmNnlzaTlDaVpNNTNtK1N6b29PTXpnazU2cGZucWFBSFdDCmFHekIwTnNiSjJ3YVc5YW5OU0tiQnFqUzkvbjdiZ05YaEVJQ1M4b0g4UHBwbG1iQzg3VktaVUZJSjV1RW5Ic2kKT2JhSlF2MTlvWThIdTg1Y0hoMjg0K3JCWmZxQVNPbUpXZlpXZWp5bFQ1Q3RVT2NKaE5uYVhuM1VmM3lxMU8rQgpkN0h4dGhmSVFXUUFQRzQ1MzJZTXI5SG0vczBHdEE3YVE4R0o1anJPQzJaWE16Tk9jMjNzQmJrN3ZqSUc3N1dyCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFZHc0xTcys5NDlPbGErcE50dk0KdXlxbHY0bFAvTzBzejJwbDlUN3BoK0hXSlFXN0U2VXFPNFkrYnpNeFc5Q001TVZPRlQ3VXhybDFiZ0hMQXpKeQp2U1k0ZFN1RTQvMG9wZnFzVHVwUU9janRDdFNUZVJJaHd0T2l4bVNLYW5TK1JmMmtSL2FuVWYxbmNpS0hxK1piCk5lQ3dWbnlSYW5STGR5R1lGSWNMV1lGWllndmd1aE1USFpJTkFPc2E1OHNUN2taWWtMSHhPbEhPNmVzVlZPZlgKcndZTTRjaCtiam12TEtKaDNuUkxrMGRJY3dCTEVBdzNjS3Vkc2VVdjQzV3ZiaG1oUGJ5SGRWd0Urbnh6Y05CSApnd0tBeEVCaWwwQk9vaUJ4QVBiRFhkcGdQVGNTSFBTQUNISmRtNFoySm04Z0prYzZPYnRPT2RFd25rQ1BUQVNvCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNW5XbnNnSjdYc1BERWw0V0RyUlgKSE5QNXh3ckdPZk50cnZZTi85Y3Z1TEZrWVUwb3RrZFllOWNMVlFHeSticHNXNHJza3dKTExuZzJkWGdlN3VvaQpsc0RvL012U2ZDd1I3aWhpOU1QS2p6N0x1WTdCV05jWjV6dUhwWU9hdjFEREFncFJaR1l5RlBpL0tjM2hEeTc0Ci9VUGJWcWJOU0VmY0NNRVZPVVVpMUw2RlBRbjZGemdJZ3R2QjdlWGl1bDkzck9iN21jWlZoYUVaMGVGT3VoS1AKN1crbitRbEdrdU5wQmtNTXViTTdqR3BOUWg2NklEdUwxWSswdlplc24rSGdNaUp1YmowbWNqOUp4OVJkUWJZbgovcHIzVWVsakV5Um9vQkozTVNxU01uRG1lMmN3OC8wOHdWYlRiODdIV3l0WVRwVVFIT1JwS2EvbHdCWDZoaXBKCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW0rQ29HNmwvejNqN2syTmFJYlYKU1kvRHN6RjhJVDl3eU5adTI1cGx6NCtVWlczRmV0amNsV3lSc0RFZ2hURklUSm9tRGdZN0wrQk51RmM3eW45NwpkSXJMTFpsZDFGL0puRGpvUXpyYk84a2FPTFpPSjdpZ29ESTNGRU9EdkZPeitiSkdULzFsbXZ0K1lNbVdPWWp1CnBQd1dwQ0sxaGJLRFhxWUtjWm1XZWNWNjk2aTNjRkVHT1gwRURIQ3pKZEVJMUIxQ0RrQkFleVFqbHlzczhkY00KYnA4YTZWTmxMaXovSTc3VkR4bDJaRVdLY3QzNzN2MFdvaVFCS2xKTVI3cnFsamhHRy9qcWZMQ0JyajlweHNnNgpDaXo0THR5SVRaWTN5Z0tQN1E0YVdldHZKZ3pIa2FPUW43SlZtWHhtbUorWi9uc2xGVkhPYWNPVjU5NGFVMEN2CmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelFPaGdkQjBqQW1LMmNSV2N4SHoKZnVBZ0VHd1VMcXBKWUFGWlgzRDRqemt5YjZ3bG1lTTRGK1k0dGNKdDVyeXBIKzZjRC9FNlgvTCtRSmlpRy83SgpIUHI5NzIzUTZOeDNnRlI2cTVOUllQQ3dWV1R0dVg0OTNKblFCcEZLVTY2aXo3UDhBcEorY0Y0K01OaEFKTjZSCmt0cnZ1dStpV2NWQjNoaFpLV1BWNHBQbjhvTnlDYUNKWllVV0ppSnlEeGlRTiszTzdLS3ZJaVcxRVJnNitIckgKckZMTUxrRXpTWnZ2NEIyZWtGakVyT3VvMlZ0VUp6dGFObG1tU0pKR0Z1KzF4Y1V3T0JYL3kwRitWQkNWN1lkVwpLZVMzbDFrRlZReTZibEtTMWRFKzNMV3d0RDY5QWxraWxwY0JaeEdCTDBHcTlCdkMzdFdNam14Q0ZlVjZJSjRvCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcm5jV3RyK2x3bUtUVi91anQybmEKZWtRdVFPZytxN1o2N0pTTWozZlMxYXozY3dOT2VNOWkxWEdhZEwrVXlBZFdJaDhSL1JKZXpPV3hsK1p0ZXZteApTRm9YZzI0SUE4cTJrLzEvOXY1b00xbDZheVg5R0FkWDU4THBZQmRQWHJrMWpYY3Q2ZDRaZitIOEVVallyT2d3CmpKQUdPVlh1cjIwc1dtRXEwZzhOUWVHQ0NOcjA0bm9iL1p4STdqYUFJeU55eFBDeWVQUDd3SWJhaVNXSG5lSU0KbGVRUVRCR3BNR3haOWl6dHBlT0o0azZRcUZ2L3hlb3FFTEZ6TER6LzJYSCtsbCtRVTNDVWc0bVh0SHFQd0RKMQptMHRjeFlIMmYrMVQ2d0RweTNXaG9lemt1YzFCTU5GTzZsNXhGV09VYnA3Z3lNTXNsMmtZVzA0R0dDM2lZcDFZCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczJMdlZyS1JWN2Q5VUY5emRqOU0KUUZRNGlZTkZzMTF0ckRQZ0VEYVhoSTdvUzFwYm9IVVZzRENaWUpYWnJwUGp6OFRwZDIvRWNNdDdHT0tUV3FDRwpaUWF2QlJhdXExaEEwUlFwcHVDWUpZd3hVWkJjbi9nMENUUHIzQkNGa3pjS1M2R09CNU9rbnZXc3BkM29kNUNZCm5nbHNuZUtud1dUd2IvRU1LTExzK04xOHBYaWhBNGc3K3QvTm50VWovOFdxSU5RRHZCd0hib3JDYUw0cEtqbG4KUUdFdjUrdll3QnQ1djlQNTFCSVVETXlpVm1obUhhMWVoNnp4TEFpL0NudWNsKzBieDhiRWF4MzNvQ3hXdmJPdgpMc1ZseGU1cmdBZGFSNDkzTDJ1UE11UVRUdGhMSndhakl2MVVwSnNsQzMrcTdKVXRKM1ZlaHJQU3BCMEhJY3p6Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkZZYm9vN2J6K1orVitQMUl2VnEKQXdtZFBHYUYyOFZlUFFPMW82WXFKWlJQOVZpZEVvVEp3SVdXaFBhamx3N0xKWmE1M2NKUnFCYmhzQm1TbXRvRAp1NE1hQjJYczlTK2I5YVFGdWxXc0ZHRjlLVElOcTJhZU1zTVk1TVVuODhmd3lPT1RORlpRYk9ObmJSSFNFZ3lnCjdneG9Udy9leDBQQklndmU2SlkxdHc2aitSMlRBamhjdlVSTTZsWkR2ejN5SCs1TU80QjIxYmlaYUpObE15ZE0KR2dEOE1SMzBWOVU1aGg1c05wamZGSnZTa1d4VGI1cGJUaDh6RVhoc1ZtT3psWDF2cnd0MWJENG5sK1FpQml6QQpHU0N0WU81NWJoQzJJblphTG9rSWFUbUJIWWl4NWdNKzI3NmVJZ3VUSk5VRFNTcmc3bVZWa1d3Tkwrd3NoaUxBCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW1iSGJUNGEyb2xoL0s0dDNIS1oKQlNTL09yMHBEQmdiK2RUYzFIYXFINUZWYzRqaWtneElwNXpUUm10Z1FsMDk3Um5DOVQ2eVZEcThsakMyK3NubQpoOElKdjl3Nm9Da2I1N3kwS21KalJWY2tJa1BwbEg1YmtqQVRBTFdYZ2NrWHVSUjRVcDdIZWtXZ1R6QnBURUxQCmczS2JHcURhYmhhR2wycUg1VDBySktKK3M1dUxJdFRNcVhQcGcvNGx3NTk5THh1UlJzeStiVEh5QWdkYnRWbzYKa3dCMFFqREw4VXJhNXlvSytqY3FqUWJxRmxSMXdCbkdzejFFMW9acWt1NVhFVEF1Y0hXc1JGcW8xVWtjV28ySgpNd2crbnY1QjZPSmVHa1VYUU1iREZXT1prVk9pcDJleTd0bEdvSTFHZTh5WTMvd0RYKzhKUXBFSm1QY3o2amJRCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWxJZ2kxVDV6NHg0bVpOelVsNEsKVW5sbG9FRmlnRWxDaG94ODh2NFJRSklSMWwvbTdFUXZmdmNsbk5FeDdtbHhxcFpIMXRaL0xsbkxZbjRpcWd3RwpMQ0pKY1lwZGY4SnJLVGVjMVBqU0ZFTTJRWWI2MUpCQzlnRnZCRWZJVDdBdm4ybTVlVUdHK3RSZWdBSkdZbExpCjFxWXNTaEVTdkNNRmhNRDdFeU5pTEhpK05Wbm9TekNMem9hemhMdkNvNmRSVFNGZDVCNXVjT1RuMDN6TjFtdmwKUm9UaXRoTXBPNHJOSTBoVVpaZEpUZ29IdnFUYWVrMVl5cFFOaXJkODNnUk1PTTlRR2swaFNJN2FkTHBUVHFUVgpWRzhvZDFja2Z2SEwrcHJIT01uQWM2dis3TkNxUWdudG0xWVJJZ3dOWEEwUytPVzNFZkNra21DYUUya1RHVFFDCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1pWTUNQNGZrb0JHNWtEUXcxVGUKVG5xRXFLSWdNVWdGRFJsUWFkbXFzL0RlczNXTllPNDlNb0p1REJzb21kcGZQa1lUNFQ5aWhGajZJMnVWQXFKMApOc0t6TFZaTlpPNG1xakczRWEvalcxUlh4dmlZVVI5RWpJNFdWeUI1SmNocWViMVFrMytITUF0cEFKYXFwcUowCmoxbkdqVVBKVmRNNkVwME54UjFSajQ2UFZsQ3A5WnZzWHZBcXluMWVRSzcvWGlxRTZxcDcxR2tYUnY3Wjl2T3EKZG52WUVHTzk1Z3JhV0RIZ0RFZjZHaE52NDV2bWFIcWxOODdjVzN4bHI4YVo4aXZyUlYwVnlkekZ0L3hKS1dhbApYQzcxcXhpS3k5ajBicUI3V3R2anRxTlo0akMzS09VSmpMS21PYjNpN2NSWVBiM0pCZnd1ZWI3U3NrMTJsbVFECmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzJ1QWdoSUQwVFNpaUJzRnMxQ0YKdW4zZ1BvYmRKWUtjNVlJZ2o3YXF1cnFQQTVPTzVoVXllSlFsNWtFeHBOeVR4Q1gvcTB5em1EK1FBWmt4T0pFcwo1TkxNcW9RZ1JJVldhREk5dTdoRWticklyeHFaT0xlQ29KUW5MQ2E2TmJJMlB2R041ZWExRnVSUXZhVDBqRzRYCnRNbDQwVmJHend3Yno5ZTYwc3dRN2ZmRVcyZzJPN2ozZnF0eXhGWGkwRHJYbVlqTUdydWNGc1NzZnY5NEtOUkYKczUyd3kydTRvbTBobTRFeEdpN0dseG1RVnpqUGY4S0o4c0dtcDlmNUhnZWpQVnhJNVRyT3AvVDI5U0xSam52eQpEQXo4dFRoV0JVem54bysrVmlSQ25vSXJjOWNDMk8vZ3RaOE9kRmF2TUFqdnp2M2phZWVZWVZHcHM5cHlweUJDCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNU9pdmgwQm9vT2xtU09xbHFGdkQKQ3phd3Yxa0NhbnVXYjBsSGh6OUo4dlVGK1NQM3htalpiUlpUSk5qM21rdWFPYytiUnRmdEQ2UjJBeTRiUkFLcQoxam5CeC9ZVzFkTVd2V0JOQTM0MEtiNG1WTEJvRUYyTDFidm1LemptYXUxQ3pjVUtoNVJ1U1p5UyswcHRSMEZoCnAxeUdxREg0TVlNNC9Hckl5N2lRUTF3VTl6Z1UvRmFoU0FacTIzTjlWY2JmNTFlR2tJT0llSXBvN3JUendzY2wKZlNmRUpPZXE1WXJTM0wvYTVvSkxpd3FNcXYvMUpicVhPVnFhS1pwTWRReGxWdnRYZEpiVWJwaDFCVXlUWEhxdQo4ejdQUGhVZDFFbTh6TXQ0YmRtWjErazRqdndwQUpmVE1CRU9mWlFuRXV6YjhPUlA3Y2ZpLzZwdnJqMUVEd2JLCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNS9sZ2dwM3RKbUhIRk9XSDJ1ZUQKc2l0YUxNeU5ZcmxXVUV5SUE1UG5ERUgvRkZJZ1JpOWtCN1JPc0ZodTBXaDhOcjBBU3dXN0xnNlkzMnFHbGdnNgpwUEUyS3JIdERJR0p1eWxldHkyTnNhNFRZOGxyeHFTSU5mWVFYTEpYU0c0YVNkMmNVN1lHeUZNcFd0bFVUVHF3Cjd6UEdoNitRZ2JxNnJmSmFGNjZTWDhWcU1GWWRLL3lTWFRpTXhJR1A3aDRhZmZaNDMzZWY3UDI0MlhwY1ZYbVcKVDFxckhicUpzODJKRDZ1RUR1VzJnV2IyWld2OGk4Vm05SWxUYmNoMTRvd3hJcGdFTjVCUGt1dlYvZWx1a0t2ZwoyRFg2cVpqWlZydExxSWN6ZGFraGw4cFNWdDFuazE3UTFmSmw2aFpIVkY3WkJJcm02WmJjNzNHOUFqVWkzQkNNCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3lrUm1ObUJ1a0ZZeWg4TUJUc3UKbklOQXFtYXVYMFpJM1dUQTdwWkxERXBFcDRjdUpOQU9KWkJBQlNGNjV4bURyL2x0b0FYdVh3RHVWNHRTVEZLVApma0R6bU00NzBQY1FnbjdpS2huQ3lTODhoSUxlQWZhV0VlNGExZ0ErWEtZcVRXdk4rbE4yUVNTcTc5MUxORDdRClRnNEtDdmRxT0VYZTlreks5dXYvUzh6dlpCbUg1M2l0bTgrNGFod0lmOTRGaS9ONmlnczFJbUhSY0V6VGNuL1oKQ2xTZy8rd0wwMDhCYlVVS0FpRDRxaHQycVpvYUp4TzFpWU1xY3B2MTFCKzNrdlpUOTluK01KbHg1Rng0bnJxWApKajd1cnl6ZTNZbm1jZWo0SlgvQVVkTlhEYnBKU0xiN09jWEFYdDYzUWFpSGlDTzNhcGJzOTJlSGRyRDBhaTVyCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXlsemttenJXMEhCbmxxOWxIUXIKZTNJUURMTVRoVExwcWlCOWQ4RDlEM3hBTnJDczAxNzY0c05kNWRsVkg3ZHNoUHFaSUUvM21xbmZBVWRaaVRzcwpndEJ4eTFFZDBySDlBWWhXYlh6ZnRtQW9PUWwzeG1sRHNJeVdKQndLVGJwWE01dXJabmJnSURQUlEyNTRzMng5CjRjRWJ5MTZ6S3dpQlpTZXJiY2wwU2dHbzRFUkR6Nit6c1p5ZzloU1h6K1lYU2tUdzFUcjFUaFhNVGV2TysyT0cKSGJ2bHJzRkpQT0YrT0dFc0lVd3U2VHQvVVowRXl6elZEY1U3V1UrMmE2Ym5kRm1ad1lMQVFWWFRibXNCNW85VApsMG1sUUZ5T3VUTTRJUDYxWno3TlRBSHU1S2RSU2F0UGlNODI3SXRuMkNxR1l3MUFkWlFKbjNTazhOMllJRnB2CmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXNqZG9aSUgya0NpVE9Ea0ZEZ0IKalF5RWNqeXk4MW4zVTBOQ050ZjhrbVZEcVdUNlE4dEtGNWc1SFVQZ2JmZHMwSXY5QlVDY0lkUHh4c2t2Y0xZdApZd2tLM0N4L0psUkRPbHUyVllzMThqVGozb0t3WnRoMFVBWXg0a0pvdmYwY284UFZTRVhoVy9wWVhWZHUyVDhpCjE1eC9tRWdLVHlpd01tOENQL01FaWU0NHUzWkVLc3Njd2VkSUpEanUvTEpjQTB1V041NUp5SXBHZjlqUTZvZjgKdC9HV1kwZjdrNnpydklJNFB3aXlZckM0aEI2MWgrSlIxSnNKUXloWkFlZzBYZEFmWVFUa3ExMC9FS3VlbG54UwpSUUNxd2J2bVM2eHdaM1h2WGhpS0tNMmJOQTJ3TlVtdlhoQUJFV3FaSXZJL0tHbFl0WlB4dW0ySEhlSzdkYjNtCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXdBT2N4amJ0NVhmMFgxeUhrcVoKd3ZkN2NuYzYxcFVFRXlua1JyTjEvak1QVEFmR0VTTUhOZVp1SEEzM2VOd3N1bnRPVHllZlJuSjVGMEdrY09VdwpGR0F0TFFUQTlQUnNScko4Uy9rbWNjcWsvZnlMNTdleDdyWll4ZVhMaWd5eUVLL0d2OEgrZE83dFRxRko4Yk5qCkhrTEMraVh2aWFJRHdkTGVFZVNENjlVaWNCaHBvWDEza3ZLKzQxWU5kMk1HQ2dweUpaNGR2Lzk2ZXBpenFqWlUKeUZzalpoRWRuWG96bDAxdDdIQzZHa01wM1p6MzhIV2hRVkZDb1ZIYVF1L3FFbFpnTlV2QktjU1dHWlBWaDhRVwp1clFsNVRmdG5FRFdMVVZrWmlkWHFjSXd4QmRWUDExVXhxWWExZ1NNdGl2YTk0bFFkRWF3Qjg1YVc0TC96cjZqClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeC8xVFVNZnl3L1ZsdDI0UmIxRVIKVEdkTVBvRjdjWnU3ZlpRb3F4Z3Rsbk9OaWtvKzlzMm9CUStRamVrRWsrVXE0ZDJRSGkrbHNjb08rZjFibWJUcgpLT3RJUFp0b0VqbHlqeEU3YTY2Rm1zTEhidW9PRCt4QkliWm0zcEtja2tVcitEQlhlRGNaNDlEdzNzYmUrN1pyCmpBUUlGemlyVUNBRWZqa1VwZFBBenFRTTZ4aDFvSVFrQlBNV05qK0dhTksxdGN2d0NIdkRTcnpZc2RBUlpWTzMKYTZScXJoY1VHaDRVelYwYkppVFhnMFZqYnJVeWJ3Uk9QK1Z5OFVRbHdaWnpQTUphdTVsMSs0TnFFWGJTb1ArbQpuTXBkZkkxOU9RQTVYR0xuY2luOVFGVVBOWHJIWFpZa0Y3dEhnajBWOFNwSlJVOGpZNFA5VzZ1ZVIxWk9GSTBaCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGw1MmlmSmJlY0Z2MDdoSVM5TW4KVGhoQzR2YU82UTdXWi9lNitnZ3F0citERmtPSjljWXExUzJlcFN6YWxSMUpTd2VjaVBxRnV6UjhvSEo2bDhIeQpHeFBLWFlJKzNWYnp1Mmx0Zm9naG9ISjFiYWdWWmhuSXcwZTVNbFJsWGlMa1prZkxDaFhkYmFvancwMWZ6akpxCjBGaHJHaXF5STBNWDExRk41ZGE5WkJ6bk5ZK3ByemNDUmNLKzUySzQzaXhzcGdOVkdYbStCenp2R0YzM1FsRHIKL3dsbG1PaDc4eEE2MUQxdEJ2Qlg5Y1lhRzl5YXk4WWhXczJnQTU2a1FVdTFwUDMvaVZvTC96Z0paYlVxK2JoZAppcEFTaFZrMkk0aWxTVWR6K2dsWnY1U3lOaGdhWlVhTjZIcFQ2YXl2Tzh4eFczbFpVM1lIbEQrUVpXNHAvVTRxCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGVZVEtHWHdRUWFKUnRWYWtqNkEKVUQxZW1reXl4dG96L1ZncW14c2F0Qjd4UUw1bWVJTm9POUVBRmtMQ3p2VW91SFFNbTVLVHk3RS9UODVldm9pUgpqR1FqZnU2RUUzUFZyQythakh3VVVDODdDb2xMd2xCQmpEb21iUGoxUlVFZVJaVWpnOSs5bHc2RUFQc05JWngzCnNwZDQ1QWgxTlBlM3dtU0RyWG9oWFdjcklHUjJMNGVUY0kydmdJaGFNdUFqYmU5RGNHUEtvNHVlTEF2ZW54T2EKRXB1UXFDV2Vmdy9xNHNWQkdIZDJIU0IweWNTQU9lNHIwQlI5Z2MzT05GVmVaNlVWT0Vwb21PRHBwdXdEdEVaYQpqNTE2VStLZU5BZzFjdGJTMUV4bzJvNHlGNTNQMWp6aEFRd0cweWMyMG5JZ01oS29QazVYOENqejVLZmYxRjljCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHp1Tno5SThqT1UyUjUzWTNsYmwKTVJpUG5aM2U5Q1daU3JrQzhBT0FSRTV0ZktVWDlydWZSMTdiT01XQ2YwZklWQ3VxV1lNSGJLVHlXRHdYNGxQSgpzQmRYTC9kNFRtQUlvOGd6ZlJvNVhVdklJRzlPYlQrT3AydXFLaE41WTUrcTVlODcvdWxUcEVaeEtPRnNvUm1WCm5CWnN0SGZRR2VreGNFbkJWQlFLYzJmTzdJK2gycnFZclk4UVc3b1dsQVhKWk9FZlc3cjlLZGhOdUtIVlFDSkoKeU1wcm02cVB5RjNEbkN1R0NLd3JxZUpQdnBZUy93WmtYYWRmdjBwZmo2NWEwNFZYZTdMWlZZN0J3OXNOUkdaUQpGTnJOVnU1N21iOXdkRWYzK2hNT3FJcmw5enVwS0JocEdOMW9xV3A2WTdRb3doenBBdFVyRWhPUTVrTzZ0dkNQCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjNqdW9ud3BINi9mUFZFTS9LOVYKTkF3QWFtQVVUTXgyWXpVeW5iRzAxQ0xyM08xN3RHaGxDanF0K1dwQURBeTUyYkhlVW1YWkxydWx0aXd6ZkV0eQpxZUhCTmtieFNuUW1OWUZtenlnSXdrRWpUWC9SQmpES014RzBxU01nY1NjQ1hpWkFESmtQL0pBQ3F0MmdtanlpClRDSHdNcENCVGdtM2s3c2plVjFGQlpTbzNwMEJ4eDU2eGVqRTUwa3JtekRISjM1TStJUDJKYUFKMC9CY2p3RzUKUklmTjg2Yi8zT2RsNmRMTDRrM1Q2YmRaeEpBWlVCOXZvTnoxZW52RmwxVTJaUUlzUGJsZkpQczlmMzZJYllpSQpiVFVzUEp2cForNzNJQmNPYkp2cUdNSmhsbWJHcGlZdEZiakdQTkNvQzlZVDJha1V3MFRSVmIxaWF3MCtmdnUrClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2l6cGtqVDZKc2NXbnh1RFgxVDMKYzg5YTB0RFRmbmRpc3lkQ0pwKytkRFB3Yk9LK0s3aXFxSUp5VnFlNTFBWnJRZUJKUThnOEt2U0QzUkFtU2l4bgpKOWJVNkJDbGI5ZUJlSkN1dFRicktTVUU5ZHNNM0NzNlRSVm4vekEvd1NmSjQ2YmFTT2hnWGpEa1dSV3U2Qy9LClNKSTRWMW50cFNrN2w2NU9lcXJIdCs5TDJUNTdFazU5Z2c3MkkvZS8zK2gxNnRweTFtSmNQRjBHUG00K1RZTDEKTTlTMDNtYkJMQ0hGRlAyK0xRajMxVUt6QzdVWGsvVnZLODYwNmlOeXZvc0J6VzJFMFpxV3BHMUJWMkJGTHhqMgo3UHVoQ0tTZUcrejZGQXVVdFRVVi9hNGc3cFROYTF4KzREMGNEb1NqaU9acXBaN2YzWlFpdlhhMi9HL2Q5OHk4CnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0hVUkJKSzZ1M0VMeHlqMXhkMVkKWDF1WXpja1krVWFDQjRWb0dCRTdtQlpoMFJIaDgrcVduL0hORVU1QkdxbWNibXUxd3ZxUkRUVUR3ZE83cktIYgoxMklGVTVTZ2RIOG8vWFZRNC9VMFFtTHUzYXA4SkZvL3NwL0VvYUJGTXY2Y1hpSC9rUXJoSXFaM3psZzk0MWVUClNwZjVqVTNRVHdzMWxFVk5KbVlvZC9kSURKcFR2UDkvTGJtcHdMYklIaWhpTCt0RFJqWHRKT0tXSzlTbEU4bVIKZCtweU5mbmVlTmc2T01CcmtEby9qR0loUjZRZ3hRR25JbTRheTZxQUQ1MUcvbjhJWDBhQk1ucHZzZ29QekVTRApscEVRVFdvSFBVQXY5NURlVDNaUy9ZUkI2TVdHS1ZpUjJuRHJPd3R0ZDE4TjRmQ2ROVUc3VWNVRGJsSGpJU3RjClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbTRIWjNXd3VvSDlSdFZiYmRTQ0QKTklCbyswa0hINXNta2JvQ3hZeUdYWXBvY1I4RHFJdkxmZlRqNngvRStQSndOUW96S3ZheUVqZ0FGbjhqN2NKeApxTzZackhJMnFsd2NqV2dZZ25BWERRNUMyb2dwZ1BxRWZwQUVNT2sza3pOT0FnVjI1SnhKeXk1c3pENiszaGJQCllRcmRML3NqaGQ4dVJoSXh5ZTl0TWREVEk0NnMrZWM1SnYvRDQ1UjJEOXJjWWxYM3g2T0Y4Mlo5ckd5VVZwenMKbmtpOU05VSs0SzhLTW5zb3NiVlRYeWdDK3VTMExUbkVXTTRYR2tFVTVRNko1cFVNRUQwaE4zbjliM0VLQU5OQQpvSTN0OUtDcGxRQU0yN2FCckpncDRuS0VFTlR2dUE0UkRub2FGSnVVT1kxQUEzWENTb09jdW5LSXdESlFFZFVICkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczk5SVRvN3A2ZVhFbWFtaC9rQUoKRU5UNFlSSUJuRjNOZ1NTM3Y0Nk9FaU5MT2NEWlR2TUxHcFZpem5VWTUyalFyN3NwTEs0bG1KVTdVR1RLSkVPRwpVaHVlUXlUU3QzTit4VjB3dTR1VlB2S0tGZnlQcXcrUWZjWlAvOWpGQzNWN2poWitQejdidWpzSHlXUHpGajVhCnk2SGRJTnNwNHZ1aFZTeXdQdzhGalRCYnRNdzQ4ZmlGUk5zRTRrRGNtdXpOUmVWUjZWd3Q3MUU0SUJucmh5R1gKNEw0bityRVhRUmpoUXV5eHo5NFI4Z0ZEQjV4MmlaTG8yQ2NrdTdaY0xFa2o2TzdDNmloeDRBeHpGR1VCTW9ReQpJYXhhQmQyMHN5SWZXMTZDajhtNTZlUFdTWlh1VVljK0JMeENPUjVFa0N3aEJ0VlRUSmp1SSs5QU1OSkRaVEtPCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlJTK3YwVHo4bHNUZG5pZUltSXQKcXluam1YWnNoNTRDcm1jejRZN2hSSlQ2UHhLcVVsS3VJeVpsbWRST1Y5c1hENkpxemUrZk5scHRTQnFmYW5XUApLdmlIeFZuL0pXK2VqVDUwalVCcFlOMDJ2aUppVWgxTmhZdFBVVXBuaXFXdnIzSUg3WWJPb3YvVGJkRWFVSXJQCjF2N0JrOGZxNFFTaWlQNTNISXFoeVFKamFQcFkvSWorb3JwQkYra1BaNkdhMHRHVXpNYVd5R3VHUmJMRWZ0bTQKYzVxTUErU0w2Z0dtcDJ2YjNOQ1pMQzVrdHdweCtiL3M3VFQzZnJDTFd4ald5UmgzTUZMcDg0UTAwenk1dFpkbAphUFhHbCs3VmR3cVBvZkM1QXlVbXp6eDFjQUtLWmpya1pJUlZnNHVyamNTbWRZclpqVXRUbkNpSXYyTmc1ZnRtClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3p5WjhFK1lDdDVScFN0MGpWbFIKUm0yWm5xckhEc2hadTJiUWJtcE5NRkt5c3JSWTFmR3kxbktHVG9NQk5EVFF1YVF0Y01MczVwcXdRK3QvQ3E0ego5Rmp6WGRYR3VxdjlDc0N2UUlLNm1QN3pIRmZ3dmpKZlFLSXFkK25RTnp1Vk1ZRTZ0S05FcGpGVzRSeVMrYUxxCjZvT0FtdDd2eHoxdm43VFVXNUE0VjNxZVJOTE5yb0RETkQzdGVrSDVOcXZ3RllmcnB5dnJLRkxnMngxM2hrQ1cKTWxQU05iTTNJTTdlRzN2RHpMaDU0aDJRQTVKdFdhUkhsMTgrWXNvZVRWdmR1ZXQzNjEzdzlJV3hmaUFRdWhqegovWnAxMjIrQUcySTEycGFrZERHaWk5Vm9ER0sxQys5RUhRa1VzcUV4aFIvTUg0YUdoM1ZCNm51blJvR25JSVFSCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2hJSFdZQXo1K2ZodlJZUGFiOGgKa3JlZmFJR3A1V0pWZGtCbG5tdHNyWFJTYjdVL3ZjWExIN2xyNEttdWYwelVvZU1Cb0hObXZvYnZsazB4WHNxbQo2Rm5DdTVLaXlPNWFUUWM5YUhHcXVBcE1wTWVXTEhGUkNYTENYemhxWDdhTEtUSitwZDJsTTVtLzdHNW81NUVPCkFVa2J3dkhBa1JWWkloVmNIeFQ3aVBsczF2cHRmSHpSV2tnUWNlTVFOS0x2d3JzZFBHSHNoT1dVTUprbWdIMjgKKzB2Mis1aW42dCtvRmlGalJGa1QyNmJGZTZkUWRrV1RPSUFYRExTSnFtM3NSa0IyZGREQWpFZEFXRjBucExBWQpVRkxydkszb1czdXRmNnBEYXhKc04zM3dmRXBVR2trTVN6OGJvWXdYZDJXMHVZWEl2YkxrTFBiOFdOZ2d1ZVgrCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEpCK2V6alFqeGl3VjB5S1dpU0cKTFhGZHdFSHJXSUNKL3c5aXdhUEpEWHRQanBWOUVMV2dJREludDA3ZkFpQmEwTGppUDhVaDlySFVnQVYvSng4TApvZlVqWEtxRE4wL1NlNURzOFFaVHIvMUtlRUpKUzFjMUJrNy90SzdyYllGRnhneERzNEg4Uit1VnROaVhqQkVpCkJoemZKWk1MN3dIcmdDYU1UT1BMOGdqbFAyQzg3cXFqWklLR3dKZTRDZ09Xak9Vdi90TmZ4aUZXbkN6M1BTYXYKNFJoQktnNnR2Z0J2RU9GZk90M0pCMjg1c1pWTENRMDM1c3l0YmgrbStDb3k5MTdaUDV5dTlkcGF4YzRIU2F5TApldHlTcnUxamZuZThpbWltVW5uRXhUZy9hakNxTXphV3UycGJDeXlrV1I2SkE5VjBJSmIwS3BuRWRTRWsyYTNRCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEkvMG5hL09YbERwVU1yRjNmZlIKSCt5SlM3UFI5SkZZcWx0TTYxVkluN0YwUWMyWkFXMGc4TlFxdU1HOGdSVnV3RmZqSGlyRys4ZWw4OHVEWmNaQgpGNFVYdElnZy9NV2xnQm44cUVDOGNxSGExeTdhQ1hPOW5HRStuLzBUQ1VpLzY3a1h4TFdUMFBDNTdDdDl4cFdSClhaUWJBVC9US1h1aGFDYWppS2pwR1loakZNWlcxOFJIbEowOVlQUHhMMXd4V0hyWTUySHlsaTc3Znp4eE91ME4KSkhjV1A0U1FKaldRQ3Z1YTR6RnRxblRWaVJ1RG45Rm5Za1l0WWJxaWxKbnczem83amdVWndwdU1taFFpUCtCNwpPRUZQSHpNZS9FQzMrWHFyN2ZMNlp2YzhyWEhHYW9XY24wN3QrN1ZLNFpCV25KL2k4aHBnS3BoajFqODMzamFqCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVdzb1lDUC9wZStOM3NVcVg2d0IKSlRUcXdOOXA5dlNzQkhnZjhMTkx6R1hCMG1iRFRnYmtzWXlJYmNhZ2M5R3Zjang3UzY4anNNUk1XVXQ1Q2NzNQpITS9CclZ4S0pqVGR0Q2dYQ1JXZ0VFNG9GZ3JEVG81Y0VZdWM3RFI0dzQzTVpQVnQyOHRFZWcyNkVxTFJiOVAwCmdia0VGT3BtRys0R0ZtUnRMYlE2SDY1TS9TWlVOZDZZTzkvYWw0bzhrZHdGYTRONVNiWXNTdm13anZvUUVCMGMKV1lmUjZDbWdlVHM2elZPRmZjamNjL250N0VIMkY3K3hBUnU5NDNzVUI4bWdoS1M5WFpjTzUwK0gxUDhkZUxCVgpUZzRWdjdJaWxMWXVDUUdHSFNPcGl4azNKaVlvNFl3b2dFclBLMStXVUZOL1FtdVpPUkV1T3JHZ1dJYmorVTJsCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWwzVms5czVBR2pyVFVMOHp1QWMKaCsrTzdLczBvQkI2YzVPK0djaFlGRlcrQmlpK3pHMFpWd2Zrb3oweE5uVmpXdU9CdVFEVGdvUmRsZlc0L3UwKwprTzVQMUN0alM1dEJWZ09IbTVObFVxRUdQajR3ckNWRkVuUDJvODl5R2c3TTVGUyttd2lrYnFGWTFNQ0lFSnBoCjJEaEhOSmI1dXFKSEdYRXQwMFZ4WEhpVHB0ekZaYmxaTnZ1azNrdlF1dHRqSWdoZGhyZlY4Z0pTbVVTYXMyeEwKdUxxRDQ5Y3lpQ3FXUEJBUzJXL3pLM2ZNZGtoZkxFZlFjZ0RtbXhpdGppVGhHK3lmZkY2YVdEUlJRUGN3YnVjMAowMWllazBONVNqSmV2ZVRrakdsVlFSS0RDbTM0Y2EwVzN3alprUzlpRFg2ZHNBVjJaU1BscnVVMXpSU1pkcEQwCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEJSMU5kWmJyMUt5ZzNET3UybzYKczlFeHJLRmtOeThaNEI1VkcxZjF3bkFLU08zUWNsM0l4emhtTGZrbW9DUGsra2lGcS9weDJxWFFiOXdsbUFXSwp5RzJSMU9sTERWcjJZaDh4c0xxQ0ZIK3FTNk81anNUa0JxdWpaUURRdzJNWHBhcWJJNk4walloVm5EUTZ3NzBUCmk0bW90UlA4TFlYaE01OFNnMWtCTVNIVmZUZ0gxc2NGU25qa0VGL1RRdW1BeWdiRU8vcnA0UUtHK2MyQUtHbTgKQndERnBXU0s2aEZESE1uY3VNSFlqa09TdnU5NGRLbEI2dHhlRXZ2NjRzcVZyR2F0SVdxQy9kNDJYQ2JNSzJGVgpyT0hBTWVvbXBWbHFFNEJyWTBwd2M0Wk40Wlo5SHJTaEhLOXdVR1lnTVNUcGtjVGk5VVJSME9BVys5NzJEdjFZCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUFuVmZEUWZiVlZYTEg1N1FWR3QKQ1pRNGlqdUtNNzMrWGpXQmJCNC9UWnZPQVVnbTF6Z3RqWm5jTmEwOXRnZVlaMGJvdVFISXpFRjBEWTZHTVRJTApHSDlzRVpHc2QrR24xQWZmZUtuU2J2RWgvVWpNWFZURHFPVmticGs1UlNzanZIQ3VmanRKQXdRcnphMnhUVEcwCnJseVVld1pDNEdtUXZ1QVJ3S3h2K0h1RVBOaVpPWHZJTjdrZzliSEh3SUsxcHVLd2dqdGNFd0lya1RCejR5VXgKNFQwWjd3M08rQlVacks2cm94VGRPRldVQktteERwOFM4dit4MXJxUExTbERqbHRtSUY1TWd6UHBCTjVjUXdrOApuTm1EcmpPNythODhrcXNrK3V5eVgrbXdZM1FsVXBCenJkU2JLS05EZGdtVHgxUloycEthUnYwVmdWUVJLY2VBCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHpyUU1nMVdma1hJeU1IYmNvaXAKTG0xd0g1dXUvRWgyanZ0TXhaMmM4Ris5Z2w0UVZjcHM4N3RQdVFMK3VIYnpSMFg3TUdMSUd4Vm42ZzZnQXJRZQo4cW1DeksxVkJ3SWxHRmkwVk1lS1pacFpzVTFFa0RqYWlscUVSNjhzRGdxOGFpVTRSM2s1WkswcVdTSzhXK2tTCnR0MUxYSmRXQTBCeWVOWlk3TWRaVTg3QUEwRXM0d1dDbEpJelZrUmd2N2twRW9PUTZCNzM4WlBSVHJvZ1pRVmEKdjlVbDkxUmlXckx2cGo0aE9lY3pYYUdYZC80REloN3VvSjlyOHlRNVFxaGIvMnlKZkNSRC9NVVpFWGNrTnduUAp0WmRYVGdMbGhhVURWeG9ZVjJXOUVLMzlxNWNXcnhvMzM3bFh1Qlk4dHYraHpIUmhxL09BajNGVkJvdG1NU25kClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEZjZUtuRW9pZ1ExWlpHeVJWc3cKNGtUSmZNU2NQcUJXOThtWHcyb043Uy84VWl1RnlEY1FBbkgyUVMwaFdPNzBZUllSeWUyV29jYnJaL0x1WjdSVQo1SmM3Tzhrb2JUSlRLNURQQ2ZMcHdkMm9DR0ZtdDl0OUFsKzBHSVVOcDhZUTVvNVRkMnRERmNVdGJqeldMNk5DCjFJNDRyL0VSdmszZ0VSUjBvYlBzWnVaUElIWldNTE8yL1l1WDdneUtHT3BhR0s0MWlvS1pMMEI1VTJDcGtqdHQKb3ZvVldUb0V3YktlTER5QlR5dW1ka2RMU3UwTmcyd2c2Uk5rek9Nem9CTTEyeHZ5bUNOR3ZZOXRxT1VReC96QgpVaWhYUHdJeVl1M0pLSS9IRjdnQlpvTUlPNStHbFJ4ZEt0eFptenlaSGpGck1NUlIvQUgxTEEyd3Q4NElmemk3Cnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVdGaWRIcGVCcHU5b1FxNG9HTkYKUHMycXE0K3lhWSswcG1sUDBMM00zWDVvcEd1dWF6MmlyczZ0Z3YyZVJkRzJhYWIxeHRuVCtqYVI0VXI4YWZsZgprc3htMFRVdk5XbVcrOFVhNWV4Q3VMWHU3cTdJYUtiV1MxZGlYNnY2N05TUW5rMXVuK05qSFl4eDBSZnZwMWVFCkI1bENCVXR1UFRxOWU0UVBGaFRDMjJSOXhtM056MStYemhJK1poNUk0YXZDQ0ZZZWhEbGRFSVNyUVI3V1FDQlMKRWhNMmhUMUNHcVVhb2piOEJUck9oZ1JEQXdqRFZKTVVtaWJ3NkpjZzR2OUJQaE4vRmpuelFMV3BRVFlFZ3oyKwpia1UxUS9tYlBEQVloRlBuL1JrdWRxVHozVm45eFBJZUZ3THlGWHltWCsvZUlKYkxkRmtveWowdkFrVWtpSmVrCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDhzWEFsOGhDc1hCMDZITkRoUUcKNCthMHViZWt6SUI3NVY4ZVF3Rm1jSkhFMERHUys2L3JlVlo2Y1c5VmNqK094bVQzejRicmhNZlBMT2tHaWwwSgpveDMzelRNOWxIQ2dFaGR6VkdML2dYelZtRXR4T2VlM0Z6d0sxUGdWTXNhYjlpWXkwK0VSTXFUbzQyMjkzall6CmlFNVIrY0VYNVZtc3pYMjg1dFRkSkxjZTRtYnZSdDlTSVNVdXZadU56cmgwZFFrN0NpN09tSWprVVExWEJ6aDgKa2hPdE9jcDQ3MjU4ZFNZc3hENmFPZ2UxUlJTdUU1RnoxdHpGSXhnRkNnek1PL3FwMDR1RVhnWDBPUmNBaFNiSgpuSE5VVXRIQnNVNEUxaldFeTNxZUpnUVpINVl1TGZhaE81YXBYWUxORGlJVkFIdm9JSTZDTGtmVUtacFVNbWUzCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzQ4czlLU3cxZHpnaCtLTjJVWjgKWmNaLzFKdHk0c3lQTzQxMkI5OHRoN1RFbC9nYTJTekRKeHd4OEFlOGJpMEYzZGI4YVowUGxsaWd1K3VxRE5NUQpoV2FnZm41T1VsRStBZCtaY3VLbnoxOGpJbU9oUUJ1WHFCcDFOdnAvbWR3TGNidGg3a0NHRGxmMllMekxLcTJuClc3NXU1YU83aFBienRPaHZLV2FDMGlCRXFrMFhiT2t3QzFuS1dFOEZrdnZLSE0rSTRLdFJ5L2tDT3NVMktZNFIKTVhxYkIyVlZiemhueVBJUkUxZm5hbUFnRllpVzR4bE9MZU9NU1k0WHZXZ1FuRXgrV3RsalpjS2pTOXA0eHdDZApYMjdWSFhCKzMzWUtybGgvWjB0VkNJVStVcDM4Yjk2RVFzSnVQdHpmZDJVa1Ntc2RoVjAxbHZ1eDVlODhHTUhtCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlNUcjNUOTBNNVBJWkNFOUY3blYKY3Z6YU55OTB4eW91QVB4R3I3TFh5WW5WRWNRdSt1Q1gzemtEWjhyWTRYRHVPMkpWc2NLTC9RK05qRXRxS2c5YQpSNXZyZEVTeS92d1BrL2tDYVA0OURMaUI3SFNSSkdFSGs3QmlXQzlRUjNrWmNUd1hFOWQzMGxBUldYTTc2ZTdJCm9OQ29OQ0xXUytNMmh4eEhzVUZKWW05YWc3MTdRa0hCeW93cWRUV00wbDRkMGZCSTlyZWxUVkFJTjhOaWRlNjkKU2diT2Y3OWYybVdVY1JzSWc4UWMrdHIveFN3cG4rRVJDTXF3VWk5Lyt2NHJPendtMklmdDZFcDkrOGJuclNhQgpHUmIxSEhaSGdrQUpDYzM5N05lbVJlZlloL0Ewa1RGbFJERFdpVit5Vlp6UDNUZllWbFhhaGZQU0NvblRwbG1TCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3RVMFdaREdPbnBxK1VqVnREVHcKajZUV3pvRTNWekE1Z0pkRTBpY2hJV3FZNW55d05BK3JTZnFrUVVUTUU0cTR2VEVGYloxankxWjRpdTlQRnVLLwo1M2lhcm5KY2cwL2Z1SWZjcHh5ait6MnJVcE8wVHk0ZDk0UjNtOWRHV0l0YXNqSUN1VmFCZE5DREp3VXBSZk9PCitla3hEUFlXN2ZnYXlhYms0Wnkxc3p2c1NIOENPZUhkZi96VDVRUEFPSDNYRGp1dUZUSkpKN25IeG9iTWpXdFYKa0JzZUFONWU0UC9mRjZPQ0Y5UHhlckgrN2VnVjdhZDUvTDJKc3VFOVF2U1ZXK2N3SDBxb3VTUVcrNHpxSnBxUApEQnFLTUlneEMzYkk2UnEwN0VUT0RmTU9ZeDExcWRKZ2ovYU5abkl2K1NFM0ZPN3J0eFA4ZjM3ZE8yRnlSMlZLCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG5CTGJuQ2pLQ1E4SUpRWUQwUWkKNEVDNGtncmE0c0V4TVRoVjdwQkgvU2ZXUmtqTFE2VWJ5VG1NOEZJdGVJTjhkNWNOd1RNNUo2cENFOEhQVC9tRgpyOTJoWHVQSUwzd3JQWmVrOU1ZZTBrbWR5VUtDZEI4NFJWd01mSEFUcVQ4RXNhVFVXNGg1VmNXdTFHb0pkQThNCmtPclBXd0RtcGJOU2x2aDZ0ZjNPdkp0dFd1TFBkeUxhd05tQytRYytHemIrQmxDU1lYVEt0eW42RTNRQVpNcFoKdHVoV1UzeGtKaFAwc1lZTUdsWjJoRytINTQ0U2RPZCs3NHhqSXNBVmh2SjY5L0lKbmkzOXZZOU50RWE0V0hTNQp3bjhnUnZpZE5kRVJnUVZyVlI5ZlgxeTgrVVhDQ3dCc2tSUURYQ1U5czdUZ3BiTHVwbTdEYVFqb1NyejREZXBVClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFErTXZqWnh6VGk1NGQwVVk5UWsKdzN2MU5pRWdEekhYQ2d1YWNUbktiNFNBeXo5bGJvUERyYWl5MExmbmt5aFlPUVhwcU1lKy8vYXM4ZzlRMktpUApnZU96aHlHRFNjNmhGRXNGVW9kZTBId0M3YlVJNTMzVktQRjI3QmhYb1AzcFJ2a0F2SVZqRlVTcTJuVFZXejIzClRFZ0orSHRmc3NmaWNicTIzY1BtdUhCS1JHL080cEsvTGZZTXFDZGxrNnlBczh4N2s1TWE2NmFwTDVuZEpjTy8KYVR2U3ZQaUdTcFdCWkZTSUViaTJ6S3hkVmNTNE5HTlpaNk5aNFVNVXZCOXg0K3oycjRHcXhiUitDUFVmQzZwVQp3YmtZd0pSaEpncEtxZ0VBeE11aE1yUFNVY1c3VEtXSGdwaDlGeTdDaVkzWmYyZFpmVjFtSlk0b09xTTA5aDN6Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEVpcnFNczV5clVJTzgrVUE0ZHYKVWZhUmw2RjNpajdkcVl2dkVtVk1ZSncvNjIwTnRQNm9rVklYR1ZDUklSOVJHcW5CMTNsVHJQT2ZJWXIvM3JhWQpsTmxhNTJiT0pOaHFCYVRxakVFV01ZZzZxVk1manhwWWkySjNaTXorcWg1TDk0S2tmZXpMRk00ZzZlcjAvZ0IvCjNWeWZ3VU9hSERpRTc2R05OamtwMEFyZEJjRlVoRHJ4aHJYaGRuSlk2MnVpaXFveE5LemdsdWhHS0F6OXFVWVAKRjNRdzZZM3g3N013YjV0M21uUmFicTMxdnZzbWlTdmU0dm95cE5EdTcrZmhYWFpHdk9iemFpQWFNSG84Nm9XRgpQa2dUY2dwWWxhTm5hQ2x3V0FQai9HTkpWa3ZWME1yWFIyOGgvWjdFV2drcGVRbW1mSzQ1aGRLekFocG1YbitGCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1IwUHVuMVl4U24wTVBuSm9pOFIKaFQ2V0hwSk83MHV0Z1ZsRlBYWk0xUjFUU2p6WmJOUXZVd2lMbWhZZnVDa3Azanp3Z0thckVHMFNoaTFUSXJlMwo4ODBEbTY3aUxpbzNBV3BNcWUrVi9mQjRJd0NUQkk0OTU0VEhTT1g2R0YxZVVXaFZNK2tabXhqOENSUHRUZEdaCk42bEdhMzBsWmNSdWdsenlVc242OFhOTW4yT2JwaTVtZFJDSDYrMytodVUyelkxcyswM3ZLdXdkYWgwV1BUTnoKZ0FEbVFxRHBzNFVDSTJnVGtsNmZWTXpDUGxxYTByZTJ2amp1OG9HUTh3d0FzWCtTYnhBTGwyNmc3UmZBYUhEawo5Y0VORHdqaHJXTURTeXFlcm5ocHNsN0lQL0tjUU92SVNTcHRPUElMZUo4aEdyd2Y2Z2pDZjMwMkhkUFk1KzVJCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUY5Sk1CTUhPcVV4VnFjZ3h6ZUIKZFBZeXVhamk3K0lMaGphTE5zVHJlZ25xZ1ozbVlPTGRBajVjQUx3UzJhcS9HSTR2UWxUT1NubVBYV2RNbmVOUwpsNk9HM3VDckp5SG03N01qejJSa2NQVFF5OEUvM1o4NmRiY0JvZDZYbkFuYk9lc2d5Q0FKOWMxMW1PRm84UzRiCkYwUElQbjZSam82ZzV4Qk9uKzl2SURINW5CZ29HSFBHbEZrZEpLNHA5aHpreHJBT09SSVlhZzJoYUVZcUloNCsKbG5uR2NldnpYRVdmY0w3TzFCSU5DNVlQSG1HOHJSWVNmc2taams3a3MrVis3ZXk4cTRwc0JqV3pJTEdOY3dVVwpaWkxYZC9QUEpsamRIMkpiMGREYlluekNjWXlyWkZMUjEreHo0OHRYL3M4eEFqZmcwMWNFUXIrQTBuLzFkY3Y1CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3dpemdYUnl4R0F0TjVBUjZyRSsKTlM2cTNsWE5VUlZ6YXZBRCtWdmsrSVFGZ0c0UVRBVW1kM1UrMnl0enA1dEFKMUhGL28zdHFBYlQrVTh0amRwSApCaFlobFgvd3Jwbnl3UW15YnZ6OG82d3JhZFVWMmtsTmRBbE9Qb0lwUzFrMHk5aFZTQ29mUmpJTlZUcG9MbjBiCk4yODJaQkk2bE8xSk5hQzY3MHNGbzY0SzZKR0RSRzl3WnRXWVpVclQ4NW12dzF6N1BIZ0JDb0RlS0Ixa3VETFIKdFRQcEpHU3Z2YUpOUy96S0UweEc3enVmalFoQngxQ0Z6clVBNjNGbTBhMlFMT1VkckZWYnVvOWlYSUlwUVpmVApONXMvNXJ2ZklBc1B3bTBaNG1jRUxsSHZZNVFtWmZqNGFDYjQvMCtNRWwvazBSQlFRL1J0ckVXenhmSHJvWVB3Cmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmMxa29sa1p6YTVJYUhNUEJnUksKZm5HUkMrMzZHenFWay9WSVJQamwxY3dORjIrY21RZ0NEL0N1cTFIdDdwMW4rRm4velZVYStTb2RYODZpeTZ1TQoxVlpLZ2lPOG5Iem1PRDlVcS82M0ZNS29HanMybVJHYjMrOTVxOHNFVDNFQ1IxU2RFZXJYOG1oZGlqWm16cURICmdNMkRsZDd4NDVaM3R3cmllT0k5ejRkVGtES29OS2dZMUZHT0tzeFRHQ1FmL1h4bW5ocDdRNzVxQ1hIMFBsWlIKRmN2eWZSTnU1dXJaL3pBRk1CMmI4NWNTMnAzd0NieUVxaTVmbGgwOHluSjc1d0x3dnRkVGFadWl6aS84MGpZcwpUU3hPS1RlS0g1d3JQSUtwYkJwaVlqc2dDZUpHME1TaEhlbW1NWHcvQkZvUVFmR1VYZWI4SjBYT1A0QmpNMEtaCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelMxUFF0VHRTNitPNGVEYjhXZGcKNldpNnEzamo3bWtoMEl5S0lBKzhCek1XZERZclhrdmxIdFVuaWdSZzVZUXhTK2pweU5aRytsdWNaUGdpaGVwNApkaUNFWGpCcDZKSEczVy9EUWF3K0syN0hPY0xmbUZaZTdidDZ2STBPV2pwVWZCRU5TVDZzbkk1SVU5UnBnZVA5CldJQWMvUDJlVSs2UnJOWHEyQ3RyZkgxcWpmK2JMQjQya2pMQXVHK05MMEVsY0F4MDZRaEpwY1FLZHQ5dFUvaGgKZlVBbFk0dy9MOTYwN2J0b0dBdmttS1o2R0N4KzdBbEI4TVFqSCtHY2h6OFdDR1gvbHZScnZrNDNZeUZCRmdLQgpIOU1HdlZWaUZyeW0xMU93dlk5WkpSUjBDcFJUanhGbnVjVzlBRThQOTluMkV2eGh1NUxoTnErSFAxY2VjVnlwCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDZuck1rWlVzbmtUa01pM0pNVHEKbVZyUzM0a3A0VGtMNG9jaFEvYjgvNGZodXNwellxUnlhVUpHOWVsVlZwZjNQM0t1aFY3THFhVnZDZm13ZmFYegpPaTRqUTQ3ZGtrWk9WQlV3cENoVUlzN1Q0cTRndGVMem9jTHZrZzcvQkUzOXlVdzEvMlVwMEc3UEFtZWdrUGRaCmFKNFZQd044ZUh6SDlyWWpNRk4rTklvaGJpL3FPQ1B6R3M3NlRGaEVUODJXUXlmQlkrTldLR2JGYWYrK3IxeDMKcWxOcHYzY1plYTJQZk9JdUIyQ0I3TTA3WDJjZ3B1WThvWVFtb3ZnTEZSWGk0Y2hkOXRLbXE0bWozdnZXYkpWbwp0cVpqOG1OUlEraTBRaWZIWnk5TXFpcWhlaXVtR3F2K2VoNmEzTHlqSWR0cnp3MFpaT3hxQ2xXdTMwTDZUN3JYCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1VwK1BMTStOWFFtdXhsZnZDYzkKQTRuZXhaU2JoNmFzYXE4bDFReitjakdSSjMyQlREM1lmRnM3bFFRQ1FoSW1PUmM3UXZoalRyUXR1N2JhWnJTNgpSRkdOMHpqbURQSjdQKzBHR29nQkxERmp0N1pIMlVNS3hucStkOW10WVZ1STcrcm9QUUNLdHNJNDBuWHZ1Z09zCjU4YURKWVRmQ2N3K05EaFlGb0FLVHhReVNBTkQyTFRXTHByZG5WS0NkMXlWOHZqcEltY01iVlhQVzlsZFhaVlcKeWl5U20wV1Y4Skx2UkVxYS9CVVV2THJaelZtd0xxbWFtbjZwbWk3eFRuRE82U29CdGl5QkFmenZmelZtVGErcwpadjVsalptcWJOcUc1RkV6WWNRbTVjY20vdUVsakdsM1kzTkdMRFY3RUkyK29JZUhGNVNFWkpHcFFNVHpJQW13CjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNW1Bd3VHNEpRMHlWbGFCWWYzWC8KbzdmN2RESzdmUGRDV1FIcGhIMGxLRFRlUDg2RGFwWjM5QUNDNUxEcTI3WU82OHcvR3BuRk10bTN2K1J6WEJJeQp2aENPNE9KUnB1ZW1XblNteVB5RHJLSjRBMjdZb0JRcVRKUFQzSEZ2S0RIRnpUbnNQbnQ2SnJmREwyc2dpR2dTCnpHQjFyS1pEc044U0dJbGw5b1V6cHVDNUZnWWJJT2pxbWhoQkRFekVkSkFteEd1WkhhMS9GTEpzdklwZFF0V1UKaU94Y1lteHNpa2dXSjdveHBBWU1aaWtESHZrZW1aYTlQOVp5TWVkd25qSWl1RmpObDlXLzVheTRoVUhNTEQvMQpQRFlPaHd0Nlp6b2NDN0xLUWNtbzgybjROaDh4MG9oWG91QkFDaTFxSENVbkgvRHRSaExEcUNMYkZSdGZPV2RyCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFl4V1N1TDREM1NkWmcvbm5SMjkKRFlHYm5IZVljUkZVNzkvUGdvUExjYjB2K0dibXdQUERXMUxDK2dNZm9hR1VEQVBNVjRIUmtCL3ZlYys0NWc3ZQp0YW1tSjJ5TVhjWDR0c3IyNFY3U1lzSmcxQ05PT3Jqa2t2SnJYdDRHNGM1RXN1Zkkyd05KWXhTV2ZwS29EWkpPCmxrQ21uRktYd2tyckM4eW5mcWZWUnFqaXBDVUxvTUtQN3VPVE1BcWg3NnFjOTdJOHZoWEM0aGgvcFFkUjdYZm8KV3FrcDEyNDVwUWV0Yjd5YUxaVkFMUXlCLzhMUmcxSndjZkhMOVE5ZzZIcEhMUUw2aUJwY2ZRaWxaNWd0cElxegorcjI2S1p4cmExOEY3QndDdjM0Q0hjZWdKRDdNd010NitxNDVxcldxTXh5MnkyRlBNWng1MnJSVGkwVm90T2QvClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemY2OVc3VmFSUG5VYjlrSTdTOE4KMjllQUg0R2M4b2U1N2EvcVJzSE1XaVNqSDh4SjhIaDdHN2VjWVBZWU1XZFArUGIxaHk3aEtZa2M3NUpQUXhlbApwSGVtRVBXU011ZExVeVRkWjZOYXBHSkpBV08wanJxWXg0NWJIdStUcFRCc0xFUENqRXA5ZEN6MkdFRVl3SEZECmJHenBCQU96MFhsTHVVanVRejh6YWE1d0xiVkc2U0F3cENNZWpJQkRSSmMza3BKY0xwZDB5bXNQWUVsRUZVcWQKVW1NU2t2ckFhOXRydzVYN25FQ0tEUHI1TTZ2QzNRdzBJNzQzbGMxb09PU2xEUTlSL05Vc1JCaDUrZnQreXZQWApQZ2JWOXphNThUdjJtZkwyYVFRMkV4YWFrWUNSSFdUZE5GQmVBSUk0dTl0Rjhxd2t4bGIwS2tFelN1enpyNmliCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG1TdmEzVlFtSWl0eldpc2dEbGgKRkhGRkhQaTZyREpHQmRuY2NMS2E2UllTYzhFSzA2NUVXR1NmdEhzV3oyTHNBSS9CdmJLSkNFSDVoM1FoK2xjUgpJTHpaRFZyaGx6RnVSMjR0RmM0MGRxam9UR09iTm1lN3A3cU9jNzFZQ3J2RXJRODg3Uk11bmNZc3RjVk9zenFICmlOMnYzZTRmWFg5RFJ6ZnlodXlUWjc2MzhwVkpFSkJxNmhncFlsc3ZtUFF0WnlQaUNoL3JIWHRxL3RtcGRQZW0KSFJwcDVQaEVyS1lhWU1iL1dWY1R3bXZIK1kvL0loeENBQWVZZmJDTElFSGhLZ0VxQWplb0V4NzJZblRIT21hRApxZnlDa3RobWdOS25BU0dOUkkwN1VGaVpmbVloMGdjbGo4VTl5bTZvSGswZkJGcS9pdit0OGxmaHZXVlJ5T1AwClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDVFT2FlRk1BK2dWakVOczRUN3QKUHdLSk9nWVdwY3kvY1g0VDdZS3p4MzU0RUoyVTBOREg4VVNVb2F1N2haMnNFUlA4a3Y3dGVYQXAwajIwdEFwVgo4b2dYNWYvNFo4Qm9uL1RjU2VXbEhxQ1U4Tm9ScVhMZ1RJckNrbG1MTWhUZ3I1OU4zMTNVaGpXNmZ1RnJxeHFRCjZvSThSNkxOd0RPUytnYXNtMlFDYlY3WERad0pLaVdwcmtvTnMrYUNCOHpac1E1azJFUDc2eU5WMWtxWStaTkkKdnZHZHZLREZDblFMV0dBeHlSVlJsT0U1MWcveWwvbWpBRWYzRklNOUVKOE9lNTh0M1pqdCszUEgydXN0dHNkawo1UnQwMHhKSkMrcDE1QWVwbFZ3UlFUUldnZk9PamJWWlZvcXhyY1dSSExvZEdCQkJvazcvbUR5K3pUdFNOYnFCCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTRVdE1kOG05SUtWZ0hPWmNtMSsKSUtDU0tQTjlSaTJvN3Znb3IzWitHeDkrbTd5Mm9yeW9QOEdEa3FGaWs1NXE1RnREa01yc254U0I0S2ltYk9GcQpoaGcvcHAzbDB5NytWcTFXVE1tVHlHaVUvM0JLcWIwR2FKKzQ4SkV0WE1TeFM0RXFCUi95UU1BdGV5emhwVXc1CmNOZGxSYUZTODVRNXBWY0FCNTNxazdOYXlFRGNmVy82dFdIOXZsQlcrODZCNlJrNUJuMjdCdmtDcmdVMjE1dm4KU3MyTlVhUjNmSFhPMlBZaEFVRGdGcC9nN3NhQ2tiTU1zemVvWGNyam16MlFZWXJ1NzB6SUNjUTkzU0dnN0pNYQpHTDdNblNyOFByZi9OUEVZa2hYQ0Z0NlVQUklTcndXWXRXbkpvRzBtekMyMytqQmZZL0JPY0lPRC90dkpnWlBuCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUVmbzhXUXZnZFI5MTBkRWRDV0IKVFd3UGw5bEJoRlh2SkZOWkNJQmY3aFN4eFh0aTVQeUsxSE1ubXZoYjJDcUx2eFppZS9qV1UwYW83S1M5US9PSgo3dzQxK1o1VTV6czY2NFhRTVc3SkhtMmxQSXlBSW5vbGVGRWtvWHpSN2hxdlcvcDJiVXhFQ05vYWhhVksyWkpQCjEzTjVlVDFQb3VqdFJoRTFoa1p1dHV1V216K2VOV2p1ODFQd0JpTlZtY3hRckQ5Q1NBUzhGVUVnaTlXNHJTSWcKd0t3cVM0cW9QU1UrUkQ0bVJtMXNzc3ZWSWNWUTBGRDJ1QVNvNnl3ZE1KWFZYWjB4UkFTM21kT25sc2FxY3ViZQozbzRHVHRtVHJsL2JuUDFkMER0V1NobDk3S1A2WE9BUkU3OThBSVJ1dTdEZ3YydVNvQzBjcUNPbjBHNTlMWXJvCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTFpS0hkNnZ5dDJ2cmtyWU5uVU8KbFlFVHZLQ1c5b0lMZWo2aGZpR2xzWHloK0VLV3NxMjVXMlA1UEpmY2VEeWdVV1hPSmJxY3E2ek05Y0o5bDdndApZZW5sZjhWTmFKczNkVjN5OXcyTGcwbllMU2E5YTN0NGp1WDNINVRnT1RDR3Z3QW9JclpNR0dkZU9hL2NRQ1o4CjRTRmxGZUlUZUFtbmM1RWpsTkZBeGkxVUl5blZ6NXBtUXRub3VNYlZMeDRyR2psZ3NrVGFDNFlkeVZOZDBUSDYKWjFxTzVFNmdqK1lzb2htT1R6eTdHMUxwZENlYy8xUjgxbUZoUUdMd0VCOFFLWTRibmEvNk85eVJJNDZjdmpQNApPc1NJWWMxMUFkdXBXS0lFSHNyQlpnWC85dlZPOXorVDE3Ri9GYjViTWF5OXJ6eXphWEcrdEJuclp1dHJQQ1E0CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmNTNGw0dzVFUzRrT1Z2VG8vRGcKZjExQmF4cTh6Rktwb3JXbGk0amM2eXFOZlF5bUhjUnQwQ3JGV09OTlRUSkRSSC9aQ3I1azhCVFg2Mm5oUWlobApvMmRxeDFSelpjNFVlSmtHMllkMm8zYUM5MHY0VEtoQnRKQzZSVlJmWEx0SHErNmoxWEJyekNaeDRzaU9sN0FMCmEzVzlad0M5ZGl6TUM1bFVxcGdCSDFUYTh1TXk0RzhYWmtkR0M2SUkwNi9JZVRDeGx6aUtFcGc3R3kwUCtvQksKQVZmTW9kM3dJVkZ0b2N2RlE3Zm9iU0QyZW5Wd3ovL1pxU0hEemNYMFEwb0RITXVSalNxUkZEVW9BNjAvMWttTwowVE9LaytLV3ZZam8vWkZwYkZOOExyRXNkNWg4OSs3cGY3Tk9CWExQK2FJQi9ld1o2bVB2Q2ZyaG15MnUvVFVwCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkpXcW4yMXVjMnJjdVovVXFzdjIKNm5jeGcwaEY3R3JyR2pMcDRiVnlJYVhEV0pDVHdSQkRyTHRzS2M3Nnc1SndCU2VzZUFpOFQwNVg1R1ZRczN1dQprNitzM1FKVTI3aDF1UFFhT0ZtZUpERDhNZUpVcEw1bTBYMnYwV2lyWUdqdUNaNEgwQ2dMNUxDSTdIYU5Dd2dxClBDelJzZ0lFNXlqd0d1ci9INkprYnpiK0c2VGtVZTBIMzJndkhJSUFCOUdkTk1xcXZnc0VFQzZxVFU5OWYvdTIKQ25mWnNzSTVBZnoyMjdrYitqKzRpQnNjN0tWcXhwRGZZeFhSenNUZGJ6Uk1MMG80WGNYNXJIYjBpUUMwblJWbApIRjE0UmJkYUkvNlk3aUlkY2ZQV0JJK2hKRURCZTVDNHdLclJNSW51K2tMZndzWUtFbTY5WENPZnN6QlVOc2RUCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1crdFBXSCtjSnBvWGdhQVkxcnUKZk9yd2xNYmtoRGFRbDdrako3NlV4enlLMUNuVjYrL2F0V3RtclNwZ1piTTN2aXlCV1lxb0o4S0JLMi8wNHJhZwpwdXlhWTFnaEF5b3NxTGl3SDFYSEZ6aVpTUkxNUzBRUlArdURxYnowc1AvTU8xVE5ic1RUN2htNnBSdm1LSUQ2ClEyblE4cytGMFJxcVd3cHdjVDFSR3hyZ0pFMDFpOUY3bDZUd3g5MHhtTWt1N3BDMlJobWNjM2EvYWpROXlySm0KeFgyMXBMZEY5d29qbFRjRTdWNVR0d0JRb0NiSEI4ZjNFaGk1eXlLUzJtc0Q5c3hNN3g5aTFWdWdPb3d6d05IdgpNNlJaUFY3RmlrclN5Z2JMeTdSWi9VUjdtcHh3bi9zeWFqemI3K3Z3VzU0UitDRmRwVk1mcDVFYStCL0YyeHZJCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFhXbGZzQ0p6aTY1NlAxN2lUa2kKejZkay9jbTJkaGttWHJxZnRQWEt4VWg5RE9hTExidEZDYW5CTDV6NFlCZ0E1eStneVlQdFdtZTBDaHVvWmNBTApXV2owdUhKbWtuQWR0TjNBOTFUaXB3dkhPdy85QXVETVRDT3VCWTkvc0FxMHpsNlJQbHNOK0xGU0VhRW9BRUVECkhHQjFnVkQ2Z3kyRG5DL1dKeTBHdnBKZStua2xLTUwvTnVmeUQ4L1B4OEhmKzF2RTdmd0RzV0pUMVl0NzBNODQKRG5hZ3Axa1l4Z2lqMm00QnV5OEFoWHIvU1NtcU00OUVaUEQ3eVhmSVViMFdlOHM2bE9Pa3g4QUpsV3FWNUt2ZApwaE1CSDhoMXk2UDhCNVpsalNrVzhwVTcvbm5xTzh4aHNXSlJSeUJXcU5HM0xYKzM4SEUzdndsZEFDRWV4ck8yCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMURQbEdyOTFweXRKQ0NuTVNkWjAKRG50djh4S3RkdE1iNE9sT3k0ekt2OGVKVVZrMUhvYUNmSUNoWnRUWitrVGx1VThwOXFsMmN5NjFxS20rVkJESQo3SkFWK2dhVEdGTlRmbHNWMFJJUVY3UzNmQUw1UDVqVmhmdHhOMndnSjk4enBiZmV4aHFrSHhuWXJHNWpONGZjCktLV2x6ZkJldkVFTXdrVVBzWVdpS1VUZEU0cEloUWpucE05Z1AzZEx4TmFPazNUbnJGOE5zNEFQUW93bDRIL1YKbGZKbFZrYjU4UEJ0bGpvRDM3Wk1oWk54OUVvTytsT3FEc1dubDlhVG1pQ2ZrdFJhUSt5anhXcy9FQ2Q1WndENApEaVVHTmhJL0hvKzVDK1hyc1kyZlpwTU5SWVgxZ0FteFpKZzBBYXhYY2UyV0Q1QzcyKzRrektSL1p1TnVTb29BClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHhoTDNUUnNSVklmOXFtdmhFTTAKSnhvZnB1dmR5NEpJUUUyRG5LVTlIRXExSi9zUFBlRDZvR3lXVVdhZHlzbXl2QzR1dGkxb0NWT0wyUDFjajR1dAo5a3JrbldzS2RzY29GeEkrOExpbG8zZUg1UjEvUi9UeVp3TXcxS2tpZUExdnJ0c1VuUFNueUdGTTB6ZnZSTzhtCm03N2syYytZYUhCRVNFcU5tNnBRZm9jOFFhWVpzczZEeWFJVktNOEtXSW1KQ2ljbnhjSU1RMDFJL2I0ZmdMVmwKUlRhS2NBdWdvc0F4aVRzYWVFWUpJNUNRN0c4Q1pSM045dVFVbVZENzZnRnZkOGNtTDRTWUtoc3lmLzBXRjlhRQpQb2ZnWDNBWWhJaHRYbEs0OGMrUlAvdlRmenYycFFCSldCcUxBeEtwVEZtNzNkT0NHc1RydGQwNVljTGVCMGphCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm1wTmphclp6ZVlkZ2ZacDhZcTYKa2VOL2xlVmhpb29tWVlSSjdETCtJMTg1LzBwM1B2YXBHMFF1Zjc2WExJR1QzcWNWMktXSVhWODA4T1BvcTZaSgp4bnhkRVQ0MXdhZEpjVVVwU3lJR0Z2ekw3RmVIWW14SjlvTlVCU1luTW5ZS2pZZzhvM0hGTldxMCtYZjYxV1VECmFNYi9VUjFMSVQveSs2eFJ1aUdPbEpTempML2lHRjJOSXFHMDBZeHJUNXkrYVFLQ2VScm55bDJzZ3h0UUNNT04KUWJvbC92amJLemxQbVFERks3Y3NqRGhmUVpLc0VJb3JNeXNtV1VHMEphWUU5QUhSQTJybVU4UXpPWFc1RytXNgp5UHFibUNQZ1N0V2I1Wlp0VlB3M0dseGdWWHpTUHlzSmNtOTZUZ2RuZUMzRXhuTDFtTjRQeVNXS1ZSSW1IZXZCCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTU3eEozZUFQYmxCQlJjS0YzVVgKRGR2djl4YU0xZjB3MVNBOUQzK3RLcEM1eHludmwvR3Ntd2huNFFaTEV6b2xDNy9XaGN5c3BNRks1a3A5VVhJUgpvUmxRZW1nbVljdTZKTE56Ti9QWTBGaGVPWUI0bmptRnZvRW9CTjlkZmhtRVQwamEvcE1XeTFTOHBjQzVHN2ZjCnVkMUpKRjVDSVdhRUFtTmZCMHJuUEI5cUh3K2c1S1dQNTRnMW43OFUzQi80MHdncjgwS0hLcmszZkQ0cExWM1QKT3VWQmM4OEtXWEw5YWpQT3NNUjk4L0hmZFFzWjFDelBWU2JidnZrVmtXeWV3a2pOd3ovQnpSWkV2ckZhajAvdgpsdnJPRnNZZCsvYy9NSSsxbmNabmdzUFdOeVU2SkxsVEZGTzVzN2RYTzRzdVJjQzRzUlNtV01uaDZDb3lyYXBxCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckNuM0ZnK04wb3J3MVF1SnFrdVYKUGt6bU5NMy9GOUZKSHU1aC9jOW5vNkNycDhUTzAwTHA0RnJYa0R6Yk1NaHlhSUZXWE9XQWM4aXhsQ3Fxdm9tbAoxdkhhakpuK2RkSW5sQWY3RCtZQWVtWnJuQlhLOVN6UXhpd1hNMjVHWUZTRkhienE3NWNqMm5DdXNUVUZ3MHhFCjd3OE5mM3B6RmtJWUJyNTRRQ28xMUpTcWNYZEJhM3FQUTArSVNjZURGdFUyL3B5MS9VellaSVVsS3Y2dGkvSGgKZXlWZVRHU0ZlRGlmbEZHOXJnaXY5T1g2b2F5VENyem9DTFdScHJ2TVR1VzJEWWViQ0RRazhSM0Z4bUIyVG1XMQp0K0lWQzdKRFA2OUJCM0hSdzlhemNWZGs0eTFFRVU1K1pEdXVnYUVwbEJlTjJWZzh1MzF4bjlYZlFXdGFDbDdRCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelkybU0wek9idHBQMlV2S2JYNXUKNHFmVFplV2xZTVV1QWEwZ2NkODJzN0svYndidERwMEpDYzcyQkhtNmc1by9neTBYdzdHQkFLcytMV0xIMjBNVwpGcjFsbVJtbWpwZlljTmY3WDBPY1dhdjk2MGkwbGlHbmY2d250ck12aUgybm1LU1F3b1orTDErQ3MwbVZkeGdPClUvSFpLeTRtL3VWNWpKN0xlalJBMUswVU9laC9GS1p0NjRERWFhUFA1YnBOTTI4d0w5aFNZWnhTdXpEeUN2Mi8KUXB1Y05pOHZ6Smd5TG1sTFVWa00zVDVOakVLSjhQdzVKUWRwWktQUmUxS0wrcGdreFRseHk5RUtLZkVKZ2wvTApob2dRNTI3a29tNk9YQzRNM3NDWkcxQ0tXYUg0Y3ExUEl4UDhVMEY3Rk1mVWlpZ09PZWtOaE0zS3dMSExqRHBwCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlBMa1hvb0Zyc0tIYkF4K0ZiRTEKazQwbHg1ZVZGNzFtSzYxZmdodWNCQlR6d3ViK3JXbXE4T0lrUUN3ZmNJK0FmVHF1UHIvRzM5OVNTbVJJdGErMgpzNXdLK2lOclRncUh5eWovUG5IaGNJeGczWS8yN3NQMHM2Z2p5cllLaG9vT1pEbHdMZUhjOEkyOWtSZkhINUdiCk9LeTdFRmszMGVVc0lnTmxpVmtHbnhBdWZOZjNTb3grYWtPMDB1L1BVZGwyRnIyTTZKYlAyb3B1VWFHQlIvTWMKYWYvL1ZiR3oyblV0RTR6WGc1ZmF1VWVueGZuOEo3MjBjcytSMCtLaGN2Vk1QeWw3K0RsZ09mdms4NUk3cFF0Zgo2YlF4Vm5HMGFCSnN6VnhWa1kwcit5UGZUcjRub3FSNGpqeWl2Mk90dnRqSnlCSmFsa3ByNm1UVko2U29lTWdECmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcU5ZTEZpajFnQ0RyQnFaR2I2SDIKaVRxQVRSMkd3NHVhdzZNM1AvaWR3Y2tQYVduVzN6czNoQkM0anMvY0E3RjAyci9FUU10OGNwdzZWUjNaSUEragpjK2tKcXZkelF2ckxITjMycGk0RmRjK0pxMmhQaVRtMmdiZ09QY3VGYkNvd1VsNHFyZVJ5K1p6cTJTbUlwMHJNCmJpWGVqQnErdmU0T1hEL2VhODRic1Nua2ZHcitXMER0ZkVhc1ozRUR0NVVPa3pZcEtPY0E0SjVEUUdQS1dlWjUKK0UvV3VPVkdGajlGbDQvWXo4eHNBamlHU3lwcHhWODVVNXVvMGxHckUwK21acys0d0xhbXhBL2NwZHUzR09wSApUN0pXSU4wSEJKZFNxbmw5RlNmNWlMK1RNODJYbVFab1B4SU9DSUl2TkZQVEZhTjlqMytDM3ZNNEMwRkFQaTNvCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGw4RUdLN0hPL0lYVkxRWTVRWU4KSmVuV2lCeFYxTXducHZob0Q1QlorQ0xIaE90ckZ2NmEvMXY5QVNsWVdvN0VmTytQUFE0NUdzN0VIWW9ZYUJ3egpwakZvaThPNnhnUFJHUlR4cGVSSzFHWXBXNFFOYW1jMlVoMzdpNEtTdmJFa011NDBOSTJPNFhYZ3FpUEhBNXlqCmwyUlJMTjV1TzNMV1djbkc5a1BoOE9oSk9sLzlqQ0cweHdrQnBTMWZuQkM3K1EzRGhMb2wwL3EwbHRmNFpxaksKRUxZemV5WHU4T0lPbzdIOWQ1bVlpYTNhL05QaTNxRFFHTVZEU25NbHdSMnFKT2poaERETXRwYzhRRFV6d3dYTgo2bzk5aDlXVThBQytlZkZ6RnJkYkordjhDbm9VR0gvYVF2K2NJYTJ1RGNhd3I3eU83anlZdytKRkFUZWpDbEJECnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUN6SlIwOEhmajZpdWNHTVpqYmYKRHBPSUllM3ByNVF1emthdnE2b0xRc3FUNlRObDhrMC96ZjVGbUtoUUhMUjk4Y0JMSmhoWDUraS85ZXNMbkN2WApnSXZodklVUHo3Zldpa1lOUndpenptcy9mOUtWRTZaNkRMQlpiSjM5alo0aHNCdmQvOVpUSWFKZDBXb3NwQjZoCjhMY29GaHlkRnlmNHZBM3ovMFpOaklkMnNLcnlQODhseU1aRExINW0vbDVLVVNIZjFWQldrVjJYRzVIdllLWVkKby9FcnlWS0VEbE9yY3B1SFcwSjRJQ2xLYlFPcFMyUXJienFVRWxOS3dCR0ZVbU4vS3dMU0taVHFwUE01SFh1SAppcEpkTXNqZ1dKTW5ab0hnVzJUekFHcUNhbXV1TWdjOGF0WXZNNW40NnYwVURySWpkVUhzaE9GSmgvVjNob2JmClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE9talJ5ZjNlMUVWU3lmTjdENFUKaG5PRllUZjRhY0FZRC9hNTJNd3RweElobXVLMzAzRmpwL01INHpDL2Q2NWtWa0QyYmJmWkpzdmpCd3BjT3ZKSwovai9WTUJyV0hYbjJWa2hMOGRkSExUQy90amF1WnNwS3ZxQnc4dWdGZ0QrT3BQeTV6eWFLS3kwY0tyV2NYbDdHCmd0R01NNDdNR3Vpd0hPUDYreE9KZmFBZ0N2YitPV2hFU3dtVWRhaDZMOVVSSHpmeFNZbDRwWGltV0prSnlJK0QKYnNybUhvTFlDdk84bXZIMFdOSmkxM3BFcEdhSHRKZHM5emRzUG45a0ZLZDVFZ3c4T3JOOHJ5QS85NWlscFc5cgp4cmdrZXJ3MUlyNUttUk9YVjFXN2pqZ2NkRHRnSkEySHd0bjdjemRDRDgzNm5jN3ppbm1QK01TNXo3WlMyL2diClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjJkalRiVnM1dGo1M1doRkpJeGIKdnFieEJCNmdWRURmTHhqcGtGT2tTQUNSc0UvVDFqS1NGZXd6WjlueFRjRlhBRHJPRnVUeVVEZVpWblV5NEZLQgpVK08yVVRvNFNiUkRSbmQrVHpKUDNiK2JRNUpoRnVoSWlUZ0dLa3BCV0tyVUdGakxIN1VXQ3ByQ3JhU0x4ZTI5CmRDNEd0VXArei9Ub05yM1VGK2RrTUxrNFMvdndSeGxJMHNHOFpVb1Nmd2oxaWJkakM5WER4NWpZMFo3RFI0UzYKMFZNdzNUelhSUmo0QlhBZjRwK1VOeVF0cXZBdDBsbnM0WlZqOEwrTmo0Q2RRTzVJQ3RJaG1pV3hzdERoa1BZbApTemx2NWJFWGVvc0JTbGt4c0l5VVdtZ1J0b3dBUHV5azgrc0ppZnlIdENkN1YwNHBuaUFOSmowY2tEdXZ1QjE0CnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem1YVWxkdVBxTWJjTHExbnFxSWQKcDFYL1lyWEEwT1lRZmtvbi9MTnRTcUJxd1lMQXVVSjlMbjJoMVVhYTlUOWZOSVZUa1lUUVh5RzMvOG5YVHcwMworaFdvSHRnYU8vZlhvWUw0ZDNXbnB6bm82NC82dU05Q011eDhpcjhBcmZZR0ZQa0Y3S2phSXVOV2txNzV2RTdmCmc2ZGt3dGI2UVlFclFnVnNZd0ZiRGh3Z2w0a3JwRnhIOEx5bVhlZlhtOG12elN0bWV2cG5rQkZoa2pmOGlLM1QKSjdhUlQ3Y1JtQ2ZVa0N1OTdIQ1hCRGhwQ1Fkc2ZSaWJWajNVdy8reXQxV3ZMVm5FNk95djJxWlNCcnhJd21wTwo2UHdiQlljazhIcTJLaCtVVWNoV0JhbnI0YXhobldGTEJUV01Sa0ZTejMzanJPNldQWEpTTW11dTBTRUhPL1VICkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3hjclpVOG1FbVE1WmpxWFVsb3gKeUJzdWJ4dVdlRk10MmtiMld1bUdSQ0tEY2lkZWQzd2NMQmdCOGh3cnJvVTFSOGxTZmM5TGNhbTh1anBBSnE4RApZUE45dmVCL3VVTk0zaU5QZTQrbVFpVVZUOGQwNjVhRWR0MW1Nb1lBOFNZVVF1MHVqVGhiVkN5aGJQQ2JtV3ZHCkxZdW5BVEVlMkRmbUR5UTFmbVIva0NFS1poOUovS0w5RDEwckd5YWU3Y2ptcFFqYkN2Y2N1L2p1bWYrNlplVFMKdktTUUZVTjZtemprMnZqUTR3ZzNybDBhZUozMVJVSEZpNDBUQVdhRFVWWktFV3duRHRyOHRRUVlWN2NnZHoxTQpxck5OdDZZQnpWWForL3U4bVFDdUtxQ3JJR2l4SW43dmU2Z3B0RFdmR3RrbHNoeng4Q1lwbXpVZzRWenJ3dms4CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdThTWXFmVC9meWhUTkhPNVVlcWYKWmFobHovWWtsV0R4TEtCdG9WdXQvOGhkekxET21JL3lVVWJWaXVidi82aGNJWTBweGVzUndIY1FuaDdCZk5mZQpFWDMrVW00TVJQZmpNMUk2ZWtJU0NNcy9XbG15WTZIYzdEVVpQMFp2eldhNStweGpuOEFiRUs1aHRXeUxueWZUCm4wSFl1UDZrWkZENjJ3Qnk2cmVkREpkek85NG0rL2RDb1BncEVmQ1BNcWI4M1F6bUVVYmloT284QzVpdVFPSFIKM0RnZzRBVWkzQzV3NzNTVWNGblNNVmd5Z1VZKzFRQXNSenowL0dYSjl2NnYxSmNqQUI0bnVMaWJLTEFpT08rWAphVjc5VHRHVWYxZ3g0NXVkdFM3OEU0Z1lucDhuWnBscERPY3JoZkdGdDc3MWd6bzN6QlA5Ri9hMWhqWGV3ZUZJCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkZSMndHbUxETjZSOHhESm9KNTYKbGRBbXJtSzdSMkZSWnFGT0lYNVJuai8zMEhIckwvMzZ6RExiaEFRZUxyaExzNllOR2xnWWNVQjhNMXVaelRDdQpOZVFzR1ZzL0k2S0RRR2JxL2xkWW9MQ2c3c3NPVE9Mc0prRHd5WDVIV21uZSt2U09IRzA1YTJTaDdSWjhKS3A2CndNL2xPKzQzMUk2VnExZjhHMk9ENVlMZ2FtTmgvK21uckVJOUdFZGV1alI1dnVJRXZubW1uM0ptMjFiRzZJelgKR2dGL3pxcW9zZEFJTnRoQVhwVUQ4S0I1UktaTkphOSsyc0wrcUpYSnhTbDhBOHB3dlJ3ZGRaeUdkL0VUMFROWQpwZjlFdWhnM1c5aG5ZREt5Z2FIVHFWSmR6NW5UWXpIVEgwK0M4dXI2eTh3Zy9MbVg5c2pZTU5zUTlWU0lsbXA5CmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkhEYkVKdGVZQkl1WFpSRitaRUsKb2ptdENEN3FTOXh2eUhvaTd2UFpud2xYS1kzbDlPMFdCT056eXI1UXBla0VaNmJlQ2NKZE5ZQm11cnlEanlrMApoMFE4VWRrc2M4WlM1ZFl3Y0hvSnFWZHpWbm11Tk9rQVBNVHlOWStiWEZnYlRqSUYxcEsyMzk3UTN4dTZIMVRjCmIzRmJpYS9HU1JzNHhvT2pxOUE0VzZHT3FWNkpYeHdOMStrNStQM1V0T0FSTXVyVWhLTmIvWHR0VkJ2VTgyVzUKQlo2SzgxQ1V4Z0NmYnF3Y1hrakVYT3FTT0ovdWdSQXZJaWl4OU0ySmtHeXo1TVNFYnl5bTN5UGRCV1h6ZVFyTApLL3JSdDZNMFZGRHRTd3U2TVQycGprcnpLMjZucjJINW1XQ0ZaYVNTd21JMVlFcFVoQVRrN3huVFZBQitXUmlZCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEx3WTN1Y2Y2TlFPRUN0UGxiU3UKN0k2ZjdiTHpIL1pYelJNL3pXeUdacnN4SEFLR2lZYXN4aWpZVnpDTURadXduODBwNjhnbzBDeFpxNGJ0L290NQpQQnFqOTNacXdacnRXRit2ZnF3SjRpbGpXa0g0MDdlSlBTczVyeXRRWm1idUdaSXBJSTl1TS9WalBSZlFPenZQCld6RUFuYTVNQjhPV2wxTi8xdjNvK3YxK2oxUm1hTDNhWVlGc29wRjY2QmFpR05qMTdXRnNESTNzMzdRUUE3d3cKaC8zbVlTc1ZnTHV3ZnRWRXBmMy9aSWRXKzhGU0xxU3hkQWJxZGJ3MDhDcGlIMWVCVVhOcHdGRWIrYkpKRktqeAp1ejJRK3ZYQm5sWGUvYW9jNlBDUEN5ZTltT3ZzdmFhUkZpdjBYdmFzTUN3RDhBOXR3ZzBqNmVHYTliN1lsL0QzCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbWx5dkx2Yy9ZQ1c0VlJhMnYwdzYKbEFEMFlnRnhIRzFiUVRUcVh5VCtuV1RQTy9DMjV5ZFo3SXFTTjhHbnkxNzRoL1pJNVZmZHBPVnRsQm1DdzFPTwpzNSt0SUxJQWc0OWNJZ21IR29RLzJ6cFM1bmlLSktsSlhKdUMxZHlKeGkwK0Z5djU0NmEvc0xYandPRXVEbnhCCmJOMk5oVVdCb0p0UWJCS1JIcEFzS2tQTFR0L2IzeGVVdWUrYkpFUnh5WXgxOFF0YkJuRUdaRVJBSFo0bWs3YnQKZDRlQmtQMFUzWjMrV3p4a0V5YnduN2w5M0RtY2tOZTRZbm40QU5yUE5QK1dLNnZka1czRDhuUERlbE5sSWdJaAo1aVUrdzg4UXo0YkgxWkgwSGVxc1laZnh6bU9rL1pNdWQ5RlpHVGVDbldqMDhqZng5S01MQ2R6ZGRzdktZVnFCCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFg2a3ZhZFNNcXRmQWIyeEtxRTEKSWt0elo4UkVFTzdSbWk0V3ozYmNZWVZkbnNWb1hhcVord0E5T0VnLzhuSFlBcWxlWTM2YlJLNG96a0R2cnZ3aQprQldYdHJSZkdwcGtBTFhQM011cmc1UUhtZ1dLcWVqdW9lenpBMGdjRXFMcHh4b0JJQ2lzemNGdlg1c1B4Y1kxClJBZkRML2N1cm9qckdWRVVQZDJJTW9vYlBrd0ZjMzhBK1ZPU1Y0eTNZNktIUktLd2VYbzhabFJxU3B6MXZlZzcKS0N1a2VXcWcveFFkMkN5aUlkNElKRXU2V2ZlOGpZVUx4TzB4c0dKZzhCR1hLckpralRvdnJROXNDN2wvVEZyZQpuMlBxWExpRDFzSWcxQzJGdjNOSGJDMG1lczg1eEg4UzU5Ty9SeTlyaHYxQmR2bFlhdTEyM1IwZEgvUDhqdDM2CllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUxXY2IrTjBrOUtocHF0L1dtQkIKY0ZuUW5WcWVLQzh1SWFuOUdkU2x4VnFveEVGUnBsaEY2cWtPdkZJemZkZkRZV2J1RTVPQWwwVUxvMjJRa3pZegpuSDV3Wms3Q2FpNk5sUmRpYjUyMUdLMGl1dHVoazhxaTJ2MGtVeStSaS81NmNxQmM4WitDTlpNOXNPT21GSjg4ClJIV1ZDWkJ5VHM0WjJsYVJ5WjhlNmdyYUhpTFBabU1zK25nbWJvSFNVTU1kVDBlN1NMZzlBMkVNSlhjWXg0L0YKWnFBQ0FZZXd5aEZXVlNDVWV3ay9hR0EwdVBaSi8ybTRiS1F0NzFrVnJ1MUVOVks1MWRQOUZHOHc2Y29odlI5SApSRDhsUG03dTczSHliOG9BOERmL3RYNHhjeFhKSmxrNHdCUjJoNXc0dmVNWUd0cVZnWWRKMG1ESjdWZDlWWDRNCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUwxSkljaS91a01OQ0hFVUZVaFcKMnF6RE9ocjd3a2pJK2x4dDF2dEJtL21Na1h0cEl3SVdZWW4zcGtNc0pLVkliQVVxc25IMGZxZEp6NXpIa2hxUgpvMjlUNklBTEYrb1BUUnF3ZE8xRnFzYllhbFhYZHlBbHFwREpjR2kvTTNnTlVHTEJGaWxrbk5RQWhCM2ZtWWtiCnRYZDByRS9JbDlMVzhGakhEOU1kODh1MU5sRDFnTEZhRThnTFdjRk1aeFp2bmtuRnpQR3BlVjhVTk9xYkkvajkKZmQyVitOMlJrUjZmbkgzT3hqK3JZMWhQUG5ySzE3aWZNSCt6eGx0K0RTb0FMR2pxMXgvRHF5SG83cGlwRm53TgpPTlZxOEtMTUVhSGZXcnNlSmk0ZDIrWG9UcG90V1VaQ3paQWhCWXZHRHJrR1pxaEJtUk5xWGhibTNDeUhVVWZNCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEt3WGlXWmNSbmt2ZUhCUUVJWDUKd0VaSEJWdyszV0ljQ0Q0ZFQ2UjNRajdBMjN3Mk9sRVpaOWpDWDNwN1h4UVVKNUduT3UrWTQwMVhhK1NWU1VPaQpET1JlRkRwZU9nT2dSWW1TY3d1NlovRk1qd2hSZDBCbGh4c0JRbTR4cTBSMjNuVUpUV2VKUFZ4bEhxdzE0OTZoCjJ3ZnVmb3VOWkw4bHlOdU10WGVxbXZyNXBXOXl0TitUMnJUYmlFOVI0b3pvWUZUNENteXJoQnZPMFd4bG1RTmIKZnUwTE4wclRsdnB3R01EdGdZQWp0aXplSEdBZDRVM09yV0dlSnd6Z0t1L2RCN2d0ZXVUYmtqL2U5ZDdUMGpQTApNVHowRzVXZURsUXVhNlI3QmswTFFicXhPL0hac0h4Q2w5R05vRGJFd1BQSXEwZnVqdGlQMkJINmYreWY4MEJ5Cnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGZQZ1VuUkVVZzZtU2IySVVOTy8KMTR1QzlHcWc1enlaVjMvZVIveXoybHYrTllTcFlQejR6bUJocm1uUk1YR0cvdkJFZDJHZzM2L1Jsdm9XTkU5UAp1UnFoTENHbU9oYThDUTEwNEF1cGhEZDhNMVJ4V2F5OVNPUzBCLzJVUnVWUG9Ec2xlL2E5VllYY1hZR2wybVoyCnNlVk82ckhQZkZXSmR0ckR3KzhKR3A3blBwa1d3TkNYeVViL1BKcDRJSlBYNjh4WDcvbjY4SHhObGlqVUhENEQKRi9sUHBQSFNSeFdNc21sLzQvZGk3RG5vdk8rcHkwZDNPc1VFT0NDdnJnQm1YdmVxQytkbTBqaVc0NXc5OXNIMgorYjRYTUZlNGxOdk5PL2t3aE9zSE9HRVVRenM2UlJmeHQ2SmNBK1JMdk1YR3g4RjBJWmFSR29NRzBhZ040YTQyCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjJWd0l3Qm9pV2VzSmFnTHIzYUoKbWFlZld0OWVMN0pBeU5XcG0wS3FxZmhZcEpSSmhDZlR0VXFQSDZCTzA3VEtwZjZ5MDBUMXZVTXRsWGwralVmVApEbUwrT1RieStwT0txMkhmRTdMdEp3ZHdxYWhtUjJRVmkwcERuZ1h1UEhvNk1INDYvQkVIRkQyVnZmcnZyOG9iClQ4b3p4alFNYUNwZFB4ZUJGVGNiS2pDak5FL1RkQTQxRTdZZDhEY2dsVzdML1hXZHpkSDNRZmprU0NBb1pRWU0KUUoxdFRpNlNNenNqbFB2NXFZSzVIQWppMDRRTk50ekZPL21zcU9UNGdWcGlZZklYMTgzd3lXUnNteW5oeFZhRgovUjh2S0FQTDJoV3BzMVllakJDZ0QrM3hRMDk5aU5zZ0RXNnF1U0lhaFZDcGwrclh6OG1Jd21LSG1qTnpMWnArClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHNKTWFUMy9lT05RZzFYcmVrWFoKMSswQWhvRDhIZjVUYzYwdDNRUTdwck5lWEE0anRiK0JoUHRGU0laV0w0eTdoOW9OZVFFeFU1TmJ2aDBTczFvdApLY2kyclhDSlNvek5URkxXdU1rSmxpTnRkb3I1dFdNM3BjUjV4S2h4b0RTT3lrcUhNdEZtbHVhaldZN29vdVE1CnlwMzFFSXBsSVJUT3BRZHphcE5uVm9sa085ZG9tVGs0dllCc3VXVll4aHZORy8rN3g2Vk96QU9sVzVxaHQwb3QKTzVqL09FSmx4Wk93Sk1VT1FVbnhXVlNEbnN2SWFnMlFNK2d0Wm00Vm9ZcGVkYy8rRDZJWXlnZk5SS3QxdFN6eAorZTNidDE3QzdsQWpJSlh2S3k2bHBxL2RpR1UzSkpwOTU3TkF3bVdDZEx2alYwcGh4QVFMeVVUQ2c2eTNERWJPCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVBDWkx6aTh4NklWbFJOQXBXajQKOXptT1EwWmtPOHNZbmhQWGxSTk04NkNEblNqck1XcDFySWloTlFMOEJwVkdWZDlTRGZnN3RPK0pyaks1NzJISwpyS1p1ZGhHRTZiSzc5S3NCeW9nbnlrOWRnR3Q4dzBxVDJMUkRGdCtlbVJydldhV0Y3NXV0M0hvdGVBMC9DT0N0CjVCaE5PRFdiT1lNdDVvMDFaQys1TXBhYTJUYXNkdSsvd01sMnpESHBhSW5pVktVNW9NZFlJY3hEbnc1ajdLdVQKeURrYk1lSStrVU1aUlFpcmpYMmV3aEpTNkg2aEV4RFArNHpuTVl5a09tdktxU0pvMzBONFA4UW1GTW9HdzdyZwo1SXZabDNGYmxkTjJTMDFTbGc2aFdYOXUzSUxwYXdWYjBwQ0w5eGFvZm5SL3lvb2ZJWXNoNyt4L0o5anUvUDJaCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1g2Qkt1b0Fwdk1WTG1nWjdZM0IKaUZmSnlDZVNJd0FodHNoTURsYS92c2dES25ZdkR0YUVUMEE1cW5kei8xNFc0dDNNZklnay9BU1o3cjV5K21TaQpkMnRGU1ZhY0tHSStXT3JHRVdhczRaazUwWEJpN1drL0pjMGluNk9kWjVvMTRiRmsxZW85UGNaYXN1cTgxcnBGCkVsNzBEcXloVDE3dUE4b1dWLzFTVUZVdEtIMklOUVhzM0lERy96akpTWXhNNVQ0Vkt2RnNnd0p1YWtXR0M1emUKem8vcTJ4ZXBZanRYaVdYekdMNEdYemxRdU55WUFqckFLWnRLcXRrODdsbU9WTExKeEdIcGhwZkZEeDM4N0YzZApTd2hKZ1ozZFcreWhwbU1URnBQK3Rrb0x3T3B1aXl2Wk1Fd1VXVlVRcElZZFRpR1pEWXhNamtHaDlYRkZJWVBNCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDJmdWticDlNZ2RaK0VIclJuYkUKYTZDWEFLa01pZG13MnFIVUlNb0dxbmNneFljYUdYMUlIUWR3MnU4eEpRcXhwSkcxcUtzODRJYzI5SlZXVHQ0YgpCbGlaOXFFSnZDSXpud1lvc2VMK1NwN1hORzZyeHQwaml3L3VsQjQwUVdXeUVOLzF1TC95NTVSM2ZHbjJ5Vmd5ClNhZjVnNXRuOVF0c0R1VWN2MDJkazdoUVhYWXFXMHJ6S1lpTUgxT0liTFVjRmpMaGJKb1dLNTV1a1dNbXE2ZWcKRHZZNmdzbnc1bjNoN1VXTTVkbUJidVN1RFpWWldNNHVIYzRNZ3F1dmJoSlVuK1Nwa3B1aGVyQzFuaFlwc0ZUSAp5KzN1ZGV3ZVUzRHNXNHBGbzROVkFOM1oraE1GZ0dEYzVZeDlxTlNId29YMUJQSkF0alpwbGtybHhpR0pBZkZYCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlhBcHpjcm1Yd1lOVWhzdFRGU1YKQzFpZFZWNEYwMlY3STI3UUFnVUVWWE9wWGU3UXB0clAyN0tCVTJCZUhpd0d3anRsNDcyRGJOckx4K0RZZ2szagpGRXVxTlZFcE1yVCt1bngxUUFoQzNnckkwZGIrYXlLNnA2UWEwdi9yTU10bmVBQ3pQZ2JpS2NQeUxLNk1pYkkrClo2VDMzTDhnUUhMY2p2SE8vWVc3aTdXNjQ4MWxuak43TDU5cERNM0lzSkFmalFNOENmNWJGN0xYRG1WTVlEeEwKM29kVmw1MU1TTjFVQzJXWEUwS05VWmJ4WmorUEhMWGgrY1VGSG13U2x6V1puSlRlZGVXajZ1YkwxTWhBdGFhdQpKMHpOb1JQenBDektYdHBCblY1eTBtSmE4R1JqK041SVZ5ZEtaa0JwV1liZy9GUXBuNTFlcXBQRXlxT29QamZGCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVkyb3ZZSTV2TUNQanFpVHcrV0cKVmpvelArUXp6NG9aNVhlRG5NNWRNTXdHZ2U1VGNDMlQvNXhzK2d3VmxUdm1yVXJOdU92ZGovNkt6UElSNGh3cQpHWExOUm5zcm5tMVBsVHJlZkhIZkdNcXBSR0FFN2tlRlFuZUg3bzZTT0Q5cHJNbCtabzRhdE54THRvYlZaQ0ZsCmFmQk9vYVJXc1VpRWdqUGJGVjU1ckJJZHYvaUxYZnByRjUvd3NFWXBGcTBTd3B1RWt2eVdwSjM1bzV0V3I1VzcKSEU1TW9WeG52dnpRQkFib0dkeUdONUtCVXRWenZuOThHTnBEenIxU1VrRXNDWjdxdWlmMGFmMzFCdFRGZFloVwpueWttMGMrQ01DTXFsRDU0ZFpvK0lHb0MvazhialpuNUNORlhsZnNwK3IwYnBaL3d4aVhRN0lTR0tpNVIwNUFrCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkw2akIxemtXUDVOTFpybExsRncKSVFxa1BmRFE1a3VsL0hCdE9MZmhzQmNVRmdaLzBjTWptVXJVU3pmUnljQm02K0ZweGRvUUlMZlVzTkUrdkFFaQpHdnU2Q3dtdE9Ta1N1ODZsbnk1VlZaK0VrMkNUU2YwL0FSemNqZXFWTlhhM3JCelRNbDFTdlpaSEJLRitQSlJuCms3Lzg1dUhLLzVUMGs2L3B5MG8xRCtDMmRFQ3RwdThLYlpDQmFMOTd4TC8rZW1RZ3kvcER5RUZCZ09rdDZhaXgKcEp4N0xnbTR1VXlPWW92TkxldFIrZjdvcXNmdUNTUEtSdFhkTWZOR0VDc3k1STRuV1RJWkxzeDhhcDNQblcxUgpybnJTbUc2MGVGTHpQMzhBQnJsbWlhRDFXL0RCNEJ1enRUcU5HMTRVbjBNZWswcGFjaGJneE9sZTVaSGw1ZDlYClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVA4S1F3cytrSld2SnlGT1hVNHMKOUQ1UmM0bmJYMGJyR2hhVnRLWkkzYnkxQXJ2SmJtSFFkRjdpR3M5dFZ4emhFRk1tdndHemxSbDVQU0E2UEtjUgpYNTV1ZC9EM2tnZXVZWmlpSEZMY21kODAwOHV2ajMwWlFNbDEzODRpM242NmpLb0ROdUZQWngrSEJIQTFxWlFFClFLM3ZUTlFsUTV3R1R3MjRqUlA0NzlXRlg5czJWQjBnTFN1dXFRQVNBa3I3cUNWUjcyeDd2Qmd4T2o2VWFKalMKWHRLY2cwT3V4V0tWRGpzcVR0a1BocFQvSFJXSmdKVC9PQmJDdjdLRWxzWVJXbFhMd1RLdUJwZFdjZzRTZllHUgpGeTl1SkplR1Q3RjRDV2FEQ2ZBZnJJbWh4WVlhQkFpQkNPUCs1dTRHTDI5UUJCc1o0SmZlcmdvVmgyN3BXTzNsCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTdrTmozRkFkYzlOUkJ2KzVQQXgKU0lSSEk1a1pTWUVLQWs1ODVaVGEzY2dRb05VdDRpajJuNXVrakN3K2c2bEhIUkdBS01pUkxhb2hvQ0NMaEZ2bQp0R1FnT0N1Y0pKb0JSaFNla2RSYVRSUmNSOUo0UVFMSExnalo1SEVaSkpuNjhWWm54YUNwYWhuR2NLQytCbHFmCjhaaHpwbnl3c2tSYWZEUWZQNjRSb3MzaHdpcDJQNi95ZHhkY3NUei8zWkRGQk8zT1VQVXpJZENnRFQrdllpbW8KRi9WSGtSRXNlbDl6Mld0Y3BqajV3QkpmcG1sL3ZXMzgyVkpnUWlkK242VEN4KzNkcUlsaHZGL3cyNU5aTmdWUQo3Ylc4RW9JM3NGLzVPMjR1bmlpSWZYR3I0cjNFelp1NXM5SDZuV1lUMnF3VnQ0SjVycWdON0R5bjdScnQyUFU5Cmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlI5UmliZmZCdWNrbmhiWkpYSmQKb0JVQTM0ZHZLOXlXVlhIRVpWUXZtbXIvUFhRVFJPcmNVUzhMREJFQ0pRb0dNQzllOXNJR25EMmJ3bDdWay8wQwp6R2sxKzV2eVdHRXVVMnliWkNvdzh3WlB4Nkp0anVMZ0hRcW40ZDNkOGNUNjZ1REI0L2R6dVhJenFxRnBtVmJDCkltQnZ5NXlJaXNyMXNmbXR3QW0wZFQ2K2Y2RzhoRXRTeU40OGRoYy9sQ2I3Qjh1TEU5VG83V1dYSllxRnU3Y2EKNGVkeldPMkxlc1dmRjFpazhyM3Z4ZFB4c2xIRWRsTHh3bnZQMDRvdURKb25kMGovcURWVXBQbU9Xd3FaazRiVgpvbWZBRUR3Ym54VVhZTm05SkwrTzh6MHJuMmVvSC94QkpIbFlFanpFcUI0ZmRqcy9JVTdUOVU0Nlg0Q2hLWHpHClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGlDUEFrZHNIS2tGTnV2R1VjWm0KUUhINUUvSXZVOVFtNmF4T05SS1F1QUQ0a0xiaEhGTkZmQlhqVnVRaXZtT1Jjdk5lK0lpWUxtRllxSjBVNGQ4QQpqSFV4SnVKOUZvdC9aTE1iV1NLS2RiOE1CTU56MHdoRGJsS0lnUVRxUHZHUXdDelJNTmZ4QlV2dGdVRytyUDRrCi9lamxDWVhqS25DaUh0V3E5cHMvNVVRTVdCWGJqRE9OY3laYXdma1R5N0tSdEZqT01LSjR6cXZ4d2gyMmgrR2IKcW5WLzFwSEF5TTRTa3lBSTVCWUJUSkhDdEtVTHZsUHRXUW5wNzBqOXI0RGl2bVNsaU1FcDk0OVFLeG5hNkgxeAo1VFQ2LzlZVnpoY0FCOHdnNWFuRG12eFR5blFnRzhDRHFxWHVZTmxpNldOU0tmOFhpU0dNMUpQbTZyRHlER1o3CnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkZueVpXMHdMMzhYN3J1NjQ5eEoKT1dIeFBqZXlub3o0ZkFEQkFaKytLZ2dRdUhZck1jVnlETThNQWZPamM2RnhNelQvUzdQWExxWUowbDl3Z2ZiVApVRVJ3U2swQUdCdnNtbmhtVUQ2bEovanZYQmYycThvRElsdkJxSjdhb3RFcjh2Qys4QytWWGZJcFE4dHN5K1gxCms2NVh2TjdSd016UzlKVStEblhlV25aQ2EvTFMrajZKeHRwN3E1OExuWERGTGc2elg1T3ZGa3FOOFRtV0dONmEKUzdhSXVzL2xOTThxRDFEckIyaWVFNTBYTnFpWm1tYVhZVDB1UTJBc1l6SzlOS2FxaVcxbkEvVlMybjVyQUY3VApwcXhCOVRzYysyUlo5VTBUZ2xRak5wUUE5TS9yZUp3cEQ4OUhJR1hVZkRWSWdKQkhUMUVyNTd6cG5DTlVVUkNlClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmw2M0ZQbEpZOXNxVEVtK1VlYVIKZ2NXT1dSMkhQekxETXUrQ2N5RThwMmZCWXA3U29IV0NJc1dIWEU0NUZEcTFTT0F0R28wUVh2RmpyV1lmSUVTVApyWUpUbTVRR29Qc3VjUUtSSEdaRVNpVmdNNENtS2FOM3ZmQWhnWk5jSkdmeVZZUUZlYmpwaDExSjArTCt0Y2RlCkNOTk83Y1UvM2RIUDlCNEt5S2lVcGREZEUzWkFBRTFIc1h0SVh0VFNDQnJVN1BQTDFTbmFBRU15L3VsS3p5UGMKV1FuT0dmWmsrbkRRVmdXN05xL200ZndXTWh1UWRkK2U4UEJPTmhmZjh6ZWMwVkVidWVjTjl0cm43TksxWThSdwpJNDF2dUErRFNYVU9rVGJvUGd2WlBHMEo3Q2Q0YWtQK0dGZG12NndIeVFpWXQ3ejAxZEU4YU1sc1luZ1lRYUdECjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjg1RGJPQzdoNnVGOW1oREpEZG8KVWVxU3hWbE93Tm0wUGVZTHFzbnJ5dUtQV29HQ1Z0MnFleThJRHl4RkRvaWNzL3ZYemF6MUJSaWEzRmtYeHphLwp6eVZhY0p1aGpudDJ4R3J6WHcxNTdWZ05vVTdORkx3Zm5sMjY1ejRPWEJLV1JHM2gvaFNjLytZazQzSnpxbGdjCkUyNVNTdmlrd3pyWkxsTzhDckt1QWx4alpiSkVkZklzUkEybE5ROHdQbTFxNU5XQVVLRWhsQ2k3TkUyUHhYSDQKM1ptck9HTVdQbUtZNTRlZTlLQml5QzBGVUpRMmZwZG5BUHpKeGZMNTk4amhWanUxOGhuL01ZN05xUVRkMDJHYwp6cEFXMnRBN0hPTDRvWWdpZ21QQlU3Q0dUUm9mYnNCRmZrOUtOdWxsZjh2VnVRM0pDSUdWbFFwZmd5eTNoNXJKCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE1hS29ORXhEdjh2RUdyOTlMbDkKN3QwN3RZSGR0bEVzcUJ1ZHUrTTJuTkN3VENSdUVtV3hKUmhUUUFkc0ZaQmxqblNOZVhReVlTejA5OWJrcncwYQphRDNjdU81K05JUGVoK09abmcyVWhOZGtuOVkwSEk0M0MrVnlyT1NNNGdUWjdSbjdSd01GaVVmUHRPRWpGclJKCmZWOEUwbkZqQVdtaXJ4UHgvWEdPbVZjK3RmN3VwL211eHhUZlBOVWRDWTlYb3VkVnhGMVdwVDcreXlneDR2am4KV1dNaDlCMVU2TWdiUk13T0VVayt0M2tPQUF1eFdYVkVsaDdONU1wN2YvZVNPei9Mdk9NM09xd2pCMjNHTzJNdApzZ04zRUp5SnJQeGR4M3dGb0pHaVJCTUFzQzVENktLdVRGSXh6YWYxbkd3UHNPZS9DNDFXRXUrdXdwV3A3K3JYCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMktkUkkvTkx0eWF1RXZwbTd5VUUKc0VlVm8xY2dLRXQ4bDNzY2ZwTWd6RDFqUG93QXhsU1ZSWFFORmNzS0xJeVZyV1ZDb0dsZHREYmFSRlExVUswcQo5WjZmaU1uVEJFV01iWUNhM2hwZkVPeW1rb2FPT3RKSGZNSG5ZMWtnWDN1UGlPQ1pOVmVDVkRxVGt2d2w5YmJoCm1VYTJCZkJRazNEZWhOR3JQeWc2UkpvWHNySlVhSi9sMHVoRGJZbG1TSkdBYmZHaWRwRzM1VEtSanRJdTVhUDgKY3VWWnBLL1NOZ1RpeW1IRFA1YkZsM0dnYWIzUEVCOXRtZDFTKzhKV3FJTFNsUDVrUE0vMkJ0TkV4YW1jRGVOWgpJOHR5M1Z3NnhtbXdMY3Yycnd3N2F1REd0VXdXUHlqa3d5M0g1cGpIZzRDbFRyV1lJWU1na25KdXNWQTNNVzU0ClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXNveXhncFdXNHV6UmdOUmFnYksKZnk4S2tVWEJSU2w3STlmai9yQllrWmNETmxIdVBtYTZFOGhqTG5OaG5KNVNGeDBaSFQwZy9YaVp5dVJ5TDJISApmaE45OFNhUXBrT0JaMEpxdHlWenRlZ0RGZFNRQlVRS2tMUTQwcmVSdjJmbmlyNi9ycHVORFhCRThnb25LdnRtCmdtWGxuTW9KcmlyN3B0ZTNVMEFLNWhiaHh2cE9jNXlEQUhvZG5ieUc2RXNma0pJZ3J3YkR5MUl1aWd6VzJIem4KSVBZcjcwVWJ6cCtTTm52UHpkNHJ5VzJXRG9sRklaaGFUTjNkUFJJcTNNQUVwSkJnLzNOQ2tOQlowNTEyTHVReAp4akJOOXU2cnZHTW5iUURReDJ6VW53QTIrT213MnE5T2hpVUV1RWJESG9HS2lZc2cxQVZjcEdtRXl5eTBNQVNUCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnhXeHRDc0NKaFIxazAvbXl2MnIKcTVqZ05POHA0Sitxc3NIYVRZdFhBTjZJbFlaWlJjaXV6WnBha0tiNHdpUEs5Ni9yc1BudWhhbjQ4OTAzYzRIcAoyVEZUMEFhRC8veHVBVVIwdzZlSGZTbVNmSDN2TVczaHludWFYdlVLc0RVMVREZlFSVms4SE1LMkxlNFNBay9lCkw3WnE3ZHNsVU1wU1NSZ3Z6VzAyWUVyeXgwaUVKTnJWbEpEdkZnRHp3NTZ5b0ZhSmhVb1BaNGR5WnM4Vmw0RFQKTk1KM3pSMjFxOUN5bWpuZm40b1BEOEZYeUx5RVlRazFtVWkxWTFrRi9lN1o1US9VaExRN2JJYm01REMweWFXYQo5blVCbSs3YUcwOTZKWm1BMmI3ODhGWm41eXdxODVwVDQ0RXpvcUZtSGl4WC9mdXJGWm5nbEpzZThOU1l2dU8yCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTlQWEZxTUhHeDQ0bVdIRDU4aGYKR0JoNmRVTE14WEdpVEp1aHFYWUx2eEE2U2ZOeGo0MU9XYS8wODlSQ2V5MDdSL2tVclZobkJKVkxsdzJNdzRHUAphV0t5VE12VWdPOVNQTU8wMlZzSk51VXdJUnNuMUVkMmlzV25LNmwyY2p1V0R6WklDQlYrY0V5cjlYK2FlL1huCitLTFJDWTVCdG1NZXh0WmJCV0RwN0FDSjRzWkFMVit6bkRabkNUanVXektWRXovYUlSeHdEWWlUb2k0aG1mWlQKZ0Jvemc4RzU3aGZWSVRWR1AxQTZsVmRDbGFpVkJnRlNreUdHRjdOS1NSbnZlQW1xUXBIbmpRanRYTmlEd2JKZApXMVlqbll4SnhTSUZXcWhqUnd3bnJyMUxrZkpKUFNZSnE1cy9UWDVuaG1mc1k2L0tXRGJzNENLSHNYMUd5SkhhClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEJqbXAyalozMG5WNkFYbHQ1bmUKSFBZWjZKVnphUUxBQThhVW54VmxLUzFJTHNsdm9JS1RsYk1qdm5HM25aNVo2bzZkbnpQUzlaeHAxRDR1M1FlQgp1NGY1VGhKZGpjeHRLK1IxUFZwZ1JpL3BRcUFWanE2SVcwMEkvL2RpSWZOOE5RcmEzdWYvcnpUN1pjZDZIM1RhClMrWGgvalZiU2hES0FMN3hTWFRjL0FLQW85d202VU5GelVqbTFIZlRNNmJXTmRoRnQzbkFMNFBsZUdIRDZMbFYKejcwOW9BSFFuQzRGRFVOSThqeEcyd3ltcnVtTjRpWTg2TExuTStWYllkSFg0ZmxOTmF1dzdXRU00bmlCQStteQpIMXBuT0JvVXdIZEVhZWtpZGp3alpjNDlFb0xnQnc3Z25ISVlEa0k1alp2SWtNeHJ6TVpRVERFYU8vK1NFWnFnCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG92K2tRVjZOQ2hCYkJjbnh5STgKWUxQMTJ4dGt3c1NGOVBCcWtXWUhpd04wUzZuRXU2L21JZGRJVE9UU1g1M3k2VEZYWlpxYUVobmg4VmJYaTdxbgpBVGFpVC8xZ2NqdkpPZzBUQk9lSllZeERxRnBXdFNEOCtrNGt6NGJPQjVnd1QwSUlmcEQ1cWtJajZ4WGVycHVHCnFsc1gza2I3TG54NVZ6NVlaaHhNQUJjUnNiQWdZUVVGTmZGVytEdmlwenRPNXRtTU5KUlFTTlJwSkZDazhMM0EKbVNIc0lJRWVSK0p2aWdHWG90RURmaHpmdTdKVDRTY2FHV2JYUE84WEE0elFsc0I4MWhCb2ZyNmIzVTBrcTJGQgp4V1NYRDhvUzlOYTJuUlZXZ0VVYjBXQ21JdTFSSHpNckVnSTFVOTVxbGhRQjVHYllOR3JyZ0tLWXQ2cWxtNWtICmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzRUVnQ5RklFMzF6czAweTUzaVMKTFY4anVNSE4zcGpZbkYyNjJQbjRDejFEOWhCaXFkWEhXODRQeEtGWGNENjBUbVV1U1Z1SGFSSzJobDFERk5DYwpOaVhTSmViU2hENUJjNDROdngxUENWdnRQQ1ZVS0NTTUVIVGlWeFVZTUpMTzhzVjRsSW1JcEx3dVJpUkNMTDBLCjF1SWUvWGIySXJtM0tXMFBpR3lJdTFZandLcExZYjhvNEVkS1VZNzBHbHV6V2lsVWE2L3FrTTVzL3o4M2VCUnQKcWdKUU0xYWRpTzY4MU9UMDFWMFhEdEtPaHhDTFg2SmN5ZFRITWhtVHFjN0Y0bitzaUlqR003MWEreWZCRnVwZwoyRU44OVM0SWhvRzNxcEdmTDFYWkJwK3VZQlN6Zm56TEJFcmsrMS9UTzJGa25oNzFMREVEMDRnWGluYzFBNUNNCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGcxYXBtTDcrUnNwQmxMMDZZU1YKcHdEZGJhSzBpZmtxR0NST2FZdDZ4Q0htczIvZHg3bEp0dDE3TlUwL1JyNmV2SkhTQTNhQ2JpaDZyZ2tFN25OSgptQURKc1ZqMUhodjRUK3NIL0dlbGhSdjdtK1cyL1FOTE4zZU1PK2pMMDVaMVBIckdWZDhnc09KRWE4aUVyZHorClMydE9zeFBPcWRnY2hpQUVaZm9IeWRhSytUTlk2blhyRytZTlpBYVplWDN5R2k4SmZhRlIyVUlmeXVoNTQyckQKUEJ1OGI0R2tJNmxmTzRxM0VYVzhOWmJpcXBaU0o4RTA1ckR0NnNZd1VpZWlNNHpZOFIzeHhFalRRS01mSTFlcApuSWNad21XcEVlSksrc3lacDRMYTRRbU1OU3RUR3h3WjlzQWdWSWxRNzVsZnl2MHVXWTJlTVZTQzFYcTJBRE9CCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE1OSzNvV3BXNFhMUHNmZHNDK0EKWDNqSEpVS3o3MU4yZm5QejZCeUtDcU5vSXFPbktpT2tVSGRHU1Q3Qi91eFJxWFRpYkxlLzkxZG5DeWhRMXRjNAptU2V5S3l1Ni9iaXpKMUVsTC9iU2ZQekRaOVFZVmhiMVY1UFJwQXRRYndvZzdlRmJ4Zkd6WlB5ZnEwQ0QyNmhXCkxkMzNFVjZ0NmlmNUlPRVRrYVZSejNhWHFDL3FtWEF5eU1OdStBTlI5cmJES1hxdlhrVjA0WG5lQ1l2T2EwMEgKeXlLU0JUd0pObkhKa0l2aHowN0xDVWhPUEVicS9WSzVEbUtTdUs5eDJMUWlKL1d2UjhmMVpsa2Nia0hRWlZtZApXQUM2V2NxZXIzWXhxaE9KWG9RZXlvWUhNc3laSmpvUmFRZnlobDRuRVJWbmtKK2RkZGppdVpHOFZIL2dkSDlhCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0NtOXo2cytJbFBFeHRueG41dWsKT2dSWTB2U2lnUXR4RTRiNTZCR1p0ZHNkK0NtY0UvOWJMa0VqTHNScUM2d2pkNk5kejl1UkJYWVY4RjkwektjVApkUkVENXVVRnRRYUJyQWtWdm14d1FzSXdDSGw4Z1czYlYyNVZ5MkpRQVI4b0FmcVIvbVprZ3NmRVJOUTA1TmhECnhzM3h4OWNKVlpyRjBwMXh1NGZEVTJwbGVPTXMwcXdudVpDcy9WT0c1bnpUQ0FGT1VzS09lejlUOWE1dDA5cjkKMlkwbEtBT01VajBQUUc2Mktpbk4remZLaUJKbnk4SVhNUTV5czVQbmZTVEJMOVVhcW1uYkhHVmYwbHJoeXN2TwppdjVlZWROcitnd2dPdUhQZDYrZkF6WU55R09pdU1iWjBvVzZ1TXVzaFBnRk14enozZWtHWU5heVlOSzdVaW53Ckl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUttZFRFQzZnS1gvclFrWjBXT0MKa0YyZFQxNVpCQTNvN1llSlJINS9VU2Ivc0FzTTdGYjc5RE1RVmFpWlpUNlhhYWp2S3p2cFk0SEs5WVhrZ0lzeApudm1ZdktVYzQ3d3k1K0Rua3RzNUZnNXpoL3R1aGpmckJleUx6RlFpeDJySEY2Vlk4Wk5JcU8wMXRFMFZEZlFoCnVYL1hhMjV0VDNoeTNaSHQ4VkRsVTZBSWlvcEwwVVY3ZDltdmt6bWgwTHJKWXF4WWNXeFRsckFKS25TcnZYdHMKb3U0OFFZUDZ4M2grT2lWRi9sNjkwc0JpaEJKVTQ0RTBNRXJ3dWF1L2poSkprK2hLT3dqK0UxOWpEL2JiUGtEVwoweVd5bytDeHB0SG1WRU4wVnpsc2d5K241enpOQnlKbEtPVzlKNEJXUUZDM3RTbUk1NEtJOWlnRkRMQWtNVGdHCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcktEck5mNGpsUmtBZ1RyOWFENmkKQW1DVzRxYVl0TFR4aGcxd0dJRFFRMUlMMklOb0lBcno0SHpIdUErMDBlZFdVMjMzcTdvL1dYUE1Ed2l4QTJCYwptWWNnU3N6N3NrYTdqcHZPaDl5THFzQzZxUGVJbEtCWDgyMFArVDFQN0toNUhnUVFyVTNoaXFrN09udXRGVDB0CkR4WTJCNSsyeGFoa1lNZi85bitBTFMxOXo2N1h0a04vUFZaZnRrSElsa1U1eGdFdFhmTGJaTUJFM3gyZDh1RWYKTHdTL2F3WHg0ZHVKVWxLcTNQakU1K2ZhSmlud0QraGoxdFpwK0ozZ1R4S0pPWWhraEdyWk1GV0gvMVJocXJNNgpXbU1xanJRNU9MemFqNlhLU0owdnhSNy9tdU9Nd1c0RGppK3IyOGE5bkxtZ0pDQ2VnSTdQb05BeHZGU1pjZEhrCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnhaR1FNM201clN0K0IyaHFCdzcKN2NSNkhETlZvTG9WZyt2SWoxZlZqZ2p0THFPSWpBc1lsaVlNNmJvSmRJU0xGKzlvUjJxRFlrZ1piQ29JbUZ4ZQpnWTh0ZWYvMUdKWXRvZlZXTjU4ZUhCdmp2Y09TeWxPR2xhVzRCMkRBam1HR0pxZGkwQTRXVHRJTC9NRk1LR2ExCmU1ZHpVdkg4WS9CWmNrRHBOY2sxM3ArMlU4RzBKZUp4ZERvbnlXUDBVaUN0R2oxY3U4WDRmaFd5ZXNPOGNtWW4KYXFlMzJRUnoyRXhVQVZ6RVBBR3hnOUJtVDdYSXh1RGdrZXJTdUs0V3RzY1NFdTk1WmhhUTZaSmpMbjJMdnFKcwo3V0FUUFJockJndzY1d0JDV2lIR3FDeEFsZXFTbnhHSTNnK0p1U0MzaHFtdmpuNlFFTVdmd1NKdVA3UmNtN1BLCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGg3WnMyenRScFpYNWZ5cnZjOE4KOGUrdmRVaUJlcmh0Z2FVaWdvdFFsb3NwY3NEdkMzaE5ycnZpMkVKTk1uQkpqNEpaL2hmS2FTVkxMRm1DSkY4VwozNU9ZUUlHbFBjNVdlTWNrbGJBT3l5VWU1eTJNRnVtU2YwQzhGQkFwSGRWbjdhUE9qL2M3aEdCREIrZGdjMklPClJuOUZjQm9yMEE1YUtEclI1WHBLa3lpc3NCc1ZWMURjUTluOFJwUDlkTGtCYW0wNTlzRHFpRTFDUXBhcGN5eXgKalVqeHJLZHFsS0RzYjBqRmlQczhoL3FITmRKYjcxdEk5bE0vTlpXWWxiTU5kYXBGeGwyRlAxRTVQQUIwTysreApNaVpIZXNQblpEWCtIRWpnbjI4c1V2Z0ZvM1paNmoyK0I0bXE5V1dHT1ovOFFkN1IwOGRYTFZ4T0VJcUpYaEcrCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHZPU0g2Nk5xSk5jb01DR0dTRWIKM0pmRnRkTjVtc2ZML0F1MTRZRUYzcTBleTQyVkViUnczdC9sZFFndUtCWXZiemFpeXovUFdzRWRJN0xXVlV1egpxOEljR0lDUjdGRUhJN2NGMVYxWklPTkZ5R0hMYTRBcEZLWkNvQzBNdTM1Z09hOVN0SmNuSVdqNktYWmFaeXhtClZETGlGM205TCsyQkYzVGhWNThVanUyNkF3eEtpd0swVFJ1SWkxRUV1eWRjRkhNelhpMWFDTTFOalRhRnpmRDIKbVZIenpnaHhPSzZ6RW92aHZGZkxpSm9xb3gxNWo3bEdJQXFMblkvZDBzZmpDS01nT01LeHBYcEUyN3ptZE9VYgpXSkZNTUVQbno2OHFXU1BFMlI3S3o5NzNrUmx2L1pjVXZyWUNteTFDa0pxeFNUMkxSUlMvanlMS0YvS0xackx4Ckp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTlTbDRSSFlTNkprcWVvL3J1ZnkKR2dUSzNwUlAvREFlS0g1QmlYRVFEMFhPQ0JzeWFuRS9Wd2pBenZiVnRkdVFWQXYram5GZkFMaE5ieHl3dFFsTgp0dWtxTk1jQUZnTCtTZDhyK3p3bW4yWkovMmRkUWMrck84Qjk1bHBBNkRpcGsvVTJkWVF0cEthM1dzVGxZVzdHCk5DR0lMOCtyRXdEYmZwVmtVazNobzd2VG84d0dUSGpyR2VSOW5GckZ0RE1DbzJFL3VmVVBXaE0xTUo5M0Z5UzkKZVRwVGE3aHNoTWdIVVVYcUhaY2kzTERmaUlNMkpUeWNKZ2J5VHRyR3FERkRCaVY0R1I4UWNGOUk2TkZxaVZqYQp4dlI4ZkRkU3dWQTFYeVlnYllrNFVwOEZNTzh0cXJ6N01TRWdFRTh6bnNWVnVSTURVOUxxMWF5WVEyVWYrZWZ4Ci9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekNlZXUyeVRyMXRiUHZaRnFKTlEKOS85TFVwZUVlZkplMDhIVkdsOVlQZzdWTWk0NTYrcXBQV1RmOE40cGIvVXJjTit2WjI4Zkh3RFQ4N1dZRTVPNApGZmtPa3FGR3JGSWEwcmg3UFJEMURFcm9iWFJwQ2ZScjA3c08wY2RmYUdzYjhTKzd5ZjY3R01vU0JWOHJoUGdFCnpuL0RHMWpsemdLNEttbHBCOEJqWVF6aUtnWnkrS1RCcjZzd21YVkxZQ3NKTTZRMnRJUEJBK0lkNmtuMzA5QlUKMHQ3dmtQSG9VdDhBelBiWHFjVFlIZzlyZkM5TXBjWTlsZktZbnJZZjNZQkUwbEZnKzdzcEk0aWRRQXpvTm9mUwpRMm1XRGZvbmN4RENRRVZHdXl4citPZm93ckxMNm40Q0I3RXBET1VrYXNyQXdEakxIb2xsM2VIcDNvYjBTTXZyClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEJTTmowKzI0VzZWYk5NY251SFAKcWdoOGxIcU5xeVZSNXBwcGhDaCt4Y21FK1BMQXh4REp6QTQyeTdMYjFQaGI1MzF6UVN2UG1zcVJ6ck9yY0RpUQpFRkFSYnliTno0b05xQzFKVzBmWG9LTjN6RXpBc2V2Uk9jaHJFNm9wbWpVNlJuZ3ZOUHhpOGZ5WHByRkNHK2hPCktzN0paRTdNMGVaeW5ZYUJ0TStqTzNFbDdNYzdJOE9xQjJyUUtBK3dIeVNCWHlQTGxiajIxeTFZbGlTTzJyZ3oKTVlnekNqc0ptWER6Vk9RaDhiNXlBVHBLYm0xTzBFWk90Q3dDM2tNREMrRDdHaTJvNlBncy9kTFpxRGZOMG1qcwpsV0ErTUUvRGcwRzJoVWFScGxCdWdqaXh5VXQ2bVc1K29qSXdic1h4OGVPQ3NOd0ZFZWhrcEhja2I0N0JDUFZTCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkNmcE9IQkNTdGJPMmQxZ2xQeG8KaHExMHVmY3A3c0I4M2oyeWJTa3ZVWm41WVZtME5EcktEbWl1czF4SVcxbllHSDZMZ1UrMVhzTzlJaXZxTndJcgozQVk2TGE0TGpNbXRlUk8wTnVyb1NhbFFJK01CMzByNlBiN0hQKzQ5M1BBMGJTMlYyV2hQZXZxYVJlRzAxeHdGCjhrc2VhckM3N0M4TUZuNDBKbk4zTUo1SElNSzNVRW1BOFNMTnB0ak56Y1RGT2tKUWVScWxZV0dXSk1MUzgwZXQKK1JqMGwwZnVFTVBxM3dOMGJOVkE5SjIwMXlValFVd2M2bENWRitROWtFMXZBMzRkWStsWXNjOXRnNUQ0Z0JpaQpnUXcwN2xpMVRpSm1BL015OEk1bmVuMVNuR05xa2VBMERrYkVpRVlyUW5GMk5qcmVnYTBMeTUyYWRYTXdnTnJKCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXRNQ2VMQ29uQldXZ2FLMlNBc3AKWTlyendsMjAxSkF5Z3YyUnRZVTNaallUSEt4Y3I0ckNIZlhsMVdYUHNVU0t5ZXplUlVaenRDQ1MxWmM5VlptbApENGRtdEhMMUk1dEZHcExGZU1vdTlzbTRPOWdqOGUrMlVCS0RKL0NxVmp1YXB1YXh5TXE2eXpwV1RLNDZKV2pDCldSeGhiUGk2RTdUSG0rbzRxZDdiQlJSTWhpZ1VpaEhKaW9OSFFBMnVYTmlMT2lWbDNVWVp4RVZUWXZxZG05SnMKVFlhZ2VQdzc2UXcvOU1kNDFEZ2tDdkh3WjB4OURZRnpsS2ViQVdKOTJCb25mLzhaVngzNHlTeVFRaDhMNERCaApkMHlXK0dwdFJWbk9JdC81T3RSMVpxaDJiNjFNMkpUYk1Gd29ValJxRnJtcmRxdHJuY0tBcWttSExIdnBsMUtYCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWZTa1RJRkhOVHc3VC9vTzk0aVIKVTB5TDU1azBBM0tYc0NlRksvRk41QjZmVTRJV0V3bmFMRU12RGdTMWx2UStQTmFMTnpGdEJuaWZ3aE1aQWpqQgpBUkxBL3dvZ0t6bDFIdkVZdkFXU2RZdkZrYmhlU0NURGFFdnk2VEJ2UDRRVEtwMFZKbTQ1RWZOLzJyMG5uVG93Cm1WTTJpM2M4NG9xV0tDajVFZmZuUFRTZmptUlBQWXZjU1B0K3kxRDFaa1BKUmUyc3ZGUlV3dUFORFNxNGxJeDYKTlFQLzFiNFR0T0Z6SjZLeVFsTUhpVXBrb1ArRDBxYVhMNVdBdzYweis2RjhPV0FFTlc1K3hLc2I5QzRRYm1sOQpUVFRQSVArdytnN2Jud2lDUFVGYTFHSzJnV2pUdWVEYlBlU2J6SVNJblZ2WG1EVDgvMHRQQlZYdlZ4dTJJQUNnCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUhJaE9xVXdJRFMwSXBJekdkNGEKOWtOUEd3NVBUY2tENUtrT2hLZ0ZCWEM5RnNoZFY0T0dVbGhucm40Y2VUVXhacGIvY2plN1QwdVhUdW1Eb0l1TwpIMGhjMGNDN3ZxQmU1aHB2R1QyTmJEOXdsdkttcTNHZDRIU3pOME5QK2UrdGVmY1lKcXZXTzF4czZHd0FjYkloCko3c1VRNFNYdFdvZW1wcGE2eE5uTDMwRkR6VU9WakVXTDJ0anluU0czVllOcjNlQWIzQVFZSlk0Zm1SMjdyeU0KOU5qYnRCSU1rVTc0NE40cjgzT0lTbG1NMXhZc0g5L1QrZm9henpNTHBvZUhjaVJtNlpyNXo1RUxlQWtQU0RreAowdUs4dlZmck1ZdW9HLzdidlY3dHM1MTRQVkxyczRabHY4ZDBCbjgvYzlwSUpya2lrK2pHdjNpRWhUVDVwYldoCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGhXa0QrZ0xjMDVlNEZOVzFzSloKUzRVS1VRQm95NXZYVlVyMVRVWXB3aVdBNXJwSzVzZnhZeDJtTG1Oa29jUm4xLzh0TWdxZXRCbUE4RUZmbFNvWQpYY1lVNTNBVVJVbXVTTVpGam90RFFIRjNFMW0rVFBwZEJDSVFkdFdCRmp0UENxUkJzN2tqd0tRTExTTmEvWlh3CnlJVHd5OHhhT2s0MG1Ld2NHTmo5bEt2L3hUQTArNVA1MmIrLzF4ek5TS2R3UEcyak1jbU5sZ1d4OVNuY2c3TzgKK1hRZWpOYmM0eE04d2JLKzNjbk1xVkwzaDdBUmVsNVpXczU1S0ZlQStqMkRDLzMxNjMzMzBYNFBXZkFaNzF3MgowZkZOVFljSnJkUVlJaEQzbW4vT2NpYmZwUnBaSXFBU3hWUmdnbnIwMmd6WURMQVZEUXQ2R2c3UU10NjZHOVhSCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK1BvY1hZclBWOTg0OE80WkpqemYKUm43WWJhdGtxVCtMQW0xbUZFcTdVZlNJc29xcFhjWlkwdEU1QWpTdUVTT0ZobFU4OHFEKytvTW45VWw4R1pOSwo2YzJGdHNKSHBhWEdieVVkZTNCYXFWdGczR0RwUGthYk9WZnJQeGExdTQxdzJKLy8wcEJacEVESGtxbVRISmw5CnZmSGcrYTZJU1RBUHpoSEhQZkYwTVRyZjV1MEJySFVTaWUzUVVsOTFOQjhDcW9uU3cwb0ZXZ1VVNEZZUXFvaXkKbnRnYVJGemowM2Z2dHcxbllEeGdoV0wvbzRNSDZKTnRiWi9JU2JGcGhWYTFvVVh1ZVNDSzZrZ1htVkdDRDh3VwpzSWp0M2phQitkSEcvc3RtNkt3OVo3QzlwamN5OXlDN00yNTFUOGIvblJjOXJDT1g5UUpWSko4cm5yanV6blBzCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjZXY1lSaWVNRkhSQ0lSaTVOQTAKZGhVRGZnNHhvTGVxTk45SHFVNTRWZUowbGlkN1Z4aytYd2VyQUxaaC9RMkV6Yy9aR0x1RWJmK1JJWlRZcFZQMApvellDMFM3WE1pVEVydVhzRklRNlJLQktnZmRpc3NlemtVVWxoSHdSVm9wWW9pejloQkgyVHNvcE1zVUxVZCtQCkdvZXZlSS9ldDU1Ly9ZY2dTeEc1UzZ1NTZ0aEFFdlgrdDlJQkZFcFhEN2krMmJMVlp3OU1QYW1reVZncEhHMVkKY2JQVGUvbkpkU0V2aFpnUkIyK2dOcU9HR1d2SjdSbUlEY29MZHNXZ3JYaXNHUmI5aFNvL3N2QWt6Qk12SkJmMQpiQnZtZ2dneFR6VDArWStXdXY4aGg5NGFDK2RESXExS3FuK1hEdUxjRVZIaFVDV2lUdXlvNnl5WXlROUFOVEdkCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0tpS3NiWmExSE0rWUZrYkZtRk8KRktRdjIzN1B3RFphNEFPalY2d0loUi9ZN3VvUjVXZ0ZJU2oxa3BpM002ZjZZMUVMNFhsRTR1a3p5ajhRTk1aSAp0M0ZodE5zVTVnZXNQdnlyWXo2T0hFYThMMXhsbU91WW1KdHF1NTBuRnZ6TjRxMnU4RXdxQzhhOUNORTQzMWpwCjFqWjJENnVyVm1aZlhPNzR1c1VDNGpVb0J3NlRiaEJTVGczaklDK1JiaWpWeCsvSVBPaU5NQ2VYT3k3TU9kS2gKYmp5aHQrNFlrNFFWS2xIa2xsQ3h5ajlPSkVWbkVsS2ZqcUJyaWxUOTllSkhjLzBUN3l1Uk5rTjJuVEovb2F3Lwp5R2dDVzA4ZEJNTHoxeENxUDA1N2hFNm5RMjI5ZmZpL1FyV0piaVVlYUdPRk9KeXFkeGdvb2VGUkY2bDk0TTF0CkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemcwaWJWVVJzTjV0ZVRsNHIrTUMKbWZTRnBHeG1Cb2tTeWNad0FTU3JqcXpRdjdqYVF3TXUxNVRUYWNFcnpMVHBNeGs5YWJ1VkVsVEV4MHpYZEQxVQpsTnBIRFUyTEhzQk1CS0NNVDdwZTFtcXR2WDkyQzZrTkgzNzlFNmFNeXg2MkEvNXM2NFFidFJHNDZjY09RUjFXCk9qQlNRRUhHU3dhZ20zb05qditmemVoclRaOGF5Y29VWTJ6MzZRbWRUZFVncUF4RndsbFhsRk9KaUhUMmdJTGwKRWRRT3dERnp4aHpPT29LNXp1V0l5bXQ3MVVjN2xFOXIxcnFtWHZ5RUd5NUdWOVpCM1JkUlpSaVR1TzVQOWs3VwpqYm02VVZmZTU2MDhGbkFhUmNOdE5VdnF1NGxvZWxCVGN4RXZhdy9NbEh0cjArQVNmY2dNYytDaFkyYWNnZEx4Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGp5bHhkRlVwbVhZKzFUODhFU08KK1FFREl3cURhWFNJTGJXQ29SdFZ2QlRYNmdMOWtTaHc3b0haT1Axcncvb2lUdEVCVHRhZXkreFdXYU5XdEdnUwpwQm16MUNyaDYxYmNQaTZxZk56VHRLRGJ0bE1CWG1nMjJFUnJsankyc0xGRVd4ZG9JWC9JVU1iVHgySFMxaGxsCjZueWp1eHVxUy9zR0p2ODdkYnhPakpyYSszS2pScTJUdE04WFlMTlRNZUJJdmNVZVNQMFh1TU9kRVphSW83Q24KZ1kxRG9NeU5TWFdBaVE1MnJQWnAyOFBEMXo3SFZWWEZuczNRTHJKU0NZb1R3bTlWbnNZYUVTTnRjWGZ4a081WApUZFhZRlNKWVhJT0lTQ0hJOEE2M3FzZGo1bGk4Q2MyK2VhSHJmdlRxM05oUGxKVm1EWmhxSWl5dUw3SVZ4WDB6Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTB4cTFRdUpRQ1ZQZDh2NDk0T1kKMThGSzRoancvUVcvcUdxakRBYVlSeExnOHlVeFJ2bVJkUERNM0txRlZPc0lnTW1ZZEk4UnhIQXVVaXdxckUxMAplSm5Fb1o3eHU2czRnbmVRTmxZanpPUVRWUEczODd4dWlGcDdYREoxK1Q2WkMwU2IrNG92ZE0xaW5vdjZwbEJ4ClB6eE5IN2Mzam1od1d1VUNHQ3QrV3ZiOGV4Q0hEZ05JcU1FOVJjeStSMlhKRDhZbzFyaUtPNkd4YkpzSU1ha1kKTklrNE8rS1pKUXpMa05NVXlzY3krWHBncTRkL2tCM2ZRc0x3LzgrQ2ZGa1VsZ2JuZi80dnJFSnZ3em94T0dxSgovS3lFN1JnYytsT05JaHJkZUl4Z2M3aEpVVW5HM2hNU3NVbG9FQXpicmNTWnltTjdPZ2MxRDYrODhJeW9hQStYCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjhHN29iR2pKT2M2dWVkQytydzQKdCttMkp4UmJaNG5BcmVST0RhRktjNzBVL3ZJTG15d0svRkJBYVZqa01wWkYzY2xUaS9LNkNIN2VpZDRPZlgrNQp5YUkxT3ZvZHNqTmI5U2t2dmRteTl5NDl4T1BYQ0lJd21HSmp2akxKTTlMalVvaUJGdGZpdm9IQ2lYb1Z1WlluClVNSkRUUHRJNEtENCt2cVN1Y0lqN1hmMkhPMkg4ZkNGVHdRTS9WS1RXRlgxc1owdUl3bW1GUktkblR1QkZBWkgKbXJjc2tPRDRlMDRDYSs0RjBXdmgwYkdOZGV2NGlVQ0hBSXJKUmxyRTZwOENUUE9iZ0tDM09IRWp3dVV6TmNWawpleC9saXppOTZQTTYybUgxVEZPSjM1Wm4vVFVIVzZ3UVZRYk1Wdk96VWgxdWZHbmpkZFBhWG03V0V4ZkdIMHV2Cnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW9SNzRLVWQ3RjFBVEY0UDl5N1oKV1VDMjdUL2xVQ0owMUFyWENMUDZTeUxlNjVhZ200ZkVBazJNakh4RUxKYXlIZVNrdlR6TUcrdUk1bHhLdkM4Qwo1eHArYjFUUVI3K2U4aldFcE9vZTkwdCtLbHQvQWVYZ2VYYndsSUFIN3M3NWV5Q2wvV0JEbElBNnFhWFhEWGhEClRmQzFhTGN4cHNITTd0UXlMbFhoYlJLMlFkRjFXdERvVGZ4SmRuNzNNSzZtYWRNK2prY0dWcUVUbG5zZ2FzcVkKam96ZDVTd2FXOWx0Qm9MbHp0V251TUc4Q2I4UWlmczBCYWFZckFTb3dXZmlIWkFTM29ZV2R6RXppQmExYWgvUwpaRFFhSllaVGlCQU1hUDNDUXZKYlluQmVQTkZsdTRiZ3JyQlZvL0VUQ3FhSnRZMmhiZGhVdzA0dUNHVi9SRWVtCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzRPSnVibk9mRTZWUUdrT3BSTWMKdk45S2sra0ZqRzh6TlhOOGZtWXB5NStBWlpleEl2dE04WTdpQzhGNGErSFh3a0NXMFFnMDloUjZqU1pVNHlBTgpxSnpRL3JGTkErdStRd2xsM3VCMDVyRWJMWFJ3ekpQOFdjZ3Q3MEFrY0kxQzNpRzd6NlVtK1ZMbTBDdFR3bXpLCm94R291dVUvV3c0Sk5LQlVmU3pkZDVSK0h5SHpPZW1TaHYySXFyaTFFMk9VUjZPUmVVci9kQzcrYWM0aDdBYnEKSnFIaEU3TFU1MTdycGFsbDgzVVczQVhzcVRCdjJUSThPTzBOWWFLQU5QSW9NUXpsVUVLV3o5VmkzMTlkOUtyMwpCd2lpc0F3bktndXkyalN2M2laN2VRSldvckQxZnVOQWxsVHFCZUVHUU02Y3pNSloyN2VWY2tBM2dhaUxQNjNTClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBa2x0bmNhNloxSHM3eUxMaUxMTU0KTFZsYU94WWZZdlRWZmZqWUw4NUxrU1JGVnIreXdWUnFjUmptcTZVdHFpSlp6MjhmRTlFcS9WdEZPMkdwcHc4VApsU0NUT241aU9QTkhlWnBxeFVHbyt2UlloQWgvaXpOS0R5c0MvanhHaGo5TTJyQW1FTXIxWGhZaHFCaEppaTRRCnBaN0FDREQrblEzWC9jRGdySFFoR2t0Y0tVSkdzTHY4NU1SbGZBRGNxSk13VFhJbEZDeEVuVUdBSzE4L1RzdHMKZC90NDc2a1Q4Y2JYeHdTUThScHVBWmQ0UTdsTFpXODJqUzluM3hUeEwzZFI4dnZZeTNCY0dwdHNOdnhFUFdsbQpZYjZNcys3cTJyeXE0UUs4LzJ2bDNyR2lQdFpMNkJhY0dTdmhvSFFqYVR2VGsvdmZtdDB6Q0JQOXNVRVRjNzdvCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmtsMnhSQmlSNHN2Q0R0NTRrOGwKcFgwTzAzZVpBUk4xRzQ0ZWFLSnNESjhxWFV0TlV1VUhTbUVndWNnR2psWldQZDNiOE52MkhQV1RyQjhOa0VacQo4UWhNL0sxVXpYcElweEV3N2pUaEttVEYwRDMzN2tnWkxQYldtSTlxN2wyTVJ1bCsxdzF1dWxSaloxSTd0dUZ5Cnh5aFpGSVlDci9ydVZuZHFTV1ljOThyQ2xxaGRTRmJxQlUrdkFzWlNLcDBOTDdPcWJKMlp4bC95VnpRMFZqRm0KRWUyVmpRVFJnNUhqZnhyTmU2RW1pY1hZaUdacktpZnkzTkNzSHplM1kxdlZQQ1NncUszcUEzcTlHdTZtMVNZdwpMbTlsb0ZIckRCNjlEelgvYkdtZTJDNlk0SjhpOVdKRGRBaGR6YWEyRUp0VGl0ZzVWM3BxVWZ0a2JmNVp1dko3CmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNG1OcktTcWlBTFFrNllwdWticTQKNUFYS0RBR2VJeWpTaGczOG5PcXltMHFBT0wrZTYzc3VXZzVJamcwREpxS21YUFoxQ084QmJ5TTE2eWYzei9QSQpnMHZFcXpMSDVXNDJwcmpMRWdpNm9uQ2FPT2FLc1N4cm5xTmFJRkpOSjZNWkRMSHhWZzFmaktNQU1UNjVVdkN3ClNoVXc4ZjgzZ2p4a25tVytzZDRuUzlaV3Y4b1c4VWRUVURSSEp6Yi96R29SV21oNGRwRU5GTUl2em4yZGZIMUQKbGE1dXA1VnRReEFWSUJDWHFTQ1lDUHBQOUJqWVBrc2daWkxEMUllV0lnM1AyUm45UGlxb3p5cDhPSjg0ZlNjVQpSakpwT1h3ZFYzYmFJajJDcVdIdlhtb2VhMG8rQUF6dXpJY1lIU29uSG00cmZ2eDA0WkJEZGdwWkdpUCs2TGo5CkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWJCUktpMVFzNHpmTGt0OUVuTjUKd0ljN2QreHA1UXoyb2o4bG5DemlzN3Rqa1FwVTZSaUZTSkFnNUY1aWh5dVdLQnkvVU9CZlBQbUd2UDNIRDB0bgpWQnVBQXN3WE5vR204am4rVUhxaFBvN1BoNHFFeHhyaTdrVXN4cGhSeVJEN2gwZ2h5K3Y5aEdoYmhYS1VGam1QCk5wclF3VlhmYWt3eDRreno3ajdIa09Pa3RwVTdHWTNmTnFUOEdIUnBhSmdleVdlSjIvWGxBbHVsT3dWcVZndUEKcndnT1QxOS9GaWo2TWdrT0hNcDJjZndGQ0hLWnpQa2lRM2wySno2M3JwUXV5NXJ5alB0S0lGckF2d1MxWEtXRgpQdmluWkM5dFZFV2M3UHREUDVMV25MTnpwWW5hTmhBdjg0REhjQTZRdUdERkJ0WXpHU3RNUm52SCtZTFBPRjNPCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMC9LeUFka3JpRUJsVHlVOFhTck4KOGtFYjlYbkVrZ2VHY3hEa1VrL2FOUHgvQ1c3OWIrVE96c0F4K3M5RnI2WE1HRnQzYjc0TDBXMWk0SkcrTW81YQpTY0U2djlrckZBWGErdllHVlVOS2lDSFdvVHcwZFladElSM1gvanJLREpCWEk2WXBOci90ZHVNSFV4QXlSMEloCmRvM0Yydm5LdUVSOTJjL0NtRVlKVkwvMEYrRWFWZHJiY1lVQkd0dzU0WFFZN2J2QXdEQzdtc29CZnI0RndHdzkKRHM3OVZBMGVHNVpEZkhUK05UdE9UUHF5MEM4Y1Jpd0J0ZHRkT05LaWRiK2JYQ1M2SnkvTUo0VUV0ODFHb1dOYQprYzBSc3dZRTRqcXJEVDRicng2N2U2UkE5RThVek5aRk9TYVZkTGRxSU1YampRcDNlTXMyd3lkcmdndlplZVNjCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBby8zQmROL1JNNFhJMGs5M2lmWEkKbDNWMnh1dmhQc2pvNGRaTmorZldjQWpuOXNWdm5tT3FWM2FzNnVaa1V0ZjV6WTloTEJPWXk2MURSVmlYcnZ3SwpDaGNpSTZTWmcrZmJiVlhCZzN3T05BbkJ0eUYzcU1ZZ3lHK3hxN1VQWHEvd2xsd252RTgvQ3JKTU5sckhWLyt2CnNuYTJZdGtGQjlsNXYzNkljTFdwMlpOWUh0ZEFnM0RJVDJ6anBUZm5WZDliOFVkd0ZGQU5xZkNDQzRlcTJYdWMKY3JpemluTlcwaGh4MExXMUh6WkJ6MUcwRTNUaGFYaWpZdTRPaDUwOGZ3SlVjbVNXU2kzTktJWEVrMW5ua2VPVgpNa0VLR0t3cHNEemtaVUJPUHFSeFpkL0UraStCTjFHalVORzdsS2NtZnBubVdzeDVVSnNtOGU4dDZJaGE0TmY3Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWxOa2hzZnVQMGJWY0xpdkVmWEsKRU80MHJ5dm5JZE42SmRQbjdGNVhIZHZyeVBJSkI1YWJMeDZWV0R0bEVmLzlTaE9peEJOS0lDdEwyYUVkREdQdwpCa3VCbWVzRDc4eWFQckhDVGYzYkFTNHQxaEpESHhvM2dDa2RGL3VwejloaUpMNG54L3JaZlBZNmxpYmFtSE5JCjZoQXgxUXNDRWljaHdQN1VlODZHT3dXcGNUOER0dkNxSnlsUmRYY1VZUHdXUWhMbFEwSVlpRG52akZBUDVXV3IKd1ZMbFE0UkZBUGYvaDRoOWt2Zk1KRTNGTlc3ZzBzN0RJUmRUNGdUM29WYU91RkZRbUJDZ1h1K0tnUzZzZDFWagpuZ2JNdGo2RDNpc3B2NXFGa1FvNzdLRHdMRUMybTg1M2F5SkxvTEJMZXNnUi9PR1lPT2ZHb29uTlVEc3VHUEorCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem41YlcvK203OU9LRlN3WnFxMjMKcEJjRXZkUnB3aTlxQUlUUnpIR1RFK2FaUmxpanlreHc1WFA0aFY4MnU3U001ZGNtbDQwRitvczdIVFM5QnJsdAprcys0bzd0cHFjZTZORzJWM2NyRjJTU1hyMzVESC8yTmVSaUl0NVJhaytqYTQrNjVPbUZGVFFZaWJWMldvTjVnCkhxRmhmampmQXZzYkUrUDlSVVFhbGI5KzBINDhBSi82QkxkWFBWVjFWQ1JGaHNJY3J4TkxaMWp0dTlEcmtZaXcKSkJLK3ZVbzJQdW9Ed2pwcURUVDNsckJxTDdzUmEzbzdOa1RpTzJQVXRoK0RxOFJwakdJVitnTlNoU3Yvc0M2UApMbkFTeXo0cjBPYlo2VGJ5U0dOMjhxMTVtcm1OQ3RUOWNsdVozRTNCRzhta0h4dzJJcWtEWEZPazR5SWUzSVVOCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVc1aHZmMHpsMFVzV1RxMThLYTEKb09ZTDFUaFRXbStDVTlnSTlDYUlSUytFYUsyMXFnZWkyQ1NOaHBSbVZVbWNJZWNSZ0NOU25renIrc1FJZDlIbQora0ZNc3RibFlJVERaL2YxbUZYRk5zY3piaEMzTU9YQjMyL21WOWZyQ09sTGVlOTkzTmc1SFkyQmZ5NDM0TWlxCml1R0hBM0NORytaL1FiNlRtT1ArWTQwWS8vNFlHZGliMzRVWE9pRVN5VFhsbXJxZWNVMnhtamdnSGFnWnZOVlMKRDlaZDNpb2ZkYmhZcnQ0Z2tybTg1VDhoUUlNOFBlZHl0dkZ0NXNDQVpZVnZ0SVdtNVZPYzM4T1JOMW1GZmMvcApIZEEzZTQvdTFKN2FJWUJHSDFpQzloMERwcGRmWHlNWk51NDlqMldIOHNkWmpRZDc2aXhJbjFaLzZGczJWR2NlCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUZiRFI2OGdKMk96LzRITGhCcWIKb2VUMnNId3lCMTdmVlRQa1BBeU9YQTlMdUtPNHlKUUYzMzVaRGlTLzF1TENhekYwK3FnQlVuSWNQdEJIREpLNQpFY244RmZZcWdIOTNJbzZORXZDM29JVmsyanFCN2pIMEdaOFVWRHB4L2JwZlprdWV4VXBmWVBibTlXbjBONzZUCm9jaG1CSXZCUXRxYml5bS9XbUhwYjZYMHpkZVpSbEtMdENaUTZuaHN2czM0SUJJOVM1L1JVUG5oK2FKbzFUUkgKNXZyUC9zSzFEdnlOOHQ2ZVVheEJCa1JtUDBjMDlMdE85K0p0SUdCRmZCeDFRM1BQSVVyUXg1RE5hS1dVQ0hyeQpUMEFMVVlaVm9LdWc5RUh3ZTVpU1RySXhKNVV3aXRZbzBmVWFYQVF5R3A2TEovL3BlWDJWbmVIQ044WDIvekFVCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1UvN2pGM1RNbS91YlNZZnBFeVMKWXFUb1hGVVVXM0hNeFJxNFROSjN1OGluMVkvR0ZnMUpUR2dSN1ZjOUtqMmFqNDRlMk9STkhBajYwdFRqOFR3TgpZQVZNa2V5endJNXdSWWlMODFFQ0g2OVZqNjV0K3Q0RFZwc0xLMXVZalBVVmlnRUJRSnNnK2lEMFRwWjYzK1BuCnJ0NG9WdWxSVkFSTzhRaFQ5UVNDU3pTL0orQkhpZFczVFNrTTVOK1lPZ3YzS0grZVBKZUU3aGhRdlJDRDRyay8Kd3I4VmhHMnVQaWd1dmJDSFlNMEg1Y05aN3gzR2NTdHhxMHNOVk96dmdNT0UxMWo5K1p4WWV0R3J1czl0ZFpDcgpGZE1qTk1hR0dhZXBwcDFKMjJEazlMcTRxL0JNNVBtdWI5V2prNFN2T0xwbGZ4VXdXRnFJSEhHU1RJUmhKUUJYCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnowUHFKbzFhK3hFN3lkRmZ2NVUKOTM4VlJIMGVrQ0ZFM2N2UUUxYmtBTTl0T0IyeFhXV3RlQUxMVGZjdkpjSzNORTQ1S1ZFZkJxNUEybHpoNGw2TwozV0d5R1ZJeFBiVGNJeXpXL0hUd1NSNW5QNFl1OUxybmIvajl1NUNEbWZtSUJNclFENzBOaTR3K1EzTmUva05UCkhiNm43WUFQRlo2anhXRzZyc2RCOUNiSk8rUktOREFsSWZEWjZtaDdwVFhlWnFmeTdhb0Fzb0kvL2tNc29uWXcKb1lxdDhjenEwT2d2Ny9iY29jZ3hjbUZPcFdmM3NLZW5TZWZPMEVhOWtJNVRKRW1tYUxFYnV5d0U2V2owdVF2agozTHBZODA0bVBmQkMxWmF3T0MyeEZWM2lJc2FJcEFZcVBFSTNzWFhjd2ZMRVh6blJkOE1ZUzVSUS9venRlUzNiClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXhxMzBMZXpsN09NV05BdWsySE4KT25pU2FvQmlsRGk3cU9aVnowb0xxaDFpYTYxK3pXRHg2Q1BOUTFVYVJuSUF4WkJRN3Bidmc0NDcxVHk2cWtFbApNdjdjck1HdE9KZ0pWMjZKbndpMnFxTmpZdFhXQUlrSnRvNU9FNWhkN3orN2dBdTBvUXZObGcrcTJTRytyK3U5CkduUVEyVUZYbmlvZ1p1UlU2Q3RSc2s5RFcyYnVvNEcrSkFFbkY3R20wTks0TlZYaTRlcGd3QW5PWlV5bFp5c1UKa2JpZGVab1BkS0RnaGx3QWllTGp5M0RYYWNYa29ZWFdWZWJXWmhVYXdxVXdLcHhYWFkweTRDMEkvN1RBZFh1MQppaWtuZGNadmZUaFZsenRBZk5pQlU2aWtEWmc2bVIyVkZyRzg0SzNLV3lseWRNc1R3RDlLa1pDR2U2YWJtbmpPCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjkvaEpHbUlSYmxFL0xRMEJPaTcKaEk0S0Z5L0drNXJQV015TkV0MGRrVGhqUmVLcjY3R2YxZ3FYT2UyNVBJbFFMVlJ1SURDUHdvVERTMUpqYWxmeQpsamkrMTdJUHFKODlqZ1VQWEtFSjhtTnpCM3RZWEF0TTB2TFovSUJXK3BCcWhJT1RRdlBwL0Z5YnVtWVQ4eHRzCjJWbHpYYmNNZUdXMFdoY3Vqb1g3WUJZbk1xSDBTRGs3QzdrUDkybUg5clo3aGFvbDBGOWIwSTNaTDhZcHR5d0cKdWVQeFYxUWtXWkNPNjE4UkpNMGV2aUJhbFNpenk1cWpJY0RYYkZHYVFYUzc4YzU3ck4wcHFOUEpkRlZjL0VZTworWW02VytLUEdCd1lFTjVsdEtUTGNjV3NpbXdlOWJkNEZNK1I3a0FyOVZjakxPS1l0WWhha295QVJNV2daWGNLCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkE0bkhPeUc1SnlrMmN2SHRIYUUKaStHbEZjemw1Rm9rbE00WTJtZzhKRnBRWnFucUJhK1JvS2w1TS9SbTVGV0ZOYXl1YUdISDZNdU1kMlFkY1VpVApVRW13aXA1L2g4cDI4Ri9pMTIvUDdETVQ5OTl4eHBLV0xDT1pzbFU2b0JTejE5dkxyUlZGRnIyTTgrSU1DYytMCm9YVXRpcTE4V1ppMFh6VHdKUENLUEpIY1ZiM0RpWkNEejM2U2UycGkwOTVuczBnaVBjUEo3V1kwYlBxdkxGcDEKVnprVWkrS3B6VDRhQlNQWkU2dFV4VmxYWld0WFpQRXI4dDcyeE96b2FLbVAzTDcxVmhEV0pIUEV3L2dqcDZoMwpOOGVnbXNSYzk1MHJlMTlyZS96YnJiTXJtMWw0K1FqSFc0b2JxbDdVNDdRek1uTjZneU1oRENYL3JmbDhoWW93CjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0NLa1V6RHJQblRWN1RjR0dPdUEKb3E4UE5KN1JHQzVnRnBkNUI5N0NSWHFHRWRwNFNpUG81R1BhTnAwZVZiZ2V2dXZqdTZCbW5ya3MrYUFsaXpLVQpsTzVxeGdtWVExbDBwemk4SVArVjRyQVBuVGdaaGFNdjloOWtnbnJwdU1Pd2d1NmRDRVo5U05rZDNLaEFJNWRJClNidStHcVd4T0U3T0MwVXRaYXg1M3FTM2h5M0hQSkRWZ1dnQU52TnhRdThrZ0JvSExybUxwcm93TTRWVDJLRG4KUkZvOVZ1S0ZUNDIrMndRK1JIbEY3UncxV0lnT2dPcVh1OVhnajFzQkJDNDlnMDJHQmIrc0k0Wjk4eDRxOTZDcwpWYUxiTjh2S2t5Unovb2hjUWFMaUJ1emYzMGdUZm0vVXU5RFNXK2RPcXdjSGQvYTZWV3VNem1JeFVxODllZjBVCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3hnT1lLa0wxN2FXdkJsVVdvWVMKUmxyTFFVM3BsTkVySmloNFl2VFE0WlgyQTFiWmo2Y2lJeGFDWk0xKzBLK2N3WjRlUDROeVg4U0pZY3hkbFZ6RApmUVV5UVp1R3o4MXI0WmVJUFpWUGJCaVdpZEhFNVpZSlArSmxRTEZuOWI2OC82UnhIemJEeTVTWXNhalVhWE01CklPb1VpL1Y1eTZ4RXlpY3o4N0tIRzcxMFMvRUJ0Z1lOMStGNG1vS0Exck9DWWhla3htWkszOEk1SDdlb0NnaEYKYzdzcUFTdlE2dUN3NE1rcTdkTGpON093QzNkUStsdzBSRTE3WXI1RXp4U1VTbUoxTGJBdG0vUjdZNFViTUNOdgpFakN5RW8wNEpERFVlZ0hkYnczb3RtZTYvQlpCb1NkWkJjLzJ1NUx3WGNZanE4YytpREJOejJIQWV3WkpyV3hzClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1BwbXBlSTd1YTVjTlFXNWNKdHIKczhITmtweW5YOG9rVG1ncHl5em85dGxuU2xwZ0NGY2htNllLd0ZPangvSnhGSTN3blpLZkFXS00wQ2ptWEIvcApMQkMyOUJ3azZmM1l0NnJyTlBWLzl5RXFCNmZqWUYvaWoveHZzeHBXM2ZrdThWT1Z0Tm5JcUt5S1VZcUczeGh2ClJ5TVpiNUdFUy8yRFhvb0Q5SjEwYTJqQVFBWmZiclVabE8xajFhRXkrclc5cWNFdVU5RFcySnBBNlQ4ZFFiaTgKSFJIQTFuWWlsSmw5TlpBUWs2WlRMZnFVR0Fjdk4zbTFQK2sxeU1ETi9UNW9scmgvSHJtZG5vbk9qSW9vNUFiYgpnYUFZK0p0dDhJY0d3ajd0TkJXRXE5Y3ZHYXJDbkJnWFc5bE80eFVmeGxQVHhoaFo5YjNJQ3RvL1BtSWgrOXAwClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE81N0pKWGJBSUlkS1p6bGp5cWcKNGhPQklDYlRwYVpUZC9nTElsaEhnZi9GWDhkM05xT2NWZ3dEamhWT3M2bWpwQkowUzVSbGxMemUxMlJjNVBSdwpQY1c2eTBQMmFJMVdGREZZZUhtemR4QXE1R2V6SWNGUHdwaDJCR1VFaHVvSXlSZ0ZjSEl1VFIxTGk1TmEydEttCmZNUFBLMVhETUpXb2lvaWpXcXMySG51UVQ0WTVlRkhkaUIrY0IyMkNqMVZqNzgxSmhGS0l2Z1NkZG5vRFcxcXcKN1Z3bGNWeFhDY1UvK1FiZ3pBeGVMQWpXMFlvcGt4bFQ1MEF5TFRXYnJHbmp2UnRZMGkycHNqU2lUUTZQeFJ2RAplS2hJRkRDTVpoblFkbER5amtPVlM2eDltdGFud0RGN0J5a0hlMDhEbnF4bU5JUnBDQmJaUzhKUHNtc3RKVzZzCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1BTbzkyNTEyYUx1SjBJeFVNMFQKcW14UWVnWnN0ZVd2WjZiblpxbjJUdHEyd2NQTXdmNmxiTmM1c29YTUY0eDI1dUlnWFhHcDZidmYySnRtWjFNaQpYT2NvZnRYdnhSMklTQ1lOdjFNWkg2SGM3NU9ZSkF3OTlsVlY2T2RPaWFCZFZoVWN5SnVSZ3pWRHpSRkd4aW5JClBOWjh3YzB6L0kyNU4rMC81TStRREtHby9wREJ3Vm5WWlIxMFMwSXNycDN0ZG5Dd2FZNmRUMFVhenBXNDdQeHIKNEE4cmlXMklzaHVCMVdVN0ViTmxlcGJaZ1k5ODFBSjlVZWVCaDlOcDNXUHJBUy9kN1JKVEJmcUlqUDRJM0lkagozL2J4eDNIVENOcmkvZVY3YTI0K0hqSlI1M2I5VTF6S0dyOFg1Nm4wWEJOVkNDeTFqaUJrZDVPZXhLMnN6MkJICk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFNKQVZENUt0WUM3RTRCZUh4TWcKMzFHdXpXWmNWSUYxNVUxVGlrcXhsaG4xQytzTjlVZjB6OVF1SVZST2hqbzVaYnFVNWQ4SFB0Wi9NSUhESGxINQp0SjJCVThLSmNLdkdReHRibUlyNUI0ZElWYXNQUEdaSVVEWlFTN2JEQmpGbUFoY25BdklPZ01uVTJzREFuWEFOCmpBbVNZNHFkMHdQL2VnRGRjdk8rWCs3QlBxbWppQWJpSU83MUhiMjBIdWFrTnFhdjYyNHVHZStqdVk4cW1tUVIKajQ4anMxSHUvaXJiUTQvT09oL1RKZXNiaWZNMGQrM0IwSGNWbU01VHM4SUJKQnMyTU85Wm9DUnZFNDgzVGVWQQpPOGNCMDRSM2xrTlkyR3R4UTc3RjdPajFtb2l3cWN0anFCMFBqQVNlVHVIMEVrbTIyQlNaR0NGbEtNVzY0aWV5ClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2RKSmZxSGVsZ2RKZmNQMk1yb1QKZUo1T210cUlydEhjWGt5c3VzNmI0UTBDWEoxOXlOWEx3NmpNM3RPVmxOQWRlbHBUWmZHUVpsc2Y3dGxGMU9LQQpJNzFOSHNXWG5UcFBueUNrYkZkbThacFAzcXBBUHNjQnJSeDJHLzNISWlpajA1YzFSaGhnNTV1c3ByMng4NU1CCkdORWwyWm9XdXhnb05TZERlME14WVNHMnpyTEdyeHlIS0w4UTNrMlFCa3BEUmhwODE2TWRVUFlCU0crSmZTUEMKQVIybC90bzc0VWgwVzBZK1orVnh1ZE5PUGlxUFVodUMxVHF5dFNveG1CbGxKK0s1Z2pWbTdrODM0bys0dGdZVwpyUmZQOHk1TEFEWUFuamZCT256VWpxRUFMR1NWZlRST2ZFanhaLzJ6OStHZlpjZTJ6dzNqa3BicTdjTzRrbWdvCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFVlSTBjbmptSi9CTnIwb0ZZOXIKekNja0dzaVdZdkNybS9lV2dTREh4WFppVDVaTnYzUmdZMEtQZmgra2NpNXJBZ0p5WFBkQXVUSVFEK1RXT3ZuVQplT1JOVGtwcURrU2lEY0dHY0VvMTQ1YlU1TXVLOEFiR1BKU3E3NWJ1cWU4R1JndFFURS9lTENLbkUzWitpbkdQCkw1Q0dFN3JpTFc5bFJyTmZsdXFPZ2hPMDRuN0htRWZlRHpnc2crejZBanZuRWFlTkdwUVBPN1dqVFdKUWNnZGkKZktmYWZPZFNDajI3aHR3eTAvTWVOOVpTT2IranB1RzhkSkdtaDE5MWRkbEtRcHI2QVN1blNNblBWTXVZdVNmVApRZXdtaDE4cGVqQVRNL2lBY2VaMld3L1d2d2dVbkVzK0NVR3ROV3NzUWtjWDJWbmd0NG5ucWdHOEJBd2k0ZHd0CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0tHTzhYd1VjWXZUZHNjY2JuRTMKR3ZiTzYzejdSbjBYMjZ4U0w2cFdDMDU2QU1nMkpUQ2grTjFwZ1dFSlhkZTRaOTc0azA5NlRuUnVCMnRoOGVDZQo4c2VsNjQ2SWhpbGJVTXpSVFVjRU5hWFF3VndVY0FLK25vOTEyaHpmK3l6WlFFZGRhSWV3K2tYaUczNEFYcFptCkgreU00d1lKVzJPMGVaOXBWdkI3ZGNPYTVTTEhndkF6NEVaWkpWcERjK0w3NW1NTThHcVFkR2p4cUFDZnhVaTQKd1RqZ0VlQUk2VVZnT0lGMk5MZFJDdVVIaldMR3h3M01Bcm90aXhSRHl4Y2NvOGh5amJPYmxrN2tBUGRBc0w3awpNQWxHK25mai9uSXk3UWpCRm9jNitPWGVCRk9ubHIwRkU0TWR1aklCVHlueW54aG1CeDEvc2JDNkplWStQSjlhCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXRiT21KR1k3NHN1emxiZjFYR0cKSjlqNlIrRC95eUUvNVFCeFh1M2FCdzdXUmtsd0ZiejBZeDhUK2h2Nlo5cUgyNHBTaWk1TUFWck1Sb2YxWit1QwpyeXl3VzdkUExhUy9DckVUdlhvOU1xRXFjeENIT2R6dWpMZzNPWGRNakZiSElhVnM1ajIrQUJ1emRSZWhmRUN0ClB0WFU0TDQxQzdRTE1na1ZKRlVVUzJaSmNjbi96NUhaUTliejBER3FEMGtQbGs3eTI1N3pySndUYzQwSUthcnIKOVhWUHpUbklVOFhkYkVmNktuc3FyZHRSSjZ3RjN1TERteUFDWXpmSkdpSTlVSGJJVTE5ekVnbVhKQnp1cHd3NApZWmFkcHJEY3BVWGZBcU8wcDJteHg3c3RpdzhHKzcxN2NiS3BTTXkybFFGbGJhbmc2MEdYZk5nVmZqaDI2WjE1ClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOU5ub0ZneGU4SDBBZGE3Q0JGekwKdTdyS1hyRVdadkJBQ0NOVGFOMFNwOXFpT1NxYjhZdU8zWUVBWGEyUFFlOWluYkR4ZTNJN05NMmZMTTh3dXVqRQpBVkRXbk15d3BIYnFxaUxyUHVjdTBKa1ZzSXlpMHEwclVkYVplTzdiODJSa2JoODAyazdiQy9oQVUwaXNBSU5jCklteVIvYmtWY2pPTUFaNWlvdlJvaDFpUXA2RVQ2WkxlZzRWdm5qckw0bFNRM0x0QktkSXI0U0VHSEFWU0RuVWQKZkVSSGttN1J1TTJOblRPeFVlbW0wYXQ2VnBlK3EzRkpTU0owKzk2NnB5aVBrcDlmK0w1WmhaVW9sKzRyOHJ6KwowY0w3MUp5dmE4dmpYU1R1dkc2RU5kV0h1bUdBVDdaUVdwRG1aVXlYVmRBb3BLK08vZGxvUjAvRldNZWlLa1pqCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnF5bkVyb2E1Q0ZQVzJ3aWlyWXcKTUtZL09XdmZqcU4rOUYyYVorb285V2lTbGxjZklUd3pCMzVHbHZjZGFjR2ZQOTZtbEJOYndxa2hOaTJiQkt6SgpxNGx2VnJERUxVaTFZd1dPVkJPV2ZINFhNVjNjbkt3V1ZROEpEMVlVVTFka3RZUzZFdzNOUjl4aTVLdjYvM0lFCkRpZjRSVTJ6OXUvQjArY2hQK09VRkhRNW42d3l1NTA1aVBZdXUvbFMzWE5mZm1TRmU0SHlaRzVaOGxDSjE5ajMKdzJiZUFMcnNmS3A5bUtPNndNdWMzTmlsb1EvRmlJdytTUEJidEN3YlhORDhkSHZDTEdlSmVNUzB5YzI2alEvSQpreUd5aytLSlhmc3ZYdnRBN1ZwZ2F2ZUp3RERzT1FLQytMSUpvQnhYY3JHUjlvK09MUXBsUFE0bG02Tm4veHU4CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMng4QlVoSWlVck9PZ21rZ1lnaTUKeU9yRkdLOEhQa3ZsLy9nakVLejhWZkM2bDdWOTZxdWNEZ3EvZDI4WEgzS2hIendjY0lkM0x4TEdzNExsK3ZFegpsVXVHV3pxQVhZTHVRR0QxWStPQjh1bFBpbmlSWDg0WDdNUU9DSVlVbmx2TzdFSlBBVlVlOHZPLytPdXczVDh6CnU1RThmcFpVdkQyVmloSXNDMTh3bnZJOVk5R20zbUY4QkhXQWJGMWhGcDJ2cU92K3hqS1N4dG50dUdIZmR4ekkKMFljU21SSk1uNjlvYmVmaXRiU1dWYzNIa0VPN2hPaGJhS1dDVDhkRk0rdmh4di9kV3Rjbmtja1czRzc0UkNNMApwQ3BqSCs5T2hTbTBZZ2srUDN3T1VPVDJiSnJEZGFJWFQzRXcwTkV3Wm91RGxxZElRNCtIQWU4UkprZDFBMVhNCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXdoM2JxRUdLSE5LYzVWV3FRTTAKdE5pcTRVbkkxWTZUWkFOcHNhbmRXdVhZR09HMCtQczJPZzdqdVVBbFNUa1dDU28xSWl5dkRUTnNNMTBvUTlhNQpXdGVKRW5HS1hDL0JpL0VyQkZabDFTNVJBRzJzbndqSkhxU1BpUWQvQzhqTFBVaVhaazcvWkVjM09WZXIwMEMwCkpmMFJ6RUNQREdTSjVWR0NjTy9ESmJ6YXFoV3BnRnlqbDZBNll1VTJaYm9IY1B6ZGRvU0c5M1ZzY3hOMUp2UXkKenhyT2twS0x5OFpVYk0xVy9CTnBjUGJJRWE1YmZIN3BVZFV0YkxlT2s3VWxiTmhZbWJsdWgrcmRJcGsxLzFpTQpNSkxCVEFjZFpLMk9hbDlsYkUwdnR2TVVVdVFrSzJZaEpVSmtuUTFjMUs4dS96YW40dktvTFZ4N01kWC9tOElJClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk5ETjJQTVpGVWVBQXA1bmZJU3oKa1piMUZndk1Edm4xUVVoLzZCTDIrVEl5enhJYnBrVnd1bFRCS3ZxbUcxRmQ1SFJyYjRQTTJoQmN1Y1V5WlZpNgpBK2dGVk1ScjFsYjN1a2hWamE4blRWSXRUMHB0dHEzbEFrd3NTaFMwOC81NklRMnE3dnZPQkVMd2tUZmQ0L2hDCmdVQW45YzdHM1FId2Y3WUpWWU8rUEhEWVQ5K2dVMU9xRWZsNkg5Q0ZDZGoxNlY0dlVZajFDbTBpTmJHZXVjYTkKa3FFSlJEazZ6WTFGWWFsRTdybWU4V215ekdPaUNiUCtwck8vRjZBUEJ3SUNiUVh5V0hJclNlOEg5SmF3U1hlRwppRlo3OWFseUJxWTl3UGV1MGVHdUpIdS9ubXd1VjZyd1c0S1plL29sT1k3QTNNVGFsVzdNbG45b3cwKzNndjFCCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW8yY2VTbWF4Y2pUUTFJeFhlUFoKYnFVZEtyUkRIUTBDVi9adHQzMzhQaDRNd2QwenNKQzZrZXRPQW0vZjlPNHlXbVBDMytVNnlDcjI2OC9BcktnbQp0VWFvKzVYTDI4V0JDanBZUnRtNTZqV0pwck1WbGNEU2c2LzBrT0g3ZXVCaVNCV2d2ME1xK2hxS3VZL1VZc1k2Cmp4Qzh4cHYvdmpVN1lCMWlyNGpOSmJwYzhtSXNET2MvYTcydHhHaDUwVUFhTzluUnpnUGVGd1RVYXJKS3ZsYXkKY0pvbUxJbE85aTZZRTVzRzFnODdTV25LNk4ybDZQMG1SVkN4NTBsbjZpZXVrVHBXMzFPUnp6SEJXbzRlZ0JqaQpKeWx2NCtXbFk1dXhMbWNzWUpwMXJqa0NOVEwzcUUzeThEUEZMZ3dTRUMzYWxMdmtQbW4rUDYrL0ZKVEJmelRDCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdU4yYmk4M2ErcUFmQURJd0V2QWwKN05EaE9wZW9EL3NCRjdIZU0vc3liVEVRVVNFeFI5REtjbFExY1grbjUrdGN4QlJMd3pKS1UzRmJBY2RLaDV4dAp5dmpnSGRKOEowbm1yemttTmpGeE5ZdDBEZUxRWnFnRnNWbTVkakdhU0FNY0dvcDNXVmg2M3daUHozczBvMmJiCkM1ZjZtVTU2dGJIeEUvV1dsVkgvVVdHTkRKd3N6dmlFMkhKdWVGdzZ3ZlRWS2dDRVpnZ1BYSnpPellUbXU1UjMKb0p4ZFBkMU5KdGRiL0x6dHhYRzVFVzFTakdpNzBmYUpmenpEeElvR1JRQUFyRmk0Um5CTm02bGtZMDkrUjg0Ugp4ZFVnOEx5MHZXeHNzbEdWME5DUnluUVJIc3FZQWFxSHgwYXpUWUt3U05reklBZVQ2ZUllQ3Bia08xNUhoL1hnCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelZLL3g5R1dINHhjNlJRV1JCZjIKVHcvRGUwSVNERzM4L3hsaUdJQWtCeFFHSGo3dUpoZ3hPdnVjcUlnUWFuWjNJcHhIc0MxU3NoUXllcUVMV0pYUQozRkFJNnhFTVFJSEIxeEZKZUZjNWZ2bEhENlFoa0l0R21CdkxOWGFEZlR3TTFpbjhmTy9LVjYxUVdJOFo3N3dQCm1kUVpySUxaRmhreTJxU3IzK3JGL2N5R2t0K1JGTjl3RXNqaGlXNzBXdmhTTlFoaVo1b1haU2hmdzVNVERBU0gKS1BhZFZVZEx4eVkraGNORG1wSHBiRnoraytWVnBMUjFDTTZmTzJqVUtNVENrSWFjS3U4OFc1Q2dJc0NINC80UQpQaC9xbnNPSHF5cCtncTQ4V0xHSlZVMkp1L0t3ajJhK1V1K3ZLRlp4ZmJ4dDRqR1BuRzhNb2tpTkdEYy9kOTJzCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGZUbmhOdjJtYWpiSWRoWnloT1AKOGVLTXVRUXFFRURsOTdvbnYyNC96b0k5dGlLcE9QUTRMc3puaDRuL2haUWtpaTJnQ042QU0vZnZoUy9WQnBWOAprUk1laTQ2QmJoTk5kVkhFb2krQWpKVEZtZm1yRGs0ZnRpWjV1MFFJL2Z6K3gyeTdVS0QrNnRGMkVJU0x3SzN2ClVmUURqU1EySWJsM3VJdjBFcld0T2xMZGhxcVhoQTBha3BkL0FYQSsrNm9QeitDL0t0dVpraDM4MGVaTHc0b2gKK2tWY3puMUN1ZEh6VFdmZ1liTkJzdytJcmd6Ujk2ZWRiNzBIZjZFL3d5WmRZMVRodXU1dU4rcS85WG1VbG84bwpRS3Q5VlpjeEtVTG5JSnk4Z0hEYmtQbWpyaitFdjJFdnJhNjI1d0p4MVpuSkNGSGR3WUtPTU9ZZEt4b3RXdWUvCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1pweGw5a2k5SWMzOUR5S1FEVTMKUUpWd0hQbVJXa1JxbGZJTDdFU256YzZhaG5iYzVXMTVnd2htNFB0bGF4ZVJiUHVTRGIxMlRLN2s0N1Z3VHVYRgp0OTkrejQ0Wjl3bW1lL01ERXc0dXljK0NURlkxaXNlMmRjVi8yWXNCM1VyVHAzdjIyd3lLQk5reDQ1aUFSeUVhCmVyR0tJckRsU0UvbU90YWdETi9SWElTWnFTVjNWQVVzY0VPNzIyVHVCQ1hXZkJLVHFiT2JGaXdrMTFCUFBoSXoKK0pQb205cE5kNVVVbFZYKzNGTTg0NTQvdFE2T0xjV1A2elRzbko3djI1U0RyUHlMMGdQcWJrMnp4MXMvcXN3TApONC9OalZES3h6c04rcnhIRDFSSmZudkluWFdFSzM1dDIrdnpPaVVWd0FaUE03eUs4TnNJMkF5T3JBSG1VVXJiClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjB3cGVtME8vYS9XV0hJYjdvRG8KM0tYc3F5UC9JYjFFekJnY01RVDBhVEVOSkVZVXZCUWhrazUyMG5hUW1qSzZrcVByU1drOXJaeFNydTNGTGd5MQpjWjhVQmdjVE9NQ3VpVUdhWWtLcW5aOVNXNHY3YW9VTXpPOWRjYmpudFZiVEJvK1ErVEU1N21RTGlaOGQ4TUdXCnhNbVE2NTJ4TVNGUFRuYzdlaGZTaXdmeFRsSDUyby9JaStySUc2cGdTZDBudGdwSll5MzlJRm5GVitTNkZjNkkKdHZjYURqZnI3QmpWQ29aSW9lMWg1MXdIMU5CRWptU1FQUjk0b3NqcGlKeE1ia0N5eWRISjJESEVuU3ZiUHMxdApzb3YzbzBJL25MamFWSWlML3pvcDBFTnVOWWROb011blFpZ0NJeXJlNklibFF3dHZyMG5zb1JNYS9QaVRVTkpWCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBKzJsMjVBSFZHQSsxMmEzc0tvYUYKdkl5MjB3K1VKSnUzQUNvOEdCTW02Rys1Ui85MzVkRGN2cUxvWVN6UDNlR2VhU3VpdEJQemxsT1NISUlUWXRWVApSS1N1UkRIVVpnM0dhSkQvamxmUENnMmdwZnhiOWVQenBEaDdYNWtFbjlWbllvT0RTVkpTQTI3NThJdXB3OXcrClorbUVYWjgyV29ZM3ZHcTF2MzFPS0dNLzRwb2I0aEpLRUloV0pNSDJOUFhWSVRWUXdNZkIzZkQycVdaMlQrMHEKWlNPcFNGZlhIRm9sZnROczJjOGdFdit0OFhscTQ4VEpXclc5RHFURjZPd2trOHo1UDJTUnhtR29yOFd5WnJoNgozOGNEWjUxM242a24ycTdYTjVWNTlBbXNzUFhJaHRZanZ5bWl3dU5IMkF4MGJweFFjYkpudythYTdkcHp2aG05Cnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnN5YlRYTVRFMXdlWTM3b3NGenUKaUZmQjBEditFcmw1N3NqNVFQVTZoM1ZETC9RVENmS1lBbjRYckNTcU1CdGo5cEFhNHphU0FET2NibGp2OEVISQpwR1M5UWRlV25lZDZxb2tDNkp0UEd1cytmV0lkSnNjblJxRWVVVDhXU1QwZGY2Vkg5Wi82QTNqZGM2UkdaUEV0CnV2b2o4aVB1ZU9UVUJ4UG1RUC93a25VMm91K254UlNpaCt2V2s1QVg4eUxEWWljNVZpTGRYZDZraDJGU2dySmsKM1BObWJuSnZ5eW9tSmFZTGpsMnZlU2pudVQ4S2prUWxzMFRqRXJCVkZlcXkxMjAydVZiVlM3MXZGUmUrcnlGNwo1amJUaFNJWHRPbkpBdW5aU0p2cklsZlNxZjc1Ky8wc2dTTC8wMFhrZFhSUzNwb2V3a2E4c2RRTkdRbnZ3aXBZCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczZVdGRwWjVBREFHcFJVdUt0MzUKcVRTSFB0M1VkaFFLV1FhZnM0QXdoSkY1TWRUMFR2cnVyanUwT0lNVDI4aEV3UFluZVMrbUcwYlhVSUYyYnRtZAovaWY3YjZ3V2IvQTZVWkxWWVVUUVdjMlU5M21GUzFGYU1HZFoyODBaRDRneDg4dlVlYUJLQ2Z3cHNTNWlPdXFFCndFN3NqU2w0Y0d4aUlrdEdSME5IM2N2VTlxNi9SMWJvYXJVeUdaMjJaeU1QZTFwbHV0eWM4L0djWjBGRVE2QXYKc2ZHajZKTmxFQmgxZk9DOUNXWHB4dkhDaUdHaHBVL00zVDZqTWRPYm9VeURaL01uczlIa2gzelBFN2lNbFFBOApmdG0yUWlKZEZ4aklxUjVTcCsxaDF3eXQ5MVUvQ2N1eVFMWDN4WGszeTUvNEdnVUcrR0xnY3J0Yk9wbkM1R2R0CkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeElZREJvU3lkc09uREUyYkVzUzEKNHRqVkx3V3NXdVI1UGlIL3dtTEpmYnUwdy9sc2xUdXNHakh6Nzd4amF4d1kxQ1pmWTYwMTVhbE8xV1ZIV2RpYQpFZzBYdU9tYURKcWxFM1g0RVRuWXgzeGJVenA3dW1nbXZGVldoWUNGZnJ5NlFrb3J6SU1naFlyYkI1cHlkVVdWCktHY0xCNmx5dG8wYkxoODgwYSttbVRxSFEzbTlkV3B1SCt4T0NQc05SZXNJODkvWWN3WkJMNGFWdER2V3hxc3YKaVdTd2J4RWlmRXgzK0ZmdWUvcHVjTHFaQTYweGx4NWxlbVVEVWl3UTJDeEZBci9TVndROXRKdE16TG1FUlRYRAppbXNOWHdLZm9XKzhBSFkvc1Y1cEZRcUZCUmpKSWVlSjZmdTNvWFhuQzVYZStTVXNPblk5WForek1MWC92T3FVClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckpTMkplcURRU3hLVTExdkttZmwKQllFcExhRjAveXlUQUZoUVJ0REY3SVNBODJCWFcyNnJWNjRWbWtrcEU1ZUNOUGZSQ1lQS2ZYdG1rMGF6anAwMgpZdGNudUZtdVA1N2htWmh0Q3ROUFRVdjR3d3NIUXI2L2dSNjExMlAvNytkZWVLRnNvS2JIZnYyVjc1dEcxTXlyCkVTMUpFU1lzbmxnRXJVZjFrL1g2Zk0xRHc4UVQ3T3NZclBNRVBodUVIdVc3VnV1SWlzeEhZR21WR2J0T0ZERkUKeGdNUDdTRk03UlgwdE0yWVFvcFV3ZmVTSmkzZ0ZkRjRiMi9QU3lnY3N5dEpEalpKd2JidEhCYXlaaURhZUw5SwpUWGU3My9FU3E5Z3FCTU44S1lOeEx6OFVGNmxHYWlUdlYrVHFvcGNGQXkzazJqSFRyaXBMVkhydXRpVngrMlR3CmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1U3U0Y2SGlReGFCMldGb2tiSEgKZnVNVk9NaUkvV1B0ZFBQdGpqck5yRkZnUWVlWmdJOG9yYlA4TUtNVVF1cWY4VlZYY2pVdEpEWVhVcTBtb0JORApHSE93OFhyV2kyQ0NsbGRqLzc5Z3hiYnIzNHNxOGtST2t1Q3FQS3VRd0N5YjBnU3JQZ3pOK09Pejg4aCs0ckVJCjlYSEYweEc1VHZ3TDZQMTNkeUUvL2hFczdReEVyMDFCUFkwRmJYZzFDbnhWUFprdHorUDdPSVJtcnloMUF2TGQKaHplK1hJZWNtTzV2MVdyVTVkazRxZUt3V3Q4cDFIQ0FaamQ0c2tEc3E1YlRFdnhPTVB6YUtlWUMrdG9QQW5FVwpWSzYwNENHN3hud2FydDJPT0J4VEVjT09PMW9YWk1jVnZacDdiRXc0SVhjNjVraGxZSnlRZVhCcUpVeTM3UE5HCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG01R01jSkJEWU9WbGw4U2thNXAKby9NeUNIbUl5T1hNYnJjcTFTNFpZbVA5WmpnUzFFaWVIbXpwMlhrNEF2dHh4aFlxN3AvRHk3TFFnK3h4Ymg2UQo3QWFUbjFwUFE1L1h0VnpjNU9ick9rRWg4OEdrRE5sb040UWtkSVBQUG1ldVkxcE9HdXhxeFBEZmdlTWVmeFVsCktSTHJ6M1pnR2orRjJQOUNuYlk0SWdNTUxKdDFwcTZCNkFvTG96SVdQWjZZcnJwVzYyNlRrUU45UDdZSWt2dUoKdGozUWhqRmhXeW1QVGhUdEVzQmlqeHJFRHZYbCtUTTBpbW8zcUxjalFVT2JRZ2ovejhCY0pIOGNQaGFlNGF4VwpreFVZUzgwTEthQ3A4V2dhMEpWMFo3TXRZbFE2L0RObWtKSktTNUN0cUR3RzA5elJnR3NWREw2bGdUdU5LSmljClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk9Fd0xXaXU4SXowdGZLYlVDME8KT3JZbXNyNEYrb05DUnRPK2MvV3o5R0RRdkhyLy9PSDQ0QUxEbnN1NHlTZitONGFLRHhkdGxXOHZqRmFRbFlMMQpjU01oUHdRNjV2cVNMTThhdFVSQVNCR0lJTk14djVqbWFJQVNVQVhuWHpLak5Lek1FMTJoTkxzVTZVZFVoMHFsClVCZVZRTGw2bHJqY3JRenJkcjFtOUVySnl2S3BkeXNPVGRjbFJST1lzTGlwTjdxblgrbUpselcwWE5NWmFxUmkKaUNZR3U1ZFdFWUs3UFdvc1FLd0VzdGJ1ZGZDY29PRmNLOUtHZTVPMHQxRUFPeHYrelJpWHExZElodUxGTm1lOQpKWDE3bjAvYXpNbU82eDAxU2NVd2lLSEFPYXpoNmVCaUYyRG96WnVJa1VHRlA3b3JhcXZzazFTMkhJNjYxMHNtCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOHpDb3JsV29TV3dOcVVzWHA4aXkKeWE5NjF2UjNkL1B6NmZidkx3aVM4T1hidFBHTVlDaHk3K0NxSDZRa25lK3BxMnRqaThCeXUxbVNUUzcwbWpITQo1dE1zbzNHNjkvdnVqcGNwNUZKVHN2dVRlS1B3RE9XLzhUekRmRGIvZlJQVnN2K0srdTd0UStFcjN3eEN3LzNTClo5Tjd1akRUTDljZ1pQc0h6T0JlQjBFSHVkaE1UaDVzUWFVMGh4RGVjL3c1NWF4WThzZzB0M2lUREVPZWR2TSsKK1dKdU5MSTBBZ2VTa0NLSlZWaExFYm1NSkhYTGkxSmFpZHRlVGUvN3ZFdWVhVHNZZzlhYXZjZHFqWDhTdnErbgpTVWJMMFBQUEIyeEt6WldpbS9SQTlCWnBsL0t2b3BhdmtuVFpNMXpFRTFUUk4zUkU1ZjlCZU1hNmMyM050QXg3ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW9IU1IySVZZVmlLTFBwVktJUy8Kd1BrTXVtMVowTktQek90N1BWMXZyS1RnYmtBc3lBWlFvdnl4bG9xMEFMQUh0R3RJTFdlOThTQWRzVTR4RUIzYgpFV1BXSm5YeGRVS3p4NUx0eFgwd2pyL2ZZUU9KRHhsRFgyWFdKSzBmRmNkcTNCODl2K2VwU3Q4MnF5L3VnZmZTCitMRHdUYVYwcUsyazRkQWh4NXM5cW82bEVMTTh4T01zYisxOVFOK3UrT25wQ25VOWhkdElrSy92MXFSNUYzV2EKQ0R2c0prVzB2SFFsWnpOQU9uSFZQanhIZHNsWHB5d2lHUDRXRk0yOE90Q21WMVQ0VG85SmtLTjZqQUdZaFg1RQpUUWhkWnF0NjlCdzFpZ2ZseiszK0MwMjZNWmttUTNOYnNpWU1uRnl1QWZzQzUwaHlKZFc5SXF2OTNLUTZkY2V4CnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXhBQVVGOGE3dkNJampIUDVzM3EKTk5hK3NrVVVwbSsrQkFXbXplU0RLd0Z3Y21RVmNES0ltS00wVnh5a3BlMWY1a0pjRi9kRFBkWHA5ay84ZmhXcwo1WGZlUXBHcmNSMTc3TDA3N1lMNTkybFZmaFlIRy9ia3Z6cXpTSWYrYmU5cnpqbXhDbElnN25jTXJ5ZWoxbFI4CnF5QXdhSThPeXBzMmVXdzFnYzhIOFZMM0s5Q2cyQkpVYkxvc1IvOVBGN3pOdFF6KzErU1JEUDVXVTZtaWtFYWkKWkpOSzcxUzFaT252UEdTK0FCRG9nSE9NRmlUQTRSaVRkWXR4T1drMTErUXR1OTJkVmFJQUdFTHkva3RxQWhpNwpRMW5paVV0Um40TGxldGVZNXMwWDZvdzB3dkgyZG1hbkNQbEFoUXgzRmc1SzlqZmxWaVEvdk9lZkk2clNhYndhCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbWxGdnBUVkRQZUovVTJnU2JGKy8KZk1nSHVjRk1jKzJJZFBPVDh0YkFyYjVYMG93ZXJsT1l2aFJUeC9JY3FCeVlxcTA1bnQvKzVUbGd1dXFwY3VaWQp6bTJSQnhmaUgwRkxCUDFrVHIxZFFac0hjV1dtRnFlUXpxYWlkQnJtZGF0VlNNWTh5M2IvSEFucEllZ2pWR0NJCjNoQWtzM1FWekkxaWRqZGhOTk51ckovc3daWGtMVVkzM215Q1lxajBrY2ZCTXFWay9KWmEveTNxTVgzZm1EdmkKYXIxTURNdGlJRjRyNmhkaEdHN1UvSEJpZkFJejExc0dFa1NHaUJEUkZIeXdmWGRkeDQyVFN2aU41V2dxZDl1egpuejJFRVZVMTZpVmNKbmk3S2RNbDZEa1NIYkRpbGNQejBPMkJmUEo4OXJxbzJnZkkzeWo5bEorU1dKQWE0bkhJCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBby9CL29paFU1MWlIOVo2ei92dW4KbkpZSUFMQW5xS3AyQitiOEI5RnZKenIxdHVvemYxQmJkeUZUZlFBSVJRb1J6eUJteFFkRHJGV2QzQjNNUVE0ZQpua3FKcHRZaExlM0xuaUhWZ2lld1dnN1RqbGFuZGdZcEVRdTFyRy9qUDg2Y05pa3RkdFRVYkVLemZtV2lrU2I5CmRaVU12QWI4UC9iZTB2S1JtbE9iUTFwZDF0M0xDUG1HVzRIOVlrSE91RWRidkhXZGRkUlhpeU1RTTJONStlT1YKaGsxamt1RDBpZ2JMblBWbHpwenRYbjRLN2dlTDFBdU1wbStBQXNON1VVT0plUnU2cjdWdHhobnIzZ3dTcUNDZwpvNzBjZ2Fqa1VoYkJKbzkvcEJSbjVWSGU2WW5mRmZCRnZSN2tVOW4wbkhmWUNGR3poWXVMdEk0b1UycWtlalYyCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXRBUHZncXpxWUQvaDlhQkRGdGgKdUlvL240dFRVZzN4T0pCMW5HZHNKTzJiemJudUJoY2h1Qm45SUFBbGd2cnRXSGNyd2JLVm9ycUhMZEc4dVZnLwpWYXc3U2F1dXQ1ald5SEk4OGh3SHRrUnYzT2pqampIeDBCVHRVWk56UjhnUG1laUw1SW1lZGxBbzFXSWN0cXZoClBGS1N6Rm1OWEV5dGkycW0rME1GSmJvR3p6VGJ1SVJ6NUZjZXhFdVdSUnV5MlQwZlFONnlMUU41SFBmSkM2bVQKWWZqQlQ0dkFtVGRjRGl0K0NRalhkODlRZGk3L0p0MDU2SU1FNmQ5THdEbS8zTUJTa081UnVHV3V1TXExR1VKcAo1TkUvcGlZakl6ZW5aTjhJOUhCTXVHaDlZcE56WlUxc2FqZWduSkY3bmlPUGV6VXpEQVhuYjdITUErWjh0dnlICjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1dZYXJXa0hjY28xTmhaTjdhbzQKRC9WY1hvZEgzQVE2REFJa0RoYUgyWnlzSFA4V3hRd0ttcnQwbWczbGVNZDNCOGFoclNFcE1SQURnZzVmYVZDcQpHQi9zYXhWUXQ1ck9FaUV4UWhUdFd0NzZjdG52VWUrRW5qa0lvb3IyUXhnTE13VEZDNVU1alRZdzRtbG1MR2FBCmRNNGFoZnRlQXNWMlpEM0ozYjlvM1V4ek9tYzBodEt5V0JuV0RKak5DY0lVVDdINDlXR3BIM2VmK0FkcDVLc1EKVDhPcFBtWWNLTzU4dGY1aGdhaTJFRThvenRmN21vRTNFTEpPRVBYdk5LUGZqZlhEZGM4V2lYUEczWlVFUlZFVgo4dDcvTE5rQ1JKSGdGbzltdVczWXVqMXZnb0kzakV0d2xlL2tLT09zL2NleHVSVGFzK01WVlY2MVdUbExqb1ozCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2xJRk9VekMxbnpvTDUzRFFLNisKdzBIMWx1OHJtcmVid1FOL2FDUG5LSHNoWXRsNDNid2hGV1JPV096NG9lZG5BQU1zYWpBazhSWXBwNDlpTHF4eQorYklQcXhYdVQzZzhnd3J1bmYrYWdYR3FHUnhlZWc3QkZjckJtNWZuOS9UWGRjV3cvZ0JFVFlyNTVPUmM3dW52CnRnT29NUFI4eXhxM2plOTVJS2JtZlNFczJGRkJyWFdPYmZnWTU3TVR5OG8wZnAxRjQrMUlFQnIvSUM2OGRXYVgKcTE4TlN1WXdGOTExVjZwS2tIblpQRHA0aVBxNVBVbWZvYndwRDFpRE83YlE4UXRLYStNd2R0dVlyenorajZmbwpqVGFManI2eEkvUCtSaVAvdDk2YXA2aXJZd0RPRi8rZzVjcDdWdTRhOHlld0NoeGJ6Vko1SlNrYldhTlV6UWxaCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTdHcllKNUNiVnlJbGx3NVlYeWkKdFh1d3Z6ZXA2cittMHZVODdHV2xxdDBidW8raFJiVkE2ZVhrZTJ2akRDN05PT2k4bExWV29GbVVBc1FOZTdUaQpWcEQzQUpmUTlqazFKZHdiSGZtZnpkc2pkYXdteTYzY29HdEZ0VE1xRVVSVCtua3VGbWFNN0QyR2V0WlZVKzVICi9LRnArVGpTMXNMY0xGcFFsWXRnTEVXbGNYMi9PUzQyRG10ZUwzeXJOd2diVnB0RDVTWmNtVzdxRkVKemVXMjAKWkR4L2duK1k5UWE3RlVkUXBnVTEyTTBUNUxNMjQ2d0N6S3FBWVFuVmwyaHBZWVE0cHRTZmdLN1EvTUVzV2svcAp5Uk5IM0xJT0VqUVd1RXU4ZlJtc0ZCWnpVY3pDbjA3NmlsQU9vdG9zaXlBL3FHeXM5Yk5TaGZ2R3o2R0VJODRqCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkNWMStBN3hKZzd0a0dDT2lxUDYKNUtsSmYvOXArNEJjSkc0KzNYL2lpZ0g5S3BXeUlwY0VQUWtyZnV6NTBXNUZWc0VVenZwYUtiYnZWTENVSGlaSApwamlwQjM1aWNQQks1UmovOG83VDRCbDJ3cGZ5QWxXa21GcE5KSk9VcHZRMi9XYlRIQ2N5bmlZSjU3YlhDTVNFCnoxVmVRQzh4UzRUR1l3V3ZQWUc3NStwbXFwNjJTMnpZUFhSenVDU0l0Rm56ajFzN2thbVh2bGFrSFFyTXZrY2gKSDZBM1F1Q2N4U0pOV3ZFV2pCVWVOdkVrMmYwSHl6QU1hb0dZa3RFcXU5YkVKbXk3VnJJOGxiTHNabzRWelRRZgozQVJOb2JKaUtmcURpRmZyNW9WZ1lFM1ZCNXpkUE5Vc1RDdlJoNWU3Y3pOVTlSUDVHQnZFMGRZV21KSzE3bjNSCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0VOajFueGVZNnRtTnpkVXJZWmsKMGNjYjhYZGxlUC9pclZpbTZsbTFRK3JqUzEvR085WCtaaHVMRWxGWUlOUit2dUZremgvTXdJQ0FzSHFHU21jQQpEL2h0UmFBNUNZTXZYck1Zd2sxbUkzV0Z5by92RGt6VjhuSXMvZFBxL09HTjlMc3JTNnpHUVhIM3JxQkxTZDNGClEyQXdiZGtyeUV0MVREZzdOSm80Ym50UW11dEtTM0Fic0dLT1FsZWp3NHcyTTZ4N016Y1pOaFNRRW9zbWhKQUwKalBZbGdEMXdKeHB3RGx1c2I2Y2Z2elptT3JmTVFtTmxkL1lyekNmWjcyZ2hNWkRUTDVHOVI1eks4aEdxRmRtRgpBT2lWWEw3ek9tTjJXZU5CYnFJTjIxdjgvQkJsbG9uNTVuZVQrRWtVaXc4VkFBaDJpUEg2bkRZNnFjQ2N4N3JHCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlVyNEo1aG9qSE5NbER1UG0yMGoKWFlKOFFQZWNWWk9JZTQwMDIvN29SN2tGektqTE1lZjl0MWpwVTU5NzE0Ujd5VEpRYVB5SUNqT2lqV0RySlg0UgpXYndxOUJFcmMxRXAxODRLNTF5T3VNNVJjcE1ScEVpK0FCeUFpTUZsVHF6RzVnUHY2ak9mZyszQ05DMW01aU1ICmtGeUlLK2hNdStLRks2YVZlTUwwQUQzZ3dUcW9Ud0VjbEQwR2Q3dkVGWlR6T09pODYxV0pMQVJOVDB1QkxwUEwKM0JLN21OQlFxWEZwb253VXRsS2RITlQrekZnbGg0aGtDREI4S3RJS2dScnR3b3pxc01XUUhlUlh0aWFoNmVxVgpKWmQ5TkhDWm4weE5iOXRMeVB3dndSZGxjSWVBYWZScFNkQmVTUEhDenhYQWJYdU1ObU05dFZjYkRydFdRcUpoClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbEV4bzU2T3hrVk9BN1Rka2FlUVoKS3JSQ0hvQWxuNEtLZnNoSmpEVWNJVWJGeXFaR1JqOS9TQU5uRUZHbnNLd0ZLQ1d6SEZXNXFsUzlIa1BibVpJLwpWYjlmOWFCUFF6YVRZcFVQQXdkeVhiY0YzZVUxcDNkZHdSN1RmVHNOaVphNDlZNzhsd2JJaDJ1T1RvQ3k2eXM5CmZRWVgwOFB2TWdJd3JQWkZFN0h4a1M4ZmdkOE1CMDE2Q3dDZzB5UHU3c09aeVBSaTRnZHU1amNSRk0vMjdMWFcKTzhQLzZGVUg0M0UyUXIvQkd2UEd1cVpMVGFWZGN5MVBTdVUvbHR2WEVjN2N4aS9WMU1KdnZBVnZvcXZRc3oxZQpjMGhWbTY5UFUvekRkaDV5T1pyaWN0K2RkMUxyU29kN2Z5SytTUUhyTTRVTFNHSm81L3hST09NbytvaE9kSFVrCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWFnbjBhaDNaNTBpN1dqSXlMdHAKa1lFYVBwZ0VOdjJjL2VJUGFGcm92UHh1blVxUWtIdUNPbG5xS2hMajRGRldWRkNLRGo5ZE9CdFF5R3R0QVY2NQpXREMxWlkwRlVod3NoQXp0NGxaQTc0NnVCK1dGQWd4QzBHSjNEcmJSNG9GZGNRQm1ZOVArSnNDZW1iRFBTVklyCi9ZcmorYy9qQU5KZDBKNmxld3V3WVB5bmRwRTQvMjBjVTU4MGpKTGdWczAzd0I4MDFYQ2QweXlrVEE2b1lzaS8KSEtTTnFRRStzeDlCUjRNeTJhUm8rSjEwS2pnZklmL2hJbTh4SnhEVGJDZkYzUXlrb3ZyWXVmS0Q1QWlTaWxIQwo2aU0vSVBZVXR6eHRUVkd1NldZZGdFOWFDVXZmaFlLaHdFTmVhWkgxYnpyMFZSQ3M4MFB4K0lMUlRKc25pbkR6CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3dvWHNYTjJnMzBGSFJneENHM3kKandXWUpMdzNZT0JWM3JFeGtsdDVPZjFmMVk2NENhay9uajRiUzc0dWxVMmMrbXlMbnFYYnRqZWtxWHpZSFdQbQpWWENZWStma29rc0hhUmpvWWdiVTRmS1Z1QVM1aTJCUzJmOVR0ZGNWaTRUeGxYWFdaQnFDNTJiSzhpcVZ6N0R2CktvWmozU1QwM2J3SDVIOXJuL2trUWIyemljcVZxa2xiajNFZ0UvTndiZ3JMKzcyRXdtbVFiY2hjVVdqVXlNU20KWUdRVXFmaDNrVjk0aXJTbHZOWDhBRnhxZ2NiWmhnRzhaNDBXM0wwZ0I5cmFibG9paUkzczRnckNsZ1oxaE5OTgowbW5RckJNbC9OWWJoYXJhdk9Wd09qL1V6K2dCM1YwTzlPdDIvQWYzUkhhUHR4Vm9sbElGTlJEYkF1RW9xSXVTCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHZyOVRIclBTSnJ6MmNyekppMlgKOWRmdXY3THNZVVdldTU0dnVEcU9IdmZHRVNzT0hxS0VHekZrMVBjell6Z3J0K1NRMHUzWDlBQmlRTDljNUdyOApXM3pWNWRSNmsvV2VGMjdqQ0ZuOWdUWlV1Wmk4Z0hlYWU1WEVLY0lia3h6NTBMalRMQ0Z1V2poNnlKaENKMU1iCi9VQ2QvSCtnY3FSRTg5WDlESnRVMUloUHduVDJhbmZhVUtObUhndExmaVNGb3VjR3F4NHpSdjZYdlpCU2ltU3IKeG03VGpXckxXNzhoYXRvSnh0VEhHVVBEY0JwTnZ3QmpaZ1gvSnhiNVhiaGMrQkNuOGdmM1ducUM2UWNNdW1NaQpSUzBzbjQ1c0JEQjR0T0NXQ2RTMlVEWHFZUzRRS2FPVFZabWd3MGFuSDZwUTJ3ZEFML3pvUDQ3SU1JcUJMRDhqCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHp0S252Qjh0cGhwR1JqTVNGalUKT3RFVDNBWXA2Q0U3a1V4eENzazVZYTNIRlFlRzJ1OXFtL0Jkd25IZFZnWWZvNHB5ZTgzNTRTSEtzY3B3MVJITwpiVzBJVm9xWERwSEhqaVhDNzAyaEpMMTYxRi9RL09mYm5mU2FabDlQQUhocWdzM1Bpb0R6Qk9Vek1nMHY5dUVVCjJKRTNGS0Naa0I5UzFNZEs5aldOU1BSU0dvWkhUR0tRYU5jb0FDSWxGUzhPQzhLM1BMbHlFK0luanFoQnNYdksKUGNobVVFSjZyQlo4WnI5SWNIajAxOStEcUt1UXdyWk5sTEJIM0lZZW5SSkRaZEpPcTBHbGVYTlUvVG1ETjI2YQozd3JEN3BKK2RZdk1WTHQva3dLaW5zSkJhKzBiL1NWeGczQ29uaGluMVFuYjYxNmxqc0dNaGtvSnBuRHVzVHdyClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFNzL1ZCNUVoMngreWJGVWUwTDkKbnE5S2p3Q0ZxRHlOZkNnbWN3VHRyMm93d2lxaUxpY3BUeVFOYUZnMG00VVA0TEkwQWVDVE9UckRGNUJxVmFDcgpINCtPejY5c1VISVRSM0VOVXpWVVltbm15eW82NjdVRjAxNHJLbmtvajRDdnF1ZS8yS2Z3OVVxS0JpVHdvWkZsCmdQVXd4b0FvcnhpRmhIOXZFcFpPSDJLVnNLS3c5WkNZc2ZPaUxXMVFjMXhDVms5Q2piT2xkRFZqWGJxSFFsWUIKVnNpa0xqVFBsZ094RFZqU0lTL3ArU3dpdlBGM2M5VVNUeDd0SEZPL2xIQXFzVUhleFlUZlJjeWh2bmd6eVB3bwpDVzRpUVdDU1JTVTJNemttdUJ2YXJhbzQrL2dZR09pNk9WRUhvMm1acENtdWF2K1ZMV08vQjFqTVoybmhLZTFSCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGpnZDQwQW9JNHhZWWgyN3c5L2sKbEplR0MwNjRBalE5UmJpZkNaUHVZOE1STG5kV0NwOFJYTUJCajR0K1RGYnBVNDRYdExpWmxua2RNVWVCendLMQpEUDlJNnhnNUVCN2lpUGMzNDdEWVE1Uk5HWlZ4blpkNFZSZExDQ2ZTZDJaK2xKYnpCaUM3UXBHOGZNa242NlpxCmJRaFV6NHMzY1ZNeHZWWkNXVkx1VjRuR2lXb2F3dnZkcWNITU04dk53bzNpSU9CYWp6ZURZbTFkRXVIVmlUcisKWWNScWxIVWh4MnhobytFOGRNMlROZUJSSjYzUmxyUWZRTzl1aHlRLytwNEQ2N25sV0N5anYvZ2VZd01id1R2TwpCd1hCV0RlOGJqMjkzOFhMdlp0MjI2MEtGMHRLRWRTdjRmZEhWWDZrWnpJbTZPUW04QjZlTGE5TTAwLzE2bjZGCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdU5BOFhOckJmbXdEd2hFTFQ1TGkKYngvb0h5WWN5b3BMTCs4dlptZUJVZUNrQU5QRFZtOTRhbGNsL1lYUngzRWQ4WDc2dTkyTkNUV0NNeDI2elJheQpVOGpvME9HSXh6WlJLaEppUUVtT0FnWHVqdVFLSUJjK05icXUvUXRmMWFsc3o2TTRpUDVQZXRZU1padTFyb0xuCmhzV212dEVZWUExZGhFZHlvRnZvanYreGE5a2pmZVBIMUthcnA2WitVaHFZNDU2TXpyMTFmVkw0bHpsMGN3aGsKNzBIMnN3WktxdzlIRVlZNDBZY1hBTzltUGFpWUZoVkpYSkpBdGxCdFFFeW82VlNGMVNBMWJLV0pkQTAvaEhxLwpIazIxMXRtYVVhNE03Wm1aclFFOS9ZZVJqYU5FVlFERlc4NWZiVzlsMWNFVG9heGVKMy9UQmNDcDV0SVdrbS9zClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG8zcS9VTER1TS8zRFFJR0g2Z3kKQ3VWZ0NIZ2ZVbm5Wb3JIYkhPWklJdU93Y3dLTkZuUkpPMjNnNk94WkhWczVJdUhZREhwc1Q4UWN4aVVpQXdXaAp3RktwanA4amhUQytMNkhGSE5IQ05YeElsQjIrK0lrOHZSTDRFRThETUYvczk1aGo2aFJrNUliMHFLZDdPTXpkClpPdktpV1NERGgzZG55dkk1eXQyUmlETFVrRGMrNEhlYmhKYkErS05iSXE3aUxheVh3QmVjd3VkNVlZd2JKVmYKVHd2Z21VQkRIOWxGendGMVV1UkpGRUl1U0tlWlNRMjRiRWc5V21jejB4Q2ZVV0JqcnZjTjYwWWFxT0FicjNSTwpWbjBYL0EzOEVMdytnTEtMc1I3TGpaQ1EwY1BPUksrR0srR1J3YXJYMlcvSzhpTXpwOUppdEJmVmxvOWthaWpBCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFdkR1FIR0tWSGJBNENpSWJTUHUKSzUwcnI2RzJoeUxZYTZhOVVhcis2UEpYUVVYU0JjeE0zSWI0YmJEMWtGbU5nU0FjSDhwQTk2eVFVYUlLYlo0cApkN253dS9rS2Y1czBJcVJpSTVidFJoUmo5Uk5zOWVpc2NjUHhvWmZ3dDBhaTY2WW40ejEvTWpsKzRJbUNsckdsCjc5eVhrbjVlaUJ6ajFkYWFCSjkzSVpKTENhY09jUDdCVnlMekdlZEdzRkJsNXRvaDRvNVk5QkJ1elFEaXJwT2YKcmZHejV6QmcwdGtoclJsN0toTGZJdEZLMjZvQ2U1bjNEditlbjhzSENpSVBNN1oxZ1RLUnlVdmZYdElXZTM1WQpybi9tSXRCZUxjbXdQQVZwSkR0OGRmWm9nb3VMc0xXUnhMZ2dzOW1RMmtLZXlEdzFna1IyN1F2VFJnM0ZsZmhECkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHJhSnIvTjlwSHIyRytXUDJldFcKWnlIVFM1bC8vNEVYOXFQVm1laGcwbk5BQjcrcVcvamFYaGxGdUFQRmNFbC9FbG9QWlU3bDB1UUlrQUpVTmk3RwptL3h4UGExWEo4QmxmWVFxRkpPY25kZWdla3EyOGxjTkI1TnN2YjRlbGd6SFF4MTBHUzZhZFRNWDFuRlZyMzBlCnBpZm42ZW5sSlNSTG00dTJjY1ZvK3VtVlBCS0JVSlV0elpqRUUxQ2Fsek9Pb1k4c2VsQnB0dzJ3MU1YUVlsVGIKUmdncHovMGxJNEV5RkVudHQzMFdDdlNxYTRlYm9ETUM3TDlEU09LdmtBcmszTWJyejlaejVxS25YQVM4dG1aZQpUQ3drcENKQXRySTB1d2UxZHpmWktTZHdYMjNVNHNEU2dkaGlmbGhKWnFhSFlldUVIOHc0U1UwQi9MbW1FRjZGClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWRFZ3kvZVNIc0ZaRDhRT0Y0T3UKTG83bm1VK0JiS0RZUlZnTGp2Wm9YTTFxTnFPa0RBN3VHbTIwbHVidytheFdhNDFTMmRYZDQ4S0xsNmR3SEFlOApZUkZGVjBQaHJBcm4wNUtGV1hPb00yOXZsQ1hleXBBVDFlaSs1a0pyL25Ed0V0eXpYMHFSWU1VL1JkRGwzVEFuCnVsRVhqVnIzQ2dIU2dwSE16ZjJrakdpV2dZQ2tqcUNtTlJZMi92S1BjOWRkRFAxUHFwS255L1pYSVYvY09kU2cKRXY1bkFSbklQTHhJVy8zVmJoUm5wbjc4ZGMrWjBaTllXU0tFS1o1TTlJUUVlcXJZZkhKRW5oYms3RGwvelZsUQowM3ptRjFwMnRKbEtEcHBkeFZHcUl1TjVGM092TXY4K2YrMDB3R1RIWTd5M3VzeWNwVDJJRDVVbFR0UDRIUmdqCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM003dEYrMzFHU2s5RGFDRXhsYUgKcGlySllqK0JZVFFiSGhqdHlDaHhsK3Vqdit2T05ROFNkUjRFMXdreWFFUGxYSllXU1NLZzJrcmxKWUtCbXRBWQpEZkxELzczREMzT1ZvNEFuQlZVTFF0OWtrTGF1L0M3aU9MRVlwRVRLNlVBcnZaZm5HN1VsaGdmY0RZOURjUWpmCjlGTmVrK1ZoUXd0cVM5ZjVoK1NDVndYQ0V4TFpxMFk3MFRaU1BLUE1rNDBDRGFFZ1VWQXRsaG1HYmlFSW1iL2sKMlF1dHlGbDQ4NFRvaWtZd2J5bEtCY1BMb1R3NjQvbGlJOVFXMFgzMXB6WTJRazIrRXh1S0VkeStpVWdDR2p1dQp0TlZIQW4yZm5tc0FUSXg0Y3FoNEhPMUxYTlJHNThTc0QrSGkrblo4dlNJVGNtRGZWTkZ1UjdFZUtHVEpvNm13ClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjNESG03N1dXa2xMTzkwTUp2NmcKeFJDeklmV3N2cXdPS04xaURRbFJ6RUpGNGYzTUZ5eGg4dThWNzE1Y1ZzYm4rbWwrcGpGTll0MDkyRHpTNVRKWApaOUxqQnloRmhLOE02RFVZSWtaSTVkNlVpMHRuYnBVRXZiOEt4TWI5NkRHQ0h0ek1sRVQxRDlFME43OXUxTkpECnZwdmtzVVdNOTVmT3BaZm1zQVp4c0wrWHNRZktzOWZ2aTZ1K1hjSW5OMlNOZ1lPZit0UG9YemQwMXhDWUJBcVYKYlgvTFZlYWt0cUREaUVLaEN2YVI4K0lXbEdDdG5nQjhRVjExcUFBYWtHMC83U3hMUFdoNzlMSEdsQ3NUbkNXdwpuY2hEVXAvaVdRWDVjY1N1T3lDWGQ1akhNTG1tVWNWRm5lUlJPOFo2UTNPd2FqZHJCVUVwaW94M0RXd2NZRzFiClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2h2c0E1d2xxZWloKzVCWXZNUk8KUDY1REwzYlBqZ002QjhCZjY2MFVIdjZUdFF6R2RWUmMxS05RMHQzWG9MWVVORWo5L2RXNjFnZ3lhUDhjTlhjZQphZXM3ZTdHaGUzS3Q1NjZUL0srdmtSUGtvUlZSMnFsTkF1cmEzUSt1NHVqQW01ZlBjSXBiMVNBUlErbmdGWHNQCnpNK25DMmtUMjNCNU9WM1Ewck0zMnowSWlRVTZ3N2ZnZ3BXWlUxQmppQjJYS0pJV2tqV2hwMGFkSVgrUE5EbG0KbTBjNEFwWHJNNFRFd2k1ZUxOSmRQQ3hWcStLK1JVRVF0REM2bk9hdkVrVUdwZE9HUCt5NVhud1NqazNzd0hGdQovTEx5bnJ0bGhOaFh2MnV1ZkJjcVlTajdDSW0rZGp6YUUwVEhsZVM1VEtKL2tJeEhJdWd2d2hvVTBWenBhVEtDCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeElHSDI4VFBmOFNHS3RwNVU2aXoKYXhvWFh0c0JDOU1CRXNJbDJrSGRveGRyREJvNTNwMFlVUWhZaldHQlNsVnFXK3dMWkxmTW51Q3NSN3NaUDJEQgp3ZHNpY09LZmpFblB3TnRLMGVmS1NvTXpJQXhaQS9DQkVJSHJWV21GT3M4SS9EVGxSRjF1K2JDcUZsMzhxeDBnCnRUeWVneVFIS2VzRHBicTF0OW1INEhBaGo4YUU5dithMmpiNXVEMG5tSkc3RWg1NUkvZDhCZ21veVlEYTNrdXcKOVJibEhBRmJoNUs3Z3ZKOEdkdG42ektQYlBwOFhFd09FS2ZSc3JpcE5LUVVkWkphNXFvcGNhMm1Yc1RwTnhIeAozMXZ4QlIzdXF5b1NZaVJLMU9meDU0eVhaMkYzTWlwMWozL3JJMzcvQ1ZnRlNVODZadWpLOVFHcmxobE16djZUCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXlDSlgwNnNTZzBla2tYT1JVWGoKVS81TUFKUkhENU9UeWFRL2NDWVE1K3dJenl0RlNxR3pTcFVxeGpHRktZT1V1SEk1Z1ROczUzMWV6WnlnbDZiNApwRVBDblFQeFhOcGZOclJ1endydUwwSEcwWFdkeWVSSWVRbTNNSnd3QmNYWlZXV2h2ZlRMRnc4dW1HU0JCZUtSCk9XSnRNVTJTa0RDUjhHMXVvY2xnTjYwamJXeHJia0hPb3J0NE1qSXIrd2xHdFF1Qk9iTGpUc1VIa2YzZFQxa3MKNkU1SGUyK1ZHRjdZY0ptYm9xWTBWd1RoODMyVTZtbWVyelYwR2pHNzB5MnBySWZBOHBPV0xtUEdDOFVvU1krRAo3U083M2E4REowenpQNXAvcms3Z2ZkdmRqQ0JPOXRFV2VoMndsZStzVWpTelp2enVFM2pzbmdDY0lYZ3QxRFB3CmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHE1VzBGcGpROEdieHdYVWs3eFAKSFF4YVJ6QkJEOVp5M3V3U3FFWFB0dGVncnQ1emg4c3ZXSnJEN25MajBMbFhsUE11Uk45UmcrMUhWeG91TXZKYwpUdGhQb2U4anhXSW56NmxGQmorbDUya1RXSUFHU1hhMGhYMjZHeUpGUVF0UDgrVVFvNVBVck5sQ244UFRILzYyCkgwRmVsUWtzNkRqVE5LN1lvTlNBZkppYVZmNkxxRzhTSFZIcEp2MFFYTS8xSVJnM08zVVRuLzlnT3J4VDVTcTEKQTI4MlFpWVd5cWhaL2xDTTdCejlTTXdSU1I0NmQ1VXNiek9wRGlLRnYwWkRpR3JEWERUeW0vUFQweWttRmd2VgplcmdYbmRwQmptbk9xbGVTTGo4blVKL1ZNcjBtL1hLS0hFUzAray92WWtCSEE2QTFiZFRGTHIvUjE3cStkaWQ5CndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjR5NnAwVkVZckVjTHJ0TS9TdEkKYmpFWWJOUWlkNCtXZGJya29NYjUyaW9WVEhnMW5TZ1NJM0Z4M2dvdklSTmdNcE5MZXVpQ2pYQWdhcEcrMWpVSwovQVh3RWg3SGdiUlkvczdSNm43eWQwN0wzeFV4eUJmN3VTTDNGT2s1QjVTN09QbXJSR2tEMW9YZkJVVzJBdjFYCkt3d0dBRm53RzR6ZEJDc3dhU3VHenZybXJETHh3T2RHeHJPckJUWVJaMTBZeHlBS2VjRXNwT094T1pmNVRmTHQKOWtTYTltbGVEN2N0L3NDSFRxWHFwUm9BUHdFbnM0K2NKbGpNb1dTVXNoMFlZTmNST2JwTkNjVXhyK2VkTFRZMQo3Q01SbmJQMUlvcnVKb2tjTUFHTmJ2YnVWcUswMi9pYXZDclF3bE9DRjVGMWlQMDdvb2hTcUtmbHFzYjNuczBZCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemhMdHNOaklqZEkvbExaaXFmWHcKOEZEZEF3M0JPYjcybDhPeTJ4VUNZSXpJTlI0Tm80ekVjejJYckhFd1c0ZzRBbFpMSmFLeHE4RWl1ZkErSmZwQQpGK0RXY0tlZDlNRXZuVnF1TmYwalIrSWhpWktPeXJ0cmxXRlpQUjBhVkZ1YVg5eSttL2NMRlo5TE43ajBDQ1hnCkg1WWx1SllIbnA5NkFWWVAzVUh0azVOUHE1ekhUc2swZ2FkdlNZUXJxdnl4RjN1NGY1V2VCelByZDNKWGttSE8KamxIWnBGRTRHNUlDYnNuUjQ3dWlNd29mdDVYbUpzK29VUkxRa1djSGFVcDhSWEpJdzlZVThDK1ZDMDNleEZsZwpaRkt0c25CbzMyK1UwZ2QwQ0VpR3ZFR0RpUlU3NHBOL3NKZTNKU3NlWnk2TWdBZU9PZ1dISjdxakZVOE1ER2VQCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjhmTXpUc3lKNHVZUVIvd28xS2kKUGhHWGdreW9zcW1FRkliTmg4SnllOENWem9Nb2ozcFpoWmVDVURmZWFVSXlmUTdTM2ViRmsybDFUK0dPMkZPeQpjT2MzN3VpTWJZc01aOHdHMkJrQlpKM0pIUzlwS1c3ME5ib1lselJwL0FVMHU1UW5vNzB2L2JSTXNSS1lmSmVQClZ1WEhjaE1URDhwNVJHcDRISTB4cmlwUTU3Y3BZU1MzaXM5OU5xbStvQy9Da3AzZ0lFRFp3WGM1YTJMZUU0WDQKbXZtRk9HcUh0S1RSdnBKK28rL2N3QnZQQ1FGQUQxWVFlNmJmbDV4czludmFGYk41dTBnT1ZueE15Um82d0pSUApNUjJiSzlsazZ5NGhiYmVkM3FQMEx5VS9PV2hnM2wwV0tERGxYUCtPbHo5TkpXdFBpZ1V3WEN1blRGbVE1Ukp4CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXF0bktodUVvY2UrZjFoSHl0bjQKb3RnbmtOaTZOMk51ZzRPYlErc3dPTHJTbTBrc3A5OUFBWW9XWUs3YmZhS3JKeWQ5Vk4vbkxscndDQXlsaHdQKwpTbG1yV2ZzTjA0djYvQUZIcHRyOE84VDF0Tm5Bdlc5M1JtYlNxMG5palZQdXFhVTNlZUQ3M3JCaXJvdERFZ2U4CjhSakdPU1FWcThZZUw3dkc3cUx6K1pMMXBvM21NZTZwZE1Ga0gzekNwS2JKRlVuK2s0SDcyaSs0Q2tSOW5HclYKbmQwbGpzU1EwN0RKYWtTNDZZT2EzYTRoL0IvRnJjbW1hMzBuT2Zhc3E0UGRBc2locmZ6Z2FMZEJhR1pFdVZacgozODNtUmh3K1FNMTArZ3YwdlhXRWlDM2JpVTZnZzhVaFF4WHF0LzFhN0R3bnRhclQwcGVLNldHZDdqS2QyWkxzCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjVGYUhjZU5ONW9tUkxqcy9UWUwKcDBFREFFVUhDOGRIbTJKa0QycnVJYjhYM05ENzFXMzA4ZktYVHRPZHdRY0lNak5od0VwU2pFaXVNNzJOOHNZdgpMVnd3cVBTZ2ZPMkw1Qkh3RXJ1cmkrWXd6dzE3aGdMajh0V0tyKzErZmR0QzVXdUVyb0lCSUZRRko3bFpETW9aCmhXSnJUc2NnVklNUVZVOWpTWmtIS09VVmZkUkhHellhMkdMNTE3dHAvbGtWaWw0VTZpRG5qbXpaOUFGNWxlTlYKaWVhZUx6SmhFLzNMeXM1MVdrWU9kemU4a09YSkJTTi9uV1NYcWN3NUpHZjhCUW9HK1FDbzlaQkJtUGgxemVzUQpmaFZTd1UwaTRMQWl1WWZ3d3h2cTlQRFFnNW54ODQrZ3dKdXplbmVuK2JacFJhUUdJUlU5dExqcm1pZkVKdHg0ClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekZYQzFudXJGOVRNZ2IrV1hCN04KTy9KSHJ6cnBPNUxwVld5ZTFibi94NDFOM2ZrM25IWTVKbXRaZlVLTEE2eUFXUnZoTGRJNGFMY2dQc2JNSzBuUAp1b25pWkZoSlZqQWZTTXlPalB6WGZGZTBMQ0kvWnJSbnpzUExtcEN4SExmblorUzFJSWhoMTdXTVByajlQTjBBClhZdmhpWWpwK1R3Rmt3RUxhdVM0ZTRYMTUzWTlYVzFTOHoxSzAycXV5OG5NY08vSStuN1F1REFTRkJXeXJ6QmwKZjRFNktuK1RjSzh2L3V5SThFM2o0TGxLZHN5dFQyc3VHbm1ES1FJS0ZKNzM3eEVwUFhGSzdVUnU4WlFPU29ISApaUlU3NXdtK0djT1FYY2dneW5LY3JaQzI5cnN0MTlWUllSd3MxS1IrSTVSQ2VoNzR5YWhmMWFqYTVpVElQcXl1Cnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnBLSm12N3RBNUpXZ2kwaGFXdHMKYXJYSUZYdW5OOHJrUVROc01VMGVBN1ZRaEV0Nkh5ZzMzckVDQldrQ2Q5dHZrcS9vS0YyWTlYMFJOMGZvV2srLwpidFJkY3ZDYVQxd09acVVrVVNLOGFsdk9xSlFNUk9PM25uSDBWZEtxUlQ4M3hPSW1xVXZZSEl2K0VjRTN4ZmQ0CmNDekZjNC9jS2NCQmg3MWpNb1JPNFBubXE4RHBzaE5TOXR3bUl0ZWVhVkFZakg3STN2aVA5TjR5ZzVGM2x6SmMKaDlRUDJPZVFISnJwSjhGYSt0RVJ6eEsxK3FOclMrM2ZTZ3lELzJ1ZXlHTEpXdEFXSGZNR2pUdW9ZTG5QZ1VYRgpvSWgyeUlYMSs4cTBQMDYvTUE4SmhvNVgyRXZJUXJBa2tmeGlaRGZjY2wrdU1aSkh0Q3NidmZHeGN4d2xraUhHCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHBoQ1VLY2lhMHQyL2ZpYlBCTTAKTnZtU1NOOVFLbUFoaHV1ZStOVFRPUXY5d0h0YlVhSFRNVW5zVnVFZXZNOTRweWJTQ3NQakUyUnR0SHVqdUJvMQo1UDVWbDFkektRRTNvWFp1TDNYRnhiMVFUdnFXSThGaVVFSWJ0a0ZWRGM0MjN2V01KbG1QSGVqTFZjZ2tTdVhzClRBTG5PWWtlWUppcUZvSUVGVU5wdEJvbFpUNUNQNVd6UG85bXhtZ1NPT1pZbElBbTJsb1hxOU9WY1h5WXVPZ3oKWThnWmJidkpDbW9ENFpta2J5aXNNeElLMStELzAwZUdrVGJDWm1KSDBtVXo5aWRmR0cwOUFmS01KU082cXB2KwpuUHk0aXJiUHBsaHZOL3ZSb3JTaWNNRitSYm1mWllma2NDenByd3krRmxkWmdIWW1qSlNpTndsYU5PNHpKQm9DCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVdneE1GcGl2UUtxdmRLa3lySFEKUk5vSDBnTzZZTzJESWRLZHc5TjNhMVRHdkF4RTl0Z2NDdWpjd3BNcmNYWi9HeloyVm0zaWJ6Mnp0VFV0QlQ0bwpPZFhZbWl1UTJJTFhQYVpCV293TnBhbVNYeFlRRHZSQjdSRXllTmJqblZVV3dlRjM4b3g0WENuUjY4UnZjRlFoCnBxekJkMHEyRDRMMEZHV3JVdzdJbGFkekFETi82Q3lmWERoWjBOOWlDeHB3a0ZxTGw3UnRRcHh1WXFWVHVnRDAKK3l6MU1VSlBiYkpCRkI4bnVrRUlEY05MbnFSTXc1MkM0ZjNDWWtnZlF2Vm5ZVmlJeWxmcnlWeU5rdUVhU01ZeApwRmtCM3pFaFF3d0VmMVB6SGRWekFDem1ib0J3L280eUxBSk5YN2h0WHZqZFZMaVd3ZW1pcnpWSkFFeTgwUk1yCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVBVdVNoNHdXYit2aFhRUTB1NGoKY05qSkhBQ3o5eUdzamxvcUtwRk1uWCtZWnJHQUdCdlB5T2tUNHdobGliTjBhYTl3RkdqZ3Q1Q2Jqd0lPSVgxQwpvTi9wbEw2dkc3QXVtczhIRVBwZXdjaXZWZTJTc0hWWUt3cWlSZVdsZHlBMUVRNmRxQjBGZUF1V3NHcU0xSHJSCm0vMXlpT09xOG9sTDlLUWJpUlUwYjgyNTFycitCQ1oxYlVoemxHQ3dBSldHNTYyZGFjdllUVVVnN0JFUWVNK2gKeDc0eWlZMEVDQlFseGU5cXhmRGQrOE5Qa2FqZGgydHJBWlJSTUZQdzlDZDhUSEpmR1NOKzRjbUovbG1YV1pDWAp6T3dmOGN1OTdUV0pBdHIyWDhVb1pnQXpVY21WTFlkd3pSanVqdWJ2SW8xN3BwSjZRWldKQ2x6WW9VUXNsTzl6ClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2s1UytIV0JnT0RmWlNwMkEzK08KTlkyeVNScHdMS0ZRMXh3MTJ0cE5IUW5BTnBYMnNPYkV4ZmpMMU5vY3lmbWpiQmVyWkhQc0R3dG11WS9QUDNOOAo0dGhVcE4rdmdSR3dTbGxzM1lNZ3ZFZGFpTkZ2L0ZDTStYaWkvT2dvS0Y4ejRrZlVHTVlPb1ZCM09VYnVLajRLCmtwYnNjc2xlMW9OU2RkUFhKYlJ5L2RNbDB3eU90cXpITFFIS3UwZUxhaE5UTlFYQWxSSGdBa1NydmY0NmUyYnoKZk14TWNRR0MrekR2MGdPNFgrTW93bDRKNFBFYTRGeU4rbkR5ejV1RE0yTHJ5cUZpWjlYQ3pnQVZlM09qOFpaOQpOZTc4dU0vVCs3L0R5cGQwdFRFZ1Y1eDZNdWZqa3d0Wm91a2pYTDl3V1MwK0tUYzdNbm81T0pWTkpNQWxmOEZZCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjQ3Uzd4dTdSNmZaaFVDeVAzY3QKUHA5cHpGWTFJMlJuRzhyVFM1Y3pOTUVPSlAxQVBQMHljRGl4anJFZ0lSNDVLY3VseDFYWTBhR0hHclZ1Qy9YcQorRHVHNXIvbnFiYWtFUVF5dlpLR0pUNHgra0EvVW4vZVRNMUJZSmlGbzlWb21WZHljK3BsNFdOMzczd2NUUjNNCjlUdCt0ZnNYRVVUV2ovcSs5bVlibURuMlNBN0hoYWo0czlBcUx4OXNPUWRpaDloRHc3RlNhMGxhTnhrb1Y0WDIKZkQ5TkdRQnhZUjViYmFDTG82Z1NuMEdOVmZwQjlMM2luUzlkcUVYd1BaSmVuNHFCRWtjZ1piZFY4aFdwT3lEYwpvVHkxYXZia1J3VXhRZFBkbVl4MEdFYXJLT1Q3VnI2SjRmZVMydU1IYzF2aGNVWEtXdzF3ZWl4Z2NmMGZCd1Z2CjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekFYWG1PMTZsZkdLTklLZVkzNEEKNnZSdGh6UGRublkvcEtmV2EwaDdKUHJRdXJOTUVNZThmTFRjT0NjS3dyMWpoMnpobmVWbDh4RzI1NG5qT1MyUQpkeXUxVm1ZSFYyaVNYVk1lS3YwV2w2VVBRQ1JEUjBsTlZoaGc0RGo1TUwvMXd1dnhGQkJXYUJaR3pOYWVyK2xLCnpYZVFIYVZZR0t4MTVhMk5QOXhiYWpSR21qdGQwZDZTV1p1OUNqaXFzQ3NWMnUzcFAza09LUHlNeTNHQVNoQXEKN0xvMUZ1VzNqZ2hEcGJ5dTFVemxjYVBUV0xsTXByMW5GMmoyRFFRZnIvL2tFbUg4SlZaazhSbk1naEpvak42agpBdi9aT1lkLytLKzZQV3dkK2E1K3ZFbmdraWpNa3JGSVNmSGVJTEIwK0ZSdHJnZUJsbXBoT2JPeGZxa1d2cm50CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmgvbm90dCtvNWRFUnFkUGlzZSsKQU9HK1ZKRGY5TERqNWZUK09Jd214ZWY4MjRsNXNGWStLbmtqVWplRENyV1hCUmJoYkdEQ05BUTZDOWJ6Q0VrdQpiNldqTjBtNmJXUzJSWCtrNmxoR3pidFJiT1JEYlJYS0xBdTlWOG9sQU9Wb2RlSUQwWnBUbVBLWDBOd0FvcTlyCkVIWjFBSTV1L1lzVit4Yk9USk5MTlVocE9VaHc5aTdFWEZ2ZVJOaW1hSWZxL3pHc2x1MkRpbUVlY1ZKeElzU0sKMjBycjFHOWdHLzBYUUFHTkpuSU1xKzd3SkFKZVBMZXlZaVF2RGxEeFRqT3VZVEpBR2ZSR3liQXhXSzZWOXVIUwpKNXFnbjZpZm5IRTkwYUhtMVV3MUJPNnp4S3NhaUZrNVBEN0R0R0F6aE0rRmtoNFdab2hpWW9CTkJNZ3YvUy9WCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2trMDhQeWVDaXV5UG04TTF1SDcKdTBBVGwzV0ZsZEw4UDZIbnY0cWhpZnZBaUVoNHVaWFFJdlFnOFc1UkF4M0tHZnR3LzA0N0NXVjY3Q2dsRDNPSAp1TFpxUUJXUG9IcDZZM3phVW1IcDB3RjZSUHFyZGV0cW42VmFZSlh3Tk1GU2h1L1BHVmJobFVDaDRxWnVrY1duCjNoVVpzMlBCN01hU2JLUVdocHY1dG03T00wS0VxdkJuaUF5Ly80ODFqbTc1aENUNTA5K0Nsc3lVNW5CTC9XUDEKM01idE9qb3g3ek5pbWp6Z1k1ZTRFeXVKcFpBS0RaQTBvdzZYV2s3WHlVRXZjQlV1Yjhtc3BDQzI2aDVXek56SQoxbmphOS9vYXU4ZjJlWjY2SmFQei9TU2w2aFhrd3ZsZFNxcXMwN255ODNYSjRMWDBBYkdVYVlGQlJXeGlyQlFuClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNm9pNko2NnVXM3JGVTczN0cxVUwKdXFobjE2QUoxOGhxd2cyUWZHc21TcjdlVWJVTXNPZTc5dFhqVGVsVEYvd2hyTHo2K0tVWGNjMkY1NjZBVm1HVAphRW9aYUFReXlTTVBsY2tyTWxwMkxHNkc1YzF2UzZua0NLcFg3K1dJUWJZRGZSZ1JOZ3Zad081bGN6aWVzMjdICnVJK2pqVWtuMldVUHJhQ3JDV2EwWjJ6S1RKN2k0VG96K0RZcWxlOXhxYWJuMW0xa05LZGJpS1BYY2VYTzZXQWgKMzVKbGNIbFBFRWJZREozNzA0aU9KQkwrRnV3VmF1VjczSlN6YlFMSWtObzVMRWtwRE9kNjZwYzNuZ0hOUmlMbApUNTR3MW8zRkZCOGpxblBIOXhJbk9WL1dHMUlKcDY5UEY0QXRkdFU3S1YzZWNENGtROThvRUxKVkZ4UkVNRXFZCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTVBQS9zTGZoQWx3Q25UUmh2WGcKalFERnhrZmhhVGVKNzdhWVFTUkllTTdzci9BVTNDaWk1K3k3THBJTG9STGZTd0NELzVXS1lUWU16ZHl5Rm5EdQpHejFkYlc5UUJVR3hQeUZpckpXMHdxR0hRUm10NWVWcy9CMHhmUVI3ZUlRb1ZuM2F2TE9vOWFMb1BPK0RGQ3BhCllHeGRmTU5CN1dQN3hpbnNPdnpZa1RMbmZzRVBCZWN1cklxSWJlUlV2Wk1rN3haaWp6eFQrdDRSb1kzbVM0TzAKL01WY3RtcHVPTTBDZzRUbjhrdE1nK2crYnczUisxLzhCeStEaXJBTXVYZ2JhcHZibWxIdHFhNUNqVzREenRPMgpFZ056TGJ3NFpaYWRyRC82ZHNKQVZ5a3NuZHAvK3JMellXYXB4bWlDOUU1U2FPMFpmWTh3c3A4dEpPSkFRWWtDCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUZVRmNpSXFLY3hRNkFrRUQwUE0Kb2c1MHF5RURJeHpFcmlYTEMzSnFEU2drOGhTOCt0K1djRVZPbnRTMkxMZUtPT294eFNMaHZDYTU2UDBYOGRzUQo4ZC9zajkxNE9sMnV0SGVmRVBIeW45dzgyTFRRdkdRcGYyWEZlNmsvbUFnYmtkUTFwUW1pT3phcnhsTnFsZlF6ClI5MHJFV05qUmpSQUZXdFRNS0xySnNNbHBXTkdVTTIyaHc3UWViYTJ5RmJCWlJ5K29NeVNJeFRNU2QwNXpFaFkKQnRzUllaR2VCWWZ3ZklLQXlVamVhVjNxdXNsYmtWaWtuN2N3UGVBVmZycWRxeTU5Z1ZXU1pJR05DVjdRa2pVUQo0aWZpSFN4OC9xSDI1NUxVeXJxdFNhSXhKelNqMU1RSUxpeGs0QzJqcWVyeTVsUG53T1dSNkduMlZOang2SXRpCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGRvVmRoTU5QRHZ0b3d0dndCTisKQ0h4cGpiSTNia0Q0ZzhPaDBlc3QwSzZyenlWZm1EZVNoeGpRdy8rQVFOWXJobWVObHp6Smtia2JEakJOS3E4QgpBMFdLbDhhUk1TcVJZM1lsRkQ0a3pkbUwwR0tnVGE2YXRzS0dtSXNwdTVFRnhnOVNXR1RHSzV2Y3U1UkxydGFyClZwcUxZMTZlNG5VY0xaQ1dQZHlERmJVQ1RHMnphWmFoTkZPc0Y3dklPUHB6cjNVTnRhVmdTMjhIRUY5eEJiZm0Kc1FYTFNzdEl3RGxUc3ErcWJ6QVNGMDRKRDJCaXpDSzBlVWE2ZURCdE5nRE5LVVNJRDMvc1lHdkQvVG1ZSlJ6YQpCRVNNK3FZNko3SVNVVVNqS0MwUmxZRXJuTlVjSlRXZGt6S1JTTmN2TFdvdm15RzZ6eDZnaVF5b2gzYThDaFk4Cm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEdPSWR3QVcyOU1ZdXhxbFAzblgKQWVCc3dxTFNLb0xSdkdlbWZOYko0VEMzc3Z1RmN2YkVmM1NraUpZNXc1bHdmREhMQThJc055YXNJL1llT2owYwp4aXRGWFpuT0UyY21WMmJUS3NGa1ZIRk92OXptQ3RtNHJSQzJYVFJmYUhyZzV1ZW1nTWF2NFZIdElPeDJUTlFzClZrd2pEQnF3QTJSL2h5SU1JTEFYOHc4M3JsbWJ6VEVtellKUGRRRlFKL1JJZFlJMS93WTdMb2IrRlZqVnFaZDYKY3NtMFZuTWdwYTd4RXBweFI4eE9GRVVIcG51UmR4L1hYdjVwS21yUnIrbWJqd0hZei9hR3pVTWVhUlg1SjluWApjc3djMzlhTU9hYmw4UzlDWlhOQ3RUa0ZkMG1yVWZNTjJTYlRaMDZNdXFEQkRDanpDT1F2dFpTUVVoaHhGbWhHCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2pyTkJKbGZMUUFvQ2NqSEhQYWsKYWZaQzJtN3luS05zSWlBemF3dVlUa0FGUzJmQ2Zid0EzY3kzS2JYa1FIbnFuWkFjVnM0Z0tjUTZ0Sjl5clJweApiYmhWbjRSZ3NpVXVJTHgrR20yaEhEQ1BZN2tQc0NidjEyWGIyVms5VnNEejFwNHRwdEdUK0Z5UTVvN1dOcC9BCnd4c3lNb2F0K2RWY01TSkY1VThWVzE2dW5CK0NQc245SXU3S29rSlZ6WmM0ZnB6K2xMaHhJVm1KeVRTVWFuZkUKSmd2UEs4eUZDVGpCZkNBS0FJSHl2bGFTTUJVRmdUamlWUERORUFyTEMweWZTNmc5QkRMc2RqMGgrajFxdjRSNgpqcExFZjg4bTRGWXgzWVBwV1dFZUlyUTltSFZVTzMrYnF4S2hLVnRDWVFYd2U1UEdoR08yTm5xUUJ3NlNxSGduCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHBMNlFicVdNVHY5c05EZzlKbloKQmJuUExWNGZIaG5vd2htNlZUalhFYjNXcUx6MEpoK1ZXZ2UyMUwrY3BmaDFFcEV6VkxnNzNPK2NIMG9NdEl2cQpBbk93aVZTRngxTFV1T1pwcHhOUEVBZE5EWDFMQ3hUb2x0QzZMbmNydHhIbkFCckRGcW9OMzYrYmtMbXRkMGk5ClZpbjVGSFhpZTFzd3dKSG5CR1J4ZVQxNlhGZXRES3ppV1lXYlIxd09EWFEwOE5RTkM1cFVnZ2xYUi9FMSsyZGIKM25Id25Gei9DcmZaUDllMmtwalNVUW5tdXpUWjlHNmEweEpOZ1l0a084VXo0NG1GRHV4a1FCUkxPZkhvQUZodAplalAyTW9KVEs2Tjdva3YrelNMSTY1d2gwaTBoU1pWcVBuUGNMS1RWWnZhWlpZMEZzQm4rOGcvRllzN21HcjBVCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN09BVzdTdjU3SGhIVmVvSlkrZWIKOTJjNXJac21rbENub2lYWXU4dGUra0xTVHpmMzQ0cTFsaE1nV3lnZ09MSW9Uakw3UkFGQkRHa2x1YUEveXE0bQpldzd6QkZBZ0ZWSnR0dXVqQThzL204M1M0N2lGU2p0TFpjNVZkazN1U2F2ZW4wZ3h3WGkyejk0ZnBCVUxmSlhVCjRLZkhROTgrMGphajdMNlVtSEdReGdDU0c2am5vMFF1UDVpRE9pZ203dUU3d1Z1bEFYZ0dSaVYyNkl3SVpTczUKeGFsZHBXbkZDNzJYcXo5Sm90RENvNnIyZ3lsT3FKalN2V1hTL1dKOE9Qc0Fudm5pdHNtREQzTDVkMkIrSHVzcAozZmp1Y1NuSjZUQytnYUIvRnlMM1o0UEhYTFE2akY1M2pURzZQSnNzN3B1bXY4RkNGL0lKOGxQczF4NDlhUnFOCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGV1Y2piYU1KSEVRT3RRVjZUSXYKQXRNZWljb0FVT3ZNa1ZZVndvOHNiSy9oS3NRSUJ3VnB5ZGhWcElxdXpuVktuVlVYaVAvN25UcytiNS9nZ0NKbgppU2Y0dUd2UEJQV2RmWkQ2Z2pBU3V4ZkZYcllEM0w0YUk3Wm5DTVBPTFBMcXUzTG1OcnNWbEZWSVRjS1lwUlRuCkp2L2xTcHgrY2oyNjM1TmZuYTNnUWZ5aFZzeGJrUEJxTTNyU3psOTlMakpkZFdkaXVWcVlxUlV3MThSa2tQNU0KSGJ2dDZLV1FSM0w2Vi9wcElCRHBSQnZEbVJkdmdVUU1OeTRxMStjWGh4VHRaU28xZ0pKK05JWnV2TFJaQ0s0ZwpPbE1rSHFhNUZQZHV3Q1U0a3RuMVZLLzlVc0xlV1lRdGVYWXU0N0JSMDhRbERBbGNncUJ0a3NTTlZJd2hPRkJBCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0tWbnJkSDE5RlY1VjMxUTNwSkgKZHJxVzVwV0x1RFEwaWU2UDFIaGFLMm9MU2RBS3dRWitKTzdDNFR4bFhyRG9oZW1RTkJySW1ibHFGamNjYlJMSwpzV1IzTVJMYWVUSHhZTWR5bG83b0N0c1NqdGxIenVDOUtBdnR1ampEczZLa0lyZHdSRnRtVGQzNnJvWmswVVJtClExbURKNlArN3Y0NiszUUlYWHBHSXNiaXhSTld4M3IwNUpmcWtkQXhMMExmaGFmU0t5V1k1WWJFOGJaR212TWsKY1ltNzZ6aXJJSFBaZi9iNTF4NU5RTSt6RHhwaVRRVGhQUHdqeFg4RlRMNHRvSEVybmVpMENaNU1USElxVHdqZAprQ0VJYXNiSzFJK3Vub0EwMk5STGdhNjFiMnFpekNkVnRUNWRFbE5lNXV4OW5Jc3U5TFpTd250TlVZSVErazlOCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGVNMm9zZmZUbDlnWXBtWDFLRFgKclpUQlVqVUZWSGczL0xwMnV6d25JTlBwdlVtbVRTeFBISlpXS1ArelorRnpOR2Z5ZUJ0eVQ0NmZDWFRJRkV0WApCejZMbkVkTkFtQ1ltQ1R4YnU5MlcxWVJXREhCSHNNeEdUYmtGSlFCVFBNdVJzbnRuRzJvNWVNM2JZT3VRelZiCjFtMEFLUHA5Y09JalE3MzdlZVdhSGQ0eHAxOEFJTGdCclJtUHpZMzMvWkRlVVJyczVDcXlaUVlFVC9heWxzOXgKbXh4QmFpUi9NaE5tT2xhWmREMUZCekN3TWJNZzk4cCtNL3pKNHZEYUROOERTS2dyeDVkTDYrMDNidG9yczFPYwpGOXFFdG8ySDFNV09jODdkS2lqcmZMSE9SZ20vQThjdXlUL09RM0doZkM4TENvelo5RXhobTZqYkxvSCt6eUFnCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBazZIM3RpczFLaFAxdVhYMk92TU8KS0VBODJjM3FubmJUMWVSTjN3dStDOFB6UjEvL3F3OU9aYnhLeHF2UW9XSDBSWEs1L0UwaUFjRVBFM2ZNR2x6YQpXNjZlOWxXS05jU1p3TFR2aTVoZ2JDSTNoYWpXUHRJZWgveHJqdlBwQzM5M1RQc1haT3JOejBVUS9mNm1YOU5VCkFBaHZPaGhyZ2w4a1ZTRCszeGNJcGw1SllYZ1BXdXdqVUlnRzlDeU01VUtsY1ZPS3NOTEpiTVFRRi9ZaVh2WUwKMzFFRWt1d29tN00wOGdza3ZlVnUxbmVjWC9NYjFKaWVLNGQ3NUxwNU1aUmF0QnRmVTl5QVo3Sm03bnM0aERDOQpFS2QvdlBEYldrRE0vL1BQZXlUMjV1YUx0NC94QndTOXhReHhkOGIyd2diUGJTcjJPcW4vU2Y2YzR1dTcvVk1hCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVN1NEdQN093SHA1R3JWNW8wVG8KUzUxVXBIOGQ5R3ZRL0VZdlRCeGRvU25vMG1aK0F0MHFLeDNHdytyUzFYQUlxdFlBc1UxUGl3WmdjRWhXdnVYMwpDckI0WGYwVjMzeWRZKzhnSW9iN0pmNUp3SGxacGpNdjMyUUdRRGcwVkNCK3NuOURMN01QcTd6bUtEU0J4UE81CkpYYU9Pem9IQUNCTWQreXBRTzBBZmJ2RzBPWnk1MUtzSFZtUjdJSWZ6N0lrVXhIVWZYSHBaNHlMdTJaakM5UDcKczdVcHVsN3VENUFzNWFpSnl1SDFhQTB1ak13QXZsaXU5Y29sRjRLaVpqamNvbjU1OUp2ek94bHZUdGt5bG5pZQpDbTA2Z3BLVFhUZGxoRy9tc2g4Y2JZa0pRZllrZnhmTWthWGhBZHl4OXVHemJDZXlaWEllb1dvS2VwVnI5R0tVCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmpVYTRkbmx4NWhVNG8rTUJtdjMKRkY1elM1cGI5STV1WTBUL0FhMnlVTnRDSkFWQXJBVkNpRlp1R2dNQzZwcW1FT25Jb3dkclczb1hZd0hYT1lqWgpJdCt0Ry9ZSlhlb0hTRU44Z1JYQUJiV0Y1eTAxVlNDQ05xeWprNm52bUxSMmVFVVhZM05NR2dQRWQ0alhuazNkCnBRS250bTVHZFlUUTI2Qmo3aFRBakg5djBmQzJjQm5veXhrY1pBek9hT0RJc2ZoY0kraTJDS1ZXcFlWbmpaTGEKYlRSWm5mZ2kzWFNjSnVkN0dEa1pBenVSMWNPYnRzWWFGRGlMTkI3RGROMnhtZHJ5SzFsRFFkKzNQZFlrcWczbgpxOHN1TG9vQzNkcUF3WG1mWkFWc2prODBMcFhTUnduTXliZnhidmFMTE9KTXNORzhxOFM4Tkt0Z0xlR0NJUnhiCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHBYdnJpaS8wS2ZHUHJ6bVBPVEUKSEdqSUYxQUpFSkg3SjJkTXV0eDJkRTd6M0xNaGhrQjV0U0NjUkVmNU5hNVdGQUVGRmJzTFhNN2hlS0NzUzZTQQpWY3Q2dDZ5QWJqeDZqbnA1QzAvOWlFVzJ5WXZJRnRJVm0wWWQ5Um1IUDhsQWZuWFYrb3ZrRVpyOHpiYUdVN3NKCmJoMUJoMkJXeW0xbStXLzV1RElvdnlEWTl0Z0tQaUY4OG91a2NNMGgwK2NBb0w4RUIrWGdNK1lqamhaam1ZVmcKUXBQbEcxVUJ6NlJwOXZDWUljajNIRG5sVldhbE1kMWJzeFRCMkZrYUEzSkpBUWtOZFNpUEtyd0VLRGY5RWVDbgpyU2hYSkRmMG45MVBjR2M5Q2xDY0RZclh0ZWRXemVXR0YwazB0dUR5N0gwajJoMWI1OVdPTlpVaTBwN1cyam5VCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUMxeWZhVHhzeC9idkl2c292dnkKcnYwOUl0NThTVkRJb0hsd3IxaWxMcjRIWFFGUDc2OUxJVFlaMmplQkRQa0ZUY2pRQ1FGdVFxa2d4N0tDTW9IZApSNyt2MlZlc1JlZ1BOTy96VHlzM0MyTzR3UDFxak9mVGV4ajhIYmJTTjh1cHM4Q0dmWGtJOFRObXk1VjJ5Qk0yClcxTm85WmhiTXBGdi9XYU5oZjhZR1JzZjZBM0hjT2pnL3VSV01MWWtjdmQ4dmtZTitLMTZkbld4WHp0emhSdXcKR0laemVzZFRLSjlSVkpWT21aU2FLbnZ5RDV1Nms3RnBoQVRXTDd3T3I3eko2SWhURmZZQVRLTWl2U0EzalJZaApwN0c4Y3IyU1pmN3ZYdFlxVldPMmdJUGFiajE3QXk5MWdjWFFRZ0F3UW1tdGtKRW1HaDZnc212L0RzS2hOVGl4Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUR3eHYrNXpBSHdwYmJaVVJQSW8Kc0ptbVRNUmJ6S045akZpRUZZRUljVzl2bVpJb3NCZzM1bGFtMy82N0dJdlRHaCtRNVR0YTRmUE0wYWNxMWhTUgpuSGJmQVFPRGEwQUhxeENZRllCZzcwRUxsc0FTVmxDYTJSbFlKSGtUVUEzbDNzN1RIdnNsZFJzTGdETWZGV2NWClQ5aUtCWTNSbDZzNFpkT1Ewa3pDQms0YVg3VXJIR2owYk4zdCt0Y09mN1NiUmFIWHpSVzByd3RTemZJZWNGWVgKMTBHaFV3Z0N3VndhNDJka1Z6MUUwWVREMDRmVlArdHZuTVp3c2NrQXVwUmZsdUFpK3dSeHJpUjhKK1BPK3dmYQptaXk4RUdTdWNUWmdwaE1JajBBSEZOUjlxd29BVS84VEdWbmxXMmQ5ckN6NjBzSnpoNi93Zm5yVnRpU3dSVkxlCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGxtK2NDekk2SExwOEdPTk5oMEEKaWwxWmNBYWJXYmVWellLTTBRU0QwbWs2dkVXeXY4QjBPL0ZQUUwzU0ZJSjhzdEFZUm9WNklUN3I2d1VpbG95dAp4U2F6QkJwaW9OdWd6S3RaYjRnODhPN25KV1JvUkFXOXpOZFpOMzNaNCthajNJanV3Tm1DZmVZbE5KYlp3SG9LCnB2eVVlN0p2bjBQRW9rTWJtRVRUNkpaYVJpWGxvK1J1T1VyaE9RcnErdzlnRFVvbjVybi9ucEhnYkxhODQ3a3cKcEM4SElremdGcUFKNnZPaEtXTGtqdzNDeW5LZXArcnVjOHYxMlJVaFgzTTByNHBod0lRMnJMWmN1RU43UVp5cQoyM0NGZnlvQjNvRFJJZThJVk9SMWZnMDJkZmxpbXlnSnBJUUs0UTZuMWx6VkQ3TWJtalExdEtTWm5leEVWVmozCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHRVUjNVMjh2QlloZHNFS0NDMDEKTGpKOHd2YzMzK1pSM1p6MTRERTlQN0Nsby9Za2dTNUlJQitwS210Q2xiQk0wM2V5TG84V1h2blkrTTh6TnBPaQp5MzB3aFhSQTAzRjY4bEdJdEI1a0w4eUNKdmhjRlRTekJVbEFDMkY0TDJvU2ZsTGJPOUkzUHpOTlU2SUxIeWE1CmxUYXlQQk1mbE5GT3pGRlZJOG4wcEFUTklFQzhDYXNacURzZGZLOEJOMmhTMXFpMm11R3huVVRSUFY2VUJRZFUKQm1iUGl4aG8ycGJQU0tOdVlXODJpN0FsdjI3b2FvOWxlRzMwQmovK0ZMWCtmY3hab09NSC9nVWhKQ1FqN1VjVgpuaHo1YkZua1Y1aXE4NWFIeURWWmVqVFA3V09UVm41bDUvQWMxQ2ZyLzZNT2phM1I3Z2NVRWNHc3lpR1YxYzFhCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUM5OE01UnNoc1Voamg1N1JMcmkKUDREaUdHMnNldTlPSmYxYW1wQmdiWUlNR1diT1BVb2prQ2NiZHBrblN3S3F0clBPaHl2OFRGcXRoQlZEK0lqdAppVmVWd0ZJM0FHTkRHaWhoQms2K044N1hkTUh6NytTbmFHWnord1pFWTNraW5sMkR6Y1d5Y2VvYUpuUW1HVlk0CmFDNE5RdTA0NCtKQ1NMdXNVRVM3Y3hyeXMzL2RyeGZkUlY1dkdHN1F5Z2Ryd2FhTlc4QUFLd0VHQ0NNY01IUVUKdkQvWGFMb2JyajRLclAvTHpsTUVUdXJ6TlB0cjdYSm5PWTRwdGhDcGV3K01TamhRZGNaTUw5clRBOHQ0TXhUcgpPTDRidnpRT05VMHEwZElDK0QrMjA1Y1lYb2YzYmVVS1pIZE5BdjRYdy80OW1adzRXRndSN2lGekpuL01UYnJBCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTFMdkVFRk0xMkdIaEloUHV2azAKZVJQbURLbWExWnlyUlM2aDEzcXlCeU5VcllvUVBqNE5PdVZHSmNWVTBtTXhxY0w1YWNGWkQ2TkNrMlpWSy9vVQplWlFNcFkyTW83VHViTnVvU1BMSnc4enJsTlBqUS9QalZyYWdJMUx6aGM3OWxoeWRkOWZZL0tScU5hUms2MGIxCjE1N0swaVlNZHJmWHBaNytBeUhWanUydGN6NUtrS1MycHJjWlBZZjh1d0VHOXVETHU5QytaYU5DSWFaeWljd24KRlRBUVoxalU0VVRWRlBFUFZtdE94RXZBWTVvOGpsOHdwVzRTNlduc0E0THdmYUdsbUNBTHNDckRnWFJnbTYrcwpqQ2J6Yk5DNFFlTTl0cVkvSTZ6L01iOVBtWmdsY3A3SWZXTDJMY2lKdUdjT2dSNGRwYnE1RjNwWmI1Z243RW9xClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenRQWCthNXo3WFB0QmIyV1lhSEYKMnAyOHdHa3dxODRzL29TYWIwdFBEN04vY3lKZENSQTN5QWdTd2VlcmsyNGpkbW1JaXBkVkR6OU5JdmpqTFFONwpjWUd2a0t3Lzl5QzNOdWxJdy9vaElSSUx4VjlweGV3YlUxRmtsVUErcnNjbTN3aE1Sdi9HV29PU0RDZlhBeExBCkNXQXorbnF2SzlHVEJzNitSaHFueWxjUmFFQ2RqRUJQd21SdmwxTVRXcmNsZTZUK0hHcVl1QVpLOENuM2d6aE4KL0VrZTZiWk1QQmJFazFVc2svdjAxVitKTHdNbmFURWNJdzJlYm1Ya0xvSjBNUHV5TE56TWlXQktaajVtekRxdApNcjF1Q2tPYkZLVzFtcE9YUjZCejh4anBBOXE4anBoSHhBczVCWDhUSEcvVDhDMFl4RXZqUVkweGU2SW5VZStqCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGVGV2svZUpjSzJmSSt3UVVLZnAKU2wxbGI3Rzcwd2xEVUFPc0NtcUFOdkxOYlNUTEZ4RWUvRjloMnBRbXZBb1ZrbEkxMkwwZWR4TCtDSGdJQXVRSApTMnY1OUhNSnNCOFNucFFHem81U0pERGRtMTMzWWdnMzM0UEJMbVNEckROczgvcGg3Z0xtRVdPR1RoU2xsWjQvCkNQaENnRUxZNGhqUjRpQ2ZnV1NTejVaRzBzdVhKcHhmbS93V0QwSk83bnJBbzRaZVo3Q3R2RCsyQmg1aHo1QXQKNUNwYkxGWjJKalJ6ZkxpMDh4cllIdXhkLzNpRCtxOWZEZ3ZRQ1ZYZkprS2RaYVZDaGNteXRPdVExVmYyN2lhcApOb0xaTjU4eExMTmVQdkZsVldvTEs5OUdMQW15VUVsMkhoWU55eHZPODZaZEJNL2Q5emZnZFdaaERpT0w5M1ZZClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0xoOURRc1hpL0lId29JL25FRUQKenJXU1J1T2tCVlByZWNSR2txRlhZMFFpYTFQQ0wyY2gyZHBySXRIODBGYkhJS1JQeno0U2w5TlNsbzduam04QwpWVHE1ZFphM2ZmNWEyblpid3I0K0VzMHRVOGZ5ZkNiZGtNbUpTVy9VVlU3dHJXd3VWazVNRHVRWkkzUmZRcDY3CnFsTmtlb2t0ZmR0MzY3cXRRSHk4NERFOU9HdmRxdHVVZ2ZOdnM4YXdZQU43QmllZW1NbXBwZmRSV0o2TVBjODcKNmlYeVZPcEwvMkZrcHlEOWNyNExoejY5MGs3UGY2SHpyejZIVGZqSVY3YmJIL3I1Q1JvdjF3TURyYWN1cVVyYgpjUmdvc2tTUjJlMTRYVUhsakM1TFFHT3dDcGtGeGhtZGt0NDFsZVFSU2xxcVZLSUpZUU14TDg5R1NWS0hnK3ZYCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelZNeTRVWmV3NnB4RUFkZ0p5MkgKUk1JVjhkQnJoKzFKcFFZdStuY2w2c3N1WENhZko5SytzQXc2TTd0SUhrR2pwWXVuNituZE9RZ3ZCd3VIUE5aZQp1ZmJ0Ny9wYitwMXpydkZjbUZ2TlhsRlhudXNEa1Qza2IwVXZtRENMZ0FYL2ZnQnJ6bHdHTmpEblFVQzVXVkMwCkdvL3llbHlBOGg2a1lZa2ZOSEJURjUzREdxQ25mTldnN1BHeGtjcUl1NnI3cmQrbWp6MVJBUmw5dFdRWmgxeTEKaHZ2RU4rdzdNUkxWRkh3WXEwWFlzUWRSNDdVTDhVaXFmMXkwY0F4WnZpeE9Yc2ozb3hVK20wVDNBZUpqczBwVApua3RrdGtGNmRmYmRnTGlOaDdPZVdaTWxOYUlqRms5LytNemp5OEtnbXdYaHA2M0JlcDNXVzRocTZTYXB4YUh2Ckd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1hFd0htUHdnQTg4NFJycWVaK3IKNjFHY2VVaXRsT3FtZVNaZGY3eEpTdWdERUhkeXdsVGRVdGtTTkxvRlhaUXAzRm0wOGViS25ycHhHUjZNdlNwLwp2c0tCTlNtNmFUMVNTcWlZbjd5MXEya1ZlSVFiOW1SZG81cmlHZXRRdVlBVUhZZnlGdk9QZzRGeFhOWFFROEpnCmtQdDc2UHJpeVg4eEtzdjlDK09iMHhjWlN4L28rVnE4Tm9PQmZDTFVOTGs4VytIZm8vT2tnZmU5MVhOTnJMQW4KTGFUY2t4YWpMOXJVRFVrUUxkajJIMUVJT0YxR2EvZE5UTmgzYnBtU2E1cGNhL0s1UUFVVTErQllzaUltWS9wYgpXUVFaWjIwM1Y2cTZMaUY1cHlPYkI1bGpiT0x5TWt1RTBmL3JMdHBPVGJ0TmVUUEZOMXRQOFM3UHhlSWloOWFyCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHVFQTVvMkdESlk1WVhDbzNSOTMKNmRGc3ErUXJtdjRQaUdHb3pHVFVPRW5tRnQvdFoyQ05ESmpBZ2hvL2NQOGUxdHZHRlVEN0hiT2c2b0N4aldQaQpxQnRKVEFDaXp5R0pFU0JiR2RWUk9XaHB1dEJkVVpGZmhQY0dvMHZEUVlmaFZndXNSdjUwOWl2Z3ltN205aUQwCmdpSW1oMUZSTkZZaEVKcUdzeFJxbGQ4VWcyQlZ5cE5ZRlppMm5vYVhjTWtsSmlmMXlFWFgyMGU4dVJPSktiNEQKd0RycmJJNDdqSDJiWlJxVkF4WXl6NHJFNTAwblBaVGVUOElSK0lIVVlBSUhHZitjRFZUMitDTDRzTi9DSk1DZQpSRm1NSFl6NDVkZmgzZGlIU3RCZU5heWJCUEdyZStoa1ErZWhGYm9seTlIZUxJL1lTSmRnQVhRS00ycjZ5d08vCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenBnZkRsVSt3cUR5WVE2K0g4YkEKUXZURVBNYWtFWkRnbmJ4QlYyU252RGIrVDdIaVBEVDI3ZnFYRkpMcEVjNllWeHU0Zkd4cDd0N3hqWU8vR3Q4Lwp5a0RHSE5NeUNWV2VYTXdEMTNuY3YrVVFDUXFvRnJmUkNkZ25VbnRIaU5IcFl4bXdkNXNZdkZwQUJWZDN1OXBGCkFTMmtkcDU5TXNQYmtRRkcreXdzeGRpcVZKVnpkR1piSERocVZGbnBYZVRkbkp2Z1VMVDZjVFgxY2Y1VHZWYW0KbzQ0YnVGTlBBWmhybURlRFpKMDZsV3o5RkQwZlFmdWU2MnU4UUdXazhzZXhQcFdKNUZkVmUyYUhudGI3dEUxeApiUlBnaVZXVktXcWpHYy9xa0tBVGozdnN3cVFkdXFNS1IzUjlHeDlpbTZzUjdpYUNxdjRmRE0wY2F2Tm9MbzVZCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVYwU1VGTGdXcExjSnpGZGV0S1EKNW1TVDNoMVVlR3J1T2J2d1lacGxiTnhDNDQ1UGxzMVpqMjhWZUhZKzNMS0EyTHRpWklYaURGQ1FBbEQ1WXQ3Wgo5Y3FLVXhDcEZTbktaRlBCakJaSE9BaEpjRGZYQUh4b2lGdnltalFuNkxmM2ZvQXExdWhNWnVNTDBuVGhFZGk5CkUrNmZKcTlMSXE2aVpjYVBqZTlSZUt5RWlOb0k0aHJMcERaZHVGc21McmNQaHArbnBaLzZUQWUwZDY1WHAwVDcKeVpscFlCUEZ1eDV1bXVJc2FrelBOS2hvMWZnazFDblBmTUgrL2FEMUwzSzVaanZGYi9mZUpZWWgybGxLRnZacgozRlNRUUw3UHlCSVVTZVp2TFJEOExuZ2VYSjVyeGkxWTRtQXFjTGQ2VVRpMGlsY1BtMFhWQmRaVHhwcHFDTXY5Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2tzL1BpOFFWZWw4NTZIMVRiSGkKWjVPcDN6aDJ3ZmU2R1daaDJTbXpmc2RLankzNDkyYk9QWHBUb3NjSWhSbVJ1WEdadDBVcmZ1cnlwNGZDaDNvUQpUU1ROdjZNUjk5cWw0cmo5NnhDdW42dlV0Z3NSZ0tHRXZ0d0dwb3k5bXJ5REVFUkw4ODNTOUhjOFROSzlKWHNoCnR0RDlyV0ZrMUxKOTNhQ1Y2Z2pYWWlmWHZFbVBhY0R1WXhLNXArYUU1VnF2VnZNQ2djYnp0aEd1TEtMZEV3ci8KU0RKdHE1ZFNoV1B5ZDdsY3hzb1gvMzNUanBmb2x0OWdrRjhtU2xPY0tMcEJUekF5aE9kSDdONGtoMnIxeGxpbwptdGp6SU1ML3NGUVUxSFBDSzlOSFNpWnVHSTdOTUNFVStZOU05WVQ2a1Z4UDNldGxoUnJLVG1TdWx0SkFmbzhCCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWdrcm5lNnFQTmFadUtPVkYyRkMKMjZ5ZENSK3FIcHhramNlN05PTXZUa0Jubkg1RUsxTGIrRUhWdU9zeEY3bzRXaEpWdWNvdFZYRms5WC9kZmpsNApmNFF1YzBqRnk1S2l4dnE0bXd4Vk00dVVkTXNtOFhOYmhEa3ZaKzVLL04rOHRHRUozc3AxMkhNd2o0enZick8vCnkvbGJqcjRzUWVJVTVvanZPdFJONTU3MStwMkdKUlVQYmc3TmdBQ3UyeDg1ZXBrTXRLRkxoYk1paVhSdDFvbmoKM3NiZlV3N0J5dUM3ZzhjQm4raW05UFZ3aXQ5YkZOSW1SZ1ZhNDR0VDJTeWFmWEVSTDdPWU1hU2pkUFdtNk5mWQpVRTFuNDdBQ2dDWnRLSDFlMnRQNEFhRThzT3JZQ01ZZnFseUVYbzlPbitSYS9id2pXZFUzSldmNGNnQTZWeVdmCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHBqMlZlVTI1NDVmWS9iR0hUZUgKbCtwaWM3UFJ2RjIzaHZDZjdnL2VWOGdsaGJZWGJPZGI0d2t1U3h6djB3VnpTWlFNSDd2eG9iWjY4U0drelRSQQpGSy9waWd4NTNrc3h2dHR6cCt3U3kxU0lzeDVscWlZdEoxeDNmR0tuT1M3T05YVFRUK0xYRE1ncUxBaGhxa3ErCjhBQmdheGJkQzFoY0lBZFBQUndWZS9QclRNeGZvVkVVa0hyb1VzUHovS290UVhIcXVvRmlXYTB5cDR6bEptWEsKZVpQc2hsVE0vSmZyZ29qWXBnR3R5aWNMaGNRQnVQalNZc2FQV0J5Z1pDeUlRL25uam5mUHgzMzVtR2lrZVk3dAo0dWJlb2k4Q3ZQaG1FQm9pY1J1Vy9MT0pCQ3diOUx0QlFTanl3VmJSTmZaRUwvSzBFUFpmSUcrT0RGekFlcm1tCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkR2dXJzNXFOdnE2WFJSRlhoaU0KN3JraCtHZ0lMdUE5WXRrQjAva3ZCRXl1Zi9yaCt4emRkRFEyN1hYVUoyWkJTa0JuUVh2NlFVYlJpTXRRRTl5RApIMWdEMExOQWhYc0tFTVdkUThMZ2M2ak1LTjdwSUc4anQrQjVncnFERXRjTmpINFo1RmVudWMybGhKR3NrcGJ4Ckh0QmlKUWpsUTR3OHZ5SXNua1kxRVZ0YWZjR1pHbVVqcEFyZ3A1czMwdnZsWUZwWGc2TTRwZkUrdTVudzE0eHYKWlVoRjN4eWZkTlJYaThHdjZnYk8wbHJnS2M3ZmVHVkIvZkU0bFRaYXk0Q1NCQWRXWXk1MFZpd2RiMDVWSnFScApudDdObXhuZE5YTjZXM2tRS1BFYnhPeWhIb3B1YmZ2QjFIU2U0bXBuQ3NJRG1lU0NvTEhxMHBTSlo4bDd6RWJqCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWc3S2xtVTZEbGszclY2ZjhQTDYKWUFjWXphWkJpckp1SGphbFR3TENBd1E2QlVsSnNJYWlLK2VvbVV1Y2FqSWVOOTNSakdZQVdmdmptZWI2cFUrYgpaYWgvVHkzbFhrT3lVcml5OW1Db3R5SHJmM3lPMEd4U2xDSWpJVlp4MlVOd2hmTCtTdy9FTUpqZjRWU3FsaktxCm15QzN2U3hQeXI2aktBa2laakFVNTcwVEVoazEvVnBWcUFWbjU2VGc0ak1PQnBkTnpPcWd0VGl5bW9YMnpwTTQKVFJlcjZKNXlBV0tQNmNKZ3ZqdDNTSjg3T1pwUzRHdTFPbDVZZW1QYktkbmt2WjJrcEFMMDZvSXlSVjRBNzlCdQo5TnFMamFMbHJrYTc2RXdZR1Y0cHhDS1l2a1pDWFMvQlhlMk5hZmNqSE93TjlPZEhkSzFtZTVWZFlEVHRjRXJMCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXBQQnBLQkVTQnB1MHdGYVY1SW8KSk5FdklUV0JTVWZzUnh3cEVGYXJ4eHFSSVdYQ2RTSjZHUkY5MzdSUloyb0NPRUo1aENEU2hFS1o2c0I4T29jMApCZ3VtdTREbjZ6YWVtTXNCQVoycTQ3WTE5R1Q3L3g5Nmx2dFp5NWp1WEtudTBXRWFoVHQ5d0J4VVdlazlvNHVkClhyMUxKaFQxK2dydDJmUFJOdllUYUVUcjZYb0dFakR1b3IvMWR3eXRLQlFnSkk3ZzUrOHNheElLMWkyYmM0NDEKQkJydDZxTGZGK1k2M1VmRlJyeDlJZzJPSUs5aVlicFVLa0oyODBLSDNXZmt5ZDJnYlp2dWJMcVU1QjFOaDRpYgo5Q2xRSmlUd2lHUVQvZFdrTnRySGNOZldYeEFIM0NGaERua1dOMmhYWC81SC9XeWRIOERENTFaTTY4RmwyM29yCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmZVdmZTOUNwMTAvKzVuOFdkTk0KUWpXeEM1dTVLQ0RIYlRhNnBBVkdDRXBJV2Iwb08xdm5GZnJMVE4xMjgrQWFOYmwraXNCZWlKcXlEbmg3TFVkQQp4ZXVSRUFWVGtlLzNCeEVNTFpJcUc4MkNNam1iQjZNeE9Ma2g0VmtWanIvWlUxazIyWnBKdVNtRXNBbStXNm94CkJqa0xqOXRLU1JwRS82MGg5bW9Pa1JMeDlXV3BJeHFtSnlWYXlsOUhoNFkrNTlPb1NQN01jbEJKOS9TRGNQZ04KQm9TNjBzSUlXQ0JkMEhpZmx0SkIzZnVGVHY3N0xHVC9qQk5TTVFtdUtPMHVaeG41bW9uZnFDUkFiN0lzbVdwaQpUcDk0aU5yWmJTR1AzNTNZR0ZMemEvWVl3L2lmTzl3eXZOZktqNVdQYjlBUEl4QURsK2J1aUhia1V4T3BEczBTCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFJMTDNKZ3dremVFbjdzNjU1K1oKU2h5dTFjUHRSRm9tQzFxcjBPbHJuaXlYa3U2ay9wbXpKUE9lL0FLb0laL2JoMGJSeXFIZmZtMlpvZ01qZnF5MQpNZmx0U1B6RWlDbG9LckpIY05RbkpuTTJYMmVnZUplQnNpUC9LZC9qSnQwYjFOUHhEVXVrOVZjVW1JT3BGOFFmCjBuSXpVV29HekhQZ1JBN3JOcnIyQ1htdlUzdUw1SGlEK0NuNEJrRU5yczU0dGlxMlJ1bmg0c1BRWitEYTV6ZXoKdlR4THpSWURoSG5zZW1XZ04zQTRMVjhzTTN0VkViUnBZeUxyQ0txK1R0TVpGTTE4OXRabEIzVWJYdllyaGM1bwpTWUVvWnVNR21RQ3NKNjg5WnFBNHZ1VDZNeUZ4SThXOGZCMXNxN01MOWlxeUU3TVp1OGlNTUExeE1nZ0pVQ0k3CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHdtTktTVkJENXZGaU9NbkRFbXQKZnFyeTExTVhhRDZ4WTczQWt1Umlwa3VNSnhubmVhL0xQZE5Za1dtZTNEWHZiTXl6K1Bob1Z3RE9KYmNmUlQ4LwpKZ3hrVHNROUVPV0RIL0t1VTRTdkdBMHVlM1ZVNi9LVDQvakRUK2p4elhMSHdoUHlRNklpclVjOExMSGxCeW1OCklNeDVscDRybUVldlp1Z0R3STBtb1FGdjNDZnNLRDFJRHM5MUsrdnMxaHhXVFd5S2xxTlFWa09BWXV0MlZ3Y1UKMUdYY1VBSjYzZmp3TmtUaHJvZHlmUGxmSHBuRVlRb25nQ3k1RFU4K0JWLzVJM3NKRnVub1lYNktEM1N6bmVmWQpVSlFKRDZEVmMwZm1BMjZESWkyTVovM2RNU0UwdUQ3RG9raWs4S04rajk2T0NaWnczSkU5SjBROVJYSStnTUZRCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemhaQVYzNVVnU29aejNqTEp0KzYKRkx2U1pSS0hXN09JZkdscDRyMkJtMnJYZW5QeTB0dE00ZW5Fd05Sa2wzTk1LUTQ5NnIxL2VJUnFhVFZ3U1hxMwpLc2hwUC8rVTRSQjB2NU9lSGFhVTNBNzM3V093Y3VhYUdaSWVhUnBzY2FoVTFsb0ZtSk5oV3lOZ2M0TGRNc1RzClN4cUUxVGVwcU90U29DOHZMM1pzWlpkd0ZqVUxmSFZFN3FQMGZ3WWxYM1QvRTNMelpZMGpKS1hsRS9aVjZ2aWUKMkZLZEo3YmxGNkRJbXBUZVBObmpnU0JGejFiUUowdTk4TEtyanJiYjQvcURaSW1rT3gxRThGVTlRUFR3Ujl2MApEQWVQN0p1eWx1MDRKNkpUdlIwN3l2U2tOSFZuN1FPb3NXYjAxbnhpamczMDdibklVZ0p0MHJEb1g0RmY4NjdyCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1NSd3d6VFpnbG9XVTZwT0FsdloKL2pqam9qRmEwWnd2YWxUZUttOVBZNENDaXhTTmMxZ1dVWHdJZHlFS2hrZm16d2J6TzYxamNuemRMUGcva0ZUZAprak1xUzNPeEdJQmJNNGx1WWhFMnRHVUg2QUsvNjRIR21OTXVDSm5SMm95dnVCalY4S2pmRmV3NlB4WFQxK2RjCkZDM0hvUmJCRlNYcWxONjI0TkpNd0oyd2pjN25obEY0eEppb1hDUWdJOFRLakFiWit0Slg3cnAvTmdtYS9mOEMKb1N1WXZYTm4vOEhrczJKL09zRzBYTi9uNDlHRnc2RWFYRDFqSzE4RlZULy9la1A4a2I3Z090UkMyS2JwTlhudAp4L0cxQndVK214L1oyZ290VUpVK1IrdTFGMzJiUGU3bmZYM0libEZWNXkramkrcjhkSUtLbndFdUcrcVlTbEhGCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGxFZ0EyRXgveEp3cVdqTjgwbHQKSkFUWkhXQzFpZGJ6b3JIQ0VxVmRJUndLcmZ6OEpGUERCTm02SFhDK1NzM3g2ZmRnald0a2gzWFA2Qm0yK09CSgpVR2Z6WSt3NGM1eXdIWHVaNWVNNUlzQXg3dUh0RC8ySlp5dmg4dEQzVEVsL1hBWUpWaFR1bTF3aHY4VHBnbHU1ClhTTjAzMDBIQis1dzAvVURoVVpEZ1BLdkJhTmEza3FvaTB3QStmSWx0MENSbFZUTnpPdWFTKzBycEg5OGxkL0QKY1lVNllxYUJUeFN3T2RZQXlMdXJBWXZOQ1h4YUVyUVJEL2dmZnJFa1VDMllYWnFYSklGVERqRTAySEtVUk10VApFTDYvZUxtVVZhWENQNXpXOWs2UWtNeUlnNFZuM1E2UGdLYzF1Nmp6NGhjY3ZlM29iL0NnaG9BeWNOUkxuV2VtCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBaytqaGxzNDB0dVd3RUFsTGFNUCsKSG1ORnltZnBGL05GUlhQc01US3pmQWg1TVlOTzFOaDBrejlzVzNhUVh1MlhsY0orQ3hpNHdSL0dXN1pPRlNISAplazhUVGlBakZvdnlnRnFBb2FENkhQUlhEREJiakVRZDVtcGVETkVNS2k3TmZYOUJHWmFTb2JHc0pFMkt0dTIyClZwZWQ4SlBiZGVmMkQxTU5qVU44aUFZV0ViRjMrUnBkcXRHVUFqcUhRZllGTDZjcytVYm1rYytKS2RvMVJIQXkKZzM3eEFZRXVxLzkzRnZOOFpnNnRYSE85OWl1QzFxaGN2ZEthVEp5UTh1ZkYvVWNBV3ZYTEFMRThodGVhSmE0egpEMU1HSDFoVkNTd21HWTVZci9mMTVqSHlzem1yUUUxWXlPYm8vMWF0VDNzVUxwWlc2M2VOclh0V3M1aVcxNkxECkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWw0VUEwM2hxSGdIb0lqNVp1ZGUKMzYyZFF3S21XWVUrMW1odzJzNkZ6aTc4SklWSFEwTm45SnpNY1JlUzEvcllmZm44NXozMGgxOEF2UlRHTXhJOApYcERxcTAxMHV5SmphaFVzQ2NsdTNtak5CWS9peFVTZ1pKb2ZjTGFUSzgvWjFQbm9YUFVJdFNhL29YaDZ1NkVuCkR4T3RaT2QxeXNVUVovSUltWGdXdXVaM1ZyRHlpekhwcTg0TlB0ajkydHNUOXkzYUM3ajg3dUhma1FGQnBwNVUKMFlGZVdTNUxuTVR1UjNwem8xNUwvUzVGSXFGTlRVOGcvejBjcHAwbW5yeGlsbjdHTm9adWlsTnU1dk1WUStPdwprTU1zRkJpaEVVcGJUMy82LzFlY1A2TGptRVVJNlZOem1NZ2ZqNUpIK1hiQzJuNC9wS1B5c1FwekQreEZKTWVxCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDlMZHR6eEl2SHkrV1UvN2lZUFYKcHlEVnBlSHZ3alFlWWN4VWdMVmJ3bTcvQmNlMGpUZ2pBY2dac0g4RlFHMHc5bVVuV3FBM1ZQMFFRRWRteWFRSwpDVFhQVU0remtFMk1mQ0ZoVnM1K0RycHg5bjNobFk0cjFqRk9ieko0dlFNcXR1RERSb2VNVUgrV0J5SU5PTHpnCnV4Q2d6U1VmYnRJUmpGWktFYjIxcGJ1UGRjNmxkWnQvNnA2WXA2azNrRVp4eWZxaXEyMTNKU3BURGxJV2RTL1UKQXdJb3c5Um9jMjQyeFR1MzhvUTgxUkRyK0hqa2Vwb0xDNmQvRGM3MjM4djNaVW1mZ1dZQS85RkI5ZTJlVkcvKwp6VzBZYkRaWm82TjR2dlp2UFIwbXVmVFBtcGhwUEFFeE45UXpCTkRWMS85WHBTa0dsc0dhbGNxNmd4ZW0rckZhCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjZnUWIvaEpjVEpUY1JoSFB3bkkKS1A1TG93T052MDB5dXBJODVSUU5YcTd2WFQrZXFFaVB6Q2FCK3VnTUI4YXB4UFk4TEdzWFRUWlArMm5lbFcrcgpXVDlZQ0VZZXQ1bFNnYXoyeVJmak5tTFNzRjdodk9lZ3IvS0grSlNrL0Q4YlRVK2k0eG9MQzhHOS9IYjczWEoyCk84WHNBd2VrelBiSWE2dHRUT1RuemxUK1pNNFNta2pHRjY2dWl6c2pPZnhOM2dJNklhVFVBYzZWTTZoSmV0cisKMzZNL3FpajhpMXFhN3hFZ3hncklmT2lTR2dRaTRpWEY0eGFvWGNHS0c3QnVjTzIzamhtbmd5U1JZQUdkaHZsYwpQdm1zMFQzNjQxemN4K1JuS2NtTHowY2VaMm9QMVVsdDBkdWNhbVJNRGR4eU1pVlp2dEJDYm02ZGJIRWY3eFpYCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXRET2ord3VmRGp0UjM1cEk3NUMKQStPaVY0cUQxRThiOSszOU5QSmY4dmpHaE55OHIzeGhsWWg3YjlKcFpXbm14NHRsYXBZTlZMam5IOFRBNEwwcAp3TlRqY3BKdjhQc0t0L056Q2NIZXFOT1lJU09lVWxMMkkrbHI5dzl5cVFFa3Byb1VQaGlXYTdhNkFnZWN3MjEwCjZBd0VnSGEwZXc1NTdtaWg5YUNaQmRtZXhycFp4UExIK0VYWElJeVZyRXZPbXlKYXBteTdtVWZkV25zU0NnWUoKbUM2Sk1qRGkybjQ4Y0F0RERpemFReDMxRlJ6SnkyeUNEY3FhS3hZSzFUeXFReFdEMnBQZkdFMWh5dVV0UWI3awo3ZXFJTkcwdGcwSUxpVFdINm1MUllNQnFobytBaUJsaU1zK2wyZnFOVVRNaEtqYXNrYXVkVFkycHFweWlOd28vCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDlqOGdHd3ZmM3cyRkFhUkszOWYKNlJMZzdXTWs2ei9RSGhybFBSUUNFMzgzQWxLeWtFVU1Mb1lmaXBLc0J5WXh6eWJkeVBVdGRZV3FyZ2wyRkI4WApiRXZnS2lkZ1NmU1YxdFhlMHpjMUliZ2FiRDg3elY1VXJXZ2RtT2JYVmF4WkFWcDRVL2RiNEV3TVplUmkwSUtiCjRCVU0vZ01sRmtwYmFja2t0TnhoSXQyUDF0TXhPMGdnaE9lQUthLzQvb0tOOGZQL3NjMWppM2N0OEpIcTRNVFQKQXphc3ZQOXFnbWNXOVNBb3JNZDVKZlArRXNYMlZ0NUdNZXo4bmQvcG90QnZ4bFdoekRWUDhFbmRQZ3lodTVJQgozbHVhT2pYNm1qdVFWL201SitBT3ByOXVVb0lpNHhycldDM251UG1heWVhRjNtU0NoMHowOE9aMnQvaFpwNE5WCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbWR0WVBjb0VLNW5WdDdoMS85dk4KWUpENGRXVnJ1bkxPUmdtU3ZPZmRiNmFjVHNOMFF2TlpxZ2c3b3N0S2JzUTRISm1XSG9oM1NlT1RiaXgzVGhCbApla3BFbys0WXhTN3dQQTB0MmpoRzJ5T0VqU2JveU9EbWlDMnd6WnFFL2JFT3JEaTVGRXdxVWRBUWo4SG53NHlBCjlpa3ZnT0VQdno5R0JPMVIzeTk4NGtna0Z6dHlmSUhRTHRnZThocS8xem1DK3ExdDhTTE9vT0J1aHo5cmZ6cHcKaVBtK3Bab3NYZDhvb0Z1RWJQNUE2ajAvaUcrUE9SMnFpMHg1dE1vQ0N1b2ZPL2FyejFQQVNldURDUkljdVpzVApxUzUwMEFuY0lxODhuUmZpbUd6M1g1NHQzcUF0V0gwcDJONmhsRHJ6VnE1SHkwZ3cvckxzeSt1YlFCVVJROW5OClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjVLcFJTOGFtMU1VZjJKb2x3blUKcGVWUzQyRHBwNFFrVHZ3VDZzQTlRcWd1MFBRQUlvbXllN2ZiVUdLSEhiYi9vWTVIWTcwMXMrWGtrWmd1OERxZgpQRTdTVVFmaC9tOUhoYkRqWGtTNk9oUyt0NDFTdjdVaFlrbU1FZUVvYmRmT09zamp4QWVUL2tmTEUwcFpEeXMrCm9oNVJBcVhaNnNMaUQwOUpyNlVoVy9wYUZ2WFE5dmxReU13cStrVXNsMDl0UnlsdG9xZ3NCcjBGQTVjcmt3YjgKQlBvSlpBb1VDUXNFeUFtdmgyamRYdzUzOTVQT1BnREJwS0NnYndYMnlXckxjdUZHRGcwYTI5dzV4WDdBRHZiWQpFN1ltazNadllTT0hrVS9sRjREamxZdm9HTzBSNWlRV2hhUVp1dktFSFBQNmZSdUsxV3AycWZQbUZCSEZmcWRVCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVQvYTJpVUtmY2xqdjArdEhyZS8KVVpzbkNDRGZLd1Boem82eFZqS2pQV2k0SnpCekxFUlFYVkE4Tk5KYzJTMUkxV1ppeHpCTk5jK0lYQ3dIdTc1SAo1N1g3aW13eTI2bXE0VjRoYys1VlRSNHljTW5SS2lCNjhWZEt2SHZFS3ZhY2gybndLZjhzVDhvelBxK0FyRXE1CkxDdlh6TW8rQVhyUXNNWSsrQUM4Z2tkUW5DdUtaMHJ6eHl4ZjRkb0F5dUlJQTBGWTZZeEd2V2F5UFBwTHdUVmYKTUw1bXdWcCtFUkNMSS84QllwaDF5T0dnRGFscTZ4RzhSN1MvNUwvYWFlNlV0Mkc4cmt3MmxBa3lLVWxSWWRubAp3dnJENzlLWHNUWGdKN3Ftb0ZpTE56eVEzNGdsZDRReVJLaGtMaEtYcE43Q1RBNDF0K1hLT2J5OVpJcVV5MXlNCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBektwSTdSdzlPTXRQb1Z5SlU3MjYKUjZXYkdrQ29FbTZqRjBkU2NCc0YzWUhEa0s4ci81dnlYOXpNaUJuRUI5NFU4R2dORHpsNkVlNXNiakJ0YWhQUApMQmNTWUtIUWRDNDRlMk9XUklwS1I1MnA0Q0dEdTZReE9XTlVMK3UxMW5qejNaOFJ2THp6OHU5QktTb3FaQU9sCndxK09tSndIS0tpc3VoUGdIUnRldlhBMllpR3BEM25sRG5LanRjV2I4dVVidFMyMnVoNUlYdFB4YnVORm1hczcKU0NLK2NPQUJBSjRXdmpvejMzRWlYK3p2UEI5aDYzRVNqNVNyV3RsdTZSYVduUUdjbGpnYW1WUHl0KzhMQW1DYgpPeG9uWnl3VDNKcnpVRFovNFBQZEU3SVN3M0xlWCtRM0V6UmNaalJMeFQ4TVNZRTFGMU4rVFdqRlg2ZktuREFxCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWk2SVN1VmhIOEZQQ0o5Mml1aGcKNGRCZW4ydVN4RVkzZlRpN3pTVjhOVXBQcGQ3WkNTNWVva1hUTHVqTFlrbEE4V3NjYVc1bk13bUF6d1lhRytMTwowUENsc2xHdWFqTnY0MEIwQkhhakdmT1E2U01EVVFyN2xFTmFkaFIreTJJYWtIODIwcXIwa01DOC9ydHg2aVNjCjZrMnJDbGRoQlp2aTZzNjZGMmMvK29UWTl0ZGFvK3hUUVNwQTF6T0Jyb2NtR0V0di9GQ051QUtscjRZM3E3OHYKRzJiS2JrQkdrZHl0SllibkdwMGdwR2taNXdXVTJGcW02ZzArZWsxaUtaYzVzazczejdJRHlxTlRuRWN1Snh2MgpoZmtpdG9RZ05mNmZMNmFtY2VwcjRMRk9kNDVZVEJZWXVIbWNpR3o3RGhyWlA0RlJuNFRyaVlweEo2V05FMGhPCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTRsRDRwTHJrdXlhalRkTkpLUHIKM3hJcHlaVUQvRmJoSHhUdld5V0VVK2Q3V0RFem5LRmR1V3h2RWM2QW9yWTY2enlTb29JR21NUVRsclh0bGIwVAprQ1VPeGVTNFBYVjdFN25KTDl0L2FwaHo3cTZSeFJwYWpMNGJCd0lqS3A4YWVESEhYTmJhWTZvcHkxTDZIVEFSCjlFeXVWWXFYVG5EMjUzK0I0YU9hamd2WVMzNlVLWC9iNi8vNVllc250Y1lELzBtU00rcGxKTzZ4L1RYMVRGcVgKdkR0bFVReW81OFA5MS9JMTBxVnlndGNGRXViVHdjYmJHNXZ6UWsxdzBIYmJjUFV1Z2EwbUdKRE5RN2FKb2IxZAp3WDZKRzliZFF1VWYxVTZNaTlpeFk2aUUxeU5yQVB1VnczQkt2K3dRRTdjcTBHTzVpd3ZTMjBXYnduMTYrcUpVCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlVUQ0pHUDlsdzdaNG5PcTZxUVEKdElYb2lzWUoybmlKbnJGNEpBUU5aRi9sQUtWTFJtK0RqZmJjZmMzTW9Ra0tZeVcvcjZxdWxmb05VcTdlOVl4cwpuZmt5RWhZOVJuYVB3WWpqYkpSNUpZaU1TcVRBK3RXL05wUll2citlM3drY3VBek1QVmJKZE5VU0JuMDE4RzhqCjZ3ZWkwcDQ4ZXpYMVJSbDQwZ05maW5RS2k5bS90Y2JWU1FSZzZsUlFjbCtBMHFDNUNZVGhxSVpRVUcxZ2lGcVMKMW5JUUtNM2ROVGFvWitJSVd1QXBhdzRvVkcxRkhBbklTNlExa1UrOEdDVWZ3SmN5SmVEYXRpL0NaYldadnJvSgp4NFRsU2I3ZE82cTduNXZ1YnJtU3haQ1crZXFBSkJqNHpYZmIwQXFJUVVMTmhOWDI1dFZqM00yZklSZXJzZE5XCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFZadTg0RDNpZlZNZVZFMjNCenMKQWJ5Mk4ycG1rQW9IcVFRM1h3c21pVFVNeUxhRERaazN3VSs0ZVphdG1LTkhYd0JNSUJNeFRucDdUWVJncjFZdgoydkxJRjhIL0VCUXhkVUZENG84NCtUemozNWxtSHBFRG1ORmdEem1rTmxsOUlGOHVEcHJndHNuOW1XaTVoemYrCm1OL1BnNUZMK1JrNy9JdkRiQWhOQnFsR1B6SWZCYkJ2Ym5CV1VNNGt5TXEzMnI0RnEvTzFoUGRMdlBNNXI2ZCsKUXlOR2NmUEFUeWlnUWpXZzN5SkVReDIxMmRVSVoyN3Rub0JaZWF5L1J0aG1hNEViU29tMzgvZVpnVW84M1AwZAo0dGVwUk84azgxQi9QNmk0TU4xa0RtZ3hpNHJYWk9RaHhiekw4Y2dvWWdGKyt6bCtISXNiYk5STXZNK3pxZEtxCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckkvdmpCb01wYldOQU0zQm80S0QKUDhxNVIwaGZZeTZWZkx2cGhRUGEyTWoyeGRXdEM4amF2Z3hubEZkZElUVEIzUHRNamRFdWdFa2FtL2dzTEl4VQpnc1o0b0J2Y2JCK2xrdng3K0dId2ovZUNHTyt5WVJXRlBrbnVUcDFwd0pNMWJFTUlnQkxWN3ZFNTB2TmRZT3EzClpIVHdtbWxFdVlodDd0TWZhUFBSZ3cxbFNsSllwNEo0Z0dGTDk4ZGpVQ2JxVTdqa05Rak0yWG9HWmEybTdOVVkKc1NhSUtBbmdlUHEzdWVwQjdURGxMYS9lVXZXSm91QTY4ZkVhVjhjVEZseFZFM1lJMGRka3pHYjRSUHhmMzVwbwpJRWJhMERkTlYrTHE5bno1RXBWenFRNGtDTFpSZTdvWmc3UmhhWkNGSnd0UVlub0F0Ty9UZU0zS3Bra1RjdTlXCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2RCcy92dzZnb3U0OFFRNHZSUFYKd1VySjh2eW1lUWRtVmZLUzFnWms5ZjQ2VFZSWGRCSldmc1RnWEl4UnFpVktRbXVCYmF6ZW1jOHJyNUwzUWdsMgpSd2hwSHA0Z1dLOUpsL3VwN1B6QnhCMDRQYXF4RUZKWVpKSDh0VVpweVhMbGhiTmFaUjJMMVd4Q1pxbkFJKzdOCnV5ZnNqS3ZOZDcwNEVMaC9IcHF0eTlTb3FjaFhUZ3pHbXpzMTZDMmlaaWVITU5DUzhmV1ZXUDJabjFhcWdydnYKeHkweGlwT0VrUWZHM0hBUklGYmdBTnNuMmJlMDdLOXlkclByRUptSVo5RFVVL3NobFhVU0lNM0VmNG5PdmVJcgpvK2tpZHQ2a2pkeTdUQlBzSlRVU0VUaXF2WTR2R3pTU0JCMTRBQllZNmVmK1luM3JleThCUVdzM0hxcG8wVW05CkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0NUbFJuOEpsRVFqWWloS1lLc0QKNlg4cTJ2d2xrZnFEM3BWTmhKYUczUWd5aEJKeTlUVVFINUlxZE5NTGpLcGZnbDRMYm5lSFZZRUFxUDROUmtqdwpKV1RtQSt1cmNqYjR4RUI5Sy8rYXZXUzNDTmR2Sk5CNGxmVGlRRW43dFlLUzR6akc2OGsxQVJrYXlReUMwWkFkCjYzRnU1a2c1d2pHU0g2QkFWZExJU3pCLzdIeVR1VU5OV29LNEhEMGlxWGtpYldpam0rVnBoYWIvOHNDL21ZZjUKaDRJNUx0Z2V5MTRrUnZaeitRYXlncVFyQ0JxdVgwcmNHTlpvUDhEWHhUazJvUjFQaVJYTWlQeEJQci8vRjZrMApjM0hGYWZ2OUNMT3puTXFYZXljRWo0VjNpaXYxQ3FzQWZpNmc5ZkhXZWpackRXRmJ3eDVYMHFTb1JtUWtHTCtTCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGh5OXhUWTBxcjBTS0NnWnlXMTEKTm9YZ3VneThiMWNDbXdhSkNZWlk3TGdXem4yQ2x6VWZCMmxQZXh3c3c0VXovaGpXckozdVcxaGNuQ0liMXNJSQpkWXNVTW5SK21oRFg1VXc2L21pZnA2QTY5dWx5aGVyLzk2Qm5pam4yY1FrL3RyalRnYkJYQ0N0cUs2cjZzM3QxCkM3LzJuR2ZpbTBnZjV4aWNRYzJnMmJpUEJLNlFDZTFKMDBNZnErNzRSSitRWHpZYVlhZDN5WUUzMS9JT04xcjIKWnY3Zk5POEJkNGlDNy96cEdsZVBiOHZtbDkxa3JzVkQ0YUw3WDg1WW9JYmJHYUkyeTRRVXZiTEFNc3A2ZlQ0NQpIMHZveXlmN3dyejR5U2h6MUt6OHVlb1NMVHkwMlVmaEhCSFNRZStvcStiZis4eDlKR3NOSDF5UjFZYnhEMCtQCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUo2cGx2b241Vjc4REU2UXExTFIKWHBNTnEvLyt6K0pNUURZSHdERUx3RGpSNXI3aHp6bGRIOHBnTllndWJRaVpUajlCV2pPK0QxOWQzK1gxLzlzTwowRU8rTEV0cmdVYTVxUS81b3d4U1BLQmlFUS9BekMwU1ZHNkl1YlhhUWRibUJZbk8rOEJYakNIUFhpczgrdUFICkZHbjd5T2k2NHNIQjZBRGM1bkpBcExjWSsraXllMGVnM3B5U3J0VE1FcStiSng0VUIwNnZjR1Nuc3FzWjNtcisKdWpubi9Kb0FRbE0rMnhoTS93Uk81ZEJ1bkNqRGFscUpZWnFHT3RtN2JPVkFhd3hoY2k1LzV0MHBONnZzNDNGTwplelltMzdXd25HbWVVTFdubDBuVmQxZ2todjBhYXlxZ2pZSjc5MVBpbEJQbTlsZXZQRWsvVFVsWS9pWUVhMDRZCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemR4c3hWamY5dEc4WGtxbFZSd08KUGViRXFLU3hucTdyQWN2b05CT3Rla0wrSncwWjRlVHBjelJFUkpnRE8zN2s3SGgrTDF6UndQUFUwb21XWjUxTwo3eithM1VtVHpYZU00NXdXK1kxcVBnNllNbTBnSjg1VCtUVDlJdzJXRUY3RmFXSmVaTi83RXV3bjBsK25nQ2dYClVVbDFhbllaRXlUK29jRVZpQkRpTE1hQVVTWGVMa0l0Y2lDekhUVXFkaktLWm4xemFJc1RiSHh5eXZyajBIc2gKTWR3K2lpVlB5eTJWeUNOdFdtbHdjT2lPdHQyVFB0SThsWlUydXhFOFg2dkNNbjl0MHB6YVpWZ2FFbXAzYTdZZwo3TjJPemlJbUJjSnF6Q3NkRzBZNmlEZXBPZEppMFhHbjFuUWFKclErdTVpTWRybTlZdlVtRW4xNHRnRzRralJPCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0I2N21aSU5peDI2cFBCazltWHQKSUw5RHNhazhXTnFnSjh5UTh4VUNybVpwTWUyVllaZ0NET3djeTJIbWVValZxS1ltK2hHRVY2ZzNjT1dhM3kyUQo5WU1QWW9LZHBLQkwrMUtEakhqODRSaGpxQzNvakN1U3p2cDI1V05WL2hkUTVXck4vYnZHWVJkRmNYcE90eDBlCnBQSnJDUFl3TmRYaWZ0cVJCaFNiQSt4NXp6d2NvbTZpaGJyVmhnNzhzTk8yNjNWM3RrWWNaLzR2elorNXRjcUkKWDUxRDVSSFhza0hzNFE0OTJaN2I2Q1NxbTU1ZDJlcFc4bGRFQVB4RkMyQXdYR2hoSjVZOWxIcmZ4UGxJcWs2RwpWYmpwMUM1ZFVPVGJUR0JJNVR1eHFLMEJOeCt3TVQ3bkVhS2U0OUFSVkpoWklPQTg1ZDN5UU05ZlJhcDdNaEJQCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnpvTGJGTzcyeXFxYmRrTFVYemoKaU15aUh6M1hVWW43UHIzcERtbzJBL2Noc0k0OUZianh0OHh4QzFZbkM2dWlpNk91anR1VDRnSUFzd0tpaDVFRApXYUx6M3A2ZlovdmRackxVcXcrWDcrcEIwZXU5bG12M0VJdDZzcFNycDJRYS9iQmFud3ZxYmZiaVRITmh5MDhZCjV1eG1NNThlVFdLZmdoMStETCtJR0pHYmlDU3NCMWg3RnZRVmJPWnJYdGVPTUNMc0VPb1E5cFZ3cTlFOWlSMDQKdVJ3VktPU1NoeGNKaGgvejFlWDVSbTN1OWZjT1U0bGVoL2g2S3JhdUplOGdWQThrNHZycGpuQ0xoNkVXMTkxWQpzc1puL0o3VW16VHF0Y3hVeFlNcjFlSzgvb29CRlQ1TDJ3VXRBMEt3dk4wWTdIdHZVd3Bhc0ExTGMwdHNpcUgvCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVd4NzlTR2RYOEYxU1p2bzBKMEcKclc4U0llMGU0UTgvdlg5dnhHVHBGSmcydW5SRUtjNitTVGNOREhUZXRyUkY5TTR4Q1BkYTdXSGl4dWxyYWtOdwp1SlpBaXNQVWZ5YVdFb2l3VW5waDZaSkk1V0wvbEs5ZEVSNHVYZi8vZHE2aTR3K2h3YnVreUpBeW0vTVF3dU40CnFtenZ6VFdLU2dJSnVvZzVoQkY3MmtDdnVzVGcxK2xTUUNxNkJsS1k5RVRRZVVKUWMvUzljdGRncHZyUkhLL1YKTS9uZWh4ZERnN3VpWExxQmV0azNlaEdLUng0bGh3TEJ2NXExRFlyeDgzVnkvQnZQZkpLTGtmcTdzUjZPNzI1MgpZYy9JNHVpemxlekVjK2lNSU9rK2ZwWFl0VnFNNnhqWmc5QWhtN2pad05YL0gvZWlIM2RSNWRXeElobFhJVHo3CkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMklma3QxUDlUQm5FVjZiQjhjZFcKU3Z5bFFWc1M1eVlFV3BnYm44aDI0MVdSeWtVUVJ4YnVaY29oQWZEbE9nNnFJdmV3YTBrTVR0K2xvUjRUQnV1Lwp6dnNpeU9DMDJ1ckFmRzNmVHROUm9CM3hGdlJjNEtiT3BqNGFJTkhtUmRVcDZ3Nmp1eTFCalNKS3huQkVlQTVnClpteC83Y3I0d1lrUzFvV3VGR0F5T3JBMnRvTDIzNHNrMmJNczJkQkh6OUNHVTZPZVdTSWpBTnR0ZFdvYlUyRzEKZlc0dDlLcUU1ZmFLWitlM3FSN3hsc0phby8xdG1EanRJVUtUTzRIZEdrb3g2Q1hqZDJGaG1BQWFDTmFVNTdMdQpaSDRWb21DOWhQbERmbGM2bWV0bVNNTXpjODlNL3dsTVVDS1Z0dXdCTWpDMkVBUnpOUU1Xd0pNdzhhejYxOWZTCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTFmQ0Z6SG5BM0Nmc2NEZkpxK1gKZEhhTDVYaWdER2xzWG54RHZGY3AyNVpoa1hpbnV6WmN5amNWdnB3QndEbi9SdHQyRGVEcUt3Y0M1QjRpTE5YRgo4WlIxb2RSR0pSK3JSdkRxd2c5OGNGM3l0aWZjMTZIWWJ4WVJmelVET1JSZTgyVnF6UG5INC9TWk5ialJYaGs0Ck0zL0orbUVuMC8yYjVYWkk1UktEL3FoSjVHUGhqSzZ1ekI0OXRyeXkzbHhnOHo1VEdXbXI2Z21iaDB4b0IvWG4KVzZWZ1ZlTjl3QTZROW5zTTlhblh0T2xDdy93OHMxaUF3bDByZVFPWW9sTDZKWEN1NVJxR2pnc1VLZnduZ25WSQpuQ3pDTUtzeXcyNzhlMnkzdFRIL0pEYUZ4UDZTOWNnQkE5bG5LVGRTTlFSNnpmRjE2ZmNkMFRDR0NoT2FHaXhOCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUhQRnFzOCtPWXVZUXRFQ0NSRzYKcjgvdzliWnhLM3RYWVJZSzlqb2hIakkzbW9lcUV4N1E3TlF5dzc4MDZ1VzNrNDF1bFRhNHRxa3hJTlhyNmZ1cwpHbElxdHgyZk1CVmp5dkpXSUt6T1YzZFdtSG9sMy9SVU5KeEpKNmpweGRIb29jeVpSeE5YTmkxVkhOSjdheU5wCkFLcFd6WWRCSjRldExuS2tJckQ3ZFVnTWpCaWg4ZmZSeDREUjg4N3dWWnowQ3drdHNOa3d3WFhKUUpRR1ZkUjkKdm9jTEtwd1dPa3VKbjYzMGhabVRPT1FqaGxRZkFidTBDMHpZbFp5a2hMNUZWeWgvV29iTlFidDBRdjRTbVBGZQp5R1ZpV0VpcVhqYSt0RGEwZlhsNm5CcTdlS0k1dXFsL1BLbWxXV1pHeG9va0xLZW55STdac05aQ1JJNzR6dlAzCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXBUSWtCKzJocXhlYndXVTFYbE0KVi84U20rV3J0MUFZS1J2dlBWQmUrMmJMZkhuT2FZNXIxbHZIQ0R2WjNxQlA5K0tJQXJvdVovamIwd2dKZzYzVgoyRjRtL1YvTXRVSldMVEtZQkxmUHZGMTBrVjVHaEo2ZUQrdUZNUkJzY05zK3duOUNxdVFsdFZDakJiTTNNNlpWCmIvbzZNZktJWjdXNVNYb256UDFmNi81RlJvRmd0NXYyc2pRS05ZcTBlc2xnWEhabDVydzhwSXUyNHE0TjNrUjUKL3NTbEJ2SDMxQk03ZmhhTm9xQys4dGU3SUZBRWo3Wit1Z1diRUFGWVk2OXFvVzgyTCs2YlI1SnZpNTVFR3UwRApFK1ZxdjRrajVJZGZJeHJ0VlRGdE1pTmFYeWpseXgxemh0aGZhb3R5Mkl2VkY4UThnZDhsOWdWQk5zVFE1T1dBClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWVFdlRGUFhLa1Jva2M3MXpQeWgKMlNkQy8ycy9FUWd0RkRpL0JzNlh3VCt5WENsOS9DUytWTlBDSjJoOE85aFVKK2JrdmdEdlpMVVdIWGd6ZWpCbwpzbTRnaktiY0YzUW5KVjNnaGRhUzlOUUpnOGU1cEM1dWplOGVna3k3ajN2RWtOKzIwaUttck9tU0NjMG9SalNpCjJUaHlhSkg4V0FLbVpzcFRsZGdJTUtGRWNpbUtGL3VjeFRGNUQyRVVTSEdpNmY2SGYwRDRaUTJIR1dMd0tieDAKcXA3dDhzdU8rU3VIN2ZFWkNQMTloWVZRVlNTc2lLT0RDYXlqbjBTWFBIbWxCbk9ETS9FdWhZZE0zaTl6VVdBUwpjWWMzUEdqVDJ2MFg3Z0FrdmZIb044V3ZvT005cGVsU0RUcGpmVDM0Wk0wQmcxSjFFTy96TVVpelBCRVFUMW9aClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBazk2WEI5bDY5S0hVUXhRQTVUQlIKTkNQUWdDMFgyWGcvWUVoZGR0SU01bUdkaDdlbmZzUmhWU0FPb09INHVweDJEOUxpUURNL1RGVFE3L2tEeUh5egpOaGVkZit0aEEzUG1qVXk5VVBxSGViZVl5ZC9QM1pZMENZK2RaRXFDSUpjb0huMnpCOFN1SE1saVJqb1oydVJICjdrMnZEbmtDbEFSY2pUMDlDbG41czdjNmdVaTEzMUIvblhhV3pvTEd1enZLVS9TNnpRaGpyR0ZUZmJMd05SNHIKd3p6VmhRcGoyUUlsWGgzRmlFN0FRT0JWcG9OSkRzZzdtajNJamRlN2grcEJ4dzFGalJTL2VrSlZkNy9YTmNISApSNlFOUmx3T0MwODNXOHUxNHNYMndjdy9lOENJdi9NMWp1QTBtQUZMZjM3c1p0c0xoVnQxS2FsTXF4MGRHblpJCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWNjQm9VcWJkU3ZvOGc0SmYwbisKMDg2Q3N2RmhqcXJHa0d3V0hXOHlINWZVemRpaUZNczZxNmFKSFZ5T2xsSkdMTW9ORkpYRmpmN1NpeVpmeVB1RgpWMWdhS1IyZDdrVGFPNnZpMG1lZExZRHh1RlM2NnZHNk1oTCtCdkFXRXhrajNMbi9LN1JBTnJwczdjNEpUbXdZCnZhc2dkdlIrYkJSRG1tMEtIV1B6UStpNnpWeWlVWVpQNFhsei81SVBzMFZLc2J2eWZyUVVCTjFER0J3V1Y2Y0MKUFJLdkZQTGJWSHJQNlhaeFB1aHViZnQ3bCtoRXZqaVR4Y0paSS9FYysraGxmR2ZaRTRNVTlrZGp0UG84ZzQrOApmNUxLbHJ0ajUyODJhK2VaOUhEOTZ5dHJTMWlBQnl4YkZEbTNhS3BOaDZHZjRMQVN2b2paM0NnaFBUT0QrMmF3CkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbW41MEJrWFllait1eGhLUWVVUDQKaTdxM0Q3VEZaSnY4ekJaZ212OXNHMC9FZ2p1c1ZPK3o4UjZEVi9wbG9Henh1OWVBcWI3WThPTlc0MHVQOG5VbAphRmlrMEtSajcrN2tuTFFOYmpBK1J2QjRGZE9iaTV5YXRLR1p1SE9HcWhKQzVsbGl4akFJN25lblErOXhTY3NZCjlDWFVjbURyYnRqR0w1Q3VyZUdDY1J1d1dKdjFwQmFKTnRXQU93VE9UMlVqNmI1b2tuelJad2VSRXdNWkRkQUcKb2FlZ3ZCTHdQeHYwSll6TkJyMnBHQXd5NlkrTFF4ZTVjdWphQnNuQ1FVcEtzU2xremNIU0praUZWTGVRUDcyOApCL2gzcWdNRXFoMm1xb0JhLzFTcmFhSTQwV0VVQVJOa2xNWUdkUU5mWjEzQ2E5ZnJiM1VueFRJZXJlM3VmVnhjCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGNxQUMzQUtUd21wNEVWbEJhRXEKZ1Q5YTR4WTQwbXFRaEluNlVQa3ZsSGx2TFRJcTdqMkJHRkliY3kyNG1VWHQyb054a2ZsYnRPd0tmSGhSM0hCVwo4WlRaeTdTK1dvMHA0c0dGVzEvOE5ENXBNNWpDQVpMUkhUODRJV3RBaklLR0JZSWZIU0dOdnlxVk45RnFjOGJVCmVGeFpNbkJqZDNad2Zyc1dIYWVBSTVXYTEwbUVCbGlMK21vakRvVEl4MUJDVUNZTU5EUFhMNEp6YkNaVUp3Mi8KUE9FUG4xeERuMGRTODFWSFREN2xVd1JpNlAyVGpXckNVbHZGejFQc0xjbjVOMjVvUjAvNkxoS1g5NkVSQkJUVQpENXZINGtoTEtTZDdGN05iZVhGZ3RlL1Mrd0tCWlorbVNBTXlqNW9TOEl2NGd2YXl0YVQ5U3duT2M2SEVwZTZDClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmNUbnpmcXU2Qk9QaXRwVFBxZm4KTDRvT0swRFNmV0NWRXhvdE80QkxmbGoyZnVkcHFkSk9EbTJ0Rm9mUjFsYStBMWcwM3Yrc2FNbW1mdG5TWXF1RQpJNjY3eXNLK0Zhbks1SzgrUDlHRnFFYlhsQlpYd1hOclhvRWwyTU5yYkEyRGpZL3pqVlhPK1F2b09SRCtWZHNzCjZ1T3VCUGpzNVhzc3VFcEdWM3ZBUU8rb252RTdGdnNWT2R0cWl1WlhCM1FUblQ4WWJvc2RNcXpzVGJjNTlsa1kKQ1NDbHB0SE9SVk5uOHJJSVF5d3NQbEo3VWZ0STU5RWhySWpuVmhoMlIwL0kwa1ZuYmJMbk5TZG9BRDZzeWZjQQpEWHlZdWowRTBBNEtOVTV5L2lJcEtvQTJVRU1OcnViWWpJdkxLb1J2LzZmZEV3bUF5ZkNXWTBrT2lDU2I2SU8zCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmJUZzh4VEJ5VGRxYitHenJ0ay8KRTAyaCtOZ1J6MXU2MEhob1ZEYWQ5UGNyaTBOL1prNEZsT3FVMWVNbEE0MEFaZTU2SjNLSlN0bkpCTmJKUWNOUgo1TVYrenNJajJLTk1QWFhqMWYrOVpWYjBYM1VqR29rS2N2R2k5TzF3OHRjT3lNYVVlZHFIbVA4a3pSMExQcHdmCkRqbnpzM1M1Vkk3VE45ZGZLbjd6bTg5M3dFbDBTcEZUNFNKNkNCaEIvTVUwdFRHRXVOUU9VaGNpcUZXa1E4VEUKMytJTVFheTNqOWVxZnJ6SXpZRkU5VWFYRzJveEFINVJZWW56Y1JkVUlHYWRPTVh2blo2QmV5a0tzdEZTOGVFTApBSGRiYUlQOTRnNjZvangwOTUzSy9NRGVxWk91dUZBZzRHUXdkQThVY2ZObndtdnhFV3VOUWxES2lHN2ttZ2NsCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd01WY0VLUU43dHkxRXBjUldWRlIKUlFlZ0t4S0wvWEVDcXFTWFd5cnR2OTBZaGVVQmk1RnZqTFZmeGFJcFpyUkRBaExMVkZGbE1EWUdEWXVZZHpLLwp4NEZZSEtVT3pHSWhDdHVQSG5CU3lqL0dmTXpDMi9mY29yVlY2dWFqVVZkS0VocVBiWDA3NkVNSjQxaHBNZ3hWCnp4QUpNNmRjajhaU2YvUmpBSW5uYmltcWszM29SRVpycUdRV3VWNUVHZUJ3Q3JmMnR3MXdvaEc1ODhwcUlMUlAKS2pxUHZhNnppajJiMHkrSXNQKzBQQzZhM25JczNZdFJaR3J1dmtHWXJnemNFMDVjMzcrdUZ5T2RXaDI0ekJuSwphK04vM3YyakZnTHhOYk5sUmxvZjBhL1Vtck5XTlJMQ29PQUZKdEM1Y0YwRXdldVhlS3RjbTBabEJXTG05VGVLCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGY4SHFBclJyKzZISG4vb1JrRloKcmlwYmxUWlQvTkNkNkZOQjBVM1hZNGUzeWtiWC9JNmpjRnZBTTJpcnlsMC9EazE5VitJb01rWkhVM2RkeE9mcgpDTFZPSHpJSENORmcwNE9KVlJRdGZwWnZuLzBQdE9ES2llQloxam1TZkp2dmRGZ1VwZ3J2NW04ZHFJWXVtVzRSCit2OGJZZ1NPbTR3cDVKbXE3SVJtR0JLK1pvRHJFRktFdVNvSng5M2RORC9kdWlQN3ZLSVJWaTd4WGR6UDhTUW0KYTgzMXFrbEtrVy9Yd3B6V0NRb2JlQkc5SDlOaldqR0F3QytUMlI5bFJrZ1l2MHllOTlhck9uZ1VnS3BoVDlkdwpNN1JJUnJkUDNlMFZNUUU3RGdmZG9nVTd2OE5nMU5vWXRCckJXdkFMMXNncnEvOTFxN1NCRXpXaW9Ibkx0SEhuCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjJtVHBwa1dDdUluZkxEMWh5Qk4KaUN1ZFNpZ0J3SGI3ODhCcWxKOFhzOFplTDg2ZWVRMzRLK1hPRDYyZmV4VWlFOGIyZ0VNS2ltTXlTbUZmOHZQcApqZThKYi94MGNtSHV1UWJDci9TUXlQb0ZVSzg0cDc4aG53T2djcXhOS3EzV0s2VGZybnNLZFFqVG9rN3JndXdUCkpLVlY2YTBJS01HWjFISTBraEcxY29yOFVVb1YvVHhYYzZnRzlDNkRBTDJaNVhPZk1PZm1rbzJIQ1VuS1l2a28KeHRSREVSTzVFa2dZVHNRbzBDS28wZ1gwVVJDbncwb1gxblNoNCtjTjlXSHpKazh5bS9KK096VlZLY21vY0hWUgo5c0hReWFzY3NNdFE2N3djdERxQ2p3U1ZvWnJhTXFDclh1RW4zWk9SSWFtWkpFVTRqQTB6RTV1UU5ZdlZMWTFMCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGtKV1RXbmt2eUV3UHpYV3h3cFEKV3hYMkhDSDRHUEZhdUNVN2kyeU01bWEyVWQwRExCbVRzTXRlZHVRM0dUcnpqRzFzTStORkF0YzJ3U1k0RDRDQwp3c1BjVW1ueElFdzJEcC9CTld5RDRZajVDRlp3a0Y3TGxYZHYyeHd5MlRPZGJJVHJWcG14ekpIUWJwVkpxaXlwCkM2ZGt3UXI3M2Zva0h6STQ2anRjMmozL1FHTU9DMkwzQ1VabnNWaGNka00weHRLNDE5ejQrb0RrVzJYdmJTRUcKRzNhQUQvWHpGblo2OXdMYWF0WWRhR21KV0hOMEd1THpsb25kWUsvTmwwT0dlZkRWL3RQSVRwSTJjekhYYjJscQpCaEo0RVBGZnNtTWY1QS9PNllnN3ZieEVweVgzVWEwTmhRUzRBcVhrYkkrSGRvYXd3Sm9TU2hJL251ZDF2anIwCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0xKS0MwOEduYXhzWDNnSUVMSDQKY3hoSlRLcCtvNkVuSWZjdExpMlJ0NlE5STR5SGV6dU9wUThmT0JQQVU4Z0J2blBCVUs1ZnVXZFpNRW54MFo3ZgpWT0VmY1p5bWM1UzhBZkhpWXdOVG8raXpNUnF3ZHhzeEkvcG1LbktBWmc2bUgwNEVBbmtkN1JIN3V6T0ZMTW02CmN4YWRzOTZJdDZkYlhVdElhK0F5Ukgya2tqTysrU28xeExPY0tKNzE4Ymppb3pHRlVvalFCS042T1hkeU9qdE8KQlp4Vm1vSU9yU2U5d04xN1duNm04UG1oQ0s4L3B3aHVKY2ZuUCsxaGlJc1BFSitoZ1JXZVNFT0hkSzVUazlQVwpEZ05jZGRidnRtTHlIaXk5VEU2WDgvcEFWNFlVYWdLbm5HckZxaUtYWlVPVkpEam9VRUh2Z1NYTVNaWXorV0tMCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdm1HckJXRjdpS3FaRW1MQmUzeTQKRGwxYUFUd2MwZHdoM2VFVVFMelI0SXZmdVdkczlrK3c2aVJjWXFYem5TMERIUTBESVBLQVNzQVF5d1hOS2FRRwpCVVAvWGM2UEFuS01nMlozZFJiOWZodEFTRFdiRy90eEtxdkNuY0VLMkg0YVpJaU9pNmRDQkkrNnJteUtJVEcwCkRYRjl2Z3JzbnUwbWVHaGRqeklzSTNYSHZQT2trVS9zY09DMFo1RFhSZG9IZGRLUnlnc0hMUEpReHQ2MEQ5N2cKTjYzOERHOHgyTXBGVlNiWFE2SmQraGo2ZTJZaDZCOGw1RzBIVTQybHVtamE4QjlXN0QvSFhqV21OMW5yb2FZSQpWT1F4L0NRVGR5ZFZXV2tzRjZ3MHZpV2JvM2RXYlBnUEhodXhNa2IzeWRmNjV3VkhEenRWME56Zi9pZHFZa1FGCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbDZBSnJ3R2dFWW82T0dvVzFaSHoKRGpsUitJY2hEZ1VONWhZck1XcGtUdUowNVdHVEN6VnpEbFhoUVpnSGZnTHdRUkxzY1VWN1VoMDZ4KzZUa1JzSAppczJ2YVRkN1A4WlA0cVF1TVh1K0MxdWt3RU1tUk9xdE5FSHQ3QXBMMkFKWUVwdDJudE1vbEdRRjB5a3Q0QkxKCnhSQjFrd2xYR0lzNHR0bWpyeFhmYlN0Q1RvN0h0WnluVUlxdkprRmQ4THhhZTh6Z2dFUll5cDdGSzk2NnBTNXUKRkFsbDFOd0dCc1NXd2IyU3Nla2dsY3Nrd0FGMmRVSmQ0ako4MDFaNzNOQkhDZnVLUHpjZ3FUcW9TWEJ3bng5UApqa3ZLQmgvd0NJenR0UjRKb1VCSmYyZzVSOS9US1BlSkNXaXBKTThCd3dQMFRXQklYSGo1WS9VVXBXUjVPak9yCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVRETzlHQ1A3LzZVcHJpeXNNckIKS29rcDZDcEgzOHhsZGs3NkIraUppR2JmRi9wZWl6OFlmYjFBdm1IaUlhK3U3d1VlVG9FZ0w1U2M2SDZadVVtTwptSW82bXYrM09KejRJQ2lhTWEzK3ZFMXZmbkl4bE9WYzBsS0pKOHJKY05sR2V1L2ZHRFZ6MzlIYm9HbUJyb2NYClNVcUtxZ2JyV1B1cHZWaVVHMjg5dHRwb1BQUmkwQ0pVOTNOdXprRVkvZHQySTFTSmR5WWROYVE0Njlsd2FPcVgKT1g1Tk5jY0tsbmdoZS9vVmsvWUhuTWdhbGQ0RmNlWEFsdDZhR1UyWCtrT3lwNDhZYzYvK2F1TXNiTjJjdGFnTgoxZDdGTzJNRHRyNkZTSFVGL1VybW94VTgzaW41MUYxMW1wTVkvMHU5WUdHRVdyYlB3UnhpckxkYnl3Z0FDTWIrCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEp4YmJudnJnL1UyM1pkTTB6L1cKbHZnREdpTElDc1NPYms2S1hVTmFGUlFveEhzcXZPdGJ0RGJDZkJnYUxVc2EyL1RMcm9OT04rZXRVUklkOTNzLwpRWkdtOUNUVm1MTVNzaXdGUnROb1hrNDJ5L0xoUGl5cWxnZHlXOHBXL2hqR0c0VHppdXVudE5tRWdCU01HeWFLCjdOTktEUktNTG0xSmJ0TXhMZ1pwNnc1OHR3ZG5qTW8yZlFpUWpJWFk3bFRHSytqZ01yYTJoWGZ6REZyaDRHakEKMmpibENMYmFKZVNEODg5YnZKRHhxT25pVDV1cVp4dnNDSXRKTkpjamkvV3ZTTFRmemtlQ2ZUekswczNuTUU4UApzTTNaREgyKzNRTzFZSDRkdm1IVGU4MUl2ckxldHovbERRTW5lUjNoMEdPazZxck10SGt5UUpjWW5ML3E0SmxiCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjZMM05tREdxVjBuWHcrUmo2WVgKbFVyL1JQMDFibFEvaEZWbFhIdmdzaHhRL1p3RmJKdjlldE9wVVRZaGFWZHVGV3diaDdjZnFQZDE1WkFHU3IrTgo0cHdoY3NFQU1rRTNIdGg3UzdtcWtTZWdBSVpQVHJSbEdqK0VYNmcwVDY3SUpOdjh3WjhwWlU2VTdBaktFQTNsCnlRNVdSUklUR2xWcTkycGpVbmJDc0Vtc1JQU216NC9jTVAySnVHTkt1Y2s0dXJuTmg2cVRXbTZTdzdVWStOR1kKZ2ZtNzdwUDlsVVJCMTBzeWYvbkIyKzhuL2hqVXhjYkhyVUZTNEdVWWVEb1YrcXFLbEF4ZmxzdENncVhwVXkycAptYUJxSFAvVmVHVlFBSEVZYTVGUG01VEZxNTI5UVlrcXlqUHlYKzFqV1IzejN4WDhxTTF5Uy93UXdXbnBRSEtMCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFV2dzQ3dlpaN0ZJb0EzU211S2gKSndjbGZXbURDQVF4V1l3b3JVM2hnVjUzdldWMExYV3ljWkhQZi9vTmhlRmoxRm11RlZnaW1pQjhzU09qamYycgpKeS9hTnFxT0FsblNtYzRYY3ZpRG9TSWlFdjBuTnBMelgyYURHNU1ERW5sSzFndXEvaFE2WVlrcVc5SUZsU2cvClNnbGJnZHc4R3hQV3pwTVNFOHErVUNkaktCcy80M28xTXpMMTB3ZjB2N0JmckpOdldrT2VKSWZjcTJpc1BoK0cKc1lpUm1HSW91Uk93cHVySE05MDMyLzNWMXFwSG1KRWJIQ2ZrVGp2ZGNuVVRzdjc5TDVzbXBHaFB1MUp5U2xpdQplc2ozY0FURk9SeWhHV2FEK0l0bVFKbXg4bGVaUFA4UG9JMWRPaHQwK0tLMEU5Tm5QM2hheEhNT1NxSjh5UXJzCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEw1WTQyVEZybnZ5OTBSbmJvWXAKNmZPc2JoMi9BZnU1YU84bFh5SlZVVEhVV3Y3VHN0ay93dEt6ekJjbW1zU3AxQlNMYUVOaW8wUStZa1hBY24wdQoxaWhxTlJ2cUJrOUZrSUJqbGZsNzJGdlMwdW5xQWF1QVhUTVpCanFjQ0ViOEhjNFJCcHB5TnVSQVpab2hqMXRuCmp6R3czbjFXMGhJUEhJaTlISGpRN0dsQzJYN3JBZWkrRHdUeFJhdHBDWk5wL2VleUREVG0waUJQWGtDeVhqZkMKY3hqUVRyTEpYZnZQWXdjVXNiRk4zSUNEc0lJS0puWWswOHdFM3VYd2V5azhIUWlUQWdESUVGQU5rbEtEQkpIYwpSTTF3Q3hYbWV3N2JZc0xrM2huUXdzTUVjUmhUcDZDcmNFSHA2M0VseVl2MVltZHBFK1RLL05wUjNXYTVUcFA0Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHVrTUwxUTB0TTZobUdVeWRPMVIKQmY1ODBsRU5SUDJFOWsrYTA5eUQ2ZkJhS3ZDWTdMNlZIWVhTQS85S1RYNXYrNUtudDlOZy9ZeXg2emx2VTlWeQowSWtLZDB3b0pJek1UNFRFWkYvV215ajRBSXVpaDBqa3BqSFhkaTdPcndaQlRIdm5FUUZ3ZFYxVUVldCt6TGRzCjhWTW5vOWhFeDNwZUVXbnU5dlVnNS9EaWplNWtXelE5cjJGeUtjMnRXMzRETW9yOE0wYUtzN3NpTjZYZUVwS0kKbE5kZDNnUnNiMkovZGVwVEhmbUwxTi9Uek1Ec3dhOGlSaXUvMW9OcDMxTzk3LzBDanhHUElQMkMyU09GNlFUMwpmWmtCa294d1lRaFQva2FHbHJrNTNGeXVWNDhHQ1dnNzZJVDM1dmJicEkvejcrM0VRcSt4SWFMMFBJK2FqaFh6CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnd6am94N2s5NENLWjJYR2E4Q3QKOUdFaXpkL0RBWjVUM3Y3R3MyMStYL0wwOTdWMjRWU0ZFSlZCdW9YM0lRNk5hRWp3d3NXcVRWdG1URmxTWktLeQpTWnNOQnZ3NHo4ZXQzbE1Wakg4eGdTYVY4QVRoaGdBODd5cStvejMzNXdzV1R0RWUyWmwwQ054TXlXMDVuRFZuCkhmNmtqUzZWZ0ZrYTgwazc1dk5tQjNPSnFaa2dwNFNFY0VjT29RQ2VlK3VtNDV6TzQ1RFE3QklnQ203NGFVaEkKM3Q1QkhWWlVMVDJudldUUUc1Q3dtRFk3NGVOWjRrcDJTcDUwdWJ5SkJBRjhHd3d4L1docjlWdjRrL1BPK210NQoyTStCcWh2MWxPT2xNRFRWQ3FzTk1PSEJDdkxoa0lud0F0ZkZzZHhJdlc4dDdnOE41UDdwMkpVL3lla1FwZUZzCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0lmN0dZd2J6OUtCS2ZKU29BNXUKV1B3cWR5RVYxR0NHQnpLMGlJVXRxdzU1dk5IUkFtMDZhM1B2cmo2blNkcnZNZnlua1psQmJuc3RjdTNaTEt4dwphRUtDOUNhSDM1QnlURVJQNGlhOThqYWFnbDlUaWtWTStNcnB2MHpTcU9GVWZzR1NVcVJNNU05ZWR5NG41QnYxCjMycnFnaDVpYjd6Y0Y3TVNVWXJuYUV5bjVuTWs1NkVncHNpelNlWXhXMlBGNCs5RFA0dFlhQk5CT0FHY3E0TzkKNlUvNEhDejBYNWpyUVFxSXlTU2FwUml2UFd4V3VtVzc1bm1mMG9NZ0ZFWGptbnZPYWpGUTZlWityb0I1YlVXTgplRnQvalBYNGhVdmZrUE9hS3RGK1k4RGFqa2E1QVg1c2JCSUlMeFd1Y1NJZkZKKzA4REJnUS8yRFpaZndPaExvCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnZ4UFNXRWEwaHFZcnd5RXZXSXEKWEthL3N3MXFLY1YzOHB3NEdsa3k4ekU2UkFyYUQrUnc3OEpmcFFFd1pjKzArakhnVk5WWDZ1dVdkUHNvWmpsNgp1MG9BRTNraWp5RXNwY2FwTllQWHEzVmtsRGZEYWdwMFZTcVpEOGFFaS9Dai9hS2I1Zzc2RFZ0emtPNGp6ek82Cnp4a2Q0UHB3SHJMWDVJVHN0UDRpT0UvaXU2UHhmTFpML0tWUFJ5RFpvd3Q5M250VXU3QWI4ckJPdFRoQk5IMmMKd0xudDRveXRaRVBBNWw5dWxOMjlaM0NsVVk3UmorNnc5NFZvd3FuU3JoL21ubDVrUFI5bnBmNDNMSlR4S1BqOAo3aFJ6MUZoRC9QN2d0anhLUTRic1dhajZmc3BPQ1FlaThUeWdJdEcvdDhDVWZ0c1RXajVVVWFwNjRObTVLdDN2CkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2EyclJTK2R1b2pCU2VlN2dXZCsKMHBDa1ZWM2t5V3l1VmFJbytZdjM1aDNxNFJ6Rkp3U0gyRUZ0OVZwTk5lY3FYU2FSb3ZGVzVVSVA5bDVkalkxeAoyMnBpeE01RjE2ZlhNMXZJOHF3NUhpZ3lBWW45eVNSZmdXTnovWkROa3BQdXBnOUd2eTlOYk03dndOSWVua3ZmCjQycjdmR1lVdVdoeVhQVW9MQ3p6WTkvMzhaaWl1YUt0aFR0V1Qzb2NqZVJPVHlkZ3Q3aURDUk12VGZjdTU5eU8Kbzd1ZTk5OEFRY0xJWGxpU2VXUVVTWDFvSE5Xckh2bitEMUlBazI1SU55RVBjcHBMbEtYVVN5YVBzZDRnQjByUwpObE1aVnVhL0p0RnIrbGRNVE02VXhMM3hnbWQyM2R1eTAwZkM5UXp4bWZiTXozSzNvKytoaWpGSjlGMXo5MGN0CnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdCtrV3lmQk1Yc21UZmNnVEdXOFYKTHB2TFNPMHNYY216TXVRc1lVdTZkNEs1b05YNUtKMEVBV01JcG9EVHNWVU5ONEdsbUNUd1hqQ3JXVHpnSDArQQpQR3Fidy9BdFNKa1BLa0ZLZWdwZGMwWHJIYnlGamRiUGZEdzlhYlFsekNrTzBuTVJ2MW5aK3VtUnorVzhKNDBKCnRGQ2VtUFBqUGtIaUpnZkFTQUpwckZkaldTa3J5RVZGSEZDMmtna3JRWm1qVmxpRDdueTMwWisvaklpeUh2aHUKSk1NdmFNcFBZd0RXWlpvVHc1Y1I0NFZJa0JrakJvY0lOQlIzczZuOUdsdkZHTlVsUVhwUS9jVEdjQnlLeGcySgptMkQ2emhEM1o3aTZOM3N4b1BKekRtTEtmSG5aNW9KVmwzRXVOM0dIN05YZXNxVTVSWlNFQk4wTGZIbm9KV3JnCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2ZodGU0b1pUSFNoOEVwU1hYdnIKT1luYVJ6WnkyTG1jWTJXdVU3aklZbTZmVUZ4c004Vkp0aXFLV1I0bEJlNU9Lbjd2dVZ1RU9aL3Q5elU0YkFtNgp1cWNJWk9Ddlc4cU44UG1GQVFTYVZEYXdGdVJsRjRyNDN1MmU5d0RpZU9tTEpLeUU0U21oMjZlZXRSS2ZtOTQ0CkpFMDRyUXU3QjR4anB1Q3REbUhiamluNGZ6L2V1RW83c1c0aCtncC80YTI5NDIrdVhXNEJkZWIxNy95eitIcFgKZFZnOUNWMjR2bkxVK3NXdHowSHNzUnA5MXlUdkQ0bllkTFRhOG16L2hCMXhDSy9hbVF0SlM2TEpaeG9KaWpTMgo0S01FeGVPTFBrSzM3THFlT3R5Z09jOGVtdDhXU0xEQWFwMElKSVFNbUhPMis2ZVlEUjVYZVJQcGlOSzVscUx5CmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2o0ZUQ5aUNsbFVzQWl0eHdJbE0KYURMQ3RaeGdUblZkNmlTT1RzVmV0bGZHYWpiM0xEbGNxb1QxOG5mV1dwajNSa3JUSHZQNFBFTXhBdldSV2N3SwpzYVAyMjZRc3F2ZFFCZmNhTXZhK0ZwVSttOXY5RnNEc1BJWnFIdkMwWU50aEwwcnFnL2xHYUZsTDdvaysyNUprCld3bzNMMm9zMVYvRHBEZmpEU3FyYW1TWkpnZTZpSEJGQXp3RHNJcTZ6ZVkwVlFqS3RuaXByZUk5UmNFTDJldysKQisyZHJWWDVQd3QybW01UW1PMEZEVkh2WVpGSzNXWFpBOEpURkdjMm03QkhuWnVMZHowcEpuSVlCQkdPRjBlWgozOFZGK1U1ellxZUQ5Z2ZvL0ZjdXhKQy9FdExQbkc0NUxYVzAxR0VBNWtGcXdDa2FVRFBBRjZPZHU3NDZ4NVFaCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1dNVnV3MjZHWXZQOGs3TU94dmQKTWJHL1p6SmRLYVRqeXA3RFltdk5UczVxUmQxWHI2R1RFdFFBTUxseGhEQUlnaXVDN2pmbnVrUFVXY080TTBoSwpZRmVKSjFaeEd6N040ZGxwVG9uK3BTTlZ3Y2ZxbUk4VmIvTFp4SEsrZnJZeC9GTGFTQmxKMko3VkhnbnFHaWU4CnpwMElNcWxQckZVNkNmMFQ2ajAzVlRyaGpCc2FlZmNZL3BqNTlYamtBNy9vTE8vOFY5OFZCd0dhZ0lOSnJYTHIKSWJHbUNnQkxJWjc0NGpiVXNmYzF5WXVmZ3R5UEJqOElWTllTdFROTFBpOTJ5MFpmYkN2L3Q1WFRrTEhIVkd0Vwpod29VRmNwck02aDNxNkFWRGw4T3JEc3k0a0krb0dhM3NFd3hXQXBMWmdhMmxjL093ZGtRcFNlbDFlMWExUUtTCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclpOanE3bjNjTDJNRUozMWtZTDcKQStPQTl5WGE0aWZvd0ZwWmphTW9TZ3I5SVRSaFFPcXJLVUVXM0pyZ1pRNVdjNG1KVU1URlhRZlhXTmh4K1gxcgoxRlM2RVZxUG1mNFdKSk0xRFYyTUtaREZFOTdzcDJzdDRxZGtQWVBGZ1ZpUDBJSGpWUlE0emNOUWg5azVld0VFClFWczBXNDlXNkljVEJmazRZWm1UZHd0Mm8yZ3RPWDByY0doWXVYRFdjTHRrMzFNa3pJOEFCUTZMN040UjdNQVkKWjRCeDFJa1dER2YwTzk3bXphbnlOL2ZGSHl3V2Vjb2FsSlg0RDRmcGx6d1dTSCtSd3hCc3RxVWlvajFiYW9QaApHTHU3WkxMa0tEeGVZR1BnNS9kVHFHZkdWcjFZaDBwc0xFMjNYcnJJeW9ycGZMU0t3c1ZMSW5zbEY3eHNHWTZrCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGtyTWlpam42QXFIMWhNUzN6TjYKeTV5OFpIdi9aRmxIZEZMOEhIa1hSbWdxc0ExSUVXNitJK1luRW82SjdodGZMVEpTanFUdkl6TTZwZFl0V2N4QgpTTFFqOWk5U1k5RURjSDAzSXhrbDZ3TUNmZ3d6WUYvNVAvL1VzY3VyUDlrTUt1aXhBQWRuU0d1bkQ3UlJOcTE4CkFJSDRLNHR3UjFodlc4VlljbWRBdHdpbFlZM1ZLUFNUa2tlN1NKOGx5OHlzSW94amRuai9PVkZmQjVqVVNNQzUKSnl2MFZkYnRORzk5YWpEOXNSK1l4REFnUzJDZXl0Wmt4elc1YmZPd0hPSWtvUDg2di9ibHYwN1pGYVVtMHdKRAo2Q01VZzZIYkZ5R2p4SytncE44bG9KVE03QTN6UXMyZU51VDcxVnJ6OGJiYUFTVi8rS2xRbVdTekpnWGRwVnY2Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnl1dG1xbVQ2NjJWOXJmUVlEaGsKOFAvZDlUWmJzUHJMemprcHB0MkRjclN6UDU4a2tWbkg3N1djVGJvc1B3OHE1RUMxQk9PSkdmWTJBL0M5VnQ1SgpqUmJWZWs3enVUYkxiRkdkc0dneDVkNE52WDRjSk1mTDdnOXBQdXdNWnRBcS9OQWU3MmNJT2NTclBZenVxZXl2CmsrUzVHVDdxeTFOSTNLOVdPenlLSzgzQ05kQlpINzVJZGlRblNHMlBFNFBWWDZTcUJ2ODJpMjJDVTVLbUVSVGoKRFVaZ1RFR1dTQ3RYUzAyeHE2U1pFcDU2T0o0OEJjbFNNd20yaFExbktLZmpxM2pWdGFlelJKcStpYjVrUS9GMQpHbk1sdGpBYVFsbmd1cUQ5WEdxTnhJOEU3MTkrVmtDSk51VExJQ1VRQnMrTC92R1ArL2NLeDcrTHpUa1JZTHJqCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWVNbUdjK1QwcHBnZDA4QkJaaGQKYmJIdTIvSGhEWGJJeDJ6ajhOWjRqVDRmcTMzMGtISE5WZFV5ZEg5TS9wSXV6YzV6eE1pdytMai8yanVWYkU3SApTYzNZRnNqQWZMVkZHRzNkY29pNkw2OXhCQUlzYVFWajQyL2pTYTVScHNmTDZURWZGZTNjZHZqbU9tcEludThPCjJINjA3TmJUY2tyeTJGUkZocHZ5YTJ4RC9USElFOTlDU1hJTFBhd3lEc0xwOFRNUW1NN0lrQ1JLOXhiUENZSnAKUzhMT3ZPMk0vNlpST2tjSEpWYk1xWGpoMHdhZXBuQzVwTmRIT2psMFFGOE93RFVaMVpsdXEySkJ2Q3kwOUZUVgpkSE8xcmdKaGVTUFdMVXhlWk1TVzY5VFBNTVZabFBHOGUxdCtDa0RpT3k3cVlJV2tHTjVGRWRtM2t2K3dWTUdYCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTkzQ1JZdHdhdjFsSG96azlpZWQKSTNjRkdmaVhscjE1LzFUZ0k3VFBRLzNsNlpsT3hsdG90eFpTK01WTEJSOHJTN1M1QTlQbVdpaE5MeWhHNkFibwptbVJzTXFmRXpVTFd5M3ZzWmkvOFg0ZXJndk5zcG1DM3M4dlJBaFpxTHhwUUxHWDJjYXgzS0tPeGhmaS9HTUxmClQyVzlFbktDZndMdzJvdGZmeERaNDcyS3ZwWGtEcEg3VVR4S3VaaERvMXdwdDNySzhyVWFMeHNKc0dWdDdoUkoKbEZlWXdRL3YzemRuSEhUNUc3M2ZkS1BqZnVka25oT25QY2wvL2ZxS2hkY0FsVU9qSDFrZHliS1hMZjE3aHAvawo5NjZWc1l3RzFhU0JlM0N3M01NZUFnR0tkd1ljTW15QXRDTzVsa3BaSm5aNTJZQW0yNTZmTDdMdUt5SnZQMTdECi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOVFsN01LR1BtS1NnNWpNT21RUysKUS9HSXJvSTl0Nkh1bWRmUi8zM1huaG5OOEJ0bjlvSU1GV214WXB6NVR6Ty9MclpTUHROcmtCZC9IeWdSVmw3bwpTanh6c0ZKNi9sTGE0NUVvUjFTQXJkS1h6b3RyMlNHZDZGUmsveVE3QlZ2SmdLeHRWdkU4WEdjdzFPU2pIbFlGCngrTzg1a2p3dkR2YnQrMytzT1g4SEs0WFVBWEl4eExMT0t3azdWL2Y2S1daUWl3bHFSRjRqbllJWW5BMUFKSE8KQzhXaUFWdVBCTjNzMXMxa2JpRnV0QUtOUjd1OXJOSHRDOUJTYmw0bFgvNHhHNGZ2U2ZRbzNydk1POWNKRzJxRQo3WjNVQW53KzlsMVFrRWFwSFN4dTlpN1FvWm5jdXYyRVptbThJaGlybm9wTVFtMHNEQ0wva0VZeEJtbXJSQys0Cm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNy9SNjQvZ0FYTmZHUm5maFFXMTgKY0MvMUJySjBsMkdaVE4rZGtvREZMMkY4VU9aMTN0NzVFU0Q1ZjFHTGdTazIvcFppM0pBb0JWNGtKNWVBdzloRQpmK0wyZTFCVng5NFhxdVJ3ZkVoWElIMTJWVXZ3ZG5VKzFxWkZ0ZmpGK0MrZGZXK01CMVdFMnFsNVFhUUdtdDd5CmxwdVRlUGdYVVp1dndYU3VpUCthNS9XNENEY1dzT3pFbHdoQ1FHTG42QnJJNW5hSWVYbnZ1ZFUyWFp0bkFXaysKTjNSWk5GWk5hOWtqMlBBdk1XWmJjaGhVVmtKUWlSYjgyRktXWDc5cnRhQndkRUo4ZVVqelVJdmdYeUEyT1A5MApsc1l1cGZjUUNtalY3Tko4VDVma2t0Zk1mcHZOdlRHQnhxc2NsZVR1czVGbVo3SEt6cStGZ3dlM0kvWmgwZDlZCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzVBUXpHQU9LaERxWWphdTkrOTUKT2ZqN0ptZWVPS2Rabk9icVQ5dXpRVkgxcXhKV0xPb2J2WElmNVlqcjR0aWZVOG02aVViTWhMSVVlcUlJZ01JYQpGWmd6UXlFdkp3UUVNMXZ1U3N1YjE2L09wNFVxZnJlNjRQMHdoMCt1R1VBWFVmUlJ5SUpSVEtJeVVUWDJLbnAxCnYvV2piZXdxZDY3QUM3TDIvN1hVbmQ5QS8ycExnVys1N2RtSStGcEFkN2h0YXR6ZlQrcC9zUi9hQ1VYOW5lOXIKdU5mT0Y5bzFVNFV4VzVMTzBjSVVERlBnTng5ODJFTUQ0b1NScTB6MVRrQ1VSZFRub0ZrdGxsMHZLSENubTFvQwpqOFlQU3d4TjRBZ0ZpLytsdmlFWVUvQ2Z4ZjBlaFcvWUxuWnptZnUxK2Z1SEhITjZUYkNIc1dCY29ZbVFudS81CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGx3bXpRaGVxeitnSEFDQm8xMnMKeG04Q3lGR1Y1eG9oUU5kUnJDNXl4WFhyampvOTlIa1JDbmNPdS9GUTVKRE45T1JoMHlHKzVWMWZseWlSU0dRRwpPY2wwS1VRdlVXQXRlSGk1OURnbkdUc01DY1lQSitiUGpKSmpaNWsrRW9UWjk1eDR1UHRtek1DZDBQRnVVYjFECjl2TUJmYW9iOUlDaEpFWk1KSjBDY29Zd1VaWWpJVDJTaVRsN2IwTnpYWDFGbUJ2YVE3eENFbjZDRmlkOHQyRysKOVR2eVZGcUxvTXBzaENNdVprQ3RJaDU2alF2Ni92V0R3RTk5czRiTjdVQkFCTFFXNHNPNE00bUoxUERxUG9hQwppYWtwMzNBeVRIRThCRnh2Rmk0WGUweVk3TVU5bE9qU0Q2NmhabkNWNUdjRFNjdUdLbmZOSXh1Nk96WU1SaFF5Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGUxRXZoZUY2UVNXei8yTjR3aDYKTisrQXhZbXhyZEVyNkVDcCsrc0JCWndWcmpKcFFHbFNvRStSdWgvN0RDMUhMVkVwcUlCNnZJRHNqVi80Z2F0ZQoyWW11Nnd0RTNTZnZVQk9lbHpNazBPdkRSSzQxU0VNT0FTZjlpM3FRWXh2OVpyUjQ0M1BGNzlPVVRDUjBKZEh6CkFIb1BiQkgxamk3ZFI3VGpvMTE4K3R5VDZMWGZjcW82eFJZcDd0ZFFhNGFuWlM1dXFoWTl5ak9mY2V1Z1pwb0gKbWxGL1VOVVpCa1hQZ3U2T0ZIbFB1MzgwK0NPTVZvc0xoZExMRWp4cy94U25nT2lSYlpjSHNlbFNYQnppTVZoZApNcElpRmFmNzJkNkRjRFEzWFBZVmdRcE5GOFFrQnEwdERDVWtpdGlYY3NTdmplN2pYeW1kSDhaa09wWG5nK1RjCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFBOVWtVM2ptcXNXVVdubGNSeGIKalRsME1vRVQvLzBxMGJUUjV1VGVEMWk4TDhYVWtCeGlzdy91a2hFUWVaTXdhOXZRNGhMTXNpSUEzM1NGSm1FbQpBQkgvVVBUSWsySnlhb0tacENONzZuSFJxc0dJMmF5VUc5b1huZHBTV1FPZ2RPVjRvWi8raXJEckFQMGcrVDgyCjVjOWM4OGlHbHdicDJxNm1qUnB5YUttenVPRTJ2WVNtVUJ1UVcvNWtmZlptR1MzeHpPYzIyMUxhZXJua3Q0UXoKZm84U01nKzduaE5UTXZiQzRndU9XZ29EZjdYRkFXQzFTdFRpaVBiYTdRUFM5ZHZ5aW44S0tSejJhRHRIT1R5OAo4Y3pLd1A4eWM4cXRYM0VoaUxjeVM4bXRsNktiOStBQnFna2RsTzZ4OGJOZ0xmQTZUZjQ2MkxkSzlobytON0JtCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUhOTW1PUVZHWW8ydmZ2OXJ6ZFMKd0xWdzNJY2NEWjB4Q0pKRUdyUStpWmN3MFBXVDdBeCtVS0x0TUJUeTRsbnRiWVorUzhGbm93dWlrMWVXNWtzNAprRS9PSzUwNFFIK2E3UUJPK05HclhkV3NwTHNpaE9KZlN1NFVXOFVjWDVBbm9DTEVOYkVQRmFYaCtUaE1sQTBWCnhUTDJESml5ditXQzhOS3FxRjRxVUgzSzJ3WHIybGZkR2hrSmsvOXB2TXI3NXZ4WFUxL2p5M3I5SXJDMnQvUGEKcmc1OFE5d293TlFlaVZDZWFlM1J0UzNHMGlXTEUrbHNwdUdDeGJ0bU5EeHBjQWRUamZrSlRxTldjd3lwU1U1cgowbXhVUjkxc3ZOZXFrSElVRWFXOFFENGVVdXlIZW1WYWhCaGV0M3JXdG8rbnFSZnJXV2pQRGRUbXhrWE5mczBNCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUFydk82a2J6c09DVHpUQVJIeDQKRlI1cW8rVE5jZ2REcU5jQ25sV2I3U3N5QTV5RWhQS2lXL1BaYk43cURRdWRodzF6Y3c5RWtMdHNDMEV6dVR3SgoxNG9QZ2NxYTFrUlpNNzlrdHM4RTNHMXhKVnd0YzNVSHJlNWtvZmNNSVdtVUhaRHE5S05DNVNQWWt0RytoV1FUClBZSlFqZTZJeVJSMXNXdmg5V29qOTAzaVNPODVQelVzWjlLck82SEs1NzQrdnpvOGRrQVZGTWRNamc3ei93MTkKcWtXR0s3WW9MOTZVVUZMalhrai9WMmxCZDBiOXZOd1pMeUhtM3hubmJBN05URTcvSTcvRFg5WVZNekFHaE5USAovd3hmcnN4Z0hrb3NKWCsxeUlSZFNyQUthbG5ZVWFsWUxHZVVpOVUxdHVGeGpOZ1N1SkNRMWxCUUdRYTYySXZnCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0FPMVg1Y0hpYzdhZHd2Kys4UFQKdlJoSkl4ckVDYnZZSEMzSjJ2eXpxR0tOdGdnQWNzS2VaQnppWE56Q2NWSFJMQ0pPcEtvQ3R3VGtrT0ZJbjN2UApkdFB1ejFkSW9wNXVxVStMdG4yclpINkVXVldQM0owMmpVVWltL2pEWEVXcm5IdWNuMVExcEx4bG5QUjljYU5xClFGSm9Wd00rZHA5VllvSEh3by85a1FzRDYxdEJib3RsbVorUG1IdkFCZGxNZzBaUGVWOXgyVTVrNHR5M2ZuOGsKbmcxRHNYUHp0L1FOS0hEdTNmaDlDMVNSU0V2VDVEaXBKODdHTnoyaWs0M1BYb1NXZlpUaUJVYk9TcXUvYmEyUAp5R01QeGdxRkR4V0c1R2ZhRlhJT0ZvNzg3cjJ3MXdqOFVZOVMzRTdBZkVLL0cwTlp6VStjMlRJSUF1eHJ3QTdLClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemFUUExWS1k2SWtGME5lOFJqVWkKK1Y5TEI4OXdEYWpyaWlIZ29XNTJ0VWI3b20zNTY1MllxZnNZYmRRNm9YRUdxYzBUVHJ3RjAwc2VMRGRSQ2hFZgpNSTJyODNDVTJHUGQxcmZ2cjdRdnArZ0d1QVNQZEIrL3pPbnpWWWpwTTNwU0ZCaUl0TXdyMG9abXlZWFNjUk1lCnFEa2hFK0x2aDdDSEE3UXV4WlpUY1QwNkIvYzZLbXEzcFZMT2ZrOVh6SSs4RStSdXIwcHNkdmh2b3BGbkhYcmwKVkx3NWRPSGM1ZE1Bdkl5RG1aVS8wZDBlaUlzeG9LeElTR0gzdmlGdXpDZjlqR3N5cXdqSS8rR3VJbkc1M3RBSgpYNjdpUnRUQ2ZXTENqZ2U2YTZNOHR2eHNXcWQrTjFnTG8yQVlUZE1hZDQ3YWZhWUFvQTR2WmZYdGVWNmhveWdCCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEZKbTdqcEpJV0xUNVpLMllubkoKVEpkM3JTZ2hua2VXM0E3TnV0MzFKVHZpQmJPN0kraSswdU1VZ2lrWGI4elI4NkxXS0RqMEQwaEJDVEhJeXBDRApKN2hKVjBEUmRYLzRHY29DOWRlK0prSGNzcjJMQ1dpS1FXbVJsQXI2NkNYTm95K1JtbWFTc09lTit5dXM3YTVHCjh3Rkl6OHVLd0lqVHBoNlFRUzhVeExLVDIwUExxQmFWRmJaclNJalhteUFZSTVIUm5MTk44czd3YmhOQ2VXcG4KZnNubEhsaEhnTUQ5TDVPZytTNlh1eG9NMHllUGE2QUZjV3RScFJEeWpiK2hkYi9QVmFpVmNrR1lTODZacFdnTgo4azEvS1p4bGRNejNpQkJsSjlPRXMvV28ydWpzdzcvRlFtVmgwN3BhUHVSYlF6VDZMOExQL3BwR1BVbDV1Q1R3CmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNC9zaThEWmNjaU1icE1VbmlDMEQKL1NkUlBqWVJjMjZqbzNFN1BkRXZYcTA0Vnhzb3VoNStBRE83NVRMQklKNmlCYkJmNkJUcFBXdmVlUkRUT2U3SgovczRjY0VjY01lc2wva1VjRHg1MGZIRTVraHhvWW1CNWVDWk9ZRktHMEtlM1p4RkpCRzFIOGgxUkNJTWcyVUxzCmpIVFRGSmNYVUNNS3V0YjlrTW1XRmlXcTVHSnozbkMreXJ2VjIrd21vZDBKOGFqaTIydnloWVlGekUyVVJ3cnAKb2t4cy9iblF0cGZtMWZWSU9URTd0VDl5VDlvT0FxZ3lQcWdRWW4zakRGVjM0UWRnZXZ6Z1U1U3pwc2N2VkcvSQphcXh5K3dqVzNrNzNRZUtNOHd0NkRZbnlQS1EzU3FDR3ZqeDZxY29SL1o2d1VHMHk0RWJ2TXNZUm9KaGswVE5oCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjZ0MXlrOEVCL3RPcFNpcnZIUWUKMDAyU1pEMmZHcGpVbHJFL1dEWUZycGNsUHpiVGJkZTRyZ2QxVUlUVFdJYnB1cDhNRVUxMUJlNzFMdXZJME5lcwp6RjRJclN0RFRnNUtMZWkzeXRhN3VHUTNPRVNaaitHaVQ5VXEyZXZPRnRoUHE0RFA3ajdIeWRaNmd3aU45MXhWCjBLdXFBaE82M09tc2Z4OWh6MXN3VGZ6ZzRFeGpVcFJYY1pQTXlvVWJaT2JMSm1uWk0rWGVoNzJVelhXdUpnWDkKekdkUUs0V0p0Z25XL2RITVVzandHODdCZTNPdXJrOENYNHNCVFgzNnBmNGRpSXlQQWJnSGtQQXh0c0doRkZDMApQSVJnVEQrc0dwVHZZTTVPYUtNVG1jNFdBYWcwSlU5bnkzaDVWS1BZMUY5ZmxQQ1Q3Z0R0dGdpWXN6T2o2MVlICjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXYvbHJaOGpocFVZdTV4YWlxWnEKNEFUV2lQNEJUdmZDUHhuSlNmcWJyZEpKanFMTmlqcUtvZmxKMXY4UXJXMGJSaFhXejUrY3A0VmNyRWlTeHVRbgpraDY5L3pxQ2xQQzNyaEcySDkzOG52bTQ2N1Z5UzBFdDV4MjVNMUt1SjRuNm9ZOFRSTEZUMXpCMzJPeFF1U0hKCnVVQURXdFlrLzNMcUZUdDRWTlN1WjNNbXdpdURxVy9ScU0ybGg1RUpwbjBvWlJobWFZS2ZJNkw0VHNTNFRGKy8KeWtFOWFlQ3A0WUZidVMwTHVnNkNhTHVUdy9iR2J1dlZLeWVKMmVKY0ZMWVlBRS95UkRmSzJtVnp1d0pCWWJsago4aXVqYnVwdm5VR2UrMFJ4ZTB0N25GOVVVdWhnS2Rab2VrdGNINUx6T1FUSWdsaHIxclN2L3Nqb0ZVTUI4U2pLCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNE9mVXdEL0NQK2VHandQemh6S28KNWxGZVkyWkwwVWV1cll2Rk80by82Y1FETzZYYktFVHNKNU44Zjg3SGJQaXJ4dmUyTW1CV0treU1lckxKdlpOVwp3bGFESitYcjJCaXhrcG93dDRFNzE3MVRQVVVSNndsSmlqcEZ5K1h2NzJhT2RwMDRtZkk0dGlTRU5GeU15N3J5ClVEM04wbEhiMkJBY3RLMmJEMlF0alk2dnl4YjNyTHFKLzN6SXFtOUpvQWtmcm4xZVpKRTF2eTgxUmE1Vm5LT1EKbUhnSXZGTlBkTldTaXhiL0MxVjU3QWNIamxmK0tQVmpvYUtZcGdjWFhrVkJxclN5STV4NTZHZGQvcFhqTlRnVQp3YURXdkZSQ25aR3NDT3c3MzhEWlo1S0QxaUtBVFRSaHk1VjNaMmQ3V0c2N3JvUjRUeDlmaFRFMGJRckZsandnCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUdvbk9jOTBzckEzaDlXNjVZUmQKOE5hZWlnVlJPaU5VVnB0RHoyUS9lbFE3aDNMTC83TytsNjl1MUQ3dHkzNUlUTEZ4NUthaWR4N1ladjdVM2cvYwo4V2o4VkNtMUFHQWpORkxOaXFnZEFIS2FoVjY5MkNxaVJ3MnhrVlJZaXU1SXhNQ0RVV3g2L0QyMXhiMVdrUHpZCmd0Z0hFRm5tOEZtK25XcEU0UHluQitwNXFnbGlWREV5NmJ2c1hEemxybjdoMUJHbUNJWkdsd244MjJseUR1d2wKL3dDbnpRV2FQWTZmamhSbHFNMEhQcGNrdzFSeVRDUFNRdHhCaXd0TEJJL3ZSNkZvRkwyMjl0KzNyNWFscm5ONQpaTVlLU1BoTUlQNmFQZWFJajlZWTk2dXlQMDYrZWp2QlExNmpUeEY2V2ZhN2QwcjlBUWRmOVVhV1JGNHZ1czhRCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUFPa1ZObnZJdTZ1ZVFyeGZoMnUKYTVweWxXb3pIM3pPdWVpYVpGbzB4Rm9NZVJNR09aaGxiUFRYdktXazFWTDM2bG1xSUppVzdMS2oyRmo2dFNiaworMWF3Q21nMXg2cnFUWFhRQ1RFL2xzanFjSWRJdHB6RzlCMUVrSFVadDVOOHQzM3FCU1Vucnp1WWdiMnFwUXAxCkh1bmJ2TUhSM3ZmTGc4QVQyVHJMU3NlejdGb3hhNW93ZFUwQ2xIWG5ySVZIZWtPQmVrS1JlU1hsOUpWNWp0eFUKWDFJYzVhQWgwNk1iNHpaY3N0Tkd3emgvZ3ExUmYyMnlpekNBZ0RsdjhpZWM0djRQdG4zQU5hOEZCRjZNMlpuRAo1L1NramlwZE9EREhNM3Z3MXIxMFRzU29CNnU2SEV0aWhzZnJtS3hUZHJuU2RaNStsYi8zUDNVaVFyd3BMcjZSCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczFta2tlSVo0WVlGcjRnR3BhYTcKblYvdTkyMU5NWExlUVA0elBZN3BDWUl4NjFwZFRvVjRLUzY4YVEwUEtkaTN3RmxSV1FvRXg0N05SMXBROSswNAp2azRoVUwyV3BUWi9JM01VWWpFN1IxL005cTFBcnhWK0NoTUFJRktXaDF6cWNpeThhaUFFaUtrdUtLMERjYUx2ClF2akZERFlHZStJcGl0ekNoekZ5NlJSYkkwa0h6ZnFFVEIzZE4vRGh5OW15R2Flem9JZGorTzE4YWkzMGc1RjIKS25UNW1KVU9xdERLWnZ3a3Z5NGhMdWhDV3NWQ0wyamlRTFVxS25jTUk1RER2UlNkSmF2Uk9lQmRqQXVvWHE5TgphcTNLUHdMaFRXZHBkdTVoelpjbW9hZ1huSnZsOVRjQjd6WGJGejNBUFRqVTlPVGhMUFBKZ29PcDRQQkNJU3V3Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdldqR0Q4MnN3ZWczMkxDb0JZNVIKRzNHZjlVWThQNm1iL1hLcW1hd25uZ0F6bXp1bW9ORDVxWkk1bmtaNzVBYkR3cms0R1o4UHl5M2FoZmg3eTZ1eAo5bkpVdG1HTURYMW02OHFCaGc3S3prVlhqQTBXRU9BQyt3L2ltNlI1OW1SY0JqN294VG4yb2FmMWtXM3JqSnY1Cks5UjZxZjhQdUVzdUhoMTBrYjBYczljZVg2SGtMeE9Nd2NyLy8zbXhXaXdzOURQWFBrQ3dsNXlSWTFpU2lBRDUKZTE1OXF6d1REL2FyNStCejlNODFhYWYwL293bkpDOTNHQW14TGVlc0JvVzRaKzdVQ1I2OGNsbWVhTDBha0FkVQpCNXdDelNMWnBvN0dNckk0WWdITUtzTHFoWDVITjZWVXhXem9zVDU2cUxPWjVBeUpNUGY1Y2xMTHpxVmgxMkVhCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUlVaElETE9vL0djUW1OeDhlZVoKTU1WVHdQaGFWanlBVDk4UzJ0enVBSDMwUyt0bi9CTW1GRC9rUzZEM2Zidk9QOHYwRWU1Z09FWFNWZGFGK21GKwo3MUZORk9ibk5zdm0vNXNyRDczWUdjMS9IbTI4d1NaUlJVczNDYzVyUzNpVzlFZmVEaXFzOVR1ZDIyeDVXSTJnCmZmbXFCcS93MWZscDlhcUZSVWZ3bW9rcTl3UVdUUk5Vakd3K1ZXRGpUdzY4Q0pGSWtRdUdUSm9YOFlJc3JUcmUKYXpqcytMNGUvOHJKQXhUb1NxcTdFY2RMbDJuYUhuUGdpLzhyNkwrOUFtbVowMmlCeWt2Rzg5MzhwTnlCV3ROQgpjSmJ6YVEzcHp1VEU0WHBUaStxZVEyeXZEbm40aDA4OGtvcWdOclZmSmxQdXdsUzRSb0ZoZWdac3hDUVE5SUpBCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeElheVNNaE1GTnRNK3NPdHVsazMKeU5ZNTdrSkRnRkwxaTJNUTJtU2FURnhuTm5mZCs0VVdEQkhFdmNPMWNnUHNqSysybzBWOGlKUG14UHRHYktTbQpHbDVQV2xFdC9wSlRYWk4vNndKTU44Rk1nRklPWWw0Uy9JT3UwVThmQXljWHZxVG9Udnc4Zmk3RzVLVDV5Rkw3CmxBa21za0RLMWQ2enNoMzV1ZFN3eVo4bzhoWW5pTlg5eGM3QTRlNGhRclFxUitBai9LelBSNEZmSFFqNWgvSEwKSlVyTkIwZGRvSGhncC9hVzRHdDAvMU1IZ0xaSm8vcE1md2U5L3EyOVJNcnZtVkZ2a0RLWlZyVy8xUWxMWFpJSwp5OEN1VHVXYndkRXdJZGpTMHM0R3EvdmF4VE51QUdKT3lNdmlRMkpubE16RGR1MUpndUdvNXZXS3FZN3JGdzZJCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkt2Tk1DUTllZmtOYXlwNnc3WVAKN1k4alFjT05iRU9aY0JsT1hBdzYvOUpOdklqYjBGcGR6UzBraFUxV05TMzNIcWZvYnhoZXE3WnVNeW81aUdjeApPU2p0Z1UxZ056TW5IdE5hcUY5L2FSSklWOVNrSy8xSnM3TTJ6WnQxdVFwVFR0V2dldE5RbDNIZ3QzSm9ZMjhnCkVJU1o1S2lBNzd4TmFQQXc0STB4RFpoZVlLYUFueTRQN2RtNDdyVFd2MDJxRGk5VkFxTTZIM2tlUzgvQU1OSDgKa0t6QzArZjVuV3hYekt5NklScWgwRVk5dDRVc3NBcTMxcXpjMUxFemxsZXJGRHZXOXhKcENOTEF6RlV0UWVwbApyZ2ExR1RhMEJJQ1FSWmwwL1o0UVN2ZFVwZGlKWlIzNjR4aDA1elVYR1E4QlN2eU52c0NrWlRFZTYwdTNOSVZWCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1BJV1p2SEpCamhzMWRpYm85NUEKTHd5VUJoNTdhL1BvY0JzbHpMd2xDWTloV0o0dEpYcmQwSHRuL0N4MTVrblZwVGZnVVNGVHZaUGcwNGFaL0N5UApPMDZneHRyUnhiTHJwcGw4QXZuSWRwbDQ3T21ybjcwd21yRWE5M3dVaVBxWG5yaEpBdm1uUEdaTVhLQW9aTFR0CjhHdEdXYzdhUHBxekRkQlV0d3kxd0ZXQy9CaFBjYzhsQm9HR01iU2JrWENDb0k1cDFpZVJsSkkwRUltV1VpM0kKeS8wZFFJVEdoZVd5bnYyOTNYQjdva09MYnBLOGU0NGNINDRuN2RVeXRFcEhNOGF2bENZYWhwU0JQV21IUFk0cwpibzJvT2o0VGxWWFJ3MlJhbUtNajJ4ZXBqSCtreVdvOGkvSUtIclFNVkZhZ1cyRzJ0TGVUYVVGUFk2RjRpNWoxCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmJUUjFZOHVGNjNFR3hsOFJmbUQKaWVoVTJ0c01MWngyYVp3cnQyVjZDa1NBMmF3czZuZkE2OGRkem1MeWQrTDRUZko2Q200R3I2ZXJHdVJGTS9sSwpiR0JJTVZrVEJXUlhnZVU3ZGdGVzQybDdzOHdkMkRTb1hDbUZBbjJaZ05TaWw0SFRiNk9TZVc1eDdPME55MGJUClVrZURab2dKYnJkcTBYcWlubTVjYTZ4ZXRubkg2UUN2YVBMM2hGQ1lPdUExNkUxT29rai92WTZHR0NzclE2UkEKbWkrVGNTRTVPTUlxL1VGd211V1BoYVowcm5JZVdMNDdtV3NvTk5IU0twTEg3OXhmRWplWnZEVnVab21LZ20yQQovL2p0NVUwZitWUm9RMWtiUXc5am5nTFZpeENHTWpoekkwRjlJYWwxa2lucGkzelNqZ2l4bXRma0lKb2FjTEdMCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDQ5S3ZsOG9ydHFwNHpBbDRraEcKVWVSRjJ2dERqYnhmSU9tWWtseG5BSDhFZUtaUnRacUc4UDJRUzh3djdnRzNkVSs4RzEvdnpyUCtidTh3N1pOVwpiMmxybzJiVlRvbW9Vcld3WHlZOE1ER0QwL0dPK0hUZEtIL0s2V3pkUDVvWmhxbzlubkhHY2lmOGVNRDlMdW9KCjdGenhqZnFFVXNTN2JsMW1wWkVsTGNkNzJ4ZHJ5dzBrZHpXT05WUzUydHhQVE90L29wekVIV085UUdLMlpxK1oKcndoV1hFbW9iYW9WaEZaR1IxRnFZcmYwaDJwRzdsemVsS2lxVlo2dHJrUDNpdUQ2TXdORzVoemVKRzBTdE1hNApWam15QUk2ZC9nNDlRemFoQlhUQ01raTY5aEpEa291QS9NS2ZmTzF0MU9YM0lVcE54L2pTemNmUmk0MTlQeXpiCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnBBV1kwMGl0UXFUTWhGOU1qdjEKV1lXM2xIL0kzWDdNNUtkWkFUZ2RJTmtkM1BFVVRRUHVKei9KMHFJcTBIQmRDTmNpNzdxU0hVUk12aGVqblVXbApaWnJDdC9RNjE4RW96TG5Zd3E2SHh0OWR0R045OUlxaXg4alhoSDNwL3F0Qk5ZTzhBd0JLRmxNU1FBbEN2UlBNCnU2Mk0ra2xwOFl0aVpPMXpOWmtiWjBkWDV0eWdCYTVSV0tsUVRsMDBDa0R5VmdhdW9hRk9WQ2xPYkdTQkU0NCsKeVU0aEdhaFVVTXVMVCs2eWt5dkJOek05di9RME5VZ3duQ1FJNHRXZ2lPeXpkZjZPQlVMcmtCMmVvQTZJaHR6bApTQm9pRU1LY2FlSWdSMXdJVWpkRE9VWVRVMVpSYVVuMGRydGZvY2Ntb1pSVGxVWk5kZ1R3OWpvNGlINzU0R2R4Ckd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNis3dDZPU1pYNkV3REN6WXBCRy8KQktna0hCK1oxbzYrMC93cnh5U1hSOFZQbExkVC9BSlllZ3JkWlRYMDYyQ2UwVVl2TVVpUnBzT1JWOUxZSldLcApsRk53aWdMbDAvSWlyajhBUm1mc1BCaE5jbC9jOWc1M0tuaGduamJNcnU4MzN0eWZNN2RNNWpoMmdEdkRqOWM2ClJEV1Yvbm1RK1Z1KzFOb0Z0YTVVd0tXRjRMbEFSNjlVb1dpQUVRTjUwMVlQMmo2R3JseG5KQVdLWk5oektpQ04KQ2dZeU1nZ2IvMkdLa2xKSFZ5MW5GTm1nOVBqbDJjQ085Nm1aMENlNDhlUU9XaWRENnpOUTFMRDB4ME5QSUJpUAp6UFNRblhTVVVWeVlYR0xuRlJZUWdmL3A4eUVLWWdHTGpTdy9rRXJuTXc3UW40RjlsRWF3MjFuMmRJZGJkbmJECkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdm80ckNaRFZNT2VPOFJsdjJUTG4KUjdBakNmbzh1NEh4UHJVYk94dDAwUit0R242N0lVNHFJalVxTkl1aVdSc1hyRTJyakJxWGZVRjA2SXFsMElsWQpSNE1NbFlzQ2JlU1REVzFzRmFOV3hRd2lVWHZDZ1NmN2g1Y2NMYUVxS2FSUkdvbDM5Um1tK0tKdGpFdTg5cGtDCm9WYXkrbk9oalZxcjFuelQwVXl5bThZMnJ6VFNSU2VoOUZJT1FISFBteVMyd0ZwSnlTU2FORU5MVGVpVHJQNk0KeUlBdXY4eU53UHV2U29HZjFKdnZxNzM1cWtaOVBCU3BJZjVQNDNibG94VkdVM3JVM1dkTTFjMVpFSVBxeHJtTgpDRE5tUDNYYkFrcTN1REdvS2ZjUTYzeXRIUVRjMndaWjI5dUx5YkxWZUdkcjFUNlNSL2VwS0JaSHNCSUtRTnhMCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1IrRVZpcHNBL0ZSdEdIMGUwVXMKRGdGNnBzLzhEcSs4ZDZmY3ozQTBoUnhNc1JWVERobitiR0ZLaGJmYmQyMmxKOFhYQUVoOWN6U0YvVWl0WGlVTQo2VHE0bjl1NEo4Y3ErTVBKV0JTaU53NFpkWHY1dVV0QXVEWEJhd2g3Q2Z1NlVnY0FNQlYwMVYraU9wWHFxMXhIClJkMHVta0xZUDlaMVYwb2RiMnI2MnltNVNGVFlWc3VOaEhyNG0wUU9CSUhMWnBJT0lkaGI4NTJ1ODBLN2tLVFMKMXNDNTVHVVZmMWV3Zm9SZ1JBbkc4WHNtWmVjeTRxVDJEVVRYaHY1cHo4dXlTVGhvNWVwSzY0NCtzTTJsNFRiMgpkWDl5RVFNUnRoY0hLa0RObks2VEF2MlQ3Y2pJY1UxOTdzM2lIdkhZQ0swM0JCTERhRVB3ZVFybVJxUnlZbUpsCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHJOeklVU2V3REFmNFZ0cytKS1IKeEN0NHFDTzVJMmlPSzZGZHBzaWMwWWl0Y0ZHOVc1U1JLT3Nlc0w4dlhFS1JyazhoN3NTQzF1OVA0VTBJMERWOApncXpEUWJpNHZoUmxRbFllajNNYmpwTWVTNzc2Titad2VEaG1pbGd4N0lOWkQvVkVjY2dhSUJKMUtUMHdLTGpICmgvWHRXOGE1ajJyN0hqMFhMZjQyWng3YU5PMVZGSXNzVm8yVXpSVTZiK0FlMXAwdkY0QXM1c1NhVU90eWxBSS8KQTRBTGJCQ1BZSUxIbklhMzl6blJGcmVMVlh4eFQyMzRvU3dJWm0vSldxdnhJSGZPZTVVbXAwcEY1dkpOSjBkWApaUDlvMkxZc0RPY09vbHMwVWhRbDB5czZDU1VRd1ZSM1cwVERlUWE4THZuVFErOWg1Q0hFd04zQjVTRk1BMGVGClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkRPZ1dWY2hOaHhxa1ZwVUdJZGUKeUJFUmxtODFMQldMdmNuZndSRjRCMU5qTGR4cy9oWEFLQ3R1cUpibnRZRHpXeVdUUUVYbEdOaEpLaWxLNDhtYgpDUXZUVkJWV3pTUEw0UkxoWGN5d0hOSWFPOFMxclpCa2RkbEY5RUJLMjhnM1NlOU0zbTB0WFBIb1JFdHZFeHdTCjJRdjduc1pFKzUzdE1TOE5QanFmbjBMSUh1cWg0M1hFTVVVaEFlQ1RYMGl5TFNJL1VWWG15OGlya1ZFSEdQanIKdW5TOG1yUENjOHpXckhFVE9jUGFEV2M3eGVLcm1PRjNsUVpqODgwRlIwYkxTajdyUy9zV3NUc1g4TVpMMjdIMApRbUtheDZlMko1RkhkYXc3WWNOVzhlY0F4Y1JoKy9LdytYUStpcE9RMDFnbUNLL1J5MWlXMk92RVhUY1FPcXB4Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTRIU1hZbS9KbjVUOWl6NXptTXUKeXhkaExmZFpnSEk1Z1FDamVOUkd0dVhQSEFLN3N2UTRiWHpqQUZaUXF6aWRUVUNzVDU4b2xDL1FCOTJTNzY1MgpNMk0xMHdOMW9yNWVSMXJKbFozR0piQkhDci9yb0Q5RXY4eHc1Qm1VamxHZy92cnpaeEpZOUpMK1FqZ0lOUFExCnFDQjZidEJuVmJ0SzIydVFDVTVBMEdVMEtKdWpqak5BSVFSZC9ISUZwYmlsL2ZFL1Q0TmYzbmZIejRjbmsvM0MKcnV6ajRiZFRMbmx5eE1UNk91OFdTa3NEb1Fva0hnbmcyZzljTHpDY1ZLa0pNRkNhbnRFUnpIcFhpeXNRRjRhZwpwZFdFUkUzczhraXN6a3N6Z2Q2S3Z5SE9BM2NJRm56T3FLbFMyWi9QNGpTUEcwbGRRNVZWTFBoMXRyRXRuRElXClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0xmZyt1TlhLYmwyUTl0MnBCMTIKbXVCd3NORGpKTDQ5UUU2Mm9VL0JrK0o4MnU0UllGZVp6VVdHbHNyNVg3NDg1SnFzNW5kY1hnUERlaDdhaVRNcgpUUmxmenZGZGFieUc1UUNWUXBmT0xXVnlHeDlOL3JPTEVqZUtiVGtzTWFSNmUra2JYZXJqZUpuNVBhd2Rqb1ZaClhEeVA3WlZWM1NJSkNPVFFvYS90VEI4T3o2KytLZnZmdVZhcitsd1g5cDlzSnNsbmRoNER3eHlMYmVjVGE4OUwKajRUYzRnYjhYeXdWa1lWREFnQ3FsOHcyY2lIWEFoSkdpNlVXVThmWURSbEZOcnlKYWlLZzdIYkxLTkt4cW1uQQpkV0l2QkxNcW5XWDVRK3BqNFZuWk5PTVg5eUk3V1hBTHZCVDdMaGdUaTZQVXloRlQ5WTI3SzN2dktucWJGeVNQCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjdiZ0FaMkN3bTF0NXZxdFBSblYKU0Rlbkpmakp3MXRhUXFTQ0txSDRyRjBmMGhiajhJWW5oYVFVS2g0NDdwdkJBQUQ3UVdYLzNyYzRWdElWRXhtcApWYTVtNmRGNWt0Y01Ld1lDOHBFNyt1Q0dRSGU1N0pkZ2plWDJMUGdrVkE0MkdmRHM3Y2FPVE5DdDRLOUFtMVFnCkthb3pZVnBxeGdoV3RHTjFDekN5ano4NEhyTnFYT0RGZmZCMzE1TnIvSHJqektyQjJ5RE81UU5pWnBsUS9nVkIKRmZuNkJhRGpYbW9TaVVuWjlCUmQ0WjVpbWlSMEEyc0psWmRpQURCa2pFQ3VCczZvTjZZemlTcUNvY1ZIQUZyRApIdFpTTHI2c1g4dVBZL3FkSEV3WHpiNTJjR09QcFQxTmtTWjU3UXJqVFlLUHVxRjhhZlh1UXVGV2d1UGxIWDduCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2dROVB4YnV5QVhpa3YyUWJ6V0IKTStzbFh2cUI5UG9UR1pSSlBuRHFZdkNSODRKTkpFeENJbUJrdXZjRXFMNXNSV09BMUk0STJ6T21RMmhFYWlrWAo2anlETC81d1dOMWNvam5GbFBIWHpwY3ExejlPMW9UdmFhbHRBTlB3YWZzNWs2Rm83RExValZMZTNsQVRCeEgxCmRiOVhpY21RelJXcVRpNkJ6TERldE9OaXUvVEJFalRBZjlPMWNCOXBnNlFrMUQvTm1oNHVFdXVmZ0RsT1NucjgKT3IrTzl5WmpCUHhvMTFNN0J1ZndwSjl2eDlyV05CS1ZsdkVVemNaNzhscUo4T3QzeHRBdUVjdzZDY013d2NDRwpvSkRVRFRGZHpDTlJwVDg4ZmVHQmtLV1hZODBaaS9GS0YyaEsvWDhRcDVBazNrYUVLUUc5TVhCOFVpUllBV2gxCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2JPTjBDL1Y5YzZOUytJWEFhY1UKMWxEMFhyRjFOUWtPRmczR1l4dEg4TXFZQ0Q3cHBqdTZOVUJKWEw4RFNXMnhHcVEyVTFWM1ZURmxLVzZwekprNgp2aDdmZGYreTIrQjErWnRiTC9qbVdZWk1keUxGZGZVVjlLcEVHSjJnR1FDWDhIc096V2NLR2xOSW1QS2tEUEp4Ckx6UXYwdHBUZkgxbFJ6OUtGUFhKcjVEVTRYV1RLZ01jUHJWRTRUbzZiOFhrUXI1anByc1ZLWjJUd3Y1cWppSkkKZlJQU2RxQ1ZTSW9pNWFjb0N3bnZ4NlhVMzRPbU9KYVZFUHZhS1QyLzFFbkxzS25JbXBNcTJZbXNLNVp5Tm5pcQo3enE3cGl0SUYxRmVnNFAyZU9BNFRwOGdCTXlYZTZ6dmFiZ00xQlRZNVg0UVhGWmlpTm91bTlCa1JFNS9neExYCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3pXR0pnZVRRaEM3Z2xQTXVUZTcKR0FRMU1XWktSdjVmY2RDcURxaEN2eEd6VXMwb0cyQnpkQ3RuRTBiVERNNmM3alpNMC9rU25iK2cvUlcrcGVLQwpndDZWQXRWOTdLSkVzSk1ta3R1emhKRCtWRjJBWUZoUmFHTTN0RFlEbXQwdkRLb095QWQyUGtOTVdPZ2NURGJ5CnV5UnBFQmZpd1N3MWx2Ly9WTld2eGI0cGNzWmJxMVEyL3hSOFRXbkxyNDhjWmF1dXd3TWN5NTkwSVpYaXNoL1cKVks0d2FiS25EUG5FUXROQnNxYjJsb3ZtblkxS1BTWElJU0JTcFZSN2EySHlJUFA4QndXN1JFNWVyUXY0WEhYRApKTVpTT05XR3hlck1MVEN4QlYvaDlhRWRvT2pYL0wrOU9jenR3U0hKQUZubGpFSDJLUVpwSkpuY3JGa0w4TWg1CmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDEyYjlHc2pRdXJsNmEzS2laV3kKMlRFNmZiUEN5Tno3Wi9JbXN1SGU5YThTRHBVcHlLQlpnVlhQaWFLMEhoWCtwQ0xyZ1F6eEVic2tyODNUbmVmKwpPVlhiTm5VbFFrbW1uN2x4TGJCU1Jra1ovRHhWUUp3VjRUWGJ0WXdVNXlQM3RyenMvd0dEV29WSEhCNzFwQlhqCnp1a1kwdHE0UjB6amZUU0ZXUFVkWTgrelBPTktCUTU4S09JVzhVK2ZLdHNpWFVuRjFNcElyTWJiMUhWaWlHa0cKWStqNXhqTXNLclJaZEY0bjhmaEk5QU8wK1ZGVmZVRk5NZ3BRZXN5M3ZWNUZ5YnU5bEFTc01xL0tUZ21aa1FLdApvNkl3YVV0R1BJbUhDcEFPOEJGMXVMOXZkTk8xTVBET0EyMVdrUDVQSEV6OVBkQTVtL3pJc2Q1TkFJZngzTGVSClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGUwRCs0aFV5UlpsdkJIekZTZ1QKRUNnVXRlcUNYRlB6WnVOME9mYzNpcEg0WitsYVB5M0ttckp0ZzZqZ2JlTnJZR1lqRmoxbjlidzlCQk40OVZqSApnZWlsNGVCamdQNWJwYVhubytKN1ZtVm9oNVFpODJQSGdpdHhZRkJEY0VzREd6QnEvby9wQ1RWN0IwVnFZY3hQCjBHYVNRY2REM1dBVW5YeG1Gbmc3WjJUeDVsQjhqSzJENmxzY05tY2hYalZCcmhDU2UreEZoT0VPYW5QaE4zekUKUDltWEtEN3Q4alFTWGYrVXpGSHRYVzRMVlpvQ29sVFIraGdUWDFEQzVTN0IvRU1GYmRZUnpFMU50cDBBTDVnegpnenZEY3JVZmJaeHlNUUVIZThCSGUrWi9hN093QXVhRWpmUFhNUHYxVUZBakZNMTZweUd3QnIxYWRvSFRpZWJTCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBei9YODM5MzYxdjhQcXFoWnBqd1cKOGsreHBVZEk2a0hPMXN5NFAxWHk3VlNpM3o1dzNkWVVzVkw5ekt3eVZyeHZCOUIybzdaaEhXbE9CYTY1Qy9oSgpHc1BFeCs5d2RxTmQ2T0Z5R3BaaFl1UEhmTzFqNEd3b3ZudHhGTGxXQWtwNzRLSTlQYjNKZjZyb0F0OVhTalhOCmFmZUNNTGZrcElrQnUwK3NPTm9Uc2FaZTJ2YUNuSmhiUW1Pa2R0cy8rbktHZ1pOaEVFSHVrZko0L3NVOVJ0WXoKRE9pRFNqc0V2djhCR1YvNFpwVmpTUGlZSWNGVU5lblRuOGVHc0IxZ0pHK2tZckY5UFBRQ3FrWDIySWROTFp3WgprTkZJVGlRSjhDVHZWTS9zbGZQaW55MWFrRVU1NjlLcHhBb3pjd21mbVA2RW9QdERtNnlIWmZnbElORk1PNTIvCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcm5pK2k3MlY1d1hrb0pzUUVySTgKRGRURU10WGxydWRrbDFZQVRueExsNXhZbXBCNmdPZUpaWllNVXB1RzArS2sxTGxoMS9WYXFudTJiTFd6d1VjLwpzUnpQc3ExeVNEZG9RR3R1M0pLYWdBM1hjZ2Z4SzVpK2ZzSm1QemVUYTFFSHlwMGVVOExGWU1HWTZJRmJyVXh6CkZaMlk4d3ZtZDV1bWVrWEpQUE5maHBFM25OVk1LSlIxdG03MHNpNUx1OGorZlNsa0VITG5HRjVEZkZPN0VOdFQKOStIYmdMZHlnZ3FiS2t5TFk1RzJGL1lhSVNzN3FFR3R5U293aUpCeS9lbUlYK3hXR0dVSldXdmpTVVNKSzRIQQpjcDRQT1NyT05Fam5kN3BKdjJiSkVoSHFSdGM3L1RINGZVTzlXMHRRVnZYbldmRGVPV1JnVWJOSnFTUVUrMVRNCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1lUWmV1YkM1NWtOSG1keFRrdjQKa0JickV2aXRiQlcxRGM4WXVpK1IxSnRQSy94VlV6ckRBZ2lFd0tEaXZqcjNWb200YlBKa3laVmpETGNnM2RmKwo4RVViR1l0MnpnWE1JSXJQZmFMOWpEZW0yQXdYVzJoT1loaTNaOWJRSHUzTFZHWjVZUTEyNW15Uk5QcGpFRW10CmFybkVyOG9CQ2xneGo2TzVwckpLNVhTVm9OUHpzaGJYZTE0aDlBZ1Uvb3BKbkJ4MHZEa00xa3NBM1h1VlFicEMKMDMrUktIU1VaUC8yYUNYVEp3L29tc2E2N2xyWmlwSmdwZm9hN0pQNFVubjdHcXpHRDlKckhYVDFCMk9WNXhSKwpERDhYRS9zZGVnNFlMclhMejdtQmVNa0hTSVE2WUxWMmRCempQc3djb1RxejRlb1NQQTVwODFXMGdabGRLM252Cjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmZSTll5clgvS2NmZTMwQzBmRVkKL1VJZ1krQndWM0d5YUtBQndyOEJuOGRQR1gwODZZME9ibW9qcDFZNHQvTTNYUXJJaHM4UWhPZVV3QmFvUStZdApudzNuTDhoVFl1MmxETTVWWTRkNEt2M2xjblBPOGdxMnBYQjJWTWVkdkxMQzlBVjhRUHZ1alhuYm1pRGRETFlHCkRtTjJleGl2eCtCQzFqRStlSDBnVTlKQnVmYzlBUkVvVEhWdjhLd25IMFZqVXhVV0dqUStlTHFZRzdtS2JwWTMKcUc4dzYrTkVCQS8weEovRkNGUjIyOUdNZEc5OXUyWDBPZUx4UUpXaFpEMWtqOCtqbWNsZUxHTWhidkxNcU9KMgpCTEUxeXBob3orZjh6V2hmNzgvWDAwVmFsY3EyczV6a0ZIazd0RDhjUWNDa04vUmZJaXlTVVpuckxLQ2dabzFiCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeSs3SGRaRVJ3aW1IeTVaSWdUa2gKNTNyV0lySjlhYkZ3eTJHYWRBUXVleWNPR2U5bEltTVdDWDdPbUkvVS9jb3pBMGFRM3NIUmpsS0xJeFZhTjd6UAphTFF1Zm9LN3kyV0x2QkpqQnhrbTJOQzUzbTJ5S2NZcGYwLzRsay9IbFlYdml3bmtCUGtJWVFaQ3Q2SWpxbzJMCitHZkR5T3VpZmRjRkNrMGNDL0VIdm03QmNOejJIMCtoeUtnVGJiUTFRQ3dYRGZnOVp3MVZMZHowNmJ2dmhDT2sKYncyVjdwRHQ4QVVya1JKcm5KbW1OYXpnSE1WQVFiREZVRHdBZ2FKZTcvamR4WlBrd2dWR0VFMXN3VVZFVE9SYwpzTU9XY084WXlTUHRQeFNRWHJmMU1FZDMvNFRUanBYQlJhenVqK2YzWFFNcjBTU1g0VTJXaGwxSUdOenpwK0t0Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenoxVHBRRmdqS0tHM2pzVncwVWEKOVRuSjlzRXRzNnVkZEZSaVJ5RmM3Z3J5TldSSUdEZVhpU1BnYnlRalZJVzZ3VFdOaDEvckVja0p0K2paenN3eQpnTjY0VmVoMS9iUzl0eG1kb2Q3N2M2cHoreTVJRUo3NmRqdEM1S2VXaHNjSDNqTFRhcU92dlVJYTVmZCtSVnJLCndyRHhQTlZ6b25MWmVCdGVULzl6VVlSRGZ3bHJ2VUZrSU84WGk1OEp0a1MzRDFnSTV5MkRnZVRibHFhODIvanQKSURxOTVkSlRETDRJck5WZXI1eUlGVWZVSjRTcDFNQTRKOEs3OFZLT092dUZzNVN6clZQTC9RVTNuYjR6Tm54NQpwaUhCK2twRitUVDRYOUwrU2ROSCt6Ly9aTGk1TzhqeWdVMHgyK2NyN0xWUGVoSVlNZVhtdERoZDE5NTZyRFBGCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDFKS2d5RmJaVjRObEpMK0daamcKTWtlQnVMemJMNDd4ZXpaV0ovQnFjZHFDL0NtZktWbFNkVWlpanJ3TkNlSmZhODVXcjNpZWRjNHN3Y3A2T3ZhRAo0cm44RElDME5mRzd6Ujg1UFFFblNOdzVYRzZHVUNoSDZXMy9JWEY1N2pESjlXOGxwZ3c0S3pwTzJEMHNmaGlKClJERXNUUE1IYTRxSnh4UVIwUGNkMW5FZGtrMHJVWTVPT0VVRG9CODduanMrUnErSUgyQ0k5WGVONlRWV01ZNUoKWG9JcVhhZ1FLMVhKLzBrbWZoTmNjOTY3QlROSGU5YTdsRWtaU1hYNXdtcFF0a2NyTWhRdlhsYWNyMXRuVUc1YgpZYTV4MUkybGJMV0NsZXJNSkJmWUZXazFzaEZVV0hQQjM2c2JIUzMwN1EzSUdINi9mak1JQTNQOTVVYlg5VVo4CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNCtKOGQ0d3duYmFRRFJITHZUaTYKeHZIZHRhRUd6SklxNDVuN0R1dUFycUNQY1hBdEprdlBpQVovdDVOTXFWZVpJRTdNSnZxSkZCKzQzb2E3TlNKVgpiVERGamRVMk00ZENoODY0TkY0Q3VxdEJBK0pESDB4UG9jejM4U2pmZXdYaEFTYlpPU0JPTXdIbVZtZGNTditwCnF0U2t6SEpaVlBVV0JPdXYvTEphY1AxR2FHUWx0Sjh2MHpSdVdXSnhGOTY4QXpxc3JiYW1lS0EyWXNKb3FyemcKRnVxQktlLzhwMkZ3TGhsQ2hoY1FpV0I4S1l2QWdoWlJlU0FaU3hNVXR4cE5oZDYwdW5mNllsbUJnblk4Y2RMUwo1SDhKdm9SSmpyajQ4K01pb2dLSVUrTjBDVDd6cjl5a0d0SDVJVVgrTytTMDluMk1OaEdVNEhoOU5WcThlekpzCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVFOclAwbnBieFFWeTdNb3Z3QngKMEVobFU1eFRhdEUyYUtTR1hrb293MFJEWC9yVjhTaFNRTUFGdEx2eXVFUm53OEQxOHBLRHpiRHZuTVdlanhGUwp1azBabTU3ZSt3TXFSWGxuR2hiMzUraDJlZmlqOGMyREtQMXI1UFFZQ0YybWtJMzNJNE9PMnM5OGdxWWlpL0NhCmgrU0hHRDgrdWNYUWpLMERGL3hZSHUxQjI3NFJ4dEs4UmVaOXh4Q2JJWFBtcVlteUF4SVN4V0tyMUtWZldETHAKNnJwcENwNld2SzY5Q3BIbG5OU2hjdUNqQ2Z5VFNSVWRrOE96R3VQKzg0QnIrNE1GN1NoMU1ESFlvcENUclAvMApqalA5aUo2TVZYZzlUK3FVRlVwOFhXNVBHVjlqTDY3cDE1aDJOUENTdTdZbk5aZ1BvSmw2dlhudXhkK0pMNTZWCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVRRN21OZkMxWFd3RCtkaFJjZnAKSitmMGc4NW9YVWd4SG8zQVBhOFMrN2VkMW40eWsvblNDdFc4cFdjajdNWUxwSjJnUjNmZUFtQnd6YUtQUit6MApJRGZWSnh1aVlEWWgxSitNdUc3VDB5RjNQQnlCaG0xNFFza1JVU09ucUM2RHdxVG54VFdNdFpNY3doT3ZlNS94CmVpYUJUMXd3NEFSTDZ0a1lHa09SK29RL1hkVmdVUGorMndUKzFyNm0rbWxMSXFXckF3RW9HWFZUQ3pPY045c3UKV1V6TzBIZXVDRW0wRFZiYWgxL1V6V0l0bmxtTWdZZUVqdUhpdDRlMzFYMDVrRFFjd2IyZFk3YnRzTTB2WUlsawp2cWdvYnh4S1EvMXZMNDFnRmROdk9DanFTa1JZUXphWG0yem5OYi9kSkFzMnVpMy81NUE1SkxlRGpvRzVkSVErCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUthN1paekY1RjVxa3psclRLUnYKeGRMeCttSnJjTVlQdEhkcDRpWE41cjEyOEhHVzZYYmxDdnROVW1HMW1CWTVDRTlKMDhsN0h3bkdNTzB3MDV5MAp6VStnMTJlUzFuaHhSRGNFRjgrZk1Ndm11cXFmd1hHQ3hxN3BMQmFuSHBORHVRK3lsalpKTEdxSGJmU0VudHNMCkcyMnhRdGNHVGxvZ1A5T0RPaXRiZU9yNDgyd21ZVTA2UGJJalhWeG1NYWNvSWxjc1lDTjZQMUdmWVpZbFpnZFcKZk5lUm44YkxRRUhWeTFWbUt0d1VJbXdZelVPdW9JTnVIaXFTVnpVeUZEZVh3eWJxSmNERG9DMksxMld3b3hKSwpNL08xaXQrUUJuNDQreDNhMWdWaytoTENxYURSblkwcFg4QWJjUEVxMVoraWxGSWs2YVlvZkxGcWowT09icVUzCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1FJSVJUV1libFl4ZERBaFh6V1oKUStNNmw5ZWZSQi9OSmtIcGxGQmoveHF6MjBGeW5ZV3BhOWRPeDlQcTZLTk1lUjIzTWh4SW02Q3lFWXZpTERmUwoyOEwzSDRIS3FRYlg1L1pCZUo1NThZT0t5N1Y0b3JHeFI2UTI3bHNaK3htK2dwNGp5RllCMzUyS2lmQ1B2eG00ClFJM2hpQS91WnVuK2dobzQ4cjZDQWZwV0duTjhlMWV4L1owdzA2MGR4K2xzQXp6cUQ3M2RNWkxHNnRhcFMxZUUKQUM1MTNVZkg0dFBJY2VWZ05yQlZIbmxyYkxRaGVqMzJmNDNsVDBDRi9mN0p4aUtyUjMyZWxaYVF3dklWakphTApRYTlTdTFzV3NERDJLUnl1TmNoYm0rTTRENXFlWXU1U2F0SThhUWtwZjRuRXBkNFNiRmdIYmpWSlphc1VXZG9pCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFF6YWtkS0N5Qm9zVG1YYlpqdnEKa0hZU1pROGN2Q2lsbnh4ZXBuYzVIeGc2blhCN0VTQVMvQ3A4clc3bmoxbHZCWi9qNG1OZmNkendnSEhWbEZpYQpqcWJ3cVRLMUxTeXNpNXQwdHRORGVxVlprMnlsRk5abjN4SWg1c1dmWWFnWmJXYmtXVlZjdWJmbHlURzR0V1JICjhVa0U4ZzBZSkhqcm53YmVuMllxcVZWcTBpamRxTFdpMjhvQkZMckNoTEUxZm5FalZtMTB2anZQV2ZxS2FWQWoKOTkzZzBvZDlReDRKWXd5cWFDWWRoVEU0cDgzNG1vRHVIaFNab1BoR3lVem50SGRwUXk1OTRvNDdvVEpwL2w4YQpidko2bzFUaFMxMmp1MkFNRGdPcTVNU1VkSHRFNGsyaVhQNzl4NjJ1eWIzYU9FRTRrQ0NKMGdmOGtBcDlNNkVECm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2FzWkFRcktZb1FWZzJmcUFva2cKMjRZVFh6UEhhUkg2dGRxTG1RNjg3eDQ4dGkyNkU0azBQSTZKSGdlbjVyMmtUbWRQRmtTa1NiRVBDai9nY1RCTApnaG9ITEtkQ2R3NjBYcnkxdWxYc3RtSG96NlRSb29ld0NzNE5uVzA2emkrdGN6a2JjcVVmM0tJdHdSd3RDUGZjCmhETUI4MnMxQTd3dlNYRzNUSkNWUUU0dFZkcm52bVB5YUgvMVVOZXUvTlpSK3JFVWFnL2VldzdkSUJkNmNqUHMKTGJvN3JEWU5iVUw1WC94YXZSa0M2NXRxMFluS1o3RERrak9CMTBjTndvdGdTc095RDBHSUZkdk1FUEVVVXNaeQpCa3hxbEU2Q0VrekVwZW9DSkVqK1gyTGU2YWlYVW5yZy9VREVYU29XeTdrTk42NzFTWWRGaUR4cG1xc2p3MXVCCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkRyaXVIdjgrTnRyb1JpNEhUaTgKOXJaNWhYcDJmMUNtTmdBK1VLY0RQaHJoRHY1ZjloSmpOcFZ3c2hOVzB4QUtyeGM1a29sQUtiQ3VVSUkrQnV0UQorYWVLZGZ1WVZOdU5GYUFzWHk0Mm5Qd3RWOWtYbGNHSXgzM2Z6bWl1cC9RZDdFaysvd2NaNzlTd25MR2Mzc1AxCmZTMy83U0gwUVNXcG5vQ3BBaENyNlR3ZDZ4ZGxZNGxQNXlxMjM0eDlST3dtVkhrTXpMdG5DQjhFM1BxWnQxbHQKcmgwMXExRG5qR1VPSmg0dWZhMUJWTFd1bjl6bWh0SnhkWHR6elNFS2MyY1hYckZoM2h4Y0F1UmxrMlphN1RhOAo0WThlZThLbEt6ZktERFpEUHFYZWRlMjlWeC9vNGY2U1JmbDVxOGtHaXYyVDU4dkcvRCtMTVJydTJ2dXJNaXk0Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVJwdHlmOHpWcUJidXBnODRWMkYKRkYyZmFzYmxOVGRIMEN5RnFkQ2c3VmdoWmxuWmF1dkE5SExxZXdCWG9RTkJVSjgzK3RSY2ZGcE9YdXhTdUE5bwpMTjZmVC9xYlEyazN4ZjgraHUyYXg3dkhXcmtsMVhpakhpNkxoWDhESzlNZjRSc1l2QU9xanZ3S0hpREFaWEtHClRZUDVJODZOMmpDOWdGUnd0UVA5c2QzVXh0TXAwbDE3RFBmYW5YQlVXb1h1aGR1NTBPd3dYL0VQeEVVQkhJWEUKOFZEUWlRdENFMzZLQ1h2RXVGVmpSOXpZNDZ5V0x5eHhOVEVGWVVGN1dSclh3Vjc4dEFOVmhmZS8yS0pDVU5HRwp6Q1FNVEZaTmJwbFN0MVRQcHlGOXNvUUFVR0pEVUZxZVFpcUZGc1VEdWNqTGJnSnVLOFY4OCt2VEMzNzJhSTI5CjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEV2ZXBmNXlYNDlFUUtRVnpEcDUKdzVLTGVTcFBCbnNRTm5wcEQ1ZDZEM1dtR3dVYVp1L0l0R2YxNy9NSGllbElZRk11TlZVYnBqeXB5WU5lT3VEagpGTHlYMUozZWZWa3AxT2dXcC95U2daQnFnTzFwT0tUWXhGNTdYaXdWZVRKNXhwR0hCWEZOSnh0d2JCaU5lamJYCjZWVkxyRTVxREZ3YnhFbWVaQ1NScmhzcVdOSXdmc0lzQXg1SE9WNlE1UUcvRStjU0FVclJtcFZZdFR0STdtaHEKUnFDMUxRSzdBOGRFeW95dnBKcHEyK1F6QVlNY2xMWmx2V3N2SDIxRUlETnRiOXE5QTNWSkt4TVBkaEEwNzhQbwpUUEUzNHFhMDNUMGU1MUVmbkFobnFKeis3ZStOT0IxNWRHTFc4bHRMSWNYeG5oU2RzQlVJZlRhbnd6TGVmK0hmCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMld4SUQxMVM4bVkwRmd2dzNkdmoKVlZLRlREL1lDZXo1Uk9WRlZBNG5qYmhVU05PVGJRUko0OUpPSllkVWhGNERTdE0vVnREdGxtblhEbTVhS3dNWApwVWhNUU51bTcybldyZElrSUg4TVhmTGpTN3VmaWJpd2JMajNMeklDb01rd0dNek5ZeVlMS3pYMzNlNjUweDlIClhhWFdvVzVlYXlRWHZkOTY4bmVCODg0S0FmYkx5alFrUDFWSFdDNlEzcTF3MUdKRVc0am5UREtJa21ST2wyNHYKVmJQY2dVVTVYMHNNcGJsZHNxWnUwcEl1VjRyYkZEaHYrZEYwTy96LzFPUkpxcUd5L0hoUTN3RWtQNE8wSmtadwpOZDc1K2xFWkF2alFXSjRLLzlRVW5nSEpPZmc5TFM5am1IZ2xUbnQxdjBVTUVaRTg5N25sQTFyTUltcUlkdTBECnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBck4rLzIyNnhNVk5hNHZkMWJrSkcKRUxWTXFGcTNuWmF6NDlIYVFxdXZwV1pFa2EwcjcwMTkrVEZZaE8rQ0VHQWZhVVlHemtNV3JLN0xGcnVxek1LcwpnSWtFelczQktQR3lua1k4Y1FjOFZqMWFEa0hWSDUzd3BVODVObVZpRVNFRzd1NnJHMTRSanhJRVpOQUpIQ3l4CjlpTW8zTDdMVktRYUxWc2k5aEphZlZOVXVjbjRoUG9HK256SWVYak1jYUk3aVU0eDY0dFFZL09YNkFLbzlaZEMKd2VjZlBvK1VGRG8rVmxCaC9wN2tOSzFIUGQ5dlU1TDFGL3NkcWdkRmp2TVJzWVJPdEh5OG5UNHhzdExJRk8ybAo1QmNvWlJrZyt6YlpVUCs4QWlGWFNUL2ZiWWh5OUUwMTFBQTdMM2NIampnTkpDeHpoNjhaTEc2OEd5c0xIMTczCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVVHNDRKKzk1V2hGYWU2VGh3RjQKNVdXNTlzU3NMRytHOFBnK2hVay9PR3J0WlFTZENBajhNNTl3OGtWNG54dTRzc2hHTlhmZzN6TjlYazM0UUlNQQpEOTNxOWxUaks4NElLSFh6VXhhckEwaCsxVEtNeDB0eXppRlZCT1ptRzc3WjJoN0t6Zms4dDRKSlplUy9LaHFlCllyZFRhVFlHQ29ramRZZ2ZOd2s1OU5rZ1BmVjBTdkNiQVRwZ2ZZKzNTTm9sM295YlZ3Mi9vV3kwSUREVSswa2YKd0RPS1UrZlhVb2kvMllaeGIzTm1xNWZvKzlLZjNYRitGRTg4bGJEZmpycXA5VHRoVzVUeE44dlp3UGoyR25mMQpHa1JycXZqSWRTMVBMVUEyUnl5M3lkbzM4V0JTdW9LZDZyZCtjaStNeW9jQnlYQjFRSGttTmk4NkF2WGdBdm85ClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNENqbzRxbzcxK3Y2bUlWclNPZTAKUjhRSGNjYVpkYUpCNXpoRE5NbVZjdzZlMmw1Z1EyaUFoSXI0QTkwVklkeWdPcTFka1RrRTNHTTZucVlRYlloRAoyaFkrdlRYNms4RlYxNG8yL3pHV0JFWkkrWDRsZ3B5TGdVMFVKL1V3L2JoN2U3SzE2dStFZW1qMGVKWkplMnBPCnU4RkRvUTZvQ3hETUpqZ0ZWdlRHamRoVlp6Qlg3VkFsSDN5WUNuUEhIcHRvc3NVWjhPYnRxaEI0c3FLdks5KzkKY0NEa2hIVEVMaW1BQWVtR2FZbVAwb29ONkRCT0pXRjRZWmJRYVhyWVM0RXJGUkZrOE5sNDJUTlZKdVlER2xxQwpockNoZlBEclpyb1pLcWFZN1RLTUxWOGQ5QjV0cmltTUNBSEpIMGpmUFIxekx6OEVzZEJEdlBzcFp6aWp6cXJsClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2dudXlSaG12bUFWS1Bac2xkcFcKWUJscFlDSTlEQW1yT2ZqMVNwMVdKTmJmMTloa0FDMkI2Z3FybmtyN0RPYndZQmliS2tTUzZSZFkzczJJWDVWbQprL1MyR3hyZFN2TjZvNSt0ZXNmazRVazZ3eHAwMXZhc3FlRHc0M2g2bnAvWWJ5Z2g2bEZjbEViOFRRSzIxZit1CkpLVGxQOHBZcEVqY2JyNlJPQUlSc2RielF6UFVHeWY1SEQ3Z3V1RFpaTFo1R1QvZENuK3FzanNxNFdqZkg3NWoKV1Fycmd6ek55dC83ZktZb1BreTZHVGNIL2JpbXZIMzBBbUFnbVVPbXlFem9VemlaWEF3QUR4VS9HbWRxNWZ1VApGMlBISWloZkk4UjV5Q2ZiWHhFR1FvbVBjL2tTVDR1UUdmNzk0VFNHQTM5a3BzQUZ3bmVYTkhFL05Va29JR2g3Cm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDJlRWFaOGZvcHU4c1o0Z0tzQjkKNmlFdjYwTVJzdHNKdWFiUVpoay9lWnJPSzV0MHNHamxmZGNnTU5QbytuVUNTcXJLZklFQ0Z1UHc5T3M0ZllrbQoxckJuUFhYbncwVWxzbzVWdDNKSlZ1djVXZVFMVDVoUWJoUUN4NzBRd2xMZ0VwSTRMZ29mL1R4ZjRVcDFkWFJGClF6SDJiQlFJM2JrcTFlbXBrc2RBeWdmYWJRdXZ5aUpXUFdlSzVzNWhoV2E5MkFkaXI3UFdNYkE4cnN6OEhydVgKQXNzWWcyKyt6TEd4YjBmVGRETUd2RWpwWWhKUjBlOEtlODFXZ21uUHpIaU5DYW9zNFV6Z2ZwbWpnR3AvZlY2KwpqdjhReHRVTHpaM1RiRE1mNWFvSkdqMWFINFVNZjZrenB6Y2MrcjhmNnVhaXR5NlNKY3RHaWVYOTlWRWUxbDd0CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd045Q0t2dUNTd2dRdWpSQWttTE0Kc21RSVFZNUhvcHZtbElnbTFvcldYWFRXOW5seGt2b2ZGSW82dURtWmdPbDlweVVoM091ZkRTZ0dVY3BDQlNtNAplM3BwdnYxSmZtM29KdEpReE9neStVUUlMNWlIclJRb05JSWpXaThpM291MHZscTRJbXhKeTRzSHNoQXI4MVJXClorT1k5M1dpYnFTRWNPSjhhZlIxSmJWdWk3cTBvaTBjQit5ZFNyZXE4dG5QMlRYaG95NG1RKzJBVGNCU2pkZnYKUHFSZzArTUF4eFR6SERCTm0zZ05tdDBhUngweDUxNTBuVDI1K014SUtVa1Jhd1BFUlpMc3A1N05nY0FDVG5iQwpqMEdvZWNuMVJadGlRZFFzSUx0QXRUUndBTk0zK250RjRkSHE2YitKbTdpekJtTXpFdW1taWE4MHoyUS83cFMwCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1J1RlRUYkpsQnVMOUo0ZzFzMVEKWkpJWlprdFZSMllEc0djQStQakhaZlEzWjF1azhNcGNMSnFSeG1oblFQOTRiQkcvU2hiMHB4Nk1adjBrKzNqZwpVa2VPcTdTOHFQRzNUN0p6VW9WbGFHcU9NVXRnV2xndFVubklmTFBmOWE5ckw3M2x1QUszZ0k1WEY0N1ExV0JjCk1oRmJuWVZINjJNYVRNaHorSXdwQUZWNThLc3R0QXpHM2VMK21lNHpqK1Z4S24zcFZmS0d3TVFqV0lvS1JodEgKRUtHeHpDSXMzaXRndTMvSFZ6WUVsemg2V1ptYVpWeExhSEFnbFFwMFpuMW9kWjBJLzJrRGV6SitESWQwckhwYwpHbW05RE4zdEgxZ2ZYN2t6eWhOdXNXV2FYMm9zR1VEcHY2S2ZKNG5hWEM3TGNzUUVwT0RKVmZEZy8vSE9VK014Cmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVA5WDdaOG82a0QwTVA3ZUFTenQKU3ZPdmpPa1RJaDdiZy9QdWY2UXZXYWg1d1U1d3lGRlJ1ZmgxNWJBaXI4MEZPWndJV3Z1SWxJNVh1Qm9BQlVnUQpWa09LRmhYM2Fhc0w0OURJUVAyeXNybVBhT1Azb2QyNlovbVlub252anNqUmQxWjREcGJkT1p0UkNkbVpFUStyCnRzTG8yaFJLdUllcHlaZWdnb01Dd2NCUFdZRER1OXpCUEFyQkdhYkkvY245eGY3bVlEeFo3M0owbmNJU29PN28KdG4yU0dOYzFZUUYvSWEzNGFrNWVteG1IT3FzRzl1dVdxdUZST3hBSGNTaEZ6TDlIWUI0RGJLSHFKT2dRWnB4bQpMYmdoWXV6Q2NMYTBVSU1pcGZlN2dEeThNZ296SERtL0JOdjRpeFkrLy85bW1rWmxYcllsL3JvQlVZYklRME5ICkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVBYM2VQZTlSTk55VHl3S2RqMVoKRndTRzRCUVByTGZmWk45alZIVDk5NFJMbUZQcnZYaUI1bkVvWitJODdqdDVyRlA0MDRQY05TekxYYklMZEJsRgpFNjBNTytoQlNzNUdwblFuZElJempCUklDempyNUI5cVVPem1iSE1hVjZvaWw4OEpsT0dKN2pkMC9ibFRqQXFlCkNZSnVUZHZxaFFGSzZmQ3hPakNwYllmL0lLSmpJQ2JEQTdPQzhOK25OajN1ekRETFM3bnhQZlcvWHlzWDUrNWcKaVZSOTdLZzNablhRZDNwbW9jWGg0NjRRQnpiclorcEhHK1AxK2kwMTJpRUdrOGJDbnltYkw2RjgrQ1BQakQraAp2Wmk3YXZXNjZpQWl2Vk1sWEp0S3VRNC9PN1Z5ZjhEYXg1U0JkSUhFcitTYjBLaEV6aWNkeFFVeXpkT1pTVUkvCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMExGdkF6RlJLUXhJa2M3ZVRQTE8KT2ZMQkJ6ZGwvNjZFQi9pWk9pSGNwNlg4TDRzK3dUTDEyNmE0UnRJNTZMZDVISlFPUjhVeWlDRGxWYkZJTUowVwo5N1F4NlA2N05iR0t6QW5RdmZxVUNWb0hvY3liY1lwR244STk0WThGZGdBQVZLSlNWTDBjdTdWQTlud1V4eGMwCkhaL3lGbnBZeEFmZ3NIT0hEb0lrT2RXaVZKckdLUEI4WFRnUWxweVAxdVY2bHBGUllVRDFLa1Y3WTZzSTdWTS8KQ0E2ZG9MbWZRT28xVFNGNWxvT0plWWQxN1R5d3Rza0FQRUtkZDNuREF6N2lrb2VrdEpTREJHODE2MmMvYmdFeQowU25XVlI0dWVMR1kwWmJ1SUJYd2FkWWk1VzRBaHlkTjlEVjJldTM4RS9KOWo5WnlOS0wwbWJObFBEa2VRYStkCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGE0U1NENGdmb1pzc0MvNHJIRHUKZVAzcGdzYTVBQWtUMFJoYXkxYzZvVERHVWlhVEYwN2gzQlFsZHVvR0Y2ajFoV1pDa0FDY2U3SnBTM3AvVzR3YQpCZ3dEM3pBb1kra1FtQXllbTVweDhCdGdZVmpaU1R4T3d4cEdDanVBS2JrQVFBZHZxQVA1d2IvTEpycUxBQ3JECmJzMzh2Y3V3UmNYOE5MYlMxWDc0ZytVVkgyQ3p0ZzI1LzJsamRsRUkzQWI5eWRnR3BkVGF5aGphU29DaWpteEgKTnFteThwekJQNnVRTi9JOFV6WnFnMEZXWFE4TEFMb00xWGZiak1mbDBkQ0o1YnVobmo3aForR1NrelF4S3prVwpOZ3RxZkFPMU03elJnSFNBVUM2WXNDdEprVEZRWGlBbUszajhOb2k4WlYwTW9lbHp2LzRIRCtFSERCWHN0YW9ZCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFkrSGlIdk15Q0VtbWNpK0FETG8KaXBqcWpDbkNGMXJ3Tno0OGV0MzZuMHZ6c05DOU9DZUdwRzFsbS9icVZiSkpWM2dLWUZzOWFScERtcnNFVmRPVAphZGxMNWtaSTV1Z1pRejhnY2VSWWdHTW8vSmdoc29UcVZRdmJ0UjBxQUZreW9CVzlhMk8rcWlNb2hSZ0xsempIClRCQ1ROZCtBT3hPaVhKT0ttSWxxR1I0T1JoTStyN2JLV00yTFFuU040eEUxVUp2MWQzYklJbDVEdVhZYTFVMkIKK2NINGx5SzNMTkFkSHRoelliMDlRY1RpZlFFYWdyTk1DZE56bXlPYUJyRGsrQ1BGNkhOeFIrdk5NZ1VyT2YrQwpRczR1c28wZW4wUEE1bkZZY3hxQWVrWkR5T0VicTNuZGsrN2dZRzlUM3RXNlVFRzdoUnNSVVNzTGQ3cjJGL0FXCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWJGelo3RlhOZXRHMTZITW93R0gKamVveXN3OW94Yk1PWGYxMExUempzek0wT25nanN1STlDaGJSTTRobHVVcms5bzg3RE5PeS84U2NXUDBHSVpkbgp1azk5VGg0Y0ZPdTBXZVJqUitOYXV3TjdEcG9ld3lvWlB6TFhBVmovU092dGdTQkN0dFphUFliclY5S2w0VkYwCmZVVEZZcXlKRlpZZ3QzZkU3YkFtUm9KOExnK2laOHpIUHc3RjYrdS9CQkh3Z25tbGxyU2cxNXVPOXFoQ20xbisKSm5iTlE0My9FNUFpaU9vQ2pkRWNmNS9ZeHptZlFkRnVCMjlrb1NIeHN4UE9GQVN1cXFIRU96Y2lZTm41d3RpMQoxYTVFSUh6U1VLc2REZVIzMnZyZ0tIbXhqWnVvVnFvbEptNEM0NWVkWG1TbTB2ZVU5Rk9lVlRiRWQxUUdQSG9NCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFoxZkpKSzBsNmlhSUN3YXVIWGUKNDI5TWNEN05rNmxObkZBaEthYUcyQk9rYnJUYldvSDRtZGY0UVBDQWxqRG1FdFNrTEJlaVVEalcvcDZGRmlzegpyR0YzK3JhNzROU3FneU5NVmpzdFVwZGhiQ2ltTko2NTVKWTVnanp6V1ZtY0tBVDJIVFFoN3RkTStHSXpqSVoxCitHSEtWbmROSGNvK0xUTGdvMFQxSkdpSmpHc0dSZ0VUcEczU2xrRTVUMkxqRU1PYkRvNkwrUGhvbm5JZkhNS1oKbDgxZitCZXJYWXd6bXd5QlBVSWlIZ1c5ZDU1S295dks2enkrUHhuSjF3MmNjU1JBS3UyeENlclE3aEtBYUlFQQpYdW1mbUtrbTdPSEVoeHcrWDFyUG5EZFZhOGJsOXZoNHJSVi83YzZvaTN3bnUyUmg5SHdXc3l6L3pRR1NUNnBMClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGwrRVVqcnlEVnBwczU4SXhpRnUKS04vNjA4SlcyWnRGblg2RGYzcjA2VC83VlppOEU5V2lTV1dUUjdOUnZST1lpZjVjV0pJNEMxMzdSRCtJcHA0YQpQZGR3VFcyTG40Qy81a081MTJKNmFjRVBDWWdROGl1NjRQSFQyenB5QlcwRlV1b0t3MHFTYTNnVDBOMWV1eDhDCnYyV0JjQkpMNHZvblUyU0FzV2dlZFRYeVlTK0V4Ny9BbUpMZ2JlNzlOWDk1czBQVkhHN0R0RzdXV3hxK1BZR0UKWkpNOWJzU05GMk50dUdpNjRpOWh4ZGpxOCtpNUJQZ1FQenpyOEhVSnVNeld3S2dyZXQrTVpRV1owSm11OUxsSApBT3pWR0Q2TVV2dGlHVHUzK01nV05xbkorekQvYTBLMVowWWxYeks3aWpGUThhdGtTVithVTVxNXdpQS8wdXQ2Cit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd09LZmdPcmJWcWdIdDFSMUlhL1AKM3JZNkswZVd0b2N4dkpmaFB6T1hucUhSbFo5b3Z2cDhubkFvbURnVm9zUFFJL1RCcDRrREJ0cS9GekhaZ2IyRQplc1gxeVd5eDFuTFo1ZHAybmtpWHplWGl2UHR6WmdQbDJPZS9ycG0xWlpaVHJCTFhoejNaNng0bmxyRmJaMnQ2Clh1M3JuSTFmamtaL3lPVk1QeXNXZ0xKRG5KUy9LSWF5T2JTRlRZRzJrLzFMVFlJVitZTXJYdlFrTk9mQ2xPUUEKUytWY3FTbkFHRlprdXoxc3BCS21ic2FHd1MrMVpOeis2Nmo4b0kwZnRrdkt4UEU4aWFTRlVVejBrclpLT1d6Lwp0SHNHVUxhaksvMmxOZ29YcS94NVNTTmZRcFFtU3lmSy9UWW00NXd6TG5mbnphbTY4VHJlaVZVRURXR1dFNlBZCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVF2T0U1L2kvN0xpRWlUNEhlK1EKenJmQXBSelVZcVIyaDFZUW54UjNkZzVsbnFsRlhqTklQT1BnSmxYOVphNXFLTFVOdXpZQ1lQQm40bVpmdGJRdwpudmNZTkJiRVkwcks2TzkyWFpWVkFONVJqYVQ5Z3JmMEN1OFBFVTJyTlJwRzAzanI2WWhqbWxSTHhnZytpZkgwCm10bHV5Si91L2F4VGg0YnRrMjJUdWU3RDFieGdGVlJySkU4VE04L3FXYmdSSHlFVlVWNkcvSEMwWVkzMjJTanAKNVVkRWpHUHl1UE4vSzhSVlJZSHBPYzlXUXgzT3VHTzFldHF3T3FVMytCTTBuTUswNEMwZmFpQkZUOW8xbktVTAo4L2FVbVpvUWF0WktyM1VtRUV6QXJDVU9PSXdhTjI3aWtYaEhvYWloWjhCNWdOMVpDMkVoSFBtSVRpQVA4OGhIClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDJuaXRoUGdsVllYT1V6OEY0bHEKZUxtVkIwR0N4UENERkROWG1aNXNxTS82cTZQMU5KRk0rRERKUGg1ZVpNRE5SVDJoVmZEWG1qYklzV0ZjbGhuWApXYmNha2h2MGtwdExoMEQvR21zNGdSbjVrcmQydzJUc1NUNW5uK1ZPK0o4K3R4Umt0WmMyMERhUGZGbUthWGJ1CldvczN3VW1LZTlCYWdTdkxlU28rQWJCb2J3L2kyN1dvVUloV1R2ZnJTbXllRTY2OXErdCtVM1lDQTM2b0R0UnAKdHpnTVlyeTREZWF6dGhIVFdyNkt4SUt2RmtNWTYxa3Z1K1ZKS0pxVzYyaTk2M092UnlDQnhoUVZXazVyNmY2eQpZRzB3Uk56NGtPMzdNNzlaWWNXN01Bam8rcFl0UFNXZjBkQzFEdmpwbkV1aFRMZFZ2a2h6UmRORWhLOHZIMkFGCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXhBWXBNQ1BGcUEyQksyT0p0cGcKVEhnZ0VuUVJORi9tTE1VOVJVZnR6ZnpHL05SS3ZrdVg5cVhJcmpwc1NtQ3htS05iWnFkTUF1Q3hzWFVzSVZrNwpoVjh2SkRndlFQOHBpeEk5WTdIU2szc1NZWUhJalA2RzlxTGNIRWhMS3MwZUdWdTJQS3lmOWY3bG9NUGRiSnRoCjV1VzJoc0V0M3ViRk1SbDRZdTNCblRhRVpUTlZ4SlBtRnJSUVU0SjBpWHkwU3RnYTJ1UmhQSTVzd3dHZU9CMjYKZ1gxZEZkek9NKy9lSmV1Ny9kWDNYZlFMRW9halZlWnp5dXJ6VG9ZazVWWkZXdWQzNW1iZHlibVNWTjZHem5DSgpUcTFZNjU3SjFZOWlDQnVtT0I3YUtUNFoxR0VPVjUxZU9yODdLOVN4T3pNdmp2bmpNaTdHTzl1N09QM1B3TzUxCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmlTMW5pbWRUVWZBZDFWL0h1M3IKeWNiTmltRzNkTm5mU0cxT0lrckZXRnh6Q1pqRWZWOU1nSVIwVjFYM0ZJMTM3WU0rUnZhWm1GU2tMdlJxQnhtQgo4eFlybW5WQ0puOHNCYmlZY01hTzN3M2pIL1JEci83ejY2d1VJY0RFZ3E0ZFhiWUhCZ05EaldyWW5aeGprZTdJCmNJQ0tMTmxTUkI0am44Zk00NXpubFZyam1DZi8rb2tRTWhGNzZVSHFDVW91RnRxMDZIa0xpMzM0QTlLdG9hS2oKZ2hvSzV5VnlpMElDYUl1WnJKWHlCcFBQN3RKaER2RnNJQVRNTmVaMHR0dm9wUzBVNDNpZUV6WVpRcWRtaFhKUgo2VW1nZnAyWFhreXFpc3U1ZWlaYUQwVGpILyszci9nVWlyOUNoUWE2VkFEVTd6enYrVWVQekdYUm15c0pOOGNTCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWFMbnFSam80cUVmd2xveUpFazQKTDA0VzZHamt6OFFtSnJvd3B3M0M3MlJRRGR5WUtqSFRVbmZXbVZaTDM0VjgwYVdvZGxvbzhqUHY4bXNRWWRMeQpEdEp3MUhQUXFOMkdFK0V1dEFUMld3ZUR6czhEM3lEaUo0K0VjUzNyQ1hCblB4WjZBcXdyQnVOandPS0N4SEE3ClJzNkFhakVjeTA0WnRXaHVhSENIVkppR3pzeXhIc0lwYUZOSTVWWVlFR0xsSDM0QjV4em5QNkd5bVpvWk1LbXkKaDNFNldpWEY3dVdCWlFqV20rQmJUZjZHNE1VNEZ0SFFrWHdzWlR6OHU4bDZReFFMMWY5UVF1QzQ3YjNXTlEwMQorMnB2WndPSmcyTGpHWEZ6OExIYng3ZE52bzlJZXAxVlhtVXRYUXgwdUxLMDZLcGtiQ29FTGRDUUJxY1NBRU53Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjUzQnNiRkNpTUIxS2tHdkZZTTIKVHd4bE1iSWdjWEM1YVdEV05QM0dtdXV3NFJXWjN1L0NjMXBtTHUwVHErOXpUMHlGMnRHbloyT0NoOG90eHNsagpUNDIwZGdhU0dxc0Z4WHpuV1BxU3Y0ZXpKc3pXL1dBYXdMQXBVYUV3TzVWY0p5N3VxNlpDckQ2d3pTcGwwT05yCmpIdENqQjFTNTFsT3dGT3B1cWdHYTdRNEpQRlNmcTE4V0E5cG01ODdWMUNDc3N4RVd0RzFYc251UE5JbGpPcHIKV3FxSjlNU3UycUEwQndVTEVkcnhqdVl2djBxYS9CR3VmSlN1WGdFd1ptajhKSDI4eEU2SUtCUWtuY3V5cEZSMAovSHNmSmZUUktMdUpvL0Z2MzZ3SDloRmFTVXdOc01ockE1aTBWUkVuVW9Vcyt6V2Raeit3MndhKzVlc3k5UkpaCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWF0OXpZZkkvZWx4aU51N1VoYncKVkM4Y2xHWXk3ODN5MDc4T1JSWlJZelkzV0M2ZFI1eWY0SmdReHkxUkMxZjdkWFZyZjhvSGlQNFF6MDY1K09hNApPZkVMUGhhd1hWcThoRXhqNlJPYlRhQ3lDT0hzdklzWFVOSWhNWUlmUmVoVkkxNUdqKzJMcTVJOFlqeGFXZG1OCkZTaE93ZUx3NmFSMUErYXZSWU5qb2gwQ2IvK3FmZTVZL0Q0anN0ZGJjcjUrMThHa21LQ2FNV3NUNGxOaTM5U2QKdFByalZtRHI1TjhaUXd0OXh4Wm16NjM3bDU4cXNxbVY5ZEdLRVJGdCt1dGhqSUlLV2xuWC9KSHBkSGRla3phVgp6M1lHcVlUcll0ejR1K0ppMjExdHVwUHZQRWVKQVVjcTZzRWh5V25vUzM2M2cvY3E5MHU5VFBxeDVwRmhJcnFKClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkNoaTVpMmVzaUxUNXpLM1NyaTAKWm1yc3RpM2wzNWRUS3lTOFZyUUhBWWZTMFVvS21FaFd1cnBySlljSjZSTWt2c0hHMU9VcHFGN1FCRGRka0RUbgpickRiR1RvdXFyNytJN1o0NXpNd0tUbm92SjV0NCtjOCthS2VzV1FDRC9oUmd3RUJMRWhyWkh4cWN0MzJ0MThyCjB3V05jK1BTTFo2aFpZK1B4endBWmQyTFdhc2hqU28vRFBBTUlrNkxhU0pIM1VtaHJIbSs4QUtLcVlTQWVqVjIKdmZENlNqSWpkc2lYOXMzR1ZqRG04QVN1N3NyS08rOVZLWmNQcjlUUGNjQkthQmFCN3dkcWIzSHBHM251a0JpaApPUG9QWWVHUDYvcG1qRmRNQXRwQmZHR3JsRWR6cndkQlEvWHJoRjBkU21YV3VQY2lZSko4bGZ5aGZ5MlZWb1IwCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzBVT2VRT2g0cGUxbjVJKzZxaXQKSDl2Q2o3aUtxTjRMaHdmaitaYjR2amVUeUs4OTgzcmZyYys0bE9CeWdySUJ0SVh3cStIVHlMbDRwRHpTaENVTgpmS3g1bUFYVncyQ3RDcGljdUY3a1UwS21FeTBQVkJ4Z2FvUHhGbS9ib2FabWlNUldIdGp0N3haYUxadlBrY2VTCjRuOFl0VXRYYUhldlVka2N3c3EydnZPUE1nb0V2OFY1bVM4aG9ycmFZelN3cmRhelp6L2dnYkNhcm5sWlFKbEMKT0hKd0FRY3BiQ2ZZUGRMV1hsd3l0eXAvb0x0aXpjcE5kekxjUHY3YnE5MFVYYUhrcE5zNXkxTnZ4WDlKd0ludgpHYXZ4bDQ5NXNVRmxaT2lYQnFES1JodWlMZXlRWlRaT3IxWmJrdHdXSHBuM00vWUdHTmpnQ2IrejF1VVRJMFp4CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczBsZVo1cDZDZklTSjVIUysxK2UKLytHbmNrcmVhbCtVQWxhbUU3blNzRTUweHJDSWZTT29OdlU0ekZMTnpsQ25GMHE1Tkp0MytzeU5WekYwclNsUwo2dGxTMExzalZaQldmclFieEJYWWhVRm9KUzZPSWVBZGx1L2FJN1dVYjU2MEtZdVJRd2Jwek9LNXZVVGE2eFZWCnhKU3BlNWpiZzU2WDNwUUZtSFBNMythelJyQmRBRllVL0ovdUdHQkVTdkd6S1ZINlJCa1pDdXh3ekVYNGZ3WlEKUGdKYk1ES2VxcGZKTExDd1h0MERuM3l1dnJaQnBFYmdCc0laQURwbmJNbVpDQ2NtbXNVMmd2MDN4YmV4TTBwTwo2MU9EV3I5bThKVVpEdUVrenlRT25XQlVZdHhjcUd4dmdEZml4bkNqYzFOc0VVclJUbUJYblNiL0FPOExscFBECkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFFFMjcvK1Z2NVFzNVFvZXlGQlkKZy8xU3B4VkErak9UNm5KSm13WXVZNmkyVGFUYys3S2FZdUhHMVFtTXE2SzJmeUpUT3B2dG4vdTI5M2JtYml3cgpNblpzaDJCVldia3BIOWpIcVh5ak9La25kcnIrUDhCeTZUN2pYOURwcFNKQ2hSaE9PemNBMU9TQjdWSGVZNUt6CjlnZzJBZ3g5N1JvRTZWajF5bnFHenQ3djYxamt0Q1B3QktFVWJSR2ltZ0tueks5VnV4TVZ0VGs1NHZNZVRaRWcKdVZEZHFFMjRTYnI1MExOdUQ5a3B3MXJadEVXTzJHeDNFdTF5Wm13ZjkyS0hjQ2ZGMUdQQk5ZV0hHc2gyeUZOdAp4OHFPeUdrVnBVeHJ1WGZST2cxaW5RYUFRSS9KR0lEclVPb3phVmpkRVlsdWc0RWV0aDBQcHpmRUppZmo2YnU4Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK3VjeDBybUhrY0k3WXZyRmptZkcKTEpDUFVlUHJISzBtaVNMZ21CeVVCSG16dGlnY1VVQnA2cVE2S25PZnpxdnNldE5ob2VNVnZqNEdFUXRDQ2UxVQpqekVocGdiTmY0dUpmTlJLU2g0OEpva2J4K2llNFJ3V3ljWFJnK3Jna1htd3NlZWlEaDl0K2JOTzNGcVduTFExCjFURmN4dTh0Q3NkTDdqYUpqaEZzck95UU1iRUFCQ0ZINmNDcE9hQi96T0JBa0RZYzVya3EwSmJDVW01ZnVtVjUKNEk0NmZhdHRUWG0rOTNVWmVBSWhQeiswbVBrNGhqS04rRkVmY1ZJOWMyTzBWVkFDOWxzZDVOTjk0VVQvTjVpSgpaeTlxZ2RrTlhtSXRWL1VPYUwvTXAzUHFHTzJpWU5qeTFvU3E0OHJ4YjYrVkZJMk8veFVZV1lEYzNkT2dCMjY0CmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk5lQkhVN3lvK3dGL0o2ay9ZSzEKL1VucEtFcFlEZXZ5bUI2TUFTTVUyd2EzRGdaQzZieDdLdU1yN3BBRVFnMjNTTnFJMko3TlNSWC9kdnh2YlFSbQpOMXZJbW1XRzM2bXZUTmhQMFVEYW1xU0grNEpkWERIdVlNRHBTRWcvYm9WYUErakxuUDJRVnFURnVNUkVCT01GCmU1UGRGVDFsRnRQWW8yZDhGVEZ4ZXIzS3kwaGhySmp3ODZuT2ZMdUtpYjlQVUhLWnk1UDlvVGNmWHUyN3NsVXAKbGR1dzlHWnphbTJOcHhjMEdFN09EcTcwVjNtbU83SHFpa1M3ZThWTXBxUHZzSVl2cXN3SGpnaHZ3Y1JXWVBMRgoyOHNuRFhZb0VndkRUSTVHNlQ1RERiUXV4ajRjOHdWSWxaZ2JoQUVWUVM3MXIzL0dvdm1nUW1XWmRwcXVwWnlBCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkVZUGx0KzZzb000RVZ2Z1RNWCsKT1lIVzRUdVpGVFZ2QUUxbUdxdmVLUmR5SlVITEZPbC90cTNsQXdLaTd4U3l6ZStnYWY2dkVrNFBrQ1oyaWg3cgpoOG9mU2pqRzRBL1c5bXJCbU1XWW5PN0xOOUM0dW1makF2UEdmRU5Eb0hqZFZ5cjZKSUZkZ1JvSlBEZ0ZYOFZzCmp2L2JHeFYrTGhGNXlUZ0FtcU1RWUU0TmpHaFEzSTg2STZXRFhuRjhYTjlPUklZYTRsRGk4YVQ5QkFNVkRYNXgKTXpjazQyRFc0WXE4a1BvWmIzUG1YWUNKL2U5OFZDVXE3UHlwVmJwSkRWc2FtMjNTd0UwSVREOGpQalhLVTBMRQpSbEZlcitoTHh5eDNScm5tMTNaT3VyTjBiREtyeU8ySmtRMDNkL0tJVVpyQXNtajBzcEVNdVluTGVsNCs4YndhCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd05OOWo2SlFYeGs3N2FmNjlSNDIKcUE1MGNKVU5yNFdtdWt6NEFYcHI2Wk9UcVZ4SjY2N1hBK0hJK28yaDBVbzhGakNQYWNXVVRKTmNMVndldDgrdApPSkI1WmYwclBJMjdFY0x3dkZrSHBLSjMyUzAyUVJ2S1VidXpHdktkWTJKK0Mva1ltS0NZWkFOQWU1SGk5SU02CjVWZ3hJM3pESDExTWNqTlVsb0hUUnQ2MTh4V2tNMVBoelNCeEhyaHlVbThUMDlUOEpXbjhBZDRiZmhHdEt6L2gKK2RqUWxIdjFJWktrMjBGc1R1MHVMS2lwR3NnV2ZxR3JBbEl4cVdPeXNTZGFqY1RFb21yOGR0THBhRFh6OXJBNAo2cWNNLzQ0Z1k4dzFydy94eE1OTzhOUWMySE1kZ29pYWp6VndOWVorYTFvbThPMHRMbXJQci83ZU5ocVAxbWhPCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVFFT1lnZzM1c0dZNlpGQWgvMXYKR3dQVFpoSXdnWkw5S1BCcVByandVcXBkVGFqRHJ4SFlhYUlqMEFMTnhEOUpJcDFORXVQUkZxenIxOC9QQkdMVgpoeVpkR0kxVWFiV2tqTzhURjFMZzJZbHB5MnZHT004ZlhQTCtVYkRyM0ljc1pyU0txR1dUeU9RdVlRaWdDMUFZCkxkZEZmN3pUbENpaTNKVWVYbm1OQnBoYWM1ZmxFaE4xL3hTSUlBWHpLQm1jTGdzNFhlZTU2U1RjK3lsUWtab0QKbjBRNzY1eVdPZ01tdXJML0pueVBQOGdubWtnRGM5dVNWRStKUGhVV01uZlEyT1FnYXNiTmhUVW5pSkFhMnQ1ZQp0MzhGMHZNdytGWGZjU3lZMTEzOUl5R0U3Y1BOSUV5aE50L1NKcVp6TC8yUWJ5S0NOc2QzQ1NXYlVObUlaeXdiCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWpaYjRCVVBWUUpEVGF6bmFqNy8KTGV3dHV5c3kxQWxvN3A1cFZTY1ppTE8xV2tjajR3dW82N2h6OC8weXVocUQ2amswcm5UNlRReXZ4ZEMyak10eApuamFoaWFjQ0FjLzA2d1VRVXRHa2hJLzl4WmtHQWZEeXI1MitVT2VCN1IrSE4vaVQ0MFdwbFhwbnR2bUZqNHNlClcwNG5KZk44eGl4c3pZOXA1OXQ4K1loY3BpU21jd0M0aW82Z1Nsb0pnbnhSdDVYREx4QUxlTUlSOHg4NzM2WisKVFlDc0MrcXpnZkVJZkJFWVZEZDI4SUFjSTNHTHA2ZzFDSFNmc0JJTi9ML2NzcEMyT3hwbFZ3eFkrRy9sRlpPZApBSG50T29Dd2o2NEx0L1JERFdoOGZrUHlSU2d5NTIrNzkzOXVCSkxrYWpkUlRnSVE0SzVpd1FsMW5QRkVyWVQzCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjVlUUhqSzVtU29FTUlrYityVzQKZ3ZUYThnWGdtcTRtbzV0Mk9NREJEY1V5L1VoQ2lmQ3l1K2pGaGZmVTkrZklVZ0dEa2hKS1I5ekNqQmRkSldZYgpJcXl5VWNhMGE0SXBMZUt6Rmk0bW9zbVEyUk1NY2VFTjhMK3p6YTVpSkkxWVZ0QVRTc3pZL0JmdEJyZFk4NytTCjZJTmhGaEl0OW9yWUtiOXNxVElaazZWREwrcUFjQ21wMjhRcFBxZlBtekR0ZmtWUmZjRTN3bWNKS25kTUlJUnMKZ0tmVjFDVWNrU3UraWJ1dklVcFV3VDlXK05Nb2VXRFlxcEYwL00xVFJYdkRMMzlUNzFEUnJoTzR1YnFDeGgyTApjc1dUdmFSYjBHdDlWYmRDS2NMTlNZL0ZTZ0dJdFlSMVNaZmJ6WFFLUW02bVh2dHQ5VVpQVXhod1Q1Y3V3a2tZCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEs2QzVhczh5b3JwL3I4NkR2SDIKRWl1S29IRjFJWnNSOElCL0lNbGpEdmhpQWJ1azJxOVdKNGIwdnA3MVpacGU3blk4VjZtNFRsL1RVd2d5Y001MApRNExENTExWkU0cHNvcnJuQ1l2U0pCcFhmcnh5bDQ1N2p0dWpkOTNvSXBNcG5kNkZtdzVhVjBWdUtjMnhpQ0E5CkxqbG8rdEJZSzBaTWlzYVZmampFd2F6L1lOTlE2T1ByQTY3YllkakVUVXdQTnVES2NzMHY0MVJYTlgrQkQ2VHcKR1hkcTRCRVVWVXM3cFJKTzhGaHVjdHJ4dlNHNHpMWVBKVElQS29jSUVjTGhiWXNQNzlYS0N4QVgvTlpvbWdKWQpKeERMdzk0UTJPUHQ1WFZCbVZ4WExJek12bFFJNVNzN3M3NU9OUEFtYnU0VnhRWENUL216RWliVktYVjdvN1dUCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnBpY1dKeUtZZVJxZXZ0ZkRzV2EKRkJ3MElOeUlGT3huZzhReTJNZW9PTG91L1NxcHJQNm9wRTdYTTlVV2VOUGVSL1FQY3pwNkpxeENYQnBHTkhDLwpkSzFGb1U5M1dtWWZrenROTnBPSUFZQkxOcDhOaFZTYWZLZHdiaFV5bHk1ckF1ZkIwaTdaWi9rQkpQUldGV1FMCnNDVUJuc1lFVG01RXJJM3BBUlVZei9tRTU1Q2VQWHpjVXJXTVdEUWJKWEVZT0pjUDZKQ3JBTDZIejFBV0FsenIKamwxN2wzd2FjZXZNNW1ITndyTHRvbXY1NUpWUTZBYnVRa0t2dlVabGFzUG82T0VXLzBJSiswOXF5OHhvM1JzbworWlp1YktCT3VaUVM2ckJIOVdzdEE2VEZGUXZ5c0F0UkVyY1RGZ2M5aFhLc0RZT2NLbzlPVE80emlwcVhSY2d3CkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzFxNldOVlV6TFR1S1R5QVcwZTUKZ2Zoc0s5MWdTNGYxcTE3M2JHOVp0UjNnVjBoSnJFVWFsWDhOVFNGYnFLQ2NGRExRME8raERKVlViWlBWQ2FEOQpuZ1djeVVIV2IwdDRkZ1hhYndGRWFSSTByZklMNDg5THpwTisvOTQ2QVRHNE1UNXpFbm0rY1NDSVM0Z05Ud3Y1Ck4yOWF2R0RqbnVVcG5rUDlYMDQzRW04dlZlUjUzS0c0ZEN0K0hjU2hweXBrdDFZYjllVjhBZVRVQTBQZzZ3eXUKUU1SL3VqQTZVODVBWWNNUzU5ZnlXaG81Tkh6M0VnekM5dll3aWFDWmpJbmtMSVZWYW5jZ2JDb01tQU13RXNXcApTWlZvVFhGYTN2SmxoNFZVRHhQajRkcHk3L1o5QzlpN0R1WG9uVm9ucDk2dnlTdk94K05nZm83d0wzQWVQaGl2CjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGowc2tGc1ZhZkIzRjd6NnlTTDcKNWNSY2txZjJoZ3dRL1NxRk5adFl4QmNMOHJaQjJUVmp2NE8weENYSkI1QkJqanF2OWlicWJCV3NWNEVXSitvYQpYNTMyWjlLTGZoS2t3UTlKWkdySmpHSWpyTnIwQ2RTNXF5NjNDcktHS1lkSklxcTNoTXhUOWZ3TXNDVDhQaHJMCmZhTDU3dFBDUi80UkVTTVBTMHNpR2JSem11WTdHUUFkSVF6cU5UNGo3SUU2L0w2MUhJNVhpamxvUmx2VmZZOHYKUHpXYXIzbmpqTXVEMTIvUlFab3RQQ0lST05MZUhnb1krMEJjeTZCeFd5MWxsdlRaVnNsNVFvZ3BvTkRwRWRtQgpZdEJ2RW51VWVMQnhFaDBmUi9pRWYwb0FXQVFzNmxXOXlsT1Z1WGhTWEdQbW0rTFF0NFBxZ2VNUGNvSG9HSzUwCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOS8vdlFOclZQckhHaVN4dmNuSnoKTFRtR2RlaW1oeXlFTk5PRURVUUdKdzRjazRyZGIwbTV6RHhQVTFSc0ZUMndmT1ZsaFplV1FqWGx4cmFiOHpCMQpCNlppNmtWWmJaemx3Y2xWRXZnY045dGNWeEFxU0RtdWE0aFh3N1dhZUlhSnAvdTREakQyRXhlcnlhL3Y0a096CmxZZXJMVGdvNXJ2RzU5NDg1dG5abTQ4ZTFBUUhKY3d6NFQrZmlZZXRCbVJISVEvb2M0b2IwUkF1OEh0UTVWNzMKaVpPZFRnVGNQNlBWWkVJUXF0VmRxQzRLcTRYT2dHQTJmMnFWaWw4MXg3SXNmbzJ1RWkvL1g3S1ZqSXRvMlpQQQpBMGFlTC9BcFlraHIvMjQ5WnFIcmkybkhobThid1pQVVpGZWUyZFd5VWNXazVYNUFZS1h5aTV3cGcwUi9SZ2JICjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkZiU1l0UU0wenBDcVVvOEdJWDEKcDI1bW9ScnQrby96K1ZXZldEWm5teFNJUCtjQkZXdHFMWHNrVTdoSGNOTzNtNjhOV0VTWk13Wm8rM0lBbnhpTwpnVk51VGtSUFJadG1NQkhHQUI5ejZYSzdESGVaTlpZNmtiRWFoK2NuRzR3SGFRNitnR1hjLy9JVnhiUFdsMUtSCjlQdWNBSW9XUUxkSlpRaGVmNnlkZ3J1SDhGblBOZGUveDk4SzQ3L1FUbXNrMURCa3cvRExxWEVVaW8xMjdKTVcKK0VpVUZZMWdJamJrUmk3bDlZKzlMWkFXTktGOUxJbXlrSUp5eVFLZEIwanllRHd4NnBtKzZxUjJGcTh4S2EwcwpqeEZ5QjJmNXpGVktWRno4UUhONEN6OGtCRU9mdXdFckhKOW9HTnZBL3JPT1dhcnE0TEp6UnQ0RlJFUzEreHFvCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG16a3lBU2c3YklFQ0FmZTVZVm4KYjZFTnZ2Ykp0Y2NBc3FBMmx1ZmFyS2JWNjhkMFNsR1I5VUxqaU5uK0xkVkE3TGtiZE9FTW4xUi9NTmNXYXBURQp6Q3dRSkZHTGNIU3hxMmI0cDBTdVV4WHdzYUpEVmwzdHVHTHpQNitCK2tsdkM3R0QxSmxydnF4YkM2bnZ6TURGCkFlalRidE1vNjBuZkhydWl4akJTVHRCS1pHSG9BTktGSGkvd3o4UDBFVFlsYWVUL25qRkZ3MHc5aEJIbFZuNXoKTDErZ3UwNVp1ak5IdWJBUWxjWElXQWZ5MVovRC9BWFpxOTZyVDY1VXdOVVJIdDUwY05SUTlTZWNlTGJqZnZFQQpLTWQrSUdZRmQ0RlBlNTl6YWN2dEVzQjI0RThMRTc5azllbEZvT0MzTUx0eVh0UGpQZFpmV0U2RVl5dUc1VlhBCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUI3dTQ4S0NmS25CZE1BODBTN2oKVEI4K0JrWkxQOVRNVXNyN1hYSVR4MmZZTWtpQTRjakMyMzMzK1JaaFZ4ajJGbXBSME5HS0crNTVCekRrL0ZxSwo3cmhBVVdjeks3ckNsL2xFakhEMDB2WmVsOHhJOXVLNUVFRkFaK295TWNuZXA0UDRYdmRrT3JDeWFTNDFXQ3A4ClBWNGVwT2lhekJQbDhCdEV1VzJDVjIxSE1HWXFyOXdKaGhSK0xRcnRGUUR1QWtCYVdXNGtZVEVUTUFJOHJjaW8KTFZjMjZ0aU9YQ3BKZVk1Zi8zZjlSZXF5Y0lPTnF6T0RWQmZHUktDSkc4Tm81R2s0a0xXTERSSHJPNVNCR3FWYworMTRPSUZKRGYwUVhvcFBqY1FhOU5rT2RQRUp3T0p1cE52bG1TZ20yaWdUbXdxVkJWd2ViaGhtM1ZXcGpIV3hSCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDMySzAzM2wvcGo4WmlRL3RTcjAKcC9vWDh3a2xSamsyYWx5TnVjR2Z5NVpNenNtTmIzSlpWVmp5aURxMGpjYU4yaEtLWitvR2w1R1dnaEZkNlVragpCK1FpaURjOXZvdDBpcWhKNnFmay85UGlSQVpqOFNPRmVOV0RGTzN1R1pEbkx6bFVKbkN1MXR5TElWMi90ZXIzClpwWHhkTEtIWkhoSGJGTVpxZmE5TC94MkM5OFdYMGhTRWFqTlRvRDBDUnc1dUlGdGU1ajY1SDZwMWNxVmNqODAKd3VvWms4dGZoWkZWQ3FvYjV3WDUwRnJBbXpSallNMTlzRzJFZnVqenFuUm9aeVBOQk9zaWtDdDVZeEkzUnoxQgpHdGFEcHhVUEdqQlpNcnBodllKdFZ2dmJ1elNhQkFVSjF5VE1zN0FnZGFGSXl3UHozQ1MwWkNoUUlIVW1IZEVJCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnBodkorQjMwUjlYRHB6azhXZzQKb25Bcmt5N1YzVmp1dzVRT2ZBL3lqYU9FYzBuNTdVdDgzQnRUbzZ5WnVXOFp0WWhyWUVnT0ttL0ZmMU9nMjhMTgp3cTFYc0ZiYlVpNXRDbjdYSnBOZW5obG8xeUpDTzNkZEQ5U1NBbEdJeTdJZnVqTk1xQmxudHJtMHU5dndLWFpUCkRNRmtZUWlDRkhGMzhPSExFYVA0ZDhlVGlHWmo0WkJSdmd5THpFdmd4b09RQzhLUW5oamlpY2hXVW5WZHdCMmIKcS9PQnJZcW15eUp5b3RYeFJmLzNJdVgzcEd3NzBOSXJwWU5oTUR0TWhCSmpDbGhReEhLc0xrU1dDR1lRcU9uSQplTXlRYVM1S01WajBiYnptenY1M3huL25kUDhnd3djN0hhQVIraVFuTXA5VytqeFE1K2V4TEgrVXhZbEdMNHdRCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDJxUTB2UW9KbmRIelE2ZENIYlYKU2xBZWE2dkFTRkk3NjBwZjlXdmkyRlpvNkRmQVlxNVJKOWFLcHZhL0dmNHU4NUlHdzBOcXNTSmpFdlZCeEJnMAowcFFXRmZTT2NaaWxNaUxYU1A5TWF3UUltak15RnJCOXQ0SXl0cHJZV2xuSC9iRmxDcGhmWllkVXR3azU3dTRXClRETU44T1UyZTlLTGhQaWVMTmVlWWprYWtPYzRJdFhhRElCdDFvbWZEdWhWam44clphdzB3S1Z1RXNDbVhvTkEKVVhTZWVSSE5lWnRCbUFVNzYwWG12dVFPbG56NUtrMG5QUXBHQ2RScy85OTl3WG04VC9XRDFndmR2Z2xTanQyYwovcnk2eityZnVZNFJIdkp3ZXN6UVFBbE4rYmZ0UmF5ZTdjbnRtaUl4Q2tzUUxvdk5PU0Y4YWN1VGsvS1haa1VwCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVJSMjdjMmFwWmxrdFJMN2piYTYKelh1SUEzUHU4T0lDNm8wcDF4UHNRVUVWbTlqNEVxQmhDUkFkOHB4MitCNUNYVjVNTFlxT1BlMkhLbURnZGVDUQpuMnJaQXBDTmlWYzk5VVplZlpGeXpLNm9neThtSlNjMzhKYlpaZ3F2eEFra0hrQXBuVWNSWEZvNFFXMjZYOW4wCi9TRWpZMm1SbXowbUtrYWVybFVjeTNXZnh6MGx3cEd5T0pLNUg1dWZENDc3THZDbXo0c3E0MFE1Uk1CUGVlbXQKUElkeUovQ2JHYzEzTnZleXU3Y2Q1cFBCOElqYVZ5aVhLS3JhNDQ0emZ3Ny9tSTREQ2dTSGpHKzEvbU9kL2NqWQo0MzJyUVIwWUErU1d0aTlpbDZBaVlHUkErVGlvRWI2L1piVTZwTkJMSWEzSk9xdmQ2b0I1K3pYZ1UzVXdiZEQxCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVpwS2x3NnJpSzh5dG5PTXBpLzMKdVJhcDdpODdyaFJyWE9VV3VWTVRqOFJWTEdnbnpQbVA1Yk5JS3RCRVRSMVVwOWRjcVI0NGVKeXFXOVhQN2IyWgpXN2M3K3hzMGI0TjFISWUrSWpzMmJhbVptaFRVeVJ6SW9zVEk4a0RGSEJFUkRhUDhMeGpJZ2EyNHpvZThpOHBFCnVjRE5Gdmdnb094WTR6QkdLL1RhYUtFSjhZTlptSnhHa2RIRisyMjhBdDhCbHJrbEJHblE3NjQ1anIvVFhVaW4KOTBBUEwzbkovcEZZQUJTSjdZK1pZbWxQZzNBK1RjWnFrWTN5c3p4KzFPUnF2OWpOcmdBQ0d5ME1BbTZkekZFaAo4a0JDTUVZZ2pQaWtGWFpqdVVUWkhLSWlOalk2MENCTUlxZzNJdGQrTzVkWlZTTyt0NnlEUyt3WVhSQlJlY1MyCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcksxSStwZndsSENFN0p1bHpCa2YKbTRhR0NXU0p4SXRvQ2RNcnFTOWtvdXZDdi9oRXR1YVpvejBLTTY3UFhSaE5tUTNMcWMzTXlFYTY1VzhqeHZqbQowWGo5aFJLUFdIeUNza05icE9wZnBUNUNXOHpRNW5mU1gzaEt4N085VmhiVjRCdjNhTTNWMlNPV2E1UUJkdGdICnI3ZlNpcHkwamNJZjBLaXkvYi8wMFJoNkpwTG5EMWI0ZW9mMWMrM0IzbWNPbjRQTUhOb1M4dS93VEtsOEozUnIKWWRTTG9YdC9rKzl6cFQ4dzFwTVFubGNKWHRlaXBRVkVjcXVidHFrVmM4eHREaU5iZGo0VzA1QTFLbmZrbG1ySQpUSXJXYUdRR2Jzc0JZV2g0UjlDUzdXcjVRK1QvMCtZU0lBZ1h5TnBDOXZvSmZzVzY0NWtseGhTRzJOOW5FWG85CnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGN3blFoVCs0UW1YMXJHQzVmS1QKT2FBVVlFcjlZcEJCNEZLOGlkSGVsUzBIYm96bndUbHAzVDVzZ1NITFdhM0J2dDIrUmpMcW5nYUMyK2t4emRPUApnelR1eVBuSEUyM092TGx3dFJac3R5cW9MMUZSMHJBanlxWGxIVVc0TlY2bThVc3cxK05iZHlpeStleWR3SFBmCmdLckJhQ2dxN1JMMEtONHpOQW9qbGdKa0d0eTdQV2h4bmc1Mk5nNXZqcHhEcmhyOVZoTGxjM0dkZWZmYnJoSUQKei9XZFJqRE4waXhoalRCVFl3aWtpdkZzQ052c3JJbTFTV0dZeUpyYmpSQmdoTUNNNjhOZjZuM0ZhZDVkM2hrYQpSQXNXbnd0UWp4SlgvZlk4RzRmTkdSek1FTXIzMGZOV2QwVzRCTWphVlJ1RjV5bXBjZjgzMFdEeWREY3d6ejNBCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBazVGTEhTREdWUGVXMlpFTHAzaHcKMGFSeUJDOTdwb1dKYzdHS1dRcG90ZUZBeHVzWVJNOG5tMHVDMUY0ZkNhcHh4ZytyeDBEUndTZzEvdTNqMkgxKwpzdmtkaWNDa3dEQUJXL3FiK0U1aEo2NmhHMVZjYVFjbGhKYWgxam5tYnpaWkFPbWV6Z21jcVptRTU2YWJlU2FuCk1KbHlieGIvK3M4dCtkamNBUldzRjByZXlGSTZTK0xTbHp0M0ZILytXNXhDQjl5dkd3ZmJMb0YyRXFKa2UrMDcKZ0M4RnRaK0Y2V3FwcTAvTjdvWFBwTEhCenJJZGZMN2RtTGExMDJBUi9XS3NjL1V5dzA5UExGM1JhV1dvL3BMNwp6UlNQQmNEQXhJRllhRkV4dyttaXR5OFRoU1VPd2FhTHpBMitqYTAvcnVIQzdzUFpMU2FWNnlJZzlpZWpWSFBJClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeW13N1YyNFhldDFuSFBCb0xiUlgKNkh4Mk9rSHQzRHhZTmJZNTBNVy9uS0xwaGpIRlB3TlNLaVdSTzkyRVJMc3RDT3Fqbm9qN29qSHlXdFJoQ3NaSQo5M3U5a1pqcTcwTlZtSWJxRWlPb2JNVGYwU1d5OEhaTE9nTzBMTm1vVDhDRVpQckY3bk9ldzd6Z0xrWkVsOUJOCll6YllHTnJMcEhpRDRPbFcyZnhZeDRlVUgvcktLR2p2eUR4NE1SMEkydzFsUnJERnNrZFl3Sk4yM1BhNk9QR24KKy9MWFhSeDJmOVdYdG1LU3ZuWHVMUkoxSytTYTQvc05PbHdFTzRSSjN6Y1lWQkRaVTZleUk4RXRLUVVUcnhmeApIdFZBQnpUZVloT2ZxOVBzVHJ0ZWhyYXA0NzRMR0FZOCtzOFpsYUZ2Rnh2Tzd0ZU0zdDFtK3c5WVFiSTU0N0xYCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVBtN2tPZEEyaWVDaWtEM2daaGUKNXRJTnRVRHNVb0dNVmNYcEx1aTY1V3ZITEpac3ZWODhPOTVmdkJKUzBmUzNPQ1krYnQwZnZOSm1saW84NG9NLwpiZFBRWmZjb1lTTFdHcEFXTDFBQzNJZEpzRUxJMGtOL3A0eGhmNmkxMGlZTlhTelE5dUZsTkN4TFNSQThmd0dQCkJMQmtHbndKR0t1NXBaRE5xVzJiNlBRVm5JaDYyb2x2QnlPempjTkhidzM1Sm1xbkZyTVdCRkREbmh0cVl1ZnAKZEdCOWhJMEg5cU9lU082TlRNdDlxYnJHWDdJTmt4ZnA1U0MyaVJ6b2tMRkc4VjlDMnp3aGZlZE40Qk9LSGV6MApyTERnU3ZWZ2owdnkwc3B5VDZ4enFPejdGMy9WMHBIamRtOU9VQTIrZzNXWVlsWGd5VlhZWGVrRjlrNXE4S0doCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWtzU2FHU1dKby9aRkl2RndVU3oKeC8ra0NOSlRqbzNXTVUxcmxMOERudzh5T3lXWlZJMzJJRDNrWjF6ZTRtMXY1dlorQXhKT3o1akxmOEtkNGhLcwppb2crOUt2WnNMRW9GOVdpdUV4cUg2ODRJTDVIQm1BSEpBN1Z1OUVac203dkhHMTY2ZktXdTY2UGJrR3dCNHhtCjR6QmtJR2xtbmplUWhHVndLeDFVMXVhTG5JWC9wSnQ3TFJmM0QwMVZXaGMvUXFueVMvN2dGSGM2c1dHUU9JMXoKcGNjclNBeUo3MXA2TFVEZVYzYmlHTHU2eDBwaUVCWnY4T3VyNVhvWFhVZklwK2wzQnRjWFN2cnFtSWdYUWhWSApwNnBzeG5telNDQU5rckMwSmY3YWEvVnJzOGtPTFZHYlBJOFBZa3lldlVqd2hySHJUN2dEeHpJM25LZWxJNmJVCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenUrem43Z1VnM2owbnpISE5lVXEKSWlnK1BSaXcveFpUS09FM3lwclBqUzU2UmR0S2MwSFBMTVNDZ1V0VmM5NzZRNTduMC9ZZUtXbUpUdDVJWm9NbgpMcWpqeFJ5N2RQUVZnZFI3Z1BnRHI1cEV4clQ5UnczY3l3ME9sbUNIdVdqbVZ5eU4yK3pid1pRejYyZ2pqa1ZPCjZXYUxOaC9SaEtjYzVVd2FGNHEzUWMwM2VXVURBdTZUaTZtS2pEU3JaY1hhOTltY2xQdjRoSURMMllXNnUrcHgKK0tVNHZFVU1CdFJSbTZaVkZKeGJKT1E5andURU9TRk5NRTZpRnNNQXBjVVppeTdrVlhQa1c3WUlYNDN4dzZubQppMW5TRnFYcXZLRVFXZEZEQXhTalF4d1g3TU85QXYrRnlDYkZIWHJMWFlmYUVlNEdDWDBWbUVLUGlrNWJXeUdOCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0hjQ21CWjNsM3FtelZKaXo3aVgKMjBydDBSdTJtL2c3dGJ1ckhDd0g1VlZBYkRqYk5PWE5OK0VmMWxZRU82M3NnWlVPQnl1NERSZHpZNDU0UU51QgpqdVZyYWFNYmNXU3dtalk4NXc0aDhkSGE1T0J3Um5hZmFkSWc3NkZHMEtSalNXYzBTaFoxcVFnY0F3eFpPbE1vCkxGWmVySlpMaDRmSlk3WGRhSVd3Z0hCY2VZSmpTeXAzR2IyQmk4dm1ZOEJFdGxmcW8xS3BtVFJXUzg4TnozbG0KSExvK0JPbWRqanM3NzZNUVJTd21xMFZ2dE1LWkRXemo3dmxZSVoyZ1d4SjRJRzFzZ3dSZk5yWmx5SzNpbkJ1agpwQlFVN3RSejdOUmVSMFJ3T0FyeHNFNStsSERyd3NKWlY0SURPcHIyTjE0dHdrVTFjM3dxMXA3VU16cFZ4dHFBCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2tYU1Y3aGdpK0RwWDhmRDV5MWcKUDZmT0JjbnZjZy9hQmVDZFQrNXFzUjJ6eExrb3FDdGdXaFcrU1BQWEhDYW9admVjcEM3cDBxZkphM3RJbUdiaQplTHMya2xrOGhmTTJ1VHBrK0JycXl6ZnhpQWY0VU83bkRUa3dDNlNUZXczS3VIdzhTSTgvNlcyVEk4ZXR5RlVNClZ1cVhLYnppSHFtbUh1a01GSkt2UkF0QTZIR05WMFlwa2N6TFI4TVRhcGhOMVlKWDlkOW1ENGhCQm8wZzJRN0kKWG42Yms2eGE4bHg1dmg0czJWbTBweG42OG40dVVhdDJvQUVsQ2RqSDBkVGo1U0NEaHJLVzZ1MnFXNWlnTGthKwphdTFzeEhzcnNJeFRvT1MwM2J2ek9hUGRJVlhCTWRHTm4zV3VJK3V4ZmZydUxHNE9NblJNM0ZHamVFQWc2Um1zCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVJFZGE5ZnhLUnFSV3V3R0FhOHIKU3p2aHZYbWIySzdpTldYNzZxY1N5bmlKOEJ5YktZcll1VGdUOFNlcnZxR3Ewa3FZczIwUUE4OW4rNHlmakR1VQoxczZrNGhYTnVJMXltMVN1OXU1QUNKdTVUQ3U2TnRwQTV5MFVRZEZxRU85UEl3bXQ1dThzQjhVcmhrTWI5K0hCCmdHUDBIY2FqYWhZK1Y1bDZUTVpxeG9zY3pSY0lrbzZoNDFRVS9lU0FSZVhyK3Q1ZmYyNVVsM2dQeE9lc0lkZjUKUGJ6Q1pYb1lsR1dPNENEOUhaaVBuV1NkNXBLQ1lTOFgwdjMzNEY0UDlwL2pBMmxXYUc3VXdvblNFbU1oaVB4aApOQTFKTUZxSzE5QnluY2Qrdy9ZRGZVenppdDdHMzhkV21uc2NVRmxKUlFRZ2x2bkxlT3VlYWlPUjg4bzJwQ0hBCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmMrZXpiRXpOWTF2Q1pJZkJBZXcKdHg2ZytMMUpjZ3Z3RnFJaG5tczdFVjJTenJPWGhRWjY5OU1qWEVSOVA5a2ROT2NNdmxyUElFVVNVT01CWGRQQwpJeTd0MERtaVF6RnNxKytBcTdtN0NGWmpqVjFFSSt5cDU1aWl4aFBZdld2OERGcTNnUTA4VUVKYkZtbittZi9RCldQQVMyZlRhcmVkbld1THJmTUpTUFFTSkMxbnd0TkNGOFRHamFGekZ1RkluL0RjZGlibEw2YTJtRk5WaWNHNjUKYXRRVGduUUNIdFlWNzRPRWZwTjJFUG05aDVxZXZ3ZlVuVXFyK09MbG5EQkxQb0QwYWtHV2NabDhrWnFQeVh3UAplNWZXU2ZzVUZPVUJnVHppenFhSWhZeDREVmtKOUNwUHk3Q3dDeWlMSzV5YnBuS0Y0aGdUSWIwbGY2eXFPbktjCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDRUdkpPSDh3UEVHQ1kyU2JZODcKeWtON3VCSS9jb1VrUnBJSjBPMHdESWJDSGwwNHIvOW9HOGh2Y3ljYnNQTS9VUkZqWnRQM0JaN0xTUTVGWGorMwpvR0ZXTkw1L0NnemxRZDVQcTlkMEVaREFJYy9wNXo3RGtVanNXWjRVN2xVemVwUk83YXREQ1hqQktoL1NkU3Q1ClMzQWZsdVAvNWtxZERrNWc5YU92U2pFT3FFUktFSWVuNE95R29yOUVKMlJienVDK3V3UFpQeTF4MVlrSzV4dU4KbHBCbnd5UXlFVFlVYUh4cXJxTCt4K3NZNUtValFDajVxa29kS3lNamlwcGk2ZjV6eWdaSzRweUNtaDZqSDl2UQpHdTg5b2V4RXpUTVpRbWx6N3Ardzl1NzV4Mzg0MmlsQmtEY0hSaTVCZnFsTHBEUE5uNFpSclc1eGIwRVh6MWpaCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWNzMCs2ajB1YjhRWW8wQXlPWTUKUUd2cVBkUWhjUTFnM2hIRWN3dm5tdG5TcWxTSUpmWTd6RkljYlQydklmLzZObGF5QXoyR290eExQTncwWVorLwpHdXRGTXZ5d1A0TDZRRnRad3E3aVAybitnMXR5V05YZC85eHA3VE1aTWdUUDNuOXpPamVWVWZ6b1MxQnJkUC9BClM4eVE4MmsxejNtdTVaWjRjWVRrdndLSnFwRkVHS2lIeEtaV28vbnVQM3ZrS1JweXFsVnQ5cnUxTmRBZG9jZjIKTmVZekttaWIyVmxjTEp6elEvM2hUc1lNdmVpZk50R3BRNEh2b0pNQWowT0p6UittL2p6emQvMjZmdEEwS2VrNwprYSsvL0dVSXcxVGdjcmkyRytrOWlVRXN1eVlhQmIvOWIvMm9waXZza3ZKUVVSUHhNMW1QYndCV3hJcGkvMDZTCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk5UdTlYMk4yUTNlTXZtUkN2eHEKa05TSFJmak51M0htTW5TbzlNczdPQ0FTLzhRR0F3aTF4NWhaL0RYMkdqb3ZJNjF3VWJ1ZWRUTWYydjIxOXljWQpuTnoxcjVOdEN4RTg0ZW15TVNweUhBY3VOOXZLSTIvaVhMT2NTQ0hwUmo0bUV0MmpoeGsyTW45Umo1OVE0T1NxCnNHTmNvUHhDaVl1OEpjamY3WVlMbjdvMncrb1U4dG1ReGFmUWppcXZOOStVdGxVcDkvd3cwVHhEOTR5R1kwb1MKTnhKR3Y3VlBpNGtzTW9zaWlLS05TWjRnY3RhWlBjYXJlSXNYdDN3dE1jaHlKZll0eTZwemVsWHNSMThvVGhPbwp5SEtHVUdxY1dPMTZrOE80KzJFUXcvTmZmMm9aM3dZaENoU2xsd2dPSE1EYlpYSVRaVDFDUFlmV3p6VlY1QUg1CnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0toRUJFNjFMNmY5azIvSElubHcKVUZkanc1M2w0U0M5ditkeVBibXB4WlVJMTA1alFFS0VhUVdlUm9GTkYzZGZRcThMV3c0UDVtWmw4MHBEa1hhSApMOFRiZEdzNjAyU1JxY1FoQjZHdUZOUTQ3YlVBQzNXYk5NWG05cGhLVVNrdFVkMW9ndHFEdlY2K1hVMWUzWE1wClRiVlVxWWFkRk9wendVRVY5b09oekpTMGtiTllNdk1kOENTeTUvcnlFM3NpVlFEYk9rbHg0ajRHVkcvZEdVbnkKZkl0WXZuNWFhbkZtQjVZSVlmZHdLMHpOVEVJSEZQY3hLcVpSQmgrbG5FRFhJUm1BQUYwOWc5ekt5SGhrUG9XMQpHWGZhK3RJZzc5MDBOYVNndmx6UHVLdEZzZjZkVHN3YUZoc2R5RFZoQ2NvdHNIeXI0NS9EcHFEa2JhaWQwSG9RCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3RPNWFlSzNyMUozamh0dVpSbjIKTUtQRGY4SVNiQ044YVRPZnJySkhIQ1NFMnVJdHhXY0VhNWVmTHZIUzdicnAvYTdiaUQwNHZLSURSQTRRNENGRQozVWgzOGhld2c2WTg3aEkyUmtqL0RZVmE5S3RzWjBXNDdJQ3dMNHA4MU1RaGovVDd2eVJKMEFudXlrSTJYK3NsClBFdmI4dmV1a21RNWZRaFYxZkplRmNWQVY3TzZaV1BSK0hiWlpVWGg2Ly9IVHEySkM2Q3dTNXpxTjNENWNVelAKa0ljSDF5VlNlT2NNWU1CVlB5a1lpa0dXODNkZEI2SDVnYjU3dXpFR2hHQnBwTUdJMUlETHN2S1J0ajZvWVZIYQpGQlZyVW9ZMWttMFlObjBQVm1HeTZYSGlrSHZyZkF3SSt1YkI5TFhZbThnZnZNbU4yVDkxbUhzak5xYTZFYnlOCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzV6RkJaL3NJdkhOTHl1YXRKdXcKOEkwTlV6dUFsT2FUVkl5UlhYL2dhZWVQNkZaMFhYcUtvN0pjYXVsOWNtMURKUUxlWGV4ZWRMNGYySSttVlZOTQp0cStFaGtienpNSFhlbGpOZFlVb2N6L3lWM3hTRVF1Um1jU1Y2OHpIV3d3YnJhL0RONXdxdHRjVGFFRnZLcWE1CkhYQmJtRUhzSW1LbS9WWUlvSlltUktwU2VnNjRMT0h0aENJNUVlTXFzTG4rREdpZVB2QVJUL0ZhcC8vakU2U08KS0MvLzhCQi9SYUUxOHh0WU40SzBLRXdZWGFOb1VTNElhWHlmZEkxUGNiaUdiRjVmNzBuV01vNFBZeWNpdStJSQo5R1ZzLytjK3F6amJGKytmQTJXMEhpY0xmYTY1WEViYWNDUzBrSis4U3JVazBLSDRwTjBveEV3VlAybGc4UXlUCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUg3djhpM1U4QVlqNGNnbmk0dm8KeHR2ZkNHWWN0MExvdVZnNjBwdUtjZnUvMVJtVlcvazMzYkJBZzIyWlRXZXE0SlJ1aWxGZ09mUlJMaVJYUXVXegoydEp1LzlTeExRUGJvaG5VYU9WbjJuMkJwVTdWU0ZTbjZQVnBxbHp0VjQvOG9Ec1NYRjUvQ2t0ZWhNeW80MEJlClhNd2w5bXVVbDNFcEoraXhCYjdXNFdPWHhPRVE3RWErWVV0NTBlTW5PQmVhZ3E2aWlUaVJwUnBiMTlZTVdIZ1oKZXlFZ2NISStPdTN5SW1LeVQrWUJQNTNhNytQTW5tcjRlT2NTRjNqQ1hMWWttNnpabUwyS0MrQlhoeUVTeThUUQpYbjJkeHlrQ1Zwd0cvMGVDTjBES3d5Kzk1QnhEMzBscXhCUlpJQWFYNEtqL1RYNFpPK3dHZFI4UTJGQlBSMXZpCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDcxeHRzZ3NOK2VHZnpHT0g1QzkKaitQRmpLdzd1T1VzRHc2aFQ5Z1VKWTZvSnp5dFQvanUvbGFPMmtTS0VrQlVaRWZWVitEMzJEZUNMTUNvU3lmVgpDMnJheUpUT3NvK1pINXhIdTFaaDMwSFlGUDU1QWt0SFZjcjRXRjd5ZjlXaTk0WThtaHJ6Zm9DbGRkaVNjZ1ZvCjFiV050OUIrZlRWakhheVhTTjBYOTU0dFl4emRvb1hBWG54UmJ0UTR6NTJONmJJeTZuUldNNUNEZFI4WU9DdWEKTVc3dDJrbFhQcGtodGZqcDVEcWcya1owYk9EUHRPZDBDSWw0YW51c3VWSE1RRlEwWlN1YVFtQ3laaTNrQ25kbgpDOHNTZXRodGJ6MU96aFd3QXkyTDZwVEF4dVBXRVhkaTNPNjhIWjVnTjZoUUdpK2dvUDBJQTR6cVBrZHZNL25MCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd211VVdRc3ZOU0d3Ui8wbGs2dTAKWWNicktWbWlHak5nL0dLaCtKVjhtU2JiQWt0Rk1UUWJ6WWp3QTU4b1VrN2lxNkdrS2luR0c2L21KMEhtV0dsMgp4eklDRWFLMDZFU1AyakQxMlNHb0pLMW8vNWkwY0VWVWdzQ2Y3ODBoMjlYS0E2TDdFMG1HSFlsM3B5cDZ4SGxMCjA2VHJLSFBORGx4Z3l5bVdZUjUvUFcrYnZKdU1remtOeXp4NVdIMm1QTUU5TEM0dHZRZE5qdmdidEZmaW5LRmoKWDBtb0UxVHEzOENEUUlsOUFSTzdzc2FackJES0FBeW1qMk5RblVCR2E4d2k0SWlLdkNNMVJ2Q3I2cURqNlYrMgovMTVrTytkMlFhakFJK2NodEJWNTZzVDRxUmdQM1pGRkk3dy9rcUZDRFJqLzFGNDZhbVMwdUI4anVhVHhNbDdnCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0F2VXUyZ28yY0s2ZWdPa1NFNUgKMHE0bFZCVDAzSzZuRnJ2Q0tDZWNheHBMSGNaa21sd1A5b1ppYlFQOEJGSUk4YTBwbXNJTGIyN0t5QUVPL1BqUgo3dE5jNGNzTVRvNENQdFR6Nk5SSnZab25rd3IwdlJ4N3JaQXVEblV3bDBMNXByaEkvK3gzN29kQVFCVm9ORVl1ClN2Vm9LUmZzY2JQaCtEOC9zVnR3L0JxNStuUTdwV1lRUlZIRE1mS3FoWmZtU0VpZ0haeVlESVpaazBhazA5Z0UKMXdpVU5UTGx0eW5EUnhiODlFZThCUWo0aU8xaEhlcVBWeWlucFdEY3I1UHROSUorZDU4RkRQb29VZG9uS0ZjdQoxMFZUQnhoNUpBSThrZDhndWx0Q2puVUplYWxvdGRRN3FIaTgvMUdDVVViR3QzL3NPdTJJYTZhZzcxbzJxcXlECnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjB4dVpNRnRyM2tNOUR1Z1pVN2MKTkpkODd4cEdqQ3Y3QzA4MWhQck84WjlFYnVhNGltK1NtZG10a0UxMlBtSkxub1pGZllmT3lTZUdSaldEWUg5WQpoV053aEhxaU5JdFN4Y09jcW1SVEpHSnQwbGpYSW9VMU9RNjJ1a1BrUlFPL3JiNWpRR2ttSy9GdVdmTW9nWjBuClFnTmhRMG1LUE5VTWlZa2p1dkF6MGRtMUtHUEN4cWM3UXVBMGRCRnQrMUZ4S2dRL0wwL0U1R2N6UUFiamZ5eFcKL1BqWTcxSnNWVHFDS2tkNWhrZi9sdUQ0Um5lK0RmQ3M3NitsWkIwcVV1MGJwZCtteFgvNWF4bExKNUJVZFo1QQpEblgxYmNWaXErVmRML09peHRneWh0R3U5MDd0MlJuUUROTWFDaloxMFQzclBpb2R4SU9RZ3phelA4QnhNS2tZCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1YxMXlkeVo4WmVlNHMwQlJaN0oKZW1ETUtiWGp5TUdnQXBrZmxOL3VVSzBzU2lIakliVlhYN29WSWxGVndjUU1iT1FFeUhmOE9DWVQ5ZGw2WHRYNgpuakE1a0JjMllCRHRCWElrZXZZdXQ2aGlkL1QvUlRJS3g2QU9sK0RwNncvaXNBRWZ0dEF2UFdtRVFGSHhEUzUwCm1wWWpsdUpDclBTbTRERkt6US81U3hCVWIrNlQwV0grZDY0Ny9aU3ZFSjN0SnZBb3RUSTF2bUVqU0xpMG0rVU0Kd2x3NVNqOW12Si9xQkRWL21CZ0VCUmd0NFZ3TW9MdXdTS0ZjbWZoa3Zna2VHeHZtaEtwelBhU3R0TzhkSU5mZgpsS1d4a2NiSkJ0cHNQOGEwbVRxMDFwaG9QSGlIR01RQnJZRWprMFdxc3Nkd2o2MmYxRmVOaGxYbURCa1VOU1BVCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzRqd2VvNFkzcm45ZFI1ZVZMNWUKNzFkOGZMSVVoRlplSEZZRjBHVVZyVGc1UEYyTlRBSXNoNzVJZWZuOVFjUzI4ZFY3dnZkSG5aR1JXYjYxQUJmYQpGWXYxNFFRNVhVVnNtTUFncjBpZWd5SmlLZ2pIaFJoc1RPR25jc2ZGNWcrVFJ2c3dZZjVaUW01TVFFc2FXek9ZClNzdHdjblR0Z3lQMEpVMDhPazdBakFnSjcwdEJNZHF2N0xJOE1MY2VyQk1ta3lkb1V1eUl5YVFtUUZJRUZleTAKMUhlVWRzWFJXaHkveUx4UTdkSkdXVUcwRzIzUFBWdG1Td3J3YUJSMlZQSzRVTUNHWUpTV3VtK3V1QmhuZXJkRApjRlAreThnZk1HRmxiRGtJSW0xZmdxMTNVVHluMDBlWmVEVFJSMm43ejh4enhrRTZJSGpjWTBtQ3M1UDljcHFQClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlRYUC9KUnJlT2ZYMVhaZVBFZTEKanMwb0FqZlVCd1hkK1ZsMU5hU3NkMW1udGExM0NwaW42K3FYWmxCc1pYcU0rRXViYWkzT3JwV3ZJVU1mdzY2bApBYVNjci9tWHFtMkF6bkZpV1RJTTFXa2lOS2ZQT0dPUm1QNDdWNlhIYmFHUXE5N29sRFpDSThqTXdRaFd2cEtCCnQ1bmJkZDJtMTBMNWQ0OStIMTM2TE9rRVNMb052YXVoaGRCdUNwcHAvU0plUkxReEF1ZDhwWEhGQ2pwTEVhbW4KL2VEYU53UUJIbmtsbGhHYVdBM0lBQWd3RGlhYlpsL2N3OXdwVXdWQ0tYNkNLYUhhRVh1N3ZDdnl3UlVzeWRWZAp2SGRLUjl2QU44TTZPY2FscnlMREJhUWtoM2FoaHBVY1Y4ZldRUzNTUzRDY3FCOHZjNGdxbWJNR1hSZFMwK2ZFCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1RUNC84K0NhTWNpT3VRL202UlgKUE1nR3RINldjUU5OV2xzOVJyOUlOclhFYmNwSCtxdktxZ0VPdmtRT0tQbWRqa202RnVJMnZWNzFkeTV6cDErOQpYMEZYUTBjWjl3OU92a204ZXRQTHhIM041MWJiVElpSkJYWnVsWTdaUzZxK0RFbVYvOWNXaHB4NmYxUkxsVEtMCjhwQ3VzRmpIMENZYUI2TVdDQkZsakQvVEJiL1g3dmt5VHFDYjl4KzIwZW5zblhkQ0xvL3VyQldWUW5GQ2c5OHIKUDRST3BFRkJYNlU0RDBwbUhkLzhoNnNtcGF0eDNJTWpWM0c2NGJqdmx0c2M2alNoS0dQK1pvbjRjOVMwK0FDcgpKbEJxemtNcllrMmxNWGFDaDQzSTM3NGR5Qk4wZnJVa3Boanp5VnFiaGNscEFpV0FnUmxpeXcwZW95N29xVStOClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcCs0UFJyM0plVXpMV296RUpiMVkKenRrOElXUUNmRGlRbmhSVjRnUlZmdE1IK2pXWmM5dk1DeGx4SGV0U1NLbTF2Q0xsekZERVZIY1BrZUF1Z3VqbwpyOXo4MzYrejFSZXBiSDZ0cmI0ODBTeW5mRmNnR1grdEEyKzRhNkpWdi9PRXRaRnM3R3Z4eGpjNkVpbkQ1ODRiCkxLeDFvaHFHdWE4cE9NbWxlTnFkNzN3WjdWRk16WnhrcFVSdGlicUFMRk1FZWN2VEd1SWc2d0Faaldsam9aN0sKUVlXYUVCQmt5UXZic1pJNDlQUlJzUkFLT2xSa21nS1JWeEhGR0d2V0tCNTBrenpzNHJPUTlHU21FWXlsVVlPNQpmMUkyMm5hanIya0NCSlFpMW1NdlFOOGg0b3BYLzg3eU85UzdkTlR5TVRyRm1KOVdQUUxCNmZ1eWdlRWVrL0FUCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTF6bDdxemU0QjBDV3V6ZGY2R1cKWVc0aXZ2M1JMRy95S1BmT1NMeG8vV0hMKzdlTjJvdFE4WCs4MjZlbUpkY2IyejJ4Y0ZZNHBOUG9qR0t3ZVRQaApVUUJIdEMrUTN0OVhjQlJHTHVkTExwV1hxWFNBcHpUZkpZVFdHN2htMGR0dEVwbUVSZGlxZURRRzdncHdEdVl0CmFucC9BYWRjVXVDcGJ4a2hjNGZCL3pqRmFBYmxQaExnMnpnMVNtdG1DTFhad0poUWlZVHgyYnhMdEtkeE9xQ1QKS2l6b2pEa0RWbjI3bUxiTm5kOUpuYVR1SmRDdG5WelhENVRNSE5yYXFCUkRqQWx1WjRxOEhibWJRd2U5RzlEdwpSUUdidTBkcHhXYUJnSmIrbUZYMVVaazNTMGZhY0RselFzUWxNYWxvV0w4RTBRVUtaemJvM3FyY1YwdlM4QmR0Cml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNW5qRHhLTDZEWTlPZm5FUzJSU1UKUHNHS05JOTNTaWQ5WWpCVWNXdlhmM25vOGFnMGJnc1dyQ3duZm1rRFV5aTVJemF0YjJQZXFKMVYwbERsSFFKMApUS091R0xLbU9BckRpU1YyRTQveDhrQVRCbms1V0hyb1JJRGlhMHJEVS9IM1BaZXMxMUIrNDNnNXNQaTdCYTl6Clc3TWoxaFlLbUxabW01ejRvQmd4a04yOG5vYXlXY0hWYmFTTGt6ZHUxdUk4YWpEYWJjbkNOWXlQZzJqSWFWRXUKcG5haUtIYjJpTExMT2lvdHVUWW8ydEl5eGtjTHBsa01rSDRvbkR4Z1RzaHBTMUx0OHEwblJQRE1zU3JKdE9XTgp0RURwWlF2N0tXU1Y4MWpqVldwemlKVHN2eThyaU12TjNIa0R1SjU2QnRNTG42WWdPRHRnaWRTQ3FnQWVGZ29hClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG9helBCUkg2TXZscXBoaEpxelMKamFlOTBQM09WblNuSnNMTWRCSUphcVg4TmYyaHZVdVpwaW1CdUZaRG00Z2hlQUJmSS9IbHdwT1FBZUd6TkFLRwpBTjZkM0pTaFd4aW5TbDlPSzN4M2lRT2VEREIxU2IwOG5qYXNjd3dwWm4yUUlSd0tibDR5ZG1HMmx2eW9TNENhCjhXTUdpT2RUTnlWWmU2NHpaL2Y3WCtWS2tGYVY4S0JGNXpYRXdhbG9RNzdwVFo5VDdhZnZXWmNVWXk3WXhvT04KMTFIcGRtYmpMYVRwOHVVZTlncllSOHV4R3JKVW5OcldBRWc3elBEMkVZQUxrY3d0RGpoNVVaVk1oUHVJTGtKZQowWDFjejR6cHM3cFpkVTQ3dkQ3L1NhMkR1ZFc2Q0EvRkJUUFdMcENKNDd0RCt3M0ExZ1Fwa2NKejVPUG1qeVFnCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdncrcTdZYXcyd0pFRnM5Q3VSRDcKQzdCclAvOFk4ck5DR0F0OExjU2J1NGFCUzFtQVBPa01ERWlkRDRzc3VmOHEzMDBrakZZSUVUYmtSM09oRkhOVgppdFQxRWIyZ1U4bytGMzhBK0RURC8rZVdwZmlKcFhwWW1KZFpyUC9yVlNJWHRINjdENFdUK01vL2JEc1ZmUDY5Ck82YU1mMTlENlV4aStnWFRERDNybHZ3OVlrVWNvYk1DZGFTNkJoSVMwbCtGUUN1MDIvZEpManBTRldFTjVYNXEKOWxzR1U0V29NNStLdyt2V0IrclFUbzRKRWwvZm1jOWx2c2V4RnEzeDFPM2I3ZVBVc1VoTUErZWNhdE1DcUZ4RgpwV3U5bFBGc21iam1HRW9tMzZzQXYzcFRvb3NUeHpHL1Q2YjhlVUhabnZBNkhaRUMwd2JHYjlnek4zWDVqMTIzClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDlnOVo0WXo0VDU2UkpUSzZ4T1oKd2JTS2dsVFhzNVF4OTZBcXlJYUk5YnFiODNlOGxKRS8rT2gxUzZ3eXIzVTVvbmovNXNVbEdKeDhIbjlUWmp5ZQoxMUxZZWhRaVVJN0p1b0xYYVMvcCtIRVJBcEhWNG8rcFBNVWtPMlM4a0p0OWxaWlFkTmp5SVVhYnNvUzY2WUZtCkEzZC9ZcmNIK0JUcDV4VUd2MDZ2NlhFQmhJNVZsQ2l3VFdhVkE2UUg5Qkg1SXJqazgraUp2M3kyUTVWNFk1MkgKZHE4a2hIVFFMbk0wemR5b2VsMWRVY3FQMnV2TDFncmlkcDNOMDB2a1JGSy9YVFNscWhHN3c4dkxoeElvbmE5UwpoaUhDV0RDamxuOTBlczNlVGlYY2YrblFiT0dyT3ZJZFo4eWRER0dSV3lRRGJsa05IZ0duVnM5ZllnSDl1REFUCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVJBRWIxMnBZZTU5U0pkZnpWV20KTXpJY0NrSEJMMjdsaXB1N05LeUNKS3pseFYyUnNMQXFrQlNKSEhkdTkrUU55UWYyak5LTlFkcjg2Qlg4VGJDVApCNDdKRms3QmxXYjhZbitvTWxQY2VocTI3ZWJWVWRXTTVka0hYQndSaVppeFp1dHR3T1FZR092OC90RXJQdVM1Ckk4MU9PbHUvUXF5cjY4dDZBcEUyUWIzMGFwZFhDbFduSGRtM1lQOUhLYWxOSXV4aGRlc1pzLzRKMk55aVo0RFQKL0JheXVRWkh3OENkNGxsMCt3bmwrMEw1NU5ubGhDNk84Yy9Ubi9sMjhqbG5ybTMxOTdRZjQ2K1lRalV4L3RuUQpIbFQzcWltZE8vblBJQmxjMHl1b2NRRHQrWFlXTEIzYVc0dHFtdkprV1ovdEdCbGNBcENNeHBuelorN2VnVU1NClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenh5WGlsT2ZBVDIyRXdVNHZXZHAKaWZkVmpYN2xPZkZMblNpallrQ0MyZXU3YUxEQ1VDMTFrN1lqQ0x0VnBQVGs0eWFPdVlFMVlrWjdUSkFnTnZaWAphYjRoSHlXN1U0OW5RNG1uS01xRnZaNDAzY092S2VQUmhFN0JnYUtCZE1zTWFXbnJxQWhXMUhIOFNwUlA1dEZpCjFNbWZDbTY0bDZ3VVVlVEdxbE1QMGx0MWw3WVhOcGFGdGpjcnR1NVg1cmRjZGlMd2ttemxkbFJJSUM1NWNJNEQKOE9WUGUvQmxEUTFVZUxTSjBHM3NUV3NSSkFtVERhMUR5S0VIVnFzdTl4Mmp1dkNpVVNmZlNNbkVPb2g1NDRtMgpUbDJZeFZ6L2ZUQmpod1VvSG9MdkhVUHBtZTVMMzNRNllUQlBDVlp2TmtGbDRCdlpKdWZIV2kxb003S0ZwZWhKCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVZ1RW5adkwzWWxyNzMrS0lPeWIKQ2dxN3k1S0xKMkVBaUxWSWdHS2JYMm5jeExTRDVwcG1ob3EzMXljK0hrdHdQNDUvcWxza0dTNE04ZlhSeTN3WQo4aGw0ckxEZzRpYUVkWDVUeHU4eHZFeGI2eTJlMmFpYnhaRUZUV0pOV1pKWHozdlhLS1czN0ZLTm5zR0dTSEFYClhEMlhXbjhrVjhGellpZ0tCZUNOU1lPS21mcHJ4bW5Nd1lUQldEbm13UXppMEs2K3lvRFVTN1pnb2E1QnBudVAKRzhpOEgwRS9jZnpqOXhRdjdaTG5tZXBsOVllNUxYbjMzUERoU0lOT1pWU0hTTTNmMlZ3bWJpWnk0MU9nTWpsTgo2RVdFM25iNmlJSXJPaHJTcUFhdFh6TVhub2s3NUh3TS91dm15TzYvUXdINHhkYTljam5yVlFLQnpiZjRsWWNDCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFhxQW9yYldxNlpPWldiOUhzQ0sKZVZWaGp0UTFIRTVWWmw3bDhKazZUVnNzUDF1YXRsOTFUL2UwYWJHQ0tqK2ZOR2JEc1hJdTZUNEhGdzBISDJlRwpvUndSMEJ0M0ZuODdjbFUzTEN0MW1FcHZTbnJYTC84eTZHc0s1Q3ArY0JQMC9YQzloSTY1cUsrRWYrTTRERDdXCjkyeEZNaU1ldjJERWl4Zm4zQ05mTzVERjMyK3RLclMxWnRqdUFhVGFPL2hYR3ZpMXFVOFhOejZMN2FaYnFjNmgKamRocVF6bWxEZ0ptcmJxM2UraXZzMnRZdG93aGVQQXRTeDhhVkRZdThVc3dscG9UaU5jOGd0UlNSa1hjTnJ5bwpWTUhXZWVIU1UySklHVWdwNGhFSGQzNjBWb0FvR1dEYy83NnV4eTB1TzJRMEFRaGlWNFBDUUNMaVVsL0JJUzByCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFhaUHcxN01VMVBiZ2h4SFVpcmYKcldTQXNmMVcvM1dVOGJrd2V1a3BiMDZzanlXUkx2eUNCYlYrZG8xNytQQ0xCeEsvdjBtQ0JTTU5iMVdycXgrZQpWbkNtRHlPenl6d3FIRHF5OVRnNmZ1WWlvV3NHY2I5cGhINTZHRjd4RzA4K2RsRHdSTFFrU0ZsNmhubW5XVUJBCnhDQVJ4QndGTXNVaDZqQUxGMEphU3FobkVLT2EvVGduQWhGcGxkLzdEeWlsczFxM0ZrcGdwUFRtdWhrV2VFMmMKK2kxRnhWYXp5RU1BWXUwZUxOdWE3cUFNR0orNTh1V2ZTS0RmbmdJc2FnYUNLUXRDK0p3ajFBNzdON1ZPWmhXeQp3ZVY4QUtWWmoxLy95VEJpUGRkaHlTaFRVQ3VMSGtjL1FvQW5XemlEK2NZZWE0ZlVack5McGNWckhTZkFvbEpPCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczNXdFJxL091M3I4K095RkJUeUQKdk12cnIvb0M1MHdpTVJJT01ZYjBtZUZvL3NrOVgzRkM1bE1aNEZhTzZGWjl5Znc1SkVhR0ZEY0RVQ2xnTlV6WApEQWxuUFlxU3BoVEdjT0duZm16T09XMVNna3pEeFhVMzM3TExUSy9uOFVzYkJVaklvYW54Snk0eFJ3bzFiYVQ2CmZiQWRIdDlQb3Y1dWtuVnE3ZHVEVDZ2eGUzMGtLQVRxSEx1SlljcGxGZkNVVE9QSEJNeVRHYVJzem95VW55eDkKeXRFb3lPWjNacjRQU2YrRTdsZ1lTdVZRUDJsa3hSQU04dWJJK0FvVW5pTHBudGtLR2g1aEl2dlgvazc3bFhWagp6Z0hsYXlRaXlEZm1PR0c0ZXlEa3Y2cXNWd2NnNDJyVlVwTExzcytaMFNMRlpoRXAzbnNoWkxKakplbGI0TVdtCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGk5aGdGVW9DOVNXNmFLUVJuWEUKdllkOUJRVmxBRUsxY2w2REpQVTlzdm9GVUUxYVVvYVZTaXp6NHdyWHM0NkdiQUIyNDIySUhTVE5MaGVHTHNoVQprSHlNMGtOazlxT3h1MzY5NGFFVSswVWZlUkxBOU1ENUxXOUtSbXRLRHhRRFp3Z091R3lGZ1FnalNDRzR2ZVcrCkQrYXNadmROSE51Qy8yZVFqVXNuenNaRU1yQTExb1g2T3pMRWQ0UDhCWGh3TTUrazJwd0RoSzZDN0JOR1ZaTjIKb3M3K0x2Nkd6MGg3OXRIMHFpVGFkZko1NnZ3Q3dnd1lRZGd0dUZ3cVp4T05OTmp5bnpxb2dMTlJ6blJmNHczNQpTazVjLzdrbnMzaVdFLzQzZ1dBOUl3dEswZGxDekd3ZC9xZm5zT3ZpSXlrbFZ5dk44RTBmQ1hpVUJ0N2JrUlRKCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXZ6UFpPOXNldEpZdE91NWFkcmYKNE5wWEJHdkFjVmtTNS96Q3UxV2ZIWnJyOWV3MEM4dGxZWlduN29iTVZPYVI3dkJKSTZURXFZS0FqQmxLVThNRQpJeUx5ZjgramFtQUU5RURQdERieFdSV0c2L1MyZVlpOUdISCtINXJmckRCZ2lrWld6TllYRS9rdWY5OEhtaDZKCnFHc1NrQzFta2t4bWR3cGdraWlLQ05VYmUrdVN6TjhWT3k4Uk9BaCt5RjR0bEJmellKenU2SC9lbVNmTkFSTlAKTjVSSlkybXF5Slhzb1pXOVNOYTVGQ3lrOFlidTI0ZmxYZ1NSMGE4Rm9XRkNKMS9sTWVxK3JiV3J1UCtFSndOdwpMMG1qSzVpZDk0TmI3cEFoREZPQ2Jxa21uMlVlV2lpOWNHdFBRRk90bkJpN3lwa1NKQ284K1BhY0w1TWdiWm9PCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNktyRzcwQWJKWENkSWw0M2Mrd1cKZzA3VHFkc3dvRnZ4Ykh2Q0JxOFJ5VEhBdE85bDg2TDJza05DZ3VPU20wWHdUbkgrOVlFbFN1Z0NRb3NMTUduSQp3UmRPYjR1SU1GWVBob0lybzUrRFNCNGN4dWNFckgyRVFjTkdjVDJXcEw2dVNoei95bm4zelUyd2tsOVd4WWswCno1bVFDajI4d202cFZ6b0RUbUZudGYrK0kxUlEvRjNMbWVzTVBTcDN6NUtYbjJVS2RKL2tVbEhFanZiMi9kbnYKNzZBcXh5eVExNCtqTVQ5UWs5NjFtRitHbTlzMlRNVjdEWVJFOVVkcGFobklRaGhiMDhKeWNsNU9yS0l3YjlLcQpQeWFkeFZGRGpqUmhUdjZNdG9nSHdnQ0EycGJqbkJ1b25PRHQ3S2k3M1hwcktSaEt0aXVvQW1TZHhFR3ZaaUNhCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemVDb1djTWQ1M1RmTU1VTXJEK1QKblUyQUFBQm8wRWlzRHk0WVo3T01ES2dQRllLRGNhNjF0ZVUzNjJTQ2prbmMrM2VRRHg1aVluUTFLSVJFN3RYLwpFMjVNaDNnWHRnc3p4VVFIZCt3S2hCMG1vMUpWalZudHp2RVdIaCtvUDZwbis4d2RocEVKZXdLZlZOdU8yWWZCClRiYytiZm4xcXhyTCtmTmJNZEN3MHdBbFowbTNsb3B1UlZJQVlXeWtZbnArOStBZVpJVndTS213a1Z6YlYvTHAKZHR5RXYxaVVYdnVFY0VuMFRnYlFxU3poRkM3UktOWGZKZEpkVVFRRVgxK21RWlh0bzhIZEdIS1d6bVN2cDFWWgoxQ3RqUHBVdUhiT20yOStNLzFuTlh6OXFsK0J4THZyUDNlSzFhZHBWaTVpSUVGWVU2SzJIQ3dReDczdStpVzhuCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmxQYzhxN3BzaTBMb2xjMmkxMkUKNGxrNUlHYjUrbXZiNG5sUExWRFpBbCtFYTkwOVNQTExsb3RWdEYwek9RazR6K2VWSlZaYk15b2RoMThaTVVwLwo4STlFUXZjQWNFbVEvOW10Y2Nzb0UxaWt5YnFQSWtJZHZSaThKcGswc0VYQzdNVEE4dk1qakFqRVhBS3NHdjNLCm9NUDN1cTgxZ0hpZTRIeTc3aEw5TitSYjNKMmhwQWRrY3VrOUdXOWRjc2ZWVWY4R3ZacHFBbFJkc3BNR3VDQ2oKTy9JVEVXMExGOFJTZG5lbGxrRTY3VU1lWDg4R0ZxdUNIajZiZjNHRFdvTTRnc01nbXZ3VlRKdWk5cEpxWE5lSgpXV2Uvc21uUzk1QUFldTQ3ZmNNVHJxOGtuZlZUc1luM2J4ZzhYZmpXWVpwQUtGeExyVU9zRXBtV2hvQnNFQktlCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXNFWXFTa2EyRFRYdDdIL3RsTzQKblZCL3QwekZmUjdFNWZ1cTZ5V05WUGZOQWJFc0dBTTZ0azZzdzBTRjlJYVhjaUdSNFBGTnFSMVNnUlZiVVVSLwoxb0MyKzUvZ1FnY1gzayswUUFMUk1GTUxMV2RPSW5rajlPSmJWRTlxM0JKWURmSjdlaDBuT1ZPUnExSlpMM3VVCnVGQXFIcWNVZkxoWGZLbURwVG5HNmhpaXBpOFZqbGxjaWV4bDRmZDVLR3hmK2pVSzdXVXo2Q1d2Szg1aDVlM2YKMDZ3MzlmanlnYURjR1RhZFVLUjg0MzEzTTVFY3I5VTU3Y2Fudzl1Um55akJqelYzTjZVTHg5ZjdXaWVZdXNHcgpQa01ucjVQZHJBVmpRV2srRnA4dmI5cXlqclFTcWJrV0xDUm5TODVLQ3N1OHRMc3pFcVpNVmpJSjRaODdDRUFhCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlViZk1Fc3Qyajc5OGNSTlJwaTUKWEFPMWxtaDlDVHVKaCsySXJvUEJvZmRudk9GL0JXWmUzUVFJVWg4TElCcGdaRkwxRlA3WkNlMisrOUlCNUtKago0cDdCU3RiYmZQRzdDNzg0aTRMeWhrbWZibGRyRHdmZmM5Z094RWlDTlA0QmpUSW13SnpZdXIyT2pqVGphQWZMClIwWXdvblVsckhmZXpLU2JIKzlwQ2t5bXpnQVJyejhacVg0bmlvVlVGMjlyalVpd1NKTit1QzJzRTZlcHpxUjYKbUNSdE9pR0ZHeFQwRVJheW94ZXdxK0NLQU5OQUozd0FmYkIxV3pvMGZ3RVNCMURGeHNQNGJ2OVdvcjlxNEh5UwpNZkh5KzJTay8zR1VhZjVRd0hkVFVMOGgxUFJyWTREN2RLaW00Rk42N1BQb01TYlpmQjRsS1dsc1U1bW45TkliCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekdMUTQ1encyNkR5WURCRGpQMjIKQ3pCaDhHT3dCWGsycVo3KzJJU3h3YVl5d2NJQXpjRDVNU2RYVndIdnVpNHJDZEY2SVRtTkF1a1RoOXVncUNPVwpLVzNhdGpiVEVSK2d6bXFRa0tNMkZGbmk4S29iZURRTjVKSkp3Wml2YXI1ZERLT01rQzNqUmpMUGh4Ry90S0NPCjk4MEgrQjAyVjF2U2JQMmg3ZGs1VjQybThRNFB5bzFON3VWcDVBVHZDQUhTcitYckhHQVZGZFZtZUREeDhrNUQKSTlCZU5MT05EWlhMdUtSR1lidjUzM3Y5Y040ZlBwZGptdHRPZHo0WlE4bk9aNzdJdCt5Lzh5SCsycmNsYWprYgpleWpnYWhIQWJ1RkVKVEYvR3hCOHBxaGdXblMwRC92QkN5djMzb1FXUm5LZ3FPdDN6aDl1RnVyOXdnS1dXR3FQCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmxpV2J5M3hKd3FHb2syeG1aYksKUmZNNUdGS1FMem5MNWJNRkRiWnRrMFNuSmY0SDBxakZYRUkxY2R2cGo0cVg0MDJ3Y2lVOC9JRnJIdzZBSEFJYwowTElpcGxTUVh4QXJSTDBwb2hXbXE2KzRrTXVnUTFtc1RyQy9sMTdxd3VKZy9iOGVUWDB2Z091czA3MDRrYi9mCi9tRmNrdDlzcHJMYnJrTFE4ODhraTJoRUNpOURXOXlPUmVRcUw5aW9xY1lGdmQrM1hRMk5ENWtOeUNiSlJHR3QKV0d0TGUwNE8xS0YrK0xMS3NES01ROG1NUmhUOGZxMVlyd1BlMVRqTHFGWkpXREFTNzgyV2UyQUorNEt4Y2VXSApHVnA2RXRnRVhNNE5mTEJBU3NpQ1A0b0YrYUtndzBCY0pLbHRQYXV1SEUybzVmUURjWEprM2xzUlZ6RnRLQmlFCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWFRN3c5WkV0eXB5Q21lQVJJaHcKSjNnWmZHVGJQRTh4azdaL1NzSHpqUUcwM3kza0JzcERER2hBdmUxdnhaSmNkanNQb09DMmU2akJYdklqUkNlWgp1VHVqR0NYaGtiQ1BoaFRMMVpyUGQ1NTRxZHcyaXBkdnNFUlAwUXM2YUpTdmVadFBYT3JjVUtoSlkvM3E0ZGU4Cm11M0VVczUyYUdMS01TSEp5NHJJWGxzUHovK1FDQ1h3YkM1ZjJPVUl5NTVsS0xoS1lOUnVNWEE4U2t4dS83RzkKT0dJZGZVa1ZmSk9sTkZYeldVM21GLzJSeENadHhteUIzZGdXTzJEWTNtL21UZGdKUzV3U29CbVpOR0RieldkTApnWmJJNXU1TzlqcHB6MjMzekZ6Vkx5bm1ZZ05kZDNlZnhyeE80MDFsNE9QaXpYR3Y3RFozYmUvYnlDYjRMRVk0Ck1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnpkWGpZdUFhL2ZhYWJBYW0rNTMKMENpajE3RHU2RGNOdmZQbGNuSzdxOStrV2Zla1lhUm9hb2pFcUFLM09JdFpLWFNXeFZKY2NobHJZTTF0dDV5TQppQzAvbEgxRWNFbS9wNHptbjNmbkhrQlZrZTdZalNKd2Y3NDdWN3doNFZVdmhGSHZTN01IcXVKV0VIZUgxSmY1CktKM1NHYXdIa045QllsTXpIU0ppaFZ4ZVZxMmI3VkllQ0VLQXVkWG9RYnZqK3h5a0JyN1h1UDFIYWxyTmxvZ3EKNEVxcHZwcERuZ2lTZVM4MG43V1NEdk1kTk5vOWR6a1lFUUV2UUdUUG5welhtZEZEODEwdzJiMkluekErVFB3NApBNkhwK3UxYk9JWXVGdDRXbnNhWDArS0Q4VFpiY1hxTnYxYlRGdEFzV0V0UzZFRElsOFN1c0hzVGxjUUhPZjI5ClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlhTSlZCOC9hNnREcFpjdnozQUwKRFhQTVhhQU45cHlKUGFrdFg1U1k1WVE3MXN4Tjh4S0R6ZmNYR0ZGWEQ4VXY3NTFyU1pSVk9nQ2ZId0VzS3dCSwpFU3FuMmJGQ09jWVZTVXBHWEdmZEpVLzZNN1k5TVdzWVJTS0M2VGlsQkVrUGtFemk4V2FMejhZYndYbkZaOXgyCkN0VHpnSWhHSkJQYTlKeG1tcjU5MDI5MW1DeU5hY2FhZ1lOOXhXaWtTY2I2bnZoN3ZpYVI5Z3dVdjR0dEJWMFAKZ09VSFJScW1hV3NNN3htOGl0RW10dmVhUXlESlBZVVYyQUh3WnBxOFZMVGJ2dno0cXBldjYxalp2L2c5SjdBQwp3Ym5CSE5mS0p2ME9hK3dBY2s1VWVpOVFhbTh0b2FrNkRrRnFQQ2Z3S2dQbTJtNlhjdUl0R0ZvSjBUL0poTmZsCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDQ0ckRFQ1dycU8vUjVZNit3Y20KSzl4K24rVFFZV3VEL2NmSEdxdWxIZHUvOEQ4akxMdGQwcVlvaVhodFZReWJCUFh4WVE3dWwrNEszT2E1ZjM2Vgp4Rlg2RGgzYTkyLzNaWWl2MTlzNjQyNXZoNU9wUDVOVlZjblhHMEcyMXN5SDZTNmd5Wm1PSSt1TkNVMU50eEhLCnBkSStNTGNhY2w2M2R2ZFlSZ0l3RGd2TzJBNUR1NnArMHFvbWZQV00zUTFFcFh4WXhRcmRiWjRiT2V1Y1d5YkkKdEcxbURNVnN0aE9FdnpWZHdhTHVHVHhEV1NJVXg1TWNHOCtxcXo3bjVKSjJnTXloL3U2ZGJkTlJIS1JXbVEwNQpGQnhQUnRjTnJPeFpKVkNXbGtjY0YzTEZRNDBKZFAyTTBLZ0R2ZkVRUFZPUWJGVjBHeXJzN1ZnUmNOM3hWSFdtCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFRkYkNMWVBTOFZBVVc4ZXVQbDUKM0c0QlNlNnQ5dTRHbm8zMEhoTXNGYldPUTQwVldWY09Zc0tpRlY5UzJ1S0Y3UGVDRDF2dDRPTUNEcnhpOW5KVQpEMklCNkxyRUJldmtFUHNveE5oMUlnQk5Ra1BrVVBpd2lucXpZbkhUWk9WZ00za1JpRUxCWG5DK0RWWXV5OVRlCjJ4M0VTMlRIYUZxWjNsYStRS01GY1hLL2w3aloraFhMeEM1VFNqdzZhelloVXZjQTV4TEUwSGlTcWd6U2lNY2cKYmczVDFXRTdnSTJ2WHFiNTBWU1N3YzBpalFqUC9UQXlEcVp5Z2Qwb3J4ZW5JRnJtRWFicERxc094V0hHS1hXUwprcFFOdGpDY0tWQXpROEJKdU9jNVNCcHZNcEQ0QmRoNllaaElRWUZKTHlxY3NpeXE5eklEZnBPUzJCVFBXNWNsCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGZMTE1ycVJWWkZCeGJBMER3aHoKV1hhWSt5WFlLalhSVjhqdG1mcjlIWkVCVTFuRlZmN0phejJxR0pEaGdBZkU1dC9OTlJDTHZaZVhObnVTUUZyOApMelMzSmU3OVNrVWgzT3BUYXUxekd2WUI4RHZrc2FINGhMaWNLcWlXMitFdGRpWTVMTG5ibHhIeDc4b3haalpDCmQwOTl0T3hMdVlpeXhoUlVxSnNWbkJ5cHNYU0hLSmZORDNobkl6dnB1WkJzNFNJdFJlOXBzdm5tcCtTdXZWR3IKVDBPZVNOT21OYXZ1UHUwbEtjanprZzV0U2xiOCtiQUcxdVZLQ0tTOFlVUGhpbVBFREtFUzZyOHcreDBsWUNFawp2MWxxdUhMcFJwNWFhYWp5Q2U2REhqMzA1dGdUOEdHY0pPNnJQc2RSNmJERzZaWWdEZ0xadGlXaEhPYzZhckgrCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjYzT3lHekltN3VNa2hRcFE3TEoKbmF2OFRwZFV6bEszVTlhTUU5SzI2amplK0dUUG94MW9PTm1nZURKeVZtSzRLOXB4NEtpb0FoelVTc0drdEpHcQpTd1N2Q0J0Z2VzcnVLTG93djZJa2F3bUh0cGRrZWUrdWxhTlZLSmQ2UDQzMDFheDFyUGo3bEw5b2ZkSG83Y1F2ClB2SzNhZlREcjRVVDBzeTB0b0J6WjE1Szh6UldxVERzQi9DdThjZ1c5dVJlU09jbndZVE56L0tod0cvR3hIK3EKYjJUZkFid2MxR0tSaFRHU01kaXMxODRFTlZzb0hwR3g1NDVTZW9tc2RZVDUycXFGaktXbElMa1hNL3FNZ0d2Vgo4Qm1BMXFRdFVESG1iMFdGWndlUlI0eTF0d1RodERHcW5WOXJ6R2Fadm9QT0pBYXR1dHpMWXdiQjlxbEFZQ0xWCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHo2TW92b2hPRWJrcDJ1VEJVMFoKNmRRdkt2SitjVVFwSkY5bnNZQ0NBN04xWUV1VHhlWW1XSEV1UW5NRWpxY2dCTVJuU3RXcmFDYitUYVpsbnJucQpoL3ZUbWhSSFNTTThPcGZ2eG1wQ0N5Qit3Mm8vSG5uMnI0VCtYM1REM1pQRzN6NlpockhKVVcvUWdVWHc5V2ZJCmh5RFB0K1pRNUFIUjFhWUxxeFEwdUUydGJEM0ViWTBSbStIYTVlTGNKdmFiakpyS0dlUTYzb1d3b2tGaTV5NFUKT1VjZUkzV2ptSUlSUlZPb3ZKMWYzV0YwL2VlUEw0TmdWbGZMalFWMXhoaTdPT3pHSThhOThNN2RFL29DL1BrRAppalBKNTNwZjk2cFYwaUJadWtSTDd2NTY0dktDQ29BMTJqYVJMTVNROWpOWFhaUzZtd0cvamprQlRONmhCSnNHCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmpOMFlJUDRmblBIbWdCWnlaVlIKeU9GVDRsK1NtSEh6R1k0TFRQakZ4WFRSN2FKSlVLelEzbU5CSzNkUXp2ampXdzQ1QVlma1p0YnF0a1g3ODhjcgpQckUwT0R3V3ovUk5jMkVnTGhmc3AvZUxMZkwwZ0pUdCs5am5DUXpYVEREQ2RQSXMrZ3NoYXB2WklnUnRKK2M1Ck54amtaNnN4R01CaHNjc3IrM2xxVi9sbkc1Rm55NWp2aHBidmV3eFo0U3UrWG9YZ1FuYnYxdFprSFl2UFBmNjkKTmJnUk5HSWRMQ1Evd2VIZ1FkZFY4eFp5bDNjblFoUGkrUHVNaUg1cUFHTmEwQi91V1dDRDBxSnFWZlZKWU81Sgo4VWpEajJlb1ZwMTdEU3RNTkViRkZma0Q4cHVjaFAvdWovSTdwR05UK0Z5dFBQZ240V0RESEJSWFBBZlZOcXZPCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0ZSWmhqMTQxeVJXZFpCeEt2SEQKNmlkanNTRjQwb1pvWjhBWW10dENtNkQrL0lORFhpMDUrNkxVc3RHejY5NTlYM0I5OTR5dFlCTU5mOThRdm56VwpjNmZvaTlrM0U0ZDAzTXUzWFlHbjFpc0o2cWhia3prWmZ6Q0NIaXp3MUlCc0J1MmFORDBRUlBWdG16OFFpVVRsCkk0ZEFXQXBHWTQvaUJTTWU3L2I3QzJOQnhqcFZJLzZpNzdnWWRrZEhTZldtSzNsR1dnbGNDQU44ako4SENtWWwKTzMvQUR3WXd4ZUpTRVZ3TGlrV1FoREJ4cm9vMVpXYTUzK1VxWEpuSmZGenRNOXlvTkJISWRYRkZIYUJHcUVxSQplQks3TEZsOGRUSHBUNzFmYmVNYTJLR2NGeWNYbElSS25sWUxkMVN6ZHp0RkdrNmdwMm5KVkwvWVV0djZTa2NqCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGZrSEdkOWRiUG9WQW1BN3BoSEgKcFVwYU1xa0FVbndmZGhFUFlEQzdKVUZtMnh6STlQcUoyVzhLSUhaZUtNbU9LckhaMWVEelArdUpCTnN2QllBVgpqR0k3THZKVDdJUTEyYkxTNXYwZFBDeUJyY0lwNW1PcytyOEZEcDFmR1lsempSRG9ZS29qLzhqZTdWRnBpUmRwCkh4dGZvODA0NGplS3YzbWNES2p2bnJqT3RSaFhjNUJsSW1oZzBkdzdyL25veGNqNElHa1FXK0Y1M3g1SkxGczQKWmVRV3lUbGNEdUJ6SEpLdVVZejZuUzdyWXd0YTdtMldJWlc2RFBuNXErREtaUzhKdmg0OTZSeDErd1ZuTnRXUwpoU1FKY01IN25PaXc3cHRUYW9idndRU09XWTltL3dDckhWOTVmWjdWSUw0RzFhY2Z0ZU1zcjVUZldwU2pPcFg1CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFN1cks4MkxYd3htak5Hbnc1enUKR2p4ZENDZi83c2RhSFNOVThjQURBeG5UK1pJZHZuaE5xNTlFZEVqUEV5bDdTaFBhQ2Ftamozb0QvbGV1SXpFKwpVTUM3a3pJeG1FRzhJK20yQ1ZvMUFEK2ZjRHBMK0hwQkZjZnhJdU1ndFJaSG82Z3h6TXNIOE92d3F3YXp1a0t1CkFzb04zOVlwUjRPa2oyeGJENnRNaHhzb3hYbWdkUUMrWHQ2SjlhczZHZXdyRmlSTjRIbkdzL25kemJuRWIwSnoKdGhWVjBsSk92bHB4VFhLVmtIclhmTGRnQkFMT2xlVUloWk1wVGNTWTdqMXRodC9GVTlnVysxbVFlb0oyS0ErdApZNGhucERicDlQRDdmNk41aDFYOTF3cmlndzBMcnBnUnRHWXFVT2VydGE3bm1WSFUvNC9Hc2RuYTVRNHBNTVM0ClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWNGeVEwY3BPZzhmYU9vS05VT2IKbVF4YVNBdkFlWFhDUEtqQ2l5cDRNUVhzTS9RVEQ1OGJyVVlpU2doeHlrWnJ2MFdCSTY3Ymk4MU0wSThzc0haMApDVzNBS0l0aU5ERTdtT2dXbnE3Uk1BMjVBNE5heTNIRmdhblpVQmVuWXB3RHZDT0Fnb3ZmdEhjb0dwb0wvdU1yCnRPRGVqWWYrbElSMzdoTXFRTlg3SWRsblJBRjY5eXlDZ2hBT3RuNzAvM2J5WUVnKyt2Y05JSDM0d082Mmt2eHAKM1VJb2lubGlPMXJpdTdTakFHQ282QjI5aTNSUEJnS0hNOUpKY01ERlNQbU9KdTJCd053WWJHbGI4clVTVWxUWgpoeUc0MkpVY0NxMlJmazFydUpUYVdZa21yeHFKckRPZVdOY05mcjVHMGttK1Z5SUc2OGdLb05tNEh3d2VaS2ZQClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHU5UEg3K005aDNRM0pIRDM0NFcKSDFUYUtZQ3FXNWtIclc5S0JlT1RwMUZQQ2J5b2pWaUdIVUlsUlpVeFlBbkdqcEJRaTVnOEVqRzBNQUdCUDk1SApZWHN2TGhkSnpSeHRHY0l6UHRsUDcrTGZmam9EMnNtWEljUHpNdktFUE5TLzFSc0J1MFlTUnlEYXZGUFMvMDQzCjlIOFNKcnRiN0pub2lPajlDQ1hqUHhLSDc4N0s4OGl0WjNJWHg4OWtUaUpTZkh2SlVoTUFidWg5N3lBNmR0Y3EKREJmSmZMM1ZOZC9iYkxwQmFCOUVCKytTTWlhWkxWR2hBaVlTV2xyc3BqWGpQU0RBRUJQbnFxM1U2cTJLVXdBMQppS0dxQjlHZUZZa2UwL3JHYzZRanFqVzI2OGxGYUNMaHI5TWc0d0xqY2NRdmhRYWk3MnVCbHRHRzhoU2pHQVVYCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkE4VXo0OG84YW1NN3AyMkR5MjIKWjFrMnBXN041TlExMnM5RmdGeWVOYndmQWZrNlAvQ3hBNXJqUjA4WmYzT3NqTHJ3d2xTY1JnTm8yMEF3a1RaYQpGcmtFa0FGQzBqekU1UDROckliN3ZpRVNFeUx3bWI5RjI0VkF3MTNxZDlmb1QrWnhReUlQNzcrdlloUGswT05UClM3dDFWbWpvaVF4cTdDT1J2T1ZTa0srSm81KzNqbGRPYThSOFljTXNweTI3WDRWSWFENmk1bVJnd1d5cEh2THgKSzdRUWxWY29FMG16cXllMTJMWmFmQ2ZMcEZVNHNRZUtHSlcxemZIdEpGUm5vc05vVVJQWFNqcTN0WFVVVzZ4dgpaWWZuVndrNGkrN1pvM2cwcUdiYWZQQzdvQ1pMWHRjenpnci9TaG5QRk5hWjU2SlEwS01STjhrcFlJMXdxV1N0CkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXFrZ3ZSeUU3UkNJWENhN3ZONTIKMndnazNYNTVtU0JmVzdDNHdEMVNQeHQydDZhQXlQZStkNDZBMkhPaEZ0bXNteXRmdEFtUlo0ZHJsM0tGc1BNWAp4WTJObzdWa3NjaXMyY2ZlcElscGdRVUNxZ3c1WjhJMWl3ZlNtb2RwSXJ0aStxbGo1L0xrcGsreXVsaUFxMkRDClJpcnNCOE1CbHlZTkxNWUVMWnY2UHFYaUE3bVRHWTZCZXVoQW01aEdIWTV5MjlZd1JTdDRmdlVyR1J2MytRMXIKQ3BaQnJIaGNTbE4rT1YrMmE5cEx6STdCK2hQRXRrL1RnOC9zZ1NpUmxDWk5UYThMTCt0a0d4WXdYc0lSTHVkLwpPM0s4NWpENytTZE10NWloRGtERy8rdGFnaXE3bDhFcjVQZFBaTjN0RGZGM3lLTVFqSlBtQWNabmJBSUxHMFVlCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVJmV0pqeUxHaDJsT3Z1YmxVMHIKUTFKbkFZekt1VldtOWs5TG9tOGljOHF4R29xU0lOQzZiQkVDNmYwdzdaemZVZzQvekpDNmlsNGI1UG9sYWhNNwpiLysyWXBXcWR5ZlUvRmFHRjdUTEcxZ3FuVVpaY3RlUGhpNElEZmlmZXdHQXVsOEZ4VG9uKzFuU0h3c3VjWklKCldLa0J6ZS9yYW1KTnh4ZGQzbkdCYkJmREhCTUJtWlFHUEcrWmpKZzBXaXBHZEk4SnBxMzFDbENlRDNSNkliSUQKbmwzdWtiOHRIZFlZU003UHZkNmd4TmZJbzRqK1lKRFBHZ2tJaHgyenMwd0J4Y3kwVEp6eWhVdnVrR3BZdi9xSApxcHYyWUpLYmdiR3Q0UFd5QnlVLzM1OENlbmJ5eFNaaXlQRTBCcmxybmdYVUVibkNKWmVqRGdDOGtzdmxHRWdnCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1EvZWRmTkRxcXF5VzA1ZVhsQU0Kayt1Z1hxbFZxKzYxVDg3U3BNcnlqSHkyZmhMMVREWWFveWxoZ05kUVFZdXZwTmZ2dXRRR3kvRmcwbU8yVkVmTgo3d3VKZGNMenNZY3B5dUVwVjBDSGpyaEREL3U2ekwwRmpWZVV1SEUvYW5XUWpzVlVGSk9Ici93N3hNWkp5RnVwCjB1Yk13MDJrS3BhY2FEeDV1V2NtYkRwT2RRMSt1RjVyVkhac3hIQ0FpZUtIT08yRmo3bFNEOFhZeWFYVjA4eHoKbENWTjNlRGZNc3NMWi95Qm81dlo4VzFMT09VWG95V3RBdVZKb1JxNFdhUXI5VjRGQ1krNFQxNlBNTEFFa2libwovRWRzaVlINmJiU29tUXhpN2o1RkNvT2FuS2FRWnE2OFFiRk9SelJTTmFaS2RUMk9aUTJJaDZ0dzJMSTZQVWh2Cnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVJmbmRvbERDVHdacm5UY2JLUzYKZnk0VG9nalIzKzJpcWZiRjRaUHVhQnJwbHNyWnZXSFlQUThGMzhVTmxyQjhZNFBUaFJqa0d3azVmTUxPMk1xVgpUcnNtb0xPcGJ6S1JJaGJxUmxCM2Z3ZUpNL3RMNEM0ajRmVXYvUm1qOEFMTTByMFJTaXgyUnFIcDRsRzFQSHhSCmRGYlZ6ZkkyTlRxV0kyaUJvdDNPLytpQ0pEMHV3NnNvd3JuSFlPclc4dk9ZbzBqRXJJL2NmZ1lCREdqQUxoSGgKUjhHSEFxL0VHNzNna2RUSHlEeVVaY1ZSaGpnRlZuY0h4VjJpR1JIMmdXUlpJZTBxekNrWERUaUwyZzNVRmd2bwp5bXBQUXFZbFRCRXl4R1hWa0VGZ2Zpdnk4S3JXTzRudHQ2VlRtYlBGMk1iakFQL1BBU1VjSXAvTG95YVVjZDcxCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFZKOThpZlhueSthbFY4R3NBaGwKbjlJbENKcld6eVM2V0tKZEhERU1iVkIxVGJOdTdQVE85Zng3NENNRy8xeGxFR2cxTWYyMjl4TENRWU5XdWNIcwpRdWJuT2x0bmxlV1ltYWNTYkhLdkRpWDlkbDk4bVZlaXNjYi9kbmxrTGk5RkZPblM1bDA1K3JvbUlxWjJaZHF2ClhGaHhxMytEZVJMSGFSWThreGlJb3hEUVdLUWoxd09xRHRPdnJBSVY1YThlQ0E2TXVhaCtPN3Y2WEZ2NEcrTkQKNVRjTFF6dTFmVkQ2OXJjUVpqeFFyTXQwYkVMN1ptVnRsM3JaanhEZXQ0WC9sZjdmMjdENStaK1dpbVpaVEJ6VgpGY3pGeERBYitCeUVlUk1oajJsOWdLQTUrOGZFa3Z3Z2dxd1ljYmY0T0p2SFd5elhHa0JhajgvcER2OHNmdFdFCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3FtRDNTYVp0ZW40NnZDaUxLbEoKdDM0NmxhRy93dzFxaHZTbXNkZUJTaG01RTdmeVc0bDIwNlo5WVZ6bmIzeDNLU1o3SjVCUUxzNGJpRGxHbDg0bgo0dGhaQTRTandWdWlLZGxUWFgzcnl6eVZWa0lIY3BERVI1MFhBRXVZZ0FQZUNxdnRmcFZuTDJsZXBWTytwKy9RCjBtQTNmUytzZXBoSG1YYWtrTm14S3hGWC9QMkVZWWZEZUZCUHl2TXA2M3RNTmZETkp1S0l3SzRSVlBQQldXS3IKaGdNRGdsOVFUdWlRaVA2WGJ6TUdETzJNaVErUWV5R3d4a2huZTBnczYxcmRCN29EQjUzQzNQNUN3RnJCWXR4bgpFUUhUbmNCZVgrblpYc0t0WDEvbjJzenJadGlEdExhNFZ4RXM0R3VRQjlsa3UrNSs0djFSWk5HTUcwemNwZ2tJCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdy9ZVFhkeGVYcVBjMWNFVWZ3OFYKeEtSUUptdVpIamI0V0g5cFhTNmlINlRTSitMaWVjVFExLzRKTWQvVk1wdGZVeEU5SExmdy9OU1kyWUIxZVR0agpGZWpONjVNUHJVM01FaHJWOEpCT1hQa3lBSHkxYXVLU1oyRGJwRFN4Tmx2c1hwRDNDajVSOG5PVnJQMWRab1NRCjlpQjllT2QwUGUwU3VxMlAwYmQ1eXVFV25oc1JYcDZZLzl0Zk5QSGNic05hSVU1Y1BKLzN6UTdEUHpLcEtwNG0KZ1JCQzlyejRNb3RVbURnc0VmVXg1TFBhbXBQcmxCem5nMjNzSWNHaVE5eHZrN2tNSFBIVVF4R3lzN1pmYUhvegoyTVplTFhMZUhjZnEzTzdvSSs1R1BiSkFnNlZPcngxYTlmQ0xjNGV2Qld2MmFoYnJSVG1KbHRrbTBEMjNmZ2g1ClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHZQb2g2RWZuMnZjQ280R3piYzAKTU1GU1FQNEUyQjlWZFJlSTJqdW5yQ2VFbzQ2TGZjRUpDZldscXYzaGl1Z1BBSXdIZ2RGRmxSMVN5N0wzVXdKVApyK253NFludjJrWkFEeEhrRnpZaUU4aEMwV0NFaysrQ3VnNEpHUFh5emdnbnBuQ1Y3ZCs5L0I4U015TGxsUVlsClpkSVE3amZUR1lWWCt1SmZHcWFUTEx5U3ZuclcyT0J5Z212K3dqcjlDUHlzMHZMak96c2JXelhFU1NWNjI3REQKTnNvdXFZUzVJL0xMVkFwV3RXQ0U2QmdQRzFiU2JLZVlnTitlRFlUR0dRb25rTkVoUnVJZndMMFZzbXVqNWQ1NgpQWnhpYVdIbklsdkNZZXJPNy85V3hUdmFBL3kwM1dmWDhXSXJDOTlNQTNKdVJ6UU8vTnU0T25xNVB6THNpeGJjCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNllTdXFPMlFPWm4yZ1hoWFB4clUKL3FmV0RDTmNoMy9QcXpIMXFzZGRrS2NRREo4RUJzQ25SSHJranVVb2djaWRib2FCRlBCd3RoZ2dITEYybllTUgo0bEc2WFJUNDY2ZVczYlQ0Szg4eGhFSmhzQW9US01ZWjdodjQxcmhra0htcWlrRm1GdE5wWHJaUzcrYXFGWW5OCkFSZ2t1endCbkJiMi9TWDhOMW5GeU1uRDhxY2EzQWpxZVZZKyswMTBKNE02US82V2R4R01sWWgyL3k3QWt0VG8KZWVSUWliM203eEh1RGZPSGVMR2pPZDZVSXp4N2dHNCs1blJWaXJ2MW52dnB5R2ZuNG9qYjRyUGhxNm1SWVUxaAp4a2VxZlIxNjFLbUFnNy9WZStHRkxkdlRoT2FyZnZ5TUVla05tSW42dkM3SHZkWVYreEdKcTFSNHg2dHBYbFZoCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbEQzbTRnNkpUdTViZkpBbWQwYmgKd28rNURBSGZ5UkE1bVlEOEVESUJsU1dwcGJaeTRsZXNDKzZ4U21pRWRZT3A0YUdDNGhSOC9OdXFWR1RUbTBTegp6VVVyVGEvN1l2S1dDRE9wWkMwekVhTzZkMWxpeVBQSFdBb1hNVkRFV2pmTFViMHcxWFRyOVdMUjMxMURETERZCnBGWGM4SUczOER4N3lDYmRnVVpuSEwvUHBGSHI2RUFqYkhFblJoOWx3WGl1SHc2S0luWmh1L0hFbzNTeFlEMUkKdUlFYm54Z1pJcGJhd1hZM0h3M0V3L2xhaGlwbDhYL0ZTTnZWeXV5NXJqMGRxakpaUC8vQ3RvUTBEdzQ1cWdhMAo3aW9pZEtES1JSS0NFSnJGTFlMMTVzWnBrc3ZLZERHb0Y2VldrS0tWeWdsVVJUSHNKRmJBMEtjZGNJYXJWWlIvClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0g4emxzUDZUS09vRGVoREZMdVIKd1VSQTRYUE4xbzMrMnJTL0p1Q1ZTYzhjYVc5NXFIbmx2d2dUVnNwSXZWajFZTEl6aTFVNWxxenJHakdIcDJxNwpvYnZzUnNRUTJLNDhhQ1hjTlh5OStOWEV1eTJYY1ZGSlI4aWI3Y04yRzNpL1lVOWJhRm9OalpkcmF6RUxIZ1hVCk55azBxeEFZTGpIb3orYUM0MlZHZStjeG5PMDRpbk9tYk05dWxuVE1JR3Rma1FibzFGYlE5Q3RlMVk1c016cFEKQVExZnNWUjJLQXl4MUo2TVNOR3VXdU8vS2liRUpFMXJKREMwcW1nT0VvUEVxekhKUHFrNHVxZWlGSXpkbC9mUgpwZFIyWUdONUhBYmhMblg5YjUxZWFEL2lnOVZiZmZMd1pFQ1R0ZDY4MjE5RHM4cTFYbVE4ZHdIdmczYnZDWmVCCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2RwbFNqUTYwczlmUUlRaWJjOXoKelZjRlBHVHhmczEydlg0bHdkK1lsRTRXTXZtWU9SRE9aU29pSDc0RjFSYVZFVDBvWGExNFpSV2N2THpyek52KwpPU25rZ3ZBc1hCai85ZVEzOGhOZVZqMlVDazBCUTk0aisyb21Zc2JUNHpucXNwSUp1QmNZMWlBOUJtWHkwQ2NGCkNRK09FSVpKMXhpMmJoM0phWEJtNlZnQnltMllzRFBzZkJLYng4bDg5K2NUVVU5anNJUFZVZjdBRndtalhFbGMKdUlHaDh6c2Y2VVM3UGJ1TG4yQkdqb1Q2UXlKOWF1Y0JSNWM0VFhRQ1ZXazNwRHRJZUIya1BiUjBnV05pWithbQpiQ2NXR3ZPRkQ5WU1Hd1d4OFoyRERtYjArRmw4WWRBOS9xcjZmVFNTUG1GWXRuNkpzb0pBMGlIcEZDQi9mbHVFCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUloMnVEcWk3TFFVT3NEcWFGRlIKQnd3bUdGZVpXd0MvWW1kaXUya2NjRS8yNWY5SVNhZUFsbkFXU2pmL2hHYno2UXh3dy9mTm1nSjdmRkhxZm5xRwplUUd6a0ZyZnNqdEhVS1dFdzQ4T0NwTzIydHFtNG84VkkrdGMrSzdnVXlhNDlFSzBvZ0t5cHYzVTd5djBMZy90CkZZTlMyUW9tWUxqdUpUNUI1djROcnAyYWQ0VHQwdnZVN2NQOWFRK0dBTFErem9jWDlERXNSQUZnOVRSRCtwamcKa2N2MjIxQ09WcnVTUDEwdy9DTGsxdkhMbVV0dndkZkozQUx6clJQUzNNelFCNlRKYVdzQVhGZ1hVcDF5cmxhawozaC9DRTFwUFVEMFprTGlnREwzSHRZZkpyeEZUckdQNXB2c3RWT0NlSmw3RHBqZG1uYXA0OXVKWEJJMDB5ekxGCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkJFWGUvd3BtSkdpT0NDRERrSHgKUm4zL09iNzArdlpDOHZ0ZjdJTlJSNmZwZ25zSVhZdmJlTW1TT3ZDWXF3UzRZV2taWmdja1gxelVQaGpsNFd4dwpHZmlDREIrU3I2SDFUaWFTTXhGMERESmxDV0YrVkphaUVEbDNNdmlIdHhpL0lRNXY2YXJPUTlCUWxkZW1iUkR2CjYxemxPdk90WDRLRjlsTFhOVGtqSG1EaTJlOGx6SWtIdUJ4cWd0d3AreFM3bVFHN0p2WVUvSGlaWlFua2ZlN0gKYmJIWGRnTW5UNUNwVXJxOFpuV0JPNkt5ZW16Ymg5R2dIVU0xZ1RGbVE3MGdkL0NkNUxFRjhGbVloSTc4ZTNkRQpucmlJU21hZWxnbGcwTm1aTTZxWWhYZGM4V1hpYmZFVDR1dHhPY3lIUFRGK1BqRzVRc2sxeVYrS3hBTVEvcUFVCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWJSNC84d2s5Zm95YlpLTXh4S1gKa2ZVRklUdU5vTHpxU0hMN1hSQmVKUUxtdFNaSm90SmkxR05TQllFRFdtMFNsazZTbjFSOTRyYUtOT1lDWDZ1dQozV1V6NFZsZlhydkVJK2I3eGpEc2tpbUc4TFZoVUhidFNjdEx2aGdBWVJGRTl0d0lUQVJEc1RvcWVLbU5GSzZuCkkzbHBSQXlwMFcvdWFTeU9xT2QvZ2NtdVVNdkVtL2poaU5SMm5hMy9MSTI3aE5xbnEvMis4UkUzNnN3ODR2Zm0KemM1UUhkTTRDcHZZNmZFWW0zb05VTGl1NU1qV2Fza0VwaVI5VW50UGlzYWFmdUpiKzM0SWJnaDYrRTBLazJ0cgpCbWxXRUhrSGp3Z1htZjVOUHd1VnlIcTVMeE1ST3VqdURnbnBHOFMyTkNRVUlhN1R2YlNUSkhEamlrcjBlM0pxClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2hNKzhkR0N0UUgrdEI2K1V1Y0cKU2twN3VRZE1WRTNLOWNybWYxZS9NZ1cybENnK0JKbzYxcjlUL1puRnhJM2NLR3BwTG1QWXEvT29kbzAxajBVdApVazU3N253WXo1NlNSRTNFQVRpL1plM091NkxOWW1DR3laQjhUK1p4UzBPTVVjeEJKZEtac1pzNTBCZFNqSERxCmo4MmMrRDN2QUo2Ny9BRkwvbWxaeGJOUGU5eHN2cnp6M2FaS2lCbGZGL1Yyc2t1T3Z2dmNodWZhZzZlR2p1UXgKWnR1TkhaaG9KaHZKYjgyZkRQMkV5R1FPcTNUY0RVRjBRVXMrYlZqSVRtU2RMRENubnB0eURVVUo1OVNLUmFPKwpRa3Btc3VEWWNtQXNtakV0UkllZ243cDZmV0E0VUtEWlNRSkI2bWxxVGJEWTZEYmYzMGdaOHg2VWlBdzI4Wm5PCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk9nakRRdlhONHNXMk83ZE1JblMKWEs4MUVpZjZ1bGxoSnRKVEZVanpydy9pU0ZyNjA3MWhGcit2U3dyREpKQmtIa09MQkczeXJ2RHRBRGh2YU5YcgpTbTZDT0xNUjlDd0orQnZOSWJtbmpCa0VoUk41NkRNclR6VjRmcUxkRytrVDg1ejV3WlNCU2NNZ0JBcHpCQzJpClJUamNqS0hPcmJ1Vi9raHBQTEpFWWtrNnpkakRHS2FydVlNVkZ0akhOc3RHMTZISUduNXB1WkM0TXN1SERRSlgKU09WcjFzT2pOOW5wY3lvVUVQemI1VEtEa21uaDFydXNqRWo2Z1kra1hsZVJRK2ZqMEJON0Q4bmNrV3BhTFZQMwpIMFBnQTUrdzY4N2hPbEdWWFJLYkNaR1A2MXJ1TURtZlptUzlGVlBOYXdBeFkrdmM0NndwbDUrYTcxUVQxQlpPCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbitERTd4ZHJVZzJvMkIrSFQyTE4KYUNXVk55ZmRER3dTTFJxZHRBUmlNT0c3Q0F4aEhXdG45WTNDUGVWNHdqNFcyTWlRRjcrYTlOMXluYmFKMHFWZwpHeXJaYTY5SkZQQXlmeGRhdEhyVVUydFIwUVJMVlRJMW5sY1hYSElzL2l0VjEwdVh2aXA2bUVSWXduZHc1aU92Cm5FTGJuUHViVHRacHdLTG1PUGNPUDh1N2QxSENTMlJaMnNISTVCRjEvWGEyYlhsWE16V2gwNmJoeDBxd1RxakoKdmNGLzVBU01venl6MnQ2clEvZzBpVkRZOFpvMVlPVGFick1WVEx2bCt4TGFKdTI4V1dTY2w4M1JVUEhicTg1UQorVklyMkFXdVVqZ05yZXo0MTl6UHR3aVptTWNOcmhDOUp2S2JYN01FaW5iWTkvbDNSdWJzaFhoSzJWc1VyNVMvClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVJUV2RSV1RlKytydjRGVi9DL2gKclE0SlY4SE5oZlVHVHZqQnpzUFFiUVIyRGhHSUh3V0sxNFhxMGtjRWZFM1M2YVdTY1VGdkh6NGQ3STZmUm96VApraGdPc0tGaWE4akhycEJYRjA3Q2NhU3dFSEpDYkVkbjYvbFB0SGpoSE9UTHFFRHhrcEZVeHRKOC94Vm0zRjJsCmwzRGVPZ0NLRFVIb3lYaitDd240aDRVVjhFQUhvNkk1emFkOWM2NE5oTDR1U0tpcWZkVWcwNzhZbVFwKzRMUUYKR0J6d01Ub1BWdU5RVFFPWXBzdXZhckczek1vbnpkenpmb3hUNXdpN0szcUdIMWFiVnZsTUxYTUFOclpWSitPSwpjelZEMlppZ0dHQkd5Vk1SR2xnbTkvbHdjTll3cDBnekxDM09ieXhmeS80MzN0RzFZMXF4YWkxd0JhRmZZNEZ3CmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcit3VEU4MXpxSXFvdFF0VUxCRkQKclJ5WldaNjZ6RjczR1JHc2k4SllOSmtNT3NVRGtGejNvcjlTQzRFcjRtNVhMNHBILzBVL0pGMkF0QTRSRENBYgo4bWt0TFpEalBQQ2NRemVCOE9rUzhtdHhXYjBEdGtBc0dyenZZM3ExS0lZR2tGWUZNZnBmeUxPVEl1Y0hXUU5kCnZBUlF1US9vMHZJUEU3ZmZjd0EwOWZrN1BSb0ttVDh0b0J4bk11R08vajI3L2dYdTFVSTh4REZTK3lGV3JkQ0QKL1BncFNrZU9wZG1obExKUmttN2o4ZUZ6ZGwzWnlibW9WRVhjMk0wOXRiVHlIU1N1OVlLLzhGcVNWcDVDQlRqcgpHVGZLdU9tL05Qb20vY094Tm9IZlJLa2FyY2w0dW5JN2M4VldpVG1yWUdhOTRWc2U4Q1dCWVpHQnVYVmVvRnVvClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHdWdXp2U1RwdTNNK0FjVDNxRVoKWkptb0ZEdUFVWTdKRmd4QlZxOGI4UzBSb0x5eTFqRHB6cEgvWGdSV1F0YWp0ODN4cjlMZ0hDV0Q5ekgyRlhzdgo0YnpPUzV5SllpN1lNNVp5akl6aGlJUGN4dmE3b2R3OGRkN01JS0xWeHh5SmRieG9Ebk11VU9yOW5PL1o1UjFECm1RR3habm02VDZUNFFuankxenBaTnJMU0R5Z1AyZmxtSkpuOWQvbXFvWGhaN0dqaFhXQlN5TkptMFB2dUZEQXAKbHQ4RDRiN1V6blJIalRZZFFCZFFCcTF3eXRtdExwTVMvRERiRXd1dDE2NkxlMGEvSmUxRjhrdHhrdFBTcURJTApaWU93dmRqejBxMlZ0SndjMjdkMlZIRWFjc1FxS1hlZ1pDZEwyZWF2emdzbk9MV01xanAwVEF3ZDdEZjhnTW9zCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczA3cHVxb1htRXFFS0xuUXlRV3AKMlhqOElSZEJ1cXc2RGVqSWNSblB6aW41dmVFUjkrZDMrZFlGZ3BBRTc2eXBYdVVPNlFDTGc0OS9nM01FTlA4YwpBb3NQOUNFSm8yZURwUWtmRFpOQ2hjVjAvZWZiY3Y1VUFuaFdGd3NJMHE3bCtrZGJmdDVldldIVEtoZDJScWRTCk1FYm0xRjY2dDJzV3dmd25LeFZGVVkyRURlekYwdnlGR283SDNka0h6Tmx5aUlURUlnZG9tVTVvMng1MEdZL04KVVJHVXNSRDVtNE1kUDNySnFvemFSaExSQTZSY0tKMVBJR080MkdkN0h6NUlhajM1VXV0S3Uvd0dwQ3YwaGFPWAplME5mbFl5VU1LcVNvczhyUUdGRU1aeSs4Tzc1d3padXZEVDhqeUJadFRzbDRDRVVhWWlkN3VDd0pmSXNBRGpaCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNG9Td1ZCenBMdVpmOU5NTm1NL3kKU3VjWFVCU2xFNjVVZWwrMVE0SHZTdWkvMTVKTHdORXVPWU4ybUpqd0pWQlJlZ0J0djF3QjJkZ1A0UkZIZGorcQpwZzRkWE9EcTdYd08rT1lKMmlKNHJxQnJKVUl0OGRWNlY5d1ZHcXRmeFl0OE9qbTRLaEZqWDdSaTdob3NzN05KClNFU0tDaXp0MEdYa2FuK1hxYUNFMzdMNEE2WCtSYTVIYWQ1dXp2K0dPMS9PNkhyVHY1YWxUWHlZY25RYVBrYi8KQVM2UTg5TGsySEhnbGNHK1lpSjdJRHlNa3pXVzd2cElOQmhJY1FHbXU0NGhzODZub2ZVZTNnOEFxZ1FmZ2dBTgp4ME9XVlNwVGs0ak1xUzFZOEh2bVRDa2grSFY0b1BRNHZLRVA5SkV6QmlyVU40V2tlWlNLNU4zaHJkaFR0cUVjCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDBWUlFQd2hoM2VkUWRnRkRsdzIKUXIwenduYTVub2hRTUVNTlJXcWdsNXRYc3NMWWRyK2cvazdDaHBGL3gwTUU1cHczSkVvQzNDU2hkaW5XSFpZbQpFa08vRW94U2ZaNFlaTlMyL2RsMlN3UnJoMnlNZDUzM0tqM2Z5VUwzQjlIaVQvcUI2NE9tZ2pKV3dmK3hkZ09pCmRYekdNdFNiWGxrK1RmVXlVVTBray92ZEhhaThRWWExa3hxTnlDZXFiVnFtTXJHdktBL0pjQWcxUWZ0c0RxZmQKekxGT2NlTHhQemNHMTJMcGZLVHlScjR1WTlpbUZ6QmhqNGoySCtvaGRINGljOGIrWTJoUjdnZ0JCYUV3VmFYcApSNnJkK2UxYXhESTRHZXRsNEg4YkhicDEySFVDcjRaZVBPK1paZjE1UFlBbXFzdVlrdkFOS3lwbkFOeXBsS2g2Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjE3VG1IR2QxT2RmVlBUbFlDMTIKTnZ4ZWNVbWM3cXhUTzJ5eEd4TDB3cVJWd0g5U1dIUFpXd2JrdVUyakNNVFpnQmFnd3N1RXRpQnQ4aGVCVkdTWQp5Y3pacjl2TWNJWkd5ZUFSaVYvSTVOeGVFZmNtL1BsT0owdXJyU0M2ckJBS3BXbi8yKzQzSlJkLzZ2c1U0aHZOCnFQc1U0Mzc5RHlENFJ5NXFGcS90OTF0enBjNHI3ak95aFNSdUFPa1NzYklQNEhkRmFRbTQ5dlFrTVl5WWJHazEKTkExNitxNk8zWHp6MUFzZVF6L2R3bVo1MUVlSmJNeVJ3VDhNU0ZZYnNqSGNnN1ZyVVBJTG5sWi9pdXVTczZEUwp2eG5LcW5BaExSdWFkK2QvaFNLWkJFVUx0SUVRYS9LQnZlS0M2SzE4M1N5UU5jTCt6SUpuSEp3Y3ZYQnI3ZEM1CmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEtQOWh5angwQkJCMHJHRXN2Q3oKeUd5TnNjMHNYWUk5ZDBMQ1RMRjRybHI3UWk2WlpPTHM1T2U2U2c3amZHVlBHMXZQRFZNV1k2SmllRTBoQVd0bwptQTVhVkQ0bUVPbHJGdVlPcEtCUENwdG54aStobE5odXpaSXpBQmdkMUkxb2dUVTRhZXo4a3RGMVZIdjJLb2grCnVxOFRlR0Q5dzZpSWZ2dFprK2JYc3RScnBlUGRVdlllWC9reldsVjFJSnBvY2VoYmR6Y0RCM2EyN21OUElKMnAKUHdLQUd1NkE5RnFsRVFITjhTN0Rpei8xREhvelJVczRDK0dLZEh3SVdkK1pJYkVRUFdwUmpNQ1hGa3pIaENSaApVSnJNc3V5cFJsY0FYOXZPWHAzZzBlYnFBNmFyYUpIc2ZjNm11MzNsY1ZFdzdhMGYrN0tram91My9tdGJCd3dXCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBendNbFdoKzg1TnF5bHVuNUk1U2MKdStSbWs2Qis3MVQvdGJpSFF6Zm5hREIxUEhYZFkrOWttclZJenF5S2MwaGRoMVdaeE80V0xyeUl3L1NaT3owRwpsMW8zbCtvaFdTUVJVYURzTG1ZYVREWVRGYnB0RDYzdUZrQkd2YjFVV2R5WW0xKy9zTlUrbldYc3BtZE5YU25wCjZnM3dUaXFSKzllbW56K3lId21adlhPaDRLdll5aUR6THp0WGpYaVVWUlN6bjVWa2hvakZOYTMrVVA2V21RTWwKUjZmYjJubzhuRUNOeDIvTENpVmkzT0JQS1Bic1ZFUUJNbWFOY0pseVlhZkh2ZnFvd1ZadTdVcnpQWno0RjlFWgp6OCtEbFU0TU1aZDZOazM3bDhtUU93Tng1dHZmMjZzVVRNR0IvWmRLMHZ5Y0lSS1RrVW5CTWh6TkZja1pONjdaCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem9hR1YxQ3ptbmVIM3MvRnlGUGQKZXBZVERFN0lHVm9Dd1J3a0NEVW5pQlArZldBb2pLb0NHY2VRaDZTZjQ5WEF2dkxZVDBwS0NvZkVjT2pPc1hPMwp3dnlDcDZ0bXc2T1hPdGNreTJVMmN3YUFsbEkwcjVvbkRSazJJVUFqZG9yMytWM3dwWkxpaE1QU3pNM1hETzYyCndHeFhTS0J6bWdzZFB4RWZaWTMyVmZEb09GelRSQVdPVllFTUNCWHBSbkNWMXVyalhOSkZlNnhoWmprVnE0ZjcKNEtzNk1xSFBUWGVCK29pSVBMaWp2dmJ5STlsK3pzSEEwRGRvYTdBd25iZHJqK3VnVmFwZkNEbWtmaG44eUg3MgpNUTI5emdDdFM4RG5WdVFjUGptSjZXc3BnMGdIdHFHRjd5eFU1T3N1alhJR0Nqd1oxQU14NjRFMzFQcStKNE5BCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFUyeWdhUzVsYmhlMUdWcVRVbTAKOUNUbTBlbDZWT0RJbmd1SjZ4VytFN2dNT0RxUzYwWWs4ZDRrMFJwRWg0MGNIM3ExV2JSRDdudFdvcFFXN0tXSApGWkVqV1dRd0psOXNTYXpFb0dTTk9nRCtjY2xvZ1Z4bzFXc1k4UFNPbERZYXRvaDE3Y1FwRU95SkF0WGM4SU1MCnFlSk5qemY1N2FWY1VWK0Q2a0pwbS9iVDVGbzFWTCtqTDhMN04zb0VkT3ErVUorYTY2NXlNeGJXckR4aFhNQTcKU1NOR3hJb09MRGRyZnYvanZqQjRaRUlHYkRHYzBTZE1MUERqT25PSkc2TklFUzBCcXloNTVDVDFLR0lqL3VVYgpNMVFYRGZHYURlUjZOWER1QW5OMWxiYUJ5amNlSzNnTEhENGxWZTM3Q1FEdjRTdGtpWFlUeUZMNzFGVUpyQXNiCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNG8vOWtieEFHc2ZuUERFdGE4b0wKUHh5TDJ0QVhKMnVwQVhRdXZncllUTmJOeSt3eUdIM25nNmxkL0Y2QWNYb1h1cnhkUzdrQ29kVFAweElMNEF5LwpiV3V5eStsSUx5cHlkalVkMWRzbWpQV0VaYS9qQ084MThDS3ZLWTE0VDBQcTQ2aXhjbDNmOXhKQzV1WnlQbEpJCm5SSkxQOTgrQVQvdnlFczBsclFLOHdGSDhNd3ozdWQrd0ZjNEcwdGpkWThDQkExdVprZmdqT1R2NDBsZlJNZVMKMWR5N2JVakNveC9kS0F6NXJMbG1tUGRxMDMxaUw5UGZncTFUQTI2Nm9zZlRaZDBhenlXWUJqRjN3NGluYWd2MApscTZhR215RjRtODd2c1BKQUd3eU9hVFNiUFE3MnRZcnFyQmVKMHhRelpDRFFoMkFjV0VVdURKTm0yUGgxNTNTCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWd6OGNSYjliTHpLYzUrNFFjbmYKUGxZM1A0NTdGZkMxSFlKK0kzQ2x0cENSb21aRzlGMnpFNUYwdk1qQWxpZm1RSXZkTFAzeGwzYzdKQmlKM3lENwpORERoNE5CMWVwZERkelNScVNZK1RtamQ3RW5jMzUrazNEakF0ektlamtNK1A5SHZYVExuU3JJcVZDYzd2eVk0CkIrSW8vWGIyc1FwT21HSHNyUFhYdmN2QWZTVUZ1cFZTbkFmMWl6NWV5Y0VqVStrQXVmbUg3TkdYOTUvMWpPU1IKZlZ1V1F1N1crZ2RKelc2WWRQWEswaVkwY3ZQdnZiK2c4K3RDaGYvNUhKUzg3TlNiSEhCMWUzREN3c3FyNndBVgpwWm1GbEVwTjgzb3VjSFBvblNZa2ZIVnIwem94MSs1c0g5Tk5HV3ZDbzF2UVFPeGVWZ1lxdmF4RDNYWkNPMFB0CkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFA1OHpoL0R4UFkybEhkWmtJMG4Kdm55V3BKdkV5a1Q1ZWpBREJPdEhpZG1CWFBhUmdSVUZPR3VWUGhYNnpXcklzT3dqbHcvU20veHR0MDk3NExTRQpKUTg5d0oyUWY4MUs3dXV2WHdwQXQ3V0NQOTY4TCtoUHhDbGl0ZFd2ZXBuNVpEZWVsYjVhcGowdTRRenVuUlkxCjN3WEk3MHRvUVB4QXBsN2M3Smp5S3JGdVgrZmpFZjB2bWEvUWdkL05MVmtTNE5KUDBKT05jTzBNaDhCU0pqV1UKemJjSDZEMU9MejJSdXF3dDk2ZjhyMTVGcndHYlN3UHNYbUFtSitvRVV5RWx3V1VrM0dkT3gvdnpGRm1FTU5YSwp4RTBUOS8rRndiWTR1OUhMR1h4NEtIUHovVVlvbThZc0JVbnovZVFzK0gzMCtNdVVFRWJURGViWnRjQVlObVRtClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0ptL09hU1FPOFl3VWp0S0wwbTkKYU5lemlUZ2NVK1lNRThNTCs2KzNXTGVabWJDVmJ1aVNoUEErS3l6TDR6eEJVdStUb2J1S29ZZGxsbEM5Y3Y4YwpTVm13eGFlOGFOdmQyV1RZOTFLQXB3dElEd1gyNnRtVm1yblVGM21HYitpNm9qWnFQeWY2aEFVVHcyS1o1RmxUCjBQcUdjTzdwM1VuSzVrMzd2enRuM3pTZmZEcE1JZFFIYVlydzF3bGYvaG9WUGtTeDdqTkpmbUs4R2p6ZTdkZS8Ka2c3WVg5NVJmaVZpQmVHWlZsN3RXcTBzY1UwOXRMUlRuOElaTDBGSyt0Mk5KMitJTTM2WDJOZFZ1dS80WERVMQo1SzBHRU9oZW1jdHFuN3hvTHk2V0pxcWxwenJERm5JTFE1aWlWR1U2N0tpY0RCdm5oQ05hVkJyMUdzL0RwMWU1CkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkpFQncxY2ZOTXhMQVdDVFZ2c0MKcllBTGp0Tk9KWEhjRHpkSDJoMzk4eHRyTVQzbzVsVy9xMzhGc1BMNi9jL001VUR3T2tTbVFZV0ErWmpzU2FyQwoyTkhHczhxbi9CVGZreGFBKzlGamxvRHNvejVySytoZVVzUXlQWEJIa2xLaWM2YTlEbW9ralZ6OTBZQkFmSExtCkY3bmt6OEZoVG1Ta2w1eU5JdFpFOEoxcGNESGxxT3l4cFVMaWEzUjdZYnJwb1E3bUk0MFRIVC9hTTIzTW5Hc08KSkx0V01PTGp0TklOc2h2L3lUVkZaSEVaanJxNXdVZGRYYzJrblJPTkJkYXlXWldESHdGRzdzdEt4d0hYVXBXRApqK0JUTkY5VTFJdjRWYzU4V1RPcXlHYmE1b1Q3S2R3NDJ5bGQ3Z2V3THZHOVVlMmRqcVdmOHdrbTA5OEs4SHFaCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDAxYktka3ZtVlhsZDVJdi8wc3oKdkN0dnh2SzdjMS9KRVlPOHVUenJaSk1DbHYxWjR4MEE4eEM2Ty9tOGd4UEZoU3VyUGJZbVJBcWdMMUVad1REUgpiYTFRWFlwYTBsNGt3eGFnNG5xbkJITWRCMG91ZDZRVEwzdWkwM0c3WHJZc1Z6V1U5cHo5d3pKQVUxRWlDN2UvCmtGSGk4SWh0c1FnSElyZW9OdjBabmNLYWhqSm5Ga1o1b0hxL2hwYVFEUzhkUjJFUkU5anZxbEtuOVhqSFFLVUsKOHZsSUU4WjBNb2dxTWJFQWhsdjBDREdDTGhMSlBRM3crdkhGWFh3U1FHNTRJamM3YkNqUFhkS3JBcVJDbzVuWQprNUZySGFNaEtBSllUbGVXazZPS0ZWL1EzQlhrMVRubVFoTXJLRis3S3l4OGJaeG12OTVHZ0xMZkpHT3FFU0pPCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekg4UjlCaHJzVllLbjZKTnFEK04KZlk0a3NqZ1pHQ0xVN0x4U3pJUlJUSGJxcG9TbXJhSm9uNDQ5ZFdiWjFiem9CdUQwaWZzQkU4ODdSbjN3d2pxOQpZYVlBcCtXaFBrV1lzTGoxOEozdUY5Q0JzSVk1SlVBMXJWdXB1Z2wvY1lzQWNDVlZFdWNiMkl0bE1IQUZPZUlUClAwNjNVQWt4MTBrK3QzS3NieGVkaTAzZUR4YktvRUV0U25zNGxqTHdVY2xHeEFkSGxzUDNvdkZveVJ5U3hRK28KT0x4ZHhsVFV1dUNadWdnQU1xUDlFTGRVbTFUM0VSS2t4UjAxWUwwS3I0S3VlVkRZTDAvajErUnU3OVorTGYzYwpWYUduVHFPczhGL05wb1RSNmttekhWTWRON09KN1FvNmxmdlJrQm8wSzllS2pHVVl5VjhabWJDYloyU0Nsc3g2CnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjQrazVlMHA1azJVcmdZWVY4aUUKOTBQT3dEUFdNUU9kSDNpdFd4bytiUTNmY0wyYWR3dTVvRXRQWjhhdzdsYzI2eGR3cVh5U3lkME5VaEdWdFpUaQpQeTQwSStKSkRjVjQvczhCWmUzeEpQdjk1T3grY1BaUEFCY1JHMjB5Qlo2QW1JNUVaVU52SUhJZEtqYnoyY0pnCnRHZUQ2MXIvTkVIaU80a1ZXRWVBKy81M0I2bWxIaDk2czlFUzRXWEpPYTBTTnRCQkRweEtYKzFxZlVMbTJWakEKSTIzT2h2eVdyazJuMHU5cG5KSnN5VGpDN3lNM0NyRzFCTFJENXRlamEwWmt3V3A1OEg3UmtmN3RVUHVkWldiUApodnhoaTA2SGM1dlFoOWNQNEtjbndid2grNXpudTlEL3BLZWRja0ZVc0hUYWtNZFd5MmthcTdTQzI0UUhETVJ2CkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFczTGVxRysxdW1kK2lmK0Z2ZEUKQVBTK0hSZEpIZk5PeWNCNGpBak5nbkllanlsemt3RG5hSmJROUxSRStabmJGYnc5NHc0NFpqY0I3R1BxeXU0YwppYnp2d3FzSEM4bHBPKzBPSWMvaUpkWHBhZGFFUWQwa3VJaG0rNmxWNXVIU1NIK1QrOS9GZTBMT3FXQUdpVVNQCnBuakNrM1BIODk0WkJuR0RNaDZoVklXNXFxeWhtVWQ2WjN6ZlI5RkRVUXl2Ukl5K0t3VFVIczVTeG05VEtFbVUKTkhyZCs3M2MzSWtMR2s4VWJZSmFVVmxqRkhUU3p5N3NBTWRSQllXa09RMVp4TUMvZXh3ZEpsN0hJNURrK0tvUApraWRZcmR6N1pwWGxnVjRTYVRKLzNvTnBxN0V3NU5vbTBHd000a0dMZ0NObVFlZE9yZWVKcWxmYkNlaGc1UXhJCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckN3Qy9lc0cxUVQzdTVtcWp6KzQKZDBvbmFlSmpqd25GTFpTN200MmFYOU9zOXRndyt2ejloYXlGaWNBd0ZFNmdxNGRpU2IxdnBxZHloWDFodXh0MwprSkNHYWFBYXIyTTd4VWRKNjlPNDVsWUNJMFVwb01QcXRNdnJCdjlxOWdYMVhFM3VaNzgydWJJNEM0MDRwZ2VyCnBONkllZVdydGo3NE5VdTFCcGJZRWRvL1ZCR20xaHgwOTJ0WmR0LzFuZEpQVDFBYzRwcFM4UmhsQjl2SHJsVTEKV292OG9leGRDU2NBVFdFTWltV0txTmhQeS9CNllMRHRZcHhEL0VvS2x5NjdWZVF2OHhPb0F2QldrRUYvOFN6SApxWllHaUU1eE1wcjRWTHVRSHpPVmVUTVh6QkN3cXRraUNtRFY0VjZBZ3ZvU2JpWnlmOWY0YzR0dDRhZUdPUFhrClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlJIY2dWZUYxQy9EMnBUVWxUaGQKWHVQdWhpd0ZGNkUxdEdTaC9YaGcvT1phd2FTcFcxOU1IQ0kzRnQrMCt2b201YUZTeThlYkorTWhlUHlUK29xVQpmZ0hSYTlGWG0zTWs5M0lQRDAxT2xpVk9pU1JRZ3NtZU1ld3dleGk2WHg5ZVVTWElQUXEvYzJ4cFRMSGpsNkF2CmNRa1VYa3RwV1NzODhBeEl4QXhJSTdYVmhJU25yOUhscmFrZ2FlUHhGZTRheXQrTXVwZXdMVGkyZnVjVXAyak0KZ3BqRFV1Z3pSdzdZVThiNW5SK2pxRGtjaXpCNmpKMS9Mbnp3TG1WU0VwTkNoSVRxOTdsNi9KOHFwSmZLNXk5QgpqTWp2amp0Y3NiU3VWZk5zanlVWEQwdzJnRjlhUk9hbURycy9DbEV4cFY2TVZhOFBsRUVtL3JIaFV4MkVFL2U5Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXpMd1NEMHJRV1RJWVBDWGQ4WTAKSDZQNnlPVjZqWnBlbmpRSjR3TWdBSGtpcDJWeEVtU3B4TXVRYi9ZNis5WHVwMXFpRFdNdWcva3JtbjJ5V21xeApsUW5SbmVFQWZEZldPc3lNeTBXWHhONVdjakNYVmQ3RHJFUnFVM0c2NFdKUXRsRjM4OGppOThCZXBORXVDRHB2Cmt1cmdLV2grUTY0UGFQMHNzYTdYTEN0TytqVXBCNTJrTlRMOWt1d09DNExKZzJkSHY3b0lXSENlYlB6VlB1WGMKbHpkbGVuM3ZRQ3VHa1pUVW1pZjhjWWxaUTRoSkxmMGg3S0dnUThMbDBKYkljeFhTcU8zR28xcDJDanlNRFVFNgpYNlZqNENmMWRZYmhldDFmd3FPdVdPTmpXTGVnTFlTZmlxbjE2dXFEQTJtTFM2eUpCKzVsSDNDZGVaSVNkblBhCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFZZeExwSzlDNlZycWpFY3hnYXUKbzJPMWZ2bHA4YngxcVUwUHVMTlNzQ3NCVDZaTUR6TFBqekVWV2IxcVlTYTc2Q2RIN0ZPTkxEeVMya3lhQ0x0bgoxYTJDQ0JOVGoxTGtQbUE5ODhzVzB2Sk1uWW8rV0x3OHNWV21SZGZxZlkxVFRCeHpjT2NEbTNkMWdxaXNPRDN3CnFuVWFHS2VweElKQ1pkSnBpemlQemtSbVNRTUxiQVNGajdwUW5SN0ticG13VTNOaHR5c3Q4UE90Y29xMmpsZ20KeTZmTWp2Rml3dzVzcjVXS0V3azRSN3BMd3kzVFJySXV6NTFrcGdrMVZqSklpYlZlM2tLaTVob3hLRVhJSGpNagpHTGFxNXhHbXh1dE5yNUpBTnpvY21taG0xNTNIYUt3dHB1T21qekpYMW5NU1paYjRBMjJ6WWt0Q1ovQnJxTmNVCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0pKNmxtSUFUb01xUlF5VnppUUQKKzlTQ0NOZVptRGZORGRsNHFheDRNY0xwZlAwaUx0VlVOck5JOHhKRGpNbU51ZHdCNVgzSmk3OGdYMldveUlMMAprWDc2Y0crMGlmQk13bnhaMS9jVWgvVUo2Sk5uZ0hPUzZuNjU2Wjd3RWRLRTM2RUd6d25Yd2NNZmlLKzMrK0MvCnNwV0phSE0zTjJOMXVwTG5PZTE1blJxL0MvckIwdmc2dGhyK0ZDeHRrUzE1TDhUOTdlcy9iMDhTQytEMVpxMmEKMDAwRTQ2U2lDcXhaZlV4TXd2U0REUC9LQXBkYUJncmNnTFp1NkFIc3VVanVpcWxKZXI2VENOZzdxRGk2cDdhWApyL1dkZUdjSXhqTldsR1lOLzVtTFRSTGdOTlJPbFB1c2thSHRvMkdkREVQYWdwUUtqWGZ6MHkweTZvR3hvNlBXCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFBLL25HbnE2TVIvaXorUEhGbzIKV1M0MHEyYis2dmNJQlhZeHhob05qWVluSE9Xb1RlUUNLZ1REUUFPVGcrTGZMNVJCa282Y0FZN2NQMG9iYXFwQgpiU2NhY2IxUTFXNzlxbkh5SXQ0cDlDN1BpK1lGWUJOcE9YbFhRNk10UVZHSG1YK2lnbENuMHFSUWVoT0VabU5NCkRWSHF3NG5jTXhrcWNFRG4rWHM4ajlIMlprN2dkK1FMWloyM0FtNHBia0hSNG9YaUFRU2o3RVEzcXdhY1ZXTlcKdk51N1VvSjBJZnM0SS9XSzhReWt5Q3paYTlIbkpIbnFkMnY1MHFieENrb3Q5U3VBOWFibEVKdndTSkoxNGhFWAp2QTI1Qk1SUnBxNW1mVjBnZjlnUlF0WDNWb1MyR1NyaytOZ1hxZGRGOG1GbW5zZTZUc0lmZEVDVWREbDZ2bHROCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXhtYVlFckRMVVdScW1hekE4WmwKTElENjhJN00vdjVhUEFBUnZGVFZZWTY2dE96YlV1WWVSVGRMNFFNUU43aFNLOGtENXlqc3AwMnVHSEM2TGcyNAo3bUcvMHJSS01qNWFhWWJqWHh2QUdFU0JFcVcvMjVTR2NkZHJiUlpqdjMrelJhci9salYyVWU1OWtSOVJzRmNqCjd4VWE3bW9FNGVXUHhpVzZub1hxYkxQQkFQSDQ0UTJtc0ZPTXlaYWtaQjZBckEra3RWYm1HN0JZMFlldzNiby8KamhJeVpTZ0MrM1dtZVExQzJCVUhhakE2SkVRL09NQmp4aHdlbEplblF6NUVWMGVhL1FIMkRseGZOcHE0UnFQbgpOTjcrSy9mSzd0UGZWOUhYZWk4MFY4TlBuTjYzcCtxZUc3Q2RWcnV4MTIyd1RxRTdnNGVHNWxPRUpyL093RDB3CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXZxZnFsaDIwUUdEN0xoRStGSU8KQ2FTMEhldFp3bTh6QXFpVS9iRHAxd3JnS1A0TUJPV3czWjR2MEhXZkV2cDVBdUd3bExQYUxVcUVYa1lMNSt3SgozMFY1bGZJY0hhUWlaUFFDL2JoUHNBYjBKY0ZPQ0tMWnpzUXcxTmhORnNqMUcxUjBXRlAzYUpkUHJNb3FkdTR3Cnd5RGZtNnhYMG5oaE5ySUhiNTVldDdYWmpQMVJQWUhvbHI0ZTM3bWpwaUVIZEN4bll5Tml5ODZvRTVIL1lWdmcKNkVBaFFVVTh5YkhkKzQ3VXA5UWZ2UmFDSE0vbmRtYU43OWVmZXRKaUlZQ2l2RTU2OUFnMWRDREo4Q3l6dWdZaApLUVhsNnNSNnp4S0kvLzFHTHVJdzZNcXRGUjFXOVFrbmhiRklCbEZPQzZaYWloRHdGam56aGxEeWd5ZE04Mkp0Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbldpL0MwcFo2OWpvNzdrWm5tUkQKSTRBa3EzRkc3LzVzMFlXdGJSQlhmeGhnanNWU3o2Q1FQOE9UNHNFZjZJQUl6R21aSnFXMkZoOW90dVFDd1EyYwpCREZEVWNac2RSbk5rM1lxb1p1OGVNY0lXWE13ZkxvSFB6T3RrQU93aTRSeExrZjJYOEVqZlV3alZ1bFZINUlRCkpHajY4NG1sM3luQnpCc1g5YU0xRnI3Qnl4cVIxZTN3S056KzJiL0ZsNk1VenZQQjdMdEtzbC85b0NOS1RZN24KRlorS2FIdmJ1cU8vdHZUTDJEVW1PSUEwWDNKZ29uZVl1TlVQRWlLcStTZklmb1g1N0ZlNGxtaTJHK2IyYzdlLwpYNy8xT2hGUUI0NHJBaW9WZW5pTWhnSnVjZHdtVDBJT1Z5VWszZ0xZZnR5cmFyQjF0M0NDa3NzbGNzQ1k1QndJClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFFaQS9WOXU4c1VBMS8zemRHSS8KM0tGOUdIb2YyUkZhS2s5Y1JWRjQwcG5oWmNrOTdoV0QrU2lybzNPZzVxOVRqM1hMdUZwL3VScFBQZ28zYlFYaQpkSUlLb00rNWFObXE1U0JqYlI2MXZCeXJTejQxOVhkZTRPcjNQRllvTEQ4RnRuWWZyc25Bd04yanVYWUg0azRQClA3djQ3dkVBQkhxRmQ1TG1vRkxpaWUxU1pxcGxqMWxXUnlhS25LQ29sTGJwTG5DYWlPMlFlaXJ0STRVRHY3TTkKclVsVDIvYjBUdlltMXNQbWRCVWV4NktXYmhrZFRtN1ZJVW5ZdVNJaEJSTXZaUldua1BGdldvTWg0TXNzMGI2MAptbHJ2eG9vYldWdmRFYVZjSDY4Z093VStIYW5xejNiK0JSRkNwUXBqZ0RCcE1Za1owdFVCd2svWTZ4N2tzT00xCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUF3a09tYjdoQWpYdzR2bjhhSjcKUEh4NFBjWFJuL1A0TGRtN1RmWExzckZoM2o1Skw1NFl5dWNGQm95M1ZrczkzWk9BaWRLekdlM1lVQzlvZjlCZQpveVdkaHlFaXpMNWRmOWdQQmkxQkJybW9TWUE4NFl6SEFuazYwN3YwMjMyamJjZHF1NU15dXJOcFZLMWJTM3VvCkZCdFc2TnI3T2JUalJVdlYyNzRTZE1Ed1Zvc1RmZ2NIZnVEQTFhZG1wdy92WXUwcEtxdUx6UFdVazBYSmEydlQKVlV6TWFjVG1WMU5PTXY4ajRQUTFqUlRQanBzVzFUUHQ1TDJ2dm94b0pUZ1dnL0hoNU9iaU05Z2trVTJ6Vi9Ldwoza1piRUJMdDJXSG9PSnIyMCsvNG5LY0NpdEZRdUJ0N2VUNy95bndBU1pMdFhEcThEdjdvZ2FDMkQra1B4NVJ3Cm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXJBOGM5a0Y4ZDZydnZQMDFRV1kKV2RKVEY3bWdjNHdRY0UraCtyWDZyVUZpeVRYck9hbEdrUVhXQkFnN21OdUpJUURaYWZFUXBwRUlSbWtBckJ1SgpPRHZCR2lGdVIzOE50S3NTU0dSTnp1L0NGeW00Rkc2TGFPTHFQclpaaGpFSEt4WHVTQ0IxWkM5RHlLWjJUTm9VCnZkblNremYzMzRuQURIR2s2QWxKUVF2SkV0b0xDR01EZjdrb21SUFc5N0hUUGljYlJBUFBDTEtCR0cwTnZjbG0KZ0dGZGF3QUZIK2tPMjVXemloVGpNMnZFeVpWazhuWElYN2cwbGJQVndWQ3Y4MHV2QXJ5Yk9Tak95OUhjVW8wSwpHVXFzVE91VjBiTFg1T1RDaEQ3NEt3Rkk3UXZzQ2JCUExXSWwzMWYxYXY4d3BwVlpxaDJ5dTUreC9yU1ZmN3lFClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHdCUCtpQ0JtR1NuVjZkRG5aZlgKMFlTeFFpSzIxVm9xSFk3KzBkd3R2OWw1S3crYUhwbGZNUzlOdGRIcWdzSGtOek4rUFJ0bUxWRlJVTTVtOTV0SgpBSVExSVNwUWVTcFlaUW52NHk4SW14c2I4Tm1MMGRVbnRNeVNmSElaN0hORGw5dFFTeHBncW15NC9mNzBsRVVJCjJTM05DKzJMMEFzS09RbkhYQXdjbHBJaWkvY1hXWWxYZVJDZ2NHcXA5MWlUNnBERFF2YkdySHhTMEFLVnNpTEYKeCtia3JrcmlMRWVjOVk5cTZNeEZRMXM5VWtzL2ZwQTM1V2dqMTBLdno5QXZXeXlQaGozT3RMU0J6Wmg0cmx6bgo1WTRGWEZUZUVGSzJNSHRTMjdVdnB4R0NSNkttK2dYVllUVmJlRzRRcXo2Tk1sbmMyWXVvVlZMK3pPT05NcUZUClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbGRPcVBLWUpWQlY5dFFuZzJLbk0KdjNhSGM1MjFsQjdjYVM1YlBaMkd5eUtOc09JbTRuUXYrOGpQVUR5blFCdkFKanQwb1krb0M3a3pnK1NKQ3R4RApGc3ZGdFNFdVgzQmRzbXY5WGVENWlLaEVaUEQySzFiYWZ4dkFJbTRrUnpvaHBsa3U2eCt3ZThVa0pjRFhKRzdCClI0RWFKcUpnV2xzVjNSVXBKQ2R4UEV4a3FuS0xwRTdJdXVnaGR3MEdzQWRzeW5pNkRmNVp3VWNCaURSaXZkOXQKRElMeU1tUzdIdjRrenRubWlrTmkwU0FJTU1vSUxwb0FOUUVoRk9DTnV4YUkzZzZYVzFMMmhTeUpQSC9uRDE0SQpZTnV5MkpDdEV6ZUFtRi9QaDBjMCtUVUV3bHlvcS9VSGorSTlxVngxWVNTdG5TdWdHUVJieFI3M2Fad1JmckpjCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUtmWjlxK0V6NDFwa0xnNjFvbTIKQUIrdVR6Y2FVdkRrNzM5L1hJYm5NZWZzSUZXenFmRWhKRTJWRGg1NWVYelhMMi9WaDI5NzloaDNTTVdtZmdxMgpWR0Q5YzdkT3loNlBqWkV3VWEzOHVTN0xVeU12Y2wwOXZKMllONnJBNElYZzlnS2lHalU1OTRSU0U5a0tFTHdSClZMWnB6Z3g1dzRyN25XemNOMTZzaWlVYkVFa1pFNkJtNWQyMVBxZ3FxRTQySTBiYVFwV0NHUCtsbnVIbTh5Tm0KZlZuVHJmd2U4cmxTemVXOStUdmZJSzhVb0ViZmV0LytKTzVOSW9kbTQzL2ZYdEtqSkUxMXJlZzN6ZXlTUkxoZgpOZWNuRmlqc3Nvck9wVWt2WkYrVUxpMHBuM1NMOHdPZEJFbzROUU5BVTVCMk1OU0dVZjdxTmV5dVNBeTdScVpSClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXJRdGpiczV4b043dG1tdzVQc2UKVnN2TWJhVTc3QmtqK2ZEdnRrbjRpWGFhREtUbC84dElVMldwK2QzK0Npdkw2cXZFY0srY2taWlMwNVVqRjZIaQpjOU1SM3Vzay9OejZ2TCtvMVFpc1NJR0daTnRwSlJmUGNnNHI2WGZXQnZMWlZ4d2tGYjBQL1YyTVo4RmFRQ3l0Ck5vaktxTlZOZ0pFSnU3aXkwYTQvQm5md3B6aU9SNVYxRnYwbG9HZVE0ZXlqTUVOYjRzOERtNnBEWUVaSHQ5L1MKekdPcmdOWU9YUCtQeE93ZzNzTkFPaXFMK2RhRlFxMXBhekltY3ZnOWlhQkdhVjRtbHVMcEdFSXlyWmZUNUxJOQpnOHZycXpFNjVWQ01TbzRpVmFFRGFVeXNrS2J6Z28zRyt5dzA2U0dMaHFzcnZ2YWVFS29wL2pSSklTamxpdmczCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdllVL2JtQjFlczRqTm0rbjVLbVcKOG93UlBocE5nY2FWSXBCM29KZ0t6VVl2RzFoVmxZck53TDVJMjNwbklFbVFTdXVXdDdSSDN6MDFnWmVYTjkvVwpzcTBQbk5aOVdqNnNrNGJQWWVkWU1CYjZEZVpZMllzSitMa0xhQ1JRMEFkRjNpd0NtRnlzY0ZKeVdpd1VhV3NxCnlTY3l2clFUY2c3elIxQW43L29wc2JlV1NtTGpCNnYyUG5ENFNBUG5IaU5ZSHE1SXFtbktlb2pJQVlWOE4xd1UKR1BOTjN0bDhua1o2MldZOGRRcEhHNXV3ZzFDVEd3Z21ja0VuZTVFNFBvNms3UDZkMkFldEVLZjEvdEpQZXkzSQpRZWZpOWxBdWo5M0N6a0NSUHhOZTFNTkhFeVNKU2xyVEVRYWMvQW4vVHEzVkdtQUVtZ09hMjB6djU4eTl6SmU5Cml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbVVFQWZVVnJPU1VDVjBDUnhXS1gKY1BJeVJGVytTWEVyZDIwTzFpcDRKM2tKREszSEtoQ0tSWHQwYnRha2NaTnlTWFJkUnhBSlVTbGJSZ1FXeTFFTgpPUk96bHpkUlhOZEZCQ0FGUGcyRkxBUCtMMW5GUWZZbk9ZQ0hzdUtyc25OM2l5ZC9CYy9wdHBCbVQxekExT3RhCjRiMFNONjQ1VHpxREFObDlKWXE2aVZJeko0Z3Y2NVo4L0NEV0xiQlE3VDBRRkxPbXN5eWIzcUpZZVpMYWdIRE0KdllDamJKUWR3TUQ0SW4wcjVBUnBpRURycEZuVHNPSzlucUZsbVc0eFYrY2gvd2lTeUZiVGNLM1RsSGRSelRVKwphVHBnT3RWbWc0RURqRTdPNlUzK0hIekJUWUI5WHNiQWlPYmZZR0t4VjQvdkJncmxUUHZmRDhmZnkrMUNuVHozCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclBJR1BlNW9Sakp1eVRlZHVaR1YKZHp1R3NQM2tLYjdCcUNlN3lvSXNibFV3aHZSSXRXVlFhOFZTcWZIMXBhSVNQY2htNjBMMUdLa1hXVXVYM1VINQpvUm8vcGpvR3FqemRnU0VHZ3g4ekorSEZueXFVSzN6YXRDb2FybG83Kyt1Nld4QkREQit1RlljNjJJcmY1N1d3CkFJMWNrcjhZd0Q1NHJ5amluckJXbUVyQXljNjBpVkNBZUpFYmI1eUNkQ3NTUkhEVmFDeHFwN2FjUzVaZTVtcGUKa1R0V2M1SXQyZVMyRkhkQy9maHBzZ0daZDZZR0xsYnpuMHFlbk5PMVMvTGhVNnVjWWR1TVlkUDNHYmtJeDV5SApaeDFJQ2dEQVFjQ28xdkl4RnFXSGhDTXd5YldVZXpWTjIvOWtialY2cjRHV1VxR2YrRWRUcThCcU9TN2l3am0wCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcExhOC9wcVJ4bE1wV0YzaG1EcHoKSmZvMzk1aG56bDFDTVNiZEw5TmJ4ZjNMV1cvK09JSTN1UzA1c0RVb0VrQ2ZjV2xYcVh4SVVtMlg4aTA0Z2hPcQpJTXlRWUF0KzVRTFpNNG1wenpJUFlGMzBPU3JxR2JWS2lNTGFXWFdnd2E4QWswTTJOQUJ5UytNOTdJZ2pZN0VmCjVmNFd4QkdDeUUybytHMm45QXNRTkRqcFVZQkdPUm1xcXA4ZVU4TG1wZTJYK1kzbVhoVlNmVHMxb2NKOHlsWm8KRmxSeHJJckRaUngwL2NITmdOVVJUZmdGQlBtaHl1Z054VW5HMmdFTkFFaHJBMHFodXVnSnpnMWpJTDVCeWJhRwo0K1VjcEZVWThqemhKVS83eUNGTHVoMlRYelpPeGk1UEphRVRpSnRKRXVBSWVFYjJYeXhMUFo1bExURHNHUVg1Cm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBblBiMnJiazR1VWx6YmxKa3BNLzAKMHBwZ0lrYW1ISm9rYUpBYkJlcVQxQkhPYTd2UzZ0SXRMaHNHQVRxZGV2ejBHdmJvckI3QWpZSmhGMG5PTyttRwpnd3BtREJVdGxOMDNyLzREYTVHVXhvQVZGWS94OTU3b3o4eE9KSUhxTkVENnFQUjJ6ZGZpUmhoL3hNdHh3UFhSCjVMQnFaZ0xZQ0NSZUlkaUZkQmkvZy9IYXN3WmRZSXlWaW9WRDhaNHRQSzREUE1jNndHTUNqQmV0WWhCOVJOY0kKVGhVTHBOUFhzT2dJR09SZWt2ZlJYYklYV2t2ZW1QMDd4VjZDeWVMVWJURkk0bHBPdlB6V1hxRVovdjd4L3l3VAp5S0pyeTllMzBZR3BERzJEREs3YzVFT0p6RzBPQmcxd1ByUUV2MFUvWjFiRVBXU1dwbjdNd2Rpc3ltSHF1T3ZrCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBck5vQWZwYm93aG8vL3BKc2gxcGgKMmFJd1dDVHlnK3dZYjM2bmJ6WWF6cGswYXJUWDBHTTdaWExyN2ZjWWp4OUZDTzFtcU45N1hGTWNFWTROZHltQwpUL0R6akUzVVIwRmo3VElzbFdKb2p6U3o1OHo1MHArT01PajRSSmhNYjdVYUNYT0ZkaUtpY3Q0aC85NjNVUUsvCnA5UDdZODNhZ1JucHhoSDYzL3JGVUV1amtPRlJJKzNNK0NyWUt3eDFSOEZYM01sMlpoSzY1L1U5aWc4MFBkUkYKNGxpVEZ1OFAwbElyOG00SXBvU1Z4WWtjNWY1a3duVG9FSEpWL3dTMEI1R2Vvc3oyNmM2REIrMGlMMFVwSzlxVwozM3Vmakc5MG0yUVA2UmJnZlBKeXVNbG5nR0ExOUMyM1RSbTNTSDhkNlBIeUYySXdlUkJCM3picUl5cGxyTCtXCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNG4rc282UWErWjRZOTgzTGdJTHkKSjdIdVZKbDBiU0R2MzlzVW80Y1FVcVZxaVRpWDBGV0hHOGNGc1BqVC9qbnhGd0NJeGppUTJqYlFGaStialVzbwp1TFF4Qm5IUjAxakMyMWlncThMMGpTR05ybUdBMGpaRTdjcGgvaCtkcGtTa3JDdE1HTVNxa1drV3k4ZmdVZWMwCklIUWNqbGozRGJCc29icEhYOUNkd05vdUl5WU9xeTdIRU1CYVorbVVRa2Q1SWR1R3RjK3FpNUZFY1NRdkRPQWMKd0pUZHVRR3hpV281QU4rWkY0YUNOb0VicnBwQ2tjU2V2aWFXWkt5NTQxSUtDbUN6Q0l4dnVvQSs1emIxYW1tNgpFMmVQNmJRc3VTUjZpeUdoS21HM003N2t0YUVlTDExMXRLQzdrNWVEWE5taXNtU0VpMzdaWHB3N3F0eDI0dTJxCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbDNwSzN0MWNkZU1MSnZXY2FmK2EKbFljaTczMWxleWtPL2ZTQUE3ZmdaRk9zMGxadGdCblVXSTBXamxaMm1uWTZLWlk0Zld6K240WDNmYU1PTkR6Ywp3aTh0RjRhMUFZaWVOc3pkZCtUY2ZrWjRnS1BSTHZTTU1SUER6Zzh0RE9OcCtYREpVd2JlbU8rWEs1eUk1QThqCmR0TFZ3TDF6WHEwVUVvZXF2alBqOUtDQ1pnRXFTajgyKzZmdHVxc3hFc2hISFNrOXpMbUVYaERGODhqTmtiUDQKOXhFUHVyOStmRGZaSUZxeTEzSlc2V2JZMnZjSkd2c29VK2lwZ0JSSi81aGxvbENwekxjNUtjWUtUYy8zQTd6RApwc1dqdmJJKzV3dk1CRjd0SnMzR2JIN3IrakRuRHc2dTBFVURtakdTL0U2QUMxTlFNbDN4eEY4VnY0Q2FxbUhWCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem5RSm1CdjlWN3ZhcjBkZkMvRHoKQnBLK2R6Z1Btd09EUjJBdDRqK2pnSElzNlRTOWhvN0hnMGF0d1dtcks2bHdFQW1YZ1d6ZDBsRTFxcVZhVDdiSgpQbW5IMitHL1hYMDRRR0Uwd2RndVl3d1ZWcmtOZm5JVFJZUTJxSHZFNW41d010aW4zZ1JYTFlkV2hUcHM5ejBVCjBXN01qdDlmSGNKVVRGdkpML2hDWnpvZEZBR05DNXZIQ2U2VUxEU2k0MFkweVMzSWhHemFZaG51eWtRVDVQenEKRUdQcjV1RTN2WUNxUGRUS3VBQis4MW5iV3ZhcmxXYitRc1loU01OTW9TZzV4cXdReWlKOFpqeTV2VjR3TkhwOApHVjJ6enREbjIzRjY4dkwwbDczUmxNTEZrL2tJV1doRVNsdjJDNkhLdFkxRFJSVDdkWnBvUjluT2tBUU1xNnpvCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1FLRXBHclVOaHVxa1p4dkRqcUEKODhEQXJDKzYyWmhtTlNTU2VNZHVhd1ZxSURrNHVtc3Q0NEd4WUZsUUVrN3BVYitOMDYrNDBRKzB3eGdKKzFlcgpSdWNSMHZaaEtKRXY1eU9mZFFSUkhIcUhVeWdXNEdSWWNPT1huTVYveTJ4T1VqSWQzZCsyZTRIVUdSUXM1U3ZsCitlVjViYkFDZGIvQ0RTMlhJajFXNHl3QWNMa21XdXd3aEVJYUFqbnozYnE2VjR1c0JBcTdCWVpXRmQ2Q2lkRk8KVHptSmpmbmhkM1J3NURDY1BVZVp2YWhKdnVDTmdoZTdFRXk5SUlrTnN2eFhiNEthOEZOUDltbktUdjQzbG5PbApDSnBkMTdIaTkzRzJDeTJPQVBUa0IycUo2TlRVcFNENG93NWJ6bTN1YlE5OTR0cUNMOXZvcmsvK1VIQzl6UWloCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTIzTGtPeUxwdDV0bUhMMWRFamgKTW5PamlKbmhya0E3NWRQMk50QzYrY1BSeHRnY1ZHYXpISVV1bm1rNjdyamUxUFlXdy8zbFV6L0xyK05uU2lWbgpMQ0JNWmc1L0tkU0JZeFhTTmFrK0wvSVFsUlJ1VUROZ2MrL2F4cCsyYmVvOW9ZemJBWXlUQ3QvYU41T0hSblNNCnNnT0w1REJiSndiN1JuWW1XaXNnSitHakRZc2VVVDlMMENOTTdkc3UvelRaS0tUck42L1k2WjFJNWx1WUdVLzkKdzJOd1R0alB3NFJ3b0tlc21QdGhGRGpjS0pHZGxMNmpURGpwYUZNdWRIbENZcXI5TjQyeHFweXArdzdYeHBvSgp1ZlZkV3NGZldEYWdMS0sveWV2azFnVUFpYzN0RTJib1E5T0sxU1B5UjdGV1Nxd1RXVFY5eUVNRjc4VEx2QnJ2CkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN3ppYnc2SmdkN1lqbXVVQ2ovTVUKcE83S1RVOFdoaUo2M2tVVDgzaldhcEQ3U1hTZ0VydEx5OFcreDY3V1FWWUJSMWdSZnpHWlBVRC80SmY2TytubgpjTklnZ3FHdnlaYTJFdTVvQTZFUDhsTHZ0b05nN2tqSVZwK1k4OHQrekg2R0YzK0tLeXhjN3hYSjJYMVAyRDhGCkR4NDFQUzdWaWpJL1ZaZk9UTWEyUUMzOG9jTGxSR3JuTEtKZjEvdDh1cmFuRFl1U3hwU1BGbDc0R01Kc2h1NHAKR05yaUxFeG5VSnV1MVF6Rk5pbndNd2FFVE1BRlJreWRJNWtlQ2xxd3BUSHljVStxQk1NYzFIbDU3VGJlbHJXNQpoUFY5a3JKN3p4ZmtkTFM5eHdxVEc4QUMzUjV6Q3FnQmw3ZDMwNStpUFpyWUNHbzcxOFNmSmR1YndiaDR1M2Y2CjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1NqYlpKbUIrakJ0VWVQTWpNbkQKdCtnZFM0RWNPTTJwRXBDSzQrMWJCQnFkaFp0ZDZkVGpIUldtMjN2OXNzck1KRHQrRjYrd25uQ0NOb3ltWTVFTgpqU1hFUFhKcXphSEh0RlJOWDZ2U3M1OWQ0Szh1bUZSZzI5NW5vYlloUGU5ZDNnM3BSRHhvYm5mYmdzWmhjQWIvCk9jSjRhOHhDYmduS1Q3VWdUZnZOSG1UUGw3MGpRUXpLaDRiU3hGUVY5T0g5K3ZrZkRBVmJmd3VmWlp2WXNNU04KdkJkOW1ZZmcwOVJTRGpYM0UxdnFDTkZyU3BJUzFBZEZnOTJBZ212N3h5RmZSZ004cExjcC82amRkUWxwV1k5WQo0a0ZsQXg4SWxNR1JNZmpENWxLWU9OVThrMm1WbWxiREVTbmhJdkJnN29uRFFydXN6SWhWRUErMDAybUJpUmJnCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEN2MDRKaDM5R2Y0ZlJFOWx5SW8KNzI5cFJyZXFOQUZjQVczVDduS0o3c25LOEpWbGU4R2l0MmJIYStDN2FEMmlZemp0ZG5KKzVBcXQ0MU1lQW4yQQo0RHEvczNPaGdZYmtRaXlPbXFoZmlEMU1DS2tQR0JGcGpUYnp1YllWeHBJV3NLTnlONHp5amg1Qm51S0F2K3BtCmM0c05kV1NMVE1TaHBKUGJVRVRwVit6NEpqYTZCMDFRTzRqVEJyTTFDZko3QnNvRUh2aVdLMmlxbVBBR09zdUMKdWhvUUdTU25TKzNIRVBHdlNOQVoyU0Rvc21OWThUUENRaDlwRzA2TFowQ1grbWVQbUd3QytQL3hUVFJQb084MQoyMTFROWRETisyeWZDSVUxZW9pWjZSSHlPdnRJY2cwdVNGMXFEQ25LY25aVDcyQ01XaGsyNDJHV0tPMXlSUFBECldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWRtQ251aWV6NStXOTdjdG83NXMKajVsK21yY2YveGgyRklJZmtGV2daOXIxYjZCbG10M1pvQlJsRzVDQVdvUVkycWtGUUpUOXFiRGh1Y3RKME9ZUApQOGZiZWhMdkxZRDFxSGMySkg2c01GMVoxSFJxRVVwSWNxSGRsR2ZxUEREUlRyZzQvTDNDU005U3B4VFZlWkU5Ckg4NkxxUzlCT01PS3NlUWJjMkMvS1JTVTRoUU9RdHVWeG5QbnhMcmJVU1F2Z0crMmJxSEFKdm5rZkVvcE81WSsKNmw2OE9TOEtrNTNpZW9CWGxxY2JZNFJoUGhYZ04wM3RxVE05SkhySlUxam41YzVuWWMwVzM1ZGxoM3JBdFNEMgpEeUFlNisxRElucDlwWDB3TTNsRFN2cXBFczdUcmxab2pSNHBzN1RYTUZWaTczWldKbXRGQ2ExWFA2azMzeEFlCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUF0dTdkNEp4KzJxemVSQVdjMGMKaHVPUkptL2Z0aTV6aVMrT3VnS0hhMWxjckdyR2NteHhZYTZGbmZFeTZVUGFMMUZsNnEzTVJYUlRheHFuTXVGTgp3RUlVOWxKbzZjVmh6RzA3b3kxemVwc1d3bTNodjhSRFJBYTBBZ0pJbXEwdGc3TkR5ZG0yLzNpZWRIQkZQNkJ4CjE4OE1DNm1OeGZ2OWcyL3JsdFBwZm1EelkxZ2YyMEZxVWlpNW9qWVZLV05RVTlDRmJuOWl0STRKSWxGYjlIMlYKeWhnVzV1cDFIVURxV1o1RllMM0Q5MjlKWXdBdW9xNC82R0x5d0VPTUtwUm51bWdxQVlJN1JPZGtZb2o2a2dqUAo5bjhPTU5XUGdqSU9OLzBiQ1VNdmNjNkVPeUFDbHJJU244VEZCbFZCVDJNcmJOL05PL1h4Ukt3eGF2bm9SU01lCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN3pQMzZCaWxhNUVqUnlzWTZjQloKc0h6d3pjcDFsWmJpRmJaYmlIekFEVXZBVXVQcmpRVTNFYkF2bzM5MjdvTi9mWEYyVmlYMW53Ym9CanY0emJqRgpVNTNDOC9ZcUZEOXhuSkNUK00zaFJERVYxYVBvMWNidzRLdUZlVWRFQ2pIaVJDTmVJOE1UU0h5NlU2N2I3WWY2CktWd053MVhMK1J3YUdTUjl1NytINlFoTGs4anlhRTB1OEp1OHBQbkUwbTFoQVZmMFJON1I3OENRdnFZRFFZQm0KNzZ5dk5XUzBlS0tjYnA5SXlEcUlvd1VBOXNFZzVJbExuY3FQUTRlMlNFOEkzVk5MUDM4d1hMbkprZHZ3WWNncgo5NTl0S1ZJWUMyN1E1VEdDMUtMMThZdVN4TDdidTU0TEVRMENPSFNjbnlOZnJ5YXFjSjJNZmdiMmJOWm9QM01kCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmtTeXBLVEdyODBtc3VPeGF4RloKS0RIWDlJVGZWOGRCKzFmVFJFSExSS2tvVFJXUW9YY0N0KzNEY3hoa0F3dFFaTVlXaWJtajJET2xMWno2dE5HawoydVowSFVuVFU2WWI3Y1EzZE5yV00yUjJXSVVPQUZrWVVwcG9qV0RGb2ErSllqVVpBcHlHVSszbDBCb0tiSTBuCk1FdExQU1VQbzNVQ01ITTRRTGN3SU9mbm9FeFlaWG5ob0tldXNrZUpTeEFHd0QrOWlIUXVPYTVxcUhnVndnU0sKVWVPV3JxbCsrbldlUlhzUTZiZ0lRdlBOSDM4UjBBVGhLaVZBbkVxWlNrNkl0Z0FUek1sM0V4aXBPUnVEdFRtaApoTGRXeTRKWi96L2ZoTW9VQU5ML3d5bjNEUDVhUVVtR2tnY1ZDOVBJMVVvb0VUUjR6YmgybzFGbTNIbElJMUNJCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjBVRWtnZkJSNkdEUTJ1bTR3T0kKZHpTbk8vYmJZeXpnM1lFSUVaa3pwU1hac2VyTEViWWZLTUtJOHZnckV5OWMyQ2laLzdQcHZQWFZSTFlibWFvMgpmckd0TEZnMWNlWCt5emxxYk00NjVZazhnWlo4ZTcreDJ1eVBmWFNNRkZmTSs3TExFNTlHeVQwNWwxUlJ6S3pQCmlSL2FwZkNDTld3NUt6RWM0TVoyZG1rUWVLNlV1Rm9MVFZxQnlFRFVpaHBkY25wME53YWdjb1FZalRTdG0veCsKUlh0b0ZPb0llOFMyNnhoLzRxcENUMk9pNW9lc3F4ajNyc2srVVRXd2tadTB3SE5FTWs0N25UdkNVNWNQNSs2dApYSEVoaTJ0SUxHYXVHYmZiUmVCWW1RVDhwdkMrVW5tY0pGVDF1YzlRQ3lSMkt6MDFINzJtWmZLT2JUWklHK0wwCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTA3TnI4cEsyTHZ0ODFvNHpwdEwKcC9uTE9nY0dKQkVVdHZtemNuWkpFbC90R3VJQ2tTbHJ6Um5HS2xleFN6NklQckErOWJZMnp4UTlXOEtHSWxOZgp4bTdDNWxydXNjd2dIM0RJcWFpdmRSc1RZeGl4bnFEZTkvNE96TmUwRHE5dWNyekdXdmU5akpKcWo1Z3IwWlU0CjVkdVIwNE5uaVZyWnJLbUNYNlFMd3BHQllXNFdQRGtMVDc4U3ZHbFNyaVhLZStMY04rZnVreUNBd3BTOUdqSlIKa284RTVhdDl4d1dWWVE4VDJTZ2FnNkxCbzlzTTNRK2t0WkhmVlAxS0E5d3A3ajI4NVFVcERVR05LVjBrZUVmdwpoaFR0My9kNTlqa1lncUlWSzNZM2JjTkhzRnZ3dXU5aHhibklXcjFhUkxyWVBKbUxWWmo5bDRXOWdXMUR3MnJsCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHdqYnljSElXSmdJbWMyOVM4dEoKaHdYTncvUUEyNzU5cWF6YmIwRlZMVXg2ZXB2SnpZUWw4dk0wZjYvRG4yYnVPTkpWZDJOMlVESzZZb3RpSVpaRApyTThoM2F6SWFJRm9vajhZQ0c2SkNkZVJUV25NWXlhS1k2Qmk4eGZtVzh5M2pjZlVyTWlVMVRnYlEzd1JiaUNECklBRXFkTUVGTnZ6cEUvVm1HejUreGJkSTRVNHBTU3NIdVRFU3ozVlZER3JyNGZ6OUY5NHp2T1FZUDFjZ1d5Z1IKZjF2UlhVSUJLa2kxOHV0RTAyZlA1MDkyOWkyOTNpQU1LLzhFQmptZ2hJd256QUxlTzRMZ0NYYU1BQkV5R2t6TQpxTjQ2dzdhQlcxeW4wbjlRRHlUOE9DSVJoR1RxZk4rYUJuNmJMd3FXclhLa2J4Qm9zWkRPdjZ2cVpUUEFOa1ViCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUpLcGRadnQyR3hoM3c0Nmp1SXEKTFU4dXRCdEZNdXcwNWI1SVFoN2VHMW5ZTSt6OE53WkRzUXFub2IvQ21HdW81VWtrTEdVTUo3c3N4MVBNdW9HYwpLTU9UNVV5M1RqWFhURGY4aVhJOFRyL29McEw1Q1FkcU1FckFQQm54ZnhMUjQ1bE1rK3p0N2piZUxOcmpQRnFECncrK0xEOGgydlRFdjZVZ3dGUXpuaHMrZHRoRkZkaVRBMzRQYktMSjJ1cUFsVEx4cGxmSW93ckdPRWI1ankzQWYKS0hhZDRDR2lpSmRybVdDRk5OVVJJMVNIbFRSUnBud2JpSkVBS0V4QmE3MzZKdFh5SjJ6Z25vT3IrUDk2UkJoQgoySG1FOS9tME1qam52VjJSaGpiVUg5elgvNndSaDhkTkJhbEg4THp6QUVabkdna1V5dVFGSWYwaGZuWE1SWFlOCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3c4N0VZSUdGOEZDUk5sRjBQaGwKQzM1NXowZTE2VEdKejFUNnRYZHc3cktxM0lCZG1hMitVcXNqRlAwN0pPcVpocFkwdDBtNThZRkZwQlg2SmVpawoxT3VIbEVaL2t4bDlYdy8yMnFZdldwdHFWcUJZUnBURmMrYVVkczliQ3htUEYxT213dzFtRllkTFBDbTJFQkxhCmxUTmc5WXlQaVdMZzljYklackdTTThaWWV1RmprcVphV1R5ZjdHWk1GeHh1cG5YR2t3QTBjZVNvL050aFN5QVQKTUFTYkFGUGE5bW41K3UxTFpmV1lRNUJJYWREbm5XUThTQTBCamZ0V3FuQzZQQi9NcTF4RUZFcHcvQ09CWWZyOApjTndOT2hLelBieU84c1lhcFg1ME5LMzhlMFhtVE1DV040OWJZa3ZLMzBXeW03c3pOSmo2SWI5U1dabkg0SG9UCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWxIb2YrWGdncU1NQ29ZU3lFMUUKNjB0elF6WmF6T3lMSHNFd3A1ckFYOHo4K29haDFyblIwNHpwVVVlREV0SFROT1VxVVRnMElpMHo0UHUzQmtaYgo3bDBzM2ZMUTdCQkhwaXRhbHdpUXhHclhXeEJPdWZpMllzSTZRMjE4S1pQMEZKZ0x1TVdsamdHTmt6bjdUUzgwCndtOFB4Z0R1VUo2b3BPTk53UmludDI0aDFKS0F2UzZENDROZzRmTFdDdDlYblNsM1VrWG5INzdHZkJiaFFnTCsKODZjbmZHOVFCYVJhVVRRUGExRHVKRXUyZGRid3FpbTFXbmJVUThhV1puelAvWk5hR3k2a0xQNitkWXNtU0M1bAp4N0hSODRKWnNpWmdtaGdFK3FCbUNuUVo5TFplK3lKMEFQTVlIVmFLYjVBNy9LMlN3WkhGU2hjMHkrVVFlREV2ClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGYxanY0QWswanpuemc4MlF3VVQKQWt5eEdzRy9HcFdIV3AreWFubGZoSTl6K0FKY1NIbDJMN2ZMZGRsS3ovMUtzK3dvVy95UkdSU2pDN1dDYitlZworWGhaeHVENFJ0RE1sMUJVc0V5dUFNZnpwMGszOTZQM2dwTk1UV1lwMHRxZEdiUndnNUNnVStuMFFob0dOSENmCmZCcURtbzBrcnhCclZ5ZWpwcWNzd1hpeUNqdnh6V3p5VTlNa2lUNDlKZDQ2KzJQQXI3S2xNSkR1VEhEU3Y4ZysKbGw0ODMyMDEzNktJZzFCVTJZbElGQWV2ZG1Fa0l2WDA2YVFhb2Q4UFlub0JKTmViVytncldKMUFka2J0MEdVagpUb2hsaTRGMno4cVg2S3B1c3NuOWpuS2lEeFhNMjNmOUozWEdDUUxhbnI4dDBWL2FXOGxCTGVPcm1HNUVvK0pKCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2lJTzlvNk03MGtHTGtYcmF2RXQKM2tubC9TRjFXcmNVaHFrOW1mSUdJSzdtbFdXNjBpSXFLNmNiVkwyUWtlWHlabTRzOXpFa3dIVHp6bmdKTGc0Sgo3aUNrZDM3QWs0OUpzajRMUEsyVlh6bEhZejQyRFUxR1dzUk5NdE50UVFCYzZHVlN6bHdEa1A3RlM0UE1YRyt4CitTdmsvVVBreWl2UkZvdzR4czJJRUxnNFNKcElqKzBaSTVPUTZZTHVhM3RISkYxTGM3b1Bhdmh0N3pZMWxFTEQKL0FwYkhOOGtadWp0WklzZDZTZUhnSkdjWnBSc2U1N1c3eXdLRXJTUmdSQVlZU0lPTVZ3WE1qVThGSlJJT1podAoxemt2SlNoUXMwRTEyV0I2VThVeWdPaDllQXp1VWZaRzdHUjRPN1pzcmRhaXFZSjF3V0YrRjUzS01xN0dYYXc5CnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW9sR2trWEhIVGlNVjlISVp5RXMKYzE0N1JaTGVQK2NyaXZTMWhHVlFSc2VhZXRHc3l2M2szMThobnd1TWVhVVhoRFRmQU9QV2tnejNEM1FiOG5WRQpPeFd1UTZCd01TZnBsYlZWVUtzMGcxL2lOajNRdXlwdDRBVTFlb3JXckd4TG0wQU0xOGV6MlVuWmxNeUc5Y0Z6CnlsNDlkekFwSlZPS3JSU1VuTnlYQW80dEs1OFpBenQ4T3RlcHRTYmROaUFwT2xCRkwrcWQ3T2p4aURpK3ZrWEsKdU5CUDl5QnloNmhSYmJOdnpzcVlhcUlZWmtBUmNqNGh3RmwzMVZNbTMrcFNuMERUL1p2bkhzSDBKYUp3Qyt4MwplOWJSZHNDZzJnS1ZwYkdlQUw5THZPd2RXekltTGRJbkp4RWtRSjZVZk9TUWtGRmd4WGJxWGRvekdLdE54Nk02CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2xSRC83UEwrLzhuSGU0VFhmTlkKU25FR1JCdWJDcXVrK2J1YWpab0tJTjBvOGZFdDlsdk1laUNMOWhrcUdLUEJDaGVXTGVwWWtLTW9la1RBMEZKUwp0Tjczc2l2bVBaY0dPT3o1V0dFYzZGRHRKNE1Dd2FFTGtmTys0cGZJM0I0S0ZZSXBhcGZ6L1VMNU85SkFpblFrCkRNbDZTdW5qQWdYajZkZ3A4Y1RpU1J5VnozbXhPR21iVzR5Nkp4aUtrbDJkNUdxdWVZRWZBdEFlbUZNNEpiK3EKcmVOZ3NYNWxaSStyTzUvTVZZUnFZdlI2UVJMMVhWYThoN1loUEp4TlRFd2RmSXdidU9jcWliekl3ekZPMWJSawplWEoraER3QnRrVUpETHJUbzVLRWY2ektKVnppSHFlM0ZqKzcwcThWdHNHOTIzN3o1bVBTUVYvdTB2QzM3UVBNClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenVZZDVtYTFoRWRlNG1qZ21nV0MKeUJucy9HWDZTTEFRRGZMOFRsZHI1cFRSNnJ5Ymt2aUJmRys2bG4vSjM0aVprMTUwR1U4R3paMWtqRkVFT0IzYwo5TVgzSm1pNWlta3Z0b0JwcjNFQVE4aVhtQnEraUFnWjBSaCszeWY3UmVkVitmT2NIUlorYzFTVnlmakNLY1R4CmhUd1dMcWpBM0w0OWlZcDhtbnpTbktsRWVzOTU2M0VGa2JsLzlrRkFjNWVSMXNiOGRURjBGeWF6VnU2M3VSd3EKcDBDSm1UOCtLUFM3aW9qb3JFanQvNS9FQjZ0VkZ0MkpEUXp6a0ZGNEZWM3NqNkRzS0plVnduS1NJN0FLakc1WQoyeUpmQ3lDT3h5alBIUXhxSS81dE96S0Nrb0NrUTdSckkrYWdZMTM3SGJvaWtUR3o3S3FjVW90YloyQ3grZmw2Clh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2xQZ0Q2ZGxycGVuNDdKdytLTmMKYWtUTUExWEU1QzdPM21iZE1TYmtIU09VSFg3Vk5kbkJQYTNnSzVZamduY0pQekIyUm9LeXZzdWE2TG8yalRvUgoxYWg5VUwzQUxpMm4wWkpKemNYaDEvQWdPZjhoYzdSQjNUNWV5bnk3VWlmSWExZTEvZC9jMjE0Tlp4RmlFcGlNCjJ3TW9tcThDSnZuVE5kdlZGa3JXcVRneGpxMEJzTHRZSHB1dnhzbnJuS0F5SkUwZ2RrNzd2YWVJMFNWdmQrckwKUGRhNnVTdFk0aWhIOEh6NlpLVUptVWcyazI1UlFEeGk5NGx4UG1WZEgwai9hN2ZIbDcrTnZUYll3NFdGdUh4bQpkbDJRYXE3V0RickZzTGZvR0loZnd3Nng3NXN1L2JFeU5PRXpjY2JzalM5MllNVC9UeHZnUitQS2Z3Wnp2TkN3CmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3U0VGl1OG9qQnplVU1OU25YUzgKNUdRUHY4WU51S1VUVTZpdEVyK3crZW1NYlFES2tzRGJOcWw5c05LbHJ2b2doZjMyNFF5d2JiRWhKOGRVWkllSApLZzRHT2xITyt4WmlNVjJmQzZVZ1AwSnlyckFvdHNabkFYcTA5ZUl6b3ZrQWtsWjNZVE5hM2hBRVlybi9XbHBKCkNMV3Bmb25RQU5JOWh6WmNKclJXbi91eXJiVkc0bERNWEd2VXFwQW9Nd0JEK1NGa051Y2lVMWhGRkpyYzlGTGEKUW1wVGs4d2RNT1dyenRkSjRzMVRUR082L2VsZjg3YXNjY0hKV25ELzFGSHJIdUcrQWl5ZThLL1g2U2lBYlhRTApuMEt1Z0RKWEUwTW52cFBXQkNOOXM4L1hwYjdXQ1Q2b28yYkpmRmRyWGxvdytEM2tUM1pqeUFkclhPclNKbDZBCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbTNuQ2dJVDdIODQ4OVE0S1ZQTHYKY1dCNWdwOHpyY0txdmRIbWk0ZzJOblRYa2VCV3NOUmQ5eDlMbGRMbThydkIzd0ZreVRaQjFwbDBDZGtxNEU0cApPcDYrUWUzeWtkV3VTOGJiZ0JsU2FDN1pRcWp3WkxaT3dkQVlnVGhISEZRcEdDQnFFTlFWU1Zpa3BkcTZWVlZaClV3dHh1S2NPODdVaGczcHdqRmd6RkpxZFVCT3VxNlN6WStMc21KK1VJeUtLdDBSYXdRZlVLUDVLblNBRXFHSEkKdU9ldHdnUktjc1RyYys2ZkRZMHgvRStXTUQxY0J1ZXMwZHE0RjRoc2xQZ1hnd3RDNDVCT3BZNHNpM3QzazhHMApuUDgzTFIwMzdUMTRCWVdzWm5UQU5IVldnTkYzTzVWSU11Z0dzeWJTTm44MmMvS1NISFBRb0I2cW5Ldk1LR0pUCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjNlWXZZRnJtMkJGQmhYenBPbVEKakNXd0t4bC9acWhBRHJLd29CZk5veVo2UUl0dEVtei9lR2NBMzJTc3dCT2lneFNyUlljUTE5UFZpdEdMSnVOYQptaHhPRVdMQlo1bkdOYzlaRGRpdEMyYlJPdENBdWpCbzBWTDhNYTVuVlMzWHRlUmpZNGhrb0l3SVRPT0hBeVFzClcxWlhmWDNWL3JaVDdJTlJUU2ZQWWNSZ0VtK3BjZGRtdi9pUitIMlI1VzlKaHRGbEYrdmZEdWFjT2RZdXRIZWYKM1AvMVRIVGU0V1FTZXkwMlVxT01qQmx4U1B1cW1oSTd1WGhZUmNJRTRsRjNEaG1EZ2l0VFdtaWNDRUZienZFQgpRK1FtYU1mc2YrdGFuWjh1RFhONDFGMFkzTFNFbWY3N3dnUUJXTUU5UGxYaUNKSlVLUzlCNzQxZkdKWk00Q3ltClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzlSaUVMeldtMGZxaGZyTjdvTG4KQ3pidXpLVm1XY0pWMDlnQTFEVjhvdjg1Ly9FdVNUK0c5RjJJL21oM0k5MDM1cE5HLzJMNGR2WkdHK0R2V0d1Wgphb0lyc0NPeEJXcUI5Y25XcngvT09RcEVTTE1pV3pvcXVQa2FqSHZFeEc2d3ExTUdLTFVTa2hpY2pNeDZjZEdmCi84b0VZc1U4ekIxUWE4K050Rno4eXVqTlRQdElKMEI3cFVOUlpDQ01lZ1lrbWhmMGxQVUdsM2xoRGxPT1dneW4KQTNuRHlZM043aFdFdWVmZnplTEoyVzdoYk5JcEl3QW1YM2phN0M3NVUxeWlaQThYa1ZBaU1STmZaSFlNOVdtZwpwRk5SdWlyNnU0NWZqR0ZkemY2dGU5Qk12Y0ZhVzFtZ3Bac0F0MGYyRUdVM1F4MDd2THpWbitCcjVHZ3FEcUVFCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmVtd2ZYdGYwcmE1c1ZUQ0YvaE0KMmQ1OTRQMnljRkkwV3lzV1VzZHlxdmo3Yk5VMWZHWkFEbVZOSXFwcm9GMGxUcVdvanRYOGRBVVIwMDFETlhsRQovNXlXVkE1d1BVSnc3VGJ0bEhndWJuclNUVTF6RHFIeVk4RTNwZHAwUFJqd2lqUC9FUXpJQzBvMUY2eFRUUy9CCkFKd2NJUnJZZFUrOUU5aDBOa3VUQ3ZpU0ZQanFES1NldDNlekVyQzNSUThUVGp1UFQzK2tLdythT3E4N3ZSTUgKK2tGekE4V2dtdFRPMWQrUC9xTUl5Qi9LQWNLWGZVU2RZdmNhSmZkQk5RME9UY3Q2ZmFVbkQ0RjJYYVBoZllQSwowV3MvTGQreE9jdHh4UzhRTHBNWllZNll5ZHl4d000ODE0L2o4TDYrdFNWZndyOTh2c3pBNTZGVjA2c3ZSeGZMCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk5uWktDdG5Gdmh3SnFSWDVPdGcKcXZGVDZZYmVnQXNiYTlheTVxR050U1F5Vy80eDIvak9ldFVBS2d6RUQrK3M0VmhkQVlvV05PK0FZemFRRGNLTAo1dzhxT2lteWw4blpLNHhoYTQrNE84cGwwNzRxR3R6V0RKNThrZWppMWQrbFRCV2RHTU1nSTBmZlIzWEFXekRiCjVMVVFyejF0RmFicVVnVXNzQnlxMVVmb05lS1JwUVVhTDBHakFEV3JBQXlpSkF0RHBWWkVlUjE5S05weUpHYUgKdEozOUxMK3JScE9RdEtBeW1ubVVjcW4rb1BGSVY0c290Zk10enBQdzJTRktNZTNXeWZQMHNmamxLMlRaWnF1Kwo2SFBkVVhQNXVYbmdwL0dnYU84QkE3U0tHQWZLSkJWWk5DalI2cjZKZHRDcEdRb3NML3hGanNnZm0xaHZ1RzFMCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmxJa2Rid2liQlVkOWZUVkxPWmwKTnk1cUNFL25zNnRNdGlkSzQ1a3dJd1FEUEdKRkNXcStUY2M5VUNnMzVsby9pek5TZDhOVVpvQmlrSE1OZXVORAphc215MkpGU2JyeGhlTXl5OUIrem5rR2xSeVNXQUxxYTVKdHJQU0RCZ1FhS2J0ZTdyY3FWUmZpeFBPVGE5VDRXCmt5M1BvaW5MU0hLako3cHZFL2poR2UwVXVlK3dmd0NyWlZPTkk0N1ZuUG9Ed3BVK3gweDlrQ1duYUk1WVAwdm0Kb3dhU2F1N0FpS2xyL1p1WVpIT3FVdlV2L2dWSW13VjQxOEtyUDcyWDBxWUhVeUViY3p0b1AxT3BHa3FnbEozcQpqbVRLV0tiUFpiYkU0RUREL0xTeS85TlZ1UDNaaEdzakpZU0lyUDdnUUJvc2k0L1k1MUtIaklrdisrbnZySjBxCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnZHeTFYZVhGOURoVURMbFhmd2wKdUFVU2xMUFZLbWZYcHEzNlMrRFY1MCt5OVJIaGMrbFdJcW5MOXYwcTJmYmRoS2MrRWpMTWZrVHFtS2k1K1k4ZQp6MlFObmtzQi9EbW5vcFpNemcwdi9mQXNMQjZRdXc1VjI1SUNieTlIbE4wUGNLVWRWYWxveEtzYmR1WU8wTVBqClJ0NjJuTHJKVTVVS0lJU2ZLSktITnFPVnZOQXRJRk14QktFUm5lLzBWRkNDOUZNNmd5eDlOdHdBdURxbUpSbG0KRFZSaU83cTRXblNnWWZlcFdNbjZqMzJUQmphRklseFI0ckM4SVBLL0N3L2ExbUdMdVp1K1YyaVk3S2ZMdU5qaQpjRXdWVGV2ZHR0SzNCVUQvZFBjZFQvMDkyY2w5Y2JjRXBrL29aMW0vVFFoaDhGLzhUYlJaL0w3Wm5tOGxBT1RkCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK3VYamJyMW1vZEc2b3BQY0dIQ3EKSHlCYVgxamc3UjNxMzNJUEpKN1Y3eXRZRS83YXdaalpRczdqVnFZQnJybENDT1R6QW56cC9VMjZxNGdVL2FLTgorUHA2SjJZT2Evd0V2eTlJMXlsdUhBMm9XbEozeHdZa1lmNUZVMFJ4NTNTdmFwV3BBMTVGQm9WcXlwVnJSUFNlCnJGYjNOekNTd2xLcXVBRUg5ZDZ5QzVaYVVkWmlYMlFBRkNZK1BjVkJIYkExMjhuZWR3RHhPc1JtQkNMMFVLYmcKMUtXRitiZ1cvQ2FubVZhYlgwZXZTQlo1amRNOFFRaE9HMFlITy9LeFAyZ0JJbStyOHZzdHFmbkRTRmw2M2l6VgpSYStqamxDR3M2Z2VmbjJRcEprT2tPS1ZIbUgrdVkrWWRYLzNQaWJ3QTlCS2JRRFpEMTM2TGpyNlZCRzhwbU9QCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekJVeFQ2YmdnUjAwL1pGQ2Vjd1oKdGhNYUJWQjA0YWNCQll0Z3daMGRNaURQTEpWWGJ1V1hKbzBLNnE0eGV1MGZtQUg0SHp3bDF5dVQzSUVxZXF1eQozeFU3NXczeER4UnJXTnlTZmpsNGpPdk43Q2VocWNkcWJnWVl3MEhCV1Jtcm83bjZyb0RyZGpQRURLbnNZYzV0Cnc3QjhwVEhoekdCeDEyQjhmRGZHQ1Z0ZnJqdmJWTkU4SGdvWmphTnk5d3ZKaWlHZHhSK3BEbGVpZndSQ3loRXMKRGxQVDl5Slp3bFQyTTdhZCtSMXhZSzRxSFdmQTVBaEdJRzdFRk9DT05STnJwcTJiQ29rL3BUUGV1YkdHYnl4OQpoSzBGTzVpZGxIWktKWnZycFQya1IzYlRsVlRMUXpyTWkyUG1lclBoSE1Md2Z6QmVsM1NxZG0vQmlFZlJ4endrCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGE2dkNWeU1ReUdwVXdSVm9sa0MKWDViUFFCRDhZM3M2U05rLzRPc28xVEh4TFRBKzFVaWNnR1dWdWExZXFJcW1McXRhSjV4Y2w0UzBQRGtsd1VoSQpMbDhPMGpYYTdGRGJVQTM5K3FoWFp4Y0pmMHZSZDJMQnB6UHJ0VUdNclVyMnRFdnJyV05MSm1CSXdnUytidm5uCm1EOHJyMENSY2s1U21SUXJkQW5xUk42R25CVDZGd3ZFV3Jzb0Y4ZjJPdnBTV3RXRWlMeFJhaXh4WnJMaW5iUDkKU3JubVVZdC81TktuQnhrbGtJOEdRZ3ZvZGdpRnBQOUhqczUyeWEvYVNMcEJ3azhaNjArek5VTys0Q3RYa3JYTwpuWjlkNFE0enN3TW5tQlFJcnJHZE44Q1NxVEh1N2tZeXZnRVhLU1FIYU1xeVV3bTU3UkNEazJUcGxtS2NOcS9mCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVdMUkR3WHBHa3dPM2R0ck96d3QKNzd0ZHRPRFJ1R2ZmQWhFZlJ1S3BwbFdEQXVaR3p2cHptN0UyYk5TWFVzWVRjSml0amVTd3JqUjNXQ2tiZXkyYgpGK3RCNDdva0FuUnpRa2dPNTQzZzFIRHRxc0ovMklWOUFsMW91VXhWUHlrb2JHa2hZcWRqQjZzOW42b1dVNk1lCkFXVTlSM3lCS21CNGpXY1JBclhyMElnK2dvUEpES2t1Y1pkRVlDTEVFbUJGQzNFL3ZXME8xYnVmYW9laXU3ZHQKNGxIYnJjeWU0RkV4VDlKK05meUZRcENxbGFnaEgrMGRFZy9uVExsNDZ6K0Fhd1ZhTGp0Ym1kaTdRa1dZTTJzNQpweU4yZUM2OVRpS0RERm5rN05pREgxSFJvWCtmM21uR3dDV3JJeFlFVVltK3NaRnpaUDk0SDJYU2VOVHRYdk5KCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEQvaUo3c0VPOGM3V2ZWSExZbEQKNVNiT1dxOVVWRktzaHdjdmtKT0k4Z3BjbU1PcnJrRE9qN2hmQi81RER1VGJqMDQwWlVjOEpBZG5pYnBUVWFFaQpUMGYyTERnUDlHaWE2WHF3S3JiNG9uOUJtUnRseU1BQlB2TXNYK200aHdiSVV5bUtPeWlxYWN0ajJUSDludHNNCnpjRXp5aXJqditxdSt3ZWlLVXlRcGNmVFZESnVQNjc3dThZRUZpV1IwaDJ2MUhUV1BxWTNMRDljN0pBTGx5N2EKaGJUYTdON2xycVZIdUVhTHlpSHFoTzJwNTUra1RKR0NaWkhSTldFU0lBQzIrNVA3dnk0S0Vic1VLaTMxRWpoVQp3R0ZDSE9ld1hZZEh3V1NJSWdpTlllL3ZZNmR3THRGMk1tZGtxM3lVWHhmQjJEOWw3enFoWWFTNkhWNmpkRElZCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVF2RDdIV0NNaktFNEZVNlZaNkcKRjJYWGV0aWExQmVpVjN5dXBBVURHYTFkdCtSMCs0RzVCM1psdk5SMjd4NFZGM0J6a0RLN2hUZG1oV29MOUo0aAprWjE0RWpGTHpyMmZRc29MaXJNWXE5UVVLYjJnM0V3WENheWh2dXV4alJnTlFxcDVZTE1YUm56K3JUNUEycWc4CjhaUWpPVkJWUVZ0Ri9LbmJqVEhGdDd0YkpzY2V2V0V4a0hyQnhOc2NqRC9JWngvVEJ6QVhpbDJUTmdNcUU2eWIKZ1FZTGw2YjRLOExwOW1uWGNtUS9HMnduV1BBMGQ3TDNxbWloRkE4MHZCanVNbVAxR3F4MGdQaGtMeFZNZGEyVQpZMEwrS3daRnFhWE5RUkVWV1pEM0hpMWFrQXdPcEtidUpmekhDN2I0YUhLZGhHNzhXblBac1BXMVNYTHQ4QmxhCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEhYa2FlZ0cxbU4rdGM4MkdvSWIKNFZVc1prRU84RkFsbDNwdWc1WVJoOXFjamRpd08yWVVvdjQ1dm9XWmJydDE0clFXWHlpelgxUnkyY0xUSVFReApod2d5QUFUbzVkVVJPVFovTUxsQ29HNzNvMWNaNER4UjlmbmNVUEVIWG9mQ2psdDZTYUJQQ3VBbUVseE1qRWp6CnJxWUVpSitOU0VqNTZkSFFJM0Nua2lmdExBOE5ZSDllQzNXMW1XSHltNFdjZW9nOTcyY1p0aHg3eWNmWlBheC8KWWE4L0lGcmYzTDhxd2wzd1FLM0tFK3pXV2Y0Nzh1QWRyZXh1dFdzcDdoa3pLbVdPNm5xQ2hQR1JiT1U5VXp4RwpYd0FYK20vd1ZVQmxGdkg4MHFmcnVSWUFsbGE1MDh3NEhabXhWNFRUTnUvUzhaVm1qQ0RvZFhYa0pjK1E2RE9HCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk54Wkc1aDVOR29VSTAwc0pjUEwKSTQ5VThOT0EybkdqRHB0TXQ0ajNEdDhOUkg2eXp4SnFCSytCZWptY2pEd1Z0WlI4K2Z0NDcva01yR0VYRDJxbgpxcXdSeVdTbVBrZnQ1angzbEFtSTZaOXdkUWUzTzNIUllxSFlkSWthRURKblpFZEsrVGc2MElYNjhaUmtQbEVuCkhDMUFodkRwY1dic1VsTmtyQmpZeWVxVWtRZ0dob0x3UmljZlJGV2xEUGdEVHhmMTJEckZyUStrSUN4UEY3NkkKalByTHgvblBJUUJxTDEycCs5SmtoUHIzNmRCRjhtcHFaa2RwRUd6dHMvaG8wK0R6TGlSTEZjR1JENGJJcStaZwpBcit3dko3Y1dickpkMUp6SlltMFBFNWFZbEVEMXVTRlNmSmtuOEdnYnQvTm1HMndndWRRZXcxckIzNXh5N1UvCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUx1MnhKVXM1Nno0WnlpckplakEKRXZsTkRDc1M5N0J6cTVQS0REeTJTSlh0dTJsWDBKY0dxd3RPRlh4aUkzTHBQTkxrNnRTWEJUOG9JY1pOeUZjSQpYdzdGMlBqR2VjY2ZjTzZ1V0FNQWVrc0xNc3hybFQvZ1l0VVRoMTJ4ZlhkT2Y0Ty9URndtNXQ2L0J6cTRGNStyCjl4NFJHeUtBVCtPU1crbGJMQXozWEFKYjV2RGhmcDhBS3RpNnNQOTdKM1FqVWFRa1p1WVJwdXRTdkx3SWY0T1gKRTJaS2pYMjVZT1FpNW5JTy85bzhUcFBSTzVEZlA3NEZLdGQ3bWZqNGQrTmxqNTc4WVI5WGpYUFphTlpuaHVrYQovelc2OVFLenE1dldsS0pwazlPYkpuUUJNWlpNUklLY2VwczQ4NGFqSkJDVWdGaHFFNTVRYzNnTXVoVjJBbld3CnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczJ3RFhVSGVlb1hmQlZqV0RXbHEKKzFRZlg1STh4UDRuU2FVRlNzdkUvSXJxVVVYREcwSG5mQkNnTGhhZmVPbS9PN3gvdjgyaU40cWFKWTlVc0hFOQpQT2I3aDRzc0xyNUFxWEpRMnhDQWFuLyt4STRQNWpQZkpUVStZSk9SNThzSWdvUFBjTzEwMTMySFpSbGFDZTlVCmlWZjZvZEZZKzFqd216S1hMQVFHdmk3Rk5ndHpUaEVnaHhxT2ZVWFREMFFFczdZTzkyRERYL1VLZGZEaHo4UEQKU0ZxRDc3dWxuUGxrS0tDaFdZZzlSS3FCUlYyWTBIWGxNYlArcDExYnRkcDZYaEdxSXk5a3hZdHZueWFZYThhZAp6NFZLWWl4ckVHMzVGbE9BK1o1ZzV2dS9PdU5ndVBuMWVjK2tMWlpxeUFya2NKMy9heE1aOXl3WmU0a1JBN3VDCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWczQmMvTGN3UnJSMm1VNTArYnAKRitqaWtuNWJnSldrZW9MdC9aMDlDNVVMZ083QTB3SkFDTkFrV1M4S2hZZUxHYU1xYkp6QVdTYlphdHRJbE9JWgp4d0NSZWtqTEI0N0VYVGhwbTAwM2dQdzE3SGZVTXJaeWZldmJBbEpkbW1LK1lTaFFBMzZENTR1YnFkbkRFYm9rCm42MVJrVFlqVDFoT3NmckkrV3BqNTQxSjBWZU9qaVd6dDBCZW50V3FSWktHeDJ4YzJkaWo3ZEorVUcvOXRqbHAKZW40MkdKTXdWTFNlVXlXVmxIcDc5aUdxcUhBZElZUG0wVndOUExxTEt5OVFTQUp6K1MvMGE3d3RqbGxuZVNOWAp4OWJPbTl5U0hkWmkvTExRVEVuTmxVL1JDUzYwZHQ0WDQ1N2x3cTZGSjlSWTEzdW85TXNPNkVudFBLYVNZR0Z2Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenpWcUhNaVJnb2hoekV4Wjh6RWUKWDc4TVF1ZjJSc2QrT1drL3hraDRnL3BsYnJVQnpoVHUxdEYwVVkwdkhsTCtsS0x3blJTRWM2Y0tDaTY3NlJIbgprYUFMcTdFOUs2bU1kbDY2YXE0MHpTVks2cTNnYlJ2aTdRTmJBOXpWZGdzc3dCQlJTeFl2QitTU2cwQVFSbjdXCnp2TE1YWGlRM1pobDZ1Z3F0dmN4T2ZDNzVEdUVsc0NrTWlVNmpIbHNKeWxVaC9TWjVaVDlWZGN6SSs0M2hnQmoKdVpSVHFGK2lMWTU0YWtwOGIzaHR5Qkl2UUJ3Q1Q0K1JzZzBLU3NTZ1Y1WmxoenE2eGtTaGYzZVRZQnZxODFmMQpDK2RBL3FScHpXdWlmejlSU1lpdGZXWlpxZmxURkswanF1Skh6UlpxZFFCNHBOb0VwMGN6YlhVemFQeDNmeFM2Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk03Y3YyeHlLUHdGdWtVYnlZWk0KcHcvK1JVdHRsc3RoYTI2OFN6bHI3THQvK0NuZmdxRHloNkJUOUZTOURCdjl1QXh0UGJoMDdDVlJCbDhMbGp1UwpRUjNRWjBRdTUvYlozZmduelNlSjlUK2c5ZnhibXpsZHBqK1ludGpYSE01N1UveEF3NTJWZEd6Nk9ITTE2RW1tCkVpLytSa2JzUE5oTWphM0lBMkZmUkFhS3pwRFdHeVRIN0hVZU9ucTRCUFNoZFFvNEI2S2NhaFJWUCtCZFF0OEUKeEFWMGRGUHVmcis1bER6RXFzckNxcmVlQ3hFTXo1UmhzcTllWjhta3ZCVC9WL3FWQVovZnRlcFBlS0xWSU5KbApTY2RVZ1F1UWFxQ2tZaDhIeEdKa3lXd3ZwMnJ4Z3JORy9hTCthTjEvdis2eGo2ck9SVnhsMjFBcWZhL3lOb1RECjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdS9FZnZSenpHalNUSlRudDgxcnoKbjlyMjJobHF5UUIxcy9vQzFiNUoxRmRLKzAwKzd4d0hUaHZPN1hvWk5QSXNwYUxReUZQK1oxSXpYdlhHVVVKRQprV3dmVGdrcDBLZGlwN2JkRnplVzMzbWdvM01RTyt2dEQxb1J5OXZ5U0N6bmViMHI3UDN2YlFkcURwTkFqclJUCmRDQnZ2dWpKY3l6cDdQYmljcGF4N1pmcFdYd2FWekQ1UXpESE9uUWZzeTZHRzMxTkxRVW1zSFJOaTN2QTAzQ3QKdkpwbk5pVmJkL1FkYXBoUlI1U0dDdVlBZ0pqaUE3bFhvcWQ1cm96cDBBU1JUNEJLVDlGcGJ4S1FvN0VMYlFKdApzN29HVG1ERU91MmpLYmNjMmNuK0NRd3RjajJiamg4a0k5Rm9mSGZsdkdLdmlvRU16RWxvUUxVVEVQVmZsSTN2CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTVObmVFMVNBQVFOVWY2V3ZIaWEKbkQ2T3IvU1g5WmlNT3VOYTJUaUduMXBFT2piaU5EZWY0dzBvNWtuNTZEcVgzWE5UOUdQcXhtYWlHTmdmNjJDcApJcEN5cExUeS95UUQ0UUxNUnJpWHVHYTMrR25oM2R0ZGZmZHFXTWxOUzI1TnZwb25KMVJVTmwzUkpJYldvQ2tPClpDaG9DMjFUaXBnc2RzRlM5andCWnplRGxyV0JFQ2hQWUdTR0JLc2Jjc0hOTlB5cHNTbHlSOHEzdXF3dmJ0L1UKSTlSZGhLRW5VNGczVlVuMGx3eHBaNGFwY05hYnRFRncwOXkxazNDNFRENTEwQjYzeERhcFpjV3QxRUNOM29xSgpTbTRBTVkwMHNzZkM1blgrNHdyNXZQN2Fsa1dHUmxnbGN0L0hDUFdDUVJtSDFIZm9zUS9YRW9DN3kvTktjM2ltCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBci9vR0pHcGF5VTE4U3BnNldRZGUKQWhyNVNjbmQ2OWpnUUZpQzdCa0ZieUZUM2g1RURSOFE2SVFSSUgzOWhMMVpTdGlEamwrbkkreDlHdndrRVN3WgpJZEk3YnBQQVY5T2psQmtacThvYms3blJyS21ZVUZMYXRWczd1NGI0SDNFMC9WVWJkMlVESWR1ZnJmcDRZa2MzCkg4eDE2UGQ3K3N2WU5EMTVXTzN6ejhsS0QvUUI5RmR3K3U4RVIra3BBVjYyNlY2M2JpUzdzUmczTUVGR2xoQ3IKc1dJNUhCZVk5SmNMUkVtaUprTTJmRXFiNk5MVXp2RWZpdUlKQ2UyZmJISjlRSmhSd1ZTQkhBa1VpK2tsM3dVUgpXNkZDSkF4KzVXL2xDZTFkRk5QbHhmR1lLMm5nTDdtTWh6SjV2SnRHY2lEYXBXNE9ZRG1yN01DT2k0cVNBLzR3CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWx5c2ZvU1ZCNVdQV1Z6OVRKOG8KMDNNZVNRQ0FIOHMyaHJCc2FlR2NBeGZVNzBIMUFzZDZTRWtmQjlXMkpqZDJ0WUZVRm5iZ3JqK1NPME9FQlBvRwpUTE84aUFGK2tMdm9NVXY3NFBqSXJZT3ZOTkFPMnJBRmp4NVUvSTE5MFRub2tqY1IrN3Myelhlb2NDeFRYS002CkxSM2MxRldYTnRqT3BGeDlTWmZsK1lDcDc0UncrRXV4OXFSVHAxTXp1UmhteHdnUnNESGV0OE1OMFpZV0daZ2QKVU1iaTh2VGE5UFZ2U2xrVmtpcmUvRkJPb2Qvcm1TYmRBQ2owZ3BRd1ZZUEk2L21JMFhkZUdla2RCdWR1NWxlegpTTnZrS1MweEM1QjM4VWxsb3lxYXBHWlVJZENRcTNtek0xSVRsZDYrV3lqZG5XK3Z3LzgxUm0rSnM2UC9aKzRICjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTI5SGVuVU5UUmdqcFJqbWplRm0KWUZjUm5qbDg2RkVLV1YyRHhEem5VQW9GS1MzbXVqZ3NKNUVPV0pxR282VHR6Y0g1NEY3RVZIMndseTUyNVNYVgp4aGZUTzlJaWlMdDZqSWg4ajZsY3kyUGYxVWFxNGhwR2ZWWndBTldtTEZNUjRQbXZwaXVBWHFZUVVJMmp6c3o5CmNsVmczU2lxUXNMU0NQazFzbXYyRGw3MkQ4cHVpYWo3ek14dzBhaUw2RkVRRDJkMHpjZnhKN0lDYmpzSk54MGsKUkw0S0RYdVJqNkhnK1ZrRThhazR5ajgxTlZ6THdBRFhFdzF6emNZSjNWZUdsb29OVXFIU2VIODdBRkdCZ3ZsWgpHYVM2T1J2cy9WYldvRWtDazlDWXR0S2pTcEZLN0J4Ty9md0lzK3hWczdWOHlaM0dYQTlBMkxpeXV3UkppTmVaCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk1CQWI5aTdFaW1VSlpqdUl2UEMKYXhXS292MDdBbjVFeU5wYkg1aVlzN2hhM3o5b0drN1Jxb09qT2E2UVlROUJ1bDcvRlE1YzVwZnBFU1Q5OEFFaAovMnlUeVAvSTgzM29yVUUvZzZiV2JqUm9jR1QvSUxPeS9mY2M2U0JQbDk5aDNwNVM5N0FCd0E5cDFIZ0kxNE5OCkx4ZU5FeFVraUwyODE0bkNpL2R3eWpQQkh4RDhkdEhYQkZGQWgxWlFGS1o5TEF1Rms3OVk2TTZ2OEpSTTFrMDYKQm5TZUpGUnJOckNqb2R0MElHbXl4cEx3ZzFpTDVHeGZrN3FBNmR4by92MFpEeHpuSkorWXlvalNRTXFVWkZxNgpNSXk5dWpSa243UEdjYW1YbEt4a3ZLSzZTNVVGVDI4M0Z0aGMwY2R0aEcxSytEcnYrYTNsOW1IdHh0NWY4RVhPCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGFMZUtWZHh3dGpWT1NOenNMV1IKQ3VGWEZnUEVac2lDaXM5d0xOczZEZFBZdlYyTW02KzhySEw2T0JBUGZPcCs2WStpL1FBT1hHM3dIRTdxRmhVVwpMamlQN0FUcjhrdnphSyt6bnFtVlh3TzE0ck5iTnRDOCt0OTlGaHFMeHNzSGZUQ0JaaFRSbkxXMVNZRUNFa0R5Cm1JdTRMQWVsK3JhY2ZoN054T0pRUS9PSHNHeDZiSitXSVFZdmFPazN4STZncW9yNjA5UHl0bk42cEJ2K0ZIeGEKbThLcVZLN1h0TDNpV0ZsYlozMXhvYmZlMU1pM093UGRRTnZHeDhHTlhpVkZiYW5NOTJtbWlNc3NiWTFzckxrQwpuQjQ2U3ZPYzVFcWNPb2tFcDQrbmRpZm1FWG5IdDR0dXV1b3B1UHZoSWlobkhTYXhNM2k5ZWgwdzRSa2llOWNmCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlJXZzF0a1RMeDZWUzI4c25wSVAKeXJMUDlNVm5Ma3dxcWdEOEhCazJ1UkY5a01qN1RBb3dKRWZ6dmU2bGVVTnY4T3o5enJINTJXSkhCWEtEcGJ1RQpiSmQrRVJUcW9UdEE0aVBpcTJCYk1oRVZscFZsSnZodGc5L2VFZ3NTVW1ONXBWZHZJL0JDQnlaUStqaW1WZnQrCk51bEJlV0hjdmFDbzdMdENaUnozNEN4bHJseVdWb1FBWGc5dGoyMzRucE8zY0RmNzRPUVBWMzlzQ3ZrVEJQUk4KU2lvaUs0eU1HS1o0RzAwTWpkdnVrT2NaSVZhRXNIVmRkbEpDaE5HVEMwRjRRN0xYdEpCbXRHbTlKRzdRbitGOApBTmFwM0pkR1JyUXh3dElkKzBxRVg1RWhoUlB3VXczbS9OTjA1cFhjTVpkbVFXWGhFZWltdkpSTDd4cWorMUwrCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3ZHZzZtTVdVRDc0a1d0YVp1bTkKK2N5Yk5rVWdSQjJEbnNWK3RTZEFpVEF1NGwzWEZORlVXWXhKV0hYRzZlMU03bWlOeFFzOUZ0azhJZlgyR3BpegpKaU5nMzBtRG1vVG9hNGJWZHZwN3BXUFVyNDYyRy9kNHhnQmYxNER4ZnJkMVV3dVRsaGo2dVpDdjZFbVk1SFlsCnQ0WW5VYU1PREtGRmZ1SXlhVzJzM3Izc0pONGltL2pYYzJCbW9zMnZXRmZIWUdEcGF2OXlET245T01xcDJIVG0KMHkxVjdEZW5EYkwyY0p2SVVBanJFN2JjR1BacWhWYXUvaEozTHFpQWVObThTUkdicVZnbHFwNUMvaUhyVmpBWgp6UHQrRFRJZ1FhWE1URmM2UVIxMFJmcGxNUHpmMXdOaTVxYy9TOHNKcmVlbzAzTDl1MFpuQmt0K1B6VVFzMGF4CjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2MvSllIQVJlMWh5Z3Uxa2xwcGgKKzZkVndUaFRCcGpOdFpYTDN4U2p2TGY1andjWCtEL2pxTjErTGM5eTlLcDFqSXNHWlI5RTR6cHNVYk9MVFBQbQpxZldOMGJ0T01MeFltekpGcWZCMXc1STZKeXNGdjEvbXJJS3dtL3JuRUxqMStEWnVTMVRyb0dXT3JBS1lOM2tzCkM3REpQMFdlam1HRnpKbnF3S1N2NmsrTEpPWTVkem9Qa1VyNVQxUVFUMTBORHpEaW1mK25TWXZ2MGJnT3UrcWsKY0o4TVM3S1FjTmR2ZnhBRW9ZN0pEQnhSU1ZYT205UzRjSXBIM1M3TExVUk1VRTU3dTE3SnVUVjh6QTZpWkFlUgpxMThCVUlJMm8vdk1UcE5jZE1GNURyZEJuK1VMVS9FY0UrUlU3dC9lZnpjY1orY3F0RUM2YnNwVm5wS3FpMzg1CitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjdSUDZGR2pDakp3VHN5Y3NvL3MKK1ZJUHIxUGtnbzhVQ1NBRVN4TDJvdVFvVGpiTkZTM2ltR2psb0hFTUFBZ2svVnZvUzR5d0FoaUtLVWlldWtJQgphNmdWT2UzUm9iUUNUU3BZWElSMnk3SVoranpmK3N2aDhRMjUrU3J6Qm9keDhXR2xRbTNMN2F1cXFXTXZPNVlDCnVQM1BaTVFvMysxSHNjMm5reEVhMC9tUzQ3cmUwNEZtbEMxbzBwc1RUYVZPZ3pyNTlWblUvbFNLcjRZbEJPVmMKRFJCQU1Ud1VDWmd1bUNPZHd3SzlaTnd6UlE4OGxmQ0orZEI5cW1BT0x5VVBDUTdNTlNNODVnZDNFMktsNEx2Uwp6Z3NHZjJlV0ltLzl2RGp3bWNNNXZQN1dVNFlFdHpuZXFqVzB6bXVrY2kxYVFOSGlKeTBjbmNETUFlUkFxMHJKClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGpkaXRBVDlXWWNHSUo3OGRJL3kKY1EyOGJMU3RaZWJPMW9JdGhwU2ZsWjEwUkVnQkxRYVR6NnVSZFNKOWpyZ3FDaHY0SktTOWtqSStkRFh3MVRiLwpTVWNpLzlTdVJRY0NYcW50M2dsR3luZkVaMTNVWTdnckk3SGJ1NFZPcldRWTl3ZzN4R3FCaWJ2N1NpbVhKM1dPClFoM0VPOHU1UERoMXZ3dUZCdUgxYlNac2d0bFZPZUxHTGZTUWg1Q1I1enJ2OFh4Nk5qdUNDMStXaXBYZTdsTTMKdFpGN2w4cnI5UnBnTjErTlYyOVE1bm55dWxMNmthUHlHVEpwai8zT1doS2swREFjVVZUS0hwaXNRc2JBNzV3YQpDOVo4cnJlZllHSTZJbFNFY2RzOGw5bllzTUNCR3pkOEpxVGdFT2JOaGhhUitCM2YycGFwSitBdzlVeFc1ZDJFCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnZhdjBDUHJxYkc1THVnYnhhMjcKN1orbWZIYVBiOGFzYm1sREpyNUpMRGhaYWhreXp0VFJKOEJEdkk5Ty9JNkltQUp6ditVZmN3M3J6dnZrNklKSgozd0ZaeDQ0QnNPT08yVzdjM2pWN1Jra0Q5R01jS0ZqaVFYMDdTdDJxcVhWcUtyNDhZclRTYndTaFk0NlA0YXNtCkhNV0JpUUNzcHVJQmlNeVN4QWlqNnllRll3b2VQdytqQ2Jhc0VMTEh2NXB2NDlVK25Sa1RhRk1YN0xNWmNQZGwKVUpXRmJwMjlyUmN2SG11WXNWQzV0SGJhdFhjYXlUcVk3YXRocVBzVldIOFFKTW9Ca2tSZXRhVk9JZHV0Sm90WApocHRDdWc2VW53TWNuQklSWG0yK09NNHdRbUdMb0kyb2cvMjFwUWZqUjJsa2N1dSs1VXJSanozeWNEV2grSDdxCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDhJRFBiUGlNTjN6WFBxYW5pamsKYzV4Nnk2b3VsTmsxRGN3cit2Z2pXcm0zaFhDVFRqZ2pYTmw5YjJBV3lZQ2FyWUNNSmVaNW10UUkreVhNYnB6Vgo1TmZya0xpRkpjbFA1ck9xVWxIc0tLZmhSdURwSk1tckpoN09Yam5DRVJMdkJ2NU1Fb0NyU0RtNCtxemtDbzY3CndLSkFXdHdjZnd4VC9BVTZlMy9YNFVNbmFoeDEwUHhhMTB0MSs1NndFVkVxb0djdU13MUFLS0MxNWtjOTY1QTcKdnMvbWZKWVp4bmYwZ0YzRm8waU9jZXhtZllCSXRqcWhxRkJ3WnVqa0g3YTdmeGZRdjJwQVc1Y0xQMnpHWEhkVwprM0xwVUgzdlVjUklSK0dGNFh6dDJCc3JxOHlhZGIrajdCdVV6WS9Oekx4N3BSaXFydHBSaE5EK29PbVRhR0YzCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVBiMWZOT3ptdGo5TjNWdTNkalcKSUNFL28za2c1cTBUMUxkbXc0c1hoU0lrLzA2WWlpa25XSW5kdEVYc1M4bldTU1JvYzVOQ2FqUmNGbTRFcXZRMgpMNC9qZHFuWnBvamNmSE1SeEMxL1gzZ3dIaTkydVhtZjRkczk0QjdYb01ybU9qREl3UUtia3RSOTVrczB2SURCCjlIV0VoUzVrVnVNWmJmV21kMmZGTTRwTjVud3VmTkNMQ0RUbjVDMjYrcVdYQlV5emloNS9mdEF3NmFWdUtGK3gKUU5TNXMzTUN1ckZjWVlxUTMzNGUwMHZPQSt1cGd5a0wzWG5QVG1wT1FzOUJaREVXUzFjeG42OFhZWlcwNGNjbgpocHBYZUtqWEpBaHBEL1FjaDhaMUdoeVAyYW83UnJnSTlwQ09tZ0FBU0szNnQrbGFCUkVXRDlqWVgyaVpNSE9mCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb05nWm5RMkY2YUo1ZnJ5bFFyRjgKdTczdGVrTFBDd1JqV0E2WStSd1lXd1JpeWcvK28wV1NYWDhadWpxMi9BRGpTclZrbEd5N0lleWdBdFlTc0pPOApDdzYxZDhqM25OVDRzS1IwYUNHeDlXbHRjRHRNZ2VzUFlYT1V3QXpjQnAwSm5jRmV2Ky9wQWorZGlYdmlKWi9BCnpZT0crTThLNndhdXh2UDVLMEtzZEpOUHhnTnk1OEZDcldzcm9VVEMyYmNiOWJEenFmb1Q1TnhoTy9sOVpSbkoKUVJrb1ByNnA2NDlwT2p2alVyOWhOcFNxbWVYOEw2Rm5uNkNlZkFiNlh5VGZ3aUorWHl0bEtKOEd3ZFltVnE4ZAowWDZvMVk1SmVMa0xBb3E0eDNYSXpIYk0wLzZvMzBJRnlsdkZDVGtGYTJ5S05VZ0o3dEw1SndocW1Qd04reXR2CmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlZCZnQ3WGYyQ0NuWlZ3LzRIVngKeXB3NURDNmhEd2pXRU5LdytMYkEySzAvaXFJR2hpRzVVakR2WXMyRXk4bU5naHVSRURPbGdVMlkyMm50WHozcQpDT2tVTVlNV21JeEJtOGE3K0Q4cGg1c2JQZVJlNXJ4ZjhhbGNqdWsyL2JLZ3VCWXFBUkVRUFhkU05obFBtVnR2CnFIZzhONTQ2eUpIRmIyUFp0dnpJSkM1YXBmZS8wVnUwV0xQOFdGblhmZFF6YnRVS2FQQmpQZ1J0RlZkbG5FMzkKdit5dFlibEhOU1B4RWpEVUUvVHBNcjhqZ0FPU3dIZkVtcVNseE9jRm4wbitNak9GcVczbVBlQ3dWQ0ErL3ZxTgp6bWZnKzJjT2UvL0xRS3pTZC9Gd094aGhCZmJhWUpYV2N1NWtMbFMrSUIyK1Q0bXhkWU84U2ZFdzZxejNieGl2CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcE5TMlBzMHFHazlmdXhqMVR6MmwKL0x1aExUZDNlb01ZdTZ6Q2Z3VFh3bURHSUdwa1Z5U054dzRqZkNLanMzR1BGKzZFanAvam5QR1hkaGNmYlZaUQp0ek01a2t0RmlNUXBoVG5qUHkvK0lPNFBadnNKTm5FaUxiQ3hXNnhmR1FCa3VMeDVBcmdRNkpYZDUyVkJEZlViCjN2c25TS2wrZkVRelNYemh2aXpBdk94ekZIM3Joand4N3NyZVZ3UWdLOW5yaU1oaXBoQTVRT2tERU94ZDJzVy8KK1J5VmdFbEhrRnhZMFVucGlNOWlKQXlmR0hCSmFRUThvbjAyWWpUZUh0ZTBBalM5MXhCZnFUODBlYVlnUExMcwpUYVQvZHc1aHI5TTJ2VlUyZFJZZWdET2g3U2NRdENubU9NMWtacXh1OVdEdkk1ZXNrbG5WTWpyTWlJS3IxWGtXCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdThJank2eFdRQW5GT3ZxUnZGVnkKOG5sWGJxZk9Jam9jSmkrTFRYQmJ2aXFMYVZKK1hlaG1KNGxaaG9wN1luYWJwdzJnYmVWZVVROUdkVEN0bTFjLwpBZGh5ZzFneUd3dnRtK1Y0enZkaEswQXJaYUJWUXRkVUV6QXM4OTNUbFJzcXVEaTJjTFBKZ2RFRjB0NVcwTVZwCk9uUVB4RGZsMXlsenRPZEp2SWRmKzZpZG9zcWxuTStkNjdxMHo4UlZIQTgvbzFlUE1SZmUyZDM1L2MyczdnN3IKVitZRmFRaEVFZytOeDJkdVpZZEZXYjZQbW43TDZ4Y3NGcFdxTGEvay9VcUpQc1llWEdFcS9TN1NaRGVNNGdHWgprZ1k0UnJrYVFSQmF4SjJsdXdYdXR3R1BVSXpzQlljRXI2MDZnZnAxOGFWaldCQ2JuUVc0a1h2NkdObUtBR25uCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWx2a0FWeElneW1pNXd2bXFsVGEKYTUzellVcmtSZmFQdXJSRC9GR3JvU3pYdW5PSkl0YmhZUGJRUEt4bXhhRVR0c0xhSk9ML1U1anR3TFBJRklPSgovYTVqZ0Uvc0IwczYwYk0xTWlBWjhDcXF3MmszY0xtbUtWdlZieU9xYTE1MHl5TG8zY3c0U2paclRoOTVtMnNuCmlTZzRhcGEybi9JeWhnQjQ1M2Z4NjVpZGVmS3RLaHZrSkpYY1NpTGdoNWNVQy9MZjVHUk42Mi81cVNTVFMzUWkKdERDQWlBWExnZkdUZjkyeDI5TlZUOHpoS2dlSVlHbTR1TWxGbWZUa1dHcTE2R0JkdkQ3WlRXcFY0V0U4VHg5egpKazkrZEduVjhxRzFSdEJkb0U0YVFQN3VGd0ErYjQyRU9sVk9CWW8rbGJkTDRhVnVmOCsxV0pTRHVzK3NlQmdlCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUlsT2pWNy9STUwxUy8zUEZSZloKY1JaS09GMlE4eU9QTnJqRUhERDlDaWwxdWNRV2JiZG14dUJRNytqM0ZXbjhSN3ZqbCtRQ092OTZzbjZTTVpZeApnNU9odmpENWxsSmhiOHBWL2tiNnZtMWdLcUtGeEZ6aG0vWldrRDZuV080NTB6Q0lncmIzTHgyOVhzcUNTWTh3CmlUVW5BRFRBTm15RVdZQ21HTFRTbVBLdmM0RDc4UkZtZC82MEFDc0N6OWV5U0FhM3NYWStManhMSHpSSC9XNm4KcjdkUkVGYWx1TU9PRnc1N2l3TFdYd3I0L0ljbnN6TW5xclJKTGQ3bUhTUEJ1UGNxcTNkeTFaaVh2bEhodUx1RQpXTHJqaFFNOStzU25KS0FQS3prVW9nampkZVBoZGgzQ2plV3U5Q00yYkRIay83U3U5SHhWQ1EzL0tSL1V3QzZpCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0Fiend4SVl4eURsTE02TzZnZzEKbm14SGpqdVlvK2Y4QmdsN1ZrUVE3NGcxOUQrd2pXclRLVEtidFlwREJPUFNUa21PcU5nUjRwaW1nQTR2dTc1SApKa3lTRHg4bUtDODVYNDFHV2VSR3pjanR3REZCQ0RvQk5YMUdvOExoTnUwN2U0VzZJT1NVcENPMWMxUGxtNU5tCkliOWpmZENtRDBsODRMZXBOcnlTLzhBZUdEbVp5bzJheE1aMTRQbDhPMnBScm5Mb2lENEtuemFkVEl1OGxsUlIKUDB6dGpRYWdaa1B3RTlkRHJ2WnJxZTJPRDRFaWVDOUQyMTg2Y2IyZzJhdWk5eUlHVElFWEY4a3ZiSHFpRTNvRApvVkhCS2ZWZURpSEp0M1o2a0FMbkZDellSdXB0ZkhMUFJhY3ZBUkwzNTJib0FPK2FzeENMNEZvSDMwT0FUV2trClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWdVQkZPMHBsa1ZHdXlJR3NkYzgKcW13bTFqbWM0bmdZVE43K1BzQlUzTG9ZQWZjdHJWOXFYWWNXK2ZIeXFOTm0zdmdHQlhLOUZnTFJCdjh4Y0Jkagp3d2FmNFJKL0ZBOVBLdWxLdE5WWFRMVUh0TkRTdlFXL0F3aUg0WDZhbUlsRnlhcThtTzVwbmZkZHRrOFoxNHVpClBDc2Q4YnRPY0h2Um81NzMrWDBST1lGVll6V1JjL1pWaDZEWlFnalJJUWNtRjBROU9hRFBoYzczZFUyeUpndGYKWmN1Y3ZMMzAzMkpVemVNUUtzMy9DUUZRSWlNejdoVE1GUHRBK29MOVc3dlFTY0lQQWh6U0Ura3RWRllmVkV2QwpzZllGdU8vTkIwM2FLZWtHbXFJOFVVV0dzMHpnYk1rc3FBYmpCSEwxU1FSUVVIcUc5NUFDMStyaEdXbUp0VDdUClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbm9pckdEcTBGZFJqK1Z3U29UT20KVWZ4OUVCeTdQWW4vNTc1anVTNGlOSVFvQi9ycjd4Nk1Ia3dLVUV2a2JxTGFvR0NWQU1pTDJMaTZjV2RvUXRQWQpDNThBYlpXdURWOHVTNmVzV052MVVldWpFR0dQT09CSlNLai9OcCtsSkJKQWRIWGljc2NndkFCdWl3a1liTjcvClozbXBJZzZlSDYzQmlRaW5GTFlLdXVFYUpDRFZYblZ3aGlqdlk4d1JhTVVlZko2NFNISGtUWSt2U0RUc05tSlgKUzBpTEZjQ2Q4clJObHc3UlRvWHpJcllKTm9SdGp2R1JBOVVIUmlmUGZESEF2QWkvRFhBQks0dGMyZnc1ZysxMAorQW80azFvVTRSK3pUT3B6SURvQnlWY2lzelI0cW1UVVgrQWFHUFR6OWxWTGkwTWx6U0xpbGVOWFFQQnNIaU94CmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclBZRmRiSVNHdE9lMURycENMdjcKRzNQNlc5aU9MZFpjRlYrcFFJcDhrTXFacmRQVFB1WUlHQXNRYmk0d3hQd1ZzU3lXS2R0RnpWS1kxZmtNaGNoUwowMmp0RFo3VXpGQUhlQk5DWnl5VDlpbXJaRWx6RURLbG53V29xNXA1SU9JUWdUcXB2ZXJncDRtdUdQN29oc3hqCktUcnVKWWpNWXlUekVkeVVhZUIwQkM2K1IwYUllSTNhYjU0RWJTWXJVdVZDcjVkWFBnN0VnaDB2dU04L2h1SGkKQ1FiZ1Nzd3J2ZUc0WTVadWJqSDBCMUtWMlVlU2FxbUN0Tlk1enRDc2kvQXdtUnJ0cWNoSmx4Ly9YMHhXZHRiVgpjaDZqdGhJRFpmRktmNXh1MHowK2RPcUVPTk9xWWJtL0l6VWlEQkd1TzZwUE9kNjdBc2MvZHhxdDNkMnhmQlVpCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWxLUFQ3TFVQT05tMWZFSk0yWUUKVGJWZnNWUlpXa3lENENnT0tnN2ptcnVsZ3dLNmViOHlRVTVkMU43UytPVVZIclZMcHJGb2s4VzZkZWJqUzhrdwoyY0VibVU4MTF3RDJYTHNzUzBnemo0VC91WHFuSXdCN0FhUHMxRG1TMFVlUVdRaW03QkN2WEhrdFNXUnhQT3QxCmE2WU9ybnJzcFFwNldHbnZNTG1UOExNOXdXbGdOaE1XVDNtNzJRckVVRHVpbVk3LzZqVFl0cUt1RFdHdkVoWGoKMzlBRTNQczJMU2RLZEx3R3dMK3EvWnpXOHhpM05lL2xiZ0JhZXhXdWxBNVdMU2EvLzYwa2xuNmJoYkEwMTdBZgpMSVprN0hxQzZhcTNMMmpnV1JENEtCeDgvenBZb1FDVDY0b013LzF1U0JycFlTd3NURUFBYlh6OG9JY1hiMU9PClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFpCT1BzVVlxeExUNmVaWTI0ODIKUm1ia2VYOTYrcHI5WjltdEFpRXU5OWRXdFVDZkppK1A5ZUI1dnhaVjY2eDdJZXViWkxUL1FRMEFOaUk1blVqRQpIS1N6QUszMjFld1JqSitvY1RSdkE4R1JhQmNka1FhZ0NNRk5OOE0rU0c1Wis2QndnMm1BWWlrdmQvdE00bm4xCkdoZWVJQ3M3TWFDRnRoN29KaW5vT3pXRHZJMFBpTkVWZ05vRE5nTDk2aWIzTW53dG5sWjl5ZEdLUzlIV2dqTlMKc1BsQThSbkdZak9GYW5hSzZGRFRDZUp6emZ4dThUemwyS05GM1FLQS9IWWhyb0V0Z1EyeU5rUFJrcGdKVUVDcAo3U2Z2MC9IQUV5VFlWVlJrSnpHY0tvZDh4QmhJclZ0UytHYzlzWnplZFpHSGsrSm92TGF4S3BhME5RcjVJdGJ0CkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHN0Nis3Y3NBeFdVRWQvakxXT3YKYmlTK1JYazVpdHV3cVlpUDdNKzdNMXU2akpydkRqTWp2Si93SXNtc0hNb1NPMklqNmVMY2RGWWF0c3pHVW5DTApKK05OWXV6SmU2VUtsalFJWGV3dTcwWDdTT2JMMGVDOTNJWXBuOHNDUVhVMittaGR5REVMZElUaEFjVGh6N3IxCkN0NVAwbmd3Qy9HOUVWeUhOUmJ5OFhvbW9tUlVZSDVacUxLd08xYlBKZjFpYzNjZWhWQmZnUGVXdnZHOGJRMk8KbGJ2ODZ6cUZYNUdmVkVwYmhDVEZDMnJLT2lPMGVtTGFaYUtZVytFVmZWL0ZKQzVzZjVzN25hSTBpUWUvWDZ3YwozUzlqcW1GTENkN2JjK1FzSnNkUE9uM0s1aGhJamhTdlExRG9jSWQvRFRhblBxQkFLVy84bngrVzUzR2JHS2RBCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzMwRXhkclFzUVlib1dtNURLbloKSnczcU9TOHdBV3doOFBwdUFjYXJVRy82VGtPL2FPaUpuTG9rclRac2lzWDFWZWp5b0xQYzRDd2hyYzJiZ3dURQpSOXNhanBKM1p1OEJadHFVaSt6dERZaCtyQkQ1RUdiUlU2a0IyeE4xUVFIeVRHVFQ1STZTeUxnYlVGZUk3WG1vCmdhTDlXODB0V1FoWFdQOEpIVzQvT1FmK2o1cGVxUTIraUNsMTV6NEJpZkpKNVgxWFFwV0w0Zis4NUQ1MnFWVUoKeUdpUjlkckw0MWJaRDByWW5BdWZFQ1VUaFBCS1BNQlBsMTZUQWtnQmRncGxYSnRSc3l1VitzRXQ2NXFCZHVpKwppbkVkR3cvTkdOTm9yejlXR0xwM0RYSGRWK1d4S2U3aElWUzBpU1RWeFFkNHNIWkRubmhkZkRzREpsMzR4OFNSCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1I0L1RNeXlBVWJWNEdZNkpBSTMKZUFvdE92Tmkxc2dpZHJyKzRmd0poNWdHQkhaUXZaSDF2SDhaM1F4WE9SZGVpWGtlWXRlcXNQeXc0SzJHUGt5bgpWdHN4b09WbGVHSHhZV0txMitFNXR6cXFlTDcyZ2NaaVl4VVdPUjhwb1hKUnZsL1FjTURDSXVNMHlwRmJGS25QCjVsamUwRnpiR3c1YmtNQ3hmYjJlSnNraWRPQTdxcktPUE9RZy8wOHJUeDJoQ0dJYmtuOTVZMHdpK3lmaXF4bDIKdnlNbDNrZVB3bXgvUHpaRnpyQ2RXUndPSStJelgza0pvUC85OGQ5azhFT1oyRU1lb1JQRy96em5QMXd1Ry8rbAp5Vjkyajd6emJqbDY4S3IxV2tQUU9VQ1Y0Ui9tT0FEU3lnTlV3ZHYzbWxmUytvYWhPS21QK3BsTEp5bXlYSWdCCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjRkODhBWTZmRnZpcU44RkxuUVYKQlp4ZURPT1NHMFJ2c2hBQ3phak1yb0ROZXZBRmMzRllrZEpKSi9YYkVrV2pPa1NoQkhWRXVYeTI5TExHbVRqQwp2eGtLYVNrTFRFVmNLTDRLb2tFaHk0RWE2MG1MdUt1dXJPcnUwM1BwamEyaTNiZnViMlNacnl5b1ZDcjB6NURpClJxTTF6SERONlk1V0RtMktodUtKRjlyOHFKU1BISVE5aDl4NExYc0FOWjN3eS9aWG9KS08zRE5CVDlvMS9neWIKN1VmclBMS293K2ZvNVFtejFPT2JjeUxCT01Hb01NWmVrNW0vRHZQeEQ3K25uTnhUdHpUYytpUlcyczZWT3NMVApvdDRRS0MrN3ZTZVV1V3NsTnFVaTJRNDJPbitUckQ5eXFxRUwraHNwYkRtMnk2RDZBTm9hVTZsQUVXRjhEUW0xCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFRVb0o2Ky8zUXRUdHQrdGVOVDAKekdRaHQzdldXVmdRUnNzSS8zL21DY3JYZTR6clZySUZ6cVRIN3NFc1NSNHhKSUc3K1NkdU1HSDVmWkliY0hLVApkbTNtOFl3cmVjNDVOWFR6OXUwWC9vZTFhS0xFQk95V0hsbGgyUVRBVDJRUzg1NXhGbjRkWlhoVjVxR1E4UXVKClFVbWNPUHhHNUdHNnprZXFiVDkwc3NVV1JGelBpd3pjSEpMcEFkYXJXWFZUUzlGZlZoeEZOU2ROaklHcG1FVm8KZ3lEMUZHbFlqVDFVMjZ4emNvTm9qTjBjVHNQdGYzYTU0dWdjOVZiT1pLcnBwRzlYUS8wM0dyclU0U2EybkF4MQp6dXNiUjhFLzVkMjEvT1Y0VVFZN0o0WGNRZks0MmJYblBabC9Qb0cxTmk4anN3ZUk4U2FvVFNVUDRldW54akVHCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDhsSXIrdnl3RFo3T0gyMnpOUnoKR29YREJ6RElVYVlpVXc2RWdlZTU0M1FpVVl3UUpGT3oyTWRxSHBrTlZRRDZ0NVdaUlpJVFp6a2ZHVDlBSm1iUQp2OGQ0VFh4MS94RjZSck5QUWtHZGlrNVo4VDZLdVlTN3R1endKU0NQSmVXSDNIcXZjdk5MTlRFTVdQNWR0dmVCCm5PUS9LTlBybmtQZUh0VU1tQmNhalVhbmZxdmZFRTMxNCt4Ym9xU3NKWTNYTkwra0IzQ2lTVU1jQS9FODRZc1QKbjlmY2FnSjAvT253YlJtdldPbXVHekhTT0wyTDRSVjFRajJESlREZ2xqN09aQkV0ZGswdWtZOVZOVWJkR1UzdQpPMnY0YUhydEVvenFZZ1c5Q2RJNnFRcVJqNmo4SEh5bUpHekVSa0ZWTnB3Q3NrU2NVS2c4TzdGUjRCTFc4Y3hNCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdU1JZ0hzbTU5Z21XZG9OWTBrWHcKak9MTVUvRFFTaFF3b242cGVKSU5taVRUTmVSTGFRVENRak5KNW5jeGhXNkc1T2xNckdBRWdHdXpyTVpFM3ExdApubndhWG1ocU9FTzRDQzNtK21DaEhLWGp0UlprSittOENjRVoxaStNTVVuT2NzaFV2T01aMFVDamUwSEpNb2pLCm1HRkFOV0NsaHFFMmx5cVoxVUZRcS82eXdiTW9lUXN1WGZHSkl0RjNrWXZoVUM1VldMclgyTVZrbkgvdFZPOGoKZGNzT3NjbFQrdXg4Z2NVa2ptN0Z4VFJCTTdvNmFvbEN0VTFFcDYzMTFWMUJ1ZnJoOHM1cUZ2STF3UUtkV3BrSwpIYU5neERJQmFuOFpKalVDT0szVm84amFKZnF0L3ovb21xSVNscHNjWjdQWUR3OWZwRkpGempNQTFhMlVFRmdWClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTV5eHpRUm1jRDVhSmFxL2dkeGIKMjhIb3lya2xuRUlTc0tXbFZKTXpQQlNlUHdpcUhDME9IVDR4NXVUTTVVUm1zN0lsbjMzSU95ZXkzNkJsOFkybQpkcFAydGoyVXR1ckFtVEhRNkJzOUhvUEtLSjVYN0pGc1E1Tk5iS283ZVVIM3BUT05FNDNZbXpkcTdXNXEvc2g1Cm9HRTdnbytYcTVmQ1dvOXYrTHFPbStCbGd2MW9ZYzkxeG40bTJSVUF2dnlMOGRlbFNVV2VwV3AwRjhBRWMyY2gKaS9md3ZucmJFV1VDdFVCdEU1dVF2Nk0weGhZczVITDdPNGRnOXh3WXdqb1NtWXVrNmVkMlpGTXZOSVRDSnZqWApLUy9hM0dENndQT01VTDd1bmNuYTBUcFl4cmROWlo1bTdOeGVtSGZGMWNXUXZHaHNwcUFlelI1T0d1QXhBZ1FRCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUNpanpIaEdwa2tVVjFsaldFY2EKcklMcUhVOGF6ZXlqK09xamFZMXBiOUZZUGwvYktpWFJlVHQzQVIxZVkxT1EybkpOd0pPcHdzdDVCRCtVZWQwZQpjYk55YWNpaXRpVHBlNmk1TnhXempvc2R6dUtpeVNjbnNuNzU2dklKS0o3RTNsVE9KNkVnWDFjMGw1ZU5QMTdqCktCSGRFQnZxMlp3Wmt0Njc2djlCZXVvV3hmSW9KaWt2MEFXRTJ0S0VMaVZPOEhXNGRGbHkxRXF2NXpPelNsYlYKaHF0L202TCtkL1d6UWNGaXJFWjFYK1dvVGpzT3U4QXBxWTQxbkt4ODBDTXJBZC81WnN6cDJ6T2o3bEo4cit4VwphT2JPYUJIejZ3U1gvTURrTEhESXhKZGV4bWw1YmdJZkZzMmJ3c0JzMEpkTCs2ckhxWWNQL0d4VlVIYWpZTjA5CnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFFDOEFXcjgzblZSMERoTktNaTEKZlR0VFp0MVl0M2gwNm5qSzVnU3BiOFlDVyszQzliTDRpdkVtaEFtV1A5YW1BZ3NpUHFUTytLaTE2WGlkakxoQgphZ1ZOemI3ZzlLdDFiSHJzeXo3azF0Z28wdDdEa210dXcrcmM1RGpuSUh5QlNGWXlZSExiVFcrSzZGTkJNY2Z2CmdpZ1pidEN4ZGNMTWxsUGV6UW14N3pxM1ZSRUNmTEFDTCtEL3VQSE9xMlRhSDRCa1E3KzYwOGhzMDZPWEt5TjIKRGFLMkNvUEtXNWU0VHI3aUQxY2RjY3dZZmZXR0RnYkhQTVdVZU9pM2VmSlJJdTVUNjh2WkZXcWxmUWFMMmR2QwpLM0g3dUhFZzlpYlZHN01zN3h3SHF3blFLc1psUmlBNU9IQ2Nwc2tORGQ4T0c5U1pEOURvR1BZOHU0TDdVcHA1CitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGY3VGtXR2FVNmgxeG9BNk9MVXUKdWtBWnFTenpNTjdna0N5eVNaWW54MHV2Vm9Ma2FkdjBWcU0wN1hhbGlFTFNndzd0TFZvRGVTd2JET2dzOWw1UgphVjh5RkVLVUpZYzFpeVM1b0ZlNEF2RUdwSlVYaGhXQjFDT2hVT0ZuektOdDQvMlhaeHVXbG9rU0FGSHV5akoyCkIyK0pjL2ZYOGRSNnRpZG04TTRtc3M5RGU3eG92MmU4aUVlTnNaWWQySks4a21KOHNWRGU0WFY5Nk9MbmNjcGcKQjNzRXZ5aDA0VFlaMU9sQWs0OHhnUGw4d3ZwQkt0THUwNUZBU1NJYUM0MExJTEVhclMvVjBubkQ2UHk0dkdveApvTU1vcmR4V3dZU0lMVHdvQU9kS3hEQVNRMXlIWHp0Q3NaRGVpL1dQc1N3K3hoNndtZmJaUVdIcFJUdE5TVkdPCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb294V2djcnNzeVpoclA4ZlZRcDcKUmNRdVV3T08xaDFXVkZJNmtaTVBFaERFQTdlUFgrQ2N5bkt3M0tCQ2xqWkZhTzFSQkdialJuZnRQTWMrQUFaUApUc2p2emIrdUtWTFF1aytZQU5XcWdVMFgrODZHUWQxczhJNjY4elhJaW1xenB3UElmaDRHNk5vODRONFp6TUdGCnd3TFZSM01uU2IzNDdOZmExR05ML2xGbHZBUGg3eHoxdUd1SnI3NCtqWjExaHRlb0Fqa1pMRHpTeGV6SmxqbDgKN3JrV2pkM1ByRHg4b013UU5sUGFXQ1NQQzY5MjIvWjVNWHVMOU0xUXdXTWlXb1hGYTlQRkUxUjVTTHVvdUk2OQpmeXFuQ2xIQ1d0VFluUUFIamNXRTV1NzIrRmcyQmVEd0lMUG1LcHU3a2VMQitNTnVGRXpBcWhXbXJ6S2IzSGNLCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzhqZ0MyOW5QczFnRU1oajVyTTQKLzIybEN3b2xZOFZXWWNNYmxSMnZ4WDc0TndzN01aSndZcW1PQ1V5SGR3ZjY5OXFvbmRnZFJVREpDcldaZWNWTwpLc243UmFBenVNV1VPWHNRU1pLTUN4Y1ZTYUIxSXVIUCtiZEYxVzUvMGVDeTd1S3pEZ1Vjd3l0K2lmUjVzcmI1CitnK3VQdllMSzlYYTUwanN4c0p2bmErY1llUGoyNkgvR3YwSnZQWFVROVQ2WmRiN3diVmdUQ3dPTVlvVHJOVjIKU2p6cER5bzZZenZEQmhzMDhCem92K1BIZSs4bkl3THZTdUt6cnVpWm9pVjVucFF3SVhzbmNreEZWNjBUTllFKwpQaDI5c2lVcW9LUm9HMWg4bWYrc2I5NmZmejlIa0txL0lQK3c0M21zL01lL0xuU05nMVZMOGJDZlZoM1M0c3N6CitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOE1lVXZtYmNxOTU2RDlHZEdCU1MKMmJaQXc0a29ENkV4a0VPaitSbUNPVG04aXEyU0xheWtNSVNRZWVybk5Gd00ybWFjWENPYy95Z0M4cHhSV0djdwpMUWtud3I2N0hTcUxhWC8xSTNwWSsyUFZOQkFNSDJaWkxEdGMwUzkxR0RNL0RvNWFHRnplbWVwSDA5a0tSNmZOCmNoZjhneUJaeUw5L0EyYjRjMEx4VVYvWmhjTDVHQlRvU3JOSlNka1lOZDh0TzN5N0xPM25yV2lkY3FQaXd3SEUKT3lUVnAyazZadlV1V2ZHZTRPVTdBTUNRMkltYWliQVdxRkwvOEZwQ0w4TTdBQTNRS3UzVFdQdVNreDMzM0pHRQo1Zm9ndGNkUC9YOUdqN0tpckx1TStHSzFyaGhXUGpoUXhqUUZEOEc1d3pyS2E2UytyZEM0clg2cWw3RWozMzhNCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmgyQVRXaGYzUTBsdVIraXd0YTIKdG5JL2QwQWtxZG9vTzRxQkx1Q1krMitTQmppTTFlQUVvT2hiQzNxWDIxYTZJbkJjL0Z0dS9kMGFjNHZERWROMgphRDVpbHo1cm5vZmlpL1hzSm1QK1pyTlp4NGh3TU9FOXdzdFRETmVETG5IeTlrRVZkSHdjMThSY0d2N1JJZ3h5CmpVM3B6ZGVpdCs2blZRYzIvbllZVitoeFZVQmROV1grVmtzaUF3Vi84eVVlMVR6VzFhUkc3WFpVaWVWb1FUL3oKTnJMQnlNejZVR3lpajc1NTFKa2JKZHAzN0JtNlhYTS8wVjBvVzRCd0E1NVVrc1dVVmxiK2lVRGFCSTNhSFBiZwpjRUtSaTBNcHF0elZ4RGg1WGhLS3ZDWWpPa1JpV1VKd09NOTVjVDFURkZ1elhOcnF4djdZenRmNy9JaEs4aVN2CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdU01NXZPKzl3ZXJBZTdtTFpvQzkKQTEySzZ3TkRRZTJoZXBCd2o0SFdUSmtueEhZQytTWVlnRmpHbVJSTGFYSnJjMFhtS2pDMlBhZ2oraUU3Y1d1Qgo4UDA3ZXBqb0gzM2xjeStRVFVsNDliazJiSlBLeVdVaGdma3lGQzQ4SXI3QmFiSW9oMFNFVytraG9HZ3pNcm5lClhBWm9tUWhPNnNjTGdDK0hTOStienFLekRpYitoUnYyT3cyczBmekF3RTFoSjVkU1cyQWVOTGR1NWt4eGxMeUkKNThMUVVXTkNCcDBUTldyeTNRYmRjWW93VjEyZmxjRytJa2NyRFEvWVdpL0dLaitzc2NBWUQ1dUQ3OU8rcjZxeApXYlpGMmJyTUIyY1JFTy9RQ2FhR3RlWEd2d0JtVVNQT3VLOWk5c3hsblp6RUlOS2JXQ3c2WW5HTGJlREg5dlFZCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWcxU1NXeWZNRmxEcDFiNFpzNkcKYzBvUHE5WFhEUFNSdjZ0L21xMDBLM3g5NisvMis1MnJjcG9GRmgyemh6eDhWTUhxNWtFVkZyQ2ExZ0xwbmd6eQp2NExLdjNTM3lRcjZQblFxeXdTd1UvNHBjMVZLWkxacDNmU25DY1BzOTd3cEhhQTBhaGRVM200enNjaGp6blByCmRCTzRkSUQ3OUpvd1VyV2lsUlZtRG9TTkJaWjBUMjRHc3hiU2RZK2s5ZUxuV3hzcEc3aDRiZXc5aGpNc3g5N2sKaWllWEViUUtlWW5RY29lYjIvck10UTJ6WTJWTGhxaWNibHZ4OWJUOFFKSGJPTUxRVVJSd051Z3N4TkJoMkI2RQp3d1oxYkFOZ09FWnMrajcxVURTbW1UUnk5MURlcTdyN0R2SDVhRG9KTUttcDdNZVA0bEZzOGtkYkJDVlUvZHVnCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlhQWGZjZ3BoSG9jVFpEVFZvS08KblVsTThuekN0RTEra2NZTGE4bDYvRVBBWVRXZkxPVDhObWFmeDZCb1dBNXd2STJ3a3dJVnNCSDlDci9hWnNlNgpGZGt2blA1RHFLSlpZSmdmbmxsSHJiYWpST1pGYy9XUTh0ZDlxN1lhdUZ3QnRlSytiRWlWN3lPT0QwOGVYSTc0CnpWZmNVd1pVOTIxUktHZjRQdTdiUHkxRFQ2UXJlTFBQT1dqN2gwK2NUem5GMFlFTm5XTzZVZkp0S09Rd3I2ZUgKeXB3Y2NoMENacE5WMERwQUVOOTMzNmRQclc4cXlPMjRUYWsxYXZTeW9FZkFsTWFsOUJIWnJpVkVlZHoxV0hHRApQa2ZmNEFZdXRXM3hYZUpjbVdHWnY4cXAzTFNnTit3cFlRVGJVaGJmNzVSbzFwc094ZFRuV2hKL2hid1ZmYzNDCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG11cWpPL0MwbGFYV3d6WFBPemYKLy9TOHVUa0YrNGx5SkFLeHNJRklvQTIwdmlPY3NLeHVlQldFN0k4cGNNd3h4UkphTXVXTXowd0lSZUNNU05DSApYZmFmR2xvZTFaUU9yQ3d6blFUdmxtc2RSMGptTU52eEhscWZMWnoyUlVuMDZkWjBYRlBqQW5helVSd3ArVUMwCnVZdjdHalkwTlcweEIxVld4QVNtYk05N1JwWTZoUFhoUmNNR2NOZDZDazlKa1g0Zk1EVzZoVWdUTHVXOU5HQjEKTGR6QUprSUhwS3l6bmgzUTVTTnZwZ3hZNkRuWFdKSi9xYisrampTb2FzWTNNMmt4eWdxOUtSalpjU1dCS096bQp6LzVFWGYvTDF6WTgzNHRWSytha2pyaUpVNFpjRHQwRmpCckVhazAyeEF0ZUNINDNFTlZXN2VxckpGb1dGdDdUCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGZjRjc0YW1UaGN3N3ZMV3hiZEIKblUyclVKTHgyeUVPY1pDNFdNU245cm1TQ1lkNmtWajludVhVVVNINGhHcnN1WExVdVZQbnpEY29KR0xXTUtUcwp5WXM3VkF6b3BOeXYvUnp3NkVpMzVMbVJTVVZGTUxBQWRMZnNibDJmdnIwTlRBWnlSNFhqcnRJQi9FYVRFUk9uClVyMkhNalBBdk9wSFJpMng5Ni8vVXhZUSt5c0lrcTkxR0lNTDRHMFAwUitwZnlRbElkdm01cG9xRVBLN0NIYlYKMFBJQnRnMGxSV01rbmhEY0JUamcxekUrR3hkOHJmbUdFdERBSjRJSjR3dFZyNEpzK2NiNnJGU0cySTJSLy8ybAo0K2dBTXUxTFdCbEFxT2NpRWVLUHYzYjRrZnRPZDBrOWpmMGk4VGFOTW5lK3QzaUoyL2J1ZWJwVXM1Ujk5N0pXCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcG5vNGJpQzZxUHRZOXhESVNNMlcKaGdRb2ZsSW1KMkJlRHA2VlgwZ3NHMG5wb0dtWTNKRW1SclpHakVCS0sxdjBpbjEyZ1FTejFCdEdacXo1LzlRUQpqQUtkQ2RkcjNHWDF1QWlDRldYSkZPUmF0MDlMVTdlM2t5aW5aQkU2WXNLdC8xN1EweklXaXFubGdrQUhuMXIxClE5bnVQYnlZNXVER2N5NGRBd1liMmVrMVRXTEU3Vi9ZREFBZEp0eWNyQktRbGNDT3cyOXhWbmlmaXYxT0NaUE0KaHUwaXRYeTZldlRyUjJheEdKcjJJR1FzS1lEVThSN1B4S2NKOFR1bmtMUlA4c3RGNWtkRFJjNjB4UmcrTUhMKwpHSnNtMWRYKzlFTytLd3hvVGZSb0NQT3Zrc2N1TXFkeElVaEd5R3J2c0lKUlNPRlRwQTZWcWVXS3cwQ3haa0NhCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0o2SEs4dm1na2wyT2lHRjFTZk0KdkdxSUk3MGRQTUg0K2tJRmhlZDdXemg2UDRGU1FKYmxGbWNSbGFYYWQ2NUhjZE9lR3Fva3FZaXFxa2p4elkrOQpjQ3BaT25iemFZL2QxTTh5aERaL0JUd3FPb2d4aCtNQkZIVDJxYzI0TEhEV0pob2drbnlXTE94T1lRK3BydlZiCms4TGZOcncvMzgzb3g4MkptOWtMT0FwdEdBMEFjV0dad1ArR0c4N1J3S3BHRU5EUmUxUzVMS2pwQ1dJQTVuY3cKZWlQK3dCZHhxdlZNNWJuaHBFSVI4bytUVFAxVU45QmpMaFFVVStiVm8veURDMGlxSFhzYUZMZ1orb0xuL0Iyawo3T1BaNWdvdTF5WU1TbnU0THRHVm8rR2tiZkRIdlc4K0M5UFQ1eW04bFJ2T3I1RDJQRDlISkxSNCtxVXltOXRPCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2hUT2g4VHVJSjg2K202SmlzaDUKUkF5SXh1aDFPTTVSZUswWHR3VFBDTDhocHpnNDZFNVFyT0t6bEExbk5SNXQ5L0xnT1VsRmJiVzdjcHFmQktsVwpaa1hUR1d3Smh6L3NiaU5wWVFzcVdHUk9qenVOVjBKb2lDYjB0V3h3TjRVbEFwWFFwSXNwTlVtUVlLYUNxMzJ3Cklxa1ZpTE5kdmxheXZWZDVKc05WdFdjTXJvT0xDanNyR1g0eUVoYzVPRjRhK0VYaGhNSXhXeEhrZE9sZFQ1eVYKNVNpZHlBcmwxcUE4UEgrWnR3Z2VHeGVwVnAyOG5SLzk4RWFvcTdaNXpseGt3a3VqVDNEcnc2SVMwOWh4Z1ZPaApWM1N4SjY5MUdVRmt5MkZ2Q2xPUjd4bUdVdm1oTTA1NDFqYkJ2TGtoSDU1Zmhid1NxMW5CU2NNSmMyKzRac1dGCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnBUazdxSzh2aGtrTGd0b1FScTUKSnRVSHhPZW1nV0NQVjg4bk5BZmROVVIvSXlBdlhKVGtPSUlGWFFUOWVWUTJBOWc5aks0TWt4Q2liZzhSVExNVQpOMFM4cWVjRWcydUhDaWlTREFnb2k4bDRrdXBnT3VMTUhUYm5hbDNodEd5NEV5d0tKNVlTM2Q2RzhoYnZBbUx4CjVyR05FcDZuMjhYdEdYVzJCbExvalJrcDl5aXlEYzhtQjh0c2xXOUdsS1JkSlQ1RndpNkEyZ3ZBbExQVWYwMEwKcno4bHdUZm0wQk44YmZXUHFTU1d2VUYwUFMwR2pJdHlWcjJ3REpUM3ExZ2VZb2lmeGhhRUlLMmF2bWttWVEwMwpzakFTb3ZSNWpkUUxwRktjMHpDS01oRjl2TjFYOXlKc2IvaERDMzdWOTYxSkc0djRSbklobkRSQk92bFc3YkRZClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnQwUXFnK0FPR2dMWXpuQXVHeG0KU2pHU0lzYWZMRHFpVFRodDY3Ym1GNUlkNCtBWTdGbjVHcnp2V0lHYllYSE01SldrM0duZDVZSFZRUWp3TGgxVQp0QVZiaVZkOTMrQWFGbDlQQ1VQTlU0WTJacTF2aHd3NmdOU3hkbUs5bE1JenRsbXNTU243WnVFdndZa3E4cHFYCm03dUg5ZDNuOUxQMzZEcHdkejUvekllTDNTdnR1dmovK0JKbWExYWFQM1lUZWdmRVhmUGRGWnFjb3BYVkF3U2sKRmdJSkpvckNxNFgzVFdyWmRTeTVNVVlxR3hlaC9qZVJaS3VhdzFzeHBRZU1nSkFKV1paTXY1TkM4SHlSSURPegpBS0oxK3hjeW1OQXp4NFkzVlU1QkFaT3loY1Vkc1d6bDhLQmhWMHpjK0xoREtPWm5iL1BnK3V2ODlBS2dOWTV6CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdklrczkrazQ0NWFHRmkyTm83SkQKMlZUZ0FLSzhKdDZOTitOTUFEUGd6ZzhGRzdzbVk1akc1algxLzZ3MmFrQ1Z6VEpQWW9kYTloNjRsY21YNHZLawpLeERHUVVhM0tlT1VrQmRMVlFDR1J5Wk1uYjdRbUZuemJOYktvam54dVU1dUwxU1dRZnJjZFNKaDc0TUp3dE13CkowRUZwNUg3L2orY3hmWmJmb3NtZFNPSEtyREo0N0lmYmJ1VndwY1RIeE1qWkdGT0doczkvazFXT1dkYTFqa0MKSEdsTkQzZ2V6YlJSNkNIWU9PTE82b1dFMXM0UDFwa3VjcHhQSVRwSHlMUUJSQXVzVXJWRFNqNll5SjdISnQ5bgpUcXNuaEkzd09sU09tUXg1bmU5cEpvSURxSGtDeWdYajgzUWtzTGlLYkNlQTU0Y3ZGNnZoN1B2NU1EOFAxTVpxCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdytZWFVBVFFDTzJneWNySWJHdnQKU29lUDNuaEtSQ1MvcnJETUZhcXVpSTRMQWhVdVA1ajhSRWlvTHVKZEN2TmYvNDNrcUJveTFGQWxCNm9LU0F1SQphZ2xrM2dyQ3ZDQ1VYQUI2TUhvSCtUeWNjbTR3RDhsMWlVTXJOZTd3ZVdwK29BTXFuSlExNVg1blNxTklYWmJaClZ1SnFNMTVUbVFueFdrRDk3dEozL29WeUF0OU9xbHVqSGM3UzhiSnJrUUR3czBCSE96cnVxZlc1RENaVWdMNVAKOHRMaUVibHlBMW5CR3JZV0MzRlpaVGE0L0lpSzhCcWtYdHRUK1ZjNVNPK2RZMWZnaUVBLyttU1IzanVHQ3JMYgoxWks2NmxjbENFamVaSjM0RUh2bUpwT09OY0NPNHZJRXR0bFlYNnhCUlQ5UGEzKzFKVDJoaktKeUNyYXFQTmNRCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeExxdjE2T1NybUxiVVNudFBWa3QKRy9ZZ0xCblZNOW5reFQ5UUVqQ1AyaXhMaE9GNk8zOWZUNDFTd0p5Kzg0eFE2QnJ4Z3J5Yk5LM2JNZmVHZUtFLworVVQ0aTA1QXNKSnN5UE43QTN4SFk3citONE1CcmFYd0phSmI4L254UWxXL3JTblhIUEl0VXdLbW9vajZRSjNCCmNsUXV1aTRKbnFDaFBFdHdqNkIyRG13NzIyeUdlS2xGbDBsYlVJNlB4aGJmc2NKTWNoNzBMT1Z5NGY4akMzT2IKWTZzTjRHOVE4VWpmMC9WNldudS9rUmtEUEFkeDQzbnBhTUZrc3prY2ZnWVFIaGFxWnhqOGtVL0hEYUxEOEJzaQpBcndpNDdFdW5zZ1dXanV4aGw3ZVR2QWo3SGN6MFNOMVNZVEhWTWU0Nk10RCtJanJUcDVYbW1LMUFRb1VzczE5Cjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnd5MjVNQW5pZ3UxKytmODBzRW8KeGVlRm5mcVYwRVNOa1FrOWhoL1FkVGYycnRxVzlDOHBxQ2NFdDk5Rms4L2x1MUZZLzFhUzZQSUNoZ2JUeVdjSwpydVZjR1U1UTA2ZHAzNU44emljWTBRQWp4RkhLQVdqN0RjWGpJSzl6QkNwM3NWV2F2MTladS9oeVVUYU85d2k5Ci9NSnBMRlNmVGVvdGQrVk05SC9pQVNGWG5wWEFYRTBoNE9weld5V2VVMjcrUWxxS1lCTWtBVTRKNDJNb0lPMXMKNTU1Rm90NTBudis0V0FZME5IaktGSEFwRUEvcjVlVkpGYks2WDU4RExBZlRlNGtQR3NXdENaOWptN1hiNnYxcgpBWTBvNlp2aXdlQXFEa2FXNmFqbzl4YkZkYW1MNUd2enA1QUJtUWtlYVlKVGR3UVR1VWFiZVNTOXc4ajVFNGlqCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbm1kSno3cHdVb2dTSnFWbTlmNDUKSWQzc3JLRTNwTE0xbFl2QklBMFZTZ01vTlFjQmUxaElrN2JKc1I2dEttU1A5MmsyK2hCNXhuUkhvMitxUm1UTQpWUHlOOHVRN2x2UFBlYTZFOThMVUVTMHVFSzh5VWl1dHBkQVJmN0FzcTBIRldDMHpnVHkrQ21PYW5XLzdXazFTCi9CTEhSUnpkVG1SR05BUmU2a3RnRnhSWTVJL1dDZDlON293dDgxajlETnp0NkdQTFVHMGxpaWJNbzJEMjBESUQKUUxuK2N3SElleW1ialkzb2FjdDI0VDZMdzRFNVZqQnZ0Z2NqaDZ1UDJKUUJJZXNaK0ZaVUZsZTIzVWJDRWhOUwo5YVBiSWRQcGY1Q2RhVlFmZHp1bzdnOEVrVGZYYkk4MDlPd001OGptb1lacm5DeWdsa0xacHJJanhhWVQrSFpZCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBby9WL1NHWmtZZ1ovc05wZE1wYzYKeHRuTjhuTGhtU0YveXZ5Rk1UVHV4NEhyK0ZvNU1sVTZNMTFKSHlaQ2NsSjZaK1JPZWRnTHlHZlJLNWIycVpqdwpYZWJtN0pHYnRQK1MwT0JGOHhzRWtXNGhUSTNwSnpENGI0RjlzNnZQYnQxUjVNc1pGaitPeDV4WXhCZHFFdGd0CkpKWW0vQXBkRDJYTEhLalg5SXEzSzdNb2NUMEg3Q2s3aFhmTDA2cnhiVlJoalQ2dllOUUNhZFpLSDJJM0JnU08KQXgvZWdTRmMyY00vS3JLOWh0Z2U2QWpROUNZRzFFRXZJdEwyLzBiMGNQaGgrTFpmTGdDeXFqeW90a2xGdWpYTwpNRG54cnJudWluZHZ4alYvSk5KVU9WUnNpMnYwR1NnTWNhQlJScDI5dHRhU0RmM2dwQXRHRVh5TDhneEc4SW1tCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEkrbU5mbnFsc2hGZ2J3Q3htc2MKWGw1NU1zem5XMzI0QVh6OXdGdUdsUEgwSGlpOHZ3Z003a1JJNjF2N1F2Mkx5ZzlydW5CV1ZuemJsQlZlUWk3Rgptb1hmRFZTN2hBQy9YeWE5OUx5WVJlTFBuY3FDcTBHdzFIYk84Y3FRbncxM0U5RC9XZ212K1RwL3JvY3RoYytuCmxZKytZNENkWkRDL25QeU93TTBuWG1xRG91WXJ0L0YxdDBSNTQrbjFPU05wTlIvSnk5V0MwN3ZTNWJ2dXJpcDEKVmRDeXZFRnZhWUx2cnc2TG44dmI3bTFlczZFb0FZdmg5YXA4aFRIRVkxNUYrS1M2Umw4emM3d0w5SGlmc2tYVwpmSVdTanNWd0F0eWdvaWpFOWt1OExsRmRCaEZXd25BY1k0eElDM2JpbllMTjhJM0ZHUkV4RnZST1NQeE9YQ0pyCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGxrSkRDY2luREhaeW96VVVVeDQKeTJvNDhsQk83SU5qTUU4WXI5bDVjRmFnM25MdUZlTk13d3VFTXVoQjV1TkdCenl0MFpTbzYrZnpKc1RaUmxiQQpzczJ4NUo3cmVrU3d0Z1BEZ1J4cUdLQnVBYTVyallzY3duWUpmL3FsK0pwbXU2Rm9OM1pZRlR1MjJKSVhjLzZkCnE0YUF6bllXdmVncnI4MU5xU01hRXJtay8ybGpHNjJhQkZEd3JNbTAxbmZGZTcrcncvbXp4QmVDdTVTZlp5aFEKUVBmQ3o1SGc1R2dFN2RnbGhnT0hwQ3dkcDNDV1BUSWpZbG9KbW83eDFPTnBNaWpVdmZjbTBNd3pHamJ6Q3dMSwptT3FVaWlPTmJhSUtlaUdqcWQzMlU4cEFDMmUrWGozRS84Nm11Q01IbkFVWmFBNjY0elRYaVVIc0FwN09FaWhXCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGJaWHFDN0xkVlJyLzJRSVdBQVcKbkY4NGVtR3pXVmM5dlExa3NKb0M1OGMrbWtmamRCeHRGYUNSdE5jYVhQZTVkdXFZdVJMaEdnSFNZc2dYQWZJUgpKb01XdTQ2V2RiY3NsbjJFTUdNbE92RFE4WnVNNXNnUmd1NEx2RUN3SnZ2UVhVa3ozOHFyOUc2S3JMSmJZaFdvCnJmQnozZTJ4ZTBFaXVTdnRaT3I1blQwclNsUzRPa1RwNFAyZkdwb0FHdmZMSmI2UzN0blVYQUtCRkMvN08yQ1gKc2lRK1VpV1hzOVdUY0c1VGc5ejlZQzFoeFBwNFRmMTY3NVFsZUt2eFlmenJ0b2J0Q3RZaTRRcnRFQlQ2MEZMNwpMTUZsYmJKSDVIZm9pMlZDRitDd1lzcnVIZDQyMWs4bHBhcUE5VVI3R1BQcVFBWW9NdUQ1dmoya1dIOG4yc3o2CnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNExKRUxhM0V6YzdJTDZubHNpN2gKR01FaWNoSWdUTlcxNnlvTW45dERkcFFGWjhSenAzMHpmV1lIWitsMUt6c2ZRdk50M0JtMGJDTENjamFPNS95ego5YkdJUjJzSzZLbG9LWVdUYU4vZHk1VjFsU0lGUU5ZN29pWURRZTZMUGI3UzQ3S0NRN1NaTnYxeC9wak4vcjA2CkpUWTY1UTN1cU5SdEE0aWFJMTJnWmNKVDQvSmUrN2VIZGpUMlBRY2xXaUVJbkV1TlRyWlJlcW54L3B3TVpyVi8KKzlHbGtHKzkxZElGUnYwSUV5UUYrNHBPM0M0dzdrU2ozRkFzRW9PL1BBc0NCcDVJMWtyZEh4MFl3M2x0ek16cwp4STdCYy9yWGE5akF4Z2J2ZjVVK2tNbTNlYXk4dG1ockxFelU2WktVYVc5bTZ4QkgrcVNpaDFkekU0QkVHeHZsCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGdwLzU0TGtnL0dVYmgyeVJDRGUKanpHOE1ZdjFlZWRDQlFwaXVrYnFrNytYSGdDVGNQNXpHTDd3SzdsaGNzc09nYUw0K1RnenJraFVBb09yYnp5dgpsQWRKbzV0OUNyaUY0Z0J0S3RMWi96Ny9GYTFQYUpVSzFwMWp2elRwVnBkcEJ3VjRSVythcDE0VnJrTW80aU8yCnpWODA1TDZtRzdOakxLdCtNbEREZ2JZa3FzZXhaUWtzRENpNUZabTlCZlNySWJEWjNGNHlTYlpLbHlmbGZWcUYKMkloVTVxcW9kcUpPUU1KVlNlSDI2a1V2akVET3lTY3FVd3llREUzSmNabWRWSDVKYW5OWitjdXcxeVRNMitXVwo1Z25tY1J1SzU0T2RiTi9qWGxtNGVMRFIrRzk0V00wbkdtSno3aU1xWHFIRHVUZEYvVDBsNVVJMHdSU1ltSmVHCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVF3TTVLd0kwVk00ZkVvTkN4MEUKR2FlOTNsVmppQWdBM2V3N3pOckxHNUJRZi96Q0R6dzZzaWNndVpYYjZXZjFaOXNhMU1yS1Q5aU5yRm0xUWdwZgp3N3pGdExXb0NKOXlvMG9HUFFUc3AzQXlmRTM0OVdYb0daelFqazhxQ3VDZDJvVGFESFZpSDBnVHg4aEVESUQ2CklVM0RVODYvbGFZTE01TkMzbVNYVWllRXRGMUhwTWRtL0lTazZ3L1VxNUNqTDJsbkpNY0VUSDdvdm1uYnNHVEsKZjl0UXMyRGg0M1loZkUxVVZWNmtoRjV0U0Z3cG4rS2ltdVh2bW00WTUxYWpMekYzSUFSYkgzeDVSMWhQRFNtUgpzQ2lmZTVyODR6dnJDbUt6WlYzUSs0WGw5S1lsWUNEL2NkUTF2cjlQNzNLREJFZU1lZjdGRnBlNURtaXB5MGxJCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkhXQTZNaU9pS3hJWVMrZWEySFYKQ3dGSWxJd1FPZUVIS05lV1RpUGUraXZwa1pTSnhyR2E0T1JVbmMrenNVTzcwWWFKVHFFUktSRWhkbmxGNWVQVwpMbndKeitoNTk1MDJEdEFldkNQSTNHbGw3VytqNWlQNTFXV0ZiNDc4UWVKMzlOc2F4Y3B2SWI2dWEwbzB1azNsCnVVaWVNQlI2NjVZNURhcVlBcHdHZE13MHByZittOGdzU1M2YnlITng3R2VWNHR4aTRDdklIWkVRb2RGRVJHRzgKajNWSGR6Z1h1Sm1yVG1mVm5UVGdUZ01XUFM4SENJbzFabnhHMlFFTnNHMkhsdWZyc28wNTVtdnQ3UlEzRFdmUgpzUjIxakZqMy9aZkFrVjhMRDc1UUsvK1FWN0pIMjJlSGJFZXBlSVE0QlAyZlYrdHdEOGxTMXhobkRLNVdlRmZqCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU9QekZ0bENUdGhCVFpFTERrQU8KUXAzMGdHQWxSV3p5K0dSYW45TjkxZnpZR1F6Mm51NHVGSHhGVnR6OHZrQThDTDFBbzJBdVR4T2FqbS9KeFVoUQpNSmlseG15bHkzMitVNHloendBcExkcTgwVTU5OWtWM05qZ0U1UWRybmcxa0pHajVhWHRqbE4xVHI3ZXExc3pBClZhd3J3aTlsaDZMVXRSVXNjakU1ZGxFQ2hUZ1NjK3RGSkdiam9XbkV0U05kKzA2K3cxODlaSDBWYTR1bmJVbVIKQVlseXQ2ZHQ3QzBnc2lOLzlMSFE4NFl4SytvNXk3RmxtcE1sSU45bVJXbmw5VVdoeVlHcXdheUNMUzZsWlFjawpKMG5FWUwzUEx6Z0s0NSt6cXY2Smt5aTZGTHR5TnRZQ294TGpjN2lCNnVCM1I4Y3JaeGxDN3ZVRTRNS0xDdGU4Ck9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjVseVMvWG0rQW9wSXhpTFo2TmoKV1VrWlN5c0J1Z3RUUmU1WWFiYVRiSjRBMC9PcXZiemluSnpHNWJudVZrMTZhY2x6VWVwbHBYaStpNXdUZXZuWAp6b1ZFemNlaGRjWnpXNjBFVlN5YXJQcnRsMm9VNGZBaTZLLyttOGdBOUFRT0pMdmFvdVYzOFBmQWFvRWZ4ZlJmCnJIWUk4UWlnZTRSRit0WG1MYytsZWNwNm5KMjVuR2txVFNabHhHK1ZsTTBaVEVkaE5nRkZTdzJnOUFjT0JPKysKSm9HdGo0VDNGMERiWFFxTDFteUx3TnhXWk5SMXJoeEpQbUNrelhRVk1kNkpDNThHbkphMXo5SW4zZGROWWZYdgppOTlHSllaaE5QR1RFUDE2RDFHWHNoZlB1OVRBd2YzQTJPNzR0cjVPQ0NaM2FrUk4vS3NLblg3N2J6aWdGSUtZCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGozYVBZZWo0eWkvYTJia1pjcG0KMXpBSGU0dTFFRWZONXg3QitQek1lVmtpY2pnaEdBRyttN3RFcjk1aVQxZW00ckpOUUdrczNNSmxPN2NYcjV3VAp2dXFSN1JtNHJkaXlRUDZjZUozUjBPMk8xWnFUcHlwTG5lVjNJNzVUaFliazFVN010NHpTWVpuK0d5RThRM1RDClg2Vzd4eDVOWm1HQVQ5RmVmU25GQ1gzK2FPTDZoM3B2QWNjZDFUTGU3SnNwamNkRkFYUUtRVDI4ZHlsOUNqbXcKQWNRUlV3SVNSVkdwRFM2bERKc2J2ajhxVUo0WXlGMENIMytjRU9Qc2NCN2FpbjhtRzMrNDJwRm5YcXBVMXBRNwpjWWVPc3hJcFU2VmRmQXBrOWpRaWtGL1d6eTJ1bngzdGRnY1NyY2FOa3lOcGNXUVRJdFJwRzFhaytWbGRwb3hBCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlFUQVFENHBzczk2dlptUno0dGEKR1dJSXBNSFd2K3ZwSzJpNnVMODFORmUwWStqdzRLa210Z2lqdHorQmYvZDFiYkpWc0xHV3VvcDRxNTZJVkZrNAo2LzdoMGIxM3YxY0VmSXdWRU92c013T3NYb0N0MWp4YlVwVEE1UTZxOTUxVzYyVzAveEV2OVlUZDVVWkhBZFIwCjZFaEUza2V6U3dtUWFMZlFiOWFYR1RNRTdCTGNuVnAvR0VDQk9yZXJaNGY0L1JnUWk1a2F4VTIwb2U0QzVrdzMKRzdjVGtDU2NyUmVIK0FIU1l6MGdvanM3MUFJUXEyWUpDY3laYVdPRzZ6ZUNhdVJOT1NYRHNjSUM4L1Z5bjhVUgpqT2dtUlJyZ0FoY01zbGdZc0dDeW9KUU9YZis2MlgrU3JBWDZsY1RxdEh4SHZpTDV1S2RTTTlyMlBHUkloUWRVCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3g1V2NJRndHbmc4WUM1dTVmbmIKL2IwdEpyWE1wSHk4U0JwR2NjNkRNSWx5QXZmNmFkTlRQeFRieVVmRWFBRUpSNUVSWEFkaWZDenI4bE1KM0tsWApaVHZjRittZi9ReGYvYTUvbHJyeUFsbVVidDRZQStncEhyTktMYmd3NFNmZUxNQTB2cDJMS0YxOUVVR01ndUpWCk02NVRORG9WTVRJRUdsMVBlZk1uckE3WWJvWlMzcWwvVFZMcmxYNFM1U0htMDZxNTVCTVZqdnNwbFVJWGlqRFYKVlZnbXVqVVFvb0I5Vm1FdXh0V2FscmVnd25aVmVMeTY5RU44dFMvemJTUkJBSERlRXdBUlRYY0dFdjdFUXZ4bAozcFdmQkpQWE4vVUVlSHZNeE44dHNkRzhna2pBbWZ3OXN4K0EwT01CcVF6U2l1RnpDMnJOcTFod2FQSW42d2xBCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWVSNWk3NzI2RDhOYWQwUzlwVFoKeWhsUTJ5MFFHTWt0dHVZaWFibWdHQU81SVQvTlR5ekl3YjcyTk1sT3MrVXAvd3BwK295Yk1nTTdyN0pvZVVVcApMTlIyckE1WGhvOG5ISkhFZk9ZcE5DOG81dmFwcXdXMS8vMlpsT2Y3eXpiTzlnRlRYbHc3djNNL283SExsZm9ECmpCWVc2L3VyK3pTMUM1Tk90bWRjVVp0VGhxM0dxcndybzh2SnVzZUFVWjhvMExuTWJiVjJIZGRRMWxKRUpWQmYKOUwzSVJKTU1NR05YalFIcjd2RjhMVVUycTJTOTAxeTAraWJGMDFPd0c5YnZpelY5aXd0N254d3BtY3J1bEF2YgpiaVRzNlYrRjZjYm4vcUR2Q0IwYjdZVVFmNjhYOTRUakVkQkNFYXlvVm1KVCtvVkl6QTJRV2NlcWNOTWxiWDM5Cjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0trdWo4Qlovd0tmeGZ1TG9KQ3UKaTRBOURUM3lNYXE3U3FqR01JcEw3S1JaU0t5bUg4MjBsV2FSd21aOFNOVGpBQ3pIeCtPMExjQkxrcmNmQjh2bApLMHRJSXlYVkhBNVVkLzNhcEdrTm53YVJsU0VUKy9mT3BjMm0rVFF3bldWbGNNVzdVbHVGMTY3RkVCc2tsTmd6CjFPUDNaNEN4cTdhR2RKaUVMckptNTJQNVd6cU1BbTlXUEdRdFVvSjhaaGQwVXJlMW1lMk1LNjcxMFZaN2Roa1gKbGlsR2dnenNuQ2prR2Rxenh4Y1Z6akpFdzJBWnNjbGx5a3NST1dyUEdYTk1EdjJxWUs3YitLSTZqUGNDQlRXKwpmS2l1SzVTTXFDaGZJeENDd3FTc05PbzAxSklZYklYd0txMDFaMFJvbzY5Uk9QVzh0RXd5ckVQcGkzbFF0YzJnCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNE1qWWUxSkJwOTU5SUM0Zms2MWMKQWduMG1TWmU1MkZERExNcXJYV3NTQ1QxdWFTVzFrMzZzelMrelVIUDN6S2xDbkFxYXRxN0NFS215Z3BUczJkWQpYTXlIcmVFeVhmWkMxTkFOc21ocFgyZXlHMndDN0FGeGRORjM4dUtaZENoYWtXMHhMVmxIdXh2aElPOGk5MDlvCjVqVlZJLzV4anhrNTRXeGNpUWJVN1E2c3drRmR1L3JvQmxaRWZFTExOWDZLbUY3V3JUSHNBTy9Vbnkxc2tsVDUKd1JXa0JEYnR5RGNOd0tWOHlCakFQZm51aEZrU0wrQ1hOVGNiM3dEck9Jb3dSWWhrN0Jhb3ZVRldqUzBRZjFxQgpzb054bXVER3V1YUNpMXlyUENva28zeFUrWWI4TVRhVmpMUnEvNEUzM3ZQZmVvVnoxQkVDck9IM2cxT3AwdGtRClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBazZrWnBCL2ZsTjF2ZVQxY1Y1Ty8KRFJYTVB4UVhHdS9nbGV6SHlDTkU3ZUFVLzY2MmRzUEFSMUlJWmpVY0JGcURkdWxycnZzQnNKYUNjbGxvbWZVQwpjYVRUSlVDSGFmbWJ6ZXpRWTR2Z28yVDhENEZNZExzSHgrdmU2K21DbDZBQjBQWDI5d09yenBqb0I3R2k4MCs1Ckg1WHdFSytESFQ0UzJrM2RJQWFncktPZlYxQkJqNUtJVmNsVlVVZnBlWUt4WENaUWtRb0VSeWU0QmdvMXNSSGUKcXFTR2c3MU1vbDc1MTA5R21IQmlFaW5VTmprWGJ3Z1NpYkp0ZUp0V3lNVGNmWjRLVHNNNmZLSC9hWGVDOGZnMwovMC9DaVlwRStFWk5CRk9sOElpb1ZDZ2JTVkxDcFJVZkVSZ0JQdGVIejdMV2t3bEdWdXRINm1LUXVJVUo1VWd0CklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjNJbk1CSXlCYUg2K1QxTU8xV0EKQ3YwY2ZpQUV4cThEOGx5ejVxdlR4YWl1UkpJTFJOWGdFWnk1L3pxZWtuRlRVMzRLeElPUVZXMFNTVHpTK1QwQwo5U0VyU3ExK3VvaFpMUVNtamRYRUdJYVlsSXhER3o1ejBuRnhIVnQ1KzlESUdpZmt4RVhuQ0N3ZVBXOVNQYWNkCkVGSnl4aUZsOVY5Y25iYitNclB1UUxRUVFwSy81aS9jWGxnd0NEazUvaTYzWHlNaDBWR3VKRmsxWkhnVG8xT3YKYVdzWU9sS2hLQi8yUmc5Nk1hcllVV21nVGdhWmZ0dVJlNGV5dHZXZzJCd2FVb3NtWTFzQjJ5UTNiU0MzZEJKUQpkazdydDRvMlFRNjZkVFB1THhPU2cxc2RsdW1IRk1KRXhUNTh4Y2YwK3EwZ2pGQUtnTG9QWUlBOXVOdUp2eWw4Cmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHNBTUdsditWblRQVzhSdHlDVlQKQlBSSmhQVFVxNU5aMThOVHhLYTlFM1N2ZFVPRG5GNWVqZ3BicFRhdGg0bDRvY0dZMmhMeDl2TTY5VjN2R1RMLwpCRTRrVWYvRm1YdndlSXNWQ2ViV3Ntd3ltazZFSmFxZm5zZFR5S0tCTlNDenhqRU5VbGxQbzZKSjBpNk9oU2E2CjQwcU5nYnAyLzhNTEJqb0NIR0k2WFdrMXhSQ3hyQ1o0NFEwYUdjSFlnb2E1YWxmY0RaRnk5dEFQVHRkNkp3TDIKZ0R5T2ZUVktibkFXeTIzU2pqU2hFZTFHWmRjaHYzaWFyUE9FbmNPYXY5M0hIek1DU08vY0ZvWDNNbFRsRk95SQp1Yk42bmxEd3RpV1hiRkZNYTZsZngzQ3JBWkEwQ0FkNTQxU1BhZFM1Z2t5VSswOFJRV2NpdFdYV05sNmhDaG9oCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGVPckNncGE0UVBmelU2R09sNXUKYU9salRrbnNCSnZsOXZ2a1c1M1ZoeWVzMThadEdOWWtVZW5VV1dDMDNtRjA4dU9sZmh1UmN1Vlc2SDhyeTdTNgpjMTZrMWNNajZEdFZkV2NQVmo5QVVSNno5dm5ZZDFmN1BtK29kelpHQjBXWTRPQ1UwV3RpRGE1d1dGK0dISTRGCkZWUnpaWGFDQjZ1QWJXYzdBa2J6NFczT0ptQmxiazUrMXBtdmhyNzY4QkowR3lHbTk1TlZkNWgrdUIzQUg2Mk4KRzc3UFI1ZFlSSS9mb052WFAwY29kdTNWRGk2VTFCNDJIQkRkeVZKUkNmQzEwbk5SSjg1WkJvcXV4VGtwWVBmawo5VG9KV0dTYkJiR0VRS2ZhNXllWTRiMXNtZHcyWHU1WCtGSHo5QVR0d1RLeU1WNVU1R3RJNnNpa2lyUllHZzcwCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdys0MzNOU0x4RnpKck5oL0RLdzcKcDA1WVVBbkE1TDREZG05RWtObXZQK05INjJzU0ZWL0NkcWo5cVkyMGxPNHJzQngyR1N3ZnZRbThHaVYzUVdxVwpNTElNamcrZEIrSnorV3JTemxEa2tmN2g4L3hpRXhQZDI3STREZHZjTVFHakxTczN4clJVYVQ1THMvN0p5R0J6CkYrODFPM00xUVFrZ1VOTlllZzladnJMMGdjUWVpK09KYk9vcStwWm9mL3Q1L2VFWm9QbU9ueHVkQkJpVTVheTcKaDBlWFluWnI1QXdSTEZaVG1GMUJPWlMwZHdFeDRRWVkzRHgwN1U1ZW8yMll4K2NXckRIMnB4ekRSQXoxeGpTOAorb2hBSWh6bHBydVdNYzZNeTh0ajVZQncrTTU4WCtOc2QxaWdxVzhveE1QdEhoRHpkcWEzK2lrazNwbjdhUU1OCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGx2TnlUUVI1cERrbHJEU3l1czkKQTJETzgvbUJvV0QzZUY3YXhHZG5NQW5qT1lTQS9nWnJQdjEvNWJINWNWU3BpS1JpM1ltUC9NN3V0TWpmZ3lmNApDNFZPbEJDUW1kRGphNExOMkI2em5RMGtpUkl3SnhXZHN3QmpYRi9NOTFweE1lMjFHei9sbmlUc2dJdFAzUEFlClBOSm5GNzlBS294M2ZOU0Rod3hkeGFGNXJGMy84cFVjWGpqVWpwQ2l1V3VvQ3FZUjZqZmZ0SkZNSGZ6S2U5aEcKN05QWUtWUnhEemhHRnlzcnBGNm1QRHVFOHhZWmNjdnVpS2RyWWxIM0t0RzVDSG5PWWJQQ0xCd0ZRZy9CK3JYVgo0NExJSU9CMUhuRUU1elpwZ0R4TTJSRGRKVHowU2xUaEpjMzZueEI4OVNLUkpSQmVsOE5LZmo4TTNZNjY4L3dHClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1U0ZmFNQ0hhb1dLZ3dLc0VxT3oKS2ZUYVgxOU1SZ2lwQzB0WnNXUFNMellON1V5Z0N3QVBDVU1hMWl3VGk0NGh1OVZlYjZVREl5S05BcEtRUklxdAo4ditVcWxWaEJlY1RuaWF3QzlKeXVUdnRLM1NMdjFKeVo3ZS9HYitHc3JzMHdnRk5HWktSUUVieXhjM2c1UDRPCjEvVmtOS3MvOE1jWWNodnJoZXQyUlJqYWVTVUZEaGh6a0ZweitoNTU2ZUhGYmFqR0lmTGZFajAxLzZFOHpLeDQKb1dXb2w3WWlmNUViWEM1akd6U1lEQldvd3RBUjhYbmZGQ0FCeW16aDdNdlNSdGJIdnIwSlJMMHZrQXIzWWpEcQpHQ2NKOXZYZEloajl3YWpVM1E4RTFtKzMvdmo1SXBlSjh2ZTBGdENYTXFYVWhHVlZJWGJmRnBMSFBoRVk2SDBwClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjRSNithdENXK0l1NzBwVGVRWDgKV2lDL2VBeHJ3OWlrUTlKSTVISFh2Z1pmNVA4WkxCUlhqeEZ4Q0pRcGJxWmdJZXRodnM1KytRelhaTFJsSWYyRwpzUFppTFY0ckNucHJSL3hWSm1JeklaSzJsUlk3UnRoLzlyL2kzUUs3RHByRmVTUVpyc1FSN0RXSUltZ3VmaXdCCmJKT1ZjQkRlZGF1VXV0ZmtXOVZOd3JrOVdSR0w0TW9icEFxYVlrVEcvSDVnNFFRTVR6aW9kZVdwbHpBVnhnWmIKM2szWTR2Y2FPVkVkOVhWNTJTUFQwckthV2xBS2xFZ0kzQUwwcTdKdTlQV0lYaG9WZEpGSEtvZmQyb21OM2xrOQo2R08xWkhxUmNZYm5zS0p0NHNVNDArdE9xWGs4M09CUUpnWkpabi9TaDQzcEx3YU5YSU53aFFyOUZIZnhHUi95Cjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1hHaDAzTC8xamRrMHppL3RGdHkKdU5FOW15bXJaa1BGWk9QSENqeU1nZ1hHelNDK2x6QnlJZnlCL1VkS2RCZ1ErYnUvRkhZbXd5amdRK3NpZkxnSApkYTV3dG5xdkFGN2pySTFGRXVaNW1ud1pOcGpMM2M0N25UaWk3eG5qUHBVV0ZvckcybmlrT09ZKzBzRnhRZkhmCjlsOVFnTEdBeWFHWVVxNktYbFJ3dWdjbTZ1SUhPdDFlQzRCS3pSeUJvYjhFZWFkaTMxbVVCL2JoN1o4U25rbUYKYXpGcVlyTFJXSFNPQjliTllaVHFtYmdtd1ZmN2pBTHNGeHY0elBKeXAzcG1tRVFBb3pMeG4wYllaUC9zcEwyWgp1MW5BOUg1T2hwdjNJTThiMnhjbFluWlRubThrY2RHSk85WWd6Tnpwb1B0UGxVLzlyaGs4dWU3eGdVQ0lmcEZxCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0g0b3FoNEpjS0d2YVVqcS90NW0KM3JRcTliTFZqV213d2JJZDVKTVZlUFdWQU5VaTBBVWlZODhyb1o2ZDZaaVMvaWwyQ1Q3cERSTlJTdEhiRFJ3ZAoxQkpRQzRnb0o5dzJ4ZVp2Z29iOHQzUFJLS085cWYyaGtoaU8xbG56a1hFaU11b2ZRem91RU8xbXBIV2VvdzJjClhuQ2c2bFhhN3RFMFJRdGhQTzZvd2RTZ2F0dUF4ZDE3VGFJaG5WVjZKc09ocjhnUlI1QUlkV0Q5VDVEcE1wVmcKYUhyOGxraHZTY0VJdFdZZVM5bWxWMWNFZVJNa3JsNWVoendmZ3BhN2J2dE4vdWVEeWtPdGV5bGkwcFVoMWUzUgpiYVlxLzVndEpXSS9LTGk2Wk42L2JVczBxSzZtK3lkbXBRYURMclVjNUJUODhqbXdEbmRxRVFrMFh1dmhMeGFxClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEZENUEzSG80SDYzNUxxK2dPL3gKMjFKaFBwT29BNzZKOFNFajQzSHJxc2w1Smw5ZmRabE1yQU9QTjY0TnkvWFBLTDdESHFXbnJnUG1saW1pZk5YZApMZnp4QVFVektYcWV2cHJrbUMzRzJGcGlsY1c3bmY3c1RsTXo0Rktic1pwdUFvY2VmQnpFZDZ5bk5qRE13OWpOCmFpODZaZ2FhaHZhSVpmTS9HRXlOS2lhZ25xQzM2bVZRT2pnZCtQREw0Tllxd00zUDdUQUM4MjV5d0ZGdk4yMmgKVzN5d1orcVpRTk1ENXZWOE9ZV2NaMWdvTU43UlVHUkdRa2lQZXV5UHJrejNkYjdidWJUbEZqOEdkSFdoRHdtTApJM3hML3NVY3VmM2I3cjRFUlJlVld0ZStJeFNRcEdLUzdGd0h0NSt0LzIxNFdsanczWUEyZFRtb3ErMm56amFmCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFlPTXZsUHF0Yis4c2E1UHdFYU8KQzNOVHdRR28vTWFzS1Bpd3R4TUJBMzdLcGZEVWNhM1A5dzlZQVJjZEt3RjMyZnF6K1JSaFdUOUljVVZORCtMOAo1VUpmNUhFanlURExORDZzaXFQNzZwN2lKN0kvNHliU0tSY3ZIL21tbklJVjdndjZiSlpnbVlGUDRtZHBuRFdiCnlhbHRjL0JTbmMxVVRkUDlzMXFqOVNqUEc5U2xEK1psbWRRWGYvODJ3SS9ma1ljSDYxZjg4WnJQZHk5eVRoc3gKRkZLU2ZSRGlqT2Q1WTN1WFJLbDJQa2FJV2FNdWFuUUNiRFJYQmpYZ0xLY3dQbnY0WXdURGN5OVF3Y2o3Z3FBcwplQ0tUUG4zVFUyaGNMK1NrMngyeG5NdHlBRmp4dm5ZRlkzd2FPRHpibjNTMlpTb0loelJnWHFEaG56S0pnaHdyCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGdQc3JNQ3ZKKzIzcm5sVXpkWkYKZExxVUk1OGtSMlhDMThIaURNelRPWHYwSXVTNnQxSlVwMkE3Qmd5VEVaL2hQRE4rZVE2QVlMbnNCSDBsWEZDTgpUTnMyQitRd2RkTVFiOTZMRjNKd3FRNHJpVzBCU3FCNW5jVTZFZzVRYXFqWFlNaHMwZTE3T1JIakVUbkZkandzCmFPM2JzMG9Mbk8vV1RuRmQ1SGxGU1lHVE1NZWVZNG5FTGZjdkw2bjBxZVZxeHROR1dibmZmVFF4TzIvZXBZYWQKL0FsdjcvYzEvOWZZNUplUlZNRmxLeFg0VVVUM0ZLYkJnaEI2WUFzRjdFV0JXaUFnb2NRTU9QSGdBa0l6bmJrbgoxOHVKM2NEQVRncUU5cnFjWCtQZExwSDBQMExvYmF6YkpsNTZRcWdKQng4UVN5WWV2eGgwWENTWnltUjUzMkJvCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVd3aEV6d2xybFhaREpLcXhvSEgKSU5mR0o4YzgrSEYvK0k1MWhkRmRUaEVQUUI0enZnN0ZqdDZkMEwrUk5YaTFKS0VHWTlERWJmaWZqVUVQVk5mMQpHanlMMGFVcmswTHg4N2FCeTNIaWM0Y0F5WjhCWU9ONUxtb243MmxORlo4NlVldlpIVzFXU1VKSU1RY3BONy9PCnA5V3BkSXNXOENiQ0xlNHgveTNjem9mOUd4M3JQVXd2SVpmYU1FbEhyM2EyQ2xzSzdYaEJaRzZLTTBXZTZ4WkwKVFlZQmhEZlRueHhZQlFUNnBialU5Z2toeFllaXZSK1FPbmpWRGNWR0pLOGlyMXhJeFYxTnBCY3RVZkQwNlZtNQpnTElxQW1KdkpQUzJnRUIwV1FKMmgvZG5KcmU0RnZCTHFyOStZMGNYQkkwVmZDNXlBdUl0azU4UkhrdytlV01HCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGluTGRLTG9MamVxWDZncDAzM3oKblNrbThibjlaVTFBYVBiSHNMTkE0OWs5SHFpdDFKRlQzdlQ4SzFPUlQ0ZmlQRGN1UTUvWklHaG1CbFRweHdENApwaFJjMWo4dnlWZno3TmxhYWpSbDR5Z0hHNFcrVzFNd3ltRFRSYVRqSHVwUmpjUGpqSmVvSzEwZzIxa1R3b0tSCjJmQ0FjV0lxZzR1ZDMzZ2pXU0tZb3lyYjFHWE9KOVVJS0JWZGRIdnV1LzBOeEp2aTZ1aUJnSHdpTldTWnFURzYKaER2Vkduc014SHRsVWlKK2lkRWRiTHhpMGtlUlNRQkFiM1ZYVGRHeDM2dUlkUVZFSXk4SS9zV3g4Ti9oeStmSwpqdytEKzhOcFhPWmxyakxQRmFTS1VEcXNqaE8ra2VIeHpVL1VBRVRsQ1daNGZMNzZ6VUJKYi9JdjNvNkNNSVA2CnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWozbjkyN2h5eFdrVEs5OS8yWkwKYnhrdGJORldmU0diazJZbU5sWWgyd1FYMmxwM1h4eU1EMTRBYS9LcWxrMnY2MGF6UkljK3BKNGN4TElQY2RZbApLY3N2NDdRbk9uQnFBVmFaMW5wZGhJRVlRc1AzV1dqbk9sT3VuUXg5emtjUEk5Q3NYZ1Q3bC9td1IwaWE1eDdJCjFaSlllRTZZbXY2RHdMOHhKYUNGbEVWYnpNV1BndmorN21SMWhKRVdHbHJlc20rc2JQM2taVkFON0VUNFlnenUKOUhicU56aHVaRDlqa2tXYW9waUN4MkdHdllqcWo3MTJsaGR0N0g4NzRGcURKTk5jd056VXNyZitMZzNVNTdYagpKcnorTWlGbmZTQ285YVM2dDlGRnhlajFBS1c2YzY1NHpYaXg1SmRSY0MvZm1mMS9ZMkhIZGZrZGRqTG9MTExICklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjA1TTRiNnBGQW1EUWJHbWZnaWgKU2l2TUtrc1p1aEc1ZWZ6NFZsQkN6blRoYWVKYU0xVTdiaDNPbTN2ZmtibmhzZzlTc1FvczB6RVNIeG5xMW1kaApXVzE5S0pyTGIrUFlOTWExMnl1YnNOd0h3a2xBTHMrdGxhYXk3VXg0QnZWRVYzelliM29oRERXNzFxTHBndndpCjBEbGZWZ2hwZmhkeFRFcFI5d0NtVWpWdm5DNEhObmNGTlgxU1Z3cWhuNjJrQTgvMi85SjFFeExFN0JCWUQvaGIKazNnTnRQYWFWcFBYeDlhOFQ0ekhwZnBPTi8vbVVPdi83YVlhd1pOdlZtMDVBakJMNXhIUXlFTFZFYU9uNFFCaAphTEJ5ZTA5ZjZyc3NmMEFTYllqcnlib3NuRjFYd0YwTzYrSGdxQ2oyQmZhREJzTnZuTVFzN0czRUQrZ2hLN0dzCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGp6RTFGTEZnTjVRTWZlamltTEsKNGlBZUd3T0VtKzhaT3ZEeUZNVVlmV2xnNzBTcWpqeFRmbXJ6SjkwbHRmQjRIWmJLd0ZxcUhWWlJCQ3RiNGR5QQoxVHBOU1dpMHdyL1Q5UlJlb0g2YXpxM1dKTWJIRlB2cWdGN1JHTlVINmk1cFNsTGdoZHNlQzFaMU9sUjRTQmN6CmZhRXVuMHBZK2F0RXlqc2R2SGZBeFhqS2prV21saGZ2UlROVG4wQTZaNEc3aU5jT3RLNWc4Z2k2aERzc0ZTeUIKV0d2bTVOWVo0b3FmbWlPbGl3RjNzb3VhL25JcGNBRHFSdEdHTmtmY3JQVGhjYTRHOVFyb1MvSytsaHEvMTE5WApuV3BTd0hmb2o0RUNqQmNHSi92OHBqTWdVa0hObVd6bEVGYVhRb1FJWHlHNWRaWUNDRUlneGFGdlBJQTNIWmRICkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDROY2FoZEo1TDFFSXY3TVdGUGYKbE4rSU50Wi9kSmxyNFkxTUlEVzQxMmUvOTFBMnE0azI4akwrWFVYejNUN1pyRGtNS0JZRlI5a0dMUDhsZnZqOAo4OE9HMDdDZ1hNa3FDeVdWR1N0Q0V6UFpIcUFkODJCd0taS3RDTFFzY3NTcUtJalcwQytETS85RTU5T3cvdXh2CkJTb1I4dkllWmFzY0E5cWpjLy8wQnRjaWJzdmcxRDRLNGJXSmhad3IrbG9jRlBMV2lQMHhRb0djNnhmNGV5OUgKbHY5VDZaNUlkcUlsQ0lYOC9CdnJzZUZFN09qQk0relZvWGFRUDJhQWI4TEFtYndIcWx0cWU2d05xYTYzZlppeQpZQVJkdXB3TzhJbzFxcGZOVUI1RFFjT0JYcXhLeDBIVlUwWkdseGVXTGFpL3pzQmxCTFoyT2ZTMzByNGFmWHkwCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclZPa2tjT0lKYnA4dTk3SlE4ay8KdEVyb2tRUEJYeDJ4K0lIT2FQTnU0Mit5N3QxTlBETnYwdkNkWkI5R3RnUlRGMDBxZWFPRnUyUkdPWHN2OC8rdQpXVlF5VnI5QkNwOEtsT0RzRVdjeGY5N0JDMU9zcTRDb1cyQVQ3dnRnd0hKdGdsRzBKeDU4MXBub3FZMUdsNzdMCnBrOFFhSlZXamZiTGZDYmNHWnZCMjlMUkovNmlEODl2ckswbjhrOHVITEk0TTgyUVVxQjlFT2RJTG5KUktLU08KZ1pDVHRrTjRWQ1V3ckVQd2YwS2pIdUJCQVNoNFpOR0prK29KQVF1WEI1bFZvK2M0aVJnZ1FyT3poams3ZTArZwp5WEx6dElpdUNGTE90aEdQL0htSTVRa3lYVi83RytNRTFqTkFNQm9Ed1VuUWk3elhnSElOS0MwcWc2bmxNVHJBCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGFDNkNzOFpZbmxuRlpuVU9VNUMKYnlFUXkwWkh3KzhLNFBYczN6a2I2QkhpRzlTTjBVdk1LdEg5OVVwdy8zZ2lOUDAxMmI1UVpVMDI4aWVFTlVxbgpLNFNaUWhQLzlCWk1QamMvYjZGTzRqdmNoSGNBRXJIZCtyZi9ETXN2SUtNb0N5aEdFZXkzWER3aXVxMjRLeUpjCnFUVFZUWDRQbGlVcDQ2OTR5WXJRUmtsaWpSZThiYXZIb2JLblZ3R3B4NzNsNjJZekt4Ly9rMWg3VlNhOEdIa0MKWEVrbFU3bE0wYW9NZWdHa2VyZmhJdnpENTFzMDU4YVlHUzU4bnRveGRNdTZadXZ3bjU4T20vME5lYVVsVlVINwpBNWMvSW9HNTRSU2tKVXViTjFBbkVNUDhzTk5rdFVxMkRDbXQ4eGNsUUJ2U2xBcFZUUTg0TjE5S1BWZWpIcEJGCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXJRRC8wM1BNZ0JnVmFyZ2E3N20KYXhrSzBhOFQvQkFFZHhuY09IQTBYRjBmRzlhdG95TitSbFhJWXNMWUhZRU8vekxzeW1uMDdHQ3lqRW1oK3Y0aApzcDBsbE93Y0FjSGVrWUtaRUZZd205T3FtcVl2Q0Y1bjRQaDZkTzBHK1VYU05uSDJuejJCY3pTL3pWZGtqMitWCmFXTTdXVzJYQ0tIdk1kRkRUMkJheDViMU5JbzVLQjZvNFNBbHlVYUFTOHo0OEw5ZEIwZ1l2RXloamdqazRDdkgKVkVncThMNVBsTk1zRHFsWG13WHlPZ2FwL1RMMXR6Yzl1RGI4b0NROTI2cXVVZDJaSzFoOFV6NTJ0Wi9LU0syUQpEZit5UjlwOXBwZ3hQQUM0aVlVY3NSdFgxRnFEcitqbVVNTGtDSGIyTkl3c00wY0lhcUZOdUJFeVQ3cE1JZm5lCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUVTMDB1SVZ0dTNaa0dXUDNFdFgKUW5mYWFUZkxlTlZaQUpmdUNTL2hLZjhzdk9nVEtDK1JBN1phdGE3bmFvT1JPQ000QmFsaGl6eWcydWlobTAwcwovaDZNbmttVGNKaEUwdWNiclV3ZW1pb2dheldDZjZwYlowb3B6RHJSclJMVjVJUU9mOXlMR3ZCSExPZFU2V0xEClJxQTlVWmROVitZWVZNREpYc2hUNU4xUGJzeU9reEEyZEo2RnB4OXQyb3d6ZVAwNTJERmdycFkrMThoeWIyQ3YKNnBQY0hkL0cyMWFJRzJDTUwvL1VObE4wRld6TThKZkQ5dmZscFV6VTRybEYyZlF5RFZTZ1plMTllemJrRUQ1TApsQ0JYU3U1ZUJzaHNROVZGMUFReGZKVGlCNTFpekVsbm8rbTFuWlFOWlVtclJraWZQZkJ5NGk2WDBnZFo1djZECll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd004WS9Ra0pvSkk4Y1ZuKzZCQjMKSENNTjZ6K0M1bVEwQ0dXY1dqeFBOZjFrbXhmenNaaExMa0Jma05WdVlvMHVGWkQyYXcxV1YrbjQ3NGdNK05jRQpjazdyUGNtMG1JenhCYjFLRXdBYy9GMHRIcjBXQ3ZqdUdVQTVNMklDTW9EK2huSnBoNUFSb1I1MFB4NTNkM0YzCkN5R2prTVpsTy9qZEVQYXdkMnBnMDdoZHFGZ3pYVnVKVUt4N1JpL3RlT1NobEwxTTcvU2YyWVRKK1BkaDRmZHUKcVpZN2EwNmF4Mm5JRFd3RGhqK3pIOUlJNWcwa0xsYTRnY24yZmVzM29OaUVLZm5BWUJYeVJ0R2F5OWJGNDZ6QQowMzVDOGxvNU05YU5DRXpQSC9yYnFtL2U1ZWVHTmtzeWVjb1Fpek5pOWphUDJwU1h2ZG9GSjhjQ1lkaklySTVZCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjZrNXJ1V01EcDVKdWxBMUt2RU8KY1hFQlRwYXlDaHBSMDFxMW1QcDZxK0M3WDdOeXc5ZzJTVVowQWRlL25qOExMaVhjczFnWjNIUThnd00rVFBlMAo3UGRwMUFxeXBrY1lzSGJidmhJWnJSYmxLM2V3aGZuKyszTVIyTlpNQzRReW43aFNhK2Q3OHF4Qmd5Z1FyRC9xClJsTE55K3owYzJFZ2h2VTBiUmMwS3dtT2NxdnQwNjRXSHNVMHRCWDM3bkRKaWtHYXlKMFd2MnAvWGdmUTZsa0kKcmpDU3Z4OWtUbU5kNVQ0SEpsbjJIc25OWTlLeGt1WHRGWnZVS2hsbzB6STQ5ckU1ZTdjWEpNbGo0d1dOaWwySwp5TS94TVA5NEU3VGJhLzlVNkxvNUlPZDEvMjNoZVNvK2dHNHlaTmVWOXJocmc3OGxlMXg4Z3NzR3prc1dOTXNOCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFN6b1lKL0dlYXNFZzVlK0hmREIKZ09XNFViazBmUGVwWmdSNGQ4SHJ5Z0JHM2dvckJ1cU5TU0hBYVVjSjVObk5pNmxnK3U1YTduZDZ2YVl0SlNzcwo0bDY3NXZEZ2ZObDQ0Q2VtL1k1ZlFTWlFES21NTjE2b2NBMWl6QnFkbFF1bmNHQmQ5WU1wNGlGQjVwTmJQd0tnCk9hMFBYNVVySjROWHI1TVpUamRiejhBRmxTVnMwZU50TytlRkt2Vy9lZTF1dW1jQTF3Z3FXWlU3SjFyS1pUaTcKWS9vYXdtSDRQZDJHODQrSmNjRXhJQlIwa2pPR3FjMm9ja0h4czhaRlA1ZlRFM0wrYXVObDVFRk9ndHByN3UwcAp2NVJTYVg2OHlyYlVhTzd1S0h4a3FmU2gxTmRWOE9kQmVNc0RFQXpPK2ZVRktDeWh3QnpxY3BjVCtRSkc5QjZyCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3c1d2dhWUJrbWJIeGFYQThzTEQKaWxMQWNnUGZHbk5rMjhyQ3BYMUlBMStCOE1HYlFPUjY1cURCdm8vbm5Ob2J2M0ZVWkMxQmhtYmdMQkZmTmFtLwpTb3ZabVN4bkh3K1ZzSGNjYzJRSlZqbVkvS1E5SEZLMENlbHNncnRNeWN1QXFoeXNPc1JqNGVyUFBsYVAvRldSCnhtT1N6bFVYTG51ai9pMXo1ZXlXYmtpd0lzS0dqNnpYK2RDTW1xbWtYeGd4RFIvVVlXMEhrZ1FxbFlJeThRYWEKVWp4L3c3NFYxaEk2ZzB3R2xCa3N1L3R0Z2hjSlRIWmMzU0xHbGlkZFZydFZMT212bEhpQVZCenBpK1F0SG9PUwpKY05ZSEhDU0tVbjdZUWhKSU9LckFiLzQxempwZWtUZ0RjQjU2bjhkcFBOSW5KOFBNM0h3SUZRelBoM01EdlhFCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkpOU1V3TUR1VmZocWdBN2o4eDkKTlNuYVVsQ1hQV2h4VjRkN2RocEFMaUROTDIxY205U2tVOE9rdStteEhpMWJlL0QzZFY3amE1ZEY1NTVZUHhSdwo3SVhiTFZOUXhWNUdsUWZMZ1VnS2d3M3BsNE95NTV5eDExWUdwK0Y2Wk0zcHVFdldsRUNUN1FydkVEVFhOMmdKClBJMWoxZEVmdDY0R0pkcHZsRytLU0szVVQwS29MdDJyaGpyUDdEcUpMbGVPSGpTV0k5ck5EZU5aSVFGV0lkVUoKM0EvSThmTjBNTzNMa2Z5MlFKT2U1Y0NIaG5UYi9ZdVUxYy9raTRacjlJN2p6ZDRHTjI3SDFtODFBaXlsbnVwbgplTEdzYW9lN0FJdDlnanBSdHlNT1JRN2hndGtTUHduS0dPN1FKWHZTb3hIdHZ5cmJRQzh3a0hoNkJYd2xqazR3CklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVlETjJ2NkQrNHQ2aS9STlBUTFQKaTlZaVRKTnBGamRxcjNhaG1aUjcwcDlLUkpwVjNTWjJPeUZ1dTR0ejZjMnhGZmZKU3BVYytSWExXZHdSTFhaMgpuR0htMmszbGVXc2VvT1lJU3hVcTE3SzM4WmdwdjdxVkN5cFRnSFRmb0lBMVE2S3dvaGkwUmhMeXlzVDQ5MHpYCnhGempVaHJ5ZEl4R2ZVWnpDYVYzQWdTNm00Q1NqcTVZQ1psamx2NG5XTlYrNjNVSTZYY05xTmVtNHZnQUhkWmwKakNpa1pXSGlTWnhNa3NZSUNuWG80cHdtSkkwbTlJNVBCcnpWUXJoS1JjbWlkTXZZRnFhdjVta1dsdElweDhUagozdkViUU44UEJ1YnQwZ2dmU1RxZEFrdEllaFhvMmEyK1hPbEtzdHlOL3pGSHhUMzJvNFh1TmxncUpsVzY0ZlYzCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEpWcjBJc2FOTkhRaG83cGFTd1YKWEVjK0c5eFY2RUErLzQzOGtBSERXLzZsWjRPY2YzRDZuL1A2VTc4S3Y4alNKSjdPNlNqaThsbGZTa3pzaVJOQwo5VVhIWWFJVUpYR3ExbzRJOXdUL0xEQ0w0STA3dFQyczFydDlnbnFYSWFhSWFVL3BlVGg4WDBPMUVWbVdBVkdWClAzb2dHLy9zVnQzeEJSOTVoUmNFMmVqam4yRlNxeUVyajhPbXVsbUFBTzVKSXU4NGVCMG1BYUJlZHFkVEFzMDAKK1JZQVl1M1hHSHZrMTdrSndUTDFrcEEvdDhCNkxhbzEvSWxYNHh4R1pOejN2TDJPZnRuSlFOQ0ZKNjRxV09HSQpDa3BBdlkyTUhhNTAvKzRJdXpEeWlOWWVKWGdwRTk3dFNyMHJwNHVSMEg0YTdzdU9UK0VpUDZKWS92eU5PeVNDCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcy9FVDdKcThpVVNSM3ByVzRHaVcKV2UxNVhDNDhMdkxNcVlGNm1VczBjcHhOZzdFZFVnNDhOV3F3TGJUNExPdjVET3dkZ3VKdmdKM0xWRGlhRjVBSgpHbEFESVU0MmdTdTc2cnM1NWFGYXRMekp4Z1dlbkpXVWpaUkMwd1pDclVoUXVmYktEdU0rWTNiVjBMa1JWM1pCCldPa3lHdnlCVWhkY3Nwek5LRTdjTStibU1yM1I0VnZva1crd01SU0Nld0xwUmNzM1Exc0RBUFdTaDdDQ0RSNncKbEJLWGUzY3JiaHZJa3lSaGpPWEFlN09XYkc2ZEFybzlsSTQyRFlMZGpERlVxRExFOSt1TVRSbTZpQldQTUs5LwpML0gxWXNyR0Q1Ym16L2FGS2xTcDUzQnoza0R2d0FDeWp4N3pVM3RYSW1MdDlBU0JreUVuUGRBejFON2dqVDlLClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcldpck1NdEhzUmQzQjdTKzB6SHkKdmtBNStyL3hMR3QvNjdWSTFOT0FmbnJEQ2g3RUt5aEJsSlJUVlNleXJwSk5MTXlvTno1SjA1cjZTRVk1T1hTYQo3WFVRenBuOTJwZEVGTzVsZURKakl6UHhPREdJUW1jcVdNbTFpUkZBZkRkRDlrZEJrT3dzL1VjUm5obXdNMlBaCktId3BjUWJxamVrdU14THpSam5zdnN0MDdwMFJCS254eklDNThlTkVyQ2RYL0ViL1g4WEU4UFdhVkx0dTBsNHUKODd1Z01jQWVjb3N4U3VJN1dJbVdqaEdjeXd5SlVZekRvTmRNV29Dd25mMVZhUnJxV2pVb3dJNHlTVVRadndLUApNdUhnckxMU2kzNDBYSlJWajQ2R1QzejJIMytoQStpUXMvOGIzbzVLZ1M1M0NVNGRKQjlBVjRPb2Yzai9zQkdsClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTFJMWZFUlJsUXplcmh5Z3hUV1cKZjJOVnI1ZDR1MlJHZmllMDNmNy9INDAwdHAyajBJcEhJU1dSUnFkZFFiMlVpbS83TTRTYUhHOVd5NWZvdC9UYQpvN1dleWIyTGJGWGI2aFRKd0oxaHRoenRxY1ZlV3hwUlczbEMvU0RDWjRyRm9SQXYveUdsblBLV3UxWXpmTWhGCktGYnplZjU1enFxTHRQeTdXekNtditWUHQrNXVaMTRIZDVGNWVsOXFrZ0FhVGt3NHF4dmdMaHZabGFCMnRWbHQKckdneVVYOHB6bmZLLzEyWERoWTRVSXRQZXduSnp3NFZ5Sld4MGdHT2ZqR0xUU0RVaW9JYktVSmxtUjhicUorRwpuY1RUSEFzZDgvYndyTE9JZStvNG5zaHAxelFDaUYyNWxCV3VFOW1LdlhSYVl6cGszcUU5NDFnbXNRdHFyeTU0ClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGNPbks3QXdnalVXd1lvWEdQZnMKQWVpV2FjR3psVllkaUVsemxrTmJnOC90bW5pQnFIZ3dDZ2UxT2w4d0tCODFkUDhxK1ZiUGtvMFg0dFd2eXBuYQpCYlNnTCtDcS9mWVM1Wndudy9LNGFEU2dOSm1pcWl0UFB5KzNtMk50VEU3V2RYZ00zWCsreVM5MWxmeXgvTEdWCi9NMmFvWk80Qi8zZ2ZKRDhrS3c4ckQvcVpwMytIUUxKWk0zbGJPTldEdFZOUml5V091dTVqckQzaEJPNG1KZXQKTjNtN3Vkd05NTEhVZE1FcTcvNmY0ZFF2R2Q4eU9ORncxanRyL2RkanRjK0VFSHhQUUxOV3hCNU5iNDNrYmxVSwoySElxYUFJaDZzeFZEUXFLb1ZadGNwU25qNmJaRzdYWDY4VEkrdjF2TnNzVC9JZEtSNnNQT3paNmt4dzIySitFCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0V5RFhmZXJFelBXQU9NZG9FdjcKbU41UFFLYUgweHlsRE5CdkNFQTJTOU1EcE5YMmVoNE9rcmViRkZPMEFHQmhGekw4b2M4Q1p3eWhKejZZNFBUKwo4dVZ0VGRjLy8wTXV3M1JvUWs2Tzhub2ZydWJEa0I3Z21RdjhTUjRWMU5ROUpYYVZOSkorTHE1Y0tJOTVhNnI1CmczamRVaWpXWGxPTHJxZ0pqZWwreWdKNGFFa29pTGFIbElzQmVHdXdURStJNlZXSmxGRFM4akpFaGFESFJiOTIKZzM2V3U2NnZRMERoS3BwZnUxNnlVZHpVQW9ZRHBRUmtMdGg0WjhPYWhZb1E0TnVKNDd0SEhsK0cxODdjSWF1MQpBblhCVGlkdkhGTlBUWGJQM1VPQzg1eGRvcEphWDVLTmZIUlNVcWhWUXB0U0NrNmhEOUhEQVhSK0I1c01WN055CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOVkyc1N6UjdvTUE4TzdWQi9lN3gKYUFWSWR4Sm04WHB4L3ZNbUJyN085UjJKcmgyZUtuOEdPWURySE5WNVJvNDZRVitZTEFZTVgvZE5jTHVLY1NjbQpXS0ZDbUZYNGo2c3lib29wLzJUR3lJVTlDVzhGT04vVEJJNlVjN1BrcGtzdFk4ZXdwb2NwdFQ2SnEvQnlmbHJJCmM3WWlJT3VncndBYXpmQ0U4OEE5OTR5SENmTHo4S3lnTzkzSlBYNTNJMlo4S2RVRm42RTFEc3BNWlNnSTVBdjEKbTlTcUJ2bHpMV3BKdmFCcXlwcCttd2JGc1lPYXByYVhLQlhjN2xWQXp0QngyTnNYZEJndFRESUNadUFFRnBmTwo2OUtiM1hKVktDL0xudVFvcSt3UGI1NjZOazU2Y3FXWXZwTmVSaEpvWTJUMlp6QUM0dTk5UXBTYVk2R28rWFVRCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGM2RXBKN29sL3pJQmE1Mkt6OWUKb1pLWDNwd2tLeWI2MnpPUFpTeGFmcVNaMEEzTHJFWEdScmxkeVQ3bm00d2JtMFBoYmRMd3lMbWtZeHBzdCtIUwowWjM0YkVVa0F0TTYySXNlMDFsQnlxejFLRkI1UkUxYkVwNUxhYjZQRmFSSnRycWNQN2JxSjdhV25TZVFBeHNWCnY0VllBL21ZMzZ0QzFCSnFMRW5oN0pmOFNBZVF3VXhPZlRHNDlFQjFyWjhEUHVDU212azQ3YytBR0JzSzRHUDYKWW14WC9RNHF4RmkwalpvL0MvblFaNUpMWkptZWw0U3czSGR2aHBBQVpxSis2MUVQRiszeUh5cDJJd0FicmxTRgpDZjJLcFRFUjVnZUxLdERBRHNhbGhjU0FYUTNoRVZkNVJCK2pFWW9iM0tTdmE1SFg3c3JzOXFEVkZ0NHQxZ2d6CmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjJoVmNGWWRFVllObGhYWTltUDQKQ0VxK2pIUW1KNy9Beml3enNOQ3hBUW5ReDRsK25kS0xzeFJCcE4wZ2srUlRXSHhHTXJmd2ZHRDJRb2pEUGpoTAo4K1h4VjNoNnp1ODl2dURISElXMjFKVms1NFZPQTJMNms1QVIxLzEyOGZmdnlIYUkvcGlvZS9tVDBJbE90aHhECktZTkdhNUp0QmZJcXVmdkdIeTFCRkpET2l6K0ovc25aNVVPc1czaGRQemVlamtsNGtPVkdYK1pTcFlXUDdqcXAKM3p2ZXhua2ZEbXVTM3p4aHc0TDZvWHk4cENSU1czblVHMWhyQko3S1g3UDYzUE5LazlKWXdVVWJmc1ljYnRpMgpTalorR1pXWHowYjNuODRVYkxCUkp2azVpaWpqUGxiSVQ3R1RrZXRkamxZYjhnSDN3WitIQ0NuUG04NEJGMDdGClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3ZETWg5MFBUby90SnhiL1UzZ0YKZEMxYmdDTXdadnBjd0xDVTVyRFMyWmtoeEJWaVVpTVdUbWQ2VXBiYnhnSlBQT0lmeVo4eTB1RjBjVjkwUEREMwpISklrdlFrY2sySzk0am10VXFIckkyaWlkY2g5T05RNi80eDM5R1dOZ2Y5bXo3YXJaYmJaTWFzQjNFS05JU281CjQ5bStJWGhIbVRPMy9pMlluLzVIQzhPUXdrQU5aUWpRQit3eVh2eWViS2pKM1YydEtpeUpoMDNSMzVNeTJaa0wKMVZJa2Y2cnFPN1F3cFk3RlRhU0VrM1FaTnVRWFVUbENGcEF6Y2JrczVETE16bnZaOVVqaStNR2Qvb09ueEJkcAprUWNsVjJzQ1NvTjhRM05PZ1NYNVQ1WmlIODJyd3ZPeEZoVkRDYThncFphWEFoOFJvdFlFbXhrVmNSTlBoS2lRCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXRGelMwR3hRYVBYWHg4THBwS1kKY1B1a082N2xXN1ppeXhySWtNakNzYWFYZlBwUmZlOVpSK1pJVGRwK09sSmJyS01JRFZCZ09hQzh3RzdOaFV4RgoyTUhZQVBOZnlnVmFIeFlXYXZyKzV2czV4STFobHNsMlZQSzRHSzZ5TlhreHFyclY1OFJ6dzZjQ2Rra2I0R28wCk5IT0ppMERzeDZMdjJwWld3STBxc3JyRUNkYzRRZjBFZ2Myd0FtSjBQd1RDSjNSMGx3QVdYZWFCaFdPeThZd3gKRkZheDM1YVRHWEtub3hGbkNML2JZanZxK2pVOGFRaEp1L0ltR0hjNncydzRrbzlVUW5zV2M5b09jSjhjWVNLYgppUm0yemxjekZyMHM0MmVIUExVMkp0cGttNUN3eFgzejNUc29tNjVCOWJ4S1FWWVVYNHNzZ2JXZDZMWlRLKzNxCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVl4QUVkakRhZjNpTWwya0NlQ3UKYVVuZ09DR1l3a1VMU3Q1dStHSE5nZ3NVOVdnc1VsQkpzRkN0WXBzWTRWc1lyMGdybzE3bmJobkR2Znl3MW5ibgpuUExaNERYd2VudEZscVJkaVR5aWwwMW14dHVRV0NmdXJGS0x0WCswdkdORzg2L1JERDRRNUJrRkYxajF0dEc0ClBWODkwdjhDSEphMDVpSU51L3Q5UlhDSlhmd2MxQ1lHMTFtbTl0WHdKQXVuMGwwemY5amI5OU5zbWlyZUpLbjcKbE5xZVR0WmtmeDNjS0QzN0liOFQzdFVhYWs5cXFTdWJJYnU3OXRQSVphUFg0Q296UTQvaUZOMkN4eUZ3RmRxTQpXZlNnNTlER0FuRURNUFVXcEl6bndDMldRVFBGUUZDM1NlUkZQcmF4WGJ4a0pVQnBTOVZxY0JqN01OdzNueXVBCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckgzeG1LR2ZlN3JacTFWaTAvK2QKUWtiVjRBeS9KRnlYRnVXcGo0OERSV0pPVUl3Yzd5clBOck9RVGN1WGxHS1hTbmNZL3E4ZGk1MHNzRnJkOW5DNQpkYU1paThZcUNqNTN6ME91dUFaNlZEM3lrcGFqRHRneGlJRmRvVTRyWm5hS0YzSS9NOVB4RGJTZFpFb3RIVWZ2CjFrcms1eWtheTZyT2FhQ21iSEtpVi9nN0IrMkhidzR5S2F3ajBQMXBDaUx6K0NLTEd4K1QxVmR4VUR0SE1BMVEKLzkrMk5Ba2NBTysyM1lqdVJQTXBVcGFsdi96K3c1SnBOVThLdkhBT2lXWXZwVlBrOXhndGlFamFrSGtQWXJOVApnTEphMk5CS0RXeEsrdk16dGhPM0szMnY3V2l4aHgvbVo1cWpRbXIzNjdZWVpTUmZNRmJ6V1JoZTU3bE1zelJJClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkpQZ2NGeWYvanhkZ2JrZTEvS2gKTjNFNlpUUVlnaWY0TUNSVzg4cGNGendEeHBES2c1M3Q1Q0F4N24zTEhvL2dzakNXYXhVNGp0c204L0EvbjFGWQorY0drMC8vc01CVytoVlRMNkZnQU9yS0VBZW9tMkFWb0VySXBKbTVmM1NFSE5GcWtxUDNNd1Y5cG9BZ3llTml4Ci9PL09KQk9VNThDeENXU2UyYzhvRmxIelBPMkZaVG5YNC9VWEt6bmJ0MmJpMWZwZmVEOHFtZ2kxcng3UFA2Qy8KT2NYSkdCc0Uvdm5SZDNCYm9GNjJRclBnOHVqeVJSeDB4ZFdBVHJFUXQ1UHN6WUd5RkpXVUNtdmNkRnpaZjVHZQpMTkpJMWN0VzNRMnNtMUhsVTVlalRXVVllQkU4NVhlemc3ajhtd01RanhuUUJ1eHpGdFVQbDlMcnpDNlNjWnNpCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd08zVGsyODlPekk2MXlwdEhEVFIKSXVFTnFsN0xlVm9HbG5LYlJCMFlNMlJZNHNMQWwzdmgyYkViRG1sRmlScXhYSjFPbUVaVUx1Z1lxZVRTQTlpbwp0bkxFWkx5cmJtQnAwS05DZy9GRGhwUG9ydFdueFpVaXd6WE8vd0lsNVhjcG5mTVRwa09ubGlqcVRWS01lMXhvCjVURFNTL2hTekozVFJWcFlwUE5Vd1NYZStBT2tFcGo0Sm1Dbk1NbXptWmMveU14V3VlKzRvc2w1amNnNGVzUWkKS2NZZDlrUkRnZGczalhkVkZwZHR0TlJJcWR5ald2RmpTUEYrVnNFUVZlcG1nZ29la1NPV3ZQVTlIRUtJSzZBYQpRajlpTW5oTEc3RFo1dVFJOGtFOUZUa2NEc0Rqb2w0aDNZZmdzZ2hqdmVPaCsxZHVEVDRWV3hLL3dnV3BFcFRGCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNERpK0VORDF5dFNaSVZIQ2ovNHEKaWlRUnJ2UlhHZUNBT1hJYUtWVWRYZG5aYWYrMnRQUXdFaFVsOENRUkpFQVdTTXNHVG1ROHdFeGhHdmJ5cTBVYwphblRsTlFSaWszUEs5VWNjdm0rQm5yeFhEWjA5d3k4MnFJOUdDWE90ZDNvbXhxREhZc3Vza2JQUTBjcVpqMjlUCkdIUTk0QWIzdUhKYnZ6RVh1NWhQQWNGS1BHa0pxOFFmMVZLdUpWYUhOd3d5bzlFZjhVbGFSMlQzUGxsZ0VoaDYKdTZOejljSENuWUV0Q2pxWjJxbUtUZEUwZTdqT3NReU5hOWdGU3dTSjgrNmVBTmYzSkk2L2dxT1dpTkNqb2FjUApJL3NxRFJPMnlGd2VkQzFTdUxwazNhTHFzRS92ajRLdVFBOVI0RkN5Wjk4S2JjQjZGRHBDbVFNV2l2T1p6MVBsClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzVDUXJFb2JlcXhMYmVQWlpuMS8KUnQ0THIrbWM4d2VEdVBhcVZYT3JVdFYvUGs2eTdzUmFiSUw0dzlQN2p0clZ3bkhFQ000M0Fxb3V3MDF6bTFuYQpwbmNYcG1CdVpUdkc5aHNvVURMT29ZdjRzNXo2STdWdVZGZ3RsbVBnYWM4TU80cXN1YkN1N09pb1l2WUtWVVlsClljRXpDK0dKQkFLa0w0VHhHeUx5MDRrMTVvN3dpaGU5RjQvSDVZVGl3ODdyUnhEMVkrUUJ6RHNGL3hRY1NnWloKVWpMbDNycjBiV2g2bnQ2R2lsTDlPYTQyWThCTjVYZmJYTXdJTWFLczRMRlZmSEtKdzBwZ2JKeUFRQ2tvaTNSRAp4dlYwVzdyT2o3WVhOdHNqb1NYOTlBVDNrdXFwcThrUmM5Rm14OFhMYktwOEhFMUVVQjE0VmREVVcxWS9Bck80CmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3BuQkt2a1Rsb3FZT1ZIdUx6emMKTWJOQlQrenNWdnowK09rdWZMejd2ZWwrMnk1KzhBYWZoQS83L1R2N3ludHg1TTN1bkJEVC9LQlIvbkNYTFRMbQpPSlZ5R2ttalFCOFNzb0VCeUZJdU1zWEduKzBnc2pkMzUyQzQ1UkhLYTZaaXVJV3NieVIrT3cwWHpseXVlK1JvCjRLcVVieXZsRUJWNHV2NVQ0WmR5dTIvMTFpUDYrZVdRK0N0cTBzR0lXU2FMK3NTNVZRaVdZQ2FzMnhsYlgxWDgKZE5nY1dONVU4ZmdUU2NkbVNKcjA2SzkweExWYWx1MGt2MGFIWUtZbnJTdnQzRjVaa3pyOUJTQ1F3c1ZJRW4wOQp4SDZrRnRWM1MzUERxN2dhYlEyMnhYTmJRY3U0V3VyRTZIOWdzem5QT0pvemsvYm9wTWNOR3YvdTZWQlJhUCtMCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2FibVRjR0MrNGtqREk2eGJva1EKV1huZndraGErc2xUM2pIVU5QREluYnR4V1FJNWMrNkU0SFpPeE5ETWd4N01McTM1ZGpWRHdZNGF4OVFWb1pVdwp2MlhFU3d6T083dUVXSS9PcnBqVndxYmxuMk1uWVA5WUMvaU83S2QwVUpZT1pWUi96UTMvZXNRZVNWamowQWZuCmF6Qm16UWIxSlQ3K2hiWXRCOURYdURNT0d3RmNZMGRvOUNPaitJTnRCWnhCOC9qK2JPLzU5dkpocllLSnRHUWcKSnhOcFpISnF3RkF5b2t1UXZEdzhkYXdUaUFYemtzc09lUkVtREF0bG5DYW85cmtQWU92QXozd3lrME5mMmc2VwpBQjQ5eElQeGRIRGV5M2J0eTNjVjVPdVVZZnphbVMxK3ByVWdrNmVNWm1UcjdEeUwwQ0RUUHVSOUZjVGYrZ2pICnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUp4QUloTDNHZXhOb1N6RnVGeGsKVkFMaWVDUi92bGtZU2U4NW13S2Q2RE9TVEVoK0RoSE1aNHdpK091RzhyOTBDUjJab2dTcHRaWGtKZlFub1dXLwplQ0pYaTJ0WnZwMUI1V2trNXhRR1BiZVF0anNqZGN2WlZ1ZjVsNVRSVkI5V2N2NUkxekVsWmN1eEMyWWZKLzI1ClVtZ2RubFF6RUN3VXlPYUdMNzk4VzlicGpqLzNMUTlTVkwzMjd6WlZVMjhBSGkwN3lSblQ4bzFhRWxyZWNYQ04KN3NiY1M3RVoweUF1STgwQjBTVDFqbVBQdW5ZU3ZsY2hPUGNwalk2NUJBMktyS3p3WlRRR1BYUnk1SjhNV0tBcwo5bFRZUE9TdCtreWdtNE9Ta0FBU1liOHF3MlA1aFVsTEhjYzUxUCtLZTczNGcrSjArcE9Na0pKTHdGMXc4bHZtCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0hNcVJuNDJNa0ozRHd4K1lsS3AKSHk0TjRPUVBFVG9pSC9ZejdTODQxcnpuS3B1MytTR2RSMWpqbUZGWktVVmt1cjN5SGNvMnRzcU5zQXRlZmN1bgphc20xTFh4S0kyelk1bnRaUVQ4UVFTQThjT0VSL0VKVkkyTHpHdkhpZVpXUmIzKzNPQ05uMmxJeTdKS3hFVm9RCnl6U05tZjRpRlRxNGkyUHFKUmR1SFVsN05iQkpScTdaMlRMRzlUeWgxMkZQS2lIa2xMYTY3U3RRb0Zld3Vra3oKajA5RVhSSEMrb2o5TnhQYjJHQWg5T0lSaWdaTUFkNXJab1BwYlE4dDUxdkd6cThpMVYvUnNqblFyZy9QTjhDZwpRZmlQT2dKZDB5UTc1TWtkWUlpby9PZzFHVDR0c3dWVGs2dktmVHBoTWpXMUYxZ2Qxa0N6bzFRNjhyVHFWVW9PClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUJVZEVLQWx2YTNZczlQSGFPMTUKZmgwQjVieWFjZTNocks0VGZoMTE5a0NDRlBZd0NjZXB5b1c0QkJmWGp0WDhBdGQwNWovUUpLNldKbUpkWGpDcgpwcGVKZFAxMTJYdDZZMFRuaURuT3pvUTRWNnRrNjhEQThZeCs0ZHladjJPTERMRENrdjdGMjN6bVhTSXBFYkVrCkd5VEt3RHZkeTg1VWxkZHQrWlNOMkVkbEJRRGw1M29zYVBMMkt2TWtETlYyNDVzZGFHSEdwOHBneGVQNFEzUVgKdmVtaWVnREJyY2VDTExCekRXeVFzbUQwck5xQ25mRm9ScC92RXA2WDdXTXd5VTlMNFp3eUdZUkU5eGJDMm0wZgpMWEtsV0NqQzVHb1ZiTldqNWplQXJMeXhXWnFnZUl2MmllbDBhOEd5all0bElpYU4vOVl4dGRNbUpDUUdQTDNPClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTQ1Y1YyekhyRndjTkZPOW5OTm0KcFVPTC9qRS8rMEpRRERwTDVhK2RFdTZESkN1L1JqRmIrVFRIaWloZG9DQWc1b2oyNFFtUGJDZks4WlRMWHhQeQpzb0FrNFVWRTViYXVrd0Jnc2hqZHM0V1BuL1V3WG5wbERteUJ3RmZMN2FMQm9SLy9TUGtudUI5emw2dVRZNWU5Cld2dThsRzVzQ3dBSHF6SXJkQ1B1Z0lzeXllcW0wWVIzelA4SWIwUnBRMGJ1MUFSeTVreWw4d0lsSkxwb2dSTVgKNUV6SXJUcHNmT2R6YVQzb3J0eUdPc3hQT1JJNnZxRUhEZm45NjlmaDFYNGppZWpxaWd5YmkyNm42ZzVUeDVOOQpmcTE4TnhGS0h3ZGFRWVNGZ3RRMHkrZWlUVUNxTVVEYldnbVNqVkY4dUJYMUYxQWsvWVhKRzgrYnpQYjY4NDZtCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1VQNHJ2RjVXNElxdDRhNXN2WGEKdFl1RmducDFNLzI5WDZRbm5WdzFPaGhabWoybkxZMGNPeTFaQWZqUkQ4TUl5M0ZrR3JUQi9VM0grb1dhY21pVwpjaEhRSjUwL2ZTaCtzeVQwTnlVV3JVZ2crcTBscyswdmk2bGtsZEFsaHpyYkJrTUFaODVtQjQ5UU1nYmlIdXp2CjFCRkxHempkdnJ0Y2RJaTRPRm9qZjRuNnp5S0ZQczhpdFIvUCtISTdFSTRyM3N5eUdZZzlGc3ZpdTUraVZFTDAKT2lveGd1MUJ3Ukl3UVY0WmF5ZTk2T3V2RXEvL3pWK05IWmJFVHN5Y1FFZGxBd2s0eGlKQVJyK2N6eUtUUmZwSApTcXNuQldxRjgzQjVBSElaY3AxeTdTaEJGZmJaYU1xUDBDNEYya2h6MmNoL0FaYWRmWHcvYVpodzFpdUdzMldECmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlk5VDdPQ0VBZVg1QUJneEt2d04KTjV6UDYvaU9kWng3L1Y0UFh6cGVrV3VnRXUwZHJDbmRuYXJjYWt5Z3BseDhpOEpUUWhaTGNKTWxNdTNlbXZIaQo1VzcvVnZUY1FpT2oySUs1TU5BSXFtQVJqZWlDSXlIRVdtWUpnb1JFY0RXdHRIUGlWbGRXUlhpZ3M4RVY0QUVwCkRSZE9kMmdyWTJiTVUvZzJIODFJODVaTVBkdzBZanJsdnA0WUVKL0hmaE9NK0dFZ1d6U0JXeTFKWjhGcDVESnIKWDNBL01CNlJMVnZVN1RkdUVOc2ZsbWYrbUVYTUpvc3A3TTR6V3lSL0dsckViNS9PV3RyU1dqSFlkeEgzeFZ2MQo4NGcyREtuczBGOTZucDBsYXZicFBjQ3BYTzZtQ1Vnd2ZVRFBvbWc4YUNsNk1lSWhxQUdyTnZoaWg3UjhxeDBvCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHBUckl2Ulo3eFVyT2RTazViaTIKS1BwcVhlZnZaT3Y0NCtwNGp2R1RKRG9DbDBoVnFLZGhBOXN2TXBBVlpwT1EwMHdXR1VLOEJBc21MQ1NUNnVDNApaNHQ5NC9UVWxMcEdHNVdzWWRBd3hnWGhRVGRLRmp1ekNXT0lpRDRJelRsWmwyb3JTUHV0Y25LWEI1SENnbmJnCjN0WG50OTIxL2dXMGgrN2lHb0tyenVFdU8xSUZPc2IrdUtIbHdQSUZ5S1N5NCtXeEEwRktablFLQ2VrV3dJUHUKdURkb3JVMkZMVDZ0THZvTFpWK0ljZmZqU0dJZVBLUnd6ZXJOMGQwUVZJc1ZCVlEwUzZ0MDl5NGgweGRuUVNjUwpjQ2pNd2hFbUlUellNSVl4QVIwRnJrQm1aOUZOZDJuTEVJNGhwOUxabXQzTFdkZkI2cFBDUlBNNDJlbkMxeC85Cnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3FPQ2hDcm8rRmNSbmxaRThKQWsKQzN4TUF3aGszaVFtYU1vbXE0KzNDVklzRmg2VFNkdWlKdGk4bGJ0REgxSGdpTEl2anJhZjhQV2dueENXTER2cAppVnp4OHhXdzJhOFpKMjNiN1hQTGJsaXFaK05QWjQrUkpndzVtOUZ1QUJxeTZ4c3B5eXBVaTlwMCtpeURwQU1JCkFNRDJwWUNXamZJdDZyTmxVLzdBb2NTVHNFR3RibFY4dmltTDEyenAyQkJXeWRXcE5wMTNVS3l1NlBuSDZiK00KWW55UTNDdStWWU9yTWg4bTZtaG9RdXNFMUNsR2FBVUdyanFMeHBsSjJoTEhVN1JreGt2KzRZVTR4b1dQcnBzZgphaUZmTHNxVG40Q2Q0eDZkRVFqL09XOVBGck5KVjZxNWM0by83Z1RFekxmM3VzeVVBQkp5SEhOUklxZTNSMC90CjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXVXUjNUVy9FbVozNStZcGlEYWgKT20vV0lvbjEzelBxVi9JbzVRZWdzL2RIdVB2S1hxTm9zcVV1d3VMc0tiTDc2VGRhK1RtVXlicGkreU84RWZUOQprcFdZcGJXK1JpN2E4Q3BHUFZnbzF0cHluWmVaVmlCbFFaLzh4RHVQekxEWVVQditlKy9BVFFBUkhxUzh3RHhrCkRQNE5Rbmdpc0JaNUVQTFNRNE9mV3BvMm5EMDVPYnhXdUszZlAxbkpaZnJ6U3BLS2JuOStFV2JsTXdTWmRNMnIKZXFxWjVsenBrRW04V25MN09Vbnp1K2dNSkc0RUJjVEZ5eFZtY1JMSU9TYlZNcHl4Sm1SdFF4bEpUS0lwVWdBcgpFUkRObHhjNWYzTktRQ2xkS1BhWWJvS1JueEcxQWt3bzd0U0Z4ckpEV3N0WitySlRjbzhERnNvaENwd1ludDJXCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdytxM0U1eHNkbkI5L2NGN3hWcHQKeXkwc0xPcC9PZDhHZWRhdGVCd3A3UzRsQ1FlY0RsUEtrRFV3bjBlcUxyY0s1TUlmdFFqZDFPdmdlbllrVVh0dQorZG1uaHdab2p5dW5jYkxteXFvcjM3RkxUSHN1OWNYTjFTVm5vUHZRRVhKc1N0WFF5WGgvZW5QV1luN05HK3o3CmRtNDdLbjNrUXpoVVZETWRwQ2pKMnZtcTUrckhTZkhWVEdXY0daaCtqY09sLy8yL2lsWW5nZkpZUEYwMi9nQVIKUzNzY2NMRmxyWjBFR3BnMTJTRnlsL29INGQxY3c4SlB0OTFhZkFyN21DUmNOdExHL1h3anZEMERHSTVwdnhhTApWdGlGVk5xVU9jeTU3dWFSWktiMnhkaHlmWUszZGV5SXBZOU5zdGhLc2RsTnB5RlZzdTlSTnhVOUgwc0RRTWRpCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDBEeFYvVUpIRk9PM2J5OFppRksKUU85WnJkbVZPU3B6S254M1liaHZDZXlrOStoMDk5K0ZsdmhMYWdDbWYxWmhYOXRnZGNxY3VtcWtVWm9XQlVYMgp6bjlVSE96cVJ5SzRCdmNKSDFobTRtckJzWlg0ZTZsN0hkM1N2UHFVWDdHL0crRDJZM3RtOUt4aml5cVRpcDRVCmFwMk8xeUtqbkNjbUYrY0tCZ2oza09UVDVKalUzb01Yd2lVcXhDcjd2ME11RmtiRGwzL2lJODlVbUlFSFRRaUMKbVZMQ2FsSE0yMGxSYXRtTnV6YUprRlFidjBZaVlNdEJTVkVYOWRHTnMzd051NGhObk5DWkdTbGhhQUxrNkZ6QwpBQWxWRDBnbTA5UXVMSEV0ZzhhMTkzU3NWQ3VSdUJBc2orTDJEQUpjUHFjeHB6SjM2VjJ0NEZnallpR1ZWRTJOCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEpKUFF3bzBtMFlPZlpxT1Vhd1QKc0JlemdnTFNkMzU1b3g5aEZVMkwyZkVMRFNrTVAvVzdxWk9MdER0dzBueHRtVXJYYUMwNFpTeDZUQ21PZ0t6WgppUkVVNXI3djJkdXRFWFVPbG5XZWoxZm96dHpSNlpTUlYvNnpIRDBpemltRWI1RGplSVJzaDVqOG9TN2ljQTF5CmJpMU9yRk5DV01rZlAzYVg3V2ZrQ1FFWHd5VUVKZ3NYUitmVURpaUkxR3hFYW9JanoyemdMbTdwUGhkY2IrbnEKVjlFVEtqclVvSGpMdzZ5VmdXRDFiVUZORUtoTVV0QU9JNHhlNlI4cDVFcWNSSmc5d2NvbkdUYnVwT0FxQVMwRAp5KzN6NGYyV25ZY3E3S205SlJjRmZ4VjYrbWdpYzkvemo1OU5HK2IvTWNaaTNuTzhMVnRnSWRTdE9CcDQyMStaCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUJRdG9BY1RHT2twcm1lempPUGgKUE8vb0pDaXc4WkdhTHhWNHhCcTB4M1Nocm1abTI3cEtqK0xwWVdJMkFvOEZhQ3Bha2ppd0orSDdnNTRVQm1oZwpnUVFtY3lPZjY4ZDBDenFSL01nRnpKVlV1c1pQM1FDY291bjhFT1BYTlVacGRzR1NTODY0NnR1eXczVVNyOWp2CjBHaHhHR3UybCtWVFFoT0s1d2JRQXhhZWwrY2lGZUpkTzVoa1FnY01BamlpSmlIU203ZkQ0a3ZvM3dPY29sdFMKT3hnMDJQK0RkbkI0M1FRYnFkWktrWWJVdUJVQ0VwOVNFYUhpMUtTbjE0ZUYyS2IzNW16bm11RmtEclptTkRHOApramRxbTBySzZhblh2NnZYVDNOM1I1WjRSR2pFRDJ2UjVBQkc3bDJoUy8rUExCMGM2Y3M0Vlk5NUV3b0tuemVICkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFQwNFU0WXgwWXVBRyszTzdXUncKRldjc1krNWJMVVU1ZVd6RXRoaVVyZEJ5cVF2SkxLcFFXbnFUWW0xbDZVRUg1aENzTTUvT0dhR2F6eFVvMGo4bgp2VVp0TS8rNnFwbDhMNkpIakE0ZnVLenFkeWJBQU5XVE1FTkJGaG92c0IxU2NkR0dmMk1ZajBEUzdpUVBSSUJSCm9YSjRMRnRpeWpoQTRlUUlKU2ZZQzRCVW5hUmZaMVBmRmJLSjNkYXpWMzN4em8vMklyaC9oZmhaRzVkbE9vYk4KL1pyNkV1UjZVb0FsZG5USUZ1S0RaeGdNdDJzSW1lTmJyNTIxNk9OdllxTlNlVndBWjF0YW9UYjlsa0h4bVNNUwp0QkRjU05GK2wvS2RqdlI0SGphbUltWkE3NGI4SkxOZlUzUmkxYVR4WFZwSThlUlY2ZDhIVDc1THVpRUNFcUtMClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmEzL0N6aEg3TUJLRCtDeGIwWVIKN3JnSnNEQWo3bnFzWUpGRGo1SXViWXJjNEhDVUpwbGxQSUs2VW9COWkvSTlzOWpKYkIyMGJKVWVYc1FwZG1CbQpSWTVkT3Y5MEI2VDVheWQvZlRhcWpPbWtpK1UzR2RxZVFwcnJyVTh6aUkvUmd5Vi85dlArS0hBb0w1ZnFORG03Ck5GRTVFY1dYajM2bE9MN2hWVlVkQzcyVS9MRXpIK0RWeFJGRjlPNm1YbnNhMHdndi9ldHdEMVIydFRwcnZkbHQKR0gyZDY0NkNJMW51dlFpS1QzODVnZEJlcTBZMGtZL0hFVEFoWU9pLzN3RS84aVRSSUJISjZYUUdsSU5qVGxGRAoydjFtTzNXRnc4MjQwS1VEUmFZOXQ5Z20wNGVYTFdIeFFOanZESjNqTlJMMnRzN0txS2puRGZOanpUOW9Cb2s0ClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDJpN2k5SThxdzNPb0QvRTNYU1MKVkUra2t6V2JycGY2ZEFDRVZvM015OGxnZytjWkFpdy9SR2laMDc2dTArU1JQR0NyeVlnVzkxaE43YTNOZkdOZgpGeE1DZndSWURsY1JQM1hIOXhKbU51Z0Z5YW54Q2ozNFUrUGRBd2RZeVg2T3lTa25TdXl4WFc4NFUzb3g3N2lVCmF1aHNHNTZ5WnNEZitjNC93TnNMN1JEZSs5NXNJQXhlb2lCejRCVFA2R3doY0RjdTVyUThaVUhxdExoVElydkEKREJCb3BzRy9UOWN1K0RYZ2NMbk5VOEpxY0JvYnd5VTN4Y2hSQnBzd3JjdUNza3Mrc092K0xmQnpYQnFNNjVVcAoxTW9zb0M1Z3RMYjFqK0xNOTY2eVlhOHdkWmVqS3l6aFYzcmVubVJvS1RRWk1NOGx5T05YUlRqWEF4SnI5S2p5ClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXdXVUZqUFBkd01HQVJJbUw0VlcKaW9hNFpSNGp5TEc1SXpFazNtMjh3VjI1ZjFKWFdoUHFPNkxMSmdrVlpvSG54Nkk1VHZzWHEvQTZEaDNaWUVFYQpxWHN0OVI3RlVBU3l3MTJvN0FjUGViQ0EzeG5pREU3QU15TFNtRVJRZ1hhQmVJK0dTWUhpeE53RHBnVHlpdFE5ClV4bFIrVUdjVFhaSE9QWDk0bDVmN0tVWkdqekdDbGo4YTBKRUxlTlgvb2VXWit1eCtHWWRPeFBKd2d5V0pyZi8KcWxlak56MjRtOUVKY2N4c09ZNG9vaFdxNXkyVTZTTjNMbEl5aXR6OTNyUUJCa0VqenJWY3VrSzNmVUdiWDJ3aQpVOW1iRlpja2dzMWdrc3BlUlNnVERIZjZySml4cVdmdy8zYXpjVFZEa3I1UzJ0MGNnbnNwQjdXSmt5WWNxSXFOCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTZURk83MW02dXR3bUtGM1NHazUKa2V5Sld2NmlzdzgyWTRkcjE2a1FHdVRXUVpqYnZ4RE1nZjhQclRmRGh1d2tLV05NOHdhSUJXS1FzNlI1K0Q2dAprM0pHUXhMRmJTcE9ZZTFQZ3ZoR0tLMHc2dHlZK0NqMVJsVHlFYkxTMUgxMkFQWXM1aHp5cUdvbjcvLzh1R3JLCnVJZlEzZ2gzRTBwNm1KcytYUU1udExIWjBreVJSOGEwSTJJSVRZV3V5UEtvRm5WN21wVExYK3ZETExaY0FDNG8KdlMzc3VHUEk2bFlBM3dIK2pHdXBJdFNlM3pDNXVCdGtPNkVLbitwTWk4UE5qQ0JvQ0hTVWdrUkp0Q3doT2xXTQplK2Q2UTE0TTJFYk95L3djV25iby9ENDZxOVVnNVBCb2FORzZXcS9BNHhzb0JEMHRYU2xsNzdTbHpheklPaEd1Cm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0VLNDVDclVWS0tJc3FDbmsrM2wKZS96c2d6ckY4QmM4aGRVL3QzbmxVL3lxT1Y0VWhLTGhGSURWd1FXaUd5NitnWTkzejRhcXpTTjFwM2cvSTNGTApBbGlyRmF1MURUVEVjWFNhc3FiaDVmcTRqbzNFQ0tWL1BEWXNjMSs0dUJ3QTJXMEdjeFl2Vi81Q1JvY2R5OWhJCkJnSGtHKzhJTkRHYVVZaWhjeTRTSW5BT1l1b2xEcTRCZzZXQWRWNWhkUnN3ME9pVE4vVWIrSDlEZlZFSGFCVVgKdlhLMnV4dkJOVWRmay9wbE1UQjJtTVoydERyVE5teHJaOWZJcUQxaVVFQWtDNDlESG50emJXQWtLQ2dsNEdTMApmaHBOQU9NYTRXR09Ka2ZRMVRTaDgwZ3dpTmtHYU1FT0pRWEdlNFh2ZCt1VFZ4eGZ3Qk1qQjg3N3B0eXprRWppCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGhFSkdVWUM1QTdTQ0hhRjR2TDkKS2pScE83Y0taMjhWT3FNZFozUGxYM1UyNVhMdWxMMm81Nnp5eDBCd1N4N2J6eS9IOTZNZ2NRb1ZSWFhWZWVjQQpSUGlmTGhNcUtla1FnOXVBOHdQWHdhdzVtOWtJT2h0SFhKazBrbUhxNFJLUTlDTnphSWxZTUFKTEw4TTlIa2pMCjRyVzg5RVNtdzBHQk8raFNEeDNraHBqZU5mSEt4V0lFSCtBVGI5dkJGbmYwWjdra2tWQjl4U3FueXJqb0JJV3cKSGNJVlRkRlhJYlJ4dzlxV1NtekxwdFVFT0gvamlDV3VXNXdWWkJHZytTRDg0TjVseldYZGdXaEtRbW5FT0NGMQowN040Mjk4akpnNUhGbkVINythVTZGRmJwdVV0ZjN4UGI2R0NYbGdnTko5OHk0Yk9MdExVdHVoYjVJTnh4RC8vCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdi9nZFJ5ZmNCRGxkMUljbGcxOTAKemVzVS9tVUZuZ3JqaW56alRRNXhPSU5ackhaTVVNU3hrT1JENDNHK2N4OUJHR01QWkdYanN2dU9sMjVJTkpuawo5V2h1clNzbFI4SFM4emg0bHBRazRIcFBxV2cyVXdyK3I2N1hOK29wU1JSZkNjR2hOYzNSZC9rK3YyZmhLUy9NCklpdjMxN1NVeVo0OWJDYlRHYXgwMkJZR3czd1NBNmo0ZmpoOU13MjhPa3dGMGJqRnRRMngzNXE2UjB0QzNTVncKZEt3a2xjVTVZQzZrTzMrQWJIbVNhZjZEVURXUDhxNXYzaHl2SndIZzd1WXVyTW9udWNITHRwSlNKV3l5aXgyTAp1ZFZhRnp0MXY0OUYwSGlvMTg2NGhpOW5mK3l4aktWMDg5emt3UWpBaENYSFRYVG4yVWhqaGVCTTN5VmRLb1dnCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU9jZ3UyazVkY0tZWXVoTjhiUWgKY0RiSW4xbHNRQTgyQjAxQVhsZml6MEYxUjZ6VHN4cWU3V3M1b05BY1p3RVAraEhxMGYxVEEyZG1BcUtLOVVjRApuMWcxT3BXaGFSdHRWamx1MGtrakEzRHhUSW1yWmJmd29RYUVQM3FkREZzVkRuQm50alAwZGZuWm0rd1Z2UnhwCndJcjJPbzFPUTI1SEFNMkdCNTFvOHNLNC9CMlpMMEUwV1JuRXd4SXArZCtSbytHdjRiWU84cDBxaGVsM0pVSGEKeHVtUGo2UTNSdmVnMTFWVmNvaTdaeG56UFY0dzNlQnh1UUg2aU1EdkJOMXE4R0h0dHlrQWg3YjFMdE96N3F2bgo0YTZzd1pCOGkvUGlSMDErSlRCVFNrV3hZa2RuamF2RUExRVUzaGlmZ0VneE1IUVpjWk9rNjZ1eG5rdVRGMExqClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdm5aTWZCWDZrelhnamVVeTBvUEsKejV5ZFg0dDhScWZXQ1l6N1dFcm16VFd2SnJTblg2ZFAvNHFWUHREZ1Y2c3lvcHNIa29WSjA5aFl5dXhkWEhXNwowZ1BQTzFzVWF5aXF2cy9EMkFGWFVReUMxZzFnSENYUlRLY3BiekczRkQ5QWNWTlMzSENMRDJFVytVNTZKTUVUClV5djZrbG5nZGN5ems2eVJJVHJNdSt4UEppQ0I4dnhoSm1sRVp6dE5YbUNLNkZaalF0NzBydTNFY1BpZG8xUlYKQjVmZFg2S0ZFeFVXL2lGeGhweGdSalA0dVA0bHBhRU1DZzRIMjdzcUhiaUdRWWN0NVBMQ2tLclZzWjFFSEc5bwp2ZWs0MllzNFhIb01OVC9MTm5mZmdWUW9oNk9HaGJ6UTJHUzZNTUNBK0Q0V0JDd0g1cHJGZ3YzT0RNNVY5NEZ6CjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0RuaVBwV0w5UDU1aVhtM0lCV24KcGREVFdIU2xiQjhtZHVwT2tRVmhocFBUTjVhV05UVXBoTytMWEJGK0RsU21XTFdWdWpxK0YwU3c3aWJlcnlIWApzYVZ6bWh0R0RxaTl1cjBhS0ZtZjZWSEpRRUxYaE1sckFBakdTQlpqMEtncjBJdzFwTk9TUTJYc1FBQWdoWmg4CjZLY1JJWjVFbndXWmxONFFPS3RXcDJMUGZrc3VpRXNBL2FpNGdrNStJa2JjQ1Jyck41YnJha2lvV0NwRmhKTW0KZUgyNDI5b3lLVkd4WWd4TzJuMHNSeFZEYkc3WTFkSmdXU0UxVEtKNUVDQ081V25uQ29EKy96TDBUdjRicHNGaApRT3dGYyt3eVYvZU0xb09vZkRvR2hyOURHSVhFZHU1QVJGbDZxRHZ4TnlubnRlK05GNEt3R0tSdlVZRHFVTEhrCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBektVRE5XS0ZIdEhDRnVBSytSRE8KTTBPb0pOdkwxanZsdGlTRjVEejFBTU9nQ2Vkc045cExna1V2eFE0UVhtYmxqUlhxVkZSOXk2UkovNkhDdndiMApzdnFjZkdpTlR4YVZyR0hBaTB3T05UQjVxUFJ5Q2tqUlFTY2FwVTgwNTV2REFKWmZ6a1hRb2h4NGdqdW5nUWFxCmNSL3lYOFc2ZThKQ1MwbW1iekZDcjNaOUVBcDRqbVRpbyttQVlvYU5CdHZlTERLeW9laTVvUjJxY09GWEJPWXIKSlNTRVJnK3gvZmt0Y3A5V25rMTZ1eFJTVHYwWXRmVXpEUFk1ZUFsSmRoQzQzMzVGTFJVU1pyK2k5UHZlUWk5eQpVMVY5dDZrRUhMWnVCVWJ4OWFWMG9XMllpMWJ0YTFDSmczcnZtYzZvbk9TMzlSNlI2WE5ZU3p0OENzbUJVOHErCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcG5MMlVBTFpmSWpESC9ockhqSEcKR0Z1ejVEaTZGbGZ0bHpoZWxkTjIyc1FIdS9pNGs3MlpBdk9TK2NSbFBNRmx5SkpRK0VpNlVzT2g2OU9UVjNFMApzMmlkWmROa1pUWFpLT1NUelpRN3VhYnF0WnBzQXpsSlFMcTFhK3d0ZXpSc25HUTRmYnVmTEczR3NrZnpRd24yCmFtdklOc3JPMVFaSFBJK2JrRHJUM0prNnlrRlZWVTEvUW5SSjBRK0xhaDFIcjBDaFE5WERWWnpvMUhpMFJjRTIKQ3ljVXYyOGs5eUxxSFNtVUwzWXBiSEVSSmk5R2k0RlFCVU1MR3BJL05jZGhWT2YydnVCZGZoRFNXY2hpOXN4bApQdXQxSURDRnBFTXBnVXpwdndUQWZqMjNMUU5XeDNXUFdvSllxRXRwa2hjVTJEOGN6dTFkRTZxMTFhc1Y4eXZLClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczFxZThEM1NoUWpKM2FPS2dIN0YKYnZiZmhwUGt0cTFsNnFEeU5ydXROWmJQZWk5TnNoTVBTbFZSVW9oQ0pxbUVRSStEWHF5dWtHTFhxL1BRelVFdApqaEI2S1FWU2JLOVY5d21GeGVnYTkydm1nZUVDc0ZLdlNNRDFFdTYvNXduTTJDWVQxbUpaSkdLd3kzcldKYmdXCjRWckRoQk5YSkdyZ0lISG9LdzVXaWZxZytHT1V6eHZrUVVYdjQ3dmtjVm9vbFk4Y1djMDllMy9vdWJYR2h3SjMKZytjb21WUnhnVVdNT0RoNUNHKzJtMVhjVGhiSmNReTNnMkF1aVJNeGJZSFJLL0Y4azFmY0RweEpZNGxGLzd2aApRcCtqSktpYVR2bDYyOUt2MVJFL0tDQkVyWkVKMnYvYm9DYWNERmp0VHQzN21nYXlYdVc4blIyeSthL203cVVsCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMy9reXBJQ0gvUm84RkljYWFOZVEKVzljZXB2blZacUJzOXFaWUxPZ1dLNThCWE8xdGNIdnJjeVFEV3hWNXNpeUsrR2lZd2h4MEJKbFlFVEFjcjNMMApVUHB4YnBLdWJSazhzWW1SY2ZxeDhud1BBUmpzcWxqSTdMa1dYNDhXK2hHZDdkay9QQnZ1ZU0wVFhPWHhGT01QCmp2UFZLajN5YmxlekJxa2ZXOXhGdzRlQXZSYVhyT2tqQy9aemYxQWg4YytjRnlBMExDM3poZlFvMFFtMFZmNlkKNytuQkE2cVR0MDBBU2pjUGZwcHVTTFl5KzFKaHJ2WXRHR2dDVEdqa2JEMmlHRytLamt3amtyL1BSVFc4UUpNagp3SHpVT0o0TjN6dTVwQ2tZMDgyZm0yaFNWSlB5c3ZJSVlnUmp2RHFpL2poNkdJb3pYdy9YQzlDeVR0WXBwdXJJCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmNWRmEwZS9SSTJYQUFQWFVSSzEKMVZFTlBqRXBsM3ZNTDdhYm03b2Z1a0R4NkRFWXVZUDFVOUQxY0JKV05najVTQ2ZTQ3crMFJ0SHdaR2kxWUVEYgpUZ2hCem1tdzFJVGlJUVpwZERLbUNKSVdvT0dCbmMwcTBDYUtQbGZPMTFtKzN2c0QyN213TE45RllsQXU4bUtwCk9CY1lmMDJjcEpyaGFoS2RiaE8yZVd3cmVpTDk1WWZTaVh0SXYzVXlPTkJyRGZVdVQwbXF3QmJ6dWM1RkJCUHYKaXBOREZnOVk4YjJwMkwxT0c1RmlYOERuelVtaVFONVZRemZhaXBNWHFWSnRsM1NWTzJyUzV6bUJSSjdWVGltTgo3bWhGS2ZMa2p0TEI5dWVPSXpZdURpV21FcGNlQy9UZ01rT3FDTFFYRUtnSzhEaEloUnZ1WHgzZnFpRkszd2Z6Cm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWlzcEFZUVQ3eVVhRkNXeTJ4YXEKMHRiZmNobS9TcHRBNWhWZHJMcmRlL2F4NS9zRHFrbUhYR2swQnBTTWpmV2xpUFYvL3dhWDF5R0x2OUNnczgrWgozTmx2TVRlVUVvMWt1NkdoSXNuZ25DNTFENlc1ZEFhR0QwUEh2SVRoemlKWndiZHcrd295N0xTd0VrUDZOTXJSCnFOcGdGWmxCT09ZekVRbURHN2t3bitqN2l0Sml0eWVsM2hMVVhtaFRHQ2ZvTFlRbDhUWGNsRGM0ZHBJQm9pSG0KZUVRZkxDQXF0M29JSThEUm54WXRUZXVrVDA3bkZXVzcxcEsrUVpDV1V1S2p4TkNJcDJwYWVWUVBkU1RMelVETwpaZldGemVpWFgwdWZXeVRCQjZUSGlkMW5rcVBiOVhyN0xtcVFjVTlpYVVTbURicVd2WVBpdXJZaG1SbXF5NW1lCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeW9zbUF2Sk9ZaFk0cXZXTFhkQ2QKb05vRWxPZEorbkNoR2h2azBkdER5ZzgzcDFzU3cwVWp1cmJ5QlZmdVk4RkxtdGdPT2tQQTJSWGZUTXpxaFBoMgpIRy8veVpEN2dTTVJZS2FsZUQwTUVzWFA5dDZLaXR4WTdZdHNFZnVmblJtVzkvRXAvSXpSQjRvbXJ0SnBJWG5DCnRIZEtqZU1SR3pZRUx0S3ArMndvOHV3T3pKL1FkWGxPOXM3ZWVoek5RcG1adzloZmNXdG5GaVk0dytZd0ZZbkIKL1VKZlNMUEVXYWZydldrYmhsWG1nc0srYjdyU21Qc2hiZnpabmpLT0o5ZnRoWEcxTXh2SXdsVHFuTmZ3dmdyTwpOYlI2Q0N4WVRGS1hTVUxIdTc4bEdiQlFaTzZtOHlmekt4ZitKTUdOMStZWnFQY0hYUGlVUHNMRTVrelUwZERvCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXdFdHBmNmxyT1ZqS0VTcnA4R2wKdXBSL3pGbXdFaTJoZTJnTTI4UFh5VDY2SXRLSndYdmNXekNveFBrOVhNQVQ3bVh4VWpuWGRIY1hEOFEzWGozSApsTjQ1RjBPZkw0ZEtrNkthM3RXZGl1R2N0VmVhRzdMa2QvN255dE1yUVRMaGlZaENqZThQU3U5QzlpWTlFSG1hCk54S2QwOFhraktZbmFyR0tHbkhiRnQvVzY3akpmbDlselF2VExQRkVQS3ZWU1czclVyT2JzeHd0K3JsazU2UGMKYlpRSHVCOE9Qdk5WQmp0OHBFTVdBNG1pVUxTeGJaS09vK3RtUnhJYk1OSGV6ZDFkQ05aNlBZZy94dE9LNTdEYwpscU5GR3JyVEVPV1dRK1E1K3J5MEZwVCtIem9hN2lIWTd2THNqS2NQM1Vpc2wxUGFaWFExNEhEbXdVNFdhUVNMClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0s3dmVsSnpoZGpsOXdtdjNncWgKSXhFRDNTcXNDTWlmdHQxNm92WlRYUkV5OTR4ZHhYdytFQjYxTGxxRlVEdWNIZkxydkVHMHRXdWtzQlpDRDZabgpjT1FjK1kwLzRTNW5mbGhvWDk0bVhWZWFLU29XTFQzYTFRMGQzYmNnZDZaWUVubEU3K3NRNVM3clNiSElUQkZVCm5scDMwSk0rUGcxNlhpTU9QK3gzSlh3KzIrN0wxWXc4SGwxOUF6VGowU2t6akN3UWdZVHBMRmtVeUUrUzJHN00KZUFXbUsveDZOZHRJTnhWaVlSaUZrNHY2VDRidzVCOVZXQnk2TlNiRldKbjkvUjV2RzFpUHdjS2lFYjduSnRUaQpkeFhVWmxTZ0M3QzZYaXcrWk1DMWRQc1ZTM2V6c0hIcnJJUW5oOTBOdjRlUElzQTdNV1F0UXMzQXU0cURoM2JQCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1dwMVYvTFM0T2d3TWFoeU1SeTYKV3pBL2VXNlJXOTQxUkpYdjZuUzlEcEltR3h6Vmx5VDdSUnUvalNFRzc2S1EwSCs3VXNrdWpuOWUrNkJDZExEYQpkaUU2bzFCOVdGelJuUEYxdHJMM3ZCaUllbUZLWUJkWUtsVHNFOE96YmFxTGZOUldrdmoyakFSbC9LQWFkSnZxCkhtYUhQd2czTG92cVE3QTdhenhITlR4cVpsWnhRV3A5S2dUU3h1WlJreVA5cmpGRDliS3I3ajl4bUE0c0ZOd1oKZE1Mc3hnM1QvMis1UGIxRkM5ekNvMEFDSkVEL2R1V2RxaWZ2MURZYkdwaHg4MDZPNlUyUVIyZ2VoRTJTaVdCSwpocGhLdlB4RHdRWkZqRmpQdUNaYWZ4S2N6ZVdFeVFKY0JIR3FHQ3JkTWhhV3RLanl2SkFlMEU0R0JhRDhrN1JiCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXFSZGRmQlJMNW4rd0hOaG1sRVkKYWJnVktEZHAxMUNleEdwWkZtQW5UemFwNUU0RzhwbEJHMkpRM29nMGhQN2E0dnhOWGRNNVlyL2JudDkxTFd4OApwVE5sbVNzekVNd0RxR3hVbEZ4YXVxVndiM0l1QW1SOU1jdklEc2tCc3cvSjEvaHVaTzZTTTBxNzBPenR6ZmIwCkpIR1NjdzIxZ25IS0E4R2hOODh4S3Zsc3pqZzczYllOK0hTQkdwUVBDNFE3aWpoUlNsSVkwc2YwZjI5N0ZWaDMKYjBSeU5SdVROc3hWT2pqc2kyNVdWSm9zb2ltOU1wdkUvMW9zN2R0cWRzaXo4UTdMaXJvWDlOK0ZPWFI0RzMzSwpMcnJ1QmJ0RjEvbUdIbks1VFo3enJnem5BWmxYQ3oyWE1NK2NPeXRaYVphOVNZSk5yQWFVQlQ0V1h3S1ZjbWZXCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdU9WNXV1VTI2eW9OWFVRRHBhSUEKQzZMNjBNcWhJQTV1YmkvN3p4Y3Evbjg2YVZuT3RyVmtNaytPRXJiOUlYSmRXb1JnRlJwOCtiU2w2UzE2bzdlYwpmVXM3U1M3c2U1ZmQ1S1I1eExEeG1UWmJ6MEF0M3U3VzM4U3FEdTRoT0RRQ1lqQUdXSlJ5RWhDclpXb2NQalBBCnpkWHlKeHozQmZOZFlSYkZ6Uno1NnZTSlE5U2RsdE9UR0lVcVFoajAxaTZFWGRWV2R6cU51bGJwbjhTMThMUUgKZzFtY0NLSTNITEdDWUg5M3ZyNWhlS29YcFZWTW1tN1dvdis5bmhkcEZ5UE1raG5YVWt0QUNsbCtmbW9KVnhIVQpDWW9PU3NQaTdPeEhzUnk0akFvRENOUGpHYytJazhVS2xvdDZmVnp3ZjZmVXhlL1FKZm5RZndCVCtrTjZvZ3hECmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGlZUnFQelZDY1hTN05UQW9mUUEKZEYvc3prZXBjM2gvUi9WdjU4V0NFRFdER2laTGxKM3lnOEFqOENSaGhnelQ0UmdRMWNoazhncUdEeXhZODNlYwowTmVVN3lpaHNBTXR5WXJGcFFxU2o5ejdmc00vc00xRTkybEtVS2FjempnaUxkK1dYNno0bURjWFdVWjFVbTdVCk5TMzBaVHFuRmQzTXhIbWZxT3cwMVRmNXMyMUtpeWVGUlBPanlKQUx4ZEZvZjEyZ2E5czVWNUh5SkFIUEF1ODgKVkR2Y2RsakxBc2Q1eXQxUDMvVkNlRnFmL0szV2ZkUTIzWTFCUHViR05oVTdoMy9yVW0zdkJUSlJTTmk2S0JxQwo3YWh3ZGpURVZwMTltWkVCb0l1SmJSRCt0a3FPQStmZFBGWExkTVdndFlBV1R3VEU1bGpna0xYYk9NeEo5dmJRCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWh1SjU3bTB0OVgyOXNUc2dBbUgKV1FsbENvM0dWVE9yb2FCZ2p4VXVqdkV4VmdaZHlvcmp5VHZmME1xS214dkVBOEpNK3J0d2RvVmxmbjBnMmMvNApMTDNHK1hzMUh1OWwrUXp3V2tzbzRZQlRiMGNUZ3dOMDFYb085aThvQlh3NTVDa2czZFdlMHMrZlJzMXlhMndoClozOXFVWHRnNEtQRnhkK2IvRWd1SnVuVTZQdUxiSDFud0tVOFQwbmFvd3VHblp2T1lCTjR4a0pnejFvbnErVTAKU0d2V1Biemxzc0tvS0h2SnFMbnBDUy9nRVZOc1dEaXBpN1ZVaTNEL3NNOSt3SUNkNllpalQ3cjYyT3VjMUhjVQpqdVN0cFp0Q0txTy93QUtDZDZhNHRLclpwMm8rU0RuVGNEZVhWaWFwMWIvRkhBb1BRRElwRERqaU9tcjkxQ1oxCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelU0N25YOHFvWkNMMzlQWHVjVUQKTmEvLzU5aHMxbXdVckhJbnVQRUkxM0E3NUNrcEFFdSt3UzRpOW5MNll3VU02TFBmdElkb2s2ajYwTUNZYkVScwpMYU16cExhd0hPQ0xUL1ljbVlDb1NFQi9xS0h5QmpGa00venhyQjM0Vjh4RnZOUGhtTzhVV1QxWC9LSXE3bzF0ClU0R20rTzZtZUlQbFcyN2xiSktTN2t5cUdNNWxXaDRqSHV6Q2VWN2N4VEgxeFFaTUVDbnA0a0tVUVFlQ3QwekQKVVRYV2tpb2w1T2VrUDRXdGJmTlNLOWJab2dwYzQyVGltaXM1SnFQUEloMmw4aGoxUTNVTzFqTE1uZDlHVnBkbQp5U3RSZzJlYzlLdVFXOUtwenRsaWI1VjRsY0s3WXl3eWVnK2Z5dDA4UGlPcUhxOENQSHBzVXlZRzFyRWpQNDF1CnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcm5pUUVVS0FtbUVvQ0cwOTh2SCsKakpScU1WaFhQRTZ5UzJ0YjA0V2VNRUFsc01iRE5uajloL3N0Y2lpblFxZ3F4M1BHZUtiYjV2S1JZNmhpNndDawphR2N4K29qeGJQSDh5dk1kamRUUXBZUTlRV3QwVVBGWXlTSWhMTWlTanhrVms5YWk2SmVaemEvckp6dGxPOCt3CnhGTDZqS3FuL0dlS0RGYTNEeGRJK0s4bWJMMWM4c0p2TUFDNENONVI0SHA5VWQxaUFwVWdZVm1yek4vY3V3OTcKeWZRM0c0dkVwQnY1emV0dm9PYy9DdU14RXhteFVWaFpOaS8xVTIyVnlKTmxrRUQ4VVFBcWJuZnZaN25yb1RFUwpFbkRSVmE0Qzlkc0oveE9xbFBnb2FRR2xGL2Zqa2VZcjJHRitBVUxWTkY4MEx6UXFKZ21PUThsZThDc1hnbUJXCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTlzUUVib0UycVplZHRocUYrS2wKWTRJSFk3L2M4RlZWeFJha3dGQjBnU2RBTmVKMnQrMkk5RElhcWVzVGQ2OXhzdytHOUpDTFBhNU93VjFhbDJFMAp5dCtOYVhKVlFTUVd4MFBua25hSFJaTmhVUVIwMzB4dGxuYzRRLzZuVFhVVmFFNVBPSFgvbllMdmQ4bU9YT29aClk4TDRlN3BCQndtRzR0WFNOc3pGSkl4Ti9ET2hnL0crQ3V1eFhyN2tpbk5mY3JPMGZ4eHg3dlp2aU5qM2Ztb0oKMEIrU2FQZXlRdHRubTNNcjQwRWdTMUsrcW1MQkF1UGxCa3BwazhRNjN2dUFWTnk3SW9DM2tmekZpblpPSTZKeAo3ZllkTU1yaWsxRVcrQk1TMytnRDNpS3MxVlQ4VWs5QmY1Z2dqNC9LVzhTM2dvUitWdmNhZG1EeXd1eEsxVEh1CmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0JZcXVZT2dKNWJ6TnQ4Q01zK3cKbkVMK3Nid3pLaHBkWmpPTlYzMnRZNmEvMDNNWnc1WlVobkhObzh6WVdsdGpNVld1L1A3TWVBVkRJL1UzUU9Legp5dE9IejZndmsxaTUyVk5xSzhTMzY1WGN1UG9jWVFhVHZRQVJrenZwSmZ6Qi9TdDd6QldKNktHTjhvSzhsY2w1CnJ1aFY5dEk4eld4NXJadkdoTWJNOGsvMDBPUndBM253RndPaHN3SitValpZMDU2d0NsdDE0eHc5VUE4Y0NjL0UKMTJ4NU5PaTNuZWhiOE5qRVZFc2JvUGFoWCt5YnYxRVFtUnArYmtqYXhyT1FxNi8yZTZua2hVV3V5ZmlNVXZjVQpLdnRBdkxCMkNGblVQL1U3aWo5YThqcDdZTncyQUZRWDhrb2NqYmIxbWtMMEpiREcvaEMvNzFjWFlSbjBscDBmCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1JlZG8xY25ya1FUQXZ4OW5meUkKTVk1QVhBQmRmQ1EwNzB0amUvOFZ6cmdtRloxcFordnc5bHNTT3Mwai96d1JPS05RT3BCY1F6aVowVTliRVlOTwozTHN0bmc4eVU1N0xnamk5MC9qWFcyNFRFRWxmTW4xSkkvbVJmdzZDblNpWEd4dG01bExrQmp3YlNaMlBoRHlMCk1zeGFMZ01GS2xKUGhHRjlsWU9RMytuM1JIMkRZbVplbGdxVDIrQzBDMDMrcDdKaFBRcXNMYmFLek00UTQybEgKUWVVZ2xrQzBhWmdUZ1ZZTUdmMjZlS09KcU1JQ1NJQld2M0s0TU8yZzFWbVZlcEJjMVdBTzlhMW5WTDMzQzQvMQppcStKbzFxMDZpVnZFMkFnc0VGbEx5emUweDhTRXNZVjBFQW9VbEJuVFZuSk1Jc0QrM2pnTklpU3dQaWgyQTUzCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWVwMy9BYTJGbTJiSTNXU3BMeXcKSDBLL08zb0tQUDdoZWFZczhCTWZRMzI2UXpib1lxZ3NWWXVCSWZqSDBDMm5ZV0Z1Lzk2OUJ2OTBCdldETTBVQgpOZ0cyVG9hTFNwcVdBdGdhK2lqWFFnZ0IvVmFNaWRPSFZ5TmdCZTRna1hSdGJYVzhsN2tXYmhNNDl6RkN2Z3p0Cmg5Qk5tUXluVE41dGFkaXozT1c2ZDdqa1dQeEp1eWozUExKdTA1UGRnN2JzVWoyYVM4azVibVFidjZJWVBKVXIKcUtSemJ5bWpsN2VKUWhiaWRGb1ZFMTlId1owTFBoNUdKRnBTSXU1UnVDZlpOVTVjaEdLTXFnWFFoQ3REdXRpZQpOOThoVFdxSGFna3ptcElSbFhaN0tuT3NzZFZIclY5V3FlSXJhc2greTgrWXlyRjlYcVdUUytRRlVOemprOVBjCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEFQU25ob21nM01UVVR4QmlUR28KNmhWdWZqWUpac0lpMkllZ2wyeWNjMW9QN1N2MjBudjNPWXgvR1Nna3RUWk1HMHR1L2JTU2o0ZXVlYkJ0NC9obgpFMUxOOGMzdU5LTU9JOXJVTlg1ZGpFdkxVNk9ZUzBOOTZ6N1Z6b1JuTFRVS0c0RzZ0cGVudTk1WUpyK0xGdWk4CmRpbmlTejVBM21IVVFXeUVPbGM1QW4xSmpiM1dCQzJPVC93UlNMYTMyRzkvTEJXcmpUVnE5QkR6Q3psNGpUVzAKYW03aEpIZHM2RTU5MkFCMGxuRkJ1UmoyYXFEKzZDZFdTZG10cDVCeDR1NDcxSzBoM0EwYVJqQUtscWpiV3lUcApqa2JQSzhWWEs3UDNvWGh1Z3B2Q09aUkxTelkrMTcyWWZHZVNIRUJCaUgzYndGWFlnSWFPb2NXYWtMQXMvUUlZCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFh5Rlp1em9oOXlRNDBvR1Q2eXgKUFJ4K0x2Ni9IV0FGdzB4cU5HSktvZGVtK0xQa29BaS9GMmE0TXhyaEYrc28vWTkwWnNpWHQ4R3NqKzdPZ084ZQpRa2RZVzM5Qkdab2FuT01aSmI2TzYwSml5TnFtNGVnMnRoL1pZa0pSN2tCNTAxdVNCMUthOGlCZTdNVk5EVkVOCllXTmpxU2Z6NWYrSVVBSTNNaVczelQwMnJGeithVko0d1d5Z0Q0TldSZVh0eStLTkxqeHg4U3h3dml1M2ZJbVgKQUZNVCtNRnpleXd2MDRkRDl4Rm5lTkdYNllvRlZDTzd6S25sYXZWeXVkY3BTQW92V3RycVo3M3FRaFU1SzNDVgpsM3VZQjB5V1JRbXdvMFhMVjdNY0w5eVpXWXlmcDZNU09SaTQ3RHpLTWtGRnNZRnJ0K0FUVE1Wekh2RzVMaWNwCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3BEWk5heWJwbUZvNWZKdyt6MkwKTXZQWmpHaFBSYWJIWlY4ZlliQU1wUkYzUE9RZ2c2MnV5bXdWaUhWQ3RBT2M1K0lYZGJaYStDcER4aERuU2gxQgo5SVlkRkhDMk1aSFo2WEMxUWNwTHhQaG9lelhsazk2Y3NMQ1NDT3JvdTMyK3A0QmhMNjkraC9LOThGVjRoZjB0ClY2VkZ5Nk10OVhSNElMVkJCa2Zta2srblova2pHNjJ2U3pUQnlQLyt1ekpSYU5YZ0g2NGFwcHdqQWxxeUF6ZS8KTXJSM2dVS2t5M2hkeWxJQURLZlJKN0IxYlR4UVk5UmxnaFQrZU9OdXpva0JsejFBKzRrandtaGRybHNRUnpSRwpUdGIySGZiOEVpcitGaFNQaW5xYnRqQytpcEIrZlJXSVlnSzZDclN0WmdTcVpFZm0wL1JHOUZZT0ZRU0lDTERLCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeS9mdVUyRmpZQkhrL2RFbFJQRkkKWUFtd2xSVkllVWhKMzBGYjFwdTd4Qk1VUkpzMFU4S0E0UzNnK1JTU1h4Q0toQ0xrZ0RSUWFyc0ZmMC9DbkFXTQpobU1wN3NZZTRHdEladjlZbHFDOHF1V2QxYW1pdGVVMDJYV1pnNWRBeGxaamwvL3lJeDNicGY0b0tEWnZxYU9DCmoxRStZRTNGMDM2MG5VNXhuaGg2ZEM0bm9YbVlnTGZVWE95UkxJT0s0cFhCd2pEQlBrckJkajVwYy84S2xGeGkKd0ZVUFNrbC8yaW9kY0gydkI5UHkzNndweGFTZnAyL0lVelYrbzdORURwSjBJcjBHVWl6c1c2cVhONzdEWEd6bwoxYURmb2xhZTROdW1GN2dYRXZXRi9OakR1ajNZSUw5NmsxWTVBWkNWSHFoQUUvSzFHU1N2NFNpN2dPYTQ4aWpMCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2k5QzMrMHdTczUzQjhzOGNMckYKZGZmRnJWaXZTVC9EekN1aGxtNHZmZlJyeUlyZWhBZXQyWS9jVXZKbEk3Qlk0VWpVOEhmTm9sQ3ZiMmJid04xeQpqYjNEQUZsMExBRy9VaGw3MGZQenUzYWV5Rk1mRlRBUFZDVHpvMlFTdzgwdW5zdklwVHhwOUhJYStWb1dnNmd2CnVWUjdUT09GYURwMFFpNHFkYzM3R296Z3F5VU9NQTBRRUkrNWlJcWZIb05jNmFVb3JOaDMzYmdYUzNhMjF5TisKdGpRRGx4cFJCQnd4Z1N4TEJtSC96MnVGdEFDdUFENmZxaWQ4QkpjRWR5MG5aaWVZeHRnK2RMNUZhZ0RDY2MvaApHWnF0WDBlTnhDQ0NuRGlveEJYOExsVkdjTmZnU1NoUm5xZ0pUN05RT0RWdWk0OFVIQVNSdEU1YSs2SEFDQk43CnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEhtY3NMR1d1bmZ6ZU4wZTdiS1UKcW9VWFBabjRQcUxYbVc4a25lNG5PbFVoM0ZFaEp5cG16ajNXZHhuSGF5VktkZVdXanZIenB5cHhPSTNDQmN6MAp2MEttWkl4Z0kveTJjUUxXQkNzMUMxamRPZ2xueTFoUW54Uk9GRjhlb01Beld0RWM4cmRvZFppT21KNjhJSm5xCmNuTXhWSVJJNGtUVWRwcFlXUndOWHpuM2d1SmFYVHJJVzJveERldlY5b2JiNDNmMktkSEZJcGtHNUdaV1BZbEsKZm0veWJ4cVo3SDJvUTV5RlpMVWp5eXF4NWFnUWM2eXdCOXYycDN6SDIwNVp4VEViQjBPMFRiZEdFcnlQd1ZVdApQa0ZjenR1Ykt3YmxlVEdTZHRyWnZwTFVaaEtVQkpiRDk5TFl6clRpaTUvZVFlR1FnTzczS0VsMTZPdFpZNXkwCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0NzaVIzclpzMS9vNkVhZS9jcmEKR1dTK2FOMTAzY0x6Q0VPZFhNN1VNK3B1VGR3TkZISWpmaGtVUnFjQWN2VG13NEQ2NXBIb2t5MWUxOGRWcmFndwp2WDdDaEkvdThSK1lYK2lqbkFUOWJHWXFBZzE0eTBNcHdGUGVZRytsdU5BTkxxcDBnT0cwZTl2amxpMHNWWlJQCkloTlBNRnZVbEVIQ2pRMXBIcFBmYnRrTlAyUjNvRVRpeXAyV3JzYUgzbHgrd055NEsvMFRXOTNtQXI3eG5JV24KOTBGbGgrRUJNNWgzZWxtaU5NYk5DcXBkcDZ4dFk3V0ExOVA4TVRGd2xzMHlLajdBUnVXMEdDSTM5dUlYZXdJQgpuWElDZ0VCM3N2WEh0QmpHRzRxc1VwWGlhTDVZcUtyeVA2VEU1aHlOdWxZUTREV0ZWVTZZcU80OUJlaHJPcEJVCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN3lQOXR2U1JDRHg3L0ZDdms2UlMKV20wQ0VaVVFsNGNmQWZXMklrR0xaZ2o2Q3hOYUlCSUU4QXVvWlFoVkcxVkJGbXdMeEVoQnlpSlRFV2hXWGtrYgp5SFdMWDJ5RFpXclcrYkxVYzRlSG1sNHBTczB2bVVMemp0UFl2UHdSNS9tb2hqYU5nMy9XQjZtSklKNDNmQ2hBCmtuRzdLaHYxOU9STVhUeisvQVVlVE5hWmVwcUdtQi9jWlFRdUcrbmNxRUFpVk5CRXlnWmgyMUVYRkV0clBYd2oKd28zbytDcG5sckI1c0t0OGRYYUV4SVNRRUNqU1N6YlJGNG5rc3NhUlB1dDBxK1psV2ZsaWJHYzZnaFR5emNVSwpBem51Q2gyVXhDRG1TNWQyL1dkdEh1bUg5VnFtRkhMbGwyRzRxclk2QWo5Ri9OM1NaN0RKUkFwUWJXSkliOVJtCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkUvY1BvbTdHb3pzOTVwelB3U2wKZEhYRVRZQmhHc3hCQjUrZVFnZWhvcVNlbkVpVFl1aDcrckYxbkRkeEUvNzJxZHhrVldBNWU3NFJMbUxDM1FnNQpWY2hoK3VwaS8zSUFFU3gzQVYwaml6ODltTHR1eGhZNjRReFZLYjB2WDM0TGllclc3a3g5d0VSK0g2RHFLYVc0CjRsVlo5VmNiWXNyaFQxTi9zbThvTXVlcmE1OFllSEZiMXdJeWx3Q0hMbXRiL1FZNTFRWEtTWEVWSDhmeVpLTWQKaC9pL210QXJma050U2Zub1ArZXg3QkhIVjhmT3ZEb2dqazBIUEwrZzdpT284SGFCeDZ0VXBUc0o4RlpNKy9KSwpoQit2NzdWZGhiRlFPYU9BbmVLOWZSWUZTRmwrM3VPUjU2ekxSdzA2Tk5jQUduUTkwOUQxdkI5UkxndnpsWWp4ClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckZzR243TWl3cDFPZ2pKZDB3R1gKa2plbTUvZlpidVh0Nlo1N2s3UmY3cG5Lb2RENUp3OGQ2M3dIWGJEZUdmZkprZWExWmZZYmsrZFM1a0h6eHBWZwpyV2F6QmdlVEsySGhzaUZ3M1VkVG0vZE05Y0I3cTIrK1Y2aEQzaUhIOTVOK09SZVlEd28xVW1KRlB6RFE1OVI5CnZOQmg1Kzg1Qnk0b2NUdE04TnQzWjczN004WlhmNklSUnhpSG5xSG9lMllMd2J0OStnRDNJZHVCNmpmeDgxRlcKS1NBT055cHdIbFJBcmFkbGJsak5vRm5GRmVqUFhxNkFud3E1YVYzN3RGTDl0eHhDdzMydkpEQ25MN1E4SHdwYgpNRWw5dGt4b3VxaDg4SER4UGNncVMyMjNtZ0tkaVUxUmw1RlFxMVA1ZzJyejB2ODlFYm9PY2VTL3UwNWh0bWgvCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeElUUWxiY3h3UHlHSiswY0pxdXYKY2dtb2V1UUdlWmJVL2JPWnJxdFFIL0lVeUhULyt6Qy9jUDdwb05aMjF4WUVDZXBNdnZYakVoaUtUbEpFejVudwpmQUdVNitOQVBlRDQybXo1R2pDMzFpMmlBeGRmVHQ1S1c3dDdqVEdLTW1iNUtsV3ZmQzhUUlp2ZXZQSFNlMFd3CjVQZFBwTStDVUtQYzVTeDJza05RK1I5Nzk4OEJoT3pmYXJ6Q0RNUnRuTUk3U29ETE00OVJhbUFDbitoNUwzOXoKSWxOdFFyRW9iZjZ2aWtiY0s2OUp0M3h3L3JQV0hOYnVKODZJYkdTRXZiNlRtZmRuWWpTdGRjdldGOTJMQjV2UQovL3FVemFsUTFZdVRWMXFpTkZsTkFmUThHN1l6ajh4RXlDWnQrWmhKM1pyRVY5ZTM2dEhrY3JwQWw3c0ZMTittCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNExsNVFVYk1adnlCQVY4T0dXVTYKdjFqNDE5eXJyVlIwU1hGeVQxRmlSTWY0Wk4wNnhwQllCYjZoUkY3UmIrOG9oQTVMelNVWHZQdXA3Nk1QTC94RgpSSWN6cHoyQkhrN2JrYjJyeEZlK0NKaURZTFNIeTFLcVpWbThYeXBIN2NYVFZsbTB6aWhYL21PR1N6dFJhUmFGCkFkdjVCS1VWeW5WYWl4UG9Hd0Z1TEpTWkNrbTZBdEJ0UUF1MkRPazN2M0k2elN5UVJGSTVLeGV6eFRZZ0lKYXcKYmIxK2lBSWVjdXBkZWNUOUg0YVh3dEhiVmxVT3NRYjc4Tk5ZVERjeklHd29OOWZsa2hQT1p4L25zOWc3Uyt4aQpxdkdpb3FJclFPY2I3L2JGTHlvRTdvZlZCVXJTRlNRODFyd2xaTlBqVUtLdVd6REZaOUE4cXJZZFRGNHlhUTFsCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczhORHN3Rk1weWF2Zm5hNnMzdG8Kd2VSaFV3ZHQ4WHFTcHN5RE94T1FpcjBZVngxSTA4cHgxcTlBRWxGSFFtbUZ0SDg0eC8zWUpiSXZDSEkxT2FBbQorNnZwVlJldE5oMi9RK0FVZmFOSTJ4UW1aUVdmbTdJSHRxQU42Z3RaUCtMM0svWFB0dGQ3aGJScXpvUnB1b2pWCkZpUXpUZ3VqWUVDSE5md2JPQ0NBaXMrRnFZZmx1anFNbFdBek9zaFhoRGo3T0NkVlBTbks0aVlwUm5ndk9ZZEcKekxwWlZ5anlUY0hscGgrOCt4ZUNudXR4aHZOWktEMGNmQXBzN1NaTDBUTjJQREhyOCtFbjZtNnNhZmQxOXBwRgpLL0xtaHZCeEJSeTlGUGNBdENMSVBmSEcrNTAxRUd0aG5IUDgxWnp1NWkzcndPNmQwS1UzYTJJdGd6MldlSGIrCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVo4RVpQc2xNb0hHQzlUQzB0SnYKaFFDR1lpSlp0bEpnUnpVQUo5bVhKTHltbUo2Qkg1MFlrRHQ3YVlhc05uS2hMdWtmRFk0UmpJY084SkF2YzdwYwpzWGlxYnBmV29CcU5aeHoyQnN5bytISlQ4RWlUeDBIOC9zcFRLVFZieTNEa0xDVWkzd0tTNDRkdHNjQ1c5aVF1CnlnbVYzK1lwT0ZCU1QzVXU2S0ZYOFVqWFA3c3RFMmc0c2JwOXoxcGtRSXJCVDhhY2xXVzN3ZUdtUWVHV3R5blIKc3VYWUcxb0RnVmE0cGVkYitPZDNlSTZ4VlFJb05USitEbmVqdDVFZWsxNUdyK0ZnVVppenNpeks5eWtFdGFYRwo4SjgwVkxteCs3VUcvSmd0VkxMVU9vcitvby80akF2TkQ1VlZZazV3dy9XVnBvaUVFV2pWRkNNWEZvcWdTUWNWCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTdGUkRHMituL3QrVGtwR3BkNjcKZmNTQmR0RjhueEpzRncvbjJlRE9kU1ZCNUsrUkVUSlB1VTk3OEJTUU9jY1liUEl5ZUNJSzl4NG05UHBNWlBxbApzVmJXTWR0ZFUrWGlTVXl3YVArZjlzR0htNWwyK1ZuQ1BTWk01RVRhWUdUa1k4Njkwd3pEQU5Eck1zWGs5TVg1CnFLa3BnanhuOGtmWndJaUhKaTB0QlRJL1M4dzNOcDlNdG5nTjdld20vUVU4ZmdWbS9ZMDl4ZDVwWElXdUFqOTUKWWdobkxZTW5WWmhmaC83OE13OHBKRFM2aEtzdlc5eHhSWXNRNUlNM3ZEdlUvS3k5eTl3YWFja2I2cm0zU2tQUApLZHRyYkROT3pQaVNLamtWY2dFRS81RUtmNFNNdjl5VG14dkZmaHRpRWF6N0R0a2lESkZrZTRnbGxkVUk1MTZaCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN09ZM2dRM2pYVXBXcWE5d3FXZk4KU3hkVUNqeGk5OHIrcVpQVWhXS2hydnNDd2E1MXRoeEV1MEJNZXdJN052WE9MQWdpQlh4T2FkY3ZMYXNyVE5hNQptK09EY3RqUENqVVF3OEo2Qkp2czZrVVVKVEp5VXQ0cUNISmJTMFIrK0lZTURmamRLR2hDU21ZZWV4Yjl6OG9uCmI3aHlxQkNBNE83SWcvSGp1MEZ5NWY2NkpsaStWL3dTOUxCWXRyQjhHMGhpNDZJTWJkZUwrbHN5b01TMWdpeU0KZW0wRTdZcm13UUM2RlBmK1ZKQzZ6SUVYc3dtVi9vRERqM2dhYVRrV25yR2JrR0NiRGdvMCtlQ2NsazYxK0FQQwpZaldsRWhrTUxnZU1vVHM3eHplWTUveWRTUFM5L1hTRDZiZ3ZOWU1aTXlUbXIrWWZobExua0ZqT2k5UEVFSU5iCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVRLK2hibEhIZXZjWVlZcytCUDEKZWlHUmN5cENKYmxNaVRNOU9yZXdqL3ltZ3lqWHZvMGlqa0xVRTNMWjYvVDRSTGxmWFRKR01wbWZtQndFVStTTQpWTEx4UHNuc05rbWxIOUtyRTY5RkY3NFUzTHMveGQzTUxzaDJZakVrRlJOZDg1ak95N2hicEROdEkvWGVvdVhBCnNjN2lFZXRJYjdkclBPdFk3ZFR1MG9PalExejZsWEtIVjN2czhjQko2Ym5hZWpjTzdjOHJwVDZvVE4zd09rcUUKNFRqTU1pZ3g4ZTdrcnRaVVQ5ODZReDFsVko3SG5sbG5tUmRvZVdVaTA4ejdoVGRCQTUvZG5KeE5iNWtqdTcvYgp0dzNacTNwaU9nQ1ZWTU1ybUNsOEg1RVFVTktoR1pJdjBKVW95UEZKRFE5V0FzVFpJZzN1eElyendWNTREaHZQCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzFJUFp1eGl3d1FnSk9vK3N6MncKR21NMkZIclZ5RDkreXc3bHdRN2ZhOW1nRFBoUzE4NWZDVTYvTUhYTDMzKzRadGNrdWh2aE9NZE5UUWVleVNTLwpUaXFTZVE2WXordEJOT3pVVDVYb3NHMnpNZHJSTG8xcWVsYlJldWJpeXNLN2w4VUExVng3bWgrU1hhWHg4eXRPCmQ5WmZwWnk5WTBIdWo0ZjdtMUpXck02ZWh4RXIvNEltL1VpSGFkTk92LzdCcjlwajBZNzY3WHNwSXdEb242YWkKQ2pVdTlYS3J2bVRGbFRKdFpzQkRWWUJkQm80YVpHVnFUdkFKdFlOQW5BTEZlUEI1cmFOMzlBQkE2b3lWY25yZgpnc3dNOFltM0VDczFzdVMvU0FQUGxreER2MzZ4dlE1RUZncjN2eDlpY2FBdk9KY2tZUURwa3E4S0xDd3ZMTFhGCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFRiYkhNUlZqdmhIcnF5Nkg0M3gKQWlVeGVWSTZLdmw3c3F0RjlONGd0ZGRmUityS0xvZXJqTnVJazd3Qk9ML0w0NnFxM2poa0ZodCtCUll1aHZsdQo0WCtacythaVdoakczMmM3a1c4YjZJSUEzbVUzdjdxNk1UQ3RMZG1OZ0FoQWxERmhSK1lKbnMyNkxpcENLRmNZClV4bjMvOGI4VmJNY3dhbWdZdXVodE5ZQkk2TlFQUllwckwvc3JnSURmYmVoKzFaRTBhc2tzaVlJOGc5cXN4MlIKNkVxWGtSZ0w4bXJYT29nZEhFSW5jcVBVc1pkOUU1WVVzOHFzY3RwT0IxTEVMM1NjRGdoY0k0TEpOZ2dQeFNNaQpSTXdtNlUyaU1aa3VxYnk4Vmw3ajZXbnJvNEREV0ZIMHBPbkVTQ2wxcGhxbUphSktEMTJvdnhFYmxteXQ5bHhnCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk1veW96eUZHTC9iT2ZEMG9yYkEKZjlnQlZNOWpNYjh2anBhNm82NlZxK2xWTThJYTRIeXdUeXMwdytYOVR5OXU4bzNya3NPRHVKMjI2UTUzcTZwYwprd3ZVOHBHVVNsTkV2QW5BdC8yc2lTd2dCZW81ZTBES3hjeFhaTVc4SVZyVzVTQkF3TUQ5L29KdU5JRjl3RFhtCjZQMHFvVDc5dEpJUjFLK2NPRlNSb2daM1Bhc0ZQQit6Y1l4MUN1Q05HakplYkpseGJEeElZTlR4eTVDc2ZkR2QKUnRNbjg4S2Qxc2xhem1BNXBJNFBHQnZMVzY5NFZqbkRtZnZDSFdQZnlEdmhTUS9QOE1jcXREWVVncm40RTBYMQp2bEpjSzdWejRIbDVNbFNuL2RnNlJqWlNlVVpMcXFvNGw4SWlqOWkrVmc5UkxWa1BmU2RnRWE3cm5qK1VSd0tNClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmRRK0ZhdllEZWI5M2ZvOGs4YW0KQW5FMjdiUWN5aUdtVDJCUkk2ZVF6aGk0blFyM2MzWmtTRGJ3NC9FcDBFa01QdkVWVFo5Sm1VRFN0Vm9qU1FIdQp3Z2hrMVdjc1lFdzgveG5JemVDWW9MbEpEOXA0aHZadXZob3c5RUx5TC8yaysvTExiVEFITGJhTEtYZkozV255Cm84aVVWdURlTVYyZllSOThDQVdDMjl1bTgrckx1cWR5SGdQUHQrcFhQaG4zcUowajFBSnlGbGQ5TVh6NGI3L0wKbzZvb05oQ3BHTW9YcnBuc1lJVVRHNzVyWG1LM2tET3A5OVZHcGxvb3FVQnI4OU5INFloWGd4S3hWeHV0bFl2RgpHUFJjSWlBQVQrZFgxam9maGNQYTJSVTlCR25OakhROEVkbUEwZW5CMm4zL1JrUko2eDM2Tk44aURaRjRtT3ZZClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTZzRUhpdGw2TUtPNkg2YnNpYlUKYXdEWkNjeGVUNmJoSUlvZDVQNW9IL29pdGwwLzFlcWtWdzRNYzJMUERoajUxM1krZE9wSERPUCtYWkg5YzlWMAoyK2EvRmE4Y0hlek1YZTNmNSt1ZTBvT05yL1dnZUEyVE5wNmw5aW4zeVdoUStCSXlsajUrM2hNOXNZd2FzM21kCmp5YVZRdllLMjRYK3V4akM1MTU4M0FZR3psZ3lRbkFJMnRqTGlUNWdOS2JjUkZDOWx5RklFaGx6ZlltUmNGWlkKQisrZ0lOQUsxWnJVODQwSUorUGpheFhqNllpWnNFMjY3UzZGZDVXeWJWa1VjZjArcHYvV1JvakdlK3pmR2czNQpta0hCejJNUk1NZzVqR1lUaDltaHR6YnZGd1NtaThkZGZaNzZzbVhYVFk5OHQ2OTZVSktPVUliTnkrZ24zc2g5ClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbldNUmtUYm1qVGNmZ211dFpDUzYKTys3L0JualIrby9EYUJmcnJEUWZoVGwwcGxHQ29xWE1JR0pFb3dlNisyd2s0RjlhVHpmZkt5ZFM5VXhrT3dvSQp6ZGRzbXR5Vk9EekF2U2I2Y3dvYlpjbXNmMnI2N20yaGJVRGNkVFRrVWdYQjA2K0Z6a1ZoS3FpRkJCY0VhUU5zCktJbEJwL3AvVktLa3loeUhaeElsaXJiTGxudjZRd1RQeFYyNTE1K0JWaGpWYkpQL0VmMEZqUVRyd0dLODY4bWIKSzhOeS9OTDVOMHF2Y3pTb0N2SGFBWHRZSTdxSzJXMXR1S25YMFpYWnhRS1A4WGs0Z1l2K2txUzI5QThQenpuawpCcEtmWUZNa2pnUjFKdHZtSEh6UmNQVXZsR2VtUVh4YllIZHBMeWlxaWFHYnRrdGxVVlIzZlJ4UlltNHBnTzNxCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEhZQ2Q4dTNsd3Q2ekJVdkxRT1IKR3hWc0p0bTRucVppRE00SHI1MU5oVkU5VnpuUjVSL0FTbDJJR0cvTzRTb2tNL3pWQmx0QVVlcmR1ck1GZGF5VQo5aER2SURqMXoyNXloUk95cHBUTDMyY0ozeEFabDVBcWliTmxKaENJVkRDNW9JV1U4cTV6ZGk4Wm9CZkZTRXE2CjdvL2VBOVh4aGxiSUpVMmJIb1pRVEhWOTF2T2N6VkhRZ1NnakE1QysyelVHRzZ0c3ZLalcxM1l3bTVGS3ZlKysKRysvSUV6TG5jUm5WcUMrc3VZZEd2bXdFNjFzR1lrcHBicUFXdlpXQmt0ZDgzdGNNZ1BKQndwd1hNaTRGSmhCZApocjJZNXZKOGgrbE0yVmRianh3aTlnWkdnNmJrNDJnMDNrSHVLTTZuU0ZzYWJkUUF4bHRQUStORU85YWEvNElzCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1RXamM2WHEvSG1vd0ZLUEhSRHoKZTlKTFE1SGxodEhRb0tGOTFaMmZrQ1RFRDZLRnBFalBaTVR0aDhzUW5WYlc4YmltNWo5RjJxM3h5Z1hvQm0yYwozdEp5OXhpRkxLZzhaNnk5L1dLMFUxNGJoSzhoVlNLb2ZpZDEwLzg3SVlVZEtJOGdpQzVuUWk2cFJ6RjR2U1VoCmFOdEYrRVMzcHQ0Yk12eDk0UlRHY0IvdkJrQ2E2cU42dGRYbHo0d2c1SC9RMUlwbVpTeEQ5d3VlVllHZjFreGIKREpBNUF3TjYrN2Q4R293aUQ3c2tyMGN5NkJLVFN4U01MVU1DRnBnVEJ6eWFYOXM0aTl1WVZaNStoWUxlRDR1UAp0QUgyU0hXdWxtNVBmdU54YXRzV1J6WmphejZaNWJYejIrZmlWOVd5amZhZytxUlFQU2ZOdkZFQ2l3dTVHMEtJCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2hzN3Ixek5PZC9IbExybmFkZW8KbXNtTUZQamVEdmdMdHNJZFdjak1LY0NHSURVUHdITUo2TDhhT2xhNnNQYmpNSnRXUC94N2pVbjBJQzVXRWtRRAorNyszbU9YdVFRMU8wYWtmbzlGSTdpYW1ldVcwTHpBN1l2Ym9CVEhtdkJ6UXJidlg4QTlLZlJaNGc3ejljdXZ3CmZmTGRSUTVtSzJJYyt1c1p5bUtMVWZuM1BRVmlQK0dRYWh6emhNZ3lvcmJqTGEwQm5VaHVlMU5lU0MwdmVNVloKNTl0Y1liekcyUGpGRUYxSFNsR1Vqb1hiL2RHK2VUbjAxZUZvK2VrMldzYmgwMktUNElQVWJFOEU0K2pGQnE4YwordnA2alhhb1YvRmFmbGpsZmpQbXluTXRzWEFaMjFjOXU4ZEFmQ1hCTDNTM2VYa3pKM0tXY2hYVXVMWThjbHhQCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0FBbUM2TGpmNks2aTZiMmVaRCsKSmI2Y24xRzlWV3dtbWtnbzk4VDBPWjRoaFNxRnJ1Yk9VcEdNMEJVbWNoK1pqZ3k0VmdHdm9Nd3Q3VjdmbUpCMwpRSzY0eXBKeVVPU21nN044NEk0SU9SbUlVeXRid2VmNGNHZ0duZDJTZDFqQmZNdndUbkhyekZ5VDRBNHBiMzB0Cks4bEFXSktlMnNuM05FbmRwQnMwSFcyZGY3aUtpMEFXWVozOXphbjFkUW40Z2ZUYXF6VUpiZ29ZVEhsRHZQWHYKenlzOUZKVnFwb0ZqV0VEQWxRTFRBcXl6b0dycDFKcFFFL3VrenUwdUsxbXJac28rRGM4K2EyM3NyR1k2ZHFJTApXM09OYlJsWGRwMXFYWXN3WWNZQXo4M1B2SVM2Ym9EN3dhZlFJM0tjeU83NGl4dTdmSzhVM0VlbmRtZS9YeGtiCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjF1UnNieGxZeTdhOUkzUmwyYU8KSFdtT3R5cFg3OE41eGRocGliUlFlcWtCcjIrY3NuSjJCMXN5dWlBVmlZVFNKODhwNm41Qys3a2o5cDc4M3VuVQpiN3RJS1JNNllNeW1RMzV0NjcxMzVmTjRYMThPZWt6N0ZZaUxuMEkwaEIxOWt0Slh0VlNoamZycTFILzd0TVNqCmJVaUpKY3ZaUUY1YTU5RktTSzZ0K1dka0ptb2N3aFRKMkM0ak5kMEc2bm9uMVBFYnc0eGtsVDMzTm9GMURzMXAKWGYxT1lpRTg0UGVyeFE0dmlEZ1gvL1llSnNJNnp5MDhSRUNNRW1QR3BvSlpEZVFxTHR4bU95bmRjbTA1T1M1egpMbjYzUTNVVFhkL0RHTjZuNml5OVpWeVZUOXFldEVkOXN5Y2NJRHBrSGxvSjJLbEVsc1Brb0FtaEdxOFBFSVVWCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2FYSUluTEtnNXlwdDZpVFhXRHoKQkJoOVoydVdWYS9ubmpEbXEwMWVxd2oxWW5lakdydUc3eS9BRDFPS2VtWk02TFlySnVPbzhKQXVRM0Viek5aTwpUYTJ5bmNUVkl1ekZVaE5IVml5YW4rdHZ6bUYwbXN1OUxsTTZlcmlacFprMmxydzRScDg2bHZVODlwczVSTEczClo5SFgwK3hXRWtKcDB4eXVLOTFtUEtkem1FRUx5dHM1Nm1YUmxnOS8wanVubXlSS3drbG5QVHl5c3ExZmo5Nm8KYVpGamZHN3dWdWFVM0lvZ0FPWldwYzJmRldqK2x5L1gwMnpqLzZFdmoxZFVQOGtPRCs3VGRBZFE1WnBPd2lSZQowN05JMFRSd091WDVaeXRxWUxiSlZsWGZ3dGs1cHo0THkwbElDcXlwRDlkTkhZd0M5eDRyQVBkZ254NVNXTS9PCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGp6aC9pTEVGVVVKb1ZDdUd1VVEKZlRJK2JPSW94a1lxRUQzQ0xRUERnZXBySGhHR3JRdXpxdEFqZW50cURhRjRlZzlkdURBU3BZNlZhazNXbERJbApsZlZOcUdwK0RHMjNydXhyQzN2ZU54QnhmZWVZSHZKcjFVc0xERnNXNXdSc2dlNWZiVWdoV3JOL1k4NGdaeTIvCmlzMHVTMjJXdnpPMUhURHBGWXNBQkk0amRNaFFleGlZUGs5VXBhWlN4alR3WnNZanZYZWx5TU4rRktYR0k0MFMKWHpsb252RFJpZ0kzZ1U2TW15RTRRMjJPWEt4VzJNSElvdUF2MjJudGxtZ0k1WFB4VTN5dDg2ZDRtVG1oUHlFdQp6c1dWMmVNemNESWd3TEpxbk0ya21xYnh5bmk1bzY3cWVVMEFzRURydDZHV1JMWXg2bDVFM2cwQnVNeUd4L1N4CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlYySHBYWGlUMlVlK1dGK1p3dDIKQUFEOUl3SjBiMkNneTg0eHRHV2JhZWljK1p5Z2xoOFp2THpvd2g1ZzMzQWFHZ05BZEFqRENQa3VGQkdIUjNLOQpHdVNsdmtRUlppVno2dWp6THpEdFZyTFV5anVEdU1ObnAzZXl2Tzh3SlNBVUhFSllYb2JYZEZ5YW5Dd3NETUQ4CmJQUnIwZ0toSmc2bkY5VGQ2eXVGbUZpc0RpWVp4dXhoY0Y5MnlTT3RZNldyTUxCc0UzanZVaDMyK3UvcUpCZHQKczVydWNZQk90ZHJjb0JLaVJrLzM2L3pFMC91anRId0k1TERGb0JqZGdIaWZvLzdadWZlcFV0Yks4Zkk1bUhCZwpuQkM5d0d6bnZYOWhZSGhBSTNId3doUTVnYU1oRWxOTXR4Qkl6YkZJSlc3TGJrUVdRczlLajA3R1Y0bUJhc3dLCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjQ5bDVMNFVaUTgrWlhSdy9BWmEKN1ZlTHpIWDQzKzdLbVh1U05kTjJkL0VRVTQ3YUpoTWJodldBdTBpTFlyQURhbVEzK2ZsdUxBVEZja1BucDlzTApidEJVYkF5NEZ6ck1hQjRxSzNEMHpvYzgxemJ4dFlTNEcxNmxwN3YrOHhoUHlCNSs3WHFadHZ2OWxnVEROREIxClpGdHhDSWxtQ1puWkorckg2Mm9YWlAyeVlHUHArZ1JxaWxXcHpjc1I5b2w2VTQxWUZqb2E4ckRDd056OUZ5b3UKTHZGdFdkZ3RUaHM4UWhlREpndVJuQzk4bHNibTdYeVZ4YllpUmdZQTBiYVVMb2I4eWY0WDJsT1drczhWNGkvTwpWUXlJblRUUHhIakZLc1RmZ05RV09iaGxyNTdaR2U1cDNHZ09Camp2TVQ1YjNKbjdleEk5V3NvZzBRMkR5OTYvCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTh3d1B3Mm05dEFQUzdhbFVnT3AKeGI2NjFKNlZGam41bjlRd3htSGRYVmhxS0dTd2tlaG4zSVp5N0sxdEp4enk0bXdtMUhJc2gvdk1sV0YwUWNnWApQUjI0YitvNktGWDYzRUo2U0tYVkFzb082QmFJUm1Ob3o4RytkK1kvaUsxbm9TNm5TKzJkQWd2cGdJek0vcWdBCmJyVW82MmtWQzVWa3ZTdWh1dWswQklERWVWZ0h4UVNaeHptRjBQYmwyakQ3NHVxUEpBSkpEN1Z4ZEUwb0xQaUkKOXJmN1JQRzdPUW10eHhrU1N0c2FLcFJUK0J0eUtjM2h4VkZMa2hQZFI2TTYxbWRkeUluU2lOTkhnRGlqc29GRQpQUUVwSGVDWHc3V2lnck41RUQzT1g2cFc3d1laQWhpTExPWDEvcGZISXNjaElzaGlnZDU4YXdrTEdPdjFaUFpDCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnpwRGd6OU50UGNIdkROMVo0aU8Kamo4eFFndUNtNDhBWWJTZDJCUmFQWlh2c1o5MEwzVFhoQ0lrcTF2QXErQjExUW1jdWwwZHRLelNXa3dZczZnRQpIaDR4aWFvcjI5aElHNTJjWDk2VHlGQ1VCMkJxQjUyR2ppb1dTTXRrMHExS2NTS1hESUswL3ZBcGI2anF1aFY2Cjhpamh4dEIzbEYyb0dEaStueThXaFZ1TFRWd2tIdFlJUXc2NkdhWU9oNFpuS2owUSs5YVFRaGJBVlkvaXg3UzYKWkF2bnZ4M0NsNkpRL2Nac0lpS2xLL2ZXRkNVTTZvaTVnNStYTnRlRVNWN0ZQT3lWRDlFR1drZ0lja0ozQjF3Mgp5M1cyMW9PZmRoUUZWZm9reVRCWlpsOUNWeWxiSkY0L1pqL2xiNVhQZ3VYdTFQa285ZlY2emZkM3RTK2o0NlYrClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHlYTUVTWjlRTDdqVi92STZ1ZU8KbTA2d2k4dUFBTzVlOWNKYzdWSXhyYmtEUTlqQ2JZdCt0MmlIUDB2bkd2L29NWkVoWFAzZVdoa29CNUR0aDNuawprZk16V3Z2STJTVDdhT1BmTlJ5N3ZKaEJEMzE0UlUrVjlzUlp4MlpvSTVBV2RXeFoyeFh4QWhucHFsWlVhUUZCCnp2MU5aY3hWbmtqWUFTaWlIcFdOUUN5ZXg2Nk83d2d2VERLOW4xNmRaSlhyb09MZlIvc2NoZzdmYmRGelBQQysKai9SWUJ6eEVpQ0lTNGt3N2NxWjJqNEcxMmZjZW5wSHN2NUtxbDdVTCtNV3FJVk1OOVh6cVFUMGh2NkxwRkpZWAp2OUlzM2FzRDlqRVgwVWJZeGRIUkwwTEVkTDVIVmdUeUluM3VrREk3L2NFdTZHRnlPcHNKcmlvTHd5ZkxnK1dpCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3VMQXZJT25oa0RVenJ3NlVwZ0YKR3JiMUpVS0dCbldtQ2F5U1NzanJwUkFMUHN4SGR5ajNuYjZrS0lFRmdnK1gwSEMxWVF6QWpXREtWQUY0SHNJMwpzSHIxRDBTM08zbjFoWUFhOC9WbDJ4Tit5MUt0V1huZW1FYjhXT2UvNElpbUNnckZHbDVhSlZxWERYeldBdEFICm0yaFFIWVpDdzMyWWh6TXppTVlVaWgvbnFkcTY3aEQ5bWNMbWFNemdLR3hWWG8zajB0UXAwQ1NJalkvZDdZR0IKNGhEVmhvTFM5bno1NGU1SjlZYm5ETFVoWkxjc3NJRlg5OHdlTTdIZUxpUCtJczNiOXJOZlZNTGJQeXorOUFIVgpZaCthZ0pSSWVNb0NxcW5PanpXWDlGQ3g1OUhSR3pQbDNtbERmVmxwZGxKVDJtdy9xRlo2NmpXT0REOW5oYWFRCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenkvdlYzUlFGSnNkK3F4YXVVeW4KVFdibUxFV3FlRmp1amFZc1VycFhWUGRGeERlM0NFeVVLVWJTSzdLSUtpOFRPTmN0NFBqU280QkNWZjdmZytvVwpLRUJ6aGRPcmFWaTFqVzVZdXZXY0Q3QnZwa3drYjBFV2JIVmJ5bmduMkhlSHZBVUhpS0dIT2EzdXlmdXloOVVUCjUzalk2VGRXL0tQTDBPWXZxRWpkQlp6ejllQkZmTjNDVzBNcTBDcUc5WEs0cEpOZlhGb0NEN3JGWGlsTVpWT2YKYU51T0hvQU8wZnpWeGxIMTJxVmR1b2MwMWdTcjRhRFJUK3V4ekx4QXhpTG1pZldwS1JSNkN4N1BjeStvMGN2WQp1WW1XMEpaYlRhTzREam1sb0cySjMwRlM5V3VyaVk2UCtxVGlFUnc4b3pCaE10Nk5FK3NWb01rZnFnUnhsR2VLCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnp1R1NvK0JIcTQxVE9mbFIzQUEKaVMzT3RuY2JtMTI3bmtsM0ZjY3ZXcGI0OHRneTlZMlVDcWZibUcxTlc1MUtVZkNHVzBTSEJJcjN4bDVjUTJNcwphcXEzeDlxcTF0KytQMTdVMWpEa2Q3NFRoZTEzemsxSlFGRmg3TmZRREpNcVE4SnZGaitkWElOc3doWCtMdEtLCkJmRjROV2NFWHI1OTJLOG5OM0YyMUdOZjJ2eEFudk9WaWFzWDRrWWFLRnRVS051bm5jMGJXS0RmV0pxYW9pVVkKR1JGRWNPNHpudXM2TW84U2Vuci90VUphclpaZ29nVlhWYXo5WmFlL0llcDZZKzBLYjEyNmY2MHJnbnRiSFRragp1M3IzS0IwSDhna1RvcWo2SnlCZUw0dzdic0dEUzQxVVI5NzRYSmtVb0JRRHVDUzI5LzRySGtyYTcxTXgyckx2CkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWtmVG11RjVtTWptRC9sSE13MGwKaUJRdFNoMXY4YWYwTkNmRkZ1THlNTmx5cVdYaEpleFNiZkxOKzhnbkFGUVlnRzlXLy8ySkdUWVNMZi9oZzBqaApzSzEwNnc1TG51L0piZGdET040cVlMMW9tYWRUbkNFcDJGUWxBQzV3RkZJUFE4aERWekU4eWk5SU5zQzNFOTQ1CnBJUWVkMUNuMlkyUlpwZUdOWFlET3lzT09FdVBXVlRIMDRad21VbjFZclF1MmdZdW5pd01ydzRzTUZIQUhHcksKd1FJT2ZPSUl4THpMWnBGUVc0RjJjbjVWdEYrUnBDcUJPRm5Ldml2YTZ2MHE5aFVqbVV3aG9XVXRGMkdMdW1RbQpLbmdaWkVLcVB2dEJTODgxS0FjS21tcE92TWJxLzhMOUlONTZNYXFPZlhsQm9EU25LdTViUHJHMWlFTG9mbWRBCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFlHYmtrUnlYc08zVVgwVTFSeHEKTVJhSDU4UTRVNUFjTlpWdGRJUit0VEZaMHNRbXQyZlpqUnZRVi9Rd1lWLzJxWGVVbEZ3TFJKcHdlZktFWWc0YwpOcVZYK2QwU0R1Y05ka2p1R29BWUgvVWk0MGZUWjlKV3A2ajdETnovYlBaTDNxNU1kWG9QWXVKeXMydXlBZU5NCkFkbFVVRjZwakZuSzhEZVBGSWJlZkw0VVVBNFRPbyt0dWx2K3ZXajYveWJHaUg0V2NJb2Q1ZDZQVGZrbHdEQUIKbzFjWWo4aWdiMUlFcHFiejB0akpxSjgvdmlxTGM5eEt1dmVYaXFvUzFPdW5MVVBzR0c4YkJNNnF4YTdRazl5dApQOUN1cXhEYTF3YUtVY0N0TWJ4Q1hNNDcycGMvc0JhNHZvbG8zYXRLNFc5MGlkNjQwTXEvQ21yUUxZR2U0SU5VCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFNqdmk0aWcyQ01JSVdRMi85UHYKdlFYK2dONVlBZGZXR25ZaEFuQjQvVHh6N0NrU3lyUS9Sa1VMczFXaUxKRFFpRFJRL0I5RnJxOHovMElkZGtwRQpaWHhma0Jra2Iyb28wTG94UGFDVW50ajlIVDFzaW9nTWxTMVZWeHdFY0NlNEVpK002NnJzQm9TZ2VVTVhZTmhCCi9WS2crclMvNldNYkw5VERlNVhEWFNFaTBOT3pwZ1J0TUdrNVkwUkxvWVgxUmJqeXFOYmZ2MTV2UW8rR3EzQ1cKK2dYYnBnMCtZb2tpUFlRSko4REtXdnlNUGl1T3NwdTZoVzdQSmZ5S1ZxQi9TZkVIQjhLNUw0ZHk2UVIzYWVEWApSWmNkQUF1STVwVjkrNEtucmVGUldYakVIdlFHbk9oZTU2WkZEMmtSYjVkdlBIR3Y3emFmaUdQc0NpWlhrcTUrCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWNsL3EyZ0VTYTQ4U0VXOUV0Z1MKZUw5MkdhYTRkVEdTcEF4Wlg5djFQOFRVNTQrWE8wYS9GSkc1blA3Z2ovVHg2djFESFd0bmFJRi81UEFRQ2lDMApxT0FjZ3AwbVl3VGQvM0lyaE9zblBlZVFPWnRzRHIyNExGdzkyd3ZER2xwUHBHTzFCSmxlUEpOZXpDOSs2N3QwClR2U1FBZXIwYUoyNzhWTXhBRDV1Z0lweVVJdFdqOUZ3MFZ3ZC9yQlRFdk1KZndSRFJhY0lNbEwxdFdVeHZLOG8KSG03Z3ZTZWRQeVhEa1dudFFOQzh4bkFVR2pUK2JNQTByQm1pcnFjbzFIZWhKTklibXJVaEM1MWhyZVhjYldsego0K2phWHRNTkZLbm4wMlY4VzhxTnlFVlkraTlBWU83azJpRmxxUXRCWXlkWGE5cEdaQktlenMwMkd3ZGU0N055ClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWNhU0trejZ4SlVSMDlvSndqbWQKTE56cWFobFE1RnlSQndDdGRJN0JnM1lGUld1SUhtdHRlYzBjZFZFcFBJdE9KVlJ0N3kxcGpWeXFwa01Kb01HOQpyb3grY1dRTUhHa2NORUtUYmpBQXJHMm1ndzZYNE90WHRPTy9qdlM0SjA5NjcwUTVTdGZNR0Fpd0hFb1hUUVhmClVvSjNlcFZLbXluT1RXRDlnT2k2bDBBcGVVcTh1eVRYOE9MeXRITUFnKytIZGcxUnRraVk5Nm1tYU5UcGdPTkIKemUxN3h0L2ErVmRnL3dCdXdVOHhvNm5UM3lWL2xaL29tTHh4T0NHMmVFUC9tbVptd01Za3YzaVd6TVd6U0RiMwpLVkorWTJCNTg4MFpaNXNISEliYkhadzBVQXppbElUSU1VanRYYXhPUVVpRUJOZHpTS3BhN1Z0RmtxUFVsczZ2ClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2hsMmlaVy9iYWNvTmpWUEdkMmYKZE5nVkt5RExFYWRjSmh1bXh1SFA4QWFnSlJUaktUbG1Ud2FiNGp5a205MFU2cDhMdjY5Y0RHam9heng5RFFiNApleGI5cDBJa2RPY3IvTHVnbk1MRFFUTkxFOVRuSDRjcUtqUGFmZDdEek1iS0JmYWRybkN3RXU0NDkxTG9vQzJKCnZBQ3RSM1Fsdnk3ck02OHVPWWF6ZTRKZzFEQUVocGliSGI2elFpYlI2SEJNWnloVWpCRGlYRFNKQ3RHWjFieU8KU1VCQnpndElTejJLb3ZDWTBSYytUSHM2eWxLVFdQb3FWcS9JdW0wNUFQY3EwcjEvOW5vbTZyT3QrN1gzdkhDcgpRWFJaTVVMVVJaczJLdStaSVQ3TytBazlYU0s1V2tveC80WFIwcVkxVnNzRy9NSERGZHZXU0VMMDdnRm12aC9XCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjROazVBNlp3bzM2TzlCbHFsVEkKN0FNbXpORHFoUkpPNzJTMmFYS002eHBLWGEyejMzWnpDVzRCamNVbkM2UWNBN1h0ZEFnOEREMjE0SXR1dmM1cQpsaFFYS3J3T0liN01RVTkxQVZtVVBIcUVNRUlNS0VUbUFsajA3QXJDdFljZlpBWGE5MmNkd28zcWg0dFpKQ0R3CkZ0K1VTTm9hTndaNUl0QkJ2QWh3ZTQveGlwbXl3aDhTeEZ6K041c3UxSXk3dDVJaHRCc1hoa1FUbzd4YnVUODAKY0YxV0g4aVlZZThnUTU0Sm93SXdEMUhlbkw2SmxGOVQvMWxXRGlBSGM3T3ZaZU9nRHlsVElKUjlKa1NXdzZhVwp6MTlHNTBPNUxKZFhlKzZjditOeDZTalR0Szllc1hiWW9oOU05QndZdFFCSk1lQUdUYU5MY1lLSDQyL1RxeXF0CkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlNka2lkdkIzM1VucUVpSUNNanUKbUhmUU1sbVRueWFUYTdoUHhhaUt5MU9BeStxL09zOFVYVk5DMEtjS205R3RscGRrZVhGc3hZblhBaHdJTVB3MwpvYzFRM1k2dnUzQzdmL2xxVnZhOHVqcDk1TytqajgwdXpScWJrc2kzTDhuOWtacXFNOHJmNE4xNm5GNkVxWTB4CkVKcUVZcnlwVG5vYlNpRGI0c0JENHk5Sk5INlN0aG9yRWFId3M0VUhMVElXci95WHB0bjI1c1A4dTdVUm9waDgKdDhMeG5MWFFyWHRCYTRrYXpnMG0wOGFhS3VCM2ZQQnNtdHhLb0RFNW1kRE0rdjlhY0ZGY2xGanpMclBBSkxXdAp5TXJpRG02bmp2b0h4NU1FeHlvWEgyM1RudlJReXloMlFHRDQrMmlYandSdWNzbGxqQUp0czA5OWxSTnErS3hzClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTVac29KR2l3eUJhZXFLMTdqQTAKQUo3UU9YTXNVRkUvU1F0MmlpVy9sRVRveEVOVlEvTzhPL0xqdTRONytyWFVlL2JQRzBLOC9RZlBSaGk2ZDNneQpCUHduUDlvSUxxRjlRVmRraWthUXlyWmh1WVVvQUpGaDExZEZYSk9naUlXTHdSZWZWYTVaRjBpcngvRzRxbnFICmFjeE85V0d0Vy9GNVBJZkxKU0dXRUREWE4wdjhxOEsxQnhRUndEZWxQVEsvZ2g3UXo5Ni92bEZHQWJLaitLL2kKelpXTkZxMEdiQkY2L3RWc0NHZXlTdWdaL1kxeUlsbnBkMWxqOHE2Y3IyU0pjMldUZDZhcHpnYmRmSmNrclhzbwpSY2xsMnhZaDV6MkZCNUlhczlqUjFRYXQyMWsvNUZHeklkQUQ5UkxqcHc4bGNzQTB3MndmMmtaN3lvVCtKUVVsCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbktLSGlsOG1ta2VWYTNhb1J0QzgKbE1ic3pZUWdvTnZiSVNsb2FUSGtUY004WitGbi9DMVE1SVViQW5DM0s5UGwyK1F1QkxhaWlkTEFBZHBsRFEyNwpCempMdTZBL1pvSWxseDMyTUx3akQxdVZ4Rmg2QnFHZS96cmdNV01ocDQxd09jYSsrOEd4QlBCVTR2MUs3Y1JKCjNDSTZNUHZiWHpYMlNIUXR2c2NWSlJSb3Zwa1ExNWZQUVNERE5yRG5nQ0JPRE9rQUlIOWg1VFdMRnVIRDJEejAKT2dqYlUrdXRMVEFZSkRYT3R1c2lNREJlMXBmTytqeWdDckdtSk5icDUwN05hUEdxcTdqVUJGd2JDZzB0d2xERQpXWjRVVU1GTlU0ZTNMc2F2Q3haZ0dZQjJUc3RvdXlnWk9kNjVhS3RweWZ5RmtBR2tlaFJ4WUZoRXNHVGozZUFZCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXZHVXFsN3lwZWZmelowVHp1Q3IKWGV0TG9TdHZlS3NYTGNQcFpuNGRjaVFLWmVnMEtFSitxd29HWHBIOUxpUk1WZytiYUJmNmVhOVBCSkNDNm5vVgo2NFhYZGVTaUlJU3h3a1BxNElSMHovV2tmWTVad2QyWWRZOEJFUk96QTZtTW05djVET2ZTVWl6WThHb2FSTEZnCk5TTTZ2NlZPV0ZDdnUybWNzSk9OMDFPVmo3SURRcW4vSStDWWJxM2RZYUZQaEFaVXhwZW5WSVZHRkh2aEVLNTIKS1N0OFJJWUtOMGpZeVI2ZFkyNlRMbDAxYU1IUDJRUGhFNzZjWWZpTWdLUkI5MnYyS0RWTkFqem11QVErOG1rQgpCOFNpaitCUFg4Tkl5VXpjYXh4NnBhYUdlSU1hdFhxa2JKWldrOUFycGUraUlla1J3U3J4ZThhMXdHSU5keC9iClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFEwNTNzaGUxWDNRb1hxTjVrVFcKTDFnU09UWnJOWjdraERWSWFwR2xHMWdJL0xFWHZsZFlkSVF5dy9YYitjQ3M3RmhoS0pSdVpYYjV3dWhZZmVDcQpnNHMzeTUwZTZQa2pFOGNtOEkzSmpBWVlONHNqeHVMVGJDTUc5NkxXK3NORkxBNGlFNHBDZ2dlUS9xV3JwTHAzCmE0SmFmbE93WUFYeG50ZmVYc3pYYzJwRlRLbjRoQ01oL1hoTk9aZjhURmhyMzhTbERLaXhOQ3JVcXV4R0lBTCsKQnBrU2RyQjRMdDdHWFJUR3c2WFNPZEFYa0dVNEVFand3VWRkdVhSVDdxaVdwNGRhalBMWURXUUNPdmdzNzhERwpKUUdiT3RRLy9tSjRHWFlmNldjcUtPUnl2NVJWQ0Q0Ymw2UlNLVkhENEJsQzd4bXdxRXM2S0FKWEUwWERWcEtUCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFY1bHZnakdMYTY0b21zWXlscWIKQnROeXdlb2h1c3BveU94clFHenc0WFRmSjJzUjhublgrQTJYU3l5UTNrZEtsRFJocEhEVkdxR1NHdGF1Mm9vTwpSdG4rVWtQWHRGSmQ3a2xNYzNkUk9tLzNIMjhGZ3cweDN6RTVGYlVMZU53NldvYlJqcHJVQTBycC9neC8vODNXCmdLNVFINi9FemNjQmVuUHhuWE1KckRlOWxsdW9Hd1craWZkVGE2NFMzUmNkRVFrcWRROENwMkU5Ymx0bEpoTzgKQ2Q0Y1Vsdk9xTUd1Q0tiR3NCSGhNdTRNaitwM2hOa1p4SndlOTVTWURrbkFRcmc1WHc3TE13SlFqVmpsRHFJQgpueFpBQXZQWFNhK2tVVElqcFJlWVEvai9JRWxDWHhSS3kvLzdGYWh6cmdQbVNVdXQwa3J5ek4xdVVnS05kWEpKClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVphOWsvc1haTjRNaWVNWWF3ZEgKWVNLcUdtcE04Y3BBTWRJeFdGNXdZMnBXY3Fud2F6NDRiZ0pXUnNqNkF5NXltTWdSM2VzTURLMXBqL0p2M3ExQQpjci92cnFLeVhqcUE5b0ZjY3JuYUJKbVNpaENPN0cyVWpQaS9wTFZGeC85R0U4bXNCd01PcUQxYktCRkJidk9JCks0L1ZBZi9JcytCWmd0MWRBUEdENURkR2tnWXF5UzY0WEFpV1R5RE1PUlEzSGpzR21PYVVwYnJ2NE5yUjNKQlgKTndKc3dkTlJhYTgzVGFDMitDQkFiaEplbWJqMlFYY0J6elRmTkRTaFRpWEorR0N5Q2lRT3JqOXdYcXpYb1pnUQo2V0t3OGdYTGR2dXBQU0hBV3FQdkkvc2VUR1ptU3Q5dFB2aDJ1K1RZOWRVUnNqU0QxYS9aYk05TXZnczNhSDRyCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBL2JvR2tvNUhxT3RlNFpnVGQrd2UKT3pOV0J4aFRhZld4bWZlUmlEekFmeVdPUHBpU0g1cER2OU4xbTYyRTdLTFkxMVBlTXNJRllEVjE0NmcrdW1OdAp4YjNBNk1HTGZvQWV0SXQ4clFteGhuWDZaLzlaK0RLMUJsalF1VWlia3ZnTlBMWWl5dG92bkt5QzZDcnAvaW1PCkdJQnVDOFBMMlZVdVhXTVhpUThsWFdWeWE3L1RlR3ZsbFVKVUJQd1Q5eWN0N20vMlg5SG1EMVh6OGg3VDVKRWMKQ2FxUHA5ZlpXWHF3OUFlbDZNem1EODZHdXVoeVRKdHNxSGVqM3FGOFQ3YkxZOGV3WUJaZzZZOEQ5ZVl5OUN3dgpQMXdnZ1NuUDBmNXUxSHNZb1pUMHNwVXcwVUVjc255SnNDUVVKRG1QMUpRc2o5dzFHeXZmVjdWNTVrWW9neDMyCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0RQOUMwUTR0VGVlemFnSG5PaXYKUDlxZDR1a082V2Z1Y3ZUcmZKaXhPQzZ3NEoycCswdkgwZ01OVHUycnZob1IzUE1GTmdNekRRNWxiYU9kQzJtUgpmMjNvdEtUS2dWLys4d3I0WlJQdnVNczllWGRLT0hUcThnalVMekI0WkxKL0RCTGM5NlNjd2k4WU9vWHBRR0dnCmhTcnVzU01keFIzcjd1U0FNUWc3Q0Vkdm4vVEZPZTNBY2xjMFJBbHlReXU3cTY2MUY4TStKVWZzam1RYkJja0gKUVN3Q2ovRnQ2eDBRaXVzaGU1ZTMzWmRmT2tJVGM4T0wwSitCK0RZQitleTJ4bG5kVG43aitINVJuWUJjR1ZrTQpkbnNiN0FSdlphNFdkSlpDRlhYcW16VE9tczR2akltVW0wcmY2dHRMT1B4KzVjSEg5VjdVbTQ0aHV6YlNvbTFqCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTB0bk5ObnphcktCS09oSURRMUMKa3hMcTZQem00QmFPY3daWGtaYXNPQUtidFpCTGNYbklJbStvVE5oY2Y1ckthTTIwQkNrSzBMNWl0Vko4MmQ1ZApKQi93bUJHRnd3OTdMRHcvNzcwVTlqSyt1eWUxQThmU3NRY3VNbWo3WlJzZ1RUT05EcGE1amtwMnJySEd3bnJICmVUdTg4TWgzYUFleXRQMDVvYklBeXFSNjkzRTFtSHhLV213dHV5Y0lIN09zTm9jSGJJNGZlUDY1SkUzWmFQN2gKMjdUYkFLcVhxL0w5THBWZCtBc0J2STdVbkpqV05tdHhuMWFKcy9WOUtZMVhnTkRqWE9HeGt0MXRmZFR2QUtXdQpOWDV4S3F1aGRMOGN4cTB2ekhqZmRPcVZ2eTAyRTRTVk9nRit1YXZpQVFVL003bTk1WVJUUk40Yi9VNFY2Q1kyClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFRkdGRuTkUyZUxMb1kyWXhwUGUKT0x4NWNXTTFNK3VhY283cFI0aXZQUUJpbDFMK2F3SER4NnFkN1l1WWluZWowTnZFS2tVSW5IT0thaW9iOTQrbQpaaS9NTU9wbFh1Qk43UUxRL0ZlN3NTUDZEU2I3WFhsdEhmMVlkdUxXb1NFNmlCUm55VGF5SE92RDBhOUttRUQvCmhoSjlJK1NabnlzQkJUa2hBUWRLelk1YzZRdmo2SE1iNGxPU0hIS3Y5STlwN0VveEIrYndJUDBQTUZGMnZuTVQKSkVoTXh2bHZ4UU12c01qOEhrZlRrdXFvRjlVZ1hUbG9jakJ4ZEpvaEU5eGNlTTlhbHU5TTFLbWJjU1VYZlVPYgptQTNtREtPUEtKTnNlYUMrMnl5WUphY3k3VkxzRHJQL0VrMCsxWmNPQ0R2djlZNGw2aXY0RGxwNWZYeTRrcytNCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTMvNTk4M3BNRnlkOVJSN1I4SVAKc0FZM2F4RDRhVXlCTkFUK3lLSFdoTzZqbU8wYlVyaWV2YlQ4NG1zYU1XZVpZT1NRay9OaE1uKys3dUFUY1dzWQpTZU1Wd05IKzQ5NWF5MzhjUnFHbE8zWVk2OG5VUGx2YndaSWVaUkdMZkRFWDBEeFYvR2pkbS9mWW45NDVrMlA5CkRoVTArT3NJTHI3S2lIb2t2Sm5GdTNyOUhFRWxQRU5EUHlJS3NzVmNUa3BwcjQ1SXcwa0REbC9UYkZ0b3ZaUUgKakMwakNjWkNxQTZHTGVsc24xWC8wMnpWMHZKMjBkWlVXdUk5THA4MmN3ZW9CcTNtbHBIUUFWbGxVTHpIVjMzUAoxRHJFNkwxelVaZnd0L1FTZEFFdHlPT25GeHdaenRWbjcvUlIwSDRFM0h5bTNwRzNoS1ZxSkU4WklXazhJaHhBCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0x5Nzh1L0tXNlRHeTQ5RmRuTm4Kdk5ycW40M2NySDZCUWorazkrWi8xL3Vjd01GSzZ3L2trcHMybnRVUFYwK2o5Tlgya0MyMnR0Um5XTWVORldRQgppcC85cjM4WFNTS0tJTlpxZS9nTkg0YlRSd1N5cEc4RDArTFM0a3F2NTdjZXN6R1F3NUt2OEZ5c0FKVlhISWtCClJGT2xyWGhGOE5Qb1JoL3FQOWFSTHhvcFpSdXFZaDc1UEE1Qi8wWFdSYVRrcjFiTXA1MEQ4TjBlVHpzVDZoSHEKUUNKVXpPeWpwVHFTVVJqakZ0YSt0NVk0Rk95S1F5L3d4aVpBVDR0YnppWHFXSE96VzR5VWNrYU9JbUJHZWtXMAp0SmRUMXFLV3F0cTJBRElvVVlWelVnVG02MFdEc0Z5NnRFczlxNmo0Y2ZGTWZvSlBRMU91bFpQUXpnU3pTSU1lCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHJVdHFQYWR4OUlyaDBCT3ZBbWcKbkNLTFVGOFQ5NGJYcXpDTnNWdXJLUlNiV2haTm5GZmVka3dRSlA1Y3o4V1lLUVZPc2dNVlVkZHhlQXhWMWpIVwpzMjNxaHlHS2tvQytKMVlVK1IyU2VRNVQ4Q1dxWWlXT2pJRXY1TmVWTlZtWEdrMXN5ajhZRnV4d2xGY1JPTDRpCmlydklsME9kZEw4cVo2VkxtU3hXaFVnbUFaOVRKSnh0UXRJRG1Xb1ZLZlhsV1hpcnZVUGhUVmM1QWEyWWJhOC8KNzRlajdrYU5kOU5yM0RwLzhsekVVcGRQejkrcWNCVWdubnR6OFY4KzVuRmFGQVNGZDNzWTJWWlBkN3R5UGU1RwpHQW9ybERsd1lwTEJ1Y1Z1R28wU3U2MTlYcCtqT0dyUUVXaHZvRWF4RmZEMkNlQ3NSYWdXNjI5ckJ2RUoySWZWCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVpSLzNLaWJEb2taQjZ1TXRhSHUKbGFDOG5mdklSdytUeW4vU1YrbWdBWlFMV2NxamhPUTlCczNGV3JCTS9hejRpdVNGU3BHOVVnMGhmbzZBSkVxRApobWl1eURvenU4Z3BBeThYVFJHQWp6RCs1bGdhTTdnanNZN1hUSmwzblVjVWl1ZlRXdDRSNHI2ZDhEZHgzWHFnCk5mVmp3V2czZFlnKzd1TXVPMm5uT1E3SVlyWVg2bGlXbHhtckk3S094WTl5b2Q5SWRPcU1BNjJmMGVEYlU5NEUKZTUvdCs5Z1FhS1d3UjdvTTZXOXFseTIwY3FLd3RvRzdsYmdGdi9HaXRkUnA1aUhWMlJGbUwxUWd4TUpUY3FkRgpERjJmTEI4NFhCRHVUMTJjb2RYQ1Q1Nm5JL1JRTVIvL1Jqd25ORWxxWnJrUERDT0NaWGhYSTRuR0NHQkVqdnlRCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWJWVS95RXpabmRJbkp6OUpqVFkKdlZHVGhWcDV3TFhvOVJWOHN0bitoaGF1YUdpaXdaeFpFQ3p5cmx4RzlKRDgxKytVMnhkYmQ4cXhGSEJOZzcwbQp4OGxVTGtTTkZCZ3J1bWJ4c2VJMjROTTU2bTV5V2N1cHFkWlNjZXRzNm1LUkFDZUI5R3pJTFEySnBoam9kSElPCnJTVE1UVTUyUkkyTitETVJJSXAwamNvUVdYaFhhZTc3ZTBrMXZ3L3dLKytYNStZRHZFSXRwNyt0TEcxazBZdWUKMlRKQTY2TE1aTXBVUDdPemVaY3cxdDJQRG5uSWhld0lxTHVUb2Vqb2dlR1Q3bUk2VXhxbVVQbHVXTUp5WTM1RAozcm1UN09HN3RNdkdleHJZRjNlSGtOU05KMG9haStqdzlOTVpmUGdDWXRqQ0N5WkdXaElNQWNnK2kxT3E3czVxClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdm1LamlUYlZsUGRaSWNiTkxWYSsKQkFhcFEvbURHY2tLQXI0MVVRZkZwL2tqRzBPV3JyRmJRNmxnQ0dJN0hLUXk5Qml0MnRNK3luNElRM1hwcnRkVApvRjg4UnJEOCsyL0xRR1dYYithNmROYkI2dHVrNGtQRFlWSTRscHpyUGZKczJqQTVxeEdiUm15cU9IVGJLZEZSCjdjYXpxNHNWVm5peUY3TmpmV3hqYzJ6L1RtN1VVZXhxMm83Y1RyMll5bVpldTJWRzVaUk1XQ2hVeUdVY01BbDMKaklhWnRxL2RYZ2FUcXBNRXh1RHVSQmxDWDV5TGVldTlIREpPaHhzRGxwb1JJZE50RTdHbWZCL2VXQmgwQ04wSgo0Mm1sUTk4bXZvQWd5T2ZtUVFuYm95NkozUUVEWGpCLzRVQS9FMEF5dHJYRWdpU3ZyTmJnaU5OZXEwRWUrWEU2Cnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGxubkNFelNmTkpObHVCdE5tWFQKUDU3MDB4U1RMTHJQRTZkcHpZZzhKTnA0NERLeHdjaTZBVnlPNW5LU3hUZlVwci95dnRMYmFHMmxrRkcrMnhPcApWUTN4cmszUEhEU0tUSllYdzFNcGdZWlcwSG9ZUkxiT0JqYmpKT1FwVC9RZEdVZ1dBTjA1a0pQdzU1ZW5WRHNIClVnUDhBVWtNc05mYzZJbFdvUmQ4aW1McWxMYTJmVTZuR3VpcHY0NnppMnNocEZIM1VJTUVzQU50b0psZ3B4eXEKUmRnbFJDaTA2TWVneTJUaWgxeFRPMDgrWnZ3RHkrWmZmeHlXT0NqTFMwT1FWOWJXYS9CaVc0M3gyckhaeFl0TApqazJNV1V5b2tHb3MwVFd5OWY2aEYvc0ZTWE54SGFOOHBtNnFyc1V6eDhjck9RMHJPY25DWHNSYVdFNnQzREdjCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBei9nZ1M2R3FyWU9HSTljTlVuRWYKeklBWUhIakhSMDVrQUtXdnFxZnR4RFBBamlpOUZ6ZytTR2hqYUFtcXFNcHdmY0RNdmRUdXNsTzVlaEM3a1h0YQpYekEwOENNVkovaDRiZlJ6MFp5MGJyN2xJK3h2WktPbkJYbFJlb0U2OXdZWTQ4VmhTM2pzSVEySnd0VUtpTmdPCi9Ya3MwVUJVQmRpVTBocENtUFlwODE3M05VUTVNS01md2pIdFUxdXBFKzFKZFRrV3RlNHZySU4ya3NEM0FlUnMKODFhbnNoRmNZOUhPM3kyZkV5VU1ZR1E4OTVoa1VnKzRGSzFYT202NjM4VmlBV3p2UXhSZlN0SXliY3lPRWNPRwp2cDJIUmNFZzVTNEphNXdQaG1qWWI0dWpTUTdJa0pBQW5Ga0phR3hndjNxYzRxT2ZDbWE5aThQNHB2a2QwZXkvCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkhKR25GVXNqNy9lWGtEbkluYSsKYVBXSVh3VGNYczRZbnppZkRQaW9sV1gwK05RbkltN1ViMzB4V0lWZXVxNGVudUNqcitFbnNyK0h5MTF0ck1GbgpTRC9VODJPVUxCekhRaWZIcW5vQ3VuaUw1QmNNZWtvVU9vUm5BWU4xME04QUVpcUlaVlRkQXc0L09OTnEzRlNvCm5HU3h1M0dGY3FXVGx2NElRc1ZRbHpZZXhhOUQydGJSUjBybXVrVDVCZjBocW9pcTFDcUo0ejRlNkhuUzB1YmUKNmU5eHE0MDkyU0lBUm9BYkpjNmN6MFBkeUhDQjBrN05vSnFYdGtNQlgzTlhjejVFK2lhWjJTdUtGN05GenNxZwpOSnFWWUozaXFKTythcGJzNkxvTDBsdUFnT0RUdWd0TFJUeGlTZVNLQlloNzlPUlQzWjhVYmtLZ2Urb1Q3VVJpCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM251NnZFOG0za3FxMHoyVEM3WVcKZ2EwQWlNYlpNdm1tRVVVRDBrR1NPR0JrcDNrcmx0Ulg3WFdtR3cwejFkMUtEZzMvanZSaTA5dHREMlg2cURVaApERGI4V0tRdmxkb2JDbTY4cis4VDdzbHYzWmRnbS9mRGY4NGY1TU1Zd2phUEFxZUt5bENMbmpTd05NL00zTUMxCm1TKzZKRms1M0U4MnJJSGFqWG5ERGorSFBVd3VZUUxmSUdXTHIrTkNBK0tvNCswb0RKTjRONU83enpPSGxuVmIKK3JmT0hRbUdrQXk0RkxWK2ROdXlyUjFBdm5SczhrWllEZElMSVNUb1hlY3ZNVCt0cUlRd0ttQVFkRWNKcXpPYQpiamFMNFFncnM3MWdJOU5XOEt4YklVdUc1aUMwTFRxZUR6bitCYlZjdzBXdVd4WldKVjB3RWZNMlVGRkFKeU9hCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenRWL0gwejVTMjV1MGsvTk44elYKTWc1UVp2cFJPMkZiMzZ2bFZkUFp6Q01ZUHFDU1V5bXdYUU1sT3dXRzduVmJ1TnkrbTVrYXJYSmMzKy9JeHFldAp5bkpPQ1dGeE04dkFhd0dMLzVPb2lQNC9xWDQrRkJRMkdWNzU5Rm5HN0g3WjU1b2dhc3gvZ2FiQW5kM1lCalprCjFkVVZlN2dMVWxrODN1dlV2Ukd6cWQrTkt4b1FiTEpCVHRtcXJRUFFtQ2diVGIzRzlYanBmbmk3L2orQS9ZdzAKN1VVMlVPK3l5YUNlREZxMjVoeFY3TnNvK0hzek1PSXU4Z015WTU0Ni9rM1dSRGZzMjB4SFZqd2k3YXY2UTNHQQpyU1g3Mm9UNUxzYTFHY3lJRlYrRzRLSkkzWlpIUXVnTDl6YTY3MXlQYkN0MlFHbmo1LzV4Y0g5ZjdNTHhXRjFkCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmk1WDdOYkVXRFYrSzdNY3l0YWoKWjFzbnVzUE9RQVppTkFrQmlzN1dHckZYMExibVdLc3F2RmRFU0MweTZRcDJGZjZtNVZXVmM5bmRNK0FzYjhscwpqZGxTdmlRTUJNTmlGcnh0UVRWU01DRTBuSGt1QXFOV0QwY2dtc2NVU0EvSnRGSzM5UzRTdytkajBONlhsUnFWCkg4VUJmQVpwZ0xNTTZmZ2Nsb0JOcERhZC9lazJ0N0pWS2tKUzl5MmQrR3J0ZFRVVkVWN09vOFdvQTRsK2xFamQKVWxTY01FbDd3ZXU2bEdhdUJlREZ2TG5uZjdYOTAwazIzUU50bGt6czd2SmFTY0lleVczeXdnMHNlTGp5cVk3RwpQQmd2U3RvMnRQL25ITWw3eHQxV1NnV09jZHNtSVo4T2Q0Z3FueGpOdldGdVZwWWNhS05Tc0srR2xGWU5Ha2RYCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVhuZVk2L3V1aTNQcmQrakx6Z2sKeGsrdEFMODBTeERjRWtEc2R2ZGo1NlpBbXprMFA1K2V4Zm5xajl1dXdxb3BYcEVVa2pCMUs3ek55eFZ3ZHV1cQpVR3VoeTBLRm5tdldCWnJIeGhrMUJITkpvYXNJTVJ1Z3lhMUVRakFqNUtrUjBXZ3U1Z3gvWXg1SGVwN3NtUlFQClZPTGhsamMwS1VEV0lPdjRGUWo2RzUzdXhWZWxuSEQ5b2RGbmV0MUNnYVZWZ2xOTzRHSEw4VGhGM3FzRXhscWIKNXkwTEdWb3JSY3pvRFgxUEhqWnZQNlVqRkIxSE5idXFzN1JOZFp0V3ZqSFMyZGdhSzVBZngxSVAzSkxsZytSUQp1K0pMWUJZQ20raDdQcmtNelBoQTNGSkVvYlAwcGZzRWNQQ3Bxd1BoclRxUkdKZXQrZmJYSTh5NjZsNlhYR1NWClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0kxc1hsQnIyaXhiUXZhZk40UVcKM0xhaGVvQ3lyVkoxSTV5bHRQN3NSVlhzeE54NHhDSHV6aWRwcTRVaVdKVTQ0Tml2TGhVSlRzaGpPNGpqcVdDZQpzNlI0UUJ3cnB4V1lVK2N2OEVWdUxqZnJDaHhOSXhPdUIzOFZBU2pyZWgzNk1uSEhnT1dKR0pGbGttVHBIT0pCCkcrZUw5a3pZaDkwWTBnZ1orUlhaZ2o2NnppR2hlLy9JdE5vOFE5Qys4TFBqK1ZxNGxTeEFKYmlidnM3N0V4My8KcGYzTVNKQmNlTnEvamFxUXgrTGx2QzB5YXlHNTJDYzBaekdCTkJJNGZKQWNmRzdRazRzZXZpTHhvck41eUFSWgpqTGRPTkJHV1Nhb0dQOVhHQ2VCZmprSER3eGpLdnNmTDlMMjlFQk1iRG9EZFpUamV2VjhCSXI0ZkVCK2VXTVMwCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDNzdldOeGFteXpOdlU1S1NQVk8Kc2N1RGRjQVUzZ1MvdGNRYlZzUDFjT2c2RGtoWXB6akZ1RENmT1Z5RmhKNUJQY3BVdndiZ1A1ZW1IdCtXSGVBYQo1WEJjamI2NUZEMkxXMjBGTmhTUU1jVHNHZXdUTzBVbmU2RithZS9rUHZMd0cyQW9OMUE0RmloRHY1NG91V0g2CnhzV2h4MDFXdzFqcGRLNjJoK1lGS3NhVnlLVXRnN1cvUzlJWlFFanBIMlBxV0NXamxZMGJaNmpaVEp4L0ZxVUQKRnNSLy9mMW1kRzVFTEJtYTNHMkFBcFhKQzBZV0p5bFlzS0NIMDNtbWkrNE1ESHdtbDFhT3ZLYjFUWFJ0emJNTwpPM0JNL1YwWlNDL00wYzVCWWRFeXdNaGZCaHpwaWVsSWROcWJDSzdzZTduRDlFRE5MMytWY0pSV3BubUFaY2dyCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3NjcjZLSzRmVlRmclN5ak1qYmYKREVFbTA3ZU9jeWRUT3NXRm42YmFZR3MrUXN4Y3BJRnZzVXhvV2hsdjVaUHk0Yyt5VEpOemZ5d3JqWDYyV3ZhbApKczdXcUlhWTIyVzN6SlVYdjVJQ1dYWWF4bFR3cUN5dmRXV2RBUXAzcjFWRkl1Y05lc2Vrdkx4NDJHY0s3Q3FvCnFjYU1jQ2xNK1cxK1U3cDZTekxvZkQ0eW56K0pITWpveWJtY0EvMGw2MHAyLy8yK0JVcHJRZzNtdUdNMXM3YnAKU3QxNEdpQzA3QkhyS1VNQkdPWEwvZEZXUVQrbTJYREwxbGp2QWZvck9RazRZT1B6cUp4Kzc1UERyTmNQZlNNRApSUlVLZ1VsMnFMQmoxd2FkYzgwZE9nWVZ5TGFVRXpUcDNZQ01tZFlDZzM5SWwrVVZhSi9RM2ZIcHZHY21MWHpxCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWh1NmVEMmxkUXJNVEx4b2diMUYKNDNpVVpjQThaTjc4YUFmVm1BMDcwNlZEOVcrTWVWNm9UOUZXV1NtT09naitFUzI4K2ZnL1FrK0lMSjZFZzdnWgp1S0JFUVNWQStRcUExYmo5c0FkcHNLU0RLeXVpUlBkTEwydTFkU0U1S3VEeFpXMDJXbVVWNHpGWllTRFZYaFNlClVWS2VjbXFkVElLZzNmZStVblo4elp5dnliam9zT3dPTDlaZDRzd0xGVkc0OUU0TFZiQTJsVmdLRVVQMnV3cWoKM1N3MmpoaTdQaEN5enB4ekVnTVp5a1N0d2daWm1BNGZlcHE2dVRmdVZpMktON0oxeEQ5a3JnZEZmREIrekdwMApOc0hqank5cjJibmxsRjhmanJ2d2Y0eDhkQWZQOGJrUlVGMUxOMWRBbmozUG9UZHRDZUhoblU3bkVrM2IrU0ZXCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzVsaEZXNkVjL0RFWlVPc2plZVUKT2xQeXlZY1FveDQzUURKSjY2RHMrWVlBMThWMDRGd1VBU0NHV1lPYkIzOUVVcnVXOEpoVEdxMXkzQ2sxU2FtTQpNejYvNFViejh6L2NmekxPRUpMVkhWUXJ1Z1JFZGhlNnVvcEkxY1h3cFkzMVlBYWlhYUlNTDFGU3FXTldzb0hJClY1blFpb1pQK1FBeVpYM1BIMzZuRHN0U3BqZWJIYm5wK2U2eXFWZ3pNN2lMVkJUMHkwTVlIYmhMWGFjNTFMdkEKcUJEZHNVZVFzSVlqOVJGQTBPZThBMEpmM1FyS0VqVDVRMkY1b3R6ZllaOGs1TG1TRUxxNC9JVXJ4QnM1dS9DNQplTWp6aUd4OS9FU3UxSUlnVnFHY3dWRXFKcEwrOFF5U0RBZzVXR0NEOU14OXZSSkpXalppQ1FvTGRuenJXQ0pmCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUdjNmxtamVmR1NUeUNWK3c5K1QKMjFqRWJpQWsrQWprSC9qMVVUSXZQL09CSmZlUVQ1Y2h2dnBMWGk1M1VYMkNVeWQ1Uk5UdDNDcWZYNlJDdzRNYQpMUW0xaXM2VU9CRENtL3l1d0JHcytGUGEwdTBHQTdId1puak0rcURtZGwrL25zeDM1a0RVSFhYaEh2akRIdVFDCnBtRGJ2bVgyTzZBcmpYUmlqNjQ0Q3N3VG5nNmxSWUh0WEhobE9uUUZYNUVpRmJWUGYvbWdRVjN0dERKOW9pZG4KOWhzRzNyRXBiTWw3N0g0MFFWVlIwdzcwZG5MUERySGdjN3A1dEU3OGVtc3lyaHZKYkRTNnEzVjdVVUZQVFBZVwo4eTJvQTdEUEpOU3MrRUtjL0RIUmxOZ0hYZUZVMDVKcUlqRmhOYnFlcVV0VlBabWRoaTJGTUsyNWFyVCs0YTVrCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDdmQ0s5MGdubFJhWUNpU2t5Q2kKbHVVTDdyVG0wUzBvV2FQQXg0azNVU2QzY0FTNFJoQnlDZWtVRmJreFEyQXdPNVZiMFk3b1AxTWZoR3l2ejJycQp6TTViY0o0MFF4VzVsVG5GdUJucEs5T2svYjhybkRiK25wZWs2RThseHhCZEk1Q0dUZXU0elVrL0tMYTVOcFMzCnNDTjBBSFRVZmFuQXpIU01UampiM24zeG9RVjlqYnBrakNUcjVwWitPcHcxTm5NZHBkK1oxbEUxME5xSis1bFYKLzJQTVZlUzBxK2JrWjVuaHI1VlRxN0lMOEhudXFzcnp2ZXgweXBFWEV3bW9nYU1CSXg4K2wvT3lQRDFkWFRpVQpzT21ZQVUrdG05NUtrajQ3Rmh1ZTBtRWsxbmxxS1p2ek8zOURYaHVaWnhmYkhlRmZOY2ExSVVYeEJwZTBsOElQClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEZGV0E1VGRVdkt3WXpMWXgxL1MKSndJa2hwZ09Wckl6NTBhajRwaDVoeFd1WDBZaytqb1NoY212MGFsWWRHKzVkdnhmZC9hMDVVK2JtSXUvemdyZwovZC9SSnA5Q3g1aHNBUldJVTBVSzZNcmVoaEN3MmlGME9CWTl0bkRmQkpJaXF4R3RRdko1YTRFY2N2UFNDQk15CjdGM293NE1OcmUvbUkxdXVuZ0lpVjhKRWRjbVg2R0E2UlNqeVhTYmliRHdKMGtSL2hicTNaNy9Fc2NvdWRnOWYKK0ZmN0M1aWx2dDBONlVGYzlGelAyTVpXdGNpcEd5YVcvUWgwOVM5QkVaUnB3M3psQkhrUG56dnZPVW9VOXBWNApKUmZuYVgyek1tSTlSbk1HZnR1ZGJjYmdrQkgxRWVwSmV4bDF3MStKMjdEQncvbXRRS25qQUt2STdPazRJLzNmCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkRERmk3NVZUOUo5NHNmTkgrMmEKbktqVGNxcEx1N0g4dUVWeVZHSkRzSWcxbVhRMTltdDFudmFQdzdwN2J6QzZ2b2VGdy9OZzlabTFVZUc1dmdYNApHemRZQmY2TTM5alk0RlhPOXFITXMxaVZZR2ErVXQ1S0s3WTF2bmQ2dE1vMnhPSGE5SW96TVRud2tTbkJRSmkzClBZdFMxOThSaFE0VVhrcFBaZlJuaHI3aGhoNjgrSW9UNW9kbU9sSjN5eUFvem5tWmtaZnAvN3E2ajB2TjY2N2MKRW5qd2dCc0MwQjhlYW81NHhKV0JqUlVkcFNac3h5emdxdGprcUNCUGtFL2Jid2J6LzR0T0oydTNHOHpITzE0cQpuTjR2VUh3YXp5eklzWUFMS3lMOHp6RFB0RkY3REpjeUVOempMek9sZEtmcWZuNnBwcFkzVXY0QTFiU3hOSEF3CkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEFyZ3dPcEFvRjFla09FYU5VVTkKL3lJZ3E0OVNVaktkNkUyRHIvajFtcXlVS2l3RUxiMnIrVUgrL2ZQYUdTUGpJK1YwUTVxNC9GaFRPbjk1Y1FrWgpaSzhOOW13TXR4SCs0NTRpYzRZbnNORmZXMndiRHZUZ0ZFSUxaTnFtK09WR1hXWXlib2kxWEhMNHh6bWRuR0pDCkxma2RpZSswZ2hHS1VsT3UvaXNpNVBmWitmSXdQYU51SkNHYWZLVHBWclVYT1hwQnpCWTlCY0NxQ3Q1MUNEZmQKU0FWbU5DNjBNODJ1WUJ4TXBEVVAzalA5WXZlREpjbWU2cktvZUhhRVp3eTRxZ1gxbXQ5cGJ2WXhPYmpGVnFUNQpOdGRuaUdJTXVYcnZvZm5OU3J3SmFvOGxGOFdUYXNjL0hwMjMwc29FWTVMbVZHUmJqYkFObkx5ZUwxQ3owMEx5CnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMU5DTHBxUEVTWitNOWZybFZIVG0KeGdlb056L1lEaGtjWjBOVFpHOEZhaGV1c1d6aFV4YzdPTHNsVlJEY01mQmRWNWJsSlNuSGRtT2gxampyeDhOaQpXUld6OXlPbTNzd1BoS05BMnF6RmVJeENKUW05dDU2R3lzdDF2RnJxb2d2Sy9RN3MzWXRLcVV4Y3phM0RSN3NUCk13cEJQV1prNUpXdGpNazFxVno3aFBTVXFyazhFRE9nVTQ5YmUxTkI5OVN6YnhyOFVWaVhuK3hZdmJFZzI1Z2wKSSt1VzVNa0p4MjRYR1hmdlFnQ2ltYzAyekVsc1gvanpoOGNseGp1WCtURCtFSTBsK3pCOEhKT2J3S05ERTJIWApmQkVMLzF5bzRuckVWUnpqcTN5K05QM21YWnkwcER5S0YyYmNLMVlPSTArMEtZRCs5Q2JMeitYM2lKdllFY1dyCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDNCdmpZbXZSYjdvUEFQTGhrWHkKN3IwWFdPdmVwT2JYREwwalVtWlhWNmNzWUE0NzIrdi9ZWXZMSXhGM1ZQUWFrUTk4U3ZiSjQ1a0g0WmtCZXZVVgpjODlLTG1YSDlnVy9HdkVwQWttRUdXdXQxTXhvYmdqb3pOUHI0UjFyZGtHbC9UbXhhWllHVFVWUE9USmtaOGVECnVNR2dBbnFzMkFtditobGhUV21JZVNHMWIzalBjZHd1UTlUSWxpUVN0VjRqN0ordmRwME1BV1IrbitwVm15RWEKaHlRem1uWFJubVJZZ29wNlUxYW1vUmp2YTVvMW4ycmlNWGwzdmZXQ1lyV3JFUmYrZEc5bTFjeDJjT2FzK2dMSwpFQU8ybGFHYWlDa0NLRXc2THZ3bDdneGw1OHhRL0xGQzJOWGxpVjk4SktCcFJzdlkwdnpPNUdLRTRYY2RFTlQzCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE9XdUxmeXZjQnR3Zmc5NFVBQSsKRUwyako4VUZCU0FmcHJxb01LTklWM2NGbXYxK3pucVdVak5uQ0daOTltVkZPcW0rbkQ1emxOcmJMRWkzU0poNgo5bmNWUjZHN0tTQ0NwUHV5WER4dWpEKytlZFQwU09zblNOM1p2bnBOUURKdGloYmVHVitqRERXQ3hCdDZFVm1LCkR5TGlHNHJXNGFGVFBQVWhMRUx5OTNGdHFDRG1ML2tZVnhsRUltaVgzM09PZjB0ZERabmVHMzVEOWxseDZrWEgKeGNWUnh0Z1EydCtDeXdRN2tWbDhJc0N4V2kvbWhXUFlBNnpNL2NMOGZNbFlMQ1dJa2Nkckl2KzRvMUlUYlFibQp5c0RCWGZYRjI3aTBkVHJrRDRIY3VFbG9uRW9TOGhnYktHaHZDOEE4MXZna1RkL2ZvdHo2SmJYdW9TTmlmbmRxClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDBMcDZOUUFCdnArMThibVlUSHkKbXppSGRldlFaNHZMRUtKME9UUWFjdHFyNy9QVzZYSDQ1ekxaQlIrbFZYZyswWGdtWnlKcXp0Sjg4ME42Tmt3RwpHaitSVVNyc2tZUDVOS3laaG44bXphd1pUUjNrdkxjck9RY3p6TXliaVZVdlFFYlQrdVROOE1DbS9ZVDdqSzkwCnliY2JwSnV1ekpxazdsa1BIaHJYZkw1a05tQmxMS2ZJSEovc3M3b05DMkhhb2ViQnIzbU4rMkV5YWp1dGIzZS8KaTFYa0gzRzFySDhDVkliZXFMTDZlQzhjMTZvSDlqNjFtbk42K2M2ZXdpemlLQmlJNXV5WEVTSUJZYU9mU3RMUgpCdlVxZkEvRFZZWWhzNUM4alJackVnVGt3K0ZaK2h3UU9wVFM4NzkyTkNaZ2FBbHhwckt2Mm5uNzNpRXdqQXZqCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcndyeUZ1TnFyUlYzbHpTWHlSWS8KMVh6TUthSkNIUXpsMExYSzZ2RXU2UU9UUGY3NlhLQ2lWU0RMSE1aRzZLN3ZqYTZFN1dFaEJtNnVnS0J5TDNLTwpGNFg5cjVrQktpSVpDZDh2aW0wNXpQZmR6UlpNb2U0akRlTll4R3hiYnN2SUlmdkxXT0JxMno4TGtKOXhsQm1mClBRZ2ZWUWdScnlNemo3bm1UUGExb3hsSUVzaEdXWVhsUXd4WXd4Ty9SdkpmaGRBYlc5d3RVSmpOY250c1J1SVAKdnpXSTZnVUw0dE9KbFEwSEE2SGRMNytPYUJxTWpPMEVQV0JabFgxUHJPU1cwdXhiN3FkNmtVb1BxeENFVXNMSwpObHQrWUYxeUY4d0xJekJtRFBFK3VPcE04MWczaXRTRnVEeU1XRHMxbnhsdTdwTUhEWTJxRExXQTJ3MCtYYnN5Ck53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3hMRnpPL3l6dGtORDlPMGY4TnAKNDV1czZTek1adDFXeFNGd3NOT0VuMjluZWVFMHJkbHZvaGV6V090N3VuSEhHRWVneUhBUTVqTGJrdXVjSDBLMwo1ekdnYUxXczZHWWpySlI3ZEt4cTZIdnNmejJoVzhQYjFEOUF6cTZRUlhmcVR4a2ZIV2lNYUJaUUNjYjI4eVBFCmFjMjhvVVFVTXFNVjRNNE5ZZ3hGS1VNeE1TRHlBelN5bzlzOGdYdWFOVThibnlQcnEwcTlrNEk2UXhpK3VZNEMKb3F2RU04VGhsVVZRZTNoZEtkVjRQMi9nbWluSXFEYm4vMjhwc2JwaHhtMTR2NTRGTTBvSXhZdTQ1dmVkVTZDdgpoME5IaG1QWUZWZHhwNCtHT0pEVlBUS0xnRzZxSFh0UW9HWDlIYmptaEFtYm85L1JobUF1L1piNmpyWDJTUVZUCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGh0Z01ZcVdvUVg4TTE4YnlaSHgKTzRKWHdvMXByUEJyR2pRN2twcnZRRDV3MUUyV2p2a2pzWkgvTkprWTBKKzJMZ3BYL1l4TWNaa2NDbWxic2xhUApQZGpnRmF3NTlJM1dPVUpzbWVuSkVOaUExaHZ2VnhKaUFMcXMxTkdZYkZYRFBHaE96UzFoWU9hbk96RzFmNmZJCnRBWUQya1pMRmUxR2xOckxjVUxuUXh4cjliTzNrbHdoaHZQK3dSUzVEY1IrY05ZWEg3bUlGc0hkVGpLdnc0bmsKWVhJeEI5K0gwRUtEOS84M1FiTXlwT0ZuQmpZQnV1dHREcHhaNGpJM01nQ1laNCt2T0pXNGhwS2N2b2wzcjRFVwo2dFpTZnNjNndJRnhIbWlkeWVZckdjaHN3V2t6R3M2aW40dDN0KzVla1Q2Yy94MkFjOU1iRG8zRmpVYTJYNkZ3CkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemxwNHdoRE9zckU3Skhpa0lBT20KdUw5cGxTREROZnR5ZmczVjZLYXFMaGxTVHJrcjYyVkhXajJaMlNiQW9MaUl4NmFtU0o0Zm40RXNaS2ZlVkNPSgpxN2hUOE1xTWdxM0F6eU9JalhmWS9WS0ZTbkNYMFpyYjRGZTVJQUNyMnlnUlp6UzgyYU9QYXV0NHhMZ2dKZFVCCjloSmtOSWZyZ1BKSXF4RVNGSjc5LzlxSXY5TGc4YVQ5WjB1eTFsZ2ZPeFFMUmU5cUVpV1Axdmp6dVgzWFVvYVoKK2s1aHdHalg4cHhxWVRkdXduNENuMmRScTgxQlFWUkF1dE5ROXBLeFFCd0VWc2g3cGQ5eDBlWmFHZlI2K0xsaApMSDV2d2VzQ05LQ2lWeTM5MDdkdHUxdFFqd2VjNjFCTnZBdTcrTk85Rml5Szl6c3UzU2xaQ3ZLYlJGc1l3WERiCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFE4Z1BvcDFsUmVsN0gzWVpoVHQKcnRJeVBmVjhiTmJxZ1ZCNWQrWk1qa2h1M01zeDVYN1pydVY0aEcwd0RtY3BsMWJxcE03dXcvaDdoRXY2MlhCVApNRWw0b1R1eU5DVkJKQ0lubUxQYWZuWGZOR3luM3dDa0pVYWJheUVBcFRrT1lXc2YyMm1Fd3BBazFrTk5SQTlFCkZLQlpER3lraFpQNEc5V2dtMyswQ0Z6dU5DemQwdlFudmt3czlOZzNHLy8yY1lhR3oyVXJadm5oZGdUQXJVVmcKK3lhVk1abllQdEp6YndqellZK0QrUTlySkxXMVNpczFWQU9pWTVYL2RuVldsT1lmbUVmc2RuVFVSaWl5eGtFVwp2Mk5MVHpPMklydE04cXI5T3EyUjVUVjlEaUxVY0IrWUo1T2FmdmxFV1pzZXhweUZ2cFUyNHVnUGpLejIwK1llCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmxvK1NvQ0lJem5kL0s0aUFTeVQKbjh5Y1B4R3drTG9rdklUSER0RTFHQXF0L0lUZkwyaUNWc0lsYWY1ZDZpOXFRQ0RUWFlTUUtXeFh4Qk40bE5hbwo5ZlJtMS9ZSmFVVG01S1Y5OWRCbng4aE9KMVF6S0hoc1RuY1JuR29KS0hZcFNObkxiUllaSUJlUWhpdWhCYUNtClp3N3JQaStjM0VJTk1JOFlra04rcmdhUTdwZXovQ05hOUNFUEszOFdrMHF1WXU1T3NmNlJ2Wmd5Vjg5dThkZHIKeThKSWpiWUI2Sk1FN3gzSDJnaU5GdzNhYlcrV0dHMDhYek56SnNma1MzVGhIYWVIdVZjYzZuKzFkdlRDWkd1RwpUMnhnVVkzRFZDL2FvQXU5Q2hMMy9TZ3YwWHJ1Q09Fa1BJRHl3WnFVeEk4OG50ZUVrUUFvN0NYTERUVnFNVWNrCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlFHTW52cmdoQ0lPMXpxc0x6cGIKRlNxQUdZMEZOdUNlOE1SaHFpMWczOS9UdDQyeE8xaUZQUnQ5SWlmY0k0TkptdWxVWCttVDdtUzFISXVEbHk1OApwY2h2c0ViQTRlVG5yZStzUEsxREU4NklRUWIxV1V2SEdvVXNsUHRJa1Q3dlM2SlZ2MWhmaXZPbWJ2U3l5UzBVCk03UmxmNFlheEprMGx3NGRueVNiUzR2VGkyMHc3TkJ4ZDlUMWNhd3hheC9zNUxUSW1GUDdwUUxvenFNR1A4ZUUKTzdGeFNXdVhldWorbVFub3hiZEtYU0hrT3A1MzBCNG1qRFpuSENZeDFxMVU1K0FOcHV3S05VQlpGOG80Sjc3YwpOUWR2Q3c2VHBFY1hTTlRMY1ZDcGlQNDkvd0JpamVUYkROaU1WVHFTM2ZWL3ZKTDAvcXl4UytUWjljZmRDUnlaCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVpzcC93SVQzYmZRbnE5akN0YWwKTUdJb0hnRmZ3Y0Y4VmhVVGlCbjVTOTN4bWZUVE1Jem1xM3dPeGcwcGlRbmNlTEtsajdFdWp5TWVNZEZseDBzRAoweDRuQlRWZFNaSCtyRkVhYTVPbzIwSDE1SUhVQWI3ZFlJMFZna0tGQWdudTF3eTM1cUR0UjIzUUhSb3MrOXp1CnJlak5TMXYyRVZJQU9ucmJSWlgycjJjMUdDaHBuazlKN0Q0MW4vcUZWZFd1OVNEbTFCVWxJNEUzUmZqN0xpL3YKYU4rVlpwWTAyTG5oNFdHTVIxdnpEZi9pbHlrSFMxc1RmZ2JkN2hYQVQxSWdvbmZmOFZkNFFySHBWZXVhUHlwYgpzczJDUUJld3RDZm1Gb3N2djlTTHJtc0xhNmIyTTA2NG1jSktvSjZnRUQ2aDg2MitiSlVDMTFIcitaRldVZkhkCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMitPN0J6THBpVHFMdlJBd0dQbDkKdFptVzYrbFpoOEZSQm8vaVZEQXBMVWZmcFd6Ui9hOUZRT1hENmpQcnZja2szT3VhUGJ1bGJ3TkVpMDFlTW5mSgpEK0FlL3d5L012T2FzaW8vMUJ1VTZ4MVUyUEdKZTZyU1hZUXUxbjhmOCs4T2JkdWFzbFlIN2FJUGZRZFBKRk9mCnFicnY5NzBrVVNpbEhneUIweXlzL01ibkM3MlN3YmVJbXprQy9IRDdhMWR5Q1lJNnU2dDdUSEFITittZ0dpc00KV2NlcnJ1d3E3NWVBNktqemF1Q3ZFcDRCTjNUMXA2NDMxU01hcmxqSWVwTFE2TmMzY1c4UVBYSCt1VVQwVlpXbgo3Q2dpZi9hRlMyQmJ6TUxCVi9FdnUyNkRPVk1ET2crcXRQWUdnWVZ6VXJXamp5M0ZwRTRJbEx4SG42bjVmUUcvCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMU9JaUZCNGZvQU5ISTRhRnFBRmUKQmdUSGFjRmIrWlJrVFg4elNrV040WWJ3N2pPbEgrbkE5VFc3OUYzNFdLMWRxaHZYNVMzVC83b1VzRFZMeU9zRQpVN1hSRXc1bmpzN0tuRHF5UWhDMFl0cTJvc2dwZi9zSHd1Q3k3T3BoRlFxd3Mwa1o4a3pXMTBneUZtaUh0RkZxCnRkaml6dGg4MENaWDNrY3RzT2NpV2Qyd1Z6MTBDSEFjTmYvSExOaUlTZUNHV1RVeW5PaVRzbm80Y2w4M3NsbncKRDFqL2Q2UmRxa2tweGcrVWpZcW9neVQ4WGhpS3RjdDZCd0p0eS9BbXlKTzFtbDFWNU5DMWN4cVBtSWY4RGFJRgpwdG81RVZiNEp5NjJFMnZOZlVuWkZyNHkxVTZQUERldTk2d0hzZmoydXdzL002UXhOM2g1TkJudnJKUDlURVhnClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1ZzRmhBZTdmdVIvL0tmcElrWXoKMENLdDdIWjY2QjR5bzAyWHdvYm1CU212cW1OaHd2U2FRNGlVL0ZsM2g4Q25saStSTXFWZVhWTHR1KzI2NUNkZAp2bXN0MnN5bEV0NWREdlpJMHZtNE15ZjFJdDNGTk9PVWZwQjFTVFhOZnRrUEswWGc3aFJIOXRnTkV0b1JTSVdnCmxXZ3ZMdE1pbFZjcWI0bjliUE5QSXI2MzdGU1l0R2djSVNqZ2lYcmpPWVdCTFd3eEczN3ZHNnJPTjVSZkRCYzAKcjZidTZBSkdSSTRwYVUxUlFDb2ZQQUt0bjRhdXhaRXplVWFudlpjcWF4Y3lwNDF2VEhwejdualJ4cGVLVi9tRwpYSFNuOVdDbFIzTytoTUVQK2hoaXNuZXN5WUJ1YXhYc0dpK0FreXA0dHlIaUU3MTZhVVozVGw2azBPVG9LcG53CkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbm0yL1g5MC9hUEQ1VTBCT1ZTengKN0RPZXJkaks5OWhpa2pSS0FXMXY2MUFERFhZSmZ5ZzJrMllxbFUrcEZrZjlWWVU4SGx6cU92YUJROS9ld2lnKwpFeHYxV2RINHN1YnplZVgxZk1xV1o1NkhySlo2cks0TkVJKzhad0VyV2hCeG44NWpGS1kzVy9zVitCMWQ1alluCkhpc0laQzR4SGVaYkRrTnUyUkI2ZXkrRVpEcm1xRXVPV28ycXdJK1JrNGc3ZkFJYnZoVFdscHFpWWtZWjl0dlAKVkRRMUhrZnZLSVZnSldqRXZvdDVaenVPRCtETE8yVUVweUlJbWdURFJsMk9DRUhYV2Q0OG9RQ3JiQXhmeVJUcgpmMG42T2xDeU5RUGpQTk0weHRwOWE3U2ZkZHFZRi81SVhySWRJNU1WbVg0SDNaUHdRKzZVMU5tQ0hPaDlnSnN4CmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjRiZkdILzZ1aUVEZUhhZUZuT3MKTzZhb09iT3JuNmpzbm02dUFMbHQ2bmR6b0h1Q0wzTXlDNEkxa0xMSHdjUC9CclBvMkRNMzlzcDlNZXJ3V3VaMwp4Qnp3bmFjYnRhYWE2d0VQdXhUOWlmRzFaQ25yblFLZHYxY2ZqdFZsc0VlMHhFYWtVU00yU1JwNXk1eGUzc0tMCmhFaWR4cFZHZU9XeXZ6TDlIaGpEVHJiYTd5L1NBWVE1cDNuTGxvT0VMVnlHQ3FHYjdpK2FHZEtDZnFUbFVHS3oKVzJXRTVPMGFPNWQ0UUZkQ3pZRDVRSytNaElRaGZ3dThUMXhOYlc5UmVPWU05cVZxRnJCYlQzbm1HM0pwLzJzTAo4cjlNUXVTQURXdmFYYWlvR0RDalh4TXl1Ty9LdXY2SFhjMWxNK0s1eW1WNGRkOUFPN1BtaGljK1RRSmhBbDVICjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2NwMitMQVdhMTZCMkJvRXJjSksKN0JWNmQyek01eFJuekRFOGZRTE1saFpCQTVDNnpEbUJ6ZGIvQmU3WG0xajBrdG84T1ZoVDcyV014RSsrbUduMQo2NUFwNFRWQmhpL081VThCa1RhZVNKTGh3SmRXZGl4NEEvZGoyZU1DSngydk5Id1NrdlBOZzhQOVY0SjAwWmRyCml0KzlnNGFxSGZzNDRCL25xVkxFVk9TelFxQkFyK0tkRHB1Q2ZleHQ5eGZSZXk4WGFaRGdFb0l2TStIZFlKNEcKZUVWSXZKS0VOcDU2VnJiQVBkdmZoNTM5ZStOanpjM0dkSEhSeS82N0tYRnhiM0oyZjVlYmxXY3VPdjNtSFJxSQpxVHBiN1F0SmhtTU1TSVFUTE9xSnZmbEpSSmxvNXBUTms1YUJjNmxuTWNoYUpmZVhDcjlVQ3MxSFlqZTRhY2EzCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUNJK2I1bzdoaGNqeEJ1eHlienQKUTlFT3VXdHhwenNrcEU1WVltL2syQ3VyejMreis4em1QeU9BZzJPN0QwVnBNT3o2OWtNbXVKVUFKYlpzUEE2eApaMlpjMEFPUERrVkNVdWtOVEFuRDdQOUZ3QlZYMU9LZWtodEZDTTFVWHR6WmpVa1RFb0xIZmI4SG1UbjlDcldVCm1BVWs3QmdSMEhvUldLZ09VM1hKZGRBZUN0YlVzZ3l4M1czZFhxUmxUSU5kQldUWUNzTWZoNExpMDlUaXFjcEEKVXo0clJOWkhEZklmVWNWZFZLK1lGK283M1ZLNlRFMHl0eStqUWx6MWpqbGtkRSs5NWhhNnN2eVVFWmc2RVZXcwo1b2ZZR2lKR3ZkbVNYVzFyQ2EyMEU1WG5wWHhkWFlaRXJROWJYMmtId2RydWtURzRTVXpJTTFsTDdYR05JSkdnCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVpCeER5UGRaY1RBb0F2TXh4cXkKQzAwc0xGNVlCWnJCTFlZQ2hldks3T3lvQjQxNTB1RXFaYmRnc3lLUVBRaU1JaUlXRWdrbWk2Y0t6U0hCT1JYVApjVW1BeGNPRm01b3FKaEo2akZwZkU5b2ZTdUdscmE5R1NMbTVYTStUak51cWY2YTNJTGwrcCtGWnNWV2NsWTZ3Cjl2b3owVnJiSFhUNkhrNmJ6ZG5QYm5rQzIyOHRhTmRJaURtNWs2bThoemoyOWpFNU9wWGZ6S09CeWRibXVnS2UKRGROa3FaNHI0U2tDMHNISW16eDRPakZYeHlBMUg1bU1Zc29Fa2tVRFEwNHo1QWsyUDlEQWUveDg0cml1STVYbgplREhLaDZVMk1Vd1YzejVNZnNXY3pOY1ljUm10SW1RVitudTBvSHc2WWh3LzIwSGJNNmpkRkUvM3U4aXQzQXpVCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzcrQWdqeTloNEhqWEU1bGJsdWoKTFNQbzBFUjRBV0RNdUg0QzZDRWdaOFRtdXJjaHQ2SkhMSFhtcERZaGs4RUo3QUFJaGVQUzZQL0NQTjZDRE12MgpVbzFjbnNQdHlKcGlsYkUrT3F2ajhxYmQzV2xYVHhGak8rMkc0MTJJL05KQ09aZzlLUktnblViSmhDWG5RVjI1Cnl0eDh6Zm52UHlJZ1RMTmc2V3pXVEh5UGVyL1YvTllaT1gyZTVhWmxPY0crbEhMcnV6OWdNSlNqWDlaNU1WRGQKVlJ3SHFFMzg4VjZUMlZGOVNtTWgwaTloSlFXdy96aXZyV0pWV3BzV3pQUjFvUzAwRWlXYVRRemRwTkl4ZnU3Mwp6NWVRNm5sNC8xUHVrTWtkbUJ4ZmhrdDRWL2R3Rm8rVjBLZ2IxRHVsMG5YaitEaGl5YmltZ1R2UXI5aTRBTmJnCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnA4OERrRWFUdGE2N2lhTGdBUGQKVXVLY0V5K0xqMkE2WjBOWlozTXloU3Q1YXBxeW1VNWdEbWpOcEhwTEtUZ1ZYVTBDWG82clNvYlcvKzFMMm4xSgpROG95OFptZm4zU3JoSGwzVDRsbjdqOFNOUzJkZXBaL1pWUjkwdFJ1SDFPcFNqTWxUaC9UTXhsKy94ckN2QXh2Ck00dzJCdmdla1BnQ3ViOGNvbEl6QkhyYUlLVW1JZC8xcHJtVjZ2aDJIZGV2N0gyZlJpU0daMDhvZURCeHZQZXkKNURlN1VhSmViVThoQnpMa0NpOUpRU2djcG9IeW1yb2xmYW02SGZCWEdFNkhUN1EyUDduUmVQV2RXUU5aMmFrdgp1bE9zYXV0YnFBTit0VUxpRnBrUGx5WjBPck9obnpkbnRHanpkeGRHQTF5ODRTT0FhZEJ3Q0JVQjMzeXp3bjhKCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUVJcmR3ajExNmdmK3NoSmpwV2MKRGoveXUzQUhIb1h4SDh2RjM5a3B0YURRV0JnT01mVWdNNEtuaVlOQnlhb2RoK2FCOWFUYTl6dm5IQjQyVG9uQgo3OFR3YUUySU1lQ0ZGMjVLek56d0p1cmZjKzJ3akVTVXZBVEFITW9OOFpyelNDSHg0SGg0ZTZLSlM5djBwd05ZCng1VTBXdGRzekRlMFB3M3lKUWFOdVFUMEFHZmlLd3kwbyt1SWRGTmVOajlBTnZtUEJ4cXNuSTRhc1ZObEovNnIKcTRaVy82T0V1YW1ONkZTc3ZENE9abzFqYnhqSGJnOWVmeG1Yd2FNYmdaNXBhNlhhaEpZZlhnUExSaDVEcTBlTgpFYVhmL1E5ZTZtd2hlMmkrQjJ6WHlGRmtSYjB5MWhYZTNJbjBaZmNXbzJtYTNDaVp0YkNaNzJvU2hWZlRwakZTCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXVrU0U3cTBFNjFodGdFZ29vY3gKMlBHY0RMNEdOWW53U3RadVVvT0tqSkM3amVoZTgrMm5KRDdpK1U5YkYyU2ZGNmgyOTJlU2kwd3p2Y2lITVQ3dQpwcTd2eGFVOEpabXMwMFE4V3pIT2RYUzcwbHFDcGxHVW9WdkdCSWxza1pwajJsY0tnQksxY1FybDhFYUtoSEQ5CkNrOHlGb2Q3MW1DSTNEMEdBYUo3VlNSbzdOUXhlWW8zaFpiM2RhWTVoaWZ6QnU2ZndTWFZoK1lzRVpMUFk4OVMKZlFudWk3blI2dUNyOWU5WVUxU1YycHZmR0Q2Vm5EM2w4ZjRRZ1k5Ym1RRzk3M2hoczcvRHcwNFVremtIcVYwYgpkTXNSZEpJaktpdkZjWnNabFZRNDBWOW5FUWhsaFNId2s1S1BzWlUzdkxMc0UzaW5ISzEreHNqNGJ2TnhzQ0xKCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGZzcGtncUxGNHZoV1FybXdRblEKM0JxUExCUXFUWkx3aXZOc09zVXBERXJmOEZpZVg0VXlKUWxIR1EvZ1BkK0liTTlmQ2ZtQ3l1ZmhvT0ZVQjNxRQpqK09IbnpLanFWYU8vVmZjbFlUOGp4cXlEeU1jQUxCOEdSZ3BBK1pLUDRIQjVxWkFVbGZ3OEVBNUJESTFlcmpZCjdEMmNSVjV4ZHA2c1pvTTRHWHo1R1MrbmluWDBXQUpNTTFXYm5URThRNE5teWtHOEpDMm16UVJmYUlDNUdyTW4KYmlNSXZweVZGUkFZVjloc2NvOWE5c0lRenZyQjRoRGJydmZGOGVzK1ppbExKMk0rVHRXWHZLbmRudlVyc0JWVwpuK2UwaEIzejNxNncrWDdQMHdYeFFHRUllQ2hOVVlNSmJBUG16QmFVOFRKUUlYUFdpUGU2NXNaajllTFFOejMrCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFdhMVNUdFBIeXpUZ2d3aTIyRUIKSzVudzRlaEFkTnVLY2t1NGlsN0wvdm5PQkE0dXlQOWJ0Qm5YM2dBbjVLVzRkZzl2ZzNkOWtIRi9QcjVSNHpDTQorcmI3cXRQWmlGTmMrelRKSktVSVkzbS8ydVVkOHNEWWVvT0ZvUXFJZ25DRmVYUElCQzVRRkcwbUZET2ZrVjdOClErQzltMllyZ1QzRUdTU01rQmMvSWVPVk1JNllTclI1bEtUS1Q4N05oZ1JzVEtlRVI5bGNEemVtWWNPcFdYa1gKNGdBQUVGWjRnTWl6aktYckdjSW1lQTErMzVGOStNRURGL21DaU94SFFBZTZUZFM4MHluNmNkTXRSWjlKUDBtYgo3RmtyR3JmNmhMakhDenNJWGE0TkpJcHNNL2xNeGMwRDd1ODNOdVlEdFZpcHgrMkZYUTdBb25pQzVRcmd4M0pPCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTl6cGJyY3VNWmxPbUcwN2kzVnoKWjJhSmNxbnp4UTB4Q1ZkNmcxRW9GOVhnSWlEWVREOHZLZ1UyZDhCamJnNXZCanNOSVNPSlpqbWVWdGhYZ2ozNwpzL1gwU25TdzJqNFVrakhFdElhMVZTaGVTNk1SRVV0UDEyaC9DNTU5Z09rWGJYUzVsYmJvV3dza1M1U295L09sCkp0ZS9Ic0xXbE95UmNLTlo1MGVKM3JPcDlBUFVCd1pJUjZFMy80aUxWRzlZMDYrbGR3dHdaNzAvZzRFamtHQkQKalRIYlNMY2J5d09vMjZITzN2UURkR0pGTmkzUTVxT3pDR3cweGY5RCtLclNxNnNEUWtWRnBGSlFEVGlybkxiSwp0M2k2bVdsZnZHdHd0NTNHbkx4dTR5aFN0NUhreVF5bWVNU3hqemZEL3JVSVhOa0lDRUJZOTZpSStlNWlLMUJCCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmdqV2UzWHQzdGtUV2lCMzNzWGIKMXhPTUdVSHF4cXBHUVV3NFNUaGpOaGR1SFhpcnUwbU8xWklLY2pDeWdIb2gxN1ZYRm96dUxVTENXU2Q2OWU5YQpRVkpteEJhV1FQcUUxSlBxWmhteElLZitzOC8wZHhoYWkwS2dhMFNtbFZLYmRCZ1BHMkdSSXJhZnRWOS9nY0ZPCmZwRENCZzlWbDJ2bDZKZVoyMEJqVEFXR1F2Y3FsZ1IwclZTRVRTdmVzUzhTWXVyYmdIOERhVUp6blNOTThvUHkKNHptQ1lhWmRXa2Y2MVpiR0NWTERzdjNsMVBCU0NwOGpxOSsxTTg5dUtvZDV4QlpwZDI2QTlMZXJTUVZvVWYwTQpkS3VOY1I3T2FMOTZKSjVOS3JBSXVCaWlQVDBkc1JFUkJWUkVReXA3amRKbTJQWVJheSttS3kzakZkbmNmbGg4CjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcU5nL254RjRjVm1JNlNTSXZqWWYKQys3bnJvaVo0Q3Nnc0xURit3cnhGYXdXbnF0bG5Rd1pSRmRtY3FkalNKRkJpekcrZkFtZ0dKUStJa3hVT3pqVgpzR2NSRCtGZmd1VmZUNGZNdmVETnRtczNMSitoK0VuM2FLRUhmNWdPVWZQaTdFNTRXNmFhUm8yRmxzSlZIWlhmCnBxcUlGR21xT0tiM2pOTWkwaGZ3K3VjQmN0d1J4TU5UTmR1Z1J6ME5aQ3hlNnFpSUJGOGNaYWd3Q094Y1UvdDEKZ3RwS3hpaDVyV1JSd3BaKzRiM0FiNzRMaTFmdGE3S2UyTVlra240RHk2WU1yTmFMT2t5d2lkYTIrTVJ3czFnQwpmRUYwdjdKcGE0d1FBUTZOVnp2emZ0NURJZHY0bEl4Q0ZZM016ZXVEZ1RCVDdRUzkrTWJuVWJNdFZMU0JhTDRLCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2lPRE9zazdXZEpVbEIrcG1TM2UKVjBGQWFHMFhyQnNiZ1YveGFGYTkxL0RaeUdYWHRweEY2N1ZPbnRvWHM1Sy9ibXQyTXptcjhENTV4NEJ6YVIxdQpsREFvU2ZSOUcrR2M2TURZKzVCWHlsemg3Q0pWYU40RHM1bDJQdUtpdnFvMy9HOEdmZFF4NW8xQ0lKRTU0ODlVCkpuQ0FCbVNKV0s2T200cDkwMWR1a0xzaUJiMHcwWklhZ2phTFg3VktsbC9uUlYxWlFyUGZBVlJUWWgyQ2FXUDAKbnlSNWFUYURyZ3dhQ0RuaTFNekZ1SmRIQkE3ejgwN2QvWmxEQmxFemgxTGdTM2hlMkRqNnoxU0kwNm9TUzZkNApuQmM5OFFBQ0dPS1BMTFB2RjNUUFJLdmU1aU81dk5HTUNSV2JrTXlLeWVDYkI3dUlIQXBRUEVHVHVXR2QrOUNFCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEZHK3ZDeW4waFcwN29ONzROUSsKb2FBdzhDQ1U2WC9RUGYwNUNHeWlpbVQ3OEJpQk9ndWVDdmt6d3plNUlEeVhUT3RCN1ZNQ2psMUhxNkZibWFCQwpVbHF2Wkk0Z0tqa1owT1VpQTdMQk5TaTBvS0xuckpDVFBGaHd3K3U1RUIwWndrK1dPbldpdVJSSHJxaW5aSnE2CjByVGtSV2x3VklhWWx3amFTTXJla3g0Mk04cHN4R253a2xFRXN4YWdyR091NnpzOWJ2am1TaDdTUmxVWkxEZUkKRVJRbkptTTZmWHZVL0FveDdJUDk2aW8rWGc5NG1GV2tjZ0FiamdBakg0bUJkaWF2NzI3UllOWlNHNXlHK2IvNQpZYkdmTFBRYjBZZkFsQWY1ZC8xNWNjRHNxZ0kzQjJYWU4wZC9UYVk1dnpqVE5PSUhLb0JwMkdNV0tJblNNOEx4CkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmZlR1YxODlOWTdsQmpHT1VTajQKNWR2SnQ3Y1pNaG5ESXRqSmUrSlB3YkhPdEU0anoyZWNjeHVvQVF5MVp4YmpLeXJ3MFEybkVJaWYxWkxacEcyVgprVU1QRS9Fb0N1ZUNHL3lONTRYaGNva3Bwc3hXTEJ6Wk1ESWNqZlR4eVpLdVZVVGc1MkdqR3JqMEFkY29iUkFMCmdzbEFNZkl5LzdibG0xd2xoOWJ0b2lseURnNHI4QThZeUVaa2ZaRXAySTJraTRPR3ZRMGdLRm81SUNvNmFaS2IKRE9QVG40aFNkYnA0emRmVmV6dHliRjVQUHZvVHVsemtETHA2bFRjNHNDY1JtWE5PMm8wV1NTMjhzK2E1dGhIQwpYNVUrSTFYS2loaUl0T1E4MDhVU3QvMkV4Rm1MckdEaVluaFh0WGFjeDlVWllIaGF2d1Q5cEgrNC9SUW5lcXE0Cmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDJDenM1bkp1Q1J6VXlqTkZ5REUKQnNobVdTbWtZTFl5clBZTEJ1Q1dhcVUxS0ZSRWNBd2VrcnJ1MTdrd1IrSjVTTVY5VlZqV2U3bG9pSjQ0bkJRQgpDNWJBUkRaOFRsSnl1ZVFuc1pZMzM4SWZvRDJXQkVQSFdQV2pHZ0ZGa2xPNFNjUThkTDhaN0o0VDBjSjZ1MFZDClc3Sm1MZjNUQzN4U04yZElDWmU4Y1BodEc1a2RLWllJd0ZwMlpzL1JKNi9HVjJZZVJINEkrcXMydy9PWUlGSkkKTjNRVm9tWStwa0xjL2hqYUxSQjJjY0NjaGxobktHTHFUL0o5QWpqeWh3NzBKYWYwcU9qMTdKWkZ2TVVWTTYrTApPeHlPa2VQYjIwOStMeW84eXd1SmFIN3dFY1FJTzdBQlQxYVgzYStocDIvck1nQjA4eWh6OU4yM1ZlZC9xZklOCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3pBM3YrTkVoR0h6dVFKc3AyY2QKRElIZUxQSHRxK1ZsYUVMeGpkUmltanJQR1gySzdzeWhINlQ3cXp0TXhpNG9zbjJLUm9lOVIwZUNxMVZCaFJPMApYOFZLaFRTVjYxNGlpVDBFTUdxK0xPYnVBeVhBWnA5WjllOEw2S2FFaEd6Y0JIY0s3MVFIRW9RRmVLa3lTMHQ4CktpdDFGZDBmWWU2Qk9PVVgvYUtiSG1zQXVWSWlQYXhidUMzM0NtMnB1Z2o1TWNrR2FGdGU2VkJSWjNNSFVkMzQKSHQ1dWpvSzBpKzlaQlU4ZHpsbXd6SXgyQkJ4UGhRZjdLVkUyTkdKcklGMk56dWN0T2h6NTMzUGYzNWFCWHVnawovMG1BSTN4by80U0tvSnVsMTJKWmNzaUJnS2ZlUXN1TDRTZDlLY1hXU3QyS3ZGU2RkZWxjcDF1d3Y0QXFKUm5ICmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeExMdEg2RTkzN2ZlUElKQzZaR2MKdXlINWFVWHcxTU9zeCtsRCtiaWhSbnZ3ZTNxU2VtS2ZuZUxQWUtVRkxVSHRqRllUSUR0RTBCY3p2ZG5CR1dJTgppSzFKV3loWWwxd1ZkK1pUYnZ1OGlzKzhFc1UvY0gwWlRGVzRrYzUvNWV3TU5zYmpmdElTaWU1UEdZbStKQVhiCmI2b2VURU9xTUxydlo0RzhrM1FvRWhLa3hJc05FdVEzeUR2SWcvTmNsaHJ2YTM3VzdQK3AxVFBTbkV6VFdyelAKbmR5OE9ONWlYbXpURHI5NnhkdXQ0K1J1NkFRejZ1OFk2VWQ4M3VCbVl3eXk2R2hpd29WdmRsU3FZU0NPalJFUApaMGMyL0NUd0xDb2ZiODdsTGY3eVk2UExtMFNKZE5TVDZRMHEycWVUbkh3bzlENHIwR3ZZUG5VVnRKeHhRVmJrCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUxQYW5zb0EwdnNEV2FnQWE5SkIKM2swSXF5cWNJc3NSSHNFV2tIN2VueGxXWXR4VHViRTFoRVVQNFpvMllTSGtUWERZN2EwMVVjNXA5aHlFNWxFRQpBUjlLcmV0c2hPSDlISEN2aHRVbHN4TytIZEwxV0prcENGcUVrRnBsSklNUzR5c1JyS2JBVitDTk5xL3FsMWFlClhOTUh4TlNvT0xXd1pQN1U1MVZCOWg2THBIVXZURFM1MjQ5TWFXa1pYTjZMSURGTGRyZUdkdVFBaHN1eVNxS04KZFljNUxDZVRTS1RNVzcvaUNkdWlUU2VBOHRGVldnQjNPQ1VYenAvcm5ZZy90RlFNdU5SZWs1Q0h6WnhoY0R1SQpJc3g3cyswMVo2RVhGdWRUT1dEbXdKVlNOUC85NnhuL3QxV1Nyb2Z6VDVrZ080NTdxSnJyZjFDRCsyeGo1RWlPCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbHZsOXpQUVRMWFdrT0tJcGt2MHcKbmM2TVZ3QjJkSnNjdW5VUXNkM2RYS3FyQU5lVWprTXM3cjFJeTVWdVhoYUNNZ2Rvc1pUZ28rSG1jOTdMT2NQNgpwVTl6Z3V3T01sYkcrVEhwSzN3emJMbXdOM2xSTkVTdit4R2liVUU5V1dlUlZXanRIWm94dmNsRUJKbmJmVS9CCnl5V05sVHJsU05nS3RScGxFcFVBWTNHb3lweFJnT21lbmk1UzhZVzREZmRXQVNFV002ZDZDelBFbkEremlIdnUKb1Z5b08xcFVncitFcHdneG0vbkFIQ1BQR3hyY0MxRFFIV2tmSVBpVUdOekJad2RCWlJZUUVLR2RrRlZ0M1pjawpHMFpMVDdlUWhwb0JxTlZWV08zMXk5QXZaN29FUlhDUmY4R0ZvQ0dyMnFlcGZ5Zy8xblFwZkt2T2pBODQ3SnM3CjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWdXYWFoMkFMY09sb3llWnUwU1UKZUxscFhQdTh5SE4zWXR6S1NpMEs2b094N2w2clZGbHE2ZUxvYTJaSmxNRkJNYWR1VHhnWnlhS0hiK09FTGpTQQphcmJtUFNiNFNkd1ZiYlBjUkJjRzZvM2R6cm9TMEpjSVRJVC9tVWtCNzhYdk1NNUZhZnprQW81OEQzUjlEbGg1CnhlaldUTE5vaTR1Q1ByMG1nek5aTDJOc2U1US9FNEY1VUh0Ly8vQzJmSkF1RHNObUFXYTlaUjQrS2tMVkhUdTMKVmRPVHNNckRyOEZEY1cyVndyM1JBMGZER0NaQVYzb1F1YVp0cSsrR1cwL1gxNmxneDhsUnFHUExhSVBMcThpSApMdnNSbzIyRlc0dUN0QjZhVjRVYWJtUnlKbmg5dVFBVWxXZGNNeDY5UERsYnUrTEV0THFTdTIyNy9PN1FFZGZZCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemhEVEtHRzZwSGtYVk5VREltL0gKMkxNZ3BjWnpMOTFmekdicnFQa2Z1Tnppa2J1S2NKbHlrSlg5WTJwSlFMTzBxT1d3ZXZqazJHQmZxWmNycmphZApuS2o1SUhEM045MlBCaGJZcVFRdTNUQ2IyYmZ5cXpMNjVJeWhaS2hSOFN6QlF1cVBCZVlCaW9VVFNHUFpLUWcvCkNPRTZTb1E1bCtyNENTcis0VDY5SG53a2NRY0kzYUdMN0QvOXlrQTkwWFU1WmRjZWRSNktuNlNCSTQyL2F1WmoKS3hxcU1oNy80dkxiVUsxTVhyTjZjL2ZWRGNsZ1YwTHVpbkQ1a1JreUpDa3VESTk0dU1TWnNUMXN5M2hpUklOdApITjRxc3pPdVdHRXJvcDdwcTZBUE5aUExrcCt6Ukp6VklGQmdpYU9tR2l5enVZbHZpMDJGdExDbFBNRE16VDhXCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3p0SEpvQ3RqTFIzcmszd253V1IKV0xmS3FHVmVLTEZJMEpjMFFCNFRQMjlUUXhyUEJKMUxrTUdYYWlvUWh6YkNwNW8wL0NCY2JaTThka1RMUitGMwpJdkJMcmFhM21DY2lkWWlWK0xYZ2ptTWErWmxGa21DWXBTa0F6Z3VtYzB3M0pNN3JEUTBrZFNPeVpZSHkvR2xsCmFNVVRoQU9ISWRZYzczWjB5VU01T0E0QzZSV0NyVGljbzFnSkp0WWE5dTVNNU54eUZVWkltWlcyeHdycWNEMTAKZEQ5bXVvbUc5Nmp1TVNMMFNBY3RCc3dySjNGMHowM2FTUnV4Z1dMRUlIYjRpaWVzeDFEMHRsUyt4eGtyMVAvcwpFeW55MThkTjVDZnAxUWdBN3JmOTd1MERITSsrNG9kQlJBQU9hblRUYmZIY3o5Y3k4eXpQTVpKbkkzYVcvZ2h5CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXIzQURKRFhTVldNL3hWUlpMc3IKOWc2cGVBbTc3VjF3OWY3OVpvcFN1NEYrVHJJVTNINlZpc3NoMitNTWd2ZVdDSWtycTVNelZ1Wk1HVHRGbVlyTApGdi9rZWVpeDdzZ2FZRFhvVDdzZG1MSG5WMkQ3REN1Rkxuczg2NnYrajhzbWpuK0FyaUxmcnZZVlY2SjcvN0E4CitEVmh4V1F0VnhFQ2FWS0xvTkdaY2ZORW80VVV5MEhxalBuZmVSd0ZSTUthS0R4Si8wYjBTdUE5NENPK1VHMHkKVWNjV2Uwa0ovQjZPT2ovS1BaVWE1bnhwL2JTVUhxWWRZelo1UHJmMWFvUHhjTldUNU1ZYURJcWh4NzZQRWdyRwpHbFBjdFZuZ1F6S1h3TnJJUzZFQ2JqV0kxOS8vWEtObk1SOUl0WXMxeTlJOVF6UVhzN1VtTlgzYUxNeEhkTFdrCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHNDc1JINVdSRzVHMFl1OERoNS8KdzFhYkg3R1dkSzlnb0ptbllVeE9wbENiS053M1F1NEprL3I2SjAxR0JseXc3eDZMNnR3S1VEaGZWRjQwK1ZHagpmcTY1ZEpORHBEVEtRaHltTGhuV3ZEbG5GNGNvbXFtbE5KcWpHdEowQWxIbVVtQnJYaXhNSDZOcFJSczdMa1BTCkcxM1lnUkREaitpZG1rOUphWndsYkp0dGI4bVZVcUNiUUo0VllYVW9LVmdBejhsNlFpTnVjSVFhL3R1SldlYkUKbzIreS9QT2dMMWUxNkZzUWtjU3FCV0NSdnVmTk5tK0x3QUJzeFBYR3Q4ZHlzNDBQMHFYdVBkcXd6Z1h0UWxqWgpWUEY4TDVDeUg5dEVJTGFXRUhzQk01OVVTditEeFdib1NGSjlWK3dOZXNiWjBZQXhEd01RVGY4M0R3c0EwUkxGCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOHBaY0hNNGZuNURKOW5icEx2WlEKWjUyWEdoUkR0MU5neDJRMVB3ZDkvNlQzc0RKMHFYcFlCRHRkSmlqZERieGhwTHJQQVFackcvbWdTbWRWbDc5egp1bitrQkdqVzB1ak84dnloOGk2eC9HU1RRT0R2TWk1U3ZtcUh1WUhGYW9uK1hZMDZiSHZpaWg5RkNmZGhnbEM2ClAzTHdjaWR5MEU2RHRONStCdEdtWjVVMXJHZkpLNFRFTGRGYUtyM3pKc2RJa2dlT2lvM0hFeisyKzYwWWZSOEEKUkhYSGFvRThWcDc4TUdXRlJISDFXTGI3VElaeG92eXhGdkVYU1Q5Wm9wbERmRGlVQ3lhUitFcGdkd2JYcDI3cQpjR3ZsbUNRV0gzSlBlWWt5UWhUdjdXOWhLOHFkU0dDL1FXL3JMSkk1cFg4WDd6R21OQWhrWTlrcE9jRGYxTDRDCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEIyMVQxZ0lwSEwzTUhnYzJYSzAKLzhydncrakI2YTJORGJYMnNUc2g4VTdoZXcrZU1Ncm1KQndybDIzOUpiUVNXSlZ1SmI1UjdEYi9qSG0wZFlrUQpyS2hWR0N3MlF6RFNRUlcrcjBvNXpGVkNCNVM4enRqdUdyWWtSaTBQQnovUTJIb2NyclA5TlNsUjVROHdLQnVyCnJKSlN3aWN2Q2gvS2ZEUEdGbnV1U25oaXF0MzdVMDVLWkRCQms4UkhybTVEQThSWlBPSVBjcUwvdTJ5YWFlRzkKUUpuTE1VM3ZZTDBQNDBKbUZwaE4rL0lROXlPQlFNMHZlbTRLZFJVVllUYm9sUjlRa2RTQ0ZFVXNiSHR4MTVENgpiWmdmM0Z3WllBNWExaXFTR2V0dzhkbmVmRytmRzFERTdKaWdnblRJN3BraEIwVGdUUmttWWJsazJ6UVR2R21PClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzJRcURQT2hYbHJ4bWRkQTNHM3YKdUJCZ0VwMXIvUnEvVTBSOWh5LzcyQzJocUhJZExxNDQzTk1jdWozWkdSaVV1NDZWZU1wNzlaYVQ1N2M0REVTSAppTUhyckFQQ2pKWUxjVDl3SldLY3lxYUxJWUt0U2xQNndyaGI2aXB3M25CYUk2ZkJ0aWJHYWZSeDNMV3AydGxsCm5UM1NRR3dmVVl4Q1d2VDRFV1lVdEtsa1Jib3B0U3VjalczTGdVUU5tU1ZJN1k4TGRaVHFQNkhBZG1sUFNoSlEKeGhPY0hSUzFMZUVibUpkWHJBTEwwOVJlalRQbjIrdG4rbHNUNmVFcm9EbncrQkdjZGpVMTJCVk5FcFJsUzNTOApPVVdWanoyczYwVHd2YStHN3BNMENHcU55VkJiY2JmOTdQa3FDcUlpQ1lTN2YySUtxMmNIU0M3cmVUTVZBZmhyCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlEvQU9Wb2p5dklhQS93OUJzNlMKUTlabjlRR3BneWlPR1dYYjZxOXphNkg2MTh4ci94S3JmdldGMHRrdC9kNVJhVkNGZDlzOTRSNE5aeTZiRlZUMApBWklEb2o2aXJUVVVYdE1sUFdhbWgzL0d1RStYYUIyaFJqOTgwcERFTkVXZVlvZGM5NEU5Y3Nhb2pLeXVwUWZpCnB4dlcrRHBXdjYycGZrTFhURHZBTTNmYVY4V0p3QVZzcVY0WXRPVU5IQXozVCtBdFdWOFFwV0dVQ3BQUWJkUjAKM2g0cnVEamdRYitnTXNLK1l4RTYxZjVkUGl3aDZwUjZxNUF4Q1dsN05kYm5zdGNnaDQ2dXhleXRjeUJJRzFqZQpQcllxcmYxcXVyOTJvdkdzOS9IbFV1VG5CU0dhbWpBbVZjV0Y0RGMwbXU4VkFQdEJBYjZRSkw0UnpBQnFZOWlqCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUJzK1RHZm9LVERGK0gvUXpyNnIKVkdzaWVvemt0ZmQxeGpkUlROdjhpQWRJY0dqMXVMeXR3My91a253U1VxRnY4NVZHNjFmQUZaclFSSUdZWmdHcQp1OENwY0FjZ0hPaTcrUnljckJWN0lhcjkvYTkyUnFsaURiSUpWc0pSY0lpaE9XRWFXMkZVbTExNHowYldzb3A4CnN0T2kxaVhSUnk0MlU3bmV3OWRjelhjQ0EvZVo4V2pxa1RLU2MzOElGYnJ0blVNeGNUZHA1cnk4enMxbWpId2sKU1QzNW82WWpsU1BHelVpRkl1WjFNV2NwcktoZHhKVzFpbFhkZ2luRUtoN1BjRWkzbzRRbW54NSszd1hja3ZpNgpVUzc5SDZSZS9SNHU3WkN0NjlnMW5FaVV1RnNhSHkwS0JzUWc4NFR4VjZKSUIvWEJBTGtpTndHY0FuK1RrZTJuClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHZkUTBOYUdvT2d3NVlXdmVEZEkKWDMxTE5FbW8rRkxQa3Z3bGhjYSs0aFI0bFRHZWtkQ3Q2U0RaUWdRbFlEUk5jZnVqb2Nvbmd0NUswSGNrL1RLVQpwSUJmUUZ4a1RndVR2T1o3SFNsTXExOGpUc2FCbWFkU0dWS3BCSWV2eVZhV2dWcnRiZ2NLODlXbmd5QTloN2FwCkl0OXA4TGRZNUNIandGSW9obFp6c2VMcktlUEZkKzVDUjBDZUNYTEh1dU10M2pDZVdnTHk0dVRzOWZaWGxlNlgKTkQ5QndYSGRZWVd0UUZSVlZwU3I1UHZETDBpSGJLS3NwV21wTlBIU0hYbHo4QzNtdjdYNXJhc09LMFErVDVCdgpvNjU2clpOemdWUDh3VEVJeUt4cjZmZnpFbXhyQUhTOGwrNzdNMlFPUEdqRjE4aENaMXBEL25PVWt3SmpBSlZXCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU9NU0p1S2YxVGpyMkVzVG1IWTEKdmlrc3JYZnQwL2YrdFhjS3dtZ2hOZ2l1Tmo2dVJCWGsxVVVKQU1uK3BvN2U5d0JEUzNUdEdheTFQL0U3MFZqagpMbExRdHVZRzdyOWFudk9ydDFqa3BJM3RBd3UvTGpxTllIRk5iaUtRUUVGZEJjcUptWGNtdUhjZEc2ODdkVXlhCmsrZCtqN2VHZEg3QXpsY216NTBlcWpPSHg2MDVHRkduRHZyOUk2RnNFdTVjbFJKV05Fd1h4UUs1UkZYZHovVE0KVURVbmxSeEhFWHJ1NUN6VngvdWw2NXgwYlFuNVdaOUtlNTZXME1CWXc5OFpQWCtaS0NQaWppNWlIUjFoR0xvagpWWTU1VytHWTh4ZFUwZklEa3FPdjNwSEVHd085d0Q1dUFGR1FHMDJ6NVNtdkFFVFJSc1B6UjlrNzFBT0JrZ0IyClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVZveXlIZ0ZkN2ZZTDVIRTc3Qm0KVFlYL3JmN3VFZmlsZ2NGYlRwWW1Sa1h6RHo5STFCa3d1bHZONGl2c3phRWtZK2IzU0djN2JUZ3VLQmErVCtuWApVYjFSeFNtUTZkRHJJTXo1ODlqYkZTNDVIeTdSOXpFSzNIQ29OcmxObTFKaGxvZ0dUbEdUWlBJdndWY2pQQUxGCmdtU1Y1V1JubVoza3BjeXRxZmdzd0k3TzNJRExKSkdUbDl2WDkyT3hHY0cxMUlCMmNCa0QvbDJ4RkJ0SGt0K1AKaUU3dnhWbmI2M0lKU21QWks0SEtGY00rSHQ1L2VsVE1MWXdNd1B6OWVjWXppME9PZm9XRmw5bzhIRnVJMG9MSApONDFib0Jra0lBSS9DYzFxOU1paTcvRW9HNWhFOTlCNG9DYlQyNGlJU1htV2hsQ2NyMkNiWDNHdnhKVE4zajdhCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGVIMGV4cnF6Qk9xb0h2RmhyRGUKMlBTaXFFNGgvaml2SjcwdnpKWTVHZSsxbVN6Ti9HVXlkSlc1RFBoM0dPSkRUQkZxRVdsdktNZVc2WU9KY3ZmKwpHSWw1THFXeVJqSVVPdTI5Q1E3WlVnNXZTZnBKU1JlWDMzM09xM2RMd3oxVDFyMllqN1EvNUdVeWNuTERxSGVUCmxyVFV4bXZoWVVlVnQreWZ0UVdaNUxqelM3ZUYyL3AxMXkrUFRUd2xGb2IrZGdWYUFkMFR3REw1Z3dWZDdGajEKRzhiZllDUHRwWTZpTTN3ZWZiUXRDbGExV25HUitvcHJabHpUdXdmWmx6ZWNhSGtHYlF4NjlRK0J5Y0U1aEt2OApBMXpPZVpvUXdLWXFQYUVnN0RRbUtXVkJrNkxqWGFEaEJocnFSb0xBbVNPZWJsNkhqekJWaVFXaGFQQWFYRktvCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGZNRkg0dEJBQ1J1R0NrbXBpMEUKNDJZdTBObWp3dXFlYzNmSkY0RW1zb2IvSXdYMUVUU1NUNUVVVjRlaDJobHd0RjBCZkEvaTVzN3F2V2tsQkF0UgpubWFtbDNuSFg2cXlYYWVBMmRKbnVPYXFNbXZzSS9WcmVRRDQ2TnhnZVZEY2l2YSs2WHUrV1hFeE9mOHQxaEhRClVnZnZjUUttVWRWblk1YWR0OW9CL2NKblg1UEtvbDdseS9ldjdxYlVhdEE4MHlmSmFaZ0pUaXVrVXJveHFrSVEKWldnbGJydkNPblV0OHFkMndWS2VxQzVGUlcrVFRLcHRRV3lDWEdpM05Fc3hrVnhsRTkrL2N4UzVGUkpSSzdqMQpkVDlyQnZNZTNoeG1sTGQ3VjVGdDB4MUNDNmRkNGV2NEhVQnNuWEJXZ09VSWE4ZEZscEZJT2xCbzFmL2FIR25OCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3RDMnZoS3YzYjVHY0xDcmNEcUsKN2h4aGhYcSs1eVdGUWFoenYyMm9GTGRRZXUyL1V6VmxvQ1cwa1IxV0x6dCt5cVl0S1JnM1p0Q2VZYTJxd29tUgpER3V6TGZDL0NVWnhPenVBVnhxWVhFeFhqTVUxcStlWDVldnh0WVQ1Sm1tdmpvQ3gycklmUTl1RmNNNnVSK2IrClFRNXdkSEdYVUhqZTFpWVBjZWxtcUpZa0ZjZ1piSVpUOXMwb3hxaWJSSkFxK3E0akFuZXl5NS96b0dvdGdwbHkKVDQxRlVQRzdlOWUwSUlsWWVEcHpVdjNHV0tOOXY2eWNUWkpmNElKLzhNajYxTFdEaUNoOW5FQ1FVUHo0Y2Y5UwpyME1zSjVsdm04RUFLcjNFRWxIaXJ3QUNFUXBBRHFaWFdjay92Ti8vb2w0a1g4OExWbmdsUTFNS3lUa3ViNjBqCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW1WaXUvcVA3YSt2ZEI0U1hPV0YKUnI4UXdiYkJrZ21pWUcvR3NzbGVmQndtMGR1WFUyNzBwajRPcXE3NEJuN3JXTGNqNTlMUXFaUGp2RnVTdzZmMwpvVFZoeGhuaDFESldrWmNqK2hpcWpWeWV6NVc2Mjl4VlBqZUp1Y2htOXIxL29FcDNNRDlwOUVrMjUyNEpSeGZmCi8wL1k3ZVVYeS9ZbzJTcUpSbXI5SzBKb3FJbUZBQy8rMmVUd25FTExST3RRN2NyVHVaL1FMYzFPWEVObmltczkKL2o2NTJlNURFUkdzV2lpb01qdGtMNVVWMHpiL0FhdHdGVml4OU9XSDRRbTFQeG44djlCR1laQVdDV1lwUmhqZgpDTFpRdTRBam9Fd0FzczFpR0E4M2kxWGtQc0RrbnVMeXJiNldEVEh4Y1ViNm5Pc0syNjB2UzNjWUNqaGdwdHo4CldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjRHelJGVmM3aVVDWmwzVi9nSEsKWER4ZE00Wk5PQmc4MG50eUZ4TlNZTzAzWFViRDBGYUlJODlDUXFSQjBBVUdoY2VvTjlnSklGRXhmQVQyazBzWAo4NWFLeW90WFUvMjhHVjZYTjU0WTh4QmwvSk8zdlQ4cWo4clR1RC93M0ZuN3ZnRGpWNUZ3b1h1QU5ic2xVRUt2CjV5Y2FDdm54TTg5a1ljM2d6UEg2aGRxUFczNEtxeDhyelBnWU9Xd0Irc3FpSGFkcWIzQ012bEZUaXBSRXBkdGUKZ3hXbGdwWjVUMERvSFI5Qk5Pd2drTnBKSUxvcGhYUyt0dEdzMDJWeGMxWDRzVVZzZENQSWFJSFVHb0FYRnErYQphb2hZZW0yMGVnb0tGU24yeFZNMWR3eU5yVFZNcHJDeE5KZUIyazVuc1BvdVR2Y1VQODMxemRDL00yOUVieER5CkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2lBZDM2UktpT1hLWkJ0dkppZCsKYWN0eFZQRzh3VjdMZUFONWh3SmErc0x0NmwwTW5qOGJhQUpTL0FMVDNqYVdPNnYrWi9Bbmd1aHp2SXF0V1QxUApGcTJpbGJNZFIzSVBldGZrNlNnZWN2OUlrbTNUZWluU1ZKU3YzUFNGc0RDWkYvRUtyYzlWVFFBTC9odDB3cWJMCjNSQno0ZGJuK29DeTBDRVRBdi9tTklQWE40NFk3KzFGaGIwcXI1aE83ZmhJMHZkMkQ0VklZOUN5MmxXck85a3kKVGFPenMreU1DaHN0dGxHU3hNWEFReElwaVdQa0Q2UWdBSFhkYktmZVFldUdBaTZjNU5xVTFaK3M0UjRtWXRKNApUUmxWV01lb3MreHpiWDdUMDJwMExsaVRkZW94RnhSanlGSm9aNUQ2bDNnanV5MkJ1VGVaUnZxU256bHBDQmVtCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2dUelAvM0NWL3I2U0k4cG40ZGUKaUw1Q29DL2gyR29MYTBvMTR5dmRKZG1qRWxGU216d1FYekRSbnNEYVNvYlJlMWl6ZUxKaGk4UjUzVTdkOTBJQQpGR3NiakRPN2RQdVZ5bytCS2lJQm5tNjZJNWpZTWRrelVSR3g2R003NVkrWUlWUzJyaW01SDlxbUs1clkyOGh6CnAzZVJHbEZvSktpTDJSMWgrbm4vcnN5azE4Um02bk15M1h2SllZb2tTN1Z6SFk3eTR5bnVJdTZCNHBVZElORXIKMWRnVXhYNnVYVCtJK1RYckNlU1pRR3RmeWNzUTg1Q0Vza1IwUFRJU0FXNnJvL25NR1RMUDdUT0diT2xxQXlwVwozRWdSTnBPTUx6bzlXN3o0cXJOSG1zSGYzeXVmWlNuSWFEREs5SXZ0NFRnWU9OeXl5eSs5L1lDMStMTXUya0c0CmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnpNZk1YWGJJenQ0QUpoMW0xZU8KUlFPeDAvejZXVlFTVzVPNEtOdmFTV1ZNSHZtWlR4c09UOENWWVVuckVjZklPM3E5NXlBa1FzaXhHWnpRTlZabgpGdy9uTURpMkhCK2t1QnlWQUtjd1IvUGtob2lBcU42S1VGWWNmY05BQ1lxdDNSQ3lHc1JYaEpzRUtqTm5OSjYxClhybHhlNmxseTBvaVdkYmoxdVo0WmtzZUxjUzB5cFMxWFJmOGY4eE1DWFcxSjV1RXdGSEhGMkRkaDA3UVFzQzUKamd5cUtSbStUWkMvMUtHdmdSTTBYTkpncnM5Uzc4YVNTOExPOGxoTFlqcHpIbk5vQ2hzYjlqL0ZZTEsxcGl0cwpIR083YW5FZjR3djVUNUEwL25WNVR1WXJvdDEwdE8ydFpoUXl2d2Q2dXd1ZVFFempFaGR5QVFFYzV5VTdmRzhuCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWV1a2VnbGdYc0tIVjRiWEIrUFIKelFrRWFEVUZQdDFPdFpZUGc0Y2JYOTBBMEdEL2dmK0lSUGtSdGxhaDFxYVJrWUpnL1RGVlcxcUNJc3dwSEl6Uwp4NHBjc2RYbWFPdmR5VVltZlVQekNTeXUwS24yRVR2NWlJVWRzZ3BKblVsS3U4dmhHd29kU01sSzV0dWh6UW5RCjBQOVZsZGdpQU9WVWZuNDF0MkNBUzFuQ0NZcDZIU3NmYmtZdWY0Z21tVkpaWTMrbURjOVRMRUN4MnR1M2tLK2oKQkVyMFJTanpXMzFZQjVhNmNjbUxCcFJDTEcybDJydkw0TnQ3dVovdU1jcjVobUJTVFRyYnM4a093YXZvNnUzVApGcVkwK3F2NG91KzdteFJtTzFESHhHY2NKSEs5WFB4aktNYXg4RXBScjBwUTlIVmdGSnYrYmJoS25BcFFhbUFKCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFdiM2t6Z2RWRGZ2R09uQVA2R0wKMVorOGgwa2ZpbmVhT0pya0lUMElXUW5Rb05ERk5EcXM0dllHY2N4d2hKSW44VFhZOWNCVzI0VG5PVU5KeGV0bQpQTU5ibTlxWTZqTDhneWx4UUNQTnpjRlp5RTZOL2RtZ1ZQbHFMWVoxQ1cvT1grZHl5MXpKeVFjUGQxQjZFTGZWClJienk5d0U0V0dQWVBIZU45L3pQWmo3UVl0dW0yQjBQZGVlQkhpbTVCTURxdTlIYXFNWXdaT0pkMEt3aEFDS0EKSFZHWnIzT3pOeTJFTXQ3czIvZVdLS3BmNU5LQmE5Tkl6RHY3SFV5UVYrVWY5elUrekZtVldmTHg0anFJWDQ0Lwp2bmU1WnFFci9XOEc0S0ZneFpOUllrUUNJVjJaRjU3a3dJY2FmWVI0U2pMZTg3ZUZrbXZsZHowbVRVVlIwOFJNCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3hDdFkzK1dUZkJSOFlWMWtuN1kKTFdKTGttcEtCQ2ZGVWhVWXMwV0NweWF6dDhzVHJvUi9ibEFPZmc5TU1Rb21nNVVRZlNlLzAxc0FEejU3Q1FlVQpJeHpzbDhCRHp3Ry9qMVgwTlV2K0k1cVYvT0RVUmpRRmJJdVU2RTRVT1VXcDJicGNReTJSazBGZzVHZmJoTU5lCmdpRWFhVkpTVlRKRmlKUnM0eTFJZllmV0JmZTh1cUxHWnNiOTQ0MG1Va21YaGdKV3hhUnVNeVJMOEZqM2V6Z1MKaGRkMjRlSDY2NTBRZU1IalM2UFU3YTJTdDg3aFY3ajBaYnB5U21Wcm45SEJEbzFGUml4azlZdFk3NEJYNFByZApIOU5QS1ZtZ0hDUW1xMWViS280N1UycmdrM3BIV3JobTl3RDJob290T1d5cEkxRHhvaXZvOE4yOGlOd3UyeVR2CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK3FBVUkyK0J4RWxpbkJTbW9QSG4KdXZ6Qko3TTlZclhvUmRYNkVCRWR0WUF3VTBqZDVIOUJmMktDRGs0L3JxRjlrVklGNnQ1YjFLZ2loN09hcFhPagozRGo2T0RuM0MzVTRKQzhpVkt6OEFiZkc4bjdmZWY2Y3NlMms5NVYvSFNKN1R5TXpaeG41YmdzSXZPM0UvczZOCjB5aWN1WFg0bzBtbHk2TjVSaGkrdlMrSm9qUmdjZFJzK2xiZmRoU21tQnZVSENDSnhrSzFYa1B3TFlGV01XdDUKZkFYMTZ5Q201VWc2U2h3TGh2SWFOdU9GTDVTdXlSaGw5ckhaTC9nSS92NEJMTUNLWmFGbVk3T3NFc3pGRnd4cAo1U2xYcTY3NFFSejUvSVV3TWl4QnBIMHlIdUFMTTkxZStEZGFHWkcwdjl2N2EwOVJWaUtBNmRPcldUZ2tqYWJ4Ckd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXZ3clVSOUpVU2d4T2hYeWFQVWMKVWxnRXI2WmxQTzkwZzVSYTRRY1RMVStQVVBaeHVsdVhCWmtGT1duRjFUMXV5Tkgwa3ZqUVpYUXZrU2VFek1OOAo3RjlYdXdtVnlnQ2d4QXFXSW8wcnA3amw4ZGQ2VG80UDk0VEFEOGFyQmFMa3liemcwN0xaNmJ6c1cva2pvZmVsCkEwengxSUhSZHZiT2xXTzRJczM2S3JIdVV0NlQycDkwOFNmbndVNE14Y1VpMmtFZlE5enFUZ0x2c2Y4Y2F4TjAKRWxMQ3ZDNVpBTlZ4MWhDMXhNUnpxamhrUmJ2V2Y5MlpncnMrcjlpVlQ3VWsrbU1yd2FnS1Z5N3BSOGVZeEpLRgpUMTVXMGcyZXJxMFhkK1UrRFA2U1JxWmowYjA2VXVPcUhlV1E3VWNPN1ZhTDBOTWp2dUFLQndTeGRJdDFHbkdOCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGJxeXZ1R2NwMU5nL1JMNTJCVDIKRWlMdm9CTWFuRml1b0tFUnRSMFN5amEweUxWMGU1WmZ4MHdhWW5DQ3VISlA3Ni8zZjRpd1V2eWpnYzlLN0V2Uwo1TytycVFEdGI4Vi96czdyRHR0aFBobC8wUjRTU0g0R1EvenJHeGhSTDA3TUt2cDdySEF2MFdOREhMQnFtNHBsCi9sWm9LYVdlR01hbXhTUEU5NW1RR1JJeEplVlFsaGdkVFVsendFU3lLNVV6cGRualJNNkFhdmZSbXpaWVJBRW0KRVlKd2orUXpnTGFoL1pBQ25Pd1grbVI2Sk0wU1JhSXZYWWJWRFc0ZHEyRFNnU096YzR1RFoxUFVLc3ZyVlFrZgp6dTA5SXlkWk1vbzNkNDZKeGhIQUxpYnFEUE4vWHA2VUdaRG4rTjNaOXc1Q1AweExvakpsWS94MUgxcllYb1dyCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnIvZ09Hdk9xeXB0ZnpaL1BBWVQKSmVTREJISmJkNU1VM2svWEMwc0luSXJOZkN3L2VvSWdPanppNUdUV1NLSEgxWHJZT0g1ZUw1ZVR0YmlOZ0tVQQpsaHB6aTIwa3BEU2lvVERId1R2b2p1RGE1a2F3Nys0K2lRZDd3Q1lGbDhXS2xXNjV1TXdNbG4yMmZWTEpZUStLCkg3OE8zY3FCMUFqbHVkcitVdXFyc21EMUV3c0hOeFd5b29DeG5Sdk9ZVVVuRzBwNm1WZDZESmRLc1FMQm5pUEUKQ0Z2eFdhTTdJS3o5SElWWm9UanNlU0dmSXBGWk9qZ3pUeTBFYVdTeGZURGgxWXlhaXdhSTljYk9aazVEUkg3NgpGbFBtcnJrYlRWV0ZtVnoyeENKdmFPbjhiRHIvUmw1aHpod3ljSHFVaDJSNkY1eEZ3ZWVWdmVkMXNrbk9VUWIvCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMURpMWVqcXVIdmMyTU1rTkpsSFAKTFZzbWtXY1Q1RklmNjl0MnRXeEZBUnBjRlYxMzZtUUNhcTNJa1FGZWNpUU8zbDVuVTVURHFjNytTNCtEYjlnWgoxRkEwczlSWlpIeW9TV3ZKcUQva1BkQzRLVk9BM3RNYnlscDFUMFpObUdtNE5uU3ZKRXFSTmc1ZXovYlJPTzY5ClB2UnpuUUp3Q0NXTFV2Nmc2R3ZKV2JmWm5MSmU3cEI4UnpuOWVTZjBYZEVpc3pWT3VmVzRORHZicnFySjdoY0QKSE5EUkpvNDdLK1lwNnlGbzBNWEpzeDZEaDZMV2kyeFVWekFrSW1HUXhDeXhSWFJqa1BuQ0NnWGg5TUVxbTJWcApOSndFUmRGc3JpRE9JVXFTRVk0WDJFR3ZMaEc4YnAzNzJlczFqWDFVM213K3ZncUFTVzB1elVqbDgzWURvNEtOCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHlZdDFNKzdrMXllZ2pYMWhYOWoKOG1vcXI2cFloMjNYTytVS0xGb1MwYkw4enVibExpaG9KaE1idFpKeTBMbi9XRmZZRnB3S0RsdTNWUnFyN24zUQpjZTY5K05GUHVXZkc0dGJCeXlzZmxDcUpyZk1HRXVxSEpTRnk5STFSOHQ3aUhqaTJxOUJWemtQT2pTa3JSWVB2CnFpQVZGc1RYcVMwMUYvcHJRRUc2YUU4OHJSUURDK2QyTHRqQS9hKyt0enB1YlFOUWR3eUNtUWxUUG45UW4rWjgKcitRanU3cjMrcWhUL0EzcDVXOEpLWlB4OCtnUFlGajFZRXFXMTk5S1BxREh1NWEvNlV0S3ArdERZTGQ3UDZOTwoxNGhkeUdyVFdreWx0cHBxUnVoTGdINkRqQUNBUy9Sejc0SlcrODJ6L1E5aFphWXVUdkZyQjd4Tko5TWJZbDBZCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXZ2SWpDL3NxQmM3dVhhMUNCL24KbkZkRyt2SDNKNGtNckM1NVFlQkszcDdVWitkR0RRelNrbGFIaktxb2duaUxvMVhDY25wTFV6VFY4RU1rSG5xUQpTbEZ0K1hwb2VtS1hLUmtBTk45NzNzSklFT2JkbEp2b3FtUWdHZzlQa3ZySmdGMlpQaVMwaWJJaTJBeHE2Yks0CmhFb0w2UDlzcFd0Vm10YVBsajQyQTZrbDMwWFd4YXNHcmc1Nk04cHNUNVRUdlF2UThGcUFUc3NpaytDMEkxaGIKZERhdlV3SzdQRVRwcU0wTzBUaG03SVNxNTcxYzFJeFF5dmlRU0pJcVl2RVQrR1hhM0dsNTRXVDBUWmhmODhFcgpCSmtEcDlad1dHb3hXUmRXZStUSm9WY3VoNEUwN1J5ek1uQXl4UnRzdFc1eFAvTUNWSlVXWHVWeE5kRmxHUytpCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdllyakdsT2xMeFVQVlBzU3hYK0gKNzdkZCtmSXNOVXcyTjZBREdVY3BvWnp3aUtpOFBYUTBDeDQzZmNHM1ZWQWZiZzZkT1dsZElLSFVLS3VYK1QwWAppMFhkL29YeEtlVGprcDV5Nk0wazRLWGd3MjlQZGxHcUhaYm9FSnpYRDR1czV2d1JlL2IxVUcvd0h5NHdlY2xTCjN5OW5rVnpSbWFzRVdzOUV5aE1HY1J3SWFwM2hhL2RQY2ZCbnJHR01oMEtMZHl0VFNLL3ZXWlNOME9PYUNka3kKR1AxcTM2NVJkdGlrZ2RaS0N3aFVwTDljdFZZQ3oyRjdxMGh2ZUdmNzYweWU0NmhkeWpzL1lNOG5FYzY4UzN3RQp5VXR2Ly9tWGlnOXRoVENGbEJvbmZMRlV0aStvZ2FWMnIzOUJBa0U5UjFUYm9qc1Z0VTE0b0dJeVptTlYxUHRsCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNU5ncHdRZjN0QWczZkJ5VUlMNWUKL1l4VGxpbnFsN3d3REZwdk5NYlo2K25yWjU3c2wxalE3Tm5PTCtITGZUU0tvSkUwdTlXNkRuZXY0RWJjUUNwRApJT3J4STlrTCsvc2c1MXFmdkM2c2hwdXZtR3BJRXpuc1R6U0JvZHJxc3BJQUtiMEJIUVdsMFZrT1BreUhNZHdUCmhTTFBJQ3VzV01IOENJQ3d4TEMxSEcveHd0S0cyankzY3RjK3MyK3NaNUVOaEZXdkF2V3pyTnIySHNFUk9GV28KVjVoMzhNcFdWTkdWUERyVFdiWGhGWXE3Y045dzRWV25WYVI3c3hkKzRmWGlSNDhIZkZDSlRuSFpLZExOaUtSZAorK3ZQc2pHVWt2elplMFN1b0kwNzZVNjczbEJIOTlVYzVYUEJ6TElXR3pmTWM2YllSZzBaVzRSbW1YZWJGbVduCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGFpb3lNUWkwd2N0K3FTQ0dMYk4KZkdaMnJTSGptUVZORjdFbk9RZFU3SHBsTFdVdHlnNWtZT1Voc09RUWFMYXo2clEyaSs1ZXB1MDg1dG5HeGZSSwpsZkhqdms5bWx4Tjh3dGpXY3ZCUjFvZHYwZWlFN3d3QVNPeVBWdHp4RzFmS1IzNDNobkN6YS82Sm8wV20wRnEzCjE4ZnZIdWFhTnRYRGZLMldBMXBhNFBLV0QwYzNjZlp2c0Y2bllVQXpnamNrcDlEQm15WlVSVW5wZzlmS1VxV00KYkdGZDZXcGYzaTkyRDl1YldCV3AxSXk0QW9IZTVpdElxeEgvdlVCd0h1QWcxM2JSVG9xTVhXNGNZWTA3aE9KUwpPbjRhWjFIMXcwNFlhaU1NajRVMDNpb1MzTXJlb0Q5NCtrT2xDU3RKaFRuU1NPam1iZHhJVnZyQnlObkZpMkR0Ckd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGRNck9ScUIweUNOdzRIMndLYzgKMzVoSUZwMHk5bEJpOTY2K1NLdTZtdGRvaGQ3SXRGMEpsR0R2Z1M5N05DNWlMTG1hdmQzYUlzalV1U0M1b0hsbgovc2NKWTJOdUNOd0l6VlNvVE9HNVpNakxvYW1PNDFkKzMrS1hMakNySnUzUDBXbHM3VUhDSzNBR1hGL3pNYjNSCmgrQnQwTkMvUWM3M2o0Tys5cW0xVEcyMkk0K052R1hyNDkxZkhIanhkenovYURvL2ZydGhLRHMvb2xnUldDU2EKZWFjNUo1bGdBb0N1RkZ1N3pGSU5zVkRCN2FSSjFrUnF3KzFINkR0UHVGdU9YNHpMOWN6bjRUaEpzSW9xWVZweQozbWhUYThicjNvNXYyTEN3WThvd1huOGN5byt4MGp4TzFmN1k3VXJRdzMwVkMzTTBMcXRGMHltb1FaMVVnd3p4CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFdPTTNiOEVibjFOMGs1bDBYSFkKeXgvRXVhc0pFRWcyU2dXZ3ZFaVVRS3hTcnZHRkVSM01kbDJibzFwMXdIdmtXbHpXYlE2VUlwc1R0Tk5adjJHeApMZ1JBVmZSSnIzZWZWdWZjWDgzZE83eWJXcldpZGVRNjRqTzQwUU83YzB3SjFsd3dlVGZDVVNnRkJpc25vK29XCnFHTTBCVXdKc0hYL2x6Y0Y0OU96dTJYb0FRZXBjc0kxKzRKdEdWNy9tU1hGN1BlZkRnU1piOW5mSE1kOG1veUYKYXlZN0Q1dlRuanUzVzhLenUzMnVuSFlxcExwUEdEZ2tzWFl4RFNteXV1aGpEYTdWUnZuOCtxeThWS3ZiQ0F6NQo1VjQzMWpLZnB5WlJQWHpoVG1UU2V3UDNZWVp5RlVxQytQRUE2ZGxCaWVJM1pYZk0wK09iajd5WmN2SGRlMWp1Cm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXVVdlNmVlExNHZ1N3VGQkpmdEsKVDRQOHJrSVJDeThFM3pBZ2wrQ3UrRVNTOEd6NXlWd1VLbWRnRHhPNXlBS3piNFFacnhBd0pVakRUZmV4dlJmTQp6OHMxSGx4WndhME5IZmdpTmFESnA2MU5OREZ3aDAwcUVFOVR4YlRrVmdXMzFQTEZidjU4TitYVFNodVh3RGd6CkZSbEw4VFUzWDRNd0gzL0ZMd2N2c0Q3eVZPeFp2Nnk0MWlrR3VURmdselN3bWV5ZnJKN1IybFBTVG1yZm55MTEKL1hPZjUyUWVQVm5oTlNGeDRJWER5UlNyNFczMGRkdTdLVS8yZHdVbWQrdUREYVZjdXFKcnBQeXpKaEFINHJoVApnRWU0MmtEWmwvY1c4KzJ0TGRSd3FjNDVaTTBDUUpKOEs3dmhKME9RazhBYStJcjUxNG9WaUtVZjdsUEtmRnAxCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGs1Y2hYQzJvTFYyK0YxdmlKc04KZis4Njh5M2lodllQNzkyUytZWjR2Z3hkQUt3WUN3NTlZUGlEb3MwaDdYeXh5blEzekhWd05VdmVNbTdZVEhRbgpVS2Qxb0pSR0VkNENWc29vTEU5dFgyKzM5UUs2dG1OV1RUTUR6YUtxZjVhRUNzK01yOGJRSk13ZlJiREdQb2h6CjkvNTFLMEVuV21mRE0xRURRT0VPdWxRRUtxaTlWWmRHMW5xV3lITkdsb2UvQy9DL1ZKb3krYnl6bXBLUlNWc2cKTTRtSERMWjlHcmNhVW44Ukw1eDlJdnhIcWZVK0xjc3BmZ0NYdWVZalBOYzRKeXBucXRUYlQ3bkppeUk5N1krRApyOHhwc204c2Y3OVMvbHorMzdXVGkwODhzQjJkd0VKVmgyT29lZVV1U3NROFJoeU9qR3lUb0F6RW9zSHNUcWQ5Cmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnJOdWlsMWkzeEZzU01IMWQyeEkKczF6Z2tLMnBZOFpoNTV1Z1FLY21nQURiZG9iUlBLalE0dGZ6S3NyR1BndlJzTXVWZDQvOUV6VXkrV0dJOG9sUgpRZnpsTFNlYlFKem9LR1Q5a290Ri92VzZ1bnBzOHlWZGlJbFRMM1ZBQ05Tc1Z5NmpZSnUyRXFRUDZhK0tiRm9YClI2RkhxK25YNmZVMys0Ymp1RHpHMlJVem9JRE53cmtWR1FxZDlSTmZVRkQxTlRMS0tHaDB4SytCZE5WZi9rMkQKbkxMY0owd1Y3VTJxc3J1RzdIVW81c1J5QzJQUzlwSmwxekJYSGhlSm8vWUxyU2Y4WDVrc1FXcHYrbThJUjI0dgp6QzZsVmF4Nk43MDd5UFl5NGZYcWt4ZmNjNUUxbGlhQlY0VkR2NkNmYkxWREtoalg3cWN1ei9pYWNkVUg5UVAxCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzVCb05oZUM0Y0wwWm11RkxQZloKT2pRRUxQUSsyNjNYUmVGdUJsb2J4NjJ1Z0dwSldlRFdUVlJxdWQwREUzMU40UHZ4eFNxTldYaVdBTG9Cc3pnRwpBYWloVjdMWEc2a3Vzbll1UURjanJtVTdvZHNTTnd0QlhEZGpwZkxFUzM1ZVkyM2c2ZTF4VTBCM3RKMTVBbllCCnZOOGZEUG9ER3VZUC82aFYzUVJleTl5TXVTWENIY0FmTXJCNjVDU281MEVVTDJrblhueXhvaFoyWjZqL2RtS0cKQ1VrWnVndnhKa3N6TTdOY3IzbEtmQmxpQjRvM0V3cC9hUHBObkYyNGg3dVU3MU9sQjRFMnBPSml5OUdoUmhNbQpFODh5ekxSWmdwckFBT0MrZlRSb1I1U1h5N2FaeVhPMjFNbENEQmloY0l5OGhRMkZoMldKbW1Yb2lJeWJaV0xmCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmF3OVlJYmJsYU5zdkFyNkNNWjMKS3JmRFhLU3NVb3ovTFhEbjMzYlN0VkRqUTZYQ1p3MktYbElDbW5mZmJyNWl0V1hKOUhGeWpuSEVidElMeWJNNwpiNDFOeGtGN1ZqU3BmYjI4ZXJWOEZzSmY5TG5UVFczMjJtVHFHZHcwcWR0ZEcyNFFRNGVxTkNLYkYrSERVd1hPCnMvYnVoSDEzTGNiTVVqZDk0aVJ4WHhIUnpnK2hUY0k5aGxwcktXYjNsSWhnMS9ialFKYW44YXpIeXdmalFjK0QKTWNIZ1FNRldGdzJ4Tk90OGJKYlo2ajMrSE1MbUFSSk03Z0FYam8vZ0hRZTRITk1wZjZiVzVxNDdVMEVQc3NmQgpYUHNjT1dOMGlTaDdiTmFrc1ZXUUxmUEI5czNQaU4vTENqc3RWSU1iNjBhMElIK0NBbFZZdzQySHhkZUJOdUFVClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2VpVVRQeXkyWWlDRHZJU2FBSVAKSUpjTHVkeklzQ28rdUFzTmRFakJiMU4xaENsRFhaM01EYWVpSENEbDFMSndNTTdiQlJ0MEoyYndUZGh3cDZnNQoyekJUbUhpQy9IeUczbVphdnM5QmQ2SnpKSmJtTTlWK1ByaHdndUd0L1NsYUVYSTlxd01sNExZaXJlWjdaVGY4ClVMOHBBU0VYK3RQN1RBT2VQWWJ5MlV0TUE1WityK21sMHo1TkNJYU92Ykt3ZWttSVVaSkFSaXZQbTdVbU9kTVcKK3FjTWNjVjJzcHlHaFhEMjRSTDRZQTN0ZGx3a25zRXdqRzNpbnF6OFhJcEdkSDRSNXJtTzdacXltRGR1THEvNgo2LzU3dFBmUXNnZldhN3hlUEtuU0xuVVBCckhvMnhHTEpLdnBqeE9rSEZETVNad0dTUVprMEpVRHpaaU5nUmN2Cm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzFlSTg3S1A4V3l1V28zcm54SG0Kd0JFLy9lcjRJdllNR095dWx5cC80TUpJS2dFenkvL3FNdkxNeXEyMnd2U3Nsa2wvQklZQ0hKSGh4T1pNMG9lWApNdU9QWElhWEswaWRTTmdreHBzN3VMcisxK2l0eW9VOElpRHJ4K2FoQW9Ba1lROUNjdE5kQW5VTERzQkppODJlCmZ2TC8rMlhsYnFwQ0hmTFcrcWpPSE8rcWFGZWRHZjRQemNoM1BpTkkwL1BvZWRtRTVIWWZtMmtVOHFsS29LbWgKenNwWHV6MUxHcWtUL3BGcERpQjFGaXRFVnlOZ1l5RDI2eS9HdWZSTWdUUEd4enlNOEdST0RTNFJhUEF2dmtFagpJYXJlY2xPSlJLdmgzUDh4a1JaTHdYNDRtMTlEVjlHMitqa01mdDlDem9ablpRbmpiZG1PbThJUTIyL2gwT25uCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNCt4WURwajBjbzhFR0lxTU1nYjYKYzAyWDllRnZVY3RsdW1ESmdXNFJ1MjJPbnorVXNpUGF2ejhMaWNhcjBDUmNhL2wvc3RHUzJCSHhwK1c2UVVicwpzTWdzZ3MvcTQ1cExpZ2hieHdLNzIza0dMckFxVkIreTAwd3FMVEpXZkJSRW01ZUJpQmJNMzV3SVVJOTI3OUdMCkVHQ2FkYXF0V21yZTRGTkJyYlhrWEF4dFdEMHc5TkRBMGxjMGN4aVFiR05IeUJWdGxkTElSVWFCSGYxU0krLzYKMlZkdlAxSlh1Qk4wK2N1dGV0YndpbGU0TWhDSEZreFBQdXVmOWMySEdyV2lwQkJOdVFld3Z2TGU4MXBtNHVlTgp4RW1iZ1pHY3pZdEdOZ3p4SVM1ZXp2WXZIWVVYWVNLaWd2TzU0cE4zMWNMN2wwQUFZTUI0a3pYZnVhS0s5eVB2Cjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0hFVGVMd2pnMjJncnRJeWxPRXgKeWplRm9UVSt3M29kOVJxUTdWUklzUVZZU2FWRmgvMitKVWxUQm5lYlRqVFY0VFNvcTNPeGhKQ0F1NGdkVGgzNwpON3dHNjRvVTFyTTRaTS8wajdiaHdMSHpoZVdyVWZzbUJhT0Z2bk00TlhJNGN4TzArdXBNa29qWkZiU3djMmZ3CnlBbFFVdHU4T2dHdlNGbUVuRDFCZjh0WVhTUXB6c05LV01ySWU2cnRCZlVXOUQ4dFNidDJvTGdMdzkvSkhvaEIKUEZNcThKc0Ewb3E1Y0RUd1BlWmkwMEFXc0FTN3FTcWFqMGFaNWRYUWJpU1J3N2hQVnllSmRTYlZtdzcyRC8yQgpRNkVsdWRHMEp0UzFnRmNNS2NUWVh2ZWRydmd4YVB5NmM4b2dsQXBLNHFJeXNhQlBVTFVHSE9qbUoyRURuQ1dqCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDY1MkJxZDJhc3hNQVBydW5LQWgKdGtoeC9SNnNHQ21Hc0xIL1UraGJlL3FiRHF2a1BWVGZlUHA5OHJQL3VOU3B5Ym1yY2FJVCtCUGsya3VHRVJocgovMTVFVWNSazkzZEZWSTRINW5jaGlwTWlCelRsYTdHeUIybDA4OEZIVFlFZ2t6S05Eenh0TkZGK2J4S2J6L0d2CkJBai9EQ2tzTWU0VnVVZjhHZ0Ird2pwOGszck9WQWozbURMbTVNalduUTlQSVZwbnlJNlFXZmpxSEVXSmdxL3cKaHh6alpDclpMOWtXOEVhY3Y3ZytzZ256UmJUdklIckNST0xkb2JUM1g0UjVyWXF0UmI5blAxYTlUMkNrTzlveApCd1pFWm1VRnNtSnhKNEhkT0l2c1ZsYytrKzRJOS8yTUdreFpBM0RENTk4Nk1vRmRLNnhFNEY4ditVazR1QmI2CkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckJWN3UrOWd4SWtXaTRDWWtzQmcKa09wMGZVcDJmOGJQd3JONW5kcENpMVFFdWZFdng5K3ZXeHg5Q0dKRDNNN2tSaHVvbnhwUFV6ZGZhampsYXE3SgptTWp2aG9vL1oxNUdhSnFBRmUxSFkrZVY4L3dPaCttbE94bG9jUWdUenp0elF6djJOYVE0L09lWTNwWUFPV3pMCjVwNXh0bm9LdGViZXZxRTZWYlBVd1VqNEZXY0RpQjd5Z0ZwV1pBb1NKR0xEbUJZQlRXQ3k3NkdsMWhHbFNYU1QKZytndlp0bVlRcWM4QmVTTFNIZjhTQ2swM0plL1NRUVowUXpkSzJXeFM0am9QeWtHZkNJT1Vvam1UZW00Ry9mcgpGTWE5bm1HWUVYeDFDL3VnemorbFFLWXM2L3NMMUErcXlDeEVQWE9jUUhEeEwwWHlpVVhhZis2NXh4RGIxNnU5CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU5EK29tcm1Pc3p1VDg3ejBhNEsKYndrbHpFa0lBV2FLelJ2dnM2Q2Jmb1pDdjBBcmxTem9Vb0xVZ3Y2ZHo1RVJGOG16Rnc0dHFXdVh6aHkvRGJwQQpqN3o4Y3pQYUd1NVpnT2hMR1Z4Q2xmbmhCQnJXM25RMnZ2MnQwUXFRaFVHZVU4ZDJUZVNSVFFyd2FUNEdUVVpLCmNEc29aUXhsSEprcmRFbnJQbXpXbEpGMGVvV0dSam81YkhINkYvVzh2a05YcXdPTGZhR2gyb1RBemJsQ0FsQU8KSlBpcmVzRkZzQ3Nta3dKbVB6VWk4WnI0WHREL0kwZEJTNTNvUU1iQlFaU0pJQktGYmhERHp3UkpmL1hPNlo3cgp3aVdKWWtmclBRVS9VcHpZa2grRU5MWmtxblVNWjIwaXplOTlQL0s5ZkZOaldkVDhoL2NPRmhWeUNOWmtqclRxCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0Vhb0RaYzFuYjRVbmZRaXpkY3cKOEdYcU1zaHdQWjVDazFRNW5FUVZ6Nmo3bVc4N2M0NTlwcEI0VVRHeDQ2UTBNd1BvZ2FoKzYwaGZIUE41R3FGTwpxZjMzeTJxVTFaemJmZ2wySzQ1RXB2UzA5bnNzVnR5bUgwbUNYL0xSbE91dmdVTkRwLzRXQ2ZWQlBRNkptRElPCnd3N2JlUDVYMTN6dlJib2pkVDlBeUQ1dmVtUCtOd2J2YlVldjczaDlkV010alhMdzhJQThLN0RvL2RoVFVnUUEKcTl6VmJveVJOL1F0SGJYMG9BNUxla0lZT2Q5dnJ6SDlGOXFDRXdNWlBvdHgxcXFVKy84V3lDZnpGNmdVTm53UAp1Y29WT0lodmpjUnE1Wkk5MWpuNDByeElWRzRBMVZmdzZ4RXczQnRscHc2cXI0OGFCeEdZQWRSejdQOTNYY3hrCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0pEWGNsMlh3RkJ5bEhVdG1Yb2oKOEtHWVhIa09pLzd4clN4anhBcUVBZXFTZzhVZjgrR0FiZXh5SXJ0d0k1d3Zjc3NLdzlSUGxnaGF2TEhoUHl4KwpveHJtK2s3RThkYjAybzhLRHhlZlIwMTk5eE0rMUx3WmIwaERzdGpKNzJ2Q0lzbFh0RjMyRDcwd2hodFI0OW9oCngvRkpLeS9LSmdQdXZqWFp5WjBBcWZvTUFXeEhaUlJIM2JRbWxCdllJMGxnQnZVaWtmbmNZYUlSWWxXODVGQXIKVnFNTStnbHpkYXVsMU1lb3ZsZXpYais3Ui9zMUUwVmw2OEEwUGg3VzFBOUJVS3FzV21qRzlwRHF2ekZjVzlRbwptdlNkd1BxVGtYanF6Q0J3RXRGUlJYNDNSWmd5WTdmT0FNUkRoK0lXV3pDbnk5YXhyV282R3I0MlpUclVQVm1ZCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2IrVXpwazNvbnkrUmx6UFlvdzgKUlN4Wm1jVXVIV29pNXQ4czBTdkZ6K2wrd1o4V252Q0xOY0ZSS1N2YzQwK01xM0QrOTl4Snl2ZXorUS9CU2lVZwo3OFhnMko2ZjMwaHpsK0tENmV5UHF5SXVBUmphNXg3TjB2MnBCbWFsUGpuTVVpRTJob0lLbHQ5eFdJZjMvUWpBCmQ2MVdaUTFueVorUTF5TUNPMjNIK0hBRnRJbmpFZkZWSTREejE0U0VOWGh2SGZvOXVqSmx2NmZGaXBMN1dwQmEKbGFSSUpaOVZPV1dXSjBkUXdiMklFUkRkSU9PZ3JXOG5sWnVvbHVRV1JOZDU3UzFjby9vcHRQTU9WcHRkQTJ0TAprZkg3SVFKZml5a3gvNVNtY2Q1cUdEV08wY0ZHaS90eVhnand6andFNlhDNnpKNWc1SUhUbjFoYmJjLzg5cDdQCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDAyaHV6QmFnd0FCWFlJMjVMNGYKcmNCQXBGSUtxa09sOVQ2RE00NVVScFdsanRDYmdUdzJlUjJKZkJpaE5SN0h3cktKbkh4WTU4c2tzZnFuRGI5agpqbmJSZnhWbDdTSW9rYi85dVlxdDc5d2FSZEErdElsU0RMbzVuMFprcm5kN1ZmOFZ4b0R5T3pjd2xCVEMwcXUyCkJ0cXBXVHAraVM1bCtwZHE0VHVhRlhSYnlYc1M4Z1oxZUVaY2d6MVFxeFo3dklKeEpkaVVpMFphcExWWnpZRmQKbTk5WHRvbWN5bUJGaHRKQmhDQ2hFNXA3UC9hbU1RSGZDVVNnVENTK3JNQmUvMjgrSmw1QUVjOU9UazI4UzhRbwpoSHlkVURGUm4zRDh4cnd0bDRuQTZJbXRMMFdwUCtuc3Bvb0xTV2FtS2FRUmNaL0IxSjhlWTU2OWlPRVgxUXg2ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWVBV2dDV3cvMWpzOE80cldNbWEKSGVJS0hNaWh4RnFidk52YjhjWVk4dHd3OUxVbWVmS1YxRG1PMTBuWGZ2d0lsTzdudlQvTFpQQ1IrblQrUnFIVgpWejR2U1FiZVk2ZTU3YXpIQ2RZRDFaOURPdFY0M2hRc09RRXNtcGhzeTkrMERPalU0aWl5S0xSRGUvUkJ4Zk51CjJtTmllb2YreFNSN3JJd1VaTG90UE0vQ0ZkNXQ3c21GQ3RneEJjYWRIWkE4Y3ZxLzdVaFRyV1cxV2FkMDhJeFUKRW9wWTZ1WWJqQmdvWTdLUUxKeVJkNkRaQWo3dU5US1UvMHFvU3hSRmVwcGl5eU9OY1V2bGxrNnN1Skt5OWdyLwpzbzhDWjBNazE4RFRBM2xqR0pJeW1pOEFzM3BjZys4dW9aRmVTS1RYT0x5cHNmcGI1ck5hbERXQVFZMW5ncWY4CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2x3WEVqT2hLbGI4enhpWSt0NnoKZXpxaDJuY0NkUFRoQnhpd1R0VTk3R1dnRXdNWDYzZXpqY0xVWDd3TUthTUNCbXJzUG5CTEJTNVRvVzNkSWR5bgpCUXJPT3I1ZTB5cHBkNE42akdGYXRoQmFjbmVaV0QxYjhzakdleFpnYW9GTWdVQjgxYlhtd1daTlFTamhmRXczCnFPS1dxK1BTQy8rUjRGU20yMmwzUmwyK3ptQkZJUmkwQ21xWkNVblpSOHNFU0pmbkZVOWxxc3ZkcGxYS0VTb3gKL2c4a1htQURXNFQ4V0ZmU2VTbkJ0YytzWERLeGwwSjZzNW1FV0tYUE4zNWl0eDU5VXdWNi9oNmxsT2dQWC9iYwpWUGhqNVlEZWVWaXlMZU9qUkdPVk9KbGZwd0JhOGlCanFwSlBaQ0JKTUVIWDgwcHU5UW1SdS8vd3I5TVNYaHl4ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWJsM3hZaUFYT1ljSzlXbFEyYkkKOFJwcW4rWllJU3NUWlN4bFZGdForaFMxa0p3UVFyUEhzcmhnQ1FkWDQ0TVdnVWVRKzR1cC9uczFlRHpQNnJmaQpNYU9pOVZoQ1ZhQU9nb2xPRnlzVTBZWElJZG5wdkF5T0RRaFNVTTR5NTYyZ0pQeGJYdWZFejZSVWdqTEdGTWNNCkNNZllvd2hIU0dWUTNYRlBWYzNSWDVoQW9STlYrU1IxNm51aVk1T09WcHBxbUlEM05mdWhIUEVNbXBET0VybTQKMUx5aFIzajFZejJvYlRNNk14VHF2MzVTRDl6ZnhKSzJZa3hpbkZHSERubzZLOE5JVkpMYWVUemVSVktJeEJJbgpVMmJOLzY0NmlHYW5wMm1zcUl0c0U2NWxZd09NbDl1U1VWZVJJVUpwRjk1SG9vS2pOOHhwOEN2YnNXd2pCeFRHCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmFFTzlDNmhEaldVRURxcVZsMTgKRm5SbVNrQTRPa2ZyQjkrd2xHRkxiMkdYK0Fpby9TTTJXQlpKSExZZlNXeGRITlM5OHluc2VpRUtoYXh5V0RLWQo2VWloTCsyUzdDbSt6YXBUbkpMUnJFYmxTWFZoQVlTc1Y3OE5hNUE5QWNSemg2REhpTElhamtMRVU4UWhjd2Y2CnB1ZzNGbGRKalZJSDVFLzgxdm5FeUFnOXhPSW9mLzFSbnQwTmlqWmZQWkR3R2lHODlhR094ZzR6R1ljRG9KRFQKU0tvV3BBakJWMWlqT0tTYnNWZ29oK2ZEZUJVOXFBTEp2WlB0QS80T2dORW1qWk54YTd6VURSdlBHYit4YXF6QwpzVkcyaTUxdWFNa3RHUmM2eHA1YWNnYmRIZU9uSXNHU3gzaXZpZHRhOElac2ZrWWp1QlVkL2VNcWpmNXNlWVdLCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFdVYVFaekFwWnYwL2lXU1FiTDgKc3Z6Q1hwLy83T2FDTFVpbG5pSmllV0RNdkxJd1NyeDN6TWxyTDZoTDdtcmN2MS8xSkszRUZaUkNDYmVqdFRFTwoyV2FUZEpoS1hiaDVEUjJXZTVaT1Y3dXJYUmo2cVRVanJCSjFzZFBORStHRkNGZ09UK1pjdEYvaWMwdFJHQzYyCjI3QTVrajdNRW5XbkhBblJCblJnUzFiam5NMFI4UEZsZks3cXNBdk9sa1F6eTh0dVpsRTlBeTVSaEJzWlUwNnUKQXhwUUp1SjR6Zzg2Y21KUVYwTGRVOEhuOEJ0aXNXeDFaK3hwT1AyVTkyMHpZZ3dhS29VTHlZVVpjOERzZ1VpSwp2NVdvVHJMR0kxNWNGaW1wTHZRQ2kyN1dxOVRYSCtHeXlFS3JzdTErUGZQd3hWaThrVGRIUDQvQUJKbDV0QnpCCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlJiREpjbllVMG1nSThDWHd0V0YKV0pxVEdTdlc0S083TWtublVGZ25jQ051UWhvU3lHSEFNcWdGOXFqLzRlRU1XN3diem8rVGhWMlVRV1VCa21MMgo3U2R5Zm4veU52Nk5jeDhLNnB2ZWVoNVpiaUszc2ZmcHpBbGpScnZsOHhtZWNjOW5xV3JMZHRFeDhaWGtSeXRDCnk1N3B2eWp3MFNuc3pjaURic0g5dFB4Q2Q0K1A3VWZ3ZmhFeGV4eFFVcVdXTXpKY1ltajgvUDFsdGxxRzNZOXUKdUlhOEs4RVlvdUlmTUxNaUcyRWErb294VjdWNXNlenpCTCtTNmViSC9tU1VnSnl0OGp3Ky9idWdWWElxanhXaQplN0xiN3VnVDh5NlBkdnNJSCt1SHJlNFVnS2Q0OEJHTjhVSHVHRVlibjNjNjR0djgvWmVTUTlKRFlySDhxWGRVCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2Q1M1dpdVZoU3dEdVFhMkVqVDcKVVlpVDlweURWMlVQekV6TU56SStaWllTcS9UK2J1bVJINGRtSW9GQ0krWDBSNTZRVktHNlFyWUZxL2RvMWpXOQprelc0Vk5sU3ZFTUNZb3BVVzhQVVhvSzV4WFU1dzNhRFJSUXNzcEcvWUMyamlPOGRDanA0NUU4WUVxTEZabExZClFqWmtrUnkwbjBkNHBma2dqVVZmd1E4ZTdDZlh5aGNpY20xOGNNZTYyUWlZWWwzd3RueGQyTmF4TkZ3WG16TWkKU21UY2tEQkt4WE1oelUzNzE2MkZKRmFNUlNVUDB3djRqaFpTcFNDR3Z3aWZraXZzU2hyZ3Q4WThobW0vak9DZwpFQ3pSVWJ3SisyNzBrbnZqQlF0ME41RERxUGdtM3Nnam04MzBtSU5qQUZ3bVJLTDIyZ3I5b3h1ZXFtRDdRMTVnCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3ZybWwwVnY5VjdUS0p2RzNlTVMKM1dGZzh0aDVuQWE5REtsbHhlYW1Gb1FUUnhXUE50aFEvS1FseU5BVDRuVWlYdmxKcWF4cG5DN2F5bmNIWU16WApJb1Y3cG5oZk12bWpTVG5OUGtQOU93Mm5TczU3QWszN1l0aFJhU2loclVlMzNGQ1RMNEE0em94OU9Xbk04K1M5CnJKeVNUWnZoaURMTmRRSUR6WktoOXBQMUErbUJMY2tMYU45UEtEOGdjR2w0TVZGU0VCMHJvdjFjNEw3Um9IKzYKOEx4WGdka2J1MnltNE5PT1Z5VkRCVnNkcWtsL256SFNDVkl3bzRIaXhZWTNsYklad3ZiQnNuUTZadUJ3Y0p2RApXeXlUdVh4QzR6RmdwYTNYRzh0cWRpKzBic1FGQW1uV1Uwb3dUZW5IN1FJSllxd09EbXljMWVtNnpEd213TG9pCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGpNQ3FoTlUrNERidXZPdE9vRnoKMFFoWFRGUUxaZUZENGJyc3cwUjM3SFRvK2hsR09PNy9CWStLZ2U0Y1piM0JiZVpINWVEZmN0Wkp6VDVjaWQ5dApkRmhvWW5FRGVVN3F1M1B5MHprYUd6cUtHRTV0bnJKR2RPUC9kNHZyNk41eW5wNGZXYTA0Sm9IaDdiSHZMdEhWCkhzUGVwM0Nid0NmcDNDOTFlcy9OWWswaVpqa3pPQlc1N0JzS2U2V3JHTHNEOVprR3BZTjZwWU5zYjRRZGErN3cKNlpwVTRNK0syUlZuY25hb2x0YWo3NXNGY3llZklXVGp0SXpjY0c0NytTR25RbmQxS0VQUWVhZWt0OHRPYm9NdgpYL3kvc2NlbFpxblNqMC9rQ0Z2dnp3TysrQjJFcFNMSHoyNXJLWmxmVHlTcDlLbTYzb3B3MjQ3TlY0Wk5IMXBLCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkQrWjdDcGNXeFBQNnZQY3FjOEUKODE4MVEyc2tPWWpsSjNyL3VuL2ZWSDVYcWN6Q3dqRUVaQmd5ZXN0ZCt6NUdlbE1OQTFjWSt1K21hMkpBZ09WSApsWDlVOFhHZUhIVmI5Njg5S3JZTU1UWVZVU3VZR2VXcWpteGdXQmgvb3BncXdteEpwb2ZiMzlSaGtOOCtqR3oxCnF0SW43aXNnUEhIbzJNamhiU2E3QURpOFErQUJUYkMrUkJ6RG90KzFUZXVmd0ZSZU1wckZyOGxuUjN3SFNFNCsKZS8zQmpMTXZRei9ZOStvWGlzeXlMZkxseUNHdGZVVnB3OFBUb3B3R3BDcDlkTU5sWHFNRnIwUSttSWVHQmR6dQpnTVJMV3RqUEdGb0J0TTdXUUk1OTVaNUtwR0k2WDN6eDNBeTI5Q2RpSXQzM1pjQnBsaFhUc1hDaEU0eVByeTVVCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMW5DM1FHVGVnSTdrdXVoSHVXZXMKNFp0M3ViWWtPODVYRlVXVkpyOVdGdkRFYkJGV0dBTC83T1BvOHVWeWE4YmxaS1lmZWNPQUI1MWJIck16MjZBUQovRUt6bE8zSlBHeW9NSHkrTHBUUUVqcnV3NzRDUkRWQlJFWENvQ3ZsVlUzQlFwNk1DOC8raUFZdk83YTlxcVdECitUaExUTm1NZ214djhodDR6dW0wekJsWDYyY3lFeUQ2YmZFanI2b2l1WUdoU0x6S0R5bE4rSTFKOGhRSi9NUUIKY2p5akM0MXRJRzFKU2RlSzR1ZURyRmdKK0pRdllzU2c2SHRtUjgrYlFoMkdZSXFLQTh6U2RJK0dSemRLNjdCVAowTmQyUTNHY29hZ3NBKzZBNmlPbU1vMUdlVHcyK3RSVzNlUGM1bWdvOEFYZGpVWnNmZWpVMGZtdkhOTm80a0psCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3JWVU9idlNoYTk4T1VaQ0Q3MGkKRmFKL3ZBYkxmUHlnVGZaVlRkZFZ4aDBCWWI1WHBCdVRiZzhIcVExV1lueUt0SFdiMlo1UzZNY09SVThRMEViQQp1bGJWMzhFcXA2NGlUamlpTGZ4cXRRYkZWcWRyOGFnV3RJZW9pK0FHQ3lRZWk0MWNWSWt2MmFIV3REcVlrWnMxCnRJSFhYQy9GbWl3T2dpVUtYZlN6dW5QSVd5cDN2TmtUSTJMUEU0Mnp4V1BXN2Z1VTl3RTlHNVpNZjBuRVptVEgKMzJzc1dzZ0cxS1pSeURycGNENUhWbHFLdGdMcEJWSWxGUTI4QlpweXN4b09vTFFFL0Vnb3lza2lkOHJwZ2tnRwpVVzBCS0t1dmo4MExlalZaTkNoZytRbVhyYmRqWHZnck1SenRKYnBucmtHVFVGWTNMYWhIeXhHSGtrK3JkSnJjCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXh2VHYyTGxFNi9TYjg4RnRuUTYKOThybUVMa2djejNRZXhNWkc0TlkvSjQyQVdQZFVJQ0tXYmZEaFlhSFp3U1k4aGRzZ204SlE1b3JPUWVxMkMwTgppZXBvSnFTNXFWNDRmYzU1dndISFgwMmhad2R6V0JEL21NS0x3cld2eHpraGJ5OXNQYk5ETis4SktkS1k2NFdRCm44aE41dkVreGs0MHJBQk5uOWErcjVEcnY2VEZWV2VhaER4M1UwMzNEZXJNcEVrcTF1Z3JpakpSRGtRbDBtc1UKcTg3NTlqb3ZnN2lvZWZuL0hxVk8zV3ZUWnNnVnRRS3E0M3Y2MmYzRjVGV0RlcFAxSFJISjBWYXJqcmtvbE81RQo2R1VhQXZHREliYm5KRHpSeGE4bEdrY1lGbHN3VEJOMGRsSkFOSmRBV0NieWRKb0hFS25iSkQ0SXpnRFZlSGRCCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHNLVHh5aFNDc0wzV0VHZ296dm0KZnVGUnBkeUUrYXQzVzNPTUdhWC9rZUw3ckFkWHlpaDBhK3RDYlBhdGozd09lZzJXQ0VscUFrL09pQkdQYUhGMwowNUhyQ1NUUkYvQU5NUy92K1pVVXkwRmVicTJxVGF4a2h4NUhsd1R0R0E4V21sd0orNkNQd0pSb1BSekgrZmdNCjByWTQzcSsxNndmS2lFNWdLWmRoQ0hwTitObXJNRytxYlhFZUp3TDFBY1JncFNuUlVHWU4yUUorVCtQdEZ3WW4KRzFNZENtKzdYMlh3MHA5N2pyWmc3dmhVZ3M3YXplbHhWb0Rxem56Y0dqZnl6ZUw0K204emNlc2pQL0VyQnlBegpNdUJtVjlVL0dJa2M3ZWJTQmFXMmltclNlb055YmpTMWsrd1FGNVRxLzJ0RlFoUjVQSWpub1lnQUtWNG9yY3FVCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbk1YYWxXeDRCLzdlU05GZGlQSmcKa0M2bWZPbXN1L1liWFhtVXVvZHdlU1IwVDV0d09XV3VscU9KNnpXQkpIVTEwUzRBQUpMdXRBYVFlOGV3NkVRTwpoMVF3aWNpYXZkTlJUYlFsNzFzQWVPL3ZldGxUSzdTNy9pTFovMjM1RTluN1QybTNxb0xSVmxLdW9QcTc3WlljCm1uSVFrdkNhVlVkaVhpa2hwcnpmWERHZmtHTTNuYnJ2a3NwY0RhWm4xajhvRDJQNzZ4Z091NGRqeXdZWXZ2cFAKZzU4b0JRZG5PajlqcHJWL0pCdUtHaDRIVmwzMDJ3ejdSTm5MLzJjamcxaC9MVkk4a0h2eEFycjNyclA0L1NSeQpxUWR6Y1ZMdkVZMG5nOGJLZEpSMWp5dDl0YWtJZEhJSEhlTDkwbFJsV3NXNmY2MTNZMGtPbWJRK1pzOEkxeWFJCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEl6bkREc2tHWUtnem1CTjRYWnAKVjJlQUNPM2tBcmpMc1Q2QTlNTnhSN1Rmejl3WExEcEFjMWtYZDE5Z2NlYW45R1NWMEpyOEwxdCsvRHpVVTFTLwp3NW5GN3pCZTVUbVBsMWZZcmtyOXpJWGgrN1hvY1VJY0dNOUpETmtEQWJEWFN3ZVQ0ZytzWUVieVdOWWZQY3NHCmJYWGQzY2ZmZnYzNzhLTnpXZDZoWlZBc3BlMGJhL0tyTUJXdlFsVTFteXNwVisxa1BWZHBsNHF0SlkyVGU4ZFMKNi83c1dBYVdHRUoycUh0NkVpMTFFeVNpU1hiSnNQUmlYdVB4Yy9xVDREMFppMUdwZFhlL0hNRXZvY1p0aTFwbgpjcGdaeUFOSS9TUW5yNWs4amkzcXZaWlcyQUNNd2ROcWhKM0xRYWh6V0g2NXZwU2o4eStrd2Y0cmIzRTNsbmpYCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEZrK1ZvSklLRS9ibDNjcEdkRnYKYjBRTit2R3BDVWRxYTBOOWpLT2kwQVNQMmtFMVBmeHZoL3ZrYWtid01rTlEwdytIbjFMM0Jnd1pXNFVLbThrNgpUQjhZUGJIM2VFQUJPRnJsRytRYTJsS21rUmdYQ2FCcHdhcUlMOHBkRnN1eTZSVTJKNGJVT2piMGE4Y0FjSXYxCktaM3FDeVBhbm5CdEMvbTJoN0V2b3huZ1lEd1YydVhFYzJrSTFsbk1jYzFMbWJRQUc5anEycnpLSlMySVVVamoKcjVZRElBTEJsdCtxRVRGL0VuQ0FSY1cxYUIybzZrT29RZ3gwSUNvZXNweFE0T3ZUb3BLNE8rWTdhS3hPenJkYQpwZ0ptN0MzRkkvT1doNmg0ZHRNbGU3UjRrK3VZMlJ0UFQxL1FMMUx1WExqek0wcmJlMGpkL0F0bjh4dWJ5Rzd0Cm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnBiZjRZcFc3QU5EQnlVeGc0Qm0KeFNFU0RMcHBkVkZNTGR4NVVpSUFGVGRPZnV5aXpvLzlvZG15ZzBOVnVFT3MxcGtoV0tEaCtRMmxhb1FzeFZCcApNakZWdHd4ZmZaNjJGZlVBcFg4SFZOVjZBSkkvZTZYWjFlLytPN3J4SUV5QTN2SVRJYm9hZUxVYTZySWF6TnRDCkVScXYyaURtMnJLNmJLN3QvWjRyM2drem5UWTluLzQ2V2QwMFJmM01Bcm5PZ2xFeVRkSUJBR09DUW1pMWEyUjkKRk9RdUFoSWVPNC9nSmYzeXVwOGhUSjFDaG8veXJtQk94R0x0ZS9QcUJSdkVSUUJLOXFUZ2VjcG0xRjRLcVp4bApwbTRSTUl2Z3FEckNJdzYyUDAxSHd2Vy9xeEx2MmJaNWVBaEZ3N3RYaHVEV1NweVhqQTg3TWpuNjhlb1pUdEFUCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWZHL0syblgxU3pseEE5ZjlDOFIKK2QxMXZQcisvUHcvUzFVVWhPVHpoUnh2RlVrNnZKclIvZVFXTGduVm00UGJRQjhNYTlaa3NwS2RoTGNOZTNEOApRQVdaY3FjUk9YcitqeEJpTGp1Q2JvMTI0M0RnaGZPZ3ZqcGluVjBLbmlFcGd6WE9GZ2QvNXV1U0tCRk51dWVlCko1aHBsNTNmUi9kZFhjV283YVZONWtHZVhhamk4eFJhTm44SkN6Rjl0YmVqUWFqdHRWdHk2eEFselFLVkNkakcKRVpBaFRJSHJNbExoUEZWaG52MnQ0V1BOT0VEZ0sybDF0WXpwbEVCYUo4STNHb1hYZm96aWV4UkRTQlFoWkx1YgowS3pYQWlZRXlOQnBUZHVhcDVBYy9VUlFwNDA3MXVBRGpDbWlYS1FzN0MzV241WmsvRitaZUxydFYxTWY2VjBICnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGR3eTZteE1nVHVlZjB1cWVyVmsKQms5ZzNaaERlTGRsTGp6TUpjbHdySGdJbVVxdWtjTXZMdHg2Zkh6amxXQUZVR0FlZUprVUd0OThHTkdLVHp2ZwpwTnhmaTVKL0dTdHY2VW5iN0hoaitucm9WQXVZaGIzVHRUVGVBU25BYVpUZXR1UmVpaUowUTNmMTM3dnBWR1pxClFac2xwZktQRXF6cVdFc1pEWnJhWUdFaEFUdXJtSU1hSHQzcjArTkYxY2RRcG0rM2VrcE5JY01IYUltVk9iZUYKUkxwci96VWFHZWNkRGNyUURQTE13RmpUZXEyL2ljODhlVUkrempOM2VLbUM0WldtOW1tdkF1dEN1Y2tRUEVxcQo1QzF2U2VIRXR4ZXFHVWptZ2dLcFVvaEthdWpNazZyc25QdDdLTW5YWXBhcWlDazUrVk54WVpMV0M0WlBvMW9wCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEdSdzZhWW5wYWlpUmxWZFIyMi8Kek9RNWlXdTNTUE83UjA4TkdmVFNld1M5aTR3L2x6bnBXbmk4L0E5am1KajMySUJteXZzT0FPLzVKVzZYdGxEdgpodHRyQmwrTWVTQlltSjNHajlobGVXN0xkd09VRWQ1MHpUL2RHZjFNMDVRdDA4SGFzS2ZYTWdINEx0eGw0WUd3CkJVeGtPcEcxZkJDMTQxdjBkZmNKQXpEQlVxcFlTOUtraXJoeWs1OU11d0piYUYxcUZ3d1pnRUVaNDI3U0dmamEKVUxiTWtYcEQ2VVBOamhlbGhZcGRsd2VSeDdJb08wNzRSaW5QY3dKMmMvT25pNmpzR0NOTkhmaEFEWWFleHJySApIekNYMk5HWkpuSFIyOWZDZTViNjkxbHRjM0x3dEtQWnFsb0YydG54cHZYUWx3WUxzai9RYjUwSFMxZmtkclUzCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnNrMzRrZzlyakhsZGxvOFB5OXEKNkpzd2VxeXJoOWVva1d5aXNhZkNWVlN5aTlhUjhNQjJaMG80UGREYjlHZVlFdGIyejhFNHZPUFVKVnJ0MWl0MApqRHN2cUtNcVprcmNicm1JbzhGQXJIM09PZWNKZXZOYVl6TzY3VmtsL1BtQVZaYzRQbVI3YWQwU2xnOGkvWituCmMzZ2tPc2NPSnRCeHgxeUpkME9Qc2N6VW9oWUUzLzdZbm9HejBkUC9KaytXMEkvdzhXcDVRTkVBMEtpYTY4OFcKZlVOOTRsYTNGb3NUdXBqemZnSzlmcG03NStBSHg4UTBybHA5dFJWVkVleFdieTVNTDdUdVJQSnBGMC9XNWZQdApYWlg3Q0R2aVAvZElEbzNMTTR4Nng1VWM5Uk1WNk5mRzgxN2JtMHcvN2ZSbTdINndGZzMrRXRRZGwrVnBsQVk5Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkh0ZW1FL05LMzJabXJMdUN5MlQKbXJtTnQzQ2c0R0lrV1kxZE9GSHJYS0QrdCtuUXY3Wk84cXNmd3R3NjVNQUtIcjRpdUJkeExlallUNUZrS3lSQgpIQnVNbG4wcTBkQzJMcHhWSjhjNEdxZkpaY3d1MzdLOUFPZE9JNXpNWXA3T2RldHE0eE81M0VtVllWa0RMQzFjCnZ6UzhZZk5aMzlFTWZSZHh0MkRxUUUvNkFqWVRUWGxDRlhhLytCR25JUDUyY2UxZWZKaExVSWQ1ODFqTjI5cEMKQmhrcTRqY1ZIWkxiaUE5ZmlGWnVEZGVta3VxRm5wakc1WkdPNjJVZXpXUzVWa2prUTc1UEJHOXQ2T0lVYWdTOAp6VlhmNkRxYVRqWENHdDFjbk11RWwyMGdBclE5T1RMRVlLbUMrNFZRQURwZWpoWWFMQ3Z5V2prTWMwVDNVanFFCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEZrTG5GLzRFemFXYnZYQ2cwdnkKREFSbU9YZ2lWVmlHcTdwcU52WmNIR1dNdEpFdGRtaDRXaHhuTFBudFRkdDNkYlhrK1dtSUkwMStWcW5mQ1dVSgoxa0ltN2drODMwakpCclNsRzRyRllYZTZsT0F6SElzYTBFeGU5QTA2WnBhTkFXNzcvU0Y2MHdjeGlLd3hjK3QyCmoxekN2UVg2aXMxdExTWFRHeVREUTlocW16TUcySm8zaTRRYmpEenVNTFhacWgzQ1JGQzhTd3Q5aWxXcTlIWjUKSXd0Zm0yMnI0cHBEVkRJeWN4Zm1ETHB2MnMvNlpjTURmVXh1dzN5S1AzK1BKeElZeTA2SU5jS09lQUVndmFwOAo0T3dQK3RPdFBzaWpnSFpCRFF5elZ3R3c2dmJxd1NxZWNzYzVZOWFnT01TS2hGSENVUHRoa3JZRUVicytDV0VQCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGpyN3M5Z0IyT25VTzBPT3dXdUgKelR4OHE3Y3ZkZEVvT2kwbVpBMktZYXFWSWdGeTJ1NWVFVUIwQ3ppSldvZ2YrMjl2QVNjQlI5SzZaYWNFK0JsVQpJdUhjL2ZvZWhLS2xCMWpHeE43ZWlSZndZSWZzNVpZTk82M1Zia1ZWNTBRaVhPalBtSUVJaHFxWGFYRkNqcEUrClArakNaaUdmRlVaVGU2Y1pQTVJpMVpWZ3FQa3hFU1lTNndvaTV3eVhEVTZlTHJCaXNidklKWVhCU0F0MDk1T0YKa3NDQ2E2ckQzclpBMEE2dUxwWnJ1d0h4b0F1Y013MHpaM3NlSHMxOFpBaGw2WURhTCt0UUJOVWs3akRtVjF4Kwp3TTdabkRpMnVBSDR6eDkxQ2hWSUYxK3IxdUEwRndWU0NnSnBsSmNPd2JZcndwMHlEaUFURHFDOVBQMVczNHRCCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXFGTHdHaUxtYTJzTFd5a2ZKUGgKck9mcUdQWFFDMFZzWW40SmtTZE9BaVgxOGNkYXFBY1owRGZQK0x4Mm5hRlU2M2xNNFNzaHhkQ2xVbGlwL0h4MwozMGpoZ2g1RmRQR0tJbXpuYjF3Ukd5VlgybjN6d2k1ZXhBZ2E4THdGcnJxNWUwMGg2aXVCa3NCNjl3ZytWOEVzClJqUG1EMUVIM1V0T21DWFVMN1N6dzRPbHFVT1JKK0ZGV0ZBbmpVcGcvaXJnVUlvanJLN1RHNTJOTWNqa1dtcXIKc09HL2svcjM2NlVENG9jL1pBRGFIMW15SXJ0Z1JTNEhxM3JFUWFkZXRQSHVudFJqZE85Z21ONWU5MG8wdCtHMQo1ZjljMFBYdzVmT0Q2SmRBRm9CcUF3QkVMYWxtaEhpSTlHN3VYM3dBNk9lTUdZRlBpRURpa2xsQSsxOFFaTkRKCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW5EV3QvVkNLenhaeUtmTHQxUHgKVXdyNlhrSlI5d0lQWFg2enRaYVdOUUkzb3VWcGYzajNJVDJ6S3piZXJZcFhobUx5RGw4azljUjJNRndOd2ZkZgpOa2NXRmlYL2twb3Q3TDVyZTZ2VUVuajNGSUxxdVpEa2IyTElzbkFQc2l4eW1uY2dDM2tDU2I2N2V0K0ZENkExCko1bzlKelZXdDBXc0RLeG5UT2lZRitoSVZ2KzdzcUNsdVphVnZqZkVVYktEM1BPUFV0QllubHBlaG03Rll3bk0KWmxIN2RRdXVLekpUcndTQWpUamczRjQwb2ltZytSemNvRmJRSHFKY3JJbG01UUNHTnVORTY3akNtM1BCVVhNbQpkYzF3MFZyVm4zZW9uem5LMGpBSE5EODA2QVhNRlFaU0wzbWdDQzVmZEZlOVRpQmJrSGQ3MTJhcy83dEdJaFVoCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWNoN2VPekgzR1Z3NUVJdnIrYm8KVEdRb3B0M1grQTAwMXI4cGdrT3ZoMXFsZmp3a2QwV1Nld3MrMnVhVk43c0lNcU5hUGlCanFCeVdPWGVNUG5BMgptdFBZVFhoQjNsTU9YSTYvN3NnQU8zQmxkMHJPMGxKVndYVEdvRHVHRnltYmUyeVV3akVTNTRvMXVoT0lpNHhDCjFQaU04US85Rm5VZ01PbFlGaGYwZXFOOGlSL2JaV2l4cjYyWjFQM0NJSGF5SmRncDZjc09kNyt2djFXcGw2THAKdnFkMHF2YXNlNnkrZmxUcytFdHl5Vi9YcXhqVXpuM3dUeTgveUxxSWR0eDlpbW1hY0J6bEJGc0wvYzY1RlBaTQo3ODVLbDJsQkNTUkZrYTlRK2dRQnpKRGV2bEZDVzI2QzdwTmhscEFBU0JxbXJPVU1pYkJacU5MZE1uZ29pR0xvCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnFpZlFjcFFHUzdXMjF5bUY3Yk8KcktPOHM0d2ovMVUreE9qWFg4L1VXSjFaR1pnNkhyaDU2bVZ6cjVsTjJYQTFVeGFSODk5ZWVnMnprMXlBZG5IRQpzMVBwVERmWVBDd3Erei9uK2dqU2ZUcnhTbUNWUFcyV1B5RkkrNUswQy9SMXNNdW9icnhjSDlkeHFidTZzb1A5Ck1RbUtHNHBjbnNhdklsN25RZHVkQTZTWHozd1BubnkrWkpyOFpyQUlyL3A2NU93OFJlVXdaN29qOGJmMkdzQUMKVnVua3V2emxjekV3Rkg5dHFKMGo4dkdNdzJsSmNLY1ljbmk5MzI5eVI1KzA0UjdmeUdDZUxmYm9NR05RaFJpUQpGak45by9hb0tXR3A3cjgzeG96ajE1dGdLSUtwQ3hoSEdieGZicEVpNlBoZWR4dWt3K1NhbjJxYW9HZFlzYUY3CmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmhVRXNkZGZlWXdtdURGUCtaL3YKdnBTRkpieVgwVGtERHRkVXJRRm5QeFVXMFRtVUlxMmsyd09FY01pbVdvY3NwWmRqMFd3VTY5VFpDZWpLMXpIeApMVms5YU4zbSt1aEt4OEtMdVhHQ1ZQaFprUVNyNis0RlEzSWl1SGtKUTBUUUl1UGpSdldmZTY1dVU5dFNXQ2swCjJ5V2lxVVAxbFBHcXBVWExKcVUvY2VpKzJVWEMyWTAvUlhjTnFEdFVTNmRwdkpET3NVZDdLbmJMSzBHcUQ4amoKQnNVK3cwSDEvQ3d2c1B4LzRPUnBFN3VIek5oR3lyTWZlQVZ6UXYvd3czc011Rzkrb0ZoMS9sNXpXS0poWmU3bgptOGFHK2tVTUdvcTZUTUtFcmtaaUQ2V0xpNmMxaVpJazJxT05ldTdyYXk0UWdBbHozSmx1NThOd1lUUzNXZUt1Ck1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzJqZW1yQXdzVVU0aUk4Nm9wQUIKREljcXRPUyt5dWxmZU9TbFBSdjdkUWhmbVVqT1hXYU90V2wraTN2S2ZYbDBoUUJHSEdIOTlYMk55bzhWRGt1Zwp3emxFWDdiVkVja3ZYckphOEdRUTlkaXlvMkd5ZWQyQUJ6Tzhxd21neWxhUlNOQnZGTEFXYmdiTVQ2ampBQ0d0CndhVEhxZE54ZUJ4R3pvT2kvR2RpMkt3Z2NXd2tMby9SU0d1Y2tUNUR1bHBhTHUwcUJQQ0xxa0JFSW8wVEw0SEUKUVlHQzFWRHh0TGhwUzVEVW1SalQya2ZYTzBSVFpHWE03OGord3RUTzJyTjF4QnEwZ3JOaFBGYWZEUmZGMW0rMAp5TkZ6eXZrK3ZJQTRvczdaRWRBV2twYkR3NlZnMXRTNURVRjc3RkoyOWwvRnF3T3gva1ZGQ0tzRnowTndLbHZ2CmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeS9taFllenBsYWl1WXpMZklrSEsKSzE3YTFUMW40LzRDZnNITUsyMnpPVms0SmdaQTZrMURITXo4SjM5L0FweVJwRk54eVAwL2pWMWtsdUpOc1ZtNgprUUtNOVJncHV4ZEFlSGVldCtyWFliSUo1WWRNeEJ5SnMzcmUxWkJTV2NMa1NDNzFZZDNXV2dDNW9HbkFsR2poCk1uVVpCQzU5ZnpvL2xzY3dVbnA4NHdFUFdxVUJmZWpqTGhrTXRlZ3ZZdWFNSmt0ZlBqbEY2SHBCSmw0NGxEclkKdWROYWZvVVpwdmkzbTBsR0VzclpoWUVKYzQrTFZMbHJySWNRdzVteE05eitaS1FxLzczenBCYkRwaVF3UWF0MApOM3Fhc3Yyd041SVFEaThPdGJjeVFkRW51akt5ZVlZMHFZdUhJQ0JIVTR3THJyd1QwMWlxNXNPQlVYQzZRS2FFCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjY4OXJkS3FzKzZ3TExCd1E2Y3gKdFBNTkRHT3IvOWVVMzFSWWVEaFdjcDhVeHBud2srMllmYUJVcDlrWmNCdUNrNngxdDhERW1tcnNYSWNxQUJJQQp3NzVWN0lJQnE1dEdvN2RuRHJzME14a29aSTQ3TnVOVm4wRXdtRDJueHBGcVQ2b3NIL1lEUXNDQ0prZURHU2d4CnVydVlDbXVNNmNIQ1JOZWJCU2l5RzI2cWtpQTVPSERHcTFaK2c5azJxRTYzWEM4RmxaNlNqdVZoM3A3d3ZhZTQKbEZqM2pGcFNEY0phcjhqTXJ0YWlpNmVIN2duK0FQNXcybnN2NTVIV05OOWlqbExYMnU3Y1BwZ1IwUXgxUFRwNQpvckVaSzNlcDVwTGdsR3RkeThJOGJjMmduOTg3UkI3VlVnMVcvVzhHMEEvS01sUzZFSjJRUmlHSnhkQzNFcTlzCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczlxTnJDQThVTDNvNjJtaXNGRTMKR3BoVEt5aDdSMW9oL0lzeVlNUUJMdFpUT3BCeHVzWkU1WU9hbWR4VTRCSnEwdTU1TDRhQVhyVm0vOW8xV0lXWQpXT3YzMytiTlJWTUpTQlAzbnFVVm1oa3NQeUN0d1pnWGlWbFFXd3ZZWjVPaE43VHVEQStBUUZLZWpOSE4wWVJ1ClB0QXB0MWM4R01PREsvZTJ1UElZb0hyYWxhQ2dTRTJkTDd3elpoTW9JcytKQTBGSXlmQ281dXpRQzd3MkxJWXkKbG5FbG56VlNXZ1Y1L09FMmd4RjhiaU1ZRGxwSXVwTnM0bGk4U3NmQ0FjZTFRWEt2VThoQnJRN0FaMUpkZnplSQpYbS83NUltZEg2Y1lTSEdzWlFuMDYvQSs0S0FoTVgvdzB6QWdjYlhiU2FPOEw5dzJrSTVudW5OeHFaMkZNS0FXCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenI0MHp3U0Z6akZrUlMvL3EvVTYKbmswK01GejBsN2Fsek9KVlBzTTlQYThrMEUzOUpHc0UxYi9QbGJiak9oV01UYnc5d21BWXlCSHhqVHgzN3B5cwp3TU5rRkdBOWxBcUxmZDlVWnFoTnVWa202SXkrcy95eHZpSDl4Z2c1SlVDUU91cGZENWp2N21WdVE3T3lsUFN4Cmg2czVnRWZCK0t6dWRVL1pjRms0RmF2YU0wNGRuOTA1T2hWODRaRUJwUEZLZXlna0E5SXp2R05qbktjY2pPV1EKZkVvK280bGZqTCt3a055SXFSSTFBd1Y1VU1vekQwellkaElJS3AwVi81ejk0YmpDbzBnZitrbnZYcUlaTzRwZApUQjAycnJ0SjEzMFZhQW9Ia0x5YmJ6bXFHTktQWUF2N0ZhRU12NWNweTQ1SnFXTmZmUVN6MmZRTUx3Qk9MYlM1CnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXRUMXBkMkRQdzhtVktwL1ZvRmcKT0RVR2NCcksvWElRcDRKZ1E2MGRnOGNxQmxPWEtpNnUxUDFxQS9FalBDOXFTZm1oTkZsaWJjZGhFYUlTbFRYZQpVT0tjVWRLdllhQTh0QWhWVEF3Rm44K0VNNVNaUUxpS0JRUGs5eGx1V2FxMWhRQTF1SHNVbDdqYm4wY3IvcmE4CmZ5MGdac1lhZ3pCUzkrT2ltMklTMk1vZUoyRmhUd2J5cUxQNG44bGh4RXFNVE9YdEN6c2lPTU9kOFBRMTRycEEKR2xFODVYRk1vUzhOdTJPSlNqaTFJTzB3QnB4SUJJdEZvV3M2bDFaOVlKZHRvS3QvcTlsMFFsTldTdUNHak5mVQpsQ0x3NWQwY01CcEl5Q01ZSytLMEsxQ3pCcjhCZHlLd0ZLNXN4ZjUxeWs5RXc0VFRFUm5HRHpEL0txOWxKeERuCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVloZ1pqeFlzb3NCNVRwekc5MnMKSlZoang1aWJnbHltUlBKc2xzZ1RiVnc3SVJyVXNmSjVoSUpFOENwRVZXMTZYRUE1N25vQ2lPT1JZS1dsdnBUagpRNzU0U3VMMGVBMVZHVVIyUkRzUzFrMS9EMnltQjQrL0dnUEU5eWY1ZzJmVk9FaFVNeFpKUEUyZnBBT1h2Q052Cnp6VVNDOTJBRm9JOUJoSStkdmRlSVlXVFZKaVhudG9mcm8vSXl6N3dSZ2Q5eHVMTE9CVnc2RkdQN1Axd0g5ZnkKTGtWSUFXQTlnVHZmNW55bXJGekhIb24wYVdBUFExY3VWV1ZJUkRudUNPZ1REa1VQcGdVK09vUDVRTHJTc3pQNAorQUk2YXJXSHArUnNteWw1aUJ5S2JxdXFXZ2hvdDRONlhWd041UGlRUEZMWmtkd0NBekRiWVN2ejNxL3oxVXd6Ck9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGExSHZyUkZMSjBYczBNNzZCTncKdVRCR0xPYUo3YjVsREdWRWUvc21ZaUwyTDd4SEo1ZEdvb05yckZ2M1l1Ui9HTG9zdjNhOFF4Zjl2eE5wYnBlVAo5Vnd6dkdvY2xScXNFSlo5S1RWdUNEaWNWZkZJdWg3cTBXUEZCMnRydHQ0cy9idWpIdStFWnlEU2F5bWdITThYCmZXTU1OUU5ibFdkdW5WdytvWnd1bHk3MW50TlY0UysrS1BHdXAxVDVLRWlLeDZRUnJPWGhkS0VQMFBRNHlwSE4KUEU5MndHWldqaHl6NWI5VDFTcTl3RlF4M3QwT1NOdjNQRUk4bHBwK3JDMWRCREpUS3dkZEF5VjZncVhveG9sdgoxWFVjN1JLYlFpeFV2bWtFSWY2bUszM1Rvdy81YVRpNlZvR3pndGI3c0lOL0M5K245dW9RcFFjdTdsdEIwRWg5ClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWQzNTJjRWFWMFZna21FWVVNZTUKK2d6VmNCbGZ3Ly9WSWdFK1h3NnVRQTY5bGI1aVlrYzlPWTlJeHBJdXJVcWoyb3dpZmRDWnNRRXRJOHFuRUtSdwprbDRwRUF3NXZ6UE84a1kxRlAyRlUvTjQ5UVlMTmRURDdvalFsSUFIc0ZueTlUcitWaDRxa3JJcURVcFVZT2VlCnpFVWxIbGEwSVBmNnlwVGZQVjhoS0I0QmNFN090cXk0am5jYkJLdXRESnh2aVlGdjhqVHF0UlkvazhTcmNCb1IKdWZiSDRkNHI1NlhFNDRtd1pKb1J3dzQ3RjRka2dXRXVTbFFvamhrNUpBMG4xeEFVS3N6Wk4xREF1ZUl4bE02UgplZDZBR3Q1Y1dXWmlWaUJoYjdIdXk5bm1DRVpFVFZHZnViUmtHTVNXTnQ1aGV4Y0tkTVg5a0NualJObzM1aXhaClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGJUMWRSU3ltSHpsMldwS09BUzQKNVpXMkRGQ1JCNmgrY2Q4NExJWmRzVnVqQ0gzcm1CeUxWTUtkNUVIM1FnL0tvZ0t1MXJQcGxnYkx6WTF1aTVPcgoyTzR3YUVwL01WRC8rZmt0c202ZDMxUzdZS2VlaDV5YVhwVElyUjk5MzVZclkyMElFKzQwV3VzMnVOOC82U0pXCkFxd092R0MxNWs4YkY2YVhsaFJJUzFFZHF6L1BhZ3RNYVBWTG5MUEpyRDVnSG5TU1RoR0pER3E5SnhmWVNlZmIKUmxsT0d1RWJtTEJmS0RGWG40TEFhNmJaYVpwRkYvcmhUK1BtWi9Ydy9OODdOc09QYVV2M212aVBaMVhSU3BWQgovOWJtVWE1U0JRRE9adVJrY3BhRXpjT1JuSyt4ZHZjMHpvZEVOWnY0MjJmaVFpMnMwVEhGOGliNkY5VFRsVnZ5CnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlNpRWFIZDNvSHFGVTltQ3FFbXIKVEpnTTQzQk8wV3k1UUpOZGs3MXExY2E4SUh3aDB4OTc2blQ4V3JESEZMQmZDN21SN3V4clcvS1J3ckF0L0NpagpoMEUvV0FsWHBGbytiS1NBNFBYNE53eHRxTnhLbzlLeVNrVlQ0RC90bzUxekFCNFY0c1dkQVYrdGpOcllYellzCk0vcG9Tbkx3eUUvMmo5MlNrUzlmL2labTFvVk01alpyVTZkdTlBMHNLYUJUajJ2VTVHeGxGNjlPN1pWeUYyekQKdktkOXZZd3hubU1GUWNoSndpOGdyS3Q1dlJkZFhkVDVBSFBqNFVCbmFtc2NkanlEZmUram9ya1M4VEtKUGpiaApEWHp3YlRnSGpTNjZoR0JVWXIySTVsRnF4bVpzOEdmMjBBM0llR2JqRURTSm9JQmpyK1k3eFhRQnRsR1lBRjIyCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnFUd2VGalNPazNzTFZKOWh6SjIKM2FxVmR4T0dLa1hpS2VWbXEwSWhKa1oyOVdWOCtoUG9QU1lSeWJLaDRKZnNZVlFRbHZtUFRTUUE0cFEvbzBNbApVVFFNRSs5UjNpbFg0U0tzdnpOZWd4M25QNENmTTNTcjRYckZ2ZjBydlY3bnUrbGE4bnBic3h3NVRzb2hGNnNuCjlsNHcxRHIwWjkxOS9nRzBDWXprWGZ4MnJJaGo3Rnk3NnFZUGIzbzB5WjRZV3g0cnFIR3pmd1ZST3o1VlRwKzEKL05aNTIxNHlzY3EvRDVldWFRdlZnY3FlMENqcGdGSG1iRmlmelUzRHQwOFVlcCt1WUR6S253RkJzMGgxSWNNcgpVQk5WRzg1UHgrMTBXTHpzOVAwOTIwWW5BK1JGT09zY05PQWxvVDE2clJheWhXcEVRUmtIQmFoeTJiY1dXS1VKCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkc1S3pHcHExY1BuOFlLNEk5Z2kKa1hWK0xrakhqSHlTYUo0clpsN2lSTm9XQzlwbjdMSlRZdEIzSzdqRFBsUDN0VFlUVjZWeWhpd281UEdRY3FTTQovK3AzYVdtS09jSFlQWUEzd1czak9WWldMTkM5MzV1T2M1b3Z5Nmd0bVc3aDBwZkRoUWJsR0tLajVLdzVVUmxwClFUbmVmS0ZhVEtPNFVuVXdOdHMzY3BWUzhBUFRvUWxUa09vN1NKU1hCZDRCeWtLTndxcXM0V2UrZkczOVI1VEcKZHlQbUxldU95QTJCWnFZSlZ1Z3VMaTkyTWd4WVZmN0FrZFcxSkkya1FrZFVxS1NWNC9sOTVhQjBTUGE4aFJyeQpYdzVVNkFLRzdKZXZFTm5Wc3NCc0ZraHQwRHZFQmN3VFl5RDRuS3RJaDc1elFYcDJDVlhaWVNJUituREkxeS94CnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXRZb002R2xncUY0aVBCQ0I4UEoKMWxtR0doTVdyUXFhNzE5dkhoUCtuTHU3K0dzUlBXS3o2SC9jaDhZYnVLL0s4MThPNG5NcmdCNTJwNTJTbkVxcAp1L1RaNGt2cWU0YWR0eFQrMEwwOVhmMVZDR1dDUFFGQUJ0c2EzV1pZY1BjUEhFTndiTUhOM3daVmdmWlNsdGg2CmJtbXpMbG9aNGsrclpGUDdkQnZjSy9Rc2JDZ09RYUxxWHVINjQ4K3NQQS9OdzBKMzBsR01RR0pEdXhMNzdjWmgKcHVkcGp3cFR5dUowQ242OGhndXhYcFRQV3NoN09od1Q0UGhKemhhdmRycmZ2eStPbHd5eGNTc3NKQjBRdW1mbQptK09VbVBVdUxIalFBS01yTHowYllaMnRMMWp4SkNwd2VFYmkxWkJuV1N4enpMODRnMVZOcTU1Qmw1cWwyMlNxCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVVlc3Z2MENGYVgvRHlTSGJNeFEKSXFLVnNNdkxCbjEyd3grUVZSSzZpTmRMeEdFTVBGT2ZOcG5SbzhZajNzSWx5VGxaNEFjbnAxUTdBL3UwS3BFTgpsaytHUW1jczcwVGh4bHFSRUFQclZzbGdrUys4dnF6bERxQjM5b0NKYUNQUFlLdE5QbEZPOHdkR0xUbllacmhICkcvbzk5eEl6bVEvVFNMalBYeENsNVpvV2ZYa1dsdnFORXlaeWlVaE9LWFYrMGpyRVhjbGc4bU5GV2x5dDFpSlMKOHcvTkxRTUQ3dHNuanJhWHFMTDFBd201ZDhnVzYyRzI2RnBRUWdyRGIwUnlZK0R3QVFYUTBXSkVsdDFnQTBYUgo1T0prY0pMWUdnRCtjWlFhRHpxNHp1a3Y4UVRidGc5UVVuMkZzdGtvWWpLS2E1OEFudGppa2JFYXZ6c3BQWXNvCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejJybUxLazdhRWowcmZIaFdHaFQKQnpWMFF1L2NuTUJyOWdmbE1kVnlTeEZDU0Y3VHFzdkVhVUFiWVhpbmR5dzUrRXcxa01BSzVKYjlwUkd5RW45VAphVGE0Y1g2N0FKc2RFZ0tmNU8zRGVyNFZtVzVnVUJnTGhBMWhERUhSYStPa0UvcjVvaEJBMzIvcU5rZ3FiN051CkNVT01IY0xkcGF0ejNxN3JRekc1UWJCZ2JwRFpTbk9tUERNZEhsZFRTVXdDMEVDQUs0MStNUGR4eU9NNVRRdmQKSWowVElUZWpkOWtNL29aMjVJUG5SZGV3VGw3em5LY2wxQjY0eWRWMWNiQXIrNWhyeWNYVlRLMWNUYzlTUi9KdwpneEo4NGZILzl4ck9vaVBNbUY0aXdjRTZ4VGJZb3UvU3pjWGVBTXZLRG94TmNZaXJmK1RVNXkvd1M3bk55aXpPCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkxlWWh5WElhRlh5K25IcDlRbU0KLytMOTlWSTR5MU1ERWNJY0hhZ3FuZnpVS25qK2JYamF1SVMwU0U4MWFuRWNkejBza2VlZllLVXlXVWw1RHpKYgpkNVdnZEo3TGVhMlRKN3VFVWVPcnBaT3ZLNFBtZW9SeXY4R0JJNThBNjBKVFpYR0V4Y1NvTlM5UmpwK2VBUXpUCnZ2b2tISlRGY0NwOCtRRjUwRGhSWnY2ZnZ5dE5iNEFDd2c0ZDhoTFZMMzdjd2QzRXJUNFQyU0pzVHVLNURiMFMKZUZNOFN3d1p0NEE3Nnl2dHNVYTFCVW9qT015S2xmY3ljNnJmTVJMYXdUNXdtdlpOaVpHUFl1TjZNOVluZTVBcgplWlBURGJ6NUNLZHk5OWpySjlYcTAvZUNEb25DU2NpWW1zVWlxQzBYWlU1YTVIbmVaQVI2eW93aWVFb3JYNE9SCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWpFcmpYWVRVZlA2ZVFDRXByaVAKRGNUZ0hXRDBiLzEwUkJwNEVtTU8yOS9VUVB0L3QwL1BaVGcyK0U2NVRzWC9mT3FIRk15M2RUUXJCeWZXOFdTbwpIdE5OQ3VkSUhvc3AvanRBdkVGVTZXd0loN3N4KzR0NEJKeHc4QzFPRDRaME5jZkJWUlY1SFV5RmhTRUJqZ0hWCm1RUmNSS2lrYVNPSVFjM3BQYzBTVWxUZDBvaWdORVQ2MXpWWFdTdUNMSHh1ZUwyeVhqOXFEM2xkUGZGZkluUEEKV0FPTFl2bFhSRFhiWWl2VEIxSkIwT1lLYnVjWjR5ZGNHSUk3WHMrVzhCUE1HTjY3QnEwN2VkeW9kcUdsV1NUUwp3Ukt3ckRWTnptMDlFRmxTYktmTFZuQmFVd3AzbzBrYndYUjZySVBUTTF1WlU2TzRNY2lHbFBhTkZlTGdVMHVBCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFlobWJ1Z3R1MjhuZngvSkVvaVEKOXdxb2trSDFJcXF6MUZTYmVnZDVrMUZZQzU3ZzU2R2FDVmV2TVQzejUyNExnRzhBbTJlRy9hWHM4SmQra09pUQpUL0Z6RkZPMUh2NGFRM2syVUtTNG5jNjRHZlJCMXJyTFZ6OW1Pc0FvV0N6blNHU3FhQ2VZaVJpcUJRUGlRa2Y4CmlCVlNXWkkyUUlKeWIrTTYyZGNUcmhyTXQralkwbCtVVHpUZFcyR0NVNW0vdnFPbW1nUWViYVJSVjZ2c0ZNMm4KMitSbTFobDQ0aU1URGdWS2M4SVhOYk8vbk54emdGOWY5dkVaN1V4dFdneTkxemRNSEZmRVR4ZUVSQVJZeFZ4awpVUGhYSmJ0dGtlcTRMTTh3QXhYeU9xT0FZWEo5bHY3a0xad041ZFZpcy80RUFKeWM5bVV6ZVZMNTNhMG5LTVhzCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem9HMWFidzYrL3VxL0xFWXBlU3QKaUxhMVh6aVN4VVBhUnZvWldvUFBtT2YxVkd3MVZmSFhOK1JwQTA1bmdRVTFrOXdMbjhnWXVuZGN6YU91c3pERwowWFc0MTR5bG5xNmdwa0l0dUh4empYUCtKV2owVHd5NmJDeUxLWWx6Y2RSRFVNVnQ2bU8rWEhnNXVROGg5Q3ZTCnNFeEtPZzhGZTRSVXlRejZGbzJKb2NIbERuOHhvS2FEQ252bTVsUDlVSXkwaXpGa256NmJSN1Zza0UzZFEzbkMKNFhEcDY4dmh1d2NYeEpLa0NlTnlBVm1Ja2xiYVF2UEpGemNJUEFZNXZkSTliUVNtNm82TCsvV2dOYWtNSW9SRApXYVl4MGl1alVqKy9WS3lDZys2SXZ3R2Jmcys2dkZRMlJ0TDdJSnk2anRCUXhORzA2OTdaMzVuOXlHZHhPVlloCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFVEUnlnVTJQUis5cmVvNGxDUEcKSThyVUw4ZjhlQ25ybTlJK01nYVFLVFYwTWQ3SkNNOS9YanZ4WFZYSmRjSjRLb3pBY0NnMWE1aFI0eVpUanlSegp1YlVEclFtUUlkQUs0K1ZBREdJQS9FTW0yS00veTlJRU5qMDVILzFKWGRWd2dlS21pY2wwZktlVkw0NlVkaUd1CjB3cFo1cGxHR2JGbWhROTFXb2NmUFZTSjJQTHV4NzFyUVNzSTlTV2t3RENueUdsalZaZDROSUZkOFJiSWNwMmkKMzlVMkZTL3R6L2l3bGRRNGZLVkhWTUhibHlZVjlSc2hoTTZ3UG8zNmh0Ty9tNEVQb2d1bEJhRDV6d2RCaGdYbApUZ2RnL1JGYitkT0xhdnZvWlFCYlkycDc0NzhmQjQyYnYyREtjb1B5M28vOTNaY3NoNm9WS1lZOVRualc5ZzZGClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNENhTWFQMHVhemtXMDZPY3NGRjcKMy9rdThIOTJlOWFVTGFOWk1DNG9uNlFOMDV4MklzRHFndHc4ZEtDOWF2NkprbjhjRE44bXFCdnNlT201WUMwdwphY3Rjb09SZGs1cS9HbWZqeHpzemFSWkhlRERmT0FyZUFyUko1WG1helFpQ3IvZHB1bmY4Y3NsZnJXSS9tZFMyCklBcHpReFZCc2ladHRkc0tnNjJOcVAxNFdwYXdhMldvczlyUUs1QVpjTUplZmxDcXhDWjF2emVoTTZDUkw4WFAKblp2S3JmZ2VDRE9vUE95ZWJnRGVoMUJONTB0dzRabjZTdTl4ZVg5OFloMHlTZnhnakJTTUhQZlpQQ2tHZnZEUwpjaXo3aWQ4b3ZudXlOMHExckNwemJVS3dmT2kvN0tMTHdjWXJRenAzN3BXakEveGdwNDErSGhXKzh3Vk9oS29PCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE1iUWtUWnk1NndoOUhFaTIxMTkKVWx1Wmc4YzFnZWNFeHVvSE9JajJta3NYOXN5VWRtRUxwaFFVTU1hWVZvYXdLTWRKa1pNc2RVUWFYNDI4N2RWMgpRQWtMdjFtY3dvRnBpKzVKeDA3TjRzOXYvQTcwRitpTDM4KzJIYUhUSm1tMzlKODhoTzUrc0p2Q0xHOVcxUFN5CnIyUlBURFZCa01PNkJjL1hveDhvQjVZTDRSUmxFVDVQNDFtckhOZ2hxQlJJemJxTUlWdFdkMTZ1V0xaYWJ2UEgKWTROZWo1Ny8rV0taT1luaStYYkpGN1BmVzlDRkY2QkErSmxSMnBTdTMreDdheUpyNGZCUWxFdERNbFBteGVtSwowSkdrcHFDQUx4VEJxR0pQbWswK1V6V1h6VE1ESHpqYUtVUFJjM1dYMEU4d2RuSW5pV0MydzNjVEs1alE1V2J4CkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzNodHpxYWM3N3FheGttMlJybXEKV0ZPOXB5TVN2Q3o1SlpDVTQzTE43Y3g0bWdNYlZTTENBVEk3bzY2MDlKenR1OVJqMXZ3OFd2NEtLTko1WkZVTQpxK1dyNUNDN0cvUFZIVWFvMmxWOVBucTZsbDJValJnWEF0VjdoTTl4ODE3SkZOZGErc0FwQ0hyMHgvNjE5dzhICldXRENTNWo0c1Y2WHZrVTNlTTk5NW4yRjVQTTdVZDh0dDJ3bHdBcGlDd3V6WlJGYTh4OW9yc2tlaGI0UENqOXgKcVpVUFRaOWJYY0kzcEFFN2djT011eU5wT2t0TytJSW5VeXIxVVQ2czcyUTVma1BOcW85RkRCLzFkNUlMWTlYRwpuZzR1SVRMcy9WbXJYWUd0OWdBSDVRZmRFZmF6OHhUamIrRVF2aXcvcTR4R2p6Y1BwSktCUk1tWmlzdkxuVjVCClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0ZsdFFNdFVxdXhqTzZXZHkvN2oKZzZlMnpIYVVYQVdMNkFQNngrWER3TEplQ3hFaWNXQUxUaWxlbFUzeXRiQ0tmKzVVRGRaMWlVRWRwYnRRZ2tVRwp1VVdmSTRBeDdSb2FDNVFZdFB1SlEwbWwzd214UnhSc2t5cUgvSForWXAydTdmYkRqTVNxN0dORjc3ZVE0ejQzCm9NQVJQWjM5ajM0RzIwRHQ0cEtmVmdIRHpuQVdvSFhGZmJEaExpNjZBRFpOQUJ5aG5aNG5ZU0lBT2prQ0hTbk0KaTBtMElnc0ZaQkswMjBzbFFxelVMaXF0NFZwZ2YrY1hzcHlqMCt6NWozcTJRNzN4cTdWU0taL1BOL3UvT2pmMApjRWU0NG1VTUcxUmE1Lzc0dTJRYzEybWhLeVJjcXE2T2pCT0s3R2RhNVZpOEhkZDdDQ0Z4U1hsVHNqL1hSVU1sCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkJFZGEwMW9USXlYS2lISVFYaW0KWWtsL0Y5M0VxWDZUUEpjd05NRTFQRWc1Umx1T1k0WmNVSU5yYmZERkk4UDBXL0sxNDZMUFNoWjJLSmFzbnJsYgpHTnJjOHNsTnllbW5qbk1STlhsV3hQcWx6TzFyUm9lUHJlZmEvUXA5amR4MW1vZEk0YUdYeUlpNGh6VjYrZGlhCk5rcDIrTUgySmxpQllvODQwbTQxeTlWa2haMG0yM3ljcVVDWUhhemVNRHRtb2tSWkRCRGlEZEFvVzVqd0VjcG4KdWhCNWVCVE5pVGFJbjdVSUdrZEovSU1mYlpxY2dnOWU2N25RN2tyMWhTeStlWlVJZmxoNnpqZktoUEljUWpCeApDbUhaWGM1Wk5SNW5KbmxrQUFNOEl2WTdmZEhJVEJBNVBWS3QxM3dydEhaMzcxUjRIQktsWWFod1dYQmh2N0NuCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmpUaS9ja1ZXeEhxMXk1b3l5NkcKN21sb2dYcFgwSGlmU2RBRVFReEZ6L0hGM29vTmw1WE1vSFJKSXZsU0c5dG9mZlB6bnhKRmxVZEJUa1hkVURNcwplbmcxc1l5OEF4d2c4Z2gvaXZJYmJpcWViK3hWbFRXbHBWRlh4Z01TUmduR0x2eVovd1hzRloxd2VreDlkM1hCCitDZDZUOUZGY0t6M203WnBJekthNTBqZzVvRnVqWFBFZmlSdXJiWjR2VWh1OVBIdmdWTS90aG9DU0hsVGZETlQKako1OUlndkRCNDYxa0J4Ym5Wa2JxakZKRkJaWm4vRzEyditweTJaaVN3Q0UzL2FmS09Sb0FDYnptYmVOVzZ3SQpndm9DWXNmQ3NkYmFBNkZCV2QzSlRzZU1PcVlFckR6V3ZtVVpCc3B4QlJlWlB2QVlHMktmazZxYjdlSEpzbFJlCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXE4Q2JLQUFjbVRyKzlSNjVlUUkKdVhPU0s3QndGWlQwMnNmZkpYZU92YlNHWmNzVmdUblZ0T2VLd2tFNlZVMk5MVzk5bWpTVVVYcUtiMUVQUlQvWgozeU4wK1A0MlA5WE9kcDlxdk1iZXhtREI2MHlnSnZTS0QrekxtNDhDYlNCSUxaaDUwU0ZsY3FEbjdoTkp6ZHluCmt6UG9sY2xvQ3BzSHlsQWdwKzZMKy9nQTVGZnQzRlVsR3dab3ovZURlUWZGa0dxQitiZnFwOTluNG1Vcnp0OWMKMWp5dkh3cGJaTm41WWgrdmpYU3JmSEE5ZjBOY1lHTDdXMDJGY1VQQ2VSUW8xMFRWZnVpakVTaURRbDNQTTkrYQo2aDd0ZytaYW5qTjlWbTY0TXlyRGFlL3o3TnV6WWdlcS9FQkhLS3ZqSDZCdVBoTktITExMVnhLRW55YmJxSkFnCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHcyaVI2NGc2SjRySUVTZm90bEIKTW1uVm1JUWxMUzFzdXdGWGRzVFZqZ0NrYkN3MlZkRUs3Z2FLcEM2NXVMZTI4Q1h4eFZOUS9jUk1iQnl2cFVpdgppY0FKcWszUitDTDJnTDZZeXFocjBNeGV2UWtkWHRzMjdrMFRLRWwyM3Y1QUJCV2MrY2FhSUJhVkN3VTEwNVZUCjcwWjVpWHgxL1lEYjQrclIySXZPbWlJUkhmQWxBMUhVeVNyTjhYM3h5SjFpcG1GdnA3TGM5bjlNazc1ZHViNWUKVytRNjFSblUweEFVNzhSYzJldE5kbXVVSGROOGJFbHVuc2g4RVliYlJsYVFTMjg1cDk1cVFuZWNCOFV4WkF3NQpwZXp3VVhnamRVWlRTcnBCSUcxa1RFcTZYMXFkY2ZvSjM5dlp4TFFVeExCOGc2QXVId0F2cWxpSjJJSDZJaGFXClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczd1clUybmxhQUVkaGtNSGduRVAKN21TazRDZ0lYcDROWlRBSUprMTRIYy9nazAwU3JlazB6VkVFTm9PUG05SURIU2lHenRQazB5UkthV2hwTldybAo4TEJvaFlZbzlwcExLK000SkRzb002aldYMU1aYjB4QktLNHNBeHprZXRrMGtvRG5BQlMwaFBJYjZTVU9IcU4vCnd2ei9KQ0FQdjU5U3dEK3JZa3phTHludWZTdDJDbkpUanJIeEV4Q0xCanArQnd5bFZ2WUplN2s2WUdTelZEejcKTnFDL2Z3ckdJTzU4VjBlRFVDekd1K2RqZUNJU1hvUTlqelRhTUpoN0ptM2k0bkF3ZG81NytwMmN1QnkwbHZaSAppR2V2NDdpRFlydFFXOUxnR0t4NWg2YnRSUXJLMlBZaklpN2VmTkV3Z2ZKdW45VGg1dlNRMHBsa2RzcCtkT1BBCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcElrVnVuNDVBNUxFTDBpVHFwVmYKWE4rNEpwN2R4WE9jZGkwbXprb2tlbFNFQTl1TUx4M3VvS05RNjJEQ3pmeVlzeFcrYkNmTk5VUG9mM1RmSXIxaApXQmtxSjQ4NkRSaWdzaUR3eWErQTRDRVhZTFBrZUowVkpYSHoxU0hqU3locHhtWVZoT2dWWFg5aXpIblk3L3NlCkd6SDNUTUptWEVkUnFYd1pLc3JaYTcyNmlVWkZBQWZKVU9qVVJ5UWFuNWQ5R3BhaldYUDNSaURlZFM2TWs1SU8KdkNFRmhoZzFSQ3l5dDFGWTl3RFJMOFg1dXhqZ21SRVF3VHluN0R3UnJ1MGsyVHc1Qm51eVVTQ0RoRWNoazE2eQprelJaazBZcGsvL29SdVUxUS9kaW1nVy81QS8rTnJzZ3c2OENBRTY2OE5WS0xKalJYYTdYN0ZCSCtkQ2hLNWJvCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkVJa1dxZ1VFSDFjcmZWMUNxeVEKcHZrTEZGTXZhZ0Z1ZDFRNThPY2IrZzRiUWFJM3BVV0dQNDJDWHpKYzFSM3dMYk5kTWNES3dwaFZLUklwOXRtdApaNjhweG9rVHVrRERyMHJxaEhJaTM2ekthOWlyeDdCN0ZCeCtaOVo0Qy9JUzE0d25vU3EvNXdlaFIrazI3WlgvCnV2a1JibWZNTWtVWFlDbjVkeEVTTkVuUXF5SnR4V3lMZVdaTTlBbnBsaVhsY3pLK2JmbWQ2bXRVV2dhWnA2ODMKZVhXQVpuVkpYaWoxSXFsWUJFbnE5WlJPUWJTZkJPUFlLaEYxZzdROUhpY3RGeW8vbVFZR0RmdmF3SkJkVWJyMwpSdjV4dzVLOXdLcmZiMHJSSm9lUXBHeGhjTUd5M05VSVdBTW5XK3pVQy9acnlOOEFGRW5ib0d2RjdXUzg2anRJCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1JzZnR3c1RGTklscW9NakpYRWQKNGpTR3hsNGpoK2o5NTFvN3Fvbkd6ZHhnM2M3c1Q4UVBOY0JIZGtURjIrYzRBYmRvNXpkcVVpMm9kbEIzNTVLNgoyR1F4bktVVS9WMkI4SlZCV3MwazRlUUlIcnR4TWtyRmIzSko0YXZwd3NOQXdjSmhaWGNQMVlUOFZ6eFI0bVB4CjZNR1lTWHhQTlRVa2tYR1RiVzN0N01VVzRnbHBHd0M5SHZ3Qnd0aVM1ZE8zdENqL2VHdHBGS01UVk1ZTldLdkYKdlFlQ29vUE1nTUVCSDF1NWcwQTdrN0JCOTMrZ0tGQmJiekI5cmlRdUJaU3RJR2VMWUFVSjlNNFEwOXB5eEg5dwpsL2NoWElSN29Ib2tXYkN2Z3N0bXRJem44MDdNNVVDeHR1UlA1NnpHZytyMlp2OXcvR0lsNVo1Qk92QTB2a21ICm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2ZEN0Z3R1Y5NVdsV1ptUWI2NysKVFl4MUFZeWU5NjAzeThqMHUvSmQzeVA5QzgxVmkwUG9iKytVVHFzYWY4Nk4zNXREZUhjUXZBaXNia05GM2l1egpGWk5NVXVldmt6bWVRYWdQRjZ6dXlnOER3b3NFYXpObG16K2RRU1hDNlFBaG51c1BRenJjWVJoSm5qd3dNby9VCkhvTFFjdjhJYnpWRDNTbW9KeHNQdFpVQkoyS2hHS0VsUzVMRzlLYU5tczlDWCt4aTJ1anc4OFdxOUo1djRUQXYKbU1ReDIzeXo2bTZmOFlySloxNTB2TGg0MTZOSjhXMmtsMlJvUHk4TFl2S2haUExxNDNkS3R3L2oxQ3J1ZHRqLwpJb2w2cmJoTWtZZ0NIVUp1Q2FkQ1gxRnhwNXpFbnpzUkpPM3hkVi9WcGFkTUs4K2dmSGdKMkhFQXlPS2tZSGZBCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnR2czNmYUtNKy9xUGJ4cFFpb2cKZGhxYWg4ZEZCSHU1VG1GSzNEVmk1c1NKcnBGSG1iN2RPbHlYSVo1UHlCd2F1RVFVclUxbDhYQ0N6Mk1Ta0JJVQpCcENiNjlhNFd0RkNlSXk2T2RMTVZGd2JwWUNZZCtFWkU4cVFvV2tObkRkRFNraXJwYndxcFJhdnozOURrdmF3CjZWSDVjQ3A5cldDVW5zUFNZME9NSVhibkMyWGRUMUdsTTcrdGhjVUdqS3FXdGFFZFV1RnlQYlplT1VZQzdHdk4KS0RqUlZRVTJ4ckR3cHk0SERjYzhkb1NXTG9KMUVzODR3VDNaV294VGc0RGVrdVlOUzVuZ0hVSFpqRVYvejdzTQpsVHFLMC80Sk1SMWYwMWxGS0Y4bWZ4OTRVM2toRDR1bG5oOWdRTW1VOVJoa2ZrTlNySzQ4bTBXM3N6MHg4TEZpCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc25WREVtdDhDV1RXOTcwMFhBWjIKQTJYK2VKdVlyQXdncTVSMzRKcjFIMmJFVHdwd0lXV2E4WmtKamc3SlB1YmRPZnFZUTBWclFnRVl4SjNld2JORQpSb1B2YXRUcnR0UUZSREUzdWxRbEJ5d2RCSmwxM0FHdzVONEIvbmFpYUtYaVMyeFdWVXJRZGZiaThFdkFWdWREClAzMDdlS1diT2d0SmIxTUpLdko1NnRZZVErTkRSd0JhZS9Oc2NTM2pJVFlUUmlML3FkSXE2Wkxpc1l2cEFOcmIKTWttL1J6Q2wwNjRUR3IxMU9Bc2VXdi9XR1NjY2dhenBpbEIzbkhYZzN6Tmw4T3FXR1NxcVE4RytxVS8wdE03Swp2ZngreUI0RlY3ekNqMTJBTG01NWczOEhDRTMzdlZMVWE2Z0h3cEt1U25zaWRvam5UQUxvSExNSDV1bkJLNjFUCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNitPMDdVS2NyRThJUWVLZGZVZ1MKRTZvczMrRk1hSnEyQkJPenQ1aWJNNjdaYkU2SjJ6TXNJSHV6VVV0N1Y4em5GWHFTUVY0eE5STTluSGlHd0h6Lwo2WGcvVFc5MzlnS2txQzVmajM3eG5TQ0E4Zm04cmwwRHk2MkVBRFRhVkErWGkzYnRDUjc4by81YWhhZThmZjRwCnRRTVpJa2xmb0MrSm51NjdGbUlxc0NWSlhENzNLYm5CNkxIU1orN2kxZmNuWmMwRzk4VXFTNGNuckJoR2RTc2QKbkFscFAxcTRhdHRjSThsOUtpV0pRN2RwakQzNnVrZy93a2I2VkZtVzU3RC9RMm0rekxqUWhxWGVDWEZuZGZ0ZQpqaDJEbTZncUJtdGF5YStId3FKNTRIcmNoN3NGR3NPSDZ2cktXVGVyVFRrd1BISlRLbndEd1dRbXYvdE1PQUJUClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmpWOTV3U3pjYnJOZVczcFlrWkoKL2pxbk15MEVzQXpERmd3QVNNQlhoYXBkSlJqYjh6ZmZreWFrcnZMMlBreERIcXFNamU0Z05PWERacmZXd2lJVgpQYXZiMDVrYnhUR0tiYVVFejZYenJ6TUN2QmJpNVRyOFAwdzloOTdsK2ErUXJ2ZWNwK1ZiR3dMUUdJL2ZOWW1hCjM2UDcxL0hhSzJ1ZVNDd2xPbGdvaWcyczIzRzZ1VTJiYUpvYnZUZUZaS1JqOEpZRytOalFHajNnS3hubkUwTisKOE5tQUpFTzNaSjNzeHpLSkw3NlpVSFlKc3NNamtSZFRyaUM4aTRBT0pML1oyWm1kdmJqOFdmUDlvS3pxMnhEYgozbzdRSTJwY21RT3c4Y1BoRjJ5NDJ0NUZQYkltZTN4QjkvOVJwbE9KdWlpMlM3bmJDSklDN3hvMVlUcG9LT3pDCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUlCeENGQ28wY3JySk5JbEJvS1oKRlBZS3NFRXJtN2hRQmpYdVhIc0FCLzlCYVdTSnkvMjlmSFlVMmxRYkdpMmVPaFVQd3pld2Fnc1BtNTA2NXdrSwo2Sk84bHl0ZjdjZXZRU29Ca2hVNmQyck1Udm8yb2RRcjRRM2NaY010b25RS2tJL096VkgrQUlLUzlnbHZ0YVlWCkZJMDNJMmZNZ1dQc0JJRDlOaVVaN1FHeGFZYW01M1FGdUFjd3NTMlR2NktPM3ZHdjVFNmZjUk9RaytROC94dVcKeWZkY3lVYUVteVo4K3hTMmRRbjBnK1ZOZ1dFanpCUllFUUlNcFg2Y25mMkNRa2tVOG5BZE9QbFdFcGl2ZG5oMQpkRUZWV3VkTnJYbldaMDFKa1R6cnA2MEMvTGs5YU4xREZuQU05bFJVYmJ1VFJFWFB6dDBoVVhTQXFXbHJzWGU2ClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1FaTFVWNlE3cDZnMjZhczQ1V0gKOHMrQ3pWMDRZRVFCVExod2tqVitMYjBiRXJ5aXB2TXN0OC9vZEhvT0kzMVBxS0EvUVlHSlVuNzIrTjVmYWV4WApraTNTU3NrUGsrL2pZNEJvVTlURkZKVjZBNE9ZR3JDSElhd3duQzdGSEhYcmhDRnFzbWVpZStOZ3NKMVVFNXFjCnkxSEt6bURKQlZzSmN2WUhNTWNiZHREOCt3K3M1RnZWYlk0ck1ZUUJpSXIxdVZVL01hL0xOMFMvUWZoT0N4T3kKMEJsUjNBRkc4ckZ1OEJRZUlyakFzREdOQWNRMjFPMUc2RncvdGhpU1BCVVN1dHFBSXFRMWJpL2dacmhrKy9JMgpRd2VjZDRSK1YyT3dhUE4xWjljaVN1Y1JwUXlHbnc2UW5HSlRxYlYwS29QM2M4WWFnVDFnZUFqTmhpWUw0a3FRCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDUyeUUremtFMkhBQ05LVDRpOGYKdnNHZEwxY0x0UDJnb3hqckVjSXFJMEtXcklxSS9HRDY5ZkRlV2dSYlBQSFp2bzZ1azBuMnA1RjJ6QUFYdGF6aQpBSThTY3ZBeHlpSXJ1NjRyeUJWYkhqTExuZThsaFduSzhobkZpT0xYSTl0U2hKdTRIY3UvaU1BdXFCQ0tuSE9nCjQ4cFRhNVcxeWs0dkpOSFQ2VXliczRQR1kxbnVRaUg3TUt0T1g5NDhTZEI1eHArUGVWYVVpNlczSC83Mnp1RUgKRmZockJxMjl1Mk9XbWJpbWdhQlZmcjFRdXhSaGIzZ3VYRVBTTjlzdjBkbG54dlFUQ2dxWG9WUU8zQmJhaGhVYQpleUQ3N1Q5YldXMmVjd1ZJaTQrWnB5c1dWTGFzU3VKaVJuY1B4MVRia3MrSEFKNERLSlFJSCs5UE1mUDlZemo3Cm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFRlRHRIMlBMdmI0YktDUTA5NmgKdzlsVllVNkM2OWxiNWIxS2ZTWWVuNXF6YTFLZGpRaUVnSWFCZzAzWkZJVzhhZlk4VjBOMHRKbUdjSHFHVmNFdwpOMGg1VDZCam1iOXI0aDBVcjkxODREQVhQQytJY0haUlR1dktwa1lkV2xqOEVyWDQvOWhnVFdxTEo4d0JOQUM5CjRTeXdlNkdNdDV6R0g1QUVSQlBuZjdURUJPZmFEbDA3RFhzWG9GUy8wTDNlZHVXLzlrbENJZmtjRjVZUnduT0oKWXAyaTJXTzlBTEhiTXhxMzE4VTdsNUhpV2xTNUV2bExraEVuYWdqL3U3d2hTWUhweVFzMGRLT2dpYWpsMHBLUAozZDVUSWZTNFVUVVBpOXVHTHZFQ0FiQnJWcWtteDEvYTRaSXBsbzgyd1Z6bnY5T1haTWdaVHg0OEpacUNzRWtqCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbURwTC9qTFpORkl2UzZsQ2F0RDgKOFhUeFZQL2ZxTy83VlNlWTlJcFR4Y0hwRGFnaEFOM281L2NnVWlRYnJaMFJGamFuK3c0eExCNzZMZ282cTRLcAo1Zlh1RXJmRnF2eGNSM3l1OEJOWDc5emVFYmphajhlTi9vSyttNmlWa1hNQTBXTVJHSngxTmRzV1NPV01MS0FSCmh5dzJ4aGhxZTN4Q3VNRXRyNkxLTG8xZVhIdEM2VGoyd3FmY2dCelYyazFhcWVDZFR6RU1pMUlsZnF3TzhDZmgKTE0rdWxtTWgzUTNrQ0x1cG5aOUNHeHBaWW9ocWdQWHpBL1A3RjA5SWtsRHUyRk00UXBFSjFxZ2JzMS9INTRnZQpRZlEwL2Rpd21XbjJZek9GeWhBUUIyc2tlWC9RNUNlV2tGMUl5clJCTXVBMlRyZUFUVjQ3ZldqRnRoaUU1MXVOClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0RVNU01dkx2cVpwdmx3NWRQMjYKbHRCdGFjcUxUeDhKY1JqNG53NXNOR3lrTlIyNklSSDlpMEt6Y2Q2N0RNaks3YlRaZzJIc1JDV2YxQ0ZUdEVvSAo2Y3dnZTIzUXM0Mm5iamd0THR4OW9LVThSN1diZEJUSmZEaGZ0ME5EeDArZjJzekZEODUrU2N0UGR5K1J0YVpMClpsUTRzY0J6NzM1ekFiUEFyenBqMXNwTEJOZS83WlBhZUlIN09zQmkxVVFacUxaMldlYWQyUUxsbGRKbTkzZEcKTmorWjNVc3BDMEVaOTZUNG4rRFBRM1dXYzh0d3BjWkhhclJ2czRJT1dTRUhBOE5pVGlrMU9CdnY5V0dWekJDdQpwb0I2ckNoRm0rY0lKVndhcFZrVHgrc2JXR2E2YkhFenNMODJRUkJaWEdIMzdPaEpYSlhDK1hDR1Qra1pMY0tnCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFVTNU1zL0dONFl4VlIvK1J5NngKZnZyL3h4cDgyajlDWW4rQ0pKQlB3R2dFdmZJdDdyYWlvL0FNTTdFN3pHOUIxbEIrUGszUzRKMWRIb3BxU21VTgpOYWdEb2R4WTB5aVZsU1lRdEcrd2VTU1ZzeFUyYXd3Ni9adG41Q1VDa21ZL2FhSVczZE5ZSDFKdmZOQ1Q2cUFVCjVQaEZLVjl6anlHcXg4ODcrZ3hlUXpGOHpBUTZQTFJiRnhXTDA4dE1KeWNveGJ3cWdEVFJHZFRlaFB3NXJUN0cKbDdmS3hVZ2FSQ0pzY1hORm9wa2hUU2p4bERFaFVPZG9IK3B5KzExOUxqdXBJOGt4OU9GVmI4RGY1bUdZaklSQwpXbkZtbWc2ODE4K09CV2FXeTdPbTk4TWp6UEtpVFkrQXJGU0xqVDdaVGpaZzFUR1hqajEyaFR2WHE5SGcwSlhwCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDVsUW50WUwwSWJUVTM3YitWZUEKcnRpUi9mWGpiYis2dVJzUnBYZU9RZ1hNd1dBR20wam5yY3ZzN0tGcjZ1ZFRVWVV2bUtKQVVPcDU4dG9RZ0tCSgo2akgyWHBUUzZSWTMrY2lFRkdleGs0VXR1dnY4NDk0aFg2QVZoZ1N2amd5emFCNmVQd0xVcWR6S1NIT29BNExQCnBXYUg2UmJUam4xK0J6QThJaklNMkZDUjdNam15b0lEcmwrWlJZVCs5bnJqU0dGem9TRGRsUXRGRlFPTGo4Q1oKUDdBbk5WZUVXSTMxMk5KVWNML1U0WVk1NElPWnVXNGJhaEZmVlNYMDJlMWl0V3IwVm44Yy84amhzbWo1R0xNdwpvTGFvSWFxSFR1WkNad0ozQ0hFVkhXaklHZzAzVmtZcklDRVJKT3JIUjhOUHo2cWxLc1VaSWJjeEJQNDE1U2UvCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXRUZWVsT3cyZ3pLVDlHaHFSL0QKM1BIK1FOYk9hMFJ4NGpmY2xCNG0xcHowOWtTYmRtRFVlMDdhN1RObUl1dWlRT1d2UnVMTE1MQlpDbjRMYk1BTgpNN05TU0NFaHc4NjM5d1RTNmF3Z1BGVW04MjRlYmFQRStCSGtPd3UyaUxYVkdzZFkwcWM1MlBMRk4yS1UwUWZTCjd2VVFYM3Rra1ZKSFVXVDVpaGRhZjF2cWZiWFZXK2FKSy9pZUJLeUI0eitLWkx3aHlyRWJrbGtRUDAwaHhQMm8KazJuOWVnVzJGL3hkL1Z5TUZnU0hjcjRiM1VxVzJMZ1dGajJJOUZueWVSOXd3WmVISG50Q3E0VTZTc3V2aWErMApKTHgyeHFtNlZPRDRydDgyN2RFa2hPRzRBZHpqTjQ5Y1dwa1RxUHc2SjlPeUpTQ2xxSDBNTkRzcnR0ZjRrcGRtCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2lHM0tmcU5xdVdxa3VKR0pYWmsKVnlySksvOVBXc0RFWm5RS2grWWozaEF1RDRLa0NYNDlOSG1sK1hyZEFjWVU2K2ljQnRmbUIrK2RxS2pxZUFNYQp3ekxkTUdmSGRuUUdKT2tvcFNvakl6QnBObFE0V21SaHJIcWhqTzA0bXRUNnBINEJvNnBlOSttVUdVWFQrR2NmCnVUYUR2RXkvdHBMMlZPcytJZ1JsVVF6eTJjQmhGNWZDeUVpb3BCZlJ5ZHJsa2RkcUdlWHFjRVl2U2J0ZVYrN2IKNDM1SVdtM0tVVHBnNStub2J5cHBVcjRwRS9zS1V1aW5Kb3hnTTdqSitvQm9pb0pOSTR5aDF0dmtqM3Fta3B2VgoxczA1Mm5MRDdic1R0QXMrb2ZpOTU3OHBORDljMHNUeWNZbnJTRFNZWldpUm5zNi9OT2k2aFBOV015d3hlc01WClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3BtNmZCa1VDSU54My81UW95R1QKRGF0YjVQcGhWNmR5WXpFelIwMUxtZFBqTTBWTE52QW5oTG84czNRQW9rem1oZnVVZ1loR2JoZjhzUktJTXgzUQpJanpOc0dTWU5pRUdyZDF0bWdXQ243MDYrQjAzQ3N3ZzVDSVVzc01vQTZYOGgzeG5lU1dQeUNhVjNBZkV1dnlzCmlJRDBPQTRyUW9RaTVtdmVwTXVMT3B1VkZXWk5nRmNHMCtpU3pDUkdqZUNxZXFGZzJZL3E5R0tEb2Rod0JNMXYKeVltc0d5cWllU3h6Nm1hSHlkSVp2cTZYdmRoRlR2Qm9rVDlrdlpldVF0Wm5TS1Fuc01aQ01nNlV4MTZQUUwyYgprdncyeWFaSGpmRFZJVm1JY21maDRhNDV3dVg2QXBlRGFRSExxM2RLNklYcVFmanFCbkRUZUdZV0NNdjh6U3FDClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVVObXN0SlFTSk1pZHFGSm95bnEKaTFBNzNwT25UUEhXNFIyWktOU3d0ZXdUUFB1RUVDN1VSK2x3RitLRkNIeXRoaXYvaFNIcTJjZG1NbTdwbVBhaApHU1RtOVlSSmVtZk51ajMranF2ZVAxTXVWL2VPbkFuWmNjTFZ6djFGNjdicGZSTFJmQUVaQ3J6YkN0cFdha0NqCkQvWklqR2dGQS81K2RkckRzQ0xLcFFEN0dYY1M4bHdSdEhkaGhsRk5tMGVXajZpaDVOWFZFRDhNcER3UlVtNlgKYU4ydVRteDdoOU14MUpGdytvc0ViTG5Db21pT0llVWZJNVV2eVVLUk9nYWtlRTZBSUxLYWdmM0lUNWRaVWt6SQovb1lEVW1pcS9jTlhJcnNyYTVIMktwYndYUTFCOXNqWEI1cVVuLzF0aGFJOXovZk5yOU9SMWh2cS9uTktCSGlWCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHlUVmNPaGx4Ujl0TTNsbUVwZ1QKU1E0bCtGbVJmeTkwQ2V0YUhnUmFZZ2xXQWkrRlQ3VzZsWUdxc3lKdVNhVGRJVFJpVHVXUVFpa1MzRmFxVHlmaAovWGh5THREWi84bGRSNEQycU01c2NCeFRBNy9ISDdDWTl3OVdIL1lZZ0FOcVJrVm1Fb1VvZHQ3TUdOMkFaSFBtClFZckpyNkpWU1Z4OTVSVU1YMDhVeVJYUXRvTkpEeTVJd05kQlVNcFF1OERkSXltRXVSNk81LzlBb1pQQ0ZUZEsKYkV4a05HUGkwQmdtRjQwN2Q5VkdyZlRyTGdjc0ltVTBwRmdITkd3QW8zWHA2WG9FeHBxSUZkZE1OUStBaDdNYwpjVGlDNjIyTGJTVEI3MzBwOG02UVk1QXRqMmNzM3dwUHVLYUNFZS9mbzJXZFIwRm50U25sS2FQRUxETUtRN3Z3CnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXRSMXFsMjhJNUxiU3BTd0RPRWQKL2JOWmJOR0JVd3dVaG9BTHUxN1kxZ0FrSCtYK2xZV096cVA2YU5YYmVBVVFaZTBKb2R5cHdaelQzb3hCTXRtMgovdk9JeWRSUm0rTkI1M3cwZW1OVWVWK3pHdU1SeUl5cEpMVHNzYWk3OHVKcVBuZWJzd2MzbHpseGJTZkhCclNGCjl0RXR3dTVkdFFvQ2Nwb29QK1RYaWhQYTBVbmpqWkFOa3k3UGIvWm4yUnN5N2JnOURlamhqMktiNkdlL1g3bWMKM0pVV2prN25HNjBSaFdNc2dvZ0tTSmJESmNScjZzVGp2UklTdmZnZ2ZvbFhnTXNIaWNuNmc5dis3YVJ4WjdwdApXRUI5WCtIWFVTcmc2YldmeStxbHoyL0pRZkwwQSt6WjJaZms2Q2FMNmNUZi9pVjlRZ21iSW9oRy9lVHNvSCsvCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGpKUXR5V2JmbThXMGNhTFRZb1cKWVQranhxWloyTzVSbm52Vm8zdHhzUFlLSUVnalVaYmM2clpVVUNkYjIyMEt1ajdLN1hxVlJhamFVdVpXaC84Mwo2WVF5ejRNbEFkYTZvelNWbWN6a1o5OVlLeVR5WUhkOWNSYWFxZDlBU3NYOXgyK28zdGk1Um9xdmpORGwvR0UwCjFsTU5pM1JXYk96bDBHeUJ3UGE3cUxTSnpEckgyTTlMcEYva0tsM2FsaWVDdW5Ud3JkcFFiL1VOWk1jNU9RT0wKWURkd2kzVlBqdmczNnF2K1Z6NDhLb3g4WmRCUFd3YjhzNm5RTnFjODZEVkVqdEI3MEtSaFNaTFMzOVlhSGkzZApiWVRSdzhSQVUzK3hhN3pWamJXd0Q3SEhtWEhIRWFDeVZiVlkxR1lwdnJ4U0hXVE1jZVMzWVpDTzRNa2lTK2JZCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnpVWWtiQnQvcFg2WTRmcjZqSy8KajIvRW92WEhVbWRjZGRNMDFVNGpWVVdkRmdWQ1NvT0YrQzRtTHJCREo2NGVQY05WaTdqeDRnbW9VcThiQUhVeApoWjBXTkJNQUZCb3dqUTJ3Ty9UeTlOVGd6RFJtd3NyaXZ2ZmpEaWpWUzArSlpWa0hBUm1EY28wR2NZOExxbG1mCitveDVCdFVEN3VoVVNjd2M0KzdZcU01RTA5OEJ3bGVmRkRmTTJuajQzdjZRZ0hlS3VCWTFZVHRtM1ZmRVBSbTMKZGkxdGlDSUZ2ek15Szd1NHU0YWJZWklXUHo2SDg5YnRKckE0UUFMVGx1T1RqUk5tUVFlaWdBN2VtZW5LVHExSgpYZWZQNmorUkNZTGVrYkhadGhrNkdNYU5MS21PVFAxZnF5Y3JIZ3pHOVVqTnYreHJ5bk5MTXozejQwZjVvTno1CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjR6ZUh0WkdhRUZ3Zzd4TG5FaTAKOXp3S0RkTk1ySmUyN0lqQzZJMEY4eVpnLzlnNkJmL1Nlekt6Z2V6ZEdpTXFLRmZ4OEQ0dUJwY2g5UWh2a2FGbwpSR05NYnpZNVM2ZEp4ajdoZysvc25EelNLWmxIZ1haWnhBdVoxNG13d3htQmdkVHJQRlZJaFRRLzRzY0FaWnlnCnFBY2xtS0VCamV0MmtwMWpiMFhmK3JvNDA3SzRrVmU4VmFkVEp0OHlkYWVpb3hhaXpXdUk1bzJXb1NGZGtaU0oKMGtjbTczdjdYYjNnR0NzUFFOeDhKYkNOUG5UM1FKRWtXWktMRnJLM2ZQRnhiY1NzMUk1dnNzSDkvc3pmNE1Ldwo2aERQck02bEFuRm8xS0ZnSnY2RmpDNXE3TzhMY25ZUEJ1ZFhXWjAwUFo2aVRQVEVCMkpRQW1YWTE0ZGtZaXZpCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcU1KTmpJckxmSU5UekJKOE1nMEkKOXlKeHh1R21qZW5QdnhHQUl1ZjNHU1RyazN5Qlh1Q0FoTDNnc3IxK3V5bWdrVVBtd2ZyR2gxT3FQUVo1aklPNwp0S1R5dGtoNG1ZZ0xTMGJFVnlNWjNiT0JycmJNU1RKdTFYWWNFSnY1YmxsTDJqQlhmbU85VTZOWEJjUkUybFp5CjVJWEVaNjBIbmFEbk5nb1EwRDdPMTZQQUhzRjVNby81VzY4bjFHa2kyK0NlNVpFNUFNY1JNRDhGcVpqRU9UeFgKMHFCRXgyNGJyc3E5d3c4UXdoTnI1VTM0TXQ0M3lqMkFLR3E0NEFlblFtQTZWSjduci8xSllidE4zb0dsa3JPbwp2TVNaeVJaL3grdGVyK1VNaWFTMWN3RzNNd1l6ZXRNeTNwNDI3bjZtZFQ2YTI4dGFpQVI4Y1hOUEN3K3NHWUx1ClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW92K0FFKzRZR3FrUjB0ZW5VZVcKOFU5Uk1TZjN6MDBkTkxOSk84Kys3TXlCT2ZQYk05dS8xcUl0Z1dwY1dvNkMwbWxNd0daZ2h5T3JSaHpxNkFMSgpMbzBXVXQzTFI5eWdsQjRBR2tFYVZyd0tIZUJVM0pNMnRHbkpIUTN0LytnakpVRzBPVmJLUTRSQ1FlcFA0QXpnCkRZVkt5SUY4VXpZK3RaVEhPWEJUSjVVTkM0YTd3eGVIRlJlSnh1VXJuQndkVDZzbDk1aDQ5OWtFbS9iZjAzSzIKQjNzcytyVXRPVC9RNnVaNDNJQUN3ZEhKR2JJUVNYVS83a2hJVEpULzYwbDM0TlN1QmQ2ak4vbXFDZHNOWWszQQpKTFg4eXJiOHpKajFWb1Z3ZW5ueGdhWlpmQUdWa2pxTzcxU0M3VUM3MkhicmhxSTJjN0V1dmViZFl1d1l6dHZnCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2wzaldUM0REd2hsODF2WVIrd1cKMDNqQUs4WHVvSVhGS0VpWmNyc2k3bW1sK3RzYnZjTW9zZ2VqOERzU05aT0FvNWhzUmxZKzNQRWEzYUxCeWtaNwpKeFBzdERLV2dVSFJXOWZWYlY1UEpDY1NpTjlpbEVJeXY1elhyOTgzeUdCRHVtTWRCY1paWjM2ZC83cmpIdHZZClhCdExIV3l2L253ZnR1T1l6dDdrcnI2d0xpYlJnR0N2eFlCZmgwNWN3MDJ2Q2x5V3h4Y0NELy8zdkN3U2g4ZlIKTTZKclRJV0tMSGVPRGkxZjZGOTRXY3RobXZhMHNIdjN3OGYzeWhZYlMvdFRsTGlCeTNjNE51b0dsK1pOdWx0cgpFMXBIN29YYVNjL1RUTEtUbHBJK0VHQXA1amZqRGtFSVRMUU9XNjZTREdwQkp1Y2hxV0I2K0Yzc1BQL1hrLzRiCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHNZeHhMMm8yOWhaOUlIYXJKSG8KaVh1Zko5Q1FhN3JiZW90bzRzQlRzcFdMd0EyQlhzQ09qUjVVem1HK3hTaHZSMEhmRFlISTlHN0pvUzRQSHU4bwpBVmpVWTBlaHR5OTV5RzVQZUlXdDFERkxEZFRvbGdieXZmZWFqbEhMWkxCNXY5VEcxVkJMVVVnSEZGWmhKamEwCi9aODA5b0p3ZVJTb3BNeDJ6UlMwdjFiTEhqSS9OeGs3ekdUTHNkamRoTkpwMmJBSytXRmx1dkQwMVUvSmpKSSsKd3lGNitleGhPVmtVZ3lrVWVqZjdTQWRwMkdxQzFvcXg0em9EcEczNEw0QmFXcWhjSm9TenNnS3pDVEE2cmFmcwp1MGM2bm9tWVFCNVNQb09wM3hacmd4b2h6WFF0ZGRpanRRMHFucngxNVRCakxmenNXQ3NyWWoycDFJNlFTdUNICkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckxVRDJmTTBaNlVPWHJuNWgyejcKbUVMaXpDMWJtTWdHV3M4S1BaRUl4VFh6OTBVeE00MFk3QTN2L1JocXBvVWVTUVJTT25LWU9zeU1LV2ZpZlIrUAp2ZkltSHRBOFk5OWY4WXdsUlJWajdoSjNBYlkraVZmK0pJMEtkZ1VXWEVPb0YwR09YckhRVkxJT3hUVUVpdnplCnhqcHhweGMxZEVKb1VxVmRaZDJWcVlaMWNSUzYwcVZCTDNHME92Yk84Tzk4WHVsMm9zcmdIUGd2OWs2Y0U5REYKQnM3aFRhRHo4eTZha3JsQ05iY2hWRnpMSnY5c2tEQWdhYUc4K2txbmNLczd4ajk2NUNMYnFCWUp6clBndXRsQQp4NjhYcWRKelREUXMyRW1KUERoOEFIZ21QZTl5QThpRlU3dytxajZCZ3BjSGZZaktwMjNPU2xMU2FkNGRJTGpECmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0VCS1FCUGNCNlc5UXhMbG01NU8KWWlKVllRSDFiaFZtdG1jaHlUUVdiUHlCeGlZYmJlNVRMdkEzL05ETElGbHBEVXpQU3VQaE02YWxOcjRicElZNgozckdRSncwd0l6a0FscXZmN1RTT0hrOFFINGsyRnQ0QWs5UnVVc1lEZHZsOUtaVXR2M0VzRjBSK2V5eGZSVjk4Cnc1bHZBWnFOTWFjRnlMRldpdUF5aDVwK1RkMW5XL2VBclFiYmNkZlJkaUNqZTB4ZFQwMzE2Tk41QXpCK2pCZzIKOUxucXZYM2pWR3l6a09RWDZPQ3Zqd3AyOG1EQlpYQlU0TFZFVVZWdTJXaFp4YXk0V01HNklWZWlvalV1RkpzbwpOYWZReTcza3ltaEVtMEpuSWU0eE1vd29mVlVib3kwSUZJQjI0QUtJYkJkRzJNU0UxMFllRzdNbXpHbXNyWnBLCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU05cnFqci9oNFBmT3d6TnZsQ3AKa1FnK2lweC9hMzh0c21laHJtWnhuM1BCM1AzbEFzUmUyeWo4cC9NUG42eGF5Vnl3Sy8yVE1yb2dLUUZVSlRVRApseVZBbkpzWUJjaFUrVmhSalluaW5QRFNZcVJsVVdiM3JqcGxqcGJoK2ZkUUcrZzJkWWpsU0lYMFhobVhuNXkrClNjM1RLdzJYS3gyaS9mT3l6NFk4Z05ld0F2RS9aNll1YWgwRGhsanhpV0Q3NkVmOGlvQkJCUzBjdU9sOERnR3IKWVFCWFZOZ1IzZHVTZnVUenhTMmhQaWhHc21GNk1zaVJ4OGJTam5janI0bVorU1VkS05YM3RRVjMxNUp0am5ycQoxNnNhb0Z1Zzh3anp4MmhrK1FoY0hjLzNoZklHeDJUK2JLaGtnRS8xdlhMNEtaa2kwUTFmZG9YTjNjcTZLUmcvCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnlRRmozektmSHJCL2ZwSUIwUWMKRSt3aTZEWkxUWDQ1dDcySFVydWtUanpCd1QyQ0xnVkluZU81aUZIeU5UdVhFMDFsZkxvM3lObThHU2tnbUhTaApZZDJiT2N3WWZjV0IyT1J4TnVsNTFJVVFyTlhBOHlnbFcxeFg2SFBVZU9xU2tMV1ErV05WRXNhcngwWEo3cWN0CkgwQUx5UEdIN2dUUjRqdS9COXRmbjBVTXhTNmFmM2kzMUUzTXFVcmtrK084VHAwdVVMd2lVVXpmZ0tyajc2bGIKY1JiZ0JwVjVKZFBsUEFFZHBsRHpOUTNKeVFkVUNRanUwdWlrOEx5ZDFCSmpuUnhIQ0dlZGs5a08xbEJ0aWZuZQoyK1hJWDd6cEZFcTRVS2RFVGFyRlFmeTVFaWlCeDBqV0VVM29ndnNGeGYxK21IZ05oeHNKNmRVTmUzbE9idDJPCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcG04YlloZUQvS091SGZPWkpBeXMKVDZKQTlVMXlPTXNxaGFBUDJZaG9nVEJHUExIZHBtOStXU1ZrU1JvaDhYTW85ZW1YZ0taTTBTbEtGSFROUTlMYgp6amk2UEthTUl3REkyM08xK0NIT3NDTXZvT3h0ZXFuTDVueWlrV1d2NG8rV2c2VzRFYTFyUm9LWStnZjczN1FqClVuamhqNWNLUGN1ai9QaU9OL3d0SlRiZFIwSmxqQmdSc0hkVHhqbWpzcnRCaWhlbmFiMDRidnYyelc0cXZtQVEKRy9xWmtMckpHK0t4cUw0Nzk1VEVWMFJIOHFra2ZPZUc4a0F5eE9zRXc3aFpTbU9HcnlzMlpuT3lvc0k5Y0kvagpNbVlQZWE3WHpDRUhlMTZJM2xhY1ZMNDVGYWJLUmJZaGJXWk40VXhSTHVOU1QraW5BV1B2QmlQYWQ5eW9FT1l6CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeitMeDJlcTM0djVxcHpjeUNHNEEKVlZsZzlqOGdWaU1vcERLeXM2TGxMSjN3eVhMazFiZHlCalduQ0dnbEdtUzJQb1BIQlRDWkNmaDdLUmx4c0xQSgpDdGtLNCtSQVI4SDJmVmtZeGRUajNaR09LeWg4cWRuRVRTOXFzWTVJTlpuM0dPdlZIQkhQelFCVVZZYUtHaEdkCmd4TTg0YUQ5MDJtdXZMTEZnVmM2WDl0T0JpcS9zTzNyWkhHTU93TXR2eERjZXlWc3hVckdYN1VuUzJkN1RIOG4KUkZYenUzdUE2RmROVklxZzd5b00rU3JHSXZkWWsvYkZWSEk3bW1oUnE3eU9VUk0yNG9GMTJiaHJveGp2NXl2eQo3K1hJcW1wNVlubERobDM4bXdxeExLS1cvbkVuYk15R2FmNnorUTNoTG5uZkg0Tm5PTDVPNHFqWXgwNkhrMUliCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdC83QUs5aW1pWnlZbHZOVWh4SjUKT0Y2TDdROENCejNRdjR0SGZMUzZSVHorSDJrdkU3Vk1ZbWZwT2F4aFZYMUQxVmxmUWwrc3V4ZmF4YkMwSG9LTAp4WTJ4WVJER1c3YWpjdW8yM2p6VU5YbXVVNkZIdmlVdjZYRlhDMjh0eVM4cjljMTdyOWJlV0V0d3pBa0NabHhNCmNDdS90QjcxbzZ5Qy93R2FDWk1IQlF0SWUrbzdNN2NzWkloTStGeTgxMDVZT3I1b25UUGoxeGR3Q1Vsei9aeGMKNi9pNUNub3A4YXl0bnYzZjhoT1JGZXNjeUczK3RZZnlJSE4zbmxMQUc4dUZNTy9Ia1d4YkRjV1NNSEtGc21YcQowYnRLczlBRTE3bWNmQjJWZER3MC9rdUJzSERSdE1QTnNBeE03K01MQkFaRmRBV2xRSWZzMmtQL1Y1ODBNNDhBCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekpLRFo2Z002ZXpIMktDbHRua0UKLzhJQ2tRNUVoaFoxdTRLOGNGekQ2Z0cxeXNyemtqRFFQbGJobnpaOEN5dkR6Y1JaNDVnd1cvNm9nNFdwVHI0QQozbTBxOGpsZmw2SHRXSnNnd25PU0pzL2JmY2VmSlF3ZTI4UFhCQVRKY01DZ3o1QXJqdnIxWENraFJKakJwTmM0CkhzM0FLSlZRemh4emdDOERSQ0tGTUxuTEhhU3lPYXc3VGpLZHlibWV4ekJuRW8zR3IxeTZpZCttNTd3Y0pPUGkKMUcrYStia1dYWTV2TDVYUlFwZnUzMjRHdWpYVkpzZkZsMktiZzVFMUJJUmVhUWU1ZVFRVm5rdzNlaW40eFB3Sgpud09zVTJuL3RLYWQzQ1dvNzNGc1VYVmZrbklucG5UTkZkc2hxeExQOEZuVUErUG0rdC9oWnI2clVBUmt2QUt3CldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2lYek4wTE9pUGlKREoraTM3VzkKcjZyWHEyeEZ1by9tQ1pmOTZvMENvNXV6QytoenpyaVIrUmF4TStHV0V6K05pd1RKMmFONXB0WnpWZnJwM1NCdwp3VHdEN1c3WkcvZVFSZXRnODN4bE9JT25hYmhPL3pINnpiNUJVQm4zSldjeEFsZkRzNXBXbW56U05YcmFSMkFXCldNTWZ6TUN5SVJDSVhyNHpXSXJaVmxzV0crVVRHbDZGNGd5Q1J2dEZpMmlMZVkyMnRXV2dQOGtja3Q1NjIrOWsKeGZaUGF1NlpXT3MyMTExaWlZZ1JnN2w5aWVtSU13YnhCTVNpd2hvdENIbVltUUhTeXh3QzJYRVRRWlN2VmIzdAoyYzVZZFBkYzQ2SUdOSjNLbTB4TzB2cCtWLzBUd1c4M1J1Nlg1WWRGZ1A4ZWpzaE4vUVgySmlLNEdDUlBEZGpPCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1FUUkYrRVdtbmpNL25idUJwclgKQVVWajJYWGlIWitqZGZzaGxnMW5BQjJvOUlsVGF0Yzh6UVM5WFArWkNmUkhaRXp5NmEyY2tvTldVeUNnNXY2dApjTjlJMUdJaDJlb1J3eWtidS9UTU5neElBZUpKVEloQXlDQUZSNzhUTE9FWCs0ZFFoMzF4My8xTXh0V0w5UGlmCk91TzBOS3VrbHZrNGdobzJ6VXMrN2NubnJwbEFPVGprT1lpR2plZzlSYmNkbEJMYnpMUWFKdnI3bGlRTkxBZ1cKbzhWcFg5U1dJMFAwcVBDZXg1WlF1QkppSVBORWIzQjF1d3ZMZHZLaEJKcGJzMFlWbWVQWUxiNU54bWJuS3lWcwpYYWlnL1doVGJ0cXZEaGtnZ0lETStwK1krYU9aYzFOMU5ZQ3NtZ0pkMUgzeXJUUHBJSEJFZ0ZDSTNzOGxBWGJuCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjhCTGZLTksyRjhIY2JmblRlRXcKRFA3Y3JhekhzMTBxbnl3L3Z3dmhHeGc0cExsbkowN3VFaXE0aTJnQlFZU0E1QmhUUEVFZURGUWY3Vi9nSXNwaAphZTIzK3RJU1kzL1VKR09JelR6ZVlVa2pIY0xNU0VZWDQ3RncySjhXaFcvOEh4MG5yVVdDcnl1ZGNtSmp0YWlmCjZpa2NSUTFkbWxrMDFycEo1bnRNalpyUGhWSWxsQlZJMHMrYy9MZVp6bzFRK3dFeEFZTW9UMUg5L0NxWS94S0QKa1g4c0daTnR4WWg5bnljRHNTaHRvQWdlUkdwR0UxeVVzSXYxUng1OWhNb0lvTVVGY1c2RmFycGNFL2d2b0RpUQpRNWZhY040T01hNTRNUTdBcTlNenpnd2E5eDdSY2R6VTJudUFxbkNFdEZCNkkvbDJGRHlBUkczSm8vOEtxU3RWCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcU5wU0YxQnZRV0VFQ21OZXVKUzkKbk9nSVZYT1hDWHNEMkJhdlBhaW94aDdBNUw4WXJ0b0hnQ1R6Q0dzcU9SZkRwQ1NXOWowdlZreFlNMUoyQjVYQQptcm56YlZCYnczRXBjTGpJZjVSUWhQb2tPWjAvRlpPYXVxcklHZEZvdGpSQjUzWnFKTDJOUjJjTFo5Umx4aHdPCjBaaHVLcTdsdDRKRmc0bkdXS1pQY0JkTlVMNnY3N1hxSFNJcTR5SStsN1B0R2RRZ25CaXlwa1FRUzRRNUpjVGEKa25KR2xlUEwva21yUU5VeXJsVHpFZ0x5YWFCcExGMlY4VnlITkVubVkyeG1KNTU2anRWcm0wekRNMmQ1SW5yVAo0YVdyN2hzczBNaytaL1NMcUovV2YyOEF5cE9pR1BoSTVOV1VzVGw3UzZHRG1FVDRhM2hTblRKRW8ybTEwQUloCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1FLbGQrTDQ5bXBBOXFwNjRwRmYKdVpUOGVoVnJuTWtRRmRGUXZzZi81K1RXZkZBYmltSmMzWEJkUGpvQXZJdW9LZDJDZHplN0RLOU9SMU9sZGpBVwpSNXVzWUxXMW1IYTIzaDFhcU9mL0FVNEpTbG1LS0xpVk9YWHE1ZXVyc2UySzBqMFBkYitoMUVBWVVJMy9HdHdtCmxCRklBdk13V1NLcjdrTzRnejhMRVc3bHkvMGF3eklqVC9GK1g4V2kwUWxmM2xnSUl1NzVMYmYva3BBSmxhMXMKWXZFT0hvcUsvbFFFOFlCOFZxOS94Tkw1WVRpa0pvN0VKWW8rMFJSMmlXOHV3OG1KU01QMjhTM1FQY1Q3N1V4YgpQRXZoVjlOQjZKQ2hOeXppS2cwQ0FDem95TU00YVRsWWxUczlvZ3MycWxhNHd2TWc3a3lmMnFVWWZ0ZFkraWgzCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWErWlVKRjgrVFJ1bWhJWVJleUcKdDI3dzAxMXIwZjVtZ0ZnSVIzR0lDUHUvK3NpN2MyR3hmMnhNYzhZbWcwVVJ0Rm04UCtqWTlreHkwSDlRT01nQQpWbE9HUXgwelhvbkVHajVYRDdCN0NlZElNODdHbzR3b1pDdzdmeGFQS21JSG94KytJWld3cjhrUWJ6dkNpdS9mClQyNnlGYTNiMnMwZ2ptdlcwYyswdktMeXpIeEdHQWZtUjFZbkRPdUZ0QVdkOXViTlFuNTlVNzl1cGpQQ01hLzAKSmg3UjlBOHQxSFhCbk5IMzE4M1poenBwa2hqUmNVcS8zTExHSzJsU3JFb045azdVU0V1NmpHdjc3TktCcnI1UAo5UGl1S1lSWkpoSmIrR3FNZHVrakY0bjlnMGQ2N1dHdkYwL0RPZG55S1dpekRnRHQ1TXI2Nk1EaFlKdTR4QnljCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFpZSHNoOC8yZzFaeUt3WkVRTWIKblRxOE0wY2NqbENJWE4wU0xmL3ZZMU8rQ2Q4bW12RmpXZFNaTU9XbUhtZWRFaWNQTXpib1VJNEw5amlTSElsYgpnRzJYczd0WUtySTZCRFJBMHpWTVdOTkhIWHQwMzVsM0c4VTJKR1ZyZU4wbjdCS3hmaWw3M295NXlrQWF6UUhZCmpyTVF0OGU1ejF0V0k3WmpVVEw4N0xRaGd4V2FlU2hUTC9yUVpFUzZMeTR4Y1E5OG1iQjFqcHE4NVhjczNVVTMKOG1DMDZRaGZ3Y3NOZ2JVQWs2UUhHbFkrRVl2N3pSZyt6NkN2MllmaUd1R3lHT1k1Vk5GNCtLRnVLdXdLOThtbQpPWnN1MWhRa3lrazZKMjM3VVgvV3o5Q1NCSDhVb0daWGl4b0pBM0Z1cUdHR054OXZ6d0twdEtNeEpHRXdEOURZCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHBjOEZ4NEdjRkM1eW1nSUdicFMKSTlrU2JweisxRk40RFdlV0ZLcWV6RGNzWllhQzZVU1RISlpGekhtQ2FrNDc4bjBvNUtrcUJaWk9qVGhXTHRpVApJNk5VeU5kYWRQcytGL3JlRlVNQWJST2hUcFhUUHR1YzVjWk9NemlhbXhDMXNLNVN0RURXSUllL1RjRXRzbHhaCm5NNnI0OS9Ca1AzZnlKWW1pMmlaOERucDd3V0wyWmlyUC9ZWHg4c0NMbW5vcVdTb2VyMmRhbWw1Uk4wenEyNjgKWisyd3BvR1dFeTZXcXo1KzRJK05IVEhRQlo0TGk1KzR5R21iYmh2dVZkcDc3SCtnZXBSbTBnY2dwa1pqRmREVgpGVWdYWElFQnNkcVhScGo1cmtsMzhvbWl6dVN1TzRiMVh0WG14ODRVQ2V2NjRFRHBaRFduVnprZWdaSk0yN2xhCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelpTRXEvNHp2bkw1NE5yMTJ5S0QKeEVSZ3ZxM3lFWUZEQ3UvM09FNnF6dU55Sk1yVUVTZ2NKVUZNS1lQM3YrS0RIbE1ib2VTZittbnlvZ0tYZlQ2SApDWWN4dHZMZ0k1ODZXZ0dZcHAzdVhDM28xbkpzQU9GWUJPcG9QVVlnRkxoRnV4RzlOd2RrN0RXcFp4bFJQRG1BCkp1MDBjd0d6aXZNSWhrek5hVHJwVUIyS1JTbUxUcncvbi9hRkJ2NDFCODVhbUdnazRFT0FvYWcyVG9TY0xNR2oKWEpNQVptUzJWL2pJVzduTkxseUQ4VVRKMzNxNWp1V1pDWWpuMjExZkFUUS9OaVRwUnBzZEcwNU5XNm4zQ0kwMgpVbkk5SU5reFhlS3M5bTVoRjlUMUVpTlEwMnZXS05YWTAyRXdhQ2RqR0lLa1pyQW1OVkN6dWF1SUdmbFNhakpoClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3kvMDZYQXNmTTFEb1NYNWJKWDAKc3YzM1FnT1F5Z2VhSjFwbkVSVHBWQ3pFcFREcjVvNUU5NFlCSHRyb0dOeU0zejVWSmhpaURpa2lmNEFleW5DMQp2TzM2NnltOXExQTAyOXNOeFdRNFFoZ0xKb2dHUldHSVhhcHR4SzFBb1dsSXVpbkRGR2tOZERNMDd4dmFDbytKCkErK3RtSDIrZVZVMEtQTFA2UnhEc2NZZjhwVWs4UkdMbndwRW5ub0NROGxpSjZyNEovaUZyYllOQ0xLZGwvc2YKdHNyV3pZSng5bjdOVXd3dW13d1BZWlNobEJBTEtsSURXTjd3dWZtbEMreFA4dnRIYnNBcFU5NHQ5Z2RYK09xSQpETGhqeFJYMlRSSjg4dkhIK3FpVWpMM2dDejF1SUQxZ3AyczQvMHFYMlVVWDdRczR5c0pBMTZNbldyOG9mTkNYCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1kzdFNjMnFQc05lU2hxeW1TdncKU1VMSm1uRGlLam1pL2dHOGJVeXI0d0REM1p0V1JjUWdPZ2hodHBTalZjb2QyODRhYkRkbGdBUWhOUFFhMUZ6VApkbVMrSHdldkMwMWU4TVM4ajQrQTRocUJtNEVCZjRlNkRrM0hNZWNuUW0vNlBKWC94OEY5YW5CeVJiTGNkL2VICm5uRnlYNXVQcXppWi9LanBaL2dvQnFJN3FaeGxYZWFLTnBLQjVjQnJ5ZEo0Q3ZpOG9zMjloRGRGOUgwVk5xYkUKL0lpdG03NVlFVGpPYk9TcXJPV2hNMHhCbUVzc3BjMVhFL3o5UEpwbmNhbHJwR3RNcXQ1MnJVamFUT2p2TW4xNApqeFZhSDZvekNCTFhxd3JHcndnN1BTS09aT1lQNmVVSzEwditYT3BFeFFRU0h1aVhOeDNIaXZxaHNSZVA2ZkhECm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemFiL0duM1c0Lyt5aml5RFVrTlMKWnFJWXFpbkxRbzQ2VkVMZ2ZxZEpkK3BPR0NsODBqOHBsVmdySlVlcnBkaFZUOW5EZEVLWDM0WXAveERIb01jZgpmeUdIWjV5dTgwUFJHTXZ3UUM3S3VUdk4wTnV0N051cEppemd4UmVyckwybFo0NnpDN042dmRWcURWWWNBczBPCmVuMjZiRXl1QlVET1IzWXR6REV6MmFRUHk5RWpyUm0ramVnMDdmVndMaVAySm5VbFB0SlRHOHhOT2JjbWs1TUUKaUpCQXE1a3IwbW92ekNYdEVoSiszQWViOTRyWDlxVWwyTFE0YVRPZFdDbS9nNzFNdEpFcVI0OEUweU5nTE9HTAo4TnpXZG0xYnFUUE1QNXFLTWlmdGpITFlXMkp0TnMrNit0ZHZZaVdXVGNJYTNneUxIeU9KQWZjWjJCUlY4UHczCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1ZKMTR6cnlUVUR3dU1FSzd5OVIKTHVwTXdLS1JER3RyNUFEd3ZzWjBMNmF4WGtUMWU5aGFYdXJyTVVyRW4wc2NvRDEzdkR6UEZWdFdMa0QxVVdvOApRZWo3YkRlcFAweHhkeGtYVFNMZk1rZ05hWEQ5WkFBdmFWckpJbjRtTjVBRHJIWDZPVEN4L0gyRDBtME5FQklhCkFGRkE5VTFJYnFNRmVtLzMrbFEwTkJGdVcvOTNuMndjK2R0QVBvYllTWWMyZlpUa2hNaVl0NU9uUFBFRENPajYKSFBSWnp4QnhLRnRSaHhFVGo1QmZBbytrZnNZTzR6VktEZm03NEpLQVRBRzd4ODFKdktZYTcza1QyN3NCRENuLwpEUVdvOVdmaWdxOWMybnRCYUtkZXdOcVJ3cmVqbzNNYld0ajIrV05iL0FoL3RDQnBtNzk1cUxQZG5DOXhmYmtOCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3M2dkNSNHF1dDhyVHEwcHhhZHIKREl0QVhUL0Zzdzlxb3Z5QWNiMGwyQzN1OWZnYVZ1dVh4cjV5UFgzZkM4dDhCKytJRDN1WTUyQUw1REE5OURoegpheVFlN0xwMC9nQzVFandRUzN2Q3Jnb01hT0Q0R3kvMytSWUJybmNzNENrYkhrOStPMm0rM3BHNTUzMEVuVThECjFwZENKMWRScWMrbSt4R2I0cUlmdjR2NjYzMFpuRWFwTnRQOHZWYktXNDNqSXczNkhJOFJIZmR1MnZCR2NKZzcKR2FVSEcyYWxoVXd6NTFuV0hBZFMycDdvQ1JRdjZKa1A4M2ozUXNzcnJOTHJ4RWVpbUUrTzFnb0w2WGNQUm4vNApNVTl3emVpc3AveTZFa0JqLzVYZ2RVWlQvVURzQnFzaXNFd0Ira0lyUGMycU9YMkh6ZFd5Zmpmc1k1ZHBuQ1E4CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2crdzJiNHhZRDYzTkVGLzVweDQKRXBrL2EvSTNhbXFmY0w2T0NUR0FhQXQyeENkbEpGTWtWazVCTUJkNlVwWGgyTjkxKzZwZ2xBNkpJTlZXRkdpVwpTYUt2cnRrSVByNUtkdFFSK1VROSsxdTEzbC9NK1R2YlFyWUFyOHRxdy93MlBWOUZ3N3FLcmtaRVk5bWp6em92CkxsZWlTUDlPSFpMWVVCZkY0cHdONmtubkszWDE0N3pEa3NFUzY3YnhiY0IzVnVWSVo4ZHMzNmwra01Zc3k1THkKdGZlQllmMkFaZ1RLTnpEMTlZaUN3bUlVcndmd0w5WElpTHZRbUtFNE9NaHUwSEtKUlBINnNmN1luc3orUnRFdgo4dUx5YTlqZzBFUUNPTHJBeS8zRUZzSVFCVHkvektLNXVzZmdMVVovbjVoR1ZSQ3pvR295Y1ZmZFBpdGt0VDdzCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemdwNVlLY09HOXM2anJYd1NldXgKTWswVi9zZmltL0VPUlRYV3FQR3BxZER5K1J1NmhuK0drUDFNRTV0eThISEtJenN1aE1ydzhtd1o0aDZsM2o5aQpibFRxT3ZGSGxTMktHNG9FUE5NUVZpUnRubGVDekI1bHhuVmg3ZnFKTU5IZGViZStUY3ZUNlNrTFpYSUlxZ3VtCnBnRkxYZ0F0RVhBdGROOTdxT0p2ck00U0liaHFMcHpLeXFqMitWYTJVTXlIYTVWazN1a1NGa3hia3ZzbU9hK2EKRyt0dWIzYmpMWG9kQVBMRExJem5aOTZSZTV6cGVZS09zUFI1M1R3NXZ1U0lTd1FUK2lHQ09qbVVOaXdvWWtsVQplcXorVEVvWlN0MS84SjZrandHaEJETDRscW1ucFdWa1VkVGJqSXpyUklvUXQzN0c5NVZyUjRyK0NSSkxId3R4CnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmdpamFHUmZsWHNibVdsMzBndXcKRU9YU0NWbGRoa0RTMkNoYXdCbHRyN1MwR0lrOVBPUTE4d3lRRUlMQmlUc2djcFkwazc0YmQ4VUF5clh1b3l2dgpQVEpocko3WUI2TGNGeGZhVVhla0ROcmw0eXg0anBWa0h5WG56YUVsbmxIa2tvSWhwZWdrR252VkM2V1pyUXBaClJ5L0tmTWtTY2MzaUNTWWZ2UXVWNEx1dWE0MkZDYlFPZ2tsOE1YTWJtdHZaRFloN3Mra09MUzlzMUkrVEtEK2wKbGpVUTRBc21UVEpJOTBRd28raFpZNldWaVBINDRvN1lPdGVxd0ZTd24xckVMQnlGbXBQa3Rvck9xcmR1U2JSSQpDbEV6VXBXYjEybTlGdkgvc3pFUnpMeTZ1bERiZjNod0U1UkxYNUdNYXRhdk85RGdtdVhLMGN0TWRBS3VjQlU1CjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMCt6Z0pKSWEzckdwUk4vNlNnbXYKUStHd2l3SkUvc25MNnM0SjBlTzFzREdNNGNYY1czUDZ4eEU4VW9yTzQ1TzBwcmY5NGlMM2NmcTVmNEhMd29QTApHaVFUNEM4Y05ocjBSMVFhSk5sUjJBeU82cFdiM01BT0tCVFV6cnovcXFVT1dGTW44djdoUkNGaUJlWGFjOEdlCnpXbVVHSHBYdHpJdEhMMzlwanBXbkhTeFFRV2ZsSUFoeGQxQjhEeGsxaEtaU3lpWlZFWC9QWGNDTWdPN01tSmkKNXdmMlh3WVZkbkM2YktZM0pwaFBzUVVoeFAvLzhxdTdHNlg2Y2dtTXd1TVI4SGt0V2x2YlNmL3NSaU5zYjBDNQpLWHN6QUR0RE40V3NKN1RWWnNCVUVDUlI1UysyaCt0bW5JSnlHTkFVM0RlMUlmZER4NmRhbGFCdmUvRmlMKzhwClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjhpRTVlRWVCZHBhOXBUbTYrNDAKSkNPekxMMTJjR1dVNDNnVmFTd3hpdVBjTFNYNUZwcDNQWDBENXRvZzJNLzBKaEVtQTVkRWczaDR6bytzbm5rNQo4Ulg3TFpIeTdqYm9rVldsY3lwY1BUQVJ0WTFaMjEyVEFYZ1BxdnJHSU0rcDkwNTFRaTdHbVQvMGI4UU44QWp4CmtNS0xFMnJuNkdPY1JQVkliSkhOSHp4Vm0vK1FCMXRObnFNUkVZYm9BZ0pnWHNKVEVCVERKeDJWR1VUVm5kTCsKRDFBVVpxanB6R3VkbmZ5WjFheDF0M3E0V2VlNzhHKy9sTFdPckZYbVBDWUlaUHlnYWFqTm5TN21LV1VzZ1ZaeQppOGw0V1hKMDFVRmY4K1NQZUkxRUljSldmaU55NitZbjg5TW5XRklXb1NaYkptSEQvZ1VuOERsdXBXK2VsOHBMClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUxiZkVTSThEK0pFMmhRMkQ3R3cKVjM5OGNsMGd4a2J5RVJpWlBjV0ZzZ2NtVjJXVFYvWUFUYmdMa1J0ODFyVWtKVnA3K2FPYUZIbGV1TzY3NTQ3bgozUGhhV2tjWk9DaGtYcUtPR3pxTS8vK2tqc01adFY2NkNDUTkrR09GZmZlV045SlNmSG1wMG9Jem5kc1BQd2tLCldVaE85SG43bGdtdS8yNlJ0Sk8yS2g3cEsvTW5UWFcybi83WXAvTytwSTdWUkt0b1FLdXcrN3kzZDhLVElxRWQKMExHUWxxTjBZcnhxNzdaTkpQcHBQN3ZkV2RhQ2ZuVU9RdExYcWl1c0hWMk5UT2d2UldnVmw2MHoyMzBHM25HSApIZ3lRTGtuNVVJaVgwR2pQdXc2MmFWRTdKTXA0cWRwQXM0N0s3NE40WG1iaUtlR1VZQjhqSHNyN2N1eUFwTlhpCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFc2cnVZSEUySFVTUk9jVFNlYncKU2JuYTJvVUdRVE9DTEl2UFZNL0hMNld1Y3l2eXVIekdoM1VYT3V5RkdyMTZ3MmdjQnhLNFlmLzAxRlBCOTlXUgpibWw4c3I4ZEhwNVZLZGhpVDJ6eEplNm52azFFSzZ5RitpZ2NyU2NRZ3BWYzVsamVXbzVyakJXOXhSbjQwRW9PCjJ6ZTljcHlrRG1zREZ0bVNkZUx5WHZhcjdEMWpQVGtRNkRFRUI1azBiL3NQdjNRa1JoLzhsYmNEY210WUcrR0MKd05jWm5WWjNNMHVpQzBIYzR3aC9aY3pKOUFoazNlSVlJUGp1dGk3Uyt2NUY4dWpDUGJ3WTFtOEpjQ2ZEd2hMWApMWXRIdG1HbXpDY2lxMmdyOFYwdmRpaTc1K0xrdVU4cXplckZ0aFJLTzhQMktETVBscUxRTitGNmQyMGxseERYCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnU4YVJnNXlPdlROYTJnOGZOVTIKSnlmRkdWRVF4UHE5Tzlxc2IxQU5qSVdiMWRzcnhNa1hoMEk0ZHVNV1Rhanh5NjVtV05uUENVSldobG01N3R5TApKdEE3Tjh2WGZDUndJdGFsODI1NkRGaTBqNnpRT2RJSnhtSHRVQmJ2SXRUVnNGNlVFeDJ3OWZSUVZ5clhOUzV5Ck5Ub0xEbVlaaWpoMUZxVXpiQm1sVmZoMlZEMGVzL0UyTDBaM2ZWNXBqOW42cWVabXV5eWlmWHE4UndkTW9vdEMKWGxpSEd2VWR3cXFrelg1aWZ0N0s0eno2M281T25HMjdWQ094YXRwa1QzaGpRenh2d2lWcmJpZWViSTZLbzExZgp5WTk3MkpHaDVnbWc1YlEvUjZTYlM1am5rdlB5bGR0Y0d5UzhUZUhDTXlCaXZIaWp1VXV6ZzRGclBwUW9kTnJtCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNk1VdUtlY0Y0K0djdHEwNEN4VG0KN3ZCbUliNlZybHFpQkM2N1owbUJEVTYzUlQvZmxhVFF1dzZmRmwzWThZNnZuakljTmw5MGNCaktRRjF3YStIaQpleXR1Z3RLbUcrc2RDcXlkTG9VbWUyKy83MkZBWkcyWmlsTWcrMzhOc08rNGJCTnZHajBVVitweTRzYnF1enJkCk9VVHpPSVRiWk1SeWpBQ3FCenJ4T3NwZUVjYjQ4aEhzK3RHaWVQNHNVNkVPSXRxOFI1ZWFhTTBtWEtqZkZkMWkKcXIvZFdhNmdUMmdvZk5jT3RjaTZPaGluc0QxMnp1UEhlNy8rNXg2eVhNanJadVZuY0Y0RjE4ZGJZSVpBRmtsNQo5d05BRG5FNjdyL0VuTDFJSXRwZFVIR05xOElFc20wdTFsNGdBWnJYbVJrY0hKRW5CUjRlc2x2VFBwVURuanZJCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG9seFNCTUthMWhBajlscVVqOEYKM0VHaG9XRm1aKzRUU3dIRjlBVkpBQkNYemtIK1doWFFiaFJDYXpPWnpGWGhFMjJueGlQYlN3MHNsYnZIYTlGYgovMkUvd3J6T2pFTGhtRWtRQ0ZsdlM3eDJsTUJ6ZjBZMzNGSFhPYkdNZ1FIbGkwMDJlMXJ1d2doNmp5eElQTGpNCnhKNlgxZUVSajZsTGhSaG8zT3JRMFNSUFRJRmNIL0I0L25sUFNscG5WQldDQXpIMlUyUTVNZ3QwK1lOQ0RjK3oKYm03RG9xbGo2V0V6YmRJZDg0S1JmZ0U1aHlHMUJhTWRFWG9hdHV5cnp0OTM1RnZ5c21mRWxTd201bjdPVkpkdwpxdkhRd0o3ZStUa3NCTGZqenp3NDZBNWxvZHEwT2YyQlBWSUtsNlJ5TkdtL3FDOHlpL29GcE9RZVNRRnI2NHhQCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkh3M0RwSVgzN012SzZod1ZmQnAKdG9nYWFSUGhaN1RwMEtTbVlERVRmUnBYUVJDTHYzVEhXTXNrd002Mm1rWEsrRlRRdnFlTE1mZ0htWElvR1MwQQoxb29EbjQ3VG45Ty9Jak1seVlaT1I5OG5qSEdFdGxGZnl6Rmt4RjBNVXlrTHZVRGs0dThqMHYzZXNEVVo5UUlOCnYzeEg3RzBFQVZpQTFDNDZVRERBbVRNOHlpTm11UjZKcDI5ODVOZ0ZIZGtKd2hhWU94bERJOXZkK3dYalhMSG4KbndLZWdVZVhFQW9IaEJpOEdTbm5ycDZXWDRmT0o4cWRISEVUdUx1WXhaZEY2SFhhamFGTTNYN3JkMmR4L3FQSgpQR25EdlFRalFIV3EyNXZSYkpBNDgrdlJIbVptS2NRbXZieEl4a2owVWdQNVcxNFVYNjVOWHcvWHhjaDdURm56CnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzlNOUUvOU1GVFRpMW1zdnNSc0wKbDZBTGt3aWZGWE5ReGRyRzB3RnBPeFNHcmxQUnJtVFl1K0p6QTMvMWl1K2gyWHZKcW1HQ3BqV0ZlKzRwSldLeApReUtQWEZjY25EdjE4MU1kYzM1WjBVZTd3MW1JNUpxMDhwU3AycW9JSWUyMSt0UHAxei9YUFhJOXpOcmtSV2sxCk1qdXlxblZ4djJkREpQL1JzRWY2T1VFZUZ0VGJLRWFuMXpDbzNoTUE1VGkrZ2ljclFvUC9ON2ZyUUVnRi9BR00KOFJsWkdhaUN3bFVwSWcxelFtbjhmVFZjWmlFb2JrNTFvKzV3ZVgyVG91Uy92V29uL2ZKanBFVzljZW1CZXBTYgpubVIrWEVnZzFoeFF4R2pwd0lRdUFyUzRJcDBKaituMFlEL2tUWWlOSmVyV3Q1T3BtMWtjWW5oWk5QUmpkb2VtCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE1CQlFFSmRtbTJwcG10LzRGK04KYWlmeXNWUE93U3NmMXVYQTdEWnVnSFRsWnFEVlY0bWk1eUZFdnhzS08vNS9xb2p3VTVmRFl3ZVlpN2h3VXdCeQo5RlRsdlo3MWVyTGE0MUwrbytOalJuMjVOQ1JlUTlqYWhOWTY5TDdFYlc3T3JlRGhxa2xpRk5aa2xYZGxmNzNQCnJoa2kxdk5mOU1xdkVEYjlUQjNZcE92Vy8wWklSd2x6dHh3M2VIaUh5VW52bW9DNDRFRCtoRUk0cG81ZVV0VEIKTG4yWU8wc0ZHbFlPZW56NFUxbENyd3JhRzBYaUs3QUJSMFRCZkdLUkZYeElTTE1iYy8yWS9XODBhVkJvYmY0NAp0WDRSZ2hYMHpTZ29HcFJWQ210TFhNd1p5THVBR2pzR1lEN25DZTRFZVdFL3hHcG1EMjFCbmYvUG55U3NJYm1lCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEtJdWVLeVNWcnpkNitVNWlxcjUKYmFQRDlkK0VqRkpER215WnYxd1pobDNDZURDaWVIamJNL3hXMnpPVmdPWDFHSmZHVWxoQnhqejluY1h0QTE4bgpQdDVGbUU2dFJnTU9PWkdZOSs3aWhDTHZWeXhZNHFhV0RYaVZTa1ZyL0xCZEhZZ0duWHJOUXk4RUVaWjFBMzFpCkhNN1Z6QkxtL3p0WjYwUjZ1MzlBZ2gzUFBxRmpUTmpidEN6Uld2Y0thblBYU3lhenQ5a3U1RTVFUGVnZE9EOUkKQ0JoS095YnB3bmNxSStDaE54OG5LdnpLVEpIem5oTklicWZqUDNpSmt3eE9hd1Q3UGE5UVFXbjJwU3d5dk1TSAphOEJzTDRIYXdhRTJTc1pwRmk0WW1yRTVsZXZuenF5MXVwNVJqZU5wbE1tL285cEYwSmFKQ0FLT2wrWmxCWXcyClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjJNcmhNOVFvK1phMXRDNW10dnoKR3VLblozTjRkcW5zQzYxakw1NVUzVEs3akhiei9RckNhK2dkLzg3dEpvT1VFc2ZWcU9hMGJrUVlLMklLQkh3awptcFBYdVh1UWVTTmJEWUFGejZHWGViTHJlcVIxMllETEhKRHpoSDVyWTd1QXVrQnZDNU9URG5ORGZ4SlZHWEpwCjV0RkhhaG1qSTNqcDE1S1FOMm1mUmdhZnVLblNFTTdhNGVPWlI0b1lKWmpYd1N6Q0xSWDJGUS84ME9WQnJUMGIKUm0yNXk4aW0wWXdYLzNyQm14VVRCbERhL0lmV1VnWEMwQzE4bDVqVkhFcVc4MjVFNzFQOWdHbmpMc1B4ZWhtSApYRWJ1UkU1SDVOaEpPUHZVcUY0aE9GTDlkUDhRcmNTaXVSYkloamVjS0x5dW1pN3JSN1N4U1haNXBrRTNKQ2xnCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3VnZElVeDJiRjhNNTNFUnpnZVYKbjlGa1Rwb1lDVFJNMjc2ZW9UYVV5ZFpMbWEwN3dMWElqUzFYVERTMDVJZko4ODh1enFVSmpsbFpQWnphOXZsZwpKWThueEhDNEwxeWd1RjcrSVArTk9CMXhid2ZVcWhBQUU3WEVQZDZPNzRQS2JQa2ZiTGN0YmhValczRHBXOGFsCmNZWEt2cHB6cjd3bnhmM0JOakxSVForMVZXbUx2K09pUjNET2t4NWJtVGRlMDYyVjZpbGkwMWJqQ3NnckI0bXoKa2IwVlFEQi9qZGo1UEtlek9oemxwOVdFNm5jUHJ0QytjZlZnMzJIUFFMbVN2OGFITTFjcG1uRGhIRFFEbG5TbgpSL3BjTVh1TlF1SUdNZFUzOXk4a0NoMTNGSGw0YmdIcDVvMGx5Q1BDWllDYUlVZ3Y1ZHpFMUJhd1B4ZDY5dENjCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjIwUUVPSGo4QlN6S09BZWc2T3UKVlVSK1J5eE42eG0wc1E3OHJBenY4MDhwWkNXVzlhTmx0ZGxCZXFjNzRZdzRSSWJFdkovUHNCb0dILy9xYmlBVAphWXRHbVBEeThOY2JVN3UyMVhTTFlyQytEV0NVVGFMSjRyZGVYMkFIcmFpU2JFdW1LUERmc3JTbXQrejlqcWFRCjdZakJjY2lKVVdBSXdtQmVXNmx0SWVLWG9vYmZjdVQ3UFJVdjd6d2drYWQvSjNSUzFWSG9VSTBpc3ZCdVN1MG4KaGk0QWpJR0VxZXU5MlQ5R1A2Q0ZEeW5PWWpzQURHWllhRWpRdVBlZ3VLOC80dVZWTVpkdUxwck9wSG5WSHdpdQp1VENjSFloYlltL0lEeG9saXRzSVQ0bEo0QytiUUdkTXA2Mms5QlZQQWkvOFRCRGlGa1ExNXBGYzg1dHozbklSCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFB2T2lFNWJiVEVEdXkxK0drZjgKQnNQNFBRYXFTNXBtb05OaXhHeFVFMlhBa3ZCWEhTTm9qSWFqZHdGdkF1YjM5NE9sb0RaZ1FkeTFvcHZyWlZlWApaNXEwd2tqSE9wMVRBVEcwVTlvZHVzZVFpdDVTRUN2cFRScHIyQldtODg3c3NKY3h1bkIyaWhwaGsyOWNyRmxRCk8raHZuR004bnd3RGRHb0szTE5uRzQwYjM5RUs3bkttWVRvYzEwb3lQU0lySnZQekVSNWNRZ1BqdlBkVVZmRVcKc1U1dGNIdm80YjIvc3ptZGN3TW4rWk53cDNxWWdZdi9RN2Y1bG52UzRvZWF3Wk9rQ1U4VHhsWEY1V2NGSDVsRApoN1dDNHBNY1Y3RlFHOEVvaDRjSTZ1QzBFaEMvdzVvMWVMVUhuQzdENU9KamdXTEdveC9rKzZXdldLMGxxY2l0CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmVYMVNaQzRhODZFcVlvUUkyRFYKQXB2VGROVkd0ckp1eTd5a3pVYmZFdGpWb3BCeURZTW8vVk1MN0oxODBvMGZBWm1SYUhvZFNmc09xaDVoMlFrZgpTYWcxYU5BNDl6Zk91c3h5ZkpVaW9Ec3B2QWc5dDhxbVRXT1BHa2x3akpVemczVkNpYU02WFgrMk5VT3JsSjlsClRvcVNuZ21hdVZDc1VKL1VwSVMxRHdnZWhsdTdkeHdscnhmUFRYQjdEd2k1RWNqTm1GOTJWR1BLM3FjNjRxR1gKVTYwVm4xM2NDajN5NGd2bWtKVVR4VHBYaHhHbXZnUjgzSmYwa0JSalBBaUhpWXM0SnY2a3ZVUnBlK0J5cGllVwoxQXVTTW9KZDI2L3pyc082ZUN0MTFJVk0wbzdPUnJFTXpVQW5YSVVLK04zUHhoQVhHT0VMSkh5a3FQakFxZDZVClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWx2VEsrQWl1aVF1WTNJeFVHVmoKUkFRR1dqRkozeERiNklVRytlakk0eitUL2FSTW02UWdVQTFHQllUSnk2Y3VaREpOUkV2a0lQS1BNbjR2UXFQRQpWRUUzMFBQMmFkVDgrOXBMMFVJMUZ4Z1pmb2IxdDgwVWNad3VBZ2p3N0VHTGhISEY0OHJMekJZTDdKWEQyZE92Cndta2o4TlI4Y2tzY0NFVFAwYTJ6T1lvcE05RjUyR2toeDhDVmhlazJxM1BiaUhQUWpqekhycy82dzJRUzFlZTcKS213VXFHUTgzZUNobXJJMEc5TmVqbkcxQmNkblRSMTd4K3JNQzY0b3hMUVFaVEQrZzl3SUxKNjl1UXZ5bWFzaQp0ZEF6dGZoVSt3RG5TZng0a0psYkNxSkFUL0N1eDB2ZmhaL1AzYjdaSHFQcWdUUFlIN1RtT0RsemdoYVZ0d1dlCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbi9nK3VlU04rM3Q5YTFFMlYyVFQKbjc5NmRGWVJzeTU4THJjYXYrdzhDUG4xTkhZWWM1YWpkc0dIY0VqOE1UM0ZqZk01U0dOTTNpd1VXYklIVXdLVQoyMS9zMzRJa2VYVVNBMVRubWc4a1QwSXJxWnRsK0tIWjFWaW8rU0kyd05MRVo0YXJiRi9rbkY2Mk1DaGg5ZE9jCjhLd1Q0c1RUektIM3BOdS9uNm5sU0FBelJwdzNhZVdCY0g4Z2E2WVRkdXgvVmt2WEExTStwbzlFeE93Mlpya3EKQzBXbUhQdldRQjBCYVJPeGdJV284d1BYbkFLTFQ3bHRkRUpHc0RTaHZ0S1J1RjFyUDMrYXl2Sk1hRjVFNHkzTApWR3pGSFhFSHphUGhwUjkrNTlsekM1c0N1Mm5pYXpaYThUMHZlTlI0ZU05NlJZVjFFenpkZTRmdThzV2hvb0pICjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHh4aGgzZ0dYMERDd2o4OXIzZE0KZndaWTNSRDRCa0l0bTkrSHE4aWdJZUJLQ09VVFRHSXRjMGlyU1BBU3liaWhWKzNZRGR4d1duYm12MUVjUG1CbQpqY1dhdHRaR2Fjb1UwcDIwNE5BWGdzcU55N1Z6TTdRYTZ3R1ZzbXlMMUZDSWFiSmc2amdyUGx6OFFDRGdnMHNqCmRDRy9KTDRreGpCRUo0Vktab2hLY2JYSXlKTlFKc1pobHZNd3ZYbHBiMzYrYStxa2Y5cVNZbk1HbGFmVmxoRHcKVWcralJQam9GbXY2b0d1TFc2SXl1OElXdkc3VkRWcks2QTVxMGdZQVV6ZGJEcXhKbWtocVhieEJRT1g2ZEl3Uwo5eHRHS29OZFBBcWt0R1AzUndDQjNVb3BGRFVnY2RlMWpnQTh6bi9SY2d4SzV2NmtFczZ0K0MxRkxvcWg5dW53ClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN21nVnc4RlFwQS9peFJGWlZvOHkKZVlGcEZvZVE2NHhqOHVPSDEyZHpCaTFGVnppaFp5N0FDSWZhRUg2MnlON2dKdUMwUCtJYlZ4TXl4bGNwazFqNgo5N1YzTCtkaURtR1pqcVRxemZmRXVtWlkwQnMvMEZsVnF5bVdoWm9KcUJqcitBWWoyOTNrMDhTcFB3MkdWYzJMCnp3blNsYkNmVjdUSE5IN1ArbDJZaWIwZ01SMko0VFBodzl6OWljd3FYemFsc3UxbjRVd2I4ektHNFJJNHYvOWMKa2Jyc1ZvWVNaWW1ISktiblEvbmJRR1daS3RuYnpIbktlUHhOd09HQS9sQkNyUmZWN3BXR1B3NVEyT0gxaHhoLwpuK01hYlRNYnlvR2FZZndpdGwyY0lnZllXZklySWMrSjR2WVJYN2pWeVJxR1RsYzgzd09CWHg3V01qS2YvUEFkCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2IxYzJpVWJteVVUS2xvSFFJckYKNlJJRHhseUs3REI3ZDVsN1cyV3Z2cTBUZktabUFQY0dKQ2pFNlBVam90eVZxc2dzVlAwMVpia0IydGNkejVDRApYNVRpODZKV2JwZmxVbVhvVXJtaGtYa0F1UlVMcjh6VExCd1FGNm1zTjhDK01CUmxDWlpIVjdKRjdMamZzRnRnCkRSZnY0U1FOTDdGdVJ6OVlUbnlmRDNVZGRCemY1d2dSNEM4ZXhJNjVVMGhTN00rcDRJRE14MUxTRW5tNndtSGkKUTB1R1pjOGNUNER2R1dMYWdKQ3hER1p2TUlrMEYwVWhXQUQvZklIQmVwTWxqdnZYb0xQNWZQYjY4STZtY0NSawpPb1U3d1hjTVdONmhSRXpPcld0eE9ZcUlmVW9BRnF6TEhoeWtpcU1UZ01TN3hyQ3hqY2NqUGc5VG4wY1hYTkFkCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2xRUVJrNmtvWGVuZHRCZHBCaTEKVmN0dXlwVkdTQXpTYTJjSWtqaDJXTDVvNzFmalNBeWhUVFk5UXBVVDhaUHdNdkFPdDdwUGdwS1pGUjA5RnpyZgo4bGRJVFhWR3JFMGUvQlRDRzFKbkpvTm5RU0pPNDJ1N0dSeDhiVVBUbHREOUQ5RStlNzBBOEFNVVhxUW5LRS90CllqSUdCL2FXd2F6ZEhhMjhPWFgwUEtES0s5SHVRWVYzNEdmUmNHSXl3dWwxeDUwN1l3UTdIZXZleUN4WHVNNGgKZjVKWis0MFYwelBpNUdSSUtrVFFsVHBJam9jTUJUWjY0OXVNVE9lYU5BN1lMV3RHRTh6Y1hQRDRSMTVaSzVYawpYc0NzTm5qNGo1Ull4RVRCMk5RMW4xbDRvZTBBYi9mQ241R1FLUDQxeExGRG1WQnNueXRMejdUTldkYlpOTysyClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcCswTnJ3RXd5K0hVUFp1eUV6T1EKQ1BlOVJyMk5yei9jbTRVbnE3a0huTzB5TUpBMCtsTUw3QncxaVJhMXR2S2E4Y08zOVZLT2NNL1J5NWFlTnYrUQpNOXZQeExLTU5kbEQ5amNiUWQxN2JPekFEdWlYeFYvNzl0ZW5DdjR4Nll0MHdadDhCMEVxemVqZ2h4dCtuUWpLCktoYmdkRWZuVFRhWmdGZzk0QUs1M1NrRG5XWEE4T1RuRlFtODkyUHovLzUvOTJKTW1BeVIzUmoxRUJsWDQ4dHAKbFZFbDVydGNHSUJGdlBMcFBaWGhEck5GR2xvT0hOaVV4c3ZxQVBud0dWblVOckZoMXFGYzJWbWZrTThRNDBBVApkcUtZSmk4MGtKSGFzUCtIMnFnSW5ORFRWSVZocTQ1WVFsNXpZVGNEcEhwbStqQ0VpSWVaZEtvRSs4dUtEL1pyCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXNPMkIwbUNHak1LM3NYaGFnYkwKNHhaT1F4TEVjWWo0aDhFTUlUanhKMitOR24zcXFCT2tmb2pTeUpmaWFXcXhDa0EyS05hQndPNTBvZFhHb01WaQprQ1M3dUsva2JUMStpeFJvcnVCazkzL2V1U1lTOXh4c2hwVGYvY3JGek93VllLVmtmdjR5Rm8xTy9ZKzRONU4wCjF0a1ZBcEZtSm5OZGRQYmgyUmx4UVhCcDllQTJwRHROdXFCV255MEtaV25qcVdjQTlJYmhXV0FhVkVKbUJqYzMKeGVzdWVhNWozb29qYWlDUGRGY2YxbXk1cWtnMHBoTUtXOUlnaDVmb1F0dHcvTWtJeXFHaG42UndneklhSjNKMwpHVnFYamwyU2RkZU81RVBDYUE0SytTMjhQSDYrM250ZzZUMklMVFpDYmFzRFVkTXY3eFl5a016aGpUS0NFT2JkCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2NaamVhc3IzaEV2aFJlN0UxTjUKZWw1bXBLeDB4dXpsS1BrZnRJMk5qNzJONk95VE5YRjZSdGdlWGtnbUZFc3gvZDNGSnJiOGpndVczblpRZGlsMgpCMHA1QVFXZ0lock5FZE5IUEMyM2hNeFZqd0c1R2dJOEtDWDNNYk1tRHIrQ3BDZ3dpWWVyaVBtOGJWNnBqeU1TClM0UWo3a2dTSFA4N3NNeEVNSGJrc2hSVnE0UDl0SW1TbUtyczZwYnpBbUNLa01NdUVmMFgzVk92ZVE4bHdGc3QKcDd0VVdHVnNjOHJLTHRaMXRGcnQrL1VpU1NDeHdQVWZ3azFZUXBwYmZad3Z2SFR4WC9aRDMvSCttNU9zNDl0Tgp1SFhwWDREUDNzdHVBaFR1ejhHTkhjeVJ1cHhJY3V4cExDaktJYUg2eTdld0FLY0J5a0htbDJnMElZNFNWK25jCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdms4bmplczk0eTBwWlNVZnRGd0IKeG0vZVgrSE1iVGgveDUvYWJqR2VmZzZtaWtMeS8ydnFGdUw2SkE3QTdRc3puQU5ldjkveGN5Ym50Q1pyRVE4cAp3ZUZCNmtoMHJSQnVQM1ZWaVpxdnZBMUh6S001dTVxMHFla1lFV1h5d042T2pSMlZTRXgxTDhxRXROYm5SVEptCkRlT05zbTVoRnRyT1Z5UTN3WERCZkE2UWM5dUZqTUljNlhRcnpWQ3Q1VkpxUHloR2srVVF3U0cycS90d0FFNisKcHVOTTJzcTM4WTU5THIyWjNXeGxnK1dRaUNSTFBudTl4MFNsaFdJclNPWkNLWDhONWUrdml2cTIzcDNmN1RQeAo1all1amt4M0VycWl4ZFFoU3l5dEV5WGRqdVJtQ1dqdDhKZnMzMjJxSm41blVCMTdaQmp1WVpsbURjdmZVa2RaClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2VSYWFLcDN3c1FjQUZzWS9Sdk4KWE1QV05mb2dxek82ajBOeEw4anRiTHNpaTZCdllrbFVPSWtCVy95KzZTSGpVVGVvU0pDbFRsbHE3dGdkYmg0Ygo2Z0ZmVU1qT3FhT241ZFp0M1BOcHVqQm5rcm5vRWVxRUdQRENEMjJVeXlDWE5jc1VQUzc0L0V5V1BCZ0hFMkI3CmpHNkh3bmprN0RXamFmZWtyeitnY0dtNUJRR3NHZ0NNMGxLb283aFlUZjIzNU04bHRERDdMTE9CVUw4MjFta1QKSW02dGsyTVcrMjJia01VcjJaMGFkRi9CUHhWY3FhS09WM2NxaC91K0xOcUhlZTFiSGJPZzRzeVVadGY5MWxmRwpzdFVMcjkxdDVrRGh3SytxWE4ySndqcUxYUWt5S1pZaytxb3RDL2I2VE9mZXhHMElwTFp5Rmg4TzA2WGNGMDlKCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHFldEhxejRpSGc3d2pIei9MRGMKK3o4Zkk2Ry9ZTTFZS2xZRHpLc2ZybS9DZitDRDRxamp5TkNsM3BTLzQxeXFqTTVuZ1h0UWlpNzJoZmlyMU9oNQo5TmN4ZFdRanpOdzJra25LQVRrTEVsWmxHSVl1MVVDZXVUL3ZXV0R3elgyRld2aEd4Y1h0cFJVUE9oK0RIWkFqCnhmcEFBZW5COGo1QXNOOTBQQzNZT0hMU05uZVM4QVVERmdORmpvM1RvUHBrZnFTY2lZRWp6NlNJdElWSFE4ME4KQ2t0WEJDTG5BZFNEM1hEMms2bWtPU2EwNlZMVnZqYlpnWWZDYTBkdWsrSVlOVlVPQWwwT0ZrVnBPREpjaE90QQpTdXM4eGI2MHV2R1g3ZWZYY2FxZnRuVXhYeUZqSG1kcmJHVWhURWpXeGRPOUpaRmc3Z3BZTVV3S2d0MEJQWmR3CmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjE3STQwYkFCUmJDVUQvZVB6a1MKS2ZVT3F3L3FMc1BRYy84eVVQdlZFRkR4QlAwbVBJOHdidENPUmhlcHpQcmJSQ09icVk5UndQWjFkejc4dFBLQQpwWktUSk9HR2FCdEZ4OFZOTXFDZ1llTTkzakxuazN4dVhnSnVzN2hCTUxiRnI1SmUvbm82MFVsbmlsZllNQkJoCmNOUzlmZlZUbUtmbWJQNTIyTHBTRjduM2pSSUUrSWRjZ0VBK2lZUnVIaU1senNtbkR2Z3BzRHZXaEdwUGlYaVcKd3JNT3l3d2dlVHJEb1dMRkRBa25UMC9DSnU4bGI0ZUV6Wm1tdFluNThYd09aR3RGbzN1eWdFeTUycGJzTEFRdQpFY3RZQ1JBbnRpWjRGN0grcmdzaklDMlpYM2hYdzhlSTQ4OTQ2WmtHcTl6amFVdDVLdWE5ZWliVjBZM1VZVkk4ClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzdUMHNUaWcva25mUW9VM1J3TVcKQStmcEIvZW5yc0dKZnkzZVBvODVpcnB6VDduS0hMWjZSeHFKNGpqQ3VhR3d6TjZRTnl5akRaaW5SRWIwUzFzUgpZZmhDVUJwVzljaDBSdzFXMGYrREJzMnZSdGdVUzZtUnVkSGxyZGNhaXJXbmZzL2l0cS9Hczg1Y2NoQTdORFM0CkMwZjhwQVo5ZjRsd0E5MlNDNmFRTUFPV2UxNTRBdXFZeS9USXpSQ3JPdi8wUDd3b1ZOUWtIcnQ4dno2eVc5OUIKSjFmWkZKSFFRYVd0RkpiRHQybmRQd1A2MnJ6Y3J3VTFuN0xVSUhFTTJaUGFBT3VMbXNQK202Ulp0NlRTQXlRUwpCTkp4aDUvZkNmR2RhZTlmaHFPVnU0ajlFL1RXTDl1bzAwV2RUTm4yTTlQN09VcVRCUi9mcFRhU3ZJL1JiS3QrCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEhZWFBNNFR0S1NlSFdwSFJzaHcKNURJTk1paWUvUFI3aHdVRzVDKzNJYk04OVhjdm5kMkRYbktoWjk2Nm54bFhoQlFKemNrbzJkYVhPdUl6UmNiSApNa3M5SW5zRHh4cVFNcGczYnVhbU1pNXU5SUZDUVIxMDlKLzNHd3BWWjh2NzFGV3Q1a2VVdGZieStiSGNSUTNECm0wWktBM1JhWDhoVzg0b1djWml0bDhMamFHSklZVlkraWxOTGRjcW5ZaHZhNmhQTXIrWjQ1MDR6SzFhMkh3cmEKWjF5N3FlM1hZTXF3WlIzN211Z0grRlZYbUZsWjZpQmdWZXRKMS9sWUZ0dTIrWEJpd0dBdW52aEQ1dEMrR21BUgpXUENmZzBwcnpmT1BqNi9xVnUzWm5vbEhhTjZGOVduTG92V0JndW1Qc1NCSWY5bkd2ZFVxekZiOEwyem9LeHU2CkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVBjcWZyUHZubngwQVVqblIzbnUKZHE5b1M2V21NcXZ0MlRCbnBYSXV0Snl3cStCaTdpeGJZUG5xNUhKeEVtczlBSytHbWxocVA1c1ovSXpkVHU4agpsWEZrYWcrNlZyUHBHNFY5bUl4clVqd0JHcjU5dXp5bHIweUwwVTQxL0JUNWdra2p6alBKQ1JaSG9pRGwrcnpVCmdqSlFqaGlub1NrbHh0aDR3NVVqRWRBOWt3WW1yNXh2Wm9USGUwaG1IMUpGM1NNWjZxZS9iT1QwbmxRSk4yZm8KZTBGa1M3blFoall6THJPUWxUV0owVDNjOUVEU3orZXhPdTJ0SXJKeHd0ckZwZUsvV0VLb0txMlZ4SzlpRXZWUQplNE9NaE9SSERyWGVVbWR3cTZDRXFmU0FuUkdVdVFzUkVpcmtWZjl5MVNpbXZuT0tIa050bFNPNjYwZWZyejdGCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzVUUGZvM2daUUUvUWl0VGhneEQKQ3NjRXRVUnVXMC9hVC9IUjBTQnVWMWpkajlSRktGcmx5Vmh5Yy9NeFV4OVhNdXRnQW5TVGtDNnkrV1lxMkdlYwpOK3ZwekZuL3ZQRjljUzYyZzJJOUZ3RkZQajczU1lQYXJpN282ZkN1aytrUmc3WHc4RHlxYm85NVNTVFphdjJDCndKT0FsWWtESlh2dElTZVBCTjZCTTdaVDRBSm9JRUtUaEJ5NmJ5T3Evc0Ryd1QvTlZYWWlwcXUwN01idm1qdzkKQ3hhWmNnRTZiVU5taG51aGFmMUwwT0hMQzBGem9mbGk0Y0tnM3BPVktSNStHT3JYK29NMk1veXowUTJxRHpwTgprOEphVytTVStaTnhBZU5wK240ak9aMnNFOHRSQlBwYUgwNHhnUmxyYkE3VnFIU3MvdzNmZWY5OWhlT3NuaTBZCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVpyYjN0QzhiUGtzZGphMGo1ZCsKa1JHYThPeVY4SW1kSWgvdnIzNzduYlVDdU9GSXhpekFwRWJveTlMYU5Nc3hDdlJnNVk5RGE1ZmhuQUdjdml4WApLZmlHTUsrcldKc1VjRkh4blhwOC8yQTgxRmR6c29sS1JPdkZHbGsreE1UMi85Z1o0c0tlVWhVRFJ6L1ZrUUhwCjRlQU5ncXIyYWhzRzZBTlVyM2doM1h2SCttRkRJVmlrYkRUc2dUdXEzVmw2dDNxS2ZrUk9PeVJWRHNKcDB1NjAKN1d3OWV0VU1zQzRzYWM0QjBLUkx0Q3BwL0dEMlVXdnA2YWM1SStucnlSbnAyY0hndmZPMWNHbndjdzliZ1JLawpKQmt0eloxb0FyOFNtc2tyTkNFaVFQQW9EQ0hPbXdNSzBVcjdGbk5lb2ZUVUhnNkFRSFpsQWh1bU1XaTdkQStyCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDFGd1FibHZIVUJqTTRrdFFNalgKazhYcnNjYjRXWkF6cEJ6WStKeDdjcWJ2czBWQkNQS0MwVnI3UmVab05PNXZtVEpEdlFFR0MwU3lSRjlXNXJIMApaYnFEaWpaeHJKWE5sSkd1UzlTKzA2WmtSNUtUa0oyb08vbUI0U05RN3JuUUc3b3dRbVJpYU9YSGZzbThlLzU1CnRFU0NhV3E2RGZWMVFvbXIrUEszNDVDalQ2a0NDTTBlZFJZbHRZaDgyU21idjNDMzNGODFKb3N3ZkJuRHRHczcKSU4xN01CcTd5VWo4ZDZBcVlPY2tmWTlOeGd5WTNkeGNRWlRxeHFBT0VNQWsrWTJqNCtXcGhRbDJzNXpJK0NzUwo5dzFJWmZqNmxRZDZCcm1ocmRoNHRMemJQMDB5d1NrYngyRXFWQ1loOS9RdjRsakthRDZWZHp2bk5tbVFEUHFhCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmxDR3lPd2VwM3FGTGI0cHJ5N3cKakFXSGMySmZwTUlGNTIwaE1aZHZocG1QQVBxVjlseGhOVThIQ2kzSklsN1AxUGpZdC9hVVk2S2Fra2lReW1oWgpMdFJnMk4zOVhYL3lFZkN3aEZodVVURkUyRHVvZkhHZGM1cVpwcHp6U2ozNUZwVHVtaTBRaWwxNWpvWldHSlQyCmJlMU5PeHEvK1hOTGFkb2RMcVdKUE9jY1VMUDhGUnU2NWhMekNuclMzcDJCcEt2MGlaaEpocTlqKzlpRVlxL3AKc1RBZy9idkJYNTk0cElyWVlmWEQzNUhmaGcvUTFRZnVEMlRYVVhuWWFnMDBVR3g0NE1wNVdqeTlGZUY1ZTFVcwozL0tpVUp4U3J2WkhsdDd2WWVHbjQ0a2xUWktHdWtQelNtUFJaU2tyOVVZS0J5N0hsUld5N2VpbTNjV3l3RnRJCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFp0NlRid2llZnNRcWdHeUtBankKcTlWZmhQMkkxWVBLQU9kUmc2c09qYXQ2bkloVjN6emFsRkx3TnVsOWRTSlVXZzhOTElwWnBCUzVRdFNCb0tDTgpjQmJ4aWFQNlNMQWRsOUtPZWRPbnFEWFlBc3lOQ0R0dytEQ1ZKcE45dWpHUEdrUFdVemJVaytwUVdoRmN1N2UvCnZVd28yVkl5U1FDb3RRVHU5NURsSWptNmtROFl4b0V0OGhJNnFtR3JYemZzQ2Z5dG4reU95VzI2MUlTODVqeVQKZWd1cU1vK2FLTHhpWGM3SjRoM1V1Q1dsL2J4djMrend2R3dselFNQ3R0UStydmVTZEFMTkRzRjFkenVIdS95MwpzSmp6R1ZLcDZ5Q3RHbkZWdUpVL3Jic3lvVG93MU04N0dNNzh1M3o2eThyMVZnTDh0SGs2NE14RERoaFNwOFMyCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0JUcU1lL1drUDJYZHJJVkNRc2UKYmhsTU80L0EwR2t2SHg1MFBUZE93MVlyWXFadTZjdDg0NktCT1ZwcUhzNk1Ja0g2c1k2dHdXVmwxQzRKL3VpaQptNUZYR2g3alNkZmRSNmVnRUl6dlJVUnNaMVc3RjVGWVhGallKcXdTYktJL3h6c1lLeFRIbFlEbFdDVGoxNHZSCk82WUowQ1kvcFYwSkZJSkFBa1oycFdGTkxFSU9YQ3ZWT0tCSW1VUVIwM2p2c3Y4Z2srb2NKbjMwODJPRWFINE0Kc2p0MDBHN0RhYzlpeFVnbDZ0a2loQTNVVlI5YjBsc2VTN3ZJT1l5MnJyZ0N0YXJBYXJrVm1RZXJadlZzalJyVQpIRk13eGQvd1F4c052TThvam9IczNhNmJnSnc3andhVEFBZFBwcDI3OGpsMlNOWVFpQloyWFBjbWQ4eVk4R3NCCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcG9CR1hLMTF3Y0FteHdCbWtoYnkKelcwNExGdWkxdkhsSlpSUm45aVEyYWJqUkZ4TzBXclJndDBTZHd5c1l0REtzUnMwVGtCQXJlQURpaDFMbWlybAo4QVllUzNCQ0xVZEpBbHJCWGkySU4yRlpBa3RtbXVCWGJmL09IdU1Oc1ZCazdleXFCOEdaOFFTcDlhS285N2o3CkhqWlQ5bWJmL3FuRE0yUi9oRUZ6SWZ5cVdjUHZCUC9hNjlrMmQ4N0tFSXV2UnFGcXdpQ05hRmVnZ2ZXb1RvKzcKMWZJalhBRFFpWGROQTk4YklUNzdTWGhzTEZ2bUdjeGJCZitJRTFJYWNFT09oR1ZzSDBrV2N2WTI3STZJbTdiaAp0NDEwbWNWU2pqU1B2UTdLTXJRVStNSVdPZXFGTUJDUjJ6ajZEQXZTeVZ2MjVlSnd5WmVSdXNFN0RmdFpsYi9kCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2VoT29ZN3ZGU0N6L1phR2JxTWsKZmtKSFhSbW9xV0Vlc1doNkk2OG05cGlpRWZlT0pqRElJL3BuUHY0REJqZy9VYXE5QzBUUDZmM1cvOTBHYVlCTAo5QldwdU1JN09VbTZ3bDZIenRXZXdnNlAxNG5GajJQNWEyQSsvK0pJSDdsbkUrNGxoRnVVeTM0RldmSTV3V3ZGCmtCcEEzeFh3RmVoZ3BWVW9yR0l6d2l1OWZmTU5RUHg1Ujh3MGJNMUl5bXpJczYzYUpMT2o4ZGU0N3g4aEExM2sKQlVWMW5tRTIrUC8wbnVNcHB4ZXZqRk40Y1dRazhKZmFRSzdqdTA1RjhJd3k4OVgveUs0T1BoeTFuR3dyMUZyKwpRMndyK1RFVTY1cGwyS0RQY2w4TWpENTJUNGhTMjlYOWkzS0pNdXBMdXJTdVpudk8yQnlJaTRzVVhtY3V2VXVrClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBek8zaFZLaDZmOVlMQk9HZUtlRkMKVU9QM0ZvQ3dGOVAreGdBdGx1blQzQ3IrRW5GUVhVM2ZmRnUzdC9yWG1zaFA3dW4rS3p1a01KbUU2RXJob282RwpubTNybFhoaEJ5aVlUdy9aRTB5UnRwWndOazBxMkpOUHJtanFycmw3WXpPN2xnZFgrQUI3aWk3VUJvdk5KcmU5CjY3WjRjSUpaUCtQTi9xVTloSjdPTHZ3OEFPT3lDaGlyWHZzL3Z6a2h5c0R4ZTQ4UHQ1VUJNYzE1NzdjdUcxMFgKMUUwc0RPMElYbXY2bUZKS0Ivc3IzU1UwVi80aXUvMHFHWkxHRU52d3d0YnBlRVBra3JCL0NwVGY0WUpleWZXNApUNGVSZHZqdks0TGNPQWxBZSswdnJ2d0hDNkMxMTNWalJpcGprV3pIdU1oK3BjOG1tc1c5a0JkSDM1YUZDbG1zCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1o2ajRTZ2hUVDNreFFtajQzdEgKa0RPeVB6Ymd4TjF2WjAwTTFPQmhmamYrcnhXdXVtZWtLNzE4T00zNHE0NzQ3U3JOYTNIU0tBWm1ZK2ljVU84bwpzS08zaFFUSlIxTHNQVjJjelZESHQySjlMeGhJc2JKZUg2dHRtVzdaSVRzb2JFaTVBcktlSEZUazlyY0svbnJaCmpnUHZQK3NvYldZRFROcmVpeUNtYUhpaW9FVDNZNndacUhaU1hxNER3eEhIUlhQZGE1S2VLM3NUYjhVT3NaQXcKUmx6aFdvZGNBZ3JOckU4bG8wS1ZDSVVxeW5JZDd0SjBuNlVLOGdjL0Z1SVEwVWFNRzUrN1Iwa1VNSmw0S0xMWAo5dkpOcGlNc2V3clZoQ0lKRWtpdTNmZWNaQVBWOEpTMk85b0cweWxmU3BFSlJlQjNrTkloaHpWQlFJUzlQa0d6CkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzQ0ZEd2Y29PVU9zMmE0SjB1eEYKazRYWHVGSDZsZXYxUmpDcUdWeDRxL2p0Y3pzTDY2M2VaNFZsNnFZVW9xamhueVpnSTJySDIrTkl4RSs3NVg1RQppN0w2R1lmOHlDbDJnUkwxRi94UUF4cTQ1ZjBGMHFBOCtZdGdGMTJHM05YeWZ1K1gzeVpESm50U0ZjMmJQNUU0ClFWRlNaKzd3RGdoQ1JWeC9LQ21aQmdnakRnLzNmZkZrdWF6d1pqd0phNFk4UXRIa3RhZW1Va3RpdjAxRWlWWDcKRlFQNmQybDNiSVNjM2VhNStZYkI4TzA5c3JoVHZweGxadkg0UVpLY1NZemZ1SWZrZ09XRU1wTUJwWlRqeXpmcAp2MGo4Q3RNTlhKWjhELzVLT1VNQm5qWG1mWi9ZVkk0SnliS2FueGlRYy8zSDMxS2thWGxENWdJNm15RDdjWVNTClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3A5RTZFakZJUUU2L2pDYWFkV2EKL3A1TE5JaU5NTnZ5MTRDMVhRc3daRXhGWHgyTWNtelRleFVKVEU2Rk13ZmVhR1JLWDZtZTVjT3VVaUpsVjhDNApVQ3FvM2FUNXU2WVNjNzNFN0d1Z1NhZDI5Z3pTL1ZETDZmTG1DenR4aG85ajlpc3U1TEF2TkNXeFhxaUxMdG5OCldaTTR1REhiY29ySTBCZUtOa0FrREQ0b0pUL1FHbUwvWWVCRmg0MXJqc2p4cnFleXZPcFpwNzRyM0NCcDhqcWEKQnBrbkZVTTk4OWtoMExTU2Z2K3JEaTFJWTJBZmMrdkhUMXE3dVVQY1RWY1hmMEhrWVBHZko4MDdkVEVSa2xNaApuZHBFTm1UQTNtczRGYWllS1hLdVpySFJxUGR1QTdMM0NkVUJRL1hVbGxPckhCVXpjeU9wRGFIM1FsaDkzcUFCClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE5nWUpTT0Zic1pjWW4xQXhURmMKMXdhYW43TUZ6cW5McDBabkF6WWdiWkJnbHdyYUNaUEdORUJqMnV4Zk5FVGtmRWtGTGluU1ZDZUcwamRSbjFtUwpBMnJZUldKckRwSzI3bHVDWmhUZHRYUWxJNWYwbnhnSkNYQ2dnSTMxUWZFWU1iN0ZRdXYyNVNIdW9zM3poNXliCnNnUWdqUU1HK2NNckRWTGlpY2o2NTd6ZmZpendxTXJUMm9XcEJjYmw5Q2FLcUVPSzIwbE0vbnBhYVFSc1oyd3MKcGtEV0xCQjl5VU1VbUxGS1U3T1BSV3JqejBzMXJEeWZadkVLZXh4bVRaK01EWHVMQTdnQjhqTzFqTmQ5QkJnTQpnQTB4QkpYZjBBT2s2WW5jeVlXWEpGRDB6dVRPZXdPZ0VRZGRwK3dkbkdnbm02MWpTbTRzaUpKR0RoRlQwWHNzCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbTRqRVNLV3drMW5sMjM4eGRKRnIKRXYrSmlrVEpyVUErUDQzU0t5SG1wcWJ6UGJGaThnQkhpSDZKbGxDcWdOSEZ0M1UwWkM1UWZuQkRFdmhhQ2ZqOQpMQ2xPYVlPRnB0Q3ZVdjgzQlhkbUtQZzNzb01JclliRm9lbmRnN1BRNm9ocFRaa0dNSTVoclNXK21HMXRpQXhGCkRPd2kxQjNUSXBuTXdNNzZ0bzBQdHpUeklCSFU0ODZ6Y0NEeHVBRmNjb1hPZWlveTFsV1JnQlZvZE1KWldLSEoKa2pOS2VGMU8wOWkrZXJaK2M4NG1lYi9GYjZPaTBhTnFvL0VDblgwYndJclB2ZEVYRkNySm50T05uYTJnWnhtVgpmM3BscDB3NHJQRUZFSDBicFl0SkEreVFZWGF5TnlXSXZTeFpvdHhabjAwb0VrUllnNjFDL0RWRGd2MXZVa0VHCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWhOdFZyRjlFUEw5R2JVM01ON2EKUzY2cmx6czMvdnVkbzU5eGpoT0FKNi9QYlErNzAyemRua1VpbDlzZnlnOUEzdVh6RTBLeUtIRVkvRVMxRGswbgpSS2J6Q0VuNjBGV2ZON0NhRHNxUUdTbjVpZ1RicFNMZlFoT3FYaW80VFRjd0hIdFZ3TElmTDZXcU90MVY3QWlQCndlZFpZL0hVdDZOaDU3QjRLbWJUd2lGbDlYa3dJZXhWNG1WMjlrbllCVmJ1RGZqOW1rekdFL2V4L0RHMi9obU0KUHMzWWNka3laQWdzdWNoTy8wWUlPSEM1eWJGMHN5Z2tYclB1Zm1qUXIyaEFiWWV6d3MvUUhJMXpHMk9hVFJwZQpEWGpBbHJoYkZOdDg5dXhzQlEzaVFFMXhOSDlyb1lDZ3N6eGhIOHVzRzF0K3kzTjBPZCswV3BFZldwcTJiSXNkCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFZiMitwR2ZQRk8yaFBuZ0g5VzUKVlNrVEV3eER3T21DeDRnaFN3MmhNeThJUmpYWWJiV0p1VTk2OEhRbGF6a053Q1k2bEUrY2FINjJkMzliWWNsYwpWaU0vYTFtZ2VTdDduTW03dzhBODdGdCs5bVFlU0pDWThtc1JKTmNSZkhBbFpaN2UrNmJQMVhYVFlSQnUrRWZnCi9EZ2ZzNm54NDNtaE5TT3p4SGlDcVBhWk5lcnArWlBOb255U3Z3RlBVdnhoSWg4Q3JPcXkza00vYUNJODJGaTIKSnlYWHREL2R2eXQ4MlpsdzhiNFVsVG5hSzU2ZEVER0lqbk5VaWdSWjI3L01nc1lIY25xSUEzUDgvZTdhV090MApYMmZtaE9LcDlGRUJrL015VDIzcG0rZmM0dHpIWDhuVGZPMGFVZWlPbUN5YWJzNTVMZHg4Z2s4ZEptcmVZVUZQCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEdwQnNXR29Xa05UVzVoZnFabHUKQUVqdFdnNmJpWDF5UXhlZHJxeDdLaHljdVduQ2d3UERwMDdqRzVDdWVVS3B6S2krZVdrZ3lkbjBnWUlBS25BMAoveXBuZlZpa1lENHpRZHNHOHpHTGFXbXBjWGFvMDBGMFl3OWcxWmJuZkhka3RKbVBtQkZpNXBSYXhXQ3pvTjRRCjc4VERTZ2VuRGVqL0gvQm1HaUY2SjdueEJmVVE4eUtHK3pQVXhVcllSYmgxeDNqT0VJc1ZZWnEzeThpZGhFdm0KMGZYWGJDTE1rUGhZeksvYU9USXJVT0ZrcHU2L0dyYTJBSzdPb0RldmpJaC9KQXdSWjU3MmdFQ1ZONVk2YlgwRQpZK0IrMUlVb0xId25wcDhwcFV3dHd2Zy8wV2h3WVNjN1l6UFN1NzBXUGdrZE95cGhIa1piMCsyRG14VTlNaVJFCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUowOFdpOThRQnpKOUtEb1VRNHAKNUtEdFRHNHF1WjA1V0F6dVY0TCthR0tsY1M0aGFRcXJDYW5zdGZFZ093cEhNTThCU0xxUytRVjlVem5oc0RqMApqb1NKdWdqa0JnbVp5eVJvbjN6RE1wNDVqaDc1RVgxYnBuZG5KQmlNMXB0OTVOdjFBUVdyd3V5Si9CaXhoTGVBCnRPb3VxVVUweVJzdVEzd0lHMTd5WUMyMXlTcnRyck1EUGpSTDBxbDRQcG1KdHl0Q000bUxocmF5VWpBYkZTdDUKdHFrNkh5aUd2WmRLRVRZaHREMzNmaTF3YkxMbzV4dHdwenIrM3dvUUJ5WXU2ZnhDNWs3Mmc3UVVzUUxCakwxRQpuTEhkaDRweDN4aGM0L0FOek9pUUYrRk5MZGs4dlZLT3kxZDJhQ215OVFCR3ZPam1GQWY4M0xNR3NoT2xwZkN2CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMms2MENsOFNFQng1OVIxb2pBYWkKbTBpZkpLTVhQT1ZlMjExYm9BeElpcjRLdFUwdE5mYUd0dEVoQUJhTUVkajNRSVVoMEFvL3RPQmRTOS9QYVI4dAovZmNoRVNiM2dXcWlaekFaT1lIU2gxT29lYXMxRllqaHNYenR0NWtBMnUzRFFxazRoRGYxbkxSM0hwMzV5SGkrCnlQTmRYUDlhK3h5WWt0MWY3WUFQZndMb1IwbUVsWW40NWNPMFEzSUF1NHFqd1FXbkdGcmRCZVppdFUvTDdBaDMKSHZlMmRacmp5ZnJISjVJa1JSUVlITFhWS2lhN2VScTFIYzA4cW5IR3BpS1ZxUE9lOTEydmlOTmtrYjZCRHpVRQpBTEM0ZEY3cFBLQUtEM2dySEVBZld3R3FtbVlDdmU3Nmg3VWhTeE40UGwveFFYdzZvdWpOK21mbUM5R3RjSHhYCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFpBMWxHRFBTM1N6ZWhzSkZqRTIKcEhCdEpRS0JvUk54WEQ3R1J4eVk3a1VIeFVaVmtsNURWaWtrNi9mYnZ3clVjcE8zMzRDM3RqOGNJb1pMYXBMNApEaEFYbTVzL2V2RnJKbDRQSHdFaExxK1UwUDFyUi8wNjBOb0NjU1p2c0w4U3hvS1BmWC9HWDNKZjBmamoyTXRGCjN1VHdiSzlRUW9RaDV5aDlDU3M1amUvVDFWditMamhNYysrY2F6bWU2VEJ5dytidmhHR2RuNTdFak01K1dsLzgKQXNEbE13MUJOdjV5Q3FtdXhUdzJLREhzYVZHR1owb0VGSlRLN1NwbnhWZm1rT0xUeHBZbEpYN2ZhMkJkWFdQdAp5UWhDdW9ublVnbjRQaWx6ZGNrbEp1enV6dlc3VFhGTVh4bzJRdG1pTjBpbFlRMTE4RWFqV0ovL3ZaNEZlM0tGClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDlNWlVGc055MXZVRU1rd1RoMkcKeFd6SHBpTURQNGVTaGpGZWdMOHB3cktURTZzUmlLQ3hLclU0UGdCYStvZ0oya1VpQWthWUVOcnNnTWhab3Z5LwpCSDRmUE1oUmVDWFNJSktJT3p1bTNVUHVraDNTd2lYYmx0QW1SVWxEa05BQWRzYkhOQzFTZHkyanVjelBzVEkxCjFmRVZxSjMxbWlEMjlHZnFsTGRlTFFGbjVYVUZGeXBSKzRmY09NWjVSNHpqNFZoY2hkYW1OQU0zR3dYWG1kaGUKREZQWlZMcm5vRmRuWS80Q243ZG4xOGVWM0o5K3dtSmVVU0dScDZ3QkJKL3FBbkpTSFhSVzM3MWNnQ1gySHpqUQpBZkl5a0FNZkg3SjU1WjJTMUNSbmgxN0ZDdlpWOVJuN0FFNG1LZHdrUGZtYkdpN2QwNTNOWkdFakdnSXFBNmhxClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzRCUmdBSExMQ1pRKzIrTnNkMTkKNGl4QTBSZENTTGdIeHErNDNlcnZOZ1FhTzhxcXBoNFk2SFovQVJkb0NtdWVDZDQ4cGFXcHNZZWpBWjkvaklZTAprWlMyNVFnRDhDdlNaYnUwVjB2UWY4dVUrY2d4TkoyQlFDa3plaTJ1ZkQ1VkN6K0I0L0d5eUh1QVF5cnlZRjJ2CnFZMlp3djhKbHBkdFZJd0psMDZ1ekRMYzJZSjJQOU80SDdEcCtVVzBmT2FUQkM4cXNNWDV0aUprVVk4MFh4UGEKUWZEcFBuSmlvR1ovRFBzWG9xeG1HMjRaem8zQ3NlU2Q5VGk0NExHOW1weElsWlNNOEpNYXRwM0JDL215cldPdApEQzlIbkduL1FyQTN5cStVRVh4WUYvVWFZUlU0NTk4WWh4L1BodXRyQWlOTldjL1ZxU3ZCYXlNeXZGb2pCTzQ1CndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDVJV3ZmNEZacENBdzkzMUduVHEKdmNTQkExOExQeVV6emV4UEFiR1o2dWhzQTd6TVVady9zOXlPeU1kdzlNSDlhS2tIVlFVMFJMalhJNTJWRXRUeQovVHk3RS82SWhJdCtBOEw3ZXN2SDg3ajhjTi9HeGI1ME5TT1IvRTcyQ2Q0Y3RwUFR5STM2S2hZQ0ljZkdFdGx0Cmk0QkhqYnBiNE1Hbk5rdGJBVVFORitQbStuZnJvR0t2WDNaR0FQSmQ2OUo1MDdjSkhhNXkza2FDZXNmMTU3RkgKS2Q2aW02Y2JaVkl6Qlh2YWNsbEpGZmVKd3BSWTQ1bHNOeXAvajlWVm1yTnJWVjFLUW16SHBIZGV1clNneDNsdApPcURIQ1gxRWpKWnJpOUJkeTBqOEtTcW0xREYvdUNYQUZsRGxJcnE2TG9BNHA2OEtlTXVMV1QxL0NDQVRIMFVGCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFh6WThDZE11eDNVYmpzZkxEUFMKRVNDUk9Eb0Q5bUtlV2ZycVhpTGpVRDlmSTBZWVhxQ0hOTFYrdkM3N1VqakswYjM1NDNtcGlFUHVwRDh5djhqSApBbGhOdDJIRm9mUTN4K0t6anFRWlVsVlRlR3RxanZxdS9zMlFuNzJSY0ZPK3JlMHJqUmZpZzhUdnpHVG93Zzk3CkZjV0svSWNTdDAvUkhyaW1vR0hwOXdvVnY1QTRXcnpUMENGa2g0K0hINjhqd1NaOXNYWkJ3TlBKc1BkOStpMlkKNkg2ODF4NVpLSlpMY0dWZjI4akl5TEFPem5HNGI5aDUvK2dlUU0xcDVISnFFTnZDaUZTeVRFOVlPUGhhUjgyagp2YUZXTVp1OE5hVnZVeE5JeGxOaUtLZ3cwNVBOcGU3VURmTkU4elZ2eDFhNUgxYkdxL2ZlYzBQZ3NIUFZvZlN6CjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFRWR2Q3R1Blcm5GVUViSFZKR1kKOFMzQi9USFlMZ0h0c2JPUWFqZW1rRmpJV2loRkx0bGlTTDlzTWh0ZTVlTy9EREsxaW51QnZxYUtTdUJ1MkcxSQpndXNtWlE1dkNGUnB2bnlJS3hrT2R4NDFDQ0d4MDhodUZydE56WTUrMXBHRElLa0hhVUJ5dE52MGpYSk13TFZsCmptK0VqN2FzNVhDazYzd2FRWndPcEVwNmU1amtuYUNOZVFrNDVNRUMxUWdSbHU5dGk2ekc0ZnZJRFhjQUNqUUYKYnB1dUpLVVVIUkVhMWtyVE1acVg1S2JKRzlLRXBVVEsvSWM2OU9ISEsvZmtnaGdtR0ZxcDZTUnRlby9aTFBLbgo4M3N6OWZVcENpR0JNN0JJTDE2UVlJcGg3WHhDTGd5OW1OZGlyMmRrOGdScFNEM0wwNDY4Rml2VnJSTkE2WUJ3CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDBIQWhYV3h2TWRPdFhxQUN3S0QKYVJGeDJTWXlGa093Qk1TK1R5NW9aVGNUaVlWTWtQU2F1akZDeS9Ub2ZqNCtVWUQyUmU5aDI1ZHNBWnZhK3pnOAp1TmxsMWxmRVlpYWFjaTJlYW5UTEJ4STZJL2FRS09ud2xIM1JxZmhJYVluZ2lNM04xenprcnpEWDc1RUV1NUdoCjlyb1hVU1JSVk1TMXd5dDdOZUFJWlh4eDJaaWF3QmFSTnVqVStWenY1azQ3TjJNVFZkdW55SWJXaHNENVIzUWwKWU8zSFowTFNMZ25peTB0RTBqdlR0SVZLV2hwU1BFbTYwbjBpclJkaEdHWndpTm1mVEw4aTNXdjN3bUozbTQvNAo3eTdQMjZ6bXVSbGtYTFBzVFN2aXE3SmhsdTZqOXBOcVJKaDRqbXN5S1lEcGNVZ2U1QTQrLzF3Y1RBY1NxSEZNCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkdqajhRTnFsdWtteXFxa1l4cHUKMEIzZnJxZ1ptRW5HZGRZL0dTMWs5WW1MRGZaUVJyRjh3Y3FZMTJqUnk4djF2eE5LV0VnVHlrYk9nQzVYR0kzNgp0c0hnS3RObTdkWkRtVjZvZE8za2xOSTUwdkFmSWZpRUVCZlZ0SlZkVldjMklJcyt5NE9uQVVFbzZ4aTBSNnovCkpTSEhaVnI2bGg5SitKN040T0gzdUdRRHdaeVlJSXpza2p2Qk1wQU5uaW9mRTl1UkJnVjEyTHRna2MrRTgwUksKa2gwVVU5QWNZdjNyY0FsQXZmR2VzMEFnS09ISzN2MVhZeEFjT3VVWFJrNXJUc1JEbExNbVlGSmFjb0dudWpQeQpFN2RrdHZKbFl5YTY0MTBUNnJoTzB0NlU4bU5tVi9PQUhrSUtjcGRabFBEV0xwaC9USmZweWJkWUNmKzhZcThFCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc080K3hCWTU3WlF1ZlY1ZVYvcG4KZHp3WlIrazdFeEkwWitIZjRQc1doOVp3cnlUTVl0bFcwY3BONlVMdlVMWkJsVy9aVitrdVczejAxNnlQbDRpKwp6OHJHR0c1UGVNUkN5REtaaEREYnl5ZERCNzJ0UlptNW9hVlVqTENzSkR4dE1PTWJYNDhnYnQ3WnhraGU0WFhpCk9VK2hSN3ovR1N6azdIQlZ3QkpZZDhlZWszTVNrbGl3N1N5bE5HZHgxTDZsTWphc0trSkFJUnplZ3BkRGNEamMKUUsvbk1XNW52SktGaUZyZmQrZVpuay90a0V3dUY5cjVKcm04SHJHUVpxWDkwTmw5SXAzWlIrb2NmRGFubUpTcApveFFiYlAwdWdMNjA2VzNBMkc5ZUgwazVHOVlhYld6aTJPNVI4c0FwZUdPSEpTSXZEb2o3QTZ1Y2duL1BQWEZBCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG9pUE1seUJmelBSL1ZmYk9OQkEKOHBLRzdLQXVpSG9zS1JQTFRBSUU2dU1mTzZkbnZCZkZId0YwUGUxNlRwazdId25GWENQYURZN2ZZZUtHS2JvNApQWjFNNEJ6a3dvV09MZTNYdUJYZkYxSGRvS3JMSW5CT0RoYWEraERISGtkMzBvc3FYdzFUVnhHWExrYkovNy9vClVkbjRuTkFsVzZWU1h6Y09NNzJlSGMxNmFkVzV2MjY4L3pEeDFtMkJYZUl5OGdlWkVvams2NFl0WlNOcWx0dWkKWURBck4vb1ZSV1FtOXJvRWdsVDF2K0xtc3Z5b1g4bXJYZ0hzQWQ3dW5RMFJnZ05WYnZUYk1oY1Z1SXM4bkpEVQpSSVJ5MnVYSGF1WGp5d2FKU2VVeTBDZXdrRU1HOFh4ZHlHMUxGZHFVMHg5L1pUM1R4TUZubVU4QUtDeXp3UjJQCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUoxR0tmNjY3MER4NUtnS1Q3bnAKeWI5dzhFdDgyR2pSdlIyWlBMeSszVHZrQTFkZ2hRay9ITVlOeUUwUmlpZTg0ck5VS2MzakdRWTk2ZjViUkV0TAp3YllpRU1HSTVJYkpHRlRDeUVhOU96V0JWbklrQXhXaGNtWHhGMDJnc3hMaGplaDV0d2o4R2JlYzNhZ3MyRnJMClRwT0ZBSTZRcC84TUJqSkZDZmExb080Y1FnZEJyb016VGJNV0thL1VIYjJ5dzNKcnVtUjdiVi9FUmFENUpGTTcKeU1uUWgzZVA3cVdnWml1TVJXUHB2K0dVdmZNR3pBV05XZmFTVmIrZlRGeTR0QzlSUkY4cDRRSC91N0ZJTE5iTgprQ0Riczh3T01xejlOZEU1eGFMVFBkVWZwVVVDUG4vQk5teVpRUE1tTnpUREVRckdid2U0QlNISnN3bkxLbDRDClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME84eG90bWxkeWdmNWlpdUd0NG4KYjdlS2VEOVQwdEkrK2t2Rjd4QUdKZkVqT3dyVSszUkwzOEszZzRTc0JaNDVBVTY2Vzg4RmdOQUV5L2lQRW9uaApmQnFQQnBFSlVFQk1wMUdqeDdtK1picEtNemdJdlk4NFNhTTk5L0E0TzE2VnFWdFhKZm90bjBaOXZVanJITUdFCkdDbzd3eEtpRVppMjlBNXN5ODBPR2lZMnI3bkpOUVFIMVpTWm9MSUx1WFBJR1pyMUZTWHN2TFlKOSszNDVXbmIKeTF1ZndCSEpHL2Vyd2ZiOG55M1ZwZTBzN2RtMU41a0FvLzZMTnRmazY4d3ZaVkZraysrNm9tdE55OVhxRE1oNwpNblRaZnkxOEVMbFhUSi81MHhHQXhVNUJpS3p5WFR4SkRqcDNFMzBFcFV2YWtPMHVJeU54NTNVUmV2RnFodmM0ClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcldScnZHb0lZL1Q3K0ZxSmlNWG0KdDE0NWtEQnFZNmVZT0pJUWlwUVRmelpta0ZoWDNBUk9zNlFkbTdqdFVyZDVRdG1OQkVFZ1FlYi8vMWowQ2ZuKwpGZDc2cHRLanoraXQxRGVnK0N1bndwT0hkR21raytCbjBlNFR5c0cyazBlbThGMUt4ekkvUlIrSGhTSmFhMm1XCk5ib3Y5bWJtMDdsSTE3SC9YYnpRSlR3M1VjNE9LNkdpMnVUVzlPOFpQUGxhYmlPNTJta1JNeHlSalAzOWVoSmcKcFdTRUNjb1ZXbFhLVWhxMmVHeWJUZEFEWUNzS0MvdlV5T0VVU3pMYklpUEgxb21qMC9XODdsKzNuZ29SQXcrNApENS9lSDg2TmZnd2YvZ2JPaFdkUzNjS2MzQm91OVo2QkM2UG9nTHZ5d1FrZDVIN1JTSVFBOTNQSStmeitMd0Q2CkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0ZYTzNWeHVtb1BKYzEwSzA3Sk0KU2VCOEZ4TEZoQjQ4blJaYitHZUZ1amlxK0JCdGsyL1VRc0VUbWE1YzN2VGN0aGlGNjVXa2ROQUNuVmpHQitRQwpMMFVtMFkvRDVPRGM4VUU2S29LUWdXZWQrUDEzZkIzbmI3Nk1JVkk2RzRUTUQ3YzlWeDFNM0lseTAxZ25BNHBFCktRTTBmYzBWNXd5WTVhV1U1NDM2aUVMbHJ0SWpjL1o5aWVIeHZ5UzNTeERwVzBZbEMwMzdaN0lXaWU0Q01SUzcKYXk3Q2QzMGdRLzBLK2JyUmE3UEluOUxNRVVvM0g2L3J6MFRTNkxpMWttL3Z4SWd0V2s0dTYwT2dkNXZrd2xrSAoxVkt4bThDUXB1VkFicWQ3eXg3SkRHU1JVMHhHNk56ZkpDRjhBWWl2dFdXbjFCR0hVK0JWYUx6L0lmM0piOEJrClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEJNUGZ3cGVuNnV6b05zdmVpUHcKYjRCUGl2WExIUjZFYTQrb3h2aVJKK1B3L2RNZWdiZVZCS1pEMmNGSW5YY1FmUEs5TzNrK1RmQSs1bXdacVQ5cgpjU2NBRmpEajhldEo5Y2tVL1NlM2dzQ0h0bHpyajY5eS9BMUdaVFZzVElIdEQyRzd2bTZaTFdSTldjS0RGN09NClBndjlwc3hhNzkzNnl0L0h1Q1dZS2VhbWtLQXlpQW9PODNPWGpqbVZpcytpNlhnb0c5TnN2akQwWXU2UEQxZTcKazJOSXdlSmJ0SWJMbjJKUlBZRlZURzhQZVd0bmdNZW9nT3A4K2N6bThqVjJHYmRPelMxS3RVRVR0OFNva0VmeQpQVHRQbTJCTEF0UEQxU2dPOHZZOGdqNkxWd1VVN1A3QTMvWkhlYUJtTHJtTTdvN0QvR1VOWm14RDRqM05Eb1lSClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmtvZW1XT2t1YnNJS3dwdDA0MTUKdE1wSG1ZVFlVK2FQNExrZUgxNWkySUNCSXVPY3l2SUdFT3RZQmtsbFo4RHR0WGlEb2wyVEFsS29VSDFITWEyWgp1eUtIR2tBWWVHTk4xTmJvS3pQMjlvWWExNXpWZVNiQmIzREhYWjhLcTlVMC9mQjhZeEN6aU5JQ0tDVTVpVGpECnR3S3p3ZUplZzlQNVVvREVuakdkeDdTdDRDWkdTY252c2lSM2pkdjJuSjcrNjN0d0xnT05aUHd6M3dDaHU1K0QKNExqMDV0cyt6N2J4YWpFYitvc3lTNmgzOGE4MzkzMzVudWF2aVEzWDNvR2F6bW94Y2FISWlsU211eUVXVytIdQptREFZeXMrSmJDMzgxdi85WTZrYkNMcjRvNENuUk9pY2NrcDJCUGsvcXNsV01PMXFHYXRBWnFSTTlTWFpzYWpqClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGFUVEY2UFRFTHVKV2RJVVhQQTcKRHNwdjNFNDI2MlNLdVQ2VHVQY1lrZ2ZXdkpCOTVGYlZsM2ZGOURYZHNSRWoycmE3K2pJRWsvVkx0bTdyd3VFSwpxeGxwQ1R2YXNOWkx4cStuNWNxaUR0VlZTZjEwOGQ2VCtleTdGamZPSzRwUWRpTjRlUFE1WlhZVzRvdkpCYVZjCklSS3hjVzNnQzhZZFV2NE44WkVqOVFrUzExMmxwbnUyODFlSDhTaGJtV3dNdmtncXJOcngwSWZSMkRMYVAvZnUKY0l1Qkl0ckc2K2Q1SlJuVFlwclFZZ0ZnQWFHNjhma0MrTW9xWElMNUpLOW5ETXBhYjVtUzdET3JUbU9kcnNDQwpuY0JqbTFqeGFjY1l4OFNuNEthR3YxMElPTXJQb0FycXNLWVgvN0UzODJQYVVUUWJjOXUzeHpvRWhvdlZOZ3d6CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEVaTUE2RTR5UEpXdkZDREhWak4KQ1dkTkt5TjN1WGF6SGNHQWxsZUpBVjdWQjJJL21IYnF5NkRpZm9CZXgrQnRWVkhTS1I3QTdXQ0xWUTJhM0FYegpXU3dQL2syKy9uaTFQSGR6K3dPcmZjaW82VDVDZktWR3VFN3ljSGtyaFg0UXFxcVVONzYwVGhnWlc5K2w0aHI5CkJxMGtvbmlpTkFiM1JiM2VVYURsVnV4ZWt3ZGNDTkwxMHNnejNiUkRsUFMrTXlPeHVVQjV1TFJkbS9YRVd0SS8KaENYM2l6TXkzRVgrVUw4dGJENCsrOGtDR3dyVXdxQUZvUXV1ajRKR3ZBMUlmQ09SaG5TNjhDeGRhNlJxNDIxQwplVlQvZzdIQjREWmh4MHRXM3Y1N0VudWxMSUNjUDIvWDJlMDlWTlpySVpxNkt3OFlzN0V5ZUZQUWJUZmdZYXZuClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFBhWE9mVTFZZS9SOXJuMG5ob0kKZnlyaXYwSTdWc0tSZzRQMmlEc0lnMHJNbG5LbVZDOVRac3hWVmVWL0trVDdJTzM3V1RtUWY5RTdXbHdsbG9ydgp6QUQ2SjBFMDRTYUFSRnRvajA5U3QzaXNmK3VMRm41NGcyKzBQUURUQXlpTHNyK1htU0hST2xBNE1XN2VLREtLCmUvZXFTQmY2elR1aCtaLzJBa0xWcEo3RjI4VHdZRzVrYUFkOWZteHh1bXN0cU9VMWl0dmRFVy90TkRqWnVpMUgKdVhXZ2Nmdkk2aHJhNmhWaEV5MFFycFRSNEpVbGJSVDJtK1dLck1MVHYvRWFpRHRsREdtUzQyaGJobk1zVVpPVQptZFZwdzlxM0l6KzR0aktTS2xxVW42Y1h5MWZZZDZMWUdENHNEcUp0UkUxYUkxRlYvSkRnK2lSRzJtOFNLb3JZCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2ozbXlBNDJuUHovZExsVm1ka1EKSG41dVhvYmFYUUtodXFMQXp2RjVIbzExTkNGbmlQMk1QUWhjZmxhS0lVQk51RGZGQ2V4c2hLT3pSc1k4NHV5YQpjUEhQWWxtNW9PcXdTS09IeHJZRTlTT2t5THEzSzNmc1hEbjA3N0dOMHlMWHZXby8yS3VsaWIyUEEvN2o0TldOClBoMXRqR0dpVjRqUERnbHJwR0Q4QlJ3SVlESmkxWERudGlVQXo5eHAreTBKc2pUYkNNcHVrUnZKcEtjbmhmUEEKaENHbEN1Sms2TWZPTGlQUGpBbnRTOWg5ci9WMWZyWk9QcVZ4c0VlZE43eU91QlpHUlA0ampaVURjMzFpOHVBbQpKWFVtTVlZU0NTUlMyMHk1Y1F5WkppQjIvTjJNblI4THQrQTd5SE5VRHVOZXhOeTh2aDlvNUMvcmh1UmtXUFZGCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVp2MHJnZGJTN0hDTlRBVndVQkgKR0c4QjhodUVaNlE2TWplMEdweEloeFJiRHlBcEJmM3lYQndQVGtZUVNZdHdSZXNjYllUNFVLNWlkM2JrTjFMRwpNRENVKzVQclhXUWI4SVp0UDJGbUtBVVBsNUNIdEhMekRXRXQrWk0wK3pBOUpNT2lNQkUzUmF5eFNxM2VSeFFjCnlYOVUyU253Y3ZoM0c1VnVWa0t0WUlTaVEreEZ2WFdwWlF1MEs3czdzV0xFeVlQR1RiNVgxNU9QSG1yTnRkK2kKdVNjeXNwaFhTay9JTkdQa3g1eHRlZGpabFRDeVV0aVI1anZMQ3BVY2hXekZhWjdOUmttQm9nMC82RURHZ2Y5TwpIa3pwMGhWeEU5WXhlaW90VUZHUC9scXZ4OTlmbW1vOCswSHNvN040dzQ1cXlITXNlRHc5dVpmeXQzTVc1UjJnCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBck5ldGV6Zmx0dE9UYk8rMzBocXQKUFRsSCtFUDc5Z2kxcURycW5ya2ZBVUhkQVgyQzJvdmhDcDNjZVJsYjVsbGE3Vm00ei8vY2dWNlBocEJMNjdXVwpVYlExS0VOZmU1THBHL1FOZkNWc1kxM2VPcnZpTEo4UytTK2JHaFBXL2lxL3Y0ZE9MNFRoc3FmNmloTjVDUGxNCitjSVFMNGMrWHdFaW9COVFRRkY0eVh2djEyTGh3Q1o3dG9FRUFLTUV1WUxwMEt2OGJia0FmS1ZCblpBUm5Pd1EKUDUwY2JYM2p5YTgzbDhpd3hDeWp3Qi9pU2R4SVJXSEhBVE5KOHdneWhaQXVSb1pDZmhvcWo2Uk04c2ZtVFREVwpUeUlUemRrU0YrdldWYnpST0tmVXJNbkZjWmVQY2wyMDdVck5HOXBhMlVwTjBRT1NmeW9QLytlOXRLU1RXSlRtCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE1qbC84ckk1Zk1JRWhMb2dZaUwKMURkeGRGN3J1NGVMRDhLVzVrMHpVcDhraWM5L3BKTzZESTNUS2NjaTRuK0NYakdETEU4VWd5cDVjYUhmWCtudgpEQ1l5aWx0bkN2RHVvYkh4N3lWY0MydEl1YTZBeXVyb3VVUjhUTllabUVqL3NwTmJHZjhnVm50Z1ZULzRwL1NaCitHVURtRytMK1pqZXZOQkhCajJFODFJSTljdXdvUWx0NW1paGlldVppeFU0dmduL2YyUTAxWHdWaVFlQ1FYcmMKMDhmV1BpZWZNY0VkRHBVZk16OHcySktCTUNKQVNvRzRiK2kwN1pPWndNYm1WNXd3c2ZJRERoWTJzNTIxYkVvdgoycGovejFHeGJKbTZHUnArL2phMXpkd1VpOHNTNk5kQ2xyR2lINEtjNXdEbnpzcHc2QkVYNmZlczNsSVV2Vk9OCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczF6NjhyM21mbUFqekxTRlBISDMKS1dHTEk3eUdNc1ZPZng2emZTb0JjRUFCZ2ZFdHRtajRRdzhyTVRRNVB4bUg5R1VCc0VtbHB6Q2l2aWZtTjdlcgpXeFhPWG42TjdONGdXalhwT1ZPNTAzMW5UU1c1MUVaZGQ4WmVTTXdHMGhLYUxjNExIYVBnLys5SE1hWEMzTFE4Cmg0NEdURHduV29PVk5BcHNIR1UvNHJXRi9CL0lldUxXOGs1S3VpK0Ztc0lvT1hIZXI5TEduK05DOWUzTmxxeUgKRERCN3k4UlZrVGlRbk1jK0hoTE9yUFF3NzBVN1JGK1VSa3VqZ2lXdWY1aUU1Y2pWZGhIck1MTWFqTDBDUmtIVgpvTkVMcGJpakZrdDl4VG1Bd1o0dGVwYWNoaEVENXVZa0x0bEhFT3BkekZrUklpK3cyN0trTDRwb1VydnE3TGdICm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzhZVko2N2xoZnh2Wjl5dGtySk0KRGcvUW1XdE5PaER5STd6VlB5SmpQLzYvQlJPUmRuT09VOUtNakNSREhvd3ZLTDJKZXlHb3VVL2hzSERHR09ldwpNWTRCSnRLUlltUXY5ODNCenhMZ1NlM3BiNmdSaVgxdm1uZnJGR3JoR2RlMXIyUjBRNCtjR240OTBTWHUrNi9NCmVsUnhrOGdmalZuZnNTV0NZK2ZPWHAwR0kzSlptTVYxSFRhMXgzUU9iOWdwVDhJR2JGZTNkODFMVGdkQlEyemEKeVE3dk9HSEExSEZmVmNWR2ptRnVxUnZjTnpOMTE0QkdsY3BtY1dZNmpJdWV3T1YzUmJEUDNQdkhMSUNTWGVKRgpVeTlUeklUaEFrejV0dkE0dGdabm1DUkRjaDhnWVRvZnl6RTRlNVc4a1d5N0tGa21CeVB2UmZheWRra1FReGQyClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0ZXWWhEZ1BMUmxtZGo0YnFualcKZzFoZFdkaGhXUGhadS9YakxuYzYwQjFJYkJRZFZVNFJwMnJjUURLeDdHNFJzQUNnVU5QbEorUklmcGpmcDlhMQpRTSs3Q2NYZUtYY1EybW9raWtRaFZ0T1pNZGtCeVdydkFwK2tyUHgzRk5kWWlUamJqVzg0T0FMWVg1dDVqVlY0CndMdENodjZUTjNVTjZ3eWFvVDNudGl5QUd6VmxuamorRkZJUGNacEVVQkVyVHF5aHp4cCtIcjN2c0tyTER0S2gKOVVkYWtGQXE1S283RS8vc01yUWdWWnhFZXgxTzJDN2UvMWhEZU0xTDFKZitGcTNlZHFBeDh6U1dnTlJNREl6NgpVbURMUXdoREhpcXY4VmpsdS9CVGMvemJyUzZaVThwSHUxOUtMWDl4VVVXUElTOGs0b2VwRDhsZTZvRXhNYTBuClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMytHa2NpZFowbmR1cFo2ZU5tWnAKUHZacVpPK2JhZ3hDTndZZFBOZUpLT1J1ZS9SUWdGWHQ1QzVCaksrdUNtK0JaSEk0VVRUWHpvbjd4SFYwenJoegpiRkp6bllMSkNoM0lXZ1crN0ZqVWtLTjBDYnQ1RmZhWWpQaCtJYi9Gd2RJVEVWZ1FVYTZTU1hzSmJiZnpmbkhDCk1wKzlkaWtUSUw2ZmdZS3E5cFM0aU5FRlZuWHFiL0ZLOEFHTEJPSVFxQzFGNFBWN3VTeUxESE5nT3dESm93OU0KOG9oYXRQd2plK2Z3U0dXTHFYUVRaa3FGYVJWWVpJeU10aFd3OUVhOGJWSVdHbnp4WTZTUWNTejRiOUtta051RgpDcUFXUjFoalNFcEoxdy9ncXVQU0xsZkhKUkQybFhVRENmSEQ3anRKeVczNU84aUMxK3hod3dMWmMxYjk5UDU3Ckp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlVvbmRSVUxGK2JaelVmV09Vam0KRHVjMDlwT0FhRW80Q1ZlZXR0Nkx5Y3daaGQyMWJWaHJVdGpiNGZQdjZvOUxUTUFSN2lvTkhUWDlvb3RLTng2eQpjWTdxRUNwd2RzOUkvMUJYNkJLMTdxa1FUc05OSkVnS3BBV0pDbTJYWmlDWTV3WjB3UkYwMG5HMG9HeUhnc1BtCjNNcmR1NU4wbnU3WGVNQmNsOUpBS2U3UkxPMkNsL3l0MllxOExzZkhyNldaSUlJQy8xZmZlc3BydjE2VklwdHMKekdSV2FEd21ndUhvemtKSGNGWXVpWnRVODNWMnpwbnlZcytLQkxVT3JRRDRNalRlZ3pSbjNLRTNsQVlUUGhYbQpFRDRwZFJUeEtQSDFHYldKKytENFZCL2FlZWhUR1pKY0x2Z0k1Z0diYlZ0OHdkL2ZHNitNT1lIWDNvdVZYNjByCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEM1b2U1aGp1QWlBRDNNQWhkcm4KUklaSmpNSFBFYW5rK3VXSzdUVHY4YkpmT3NsQ1dIU1JBdFlsRzM1K2c1bFBCOFFYZ1R2TFRnTkpOYTBzTnNwUgpPWU5NWnhWSWNsY1k4djZzOVRBbDBBL3FtUCtHdklFVjhKVHQ2RmczWEk1MVIwcWM0YU80Y1V1Ryt4c0NHVjVRCkpHWVFxK1UyZDVuelhUM2YzcjhwbUJnbTRScDk2dFNRZHJjNU5STU1jU2lLZGFjNXFUTGRvYTVPWWx1WENWRk8KQ2cvZHhBdVBHZUJJTVZQcDF5Nk54ZElnTjFKQVJVYjFJOVEwTk5PRVY3RWVlMllXNFAzbUlQL1YxMXVxODltTQpDeVF1SUh5amNPUWN2VWV0NGZnOXM3TXVIbWZkendtKy9xaXpFWUFmWm5RYmNLaWZRa1l0SndiZU5xQXN3S3lMCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMElhMlJzM1JZaDFsZWlRQllLb28KR05XUStsTlMzWk1sK0lLQTVFdzdteGlZanhSOVRYbTF0WXhROG1jWHFZR2ZGMVR3VyswVGhtVmF2MFM4WHpoTApYU1FHczZuVGxoQkxKdTJOUXUwZ01RRitHTTdRWjM0NlQ4MTNKUXU4SVVOODFMVDhFSzJIYnNNcnlFSkRzWkhnCmtHRFczKzhadTExL0J2aEkwUWd0TUEvUFpQMFJ6YTArdWxGQ1V0N0lpVXVLb2RUeVA1UjA5R2xHRCtBc0FLdW4KNzdCbWRyeE1JRmFucDdhMDRZYVJkWkpEYW9SWXlQS21uTElTS0F4VENubW1MWmMyVGlhSUxxRUxrbHhXcjJiQQo2Z29zTUZxajdjUzE5SkM1OTh6bFArRGIyWjZuZWhjY2FiRXBXcld1ZlV5NlMxbFlnQU9pYWJhWnJRWmVnNkVZCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWhnc3JvakhFZnFvRUhDYXc3MkcKZ0VZVEVEZmVGdmlCa2s3UGRuTjQ2MXYzMUdvVjcwUXhCQzdXU3VUSW9SUTBKSUNzKzBkSkVDWWFYR09ackRSegoxc0NxUG9mMTNTWnI1L1VMOFFjTW9PTkRRdG5qc1hzenA3ODcvMWRZVGI0YUkvZmpqTjlOcmc1VDVPQmNIRVBxCksxcU9pUnNvWllTMjBHSjIvZnBpdGlLdFRicFR4VXJUMHdqMUQySTNmU0YwRityUkpURk1SWFRnQnJFbHJ3OVIKRytOWGN5Y0xtRzM0THBldHNXSzN0Z3hyYVR1K01URmpzQzBZWjV3Ylhoa3FmbGNYYzhwQTRON3ZEVnJwdm9tSgpjRmwvbktDSDJGWFhyNHVaemlSTFpadUlVRVAzTDBscU5FZHM5eUx2bnpnNzJFemRvK2VzbzRzcjl5WTJXdUM1CmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUIvUmdzdmNUdEc5SHEwMWFERnIKYURsRXhxWWVKN2hIdmVaZHRtSjdXYzlkU1dzNkNINHVqUVA4MysvNzRNNm5rMFpRU2dMbGF2dW05S2JKbHJnUQp0bDY5c2l0d09rVmR3OGdyNU12ZmlBa3k5TUR4c3NPYUE0L1plem9RUEVsc3JKZGhkT2pNNEdodjZjdW1YT2VzCkZRU0xVMnBWaUtCWVpIc0dSdzRXWWxMbnVET2RyUGprbG8rMHQ2WFlabE9VK2l6a1p2OXNDK2JGZjdNaTZTL1AKdG5JSWFEZ2F5THRpQ0k3cDdzZmRjV3gzZGg3cjFZbXFvUzgvdU5lc3lXRVdSa3VvdTkrY0Q1alRxcnkwd21WSQpJb013ckZidVdmTDNlTUZIaE5WT0Z3Wjc3WkxTaDh4M0tFMTFGZ3RzVzRKYVNWM2NYUklVeWM3am5JSXFqMVU3Ckt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnRjM1doZWhmVWVMVmp6TEU3Q0gKMGNURTJGcGVvNzhMTWtFa1poNmtZNTNxZWxRZ2VEaHlxWjcvQWVzeTFxZ29oaVQzTG5PYlRkck1hM1RwdGZjYwpWa1EvV1ZIS3pheTlndlpPUlkwdWVTWkZmMUljc1BPYW14eG9uM08zVDRkTXRSRldYRzRhV3hMWG9YMkI3WWMxCkJlUGFvRXBZSmovdE5oaCtPMmRIL2tsNDBxK1RVZkFkWTA4Y21PSE1iamd2Q2ZxenBqL1N2aTNXOVhpYlZ0TUEKOVgzZk1sRmRXRWJyeXRlMWlIbXBJVkZ2MWFkS2ZiT1V6VFduZ2JjSmNVMzJLQ1A1Tml5Q09EcHpZZ1dmREtlcgpYdWFoRnFua3Yzb1d0d1h2NHI1OHdBVFJlUnUyRENweTQwRGtzQmZwa0dKamdCaWRuTGZZRjIwVkxMNXVvWGN6Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUFUM3RJZzl5SUErWVhRMzh5ekwKM3FraG9FSHJuZ2l3WEp4T2VlTmV0blNZUkQrV0V4L0JqT3JhS29wUVczeDZTNlZJZjlXUUhRQlhNNkt0MXJWZQpINjdVUGdKbG8vWWlHeWZsRlgvdjZjbExIeVBDNXUxcDRxWThmem90U0lMWWk0c2V5T3lOakZReS9mTDZ4akw1Ck1IVnRnZFB6UVBROTYxN3JXdC9FVi9Tdm5sQm1ucXM5T0FveGJxUC9yL1FqT1I0Q1AwQTZURVpNejhYODVOeEYKd1FpQ29IK1hJeXlBL3MyVGZOWkNPeTIyMHQwY3ZKSFpONmlUR0pKc0RJTldpVTRJbkthc3BUMHB3czdiQTdBRQpKMTRPNFg1RXdIaXNST3FmcHgyK1dMYWJkQ1JPOWlaNG83eFA2d25jbkNucGZyTUZYQVlWbkcvTG9GdTdCQzI2CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd01GRWRmaWN5MXNBS1MveGtrbHcKRWtvSkVpM2V3RHJIUmRXVFdDQTZmdHVYdHdwWnFNcEUrZW5YSDdsNWlZSHpXV1kvYjJtdGRTZmRybWFscDJBagpCeVhvUDRmYkkvT05xSDFmOHN1TzBaclc0cDZGeDY5ckdwbkY4bTlnVk03cE8zTEM1enIxRDNRSVFCTmxWSzBOCnB5cFhrSGlaaUIrektBdWl5S2pzM2kycWxkQ3NlMzlCMldWL1o5dmdlV1pnaGZJeTJneVFVcE1jMmYwa0hqRnQKTE5XVUtGNEZqRzFzSS9hSTRLbFhTT2pSd1RRNExvanhRZTBzT3l2UDdOMWNEaVhrTUt6RitPTTZxWnZuZnVndApZektJUUd5RlR1ZWgwaURRR1JsNUJxN1JjSnlwYUxQN1AwU2VKZldDSUtVVTJyWSthbTFyUnVXckIvNjFXRmxiCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBektMMTZyYVc5Um53LytBUGppc28KRzUxYm1EZFd0T1hPSzRVd1ZOZUF5WUlsTWlVU0VQQ1QrelI4SnJUZXRaMU1JR1IwRldBL0toUDlub0tFT3BDWgp6WXcwNFBQejBDUUJUUm9TSHhVZW1IbnBxcURCRmIrODRoSXhzR1BmQUFJMzcvN3gxR29JNGQ2TnAweTk5aEovCkM0djNWY1loSm1HbFN1ZkovNGV0MUpMQlFiTWdDWlZDbWNhdzh3bVBHd1JCcUN0YUowd29UOHRnSXI2eC9pbEIKai9xem5iMHJmM3dlbUtrTGdqbUhFaWozQTBoZ3RROFhHenVZYS9IVGhHY05iU0VGRUlWRlF5dkFJWFRUNFY1WApWMlNadkNBQ09ET1ZUVjEwV1IxM0Z1MUFDM043N21Rcms1N2QvVHhKZCtGS2RmREhKUWMwZVpGRmlFRVJ6MmlhCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3UvVGhtOElaK1g5ajhyUi9WMDAKRWNOYVNiQUIremVCYWZhekZHYTk0TkVQakYvT2U1SWdqKy8zREw5b0drMHlOQkNkVXlSZHA3WCt2UmhHNURjRgptMDZscUtKMnl3Z3BibXhaOUlJTU1BM1JjcEhxemM0NlFiWHJrQzQ4OVVJaGcxNTVHUktYcmgrbDNDeXRCUnR4CjZVRjYvcXl3WWxxWDRndTlaYWFuWmU3SVRnSlliK2JRZjd4YWErWGtuVnBOTkpvWDIxWThRblQzN1pYVnFURTQKSmMrYis2RFV2RzJSeGxJc01tdXRIeEV4VUJIcEVPeWdxZklTdnRyM3kxVS9CckIrM2RWNWUwbklUU3JOS3FEVwpEaTljTWs1RW92QU4zb3l4MHJFTmdiQ1JFeHFRTVg4REVJNkxuUXdVaHM2STB2bUxsMzB3WUN5dzVsRTNVM0tBClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnQrQUswNGZ6VG9ELzd5bzhZRU8KS29wYmRrUkxXOVZvaGEvTGxKdlRuYTZnWEhvM0Z5alAyQWF5MEVWZTFHZjl0Sk4wWjJKZFVPbFl5R3FWWnpZZApFUzRJVkJ0NjhxeGloUGcrV2tjVDRjNGZDNzl5dGpjdVJUUmFFSFFRZ0xDdHpkZ0hvRkZaWkRibkFoVWpvOU5pCktKeGZRNlZhbUFaeDdXL2ZUMlkrbnNkSTJJWEY4NDhFa3cvaDQwajFmL3MwbjY3VUpyZW1zQ1FwVjB1Q0l3dzYKTDZ0TTQrYkx3QUQxd0JMNkp3M3RjZFUwVmNUQnhvYjVESklUQUdENW1jRkZkTVIyajl3Z1czTUZYbWtESUI2RwplY0ZHUW9Jd1VPc3FSMGVGay9DdlExeXVyNGp6c2p4bzNpdHQrT1RCUlY3RGplREtNODB2ejd1Mk8vRWgyZ1krClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2U2eUFEYmREemEzMzllN2dxcHIKUTNTWVdmaU1Md0hxdUdLdG5lRmJBL0VrOVE1cU52cmN0NWFMaWR3M1FRVmY0NzM4YWtlelpwYnJYS2s4aENhVwpGaEtjUXVJSUU4clA4WDZMRngyZ29YbXY4ZmVZc1hEZ2J6OEg4MDYvK2VHL0JwbVRHQ2F4Qkp6UThVem9uYXl5CnU1VU1tN0pXN05jb1IxNzlBOG96VG4yZTNEZFVrMEp1b0JKalNhM3lpamorbktTUzRFZTVJaEVzMkcrZjJuZncKYmhidmh6SFEvMmdKWlFKOW00eFkxYnJ6V2hNTkhFQ1FvVURTdVNZc3ArZGEvbEJXMTR0ZmsxZlNQbnlaZ2JQMwpBS3dZZzMweU0xS2c1TEdocy9ZeUpOTXZNak1ZNE1KRW1iL1ZoUEJUZTdjdStaTUVLcXR1OFFkQ0xTVVVZS2RECi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1lZNHpmTjdLWExPbDEzSWNsSDMKN05nK3EzWVgzcG5wR1BpZ2NqUUt3c1JRWTVpWFduN3d1RDFLTGRIOWlMaHhDc0lpeWxMRndJNVA2d0t3NzBTUgpRTWdiWlRVKzhQcXpxc21iWDg4TGpEL2VhQ2FrM0plUFpRT2lzemszWkY5K2ltTmNvajduSzNoOVRaaGZoMktSCjdyMDcwVXFRMzZydWJTQW1wZDhMN1hzL2tHMStBdlo1NDNCSFBmVmJrakx6NWVXU3QyWXppc0pPSjZEbzdaeUkKd2lNL3dDWGxkV2pyL0pueVU3TU03QTY4djFsZHFKa3lCWTAyd2NYM2REVUhXUndrL0VpVVoxSUdMakh2eXJIcQpjejlzRllHV3dsRjZRUy9IMkpTaUtUNCtjd21QYVVSQUFySm5Ea0NGMmxWb3pHcG00dVhUZHE0UmtzVW15MDlvCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVd4czNMUklJZmxwYzM4SlNIeXkKUXIvUkZaSkFWdEZhZXA1OUNGMldOU01hZ1NKL1dpR1hoeGZCRTYyMWF3NEo3VlRhc3FtNWh2ZzVGS3RtYUdVeQo3NE5HN0Nab24rNE1MK1NtNUNqZlM2ejJBVDkwT0FySW5rQlNnSWM2cVp6Nko2N1hUdGNkcDl6cXBGd2lMQjJDClAvZlNMdndQaTE0dzlPLzhubWF3RVFFVngxdUM0dHBKbnJyRy9YL2ovb0lSNGloL3lsSjh1T2lVL2lQdG95K20KYW85RUphSVFMdVBvWkFHYVkzWk1mRWVVN1FRSWxzUjVnRDNnSDI3bmVKVUROS0FFVHJZejROSnZJWEwzQU5ncwp1VXNOdUxSNWg5eEhNYWxTT3JLVWMxUUZkbTRaSzBYN1pDWjJiczVjQXpsci8zOFp4bVdZVnNXVUxNc2czL0d1CkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0RhVUgyNUU4VTgyMWVvZ1JETGMKT1lMTWs3YXM5c01iSXZ0OFVYcHVrS1dvTldaRjNrWWUwSE1Fd1hYNG9vZ2JnMmpFZ0VoeDNUbmtUZFhkNG9SUwpGK04xcHFJMmZhT214eFRaQmF3NDR0ZlVVK1pOQmtPU1ArRG9KZFB6Y1FRTlJSeisxbnUzakdlUnExdlJPNVkvCnViQVZWcHkycW9ZWVRZTzBINWF4S1lGbWZDbzI1b3ZXQkJwUUJ3dU5sMW9EWE9hSFJQY1ZXVklpQ1FCZmZoMmoKakpvNjlGRTRSckk1OWlYejB2dXFOMVgyVFFuR3laeTg4cFVHV0d5UU5SaU9yRWFXak5qVEJ3NWIwSzA4NloxUwpjM2QwbWRXTGlPT0tGdmV0QldXVlZHYldLelJxRGgwSTFCTzNoeDZ5NW5CSXhTRi9BT1dLWjVJb3BPYzRxUGtuCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekJlUjRSV3FNMllLakJLOXpMeG0KVWU5WkNkT2JTWGZ0WU9oazMyemQ3d01iWVpRbkowT2c1R1IxVE5rKyswbXlheEtsVWhxSjZaMEF3YWJzSkNORwowbFdjbVQ2c2JMYUtJc0p5ZUQ1dWVEY0lPUHQzcjFaU3J4eEJJSUZzekJZK2NxVy9raTRIMU5wNkxvQ0w0bnE0CjBsUzlDNEFTQmtXOFk3bFFFMFJiYlBnVnNjL09YRVBkblp5R25VenFEc1NOVWpDc3BEWTdtSWx2bkNpQkZ3SGsKTXprV2VZRGN6bnd4QllkYnNkM3pLK2VObTVHOVV4QlJ1akwveEFWNHE0Rk1FTDlRbldaWjV0bndZaVJ6UnpOTwo1VkJrKzJuZFErd2hSSENUSVhxNW5NeHRGV0wxblZxbVU4ZWVPTU1uVHpDR3U0S1B0d3puSFBoNHpwdU1XUEp5Cnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHdWUmQvMUF3VkJNNmNoZTVIU1UKa1BVVGxtUHptTGxIeExuUE5SRWtCVndGOGh4SnNST3NSSTJPK1pkam54WmJlb3J4Mm5vMFNBN0NYdVg3ODlEdQpZZXJyaDJ1TTBYQ2RXWkhVUE8xdkVudW9NbS9CWTUzem00WXNraklqZzYxWlY1akZPVG01MDAvRkp1M2dqbUF4CmZxdWMxSmhkdVlxYm9CRG9mcUVEYS9qVkEyUSs5STdDZ0Vza0RtMmJ5S1V4YWNIUGxCM3pYM1BsdUJqSnRzVGkKYWczR0xSU3VacGtnV1ZUN3phZ2k1b29rQ0VuSSt6VUVNTkVXVk9LbWN6dE9kZmJkNDdWM0x1VzgzOTRuWlNIQQp2RFVQbFluQ0pKVFN2Vnl3dFcySkV1ZlpPbGxpcnRYZXNNT0EwemtoTDJJRTN5WnA5TXFTckllSEYxUW1hVUU4ClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWFDMm8xM083SGFuMldDU05Sc2sKN041QU1tcFFZek9Mc0xqMC9TbUVQK3o0cjY5SUV1VUo0Zit4RjR2MFVGR25HVVNPYWFJWHN0ZWJQOGlucnR6OAo3eHF4SFpnWnBtK2VtTzdZZXVEU0hWOVdiaGRiaXVJYmlpOU5WSTdtY1FHM0Zzd1lqclNqS2thY2JISVRXUFBCCkVmOSt3cU9yK1hJakRRSnF6R0orSm92aDhvR0hGT3hDay9RL2FvS3R6WXJxKytYVkM5QWdGTmwvaXpmbTRqSWgKZjdkUi9IcTBra290SGVhUHhCZGNpSUs3aHEvSlB6RFpuMFZjb1dVTkZmV203ZUFybCtEcERzTnBHQnQ2dEFYMgpndkUxMTFGRHBRK0VpOVRRb1RGN1B0N3VYcHhqNnFRbnQxdmx6bWRiVnRwanU1bTRON0ViK2RFbk5nU0JVcG4xCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbGZ0ODRLazZ1R3JNVzVtWVUzK2EKWnlKempDNUpyZ0lEeEJETitWZi95ODRIVFQ1V3VvNW9vWlgrUWFCOTJDVW9sY0U1TDFSNmtOd3BKemE0NVArWQppcnVuR3h6YkI2Ym1DUmhlbUI1dWUzZWdDd3RjOHExQTNYS1FDYk90YzBqa091am1tQVNLY05jSUF6SFRUcHc3CjlrQ1lIUVJRdnZuY2lOR0FBcVZRRHhKQ0pTR3lBeW54bjhwK0VQa1FFdWt3OHlYK1piTENrKytaVGg0ZCtWOWcKWFRyUUFnOXh4SWcwcVArTVhWR3A1NVp1MGU5TGNYc1RkY1NEVFlncUdreUk1UFVGYW9yWklaZEJZbUMzelZ3ZQpPODdQZmNIZUVmRE9jWjdKU0NmamZyd3hRNGZLTERlQUN4SFlzc2xQVnBjNVQzTmRFaUxhRm1RMUJ6WGZzL05xCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGYwMmYvVm10TW5kbkU4MkRLWWwKck11NDdhQldJcUVXcFBsdG9nV2tIaFlkSmNhUDdwWVRuZVZqVW9LV1JHUHBnWmRjenNtdVBwTzNza296b0VBRgpUbXNpWFFNcHg2VVZFWThjNzNGRy9kaDRpZkovVWxyUVBSVDdxWmJQSXMrTThPcjYvZU56WTNkTFVteGV5WEFBCkRGUmVvR3BPM2RrN3U5K3RUSSt4RW5EaEpXTUZnY04ycUtNYzhiWlcxZHpWaVdndnBHMnFUcVdwOEVoZ3psTFMKQk85aFJmMHlDOHlETXhERWFGNTZxTXlCYnBLZlJMRjNvNnQrc2ZhZmFweS9sbVY2MSszak4rWDV2OFB2ZEZSawpjZFdwcDh6SWZoa2xINTN5ZlVMbGVyMVRvem1SRkJXS09COU8vYWJuZHpwV1N4YXBYVnVUUGNZR3BscUJQaVdzCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjB1VDlvenFjVFBPNjJLUTZuUEgKaG82Z2FZeXptVk5qalREVmhVOGJxcXZ0THI2T1lBVDRYTWd2VWh2dVUrdnVtMEV6dmpnaVhFck9jazJldGRncAo2RnprQ0ovblZCelZsbGtLU3BBOURyL2tPT0plK1BNb0I0RExrT0xlai83aTFDV0h0Tk5zazlXbXlEallWM21BCjlJOUtSUUU0UEVOT1dlLzQwcmlGVGxqY3FDalNCQW56TzNrdDh0aU9tR1lXZ1VDbTZuSU9PMUpRRDJsYk5RT0UKVzlvNjJOenpGRWR6UVVOVHdqRFJxUVMvbkNYTEpMQkpJZ1c3c2J0RnRnQXAwaTdCZWwxbWNzY1Q3TFJIU1RWbgpLakZsR3dwcVU2WDQ1NVpNS1pVKzl2SnJJODk4TmtLUmFGMjB4MjZVWmxFczAzd0M4YlFZWFpnR0MvcHQ0RWxtCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG45T1hJV0IveTBsWUt3R1MvT3cKQ1ArcVd5NVN6TGk3RUFlMXVMWm5OQWRDMy9GcHlvVm5oZ1Z0aW5JdDRIMU5JKzZXSVJMZWE4cXpYWndQbTczTApvT2llYmVRYWZRMnVQaUZTUjhVTVFVYjFhUHk5RUwyaTRkZVFTbmRjYWdEaGdRZTNOeldoY2x5azhiSWwxZ2xwCmRMaVVuNERDbDlibjkzd0R2Nk1maWJ0OGVSSUh6ZGRTSk1qUXliSlA1YjcrUUU0clk0cmd1dTZTckxwY2orRFAKcnozSEYvL3JxTGtCeTUyQTF2WlhKajlydWZ0TGpZeTdZcWlja3B2cFczbjhPek1FdFgrM1FUZU15VjJTK2NQZAppRCtZSXMxRTlMYUhISkU4TE5XdXNhMUUyMHQ4SDg2MkJJYkJKMmMvR3RPUjU1MmFwbEZjMGEyeFR4T0x0M1FmCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDJUd0NsQTBhSi9Rc3ZYaEM5Z0IKTE03emI5Z2NKZktlbnhNWHZpc091OERyUFFJVVFaalkveHJSdSs3V1NQU0kzL09nYnRZL08vMjZoOXdFS1ZyMApKL1RBaUZlMitvUk9jR3NCYlNBa1kyK2x3cmxoN1ZYb3ptM3FheXhNQ2UrbWFqb0pxQ2sxQzBZSkxFTVVjWXVDCmFUL1pWZUpSeUVZRVZyTzBxekxWY3hrU29wOW9FcjR3cXpRbjJ6Wm1LMEdlWWZhWnI5TnJQSit1RG80Z28renEKMmFYczFPNmMySDhaYXFjRlFDbXlrL0RsejZSTXA0akYwcDNPNTNjZElHVzJuMzU1dTBuRDZxMGg0eFZaL1NaTQpCNFdLVzZXL3JKd3BQcEJaQnc3RW5jTUdOSEwzWEdGV0t2SkxJaUo2U2U3NGZIKzNHK3Qva2dSREFUY2dvaUpiCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTNreHlKaXdIekx3VzNaMDdjUXcKSXM4Y1NzVmFxbGdtMUt5Vldub3p5Z2lVaElacDEvTldnVmJ4RkNFZk9zVVk2eXRLcGFsbm1STGtJdEZXbkIyMgpQcHBYN2ZvbUhxWlNUM3ArZlhVVWVEM2trR1BZaGpwVUhSN1dJRnJ2U2krK055L3duc2o0dWZzRC9uMzdJbkpnCmcxeTRXelJZclJhcWh5N2ZneURoeXM3SldtNUdkWldVcVBuUXVRa0s4UkZvN3oydThsY01rWXJweEVmNERBSDIKQk9oS3dDS1JWTzgydXljT2ZvcFllNWtEYWlZcE0zd21nVzVrZjNvOWZMSGNnQ1Q4Rng3RDlPVjdMWUVJSW1qWApmdXl2R1V2SnkrbUNjb3A4VTdYa1J0Qmk4WDZ1aW5FL1JDN2ExLytNYkI2SEFHOCt1NnlhV1kyT21VQ0s2S01zCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0lkTzUyL2dUU215VWh5anlEclEKVStDYnVvOEZOYU8xTUplQ2hDU0l6Qk5NbFJqZkdHdzNBM0lpakhsYXZtSnAzaHVjKzlkRG45Mlc2dHVWeGFZYwpCZ0U2Zlovb1U5R0FCdWF4TlZTN3k0dXVUd20wc0pTZVA2anJqTHp2WVJWTEt2citCbS80OWNqRVYzWExKcWhWCnVTM2FlVDYxekFwRWpSdTZZY3VNVlNXM3RCcEs5RXl4NW9JNStlUFU0RnBWa204VkNodlY4ZjJHd0RIRTZvRjYKQzA3dUoxbFZrTkJsa1YrRVA0ODg2ZkdDTXQ3NmZUQm5mOFBoRVJZdXhBd3hHajdJblBaUFNaa1BlV0JmMFM5cgpSUkNzRU9mUTVZaXdJTDFkQ2QvamtOS2NrdG9SWFFYVmY5UUNGbzQrUStGOWdVQythM0dSZG1CLy9CbnRnZHJICkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0E3SmpGcG9MSWhwUDJITVAwQ2sKdTJhdEdyV3RzZ3hxbnpOdkF5WVlTN3drUUptbnhkZHk5WjhTS2pZYmZnWEdScC9WZUxGWGVlWUd1d2M0VlltawpuanlMdVNxU3BhMHhRb0IzS01NOE1yYVhyTkdFN2NsUVhkdmx6TWNSaiswTGFzNEpwWTJ5UHpYYy9hL2IzZWFSCmgvYlVnMjB3YmM5SjhpVE9NejdsdmJ6UDdnY1VQUUlhOHhRUktMckpiaEpMcSs2SUw0SlJteE8vTHJmcFQ4Um8KYXg0S00yeFFSRmg3L0xuT1MxVVZWcnhyUGtockFNNFdZTFNkU0RpdStNbWkra3Q3ak5NdGpIUytkc0lKK2hIZApIOVpGbjJ4UlVKWXdLUnhxUWR4eVV1ekpSOVpMYkt1eVd5cUI1eFdxeWhGZDZFelJxZjNkcjkvd1VzRlBCMzJ5CkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckZXTlp4QW8rT2RoTHh5cTV6TnQKMU83UE9WMXBiNG5lTEhYQzAxeEplZWtTNW9GN3Vad0xHR2FISlg1Wk4wYzhYNzYrZWRMOE8ySHpGNjIyN3dyMwpLNlA5MWxjR0trdUFwREhBSlN0bmZVMmRUc3ZQaUNPY2FZUWF0QjV3YlkzU1NHamRPeFpqVkU3bVBPZUZtQ2p3ClB0MHFwaU9jTDhRMGVjK2xaaCtjRzJhMkVWcW1DcmlKNmdXQzBrRHlTQndUaUIvUUNJZEVOU1NrTGVGNVpkZUIKS1JrK1MrOFRrUzIyQlo4UGZWUE04NXM4OUs3N2RLL0EzcDVhNDZIaHY5K3B2elBFbVVsUHYwMmhKTFFDWUx1Ygp3YWZIdG1ZUjFvQitFeCtTd1I1NUMvT1o5YXBhZUVpTTJJR3B3WUxpd3B4Y2Jyai9Eb0FEZkdPL2dnNm54M1pBCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkxyRkt2UjRvak9DUzRhbEVQSm8Kb3VFd2lrQU52YVVnNjJYcGlGWjBzVFZEMEovUi9ueW01UGl6cFRHcCt0ZlpwUVE3YzhJaGt0U0dOeWE0eFpBQgovNUw4ZmxsOXp3b0NFN215VWpsWElyN2lxYTNqYjZybEs3bndUQldySmNFazNncEFPREZvU1ArZEQ5a0xnTTBNCkhMMFZVQzc2eHFlaU5MMjBzb2VKM2hhbWtzQWJJeGxPSFFQYU5SUkg1SWhXWnVFMWJJQTROSjdNOWExL1l3YU4KYlRFM1Y0NCtUMDF2UHhxakRUcnBUN3J0ckhsL3BwZ2ozZVR3MzdQZWVaZ0hyVW9vU01ibGwzUzhuaDZuMFg0MQo2RUZ1T2JyaER5VjBwR0RFaVVydEJvNGJhanRycEV5V0thbWVncTNzYUMyeWRMUCt0dWk0eWdXTkdlbThhazFpCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNS9OdXJoNTNObXlBSFo1S2R5WFgKN3kxbTNiTThWdlBqNGVoOVFmbzcxN1dBYXlsSnpUZWY2ZHFyMVF0V2h4eW40NW9KcHlxNWpGVkV5WEhhdGN4SAowTzc0TnBoelBrdE1yVW1LbCt2ejJkSFppMUlZN3lkVDZGZG1yTXNNT1RJY0orQlpSVmJ0c0o2UzE5cWl6R0cwClJ5RzRLdWNpaVVnQTM5WnFvc1lhV3ZoSTFZbHVGUzdzT2hGVHpLV3NGalVQZnZUWkFiNGRRcTRjZFRCZHIxM1YKYksxM2l4MUp4WUZzMWk3Q290U0tWaXZ5NmdhU1l4b0RYdDlmQVZzdFpSMFBodURvUGdia0piTzJoVnVFRTl3TgorTUZjSnk2UmQ1bXNMZzVpcEhyWkk3a2QyajU5NUIyamxpcGVkdldDMjh0SU93ajZzcE4vMUFtMENIaGM4dnBiCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDZBZFJCa3hFMThTeEJ1Kzg4UTAKdDVad3J6a1J1WTNXSXlrUytsaURpeDZwYzdRVWZSTFpkSzk2UXp6SlhJMVV3aXlMVkpaZTJoTVNraER4UFNCSQp5WkVRQkx2M0MxWitCZEpySGtmTzVEZjIzRDN2QUl4TExQZ1E1VmtYSFVsYWJHZnVkbFFCenZUdTQ2MVBZNFBQCks3ZXFrUmpva1BJZmp6cWRVK29iVDlLSE9RaGZib2VJSGI3TFlibzlHendlNFlQZXlqNklNZTRoLzZIT0EvK0YKMkdlb0lzWHArZks5M0hWbTN4bDJGNmVrV3l2WnRLclpZd3Q5N2tSbzNYZDZTbGxlL01rb1FkREhnWjVKeWkxVwpXcFRYdVM5bDJUMFlPRDFieHlUb216QVIwNCtyOUdydHhKdlBVdDhnTWNYSlRsRkRham1jVmRwVzZrQ3pDdGF1CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeERRSjN4aUI0WWVjWkFtTkY4eCsKV2lLam5JaDNaMzVDeXJLWXJRK0lOWEJ3MVZmRzRtd3EwL1RVRVZ3cVhCVlNRaXFSMGJqSVV2KzNXNVg5SjNoeApYdmJYb3Z0QWIwOTRzL1dDQURHTDBkZTk1ZHExSzlHb1FqR2ZteExWbWNLeDk3eERwOU9SV2ZzZnpnYkhISHpkCkFpMXp3anliYm9PMDJHTlhXaU5rVGdORUlqenA0ajBSTXJlNDA4bC9KamRlb3ZBUG0wbXBWTk1hVTRUa2dSRjYKem8rUWpaR3BlWFQ0cmtnY0ZHZXRjTXhpaDFoL1ZpUmJFMkxxMmVTODFnU2N3MXJoaHgvYmppOU9jU0pHbGRHcgpRY3pwTmNTcy9GSEhQQ2VlNHBta1pwTmN5UW9ZcjNwN0JpM0Q5aUN5Y2RPMUN6MFkwY2F2ekd5UG5NcjZ5dkdLCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2RIbUl4OHdybHJTRG9RbDdGTmoKYmFDREF6L2YzaDJRVGloTTg0YndXK2FzQm85MCtuMWtpN0o5R0NhVzVuYmNnbGV6SDBWWmtpM1REMlorYzhnSQpLY0NWcGlNaGY4M1d5MjhwSmdGZllyMTRteHBvWUJvcmpSTXhzUGg0L0x3WFludHIrWGw1aHYvSXdzeHVVbllHCjRGWEFwN1hTY0dqWTdDdE1GTk9UZ0ZCTXRXWHhDSGQrWkpZeWxtZlRCN1VNWjcxVWFwMHo1alNqdTliNDIvK0MKN3N2ejF6TWpVZHBWaXBBMm0xVWZEa0FXaXlmNDZUemk3cDQzNmRsZ0w2aC9meGY5UlpJNWl1c1E4UFBoMFRFQwo3RUVuanE2SmJETDNDRG13WlJSL3psbS9WYzNGMGphQXZwV0wzUmcyV3ZORzRGLzhMbzJMTW93ek96Qkt2aktJClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVp6MWVJTWh3aUtBVXJROXZCaWIKTHZFTDlpWnJRYVBFSTBDeDdSbExmK0RkUkhSeS9ORkhCYURWY29vdTJOSTRLdXh0ZTZlVWJJSzBHVk0yTE00YQpYVjBvMUl4amdubFo0R0o1M3lVZmNRZitjbkt3OENidUhsQnpSSUxnbEVzaUVUZ1JSem50TDB0ZUgxaHRldE53CmE5ZGc2alB1dVlGUnBjYUFNRUpQN2pUSWJiLzd4TXRWamU3bElGSlBkc01nQTBLdS9qemtjNnN0MGNKM3FhWDEKcngrUzBuVXVud2JoOXlKRGpaK2RUS2lJcHBsbnpqTSs1Nk9CTElkM3l0Z3o2UXgranF4aHl2VHRmUzJXZ2M1QwpoemlGNkVpMTBQUkxveURGV1AzN1ljQkpUYmp2N0tBN05XMkdGeHNUQzdtVDQwTHNhbzdRa000UzVpclRUbFJJCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTBvdmtzVE1tc2VBZUlIa0VHWVEKWDJqYWE0TGM4RUI0blpQdG4xMllobWgwWndiR0dDd29ZUnhBOWk4bVRVWDdDaDJnT05KZlBoTGxFTlEzQ1FSYgpaMHZkcWJWYjJDS0ZmdGZ0alhzMUJ5SVQ2Mlh5Yi8rTG9qcWdIWlFWalpoK1laUnFyOGg4SHlNdmNGUC9sOWpzClUwVFRvb3hXUSsya3BtM1V1cGVwbW44ZHd1Z1V0bjNUUXJBY2xwT2p3WXJHeWVLckV5cFRZaitzRnNtVkZoZDMKRWJYOTk5TVdLTXRhWnFmWmlIZ1pYWXZGQVArV011b0Ftd2xtcG1kVEk2RU9IZ2EwWXI2YjZ1WWZHSEJ1UGJldAo1bS95RUtQdmdhbGtWTGxNakZWNktta0gzWWd1YVlWNEdZN1d0cEdGL3JBZ21sVUYrQUtaaWpqZVlId0d3Ris0CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOUc5K3lPSkZ4ZGlRSWR3TlJUL0IKcVZwSXRFN3QweFFwWjNyQkVvL2tlQldabXk5MnIyRHdEZ1VRczFEQlFLdnc4dXVuNUVuQWNqSm1SaWM2cDBodwp3RytpVmxHUWNJb2xJeUhGdGZvWjdsMEQ5UFpWb3dlTWxHQmQ0Z1RVcGZJUWtwVHd0UWxPYlprWHdsOVIrcFpECmJZd29BOUhBSFRwWVZqeERSVm9laFNETDk0d0pUSEhzSEZvZFFoYmNJTGRsUmtoYjZIcUdMUnhEZFZvQ1JROUQKcUZ0TjlJWS96anY1SEk4VWxYSWtYSFNJbVU1ZVcybnEwdVYreHViMEhkejFTc2hQSUZGMDVoZFpYQkdWaEUyaQorcEZGN3ZLL01vc2JHN0QvWHJsZUlhZ3Q1c3VLd3E4TWpoeE0wbVVXSmVidWlIT0ZFZTBTbHljUlp5NkFwb1hKCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnh4cEZRL1cyNVcrbHQzOW14dVQKWXZXZFdEaGZXai84U1dSWXlMMTA3ZEEvc0FuV0lUbXVucFRDUFdXa2pqbTgvRTJLb280d3JkY1VZRDNRODI5Lwo4OEZYbUlNZlBTRlEyYW1vSUhoVS9JbHlmWDhPODZWaytVTXVwMThHRXdaU1kyRW9wUXRVcHM5MEp0UWVQZlNuCk1jcGVLdVJQZWFwRjRYODJ6OEk3TVBYZlc2L1Juc2hQSFh3YXhCR3JPVnNPRE01VzNLK3h6alY2WUFIQVA5bWwKQmZFZlh6aENDR0g3T21BZkFQT05JMWVUc3ZzUnQ3OU12NFJCcDlHY09mQnlFdWRTU0x2RkhMeTJYOWk1RGZrWApCNWR3ZldpMEVMTXpjSkprTE1CSWtSM0tadFkxQWp2emcrbG9QNnduTTNWNHBxSnZrdEJZWEhCK0s0WVJGbGRBCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2NLRVRVYXVIQjMxKzQwRkhpeFYKeUxQWVhqRDEzSFBnNEZ2SmthMzdCV0k1dWJlaVZWM29jSWhsTW44UnlYczJ3MlpQTGdQcVVjbjh5REpPRVptagpNeGlDL2E1NFdIUC9HVGJOdS8vbW5KNCtxNmlMU3diMUdmeGE5Sm9WeERDbUEyWGQ1M2lsTDNJR0xxNlR3bXhxCnZURTlDMzEzSXppZUI1RThVa1ZQRW5LLzlBenBKeHVTUDFSWmFGaUxCeFpUd3RtNHpHeWVjWk4rSzhBZzBRbVcKQlhjWEwvSURORHp5OWRreGtoNUY0OWgveFNLVWZjem5lUFlqVFBtMk56dDYrRVBmZjRHZlpsc2prTkFycEt2TApwdWpaa3VXM2FKVDJ4RkJYN2NnblNSZGpSaGI4LzdNZzAzM0VaWExJMXB0WDZRa202aTRtTmVYNG5OV1J5ai9mCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzA1cHdiNFJObTN1UW9lb25DdTcKSEZoMWNiVUFXUDdIVUdVRzNKTEhoaEMrQzhJaW9zcTNKZ0tKaWNUaFFVTVVHR3dMVW5CQTRIdExvN2J5ZVUrOApSZXovYUtKNUpTUlNWZ1ZEd0REU004Y1BtR0tTKzNmWDRKaUlvYlZ1ZjNrU1NEcE9mU1RJU0xNREhJbHMvVHdlCmZJRGFEWkUzMHNHR0xMVlE2clV6alFiTkFJZEpsaTNUNFdiOXpCajFFN2w3eUk5YTcweVUxd0IyT3BOMEV4Yy8KOTJJS293bGlPVEJBZUJ4ajVmeHFCUEljaEJpbkQ4cW9OQlIxWjV4Q1hrZVhibldyNDJBVjF5Y0RyZHhuUGZQaApBVEhFLytYMTlPcFB6MDlLNTB0OSt1Wngvc0syTVBBSzUxYzJKeEd6emtqZ1RyRURhNEtQUUVxK245eDk0MW1JCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm8raVhqNzZveWRhQU56RGNXVU4KbkREaDNqSy95MVg2R2ljV1FRZSt4NmRxMW9RdzNoNzVRK1FrcC9HR21ka1JxZnhXM2RrdVMwTXU4ODFCWW8ySgpWSmhKRURqaEg1VDcrNzRXVGxicG1wUlQwdWZ2QzVORHhNWlBtY0EwaXl2OE1yWHBBNXZYa1IzVXY1YlVoTjNhClFyU1R2Y2MwYlAyVk1KYld3d210MVJrY3M1SllsMnF1eE5ZRWVJTjhjMmhGT0FTb0hKTjRIRVk0c1FUNUZSS1EKLzVFQWQyNjAycGZ6eVNvTEZRYmpsS2JrWnFWSEZGUEl3cGtDeVJIRUFsRU1lVXAwcmo0NUFhTTcvenVCb1Q1egpQSVFUc2kvZDBXUEczNk9iYyt6NWUzdXcwek5HdG5zNHdmQ3F0emY1eUxld3VJcU01bFg1K1ZFc1BoN2ZTWGR3CnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2JHYzhCWEhDUzhwUTFydmlxQWQKUGNOa3pvWW84YnNYbDJ5RFNaR1ZwWHlNNndWNlRyMHJlVXM1YkVHMXM1OGplYkNsRTU1dkR5alFNd2ZoOGVURApZU1FKellEUDNzZzUxcmRoWkNwKzNaVzFFYlJaM3luL2lxUGoyWHVDM3VuL1hSakFRcDBYazhMTnp0SkQvY2Y0Ck1nWTFIaUMycGV2TFhpV2xHdHMrTWk2U2tTcXMyUVYxbkNvRFZ6SGhUZFJsZHdWa25CQkZZSkw0K3JLN3lvNlIKdldYeDJYVDNXZ0xmbko4OFJXOUl4aGZpQyt6UnhrKysyaDZjait4RnV2V2NwOFFvOWw2Rkl3bTc4TlVORnc1RwoxWkRlS0RvT0lFc0ZTTW5qSnZQdnZsckJDQ1IzL0pBZnpLTm9XY3lTdjhQNzVJS2xGSnNWSjZ5blpkZGkwWXFwCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFVFV001TzlBM0paQy9xRE5RZkMKdVFFbHNhd1RrMktaS3I5Z0gxdk8yR2p5Vzc2RGZScFpDUkE4L0RYenR4RkhLeHkwb0lKTmVibW1ZMkxZcGpRTApobTZFcXpNYTE0R09jN01kYmRDMmVxNlcyNWg2ZUU2WEZSMm9oUk90SnhnTk1aYUpObVJBQU5mTEVUS3IyVUhtCkNjNy9iNzc4K2QweVJnaWlVRDFGQzFkK0ovUmZzbStRV0JydkRXa1lpeFQ4MWpvYWlxRnJqNGJielA4R1BUSXcKa3RNSVFvYTcxTVFxNGVuUGcwcVhFUkZVTklqSHE4ZzVrNjRvazdNUmd0aFNtUzRpd2JuQmtSdVphRTBnRHQ3Two1SU5LeDRhQTRaeGlKTHhzYlBvYjM0YWkvNURKUmFCV0tvTlE4aTNPYitiQ0lxdnJvbHNFaDJKaS8xQVlIY01mCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcytKM1hNQml2WU45YmxpbmU2eVYKMXBhdGNFeXNkS3djR0Q3MnJPYXpndjdTVzljTWZjWlZ0bTdJbkZXSjlqKzlBdklFcHJpa3RmSERSVlFjZitsSwpqd2NLRGd5dkUrRXB4SW8ydDQrT0ZaK2ViMW9QWEVDQUFKbHhsSVhDSDhqd3Qyd3Z4bnZRZG5LZHZFTDJ5NnptCnU1b0JQc2tOK3BiZVhwcXlQK2s1Um9TSDZ0RWJpZUVDbG1oaGFZWmNaS2RJemcwT08rUXdYYXgvbFpJUjE5WFIKRUl4NVJCYTRkc1hOVGtoTHQ0dmx1T2hYL1FwL1hJZVBHRFhHT2RnRitqNlVvQzNQVWZHQStBYXh4dDlnTjVnLwphQTFxL2UydXFBTXZ3YTROQTlYZEhhdTVCaFp6eFB1RVJ6UUR6d2gzUUp0RzRzVmpGcHBHa1hUSVFpZzFoMEx3Ckl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm44OGl1dlc3YUs3ZTlJSytwaTcKYkFwZE55TUp5NGpOOVRRcURwN1JPYWFxZVhMaGZBeHgrSmVzdE1qSHZJR25QczV1T09zQVBiakJPUDlMai9LZQpma2lDNE02eGlCQXRCdjNSaDJEVVBFbzh3VFdjYlFkazJ6UmRvL2JWRm0ybEtmU3Y5Z1ZIcXhSa29FZ215OERLCkNlbDhqYUFpQmcyWGdpRDgxMHoxNkpXcm1Oa0U4Y1RDeFVGMDhXdUswVEdHV2pqcE11R2g1aTB6bi8yNllxYkEKV0pSNndpa0xxV0tYZXM5dU5WRnd0QXdQTEpES3F2WDdNazdaaVo2bjB5aWYwam9JNUl2WkxBZitoWmpNSVBUbgorNm1OWmFiaDdLTmU1SGs2UGlLQjNHRnJRRWV2VVZBNGd5QURlVmFMRld5UkUxbWlYK24reUltbVhrUXBIeDcxCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlpITTlPdjFlZ0Y4MVFGQlpidnQKRVhJT240dHBlVGlsQUcwbnJzUzRVV3ZKOXpVLzc0SG90enlKT1I1TGxyQjErazNQWFJNUHE2cDhSSVlUdzVESQpGN3o3Tm1lMjZLRGtkdnVHRGUzM1gzTDRTak9YdmJCY1E3Z2dYcldlelVaY3BvZk9yU2t5VnJWYzhWRE5vN1hlCkYrQzF5bjdNNmFteksrbk1qdFNjeWVLUGZacE9nS2o5TXpWSWNNWHpoWTA0ZWVJNlJWS3JINFVGNWNlNDZSSGcKMG9FWjU0TWdRUi9DS205Yi9xb0lKWjVSQk8zVzlaTjJEVjFCM2p4RFM3QkdUTlFFem9iTVBJTUV4T0RGMUY2YQpadXFIc1cwV3k4MWh2d3laV0ttMVVxSytjOHE3Q1Q2aGEvRW5keEZzS2doUzZFY1lwcVgvS002RWpJdURXV0QzCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGZFL2diWmtRWGNyMTYzUHEwb1kKS3ZrSHhEVVBnUVV4TjdkamhXY1piVWwzOXlpTWt2TERzbytPeVVPKzI5ODVTYzdGb3JheUZzQUdjNDJBRDFMKwpSMjJzbkdWQU54QURZeUlWOG9raysvY2EzZGV0MzlEc0lMbjEvSElwWTVrazVOYXE3TVNKSlpsL28yR2lxMGFkClRiS3Z1dTlQWEdUTWVhT250TmMydElyRDMzOXd6ZzdvR3pvRmpJeU1sU0pEaWpyWW5OZ0JIN1d6dXFiSDJWY3AKWG9nUUd6a3ZRWlZHTzZ5VWR3c2t5b3VoUzJJU0Q1QW5VNHZRT0dQTEZCUmdidHFWNTRHZkRWRDBUTjBTYlA1Rgo5RDRuL1NWMHZnYktNZ1JVN3Y5d3p0UHhkSzRBM1pxdmRoakYzOGVzc1lTYXBGSm0rTWdUOUJ6NkNQS3pBVDdYCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOVJsS3owQVk1cm5oOWlESHVGY3YKUnVvSzIwU1QzSWx5NUxsMURyWFRlY0hyaEQzWitsZktqK2Y1dG5wUEhGcG1DaXJMUTJwR2ZLT2Q0WUFxVXNHZAp6SGsrczRBYjFnVlhpL3VnbVN2QmRBVzhhQ0pCdHlOVmRXTy9uaXBoSFl4bzlrd2RQVVF0QXROVE9MSzQ4TTVaCjJ0b1o4VGJGUEFSZGNRdTVoNkdWMXNONXFFOEErMHdVS1V1WUNBdFBZSGQzY1I1V0JadjV2UVpla09yL2pjdmgKYi9VTjkvVVUyUFkxdm1BYlpRK2U5MjRLQU5XbGl4S29hYUdrblVOdFZpZWxNN1RlYXpOUmNKSVVnTnI1eUFRVQo5aklJc0hKWDVCQ3RIRTNxVGlhaTd5U2c2VWxhVEh5bys2TEUyZUZ5RmgyTTNGYWc2bEVYUS9Vb0I2NWdZd0hCCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdktWNWN0K2FBTWsxQllKbEJqWUoKOWFGUkdPS0lDU1J4SEZQTGRHVHpnTVU5YzNZNjFxa1ZCOWJISWZPTmJ4Z0xPVTBPamFQaStGSGZpYUl6SGZZVQp0TTMvKyswbGRSZ2tCU3FxREUvWld2Q1BxOHlIVjF6NFNGazFHLzFZWGtxL1RIVG5rOWd0ZTFmOU10bHQ0T00zCk1hZllEZWRxUy9LdHBVSkNTc2U0VWQwckZyRGxobFM2NlBXTzF1VGRQakQzVFlUZWZJVlJBalBsalI1SWlHZFcKWGJKN3pWbUN4L2hSemVJMTFkc3hDTTBHTWtIL05pYmxZRkFhc21qNXdyejhxanUwTHA0MGY5ZkU1N1NYQkpuNwpsblcwbkRhUGQraUp0SUJFNlJNRE14OE1Qd1U2a1RNdHE0ek9qeGViSTRtWFVDNVZaSGROR3U3L0xDUGlML242Ck9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDAvVThZUzc5aitCSis1OVVGdUgKL0tzY1dWRTdsSFQ0ZWtsUTdFQVJ0Y2xGaVpSZk9QamFuaFpqM2ZqbkgzYVFPanhlb0lmRmFDSllVYTc0OUtWSQo1VEtjMXMyLzVlZXA2MUpDQ3BwZVJuemFUb2pnWlNrSmxMbWlaMFZzeG9LY3V4T3JNYzZyeUVKM2RjT1dKdWpqCkpYek9ZVXFpSTVxZ1ZnVnQ0STVnUHdINTdHRGZvRzhUeHFnODBPTVVTQlkwZWdXQzA4Y09HbnByUmhReG55MmcKUlJ0c3U4SERReVhDTmxlNEZZeGYybVZBbEFUM3QyckkzdWdEWngwL1c3Tm8zdDZzSW1Da3Z0MzhMUXdGQ0RscQppWE9tWlhQUjYwb0xueUJFVTk4NDhKcDhmbHNmTTJ5NmV2NmtVM1ZWVDVDM2M5TmlsZDlCcHM5RjNXZmNseUdPCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE5teHIxaWtLT0ZmRCtQYXJubjAKMW4wNzBoS3RINEpySHlUZTd3OW9QVDM5d0tBbThhaVdVZkxxenhOak95eUJlRkREdkNYZ3pKYkRBbFN6dEllNgo2eS8vekdQUTNkclBGN1dXdEZiTG9CZUpsSERNcTNSVWtyOVpldklLclFyY3poK0ZjVHZ5RmdiUzBBUC9OWEs5CjJ5bDlQTUloWGl2SGZBR3NqN0RQdE15NExPd3p6WnJSeWJHTDJIOUVHb1RUcHNUV1l1L2lwLzdLMVpnbFh0VXYKb0FyN3BFd3VyTFdUL0sraTRjd1FwM2dZcy9Yd0NobVBjTkdxZkVVY25kQmQvVENHb2F1blZLM0VSUVlEd0xySwp1THkweTRVVE1EcTZVam43RGpCckV2b0p2QzJaNUpDL2QyWDRIaFlaWFJCRjBSRHlvYzFNTWI1dkNaVjdOaUpXCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekxpaDVQTmlvREVSQVQxOUl6NGkKTUx3ZFpkdVVwdWdIbFZTbU5RYkhBYmNoZERyZllmUnI1SUlERFFGQ041c0kyR2lCUi8wME9HLy9yQ2JwMFNFcgp6c3kxVW5kcUxSTVhnTUE0RE9LK29Qd2ZqRUNmUjdDeU02ejk1U1pvYmJ6czB6UkZDVFZLbExXM1dlVzVWWHQyCmd3N3k3QnpXZi9JR2J0ZXF6UVZrbzlVd2ZsWkZzeGxrQXVSSHF3ZlVBVUxaQnhLWmhrc09qdTgrbU84dnFiQXQKc2k3UkdpdCtCT1VZcmJ1T3NQUml1VWJCK2JWMXdHMjhpZEVpTlp3UFd1RnRTdzFuMTNza0xBcjBGaTR0dVdpbgo4bEJhaFBhWitNVVNGQkZ3dlpkZFFhcEFDMkN2SDExWVlpbVVoODhEUnhoSW84U2hHcjR3ZWc3dExQcWFPNWpwCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekw2V2FxRUE0bFFwZlNXWjlBbzYKUm1vVnJ2dloxMHNsR1ZNc3JzUGtUVDgzOGtoRVBjZGZpNFFiQ0Q5TmpUTGY4clR0U01tRkcvaW5ucG9ibDBwZQpSLzR4SXk4eEkrRkJ2QjVDNit5Y1R5SHNaeGllMzhyaHh0NDFZQ29TdmEwZ3ZNN3hHU2h3MnJvVWxod3ViUVJOCnpGVjVLY1pXMEtzb2NuL056WVpxRUxVeEs2ZThFcVBQUUJWaFdZMUFKQlBPTGQrZUhsQmUyQXh3SlhsdHBuTFcKbVZEUlZ6MUduVGJDSXpVUXJDSGRhbTFwbVB6dFpVVkdaK2d6NFIwTVpISzRqNGNEdDlOZ0d6TmlNSzgzcVVmaAptWDlOMU5nT3FVR0wyMStNWitnSEFvMFIvZ3kyV3FleXNZNmpmd2JFMTI2bWxxanFma1MxZ3JEZTRTWHJBcGpRCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeSsrMStXcnJQVSs1d2pyUiswQW0KbG91bjZZY0dMZWpsSjRjTklxcXc0VVBPMlRGL3IyRmQxQzlDa01Dalh6RTlLb3RQNGU0eVFCMEJuODhxSUZxOApIdkJiQWhvS0F5cGtzZ1hQWGRrc3d5QjZUSHRkNUcrcXJCdTdhK21Ia2tobW1wUHdmcjBzcVJEOXE3ODN1WlhNCmsraFZBVlhDVXNaNnFlWVBGcllBOStIZkZnNzJuT2YvSHJpcGdNalkxdTBNSFRlQjgvdGp2UlFzdGxGZ0UzMGYKRUdMK043S2ZvT0hQeU9SbnVLaTRMZ01nWXc1bjV1OXFFTTJ1MlJnZGxRVnMvWmVpTlJwOExic0ZkUkgxYzN0aQpOMG5Qd2JGbGtkSU9hZ1hUR3N5QlNqclZGcCtlelhMUklYR29zcUg4U0RsV3pnZmFWOFliM2tySXd0eGRRSTU4ClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmVNSUR3V21sRVhlZEdxUjRqalgKYmFkVnFpSSs3ZkRCQjZmRDdDTC9nSjIyYjVDcWRkc3pxcTIyS3RnWHo1VkxPYy9OY2pUUkxlVmpBVTRuQ1lXaQpldTh0ay9rbk1LbU1NS0pJZ3RMd2M0V1dQajZUZkZPdmlDdE9vNHZrMFJuR0piem41emtNUm9kWlNiY1JLS1ZFCjFUTE5mTWRoY3Zhdk1tNTh2QkgxWm9JUWNkd2I5RmhjWmMyaGRtYTNHdVFUOE5GaE1rMjVKOFdKUGh0R1I4VUEKSUdqaWxlTWdJSlB4Q0FiY243TnNLVXBjRHVHSVo5djZWUVZhOWoyTkh6YkpMbjdBL2lIbkhBakR3dnVEeThodwpUQUFjOWJsN3p3VkhzcmZDT0xHN0lRTjFyZnlKVnJJZXlVRFNtalN3WERPR3orVUNXTkh2U3JzT3NLMWdOOCttCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdElHOUxJSC9sUGE3bUFWbklRYjYKVzRKdFI4ZVFvU21PLzJHNkFJVmI0a1JyZ2Zvekt5dmhUWUlGRkJqQlFtakV0eTVkUFVLUHhGbG9BM2FnVSsvMwpCOUlDTmxRVFhDb1hma3c0WG1sZlUyMmdTcDJVOEs5YzVrcmdxM3dKb3FBUGorVHQ4cTJnbzZLMVFhSTlncy93CmtkSmJBbVNzUnp1ZkJ4ZERqNzdpMXRZQ2JFR002eW1RTVRvWFhVcHN0dS9RTFF5SllXSG5oaWZUcTd3VnU4QXYKMExvOUIvRHY0MUxDWk11TUMwcGpnL2xNZ2FpL1k1alZhRHFGK0pKeFdLcVpVQW1qYUVGd05lM29ZeUszWk1QaQp6N3A3Wkx3Zmk2ZFYweFNDQ1R3VnplL0Z1bk5zbEZ3THhqNk14T1V4Vm9uTU8yWDcrcU1oRVJ3UDRLWEtaNlZDCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNnlkck9qNU9KeFJSdEVrMUVSZGgKOU5NTVlCN0l5S3VaeEIxeVV3RWczVEZPbmdlZ0xRVXdtVWtnQVhrcG9WRC9aWXNOMlArNXR3WGtwNnJFeE9CWgpDMS80ZmUvVHJ2S0toWW9NUzQvb21xRjdmeWZ4YkJKTE91YUkwclpHclNodTJZQ2IwTTdyeFdZV2RlVFgrdDNOCklKM3dVU2pQQUtYUFl6L25QTGhoaUdybVpqZy9WM29oS3JzL1pmSDJvTi9ZR2p2bnp2dFhRbHJqd1NNb2hkWmIKQTF0RktqWmJxMkZ3dDVGSWtIaUo3TENGRzVCNmM4UTF0aE9QZ2o0MENuWGxLdTIvWDUwMDNtNVVVUjJqV2tnTQplaWkzeGRoNzVvTWNqejlPaGN0UDhMVG50dGpkTHVQUmh5c09RQWVKa0ZncUxMbUt1dndNaGllYXhQTU5rNXdHCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0xLNDFXQ1Q0TWdIR1hkMFhMbGMKYkcwR1R5UHlZOXpTWUVHbXBaUVJvNjRaU1R0UFZzN2wwV3JPc1ZGRUx5Z2dkSXdyaEl0L01VOC82bnovSlIwaQp2K0MzZElGRVA1VDRLc1MxYXl5cjE3emdYK3BGT0xBYTBFT05CMDYra3EvMkVXVjVuS0hvV3FLamgzRHdTUzJzCnpiTkl3eGRKNXAvam03dXlTUjB1cW5Ndk5RY0JweGtuNSt6VGYrUGh0YVBuZVpWemhjOGsvTnJlU25ZaDFBbDgKaUdjWDV4TWJxcUtBMkpxSnpDZkIyZlJVU1V6OGE2ODAvYklGNzNpQ1pnUU1pZndqYVF4UlNwZXErQkZZa0lJVQpZbkQveGE4SzllTWV6R2dIcEpKZlJJTklIdWNsenF5OVk4ZjdmdXpPUWpmMlpKT1FPRzcwMnVtUlhpdEMyTUZDCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWJta2NNSkdCVldzOFViLzBhQS8KV2g0WjBFTlUzTkZQLzBrYjI5U2JqMjRGN2V2dGJZWmhVOStxU1dXdWNWQjRMQW85U2tVcGZHNEtWc21KWk05VwpzTFpMdTZPYmQ4dnp6cjJ1Z28zeXdrV0ptdHFKTzUrVmR0T0hDaW5TY091L2Fmd2Y3TzdHR2hkem1yaG1ZZFFhClhSQ21PUExkdEpabWVacG5zN0VyZS9FbXlRTDBBTHIwdFA4UHMzRVFEaGtwR2lwWk5pNVk1VS9tS0RVWndxZGIKL0dvajJzclA0eEVFTWdzN25EenYyR0FlOXhyZWtGazJBMktJWEE2N09WM3VhUTgzZkdudWRTOTBENjBUa1JMTgowUnRlc3BJZHNvVU1XVHhlTkZSVjR3NENuUzVJV2xaZi9hRE11Vm12MlBzeVg3Tmtma0lFVyt1Q0huQTRPWXhjCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlJtb1hFalhQRkx5OWNqZ0ZIa20KbDhqaEhiVEVGa1JIblE0MnpjN0ZwM25pcmNnWG01RWJvai9FRjV3OVJPVWg0OG14TXIrUWRxeVY3NHdXcmRUQQpPN3NLRU9xY29QRmRwWXZvU0RoMmhRRjlqdkJzeFVxY1JqRzFWNVIrc01BaDBHMjRVMzgwT1dJOFZvWDhQbmJoCnR2bmMyelM5eE9abHFXS2Vwcm5Hdmh5TmRIYkdtQmlmSEhIa0tHUDN5UlNjTGtjdW44U2ZVS2p6NUVFaEJVai8KT1ViM0dIZEZ2b2VqOEFBMjNDbWdxTThyRzFxWlFLUGpvcFJJdU9uRnR2T1JhRDFBWjV3ckZuQkNrTDBDVnZFQwoybzdyNmxvS0dFS0wrSjNHYThsZk5CV1h2ZE9HTHB2QlFEUjVsT0FQSFpSVGFURUt3K1BtdTlGZ1ZBS0doUmZDCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjduTXJGVEtnZ3A4WnphTFBGWEIKYWJ4STY3ZTBXaEtLczlRTGpLRHVoTXFCTmNjSFJiTTIyL2tHci91RUNVYVdSb04vT0ErQllGaGRUcWFDVGdZLwpoRVZtVVJDN0RhRzFTVE93blozZTFPRjJBbUJyYi9OOWdvaHZWL3JzeUt4TTgxWEUySFBXVUJVbyszY3RaZ2E3CnJMejNaTlZINksvV09KNS9JREJtZHRPdFhIUVVweU5oY3ptd2pYVmk2TFNjSk1zZUUxRGlaOTRmRldEYnNoMmIKOTFYWVFiTXAvVXRnQWpJQmV2dG94TXExQnRLc2VUa1d5WEhUdXUvbC85Qmx3YnJiai8vaVRnMVJsOVFpdmtBMAo2bWVRdkxmeFhKaFJjU0hueHlMUVNUN2NQNzNJTUlHaWJja1gzSldpVUJwUkcwd2lJWm40NXY1VUJVSU92ODBOCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOVJXN2krZjc4YklXcnNpOFZGSVEKNlloVXphWU1vb1FYQXdWc0xoYkZIRVVCbkcySHdLaXN5UWxpcHRzYTUvMkdyNEJ4bWJKRkdSaXR4RCt4ZVBPTgppRGttRTBJb1BuODlyVnE1T3pObmNFYTlYcG9FZWhiOHVlcVJ4MHdEcWdxR3hmdkpFN09LWkZrNzIzV2tCdGlECmJNN1RRWEsvMFl6Zzg3aFNKbDh0SG52dXVTa3pTYWh4N0ZGeW5keThYenhXM1F5NlA0QzZPN1U5N2RmTUlqMlEKNGtuTTFZNzd2VlNYS0dmUXNNSG0yQndFd0ZPK3NudUNBTzIrZlpKcWIvOEhzajh0cUdSRjZ0MEhSWE9GZGt3dQpkR0x2aFZkdTdrcERoajJwVU4xSS9oN0orb0xydkJBNk9GT1RvREdHNzFIZVg1SW1yczVrZXdkWndIVk52a0drCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejVKdXQ2YmI5NTVOWnZHNGxOS1YKdHF0dG50THc5aGVZRmZDUUF6TElMZUYrSWlKcXBjangxS3VNS0wxVHR6Skp6QWwwU0MrVTRZMERtZlE1UDI1TAp3eldGRFlMd0xVYjJsUzdJcGlsQ0F0YS9IbEZNaWV2RXJZNGVrQTZzY2VhdXNiN1c5TUZqLzUrVDdDYko3YTg1CklvMEZxaDdQWURLbmRjRFh3NG5WOHJFOTlmR1RBOU5uejJwTmx6NlRvZDR3NEcvVktqMEY0bHdtK2JnaXBseWIKM2twdWVJQ1VQcUgyelEvMk9hT2RUUXExY2t3SEx1blBvV3lUL2dVODIxZThZOXNVaVkwVVZHbTQ2b0NuLzQ3QQpOc211U3l1QkxnczFiSTlmOFVyOTBvN3gzUTJFUzJhdE8zcElpdjVqcEFmU2piSzdUTnZXbWF3aUF3ZUNId0F6CjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFBpckNmK1BhdnowTFQ0S2c5NnMKT1VORjZOczVTNWdkc3N4TnIwMVlTTVlsQWl3QU9oR1o0Z28xS3hCNjU2TTRmc3FaZE8zN3FDaGZESFNEUVUzdgpZZG1Qd0NqUFJhY2swaGdzRTFFTVJVREh4Tk9lMHRTaWlBYTc0QXlQd3NTZk51K2JTbHRZLytLR0U2azZwKzJmCktGYUVLZTkrUW1iOWp4cGdBZTB3emkrbVRxamVOOGhTUllQUDVEbW5QbkV0R2hEQmREdTE5V3BLMjZqeGQ1Z3AKYm15eldrVE84V3hZaWpQcit0RnJYcjZiMjk4b2tRK2ZMZGdycVhWWThpQzd6ZTFRbTNUVjJzc0RTMFVUeFZDZApjZVBOYkRVeGludkt3NmNBTnFTbGxjZEJUaURDR04yRHRJOVBNdlIxMHNoNlNwLzRyWHhQcGRpYm9GREliZk9CCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTJLV2pEVlI2SnFBM1MveUQ0aDEKaWMxMURQR2dSaUY3MVFkNjZGS0gyMlRrQ1pqY09XaVVaWnp6Y20yZ0YzVUNCNlJycndXZnpBVFU4cjEwQVliWApZa0s3UUF0b3U3TUVveVI2V3c0T1BWNWlaVVUyQTc5NmdXK3g1ZmtiL1RsSzhwQ2NQWXk2NGFRTk9VeGlCemY0Cmg5VjJ6bE14MmYzcWxtTW1ueGRSRGk4MHVhbGpHbEFLdk5yQ1dBb3FJV0RlVWV0c3poZDVYRmZlZnZEcU5oSmMKMnh2MGt6SWhmQW90OFRkUEpYK0JINjNPZEsxZS80Vm43cndqU3IxbVo2MXQ3MVZmazM1eENJY3RJNGtnWVR3RQpPNllKQVN2OU92blNzM0ljQzVHYVB5ZEoxNE15bVRXZ3RSV1BlNzR3Qm44V0ExQVpMUk9TWFZKY0JYdTYvVUE0Ck1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUhtNTFvUElVTjd4Zk1xclNKYmoKd1JMZnlHYVFISFIzQk9oMXJPTnhCWVI0VkwvY1kwdGxSMVdEdXpYTk9QVXlhczdEVU9pVkJlVnIwUG9uVEJzZQpqSGNGbmhaT1Y3aUpldnNEOFZGTWdKaFFhL2RrVFJ6d2o1MUFqWVh0YzNneCtGeWVId2h4eVZUMXZGaEt2ZW42CmhkNGRKMlFIYm9HRmpmQldKdk9zVnM3WG04NlpzSTlMNVN1SkNCWkFmcy95aElWMC8yQi9sWjVSVUQ5TlF0NWUKcnlrR2ZnbDJSUXd0Rlh3S3VuOEhUUHhnOWRRaHdkTGNWRkpmZU0xcXNoTk41dW15ZmVrN0U5N1Y2c2JoSElEUgovaDFhaDJRT1RIcHRzcElRMWJQK05tWUVNS3BQYmxscFJSVStNZ21PanEzWWNoRTZyM05LTlNRSzFwQlVvU1E4CkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGtSdmpBQ28xUnBtUllzNXBpY00KbTR5a3pXMjVKUnN2eU5vNHl1YTFQeEV5N3ZPMU5DWHVBTFlJMG16ZUxDQ1FEMFVvcVFSVkhMalpkczNRbFBGagpodHVlKzNkdGtFUWxQcVZUa1N4N2lTNlhJZVJYVWE2c2RRUU40U2xlYWxtdU5QTVY4dFpwVzNDWHRMZXdURERXCjh0M1VhRmRvZVk5aHV3eDhldmJ3SXFqT0N4YUpQc2lsOHhQY2xxT3BQcmZ3VzF6MXZoeGIzWHJoVEVIWE9aZGUKeHBrUVRaM3E2dFJ0aFNKbzlPSlZNYk5rVVVsajFCWFZXMWUyTE94cElaam0zV0d1NE5lRFRxcEVzQUg4SnZ4OApIK2s5N0IrZWI5UFVTckpxOVJocG4rNnBNT3BQVUFnSUFNZjh4MGwxVkdKcHVlQzJZOXlQdFZnNUpBRXJBdGRKClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2JjM2lMaGNSR2p2Sm9qa2d3VXkKdEg3cWlYWE9VdmdIbFZJRFR3dlBaZzFVWWc4WDZYbjZXcjA5WkhiWWtBUUEwdjBDcE1pV2FZVHdraTZKdXppNQo2UDFtV2l2QVhPZGNiVEpXNXo0Z2lBa3BORXJMeGdpNWd1QjRrVnpZQXppWGkxckRScVhXNG9KRS9MTEY5b3ZCCnR3NGp6Q0ZvZUxlaWEwRHVjdjU0S3prTGplMGhUcjIzWXRISGJzTjlSOXlyMzMvbjJFQXJmUS93SERGUGV1L00KMmI4ekNITFpDempmWW14anBaa0trRGp3YVRJVlppVkFscFRGdHBrVkNOcENxQUxSQ0xVU2VpakxEMFpTYStqNgo5NUlsc0xNLzdtUzcxWFhLUHZZM1orLzVVR0R5TjVrOWtSeEp5dE1yVVc0ejgzZUFmbEliYWQ1dGI2ZCtjN054Cmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTdwU1dLS3cwVE5DVnNPeHZEZ0kKTmEyM1NBQ1hYTFViS1dEMkRwTmVnTFptUW9tcWRWenNFSys0YXBUVXNEY3MzcVRQQzlWUTMrQS9xRFpoc3ljQwpMVENIVmViMTV3YnFoaFBSWXpKQzVpMU9Nc2FqSlZSVThsbnJ0SFNGa2U0eTcwYWIvZndEUmhjQVVTVmQrL1hFCkJlc0NMbkdOMlFscUdVRFJBQXk2Q3hHTTV3dFpiVVM2QW5MZzFSYVFkUTNwcVc0bmFER0xIaC9VUU1XY25uSmsKRy9xSTMraHZSODdXTGRZMlMxVmwyRU1KY0JKQVl3ZjJEOEt5bllmYzJ5eTFpSnora0tPY1FFUjlIVjhhejF3cgp1Y3dUMXN1ZjFEUktnZHA3SGVmaytLOERPaTFLeDZvcVh4ZUI5dlBhSWFRQmcwdElEMlVlNEtZdGlTRndlYXZuCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVRsMitzUWJBZmVJeStWZk0vK0wKSnJadVRIRkk4RkZjcmVqejcweEJ3Y1NsdlZwa2VyVTZEWi9DdnY2RmZzVnB3a3ZqdTNHMGRyQWRIdU1JRVFKMwpjWnY4cDFFM3pRRFEzMlZpUHhjTnpkKythQ2U0eUdUN3B6WGxnd0tla0IwMjkvVjdHY3BtVUpOTlpGcWhtTkx6CnUwNUZuZUt6ZlAydUZMakhOSFc2V1UxSDBWd3pYRUtLeEdYTkpKRjRSRDNTUWZSS1ZyMnpxc2Q1Q1N4dUdtMVAKL3JTY1VXYVROdUFPMEI2d1p0RG9QUmlQbTIrMEFEVjlmcWJUc2FEQkpseEo5ZlZWVm9MTVBIK2pCLzhVeE9EYwpYRDlETjF6a3ZEeEJLM2ZGeVRSeU5TbzNIN2tmRVgyNnhXU01EcWRRcS9oM3BPM3Y0SWxwSStrdkp1OUE1QUtMClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGRoS0RRdEtMaG5GRGN3T1VlcUwKTGpwSERzTFFMYXpIdS85dVR5NW9CWGgvZzhCTS9MMXVQNTNNYW4xck5SazdwM3dDd3F2ZkM4QjF6SUUrU1V4cwp2U2tpZmZNeTdEYzVma0tBWTBQL3YxbExONDhub1N1K3Z6cm1USHhubXhNd3hsTkY2Zlg5enZlZlRIbmR3RVdFCkxDcFZmcVFNaURta3BYSUtaMWtqd2RXalZTWW1WNDhUVGhCdUpLUnFlVzRncndmbUF2d2JhSzQ1aFY1aGVxWXMKMkFrU3oxM0VmRDYvNENNUzlNV2poWUJkQVZVLzAvclk1amZCa0tsRk1zTjYrOTZtTThyK2U3TXVEWXZFK0tzZgovMGl5d2YrRWRmUmN0TDZnbldJMmpCZHlSRXlJQyswbDgwd2tRN0RLYlJHeXkrdVQyeThKbXdQeFpiNU5acWFTCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmFrTGlqMm91RUhJU1l1UmFVSisKaW5udEU2aTVvemZuYVRmMnJPcHZvclhabXpaOU4zNGlZY0o3d1RaN01NRTJlRHplV09qL29JL1ppQWtVMzh0bwo2ci9wZlUrd1VkY0ZuOGhyWFJuaGtGS3YxRGlBNUNrUVR1RnpySTdsZWlRaDUrdEYwSGhBSU9PTWRsT0hsSjMrCmFZZlJoQ094VlFXV1NCZExCaGFjZlZSYlhMSXlHSGtzRGRRNEdDTHpQM09uTkU2UGhUSktJWHFkVWZ1WGtkVXoKb1pORlZobGFkOFl2b2Fwd1pUZGd0WkloamM3RUYvbHo5bGJ5RXpzZ3FrVnB0SUdiSnhpNU1uRlBRa2ZxMjhITwpueXVOa0o0aHFtVCtXdEIrUERGbitZOHJHeHZMTGMzRVgwSVdmWkw0QzRvWDMvWmZFMndMWW9TSHc1eGNEQXdiCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2NFWmMvd2EyMUlxMzZSZnUzY0kKYVY1ZDdRYXJwalA2K1JlZFVrcUhpc3kxYy9rL2FOZTJJdTJiM1o2N0pab0Q4VnZKeEJiRHViYmJpdFBRQWR4NAp3RFN3Sk8yWUs5OFJJMDJ2Wmw2aDFrdUpGK3NweGdobjYvcDBKVDZNdFZIV0N6cjFSbHQreUIwUkNuRW1ZRzl3CldlS09iSFdFMTcvSnR2Vk1Fak9DTnNwSENiN0p5TlV6aVpNVk0xMWhBWE9iazhqV2FVdFdYd3dGS0RiUHFJUmwKeWZidzBFd3BVMnFLSjJIVjBkSHFPTUVVNlNUMVp0Wk0vdmgxd2FWSFF3QTgzbXJCbXRrTkxibCs3VlRsSlZ1OAoxQUYrdzlrUUNBS2o0MHp6TkJaU0R0NDJTRHc5VWVrbDV5YnowQmFTTTVUcEREbCsrRGk0UXkxRUtERkNEekRlCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFFOam4wNFB5MDhNY3lLZ0pGNDgKeTJ5ZUxtbmw5WGpMaXV6U2tYR1pBSklPY2ZMdGFCT015blFBSUx6WkwzTzgrTUFBK3pQTUJ3QnFlMm1MRkVXNQpVaUFEbWRJdWlESmRUTVJKcHhqVWFJTW5JN0hSYzVkY25Jd25aNGt4MHJKU0FzdmNhMEtoRTNoSUpaOUJrT1hjCjFKUFY5VnhVVE1GTHlDZjhrVUNJRU5BVHFESldESXh4VWlmM2JOV2VTUnVFaTNoU1N2SGxyZ1ppa0tCM3NKWkYKM3pvR2hkN2l5bHJUWnR0WjVhZGxPQTEwNG5lbWJJN2NQL0Y1TGM1akVnYVdnY2hueHVpaVA0RElmbnJCc0JNSQpXVU5GZE1UUTAwVFFQajVYTUo3aUJzV2h2aHE4RmVNR2FiMkFnMzFaa0d0WnVVcWRFRktubjJlSTVUd2p4YXFMCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXBaTjBsZFFGSEV1QmpHNlN6bnkKaXdHUWIzN0ZqMHV5VXBPNlpRbGRXR2ZScmlZdkJxUXg5V1JKc3pNR2IvcnlUQVlWTHpicWtkQ1FRaGljc3MwdgpVU0daeERDQ1pKM3dMK05oc1NUeGFYcmVkU1V2cW0yTkF3VVd5SU82Ump4RFU1TVZyeml0UDBLMW9qMnVRd3Y2Ckxsd09WRWpaTEN3R0dEcTFxNFFpSENqbjh2UlVNVys1dEVaZmdXdzVoZ3lXaGQrYUt2Z09nckpFckNaakFDSTEKZTU3cTJ0d1g3YThXTitpNTdkYVhkYUpWc3hUSHdIQzdPN0tuZDEzK2t1QnlTNUFYQ1k0TVBtbEdPRnUrU1ZhRQpidnZmTVNIa2krMmtwMjJTN2MweWhhRzh1bWlHMUVjeE45SExsSUdzQVJyMFAwSW1JRFc5Rzk2WVVMVDF6NEE2CnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnRBVzlDa1NQQkdhc25xSHhVR2kKdldlV0NwcWV5Ykt6SEpic2NOSG9zbENONlVKLytVeldtUUF2WC85ZU9xSHZuVVBkZHhSdk5nOVZVV1JDc2pOOApoSzdaMk1IWFNKYWM5RTB4VDJSSWo5bVZLU0huSi9CdFRiNU9aOXRXNGdMVkRJYzhFR1lQNk90TEgvVHArYzQ5CjVtdFRNVkdUd3VPWmhJMEkxZHFkNDA0Tmo0Rm4wMVYvZHhXcFZqNGRRRVl1TnRJWVBiMEFxL0N3YmZIOXd3ZHQKa1R1ajVEUEN0dlZlSlBNQU14dU1vUXZGUktTMGdlZnNZT3hPVTJNaXBGZDVYVUlwa0NiRmVXb3RyM2FyZm1NSQpGZWtDVEpiQWxZSmEvMHVEQ3dYL1dLY016TmNxdE9UODNHNS9jWmhpeHp1a2hnTUQ5UzVWU2U2ZHRqV1M0enRxClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDkvNFRjR0VULzNTeFFmeWpiQ3IKbUhkY1pKUEVxOXpkcVlGeTZtbmZQL3h5VkdxNE40cFcwS05aSG9MRW54STVxWHlDa0tkQVdXeitoNUlwTSt0VAo3Yndwb1pVZkQ2Qk02TDNjaUZXQW5JQXlMb2p3ekpwV1l0QnE2dk9DSzFTWVpxb0svQ1JUeHdVOGZsWXBnSjUwCk53blVHOCtoWHE5R2prVzhjaXlMK25zT2hTT1VUS3diamoyV3BQUmcvazA4WTluZ0lGeU5JMTZ0aVdERkpFUVQKUkVBS29HREFHY2VEdENoTDEyQVBlclJ3Wm9uSDVWazhlVTR6VENzMXVTVjBVdjY1T29QUlBoRUlDTzFqWHhKNwoxbzNRMzFIdmpaOXJwZ2wvQnBRRTVnYVB2eE8rNS85dFpTWVdnaTM4YldQSzFLalFBSVhjKzRrbG42NjdaVVN5CmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0dEemwxQUhpZEpUL1lWVy9Xb3YKMmcxcDZpakk4S0VGVGZ4YU9PKytXTzE2eUhCbnB6RjB1VGFMd2VnTmRKRW5ETExDVnhVbVZtTDlxMDV5RWVLRgpweXAzWktnZFJDeXVZU2lxWHZFWG9jYnlzVUtaZFJ0Mm9DTmMweEg0U0Zrak1yT2MvbzZTK3VVNzNxSmMybWtoCmpCTm00aGlnUWxLdnBXSm1OR2ZDZWtkclBiZEN5M2hjdlhEMEw4S3hSenk5R3BMSERrVlI5WndLeVVSNmExdzYKSUZNZDBtU2ZuWnZBK1llMjU0Ymxub2FZMWxySElFR3BpbGhFQUxCTU94RHdDMnI0Q1o1RXVtOVFYVk1majVvTgp3aU5IMFViN3dCb1ZVdmxlS1ppdjMxbmtuY0VhcURsMXBlTDIrNng3S3NNdi92cEVpenBFMWpOZWd4eFdFZDlCCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVNSNUl2K2pXbE4zNEw0NGw0N1gKOVg5S3o2ZTBsck9pbkFVcEJuSUpTbHdhMnliQTd3OEM1RlhFSmdQQXBvcEZlYVBoY3k4UDNMdjM0bWo4NDI2bApIR01lbGZlaW9qelNqZkJTcjJhOEplZnFzWk9tbzR1YTBOMFlEVDRZZU9IZTErL1dwRW5EemRCcERtQXdZU0NtCld2VkNPdk5UY1RYWEowVmpOTWtsalJtUGdEUjc2QWNuSDNYUndJNlJab3F1dGdBSmJxcGs3dlEwM1VjWGZKT3EKb01qM1J5R1RPTTFEVWNLem8rekhWcVJMcjFLVEJrWjhKRzJvbFRZeXpUYVlRdXZKdGdYZ0RQYUFsVGJ0bHgrNApsbExrcEsxYXpSanh0ekVzallNMVZxMFVrZ1UyakNSUlMzUmtkb2RPSFJCNWMzNzBHT25HcStBWTVJcFdRTlNTCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdllVTzl0Z1hObHpGa3RBZ1lyS2YKa2o4UFJad29Yek9va0ZsOGhGMjZWaXVTR3N2RUE2RWNObnNaaXdvcFFwaC8wL050bnlvK05kRGhaS1VuYjlNLwpnbHB1VytDaU5TQjV6V09scWFXeVJhdnFPaDYwK0h3ZVNveG9UZXNBYlJCZXBjbCtNQ0FNK0pyQmR4T3d5TUtxCnZpd2hUYWxNQnhpd2h6a1NSVEVjSHRHZW1zYXR5YjFjc0VRbGdOMFRSNDFmbkloMURvMjBuallEWFJma1dVUEgKWndmN0Qvdm8rNUk3SFhqc3ZXSXdvZHNIeGdPQUhzTWFTanFqcm50bkVsMjhvV0dWR1RncWNKRE5LNlJ0YVljNgprK2YzWGZoMG1KQXM0YmU5Q29uRlBuMVZYQTJzZ1VGL2taUWV3Sk5ZVkFkdm9IZmtuYkVXcXZXdUo0UzRGaWNhCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTR0ei90Z3ltWjNzYmhxQXZzYTQKRGZsaHVmSFp0WjU0K0NDQmFRbDUwM1gwTDJxTm9EYVJGZFhhSElkSkRFRUNpQkRneWdzQWdENURmamdKMFJHeQo2QzBRdmtXR3ZoTHl5eFFOeGYvSHVLTTVxeFpQKzcwcXRBTU1kN3FaK2Z1amNWeUZ6Qm0vc096eWh0cnRDL2pJClgzRjJ6cHAvdEVudjRFZ0U1aG5mK1lnZ2laUkduRWpKelRSNVd2NWgrWGdoT2ZFaGoyR0dLVHJGekRuTnh5R2cKbFNYYzEwSVVhTFp3T2RTM0hlNllkb0tucW1WSUU1RHNDbnJ5NUhyZE43Z1FSZENraXZ1STVwNFFnb2lIZmpRcQp0VUMrTDZlNjViMzNwVDVrMjEvWWk2aWN3WmpxRW82T3FKTFZVNDVHbmVieEwxb2JBVno1Q1hNaHJHd2YzVjdFCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmZRZjVjbHhkSHNmeFYzdVNuQ2wKWU1WSTJ6UGdsM2VHWStqZmdQS21vRzFkMmY2NmM4YjBEZ2N3bGJ5RHlvQ0xlN01zWG9kUkdKYkF1eWgrczYxeQpzUERnTFpjQUF3NU11d2QwVzdtR2kyNWpnSm1kaFZDYzFITnBXUzZKeTIwblJiYm9EWjIzZldRMHl5SmFDYW1vCkxqRVZYd1pBd2MvT0VtSUdPZUNDMHhwVXY5c2lJTUZqTzZnbnNIZEFpM0xzdGhucG5uTkUwUlVnTjd5UmhycEoKNnB0RWZPdmROWXhQZ05HVUpTNjlqWDN3dnRDaDhQNSsxQURqZ3JEcVVoWmJYbUU3cDFUWnVuUUpWcVc4bkQwYgowQWdBUU5WWFZmYzNEWGZvdTBuQ09hYTM5ZUdRb01icEgrUWd5SlJka3l4eWxxWmxUNzJBUUFiSTBLaHUwWERGCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1RqaURTWWk1RnYyODI0TXF2T3AKcjAxSFVpamR0aWh5Z2lUc1pXSXFESVArc1BORVFVSW9OTlEzWXJJQllZU3pzVXZvMUVjZGxYdDdZU1NPcGVQegpXZGJ3VllNRHdnd0lITjM3MFN4S3BXL0FmNkNpRGVpemlaQUdib2txbklUVFRXQ1c1YUI0Q2ZsRnhmUUY4NlpaCm4wY3RlWTBpWDBHUTh4VHpmVGNFQW1MczMxVlBhN2trRk8xdEJyeUpKQXhMeU93ZFRQYSt2Y1Z5c1NsRWlIaTkKQi9pWjBIRm9EU0NWcDdBUHEyUktVU1RxQ0N2ZUE2RXYyRU13S1JJNUkxR2NveVRKR0R1NDlyVHl6alkyY3habApHRHhHcnJkdG9iS2NESTkvV2V6M1MzQnlmek9UbE4xZTQ0NHYyRm9lUDNleWEraHMxNFZTVGh2aTVzY1ArT2pFCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWttc3ljNC9PdFBZLzRKdElOSGIKNEFucXE0eS9rM3hJaGlwSnVyZDdaQmR1ZkVuMzQra0paNXNCVXJDZU1PdGZYek9VdjJzbmg0dENjZGhvQzhOdwppeFgvUVl0c0lPV2owT3c5UG56alY2NEVFWHJNczdrQVZPbGRMeGdrYk9rRkFUdTRrRWtrR3B5cWxucWRBOUl1CldMKzkyMmJibTd1T01XMXN6ZnNZNWVzT2JxSlVUeHY4MVYzeFY5ZEQwd0V2ejJMWjFOcXB0ck1KcWV4UExoK2gKUElDelhESjkvVU1rVVplbXBpU2Q3S3dQYWd1cDhiZlVaSEZlK1dVbm1TZHhiUDVEQ1hRUkh5QiszSDZva2Y5WAo5Nzd4Q3B1UEpuVC9pT0luVTJmaDlxTm9QYTM0aFFBY0txck1mR3FhSXE1K2F3NUFjVE5pWVo0ckp3N2luRDFaCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWtzWUJOdjhyNzVucHY5a3hTL3oKSkkrY0R3VUpYa2VEN2ozbXFxQmt3cXZpaDNyMkQ0b2dNS0c3RWpONi8zUkdPMlVhS1hSVlkzYXJtRENXZmNUMwppdTlZUHNtcTFVU3ZhMGlnU253WGZVZ3J6eUxjVWZqK2YrLzcxOXBBWkh0WWg5MXFzZWVlekdNZGpOcExWOS9QCkFRaisrdmNmVWppVG5uWm9YSUQwMnJTQkEvdlpldHlLQmJWMzNDTk10T2JBdzliNmYxVWxrdnB3NVpmVUQ0STgKd2QwTG8veFNnM3hTajFadENuSVVEUllZOVExZEtNTUJGczVOQ3Z6a2VTY1NhdzRRTWlOWU01Y1VuMDc3eUxYMgpPVENhakV1UUl6TzFMRXlXRnV2bTZzcDk3ZW44UXZCQm90YUVTcnNVV2pCc2tVUEt0RndwaUxrNzlYbHczeU5rCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDFHSTFhMk5WWXVsaS9kSjFZNGwKbHFubHQyd0ZHOFRIKzYwVTNIZUJUcjlEMWkzeWRNZ0RCVGV3Rml3ZnhrUGVNVkk0UVIwQ28zNUpPSEtXdndCYgp4NGdCYUxEazFMdGxrc2NDRlBXWUNReGd1aWpVa3JuQWxlbkl5UkpzMWk3MDV0S2pKQjUyYnAzbnJiSDVsbkJaCkpkK0FMMXlZdkVsVmJSVm82VHhjOW1KVjhmZkV3aHErT28wMVNKV2JNbUludW4rV1RTd1dWY29FazM4Y1FTSE4KaGwxcUVsbCtZbTQzRFpxRGdyZGc0MVJ0S3BWL2RzeVNyRmJkd2orK2Q1WGRDK29Oa2FWWnVJcHp1U3NBbjZTdQpQb01CWEk3OWRGV1RiNFJtNFBTRlljYVZNZmFndFFkTjdRRi9RbEZwV1h0REt1MEExa3dTam5KS2RyRElNaXV3Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2g4SXJIK3dMUXkzdEoxektpbFYKU2dWVnQzM0ZVYTRjb1Z1aXo0UnA0aVgrSkpwWXFpSnpDazhnWE9qRzIxdUZvWC9SU2JEWEFQSmlZeWc0R3pMTgpYVnlsUWRvNnZta2k0bVRhSkJ6Z0c4Z3gxYnBocTNxcDRjRy9RUUdoUk1hWlRFb1NXTUhzMERyeityR1draXhICkU2cE5heWtoZW5LeXBuSDRITTEzWlAvU2JVQlRUcGJXR2s3aTRLNHdZVjN2Mm9wUTkrWXRWcGVHbHo1bGNHbWgKZ3FVbFZTZE44VzBZTzIzcTltVmJQaUxaL0FlSW8wd0kwM2p5NDhOdmxkblI4UUFQeHl4SUhnR0VJUCt2OGwvTQpYVm9BV2NZN3Y3MTdvbzhKYUNDNmg5ak0zdXhuRG1uUnNrOGJ2OElaRXVQd2ZreTY2OEpyU0FjTG9zNWpnUnRlCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeER4MklMY0V6ZUttWEdDRmlvNzkKUFdyS09Bc0NVaDkxN3B5UHhVTnEwQWxhbldNZnVsOERneXNVQXdhcmVIeWt5U1FJTjJoMXBDb1ZNR0JDWmVOaQpIWFgrV2RadkQ0MVZjZjREM0MxbllVeHc5cFBiMnBaSDVBSVZZbWprSjVVRWtOVGd5UlF4YXVqdGdtUWgyTWhUCkliUVdURzFLMW5ZNytDcHlpUWNuMzlTTzFMeS9DbDBSdWVQbzVxN0JEc0VpZGsrTTVIVXNwdm9LeXdDZTNXa0kKbTMrNDFONnZZMFgrVVNlSEVBNWFhNEUvRW9mUW44R3ZzL2VrSytESDZPT2lmdDdVaEJ2dHZGZS8raDRxMVpUeApyKzd4WGpyUE5CVGhXaUdDVVpRRGJic3NMRTFTTlNraDdQS1dvNDlpclRCbldtTGZsVE5TYnMzaVYrUkl4MVFOCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTAwTkpPVGdZbVpsMDFJMTVTU3UKTjZJMWhXTW00WWMzMjdhcWNMVTR2Rk40b21WbXZ0SWY2SFZjWldRQ3lpNko2RXBqOHp3QnFCUDZ2N0N5TUw2Ugo3bDdqVmpURm9BOW5acGpNUTlLMWZWVXJ6Y0FWdWdyL0d5ZjRTTG5Jb3MyeXpwVVZnT0ZnTXJpTGtra1ZkWE9ZCng1bUR0ZlBwWWYyMkhXTXRvQitIOVl1Mjkyb012RWhrTnF2U05Zb05QRDdpNGIzUFBlOUxVRWRPWFQvZ3dkM3EKU1prUk5JZ3M5M3ViNWpqM0dUdytnUU5MdEh4VXhENHA0Uk5JM0RaYXFJTEM4WXRVV0FlcTFhM0ZpRG9SckRNUgo2Mkw5TmIxcDNFSmhTMWVoMmFVQ2tLSmVqQzIxUzhQRzFJTFBNZlJkU21YQW1wZm5PbmNMbHZYUExLaWU4MitDCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXdZM1I2bHRDNG5nNGN2anZ6eUQKVDgwYXZPai9xSjBYaFVSd3VUWE1LdEJmcDZmdUxldDF3elNJelI0SU02Yjh2VytpYzcxbVdtMkc5TDFPZERnUwpzSmhRK1JLL0ZtUHdXMmVmUHM0dTFVc2FSMVlhM3hqczVHRittQlpXUW50MzJxQ1hJWUZqUkNpRmZUQUwwOXNDCkVBM0ZNT2xXbVNKQStMNzQvd21Edno2bWVHOGdoZUVteHJSQzFaL1VWdmJWcXV4SUFtV05Kcjh3VEwvTFRGamUKSjlkNmNnTG5FVWNGVnR5bHpna3FscnlvVUNDSStiZFVYN05IeXRyWDBQV25iVzBneVptd1NKUURDaU5saGVLKwpLN1NqYVBSNWF4U3FZb1FmdGZGMk12T1V3ZzhOZmZZWnVEcnZjQVJvRitPMjZ1V1VpUDVQZVBaNFZXVmtXSEZSCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMENVQ0ltdG9qRG0vbUpXOTBUMloKc2daNkM1UW5RdnZybUh4R0YzL0ZUTjIzbFhkRHlVcHo2aGxEcENKNG9wZnNzbGtUd1Z1bHU2V2wxU2tTb0NJagpRNkI4Q21WQVBqL2pNWk9XLzFTbXBFbjJaeTJhUCt1SHdtQlFCYlIwclNURmhGVXlXWUllQThwQ3ZURER4N1BQCjRDYVRNUzBzRllNY3V5MGhJUVhxeHdEU0lId25rWmxSQVlhaWM0YjFKVllZWXNqNklrRHIreU52dnZPcmpwZmMKcnl4cnl6ZmR5alVnRUtSODRvRUp5MXdGN2tFU24xOHpvTXdOTFNjRWpLN0FocXhXWEM3YkJPWGVia1lIMTh1MwpiSHIycCtRUHV2MTRIN0hRREdpMmNaT3p6aUptSGhHTnRHT3Z6WFIwMlFxU2NzdzlFa2pZWHBmT0dhblZtZVplCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWxVejZ6VnhYOS9nSG8zbG5WUWwKSHM2TEI2RnFsc3R0dlV3N1Foa0M4RTk3T3RHT09nU1NWOGJlYmZ1VXFkQlBYdTUwQkZZbXVacDE5L2xBQ3VTdQpEUzcrbWZNM3VmOFZ0NDJhWkQwOUw0K05zeEpNUUpJdW1ENjhUMkphaUZzS01uQ2dtRmN3alZIMmdnVW0rYlFSCnB0MGc1eDVtRnI4cUJ3Um1PcmF3V2phTi9VamdONURWVERqYTYyaFlGTFJhNFk5WkF1T3psaFB6WGJVWXlyMDEKSGY4Wi9scEtpdVY2QnJObml6ZHArVUFGaWtFbEg4TGJ2dXE0cWVjeUlVdlZURHErZUhocFBpc0lpN3JpVk5HYQpMRlVyazJPZEpLL0VnS1pnN0tEQjBHU1FkalRRSE4xZ1RxNkpWcCt5T2MyY0FWM1d3NUZoeU1LVGhqYm1lRzdsCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBa1ROZFJ0ejdIUW1KTTNZNHV1R2UKdUllRUVuUDFGQU10VW8wcnRCb0NkQ2syM2p5SkxnUVlFNnp2VzR1bDdFWEZRaXUvTFpaajRTY1JhMjJBOHFqawpwQnBuZXpCYlVCVHErVSsrT1lHWm5pNld5VjgvRDhFVjVqM0N2ejVTZW9PMFpSTE9jWVJ3NTJmVlc4Y1g4ZEJ5CjBUMEFqbDZZamdLUk9LN1ZyR0J0SmtBd0NNU3JTcjVBWHoyQXBka1ozMTZTUVFHUUE0NVBnbVIzMmc0VDBSd0EKUzJ3WEtob01SWnZMc2J5ODY1VVhpNmh3MThwUkFrV2RoK1F2TS9JZlplRjZmVzJ5UFpzeUVDUHdxdUJCMW5taApjbVBpdHEyN1VQMTM1dHhUcVBhZ1NteXE2V0phZ1lmdUZleDlrdzhaTnBFYU43WGdEQ01zcC9UMnI5VVIzdFVmCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd253WUtnZldiU2c3QVQwOUx6aGEKbmFzME9vVE1JV1ZUSHFReEJKUzVDUlNEY1M1YjhwcDYxczM2YUg2UlZEM3phS2p1SlJuTEM2ZlpxWDRLVWdBTwoyZjFYRENiL3JsSFVDWWR4K21HZDVkelNLV3pRZXBGM3JmYjRKWGV2c3RrcUhld0lPOTExdWpyMHJkL1ZWbUNCCjZpOFpKWmpsQms3MktkZC9SWkxzN0kyUW15aHU3ZWE0T1RPZjZZWWp2YkNwSEpSWFdwYWJmek1oeGF5cWUreFoKbUk4RHNYOHpZZ2NldDJDVFlLNTRwNDV3ZFZmcCtaS3M4a2hJaVlrOVBNNlY1TnBpWi9qY3JDM05IeEd6VzZsMQowRWZTdnF3bWRlZHp0VTRRblRNM2Y5MXFKbFlnZUlZS3dZZ3JHTVZpbkhHdEd3bm9MOVRSZWVxOHFrZVR6UXV3CkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOTB5bVZ0ZUZDWGN0cmFnenJEV3QKUlF1Z0gyTlhUZXVEWE50cjM4SGFuamtJSklEMTJNd1N3VmU1bHBPeDhXUHRiMzBxMTVsSjlURVBZODBydTdhbwplaUNkbERMbnRVeFRNdjBEa2g5OG9KVmtrVFpHeUpZUEhJWkp1cnZxbHFPNjlVVDNVQlRVQmlGbWJ3Zk1lSmdQCmtBQjVQNm1LMENGUFZBbmFCbW8vNGFFT1ByTFp0SGw5YUkwNWxYUVM0V2JHcjJrbFVCdGlIWFhiaXEyUDVWdEMKVnc5S2Iwd0ZiVXFxTkQ4b0hneW5xYTFwdWNpbkdiYnEvRWVTQkttckJnb1pDVFpIM1lBa3N1YzhCMDJUUVBqUgpOaGMyMkNGcnkxU3dQT01VakxQUDQ3ODN1TnJFQWhyamx5QTFlN0NKYldTUks0eXNuNGE3ZURnZThWbHYzUEFSCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTlNenNDNGpsam9jM0F3aG5zUGoKL29IN3V4STc3VEhNL1djdlRMZXJLWmxPUW42dWd1L2R6bURzU2VmbkRobTNTWmt5bUJVZjJsUGZMSDdQRWhwdwpOQWp0ZGZpUGQ3OHZwNitFbGJ4NSs2SnNETE82RHh4VGFleFVnTXlmT251Z29oRlJuNkpHVFhRZDkyRExrRjMyCmNUNHp0ME0rQXFaN213NEFHNmtISFhZcGNPYjVmWVk3OEpjcGh1QjZZK2RucVp2VmtaY014aWhzaG1vUmlQM0oKNGlhemZ3bUVqK3UyVXFoT3RBaitSMm5nSitOZTQvR0pSdGNCSU9FdktkRVIvcFVybGZnSlI3RDFaaTVOdmJkRQozUXovR0lSdFFHeGFGdy9XREtaQ2xmSmQ3UkVRTk5PbzdEVUF1UWdtWEhXYVZpNmYxckV3TGd2bks0bHUyYTRaCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVpBaWh3NEpXOTA3YjJKbGRFa3YKUnRXcmpUcEx5dmdaOEJjR1k0UnV1TEV3cmJJSWlwR1VPVGZQeENKWm4vL0NLOEV1THBKdjk2ckVyRWhpSTI4Swo5NVpYQ25XT1BGenYwUjUvSFI1MWNjWU1mQ1JJR2JLcEZucTQ5Q3dwWEhnRDBLbHlUSzBnMlV1aHVWNU4wZlVpCnNaNzlQNkFteW9JVTdHeVVJb2dCcTNJZDRqR1FQaFg2RHBCeUwvYUZXMVkvdWpOL0lSbTlhbHc0MUpOU2NBc2oKQ1lWTjZVZXU5Y0thUlc1Z29nOFJWUmd6ZEpzbERNVDRtUktLNDMySEtKWDlYMDNDb2xQVXpLU1FWOU4wM0s3UgpmcHc3NCtkNHBkWndVRnZSMmdQdFJSRDhFcTljQlVDMjRrM1BGbjNxQWZob0MxeU55aDgrYmZ5cmNKdnJadkNBCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3diVkRmSkpJa2MxVkZERU0ycHcKMTBveU52ejRIUlJxWFFtRlliVzhVcXNpMlBtUEdSbExsbzNQWC9RZHU2SjBaSmFXYkdwT3QzL3ZSdzBNaHg1dQp6Mk52ZWgxMmRlSm1EaitVYy9BZVBpbjNwbVFHT25BdGpMUzVaN2ZNdXZIOWJQNXM0VEVIWXJyZlpjVExGT29rClA0TStQLzF3c1l2clhBV0xTd0NySGVaVjczY2pTN1R0b0xhL3JFYTVDWkxZTG16L3hHSitVTncvNXpDZktHSXMKVnowU2xURUdYNTFZSTZmYWoxdUJBODlGTHVsWU05WkhVZ2k0TG85bUN1YUNzSGRaVk13bEI4WnljRnRkS2xOWAoyc3UrdVlVMW5Wd25oZisza0hoVXVpaldVNWpCUEZzTkhOQk9Fb2dPZG0xcE1TTnczOGZYVTVXQ1RCNnFrK2czCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejUyWTVsVU5QUnpLUXlqVjc3RHYKZGs3NDB4ZTE2MEVUSlM1U05XaUNvREhaZTllVk5vazRacE5FVDRLQW1WRDRXL1ZORVQ1ZUlzdlBYQzhJM0xoVgpNOU5oeXVIS1VIMkdrQjRlRWRNTS9BQzVINFhvVWo2QVF2UzJmU1NqOVFFTWh0a21vMkc4RU9WaS82YkNpeXppCjJvNmoxcHVpSm9hK1d1dFFFMTJxSnpQM2xTMGJsdm9NdXc5Y05wN3FoR1hqRzI5QmlobGxKeHBPakx2M2ROZnkKM3lKTnNOUk9paWhsQmEzWm9jN25uWkN0RGZLR1d1MTU0SWIzWU1iaHBwT09wVUVQWWRiSEJ0ckRXOG0zVUdGeQpBVnlJY0VKYjk3Q0J1eEsvR1ZhMmVGT3V2ZVJTTkExVkRrcjNlZGNaWlJIcG0wRVJwSHg2Sm5BczN5OXBiZi82ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUhJOHRZYTc3cFdkWFlZZ0xaS2YKWlFUYkVBQ2pnSENUVHBFWE9Fb1lqRUlrT2Q0M0VhOVhWMUFKNjMxdzlEN09VTG9TQkQ2SFduaG9lcmt6dDR2dwprRGRuZmhBYWdSbEhxdWg3L2FFdUFIcFpNeHg2amJDQ3FMNXBhZ1B6ZGVFbG1xVUcrbW5lMTBYMVhsT0hDSU5vCis3MVpITkN2QXNBSTYyQmpyQ0gxdUpqaFUrSnZCenJaMWdjRHdiYVNmY3pJNGJ0SE1WekFLQTZwU0ZpZG9IMFAKbStUK1djZU0zWlFLZklIV0ZxUDRBeDNxRjQvMk1BOFAwK3NiR1JwQjg4NFRWVDNtdy9VcVVBTTVUbzNuTDBFNwpmRWpzYTkweDNXcnRWSTh3OUxMcHhGTlVIcnpRWHNRUHdja0NUcENIa0V1ejF4R2FZYTNjbVNMR09BcUQvaERFCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVlWZkJlVUovaFoyWnpGUk1iSG0Kb1VVMDR3Nkk4MWtETUo1dkdER0NwVGVYZmt4WXg5UzYrWHI2Ykh5WWdhK0RBa3I2T3FVREVOUW9OelV6SjhMSwpGS05CODZpL244RDFkQ1lDZU9sRW5FWHphZ1RaSndmYm1UNVBXZzIxclhqOFo0SHIzZzRHcWM4MnNPUDJyOWhpCjVYWXdIYjA1ZlV3cXVQVEhlTXFOWEE1dzM0RjAyN3VGTTdGZWtpVXZhK053V1lUNzI4czdTMEpINDFHRWR0N1gKdlByMHowUlEwWjZYclZ5UG9PQlllZTB3NlRYR0VFMTZwMCtJVGovSko4dUl5NXpRdzIyTUJMVm5YSDZHbUp5Twp2aVdOYXFrTG5wWGdrdEdEdnVTWm40TWFSZGdZNUc5b0F4U2htQ1I3VzAyVFhTdFM0cXpYL29JMHF0NGJGcVJpCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOTV3ZVlYM1Eza3MzdW1LWk1ra3QKTG5NNStUdnVRMHdvS1hpREtOckNXZXU2Vm9PUEFNUkF2WDRJbTNNOW9QVUV5OGdmUHZUTUUwRndwUk12b3UwMAozWnJlM000ZzhLd210ZGhaWVJSQXRnemF5ZStCL0k1WStlQnZWY253VGJjTGpRV1M2bzlFZUxDQnRubHkydFRvCkJaY2ZuOUM5enVkZStwYWZPUjk5cmdzQTdDaXNQSkVjbWE1MkU5ODcyS05CSFpldWYwL0xzWkN2b3dXSnpFa1IKMXllckM3bnZYbEU5MnhHS2NSa3hVb2c0N0pJN2VXMVFRekZvNXlad0ZsQUVSOStFbUNnRHRLSCs3QjIxRXRHbAplWFdVVVpPSE1XZlB6dFkvUUVVR1c5R3NhSGp3RjUvSTlwWE83cmNEUkJSVEtXblhKVUdEaFJJUjl1aTJGTU9IClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGZGZnpMN2xjb3V5emlKd00xeS8KNzlscWRzaDZNRnBGMDJXQzVldDJaR1pxWEhaMThkd0ZMNjBHWGd4VTJNUDFXT2RpY1BhZldUaUdRVlVlam9YVgp3NVZjT0NGWjR5dkhPZFhNMy91M0tFbnc3a0dyYW5vSW1YOVlicS9RcEZZemRjd2ViWnBhcjBHWG94UmNMelo1ClJSV3FZVlB6T3JwVndCWm9HL2FoeGZBby9zRXk5d0tmR0MwMnFqNjNWUnBmbVhqOS9JRzY1ZmVWcW1QMWZCQlMKMG9xcUJENWcreHdtcU1qd3Jvazk5cTNnTUFzemJiZmxxZEJZRVR6ck5ubHpMY3UxUFRHSnBlQWNvUDhOaXFxSQpvS2YyTmo2b3pGVEhmcjRLeGdvN2hCdy9RTmN1YXppbDZPa0lmNllFVFhUcWVhMWlHM2hCdHk3S0d2OVE2amZtCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWpEUTg5M0hVZ00rY1ZkNzZOUTUKU0RrMzcxdjUyWVRZQnRSSVR1dXRDUDBaS3M1TFFDdlhTQk9TejVzaVc5UUxER3k5KzE1dU1HTnQzQi9xaGs3OAo5K0VpMXcreHhlQ1ZSNlVLb1FReVYyWm1MZ2xMOFNtb0lzdlZBQ2hPTzZjcUxlWXd2ZUdFWjE1L28wcy9peDZhClo2OHhDQXpnRGxycDdEZXovWkptMEIxbjhzTTVuRURjZENQczFUQ0hlVit6MFoxNW95Z0VYRldEZVJsVEJ1Y0sKZUxCSkVpc3VJWEI3K0o2K1UySk1sM3BodW1TYVRBTGN2TzBCWFd6Zm1wQ2xENi9vdlVkaUpMaVdJS00yVWk2MAp3MjQ1TWJnTVdmMHk3d2ZVMkh0MU9RZEJ5TTJKUGdmSnBHU01hdGdLOUhDYmVVUmdSU0N2MUkrVjBuanhoa3YwCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHJKdmh0M0xWaDRpb1E3Wm1NdkYKUThPVG5heDg1TnhaUVRZR3IzVnQvTmVHZ1hBcms3VmllV2M0ZHFkNFZ4b3BDUG1NWVhwQTFIWlI0cGdSZnhsRwpxSk1DellSWEZkSHRLTERDYWt4azcyOEQ1aEFqTzJHdzZJTUdwK3FIQlNHaW5OM0YwdnVCSHJpbFVEb2lLV3RzClpNa0FYVTdQcXBtUXN2OCtzZG15d3E5VXhKTnJlbGVERzlJMW8xY0tQSTdzY1FFT0hxMkJQeDA3Yml5djArbDEKSENaTlJNUlFDNUNndW9FZjhoK0hEY2R0RDhRZEhUeGR0U01IbFRpV1BkS280ZFBsYkFlRmhhaExtd2ZXTnhULwoyYXFtRVluV0F0SGtBN000RVNMLzJtWVUzQjhPUzQrZ0p4dW9lS3h1ais3WWZpemtpdXpFVlBsZ1NQY0QrWTJ4CnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmZ3dS9NYjZpSFREcXNXUG5zeGoKSDVoWVZFQkJrUlJPVlJFTjlPMkQ4aVZwMUtTMnF4M3hBMVNraHJZNmFOTFpkNUYyb2tEU2xVOE9seFFRTGtoRwp1Qnk2M1dlQTNFdmN0WGQ3ek81RlBpMCs5bmdzME5lQXY2RkV1ZUxyTFAzL3NSZ1d4QVNxOWM2N0NYSnF2NUxDCjdXWXlDbE9IeWdwZXU1RW1YTlVHeUp1V2lDVVB2QlJ6MUNVamh1UTVzdWFXckQ0RWF6Z2cwa2tjN2hhSkNnVnQKK3ZCQVgxRGVpMmpSbklNQVlwajVhT0NPUDNuMXlLcUJwNTUwbTRDcUIrUC80VmxKM3BsUU12Q0c4K2ZuNzVnNwo5QTEzbURsbDNtQ21mRmRva2d1REFhUmUrNHEzVzdhdEorY2pNYjVEKzNWRzlyT3RTR0hFWm0rcTFRMVBSKy82Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDQ4aUZzRDBPNU9xSDN1VjZKa1gKaVZVNEh0Z0NsRzFKQllCbnFBcmRmSFdzS2ZJbVYwbkU3dXdwSzVlWmk3Q2pRQmpLclhiNVdGa2svMlNoMUVjdQp6VmJNLzhHaURLcDRnOU8zWEF6cUZmeFRicjJWbFo5cjBsUWtBUnJ6SCtQTVQwU3ZZSFpkTTIzbGpEalo5UW5GCk13aXp6MkxsdGxEM29TMUlqY2dybkVaZ0FYWWFQNk9SNGhxeGVySG5MSVFSdGxHdXNhUndEYzROOGlXVEdvbmkKSzl0dytUVUl5YUV1bjMxZmY5eFpDY2ZROEFWQWlheGJ4Q0s5K0JzZW1rdDJwcFFSMHl1Y0s4b0tIMnM2N2ErcApTbmQ3K2pYaENaYStWamhlODFvOExBaEkzTjZKZzlLSUNmN09pUHdNYXVITnBHZ1pCYmtOTlRadHBEZ0lnRk8xCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBck1ReVRaMDdMZXZwRVh1OGFRdkwKeCtHYWsvSkpPdzViZUF4SmI4ODV2RVdZNzE4YkZNdHBRSWFWM3BIMUkvSmhTMUpkUkgrUHc3aEtWcmhJREl0NQp2dEhTOE0wV2dlWEFYcWZ5alNsekpmSExscU1qYlkwa3lSL2c4VjFXdFN5VXB3TG5rWUYxNDBZNjZnR2YwTVhoClVidm9XajFqdTY0OU9EVjZ3VnFOaEpXNVo5MG9GZ1hxNTBnWnJaUnhRdXFKTDNKU3pTUDVCZjBxbytQZktyaWsKc3EyRUdkREZ0YzVtT09HY3NDR2RrU1RZenMxeFlMc01PcU1qNWd3c0kxN0V3OFBpM3hvU1Q5VEU4bllKbnM0aApERjdRY09YQnhhY3FTQkxOTEJLeUIzbDJLOFN0ejdBU3BiaStzSXJrSm0veFkzdnhGRTRFMjFvWXgvd3h5dVpICjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXJscjRZWEZBRjFjZEdCSFN0WEEKNHZmOVJIMEtUNXFkYTd1Z0YvUjBDbEFtWlNoNFQySzJRU0VmVWUzM29ZYklQMlQ4bTRtVWhIelpBcm9HTkM5MwpjUHNpakVzMlJGU2Zid1NOdkN3a2dETG9sUURBWkpmMU9zYWM4d2t5and6RTZjR3VuKzVJUTFSVjUwOG80Y2RCCkVsVmVRdzZ4UWVHaVlEaDR5bTRZZDdkL20yRDlYN3RnTnA5M1l6YUx1Ukd0S3FFUS9tZE1HNkhhMi95bTFXeHIKTDMwcTNHelFzMUFYZGdIY1F0OFZDeXZIc3RzcS9UNlgwb2tqM1BXRTNNUzhPTGRqeE1KM3hwK3BVUlJMSjZ1VQorakc4QTVyekZGZGkyTGpJSnZ4Tm4zQ2tUVG5ISnVVd1NKUkk1elo5MzZPUlQwZFBxejFrZ1RPdmt1RHpvVmRDClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmd0T2dUUVBxVFg2NkNadXNpeEwKak1hajhuVkxYQW1JM3NlenN5emVod1FNRmo2bFg4bTVPRnh1eTgyN25QUllwN3VRb1AzNzY0aVVSb1JyamV4bwpiTHUvM0VzT2NpbTFZcWJNbFpxZDJ5b2cyN0NhaFJqclozTTdNZWZWL0ExcWNEb0tZaVp3bUF0eHRLOVBoMHdKClJsckpnTzlGK0pUMkhCb253REYrMDNmdzdLeUVWWW9nTUdGZndLaTZwZVc5UnpoZFZoNTdtOHJ2U01zMDUzQi8KMUw3bG9CRVhobzV6bEFKOE9UZU83K0ZRLzdNSU1YeEN2Yll6clVRK3JCTjgvdkYyMW5BTmduVW9RZlEvNklyNQpjN29Gdy9WSllEbSs4Vzg2UVBtYUJvckgza0tUdHdYZ3g0ZnJFdVlIR0xzT2xWSmMxbDE5K0dFQ2J5OHhnNFZZCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXdkVmtjTlhGNVpvcm11WXpDSVkKN2Q5d09iM05TMTdxNC9ycDRsNDRiT2p2V1pGQlE1ay9BRFN0cWFiQ2NMS0xiM2hpUTRFN20xMUVQb2o1Y1FGTQoxZG94RE1XbSs2SWRDNE1PUSsrY3NUVzZGSzVSL3hldkVuaHNqVjZUZjZYMEpsdWk2M2dTNFZMM0Ewa2hvSzUwCjlJaEMvemNrcjZGY0MyRHl6SGhkSEdIVUFEcnVIUEIvR1M4MzQwam4vODhvbFlYQkdUQTRDYXVwS0hFbjg1ZzEKMTRRWGlwMjJxb01vVFp4Vy9GaUVkd2swZEVaRWhuWnhjSjNmcVZIbHJyVkR5TXM3bW5iRHpvVmxnamVCVWxwRwoxU2plRlp1RFVFWnlhU3JieU1ERCtvNGJrOTg5UU1EaXQ2dmJpTFVYSkVzUnRQRXBrR1hlcm1lMjBsNVBBeEF3CjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXI3Ui8welU0aU5FZFMyR1RDK28KenVGQWpyM2U2NUI3QjltMHBKbWdqdGlUVCttRDRCWEpnRVlwYlNnOEw1cHY2QWw2M2ZQZGxwWWF1bm53dWF5cApHOHdoK0pkZWFRdDR4ZEx3UzB3U3N2cE9jN3JObGFZVEZEdGo4Z3VvOCt1YSs4Y3B0WG8rc1RGay9wTEIyTkpRCjNEVHVCU2QrZ0NCZC80cmsvZXlZaTBxNTVkMzF2OVQzVGRnQ1RjWHY0cFgzdTMvY0VZc3NGTjR1UEw1ZXpoejYKdU9zMktPOHc5cU43ajJranZURnVtVDZOSVJya3ZIYmpVZWlJVy8xNXlWTkJTLzRBVzJoNnZjQjNjREFFckJrVgpsWFZiaDR3WW00WWJkbUhPQnEvOFBLUXRTREltVE1vZERYVmhiOXlVU0tHK3N1NFl0bjR6UVJGZWJyUitMSmtRCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGZLMVE0NWlqbWMwWTN5MW02YmIKQThkWno1U0VtNGgzZEc5bGJDa0pNOGEvNkViRytPcnpaOHJoc2t5UmlYakcyQVBsUy9WTlJKSk4veHJtbGJtNApacmx0emQ5QnlnUndkbDExUVFvZkIwd1lvNjVuUWo5N2F5SnV2WXJudk9mS1d2ZERIZ0R2cmNiRHlZc3BNQW0zCkR3d0NQdlRVTkZZMFkzMXdESlZuenE1QTU0OUFoK3djYTN2OC9FVGJGYXVIYS9rcG8wRVZ2NFFnb2RaNFV5M08KUjBhL1hPVDRuamNYU3RmUE1lODFnTmZoSkN6RXAraTE3MVhjL2NCdDBycW45bVR4UHVrUVpPaUdNQ0xNT2syUQpaOENjRGxvVGhKZTQwL0tyUEFwdGhRWmFobVRpMnk3cHNVZStWUy9RNXVSL29ieTZiYXZvUm1JREFMVHlJSkJqCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1lsV2xJWk1VSElSMGk0Z3pnSDEKNFdST3hsUCsvRE1aS2xYZENTaldZS3VOcFhjaDhPaWRzdVJTb2wxbDhBS2VON0g0aGpKZVRTYy9OTm5UallLdApwL29kRlhnODBYbVN4TGIxbnBlNzlLaFJHeFo2M2JHbE93WG5wZnljN21yajZIRWVVWERVRjdNT1VIQUpRRVRnCmxwU282R1llWGhoTDBSeFBDWEp1V0dYRWkyMEY3SzE2cHl1S3ZaZ2dRTFV0QnlXV1FjTzdvbU9CQWpxenFaSE8KdnJBNjVkRXJIbklDV1gyWEVjQWVpdi9GV0NjZGQyZTZYa2NOY3hFT0JWYkVyU0dhVFpUSVdibzBLU25wZUJ1YgpOdmRsOVB6c3AxWU5NZEJmNDB4VHhXU2JVQ0kyMnRoTE92cFIyaUdYYWpMYUgwVkd2MWw0VXVOTHVYUTY4b251CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXpIcnJCL3JKc3huRkNKZ0hzTDMKL0FtVDN1Z2s5QUc1Ym1VMS9WVDZ1SVlJVWhtanRQQUpBMndYTGd0R0IrVFMwMzdrdlpqUHpGK3d6KzJlS0VFWAo4WE1aRGtWQUVCdmZNZGQwZWRjZ0FhWWZYMVRNa3MvenowK2pWY3ZHb3MzSVdNT0g4RlEwaE1rZ0F4dVAxa3ZDCk5Lc1BpWGRSMCs3QTM2Nk9nd2IvWG9RZGdVd1JLQ2hTdC9SUERxSnJ5QmxDT0xkOG0zajZNeWxkbjVKNXJ3Q1AKbHgxZFYycWVjT056ME5HM1ExZldzaE56RzFCMWxHWWZlM0x3RVZXRVltbVlWbWdEUnhkK1g4VkI0R1FEM1FOOAoxNk5JdWYrbFJFSjJ6UUR5QVVIZWdYaUZaekIyNHJLYyt6S1dKd2lZNDU2L2w2MU45L1NRTzVWd0dMR1J4SG1YCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUFjd3dTSTZCNndvaWs4RG4rMFoKN09lWkl5S2dhWHpkV3dUNzRxK1NsZFo0TUZDRGxsSGljS2x1cmJTT0NsTCtqcGU4cnV3NWx3ZDJteFN4d1A2QgpqdTVDeXZ1ZUFJVWl0Y3NHZG9aSGlWMUhucXphZzV6cjJtWklzWGpSRzdTU2JhdEtUV2dxdC9VWGkyTXhJVjdrCmhZQmE5bGFkQVFraUROM2RLSml0N1J4bk1uaVgyM1ZRcm45YzdpdXBYckg3YU44Wk9jV2hpL0xiOTRBeERvd1UKMnNBOVF1N0JBc3hHek5TanM3TnVUNlpzRktBS0gvYU1BY1RYZlp1RTIzbFgxbWhtT3NEMmJRYjVQNzFFNHhqeQprdnRSRjhsQnVsOE8xTHFrYUlpdUZENU0wRGRtaG9PNkJibnRhdzBZR0wrM1QrZ1ZWaHBSdXVZNnl2MFl4clFmCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUorR2FuVFJET0h5TjRjSzNMUmkKRVhaWHVNd0E1S0o3ZUFLRFM5ODEraUtJUmNkUXg1Nkl5YnlURXpVT3piY1NiMFZxdDZCTkxwbzR5a0pBc1dnQwpENWh2SFZ1aTBDVGJmK20zM3MweTdSVGxqc2pUaE9pYnZoTUUwemZoMXRRbHhMbHFOcjk1OXo1Zkk5Z3Z2MHdnCmdTL0J6SEl0bTltQVBkSlpOY1NBZU92MkVJck81bFFzMUh4Rm1vbEZtWEFqb0pVVGU2UEFzTlRkWCs2WjdxeTAKclBSSWgwMUtoNk9WMWdWRlNneERBZTZSSnZ5QjlGK1BiMlhxNlJFTG1hZFB2TC84VU1HRklMeW4veXVZWmFjTApZbEZ6ZlkvME9BVHVETVNJN3lCU3hYaDk2dzhZdjdzRmtyMDJ3Tk9EcmIzeUpzbFJFV1FOUDQ2eEVMNE5iT2hsCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFBLcVRZdC9tenpRWjFYWUJWdDEKRHNlS3BhYklmZzZISHp4UDZsaExVa012YTZrTXlLR1lhR1l4QzJtZTVnVXJ3VTV1UDVPMTg1Ly9nM0lWV2pneApDNjBqY2c4THpTWGxEM00yVlhvZ2pScXMyTFRRN2gzVkFHZVpodE5YazlrRkVhWFBOMUh3ZVkyWWNPcS9JYURqCktlL2c4SEZvQkE1bnZaYnJObW1PLzgwckxDeGljTzdxL1BsN0EvWmpqUGdvWU1aRzhPdDVBQTliWkRCR3lqVHQKTzJKdjgzMHMxdks2Y0oxd3pDNk9HTHFMdk4ySzdhZjR4Szc4RlhoRGh6UWRqSVJ3RXEzWThkelkyZmowYkdFcgpsVjdwSVU5NStvYTlRVzZjT1JVZFlTUWN5QUZITmk4M0w0ZmJOZFZ4bHRzam53K0dyWDk2bGluaStTZnhiaTBUCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0s2a0t3eEZQdzg2ZnEwYWtQbSsKeitackdZMDV2ZlN1VXZHeWxpOFhLYzFHM09hdlpJdXZiK2NZRzdFYjBwYTJ5Vm5wcWNsaVprUU9paGlUWSt2bApCVlk0Y1JkODZ6d3djbGI2VkhUTTZLUnNTV0tiWlRNeHRYYWlwNmt3bHpNVHRocDRtYmRvK0J5elJaMFVyWU5zCmwraUlwMnB2RnhpSXBVUFFNRCs5N29JTmZvMU1POWlWcDdmaU5RSmcvNHcwV00xOWRXRVRIWmlSbXd1UzRMU2kKazg4QzJxNkZiUUVtMDQrOExYMzdVQk50cnI4cTIxNzlTSkFGSC9qenBjRlU0ZDlldFFWcTV6L21hLzkzcTdIYQp3VE9YQmV1Qi9KQlRDUktUQnBMOEMzMUVaOGVHeU80ZVUwZmdJaDAxRHJEOUFVWTR4VnA1ekI4djI2Q1JXZjJkCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWlSTm8xZHUyQWRjUVEzZEhoNE4KQ2NBd0pCYURRY0I5Z0JodkRPeUZ2NmhTdExtcjFKdm9lTEhWVkJMZkVieGFNc1JrdWhzNGY3S3Fib3dWVjJReAppRlZSN2hkaytUMW5aZlRyZHZOUVhPWk9KelJ2VDZyOUZzV2Z2TEg3dlhCTjhxbUoyVWk3ZlVGNzdzYXlYcy9ECjl6V2c2M3laZ1d5SG0yeWRsdmljRkVONjNVajRQRTEyRmlUYTFtcjQvazVuMlB1L092dXVZbG9HUVROUXNBUTgKZ2sxK3RkZnlETXJNcFkxN2NCSWNRcmh5MU1Sekd6U013V29YaG1UYlAyRWwvT2l5a1R3cHEycWxOTkRQY3orTgo5eHNScFhvRTNYdEFkejlKRGFuVGtQMWVWR3JSeWEzM2JYQ3ExU015U3l5TERMRmR1djRyTnBpbTNTNENYZFd0CnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUhHRkZ5MmE1N28zWXEzbW8rZ1AKTmRBTjh1cWdORDI4dyt4WmpxR1ZoU3NJcTBMb0pPWVNTQlhpRDdzVk1sNEwyOWRTNGZBTUQxNVVQblM4eVhqYQp4ZDZKcE04YXkvT0F4OU9wWkltd3QwaDRxTlJVUmVHbndMcWM2d3hqeFNFY0pETWQ5WmlWSGtLcXZ1Nzk4YXowCnNXTzNmREJ4enVjN0hzNTNnN0k5ZGZLS25sVzRVNG9HT2J6Y3hWeWxoa25Vdjk0a1RrSEkrbVo4bmI1OFd5WjYKQW1jKyttdHpiQzF1MlRRRTVIUnVyd3E4SE83L1hlb1h4dHVHUmt1TzUwQTlMTjFDdThvRWtpY1RQODZOQ2dnegpycmgvN0E0UitVcmdsRGdHVlc4dW0xLytWMnR3ZHJhcDdEanZMcVpnTVdGV2tKZlpBN25tUzRnNVE2bC9ZRENtCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUVuTzFsOFZNNXRiM2pNTk5DdFYKL3pHVlZLZVl1RW5teFVPblI5WFpsaTV0RkI0c3dKRVl3MlVjc2JvYk9JU01OOEtKakVmZ3A0OXhHM0t5d3RscwpVTHBoZFc2d1h4QXhSWVBmaE1rWHhsQkZET1RLNFRoMUlzL3grUVVIZkdBZkJIYVRnaEpSUWNGMDFQR3pWSCtwCmg4ZzA5eGVVK2N0cFk5VHpHYlRtSmNNUGUwaFpPamRLVTV6andJcVNSK2IvUGhweVlpUzdzbUhIR3dHclVBUnEKM0g1a0RFSzBseE1TcGd2Zzgrb0dPYmQ0UFp2c3ZaQlVuNkJJV3ZsYk4xUjU4NjhYY3J1aXNOWVhDK00rRHhNTwpQY3VKNEUveHVkSklvSU9DSFNzdzhPb0ZNbjBNbmpnL1VaZjE1NkFoTyt4d0JmMmlEa0o2eXZsUUtvbFlncWJ5CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1lTYXExTFQwRlRucGpnOExLZjgKZG1EckQ0WUpkVGxoYnk2eHhqb3ZMcDdicXNkTGkwNEJBOGxCa1o2T3JnQ0JaWjJLUXQ2RG9KVFNPakUxZTF3QQpFZ01GeEY0Y0xTajZ4MWxidmVCM2FRalh3WVBYOEtMQnU5NFF6em9zTkYrd0Znc2p2WkEzeVpPbExJdVQxcDNBCjNVaDIvNFVUL3RDMVdZaDcyY2NvWEN6MUs5dno4QVYzWDVIam4xNG9tSU5ITDNjYzdvZEVsRTNMWlBnYU5PaUUKMUJIVXdRQy9KWE9FMVhIUEcwLy9zNWQwTEVVaXJpUU00N1dYRE5PYVRUMXdkeWkrZmluU29Gcm5RWVJPVUJNNQoza2hwRklzbWEzNUUwcW1raE41eTNxb2FDa2had1Axdy9vUDVMSUFIdDZUQkV2R1p1aGN0SEFYbGYxTzFSQVhZClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU1HSEJkSGlRME5vZENUWU1WMlUKZUt5ZWh0cE1xYk1waDdJSkNXME84K1BkdGl2TTViU1hXS21tLzA2cDgzdTVWdkc3MVpMZmRyK2VnVnpLTnpKVApBZnQvYWIxcG1ZYTFzOElwOUdFQVUvZEtCUWxhYTZYM0lkNURPRFc1OGJ4OTN2WFhETzRDTkJCSVFFSGpmUVlWCloxbVdjNUVEdVJxZmxvSWFqRThTYnAweDJMQ1N4YUUyWDIzSWkxeDNkTVhibXNSYlZUYkZKQi9naTdmNHhDYmgKYUhDQTR0SGtCYVdibVdhVXlpRldWYVBieWtWaERVZXg2bGdRMmxieEQwbTkvQVpHenVseU9pVmdIdGVST0JrQwo5eER0TUNDM1MxVE1iNlhGSEU3YitLNUFuYm03TmJoUDduOEZrTmJBME5vbzdwcWp1NHpaT2tVcGZLTmdsSzY4CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMENMOTREaVFYSmdITllFTnB4REcKKzR0R2tZd25xWXpQd0xKeUJSaUEyZFMyYTlNdnl0SjVKTE9GWUxURHBlOTljc2FQQUxKSThCOWlqSm5DWHNUZQp5MWZ4R1BkQWJzOTN5UWtpdEUxbDdmUDZrU015TVlsb0JiZVh2cmp4cmIzS2gweG9WN1Jsd2dBTXNVWlA3Z2pTCmhiRUM1Mi9VYnJQMTlYOVkwN3lWRzFOL1pnaExkaUlhNUJjeDQ3STFPQnIrdjUvdFpXUHpFRHlQeng5T3BDcVcKSEM2ZlgralBYcnNlZWJQWElBOHVmbFg4aWdDeWlIZ0QxVklQVzB4aWxFNjU4alE5a0VrL0ZTWkJ4aE5YaGJQRQpKcWZxRlVjSTl1SmZhaW1Xb29BSlNWbzRITUJmay9EQ1gyWVdwNU5xdlIxZkFSUGpGODJmSEs0VitoNmFDdlRhCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1VqZ2dnbG5IaDZnRHV0QTFkYUcKaGJJbjVuRXpoYUlUT3BPSHlpVEZJWEJzdDZtbmdwcld5enZkN1VrUWVpdFVmbkgwWWdHTHpkZ1J0YzlOY2QwMwp5QnpMUlFMaFhNUjZJeXM0UEZ4RmNYSy9xdGpraXNRVmhsZzZwMGU4d215TkRHeHNnZmh5VmhuU2dINzg3ajlMCjUyaTh3Mk1UT05LNmxRZmkxdEh0cTByOEVGc210ZFNmMW5jeDlMSDd4eThFNmo5Y2k5VGUyVzNkWnZuRXR4bDAKUXNtN1E3STBwR2gweUp6KzNFRDlPWjFocUxKQTFBRThWcnZFckxiWkhoVlJabmlCQ25reVB5emxOV3l3MVVnegptblpCemU3V1ZpUUdQM2JIZG5UbFIrRWcwTUo4Nk5yRjM5KzZOV1ltVUJKNEVMNU1qMS9VeFBOdWhZcDBLZUxHCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUVwaW5Zc1VtVHMySEgrUUwraVkKcnFGT1h3ZE1VMUxsS3FmZ252K3I1YkJ4eWhuQmdmMC9QdHVJdWVaT3RaTmhNVUJvdzZnL2duWHF1SVJWcWg1dgp5RXl0VHJZY3UxNDAxVDN3eUZwQTY3RjdzWXVqazhBMXJiV3doa2FPQzJqQ25NendkYWppS1JlN1p5L09Db3YxCnRFVXRVOHBYZmxuUGZRSG9obzMrckhqZ21tSmtUSFpWaVlSYTlaOC92UUNibW9ZZC94VDlNUGlSTU1XcEZtc00KT0hrM1ZhbHNrTklST1Q3Z0JTanYvN3Z0NGxLdmZON3YwOFEvQXViR1RjTy9GN2w1R1cwYnc0SGZuejFJclNMbQpVRVFtbVNCKzgxbFExdmxvNEFIWXJKbDUvTko2QURBSlBXWm9hZmJkclZNSGZrcFpwVzE3MUJiMnAvN0tPWm1mCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHNUUzdveE5OQUtqdnRMRzdmSmoKVHlTT3JoTEhSNjNDTFppSkp4Z3c1cE56Z1lPU21JbU42Z1U5UlFZUFE4dEtLdnEyNnNucW5tb3VvV00welQ1cApLVU1hWWNlZnJ3eUovUHZsczlENUlkVDdOS2MxOUQ4NE83RlhDWUR5UmFpR2d6TUM5QitkNER1M1lESzdaeTFiCi85TWFNR1A4cndVVE93djg5UEhoRENsT0pNL0NIUG1LK3hyUmxHUW14WHFyV3VoanNIOVRINVpwVXdHMm5WelMKcnBLcm9pemU0U3phSnFobjhrY053WHVUN0hPbEZreUNuUGR0OHBSY1k0cHpoN1VoY0xnR09WYkZjUlJKMklYVgp1LytQUThGRnEvYm03ZVE2bEY3TXVzYXZ6SGVYbUNFdERBUFdDcFEzdm9hblAvcnVCUnRNQ2JUOUp6NzYzUUNyCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEM1blNrOU45TE5RZFBwOFVGOUEKa2s5dFZ1dWIyNU9kemQxYUVLUXhKUm9yRDhFZXUxZG5meTVNQnpIcUFrcDFwb2prakZEaGt4bjhaRHpNZy9nNQpwZmZVb3N4T3JYTE5yTlVodjJzT2tjZk91eldRR0hxdmJlUGJBajZLWG1FNHZiOGxSSEVLN0dnYmE3WXh1eHRPCm5OK3luR0twMmtWME1pQzYwMlZLWFVXZ2tYSXFsYnRyV2dWbUNGN2J4UGNYRThIOS8xRGNmQ0R4RHBIbzlMTWUKNEYwRzR5QzZob0JKcFVXU0dUZFp2bU4zVU5BLzMzN0JsMWd3dlQvLzY0TzhhTk4yOFVBY2Z2blpqTVJMdGloSgo4WXdGQUgxS0Q3NjIrbkQwSklOR01qNnJ0WFNMUm80U096VEd5R0s3eEFLQWJRUG9vYTJ6d1F4UEV1Ukd4eE91Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekVLYmZSQ3V5ckhFV3dRL1NINWEKWmR5bU9oa1YvcXg1UDlGOEFDSGtmTkE2SmdlRXFOY3FQOTRYUzRBZUdrcTNycFVrT1pNdEhGSG1kdk1xV2NDMgpwRTVpNjVsckRkenlUTWp1N2ZjTzBlQXFjam1ycnVUODl3aGtraWRtdmg5ellEMFV1MktMLzZ0LzBsLzlTQ3ZpCnVGNnV4cU0vOHNyYmw3TDd3eFdTeGJCUlFzMWRDSFRBVGFpQTVkdm9ZaC81R21jY3dGaUFrZFU3cnQ0eitVQlEKTlBNNGx1a2pJbDdsWksrajg0dzhDZ1pyNjE0MUwrRnV6Z3VyNDZxTlpxdHYxYjA2Nm8xcGNJS1FOUkV6R0RMMApHZmFoa2Q4U045eG03UERPRjJGcEI2SzhaYjlYNVdNK29XSXhsSlM2U1d4bXpaNTBLUEpQZ3lrd1V0UkRZbThqCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejdHWlBkTmk2SFFPNVkvM0FqN0UKTldJbTdQVklhRmk4Y1FNREdqTmt2NW9reTduOFhOSWtka1FtOUREQkdOM2kwbkRWRXRpZllDWU85cW10K01RcgpsZVNOZlVmazdsSCt3aVl3SjhMTmVvMGFpaldWVyttZk5yMU5aVHZZM0ZLeU0rOHJLWHZwOFhWWWpNZWlyWEZhCjJpUGhNZzd1VjBGNjM3STk5T0Q1dlRZU01ieEVRTjNOeW9QMnNhcjQzUDdlNWw3Z0xWWUp3bzhsSDR1QUtaY3gKNG8vSExqeENYU0F0V1JQQlRrOWVyemE5SHgxQU5LRWZQQ2hHaXltUnYwdFpYVUVoTzc1NzRjT3FXaFZBeWtQcwp0L3dUN3VFUnVVenQ2cjV2R1RDWUJyYjUzb3ZweUFwVWFoMzJCQjRjdTEycmtzQ0NFZ0VPRWRTaFo3WGY3QmI1Cnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkFvT0QvaW9rU0ZlaG51N3ZVOSsKb3NETklPMzY0ZW9YcGllWWVvV2dock95M3dXNmptcmd2cDRxVk1ES0tvMGtnTnVCQzlqYkRjcUdxdmFnOFBIZgo5ZnBVK1RQZUc4TjIwSFVGT2hMTk15VVRzT2hPY2xFa2hneW1hMlZJT2hvRFdZTzRzblBQNkxEbkY2ZU1PdHhaCkcrS2hOOW0zMXV3VmkwTlNubUt5MlJUZWNkNzhxeGozclkrUEZBWlVjNjNQb2JaeGh1RkdYSG9LM2JXcTlVMVYKYmd1RFF4YWlqaDlwaUgwU01NSGkwZnFhd1BzeXJrRTFEU0QvSzFXd0tKMnBrWEF4YVpIMGpKTjk0UDJrR3VwaQphRjkra2g5a1NVUkVTZ1lhYnhoY3FjRDc1QzcrRTB0ZC9mWUFJek5Obm5NN2ZDOVdtcnNwU1d2RWtSYUNIZDNvCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHhFbW4zWEw0OXVjcVAwUDJMd0QKQXFHazNEU0w5VmlObnVQUy9qT2JwMlVEa2dkM3dTdk45aWVGejdXTW95MGdjSG05L3BCRTNXdE5IUlJReXdaawo4T0F1dklOUHc1em1OVDlqdDZOa2pFMFlqdTBoZkNBK25nKzRJYnFiTVFEbkswT3hTSEYwQk5jRG5mZldYMkZ1Cmhpb2ZFK1dzWGZqNE9ERGZEUHdjbHZULzdOSFlNaHNhYk9ZUldLMnFackJHNXVUeTRMcUJCYXErLzYyb2M2eE4KM25LL1lPbUlYL3VOaWFQNFFubXBSeHY5dG9LMHZKa1k0YUNnSmkwSXdkeE53WngwTUtqOHg4YS9iUldTU0lONwpQajd5VjVxTVplcEkrQ1NUbnJEWE5iY0xkSGNNNXI1RC9WUFlVbXo1YzhuTjlmSlN6VnFreTJ0WkEzQmQxNlphCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEJvU2czN2tMbGpZcWZjSFdLOUcKKzRZVFZjbW5XTUMvTUFER0NYZkZ0VUVXU0hBbmsyV0dZcmswcjRJcytwM0JzL2hBOC9LZjdNRW4rZmlQNXhyTgpQZGNpa1BSakpzZ1BXdHFhMy9UNnpTZ0FFakVOdjgrMTl3clVhVGRZdEVjQldTWWVQdHVEd3pmZkpOOXNjbHNlCk1jM2d4YW05ZUNNTTM5RzFKU3dSYTFVQjRITVZJNWFlOVFaYS8rTEU4RXllaUVUd2F2REhnQjAyOUswWHNJYjAKNDkvRXlsTERIYUZ6U2x1bEt3bDA5NHRKZFFJQ0cwSjJ6eVZVR3V2NFlTQ21GTDk5ZkV1SlNOd3Vsbm9FSUh1TgprT2tDREc0Y2lNOVZ5Kzk5djFOOURiUDZEbEZheitxZTU1UUt1WGJXbTZKRmNIZEFXSHp6dmVxNFBGM0JXaHhnCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDlLeitrTUwzLzl0QUZPMmkvYVgKNFB6R2xPZUhlZnBnNUNXdUVYa0NuWjFtanltSDVSOGVucXNZVHJ1eFloRmVJZnNWT2c2UDQvWXRhZGxTell0eApld2Q3K2g0NXhoVkloczJENlVQSS9KUzhkenFULytDenpwL1p5blJPY1diZTBpR1VkUTlJcmFuMXhvelF3Q1hpCkRaWnhQTWkzck9IRC8yU3ZNQldOankwYjU2WVNxaC9DTGZCR2dXM1dDTng2R0N2dEsxeWxVMmcwQVBBU0NnakIKNk9hb3NqcjV4ZDczN281YTZNRHVlV0w3MzVpeS93dFdPeHg1TzhDUEJ3cGFBalIzcWFqUk1zbmFRT1M1Q28yWApaenZRSUFDWnVkK0ZFMzBoL1I5RjdFVWVxOFNZUmgweUhJZTEveldnOFcxcmxTSG1aTjJDVXJXRjdsN2F3VFpUCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkxGalBUM29VZFNBUUFTL09qTWEKUVU3YnNTU0tlZjI3R2U2SytNTTlHUUpwVWdZck8zL3dQU2tsWkx3Mmh4NGhWM2lYS1Yxd3RVcjhUS2t5Qlo0aQoxK1ZiblZOZGowbEdYNndRc29PWjY4MG1SSGRwdFFvVTI1VVZVbEZtTFI3eW1DMk9sc2tmdkRYU29DMVFsYU9MCkI4US9GOEJPOTRzWHU4U2JNZWtha3VOTTVZaHhwbStySmlUWm13M3JadGpOUmlBQ0RUTlphdkRqMGtaZ1NnbXgKQVNKc2g4dkl5M1J6UWpmQXQ5MmlFYmlTV2JPMGRmcWd4STk4Q2NlM2NrK3RJSU1CcGJOWk80UGhNdEI5cmNRMgpQQW90cWtVTmQxSmxtSUJOdGtqTmNrd2JSd2pBRmtMVVJ1K2FBMU5KTGV2bDBEYStXeTRTT1Z4R2hKQlVjRHl3CkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnYzQ1hDVk52NlAwdlNiZkg5c0wKQkRUY3NvYjZRUDJmdVoyQkRGYXJsQXdUZzYyN3hVRVZlUUtMeUJhOWFHd1R5bmd2WmdkdHNNOUVhNUx2dHdYMQpIblB5TUdKaTdwMUtlekxBb245ZXIzVExIRHhzQmVHeWYra013MzFtdmdpcGdwVXdhREZIZTVjUXFtK0V2ZEM0CjhUaEJxbHExd0llM25XTStnSUxuUitteExHanVoKyt1S2FyK2h6VXphTktMcEd3MmpiRXFFNjNEdDlPTnl4d3cKSytCMWVjZjFmRFNEWFFsd05PMm5HeUtlQmE3OUdiWWhmMFU3RGMxSmtaQ00wcTB3cXVjVXJVQk54c1BpZ2ljRwprREVLam9uRzd4TkZHKy84eVBIMmpzdE5ITUJPRWwzZ1ZRY0VIenZINTJCMWdKbU1zZmFuQjJ0SVJ2UVdXWEdiCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEtuaExOQUNqY0hiRnZhd0VseFEKblJtbjhHTjRIdFU0dVA0VTdRbU9IaHoxbmFEVlhTVFRJUUVWWG16L2NaSWN3K1FOc1YzTUdIWXptRlljWWpvZwo2VllIMC9DTDNueTMxK05GYnpZOVMwODVzTy9sVUZpbzRIK1FBdFViL3RvZGxMdGREMzFFazlvK0pCUFVNanNTCnRWL3NVNDdrbXFKODVWVEpsalR5NlhtbXZMNGk0VENFWVhFVGJILzg3bUEwS2V6VVcxZHNqd2xnb01HS3pjYmsKMDZRZWlWMlZLV3dkZzFFREhEemFZdk53ZDJ4ekxocW1scXhLemJOdlhZMng0MzlRTzhzeGd1UXpubDB5TExzNgprVUdJRkNzSkppTU55T3FBeHIzcXZpRTl3cXZKeGFDTlpkWW0yZUdrWlZIWE0yb0lzSzJXbXR2Nlh0bXpMbVVtClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclpLNnh4Z1Znek5RVm9qTlMrYmEKbUtCUHZkZmhzeGhtcWpzYnVveCtkTVlyaTFHVG9ZMndIVFBHQUFGS05YWWh3VEkxaStzRlNqUzFjc0kxdG8zKwpkY0Y1Qy93Sk1rQk5lNzcvb3l3ODRYRlh0NW1sSFNZbTdsYTBpQ0tScVppdXN1aGJ1Z05XYUlpMzVZRXBxU2xoCndBb3h0WWR5Q2lFVjRsbXBYa1AzLy9zVmdQY3hOd1gwZXY4ZFpXRWVYenlLNUZGa3ZVNjQxTzVDQUd6dGgxZzMKK3dUQlA4Q0c5WVF1NVYvWXZaNTNDaUpIK3BmNUIxakZpWUtnT2tXaEpGZWJncWpTQ2QvS0QvWktERW14cktDcwpvZS9Rd2VIbFh6K0tTa1FzYnd4SnZhaWVTRGpEeWJEZE5pU3ZNc1BXU2RjdFBKSFpPVjBwUkFwZEg3MXppT2pOCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWFya2hSa0s3eU94VkRySFJSd3YKNTArdHROZkVFdFlVb1lnY28xcmR0T2JMWXJWS3VsN0VEa2FCSXd0Z2lBbmNidXAxcHRodVAzR0VETUsrVE03cwpjWmlqRzRBSndhdFUwLzFYbi9PZ3hpT3dwVWNHQ0UzR1YyakhiU2xTcE9BbnVzNjZJUTFhaFByRTJNRHBrMkdxCklCRlF0ZDVwM2x4WTQvRjJFSEVVWjBIbmU2R1NqclAzdkZRdDZNVzJpNGZRemd6cVFaQ3R6KzFXK0svNmlLWWsKaXZZRnZDbmRJaTN0U0RKaDRDQ0FJdkRGM3hmajZzbTFUSjlaOCtYVmh0aXAzcW5VeUo2VWZDK2RvdW1wRzdMawp5K0VnSXpSckhXcnZXTmxTbFZvd3VIYVlYRjVOMDlvSWYvK3A1WVdkaGNUR0lSYzMzRC9TcEtyVy9LamVtYzFaCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVA5WkJPeGpwSTg0KzVmNkxlQzgKSkdZUzZTVmVEdm45YU9YbFkza1NuaFdTQjF5VnZBRy9wVzNEODNhU0RnZk5tWmhRWmVhWVJjU3FwNWdRdUN3VQp6bE8yVC9tYlpuY3dGZ0V1c1d0VGd0aVZlRE44NmFQMzdYamVLbmhZN08zMENFaXhPT0o4U1VWNnRZQmYzZ0RGCllHTkNjTkNwR2dtVndDSG1WcVFNNlJ4MlBIc0tXU1ZWTVpDNWxDVHRQYkJTcGFQbWxiTWZoYUw1Q0wrM1dDclYKbDdzUkJXVGFtN2p0cEg3Qk1YQVp2UnJNZDY5RHFiMDhnK0tHQzJ6UTIydXA3bitnODNuWG00bFZPSWl2NkRMegpKdllrazZtTnlyWC9hSTRRZG1pRWxQUTBROGlIS3J0anNiUEhMengrc0xFb3BoVEpNSmRLTmxCbktsZWVTVUhLCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWxqWElVMXdYV3pEUDZRL0lsUmEKS1h3WWlWZjliK3ZHL0dVSGF5dHhSMTNBTmduZ1VQZ0NoQUF0dk1WTnZCZEpJck42eXFDR2ZNMW16REhIRytTUgpJMm8xTlQ5cFBZYUdoaFlvVm1kRExCejMrbk82NG9ySVdCaDNHWlhLempxZWRhZlcyaDN4TUExT2RtUHNPQ2ZNCmdJZHpHTFptNkVVTFhPZHdSREJvd0laZTA4bXFxNXZIYUc1Z2hkamxNWC9mRk9mY2NVMHdhWEpLOW1GWncwRjkKY0FHZWErNThCdy8vR3IwWXlCckp3WktBRGdESFUrSTkrS1I4Z0lqcnl1akluQk1jYThYeXJGTFc0N1ZNVDgybApsd3d2UFY5bFkySTdhZTh2WnZQeVlvcDFPa1NKVmI0dlVKZ1h6OUxMcWJyNzkvQWdpcWNHTmdFUE1rUU92MEhHCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFdQK3Z4eWw1WUJONXFvK25QQ3IKclBYZGtwMWRmSHdXVVVTRXcybURocXM1TTN6cVlZQkRNR0hwMUJSMnlrRS80TU5QY0RTL1IxNk45WTNLMVdXRgp5cWdXVFJybFlyUnNiQlZmZGNhYmN2Q2lyMzM4bFlLSXFvWnk0bkNpbjNiQXJ4aXNOZlBVYVl1Ym5adk1DUzZZCnNiczFlbWJVS2JxY1Q0WVJHWTZpSFlvS2VoUzRGZnQrLy92S1N5Tzlna0phWDRhVnJ2enI3OGZqUVo3VTVvZTIKaW0walZaK2F4aHRVRGJ4OTRjYVVCc2JSNWgyRExZOG5RUG9uR0tJQjdHUE5wSG9uUCttcDg4ai92MUhJcm5wMAoyaUpuTXkvVlpjVlprMFExNVd6U2RzVkdTL2pLTjVnbjRWZTR1OFJpQ25hWUNGS2gvMEo1NTJIU1BpRXgrb0Z2Cm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejVsc1pzWUdsVHJIdVZuTHdLSnYKNjhYMXlPMVpPM25VciszNENYbXlQUllVNjhZeTFRRUFtVWNyRGNBdDFWNWhWU3FhaVVLZlVWU0tmcndLMHJBOQpCUmR5RysyQmswbXUrYXpDY1hRWG9ad3NiZlNKZ1pTVEwzUzNOeFQxdTZMNm93VmhPMWNCdmV6SVpwNy9iRENICnYxSDdEc2o1endjcmcyYjlYNzQ4VjlINGhKS0dkamxiZks4VWlhVzN0QWhaYTU5Vjd1TWwxYW9WOW9aVlFMSjYKeTJEdVB5ZFUzYzBLS3VKMXptQXh2VkRtUk5QbzZsdkxIT2hKMmZQUjJPc3hERUo1dnZCaE5uMnJTdHJkbjhlOQpWRUdXVUJlTWZLMDdiYWtnZ2MwTy8xYWVwRGcvdGczRkZ5OTZsa3NmSzgvSW14RVlUZUNJZjRnNmFWL0QwUyt0ClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGl0Zk9BK1E5d0tZclB3WXJpVk0KWC8veVNDTnFrRy9oa2VrcjUra2RFZ3V4Mm9WbUI3SERQYnVqLzlHZHhwMzdSd2E4aFo3aU1kMmhhTmNjY29TWgpSUDBHaENWTC9vcHVZODh0Q2JZYlYyU0plOVUvUkNGTHV6UDJ1Mk1TU1luekNoT0lDMTZjcTJvS3MwK2RveE9wCm5DRmNsN0pBY2lEVlBQekZ6ZDRoellLTlY5WGVEdFlIdmVtNnlMazVWd2h4QjRiOU1JTXhNTFd2Y2YxUU5FdnAKanZuSCtvak5FdzJ1ZUdtNXZ6djhzUHBnbTdDb2hISktoME9BdjNnOHNpSlpEWVJob1hQNDY4UFNLc1J6dzkrawpEdWgrME1CdDUvKzlZSTdsaWZIY3k5UEU4KzRjYmtwUWNsMHArc1ZzMGlhbGVBbVk2U20xQ2sycVN1RUxWR1c5ClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUlMd1kwUk15UFZyQmVzd0IwLzQKMTY4elJLUnBoVXloNklvTi9GNGl5dEx6WFhNemw4TFlJTGIrVW9oaENPbUhEOVVuVVBpaVRGUXFQU05Xa2taWQpKRXczendldW1DazgwbmRrSGsvc0UyZCtYSnBHa1lCZjQ4cE1FRmxZVm1NOGU5b0NqYzRlU3l2QSsvNmw0QzFECkpLeHFIR3dsa3psZFk3VGdMak9QWlptYThScGxMUU1IeDJ5WkprTGxUSUJDejJVTEQrOS9iakp3bmx6OXhLWnEKa0w0aGljWjJ4RXhMbkRRZEZ5aEx2ck5QWkJZWVNTYjlRbU1aTVg1OC83S25RdVE4WFpmMWxVREpIOFl5YUx0QwpFc2Jod3MvOEduTkt1TW1zd3RNS2RJVVNEbm9xQ1E3S3NuU0xIY3d6NFFVenFaeDBQejFTSmd5dVFzNFBTV1E1CjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDBRbFdhZk9ka1Vobi84YVJWdUwKUEtaQ1Vxd3hFQjNsYm9hL1Q0Sy9LNkhwd2ZFMEJCVUxZRzNHZlZSQk9NYzVBMTZDUnJEdWg1WUdUa0kxSENETQpucDE4NGpjamdsRVFmUVpSd0pndDdDb3I3bHlkSTBLMnQxU3A5aHBWY3ZYNERMZS9LTExQMnBNWWhBdkk1UjBCCjY3amF2M2tjYmhsZHdsTGZTc0lFMDcwNE5uK0dTelQwWVVhTTlYVEVudjNPUFRZRlFNUThRaDI1b25TMy9KbC8KMzBGTmFNWW9YKzdUcDFYZEUvVG9sTStEbjRQY2ZWWlk5d1RqTDhWUEI0dE5JUXJBL25ob2IxWjlWb3g0K1g3SApZdVIxTldWVWs0aTFUTVFRYTJrSGt6UHBlMi84bU5BVzcxZGphaFZMejBiTFhnbC8vMXFITGdTZ0tGQUJ6bVByCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXBiK1RwUzBqWUhVakZkK0dzRGcKY0FiM2gzRlh3U3l1aFhaVWxvdDllV2VqU1Z6T1dpdzQwTFQrSFJwc0QyL1BBZzJob1l4aEZmdVdsNWRwMmMwWApSTHp3eEJTQUxPbWpiOGVaMDdmdDAzMXhjaVN0Y09YNVVLRHVwcXM0bERINWlVSi9adm1pZE5hSjYxRkFQa0tBClA0QkxBQWpzR24vZW85SDhYMUtVc0h3OG1vKzRoY3EyYXVUeHBpTkRuQ0JHQ1cySUtWcEtKZlFWYWY5N09XeksKaDFUY2hzUXJLUWFRTktEdTUvTXJpbWs0QTdmOXN6QjZBa3VlUHlKMnFnRUlOZGsxSEgxdGxzckZ6bDA1ZlhvNAp0VjhVWGJZa3NlS2tpSEFYRmhCSVczdHl0cUdpZ05xdTlrUUErK09KRFk5aE9VUnZaSWcvOStzZGk0TGp3R3N0CmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmY0azhSMEZnblVCd0VkL0VCSDAKbWhwOGxJUXJzWStCVEhRUkRqKzlnTW5YY3hJRDZvSDBkVVh1a3J6RS9HblZlamZPbVhKcVMvcFdkdVZOYTQycQovWEJUYmdMYitoRWZpc0V5NVE1b3EybUppTitMUmdMVDNZRGhMMENNeDNwQms5K21vYlB6dHkyK0w2QUl5L2JLCk9UUklicXJXanJDanZKRGc3S1RRKzJMOUZBc0ZvWmxIblVXWjIrdVZNcThyNkJ6c1UySTZzUCsvUGlRbEczN3kKQ2MxNTFSMTJSYUdzMElpMHFoaXFkWnA1ZkllRnhhSkoxVWpaazdXT3MzSkpFSmxHa1MreXNadFgyR3hwdElHRwpTaEVid1IxWTdOK3B0K2E3Y1VJdDhHUzFIdnV5Z3c4aE84OGRpMVgzNFRjcW1wbVRWYmQ3RERneDFxTlB1d0FIClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2hRNkVnbTJZZ0tKdDNBMy8vNzkKeDBWdkZZNDhHam1tc3RrZmhEVXpRMTJUdkxWYjk2UkxNRm41bysrdG5wempwbXc1V3ZmbDhCakFEY2tWNzMyZwoxalVPQzY4cHl4UEI0TkRBemNxSHVzU1dUQ3hQb05lNjNrN0JqcGFScVBucGRsQUo2aHg2VzlpYUU3VURXOWxqCmd6Z01sVGtDQ0xqbHN5UVVhdUVRbDc0Vm9oam40dng2U2JoWFhOdm5SMGY5UndFL2VPVjVnZlFvekEzQkFJTWcKTnFNK1hhSUZOSjhoeWlFL0tEL3lqd0EvNFZtYmg2L2FmVjAyOE4zcTYrUHZhZkEyU0Y5N1lkYzNpSUkzMVdZawpudXl3TFgzNEF0Q0hkN284aWQxU29KbVR2azNrYmtOTDduVHhoYnFjMThGcUxva0Q1eDkxUmp0ZmRzYXF4ZGNJCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzZLNkhmR0Z2L1F6ZGZ6UDMvUUUKd2diWUhiV0prOFNIZVdsZk9rK3ZDNDZKOE5PMGNqZk4xOHFlcWdvcWQwWVhYMisra0V3Rll4VldPcmdTZFkzYwpUWnBpMCtDS2JmeDVBbkk3L2huZHBoL1FsR3lrWVpIWUdPVkM3Vk13a1VtQjBOQUdONERjelVtWVN4WU9PcTRwCndUTnFGSFlVNEgrNlU2aUVDSWxHalNRNnV3SFRHTDVtZnRGRzdVM2xDUUpyUFJNRExQejZCVm1wa0JBdVA3bGsKdVFsSFovN2RsRlV4T0g5VmRMZndYTHF2U2FRTCtxOE9sNHJVL3lMU05Gc3l5VDY3d0R2OEE5YkVRc0lqUklrWAo0K3ZsM1hNbitLcDFXZDVibnNmVXgva3ZrRlJwd0gwL0xjTUVXdzFSMmc4dXNKR0U0RTVCMWFETTVSWndJd0JFCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEJOMzFhQjVGcGZ6SFU0eG1uL2cKcG51anlyMFR2VUNjaUJaWUI2OWYyQmp3UzIrcHRlU09EK3h2eHFKRlI1N3lqbXduNVozY0phQVlKUWxDWVlyNgpHQ3RrSUppaXpvcDFIbk1sdkl1ZHZPRFNjd3BHOWQ1UEprNUJMdjlISVY1Vmw1ckYvSHdSeWFCVGp4ODFzSEpBCkFpbXBVblZiSEFPZmNjRWl3d1d0Z2lDdXFVaHdKaVErd25yVjE4TU9yNHJzdHBsd3M2eklpa2hwMGtPYVlDYm8KSGZabWJITGxNZXMwdENVcDRQOWJoemdvUlBPUUZiTTY1OEU4YkFDTXA1UjhEMGpET0NzcVczdHI2WW40NVM4YQppbGhiWnJHRDlRYWZNb24wS05hUlZVU1UrRmliUGQrTm1ianhjeDljNHhrR2EyaGlVRGFRdWk1NFlBcklscnFJCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczZ1SnFjNGNtbStYeWpWQnhkM1gKZm1VTU1SL2RKbExTdDVkT3RsbTN3azk2ZEV1VVVpSnZwVG5WdTJHSVZHU0k2elFJUStoMGw2clNwa0t3QmdLSwpkejMyMVNveUt4UFByYk5PYjJqK21qRkNwbnc0L3VZV3Y4MEV0N0tiNkV3TC9jd1pIVS80V2lTbnQwekxuaXU2CnMwVjRsOU1iQjkzeU5jZ1Z2T2NSUXh6R3Z4bytkZkhwTWM1QnZlS0VQVStMVXZLVWFGZTVWOXM0Q0Q4dUw1WkIKbnNaRWpTZ1JoMFZUa1F1bUxITUhIVzdERm45WXYvS2MyRm1UMkwzbEQ3Znd0MHNKYVFJUVBxUEo5amQyYnh4aApZRDBHM0ovbkFRd1ltUGhzNTQ3WVk0c040cUlCdWpsOWFOaUtmdHYzS0hxODVKOVo3NTBFNmprSnZ3R2lZVUo3Cmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmFUNFBlcmVid0NvckQweFh4NGkKZ1lwSmlTMkdTSW11S285RE00UHRIWnpRa2ZtVnVxWnFqZEhiQS90a3pNbmRIakJ3TzRSdTZNQnJnT0pCTnVhegplTys4UkdaSjlmZ0ZKOTlkZEtpSVEwb1ljVDVSVmFPTFR4UkhPSm9aODFIVzR2aUFzcmxkeHB5R2t5YU9ua2l1ClRoRkd1NzdCUEJzYzhSV1NZbitTekdiQmdFcVVaQVVzbkwxd0hhQWswZWFJUi9meEt5Q2hCNERHZnMvUFlYZVcKbUtnbXRxbTRld25vbFhBcUhTOElwS3dHZkNnSFR1NDYyS1lsNnpmZzdQUlZlOFhEeUYybUFEdmZGL0l0UUtMSQpRQXBEWWwyUGdGSnY2cS9jQXc0K2hwVVdkdldsS01XUlZlU1A1dk1CN3ppdUJMeDdTNkd6UWpNeFV3MHI5MkV1ClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejR3c1pGbWxOV0tsK29vLzNyYjEKQ0hpcmlHZkpMYmVqeHh2MHNEVUtKYUVPV2dnaUd6dzAvRjJCOTJZeFJ2Z0tjTUpTYTRQYXpmV2pBemFIcDhnMAo0MWtkbjhpUTZ2Nkk0Ti9VQkFrRmNSR05LakR4c0hTQkk2dkdCVmlsbzg0Z3NJcGVncnRML3hYMXVkbnA5WU8zCnFBc3g5UmJac3JkUzUxRUVBeFNvbUNMdDhmcjdxOXFNOGNheSsycUoyMHFaZnY2ZFhnOHNZNFZIK3dUOWttZ3kKR0huQ0R4RmV1RXcyZ3hKSk45Uk1GU0tsbWVTQnpFTVpUNTZzOHNjaGlwUVdWaHNwbkxHWnN2czdUaHEyenp2MgpWSlJJby9MRTlYR0E5VGhwKzdWOEtud3hiUjZnZzZMWTYzM2hrY3JERFFseUtUV1Awdy9YVkxERXhQU3ZwRXNhCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXkyVG9LS01zOHBkbGtuZVdzcFkKS21qUDEvK2YwL3VyZlZsMUVwUHFSQk5mZVRSdXg2RC9BUTFyOW1ITUhqNmRMQ1BBaVBOOVh0M2J0V1NWVTMybQpjUU9UOFUxQS9hMTZ3dnBZQXZZcUJLclpyY3hlQmxDbEJPZndzVG5YM0duNGlUNG0zM3hZTTVLQ2RTSEJlWjZJCkt1L0F2RDVaU0xKektULy9rT2k5MmhQaEcrckNhblhQdEZvVW4rK0x6ZGU0SWtrTy9wT3ZST0UyaXRwSUI4dFcKNEhGdUo2L3BwNFh5VFNqT3lPc3RJZjVhTGxPQy8vMkpBWm9sdmVSUWFqeUdSNks4SVAzME53UjF5Mi9mMG1yTwo5K2pFbWd4UUxZNVJhQktUTlNDWGI1UXU1aWJpcnpINEV5Mml2YlIyM2Mzb2k4WE5yYXNTUG9MbnJKMDN6eG5OCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmpmd2hTbUo2cDdaZDNyUGFaeG4KNUZSZXhMMzQ0Q0JXZ2tQVjVDclBSUGEzN2k3eXU3S0VORS9EcEdaQ2xyN3Azd1NSYnRDOTFCd0VWL3BKZFVQVgpleWZ5OXJyYkhEczdvRUdzUzZLZWo3VStISTNtVDJqeHhPb2EySW9KSGlReWswVkU2L1hpR1F5Zk9kalJHaFd0ClNlSFBCMWpFTzByLzdJWVNTMU5sM2M4SnAxNTUxQktlUUtlTWk1VHd4VlRSdnl5MTNQa1JHcHk5S3ZITi9iTUYKMFB1d3Uxa3NiME1GVjZ5d0didGRONitiWXJQNHlRRzdMWDVjd0RYY2tlUGJXZVBQNFVTWndvNnAxVmVZZktRNgp6RG56Tk5mUHdWVXhuTzQ5QndTMURoTTZJSCtxaFdKUHc3QitsOStVNUtNZmxyTW96SGtyU1RET3Fudm9lMU5lCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlZjZnYrYW9qdWNIaG9Fb3lpTVEKQzR0blA2dlFBZU1lMWRMYitwSno5M2hDYmovZ3hBUitwaXIzQytRdFpzekFoSlBvMzF0N3VRdmJMN0NubTA3egpNWThiUStLOVhFdGFSdTI1SUhJWWN3S3AvM01mSGRvNVk3dm1PNXFlT1dpTnpwekN2VDZkdlBNbURqdzAreVNQClo3Mmk2Uk0vK0Y1QS9HaFB2ZnZNdnlXa3FEeXRMMGhWRFFUcTdvMkpvZkJQY0trUDZmeThDQWxpQ0ZtTlgrZ0QKVkE0TEp4T2pVekIxY3JvclZkeEQ4S3NDQjkxZ1RjUk1HL21ETHVONXFWRWc5RUdTbVNkVUMxQ0hUNUplTUllVQpMWHk0a08vSG5zSitiUHVCN2JOTXV4clFXYXhWWWt4aFJUOXVMbGwvWm12V2RHTmRxU0phalM2OHR0d1R1QjFyCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0h5L002c1hhQjMyM1pneStPNHIKTXM3Tkc4YkhUM0xLN3U1SWJxYk9aNXVuYUpzbjJOWXRvVmlsZTZWWEFVSXR4M0kyOVNzS1EvQzhLSUVLTnRWWQo2TFZiVjJWejZmM0ZoUHhuQmdUYmoxM0l3ck9qRHlsN0E3ejRpeUFIUE1QL1N2WUloOVhGQ1VSWFFOVEdLVkg3CjdhR09DKzZENjBndUpRVVJORTlQL2NkMkRnc2R3UjAwYm1YSUF0S0gvZkR2TStiTDI5YzhtMTNWY05oUG9IWEgKYXlKVVgzRllTYjlSYm9kWXNlNkpSbzlKNW1TQ1MvSDV0TTVydTM5SDFhV1plQUhjVlpaTC9iMldCbUdtRnZXSAp0QXlDU3hNRzIvMDNpNzFFUEhnQzJ3WmhTdnJxNnpyNnVZbllpR0RHRThRbnZKR2UzbTdraWhFcEExb2VabGRwClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEJwVlFVZ2RKMW5qTHgreHpVMFUKR0RsUFExOTQrU3lJZVk1Umt5ZlR6eVhrck5DN1YvZ0V5WlRvbHZHY2dUeWc1SGNocmtsYnNqM2FYRmx1WG1IMgpya3JQczA5V0t2YnNUSnFzZ2RWeityQzh6cVllQUdMeXlWVVBzQXdTRWdLckszU3VFTXlhaUNwMWpBU1hSN0ZvClFSQ1JYQmRlQ2xiRVE2MmFIV2svaHd6ZzJqMGJDTFd5cnMvdCtFdFV0bHN5Yk9hYXlhOGliYTA3M3RJaTh6UkwKamY3amNRY1lVYzVHa2R0VFpTVy9UOEFERW05RFZ4aTdKWUk1Skl2SjVkakRrTmtjdytydSt1aWp0R3Q1bTk2YgpVZ1lyS3gzd3l0dkRTT1pzd281SkRpc3RuVUVnaHMydnZlZG1ubHltclBsSDJTVEFkRHBLZWdrM1NqakVrUnpECll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGF2cFlDUzZsOEpsYWo1QnJQbkoKWG02SmtNQlhpSU9zb243eFJPV29SeW43alByRzQyYUZFVDJlSG9Bak1jRmJUbG9YRi96ZHhxSk1PVUpxWFAzTgpoSDBtZXhGdzV2YTZVeFV5UTNKMHl6L0U3Rm1lWk12SGxmV3o0b1ZMSWtOUXgwM0xsd0R2NDlQTlBvZnJkYVluCkIyMjh3aWd2NEJ2MVFxeWdCKy8rK09EZmovV0ZzODdXaGl1RFdpRVpEUHB6cExNYkNMMjNoWHFaWUdna0ZyK24KVXo2K0EvdmszWUtxcmd6RXVCaTNLMktUdU9pcWYzVEFNMFp0eHNRUnJzKzVUZGM1SnozbGd0b05tUENxWkE3VApQUmc4NlhONWRiYnBydUZNQ2ttMzFZcWY0cVB3anBmN21ZcEkzWlljVWhDZzdzR3lzVlo2ZU13c0RDL0RyNUtWCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbS9mMkZZN3MzdVlsdmZUNThRaTUKSUhJVC9RVE1kSVVhN0JqUksvWFJKS1kzYU5HRWY3OWtReVZxNlk1Tk9PZTQvbWVKUkZHdnpCNDdleUU3WEMrWApjVzMvR2VSM01rUHJBcUJLYjUzWlRHY0NsaTFpV21LSnNCajFETzBBTDJuSTRwc3lxWXFvOTg1MnJWZStvdmtPCmNzYVlUM1B2NEdPYUUrWi9HMytLUyt0bC9HaytJdTJJL3orNnd5RW1mU1BTaDFhZi9qSEw5VU5SQnIzRmNVMEgKQktnL2UybnRSNWVVMHNEcUpaTFJkQzBNYzZYaWdSTldsREZtYUxuVmMvSSt3aHhpZVBxNlFvbFBiTXdDbTFpWQo0MFRucTlUYXVEbFlCSDY2bnA2cHd4TjVmOENXRDV4VlN5SEs5TlZKWGUva216enc3YmdzY1JBTUNTSG8xbHBrCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3U5OXl4ZlUrYjZRcEgvTk1SWEMKWFl6MkUvL3ZBS3R6aEdDM2EzTnNVN3RWSjBEajhNNVRGd1c5UDZ5U1NSUXE3cElIbkQrVVFsN3d3WnpMQ1FocgplbmkyUnJpNUJ2Y1lmK3lLRkVnbEZEdVFkSVFLU3dtNFAweG1lMnhrZnhjNzVXZ0FWNXZWcXFQTHphSmMwREtzCmtUZWJLOHZSbWxKenhzRHBUbS91SFMzRXY5VzVnb1dmWjVXRmcyOUxhc0lqVVB3QUgvMVRNUnVYWHYwS0p4VEYKN1ZKbjY0Mm5YY0ZLcmR4MEdvYVBJYUNYajUrZ1lpNm50MGN0NzBjeWtNVG9wd2pOU3NyNlhCRkZCakRHNkYwSQp3Wm1QbDlXYmN6Y1RJS29OOGp4ZjVwZWpsYmgrMU9RMEdFQVV0UC93ZndZZmxhR3BPWUpmNFY5Yyt6QlVKQnV6CkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGhBT0d6dkJUaWhOT29kWnJydEwKODEybU9zS0d4d1Y4bThvcWRxVFJqRmdyekJhc0pSbkRvbno5N0RteE9lUWgyQUZUbC83SnpKdnlRcWdsYkRjUQpOd2YvalppOTRmc0c1UVR5QXZhVEMrME94VnJpSXBqRjdwWnl2aWRKeGc3aFNOUUw3YW96VlNsRUZJaFZtcjFSCnFQVHRUbWRybkkyY29YRUhYKzBKODF3ejUzbUVPMG9wZGJtZ0QwRWxwOXcyemoyTjU4SHZXYkUrNnVSc1NUZXIKUTVLWkJocmtCSXJkSGhGbFpxc0JrVGM3ZVlXOWdlci8vU0owVkNqYWV3K2hvSTYyTlNSSm41QXdKQ2NRbDZodQpXNUQ1aUZwYzdCTTFhTCtEMjJhM1lPM2ZEelFqTEJNNTRiRytLNmlJaU52QnhhVXlyRjFRTzRvQXdrV3FkNXVLCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXd0aXdrNTJsL2RSWXVZY1Rrbm0KUUpMYkVXUVl4YjA2VUdjYjV3YmdCL001S0Y1YVNGdGxPMEo4SHF6bUl2MzVOTW9DQWJXMXA0TE5kKzg5ZlU2TwpTOWZTZmNaQ0FwRm1UaVlpNm5FTGh1eDlFbXpBMHpYNUlZSFV3TTZWTVljcys4dzBOVzAxaGlHRmVJZU0vb3pJCjJMTkxneURXbWhFMkRlRWMwVGhlcjRtbHVOQThGWUpnamhKR3RjU2tzYXVFR1lHVFlYS1kvYVVtR1ZxVEsrUWUKcTQvV0gwN05qNGZDd2JRKzJIdC90MDJxck1FUmdIYjkxRFh4M3FvQ0xIYi96Zkc3TjU1cUZkTzJITnFPNzJKKwpaazFoN0p6R2lrZktsNjdjcU5ZR2R3TFh1SFR4SG15Kzc0aVJkOGlNajN2TUgxMWZHc1c5OFQyalA1VlFMeHI0Cm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK2h3WVNLZ1FKazNCd2JBM1JocjcKendIQ2QreStJdnJSTy84MmlXRXpxR0lCMXRhUnBneUFHRlBPeGpsWHJGZGdFSE9uQVVZajdDSlByeHRxOFNpRgpzNVdCdHhwZ012VWRSaW1heHgycTltcnU0ODFaQkhrM2dMRkhZR1VzbWZrelFtODE5RnZxTEwzeU50S1d1QWV3Cit0bmZBWHZmMmJWY1NkUXVxOG9IMnVDQThibG5GdTNvTzEvemMvNnpVVnF0Z1llZEUxNkxxck0ySTZib1oxNVAKZCtHSmlFSjRBWjFpV0tTS3o3MkhDaVplZ21TVThkbWxQVVAxUTNFV0FjWnRhQ2poaWd4UUY4WDlOU3IxWC9pcQpsZy9kQ0V3cmFCSkpuUlg5aVdjYjUwV1dRRlMxdGZIbm5kKzdKMFdGSVdjbGZRWE55dys4dG1FeEFvVlNCN0pqCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK0NLRWRtK2hFSXZEaFlTaGlhc0IKbkI2ZWZWaVk3RkIraHVIUTdSc1JVSXdoNmNrNkxQSzNGcXQyWHBWQWo1SWJ5Y1Z0SmZNYlZyU2RtR0o4d0NYQgpUSVoyR2hiZnJkL0Nockt5b0c4OWdjeW1aTHB3SDVoNFN5dnlyT0FXNU9OL2hHb3ZCZTRqemNXK0xSUml6QnZvCk5HODJzZ2hUUGg3QUI0aW1pSXdKR1EwV1BZeTljcUFuKzBSbG93d1pBL2xpUXRGMmcvUVAxOFdacDVWTVdsTnoKdW83Z282dWxuYlh1TUN2UVJvR2RmNlY0WHk3eFdMMmN1czhwQXZuclQ5RHVLdTNkVzNrQkRCVDBBaHNPaVo1bQpFZjcyTUNpblJMT1haN1FGRXRjVk5kN2tINDdpbEVtNlByZ2M0Mm1qc3U5ZlFqUFVRdWJRc3RpL29pcjI5TFpwCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlZWKzFzM2ZmbGN6cG1OVi9Ta1QKU01WWVFvU1BjWGpMSUJ1MTg2b2dXcHhKU1RORSt0c2lrZE9oQmo5Ylh4LzYzLzBuclpOMmNYR1M0alRMQVkwYQplRFJqS2tKb3puZlVvNjd4dkhZS3ZXUFp6UUFkT2NpdWhnUlBXUVZpbk9kT0RGYk81THVyOFh1RW9Wd3pTN2dTCk9oQVdnTzFEN21oelUxOWJlY29XclAvNGRKZ1RwTTNPZkdJSDgwd0J6anJDRnpZWWsyU0RCMHg2SU54TlhLcFcKU1JvUHV2UlBvY2kwTGRzZW9vUDNWMEtzd0FrK3dYa3RSakxHTlNOVlVvRUxFangwUGN0TmF5MUdnODFKVzgvOApCNEY4SzJmcFlGUDNNNWJhZklYdzdkWURkRDRPb0FkdVdJOTFnNEZSWW44cUhMRXh5U3ZtTEZQYkNreEVPV3ZGCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1YyOURWVC9zcXZiMnNXZUVYa0sKQ1c5cmVrMVptUVRLV2hxZkhXRzUrWHI1czlNR2kvV3I4aExiY25nL2V6SlpicEtPVmNTTFdvMzdmbnZvTDBYegpSQTZ0cEtFcmhJcmRYR2RNb1BGMGVWOFBhZWNwMkhVVlNuSFV0b3BtKzZEdVM0RTM0RFM0MXlyY0tNZXA5SUc5ClE5dWY3WUhDdHdScHdnU1FHTFFqN3lTUW0zWCt2N3VwQUJSL28xOEJzdmZzc3BmNEYxbFFMK1ZXd3NnTG9qSmIKVFVHbVN1MXhWOERIM3R2SUxUdWtBQWhFeVBqWHMydDFHVzNKYVNveVNNRGpET2pMTG9pdEZaWGttaWduWjlYbgpxV0ppUG1JTWhzMURxR1ZoYXdFYlcxWmJLN3RDQWFCMENIbHIrRk9ENjNNSG02eWVnZTkra3dQL2tHZzA3ajdxCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnFYaTJ1WG95dFZuZUR5Mnl2MjcKbG9VVFE1MlpLd2drU1dPSElCMytZbEUxM3Zyak1RWnRYdXZwN3drNldPa3NVdlIvQnJGMVYwZzhiRkJjbUxpNQoraVN3WDM0bTNEUkh5SXBpQkZMS3F4ZGlOVEdsOHJDbW9Bd2VqTHk3a1Q2bHc4QittVVkvOFU3TVVMWVM1cDE5CkhwLytxZnFzNUpXVHJ1ZWU4NFZQWEp4bnNRbUFtTE1PN0VmWkQ4QVdlQmo2SDUvMVFIb0g0YW5YL21HM1c5OXkKc1FYMWdxRDhhbjlvMWRNbUQxdXZpczVBZU00UkhRK3h2RHlaWFRjaVlHOXRvNUcxWnhRc3ozakZ6aTN1dE5RWApvank1Zy9WRU8xMWJkVEZpWFo3eUhwaHJydUF3NWladmxVRms1OXY0eUFDcTNpdFg5VnlRNXV0cFFqREJvYWJrCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelN3aDg5ZWxMUVFIRGs1aEFnejEKNnlPdkVGdStjQVZyc3JocllyT0tYSjRMNWQ5YU5wdDhOb1lKYkxBSUxROWViY3U5RkFQRWkzd0llR0hUWmJSMgphYUdNOEFQT3pqYm9nWDNvdmowSmZ4Q093OHJ6Zy9vNVpoT3NuMkR0eU9ITjczRHlIS09LdGlFV1hCdVBITktpCkg2R3YwU28waXRQc2krLyt1QVNkSFg3YzlEcUZ1NklNdDlySTRVU0pGOXpZM1l4YUdLL1VLSmltY0RMZXo1dlcKb01vcFNrWk5LZmg3RUtKN0Evem1VNXNNeDZaSGVGZWpZalAzSW9yY1paQmpTamJVaDB5OFRhVFZtQkgxTWwxQQpPTzU2cmoveXhTTWViQkxmd0xlQjlNcWU5UjhKeU1UVkFoYm9IMTlTZ1BBYTN2VHUvemlaWFkxNE1jOGFQT0FnCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFNJS1pud2V6VG8vaSt0ck9wSFUKWDJxZnlqbHhPVGtKK3FlMUtFL3VJNkh2Z0tRYmU4RzA1SG9XUTNnME9KWE1DZmUzeTRSbG52bXVlRjFsYkswOApYL3M3RHc0SVhENzR3bEk0T01LbWh1QWhGNVFGbDZKaFB0SGJWdlFocjJYTFZjL05UcHZ2Ujl1T2lLMTFpNVY4Ck9FNDl6S2ZqaG9ySGthcU5vWWczTkxsVUFoYTdRN0lGTmkvRHc1SkpLcWFnKzhmelluZFVwTThVbUFqZ2J4d3YKMmw5U3BVaEJkeUIyamV6OUt6MGpjWi8xdFR2RmYyQllrYjJGY3E1MW0rZ0JGVnRJMWljZjBEMVJRYk15VU9PWgpkV2xwOWpvUkN4ek9qc1IyZkczNGh3TlJzdDhvWk9TVjlYL0JnQ2V4NXJpYVk2dDRXRmplNVFTQVNCS1lFeFQ0CnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2FQZHk0NXBzZGFieHBldG5BaVUKelFWVkUreGpOc05tWStESlNSU3JTdFAwMkhxOHJ2ZlZraTJhbWEwQ3VNME9lRVZEbnFpUm1ncjJqSEtBT24zZgpZa3o0RE5UOFVNK3Z6QU5rTm5yQXpiUzU3enZ0UTNhcTZBa1VFQzdqUS9INHBZSGpSVWFZSUhpMW90OG9acGJKCjc4NytkZkdrcVJjbi9Nd0Z0bDd6NXRWQ2dkUGR3d1I2M0hFRUovZzEyY1VyWTZrQ0VQb2JLdG9JejZGVVp1RTUKU3VtWGtYcW1KeFFJT3VpbGh0b0ZqNEZKSlZwTGJkenNOb2RRdEZEbGxiWkZNTTlBa1YrMGhtWG9qOWpncFRDdgpaZngvZVAvSVlkMDVieEcxNXAzeVo4eEpTeXhGank1by8yMEt0SEE3dEJoM28xbGZ1MUpyZkNkMTM5cVZEREJOCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWQyMjA1NUZwdm02eWFRWU1Na0sKaDFKN0dOTEFKUGxJTzRDdlc4dVVkSkJ3MVltQSt6M0JYdDV5cXJoNXlnYXVuUVg4dS9nRVZ6TWpFS1YvcUhldgpndHhGS2t3ejN4L0tUUUFoZDFaaVBKU01tZTMrQXFKYXRjdHYyL3h2cTNUNnA0VTlySnk2aFdCTzFrSU0vM2VTCmtQQm85S2hmSDVobnJPLy9KcEk3NUJvVVoyYW03SWhnTWRJQ0s3ZHVmQ0wyZWdBbE05L1YrbFZxaUFPRHE1ZkIKVTNqWlZpNHFsek5NS1U5QXQ3cHZ3V21WcGdReVVpdDZJSVFsVzhrUWVJam93TTlNYi9ZTU9BSzVNRHJRZ0M1UAphOURoOXdMTlQ1QVdXd2N3WEpWS0lFc1dMS0J0RVhVdVQ0Nk1jOWRLaGV1a3lIK1p2LzBwUDFzelYrNXFKdDE5CkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWFFR0NrelIydFpKMkExOXhRaksKZ1paSWtzVUdocys2YnlWTnNZQXZKdjI5NXV0aUptcklJSGFpVjNiQXBDMDFyNVlWOEIxYVRqTlBrVGdPWHVYawpaMnZiMmdvd1pycGVGbVh5UWhFZjhVaXM5UFlrQVdKQkM0MG8rajZhekVwVHZoeTZwbkxjTE41TFdNZjIzUVFTCmZLeUE5a2FKcnJMcG56QThtY3lTSzFNLzN5Qy9wRyt2SzZQWVgwaWxnWEhPRml6K1NYMW0wNVVYNE13S1NGcVUKallzc0tiNy9QR2tjVzRUNDByc3BLOXdLT2RXRVdqc01ENWltYlJUeFcvL1k0ZmpVL1BKUVp1OVlQSmxUZXh4bgpYMXFVZzZGdUIyd01aaHV6N0xuNCtNNTA0Sk1WZzYrQU9vSEpNTEZ3d2x5R1BDbE9udW12b2MyVU9ybU9HTTBmCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnV3bmNmZWJWOERyMVg2eGEwcWQKYnhHMyt1dllQRG40L1J3SC9BYlUxeUNMTUlLOCtEb0F1QUpkZi9RczdYTG8vMWdNOWZQVXRVSEJ3akNFaVNQQwpWcVN0akJzYUh4LzVuTDlYdzJnZHZCRisrblpYcFMzYmovbzA3cW5YRGFxQTN6TlNYakxsUnhXSzFzQXMxc2pTCi90TnhQL3hWS1J5Rm42VnhQSFVURVlDenl3RlcvMTRYdDNDcWM5aTh5OUN6eEZMNWloUGhnTmU5OTh0MnFPaG8KalhqdjBiMHdkZzNqVnFFb2I3SlVFV0dHYUpNaGF5cUtsWjNNV2duVXZKOGMyUTB6RVp1SjY5ODNHZ0pNV0lzTwpzL1NCM01qdUJOZkM5YU5KQjhnczlObW5jOTk4THZxMFkwWlM2L09Zc0xFRXBGTzUzWi9zYno5NXJ2WU8vUjN5Cm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmpQUGJLc0UxaGwvUzlnSmF0Q28KSVJtNFlSNm96dE9PUll0SnNBNXVROENOVzNYVmIrRjFqV1Y0Q1RSZFRua1k0a3dTN3RMWGU5ajA2SmYxWXZCLwpiMFdKWW55Y0liRnkweTVUbU5BRVJxVXFNL2dKakVNNVZDVlVmZmpmQ3IwTGpRY0hLeXM3UUkxSHRidE5VOUtTCnFQV0lPeU94b2VXT2NhV3doRnhOUEtWTVFsOXRXTHpCQlV3SEJLWmo5RXdSLzdvdXN1TjBDUWYzMFA5TlB6YUYKQ3hwNkROcGJuRkRDYklyNWdsSWp6bEtCejFiN3E0TndSclp0bkUvcmNmUDN5WmV0S3YrcnNIbXZLVzlleFlIOQpiRTNvb3FYa0kyeUFWTlhnRkNET0hrck54bHZibWRSOFVwOXVadXVVc3VGc0oxbmtWa2lYK25oZG1sTlJHMS9qClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2RYamlUWXJnWUNkVnBYbVFKYnQKTUo4OGg3T0pHVzMrWm4rUEpIRVMrS29saFFLMW1aajczNXFaOWZERW1YdGV3NXVCalJXZkRjMFZmUTIwQkVIbwpzZ3hHWTJyVFRuMGppdVhXcVVhOWtPSlcvbEVZcDlxeFl3dkFxV2NnWnBVZmhyUlB4UW9kNE1kbkRKN2craGxoCnl1UlVGYU9ZekczMzZYUFNwNCtHNWl1dkdNbjFEWlM5RVZhamZEcStwbFNNZ3VoZVhGL2VVNklJaWM1WHIwRkgKM0tUa212T0txNG03L0g2YzFZTndxMXAzVjJoOHhQQWRCZW1HZU8vaFArQXlvMFZuVUV2aXhzdS9RaUZkd0VlMQo3bDhyaFk3RXpFWEtvdThGeXZta242YXIzRCtuVm1JN01tMXozVEFJa2R4RzYwR0tLek9pWVRKUk41Mm9UTDZOCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG1qMVhEeTU2eDgwZzUzY01DaXYKUVhGM2tGRmZnQzdTSHVydzdmZStGb0NSZUNCWjY3NzZsTVd4R0d2ODdjN0MxOE9ycTl1R0NwNGY4TWRxNUVlUwpCOWQwZGhFMHkybk12NXJMeHE3bzZrOGl2S09TT3dKc01KUkhYVzljZ2h4eGcvMndCU1BDNjBIWGlBUEZkUGhSCmxRVXJqUDZMV0JoSVUydWhXNnY4bFJoNFh0WTVFWUpER010VUEyNVBTZCtYeTVOMFNxMjVvdHFRWEhJWUhzM3cKNDViWmdZclNoSHFaSFVLdGgwOHZTai80UTFkQ0tnaEpBejRWNmw5Nmw2dzAyb1VPTS9PN1VNQnBXR3VZaThxUgpscDFYQ2ZkaC84R1NQN0lqWDBoVlUyWVVvVXlLOWprWHo2WnVCSjlwWDVIMU5mMjF2Q2w3NzZFU3VzVHpZTXFJCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckh3dXBoblMzb0k3RmQ1dW1acFAKYjY3SGt3aklaQ3kvL3lWc2FqNVZmZjZDY1hOV09ITFVpQXJrSmpVWjZTajB3ek5rajJscVdaVFJWeDdWYlVDTQo4aGhHb001Y1RpKzlMYmNKeDlUWFJYNW9xUytrbXJMenhCRFNvUnZoOHJsMSt3cFdwQk1OZWF4SkNCVEF2M0VTCk9pVmw5M1RKcE5FamxOWjl5VFRkOTltd0RmUE53ZU9nT3lXTDA5N3ZtRHpDQXV1NkdSOGI3Y3hiT2VRM2hZKzYKNVpFWUh5UU5CektjMjladjhEUERUUnp5a1MrM1E1Q25hSmg0bytjVTdYYmJOd2NtV252OWg2aDFjenhrb2pucQp4M3VlQ3p0R1lTSndhUXdHbDN3UFlpZExRZ1dlNWdiUXhxVTZXTU56Szg4NUhHOGJDS1dWSFg4d3dpWFowZW9GCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFZjY3VTMEszR0FmbEdmdS96SXoKbzYvOW5NbnN3dHpyWC9aQmtBKzVGMDdDTGI1dkI4b1JyUjJ5cjBJUGhiQUUrYU9DK0NSQ3hHdEhDb1NGTHo0bgp6THdiYjN4RmplcEN0eWwyRTQvME52Y1hCR1hXaHJRS0IxdEQ0NUw5eE1oM2hXZ0dzWElHbERJZ0pTdm1kTTU0CjFlZWt4QzcrTEVYNVZWMEdKZjhidGgrOE55L3YyM25jS09tajFJS3ZtMlFNd0lIOTg1cml3a0lld0E4d0pGRDYKcURqN01KaXZzMlExWjJpL2Zwd3B6SzFIYi9NY25Xb3V3OCtyK0xJcVFURWlndDRBbENsbElZSThnUlBkbi9kTAp6MjBKSklSOSsyTUgyNDlwRnIvN3l1L0dZNTNpZzdnVWh2MlF6RjhwZUdrakJmcWlrRHFQNGM5UEJUaDhZeFhkCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmlSM2R5dTZNeFdYanJKVDV0aVoKcURSSTlQdWR2V1I5VVdEUExaNnNqL3poNm1kdm9pSjdwZmxoVjYvL3pyUEs5VW1Ga25FZEU4WXBDRUcvMmdlMwpScS9PMmdnRC9rL1Y5RDF2YkduVGdvalZPQnR5SUpTRCtLeVZrdk94ZjlENi9qTUlBdWkzdEU0ZlUzWWZwMnU5CkE5UEtIVlNWblVNWS96ZEF1eEVkWWJzZnJzRjM2NVdaQnBlVjd2bXFlVXdBeWlXcGF2QjUyQ0dUN0tBeVBIcHgKbjBsV1ZNSzVIdnZRRDdrbFRNZ0tRcUJIZlVjMkN5NnQvK1lmOXM2cndCQk1GazdnbGxXWnJBTTE0eFFsVTd0VwpzS0doTzJEQy9ZTUg5eVhDQlFBanhDWUM2UURyZmlHdkZPV3NrL2c4TWdyUlRnQ0tvQzNIMFoxYkp0L1AyL2NzCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWxtNEZaS1U2cDV6MDM3MncyYzAKT3Mwb000aWZha1F3djdyMUl0b0d3aU1QbGpJOG44aks5TWF3bEtJV3Vqdkw1S01wbnFwdVpJbVIvOXJNSlJCQgprYUZndWpISFBEUEwzMUZPelFaRU5PYkdmUFFsTFB2TkY5RTYzOWYvRlB6NERJSlFnVndUYmZDLzlzbEIyQ1RTCmh0cUVUa3IyVG4zUU4zVW02SUpKQ3p2Mm5NbHlaamxhYTlMaEQrNU5WZE5WQXRzVzdOUDUwMzRSVDBFWklGSnoKSXdORktOcU9CNUl2OUV1Tmp6bWhLa1BiR2NBQ0liSFRGVEpPVS9YSjlhb3FYSmtqN3lSNlEyNG1PZHdrL09jRwp4cFhKbEMycW5OcWt4Z2hCRTh1NWVVQ1VDcTZRRWJxZ1ROcFowRlVCaFp2THI0TURzMTVGY3R5QURQTk9KUUJGCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0xudFFRd2RIaUNEcVJLckdQOUkKSlZXZEh0WXgrL0hNa1RON0E1bjY4RGkxMFNMYUhLeDdGVFRIRUJSREZKV2ZsSUpGaDlpRm5SakMzTDFoa3ZrSwpyT21SOEU5RCtPRjVvUE1INUVhZVVhRW1HakdxSjVqZVcrSncyYTY1QnNZak9naTJteFBlNWtWZjhVQ1kzUm9HCllwNmJvSjVmdjNKUFgrSDVnYXV6bGdLN3FtaFR6amxaMnpMcHFuY29Mc2E2OU11LzcrSzRBc1JlaE9WRGJlVzIKQjBSMWo1OXZ4NWZvRGV1ZkRBMmNzM1pCcmRKdFZDRGFlRnErSndPVUhsaCthcTZSdW1vQTBkeGtyUEdkLyt1OApoS0I3ZVFKY25GaHRWZ1Z3ZDlzVnppQWZBR2VHWnpGeFo3SjNLeWhtdm1OUXdFQk5xbTZlcjBiMFNKSW5HbjgxCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1JEZ0VrcXd2SG1MbkVxcGNhdVcKNkNCMlI4S2YvZ2VNSVQzbXBTQU5iRitiUWlzSy9ZUjFjUFBuSG1PbGlRODZDdGwybUZNVS9ndDJUVnUyL1g3WAoycTk1NmhmRzZwM1hPcGFLTEQ3dnFxRUUwK2FnQUdjWDNxbVJaMWYwQ3hiZ241akFxZ0N1VXh0dFZ5c1d0a3FJCmZ0WHg1SFNpYXlnK1BLKzJia29ZSFRnSTFGNHRYSEx1MEI0OEdHWmFMSEprOWZQZXl0RmJKdnVwZnFtTDFtQ1UKZ0kxSy9GRjRlSVBLbFVGenNxeUw5TTBCWEVQZXUvT0t1YVFTVEh5QnBLdHhaL0tjOGpOS3RXMGJRak0wY2V1Mgo0dnFMYmtndGFYc0xPRi9GaENUMGlmUmpCOE1pcC8zNlcvZFB5NFpVRHp5ZDRUWllTRzFINVNNU2NFbDBVVXlSCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFFzMGFLRlRzd25ZVGc3QkJETlcKaXF1KzhhL0dCaWFMMWxwMHl5T0t3eXVRb0RnM0wxa0txT2pmZUt0VDh0Q0UwcCtTK1FTVENrNFVRSUNEMmFjRApmZjFZQWdCVyt3dGJaL2t3QVROMVUxajJkZzZGNFFHTFlNQ0RuMEowalo2WkdDd3ZDRWVnTlF4amdaeDRsYzZ5Ckl4ZUxud0ZzakFycWRCbTY3QmcyM3VBMmZ0K05GQW96cTd6YUs0bjQvd3QwdGlaTkZrZ1dhaGowVjRXVlJnSEsKamgvNGo1TDJYOWl3dXpTVmozOE5YMnNOUVRTR0c5NTFRMDYvVHNEbzlSMEgxdm5NS0ZsSWZmQ3RQNm96TWczTApWc1pCNStxSmhEWlhWc1BOUVdqcmZqQTVuOEdsT0w2aTlNbW8wdnBkeFB5R2FsZW9KK1dzaDNpYmxQM0lFS2dxCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMy9XeUpTVjRGVVlhdXd6aUdLRVYKVjVBSERSRXFqNkxZa0lJMUY5azF3U2Z2bEU4UjRvZTUycm5Wb3pZeTZ5S3ZTT3NJTzNFVWhSUnhXT0JkMG5PZAoyd09CODVQYURab3N3SkhnRmpNdnd0SmRXNzhFTmNVY25EdWgyMy9uUXpZalJHZmYyWCtlWDlxUkU1Z0dsYXB6Cmh6Z0JDdFlmZkd0Wjd5aWxsVjhSaVZUUSt6MDVtdUdMeGxpbnZVaGgyeDNuTWpDc0ZLRkdhN2MzL1liTnUvejMKOXVoenQ1YkxBWDZXT2ZaY3dOWVVxS3hIS3o1bUp2UzJQQjRob1pJaDc2S3JieUR2N3JnWUxXWGdmbkpkanBpSgppaG5HSG9FVVZBdVhmeHNmOGhsMjhlR1dQOWNrekx0c3p5eDZ6ZC9xVEgxa1loM2tIUzhNRlh4aXlHaWZROVB2CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcitqM05Ma2xkdlREMUtMTjhaeUEKek9Zb2RKUXhJWU5wbkFHWjd0aXZjb3NkTmI4cTIrcWR2Z0hTZDFDeXpLZ3M0Z3Y2RXNXSVEzdkwxZW4yYWdObgpOcTVPTjNHcWxHVHlUUE53TzhXQXJBTmxadkk2d1NjVWpjS1YxMTd3T1lpMmxBOEJmT0tQU1UxSStKN2lidGw5CjE0V1QrMWhWaTFHK01VRk9tOGEwUjVNaHd5cE92eUxRZmZkQXF4S09WZTRUUk5NQlowSiswclhia01qa2NZRWoKYllvV05YS2dJb1p4dHlJdkZSL1J5Z0hPclRCb0RkOHdnczU0K1pIQUx0aktGT3lsRkcxbEFHWjk4R1g0VVVzbgpnVlNJYys0Mzc1ZFpyRVQzWStGRmJxM0RpbU1BdG10TUR2RjYyWWZIQjBhRzRORlpWWjV1RjdXSmF5MjNWemIzCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3JRalpBRDZGUFNzOWFJQmxYNlcKYmp4V2J0UkdGQ0V4cW1rU1c1TFJicjNYUTlkckNTRGRMd3pQc2ZzVUJKL1QxVVFLS28wN2Z4elVabG9HcXVHLwo1K1ptWmRKT0p0ekpLSE9SbHVoNWUxOWJtZUNub09NK3Q2YzBvWGlSNSt3azQxRkRmSXVmVlZ6MmVGcXRxdWFIClJ6dHIyYUFJNHBQUWMrU1BQRXU5cGF0YzBxZGNuVVBMbnh5ZGlGYVVJbVNmZkRFbUN2M0dTMEE0Zys3cEJUU2UKS0FVb2ZnKzVUYnlNK0JJSUY0VWtyaWNrVk51N09zT2JZVmw5TnpuV25IMU5aSUl1Rm9OQUFFejArUXQ5aE5FawpKMTZvOXExN1o1R3k3enl3TWlFUkl1SCswKzlJSk1ZUURpVWluQVJwTGxHOTFIeXoybFdxVU5UYjBxSjNTZHVDCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG8xQkgrd1VVUUlKUnpSOVg4QTUKM3ZKYWVIWm9zZUp1V1hKT0kyNFhSYVEwd21zRGVPSkxhMkIxU1pOYkNMRUlpeG1lTTFaWWMzTUpEdFZBdElCTgpueXlzRHp2bkpBVVdmODZlSURERDRONWttMEZhTGt2eFo0bkJoVWo3Ukh5LzNiUzgxZjRDNmpUQk1aMHd6TkdECkNBQjJnTEZHRXdjZHkrc0VlaC9uc1pMZFhOUVdBTkF2SFYwLzJicHk5djZtdDBvUEQwaFRoRVJ1UlF2b0hkQzkKbi82WXJweHU5VlVkWVZDN3hYTTl1R0Jsc2F5L2FIQmo1NS9DS3N3bjV5VG1qTmRNVm5JeG8wOFdrNDhWODZVegpRZWRpUndPOElTWjNPVkJOdkdZdG5BY2Urb0pwT0NQc3B3blIvMmxXVDd6MG4yL1ZkN2xJKzJLd3JqbTl5L2xWCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbytrV1NPKzV0L3VWUnVVcmtwUFcKalgyajV5Wi9PVVdTSzJJMDk0clZCSlJqSlJ2Y3hlOVhvRjlQNG5KbmdSNnJNYk5tWnVIT2NDcDZhSVBtRjcwZAorSlBDSGxKS2hlTXpwTnRpdnpIOFYxdU9XYkZuNGh3REVsNTBGc0haL0FKaG1JVHpJVDBVNnAxckE1eHVVeDlqCmNXMStUUWFPVXVIejdManAwbDhROGlCankyaGhENWgralE5alQ1ODBLYjNXdHQ4VmVVR3pPMkxYQ1RoNUZmSVkKVGdYRnFYRnZXN1VvdXJiWG1ZSFVONnY1YjBYeUY5OXpMdXJkLzI4am8zaFB4endFMCtEeVZNVFhVd1VIbUR3TgpZc1BsUXd1KzdvQkxyZEdobkpxRGkrOGQ3Q3lhd0s5eVpxOTFkUWFneUFTMFp6ZUo2Y1VLVCtsVlkzMEJabjdmClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVlHTEc0YmFSWXYyU1BObWQveG4KenZoajFLR1hMRWxIc2dKVzhncGNOT3l4SHVnUUZXcnhzWFdwNVlpTERoeXVvVDFyZmUwb09TOWlVVlozUmkxRAo5VjJocGhrQXdGT3VBZWxXMWU5dGlUOGNnN0VNS2FsUVc4N1NOY0RCNThoRHU5Tmd3ZW5XdHdrcEtBR1dkdzY3Ci8wbHdaMUZKTDkrZzcvamdXM252ZW9GMHlkYnJqTHVGV2lTbzNyQXNhZzNYRkFtUlIxbnErOWRORXdSNGoweG4KUHExWm9xRHFnZEVORndMZjJXanpic3BZT1BheS93dlRyL253VWNwaUVKaFY5Rm53clE5VGRhajNZUzBJN0hQLwpiVVU0NU5hL2JvQ3BkcGFjSnp4dkxPZm1oeVRvMVdSYWI5dnhnK2cwUkUvbUxsTC9yQmJCL082YjhVa0lHYkV4CnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVBZR1UyVnlRT29RdXRPZXh3V3QKOC9CVVpJWTZXRzhGREJaQzh6UUQrd0M5N2p0bEF6ZVNDSWdzOERpcEtDUHA0TEJQMERLM0ltMUNxTUZJcGtNcgo1ZDVYeW9GU0g1YVVpek1zV3RXWTZkNVM4emx6cHprei9uQ3NnOG1yNUU2V2x4RXg2SG5CaHhkdEhuaGpPN3JSClY3K3U2WFN6V0FXVVQ5T3RvNHppOTBqWDJaSWdGT0FXTUFZUFBIMzhwTHNhMFEycys0OGpPY3FLdXd1U0o4OEUKVm1qY2RKaFRvaGRZQUZpQ3FtVzgzWG1vSTdLT3cwK1ZRMnZNYUl3Uml1ckFlaDY3cUxQSHIzN3hnanVkeHl1VQplMlpPQTkvWllZTFB6eEpWby9pbUZrSHFjeEFITytvaFFmd2VrZ3RrTmIxSGo4OUJrREhiZkRobWxzc1ozVEpUCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmtXNEszZ2dmUFZJazlXNXljKzYKR2x6UGJwYlJMTnY3QU9SdndDSlFCUi9ObFlwTHVwcHpzOGNEcXVWM1dIaW5CVFFTUWExbkhuZWNhT3ZzWTBpMApTQ0pjck95em1xM2lna2NuMm1wWHpHLzJQZWdrWVp2TExoZHAzaWdMNTM1Mk9LcDJPWDhYMm1qc3ZVbkdnbTJmCnRaUTJ0SnZnNmkzd2w5RFdGR3Y1QnY2cUJNVTZZdGNNSWxTQzFRNXk1UFNHOWdGbXZuNnQ4YmNFcjM4QzhSMnUKV011Y2lTT0crWGJCWnY2SVVaUGlsQWhveDZ3NUNWMDl3VDMvcm10YnBpK2VRV1FHNStFTllXZmlkNkJ5SDk2WQpvM1BJNHQ4RERtY21DWkJpUkRRcEs4WXg3ODM4RWNGQ2U1elY2UjRSQ2tsRlJVc3RMSFJDK21EMUZxWXlxc3U4ClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnB3bzFHMG1NWkZjUXg4Rk1MZWIKRm54aHg1ZlJnbWxJei9lcjBVK21MQWtJZlBrUDJmOGo3ZHJ3U0tYZGI1ME5xa241NVJSMjBXS2dGV1VKbXZxNwpGYWRleS9xcXFpaGZTWEZRZCtETWp3Q3YvR3ZEM2hmblpzeEYybXhsR0IwV0lKakthcTBKUkVmZE1taThOejVXCjZ4ekludXVrUDBsWTBoamFCSzg3WlFoMUZYaUo4dkRMS1dUOFBxMGExM052VFd5S1BBamc0bkl2RVRGcGlTbmUKSlBSdHFrQW1wZGt0WHlkUytPYVF3eitINHRrMGM5K2Z1VGY5Uk1WcWhoRWcxWE5aTEovZHY0REc1YzRxelJaWQplRFNlRWE0V0JIdjBvcGxleGR1ZnZsRHkyM1htOVY5cDNkM2JRd1JEV1VwRFVQcUtxVWN3cWtRQnJKVW5KMTBuCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemlBVUo4MnNBbFp2VlpWWDhTYjMKT0hMbmZsdUFUV0ZWUUJXVE1yelAybUN5YU1Fc2xKa091alRwOHJ5dkdkSklVN1dRZHJweXpiVVViV0U0MThkUAptOWMxVEl6UnZKUjNoc2F2SmhpUjJ4QW45d29LMFRxbVU0K3lpdUNIVjhibzF5Q1ZuNXNaVkVoUFNGd3V6RHdMClQvMDc4MVVOdFpEK3I4Q3hKRTZWSS85SXlKdjdib0NHaG4rK3NJTDh3eTdPQXlKakxvM0kxdUsxMDNzZ0tlQjcKOGVBYW12WDJUcFVOTlVIYTBOa1p6S1c3a1pTZkpFemtwb3gveXY3OXZpYUtvSzJXQ3dKdUZodGRjWHVjM0RTNwovY0pTS3VjNzdvdFZvVTR1Qi9HN0M4MTVFd3o3aFVVMHFrejkrSzhUK1cwcnZWTzVCeEJyMTIrQzhsTms4K2k0CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVNJU29HYUxBUlpMOEs3MG9ITHMKWHgyN1RWNzRtSDAxamlwRnNQaG11QnNxUFVjVkNIMm5hdXpXWFdTS1RLUVdVK2VLVlRqMlQ2bnU1NzIvdE56RwpNYmZLcFM4eHIveE5xTXFXWXBWMjRYOUc1VmhDRkYra3RaRjdXdG9Wd2xuelhkb3R1V2dpbldNcjVra0FxRXI4CmNyQXp1UHJsRXptT2NRYlo1d2N5MFJiSWZDeExSOVFHNkVxT0lHQnlmUGw1ZWp4Q0hIeWdSTm45cHdoRmFkWXAKbjVHSkNMNGlVT21iOE5pcmdFYjNwczh2WnNrZWk4dVRvWlduVG5WM013VjVHSDZZMHB4RVpIK0FHaExFV3N4aApFL2RGcnhwK1ZpdUpMUTdaUmY3VTFyNkFZN3N6elJERFlOdXNzZVo2WFMrMVU5SzFZM2tabk96ZDZXL1UzejlzClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2ZZRXg1SXUxaEJySnd0d3VINWcKVlczc05sV1ltVE1BT3RnZm1jOFppNmY1cW02alF5bFFJbHh2TEo2aFRZOE5ZNmJsd3h0QjMzaTBhVnd6cE14MgowQmxkd1BxZm93U0NseGJmM2lTdDc0OHcrenlwc2E4c1BTTThNdnE2YkZKK0E3K2pUWWgvSjRRb05abVBFOXU3CkpIWFVlWG1RUzQzRHFTcFNuZGdUVGY1ZG44bHlRY2JXT0ZpNmNQRVZaK01tNyt3WkJXZ0I4aGowaXFVeGVvSXcKZTBKbGJZdSsxdTBzam5nZWtZQW5sMkJPOGdEd0Z3ZVg3Z3E3bjVKZ01JelJybGJ2TVFRT3E2bldRMmpoemsycAp0cVBSTGltOXdzbVJzZlAzV0l6NHZ0OGloR1BUS0dyZUtzWm85NUR3UGd1RnI5SHBSeXhPMVdwbGFwNmpCcmRYClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMThFLzdVd0tBQUtTNlVXYTFJQ04KUGMxM1ZlSXEvc092L3ZUNTFRN0ZMV2lGTW1tUmFrc0pybWtrcjlHYjZiTXU5OEpOSWw1YWJ5YWRKT0hRaG9ZdApBWHZNbzBIYkVUeFZJN2xWVE5FSzFnK2U4cnU3N0RUdm8vaDc4dUs0TTROTTByL0dvYUx3WUhVMmx1bW1ESWZQCkN5Ym1nekFzRmI2YkdLYlhnb2dsTUx3QURnU3ZtSWVla25qNWQ1RzV0UU1vckQ4U1ZKcnYxQVM2Q3VjdzhGTDEKY1kwTUJoQXVRbjRXZjRrSCtCc2YrQ1ZjUmZndmtxQmVVTkhaSzRDYld4bnZsdWNsVmZLNFFNZUxOdFhJVmwwWgpEclY0SnJkMnlOQnR3S0YwVkgzeHlGV3Y2Q1ByY0FFUnNZUzRUV2o1Z1Z3U1dWZmc5b1pxWCtWZW5QYm8vQWp4CktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmRIMnFGVXBkWUdhWGZueTNsL1IKdmk4MjU2QXZwYU9hbmxwdjdMQXVRaDBRK2JJdHZWaU1MZ25GRmNkL3l4QmJQbVNKdWxTdnRQNmxEN1M0TkovdQpIN3o2aFhMRnQvd0IyWFAxUFBCdWdUbENkU1RlRTFQOWJzTHBwNnM0SFBOMDZCc0NHbG03U3NpTlJyd0NRTnZkCkV4SkNldmNFQkt3WnBXMUdheWJSa21VTk4rR1EyK1U1ZVVaZWxXWjAwUm5OQ08vOVlLVFg1cFRVOFdoemttL24KZUNVblY4cklaL200N1BQMHdXbDlMYjFUWmV3T0pNL1o5U2hUck5XcGtsRWdsNm10ckFRNDNHb3djS2F6QUJlKwpKTVd4WFJia09Gc1B6dC9WNi8xNDlVS2ZNNHg3MWk2UFV3VVlRdStDQi8rdjVPSGRnMXNUMXVnRXI0V0VMNTJGCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBei90ZVRQNEkvbXE2WmxZU1FhcmYKMzhJWlJ0VUFWZ2NIN0I3YXl2bVgwRjhWT3oyclBYVEpXQ1Rqc1ZuNGp4WlNiKzQ1ZjRGUnRqMWMvUEcwRWpmNApuUFFzc3V0KzVPR2pCVkxhVlEzdGVBdU0zMDRuaXgyTUl2RWs2R3grYmV2NGRTL0xNWm5qTlY0V0Q1WldPYkc5CnRQbUhQZ242WW8vRHNuNTRNWmpyTDAwMkpjaEV2L0l2Nk9WOWxlRE9rWHZPUkdoTVhZRGViclc3SEVIRGdrV0wKd2Q0bkdlQkhrOGk2UThIMEtvNUdUUTZSU3drUzAzNWxFdWRjUEorekpkODdqTFpNd0pPRXNyVjR5aWE3ckpqcQpVZEFuVVhDTUNReW5mQXlmRkxzMnlwR0wyMHhyWjlJMllOTWd4cXpEK3FjYkpBZWRSM2xQb1VXOCtRZUNxWjQwCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM015djcyVzBRdmhuOTVSWWdjZVcKMHFBNGpTNEFSeU9kbUltL3czV0E2S3QrRWo1OXpkNExwRHpkR2JoZFRZZ1NsZFVLeUVnQXIrNEVuSmpWNk5lcwpod1BURTlDVWlWaUhpcCtudG9qd2pOTFNIQ2FNdkhrTmRoOTdZbUdURG9TRU0xTFVFaExOdytMZG53RHZwQmpjClNFUENKMGEvYnRRZTZFN0xGY3FuVU83SzJnRTJia2FlQ05NWGJtU1ZGVGx6d1htTVduMjh3L1c0ZVFTcmoxN0wKNWdjWmZBSmlJc0owd0VxQjZSM1ZqMW9sd3Axb0lYZ01SanNveWZaSktVNW1FNDFyRldINGlpWm1KUFRBL3ZFNApQMlFTWG9xZWpldE9NK1kxbHF1ZjZPYTFhZDNxclpnQ0dVZkQ3dTgvK0svYm9ONWVpSjdycmtmMDk4VXQ1T3g2CnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkMyY0pDaTRqVUtwdlp6UUpSNlEKeUhUaFhOOVgyek5Xa01XT2ZtOE0xZ3pkaDRsTndxbE9uNFhpREtKaTRhaWxLQy9oci9zMitDVnF4VmhBM1NJbgpLWlkwbzJHWjhOaFJBa3o1dG5FVjVyL2h3b21Tb2UwdnZXTXlYdXM1MzdqemtZL1RBZ2dwenFmOURqbnFFNElFCmRPRGJyVUZJWEIzSlUzSVhRaWM0T2w0S2ZHS1dYKzFuR2JyQkhQS2RkTXFibzcrdGdwLzdnMlc5dWo1QnUrZmsKWmdkcTBTRjc4bGxsSktyV1QydEtZR0hHWDVha2Qxb2Y0R1dzZ3l4dzgzVmxEeTQ1U2YwNzIxWGRGcjMzbmp1UAo0QVhrV01IZ0FjelJTZEp4bFJySkx5QjVuRENBcENaWDZXa25TRXFkYjRWbjBxR0dyS2lVUS9LMXViVlN2aTZ5CkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWNtRk5Gd0xodmViazAwZGtpV3kKZE42Zm5USUFFNCtJQzJkT0pid2M5SWRpRk83ZlFLTTBZVnV3bUZ2S0YrZWEwWGl4eWNDVXRLblpuRzQrZ2p5ZgpkaC8rMVNkbm9TVEJoMkhTdHU3c1NCUjllK1BQMEZqb0ZTMXpDSUkrdDI3Tzh4WDZVOFRCWERpamV3UWRaTUtqClRSNytydExXQkxrczMzbExla3VxVndIaW1iVlNTQjYxQUUxSjNGV3RacjFhKzR2SXBjTnhNRGZvVkhXZzJvN04KYTIyajBlZVd6ZWNScFJkbTZ6RGxwNjdJQ2o5eEtGdWlvdFZhSkZoM2FsMFBxam1pSEFSNUtmNk5lVFIzRk1YVgpYcU1iczJPUDRmMlV3VC9aamdCb2tocE1DM1dZQWpFVkJDTW9Ia3oyZmoyRVJNVXo0YWxhaFhTM0UraC9TOSt0ClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFdMYW16eWdCRnQvL3dGN2gvSDYKclJVTU5yYVArRWQvQ096U000MHZQY2RkT1AzZXFTUGxnTkMrYndqSThCMFhCa3BSZ0ZiVmthYW1XR2VBVVNsZgpMVzByMkQ3azBIa01FUFhSc21icnd1Y1lMQ2x0TnlGNlhodnc1MzI4azBaUXNnQjVXcWhvN1hYZzBlRFFJRmUvCllVL1NOeUhWalVzejk1Z0d2My9SSkEvQ2dOSjgvdTBqNytXOXRmdW5YUG1nbHlwejlTT25HNHd6a0IwU3FJMEYKQ3dQdldYdU1Va0pqUkh6V2RIQ0JkSzBwTlFrOW1zdjJqVXVXUWNwY2VreXplUS9nOFVGSFdxajFMbVZQbEVWKwp3NFRSTTlPSTFrNzJUQ0lYUVBvVkhGMFNteGd6UDMvQU4xVlhKMUREWVVZbXZnRHR0OUtpTDBrWVBQV1o4WW5rCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEY3SFk5YWpRNEZSajI1RXBobXQKZFRHbVVJMlMyTXArZnQvWE5uZ0JtdWxnUzQzMytFSXZ1QWxmaHBaL3VTcEgzVnlmRmNyYmRVczJCRlNzTGhBaQpqSGZyYi8vdVFBa3dTS1ZybHp6a3F5L1ZZMUgzRDV5endnUDg2TXNNYUU2QVZkdFJ1cmZxZ0ZON3VDZlBocHdUCmxJTGZvQkk1Q2syWFJabjVEc2xQeWtZZjR2UE1EbFBSaVBqK25qcnRzSGtEdWN5bnRjWkVkQWZSVlIwUjk4MU0KNFJIZWVrNkxBb0JoTHJzZEUzZzNweGk3TXQxYWd3dm1EWTMrTkRzd2ZpeUNLT1Q5UERtUWhyaXFsMmQ0UDkybgpDcEFlVGprdzA4d1MrRU5ibkNMd0xxYm1VMUYzTStMOXh0NFQ5bnlpUFZqWjgyRmpHaTRSbFErVjNpRjZkMTIzCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmdSUE9uVGpzanFLUzE5OGRXangKNkJJZ0hvUDRZMlovTDB5UlZwc3o4WnVHQ0hIdUlLcDlsaVg4QnI1UlFFZlI0TkJ1eUplbkJPM2twVW81QlZsMwpFNkc4NEZjazVrMElMVVc3clFWMnhSUVU1dm9rSkl5TTNrZDVIb3V1ZWpVc25uQXRzK0Z2N2s5Sk5ZbGZhM005CmJiRFJSdzZoL2szV0gxZUFPcDhJQ1VyTDBKcm5QbnBIYnhVb0N3K01MaDdqcG9OSUZ3eEEzY3B1U0VKUW9XSnkKc3oyZ21JcHZUK05kYnpSTWQrcm1remhwQzdLQVI5NURzOXNaR2cyUmRuTU9DTDJlU1dqNThLSm5aZE9jNzZGUgp3cldLSkVYeGZ1aEhQSFpidG05Q0ZpYmtHMnFCS083NC9NNlMzZlUxdFlLN0JzME5DRXh5b08zK2gvZTRvbXVFClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDdQa2dTOHlNOHVCcGcvbEc5Mm8Kd2l6a0o0ZUVKUlhjNUE5VkpzTUI1dGh2Sm1iUncrS2RndUdKSVN1UDdGTHQybEZmZHUySzEzUE9ZK3hTNjBiaQpwREhhbXBiWGF0TFJMamM2VXVSK3MxNnJxZXFGQWVhc3hrbVUrdjZtZysyWklYL0hRRXBsM01MM0VSdnFjZkFYClhJSFFmb0Z3REdjNEJKY3RsVWM4VHcwZmx3cDhCTG5mRGZnMk9NTUFTNkY5cVlYN01HRGRjNjVaN2p6ZzlITmgKcy95UEIzMVRiZ3VzYnpLeWxCTjB4aHhrd1gwSmpQR01EN28yYXErbDg1MS9DcldkMUVVREtQVnRTeE1PNklMeQpqYmdJSk9oUkliR0hiRWY4Uzl0bEhWN04yVXJ3c3MyWTlzT2hTYUF5VzFXckZTRVVoaW1SbGEyTmlKRDFhN213CmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkFyY2puc1RvY283QmxPd1ZkNzcKZkpkRG1IelpxM3oxRmNtNklRY1NQVWE1dUtabG9WTDR4RTZqWE5CUERRSzBMNFhHS1gzZ3VJSFovd29QeldYUQoxcEtXcVR3a2F0VFBjdDRMU1hTejU0NmFvS05ybUdML0N4aDJUc1h3cUg0QUpueEJoNHg1My9wRDNUOENjbnlBCkMxai9VY2FKMmpyL3ZWdWsvOVpJdFA3UEFQWVMxNWJGZHE5eExPQzhPZW9HSlJSTnFURFlnTWxrb3hJYi81VjUKSCtXUVVVSGtLcmh1SkI5bm05SFI0UkJ4YUs4ZEMweDlUUGo5c1FhWnZPaWY3cEZSZjRGem5GSG5waXZzSDV6UQp0a0ZBaURmOWp3Z2ozZlVpb21FcjFKQVhOVDN3Qk56OElWd1ZhYUxCSW85RXNVL0dZUU9rajJDcVR6L3F5ZEVQCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUptRDJPSTVZVTkrWkplSFNmbGQKU01oTFcySlpsTDBtUEVyUnp5UHc5K3ExKzE3dnJYT3M5R1BlT2dZdm9WcHlrcEI2eGlpREZLSWNWbTdackZYNQpZZGhJOXAzM29WeFZ5NkRidzlWNG5wT2xGZ0lMejNPcWRYdC9ISm1uZkhVaVE4am5nRWxsNmpGSVZROHhvVVJ3CitwbmNaNkdDZHhZSUh3YUlMOE5NdWYwT2JvUzBPNDA5NFdXdjNqbUhjN1U3N2N3cmlxbWE3eEF5V2VWMjJOMEwKMG82M0hTOE5Kek1WUWlEaEUxZ0dTaUVFaWE4QUtLaGpWemowVDAyUnpZYzN3NmRzMVM1YlVVMzlodXRLOHFiQwpxdG1DclFvaGg3ZEFPNnhPTFlsc21FVjJOL0VWS1VZL1hUdEJ3eGtVckhkQzZmMXBVYlB4bzhybWpiMmRIWnJOClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHg4VFkxU3A2Wk8zbTlYeWJpaDQKRVhjZDhWdDZGM25VVDhwRSswU2xJc09BOWZBV0hhRWY4UTRxSXRDV1B5RFE1SE83MGwwRmcvQ1dQQnZJQnFCYQphRG83akh5em9yeHR4cHFPUjh0RDdJdEZ3WmdtSzY1dzkwOEluakRZQTZlTEp1WFArWmVwWGU3KytscmtJbHpQCmpzR3ZqVkNobjdkTm54SUFmY2VZSjVhdE04R0J1cFV3TDlmNW51TWxCaVF0MFdqSTlDT1pLM3J4anRPMC9GdzMKYzR4Q05BbGpTVEZmNTFaRXdmMTRTQWMyb0dZTUJUZ1A5dE0xK3c2c2JXQzBOK1IyRUNPTlVlRTZqbWxvN1ZFMwp1YTIvYjc4SWI4NmhKYUhUK0xkbHZMNENkUzVVdk9Gbk9LL2tIWTc4UUFyWVdXTFJkWVNTNjJWYnlETWFxMUxoClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEtPT2JrZ24rRitxT1lCSTc3UkcKYjR0Ty9uZ3lLdWlJZS9Sem9tR1FVSU9oUzdWT0E3UGdWQWdPM1hpbm1zRnI0ZkpuRzNEalhNZlBpcVYveHhsbQpBUmM5S2oybXNrZE9vOGtlenBZZ0I4bVM2ZXBnSDJkNFdDMnBIdUxRWVNWRXVRRkczSWFxZERvWWloT1hvN00wCmVDVTl0RS9uQytCelltMTlETzNHTy9yaE9DWmIweGptaG5GMVBBMUFLc0g1b2N1QTJKa2Z5NDNhckpGRFp4SWEKTFZ1WVlFZ3l2MFZkQTJDL25GeDdGa0kyQlFxRC9jV0NLMDZUUmtzbStEc0lEYy9uN2RpTWlPbXpTNFlaMjNKSQpRQzBVT1oxWmJkV29jOVVxYVdrbDRrRUpjajBmaEsvVS9wbUpjUEYwdG9yeGpKa0Q0M3hJMVNSUDM0QmVZd2pHClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTBaR0N3YWp2RXJKa1krNzM0L08KeWc1WEtOcmt3ZXA2NGp6VmlkdVRZUXhjYVJJankvRDR4MTNyREJRZTcxektpckNrc0Eyd2dvZHJ6RUxwWENmUQo4VTRNbU1pSXl5VERJTy9lbVJiaHRvVkowdWU5RmlIclE4eHlsQ3BlL3JYUWpMdEVjLzZsQnVtVWpvMWVud3R4CmNGSnRiQ08wR1loa0Q2YmZma2Y0T0daMUpZdCtrVGZrZFk0UE5kcE01U05rZ1BtbFIzZEVjb1Ira0JxRlhZK3UKOVRlR08xZUNYMEo4a1hDeXduTHd2c0hHMkNybU0vaXFPNCs3c1JvWmY0ZFd6UnlOdmFVT1hqUzJhaXNzOGdVZwpQN3oxMjlIelg5Q0FZcHdtam9pMnZVa01HRzBrZk0yWisyV0tOelZPMW1yNzFVU09uRFlxOG9yWUZqUzBHaHhXCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVl4aHdXcUUvcW8wK1lBRWdxNEIKaytVME4yRllIVnlqakwwVklMY0N2SjVDUllQY1BXSkdURnd3aFY4dHlpQ2REUkZXUU5ZRGVaYTdaWDNMVnJaSQpvcWxNTHZuVUJOVzhheFhJQWFEZkdpcDZSUG9SdFhpb0xsTngvb1VkVU14MUdHemFIUURRMWNtb3FGOTJ1c3RJCkY5LzNGUXE1ZTdzL2Jtd01Qb2NMMzVnR0c5aDBUNDZoOHlRbkdVNlNRTjBWWmVHUFhWQ2VTWUVvMjJ1Z05oRXcKMTdlbjJjUERSU0pXTjVERFhFVlZlSkUxR082ejBpZ210em9zTURQT0xla2dZYXBFUVFxY3U2UjA3ZWRpZzZNRQpUdlN3elNPd3B5L1hhcXlrV3dFS291QTh3WE96SFlWdFdRV1oyakE1ZEJzU21mc3MwRXZPczNLdmdCY2xyRU52CjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBei9HM0NxSkV1Rkd5Z0Q2ZE0vZnkKdVdRdDNuakt6K2lnWXlMUmtCYzdFbEVEanFVOFdRMEZOdmRRYjR2WCtYL09yR3JLcGpZZjRrNStpNFplOVl3eQo3RlZyS2MrWTZOL2hQV3pOYXZaMTB0ditlZFRGWG13Z1Y3Y3JHT2lObjFCQkxIMStmMzNxWmhKK2pTeTdtUVN2ClhPMExvSFZLY3NPSTg5RjNMVDArT2RacklhUWRRd0w5V0pyS3NEZGE4T3hHbmpuTk5yYWhrMGd3RVc0M3FuTGsKeTkzQjNtMy9OSXV0N3pLTkVyVlk0eDA3NXlKNnowNmFZb3JqcytsY2NZM0JNVk0vd1k4cndxeUgzdDBjbTJ0bQo0T2JYbjNIMnlGTVFYak1Td3gzeGhjeFprMnd1S2s0ZVlvdnZSM3d3TFVrWEhRblNWTHVIMEdxN0wra0lKVFEyClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeCtrTUVSdlhxMDZ4ZUlpQ0poL3gKTmtyazh4OCt4UGtMV3l1a2JYUTgxbFpVOGpJNDA5Z1RhT081ZkphbkNUbG1Cc2V2cVpCWS9vR09hSitmVGMvUgpvRkVpQTVPbmZ1NEVmWGNuQ0Q4dnNHekE2aGRIYU5wbEc3b1FiTlNuTWxMU3JnSDhwcUpJQXgyM3V4RXN6eThKCnFvMU5vSE11bU9lZ040bmh6M2ErMFBROWtTQkoxZGtXalhNV2RnUUZyQjM2VnRhYTJ5NG5zRlFnYkhmZG5WMlEKVFRKQWlOSTh3OVVMVW9KUVh1TG1UZERxYVhHQ3JOWndGNlo1UDVOVHRPdHVTQWZiUVhGd0RKcWlBYjExYm1CbQowWWhwYm5hNE8yK3ppbTlNRTRXdWNaWHBVc0UvUk9HMkpmYWJlaytXNjNDYUlTTDNFVXV2bThZVTBicFl2L1ExCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTdEU2J4VUk5L2JGWnBHVXVmTUsKdTZSZzAxb3dBNGcrc1E2SlNCbVd5R1JvYzRNK1BSU0RSQWl6WnpHSGRwbm9pRlg1dWhQVFhGMGpwNWp3R1ZleAovQzl4NCtZREJIekJIMVVrd0E0TVN6OHgyck9RbHpSZGVVZkt2Z3dJcWRZalQyYTgyWEE5MkxSZnFqWm1vQ3EvCmZhR0NpWWg3MG12ajdLekVoTHhaVDgrc1VmQkpodVNlNEp5SUZLRW5wRjZ5cXFFSlZWVnNWbEw1RW1kVEdMd1kKRWZoNElwUzRaMVN4eU1wd3VveG9pNUNHUndoOEhZZ0txMmtVMmpISm92dGkzSHBzQUZZT3Z5azNFUmFwcXZhNgo5c0FkcGxFbmVkRm5Ua29oZGkyL0NsNDdLc00rQmdiYzROc1FtNktJVnlQM1d1VWp6aUg2SlpkbFVzaEdmSVUxCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXNHNi9ndmI1QnJyR0o0eUY1N2EKbUlhOHFWZm81b2IxRGkvTmdvL042ays0VFpEakRZc3pXQVJyK1Q3dlcvdUF5SG5zQTNPTFVnVnBJclF5TTF4Zwo3QzJhY09kbVhEUU5TbHJBbnNZdERCNG4vRzVVOVBSM0lFRGNxRU5UUHJrMGZTYXd2emg0UVBJZDBvZWZBQnZLCm84cXQxSHdiWTh2SGtBZmVlUDNpZ01rZnpQNjFIMGpCY1pEeXhva2xNNmNueHlpVWdZS1p5OTU5RHRENnpVUnQKdmJlNHQzOERxcVVHUjJab3VkNDNPb1VWUFRwTTVQU0hvRjZaTlNtd1dyVEZCTFVPWFdYaVJ0T1B4RDFCU0tncwp6b09CZDJ4eGplTG91cGZnZjNETHMxczdUMWFCckhmaWFTQ0JjaVRvOWtOcmtGV1BieFlJSG5zUDdqNnBNMXEzCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkxYY045U1lBUzRDRlE3S3dMMXAKbndha0hsOHJrdnEvVVhsOVozQlRyTFA3VG9iM1h5cDNZQ2g0OCt1aVcyZytUbTEwY2pxeFY2RUl1b0ozYXgrRAphUEZPNytvNGtXMTVYNWptTjNEcTVKTWFqMER4WDZLbHpaSS81SkJOZXR2N2U4WTFZZmYvNHVSQU80UGRFdGovCkNlVmN6MGRXemFqTDJjclJyZVgzajhJU0lMalliNjU2bDUyMkxvUXI0MlA5MDhmZG1FV09iTFdlN0VLaHUzaGgKY0Y1UmQ2WWNjL3Rzc0dXTVhDcVIyaG9Od3pYdjNhdU9JelZDeUh4R0Q5RkNDWmtEbXBvV0VySG1UYjJOUU12eApWTFpIR2lMaVkyQStsazFlZ2p3Ty9JQ0dSRXVvblB5SFlzZ3hGbXl3N3h5a1J2MTlNZzYzVnR3bFh2d2Mzc3FnCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2Yyck9pd2tQZ09PaEZnaHBCM0MKNXc5UjdrMEMyZ1d0dUJweWYwQ1cwOG4rWHU0WW0rai9XS0JOOVpaaFZUQXVKZmZiTTJhMTFrN2tKYXhjQ3NMVQozL3BXa1Ewa0NBanRqUVpUN0pYK3Qzby84aEgzQzVNYkVMcDdSWnRMTXZFSTJ2SzRUbVNzQXR4VFUxcWF6RnNqCnV3YkFnMTF0bG16QVFQaHMzL3praDgrc0ZkMzR5MzM4YzhaQTkyUlFWa3hBeDhnd3ljcW1FL09QRzJIVVVld1UKb1BhRmtNYmUzdENhRTFzOTh1QXdua0dWOWE3NWw5cVZuUkJwQXF5R1ZJZ0dmS2ZqLzVvdEhOWnpKSWY2ZmtXLwpKdDBLbUt4QW5FTFJPNTdLTEJ2bWN6SmdsWTNFVmdqK0ZGMXoxOUpVRGVjZG5mNnFrbXEwVUlramdKRnU0U3F4CmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlpOQWNHTjBOamI3bkMwcmx5b3UKUzhndEMva1I2ZE4waXVWYS82dWtIU2xwZU5GaFFIK2EvQ3pYRnR3TEg1Sk8wbjdtblVWbWpBdWVOa0xKTUpVawp5NTUxY2d6SHpkbFlTdWxaN2VUcnAxL01tbFdVdjFLNnBZTi80THJ0U01xM0dzamRyOENXeUwvSHhiQ2RuUFkyCmZQNXlqN3cxdCsrUG9ZWTJaZWdQZCtlTHdCNWJxK2lDUEQydmhoZGlQazZyRXBzNlo5bHg2QWpBblI5K1lpWG0KUlJNUlhIem91SjE5c3hqcWJZRUY2MmU3aWV3Y3RrbkRQbC90U0xHQjJvZjNOdjN5MTZQdlIyN2pDZ1pJelhVUAoraGx4QmdnRWMvcTZBWk8yK1B0R1A5NUF5emtsYnM4UUZ1R0dMa1pKbFlIcWdqdDM0SGxoenhxMVRmWmlpWlphClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbGxadTE1UUpjVUZ5dzRUM29TWWMKQ1NyQlJXSXk2M1ZFMFpIRTljT0VLZHBNYmgxVU5UTWsvd0FwS2k3ZEM3Z0NZVWRYVXJjc1E4OG9SR1grVGZPZQo2eEdxeUZ3UktRVlAxS1FWZnA4NUlwM2trMTBSaWtwSGQ2RUZSS1NuQWl3eE1yTENIZkNCVW1taVJTaVY2aVhHCnVQbVA3dHhpa3ZyQVhWM2FhZEtlQ3Bha2YvSTVTendRbUJHOUd1MUltbFlEeFFpU2JVS2lTZEpxQTBoOXllcEoKQ3NmNnJRSHpZRWRaT0t3ekZ5QW0yRTdrclhDNHNvM0xNRVVwM1RnMFN1SUJCaFpvdjZlUW9vL1RtN0pLVWE4eApvaUNhT1V0S3E4eHFtNU1aMUV0V0p4WXpvU2xrMFVZMnl0dFk2ZnpzZ3NOYTVBMGhqZ25vSmNUbHZLeGZxaDNMCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkYwVVNlNFppUERDQ0h0aVNqdFcKS1U0SWx0RkdCaUVDeW9LVS80ektLdlU4M3Z5MjVDTXlpSDMwRzcvTzg5SVR3Y0NZczlMNWhORG5oSHBQMDZrYQpaU2IvM0FrbnVZQkJtOGhTQUs4NVpXS1c2NU5kczdSVzFwN3kxWXJGc0x4aTdidFh4djEzbHdmbDFwSVNGb21aCnNZbDQ3ZXpBVzc1UzF2UEQyOG8zWU00ZDh0Qm9TUDU4UXpTOXExZlFXaVlLbkIxblNjdkZtRjRSZWdVQWZkck8KUkJTSFBscHNGMmthdWh0eVBGbjZWSHhCbVFJdlFhVExYMW13SlNHemNiQjlVZlRjWHdMZ0FSaHlmYmVoZ05LYwpqNzJWUTMvUTY2aVJreFZwZzlPcC8rY2FzRnQrVzBhUjh2azBTc3BpUVlkVWo3aC9lMWRGcG9kdWNsU0ljWFZaCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbWIzeDZPT2xUVGllMVNpN1JqaVkKanFramhGRzJ4dnNDcWRoYktKdUNtcFRNc1UvN1lNOUZoZnEzMmJkcm54QmJkQ1J4bGpDSXI1RUZiOWQzMytodwpCNktENCtIWlQ5aUl4a3N5TytONHk3dGVXRHo5RDRRU0Q4aFVobXA3NUtVdjlBWWlSdE1Kai9aZUhZUG50U1c4CnV5S1RiUEQwc2dqZ2IwZHlKS2JDT1RsaEpKdC9mVVdYMWlUUmlQZExMKzBGNGpKcW5XbzlJUlNLSFZMTDMvVFkKYjkxbWx0dW5uK0VsU2xwQ1BSUEdKbjhQekNFeVdJc1RHcm9VZE54OVduL1IvME9RUDBwVnQ3L2NwMTZBeXVtUwpVdlY1UC9uU2hPRlhQVzRJa2J6UUp6VWNGSE5tZGFpVWtUeGtZanpvT0hZN1ZVZGIwc2pQdHN3b2x6M04rc2EyCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDRQNDBYbUV0aEtWK25WMTB5QjkKcUhmaEUrNWRhcWxKQWtlQ0RrbnJVZVk1Vi94cFJXdTBycG1jeGZLZkF5RXU4am5RMXljVUNibnIrb2FDM0x3UgpuaXVwMEJWK1Y5QW9HUFROSUFSSk5ma2JNUnJOemVsRnZmc0FwNk1PQ0c0d2oyL2Z4QVNxdm9HbkhMMkdIN1RrCklKa2VkV3k5cmMxSG1VblV4Y2gzQmhpS0ZOVVFjSjBEVjFKQUdKNTRrUG9MUHRmN0cwcWRhbDlmY29YRmIwMDkKV1ZEM0hUYW9JVUdvcllkRVA5dVZoS1JMa3pWdlVnNTRlaXI4dnFVV1FEYzNzZ1dJd3psTHV2R2lTR29Yb2NkeQpYdStoUWdhQUhEZ3U5a2FaQjBDWGZKUldrU2J1QUN2NFpRZTRQcFZZM1NWN1pqZTY5M3U0NFdaeFNDcnRKL2IwClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBazFFV2hxR3VJQWl2b0J1emk3eEIKT3pOajhwa2F3RWFha0JIV0p2ZU9naTg2d2NGRnJCdVY1RXhTMHpoa2NodXRkZFR1NzhjaitsWTdrbTc4RDFCZQpmMHpucmVmcnFyOHVJZU5VZk4rR0NmdFRZQnlqeU0zMk5JcW1GczlLNjRmaCttaXlmZHlmbkFXelduOXhzeE9PCnlrRG11M1V6d0hsSEhzQ3ZYV2FMVDRYb2dwWXpoeFB0dnZVVlJ2U1FuMFlST0kreWpuVlhOa2RCeWpGYS9uT24KaG5KU1FuQ0MxQ1JlNm5Dd2FHN1Z2R3ZwZGxxQzRUZXlxUlFPTy9BaHVqaDY3U0daYitGN2s4dnlvVWxEK2ZJTApxL09ZZ0tTamMzTkloNmxFODFlei9ReFhET3NYM3k4ZXpyc205dGZDaWNmdTNJemlGSUthSDZvMlB5Tk5JcXU0CmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBa3BBaWtjdFRueDhqQkVJN2d6cVkKd2Jkbm1aV2J0R0t5bW9aTXF6bFk3SjhDUnRCRnJ6UUlaQUhIMUNUZUpBT2lXdENlYVh4MCtXTlMxKzR1WDdMUAp4V3lsaHhsZzh6Q2JEd1FCVitIWEkvako2Vmw5K01Dc2dwUGpUbHpva0UyRm80SHF0ZzdBUXBEN2VKaTZSQmtYCkIrZGdJREZRWFNGZncyNGd2bkVZaHVrckVjR0FtSVo3RmMxTFhZUGo0VUtWTkZGWm5UamlLTFQwc2UrMGJkaHgKdy9haDRjeHBWbllsTEhROFJYSEZ6anErMy9pY2pZNXo4YXhJYmFtdzhramw5dThpNXdXV0pzY215Z0d0N0wzawpieWowbzRNZmwrcU9YNWpVVjRQQnUzWXJFVWZ5Ujl6NUcrM1JQQ096aE85dEpPejBYQU9ROG10NFJuRWljdzlMCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVBTVDgzUU80eTVQbHM2R1NHSnMKUTczSEtkWmNBaU53TnB5SmduSzduMWQ1R2orUVQxeU9DcXZOOGFmSEo2QlMwSkJSR3UxZkQxSzFYcUR0c2ErbgpSVVpaOS9FWGZVNStyTWU0QlYvU2owMEY0UmRkTFZUbklEV0lhMVloek1NL21XMkN6SVUxNnRSVkRqTTFiSDVsCktDV1JUSDZBR2hkSmxEWlcvSnVCMzVYcXJOejNiVHROVi9Jc3hGVnlhOWhzNUVwVkhubndQMWpZVkpjNGcrVlQKNC9HdGhpM0RMeDRNQXlmbGNWaUw0SG5HN1hZMGNLSlV5emNlQXpEenErUEFXaVNuWXZIa2I0M1VwM3VvMlVUbQpUZ3BKd0FmNm9vRG9Mb2Y5K3NnT1BPVXJISlRFNS9xU2FSUFYyVDFmOU9xWnpIZ0JmL0tTKzZrY1BOUTFZTC9KCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUxJL1JHZFNIQks0aDdPMndtUFYKRFprRW5UdjR1bHRIUXRhTEsyZy9uRXNYQUx0em9RVURKYTdCVCtEZ1EvVXh3NEZLTWQ3QWxsaVdoN21pbUdZUwpyVG42VTZjaUhzOWpnbFVTMkhnVjVmSHdQejFSeVNJSTcyVmVGTkRhbXNLRDVhblJlRTZSMTVtQ0FrY04yUUQ0ClJkVnFVZEhvZ3R4Tk1PNzFWeUxFemh2WnpxWWFWYXYyckUvcHhldS9ZVHRseUlWM2xPN0xsc3gzTUhvUU9NZncKZzRQMUZ5MXVBZmdva0J4eHU3cDdQQXlPYngrN1R6NDNxWXBudFlCMnhsOVFWZUVlTW0rT255TEJyRTVkL1ZsUApnZGJmU3dEYzlkVEUyanExd084MTF6R0F5MktGN3lOMXQyK25HYlNXemZmN1dsS1l2MHdlTmoyMEwrZzBpVXhKCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVRxSUZSZkpLOW5PenNLbWpPYVoKRHpaTStCenowZkRGMlNMUUg0dE1SR0QvSHEwMkJLclVXWmh5dzJwT05XY2o3bHY4NXhyUW15bi91RmpxNmZUQwpmekNDZVUzQVZ2RHd2OCtMRXNsZ1RSZ2N3NU1GSzhmeXpzRkxiVXZwSzluL1hzNTZja2xXUlBva2RWQTdLYnJGCjdzdFBMcGJxMVE5bll4NEtXWEEzT0FoZDZSeU5WQUhka2VQZmo3NVdpbEtRL3ZiRTl2cWhRYWZWMGdsRzlQU0gKSGVkT1ZTcHl0bXkrQXdYQ2pjZjM5eGwxbmIzWjROdGxTOUNTSXdxdnQ3T2M1NlJFUmRNWnc4b0QyeXZvS0N2Rwpqb0NBelVBdVNieXZPQjN0bFlna3dnR2F0TDhNUWN1NXJzWFN4QktuaFhOZm9mdWJhVkRNNzNDNWZNNnJjd1lDCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHdZTjM4M1BER3owL2RCbnVuc0kKSUhpVEpjK01pUzVVYkpHdTBxczIvc3dTTmJhbm1SSHpDZTF5dzh5N2JCWUtRa2tUaGdib1B0a01DTEJlbnZuRwo5a2ZicFcrS2Y3QnBrQ2FwUHFnd0FMWjNkM2FzYWJUQkNLd1pCUnFKaUViZGIvTlllR3Y4TEo5WnYwQVdUZ200Cm1oZTlGeFBPS0ZyWVg0MCtwaWhTM1dFQ2tqSWZINTNGQXFjQUFWZW1neTJLc0xlK2d1RGx2MURaY05jWE1kdG0KZGlwNGlwTGtGY2NBVlRRekZONU4valVIK3JwY2VEaWdlYVF5NTdrTHYwRHhrY29ZV2lzb09QQ2RjeWVrUGRCVwppeGY4Qnh4a1BkSWp4TGlBai9XSkZEZTVIeTRTWFNLL1Q3QW10QlUvVGVBM2RiOVZaYlU5MG91WjB3Z2R1OTlECmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUpQQ2RPTm5mZHRST3ZTbWM5djUKekJtUjRSVGc5ZVMwQ3RoZDB0M2p1OC94b012OW8wRThMeFczYTF3L2FETnhrcEpFS2wvdG9HVkNJZGJaMTFvNApDYXlsb3pvT0ZBZml6OHNGSmZ0bmpvZmZBbDY0MUNTZUFkanJzV244S1JHMmFRTE1nbzQ0RVFYR1lyd0dBbjY1CldSMkRDSnFTRnZpeGZEY0ZUVUNqVnhTWGRxMVJnM05vc0hjTjRmdkZvVUpIY1U0QUJnVU9ROWhVUzNmYzU1R3IKTHZIMUtLRmFwZU9TbEIrOHppOEJpTHpWcXZKbG5PUDNWWGFTRnlyMjV4VGdzYzNyMlNOdE8raFJtTjMveWlPUApCUUE2TS9jdnFkNFZHa3A1eHgwMEtCaFQyM1p5TjVjVTVpaUx1MmZvVFFENUlsRUxzUDdpVXVmbW1vVElGaGJqCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnRhTGw4ZHdWZkdPZHltTFZpZVIKcFZJeFIyZEIwQjRCV29iQ2h4aTF5bU1LWGR5cjNZMVZYcVpheUt6MHZMT3dJUHg3d292bWxXc0VnTjl2L3diNwp1SkROMkIrNi94d3ZDUlo1TmEzbzlpOWU2WXVSZ1REeWt6Q3dXSEtwS3JHSTRuckV2dXFOaHhNMWY0MHBqeG5YClBQSlM1SloycWhyeXFJRU5FaXFoZTc1bE9xa1UwbXJtMklJeUtlM3JzQmZaSjB2OGRlUzNLSitRQS9sRzJIWHAKdU5ETmxjdHExYmxaZGNGdnJGY0wvYkg2NkdObE9VYks5Mjl6bTAvQ3kxMmpyZHArVkx5VmJmeUFiUHZ0SlZIQwpCTERaRk1ESXFDYUhabklLR21NUU1qZmV5LzIyUzVKMlQzOUNSODdCdGY1ZU00WHFhZFdUZG1kVXRzNmJ6NFprClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnNhUm9QbjRQSkp2S202Zi8yRzIKZ2U0TjJJUStwbmxmOFB2Y1ZKM1pKelBPZG1xa29pWHJKSHNWSElLY1FWa3ZFN2hnQ2ZBTkNTczV2NEMrTVpINQp4dm01SHpsZkg3RWt2VE1PRktaNjJRMU53VHZJM0pHVlJZMmxKdlhod0p0WWlBWUFDUjM2Qktxc0RlQXhMOVhQClhDVzB3R0QzSDVtcVVHTkwzeFM3LythdjQ5Q2pydzlUQW1yb2pTaHY1TERYbDE4MGJvWW9PTjVPSW1RMTUxRnUKeHhHWWxxdzduUVdzUDZ6b3RGOGQ0RUdZNkdHbmRjY0tFMVh3dTZrdHJ0RTBOZDBtaGFXRzhjK1FaQ3cyNDBqTgpZUWYxRGM2SnNad1NRMVFmQllaUVBDckpuMUh6YkhQN0d1b1NzbHV6ZlcveGREU3pTRDlpSjNlRGMwOXdJMUE3CmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclg3SE80ZXQvK3c5TEpSLy9IVU8KdENwUVdRSTd1aGYvRSsyeE5vVWNqOGtvamlvUlE0ek9DdldhclV4R2d3cllTRG1UNjA5SVVwTm9udFFEQjI3YQpiNHJtMnhscE1jRUFQQWFvOUVOZlQ2UWJnV2NGTlAzaXp6QmQyMSswd2pxQkxUQzF2ZnVMZlYvZUxTakNEOTUvCjRML0M5Z21Cc3EzcG5kYk0xT0xsVjFoT0dpT3g2ejBLc0Z6VDdDdExWSzRnUi9Vb2hoeERQVmV1aWdKTUNWYlEKeW5WMHlRNllpSS8rUzhDTlpkZys4MXgvT1l5bDdTM2RBRFBVUXNNWjU3NHhJbEo2L2c0ZGROVGZFUUtwTit3ZwplTkY5cDhvYkJ0SzNraWdlUGNrMTlVL1U0c0wyQ3FhbEc1VVV1WWcrbUI1WFFsZEdodERwWTFvVzI1TUJzWlNhCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczdtSkl6bnpDWVBaMEd0T0pUbEEKVEJKQVVzZ0RrUzFNZERrQW8rMHR0WXJGZTRrZkpQdlJVY1h5NUtkeVhKQU9Pd3BpNG9GU3FMWWlKclFkaUpoMQpQd04rVURCWG9SckdMclVmZThqdE1IdkxtSHR4UEtGaHlVTC8yYWdlZHlaTWV1cXpxR0d2UjhlTVdXQmNESWt0CmNqLzJ0ZC8wRXgvdjVRTzMyRkJiWmNVUFhNMnUyQURvNE9HOU5HRGZPSnJBdXBUTWtOZEJ6WHRRclJTaUw2RWwKbG9WcGM5TzdWMWVsMTdWWDBvMjl6RTZlZ1hFQU9weW1UZEJGdVF2akY0Y0MyUnFwb2N3R0o3RlNsbWtxZFBJcApMZmVIcitQaWNMYlpOTXhGN1FKKyttTEZCZ0tjalI1d0NNQU91UzNwRmYycjVvSlgwcGVWQXhScDBDNXpNMm5CCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkdPOHBHQ09PWlZoaEM5M0hzODIKMEVYbVd1MDAySjRac3hOcVFhanF0NktwTkhQSTMwNkhqUVpaN2lGcE1Qc1gvK3N5RWpjb3FJc2NMUkdqZUttegpTZ2hnejhCVDFJamY5ZEp3a2I5UnZjbnVnUlc1bkVRR0haN3c0SUdyVmFHZ0xvREFOVkpWT0V5dnpueWZXTnFLCjdCMmYvZ09lb1F3VU8xdGo4N3hka21ISXVOckM4WHpwa3NBR1ZETmlSa3Nac1hydVJBQzl6V3FPeUJqcHcrdG0KTERvbXFRL0lvOGFKeTdFd1RSeVUvTzFxOENldDRLQXJ2eVBxZFZ1R0t3ZmlYQzBhcDNDbjgvSEFmY1dsRVJWeAplTjNqZEpSSHBQMmNHUDBvYXc2QURCVmUvazhING9OcjlRay81MWR3RXIzL2JNUW1wbVYxU2pVdzlRZ0hTcUwwCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmw0b3VseXhUMlZDd1ZQTHRpenQKRHVoc1dwQnMreW5jMGs4OGFYeWdVNVhmYm90UzBIVFVEZmx4ejRhZ1dDL1pVM21OOGR4RXJDU2kyVEVrWmNJaQp0YUxXTi9xa05sbU5uai9IZWJReE9kZVhVU0FPSXNwOHJkZUFXOXN0aGdJSEhTYVZ4bUR4T09IdHBrTFIrU2tBCmFWT0p5cmJOcU9Eb3R6YTlYTGh2dkVTL0VVU1dqa01BOFQ3RG4reUc4S1hqOWJRZnowcnZKaGxqckNDWDE0T2EKb1N2ZmtlNTViNDdWVnlubVNkS3FGREtCbzJ4UEg3Ykp4K0tmcTdBWHUxaVpUVVM5NS9HWTlqNmFTb3pBRkFuZgptanRXMjBtczBTTEhlaU5YeEZzL2Irc0hBNVBsRVU0MGF4OHcyZUdSS0tXWmdhdHlQSzRMaHhna2cxcDhPRGptCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1k0THhxbXZGOVhLQWhlaUJwODkKNnFsZ2pLaVNkcXVBcjdFUXdDVnZOL09YNzhxdUxBL3hZTDVwMDRlNFNWNjVMYnZ6cHpFdUpzWWpVWEUwTVhVVAp0VGN3UjI0bWRnRWlQSjZnNXVZZnZTSjFOMlYvcytkOWE2U1BhWlRPbEpaMDJwUklYRVJIdkFmTGF4TXBCRWRFCmI2VTl2N29rSDRUUFZDODIvSzBhT1JiaExZcHcxQ2JKTld6ZUxqakdaSnlld01IWHg2UDZLR2RRTk84Q3V6NHYKSXI2a05uWjRIZG5uTTJqK3RQN3FqZndxSEUwbzdZMmMwdjAyamNqQnVvc01qRHp3WlVkdTdSK05jK0lNdDIvOQpPK05VNFRhVEJXK1BrSThnWFN4YWN0YjlDOWI4L29UVCtoSFJoeWNjQmo4cEJlcFNnem5pQUpYcUlyNnoxd2g5CkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXE3MkozTWJ2WXlLeXA3YzRhaWMKQW42Q20rU1JHS0M2SWxWRXpmanVqUDR5WEY0aTN2emxhZ1NqVHRDcWdUNGU4d3Z6algxTWJnQ0hsSG9NdFJ4NAovakozNWMydDRhc2VkOWRmaDhyenpPWUtWYm0yVXBQV1JyUTNVWWIxakxMTWlTNzFMSWNiTDVEbVVFTHFYVkxOCkQxY0V1dWc1SjJrVmRHUW5WUTB5T3gwVzdnUDhIbFNralEyQk5ndG5lK3JBVmQyMVprTTl1ODNtWTVidUZORzMKbVpaQXIwbEtCUEdVWjFSZWIrand4S1FEVTl3c0lJanExeWNKSnVMMHpxYzZpNUxjamhwS1AyK2xOM1JpZldlKwpJS0JmWDN2K0lGU2l0Q1M1NVphTDdEcGN1RUZySGpYY1kraVgyTTgvVURMM1lSTlB3RW4xdnVGYWxwVHVRM1FJCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkFtRG82Nmsvd0tjaDMrQWdrbTgKcmZXRVN6dFZsSFFnQ1VNWGkyVEo0ZHR2eHNyVEY3Zy9PR1pxNnlTUmVOdUFjKzlKNWFFQ1d3NzdIak44Q3FMVQp0RXNIaytpT2lTL0NhVFZMZysvMm9IK3BHTGlYU2JEd2lMRlV1bTBETWFYUEg4OGdTUVc3b082aU94OWw0cVduCkJ2dmtidExKRXZicFR6N1ovemNlZitOdm5Pcm4xd25sbjhSR05McVpKYy9YSlQycjBwRUJFM0JKR0l2aXBVMmUKTzRpaGltUlB6NnQxN0dxaDdTTk0wRW9OVjBIaTh1Q0Q1YkMrUjlDcVBnNjNhZldGbnhvVWI5a1pYZHlXenBkWgpqNXN2d0xmWFZiUlZjZWpPYTRtZnpWYUNhVVhKSGptYWM4ek1JdlZUTnJLWEJLaDU5anBjUDZqT2c0MXlERW9hCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0FxZEZvbmlmU2ZROWlzUnlyYnUKZVNFR2NJL21FdytnVTN6eUF1RjRhYVlaYXc3VHRWT3hsaHNXNEt0NEdGNTlhSXYrZXIyTXFaT0VzUk9xMWdrRQpmVFZ1T0tvcGl5cVNLZktlSmpST1o4N3pJN2paUytIT1Z6WmNIM3Y1RkVQL0pnQndrVmFwWHh1d1p5ck5pVTFwCklQQWlvK2NBZHE3dXFoL1o0WllhMlduZktoT1RvY2gyNy93VUpsNUJVVmlSUzN5YkhESTQ0QWZRcUxXb1BRVUoKdXd3Y0MyTTRYamxEMkpyWDFTaHVCVmY1bmZDTUNRc1E5SDhGTlg4N3JWMkwyQWorMFN6K1Y4bnBqTjk3eWpwRwpLZVEwTzI4WTRPeklhMm5XaEtYZDV4UmE1cXVTQmw0L3JnZHdQeGtlMDlOK3ZGaFo4YUhEQ24zNU0zR0VXVFJuCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2d2YjJ6UTNwYkFMVDJOYm9CNFMKRXR2TXpKdWxKZXIxL1QxWGJWZmkrZ3Mxa1U1RHYvREx1VzBwWXdZc25pbWRXUEYxTm15eVlUZUVGWTFESjdNTApZbGQ2UW9VTDRwOU5wQ1JIUitSZ0EvTDJHOHZIVjg2OGxsYjEyN0t0K21KcmY0YkNLMEMwMER4K2pEN0VrdUJ3ClVnOU9XYkt0UUF1NW1OVHA1akZZSFlNa0FsT2lUS0JqS1p1VUF1aWJaYlJIWlQwa2plaExGWGRBajZRNEpvczEKSXl1SzZ2bjc5K0ZtMkI0UTZUWk0rTCsrUWhQNFVxVDRDUUhHRVlxQ0lLS1hKTnNSRFducFNtRjVTMWp3Yy8zQQplaE9OWGFIdHdCUThPa0dOK2JPS0lSNS9sSWZVRVl4M2Q0aERmZUU0Y2pvUzQ0Rm8yVENMVGc4b2RRWEt0bXdBCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN05zRG15VGZCOVV5TDYzTldWQ1YKeVdsOHpEZEN3bk1iblczaDc3WGRLQ2N2dC9QcmRrK1ZKVnJEbkhXSHIwM0o3LzZrMFpCcENTZzI4NlFYOW9BLwpJa1ptcEtqOFFFZngxRFUyaWFWYjIzdjdGb3ZSQ0hKRUZudkpQZVJVR2JSZEdLWUFnTFE1ZmNsZFE4bEg5dk92CitBbW9mY2tMajlrUy9xZXVjNzVBRXNVN3JacTBQRjlsdzRVYXU1MGY0cThhR1k1ZUpHYXFocXNWVWxqS1JtWVYKSnZWYS8rS2c0OGVGTmNjUDdjOVZlVjRGOG1STmQ2ZGdnVHpYV0trdnBzSEpncGpZcXFBampRUVk3UkdQZnRKZwpub0ZKQ2tWN0tyb1JRaUUvQUJ0cGI0eUFMTXZmQU5lQkhRMW41VnJLeXhkZkZRS1BUZzJGekdxVWFwNzB6ZERKCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejhaTFl4YXRuMUgzblNRUmFhM3EKZ3UxKzh5U0VWYmt0K0c3bVZZM1RWRGdtNStXR2ZqWnRZNjJYY2tIeG1Bd2NWRE5lOHgrRnlFa0xwVWhQN1l2VwpSZ2xudXZDTFFWWDZxZms3QTQwSGkrUE94RHdkcGNEV1ZCTU1lV2pvdkNpQkl3djZvZEh5Y0hETVFZbWRxeTBmCmUxRjZvVTlheVNNVVlGTGZEUEJ3VWxUZ1VUbzVoQkE2VmNtc1g1cXEzK1MvaitDbTRhZ0t3UmNpaExucldyMnEKTWJQaGZFTWxMb2tXT3FxTDlpVmd2b0tNbTl2aWFOODVnRUhHNkJJdTU4ZUFNaDJLcGFma1B6RmNoL2kxeDJLLwpRNlZWTHdZQlBqQzU4NU8zbzZXWFBZcUhCUU1xcFdOc0VnWHk2STd5KzNjRFQwY0ovbFdVdERFMzY3ZGd2QStyClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG8yeTZNRE5UU2duY0NtS0xyVnIKUldJSG5BYndCT1B1djVXL2VpMjdJOGtuWERyOEl0MHByRHhWM0JFaGJPVi9ObExvMm5KWjZCSHJPTTl4K0dURAp1Z0YwMmhhV05FY2gwU2Yvby91K1RGNVllWXJJbFJocnpLWlN1WHpPajEyQWU1bjI5Z1E1WTZJUEVSWnlGZ2VMCmJOalkvMnZwRzE3UlFhTFllN1Z2OE5nUUlTUHZGUGYrTUh2VjRuU1pCQXdaNVNPNjI0c3h6dU1jRWRYQXcvUFMKNmlsYWxURjAyV2pZWmdib2NmTTRiWUxqMDY3MDkwQzZGMjRSaWg4UVM3T2ZWYk53U1k5cFRDWXU3ZkpNbnJKNApncHA2NVNVWXVEOUdCVGpjbmlZT2xSRHNoc3B3TVpQRXI0WFRCL2h1Q0IyalArNFM0NnRjcnN0amtURXloY2ZEClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2JMallBNkp6K3NYM3Vjdi9Jd2cKeldFb0pYY25BallSanNMejc1cC9KcmgwWFFHYUJudWp4dUpLaHpHV3A0VjRoM1J5WmR3TURwZWZHMVlvTVlkcQpJK2oySzRmd2hlcFZ6VERjUlB3OW13TE1IcUNoTUhkVDlKaENDeDVQd3hSYXh1dEZ3ZTFjZVNkdUtsTnFWWnhFCmZKSTRQVGdPRkZKdFNXQ3Jxdmsza1ZHVjVEUEF5WXBRVVg5MVFFYnZQVzdLeVlKaEdIaTRMQllQa01FL2RESUcKTmpKMkk0ZjlleklENFZSS2J1Z2RCYkdrOG1TN1htVXRhY25mNjlZYktTSEsxS0tEVTdjMkRpak8rMkNhWkIzNApBR0w4a0ZrZ3BIQXQ2SjcxNWFuaFB6OC9vSFJyRHMvTlFCdDFxa1BSUzYvU2dESlRtdkVwNEhaVFpMVkxaNTkwCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0dVVGQxMUVpUlJoeXQwNS9OckEKTFFUL0FqOEhEbnRyeTZWcmcyb2t0R0NKYjBjMCtoV1VlMjYrdUIvN0d6RW1vNFQyWVQzWXNMcXk3NEZpdEYyUApUZm1YSGY1cW9BV3UxdHRvYjNhY1ZMaXcyeTg1UVc5YkNLbktWZWVqK21QRytsQzFPSGpiWWM1OCt5cytMaWFnCmVIMkUxVlN4M0kxSmQ3WVM0YVk2S0owRktsTXZSZUVFNW9OUlNsSlZJM25OZ3VHYVNjYzRZTkNCeHZYYnk4aFUKaDk3OHIyTGtldUxNZTdodFVPYmJPeFd5Q24ySjMyeGZrU3dhWmpuKzNMYXpXZmNiQ1lqSEd5NWN6OVgwemQ0dQpLVldKS0N0UUJKKzBZNEhVSVVsZi8ydTVTbnEyNnc1NThzT1ptd3RDMnc3QmxERDZrVjVCL3EvckpzWmlVaUVZCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmVKZE5Rd2ZTQ1pmRStyWmM5NlQKZUNyN0R6a3F6NE5WcjVPM1B3K21idjlBWmVIZjF1ZVZtczArSkhxL2dQWmxOUDBENlA2TUZMbWtzWC9YTFgreQptN012MVdHR2FuRDZhTDFYTlVwZWVMc3paRTBCNlhhYWdwazV4TDdSWWxoNGhvZ3dYaS84QUlRUkI2RGx6bWUwCkZUR2NqcXk0WDl0OGhtT1h0TWl0R2p1NWo2dU53SEx2Wm9LY3VWbWhtQmhvckEyV3FSZDhGYm1wQUtwbTl0c0YKUjhxak1GU2VYaVBEQmVqVWRLSWJPNm9saWZ5WEtpbVg1Y2k0VGF0MmpxSHcwN2V4a1NRNHNZMVFJRFRJV1h2UQpNSm10NzVZdGg5ekZBdEVFTEd6WHIrdHBqbC9sTWRUeHhNTTdBMG5JNUpkVnhHNjdzSE1wMkw0ZFY0dmg0ZDNwCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlp4bzR2TEdrNW5meTJSZ0VQWFkKNVhWNDlRNi9NRTdFQTZMRVJOdDBtaDhLdTlRL1E5bUM2cFJhSDB3b0NyaDNXMFlPcUZsWHZKcmd4NHNPaHg5UgowUTdSWU1NaDhSNkM4Y1JickIyYXBvYjJUbzJYSnRaejVzalVzVmd5UzM0WTJsQTMvUklka3VEN0hzNVkzbDd0Cmx3OWtNZzFyYk1Sci9la0Vkd0V6dG45TjJ4V1Yrb3VJaU56NERHVWlKSVBuOFdjem82aDVyekk3RUNRVEVMRGYKZFhFNFlHcnZvRU9uaXQvWGxKOFo2NHp6Slc4T2RNSm5wclkxQnY5Yk9MeXkyUlU5SmFmZ29HQmxNcVg0cC9JNwp6YkZENHlROTVhOVhaME5qbWlwMWl4VGExRXNZYnNueXF0c1BVbzNBalBuakFyZWZUY0FYeVJiTWJzR2pSaW15CmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlpjb1BVNVdDOXczb2Z0SlJZcXIKV3pMY3dJQUJTUnlSRGhSR2dFVUp0UzBTK1U5QWV3TElIU2FFcHE5SW9nNTB0UWZFUUYzQlZncXR1bm00SGpmRApuUWpuN09tUzZ1VGxEZWRGY3BLYkIvaVdlYm1sdDUzMGFDcGU5VTBKZFlEcmthanZ4Q0s1ME1GMnBXdXZXaStYCkIzbXp1dkhCS2ZXNEJoV2g4aVl4cjJ5enVaNDFQZGozcWpzc1hFV2tFUzBsTXA2dmR6MHVVcVhrMzYzOEtqNFUKaENwVVdOV25XbWxuTndCWFk4NUVmRWY2ZWhmcEY2ejQ3S3pOZWdLS0pwYnFmblYwcllNdjZweTNpZW05NXIwMAp1NldwRnVMb2RaMXRuVDl1Umg0dDJHcHFIdG9zbVUyVmRQNlFJQ2Y5c0tSbStjTk16Rm9paUE0SklwR2krUS80Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWhTWndXUXpuWVk3SERnM2E2SXMKL00zM1JiLzhac09FNHVsUXgxaGFRN0QwVC9jY1RqYXJWamtSRU15SW5paWpPSmJpYUwrNFZsL3BsK1lMeHZQRwphdllKWkdJYjU3ZVVER0h2SGJiWWlXbmFGcTI5akN4ckI5d0RGNVQweC9WQjVqcUgwZnJZL0t2WGk4Z3JqMmJ6CmlvTHBGUE8xRWh1ZmdEeUs3S2JHL2NtYXNMS3NXQ21WTXJTdVBUaDRXUWpaR28zbWxjUWE0YnV5bC90NFd2MVEKbWtsdlNsNS9JTmRSUzA3bHdnMFVDbU0zbHkxRGxYRTBtbDRhY3RWQU5jdzZDWm51ajExeVQzWnNPZ0hQZ2gvRQorWllNSytITEg0MXQwTWFCSGY5Q2Ird25tQWttNjdkSVo0cWZaY1F0NXhBU0d3clNMcTErUUVmSjUwa21Sdm9KCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDV2M1BHbEJBR2MveVVjN0kzNVMKaWlsVjJyNTM1MUluVTdBS0ZzaUZWREhoOUNiTTJ4TTZIUGgwRDFXODVES1BiSUI3R0hTTEh2bFZmTHNnaFV3UwpRUGs2MlhKK1ZSdFdSZU9PRXdHNHJYNHFTNkVLQTZFN3VCTnNXV1MrUGZwV1pLQ2t1Qnh2c3NHd0dhWHpYcGlPCjZEakNFOENITW01ZzB0MUNra2dyTzNQSjlqSFhkUzRySFhLTnpUbFZ1Y01FOFY1OC84SFE1T1pjSzhCVEE4azgKV2tCOFlpeVRqcXpQMTRmems1d2Fza1d2bU1JZDFSdlFWQkx6OHFhVWhoWEtrdjltci9ZMVoyWGhNTll1aDY2QgplZlVQb0dseWRlOWZEYjJyQ1oycytBdndHSmUxZld5Yzd3c2kyS3VDbjJSU0NmcVgwZm1KcFkvUzR3eFRMUkdLCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdy9tKytzWTVnOXdkVUkwOWVKQlcKdGg1ZGF4L3FMVDRuOXd4ejMwYTEzUnVGTlZiZ2ZHbjBNNVE3Z0lqRFFvZW5Ca1FqMTE2aUlKbkJ3T243RzJTQQplWUlkdU92aWtjOGcvMUMyaU93QlRPR1FpeVdlVDBPM1I0cTk1OFR2UWZoTFRGMlEySG1VQ0ZQQjZHbnZHTjJPCjBLazRUNGJaMGRBRnNYc3MrVTdZVFBydG9RTkR5c25aQzFtQVZXRXMyT2RFOUxWWkFhSUFkL2hNVVg1WEV0S1oKNkdsTURwcWJWdk5TeVYxRDE0RzdxdFJueWhpbGYvZ0dWemcxaHdjMEFXM3VnWXRhQmF1V09TeFA0NmVCR3BsRgpsWnZZSk9CL2MycHRPcEp6Rk9abTh5bVYvN3VBcVJWVGxBNGV3eVJRaUVLeWlQVDBrYW9MbUZkdDdOTEl2MmJ2ClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNE1vZVBiU3VEVUttd3lDVHM3UC8KSWlWVTkzeERLcnlodlpOdmJzSWZTTFlUbkRucVFtTVgzOXU4eXMvZ1FVVVRTYkFBY2h3RHJYTE54Q3kveUlxdgpFbDlGdWwwOEVuUWNDU1lwMXFRa0Jaa3NNUHljdmlMUnNieGhHYnc1MnBRbEY4MVJCb25KNEZrdUpDY2w2ZENMCnNMSEpuRWg1K0gxTzJnU2l4dmtmdEFnQTdNcnBpWkF6cjlNRTh1MFBrblpkU05wWlQ5RXNFREtCcU50RmYzekUKVzNWVTM5VzRkeWo2UWhDQnhCYm5FdWhjdnVudTQ0N1l6TDBGdkRWQUt4SmQ2dWJRTUhjZkozTlZ0OCtZTjNWOApJdzEzdWJmVGl2YWtmYUdCb0x6RXF4WHdTSlIzc1hnbDRqNm9OTzF4ejFIY2g3QXkyOHdOY0FTb2FFNE1OR0dRCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG5pSFR6SXJpS2dHKzRWK3B3bWkKOEdTZ0Q0OWxhampZM20vVExCZ0d0QXRHaGJnMUdYT0pIbjEyLzNJWFJnQ2VZeEc0N2kzSlBTQThnWXVwK1ZtWQpxNTNFWkZYUjNUZjI2N2EzRlZQNzBDMFphdU1zN0NGMVFpTGU4S0QyMGwyRS9GeVlFU2tMbkZJSTlMUXRXc3R4CkhLWTRJWjZGWnVvTVpZc3V3VlJJbCt1ZXdySXdMZEFkUDA4dW42MjdVZjZzdFRjSll2MFZ4VzZnM1JOT1Y1UTAKVEswVmVydlZCcEIyZzh1Z3NQenprL05NV043L1FRV0U3VWFXa21sd29xVEc2REN2ZGVPY0wvbTg5VU5GemxjZQpkSkxEY282Rk42aWdRRlBZVUgvU2NOZklkdkFoL0Y3WjNndjRYWVBmUVE0MVcycDMwenFiMTFmbGl6dng5d3ZrCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTQrK3RLOHZLdEp6ZExWZDNobGkKTGFNejBiYVp6RFBWT0Z1WmNqUTJDbFVpK0JEUy8yejRETXRUbUJHRW1iZi9oZU5hNTRpbGVZTFU3R3VzWlJTdQpiZTFkMXVzSUl6bVJXQ0syOWwxWmFyYmZteUpQYlZCY1RLOGw1YXMzZG8rSmZnOFl4cmNENzNGa0VLU1pJMVA5CmFzNEp6NGZDclJpcEhOcjJ4b1htejVQNTBBWENjNXo0dmZ3aDFEOTdTUlppZ055ZThQQ3ZQWDhPZ1FrOVZKc1cKYkN1Yk1QRUpXdVhHYTZMWFVPUDBUNHIvR0tmdFpqM1RFMXpWdTRGVFR2UkdMemxBTG00SnBLc1JjM1dUdzBlUAo1K05NckFabGR6NG1RdzEzeUtuZ21SYWEwNlpmb3FnSnZyZEZJaWEyZExFNENzTDMzVnNDZXhuaVVuYmM5NzRSCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXBvUXhJdWxlV1NiYkdmVUNMSVUKSGZ1ZTk3aVJ2amJTRk9GWHE5Q1h1dVR1Y3RIbkswZXpvQzZUa0pDbFlESGY0Y2d3VkhLZHpQblJlQ3ZlNFNDNwpUaUQrWG1DS242WGVRekM4YVpGWWVjWGRUMDBnK0tXNzM2MDc4UHV6R004YzNqQmtUQ1dxNTdLNzN5YVR3TThXClFKL2NsdjBlY3IvZXdKbFFnaW9HbDljQlU2REhwaGlPQVJIdHR1WDVHbzhwcHNEVzJrdWJ1UnJQeUJsL01ERXIKZTc0bS9OZG44SlpkS3p4MzdQRXJWbU5hVk1sSUYvVnlDSytCbjY1Z25ZNWtIYlBNOVQwcUtxbEFuRy92YXBHRgorby9RREhXTWZuNWw1M0lWNWpHVE1yWFdtYWp5Y29DbzlldndjWGc0Y3JsMzlVVDdNSmF3M1FpT2pnb2JuY1VSClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemd0NVJlRkM3ZlpWQjFNOVh2MTAKZkUyWXFxamtvbHlYS01wMTIrY0lUaTlOcG44SGJRSVpyT3NyTkM4RFZZb2pFcDdnbktkNWEvRENlbi9ZbHFpRQo5dlRNZzdnNlpRK2Q5YUlramlDZ0ZsMGd4UzZzbTBLWmRqRkRWYlE3WnFQSkh1eFRDTGJsL1U0ODltRGVQanlRClZvRFl5eTVrUzVtMGdsRE5NM3JZcXdJbDMwR1NpZEdoSlZJMU1uRmdpSWpoWEtZL3NyejhXYjNPUDJML1kydFAKTmdmUWFDV3lGY2Rqa2pId08vbTBqNXRRNGRWYWdrc0IzbTlBOTQ0VTVSd0lKbWQvVldjUWZIWUx3Y1VvY2E4cwpBdFRNSmpReXZkUDhMZHpZMjlzTFpTYlo0b2FFc3MwYmdDQzkrb0M0bDRva3dDMFltOVJWT1FEVFZCODAwV2JQCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNitZYjVUTkVzSXIyeHNKSzB0VXUKUUFqTTcwWkZ1N2JJeG1DNE1zUWhjZkhUQlB3UHNDMDQ0ZkNXeDZmc2djS3V3WUIzeE1uZXRSVWJvRW9GZDJXLwpZUXMrc3lFSnI1UTBCb1F0cFdPa3ZlR0xWREFlem0rZFNCbHdkdVMzTksrbDUyNEEzNVVmTWVwMlRTczJQV0ZpCjZtQ3piV0VuaG84OTRoTUtnTEVGSk85WlJXL3lFSmhiZGlnSUVveWNpTTBwdG1jMGN6VkFlUFlGMU91aEE1L1cKV2s2enVHd1dhU0VkbTBtR0djYmhOdHF2SmlLak93RGdXbnV2SU5wTExxbTFHYjlXVVlja0VXK3Z2ZXVOMDhOZgpTQVJxZDk1QWc2a1dNa2c3LzliOWFTMTBZYzJORGhwL29GanpIVEZVMTZ1cHRZM29RRWVnWFpVeU9WRnVGU1p6CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUo3ZUNsTWlyaHc0ZTVIK1hoZmQKSUlJOFR0MkV0ZytUUURJZkdoN3B6Yk5MVkRZaStwVDNVT1ZxTTJ0RndqTnFiUk1CSCtMY2ozQ1M0UFBQV1IxYQpKU09OMXBIeld5MEVGOTJvekhLTXhGaHFiZk1uQjg0RzNQeGRlVHpkYnVkUE5aaGJBeSt2L0x4akVmT2FESU4xCkpyRS91UytGMzNCVkQyS2RvdWNnQjRrcjNtRmNJT1kybXRKKzJweFlOc1ZZNjVmNVVhVllyaUdVbS9Dc2dmZFAKekFjUU15L0dEbDJZdG1ZQUVLVUFudVE4RWl3VVVXSVdnem0zM0V2RW9Sd2kwQW9KclRNN2x6YWpjWXVWNzVnYwphalh1SmI5QnF6NmV4eVNFWTZMRFFZSUxtbTlGc0JMbTZGaEFBZ21TVW9YTlBmazVpK1VGT1Bid2ZNeXVZcHJpCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMExXUHBxeW1SVjY5Wms5SGJJcXMKVzQyWldydVdNRWdLQUN6RFl6V1dEYXFaL2Fjek5PNjAyTEc1SEpnRkRiVERFVU1rSWdLa1pOdWp4cmlHWm9adQpaY1AyK3NFM1ZIeXdLN2xXdmlRMWl2QmtueWdtYmR3S3FqRjgzSzhBT0E5eUU4emZyRDZ2TjhyNHZzcC9VbGY2CmRseW5mcUhYWlgzZHc1WnhLNUNnNWhxMzV5VlJnUWdwc2NLUlhURENZKzRYdDFRTjQrZnNobFd1b1U0Y0habE4KUFA1UTZEd2xqN1RrMndBVzlxSU9xNzI5L096aEZRUkhjcnpZV0F2YkZpTHVYak1Vd0FuZ0ZsNy9RR3EvN09MVApkckVnbjluK2hxSWRURGZtcER0QU16TUJ1dVllczRrMk5DaThpcGlQbkFKRDdwdnhpU1JkUUVaTjlWNmtoMzAvCklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmVFSjZ5dkRhV0NRdG9abVNJYlYKUHhLU0JQU0gzQ0k0c21lYWl4RkwxbGZJS2hjdTM2NURaa2FyamF5aUtYMFFvZTFQbTREMVNEU1pUeFVBT1BDVApJV3U4Nlg5TktsNENhWFp5YUVocFBYdG03bWxRRGQyRG91OWlrYjc5YmJPYnNDWFhFcnhOamlzUFVrT253OEp0ClloQWJKMjVCWCtMaG8rT3F2VVhPOEZSZ3hMcjk5R0JnQ2RHLzhDRU9Fa2FOc2thVGgwRjluV2x6NitYd3FXQ0YKZ0ZVa2VXMEdtK2ROS1JIL3MxcDc2d2szK1BDZG5HSXFGWjdsa0syMVRmRUtsckpTL3JkU2ViSzcvaXkzajBLVQpZdXF5T1FuQ3Y3OTRqRDhwVGFxRlZHV0ZKZEUvaWtKY2pGM0pqWXpBb2FzWWt6UlZZcml5elM4dHF2c3M3OU4yCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUpVa0FSd0RYSjFSdU15N1RRK1QKeFJHbWhYWTROWVAxTi9ncldpUzhGSzQyRFFCSjllaXBSd1k5RTlFRVlRVjA3dlpFSTZGS2E2MTlkRU5TRmw1cQpHRW9sSTU4c1FqdlZTQ0plaTN6cHZEeDF1WlNyN0pZSWs0dFYxRndjcnlLbEZzQytFbitjd0ZQMHpDNy83S1I4CkdTY3dIQWFHdnQ4L0dRK0V0R0QvalBZT25VQmJGSnpjU2VWQ2ZoTm5iZmNoTk5QNTBEdXYxeUJwVHFaYW1HcXMKcm1BaWF5b1NrSTRiYXh3TUUyUG1FdnhiZ3YwbTBtOTJxdDdrUExvdHhZRHRoQVBLaXptNkdHT1FuNGZscFZkKwpjeFF6TXJ4bVZpUkMvNmVjK2hSa2sxd1g4UWJoZTN2S3JkclIyU3pxU1dCb09zZ1Y1Qk1BN3MremdLdWlWQnFyCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbm9IUzB3Y3VkYTR1T0lYSXF6R0wKb0VzQXdqcnkzeHR0QzZtbGxKVVJVSmUyQmp5eXJDeTZYWnBwM0JSWFRmU3Vpb1FRdUJEQm9FbDVLQWhJZDkvYgo4ZjFtbXZaazMrWE1pY09rNTRqc2xLUUhxK3NSQ3VROEhxdlRZSG5qNGNDY0trenBhNldHNDFJZS83bHAvOC9kCkFDMDVpTm51dXJoUXhFRW1RTEdrS2FBbDlnR1J6MEhMQmY5OFBBdDNzWERVMFNXTXphVjhnSWtrTVRxNGlNK1UKL01lbjN2ZnhYMzBSbHRHa0loVnhkblRoTWVqOCtLRzRQYTRSeW40M0NOSHg5WmhNdm4vQXlBV3dveHFGNFdtUApWOU1jOEQybythejVXNURNU0dNMnJLS0tXSkdtcUcyWmxTOWNRdmZGVTRGUXJwY0JxMEg0bzFIQWFVUHIyN2RWClFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWRPV2o0VkFuaVlIeVVudklEQWkKcWw3MkorODAwK3hxVWtuVW1tSFhqZ3hsOGxzRExyLzRXaE10cGVXNXJiUG5FWkhXUXRCYmxiODB2M3VFOTdVKwpiNDYveUxndmxHZ1hzWXFXay9rWmZScll2R1FTb2pDRGtwNVd5dDh6bXA5WDU0UlJFMGE2OVVmMGpOd2FTRFJuCjZDWlV6dk84RHhwZHppVU11TVlock9tVkxtY2Nhc0IxSEt6dUJlRkV5TWw2MGdRQS84ZngwZ0p2c1NJak5BR1EKak9tTUs0VUIxSERBTU5QS0owbWdJclF0LzJySVhJVDRaczA4eXFMWjllYldhTmxDYStJeGlseGFraFowK3NkUgpYYllnMnNuS3BTQ1QvelUrcjhlTlRxRzM3eGpyaTFJdnNGSDZIQTQ1RWV1a2pabVBEdU1pVkt1YzNtM0JWWkVYCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdklLdjVqL2piempNUDh0L2dyWkUKZFBYb05udlpoSk90VXJ1eTFHd0tzN2RKTVZLSnFRYld0eS9JMmgxR3VtaXNtQzI5YXV5d05UZ2ZIQm5VNE5vbQpRd1pEU1lnNXpHVko0eGNjOWh1V3RYNng5TGFteU0wZHRVbGN3SENFdDFBeFdhcXpLVkdzUzNIdDhRNFZJbk9SCnVJYlpLc2dITWdiejNqS0IxNUs4NmsvNThiL244UkVkMTZMaEFiZG5SZGRUWTBqQUYzWEZ0cHltdVRXZnpNaVIKVmI0MVdkeVFJN05JN3lUM2d2K0JHRkEyUFVQQUNaSWxydThaQkljV1RDRjVEUWlMMkd4Z0phUTMrSG5FMEd1Lwp4UDVnSExBdzF4Sy9PQ0Rxb1VDL0dSaDNBTEw2QVh3cWJNaTVxbUtROERmUUx6OUNuenFSdXhrMmw4ZDQwODdWCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWFrUVlDeDdBNFJmZzB0aHQrTXkKUHM2eHhGTHl2dmU4aCs0TmQydnFZS0xDZThhQkdHMGViNWFOckFGVCtmL0R3VXRwQi9DQzl2Q1RvTFV6MHd3MwpnV2lsRExFKzNaYW1UZ3B6TkxkR0x3TEpobzZsRkpzZUhKc1RYeWwvSkJ5NGhqb1NiMStxWXRIMWlETWpKYWh6CjVHNFlMWEVOQ1VoTzdtTTNIeDZrMFdMMCt5Q1FCcEl3TGc2NTFLT3U3NlZpVktCTU5ZOEZ3Y3lQdzJLaEFQUVQKYlRBWWY4ZVJoYnpBTWE2NElMOVh6aURHZjFobEx3Tjh3WlZzdkwrZFQ4VVpNaWdmZ1pIaXBPZm16RW5WRXFwcgo4d3hZMDBzdU5uMytJRXo5WWtmKzhGbVlBSVorT2ZlTzh4aDROSGFmMk1HTlhOZjN5Vy9wRElmV0hLb3IzRStTCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2J0bzg4UVZQQU13OGtjd09vTEEKTHN0S3F6d00wZTNIejlFcEZ1V0oyaEVudU94aVJSQnlIUE9ZeHV0Nmw3QmtQV3V1ZDBlUmRnZTZHWmFmWkhlNQp1TzArL05QYTJHRnV3a2RTNzJsK21kLzQxK1lKaTVab2ZGK0hjcDdWVjRzWUNaZHpqUXpDVktObVpsazlxOXBDCllVM1UyUzhrK1hrWlFIWVFpWXVxQy9qOXIwaktHUXpxSnlkWmdjTzM3MnI4d2JFcEs4Q2Q5QmtxRksxU2psTEgKb1RCdEJIcDcvMFNOaXJwNDVJaG1tRkpaaUhzN0xrY0pVNkM1c2lMcjc5aFBHb0NiWXJZVktYUXE2eWthUHIxZgo4QzFRaXZRMUx6UUVFSmRzbDFORjB4bUVsRWVqM3gzTkFYQkNGcFI2LzJ5N3NjNnVyUGdrSklUaW5TWDl6MnBwClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeklHcDNVSFcyelRpdi9KdllYUTUKVy9pYXc1Rk5ieE5SK3lqNEVLOEM5cGU4cm5zczhlQmtTaVA1Wlgzd002NG5sYzlvOVFzVEhyRG1IdzZhcElTKwowYkpLYkFXOC9IRDVubW1JS0pNWW91N3ZLakZOY2I5NlRuRytaREhFN2UvbFJDRjZtV05YQzFWRHNyZk5mSVR0CnJlcTZDK0plL0toRVVHWG9JRlZOemY3YUJCdEZDTUpmbm1UQkRKbEVPVEFlQ293SG5hNG9uT2FYbExCYUpZd2oKMEZmWGFsNzNTT1BCN0hyS1lOYXVTa2YyOGRuYzMwYnQyY3pQcUtkbXQrcWZyVGpJb3VsUkh1WUQ4ZXhSNUpSMQpKUW40cVRFYS8rb1FkRktNMWZndFNSSytKcWdDVGVPMDQwMDdaNlA4eW1CbXdBZVpBRGM4STVhNnJHYXI2RG8xCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUk0UG5yZkNBdHExcTBmVEhtRTgKV1REYjB4bHZUemNaTkVXUWFaV0djKzFENEhVQi9BVmd1RE9vQklpbjNLeHdYc3RlZDJYMHhKL29McTRZeHgvQwpJc0p1RW1QMUprbjFXMnBtdENWWFc0THBMQ1NqRjVSQnhwcmR2WG16M3RwQzh5cWJsZWxvam5lWGhRL1JsakVJCkpmNzVTNmI4RFpnUGtHbXFHd1Nxdzl6RTdWdFBFOTdXWWxQbW1SR2g0OTdtc1pBWEFndTdaVzdPMjRtUmlOUnQKQmFIY0hLbmZFQzRiV3A3Sk9acUxYUXVtUEFlUFR0YWdNK1dNQU01S1JkOFF4ZGNZVWZKSjcvU3RXSUo3NWlRTApjSEtnUlhnQ1VpSXBITmlINmZlTVRYVk1qSWRYOFRuZisvajZOTytUUkt6TEo0UFhrRGN1QXZCVGQ4RkliRXY5Ck53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejdMc3lpZURlSVNkUDUwbEp6dloKYkh5WFFrVDR6WHNCWXcwdSt3TFhKNHN0STdReExLNWpqZk5MYXE3R25nLzhISTU5djBlUUFRckhOaUpldXpuTQpSaXU1Sm5DN3RNeGlpYVVFZnZGY0tweUxyWmlQSjBCK0ZHZytaMjd1NVgvVDVJKzNoeTN0ajVMVmptejEzQkN4CmlrRzNkMGF0dVQxd1Y3L2pqRzdnWTFJbDhqUU5CdkFxYUFXamVKbW5XS0kwV014cGc0RWRqanNlV0lUVTF5WisKTFBCbTJOM0loQjZEV3MvdStzVHRFZWVld0NzSCtyVWNFNDF2K2VCajk3dDJIOWdscmdjRDNNSTVQUFkwSFBOQwppZXE0NHgybEhOalFCdEhhZktCNXAzOGplMTd3anZJMXVJeGpRZDVDSXRhU3cyS0dWaElMTEdoKy9YYUQ1VkxSCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOHd5WTh5aXV6SkdnbXpiMys0VEcKR2dZbEVFT00zbllpRVlVOFZwWVJSUTJtNUk2bW9GSGVGNVFoTHhQVTdMQXZla2IvdXJoUHFsWUZVOEc2RnN1Rgo3WWhMR2dLSUU0QzFsejZYdjZTRlUwNVlpeEl0VHVLbDFvditHbjVUaTVnRzdIVVBteHJrSEQ2WFdpeWU3V21oCjdTaUd5M2xLK2ZJbDdPbE54UnFWUEJHcEdKMUhhaFZCbmlGaWlGNjJZQVlPaUoybmYxdDZXMDhhY1RLcElLQjYKS3VCY0NuUktpR25RTXRNdW1nT20xSXF0MllnRjNMcGZvR3hTK2g2TjBEOVpCek9peHMrYmN0Q0pjL1Y5dHQxUgpEb1BIZGZtQ3BySGcremZOZk1CeFQ2MllYUU1KVlE1VlFPYWErVXRzTzlNWmdNdnZHa3V1VWRsRi9BRmY2WU8wClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWhwYXRxZk1mUDFNaFlKRzlEMjMKNlNvejlzUkZ5VGp6NEFZMFZldms1SzVFbnVaTWJMVENnRjBpNHd2akxKZGo4TlBYNC9uT3dWdFIwWW84TEMyNApYczZYTytPQ2J1T01rK29QMVFnV2MrY0FrdnVWV3hnNGdJWjU5WVV4SjdnSzNhUjNpQkNERmd2VzQzeGU1cW12CkZMRVh0dEVxbGxyZndyRTE5d2ZEeWRpL25kSjJlQUs2a2wzU1VSWkVwVC9ITXdVb0VWTGExVEIyNFJYajhhZGYKNnZJNFpOUnZocEtXWk1aR3B5QmxTU0JlMUJNU1ZsQlh4RnlocG5qckxvUHo4OUhzQlBtTTVlcktJSFMzVXlzcApaTEZzVmw0RmJWTWppL1lBQ2pBa2E2Z2N1Q3RQSFVkdVUzTW9SS1kxejhjU3Y1TEhTMzA2ZC8yRkNBbUdvUkprCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzZPRGxDRnhoZWNZSDZRa3NGdWcKWVJVM3h6VmFBeTRMaitVMWxuaitCbFRUckJrRkc1NFYyWHUyVlFoZ1RpNkdrVCtibFpwSE1ON1R6MGZURTNLTgpRVGlXQnlFVEhCQ0lURm5oZlM5cXJ5bndJa09NaTBJM1ByeUMyMFF3V1ZWUC9OVEY3TWRYeEJMQ2k4V3Vzelp2CjhTVVNYMDZBS3JpVU9BKzFZdXV5UGZZamo1NXh0dzQxSFg5aTBUSjM3dlhvR2VxNWk0NmRySlJ4MzEwU2Y2dmsKTExHM0tJaURwc2ZSNnEvODBuQTNNS1dSTEUzMzBURFhWQUNOQ29FbUZuOUVla0FsTDR3Y1JtbmNRY2ZFV2p4dgo5ZERDR2RXa2xDdFdxYzJTRmM0ZGpLTktBQ28xWERGUGxyb2wydE5KMGQrTTVnVmdMUHhRV0ZTa3hLVFNHcWE4ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlNCa0YweUxsYmFIZEx4clBmRzUKVisySE5INDdWRUV4ZTlFUU56ay9Cb3QyaW9HUTNGcnNpYTNrNWVRMCtoZEkrNERhTXpvVnQyTEFVb01Za3RZbQpiSERZSDJBMktEUU1JeHo2dFVyeVRHNDRhSWJyaVlINkF1THhlZUNVZ3ROM05DblVXSDFwMGlzVnBSSS9CVndSCmpqdnZaMzZwNXRVQ3JZVVNpV0U4L2NuOTFEWG5EM2JqeURwb3VkVy9yZzhZK3BBTzhSMVFackpKSnNqSE5aekUKdlBCSzJpcXFPZUQvV0pjS2IzNk8vRWptcTkrS1RiTTExeXpNY01xTVB0WXF4Z1F1REFnR0FvUk0zTmNDREYregpZZkk0ak02Zk0zQmprSDlVS25JMUNnUk9FVTBnNHlDTlZpVDFKZW5tWUJ5dFpHbHlyelBCY0xjTEFNeEI5ZFc5CkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlZFUFdoK29mZEREUitqNG51NEwKSkUzMlgrMnBpZXRzT1p6VEQwQWlKL1kycm4vWjNyWnBVK1FGendSRXJwSVd2MGdCaTBSdEN1cStKOVpoaEIvYwpkOUMxdkRST2p2d0NzZG5kSWhWeloxY1p2Z2hnc0NhMWh1UjBXUnhpK2FrekVHYUNHak9HQkJWWjNtRk5BcDh2CmFhYmZRRHZUUWJ3SDhkdDFBdTBMbHFqWFVRQzVSMG1zVHNVVlFCZXJydHRrdTQxZDluaW01V1ZNNjcrYi8yZlkKN0dRcHVqS1dKOUxzZFQ0V2ZlU0xkLzdXQ3g5cXZYTURFT1o4R2pSUDJOVlhFb2FaU0poZ3plNzNrb2pSblpFRgpMYXYwaTNyd1pLU09tTnlZbHBEdXhURVVGblVmam9TdUVJekt6Qm83bzZ0ZUcvenhCRERvc3NzOC9HckUySXNyCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejdzb2RobnJTb3F3QVlQOEdTT1AKalNvQktqeVd2Q0JsRVdLTDBBenVUWFJqcmdiakNnc0ZJb29vZlF5TnE3bGlzNG1jUGw3c0VtR1Y5TnlobFVKeApMMnJ2b0pOdzE4YzUwbzdQbzI0QUNVVVBta1NOaUc0WW9UUjJSbVJYbmxhWDk4ZHNsOEpJaTJnSUxHMmxuM3hhCkFDTS9MYXQzWWVmV0JzenZWN0grbVRyT3RUUU1yM0VIblF3YUxweXM1eDg1UTR6MmpHZ2ZXNDNwK1laMlV4TU4KazNwZk14UTUwL1Y4TDBmYm9ZVzBCVk9vUk8yNlBPam53TnVxNXdNaWF1Q253UUNZRGtsTFVRUW1GdUoyNzI1SwpMUnhQUnIwTU5rSXEvQnJDM0RBeGY3aEpGblVhQ1BReG02K1FlaVYvSmlNZWdVbkE5MitTZzlzSXdtanh1b2liClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0JSanlrK3ZRK3hONGFqeFZmZ2IKWmdhVkdHYmR5U2JVZmFVNTlPelNoQ3daRy8vQnZVY0tqYkFTTWMzcTV0NU9JeHdiR2NicVNndndyT3dmTWRvNgpmeThveWZvWTkxS0thOHZYMjVOUjZDN0pvRGxFcloySFVRZGg3UVlWeVhURXdGQ1p0eEgvZUc2MGIvOUxDcWYzCjB0SitDTnN6Ri9BS3g5ZjJnYUFQc2ZWdThpOStEdWlZdndKY3NIcUxkSFdPVnZNR0J6bzB5cVZnOWN5RnpPc1gKblVPcWZXRGJxcjlxTFJ5U1FyNFNJNHB1RWNSRXg2aFdoZzM0eGpVTGxPa0d6ZW45TEJRZk9YRURhb0lBTVorSQp2bzh3alNHM1NMUmhoejJaL1VhSG83Q2t0bUY1SFJyYjE5QmM3cTZ2cThES21QYmVRNEorVEZCSUFpMngvZmk1CmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1Z3VjhBR3FQSUI4ZjFOWTlqOGkKTDF0U1lsVmdid1dSNlMwSFVuQWRLZEJKVFBqZWczdHVoOXloOTdlTVoxTnU4YVFDWHpab0NRKzBNL0hObUV0Ugp2OVNJU1FVN3IvRGQ2REhmcnpOYjRkaUk0MmdLUTd5aTlDRWRPRUJKa0JlSEZLdStlVjMxWUdBWVZNQ01pRFlCCkZraEJNd0RBK2s1OVVuYmFnSjdyRXdNV1k5b1NSRThJVy9sS2psNHlrZDJqRXVOa1lnLy9zcnJGbEFKalRYRXcKazU0a2NURk13ZzBIYlhRYlNaWW5rWWQ0UDBEK0dZc2hUWkZFY3NWaW00TUZPY2loeFExN2pLOW5zOUdLUmdibgpNVnd4Z0tuK1hWVzFDWlVkdnhLNHpGc0ZoUlRCR29pYmUvdi9qV2tJM3c3YXlPME04MG1GRDEyNGh2YzJCdzhwCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFJqS0UzN1Bhcm5KOHFWQ3B2RE4KMElaZlVBcnl2R0w4K2NXQVJSd0YvL08xQWhTL0JLS1RBeUFKQXBZTDZvYkcwWE1MT01HYW1qVWwzVW81WllwQQpEaHUzall3aWg2ZUZwM0o0a2tzMktFSllMNko4OE1IUlhUK1hpRWpYTXhMK1FyQmk4OHVIcHdSd2NYN0RsVDhrCjYybVZwaXNhU2tOR1ArcVMraFcxYUNHS2VpYkhJbjk5em5mdDZ0dnpzUEplcG5HVFJZbnZrRHErTFY4aG1XTnIKc3l5MlNoV0hnYU5qd1hWQnZZZFhYTDc3SjdlZUpSNjRWbzRKaW9QbUZWYmwrMENJdXNYNytpZDBBcGl4K0hpLwpjMitqT2RBZ2NLNzhPbkJMYWJSL0xHTWo1N2kxaDFsQmwvQmVPQ0ZDaU9LZ0lQb0dvYVlEelVLWWN2SkFURXBICk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVdQQWplMG9aQkRaamJxRnpwTk0KTUMrNnBPMjZWeFZvRWV6ak4xeURkY0dESC9tak1BZk1JZ241NXJ4NVd1NnFNWmhwOUx6RXJGbk5pcjhCVkxWUApaTTBaWkY0ZUNMN3lJOXZQZlQvWEtGalIvVEJPRXN4Um5lUU9scm5VMDUxdVUxZXEzVmpnM3paRkNMcVF0MzdPCmc0K1IwS0lnbkZ2V1lwM3RZeVFtTWdDSlJ5bFlycklIZ01GYU1KUUlRNWRjWXNKc3d2TGo1dWJtbTZ4SVpQRnMKajJxcE1HL2tIaGlwcmRON2ZWdkpldkJraHhBN2pmcjBGNDcya2I4OEVqWjl3czRuS016UjRxcWs3aXlDRTdLdQoyd05sa0hPa3hJWFg3NHF6MWw0dHp1QUkxbkNza1hoQjFjbktGa1A0WDlQVThrUEJYdW1wL2J6OGJMeUxEMHdTClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0ptQVdGSEwzOThxZENHcm02OE4KaFFyRG5ORkczQjNoeEVQdmVEd1U4REwwdlc0YklKR2w2dEFQT016TnM1OTMvczZpSE9rWHJsdzEwTHNRdW5JZApQQ2pVcVBqajkzYWhnUnp6TklLNXdKK21MMHozOU4xYmpyZUdJZkFGcFdwMmhWMlpReGczcXAydzhCR0NvMGI2Ckh3anY5KzBBZU1xbU5jVGFZOHA4L24wNG9SVHRFanVlYVlkRkVNaFhmcmFMblM3eTkvNFQwd24wNDVVSFhXMDAKY0R2enpsQlViTmNwQWhRYXlSOExocDR5cnZ1TS91UHNHM2JuSTBZWXM3V3d5bDZBbVRjK2ZpYTV3NXRONFJrcQpJMDc3eTU1Mmt5cVphdU5uZS9MRVpwa0s1WEs1SkNDemFud21xYnBObEJWMHdsSEx3VHZIRTVLZlpzcTFGV1JpCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE0zWFp0L3Y1QlFjNndoTkk0YlIKZ1M2ZndrcngyUGFHeG1LdHhhUWNzUEpLVzBXdjlGM0ptVVA1YS9KMlBYeWwraTNHVmpWK2JLb0QzZEdHVUFQZwppZUh0ZDh2c0hNbHloNWY5MEpWRDUxSGNLc1AxQ01FMTZkQkEzWVA1ejVBY1Evc3ZwbkRzMGpZaGRmaHNHNGlwClNnNlQxOXJUUmpGbWdCaVNXdDNFd01Md3FBRWhWNlJLWTd0Y1Z6Y1FHU3FEbkxJMXEvazdWNUdOQWVMaXVoVkMKRXNUVCtpZmlVbjJrVlE1dnVmMFBOMjJ1Yk5wdU5QajJWTlpHNVlFOTRMMUJzMDgyYldHQk1oeUNwVEVNQUlVQgpQdGxRQ2NmeTVSVmRnSlE2eTJKRTVSSUJ4cDlwRWVJWnhjQ2RrVThnWmdrb24rSEFELzFvbVNLVTlGaXRLWW1zCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUtzOHlkWTFPNnpEZjZwanZBMVoKZGdjeE1zMzNrK3JHcFdIRmdZY29FOUxuVjZ1ZUhObDRrUmt2QXdzMGltaC9jVStSeWhuR0UxM3U3VU8rOFN1Rwp1eTNBRktpMk9ZTlpiT2ZWNGRUM2ZaMGRsa0ErSjd1Y1NLam9ObGtBOGVJTjl6WUxRbFZvME5yS0JLbC9vMVVuCjAyQ0R5NXlwQ3BWayt6Y2g0bkgvS2hiWnByMFVnekhuUW9xMllrNkI4RVlQVmJQMGtoUk9TVmdNeXNtUzJ3S2wKMEE5UEdvc3lzZlJMZE1WNUFTb3IrQ002VEdjeXkrUlo0TTNGNmJyc2laSk1Rd1cwVWo2cyszSHhSMGNlTjY1VwpMc1FzL1JNMHc1am9idXEzQUZ1YUpMbERHa0ovemJaUXhnd25DM0ZvMHAzZ0ZKUEl4RlVvS2pPVDgzelhTU3ZVCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXFkay9QL29hc09va1pwcHZyZ0YKQklVaHJQZGVyZVk3SSt6cmJHWXREMGpNZnhJTll5MndWYll0WWhNN2RrTis2Y0R3WWMwbkRSRitGbEdkMUlhLwplMVp0OXMxK3pLckJDa0dzVkNpc1B6UG9yWGI1Q3NjMk9Zb2o3V056U2NkYnNjTXdNWHUwZUdzQjZPNlJWYlNPCm1TTERMQkZQRGU2TU9kYVphb3RCUERxd3ZIYWxGUVdiWEpiK0tCcHgxRjI0YUlDcno4RERsOGRlOGwwSG1YNUUKT1VSS3NVQ0JsWkNWMnIyYURjL2NNOGlTQkVXVTlDMExjTXpQSnQ1T2MrbVVHREovNUliNTFzbVJ4V0RxQmtDVgpjaFZHVndRQzY1MEhIRTJSMVVid0Z1c2xuY3R0eHduK2NHVzFwQkxQanlmamRaVXhLakJaM2s5ZTl6KzV2TDIwCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFB2T2FBK1kydnBKSEw0VHByci8KcFN5ODI5a3FXRUNSWU9ZcWlDUUhNQWVPRDd4OE52UlFHWlRoU1pQaVFBYldoa3pwY0N0dlN4RHdZbGNyclV6awpIZE1mZ3VyckNtdTVuZWl6bVlRV1NvNVhvMkJqOUNyb0NDZnN3Ylc3Qi8xQWU1cjd5blJQTWNkSDJpMy9uc3BOClYvWHFIVmU0ZlBnZ0YvWkx2MDIrR3VValY2Lzl4WVRzZFRpeW53Mzh3U3l1TzNLTi96TjllbCtuVHQ5eHUzemsKVkJKN05TODRHL2VnY0JEMFBUb1hLZEpSdmdhWFAwd2xXT25MK0FLWWc1dC9ScklIOTRseVRDTGlZZ1RsRU1vZApyeFY3bWdBMmRWQXpOcEVJU3VoMWlVZW16RkN3d0JUTjl2MGtkTHdWeEtmemdFcGZHRFJRTS9UTExkOFhaUCszCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWxNQ1AzT3NLcjROS2k4WERIaVcKeGlSRjMvaGVIK2NXWU5nV1Y5T3BxblV5Q3VCdTcvSnNMRWZQeFVEKy9oZGhYWUV3K2NLSVpxTmJOdlVUMGljUgp0WmY0NzRVKzRNdG1PWWp2Q25kNUlONERxb2hyZFVydHFSejdOWndkT0Ywd0hHOWh3bzFSdXVRUFlTYUh6bTV1CjhqVno0djRoU2VCOUFtaUlLYXk3QVZKbnd4NGJLYktrWHBmOG9BZTJodDlVZTBrR2V5d2RwL1lwaGxMY3lRNkgKQUNzZ3hyVWdSZGJUWk1hRXlDSTMrR1RLMVdtZ3JPaDhiWFFEeDdpRGoyN2MyMnZKMWpoVEIrZHgyVWsrL3huYQpLbGxrVElQQXViVytWeGNuempjUnh2cXJQclVkdVZ0TTB1UC8zL2grSGdlUnZuRkZMOGRsYVpqeVljS2w2NGVpCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcC8ydVY3SFMxd2R1dGVGbm9QZjAKTmk3QnBOU0hKcU1VYjZlQlZMWVovNmZwVHVXOE5DbnFFRk5PZ3FISWRNRFNhTkRQOHhrbEZQeFF1all2SUxIbwoxc3lvNFBnUUJDc1I4dDkybGpVcWRQV1ZGR0JPKzJUaXZmbUV2ZGNoUHFpYkkrKy9zVVhMTjY2aStqOXNkZk9KCnhVWmxDTHJiUU5YUTVMNmtpcFM4VjN0OThEVmlUbzlvVlNDbmZadHFMSGUwQUs5Ti9MWVRkVkQ1YVRaK0NaSzUKWjVBUGl0ZGgyTy9PbUFSZFlXV1ZFbGNPYUxvVkh6dVIzOEU2ajJPUHhERTB3cHJ6OXVBR2VqUzd0M0RnYTluagpMM1pwM2tlampLSmhNTGVVU1c1SFZhQy95U0htRHUxTjVHUUlldnFlZitOMFdqc3RDd08vbGhZT2VOcFVtckpsClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG5IUnpWNkt3UThodDQwNmgwcEIKeEp0cWMrRS8xUEo1bVp3VEJFQks2bmtsVlEra1ptTzZkdDZzSDNsU3QzTjQrclRRV2dDanFPL1d2WkJnV3VRRApkYm1wQ0Zxb0tPeTV6Nk1sZzJvUlE2bWxKZjg5NmdDakIrRmhFeHhLVFpWbUIzbDBkZEtnVnRGcGVEazdkOFdICkNyRW1pTkxiRHVrcEFTYS8zZFM5NDY1K2VSbDNVVmhyWXlYdFIwUjJzeUdHb1Y3NmxBamI2Y3ZXaW1Kd2dEYWMKdE9kNWd2dzhSQW1hVjBMaFE1TWcxV3VpUFdGT2kxaitMcStWdFJUVVkydWpIUzV0RkNzNFQxZmZQUVpNaTJmMQpNTkxjMkE4ZlFjTXRaeFhVRkc4MUtkYWFoUUo0dUFNT1F6RW9CVnJ2RnprWHhkNTg3d2FudndURjZvcFdxSlJxCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdURsQ1M0dXFvcUpmWWMreGNDUkYKbVRsV2ZGOUZDUzI2ZjBQYyt3WlB1YTdOZGlYYnNNOVJXTTh4OXZmdVAyMDB2V09VMlZzb1NTVlFXYUxDbEJPVQp1UDRFRlplTllxSXV2ZE1XZTJ2YzVwb0lHY21BTUVXWXJvenZ0WDdMWXVabEhEUnR5N0h0MzlVcmxDaUkvb2lECmJFdDN6RElYZDlwUGNVblpnV2taa20zYzA4am1LOGFvcmN3Y0pNVGQyY0hRMEJTUlZHY0hldHpkWlA3enRXUEsKQk5PY2dlcVNmb0pMbGZVMHlmbWMvdTZDcVZiWDRsUmlWY2E0ay9RV215bkJhOHZBeEdscSs4TEY3dSsrZTVONQpDUXcrWUpGQmJzbS9PR3RscGJZRzNvbkJXUnpKYUg1aFJnTnJWb3NGTzhDVUpUZWNLdEw1OXFDT3hTWGF1OGV4CkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGFwNTBVckI2Z0tjQVN4OUN4M1AKNFZYVmJMWlBLWXlyREMrdHEvekRaM3h6cHVJTk54dk0yODl6L0hXbEtvYnNBUUREQ1dWRzhDMmlzUVc0R0xsVApjTTNOU3cvalFhNXRWbWtTSUxXQUNPbXFQZDZFS2NmbFZRck5JYlF1UmN3OW9rSDI4L0IrOGJMd0NNaEYrLy9vCkxhdjJDQ2tDVkhyR08zenVFZC83eTlvOHpoK0xiNnR2bjlwRldobVp2eWJkN0IydEhnRVNJTW9IOXg2dGtpdHEKcmNvZzFiVHM4S2JzUFhZR2tmWE1GbjR3a0hHRkZhOWpxZ2Q0dkRiQU1mRW1MZUhUaDhYZG5wQ2E5VmJDQ2ZicwpteXlzMERha2R4Vlc1TzdTcXlDM2NMMDVvVDJ1Umg5U2lyVGxIdndvUWVab3BRWnBCWGZvazA1SGRvRXZxSnJDCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0I0ZVc2QzJXRW1WbTEyRmVIaGUKK2lWcnhSbEd2S3o3ZlZQcVVOYy9pYUdybk9tcTVRZkMzY2NIOGNWSjhwdFVLMW9jWi9Bb2FBY3N0Sm1Rczc3UgpSYW1JNEhmUi9URnd5MWJSUXg4UTVTVHpORzVFZyttendJRTVsMnpmR2FMWmszcU9wYjFRTEd0QXY3Ky9KQmp6ClN6clFXcFpiR0hrQXpESFNUYlJiRnlHR0NZdjg5emsyb0VISktVVmdRSUtrQXBnQVhlQld1VU1NV1VDWk9kUU0Kb3o3YlhLTmthRnM1bWFkTExuZlpQUUVFV2ZQVDdKbFBoQVhhWU5iYklXU3RRbGZMemN2L0NkNVRTbFRGREczQgpPMHpLbmV6TUZOaDVUanNGL0kxKzMrUnJQSk0vZXV0MG9ZZmRqQXRDUVBuR2k4bldmQUQ3ZXBBQTR3SVR0VW9MClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnNRcVlkcnFwZ29ES2ZOeTY3cGMKQy9TbDFLUFV4ZEloV0ZMSk5vcFdmTVM3T0hURlNSQTJma21tMEk2Sk5XUkNLTXRsNEpCNTJScm0wd0orL0VzSwpxZitOUzVtMm8xK2laMDd4cllLSUlFdVZEekJ1YUY5R2F2N3cwRCtpcjA1VFNrWUZXRkY3NmpjWW44VDV4NE1uCkR0Q1pzaWtWL0FpRnI1VUNCT2JpVStnWVdSc0c1eDhNTnRuRC80OXRGblNkUG9sMFpTblVDeG1MMkhkeUhxWU0KYmRWcHRLRlp3ZW14eDBjR3hFSmJrQTZZNmtrMktqNEV3eWFXcGtMdlB2K2hNczFYT21LSHljUHJwVVNqNjJJNApEWUk3V0p3UDhBLzdQVlNxbEtjRlJxMGozM3ZONXhvYjk0eHg3Qi9USHY0MU1VeHowRDZWSEh6bVRmbDJjemt6Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3JCbjgydXhrM2wrTjZTb0lUYmUKTnpVQTV3cERkWGRONGFSUDZ3SnpXWk1LaVBNYktQOFdKM3p2SEZkYXhSTTloYktVb2xtTXFhZGp0TU1ibEdYVQpVcVp1M1l0M2N4RmE4a0FsMktzM0NId1E0ZE9tdUtEQ1k5b3BTQTB1RzE2WDhjOUpIaGpHd2VtY2phVm1qR3hRCkxnVlJ4Y2pzU3U4QktUck5BNnBGY1NmU000NC9iRlJmTWZyaWxTaGtzRTNNYnYwUys4L0FpZTlZMWFPUTRyVlAKYmZLd2JYZXM1ZVcxMElBenNxMTE4aVRpZjhvczMwbUF4MCtWM09YMlY4Vy9QY1Qra0tBN2Q3MUlidi84SGJRVgpxNUZVVkh4WUlCMllyMi9QRTBGa2pWSzN3UDhxbytNeVFxbGNFbnBzc3BuY1gxQnRwSTVDbC9TMnR2WDY0MG5lCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnRUYkwxZ21WSE15aEhoTFVNZEkKQWlmdDJPMUFuY1JmMFprNFlwa3BMT0RJYk9kNXFMMnp5Q3M3MlhuQ0NTaXRLa1hwb2NaUWtRSkluR1BqNEsvUgpIYXY3cWlqOWxhUmxmWXlqa0R1VFBLMS9BdWc0U0VUTk9sWWFSTmVhTytjMk5UcW5UWTVwR0JsQlU3V3YvcFNjCm5OK1pOS09nMktXblRMTmNnVUdPRmEwVGxrZUJOM25vSXNrSUgyWFU5dzNFakZLbG5CTnFNYy8veW9QVkFtMDkKTng4WnQraTgrbkRsUTVoaWRrQWtJbXB6aG9XaVNLandaME1pMHRKQWVwY3BHdEZTdXkyMGdvM2dQalRBUk96OApMV2lTOVhOSUlmckpiMVhZTnhMQmRDMHBuZXFwZWp3SG44T0dkTisxTXducTQ0RTNSTXN6VnFKNlhPN2w1T2JBClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEk4NkxycEdOOHZsdzIzdG5LblYKNVVnT09qVTF4aUlTY1FaNm1pcC91QlREZGxPOTNqRHh5aGtmUUJWU0pvUUZIbzdkTW1QZ3ZSS0psRURMNTFHcQpYM2svejBvOEZtZFF5SUZ0a21EU0Q0MEFPRDd3ZnF6K1JwMHFuMW5uYWlObWY4cDdjK3Uwaktrbjc5emNlcFJ4CnFuakxSRHdIUmZ0KzkyQ0JwdnJmOFYvdCtpU01lbmVzYk10OWMxeGdnOGRiVGZacy9GTkNtdWF0eHdKeG9XMEgKRDgvbUVyUy9KZFJubE9EMFYvT2xmbk50NDdHUE1UNEZ6cVQ4N1NILy9qKzZtdWlHZkE4WjFHcVZ3Y3h5RUZVOAprbVREU2pmZlRibVNIWmVXTEJDb3pPMmJhNWlJYkd1MG9paHV5Q0VLLzg1aEp0ZmF5TzJmOWx6VThBMXlVNy9UCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemw4bHRZMzNqeDBkOTdqMmtldXAKMit6dXZ6anRSQjl2OFpwU1RtQU8razVjb3FCcGhGbWxxOFZ0djFITkZHTGNiSzVBUnpyazdkcGZsNUFmdElNKwpFbXMwdzdVNWhqbmxINGFOWWZnZVZRbWw3OG9VQ0lqVEp1bEVVWDhBaE1rNTZtU2ZDWkRXRW9YZlBlVXlYcEJjCm54bDJJNjk2RDVRNndKUWpZZUVWRjZsOENwUGNLRnVYRlBkeFNzQ0tUYTlYSXJGV1Vyc3JzSGkxQkt6V0xJU0UKeWdWQ3EzdGs5eXNUd25jSjFtZ1lYV0E3UWc0ZFg1c2NDYUkwZld2QWc5QTh1bHA4K091aGRxU0R0clcxYmNYZwppOWVWVTNsQWJwaTBPazVNcTRnNmYvYVdJOUpZUnlMSjFyblZCeFpyOUJvVEhMMVdIbHNYM3piWXgxME5XeTRJCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkp5UU9LZVZWV2R5Z3dieFJ4UWYKbllHbmdJenlBQUw2bTJaS2hPWDRkcVpYdnBKY0NJR2gvQStkbE9JT0hUNVNJZ3prWFluZnZobDRjTWE1S3dEeApxcHJ4MnMvUm8wTXYyNnhEa0czU3N2Wnl5aUxQejlkMkFRL3FKQ2dkVjd3RnAzL2hvazZJY0ZFT1NVNUpQTEMvCkhXUFRiUDc2TVhiSGRUUlRDTVowL3pRUCtIVGp3RjNEcFNlSUhCWGhBd2pQdGdJbzNKcmVuNnBMNzlvbCt3VnIKL1pQdXpRMElvQi9CbEdsMjZUZG0zM1MzYTVYMTlIYjZFNmQ2eGZWc0REZ0JYTjR4QzM5QzZjSEczLzlNaU4wZwpZUkV1NFByNUo1dGVJNjFIVHAzeTFTaEhpQmN5NjAzUFlieU10K1lUSURhVSt0ME5iekhnVTlYUU9hMzBvSkp6Ck1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzFGVExIVTNVNVNpSmZTNFJwUWEKcjFJV3hIQXozRHdaVHJSUlFkUGs0WGtwOEdmQmw4aGd5Q1dUMHJoS2Y5ak4vM3hGYjdoaStQV2FKNkRyWTcyLwpPWHFTVi8wdVR2NGNIOW04Q29rZWdLbm40bmhJSTdNd2VEYjltT0RtK0tQWEFDMmp0TVlRNmM2RFlXcWZDQzJPClBpdFN6TGh3dXIwemtqYzhvNEJobllXb3dPeW41dTlObWxReG54T0NPME03a3pBMmlGODJobk9SQ1hRak1JY0sKeVY3VnJYTmxwVjU5Q3NVcGY5RHVWaHJKc2t4RlNnQjErelNlVThGMXpCZHc1YjdRazMrV05QcmtCTEo2YXZXYwpZR2xlSzI5cW5oaGZueDNsV2FoOWE1UWtieDcwa2VMRlVRcGlybnhoSWpjSy9KbVhJK085MnNKVy9OU203anUzClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd01pYWZuYk1ITFhTN3NEZE9zME4KZHFkQkp4ZnF1Z0JIellmd3c1MFdTUEthLzhDak1hNjZHdlZKSWd2cFpzOEpXdjdEb01RMC9kTE4vOUZFTnpneQpwMm1IbVowTVpvUzdXQlZXV3o2WVJtZjlFTHhPMm1Hdm1KVUxEbU5lNVNKNVVwUjJ2eDNIeUU2TDJkYmg2b1pjCjk5bHFRN1FXdmtyTi8yVXNwOGNZM292b3Q2aW5pUWR2MHZzV0VERzFSVWpLdkUrdkZQNmx0MTZGOWxHVGNTNHcKUVJwYlB3NDJzZjVtbENWT2RkZEJzdkdHejlld2ZrdzhvSUVVRUdNTjdvLzZvdU1UUDZ2eERhbTAxKzk2eCtWdQpCNGFRMFRtT3pnRWZlNGxpZUJmRXFLckExeDhMMTNDU25TNGN5dzNPQlhGa2UyZHFCeXhzOXJTK3dvL0JQY2RZCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1M3MFhPWUdyc0o3d3gxVGdjRlYKMm1ZRlp4Zkp3V2YyUVVYcCt2aHdqaEdPaVB3M09YR29GYkZ6a3UwSTh3dTFkdG1COSs2RGROdXNHTUpqQVE1NQpRRWhIalpQVjhQUldVaGp3d2FPNVQ1eVBzZzEremRYb1Q3SUhKZ0VxSW1IeU92Rjc1RjhHR21tR1JoN3d3WEM0Ck5ZNCtjUDNZYlN5b3I0cTZFTW9xcTdwM0wwTWMwaEljV2NENm5keE9rZXBybTRjbS9oWG9JNEhTT21mS0pmNGYKSGMwbm1xWjRodkU1cHc1N1gyYlYrZUtwT0RudHZ3QXo1UzdjVXdvc25UczdmeTR5Y3hxamZSMXlrdlUvOWsrawpkNFJZRW9SbzJ3YjU0djZsRFBFVldqNURTbXhraXZFUDdtWDM0eFFIS05vTitxUUkyYU93SENEcmJWNWtuTkhNCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjJPVUIzQnBOY0ExclhyRElqRFcKaDN6MVplV2JTWUpsVFNuc3RXR01zSmhrUHZrdjZ0Y0NJa2ovaGgyeVE5cWZkcWZpc2ZUQmpSZE5tZTR3ZGZXbApNMHRTRU5vZlNFc2Yra1NtSjlDdDJYVWZ6a1ZneEdoOWtQR1Z2VmkzcnVCdEhJdGxIbzZTQWxsSjFXa3NPc045ClphWi9ZbEl2d055V3JUSWwxSzNmWHoxcnZRa3BYbWZ0U0w4NUhUcmJlb2EzTFVYTGd6VFhIODF6QTEvQ082K1QKclBYcmduZkgyaG1tOUZtS3N0TTljNzZrK1VWYzR4ZEpoN3pKNUU4aFRkY2J6dkZBVHZ3MS9tR3lpMFNVNkhzUApINkkrTno0MmE3a20rUlV2VkNlbUJ5UXRQV1czVjV1RXZsOGcvUlc4TFVsL2g2WWNTTU5CVnBWRklmSUR5TTM0CjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHV6YWhwTWtDcC9EalRvSXR2NlkKclROUkVQNWF5eGM4R3pTRUlOVUJwUmI5SjEwYWIrTXNRUkRSMVdMTXJaZWlGa3BMUGpPYURFckpoeTFHYm1yaQpjakZwQmJ4MFlVNndtcDJYVnE2bDlKMGgveXdIbTJ2QnJVWnpVdWNtVG0zRmlvRFZCejBDQ2tFV21JbGc1TWlsCi82MU15WDZyQlh2RTR0ZXlHaHFURHI1M0UvQS80TjZHanJHTzM5NzN0ZUZFM09lblpvR3kzbHQ2eVNWMGtZMjYKRmRNQndlZSt5d3pGU2VUYmRraHo5c2hESlBEMFdkeUNvOXVkcU81NjBYTk5DenRRMXUvSkdDNlhPL0doMWhJRgo0T1ZyRmZGMGZBTUxyczRzRkY4SjhJNC9pN0JlZEhhOUVWZy9BclQ2MU1WSXZDZkwyM01vMnRBa0JPNG92eFNFClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenVjMjBKRFB0OC9TMU0vZzFjMi8KbmU3SzFVM2N6eDlmWWJVbGlwNFF1aUVjRjBXaThRUGxnVmRMaXVITkIvYkNadjJvdW9FSC9Ybmg4cVRIaHQ0ZwpMN0FOVUJLajN3S2pOeTJwU3hMaTRJMVNPSmN3MWhLOVlQcnBjaTBTSWFoeEQzUG5VZm1HNDRhVXV5cXZNTms5CnpLM2ZvU2dkOUkzdWVoZndtem96emkxVnJ1STVPRjQvMWNPR3VoQllVakRCcTBsVDFWcjJrd0poUkY5L3R1Y3MKMFZhNmxNc2dabFAzaFJVTWtNQXlTK29QMCtKSzBqVXFrVVpnZFcyRTM3TUhzbzNCaENTY09Qc3pEaFJtQ1VhdQpZSmZCZDVFZnB5YmQ3dGNFMEsyMkRsY3BOQ1RtSXdlVEM0RXdkRUI0SVZuOHloYXlwTkhwQmJWaThiRTZ0aVBHCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0dXRWIwTXh4ZHBMNXZYTk5qQWIKVmJ5UVFPYitrK0RQL01PTFV3eUkzME1OeW1JUnlhZy9neUpTNEVXZWFsZG91QnFWeU5VcU9mMGszTGtYVmdYQQpFQ1FiOWlyenQ5Zmd0eGFubVFRWjdxVTlhd1VGZUlXSStwdmw4NTlXTXZjU1pBTkFLaWNvM1Nyc2RTSkI2OEI0Cm9vRGVJUzdIV0UwcDQ3aXE5SmZLZ2hnOEpEcVlJN2lEb2VPaWxYNVIvRGE4MktETGxrOWUyZ3ZYTnFSV2FVSlMKRjV1b3lmUENxei9pcFZIVm5Dei9xeFc2M3ZjRWhzRHpFRU5GalphZm44SVUvS0cvbWdKRS9WRU82YnlFMUIvZApJbVBDUittL0pkbmxRdTBORkRiMVZFbzJobTRPUVc3bkxhcjVndXp4ckMwTU1rMldHWWxaYzl0bktSdDZRcUQ5Ckp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0NlZHE5N0FqMVZUZ0dDMWRxTSsKcUgwTXJSTjJtcVRQVHZaRDVTWHBGNmc5OUZUK0orYktMdHNUQlkrVmZXM1ZQVFFnZE9jL29OeW1KS0Q0TmdvNQpmNXRIc1RWQ3lSRUdDdnNxZ0Z4dWJUanF0c3RBLzNuRk1rR3hxRGRLM1pOOGUwdWJJelJCVnF0eDJnZW9WWGJRCktVK1gzcmYwN21MQnp6THFkNnFSay9Bc0V6MGNQZWo1RjZ1eVpXVEwxZzdRNTQ2dWNTRHZTODVoTlcwOFBJQUoKekhhOWtmWVozR3dWcytXM2p6WmgwMEZWRFgwZWFCQU5US3NkUE9lQlpMbWEzNXJIK0FKR0VUVmp3dEh4TzNFVgpqWUZUSG8wUmxCQlRnTC9oREVWa3BlbmpmTkFwRUJGZ0h0SFNYcXVFNWNSdlZzQkVGd0ZnYU8wbFlWWFdkYWVlCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1g5RHl3Rjc5d1lIUUYreE96aVcKNkUzeUFaczh3M1pxTDhwK0h0QnlFOHZOUXEyZ2Y5a0VtZjh5YkdlKzM3NEN4QlJ0NEZkV05WQlF3Tk03SHMrbwp3VUxjNXJJRlBJOC9sdFRCU0dIL00wNXlBUG5sS1Iza0F4YWpPMUVzZTFXRlpVblU3YWxLdm5QcVQrMUNUTWdqCmR3T08xQ3ZXaXk4dE5DNjV5Y1N5MUpaUU82Y1lMMnRVZzhhQXAyM3ZOL3R3d2NIcmJ4K215cGpCN3JyV2R1VFcKTXo2TGZmRGZORWZMRERhL2dGbjdLTGdoRGNNcTZCV3c5bXpVdTEzSUlMaXpYbjc4WUU0WGR4MlltbkFsUGN1QQpRcjZoMFJ1UXgydTh5R0xpTlkrQ3BrRzZ2TnlGRFhzV2hVWlp4UVkySnl0QStkRDZXdmJKaUZNaWs1cDRGb1J0CjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkl1WVJvOGl1VG9GSERScy9CUUEKWUtMMkI4TzVTZkxtNGZEbi8rZFRjOG5sSFdtNnp2M3h0MGdQT3h5RWx0VXlXaHVMOHM4d0Mzd09yTXVhOGQ4dQpVS2pMamZNY0VVRWVtd1Y4T0ROaFZjQS9DS0JyUUNRSWFtTjMwWmlheUxZbUc0bVFTZUVmdUczeGpzcXd0MWxHCmxHMEY1NHdiMW9GaFdMTVdLNlhpV0FlV0E0YnpMMVRSSHBiMUozQlhIYjkwd29OcEkxZ0h0OHFXemQ4Z3p6eEwKVFc0QjM5c25sTmR3UFlBVDZLV1B4VVhuT0JQbkNBbUprTWxxZk81ZkpJVkJhVFVncEpCQVVzdmczTmg1SnJtMApTYXV5QmwrTllkeC9kc3lQcTJ6OTJHSWltRnM4S2JuYmFZUTFTSzBDU1d6dm90NFJ4OFVvOXRwbE1RY1J3MTAwClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG5qZnl2SzBCL0QxR3pGRUcxbmgKVDh4Q1dUUmhZaWxNQ0xXNzlTN2JaY0libENwSHFJd0hJa1AybVJvUitKMFJvWkdVZndLQldmWTdoNW5MVUt2NwpyME1zNjdsZFcyU0VmL0NBY1dvOGFBZXQ5ZTFCb2I5ZUNGYjhtcm91SmJHc1Z5RkE0YS9EMVJ5WUFXVXl3OFlDCnhYUXVJMkdTdlovN0RKOGNIbWtNVFBVN1BYWkFmMTF0QjdvQTRCbTQrRmFza2ZmWVluT3pOT0Q5RjREdjlsNWwKcHBGWXEvRUhaUVlBQVRrdTJ0VUVZcTI1dTE0VnRpd1owU1kzcFpyQ1hzQW80UVlYMlVSSnhGOWRBUWN0SGJjUwovNHp5R25hYjRHVmx6R2FnckY1ckh3SVZsYk05RDlyR3BrSVRKdkhkRHZZbjA0TzV0QWdQUzFxM254emZXZXhjCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWVQY25HNm0xMlN0aTQ4NGpYNXQKSW5RN3JqZkFIWnhQQ3lSZGptRVNKWE9SckhqeTAwWjdSOE5vZjJSMUNiYjFLZGlJTmZyZzI5c0kxU0p0Qml0VAoyREQydXlra3FuY0RFd21BVTk3VDhsb3hobmxOcXVldEtrZ1RHbkdIeDgxc0F6akdYSGptQzJ2diswSFN5VVlNCjJ0eGVwVmhPeVJaWmdsQW9hQmRpWTg5VzZFVlhMdGtiZktscTdSekJIYlNjRU0xbUNscXFUTEFld3d2Q3M5UHgKUXFxbWUwU2xELzU2US8zeWs4QXh5QVkwUEpNcjRsUWNMMXlZeXVLelRmci9rb1plQy94RzZ2NGNkRW82aFRuYwp1TDB2YjNNQStBQmhsbFZER3NVUlJEZnpBNnVaSW1abTV5T0p6VFBpUzJpSThtS1Fid2pHampyK2hHVkRyS3VHClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenYvUXlpOXJTRVZ4NEsvNElZalkKKytlWmxUTnBjL3l2Ly9KbWI3MzlNUTNibEg4ZkpPRmxONnBIT0d1OWxOM0NGaHV6a2lVa2R3YVBHWHFqSlkxMQpnN1VpTmV1WHlUVTFtTXNhOHo3K3ZlUzIrL0pTMjFVNFcvQVF2UlFWUjZqWVIwVmtYaGpPeEZYS1k2cGIvYU9EClR0WkNncEtvQXJhQ0d1UkpuNlRCanJsZGNacy92L2xnc0dtU1c2Z1Y5Um00Q0IwTlZ2WmNKNjZ4eXZvd1VLR0MKSXY3aFFPRlBqMW4wUk5iRlJpMUpqTy82V3FuYVZEV2p0VUdTOGt0cjhrQXlZYkwyS0I2L083bnltOUVUTnZhegpMWk9qT1Z3dm9uOGtwRjdBbUhqYU1XNGtubFZlZ1ZyMUViQ3pxQkgzMU1qeHVmWHdPeU83b2RJMDZkRjJQbUowCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWVIR3c1SUxEY0NkUW8yWHdmbGEKSHVWeDBxOExQUDB3cWgvenAxTk8rdEJBT0JVSGdTZC9jeTR6N0ExOVR6RjRrMHErVHYxSkNHdVBFdjJ1VU85cAovUld3RjBBajVEd0hvc3JvRW9rTk1md2pyK0c0RlNSQzRibzNEVmxTZWg3ckdsbzV2WFd4dldKR1VDbEhXNkhXCklDVXN6MlVmSmxCOEtQekpKaG5qUFROODlKK3VGYXIxSzN3aVp5RWtSUjFFM2Nqb0ExY296aDQ3MEFrQmxvbzMKcDhHaWFjQm8yOXpPeWM4VlB6bDg3dkVzb3FscVVsU2FmSUx5YjE4b0tPU0hDVXZFclhsTnBjaFJNcHVRcEFaegpncHFTVm03OEF6eGdiSGhoS0hxcjBJTVhPcllFbndyTy9rTW5JVmtLS0hHQWxiMUp3M2tlUkZVTDRlRHRrQ1paCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdldSck1QdDFYV1FlSzVUMHN6bmMKL3JESVhzOHBucFhSYlBSV3RjbW9GMzJNc0tWbUovbkVKZVdkbmVjZXc2aElDRERsMVlvZjJxL3JycG9hdXlZego3NDFWemZ4bXRGQ1hwZWlrd3U5Wi9JWXR5cnQ2OHlFeFFSUkVRL1UvbUhoQ3VnQy9DRHlLUDdDUkJ5QXhmSmVPCmUzZE16MEUwelRlVEtidkVLOEw5cnZNQjlYVDdZN0c4Zy9iMG9ubE1pVENUUVExS29IektwL0QzVjZvMll1UUYKY1cxWkVzaWRlU1M5OTBxQTJFK1lsbzFRVGErdld2bjJIYU9uS0srRUhTRENJS2NBQjU0aXVNRW5xcmtyaXJZLwp3SkVneGhqZ09HSnZWQnNHVGNhbVJkc1FidDkzOXRiSVFVTTdMdVhQcXNHRTdCRXJKNkZQRmhXUlNtaitRL01pCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVc0a25WQXRFNmFuazlkSjcwYnIKMCs5QkpYS3F1bE9EOEVXMTRlekw4ZHl3Zk5raUY5MURtUlVlcnpUdGQ4NnprM0dRd3ZFVE5NOUpZbTFVS3ZhNQoySkNTeHJuaHh0MWhKdU5qSkFzVWZTYXpTNFhhRVN0ekExOCtjQURZZDI1bU1OQVQrR3A3Vit0ZkdxVmpiOU5BCjhYdzg4SFNuZWppMjlsZGdoOVlCa1RXKzZIMGFSeFhPdGR0Y1pwbUZ1N2NKZFUySEw0YWRlRUg1dTRGbCtiWm4KcDk5Q3oxMEZoN0s1clM3ZmIvVFJId1JwV2E2UklWS0NnVmNlb2tiNFNnSHBkWVR6N1V6WUxva1JkTkE3LzZjZApPbllHUVlpbWhvWkpBTEZkVkpqekxWUFZyYW5NY2UrS3lVblhLUDlvL2xFOWZ6UW5YNkc0V2dRaU5DQWlKQXpWCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3Btd2RNRTF1bG5MZDlNVDk0OWwKd1VLYkp0TnR2TG40UmRrbGhpQkNFLzU3L0YyYlRsSEhYUzlJNWtBYkFWZmhPRGlqZit4RjhPMlFFTWt2RU1LeQpkNDR2MjFpQ3RtU05SRU5lalM4MEtweTQrTklGSTEyemUxbU1tVis2bm9NdENQbG5jbnNpV2o3U1JyN0VOWWRkCjFXSHNqWThPSWE0TUdIakdCVjJDaS9La2J4ZmRtVjFEaTdNWGZHTFVvUjdPNjFtdkl6WWhFRFlkTHlPcnR2cWwKckU3MENSclhyTlA0YVhKbkRNd0huMXhjc1JOWHpGUWllN1hzaHhIQTQwMk1INjd2c1N6NmxXOFl0YUVGY29BLwpsYzE3M0pCTzNxRU5tN0F5WmtxYUdJN2dZeWludWVMbnQxQ2Z1QjVwYkZ6aVdwZ2t5aWVIdU53d0tPeklrekU1CmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmcxRUh5YkVJYmVJOUNTZVB2L1gKR2tyQ29NWVp1UDU1b2ErdTZJb2xmRDJOby9kOGl0UFZqNmNlbVdSWm1xRGJtdUZQOHhwR3NmYjJzdXlGWEwyQgp2b3hnNy93ODZ2R0hhNEM5UEwzajB5NVJJVW9QUHdZSlYvWi8xZzlnb2txZ29UOUZCTW56M09IZWx0SHdQc1hmCnd2aVZzU20wTGhvYXlYckpQLzBaU0lQVzUrZFBPMzJXcm5SeHI3MVgrUE9xSDdyMUFoRjVZVDBkWkcwLzNiSDcKdjlpVkl5ek9odjBFNkpWd0NuQUJqUGdiVll3OUlQdFY1bE1QOFJPSlhDeXdhU3pRVTN3NjdzViszQjdsTFk4YgpkekRFLzJZbWNVUVFsTW0vREZ3REhWU1pmM0RMZlhWS1gyR3hVUGtiSU9LQU12RHhpOGt0dy9Na1NHZ0N4RDBZCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTZGRGx3bzIveWtqbFliUVlranYKTU1sUjlVK1VGdysvQVlZNHBTZWUrek9FdHRsdjJ0Mm1TTlFFK1RUODV0UGUxSXp4dGNMVy9RZ1ZaNEpHNjRmRgp0VXNob2FwNjZkakNHbUNabms0S3ppaUw2TUdWR1BkV2syY2FSTi81bkwrcUN3T2NCK3ROb2trRUVIYzF1OVQrCkU2NFhLOWd3cFFvZEtEWjFuaURlMFUwZ2o2dG9TeUFEUmkwQUZ6TU1kVWtML0cxSDJPMUM4Qis0VUJUSWtIbTEKdWZhVFNZQ2U0VG1mMWRpc3lhdUhJSEdmTXNvdGRPTnJRczZ3TWpMQVlMSk02VXRWZEpZUFI4ejRlRno1YnI5MwpEUTVSS0c5QVNZeXlrSVVjUS9YSkFnbDBTeE12azB0dWlWcWEyV2wyU2UveWpEeEtYeUU1UnQvNmxaS3padEJFCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDVNMFNhWGhPY0czeW5MK3FMNXAKRkN6NXVXODBoNlFqMDJpRllFcGtsdklCeVlTc2x3ZlRyM1VINmxrclQ3T2MrckFBZGl5VGZOT2NzYjhGYW5qawpoTDR5UUkwVUtlT2JIb2ZndHNDVEZFTm9hNy9FWWJWS1B6SDVoWjRubDFqbDR3SFkvUzJENEZ5UkdtQ2lzK1h5ClRtaTZzK29zRE9Wb3BDRk1kZFdFZmRabjNXTEhOTEFFTG9yZS9nZWhLMVY2ckRuNlNVWnZydEcvZDB2dnpUSSsKS1l4WW1SdlplcjdLOUVGYnpnMUZuZ3VDdURUTmdLRjZyaWNiZEFtZE9uWnFKcllad05GTmN0ejRSNEhHVDd4WQpHMThRZ0NqRjIySklMYk5mNlhLUk9mdS9mVXZybTd2blM5amhxSVh3alB1TTMyRmJkNXRvSEc1SnBFU3BjNSs5Clh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0ZHMVdpVlFzSm9XT0tFQXBUR0MKdTBFTkJvaGpXd29Fa00yajVVaGFYdG5taVQwaHpVb0hxTFQvWm9rU0ZhVE1NNCtpc0t3Njk0K2Nac0RKVU5sdQpmRlJyaVFQMnVFNlhyRld4UXFhakVxSEVWTzVjQmlOVmo5SWViVDVndUxiK083QWE2Sk5zTmExVHUxMDJsTnhSCjR6K05tcWdxUExXQllnaU4ySVNZaVBaOGxCOUVrRHFweTJsTktsUXY3Z3I5OFkxZ0tsamhpeERpQ1YwdUc5VUEKUy9FNE5SazVZN0xmZkpaMzYxT0czMDJvYTFkQmw4M0U5T0RZRVZDeWk2RFVNS2xIR2sweXVBMHFKekxNNFluLwpabm0wclp3eWVsM2phdktxSjVQRS9JMnd4Q1JaQlRzODJlQ2gvNlpmZStBMmdPYnpZdktoNGVxQWkzcW4wemJMClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbitTQkEvWjlaang0VC9QRzJRdm0KcWxhK1ZPb0ZySUdVcExJaG1DankweEVrRFQrakNLTnpycEtLMVBrUm5sT2RKa01HMllUM1dzMU4vVDRZU0Z3TgpRMVZpMEszZVpOazYvUC9BS2IzUUg2OVlkallPWnF2VVFEVG1uUmtsU3VaZk8yU0Q3alUzK21Sdk1JZ2Z3Z002Cm1NRHRJaExkcGpTelRvcmZ3N2FuL1dVb0p1Q0szZ1NIdHpLOEoveUt4RmUxL2hjVS9PbEQrZXBmS0U1WmZuMXYKRGdFK1ZMS0JYcGU0OUlPbTZ2anRSUFhSK0JIQmxtUDI3SmN5ODlJamt3cDBRN01pTFRJSEJXZlNvdkJWMmJUWgo2a3BIRVp3WnAyc01oTFBjMTFDVzJudkkzM3IraTlaY0lxdHoxVFZ5OFhNTXpRQXdaRFNlSWxLYmxFanc0MnFLCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMENlYjR5MzhNanBmNHMvL01jUVEKVWw4RmpoVVd1aTVZa2JnQm4vZUNLT2hZV2oydmFuRkF0Z2xDYkxId1hzSWM3TWFRenFNL3lzbVZZODE1RnVxOQpJbXEvWXkya2hMbFg5KzYvSTNKbnJyejVHMzhZbWRsbjJlZ0hhcVB0SUh6Qlc3VTgvQi8vK05CdWQrRmNLM0RICkxjMHhodHdPdTBCcUo5a2xuZURPZlRSTjJqMDYxNTBSb0ZiR3RqOUxqbTlHWEFpYzhDWEpQOGtIbE5RWUlZTmoKZ0tneVM0VnZoSnRkQ2FVVGFDTCtZRW0xVmY0aUhGS3pka2NYMVlSbmtWLzBWQWpFUzFlbzN5LzVOOGNsVEsycQpuVXgzVW1SWnhVTHVhTkhSSk9vcUp1WFNVSnlLRWNNVE1yS0xoZ0hnZ3hPcnBOVDFiY1diWkQ0VFM0OFl2WFlNCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE9PMVhiTlNtdjgwaFRXVHlYdzUKTHpLcjVlRlpMRHoxMUVlRDk4WTY0cDRqeDVRVmlaSDdBVG1lN1ZoRzZ2TXI0MVVVQlpKQmI1enc4OWQyVlhVNQorcm14Rkd4enIreXd3RThGUWNBLys2dWEwS2FNKzJRWjB0bzRXRHN4QXpuSjYyZ3Z2TlFIa1RwRmVGc2xSRkRoCitmT0tQeVF4R3RzRHdObjMzWWhJOGVRM2gyOXJYQWhRR2JhUDFIWWNOMFpBcnNha1JzNGJPajZJSUx2a3gyWW4KZGd1VG1aTWVjaXhKT0VObHpwa1lhWlB1UVkvVG10Zmg5a3k5dnUweDU3OERtYTI0ZzFWVDU5YTlhaFhGYkQ1MgpodlBnekdjQ1loRmNYSmlYcnZnejVNMXVlQ2FaZTVhdjE4T2RWL0l3eFh1NTB3R05MbUV2UEU0eWRHeEQyTnluCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHJUWmc3U29ieGNianI1V1hjVHoKbUJzaW1pWFY2Tlh5eHNRek9YeEg1d2RkUEJyQSt5cGNXZVRSVVAvK3E2Q3lpd05VSm9oTjAzOG5tREQyZ3dYMwo5VmxOQktJbysxcjVvc0tqN2ZiZXhyOTUyOE1OOEVjYjQxRGdndy9Id3c5Rys2SEhqdnZLQit2dmk2R3IrODZMCkdYZXBwOFkvMnM4ZXQ4cDRiTTBQSGpsR1VOMzVWaGQvUXk3QVVXVVlramE3Q3lkQnpDWXIwN1ZMK3NuNjh2UVkKd0lOVW9GVFZ5cEQ1aEVialcwajkxYklxaGVkOWlTbXFvK1ZFVVZtdUpyeVVFRVhiTTIreHFQRVJEZmdJeXZ1VAo5YlZCUzJVR2tMNEwrb1ZCUXdoRFVMejdQK3czZUt2YThSS01HR0N5eE4waWQ4VmxlV2lJakpHakxGM3pxZldLCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3lFMVlEbTRRMWZ6eHFFZ29CZ3EKMnpveWZUWHlBd1hTRThCYmJ2eVpRVkFZL2ZzMG1vZ0NFbmJNVzFwbkFrZklVaHZuNGJkZ2pEUjZxV0doUUlXWAplVWhtQTJaOVdCVEtWTkNSTG95VlR1OWhHQURGWGNxZVA5a3VraUgwZ29vaGhWWWROY1dIaDNNcktYdDNheHFZCjFEZHZ0bFJJQVRkQ0l2UGM5VHdwVHhzbnNTNWpRbHdhWlBnV0JsR0Z0cE5YbitTVHNLZzJPZUtTQi9NRzZSaFAKUGFjOXRuWnZ6VGc0dHNubUk4Q3NCdWxNV2FOUVNJV2ZLSWxLZGpsNk9mZUhPOHVoQlAvajFack9neEhJdkJYeAoybHBCUndpNWtuNWtPeFNKT3p3QytDUEM1N2x6UXdnSkdPZ3JkL25tS29sY1dBa05BWEwxNmdYaGdNNkdkelRFCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1pVRlF3cFZhVWowQzFUSjl0ZU8KZ2puTHZPdDFsSmZrVUpBUGRJSkpkQ3d3WCtIeDM3aDNrTThhM3Z5bVB5cWJCU2hNcC9lK05UTWVhUzBHWkh4RgpZVUl4Tk5hMkpiWk9LMHBGU2xLdnBya3BuMnY1OWd1TmdxRG9UczF0a2JJcGI1OXF3RGY0QzB1MnVVWG9yeGJjCklDQTU2WnYxWkc5Z1l2MzQvdERvRnNTd1ZuZ0RGMWYzVUdJUTBjRjZ1VDFsc2JPWk9HR1FCbXNKeHZtbDAyeDAKRkZNQWxKdDUxQ3lpR0oyOXNRUXBvNFBUWXdIQU0vMnR1UVp1WXFmeEw5WmdicVc2cENZcldTOU15YmxLcTR3SAphZ1hPaERMaUF4MllYMGxiaFF6WFlvNVZHWks3bjhCZGNNckEraGtQV0p0UytnMmhHVHdRQ0c1ZGFPaFhLbW45CkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb084WEFFUThRUVhwQmFQS1JaT2gKNmRmZnJDUEZUWWJKcTZCV0FhWE1xbWJyaCtLK0E5WGdYR2xQUDh3RTlYRTYwUzZ2VU5LZVpqbE5EM09UVjlHUApZRkR0TzdYS3VrVWg0UXFKaEhmcFlHbkk1YVl5YkpWQjNSNmJMbHl5TjdZUkppc2VabS9UbGxiRnB5WmkwU3VJCktLWnE0SWl6YnE4SzZNYmhnMis0SEQvWXRKeCtVTzB5VXZtMUhVSTFrUnc2LzJNbXhOdG5RcG1kTE0ra2YrUWMKVUczWnd3cUhOWnVqN2ZVQVBQMGozYXRnc3V3NW0zYVhpY01LSFBCNXlJamlKQTd3SExyTnFLNU82b3Y1ZXV3cwowOFhtUzlmdG9WN0E0aTNXdVh4Qk9wd2dBRnZWNTJwdlpHY0tpNW5hVVpaRW1uVkVDejVIVXZHUzJPUGt2a0RsCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNW5FNXFEVXZ6dTZYcTRtMEN5enYKYWFqVFRseVJiL2dNYUVDVkNIWXRzS0hJeTNrcEluOFQyOXEraFZrSy9kRmp0OVovV0NmYlc2TWd4WGZ0ZW9uLwo5Ui9QemFmZko2dnpjZkRsRkh2RVdZS05qdC8rK0RpRXZESTJnZCtsRkZoemxGTzdyODgrWHdaSnpOQTh0MkdoCnFsZlZIWEdGd3JQQlN6VzZuUno4UG1YelY0YUVUZkdjQTB6VUJiL3JwKzIrQkhmYzI3Y2RuQU9ZeldzSE8vVVIKRnUyY2FWODdkd3prNTcyMWNMaGpCdURId1BPMU5HaUZ6aDhPMzRucjRrOTVXK1IreFlTQW93UjJBWjYrMFJ3Sgp2Qnp1Y0V3S09kRjFyajlaNnBFUUp2eUtwaTl0K3U1WHZWVXk5V0lwcVBWcVdmbUY1RUszdjhTNnpvdmd6S1RiClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3BuWHJVRW83NWNraE1qbVo4dVAKQm1DUGcwN1pWTzFTNTF3djA4MVREQWo1UXcvU1FxT3FXc3hxdi8wTmRYaG1qZlFLenRGbytIZU1PVFVpL3pzUApHa0RGQ054VjFvUlR2NFQrSDhPZzN5TThGTFcvSjNuOVNpdW1rQUp3WjBVNHZUQUxWQWRwcmVscWFlOUNiS1owCkxwTzgxY1cxTFE5K2NHZGl4S2VOK0puclAxVzRYRktnR0sxUkxUaXQraUVJUmZYWGFzakVXQ21UQk83UHVvcHEKejdhOWwzdTh5YmNTVk1aRUhqOU5GTzRnR1BLNElGOW9oMG55eE1CWG9EOE9MbXpCL295eGhwdXRvaGc1K240NApxeFNOMGRuN0c1YTBraEdXclZVTW92cmhaRlNOQ2NpTjJ3OGlLV2Y5MnF4WjBjbmozOTRaU3g1STYrQ2I1UmxTCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDQyK0F3bkh6THl4RTByaFVJRlIKWHRNUVkveFBrTnVFQm4wOUdjTXRiQTF0WHlZMHZnRStJbHFWNFNBTmhyeGNNT1AwSU5FMDZHTElyUVhOeHpHSgp1RkpvbGJWaWNZWGJ3cW5maWE1eGNZemg2alRxWnY2bzVkODg4am9GaWlpWXFpbjRSbVJhelJSOVo5eHlFbXMzCnI4VTEzQXVTM0VNRnZJdlJsTW53QXNRaVNZUzY3MmRtV3crMmJaL2d3UHdsc1FjdUFmOG9kUlA0clM5emJWb1YKY25Yb2Jsa2hBU05jbERzLzR0aitPVmEyMVFPYU04TVhaSG9YWjE4MmlKVFlNN0JEOEV0UGgrdmRuaTUxcnJ4bAp5UFg2NkNyOG9WMGhnTk8zdmh4Zm4xYTQ4Sk51TytRUkwveFVzVzlNejE2NTVmb0E0TzVUUXpwVDhaMFcxcUEyCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3h4eXJGZ0NEQ0VtRTNnNTZyc3MKNm94RHUxUkd5bDdvellMM3ZYcjEwaTExWEpnV05pdkJ3bXVxYnJFcEZrbzV4U3B6NThnS3lPZ0JTbzVSQlo3bgpSY28wUVAyd01iRURHQ2lvMVZZb2oyd3JveTFEMkJGaW45eCtQTytPSTBmWmhSMWJzeXo3S05kTUFEd1lyd0VoCkpJUkZaSTgvS3RmZFZ3dkxrVHdYSFVyR1B1L0dyUDBPaWVIM0o2VWNHR3VxMWV1eWhSL2thT2hTZUdUdjZQQVgKUUg2a1JQYkxXR0ZZbVYzNld1NkIxMCt6WjdyVE9Pbm54OVlVOFBCUkREWTRKbVhZbWVibjU5Z0gxdHdmWnFPdwp2QjdGVU5MU2lEREhMUVFjVWgxVjIrOUdBY1pmZ3ZJeDArOVFtSlZZOUZQZW55YmcyMXlMMWZiWlltWk4wR2JRCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemNqNDExSmdqbHlVVVJpOGlPb0QKM0szb0lJZlFyNHVtNUFTOGJqZGR0ZFo2U0NSRVNjczh1ZlpHQndhT0g1WEh0UWgyYVFQUzIwL2d1VExMN0JzbQpucGlpdkpKMnQ5aVZTeGh5Nko2aWZIOXBYdEU5ZVJScEM5Tlp0TU9TNStKaEVsbXlKcjVZaXlkbnQzQnl3czJQCnRvU0lqODJBRmFQdmpCTlFkaFF1VXRBNHk2cGhVR2pyYzVwTlZ6cXJnb3VnejVpN0hLQjlLWDhXcFYxTm9WcFkKc3dYS0ZsTkpyelBxZFBUOUV1QzV6L1lvY0NFaGZxY3ZKc3hwYytmcm5XUm1wRkE4WFJYLy8waStHNDlRUHdadAovZkJkWTVkZWpTZ2U3RSthdlV3aytQNXl6STZZejFPMFQwcWRKMnNUK2NIRGRiQ1NzaklYUHdQK20xSWRRTXczCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHF4QnNTV2g2aldzU1MxVFJUWDEKV0hlT3UwZEhhMU1SOG9CN3FELzdTY0UxS3R3cmxFTWdvZ1dFOVVTVjlyOEhYc09yZU5kZHY4bG9SYWhHcm5nKwpnajg1OWxBTTBpajVwZmt1cjJJdkpkOTlQbG4vWTJGaUtDTHhKNDEvb1ZqODFpNG1RVFQ4dFRkcTQ1MGNaOHVwCnA5L05LblU0Q3ordHBsajdFQ2lWNUN6N3RIempsbFBpT0dnN0pJZlRPMjBqREVwdzBSc1Zuek1sbUIrcGdhQ0UKakJGTm5DTkxGNUdLV2ZoandpMjNLTVhxUG9JUjlYVDYyOTlrcjdjYXE0THFaczF3VjJ5WWJheFQ2VDBGUllkYQp0NVhPNDFxemNpcW1zeVprNUl5ZGRQVENSWTkvRjBwSUp1M3M2SitLb0Z5SFZVbmJFQWpjaHg5YlEyM25hRDczCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNE1MV1hMVjVMOHRXaDdhekYzSVMKWFNXbnQ2by84UnhCOWRNLzVEMTBJVXZKUC9KZ21qeWhpZXAveHFMeVpYdFpPZFB6dVI3VGw5cVhyQ1ZSalJjYwpiV2tQK09ZNXBTZkcvMUdPalpsTys1emg1NzRVZFJOdEFsNVFMSHlRN2VNMTVPZXI2UE1tTFRLc1hqTkVTTWFKCndRUWVFQWlnSkNrSVVNM3Erbk1qeVhYelhNdlgweHlyTXg0bzNtNCtkQmhDbWlPZkcxSC9OakxrTEI0RFFGV0YKNDg0WGI3UXB5RnZqclJET2QxTHEvS01MdjVpL0poTk1sWTFGUzZZTWJnWmQ5SnpwelJxUjBVRnlPNEtnQTNveAphaDhuZDAvQVBHemNiVU4vM2x6WHVwYkIzd0VBd0hGNldpMDBrYkpVbG50aFo2d1VINzljNnoyM0tHTWR3M2pJCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFoyMzdGTXRJV1k2ek85SzZXSm8KeVoyQUFWYVRjbDg1TXlJVWRYWGRVWTVTS0JtaXAyQUFCQzRGYzBMbDJ4UDg4OEhwZ291UjdPQUl5UmNqdGVnTApKMFQwa09yY08vNWJiZTg1UHptdEh5Vy9leGNSVDNRUDRGOVlhUjlrOVoyTmkwR2daWHo4bzhWS2MwNTQ2dll6CkNOZUcrQnkraE02bitjRWlPVzJNcFV3S0JEMkhBVmpGRmZ0eG9HdU92REw3a2lkTVpreEZQNlo3Z3ZpbzlHQTkKMFYyY09GV0dHeUhYSzlzeER0di9TMGg2TExvbVl5Zm4xV2M5czJxZEdvdjczcVF1Q0dwOWNBdjhGWGUzZkkrWgpWWjhvbEIvY21lKzZyTlg1TFA0Vld5L0FMMGU0Q3g2OFl6bmhZUkFmV2NPY2lHcGl2a0hjbXorQzQybmFZMFl1CnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbGtnVWtHM3BDZk5iQmwzNGd0RnUKN2p1dm9EbEoyd1NMZnJ6eE96bXgzckJ5cm9EYnZqdkJCaW1vTzUxWXlaTEtqVUNxYmNjSkNiQkZSdGFmYUNWawpMWVRIMUVqUGFSSyt5dnVQY1FURVZKL3EyRWNBVDY0WjQ3SHF1VjJhVWJGc2ZPamhsSGRpVVpyL1Z6OVdZVjBGCnUwaitOcmgwV2ZnWHJIdndGKzhFaFYvTVQwM29QT2ErbnNnTWRZYzFZVWlISVdTRDRmYkJOVmRVSHEwTVJBNTgKYTdnYmt2OHdJbUlXQ2ZLbEkwOWF2TCtBTExRTHptQ051TkwxUmp0TTJ2TzNxWmM2N3h0RjRWWHZMV0lVQUtHSwpSVXZPSWdkOWI4ekJtZVJ5alZHRVc3TE5RdHVYMzh1MVRXcWswUlpUbUdhVFFHR0ZyMWVTQXdPckhCb0thVEdUCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVZKU25lRGUwK08vMk5wUm11UTUKWWxQeTJvU2IxdWl4RmZ6UVJUbzRjRk5BRkZXbDNUYVpYeEtZYllRdDFQcTAzOFFSUWNtak1WeDR1R1VlUmR3cgpYcnFWSG9uZDM2cVVDZFc0Q0wvU2dpczdvQWdoVmJrSDY3ZGUvdDhuOFJ5b2ptUDUwYkd4bFdjMUthTG4xU2tnCjA3RVM2eWRLWEJFaWRaeEtaY0ZVajlHMU02SGhZbUh4UG4wZjdIcHJ5UmlTeEwyTUc0WUVScW50ZnNYV2JOMk4KTjdEV0ZjdzdJdmtYMGxaQkovQlNsTU1NTi9VeEU2Mk0vck95MG11ZHlZbmQwUHRpWHd6MXgxWUt6VkdHTEFnRQptbXlVTFhHWHMxWmVrSkIvSWIvQ3d3UHg2WFhNOS80NnhkWDZTdi9BTFBCYWt0V1ZQM2F0SGpxVGpOTlFPa3RtCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeURlNDNQdG04TnNmVy9UeVpIOFUKWURhYm94NThEWTRwWG9uQXJNMmdVWjBBVHMvMXd1M29qU3FTMkNUeXlJNHN2NTZtbVgyamJUS3c1cVk0Qmo2dQpVaXVZQkMrMFhzdHU2SjF6aUJYQ3NOTGhMdkVMM0k3QVE0MUNDdCs5a2xVdDludktCblRHdWh2UG54OURHSFJrCmpuays0WnpwemIwREdqWVlPL1FLSi9idGtGSnczaE5qVkllckdJWmhHZFNWdDRBSFZhRTQ1WUNMNW43QituSkwKbzVReFEyL3VGb2cxVEpwN1pTSDN4V2Z4Y0JiM3BzY2xWME5TVnh5cDVic0M5NEphQmR6UlVnOFlhSXlEMzdoawovaXNLVURISGZsOEw0WjZ2YlJqMUZTSkwwNU1GTDFCNUNjU3pSTXR1aS8zMW9FV0dhc1VvVDVXbTBoQ3Z5dUJGCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1k2MTVVUmorRmtMUWViaXJOREwKcXFuZGF2SWo0RVBEdVJZNkNlb1hyN0dWZFdNdmUrZUx5dHBjNS9uY2ZGYTVVTGVZUG5nNkhZRk5FZExZeGkrQwpyM2I2bUxPajMxZGpXUG5RcmNZT1ltUWNjNHpLakN2dXZQbzQ1RXdyUWs5Lzd0SllsNFM2VkEwaWtyVmRmOFRsClFyNGo2TUpLOWw2dmMxM096RDdDNHloN3o1bWFWWWZ6M1NlY0ZJc0FET3cyV0hOOGlVbmNSdVhTNUo5RWR5cCsKQTlKcHZTOUx4N3RkWmQ5clUvSldRM1dJU3B0elM3NlpIQis2TWE4RmluL0U4Z0pid2I0RFR5dGFIY3ViR2VjLwpDQk9RYXZRTTVWRG53d1FiOUdvcHQwYWZ4Ni8wZFdqTWVnSC9Ba3JrUm9XNnovWGF1QjR6Qm0wY2VybXltUk5SCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEZWcG5KSDVmNENOMXpzTHVDalcKY1pYWW1iQ2NvMU5VMG5NUzZhMW56R2NNbmgwNzlBWHlYQUc1VzByV3gvdkNwbE1hQzFOR20zaWwxSHJkWXVSeApNd0NKQm4xWVVNZmRWbTVjYlNQQmUxZm5wVktXbURIRnovY3l1dGVoNWhOcDZTT3JoTzRYNjVJVkplTmExTWw5CkJScG83OVAwMXB0ajBXVmpFbjZYeUpreXVXcHdML2UvRXBUZTB2aVpYaUFtcWNLSFV0UDBvTk9YOXJwMkRxdnQKYVFyMlJFVU90NTJralJLZTArYmlTaG1VTEpvUkFUSFJ2cWhRb2U5dkJkMkkwbzRkbjFmUkxCZ3JxZHFCQnVSMgo1WjRHRS9tU0FmYVUzQ0lDcml0cXFELzB2QTVtVHpuZDc1ajZMNXV2K1lrZm1NYTMrdHQzbGIxeTFtc1JydkdjCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnhqUGxFQ0VtME1YVnN6K0h3SUYKY2twc2lRUXNyQWczdWM4L0xIdCs4aU9Gb25xRTFnejVXOWJqeEFLQllyU01jNGFoencyaStBczkrRGVvTENWbQpybjlPcEUrcExMcUZvcWRCY0pVTXhZVktpRmVHWXo5TTZ5UHRnSjd0QUN3MVYyR09hV3RHN0QweUlWbVhveGVWCk5GY3hlbFIzQTF2SUpKcEY1dmRXR2ZMTXgyNDRoMDRGMjR1RjM4djJCMTVGMjV6c29aeUdaR1pzMmw0Ukp2elQKQjZ3bWxqWFR3Tkx2UDk1MHBROEx1RmMzbkhnVm5PWlZzR09obDJ3SGlkcTk5bkU1WStSQnNxbTVwSUpCelEyOApXcllqS2ZEajVoYm5sWDhFaHRHZzRRaldySzI1VFpPeDBxaDAxZ2FmTGRVSlN1SW5aaDFCcDdKVENqNjY2MkdOCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0JhbjZWNXdBbzRONGlkS1hxcW8KTFNJZVN1N1hsT2MrN3dsVDJLYWR1UVExWUlwYUN3b1NMRVl5RU5Fb3lRY2lEVzRRVXN1SWp2K0UvOXRVcFY5WApZQ2llVUQwQ3JpRFR2RitLczVZNmZwaXd2TXZ2T1pxNHlSd3FrL0hoZjBXRURGU0FJNE9YMm5vOVExclhLdDFVCmN2ZlFIaE1qbmp1SllYdWJWMDBGcWl5Q2RqYVZzNGphVU8vR1BZYWZ0eXhsWEVLc2xVQkEzVFdlUEZjRXZUL0gKTk9sU3dqR3M4RjZuQWdjVzlkZkxhbURMM21OblBaNnhoT25TbjQ2R0FJd3Boa21IbktaRUYxWkVSbmRLQW8rdgo2R2gzelloYm9yT0FUU0szN1M2ckNVc3FLM1lqai9KanZRWEZCZ3hpVTVqWmVRUTdrSGxoOWFBTmRBSWV6c0xMCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTBmNVQ2TkV1RXk3dTM2UTRpRUsKbVBQcHdUR0M0MS8wRUk2ZVE2WjI0M1pYNmZmK1RUdEFGR28vTWJiRlRYZXRUWXhEZFNpeTJBVjdhcThaMEc2Qgp3SVRMelJkSGFLc3FleldPdjBMa3R6Wk11NkZ5WmNUWHpBdjB0RXhBV29YR2h1OXg0cXdoOUJzTzZIZXZRZDJFCkpRSER2S29iQzUyN0wzcHBmaHZOTXc5Sm1yQVBwM0hvZk1GSkdwS25YMWExQjBud005Q3dDUzAyb1g5LytEeFAKZjVXYWpHY3RvSjEyK1pKN2dkVDZEOUJSWnRFV2FTeDFweHd4OVpvZ093MDJxVDhFRlhNTE5RUE5aWWhQdldpcQpPaVdEMmQzTTdTK3JSZW1sODBacjh5RGlyMmdYWTRhTDQzaFdCNm5HYzdsRGJCbG02ZHdFZi9YczJ1ZUM3U2RMCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2FMZ3lTZnI3WEtpYWRYZlZwcHoKQnYrUys1cnBoakd4T3BKTEx4VEJkN3RIdUcvanJkWWRubW5KbjBmOE9hSkhTdTQ1ZzdIZ3dTemN6M1B3Q28vagpHR0U2VXFmeWl3bzZnbndlVFlmdjJEb1E1K2V0OTdZa25WYUpBeTRrWVo1L0U0a09wYk1wb1NjMHZnaHZQdTFqCnQrZlY2L3F1Njh4blVtNjFZdndGUVpJU0lpWUdHWWxEQ0R3aUExTG0wYWxGUDJFazM3cFp4VGl0OTdIaU5mUnAKN3c0OVAxMExSRzBNRHo4Z3djZVR6UWFic0VEdnNrc25oM20yc0h4L282a2thRHlFdURoZ2hOUmlPWE9HQjdzRwpDajhIVUFMWFFHWDZBOERwZWpuMlZEMnZBVzJHdkZ2QkZ1YS9LT3QzNkhBTXE2OUw3d0FzL202eG5VTW9XbVIxCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclUxTW9pRHFrZHZHN1FaWkRBQm0KcllNdEp2SHVqZHI4ZDF2SGszcys5NTh4c2x6N1I5S01HMTJpZkV6WmpkREtyK0FNM05jcUsvZjdlcHJDb2RaZQozVDkxWlVLMTcvdkp6ekUwMEkvdDdDSFgxMXY2cmdWZW9wczQ4bTVuVVA2RmFhMlEwc0JreEcwSndCM0pYaFN5ClJWem0xdmZZSlUwZlNvUzJPRmJvOGZFOWtWMEMvVFozYTJDc1RXbGh1QXdZaWkrUUtRQkdydEpmVXQzWENVZW4KaVFUSzV3dHRYaEVLM2NtekR1NytCMFpRQ2Z0RTAzdWg3dlQwTlhSRnJFR3drdGxPUVNSZDR0eHZJTm1zcEFiMAp5OVdmYThGMmVjcTkrMWlWZURwSTBSeHREd2F3VTZwVzlBUENXV3Jmc3hIWndFNlBaeXltYWIzU28xOE5GeGFTCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm5hdGt4SjZiTkE3UDZtSUE5bjMKRjVLWHNDZkJ3RXlvMFJDenR4YWNxZXV1T2srdXFqbE44NkdBcVhzL3pOdWRFeDlqbEF2Qm9YWXN0eTFmUFJGVApISjhjejlQSUtqdTNIa2xhYU85eTNjSlhrS0YwdEdla3prOGRrcFh1K1ZZMWc2endVeWNOYXZIb0ZFRzZTWi9PCm42UktsQ1hPL2c5NmlvMlNlVy9iSDFaV2h4S2xyWDQ4aTg4RDcvbldNL3Z6Q0pZeGp1RnBrd2VtSXY1ekpUTncKVjkrdGk2QzBudVljejlINWpycXdJZFpkeXdFTlJGMjJZcDNjQUliUnBPcmk5Rjl0c2tYUnFwSW5YelB5bVBDSQptbG9IZkkxS09uMlhleTlvNFA3YUJFZVdhL0tlV0l3eFR6dnBSSE9jVUtjSUlId3N5T0J4bnkrQzI2VllJWmhMCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEFxYnpESlFKcUtpbU91NUR3ZWUKdDZNTGgxRUsyVTBOV1htaUN3bVFERzYySW10bk5DWE43bWNGZmQ0TjZDN0xJbVMweVpuV2IwMGFQQXVuNjlBeQplT2FTYytGQnZmc0lhRCthOFVoYVE2aWxGUDdsZnkweDY3UzcvM21aekNUTHYzTWlIQ1lNZEJoQnNnZU5NMTNWCm5YWHRzV0VqbE81MFBZSVdKZDVtWVhVaXpiKzRiOWhEN3VPT1krUTRaUFMxVTIxRmhEaTRraVdqSmtUdUVWa1AKL0JxcDkrQ3U3YzFlZFhUYXNNNGlQSTc5U01xT3RxdlBOT3lmTEY1Y0gxeDFwTDFhNE1JQStKdlFISVRqSnJISApFTnZkM2ozTW1zN0ZXaGtDRTMwK3RwZ3JKR2NUazNiakVXbWlkRTRScEhCUTJPMSs4YkUrcUJMU3ovbTBuaXU3Ckt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3hmcE5GMURBY3hueDFXdHBYMEkKTzczUGFKV25yNWFKdk8wWXNWS1Nxa1BSTnBPQTNqZng3TG9NQ1NrUU4rOHh0MEgwRUJNdUxvMmRIdmVobnNMdgpBODRNVXoyWC9kTSt6UVd1bXBWOXdDd1NpSnV5RDRPa2FwZk9TTFp3SWM2b2lMaEZBTEpqN0pvdXpDbVQ1dUkzClpJL1ZMSjZDNFNuVSs4T090NkZCQUNtbmFHY1B5T2dmK0VRR3V2M0p6YVRWVG8rMWo5amlyZjVjNlYyODE1U0wKbE5JemxWT3ZxeC9hUVRZQ0F4NDMvQ1publlaL1krMk00cEt2WTNsOUhPWTRON3FhbUIzWXpXdFZOQURXc3VsVApsWm13STN3RVdLZ2dZVE1PcVUzMkVIOWFkV2tkUTJxK3NTVDNTVzU2eWtJbkYwUW1raGtFNk5vNWtXc0ZIQUtVCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUNCbDgxZTI0ZkZ2QUNWTTdPTnYKeXNuUk1Oc1Z0TkRncTBNTTI2R3l3S0I1MldQT2taRmhZUmtFM3hIbkIyYkVxWWJ1dWlzdyt0OWRsWG42RnhKbgo3Wk03SEU3SWZJNFpKMlp4aHNEUEw1TnljcGs0OW04MTFFQVlhbmFZU3J3MnVCaVNtZWxFV0Z5b1c1MnJ1UTNDCjRmV2JFTGQ1RmJZaG0rUllscjdHOVk5N25MOTRBVzlFdXIvN0YrSkU5QmlnRDE0czh6VFlDSVNmU3Z0OTRiRkkKeDBIdW1pN3YwV29sYU1oSjBFSDM4NlkyS09CbGVUR1FBdm5FT0xxWXluR3FKS2dydFN6bWljMkcyd1lIc05GcApIeVdQMVZnM2tBSmdrcHFtMFJ3dmpGZUljR25KZUpEK2FlbEg4aDFqT0ZVdk9IL0FodnJCVDZGZVhhMGJFNnNDClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2crY2JqUlVKWm1CQVRRTjlSYnQKcVNSNWQ0L2ZieDdYVHRPM21LYXhDTWFFSTBrNW53Z05ERHkvUmx6dGRXcFk2UU9lbUw5RjVJWDBCblhzUUxZSgpkM2sxeURlN0NodHdZQTd6OGU0ZHhLRG1JQU5aWlRuRjJwSWxPRU5td2hGZW9VK295dVBCRlRSMGtHZkNmczNOCmhaMlB6d1IwTXQvLzRwVlg3dEgwMVF1VGF1aG5zVXdzbDBpNXMvaTBxM2RMZ0k5Z3kwUmIrS0c4QkducU9GT1oKeUtOeWpKby9VQVNJUmtraVZmNGRpWG5KMzdqMGlCdTBhaktJVzBRUGlDOWJBZFQvMHdCUjJGVUJYTFR0NDkxUwpXUGZyaFM1azFjMTFFbEhxNHlaanlqZ09LVFNPck10bzc1MW1mK1lvcldEeXRJMmgvejloZ1lLV0ZldFZoRk01Cmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjVwRW9EL2N2NWgrbStoSEl0MTUKcExEOWwvZG10K0lqdnBzWFRMM2k1Z05obWJza3BzbzMyUDBNUFJxK0YwVDFYSUJTUldscnNyYlJHYkMvMlFVZQpLSVJlbXM5dWpCdzlnSUZkbStKNENWaVQrT0w2dGRGL1B1eTJHNlZQRTdIVlhTQUlWTFlFbDhMcDQ5SkNnSWpiCnJrMHppVHNDUG9JdXlOa3czWWx5cmlXNEVBQ1Awa2tteGZLMXM2VTdMMzM0d2YrQ1Z3YThXL0VEbVdUOTRPK2YKUGlDdWZ5OFlKeHVYQ2Zlc1ZyQ250RWlqQjA1cng0TXNDMldBQzRCdUQvUkt1ZkV1MHJhelVGbkg3Skp3VHNrTAo1bFloSUdrMk1MWDlHdkVvK0VadjdTZU1abW9TdUtiSEVQcWJDT28vVlI1c3diMFdQbHp3UnM5dUpITDhFWmJFCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFI5YlBSdzM3WWVmOUxjazkyQWQKMFFxZEViNEVjeGRlTmZwd01VVjdpQmx4L2gzRG9KS3lWMzRtZGJ2VFRockc2M0JPNmlkbTdSM0gvUzY0VFhHMQpONlNnSTRBMGpKQTg1V2xENkNQallBOElUL2lyeWI0NC9tVFhDcTU5Qi9GN0xYVXNmYm9NTWpwTUNPVTJBR0tvCmRHWWxoa3Y4TXVkQVVGbFM4QkdGZlB0c05ibDQvcUt0dndCMTdoRzNRZ1RqdzZmQjlXWFlJSml6ZVYvdUsyTi8KUy9HekRNWFRSS0NjcUdZb1R5blhXVVdsOVN5WDZOcUpiazNFM2MxMWw5Z000Y3RRRnpqengrcElGeFZOakozKwovU244NXJEY3ZBUFlaUDhOK1hPVnNuS0NsY2xma3BJQmI1emJYTG41YXVPdzhkdytvUXZkTzhaUC9yM3ZFQkRaCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkdBbTlLYk0vMTRCYnpBQmk1dEEKcXhjcFloZjRCUVJtaktaQTRaNEpGdWliR1Q1UFBOcWx3MzBGdjhPdThWbEd4MlpmS2VZWGoxUmNnQktEVElLNgpPUnoveUEzbXlYd3BFdXlOTWZndWx1dTFiNGhBcjBwRzkvRzU5K0NiQStBbjRkbU5sQXVDNlJUMmlhcXUwNHhqCjl2d2JBS1V2Vk9RQTZscWh4MlNYMnM0RmtWanBjM2tjZ2xpMGJhNURtRENOTGs2N2JBbC8rT0Y4dVVoVW1CanUKREEvNzBkVXJXZHhoZjBKdzFzSGlIbXVqUDBlbTlTSnRaeUw5TXh0d0pZTHF2VHJvUDJ6ZGppTlVUU3hxY29hUAp5MUphRExpRnhFUGxvTzFSZTlpSHpMVWFWYkRTaVVKOGxkSFd3cFB2MWdTdmZrL3F1U0RnMC9PR3RxSXhFYS9FCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3ViKy9PelVROE5LVXJERWRheHUKc1dWSm5LVHZKZk1FcmFRRitIMGxNMGZrRW1rM2ZPV2RQSzQwaUFUdWY5ZGdER0czQ0dkQ1Z2QXF1YzM1ZU1QcQpjaUJnVWJtT0Fqb3ZqZmphRzErVk5ZNXVlcGJsZjNtcXFUL3VENkozZXBXL1B2T1dnTEx5WENTMW80MnpBNjB1CnpmMTVsanllOEJQbnJiY0poVXdEdHZ5TFBxQU9pWlFqUDBYcFdQSno1anQ3YlkvOS9oUk9rZWRMWk9OOVQ4U2sKdEdDNjZvaENSNUVQZEtUZ2NKbWlNTjd4WmJWS2xWcmxVUXFoeXRMZjJLanRrOGNKWlRWL3dwR2RDSlpJZ0RUZwpJYmJUUGNBUnBqTmprbXNJYjBFT3h6bmx1T0F1bTRxc09ZYWNUWnNJY1JzMGwzTEkwSDZ6NEFzMSt6WmxVUVpNCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnBOWHhXUlUybXFHUnFVY2tXdkYKNmY4WGh3T3JRQ1REanZZc1BWU2JRODVzVFRDTWw0MVMwQ2xzbG5VSGF6MjZ6anN3OTNBaGV4R283NnF5ZHVoQwpBSGZLeXlKNGdYQXRYV09mMVAzcmFXS0lLem1hNGdIU0JpVW5DTW9vUytzUFk5T1RQajFWbXV6WldIdXhTRCtCCmw2MjhRdmw4aXN3NlM5Zi91VmNSMGtqYmpBTUJFT3VRZm5qcUkrdTZEZzM3cUhESDEvOHNmSXZ6bDlSRHdmSG0KK0htTHFRN1JJTVJKK2ZIclVjOWJLNTc3SEZXZ1YyWm9oVXVNNkZvWVlqSmhRNjBnNTVNOTZCNTE3MXZVU2hFRgpWcThQRHJYaCs2OXZlb1NtZHhCRWN1UzM0VkYwa1pycGEwZE5INXUzaTM1eWNZbG1SK043Wjhoa0pKKy9nM3VzCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1lydVhxZUxJZEZWVzN5c2RlK0cKVVF0YStOamtscTRrWmRtZlJFLytuU3FJSGtXdVptYzc1OWRxT3cwZXRjNnRKbS9Pcm5TNHdFdmhKMXl5RjlKagpJdGlaanFNQkxjZG1OMXdNd2ZBR2lSOWkvdmgzZFlmeDFuR1JoK3MzY1lPa1JHQ3pHalo4NmRTcm9vZkJ3S2h6Ck1Lek5xMkdHMW4wOCttQXlzYmFQVDJyWEtKMzNnblUwWkIyak9UY2NwVk03UnROenQyQlVGRmVQQXNsVkJPSXgKdEROQXpaYkoyb2ljZzVZWGVWajdMZTdkVUtKNW9BSzg1QXM3cmQ0dk1rbGQxWlhNQTlyVG1MZ2Y5Y01Cai9jdgp4SFhjSlB5VDhkR3pQUTNsT1JYSjR5VExCbHRXVlphaWUxTHdBVkg1aWNnL3FOMVlZYk5FMnJFZEVxeVN3VHlGCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOE14VEFOSThWVk94cGY5NnppVlcKTmUwOFNkWWh4ZE96ZmpEQ0VlbTlJNnVCbk16OU54d0hmSnRrem5mRFR2Y1l5c1BKdWpoYzFDQlNUbmFiZEUyMgpIY0psck1HU2RwVFVkRG9QNkxOL2JtRHVmSEdWajZRYnRLdGhQYWlKVC9iTDIwY0EzeHh3ZXR5V1M4VHc3M2trCmhJZnFUdGtRb0JQcnI2dmp3M3BYd3pYUnh2S3FQSUJicDlodFBucEUyVjZiTUMyeC93dlZzcWdyTm0xZHFLS1oKT2QvUGpocmw5N1JUOUxOQTU3WmI3U1laRW9aMGpyN0hrRzIxMWlhTXg0cEtNYzJiY2dhWDN1R29wODNxdnc1ZApyeXYyanhWTi9vR0dYUVhvTVpzVFBmY0ZIcnA0a1A5NFEzTFNVRkZNWmJBYmlsNVNNT1pPYkpWcU9SeDlEY0tFCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHZSMTdkSVV4T3pESUliMEtYRW8KQVZtenZldUtDbVhXWEl4ZEJtcjBwK2xXR2Q1dnZ0KzJJUnFFWSt4MGs1NXBuVzZpSFBPaUx6NU9VOGxmMm1Jagp3aHFPZkZFU3RKTmIvRFBXK3FjMGhVbi92d0NGZk9CUzl3c2VTak1CaHg0YnI3Y0hFS2JEbjI2dUhBbFhLbitBCnl3UHB0aEVRMTNNRTZJNmNJQjlZdmc1OEZMK0tDUG9yU3lPSXBkOGNvMlFqcGgrSklRK1BjMWExTVdjQXVlZC8KUS8yVXZJZ2t6NjRSMEhRQzF5bzZtZGZJcmJ6dW5CVVh0UlZGNDlBMkhXOTR6bHc0L2dxZTlJYWNpTm1pOEFYZgpQd3IwaVVJYXdIelRoWVhJTFpudHI5eHBYTExDV2U0YVQ5c0gvM0cxUS9LN1Q0dzF2UWlTb29nWG5UaDllWm1CCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXU0WmJMU3ZUZHorSmVLTXNrRlEKL1ZueFc5U0JXb0Y4dkRmNXNid3MvcEtBd09Odi9OMFNQSGd5Z29GSTZvY3drQ0tVd2JXdzUwK2VBdXpKTkpUYwpiNTZGamdqUnNmcFc3cEs0Mjc3a2k1V3QxQU9UL2FPaDhqQnNqcTZaaVFIR3E2MFFsT2RNMFdEdDgyNG9ZWThRCk80cnU0SWcrc1pEUUxkbGtwV29zSlU0M0N5UEppYlVaNW1MSmMzOU5RdUJMQmFSTWE4VFZxMkV3c0hBMTdieHAKa1B3NExiNWpMcS9JMktSZUxPcU5aaVRYSE9ZaVJPTDNUblVyMUYzdjJTQnFaNzEydzdicmQwVzczQ1c2NENGSwp1Zm1tU2ZQb2thUlc2dFdES0dodGZFZWtCSG1kL3QzaCtvdzhHYWR3RGhtWmxlZ2hyRER2WXd3dTlyLzRTcTVqCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFJscFJHUVRHQks2d1h4S2pkTmIKOEF0c29UY3B5Zy9jVGpXajRCWHRqTGJxa203T0F0UlpiWjFMSVVnTCsvWEdrUWpuSWhYdGlJNTIzRThQOXZ4OApZL0htTERLVTJMdVRGL2kxWERxUTM3eng3ai83Z01rMlhzcjAvQWtqU3p0RnJSK21oeEJubzRPSUlFdFR4eTJsClZJN0MxNmc2MDJXazhtcXNGcURLLzBzUzErd095S2ZKb3BNWnJwMXZ5Y3BsaXBobE9ubm51VGtaVC9xQzBUWDcKWmh6WlpnSVQ5UmNaQTZtOHdQU1V4eFhUOHgwbldXZ0NGWDV1SHFQSlhhZGo2Zzl2OGhhM3cxanpkdko0Wkp4VgpkeGpMWGtSRFY3V0lRdzJvNnNZUVB4S1pWS25qN1pxbTFNQnludEpUL2dINEc2STNsYnZzVFM0c1JYZ3Iwc1NrCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFJ0dzBUcnhpdnZrVmF6MGN5QS8KbUYzNXNVNE1LelgvWDA5clRVeDNNb3l1VEtKWGJtaFNpMEJIYzRUY09PQUFxZW45ZW13VWxOcFBGUXgxdUFlMgpvWkZKOHMxdksxbStUZjdkR0lmVStWOWRuZHlVNDkwdGNHRnNXRDJwMkhzTWFEVU05WjFPQUZLSTNyM0dPNnh4CmdMUklZVzF3dHZuZS92cVhLR2UxUjc5aE4vKzE2YXZvQ3JTRXJ3amE5bWdmZk5UZmFleGNuYWN6RTZKckNBTHMKdTl2Y2VxYi9pUnFQTVRvdHUwaUN6RlZlWlJFYmtUdHJ6c0hybFZFeGMzNGZ0Q25IekpvOFNDRm42dzNROU5wVwpMZUlhVWxzNzNldEpFNU5BMFdiR09HT3hiMGpkb3hBTWQ5eERJaFVVQlVETjRhR1E5dTNIbGNUMDZFZUc0a01lCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0JxZ1B2ck9uVzhhMG5yMktGSFcKZW5zNDdUNmRCUnFSeDNTMlNNWk1VdHAzN1JCWVNjWW9mTy81YXQ3Q3A1T1p2ZnBBcTZrbzR3MTVtdW9TT0ZqUQpZNzA3SkZ6M1VEVW1OK1c4U2k1THlvckJiRllkZnFoakFvalhzYjFtbVd2ZTZWUzN0Y0doaUwxc29MUU5Ca3pPCk95MFcwTWhHS3dPWVdnNFdLVUpoRkF0Z0RBcjloSWJlWnFMUVpUaDF4cFlNVHFsL2V4bFYvYzZHZ2NGVnZOcWIKdkhaQjZSRkJES1dIOU90enYxK0oxdGxOMDVIQkFpZ0FUQXVEUE13WlY5VFFWcEc1WnNWaFFYYmdiVTdObzlBQQpMTkJsdkNKR25raVFPVmxjOWlrQm5mTmJUbUxTMHNQWmthNzMvdUFEMjZ3bUlRU09kUkYwRjVjQkVneDhwc1I4CmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemI1akVBT1A3bmVMNVBZemwrOTgKelA1WnVXUklKZFR5Nzl2TDVDTzhFWFRMR09tZnFrSWFBRTR2QVR2YWJBQjBGZDcrcGpOeGhZUzVsTlUzYUFXQwpaZ09Xcm9WSnp2eTFmbVBlMWZyTDVHNUdPTjAreHlsOGxSZzZJbzRyRzdLUENkOVBraDdhVURDbHFIeFk2OGtOCnB2OGwzSEhSanQ3Q1doQ1ZxeDZzekxCTzZ2ODBNMlJzd0RwMTFGM3c3L2F2VlBFQjRuWFRoTDdZYVF2ZHM1MUQKNElkTTdxK3kzbisyZHh2ankwcFlhUnVNUGF3NkxodVdrWTFMRldQM2dMS0xWOFNuSGh0VWNHWjJTQm9BQU15aQoxS010cUZxbXpERmp1dFQ5NUVyclV5VlNNTEsxZkdEV3IvMjBhdmJJTForTmJXaWZUVlovZEVmVXdkUnEyVGRMCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1o2VUo2bnpodlVHOW1PVUl0bWMKQ2YwS0lOVmc0OGhsenowUk1hWEk5Y2JISDNubzJYYk9DNFZhSWoxSXF5ZTBrM0lxUUVTakczclBOczk3MlZyNwpHd0ZPYk9ZYUVTZEkyYkdCY2xuUE1hT0k0YVBrNmZSQWtUV0VYM2svbVFseERTdnA1TStveVlMTkJIMmJDdTZkCmFGNVZTWDFMNXdMNmNZMHdFNk9sMHFwaFNuZEtrdjJBa05jOE5lVTY5SDhNdzdwbDhoUFh2WUlmNGt5STh2bUUKejBnSFZDNFBuZVBXOGo2VzNVNGc2dFIzZVB5S0hBVzNRNitVQ3J0QmtVeloyTFV4dWgwUmVrY056b1QrY3NOeApjYzlyakpZWGxJMnFBN285YVhwSlVuVXdwQURzWkpEcmlJdWFlYTkzeENxd2RBNU5RQkJSRWZ3ZVVBYTc3RjBkCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjNhZ0p2VnFrdFhzNU9idHkwOFoKaEE4REhBQWR2bVVHVW5Lb05lQ2FtcXRDbnRHSlBTZ0NJb2lSb09iRXQ4UmlTRE1jVTNjbG93SU8rVlRpL0RBQwpMVjRqNU9XdkVKeThvQjQwaGNtTkVhWFhwUGJOYWl2NVF0Tk9qUkVRS2gvMHBod1l5VkUwRFhUMGxCUTZ4ZUdVCmtWOGV2bENGZ29YODB0YnJVc242WldLMW8yTEoxTUZWc21ndU00T1pIUEh2eTdHdDhNTFhncGRBNU1KcnhlcHUKL0xUL0xzK3VSNHVnWHRQZlZxOUhYQWpGZDVwaDZnU2E5ZXBSaldaemd3RVp3WlJsTHdxVDRuS1FGNTRTdUZnQgo0a1ZjYjhqait3em90SEtsT0VXMTFrUEdxY0QvS0JKN3paTFpobGl6cjd5MXl0RWFLS2t3SlJXeFpsVWJ1TkhGCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVY3L2lmdnpZVnlqWTlIU0NLWmMKWm8zR0FMa25YSERnVDBxOUNsdVNCRXdaQTd2blJCMmlUSU8rajM2TkMwZmZDdzBuNWNDV1ZWQTEvWVFRd1dncgpyYkVTMDdtN3hraytiUjRTclUyYTlwY0VsU1REd0RnNSsvOGRuTG56ZjhzNlE4VjhKKzhHN0xNcllDY2ZHNVlXCnE3WkI1dXFOMzZkbFgyb0hGekp1aFhSTUpheU5Fd3d5aERjSitoR2pMeEJEUjRYWUM4b2dkODBRSzZpbHhXTXQKOXYrNFV3V1NwRk1SNjVGZDN2TnBsd1pnZEw2TG5sbjFKVDZHMHZ3aGZtTUdwMzlFMXFMQk90endwL3NYR21HaAprTW93NUZuS3IyRE5zcTM5Y25mWjVEb0VKNk5TVFFLb2wvWFJ5SDhCcDhtNDRjOHdlV0V2bHo1aWZzMFlpdGxmCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnFrQjJHelB4V0lCMXNVK3lTOHAKbk9SSjQ5RDA1ZXFFQ24yV0syT3RwYU9Nc2JxT3hjVG9JLzhPcTBOUjlONThKaTBLNzJ5cEF6Y1ZnTGdVSDZ4VwppbTRsZlNXM0dMWUZ5YUZEMk5tYWlwRWFJbGJZT2VtOWgvNDJRVCtPTjdoVzZ4dTZpRElhV3lWVGkwVXBYamRrCldTQUZEdTh4anN2aGlzN1JTMmlnenF2OXA4QUdxbHFweXRYSEoyaFVOVXA3SURRZjRJaGE0NmZEREdLWU9GQzMKc082cFNHTFpLVDFoaUhvTVRaSnF6ZHExNXBpbVhNalZMR1lIa3NwK2VUWGJzNWJsRzRHNTNoMVhqVHJUSnhBSwpNVUhjREZrRm1YMmU0ZTRaNlRQYXNONVRhUzAwMUljaVNkRkpFL21WNXVxU1VGVWlRUDA0bmV0ZkhJOHZnZm5nCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenFUMnIvYzl0Q2tXVDJML3dvbW8KVWJMNkZ4ZUMrVlpYZmZhaFVOa1crZXNjMHNSU1NiWUlRZG1aY0VkdUJRVUE3cm1NVmNtNlEvWHdpUk1OdHVXTgpTRi9YN1VPOUtWSkpQQThNMXBsckVpN1BsclZPYXJJdGFyYXRQdGt0cnpnNldwd1pad1RUalQreHB2U1J6WXVTCm5ZcWVJRGV3WmNtNzZDbEhjYUdVOE5hYUZsMzMzNlVGR3l5bThub0RrSkxLaHBxWEllMWRIN2xvWEV2RGM5NXkKME82ZytRR0Rnd2tWby8xZG9QN09yMTNhRzhqMllVTjZieW9HdnIwMHJUN3NPT1lyOG1CMGl6VTdsclJtWFd6SgpoRE52ditHbURTeVhFVC91eU1HSVIrVUdSbW1CZ1E0Z0tScTV2N1lpWVI1bjFUT21SMWNxb0NGVXlWcWVBN09pCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjg5dWxwaHl1N0c3YlVQMnI4RUgKMDVoZmFkczgyd09pS3hlbHVkbWhWYWRIS1E5S0hDeDlWWmlodWVEeUtlYUN6MFpZV3E3MEEzUDhhK3ZEQ3dHbgptZ3NhNHpaSUZWRVJXVlQzWnY0TkFTS1AvS09vbVpieTB1Z0VHN2o4YnVyQzJlQ0E0WEJCOWkzTk5KVE5rWnRlCkJrRWJDR2RBS0dLWWxiME9ndkppYzI1TXBvdEh6Znk3bzFXc0tORnYrREdmWnRFekFreGh3QkZnb0p2dTVuaEgKaFBjemNlU0FuK0pORFlQeHREcFA1UndRUDFzN1ZDTG1ZTW5PVUxGVkpEQ0JIOU5HQWlCbng4RitRSDE1RU5LdwpxMlFyWjlYYmI2TmlnNEt6VHpaa1pGWmVCcWRxUXhZQVh1Smh5ZjVkMXhLUjFEaEhKZGFKa25WRk85UTYxcVZuCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME9lR2JDWmExQXpHWVc2djMwZUgKYjdqQ0F5TE9ReWg4MFk3ditRQVMyZXprTm9yWFFLTWhOb3RGTFI2UFZQcHNjS1lIUUY2QTlhWjFKOWRYZVNRNApjSUNRV2lMK3lpWHpvdjRZbG1KWjNaME5UZTdJNkozdG83OHRwdGZVTDhEUm1SdUtxeW9WSGE4SHJqRlVYdHd1CnZwMTRXZ3V6eEphMStqSHdXOUdNdUt0dnBjNk04TW1rdTVJRWJsNUl4bEFOUENHS1hQY3FRbEptVWpKSkdNNGgKYjhUWDhhcmdUUTdHZlhydVJaSUU0czN6YmowQjdseUZuc1lsN0drUitPaXJ4dmJmdmkxb2VvK0pMMjNQYnlUbwp6MS9mQXhWUFBUSDJVQktjMU9rMlF3K0MybGRzdWFUNjdoM0RGaFZQKzdlakJKcVhzNG10c1FIVzNvRnpjelZEClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEhGM1RCdmEzMHg2Y01Oeno0dnkKTk5GNUM3Qm9uUEFtcXViWWRyQ3FyNjhZN3BlMnNFYlY3UVRPdk1ScERUTlgzMytLZTA5NWVSelN1MENhalNwOQp0MUN4VjZ5NVNkamZxL05IOEp0OVVlVTJtT0lGNVRPR25DWit0d1JhMkZ3QVpVQnJqU1M3M00rZlVJaEc1ZzZLCmExZHNPR21yQXErZFpBeThjWEszNld2TW1uWWJETyt4bE9SdmpFWVo2TFpwaFBtemM1elZTbU9Ua0pZWUwvMGoKdDdoZkRWOGdOeENZc1dCQ2dPQk9pMCtsSGlJdm92dEorQUs0b29CeXIyUGVZSENmTXNYVmRkaWx2ekZ5K004TgpMYnZoZnYybTRXay9PZllQZWxqOURJY2c4R1dzWUE0R3pGeUJyaklUV0phaTk2emZsWkpaZHF4QUROTHpDMTQwCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE4wV2FJcS91ZXdqMmZYQ3lwWE0KUUM0ekFjUmI2RnRiclU5VTQ0dVR1QU92eWw2TWVVZmhCNDdIb25RMlFYN1ZpYlZKc2xqTUtjaDFPQmExVEtmVQpsOVRkeUJjZlUxTGhlVkZTcjBLaHl1L2wxcUFqSDRSeHJBUXdYK0VTMUtjdFpRclFsMkllQmNEVTNVQ1NLNitOClNIQmMyZjY1UVY4QkpQS0w2UERha29vV3dKTVZGMFIvbFl3UFB1RlF2S2hIOXFTckIwZkJPcGZhNEd4b3RodXcKaXBtemdCdnQ0bmRyNE4zdUd5UWFiOG84N3JXZTNmd1NpSk9kRGkwRWlkenQzdmpUYzd0RDZOZU1QRkZTbnE0MgprcVhFNjhMMlVXa2V2ZlpHSDVDNDZaNGpodXlCZjB4SC9yUnhoQ2RsZFpYV0tuTzQxSkk3N3NWMGNRSWVVbVZDClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHZWT1dOcWVJNDVtcGRxQmtTT3AKL0EybDVCUXRxNU9DdWpjUS9rU1BiemdyS3ZBcGh0RHNMZjFlcjBTdjJzMkV6VkN5RlRreGRRU2prdUdrb2VjMQpkZjdvSGVTOS9sQnBrN2RXaXRvb0UvaHkwYitNSUZaMEh1SWJZS1pVMUlaaGdoZzEzaVlkNTN4TnpDSlFUUmlDCmRHK0ZMQmllQkVlbWk2Yysybm9EWkhJMHNGOG51dnFiMi85WmI0RmZaMHZwd2RpYkYrd08wSittYmhXSGtvVmEKM1doR3k4RFYzNE44RERKQWlramFPUlByenR6Ukx0UThDRVc2azVzc0krMXJuYVRFajdJT09TaFdubFFPaWloegpmZGtteDRicjRFWm1tSEx2V090VVJHZjJGOE00Nmt0S1hWSHFGKzNGMU9CaEtEaDdUbG16UTZoOEFWbHlMdmdHCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNklaS1dlaUxXS21McTl0M3ArWlgKVnA0bENQb01GN0Vjc1VxZUZTQjYzeGdnOUh1Q2VrQkl5OUJ0dlZEVytjaTNjSDdoTnk2eXRwbTVlOXhEZEI5OApCNlZEcjlJcU8vTlZOME95R3dYcGhsWFRoSFR2SDNhN1BiY0NUS2tIL3ErUXdzSkZqd2pGeCtKMG0xU2ZoU1MrCmFEZXpnU0dLOXZpZ3hobzRMK2Rya1pWV1VleWNiMk5YRXFWMHc5Z1lLVTl0UzlTWU1FSk02bjFBQndkM0xJQUMKbDE1dnp4dTRaN2hDLzVabnorTlhtSitST2xWRUtnTjE3QUNlZ3lWUE9mWVJZVGVuOGtqcW5LYUxqeGU4SWhoQQpRa2dUSkhCSlpRMWtFMXdIVU1ubEVtcDFJTGJQa1l4VVBTbjFodytKd2JreVZLeDBWcWI2c001emJlWkRuZk4zCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk11VFpiZkdDS0VYckZ1Rkt3N2wKVWpOMUE0RHlLaEd3UStkcmg5WjB0VUFJSEVoYS9WOXFPN2lWeU0xNlVsQVhmUHZTRFVQSzRSTzBOODU2NDhQagpNQkp0Zi9DL1ZycUptU01zQlJ6UENsY0Q1b0o1cWZkTllrRXVuamJuNFQvQ3Zqa1Q5dmF6NlVzWmhDSTQweUFTCktudzFjN2prQURoZU5EcndBY3Z0eW5SblBqcW42V09pWGhZZDFKNytmNmQvMVE2OGF3RDhVRi83WWMvOUpVM2YKWThUMFBzR1ZKQkNKcFY1VWF6STU5N3AyOVJkWm1RN1FGTUtnRm1VUlBITzNMb04yQitZZjBwSlRLZUJKcW5legpEdndtN3dsdHNVY2xoSCt0di9OanpzNnZKaGMzcFg4Slhzcjh0UnE0MXZZOTFsRWhSOHlRZkdtYlRjTUEvM2h3Ck5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG0zbnFNUHo3VkdIaUhLSi9rYnQKNHMycU9FdktmVkVjSytadzFYbzBCU1FnOTdZL3paUEtvODZFdjJoZjhzN3RuWFVIdmFsalNkMmtCelV6emhmMgo5QXFIeklITW9pdE1iZ3ZObHl5N1dWSjh3anZFTGl1N2VESG8wMEtvRFhkYk92N2JPd1JLVGZpaEVYcEEvYWc0Cm5xZ3lYeCtwcWdzalhsclVxdC9Yd1Vta2VYbFVBMDJKT3h1S0lmQ1U2VXB0OGdnSnRxTkVTbEQ2UEVWNis4Sk0Kb0lHQ1dhSEMvbHI2RUtoNzdpNmc0eW1Bb00zMncrbnF4eXZZVFFVelUrY2kyaFBDbFNNcnVUWmdGTjFPakxoOApPaWxIRHppbGo3K1cyQWxlOUZOVjc3a0UyQlVkTkVuTmJDZzFjT01GMXhXZmJNcmJ6cHMxRlFjekZVeVRUTG5YClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3NpTnFoR3BwRzVTMTJHM1VDbHMKM3ljUkdWMTJEQW4zQXVvYTNGa2tWcnc0RThaZ0Zvb2RUOFdIam9aTmkyQjZrdXJXd1Q0Nlltd2wzbE03TnE5ZQpxdEo0SkJaRnEyZS9GeTBudm5GdC8yZVZVNm9naWxzUmEzV1I2NU5BNXJYZHBMZk1WcnA3bjVEWXR2SFlaVVV2ClhLMGFaVlRDdW9TUDhqL1U2L0F4RFBwQWRhb1VzTEZNRXB6bEduUmFnNXp5RVRwWGxhZERKRWwwQXFONWJGV3AKVnRZMlg0TDRQTlJkaHFnWk5ENEYyampSNHBjcUdvNW5nT0k2Q1ExSDlVWmlmbFlucnV6dUlXRTZaSVlGNE15dQp0eWJDWGoxd2xWdTQ1aEZydWVJY1FmVDNsSGVnWWQ0ZUkzeWZVT2JQUFhsNGlJTWMzZHVMRldnSlNyN3RvUHAvCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelBqQ2RZV0ZTT1haR1JWUEwzdlYKTURUQWdEeGpPODZpeUZqaEdpdTUzR3BaV0tvdmxzZ3licnlyd0lPcVhvQURJZ3lhbGFWWVQ3R0Q1MGlLUDJJbgpPQjNYQnVlNytSWUdDeFpRc09TbENrQVdwNStaYUFYTmNWcCsxV2crd05SWkNGOFRQRVFRVWZFOVNlRTZYcU1TClpEVTUwK1dGbXJ4WFZYRTI1cDdjeEJDSEw5b05wM2lqaGNKakx4K3hseHBQZUlkSVZJVTYwRXNEeEdxMTZGUDkKa1BpcXRBMHJyell2SkVrSXJuSnhlanN0WW41QjA0eDF3TGM4M3U0WG9QRVNWTkhNb2F6N3FkMTliN0FRbGhMbQptQXBMc3dsb2lvUnJaUnFQT2VLZFRlOGlGcjdNdzhibHFwV041SFMvcHJSenVESGNtelhNT2F2MS9pNHpWNGJmCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUMveTh2SmpKUGFkVlFLWmJCWlYKd2ZIL25HQ2Vvd2J4UGpjK2dWdlY2b1hyUWRERDRLTUtwcFh4dHNmZnUwS0NvRnlJeThFUTNvb1cvRmRlOHRiWgp1UDR5VjIzbjcrK0lISnZtOWM4dGcxMnhqWU9mQjNmMlo3Ynczb3pIRjFzN0NIL3pLakpBQkFURnBxc2Q3aGxBCjRMYUdCaXhxbDl5UzZybTZ4WFlqLzJpTmJTYk9yOUlzNVBIU3kwSHQyZzdyN0t0cTFUM1VqZXBGZTBEVTAxYlYKcG02REtncW1YRFQvMkZaME1XdEFXVWNvellybmtkU1Z0bGdlTDZST243SGJzYUorUUM3WkRXalBGVS9jeUFieQpZTHlORmJIZnFzSnRyVHBMQjJ3ZER2TE9nWXh2YXY2UEp6YnRPZVV2ekh1MW9vaUtEaFN6NThpT2MwY2ZWaUJHCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnpYQlFiUFpPdW5DUzUxRnhJQk0KQ1R4NldGeUluYnZmVXhrT1RYeHduTW5Fd0FVdHZYelFXMlVtNzRjQS9sMnlmZS9HZUNRYTBVNmU5OVNOdzY0VwpBeFhMcTlwQytEckFwNFVtb3VTYUxNWFZtK3UzNkRxdmhMbksrajZDVmgrbVpwdlNnN29zdjdOU1dpYzBvdXhyClhGSnBtNnRHOFJZUFZ2SXZDTkFOMU54TDYrSmZ6Slh3L1dkb1orSnB6eHVlbURKRDRrRHczUFJqcmc4NWQrWW0KM1FYQmJTZi9COFVIbVEwdGkwc3hoaERpTFpTa3VmTDhodXpnNllqbzUrZi9IK25pRTJDYzgvemJEdzFUYTR2WQpzbTRqTWNySGVid2w1RzlhNk9FK3lwekI4RlF1cnhOUFNKV0RnME1VbXZDeFJ5U0cwNlorSVNoOWphaHUxUnVyClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkYzM0E4SWIrMnB1Uzl4WmRncUcKQXRPN0l3b0FHNGdkSnBWaEpBSk9xSHp3eGI5NUtmOEVCQy9LbzFJYlZNWGpVOC9CUFIzWjdpSHlldDR3d0RyRQpNVndZOWltT255aEQzSGthR1VUVTJ4ZjR6YkQwaGRKWmd3ajlZK0c3Z1V1VklXTm5WYTdYNlpXTEpQZ2xiSTZCCjBPcXFuNE9SMXhsYVVCcHpobkxWYkJtd241VkkxREVLQi9wTzVrMmdzT1RheEJSVXltdGh1clI0SzR4QmdnaDAKb1gvK25FQlpVMmxIeFVNOHd0aS9zYytZTzlQM1NpY3JKOURqWld0Qm5mMStFOFYrMm9OZTNCR0h3L0xTY0laMApHdVhIckM5Y2ZtMVQyV3BGSFl2UUVaenJFdXJhWm9HNEhmZDd2K0pEdkQ5STlKQUIvczNVVHk3RXhqdEhyNURlCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEkvZVNKMHVlNWUxUSs5ME84VEYKblZtY0N3aFhZN2FYWXNGdHVSV1k4ZFhEejRzSlM4dzk0b0hWYlA5K2QvRm5xS1dPZVBseG9kbU9MbG0xZ1IrVApRZlBkNCsrbGdtQk1WdTZNUnhqOVNnWXFGSm9FK2RyYlQ2R0txYmZHai9yV2w3cWdia3pIQnJaMUZzMWZMWi93CkhtYVgrcjNZakgvVWdNYkhDOElLTFJpVkFyWUNVZ205S1JoS1Evd1ZGR3BwcDNDT1FCL1R4dFVqMXNhMzhvekUKMklKcDdCM2pPNmpFR0xHK3dxOUVmQloza3JZTUVUbDdLNmJVVWJ5bDRnb3ZyT1hhRjRYSmdER2FpMldCWk1NUQpxNDRtdlNxMng2NUJTanBjRDRvUVJLUG4zck9wZ0tyNXhkOVNMVjVBazJ1Y21XUkV3bko4WEN5cE1jZngrMC8vCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFhDL1lwWWlWdmIvS0NZSlhabnYKRXo0WnR4aEZQdFFPV0ozQzBCYkZvWkxuWGNEc3Y3dGptR3dLSkJodmdvN0pVVlM5WExGSVd1dGZHWU1ia2YxTwpWSmVuakt0R2RaWXpqTGlZWFpTd1gzSlU0K1NLUGMzcElTZDNMcERGdDBwNFUxUTd5RVUvTE1LY2FXRWVqM1I1Ck5yaFJyaCtDcVF1WFdJTjlXWXB1NVEvUElmWFRTa1U3RWF2YmFSOWt5YjE5SGZoRTRqRDhhRU9UQ3lKUURScmcKekRhdVNYYTg4ZzlNN2tlMHg2cTBLVHlvbWgvdm5EdmkrQXozY0VrdmdhTzlXeElNM2Ztb2dOb0hLUmwvZFJRUApvV0FCMS94V1hJOGJWSWRNVVJKUmhicVVpMDlmL0tpeUZTQnBzeXJMYmNLZ21aZTB1dDV2RW0xNDdDOVExcnZhCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG9kVDZUc3ZlaVVib3pSYUZrS0IKaExjdzBHNFliRE80cTRKTVkyYmNRWVMwY1NJQ1lmL2E4NkNsa2xndHE1ZGE4WUw2VW5nK096WGRJU1ZqTnpwOApzTkdWcjJpVksrQ2NFSUp4VkdEeU5mbHlKK29pcTQ4R3liWEtFRWNsTEpyaHpqcDRKWlk5eG91cjFFOHpDQTEzCm94ZmQ5K0hyY0NyWkcwNkNqclZzejRJaWE1VXpGOGlXcHRYVVdIeTRMMm16WitFRVhlZXVZVG0zM1lua0JVVkQKR0ZweEdmN0tSVkc0MmwzelZZanJicUg3aFFPblRNWndXL2NlOHM5VG9iSXlUK2ZvQWcwNzJRWGRUT3VTaERuWApRWS9lL1RkSndWTGtMd0RWTnJ5NzdIdXdmRitxV0NHc21KNnhVY3NwSzNna3NmVWNSNi9YT00xR05hZ213MG45CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0ZHRENnQWtHQTErNVlXQitSVjYKaHByck5oMm1yallCd2RUcmdhQkQrdUxDNVEwSUttcW16cEhFa2hkcld3aVRQTjF6dWJwamZFVCtKYWRuMW9aaQpiMG05VGl4dExrOGpJckR6MXpIUDNuY2NGWmh1Smo3U3dsZHpVeDY1eG1vNzd4M3RIVSt5SlQzM1FMM0NVRkVTCkNDVDZtSG1IWHo4KytWd05xU3R6cDRvalMwN3loTWY1aDlvQ1U3YVNkb2RjbDh5OVJuL3VDVWVyOW1jY2h0M1cKOXpqQk0xYy9qSDljK25BN1A4UDVZWlJnN21vWGZHNlEyUktIc09LWlJGVDgwRzhDbVpyNmJHYkZyTVUvT0lFUAp2bTEzcFBsYndQYVcvQ2RBa3YxM20rNmFCSFMxdFU1R0V4ZnJFQnNSdG9RTHgyMGlVTlJCS3RHc29hQ3BTU05VCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVNmdXZBNnBlTVU1VHVpZk9GcEYKNzBmcnZPQldkR0tDK1B3Y3BzdmNxWVN6bmFSaXhBamJqMDVCaUlPVEpzcERMbTVJMkxEblFtV1BBbE0rdDl1QgpVZlZ1UUtQU1dPVWhSVWRublllbzdVMjJhenRRajh3M3p4dTJtU25QTFRvSWc2RXc2bloyeElEaU13SlY3RWU4CktGa0xzY1VkblpuZnl5dWNCNVJNMloyQjNCRlUrVzA5Tzdsc3hrMUhvUW5yTWVxdDB5V1NlMnhpNHNIZm9tYWEKY3pvaFFvSi8yWENGKzg2ZlRkWENHaXpvU1JjRFJ4ZyttRUwwNUxGdXBDWm03L3VWc3FoeUxycUJtOXZNRmQ2aAo5Y2lacUpwV2E5eDA1N1NEd3puYzFEMlNqbGxuZzRHOGdXL3JNTlZTd1NCYnJyWjdlK2REbit4WkNjc2lsaEFQCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUVSTlRvMFFpVDg0N2pNNEswc1gKRXJJSDNuNlpVWGkrK0FnNFBQWExTaE1RczVzSHI2NnJzclpGMEFhYVM3bStoSCtpK0xkdExvQXlqVHJNRUQzbQpWK1ZleUxKNG1XQW5GRDk1YWFHRmJNTjRmV25kc1FrVXNmVkxGZjdEbTNIZ3lYcDBHVTVVbkovL3NwQUZWQnhPCksrSjdubytiNjUzOUg1WG91RG9hRFNNNlk2U09OTGxHWmdqVHhRREtka2ljTUlYUnJ2REtadmNwL2RyazhBSU4Kd0JZZFlhb3dNWmhiN082cGxiNTVWaFFheHM5Y2xWU2YwUGZDNHlucjU1a2JUOXBpc0t5QnAzaTdaUFdMOEdyNwpyaEZ4SEF2OFhqVmo3N1VmQVMwN3dNUW1tbkNXbHBoenc1R1V1dmpIQ2lWKzBLbUM4NzI1T04rMzJYaTVWQ3dBCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbEJHdU00dTVoanI3Rjc3N0dJazUKZGVZREVNYXljMmluQUM4M1B4alRMVjNBOCs5aFZZY2pFS1lzSm1JSHV2VEcyYUNUN1NacHFqSUNvSGxTQUpKVAovNU9WMmJIUTRpNVRWTDBHU2FjRjVGN1N4ajZLOGpNYkZISjhKOTA5YjlPZU16ZnEzREVNYmJRYkJTbW01QzhICmgxVXVmRnBpODFXYjE3bkJEZEJURi9VZXpDK3NVMzJqQWoraVh0bXRreTdIYUg1UW9NbjMvQWhxazl2Q3dOK0cKdnpiNWxkbjUzMlpLbHlSbEZLRnFiUXpaRFczcGU4eXZYcDQzQ2RRQ0ozc0VGcHVIN3AwcHJRSXVIc3Z2dEx5Kwo2MFMxWXltRWdOQm9KK3lyUkpHdnhFZ01pRFQzTm9IZmF6bWRNWFFiR25QbjNhSnk5Y0lPcmc0TGJYMmFyWTdoCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW52dCt5SEs4NndURW1yV3pkVm8KeGE3aDFFVWZxdlc2N205UVFKREpYblJpMTd0cWJ1UVZOWmJYUjZvOHdxVzZOZEh0SGpINU1FWDZtV0pvVHBVZwpuWGZlaGJYTnlDZUd2TVVmOTFXaHdxTThRY0JwNFJLNjJsdm5zUFlHcEtNQ0dPS3VvV1FrU1daVlltM3p6TVdsCmJZNitsWHhFRnBWYnBtdUNRQ0lVbWcrd2srVFBaam1Gc2xCK2FFYjdyVklHQm44OXFXR3M2WjRMMkNqTGRkcXoKS1ZRcmFEbzBBMFgrd2YxV2srYzRuWTNnRC9DTzRES01GVlVxK0RBZ3dKUFFjakRnejkzSFdDOW1KVVMvNS92SQpCcDA1QnZpazY4VVp1Y3JpUUNmTGwzdW5yUW9DV2RDN1pTNzhsUFVxRzRXekRUQ08zL0FIS3VQUk5lUXovZytlClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekNqMFliZnU5LzMyNXhlc005Y0sKb0d4UDVoVTU1d0JFbEJVRXprSjRhWjFoeGg5eTBobUJ3Z1hVY04xNElYL1hEWGhQTlR6Uk1wdDlBSDZmcmllbAptY05hNWt6SU1aNTZjVU5HK3luKzNXSXhOb1dPaTlhRXI5M2xoMFBVb2luK1RqM1hpeFpSR1NzWHlTT09sWWxsCmdWVEUrUXVPWHVSM0RhQmJDSUlBbHNuc2h0UUlyQkRWVUI2QWpoMzZJZURpMkhCM0plV24yL3R1ZlpYd205aHMKODJyTU5hbGJBSG9mWktrWERoSTZWVzRZeDVCUGwzMXRoZk0yQ09RcXpTL1ovQ1UzMzE4cmpMZTZlcDdEQmNRUQprUGl2RjZOUDg5ejQxdG9vZXlXd3IxdjlsSStIVHorb1RWaEFZZC91TTBWcFdnZndGYjBKZW9qKzZCSDJzQjhDCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0gyd0hmSHRzT0YyLzZlVHV0OFgKbDEzbmJZT2JHZXRRT2lQTGdIME9WbWpXdGc3blpnTlhnWmdaWnhaSVJPS3ZlVEdjQjh4TmpDVVY3ZFQreGtaVApoVGg2cHhWaml6VGR1TWFncTN5cUV5TUJDUUt3NHE4RHBId1BGdzBvZjhnM0FpUHpmbDdoOWlpMWUyM2JkVnBxCjlkOEk3QXNsRGw4WWRnN3hCaVdhMWwwZWlaVjJSTUlhdUM2Y0RSMWZxdUdCdDR2aWlUbHdXcytiQlQxZTVoVm4KVS9MaS9NUzVLOS80NExpREdseGJ5QjNtRHZjbTNwVHVKampQeEVZM2hQYnoyY3BVSFhsaVdVSUxWbU91ZWE3agpNZEFnNjI1VCsyVDlDOUhrbVNtTElNZG5TMmR1L0pYLzRDRGQ5NzFiK1RnYmwzTjhReTN3YlVEbWk5SktMQURYCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDlUNGsvcTZuaTFEL1Z2cTh6OUkKMEUwZ0NIeTBVdnZwNlFQL1NJVDcvVlhUTWdWTm52dkJ0Zyttcm5EekxSS3F4dmh2UDBXaDF5Nk9XNVpMR1BQdwpxOGxhYks0clRLUnJxMFA2YkpQdDNQRkNzcWF1eU5lN2h0S0M3UFd2WEpOa0ZEZ1pFNDhuaGN1Z2l1NFJKYmJNClV0N00rMy9rNEQ5cTM5emE3dVNKVTJ5cFBpWU8vbzAzWWlVT0NrMGkwaGloNkNibEZjblljOFVZQUNXd3hvQXEKU2lRYi9TRjF1UzlqbEhkalU5azVnQjBUbFhIeGJLRnVidjZHTTRKZDZOakhsZ2pXRXdoZHRqSWlmUFFubUJGbAoxblVlWWR0NmRSR3JZNWp5L3hLbVlTZ3BtRE1tUlFUd05uYWViWnZ4Z2FyRE5qdnhFamNTTkNHWW4remY1eUtVClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXgvUk80aVFUOEJ3TUZVU1RlSWcKUFBvbHJzWXNpZm85TUNCZlBVRUZtUitvMUE2TmFuMit2L1JndWZGaHF4KzN0WlJzOGt4c2pndTRVS3R4TEU3QgpKcWRsNmg4L0hWR2tMT3djeUVvZHphbmVIVDdDM3NNRUF4dEVqMXRzVlRhbi9LdDVFbzRWVU02NUJyZVJPTmh2CnJOcDI4a1JNWEpCQmxVSEs4d0NuL3gwUHE4eDRoRUdRSm9TZjZFcG9OVlpNTXBYZE10QmJvUUd3cElPNzlaenoKalJUUytONER2MThUNlBCZWtjRkp6Mk9rdjM5WXNRNFVGL05EZVM5L1pCTWpFUUdxcWI3U29nU1JYTDVYVjVhTApPeCt5cVZvQmZKTTdQS3NqSVM5TmFycjFZbDVVTDVBQXUrZHRVYlZhNDFoVEljY1JNWEVaMnBybFc3R1Y1M1JnCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUpZVHpSWjRXelJyalZjdEpBR28KREFqbElMTDdIQWJ4RTNSOVA0cXdSWkVHek9yckkzUTdMMUdQQlVTOVVISHU1dGd6QVNVZXExMFVaTllrSGkvZwpEbS9SR2NjaHZZYW1lenhPWDZwZE40SysyM1JLS1lFM1JZamVmQzhjZDVkOVhGc2RKdEw1dHdaT2tTTVJQRkZ0ClM1aUEyS1ZidFJCTmtRaHpOaWlWZ1hRSEVOWDlvOFl0eWRTV21oMXdadlE0MVhGS1FHcnJ4a010Q3c2cXZzRFkKb3RWTlZZL3pBWStXa2lhZTB6bGRVVUJDVC9KQTdNb1JCWHptVXRWRHdmMEN6dDVKUitRMGlxTy92NXZsWEd5RwpuWmdTVUFtR0w2blFzQjE0V3pEcmR0ZG5qM01DbGx3TnV0WFRNLytjYTdXdlhLdnBJTm0yc2x3Q1dkSk1Cb0h1CmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1J4ZzgrRTF4Q3ZvMllkV1lYOE8KaHNiQzVKb0FnSTNYeVQvcVJUd3M2Uy9HZGZyUTJ0MGZ1ZGhiZmxzTW9hMXI1Z2pnS3VGaExzTHpQMWZwaEE3cgp4OUg4Wlh2eTlJQW92a0o5bVZVN2pVQmwrcks4Z0UzN1M2UlhXZXFEenlWL0cwMktvSGR6dEtYcjhHWWltY0lmClZmdUNRZm1jdXNSN09YWWpGVzlyUHYzYm9IYVRZTE1QVmlXL2VLVXJKUXdHNm9sMTFMNkJjcU5lQlNaWXVnaFUKSDJEc1d4eXZhcy8rRG1EUnRtY0JoWmFLM3VDZWVITFczQzBLVlg0ekhtUVh2SU5sRmxJRjEwVk9TMjMvQzIvTApjSG1OM0dpY1dlUDYvTmkrenhpRUJHV3I5TDdpdHdpMGlVWHhPb0xRbVFENkxzajh0U21SUk9WTkZ0OUlsL0lCCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWJ2QkpJNlZVM2RzQm5nTC9EdHEKQ3J3WHN3L1lNUXJ4QkVscFhWamh2OSsvZllndmZmWVRxeFhxTWpGWHpoeWFaNWVvMENlckVoWHZpRVJmeUdIawprMU1ZVm8rd2J0a1B0bkhEbi8rYVBQK0JZM01sZnZRRjVIcE5oMWNKZnpydFJsWWYxYmFvbFRmZ3BzU3lvK0xZCnhMd05icjN3cnl4c0U1RzZxSXQ4cmIxcy83V1VqNkR1R2JLRnBXMXFIbXNrK29xOFlWOHJubXBhZW1lZTFkNVYKRnBJMUJtQmt6WGRKNVRJWGZiMVl5RnFwN2YzdHcyS0tvbVlGdGFEaE92dUQybVg4bHl0c2NyV1ErUER1cmx3ZQpBZGo3MmpVdmh3K01pWnM1ZHlCL3JRdXNpK3VqZVBlZXhWd09NL3RuazdPakt3WVFRcng3STdNcjRjYXNjeXRqCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUovcHZXdkZWOFRHaHhqNEcyejkKaExqdVkwTm1SbHk3SmN2SlFBSk51dk9Yd0ZwOHdEWTY0dUlETnQ2V3VuRUJpaFlyUDdFZDNOeFdoK2QxZlc1RQp6TmdkOXRtcGNsdWNkUnVtNGxqVWQxQkQvdTRmYzVuaVp2ZGhjYUtFeThNeVB5K2dyeXZ3UER1cVkvWkJ5bGRUCnNlUGt6MzJmc0M2bUR1YVF3dWcrREF0VzlZVWQ2Q1RpVG9XaXVZM0ExdEdEQjgrNTZpMlJqSTZSQTZNc2liaGQKdTVQZU9uUy8xanNKbCtBQi8ya0wxWXhVd3pkeEVFbDNScVNOZ3FodDdyajJYaS9haGswZVR4dWpUU0hsR3lCZwpGVDd2cXprVG0yOEYvblFqb3dGSVh6NEZmVE1VTWt2am83YVZZNWFiTzkrbkowUk9TRWFLY2x0czhyWFozUjZoCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdCtkejFIYTJkMlZFbWNCTFRzTUYKRGJiQTJPY1h2QzVCeCs3MVpzOER6YjJuayt3dkdWUE81MElNU0JqcGVqd01QazZ0cGhFRkVRZFJSaW5tanZ2YQpBOTc4K0ZCZG03dURVMXU5N2pxN0VMOHQ4M0ZuQzA0WDd0Vy8wOXJiUXhnYUswMldwd1VvOEZ0aXNZWFFuQmNNCkQ3L2EwamRtOEtyZXZrT09mQlYzdStLUjFRVVNCb3VLeHRuS1lQMmpoZ2ltSHl3M1ZVdmVkd2phT1Q3eTVQbC8KUG0xUHpyc1FVRkY0dGJuaFZJNTdhcXVUK0x4bEtSMHplMXFQVnZzb0pWcXl0ZXZJNGdJamk4cy9xYjRPT0dESwpWVzJqa3hPOEszZFV3TnprUzF1b0wrdWdwV3kyNi9OR0FsRmlQaUxLeDh3QmdEUzlGMzBiK3hvSWdjTEVxcVAvCmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE1JREJVRkc5bUJKN3k1M3lLMnUKM21LbXIzOXl2OUtRVmQ4ZU83YWVVZFk1V2N1ZmErUFBWcEk4dzh4V3lLNXp5d0ptdmE4eElMajAyemdEdDZIRQpmdU1OaW5heVlHK2QwSlU0Ky9rWXJacXBmWmxhSVFoWndabnAvaTlsdVVJRTNQZUc2SC9ldVdBcUxYcGdMYXhrCmNRUnN0TmNrS3NGMjRMVm9ySnU4UUUvMUgxbGpDUkFlc1ZPcHJEVGRZMWd4R29vdjdia1ZWMEFXVzUrUHg2RVAKN1NXYjlQcWtpOG1ocmd5LzNIWHJLYnhUUzBNb2tvK1JCbkx1R1NIQlRWbXpZdzN0MVFTdzJJU3ZxRUdHYTFtQgp0NVVJTDdCdjNWL3RsQUI1amd4cWRLUXVuaXRYaHdpVTR2MTFKVEtXNVNOWkZMNVYrWHArUDNxMUZzbU5qVHZJCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmhuakRjbXM3TitkZEIweitXWTYKL0ZpWisyanIvTk1jVkwzL3ZmbmFFYXZSSHVRYnpMdmxrSWZnaHFxVVlNeldQWHlzNWloVlFXUnowOUw2NVhIYwpsSER5bUgwaU8yNW9MWWtzQ01QVkQ4aXluZVBHMlBFUXhwNW5pSkM3SFMvTTRlWEs3VDE2NTM4ZmlhTXo0MTJWCmNKaWl5Q2R1RGh4bC95UHJMcmxNM3Q2OXNoZW9MNUN6a1BBQzRtbTAxUEFxbDJ1Y0RGZFF0RC9ZWEhFa3F2R2gKQXZiK3NTTmJ6c1k2Q1pTQWcyWG80UDZuQi9QY1hsdmpBOFZnZGFnZkxDOVBEbURLTU5IbGpMaDY0N1pobzIxQwo4dFFnRmNQZng3NFM5WWxXU0dnOUoyWnoxK2N3dk00bUdORW8yR2trOGhOQkc3bzhwakQ0VkF2aGljbUw1WjZsCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2VMY2lvdDVudmJrYkZDMnMzZS8KNXc4RHp6eW9iSzE2em1BbGtUQUo1WEF5Ly9kbU80Y2NzNDVFY0ZaNGRvbmtySTRRdXIzdTZNcEpyN2tzRzJ0SApKMkpzUWx4eU54dlVaK3RyVjBGcGJOQTUzZStHZDJRbjV3SnFhZmd0MTVQOEYzY2tiYU1JaUc1ZjhBMGdobDlDCk9lalNlVVpkVnJWeUxSOEZCOU5hZTZRNFllV09jeWhJQzlsQ204QkNrS0ZUWnRWRlhXRmR2VXErcUxlUEliK1kKbVNZcDFaMGM4SFI4eEtIU0RWR25yakswRDZ2eDdEdTc0cVRJUzZPaWVmcS9iTko3RWFFdGFONFJsSVhFZmxLQgpvTENhTTU3NHZQNFJwam0yNHZ2VE9EaHF6NzhXaVl5UkU4VDF3MkY5SmhQVXhvS2dIeXI5N2FRS1ZuMUw2V1lhCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3U1Zm5kUWM3WGRpNzdOMzJzY0YKdHRndzNTZG02R2dyZmNqbFIxUXY1L3BFbUdhZEJ1cG8yc1puK1lwSTBaK2ljbW5PQ2ZldGhxQ2k0UzRGQWpyZwpJWGV0d2FqcDJBUGNRL0xwcjgvOSsrM0dLSnhsUFoycHNHckNlTDI0WXJTSGJzUnVjamZaMDlNZXRUTHdwN0dMCkgyckcvdkZ4SlROYVlnMVFENWU1eDR1QWZqTkhLWGN1UFhSU0FrN3RpTUJYbmdxYkgvMUpuMVNsK0tTTm5wbmoKN2xSbmN4NmNhSXo4WFA0TGtDS2ZVOWJPWVA4bHVYWWF1anU5cWN0cXozRVYyNFFlRC9SVkRKR0Q5QmU5aG5ISwo5QksycFhTeitxWUN4UG9MQ01ZU2Q0clJIRGZibjg0QjJ2QnNTT1IxWTQwZlAraTYyU0pjUEp3UnZ2enFJNnZQCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFlZUE1MeVRCMnJPMXptTmhYaTYKNUNDSjBuUElvVGZSc054cW94T3BXK1Q3NHJ2UmdnNzA0Rytic2d3eWRzMG9mVmN2SGYyWm5qWWw1VEUwYmpTQwpsVkNERG5OMGJRYmgwb0ExY0ZmS0RoSlN3czFaeDZFZ3E1QjdaQnRTaUptU0tJKzVSRDBMdkV3SUdKbm5qem42CjM2eisvQnVRL3UybzVZZDN2c0prZC9lOTFrMmdlSjdreW9kcitTbmNHN0VwSmFrc0VBMTRtNy84Q2s5bEZpclgKWWhTNTRrNTVNTnhJM2JIWGpqRjVrbWhReWNFa2tYZ1BIZ2ZiQ1BOUUJrdG1GQldsOVJCS0d3TEZJRThGZWI5bAoybHpuWnY3ZUtFVGZ2Z3JnVi82YTR3Vy9sbjJPbkFjak03NWNxa0pOaTB2VStreHA0R3kraVF1NXA1U2w5clVsCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3ZKdlo2aEJoM3ZyQzVSSnU1STcKU2pLYTNROGVHY0RkVndld1Q2SWliWk83b0V1YnlKQXpaT2RvbnlURXptNHRvYUs0TFhXRW5hRU9aRkpQM0Q4Ugpib2VKWnFZMWk4MUxDMHErWTZpZTVnVUxOQXVONDdQaTNURVZkb0ZpWkM1REg3b3E4ZVlOWkk4QmtVRm5RczlNCklwUEowSER5WUh5aGNtcTBudThrZ21IOGF0V1lyd25JTzNPV1VRbXZ4bzNMcFUwQ2NXaGEvMXcwYmFkNnQ2bCsKdHowSGJMTjdjZ2pKSmtuR2ZFY2Y1S2JrdktYRXF4ZUFlbkFyeStBL3I4OXJ1SCtOenNKS1YxSllQVHRyc1hEMQpqTVl4QUkya1p4N2Y1bUQ3cW1rZVNRWldOME9JVzZ2NUxHd0kyOWFzd2tqTi9LZUJ1V3gxSk5KOGJ1K3lUNUI2CitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE5hSDRXK083Z1hXZkFVQklVRDAKMit5bGpMN3FlNEtIZGM3WUQ1bTFzbXd3RHJHdEVvTXBYQjhaVHhScDNnYXZtQWRHUFpXRHZUeUJvMktNYy9UVApnU0djU0lJMFZqMmNIOVQ2QlhpMmVuMlVZSTllSWZNdTJXMTFjbjNOdWhtc2Jxell1U1QyeVN6d1NoSHg2MS9YCk8yeGQzUkhkdUpRdEFraVVHcWk1YkJDVXJxeWw3bmNDU2MrU2psa1VZSVhIaDc2YmRUeXFYRTRDRVdWOUxaZUIKWEZWVit6Vk9FWUk1dkJPbmNUcWxrSU1CdWF5cG9KdVJBYXNHVXBheHZPUjBNR2psSkNtQ2V6ZVE4cXVOT1dUYQpLYkQwWnNJQUdUdHR6akUrcDNMSGZRakNKMS9YbVlST01PS1RtTVdaYW9BMmJyaDJvenRhWitiL0tGTG91ZE5vCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFZmS1VPVEp5dmJLcEtmTVJDdGEKWFVIaXdPbll4blBtaWUyQlVobjlwbmVZSldOVWJwUmdaTzZ6NjdnbEN3TzNKWDIvTVA4cTg5SXBkNkRRZVBraAprdEM1WXBCNHhhMVI1Yy9IaHpwSXFodjlnakpSbS8wMDdGc1hXWWZoYnNQcGlXRmtEL2N1eEs0cmkxZ2N4N1NDCjFFcndBQ2VNWlNDMlgrVTZPVVJpNmg4VGg3S1l1M2FqNFBhZExIY1gvOWtBNkFIRGVvdWFNYVdRTlU5RExLNkIKZS9qMU1mQlZzZEtuWVJkMTE1OEg5cXN5QlJDbWxNUDQrQ0ttMnh6bWRhTG03UWNEQytBUG1yT043Z2dHSUtDSQpmWStnRllhczJZbHNPNnF5RXFhZW0zektncnd4VzZyaGpxL0VjT1lBMDNaaG1rYkx0NzI3OVF0V1NqME1LVEIyCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnd5d0VtSkVPNUFReDYxc3lIUDMKMFU1T0plT2ZobmxVRDFyc3VSZUN6a1dnekhXVUVieW93NWhZb3FhaXFibnU2VHFhajJPUUIxUXVQRjlJQk13QQpJMDNuQWJ6TUNnaFBXeWFjZFJjTW0vc3paUVZHMWYzb05SZW81OXBDR1RNK2ZYMzdRMHhiMkdqd3Y0UU9HWEZCCjQwWXo0dmtyZm5QUzE5YU92VWxNTnJDUmxtMkR2K3BrdDIxZ09ZemN4dUQ1OWxobXdFeGZ3dEQzb3puc1BtV2sKenU3VHRCUFk4RnZLbXNTeGorR0VzNFlhUU5QNG1kSXFXUVRBS1lwazh3S0JZYkJJMWNaRVpRZElIRC9uWHdGNgpLanBScHdzbHlXSGpGNmpTeCtQQTI0dEFYT1pjVjBVQjVBUG5WRDA4VS9LTTdxTXIxYkFZYkVuV0dtUm9TL24rCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFpoZVJFRko4aEg3Qll0UUNVRWQKNTUvOUdaYUNIMjZzaVprU25lZlRsQmxzQXVpQU5JSnhiTk1pOSs2Wm9IRFl5MWVVeXJDN0dabUdSSW92RlZtRAovWTZRbzk4WlYvNlB1dDdTVnB5ZC9NZVRYUGlWek5Ib1VaVWpXOHhTblNIM1QwREFrclI5WFJJT29WbEMydlAzCmtHVkZPclVDcTFHcHNPWGF4cGJGVkxHTktCWXBJMEdySEVjVHlsM2hLSE85T2ZnWkk2YkVLUU9qanhzMFJHeWcKbEdpMzROWjhieHJaK3AwQ2dQcS8zVUg4a1QwSkNxaGw3eTNZQnd4SVhuNFdlcWNYYUVPR0Ewbjl5YXNkdFZIYgp6MXF5N1Zaa0ZJb0llOHNUcDBpQ3UwVnpaZEU4djJGYTRTMCtTSWpleC9oM1gxL0JWMUVyWVVPMFpZWnpzRGp3ClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnA5RFIrNXQzMFAvLzZWMi92R1MKSWpwZENSdWM1OVpGdzk0ZFF4YU1wM1hLWTAzYXNmR1JLQVh3NVpsakV3bWxxejJGSnpUa3hRRjdHY3piV2dnWgpJRjV1MmUweDlXZHRBSnFEN0UrNTFHS0VqUzloNmk4bWNWVDRTemlweStOemFZQjhJemoxeXZ2TWUwZWR3YnRPCmRZd3JmbWViYTVrMUZzTVNJNStxbisrd3hPYkF5a2RHaUJiUTcyOHdzQldOdEN2bU8vZFRTOHdsLzVoeVJjV28KMGQyYTZreStnd0FqVlN0SE9jMXJKb3FLQjdxTFVaTEJCS2dhZXZsellnL1BscERUTi9LcVQ5S29Tby9VYy9vaQpTNFpwV2ZiNzE5UjlKMXpBakZTU0NxWUNoMkt0QmRUR1c2dXc3MUdlNVVzNlRHMGlreDVXU3FkVnRJTW1xdENNCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1dUUmFTcGNBUVhZNHdpZVpBbHMKOU9iL0FSK1VrWmplUmVBYmFhV29ocmRRVi92c0lJUjE5aWo2R0UzbmpNRFl2eWtzNW9tODdoVGY5N2FReGNHLwpJQmMwaCtYNGs0ZDVhSEQwREJiTU5QMWFYaTNBZXQ5ZjRLUTR4WjUwZDVPOGErdURGd25nSXpFUFJjTlBYYWxXCmlPamJRbm5YbzZDY2RCQ2FpMWFmMUtUenNhUjhXYmpxR3JBWndpbWRVMTBxMnQ1NUlkUGtEYnB3bnhEYkhwY3YKaDY3RDFQOFZ3TTQ4NHlYUDA3RVRBV28vMmd5WldCUjBIRDlRVk5uQy9Udms2cWN4RDFITWdIQlRUU1VMZldoLwp2blMyRHpnUmdzK1NFV1ZvMDB0TTRZRVBvU3hIT0dFUTBNT1FPTWhjZTZsb1J4RTRpTEllUXZ4SXVGRXZ4U0JFCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHZLM012VkwrZVJicTBsME1vL3oKa0xOalBDS0NrNTRUbWFiTHN6dHZuT3QxV096TzRsU3BrSGxkSWp4dkg4aWpRM2hqQTR3cUV5a3JyNThnZThreApsaEM1Ymo3K3pMckVkL3F3eTRXeW1iMEFTZDBrQmZ3QWZmWmNudVhJd1ltUWlQQy9tSGtQZmN5SFZBQkkzL3FiCnpyZnJYVlZvaEtoMlNkaVU1am9za1ZJc0VORndmS0piT0NwRFlmTlFSNlpzTDNYOW40cjk2VUtlZWFubzNRVUwKVjljWmZ4MTlJbU1hMzU4OVM1YWJtU0pjSXpFWFczR0x0cmVXRk9NSVYxQkJaUitkaE5iOCtxQnFpWGg5b21ISwpsNE5WbGp0Q09VYkR4YURJbXhuWWtkWnFyc0F4U2hZL1AxNjFnZkJwSVhyM3MrcmVJUXFielRhUHhmQ2svanRXCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGNIakExNkY1TytSMEJYYkdYUncKY2xIRHNSZlk2Mkp6c3dldlBLYng4T2o4VjFwR2pMUUJiWnRYVkRtN0NITDVIQjNiR2RXV243WCttRGExMlczTQoyNVNPaHRHTlJJRFpqcUM1TjFvK3VDVzJ0NExVb1JKOWw0YzZUdFpmcmNSK1E3VG1FaWVrSE1lZDl3cWRJQmZHCi9ZV1gydkVFWjVlL0lRNmFibkVvTlF3SDh0RzFrUEU1UGRTM3NXWVg5VTdXamZZYTFCbDAzRkJMb2RWOUZyT3oKNXpVYktxTkhVcThkVDJzdmNxbnlzUVNUdUJpangrQkcyTnkxNnl2TnV4RHpFWitucVVnL0szYlYyNHA5NkhYUQp3TmpiNUhZbktCYmh1VS9ZZTcyVFA5SHlNUUFhdGxiRHl6ay9qOXN0MEhzamxiVTdaMVBiQ1p1MG1xZWsrSWdjCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHVJZXBoVXhuTDZHTFI4ellOM2YKdWVIYWpSTlBYTzhxYm11aHhRdlRFbGZ1bndIUUZaWjR3V1NKc25URkt2RTZ1ZEZIMGRvWVFTOW0zYzRnSDRzTgpac1hteSt4UTFkUzVjUEtRR2ZpeUlKdlJhMnpCQXQ4aHI2VlFrbCt3dFZlZEs2d2FyYXlmUEZOazd2d3FhWnNDCmo4ajFFbEI3UjBWY0tsbUVLYjQreDlSS2RzRk9hb0hJSXB1RnJGUVZvaXV6alRic3N5bVhsRG5NaWdGVXc1Q1oKMUx4Ty8zODRMSnMvdmVqcjMrUkJwYjVWWUQ1VXVoS2J2aDl5OVRVek5NVWRSbnY2V1Vmbnd6UnRsL0pRKzZMZgp5bHp4aktkK0tOL1lXVTZOdS9qWDFhd2RBdVNGdDJMOVRyMzlVTTRtVXJSKytxQldjS211ZndXamhtQXV2RkhGCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUlyaDV1ODVFTkdSR1RIbGltckoKSE4weGtnekhsSEQ3aldVcktOSU9pODQ2cHJ5RU40dmZiL1ZWUlVsc1lHdTVRb1lEWDdRSHloMkVHSE5mRXhYNwpqcnpla1Z6aFM4TllZeloxMHNMdk9Ub0pWbXlybGpGVzRBYm10YWQ3MnMwb29pYUdQZm15b3I1b3Nwei9TU2JoCk84OEZINXQ4Zk9MTlRlNVE3MXZpZG1QRS8zNmhZWWppY3Z2ZXJRV2ZLYkF3aDIrMkx0Q1F5THgzNUt2Mm9UaHgKNDlsVFBmNUQya1Vhc1k5bVdwU1dBeWtnTDl1cTZubitlc25IaEkxK3VwQlA2RTBxdEpvZ0FRLzBQTitNazVJYQpIMjVTai9ZT0s1Ri9WSnl4MGtaS2kyUEtUT2VvT3libEtNZDdFU3lMMGVjVzVIdzhWQnBRaXlPTHlpZnYzRlE5CndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU9xSVRzTGVUeUg5RUp5YjJ1RXYKTzA4aTFiM1pnREEwem93NGljckFkcWNReTluK2Y3TnJMQ1VOSGluOEozaWNnYmZYeU5tQzU5eVhsZFBOL0FDSwovZ2RzV0dRZ0ZncWVHUG1sb09qQWN5aTI4Z3JoMVM3TW9JaWIzdEd2NlZpRnc3b3Boby9PMjZRby9mRUVsMEZiCjZ6bDR4RTFIbkR5WXA0QU9CRXFuWEhwSnlsVHpqRXo3LzVISzV1N2tnRDR6aUMvZjVwMEMxRTVqNGdhdHU4RXUKRmoxenlQeXNLSGpNYzYzRzFpSkhhTWliSG0wRTNTMVJOWmIrRlNEUW1rS0NncVVkVEJEdnV2dXpZRXlVaTJNMAorVHhGVVJoMi8xMG1Ic3Vkd3F1L29zM0RRWGNqMW9PZnBOSk5HRjJDZVFJS1VoRmd0OEdyQ2gxOFNUYUJBbGNICm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBempHaDlkd1FIYUJ0OC8vQnVnQSsKeVZ5dHZmQ2VJOHlaNWpyVjJYZGhDbUtZUEYvL3krRUhlWGo4S0J0VE0yWnNKZXQ2MjlBeXQwNVZkbXVOcTk4UApIc01IK1hRdVkrdEZWZVNMVW1FaFkxWEZpc2l4KzR1TjluUzhuWTRKUFg0cnZCVTZjalJOY1B5NmFxaFdtUWsyClFLSk5LSmVxQ24xTTRiQ3dkLzMvak9RU3NXWjh4bnc3c0ZJUVltVXplSktyUVlSRzVEcGRUdUdUVGlNWU1xUTQKdDQ4ZWxFdTdpZVNDSzZDSCtOczBBTXY3amYvRWdJSFgwcUdDd3B5ODdoY2dCSzNzWWlySUVDR2xpd0NRNFAzMApMUCtZQmxYdmpVMEFXd0UwdTNrMG1SMDM3K0ljNFArMFp3dnBrTGlncHRuRFE3Z081OXd1b05HeUNOa3dmcDFlCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnc5N3Q3R1dPdmFwT2svUXJwQk4KODNKV0ErSDVrMTVPcVg5NWY4SjNXRzJOLzNxdWtWUmNrV0p5TzlSZnNxY2ZGV0VmcmNvMjhRaTZSbHloUVkwdAphL3BBWFpxbzhldDJlRG9HZDJrZFNUaXAyeHJMSHROTDY1SkxGeE53VE5ZRHYzNjNGZ1lycEZ6Nm9PUHRsTjZ2ClJGS3RhVFNxUnQycUY4OGRiMitaaFN3dnlRZlQxdkxKU0kwVUhQNFpHOGlBcmZJdEJuTjhFdzBzNHRtLzlpaE8KRHlmNndPYVJLdFRUZ2JnSXgzb1kvR2ZKMTl0cmg2VmxRNkNJRTV5K0YveXVXYlNOemRIK3RhLzl6RVpxa01kcgp4MDZ4Mzk3UHNLeHVwMVlHZ0QyaVN6OWFodUI3alI5Y0dxL1pqd0pjWjA3U0xnNlQyWEsweklySmZrMnZRMDZDCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVZkai9sd2l1UUZ4WWNjVXpQa1gKNm5jSTFVODZvMUVadVVMQmRuRW9jaDcvQmowTzhJZUpOMm16dDNDajJEd280b2xNakNDcU40QTJWUldkMEhocgpNZ3ZabE91YW9wTk9DSE14YWNhNG53aERRdzVjQ1VqRTZuSzVuandCYUVZcmN0cU5SMzRsbVREK0ZwOFBmUzlVCitaSzlvcFBIQkQyVXNnYWQ1NXViS3l4MXZWdHh5Y2UveUNvZ20xVlpKWnB5QlRIODc3QU1WbkNKbWxnZmxJYkcKZEZ4a1cyTmk1cU0zM0pzcUNEd3pEWnFaT3hicHRJQkFZVWRJYXo4Lzl4RCtsNTMwMm1XWTc0SmxVTFVnRjF6dQpXeHZEcnFYYzdsZS92OUE5aHdTbzhhQkQ0VlR3cGVncDBmYkh4Vk5YSkJ2TmpHOHJVZWxXYjEvTDRKWHM4OTU5CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnlORHJhcFJPOVluSElpUmc0REsKelJvdWM1K0Z3WjNLZ3NDekN3WFJZV25zVmtySm5WL3I5Y2l3Z0VBZ3F5b3VsSEx4c2dnQTNDWno4SXU5S1JsMQpTOGduK0RsV3BnZHE2bExXQjhEQTBUMXNhOEYxL0ZOOGxzRnROL1FZNjU5UmFqbE9LUUwvVHEvdUd0VjNrU2I0CnhoT3RSV3ZNS3I5V0JPd1llcG0rTWozem12TVdETGtDeUJDWTdxYm1aR1dwSEhSK3FoV2pWbG8vcVJGTll1T2gKdU1BM09ZQmZHR0pDalJXTFN5UUY4Ulk2eVUvaFowZTVoMjRvMk1uYnhhMTAycE51ck1Kb3FBbDRFeHZrcUR5agpkNHVvZGtsdDBpN1l1YWNhOWFWd0dLZlllWEJ0TnVYWVZmRkVrN29nSGJYWG5XZGtuUVI1NU8rQjlMMk9iQlloCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXFQS3c4c09jRUsvSWpWYUhXKzAKUzZWL1VuSVh6dzhGbVdIOE56c1haSzZkbnY1OHE4bEVWVVVpVjNCcDlOTllTbDdaQ2FBSUFDMmkreUpnT0k3cQpPNTRwMU9TWEloQUNnNkF1ZDIweEpHZ0srVCtGRm93cHdhTHRlTEprY2x5dkFOK0piVTNqeFZUak4xNmFiU21qCnRKNzFncmNoUW45T1A4bm9yQTdyREhqQmUycmxKQnN0RU93SXdaR0NlUHE5Z1FGaWRhM0NnL0JNT2RUM1FZVlkKWTc2dHRma2prZTM0Q2lvY3A0TEFHRTBQUjhaQUtra0FIamN0RWthOVdkMHA2dmZkUnRGaEpyMXNETVNQcGxsQgpXdVVmeFliZ0phUWpiVkZ0akl3MExiMUkrYjFLQ2lKT1pyaDlqYzI4cStEcHZyZXlqZEpLb0x5Z08wd1k5dmdJCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemFreHBZalB5ZnNzQXJuUUlQcmoKcjNoeWkrMTkwWHFyL0M2RHBId0tpNFkyL01mSzd1SVdFSFU5bVRSU1JiK2crQlpudVlwenkrUHluU0NjQ3EyUQplWmNqOHY5U2ZQMnpvWkRCemtkd2ZqbUxoN0pzUFEyMGUxdHZqUUMzZjZydnZPWTE1TlUyc3prRXpRU2R6MnhrCnp3SHE5RUtyVWMvZDR0cGI5RzJlenRmOGs3Y0FqMHorbThUaWdzQVJnanM3VUdYS1pIYjJVYzRoaGg0YUZOTE8KWDZFUlBtMlp6eVhXUXkrT1AwTkhGR3UzZ0U4eFQzclV1Qmt0aGwwc1dmRHpab1RBQ0JrN2hha21RelpNWFlCNwpla3d6MjZ3VmlkckFEUjlVK3dYd2tvZndmd051TU4rVkVFZjVnTDhaTmYyTDNRb1pHaWpmYm4zV21kOEFQS3dRCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGtGUjhiZ0F0RDRjakNYZlZlU24KeGlCZ2NiYzNZUzB0cjgzK2tqNE1zZkp1TkNWeUJzUU5wNEhMbjEzdHBYVWI5akZlVlB1dnQ1RTBZOWtWQ2x5MQpOZFBBS0g3OFdwZ2VqeCswMWFmNWd3bDRXVnJZWEplWTRqZlJhM2NPdExPb3U3WktTYzVSN2xSR2pnV2hUZ2o0CmlManhmbFU1SzcxZUFJbDV4OGZWTmROU1owaUoycmExc08rem1pOExWY0lsWnBIVzNZTmRBcm1QclRxTDRZRGwKS1dLT2FqVWtjM2ljTW5ZRzk3SGsxWFJtT1ZjWFFMa0M3UXJiRHdOZGhFTFFadXQ2Um13VFNnUWhNbXV5VVJEQgpGNmJxT3BJdmpiejErN21CN0hscmJrTEFxYkNlNlNGZExTaDYyWmxHUUdBL3ZmRGxvcXlNalk5dEF3L2g5eUV3Cm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnJFTUlMZXlSTjBXUjhXT2EyQkUKc1cxV3JEYzFBQW12NjROWXZMdXhSVEV4RjdDM21EWlp1bXNQcFFLL0hWMlNncW4rTWd3YXRlNTNGQm03NkVwUApaWjRJeW50VXhPRlJWU0h1U1k1UXljbi9kZ2djSWJmU21uVTNGYUo5Zm1mU0FMYjA2cmp5bTc0b0tQZ1Q3SXJVCmpmNUN5bnZqYms5bVZ0cFFlTzFKbjBLTmxVOEdMUnNvZnlOS1I3TTZYcndicmhOYmszQ0xVMkxHOG5acEF6WGgKSTVoSnNQbnE3cTV5OU9wbWttcXVGWGlYeVhaR1NBSGJMTytiN0p3VVVpdEcwRkZyakp4cERwN3hLOFBDRVJJTgpOT3FFenV1Vi8yV1F5eXp3bzRrUGVrWjBiNUxNL2RIOWo5ZWZ0M3FOR0s3RmdoN2EvRUwwQktXNS9OWkU4OHJWCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFJZQ2NQeUhHaWVWM21oVlZMSFEKY2hnOHEvOGVCL1BKcWdVZ2E3NWEvNVdEejBodVowNlE5K2lBMW9ZSUVTbU5Nc3NyaS85NXZpaHl4bmQzMWlhMAo4VDBUT0NrcHBUcmtRMmdhWU1NZzYyY3BEQ1U3QzFkdFI1b3N2cmNPL0trbEthaFo5R2tRTFFiVGNlYi9XTGJDClBrREtiS3h6QWRXNWZaT1N4VzNwR1lERjg4M1ZHTG1XQVNBdkt2bWF1VS9UUzBEZk5XVjNSWVMzSldwWEFhbVoKU1hkMUg2d3RkSU5rRHZZVSthQW5kQk9oQXd3QzZyZ095a2oxMVEvQVQ5TVVmcDhNVHJBWWNpUGN1VmhuOW5GTgowSU9HckxzaHNtenR0bVhiWDZMcklqbkJHZUFUa3NTTUxMY3NGUkd1eTA0S0NZUVRiS3RhbEpIRVg4QllvakluCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG5CMHQzRTJ0bGxiVmd0MnBCS2kKa1RmY2JrNHd4MXM0SkozQURtYXY4MXhGNXFCTUdJdnhLcVZEMFl1eGdkSnZUYlFGcDdFWHVydUlKMnl2WlBydgpxK2dyV0tOQjFTS05WWi9VMGhYbERSV28xNHJsY3Y3Q042UjYzTkUwT2xyY0tBZGpYMy9pejZHZ0N4bElpaEh3CjZjWjRxSFUxL3VZWjZzMnZEMEFKWlNLRHZXU0N5ZDJ1ZjA1S052Wnpjb3VTYXJ5UktpbmdCRDdkSkdCSm9hcUYKaWhoYTlYekNrblRSNzV4eXJUWUcyak8zVUJUbGdxUlR5ZHFKN2Zxd1RTRitITkg0aGs0aXNTYTNUZFk0eCtmNQpRYVZoczZkS3NFQmJ5Y0lmamRSejJiWVEzbGxNTHdIRE9Qc2ZrMUplVTNFZFZzem9IdnRUSHhWcExUUXo2dCtECk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHgvandjMGNwL0s3ZEd6eTNUUkMKSXZ3c1ExVEhJY0RPQytPZjVYc2tOWGcyY3hGSm5wRGRiWHEveWNaUzlYQ0NYUURxVTc3blVUWEFMaHRKaDRWTgpmZUwxTHdVMENQUkhudjRsQkVKbjNDMFlmTjlyVSsvWVlDRDFvZStIVlBKcFFxcUNMUCtlRG9HYnIyUFMzMWtNCllXVUtoeElPNHYvQktpWE1mSWc4cU9FeHYxdGs1VjFNaVEyNm1hbGdNMm9JUTBoR0pIN0pCY2cwYUNuU3VNcUMKZUJHOEhUOWUyNVZlTVFXS0ZJTDlNWVdlT3ZqN09YNjYwZDVlbnZJZkRMdzNGQ3l5YWNZRTBaUTNmSmlKWEhCegozYUJwaXJOaER0STdKNXhTWVdHSEt4U1VYeDJ4K0JnSXBWRzZvRnhkdHRIT3RCRHNNT0I1QzFSdzJQUWdCSDVqCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnliL3BtUkFnOGZPMitQUjd2eEsKSitLVHA1dEx6VndQa2NZa1VyQmdnamRmTlQ4a1A1dkZLVm50a3c2czZyU2JybDUvbFNDcGtneXVtemk0ajl6ZgpnVWlwS0Q2eHB3U05NaG5ySjNka3B2REwvODY4K3MvTlB4cG13ZTNHMXB1VldXSmt0VXllNkdRTFZZckU1MWlFCmFqejN6MDlFUE1tckZRQXlCZmgzZXFVU05KVW1IYk05WWdJVGNCRGdZem10UnZ3VEF2S0E3ekRoeFY2UEtyQWcKVWtWazd5K2FoUEJwdXRaVHp1dE53WHhrZWhzdVk4QkJuR1g0WVVHWUVYR0JDTnhsNTFwbjJDaldaU01rS21iWgpVc2RYUXJGWFY1dC9JNlhHT2tmS3dqUUlkb2s1WEN2eU9kQjJJRnZtYllzemVmc3ZDeVVUZGg1YzhQT0FVT3BnCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDFVRnV5VzFJbzBXVFBKbjN5aFoKR2RVYmY2TXR5SFJ1TVV4VjYvWjZtZVBZdFNCdXp1M3QxWGhJaFVwRU9hSm5SSHJMZ0lKckdQU2tTWHJrTHBTLwpYSi9qbmxhaWNlVFBhM3lqUzUrS1ljeDBNditXOE5aQ2pUNDQwNy9UVVZKMUlFcnhHVXNlMnArSTBPd3c2T2s5CldFeDduOXRIaGJ6TEg5RmtERk5rTXczUHJ4QkZPV05kZ09DcmFlZmRQUWZ6TVRwYzIwUnNFS0J3cnI2RGpyTTgKZUxMNUJSOUJmaEFyemMzQXU1UlBQSHdBMURaSWZzc3FaemlkeGxBYXZGa21sOE5JVHVOUVZBUE1yelNIYkV5VQpxN0Mva0xQZHlkbUNXdlNpZVpQaXREd2xEUWw3OTBBblF5VHBQelVZc3FBUFllQngxZWJTNlpHV3ZzcWVWQXRJCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXhNeWdqRlVEQ01DNHczOFdKZlMKb0d2UnczVXRFQlg0SHpGbGd3NHBuLzlqbGZ6emh6Nk5xSVNyais5bVA2S1RGV2M4djg4Z1NVcHBjZW05c0VVcgp2THJlY2NaenJRN2ROWDA0VG1pbm84a2dOUy9VbXkvM2pDRXNqbmc2RnFuRVB5alBVWTQvQTlqMEpXTUhCT3BXCmhGR2I2eTA2dEthUVRjYk1jMi9CVHRzcFZwU2RDdW5PN3FUY2JFM09aODc1Nmk4VFk0UUROQ1V5ZzkwRVBJcFEKRTYrbU16YklNS2xEUlcyYUxLZmZMMEIramZ2VEVEV1RiU3hnMmlJdy8yZTFzL3dMMDQwZFVmSXJPZ3ZlNUhZVQo0cExmUmZhWlNNT2RDQU5vL1NEYVVIeWlZTWlTdUo1Z01hcEVldG1WN0t2MklxdTdFaGhka285bmt3K2Yva05tClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUt2Y01BRHNsSVhQdytBVTVwT2YKTjJjdmNDU09DYmNnYlRaVFZjZ0d6aERXV3d3VmxyN3h2Z0lOWXNlWnBrLzh0ZlFPdGlpb3FVMjlEcTR5N0RETwp2eFJyZ0NxSlgrK2dNNzZxNnpvVTZLZnNLVytQUDZwLzdiRFo4Qk1FdHN2MW9iVlZIR01CV0FWT1dhZU9BbGRVCk5Bc0dvcUI3RE1mZEErdncvMzlGc3BQWDdhcFFTRmN1aXgyYkhlL3kvVjgwME1Lb2ExRW1oSGVhcXlOUDB3dDYKZ2UrU0lpbkxyTUZVMzdrMUxvdGYralJLN3NTQkFDLy95SzI1WVA3MUhiUHFzMldkYmh4Tnp2dlpEY0JGODNEUwpnZ3d1aURFQWZYWnRhSEtTTUtacC85VnZxUHVCYXF2T3V6bnRWOFd2aVJtUnVkbksxNnBYSDlHUVlBSEdPSnd0Ckp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGZneDhPaWg1OFErZkZPZU5BTWQKbjlpZ1hwWERjNWVVZkVqdlNUREJkbjN6THk5VXltT3ltdkxPWHZPV3MrZ3BLQW5LZkJGdzNSNVdyZ3dkcnlPVQo3cVBHV0NhQXA5WllUZ1g0d09tT015aFNsTnNrdVQzZU9Wcm1uNlJMc25ISmtRYy9SS3hjSCtTdjJidVVSTWp2CmgvTE9hT3lMWEhnckRSQ0dPYzl6bE5TVHBSUlJ1bGdmeDVyTlVjZzdERVZTek9yUlJ6MVp4eTZZc1NOeWJVY28KdWJ5ZVE1VlpNWTNyNkNWbnVQYWlDdGRMdENyZm11bFRqcC9nN1hrRlZIaUJlcXZZYWc3TmlkQXhNUjNheGxlSAp4VStKeEY3YlFhRGdKclQ3UjcrTHh2VXBxdk04Z0xjd0x5aW1NWXd2cGtPT2s0cTZpb0wvMUVZTWJITXNWUXRvCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTR1bG1DN1B6dzNIMjhXUm40N3QKUzVuTThiTVkrSnBpblhCY1RrdmU0UEovNmtKVFZsZ3RrU3VxNEZtd2ttTXZhZGd0S3FISUVjOVVoU0tOSE1MWgp6dzJuYWV2cVp5U2wvUTZPUHB4YTBybnVpWC9EaWg3b0p2NUhGaVNmaE0rK1duL1FiUFc0VXppTjVSd0pXRXozCnd2RjM3bXpOZEZUTUk4WFQ2NjhRTVVlbTdYQTZQNDlrdEk0RC92Q2cxUGp6N2doYjlHL1k0QWFnQm5qVnR5djMKajVmY1VVMkxEM0dEZEloR0N4ZXZsdVd5QTBmLzV4aE5oT1hmeDlLeFUwdkQzTkFCY2JDMFlRc25OUTJ5bC9NVwp3WTVHYjRQU2NRNm1YU2FDcjZ3L1d5VXdoRXFPa2JUVE81YWwxU0MrbEVWc2ZlQ1BDMUhvemtCVFlrdUl5UHU1CnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWZmNDFsaWoyRTRBOVhDcG81NEMKclM2NWVFVkt1S05TSEsxU1JBVE5MUitlSGlHT3Y5U0V5R0VuZGVzQ3J6YUlqN1hKOXdqUUZKMXhMY3dMSTJmbQoySHVjc0FwNXQzaFMvZlZBY1l6NlNJYVdtZytCUnJFZ3NYUzd4RVZtTnJ4eHRRTFBwWlpoVG9ZdWdJUmROYVBrCkp0NXdIOWlrQmNqTDVyTUlzZzdEaDNLWlN2WVJ0dW82S0lCdGZDZkpFREFKcEY1bis1SDNVbTBVbEZGVGhrUngKTVd4eUYrc2M2LzUrQ3cxOTljVnZuMzlVTjJzc3VFbWFBb0pIbmFONi9NT1JITnZrdHNIRW1LM1RnRnUzdVE2Swo4N2tQbGhNbjJaRVp1eXVaR3pKaFE3VWZmSUY4UHgzbFkwTkhJVHdNY2IzWXMvRDhvK3Q5QWlvOS9BakRlbGdXCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2JYV216RFRPTTR1MjVOa0UrRDcKazhFMEVwMGRmUERCT1lrRy81ZE1yeFFIMk1zcFpBc0JCU2srSjhHaFF0d3puWWozbTNUV1NEbVNmUllzM0k1KwpwRlNBQTV0d09qTlYxejBmREtnSFZPbHJ1ZXk4UmdZU3RwdDZOZHhPdmZoNXRYMjJjMDBCQmRIbnhwVmI4YWZSCmFYYVMxV3NIUzRVeUsyQkMrMGRqS3lOeFR5L2lCQitSd2tZVFpvbDR1UFF6M3o3T1BGSlJtUy9mNlhmU3ZMTUEKTGRmME9aNkcrVjNJcktMbk5WeXhmOFhJSzRmRjB4VytuWWdGQXZLUStHdjJYZnprMFFhd0labFo0YkUwNGlrZwprWDgyY3FVb0RHY0xmYVBsS09GMTBFNFVBcmhoVWg5b054ekI2bVZFMEJXTWd2dWFORHdTdkYvdUg0elRCV1lWCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3lqV0ZpdU9CYWsxKzdlcFQ0WEUKN2IzNFNrY0hCdjVUK3FSWFVJU2F3MWpNNkxlNlhVVFBxZC9WclUvVklwSGJaZHkrVktZMHptSE1jWXBKYVRTWgpVSG5HQUd2V2FCc3dzSHhZOGtJR05QWGNWNm5UZlZ1eG5DaUZIVWtiSXVTSlR5VU5KNkZSeUd6WHZPZHhLSFFsCmtJYURhajZJdkVudy80WmdjZnMvQUpvLzNXVjdYMkhXa3lBTStJcWd2NHArbHZaWG5rYXhrbEhwRE9jb0RDaVoKVCtIaGxwTjFnUVNqdGZiWXkwaVNGYW5KeHRjckIxODJLTEdrTG8zRlcxQU1VTzdyYzh1UzlqbFp2NUQ4Nnk3cwpNSUlsZ0pPaklWR1Z0dEpmc3BybXJGOGpQLzlUeUFWcmVFNDhVSmhVd0VYNkVqc1YrTTQyakxQbkI3LzloaU5yCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0xnUzNpNHlGd3BzUTNkRWxXT2cKcjR3NThoKytQVGdkRzRXcWNIY3dTNVQxRTZ1cmxhamc1bGlBQUZCVjJXVjdSWnlrcU1vcTFBRTNsdGtFY3JUeQpsS2VSWERZemxqN0RLbHY0TlJzd3JYS29tQ3V2eW1sTG9OS25BZHkrL3RZdkZIQTRkNzQ5eWF5Um0ycmRTdzdaCng0c3krSnFLYTRhTUJRNlFFOUFmSW02ZjdBYmxObC9iS2tTS2dudGxHOU9jRHNJaXVzcWk4Z3FHaE1xNWlVUkEKRlZtS0FCVWpqeHlYeTlnd0pvNlY2ZmFFRW5hOFdVR28ycXdwMmNLNzJKaWdlV1A2RitBOXJxQkoweVpNS2ZaMQoyNHVqVnlVeUtWekg1K1Q5THViNXkxTjNCV3R4V3pwb3VqVEw4T3R1OEEzWGMvUS9nUG54aDlqZ2FuM281cUdWCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclVFdXNIS3BLRWFveTlROHR4eWIKL1NqeDVwSnpCZHlNZW40QmZKNDZid21QSVBqRUNDdzl6OHBKbjJJd2FxVzZqaVRsNzlvdk9pMFRpOWNsOUJ3cgpDVm14YUZGSHIwa1l4SzlKVm44cGt2ZkNPQUNQd1IwMStERUwrV2JMcUVSd3FZakxNOXFsZlRpOXNtVFJmK1FwCk9vdWtKaHRWZiszc0h2UFp3bXMvSmY2TzFzNzR0dVhPRzdLLzdoSDNMV09lMTU5ZDJOay84ajNoL3Bud2s5NisKVld3K0REUXJXN0JFUEtXdlhwb3JGd0Q3WW9tVWZJdnI0UTIwMEptR1dFV2haNG5rUzJ5TmNXa2NSMy9TczNIeQpwVkZtOG90aWcrQUV1RVE0SVorbUFWQzBEWk1HdDZRa3N6cFVTS0l6U1V6QjMyRWdMc2xmT3FRcitrMmtmNzQrCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem51L1U1MCt2N3FkSlYwa2VXNFMKbVEramdnOTFTQlZlNm5UUmFOQVRkdDh0bUw1VjcyRXRQZllHb05IQVJodUhkU0FYUDhHcXdqWGpBRWpEL2xILwprT2FVN2hNNDVzY2FaQytSTS9LYVUyVHNoaGRaalFNYmRSL29LQUMrR0Q4TDBrZjZ0clJsZXp3Qjg2NVJEYnRoCkV3ZlozdEZaUGxLMUtmTExOaTZIcjQ1WW1PQTREcG1EdHNxL1Z6WlRtRWE5YjNPSk83RzNpRUdjMFEvUFZHVzkKYUdndWRYSE9TV1grMUFTTVZlelY0UUo2TTg1RWRuakIxM2htMnlwSkM3NnlDSW9Idmg2TkZMRGN1WVVPMXArcwpobzNxaWdwNHkvaEwvLzJVb1lHNXdnR3JuWEFBZFhvNVBTbDZZOHZZQnU2L1k4bDlJSnpqb2dnbkRvQmJWampFClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcllXTUFDWHJYRTdRY0lMZmVVdVMKc3dCK0Fkd3duNDdVb2RZN1A3MVB4d3grSWw1RHhmUWxNQUZRRUtRNXduczlDVUNvZDZ6MU1obDROL3ZPOUNHMAozemtiVzNLYzFPWFlRekovRGRkcXhwOUw4Qm9zcC9BOThZY2ZZdVlCSVdiTnIxbG1sYXJXQm9MMUhoc0h6dXNoCi9DaEVUQXUvcXROQ2d3RGc4YWhLa05rbzNibDh4T0FHbnhCN1YwNWlOdWVOOEp2bHltaWZiV25VcWNpSFJvZDgKN0F1Z0pON2dCNE1SZU9BMi9LVXRDUitIanRnMjZOUWlwTzRNN3RZTVE1aU9ZeXJxdVFQa2tEVEt5QjcwZG5GYwo4VjM4SER3SVYxQ2lNRVFuU3Q1enIrR1pLQXBPVDN1cGVhNlJDMVQ2bXNuQ2RhbmVTaTBWL1lJbGlhdS9tWDVXCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclkrK0l5bk1HTEZGNUdORDZPZEkKUWd6TVJDU2psaFlQRkFCc0tqUW0ybXBDVGV4T3FkSXE1aFdWaFpuaGx6aVpkQ0lDS3kvSUZTK1U0Y3pDQkpVLwpWV1c2TEYwMHZXck5ONGRmVU5USEdjaEV4K1VqaUtVVXRoS2crVlVHUTNORW92bjNkVTlYNDl6UnBXcTZVT0pCCkhDWVZrditLOUFPOEdwemk2eDYxU2tQVklGZFIwWXlYb0I4VU4rN2NCQ3YyVVdVZjBiUyszZm5uT1RpdC9IUkIKazZQRFpzNDBGNlpvcG9ibzJ1YVBmd3E1R1lZTHUxalZQOHZ5M1RQVDF6NFVJeXVacWFJWGU3UTlWSVlUWGwydApRVTFmQW1uYmlQRksyUDc5bDhUUmJRbzQrQjJYT01ERGxxOGtLN05qTXQ3U2RlRmFoMjVCYzM4NTJoUnBkTnBZCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejBZZDlqekJpNFE5eHJnWm1ldlgKMi9ZQUszUVJsREQ3aTVvZ0U2N2ZzejJ2YWlLOXNIbm9qMWlISlJiWWM2dEd0eHhock5LakxmMU1lUjVlb1BPdQpObWJ5QWc4U3ppNzR1Ukp3QzcxRC9QcWE2eTY4c1hLTFY4OE83cXdGczRZdko2RDcwY3ZTL2ZkT0lTekppU0Z4CitYZmRKS2s5N2hzUlViRjdocnJZaitSdEdMd2ZWZm9KN2FDMGVVN1RBYVlzakc2TnN4eFNielRLTDQ1MlIxN2IKYkNGbEFHZ3g0V3BPdW4yaGpVVDJPeU14Ty84ZnNJZDdLYUVoK2R2empaZGRuMWU4VFJjTURDSG04T2pWRGtFRgp5ZGRLcnBlcTlSUHM4QURQMm5JMG1WSEdHamJEMm5tY1dsWStkaTdjTkFaRW8wc3FPOERxbEMrTGdMVFNDVnRkCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0VzMWZqMDBKNHFZTEViZjFwVGcKNEpqWkxOQzRuS01XZ1hmbWpVbHFqSmRmNmZYQm1QNjUzOUZ1QkJvOUhSam1xKzVqajNYbS84cWlkcW5mNElWaAprcDBEcHJzbHNPKytJanNaTThyTHFpRGFaN3VjN1VCTW9xd1JWZWN4R2VrcUlKcloyQnBNNTZNa3FDVllqZ1hKCnpoQ25SY0g4OVdjbnJ2NUtqcWYvN2VHYml1TFJGTEw2MnJoUks3SGh1bUZHNkg2c0Q2UkVXMTV4WHlncVRMUEMKWDZrUGVDWE43MHdnRWNxYTQ0S0lMcE9mT2ZmNWNJTVdVc0JHdVlOdkoraFIzSm1ENkhhN2xaa2tqNW4zbjVYeAoxQ05OaVhobEtRZm5CempFYTJRU3BhSkRiVHZ6TVJ4eGd4Nm40amJjVXlRakxoQytReDBXUitkQ0NzTlZ2elhLCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk5PU1paSkVNbFZqVVhzM1BQSFAKT1dlczJMWjNJbmx4enZkOHYxUXpsNVdhMHViYXRDcnY0OUFxUGNiSTdOMkUrTUlNVEdwcmlrNDhhMUZmMzQ2UwphbUNjSk84dzd3aDExZE04Z2E0bGo2UXFPdk1BbVo2VWRxVCtXdDRXN1VsTC9BcEh6TFlGS29mL29jb1lXTVhVCnpRTGlwZTBlQ1lzSXdNUytsQ01WNzRWM05waWdCR2loNllpMGZlNjVsTzNicG00bWltNkk4TG1rT2VhbkJTYjQKbmFDbzd2QVVORWM1OElaalZUamZ3WjE2YUlmNldIVWNEZ2N6SmRXd1lWVW1FQ3ZJMW5rcXBEc2xOaWRLbUlsYwo0UTRWdGVUSTF0eGcrK3FlWlZORTR6dXdZSmZjbmJUN1NEZEU4RW5WMUxCM2VOYWNNVmVPcUV6dThYUjJjZERZCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1EwSGdXTGlURUg0ZFVzOGtmMG0KNCt4YlcyZ2pMNEpTY1NMTGR3UHMxKzc1dW5uK25ObXNkUVRBaWFrb3YxcVNRd1IwcG5GQUdqOW5UU1pHRkRGSgp2ckRmK1NyZHhGQmc1ZEhvK2hPSXZPVFBwKy8vRjltN1c1aVFGNm41ODVEL3F6S2xGQXQyVDY1U2gwOUNkY3NJCkZwSm5td2pIYTlyRk1CZnFqcmdHNFpBN09ON0tiQitQcnRHUTF3c2RRa2tVTWt5K2R2d295OExnVldGUTVNWTAKeFFONXhmOS9oSXBSU1k3cmlJWThsQjdxTFVzUVh6VVozdnFHRlVIUXZsZXk0cncxUi9CYTBiVnhDa3N1TUtBUgpIM3kyeE1VN3BDMWhySFg5ZGtMc0hBdkJtZWJwTXFhb0MyaDVBU05iV3d6RVIydE43NU1yMlExenV1R0RlRmNXCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMW9JYmNDR0luUGtoQ1JxWEsvRjYKNjRYYkJGcmRheXRkWHRCMGQyNTBnUjF4am5BcVBwbEpLQXVBOG1RNjZqbXlQL1pRc0RGU3l3dVdCWEdLUjZZVApyUjY4cXpsZko3Mmgyc2s0azVzY2E0dGo3UmdQYXJwNDlDSC9XdnlrMXd0eDYrNlNuS1o4N2pVUENCdVhRSXlUCk5oRmoweStoNzQ5NVp3VStzSytlUTMwV1hLazc3dnBQVThvRENFaWhza2Q1NC9OclZCbDhIY0FjV0lpNDV0OGYKamZneS93MDFGZU1idjR5VGd5K0RFaXNndjRSbnBYTk5Fa2NIUlYvV2RCTk16WFNVSFM0WFNnYkxRY1RXdTZVYQpXYU9KNmJBSmIwUmU1M1dhdWVuenJPcUhUL0NpTnpXN2NBSThDNlNOVlBtVWFjaXAxRmcwTGp6R1J3aE12Tm1PCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1d4eDI0bGFUdDNtQnU1WENScmcKaEg0SkN2SXBtU2JGdW9lanFSM1I3dnZhenQweVdWODVNeHJkOWdOM1pHUkduclhtdXg0eXNOc1JiU2tGcklNUgo4bE1TeVJ5VjVJTEZ6VnM4U04xNkZwcWJmWThvNXV3Z0ovdEtNYlNGVlpFZGlwRGNTRkJ3VnFiR2pOSjg1RFd2CjlWRmZxbXZPMG83V2VFNmNKbXUxMlp5eDdqMSt6MmtuN1pyQmI0c1VEdlB6ay9EelZSSUVEU2VCbFZCRnR5cTAKQThIYzhpTXJxS3lPMHRiMnFBYnExZGlGdGV3TzQzM0l2UGxlcTl3WEoyNzlRTENTWWVqdzRGL0pZMjZHN1VERgp2TTVhVys4MDR2THJUVHFNQTNMSW5oY0pMaFBURjluVmVGSGxKbExxWWNpT2xBTHVTSFR6aUYxQWIveWEzanl4Cit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEl6UWQ4YkY0VUFCWTdOR0lMOXAKSnF4RitCbW9Ic2xoTHdCSTZ2Y1FRKzVRdmhzMU56NnE2bXZsNENab1lRWmwvVVdOV0tGbGpqR0VONHJVd053ZQpMRU05dGxONGJ1angzSC9PdGJ0bk8wa3dmc1FleVJnWWVNSmNDMkx0Tkh3SS9PZ1dFUFpVZFJ3RXJBUWVDa2ZjCitNM29VVTF5TlVxWWhPSUtwOEhlQnBCZ0RVN0lJRFZzWnNHQTl6eVpPczZ3dGFpK3JnVndGeC9LcldmaEE4TnIKZGEyeVNFS2didXNmT28vbk5LRXNoV1MrVmJTd01yMEVqWVpBR2hwUXFMY3pabmJWSHk4V2cxenVzTUxTRjVqawpOaTBLSVJVV2IzOGQ3cG1sa09EVEwzRnplejhDc0FWek9NR05xdXpNTTJ1WnRhekZZRUMyUENzN3hQRk93VTlMClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFJSYlZvQkNqNG9GcFM2ZDBZSEgKT09EZ3pYcnZrTmIyOHlXUXlwRDdubDJuQzNCazFmWEpkTTJ0YTRhVlgyTlJYdVFXbDVKZXVjbi9WSitBTkVCTgo1V2ZEY2xVbklFalJzdEY0TFhWU0xiQ3dlTDF3RWtQeEpEMEJmQ2poZjNpNS94Y2VVdXYxQUljS3ljNDNtZ0dWCjBkNmNOV3N5VU1jRThMS1ZXMkh4czBaakw4SkNzRnVnSG1KeGpEWDdSdVI5MmpCUjNVWFQzSFJKdCtlb0dKc1QKSUtzYkwvYWczMVhOU3lpWGRGTSs3ZlpiWThuSUxmei9IWHFiWlVJM1daWjY3N2dzUXpITVBoNEhNclNwczVWNApGZjhhKzFwR0VjQjNKb1NrcWZaMEp1L0pVSUlNby9LWWR3QTEvaXloanJOS293YmJ0Q2pVOHc2ZTZOU1B3N2g3Cmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnFGeTZ3LzBsbkVGUDNOeWtxaTcKekpkYzg4REZvSU5PQUxaRi96ejNlME8xd1V2aTRyb3NqYnltUUUyTzhxa0xoSU5SN0NRWkd6RDlpY2hGVlJrRQpDVGdiN2EvT0FoZ1RoQ0o5V3VuckF3NFhHcUlCdzlCOFVjb25PU1UyNGcwb3h0cUZBaG8rQlZkcFRCYTBsclNoCnJJQlFGekJwb0xXc2FwcTlUa2FCWWlKeGtNSE1JMGhXQ1dNWlVlU3lXKzZ5NUlsWk1HTmRFZE9SczFnajdpUUgKbWQvb2EvUmtKQ1hvV0tiOGFNMzQwaU8yZW85U2o0bm1MR2I1N2dYUExLSm1jb1NGUDBNUWNuRlpjc2Z5c1RRUQo0QzlPZ0RsbHk5amRldlJGc0Q0ZHlxcFR1QWgrZXA4QS92cHVnNkgzdGpJbHZBaWM5a2xxZFdBUms4amNualBBCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVdsM3AzR2psM0RzL3NON2xRaEIKbEhJVGZFSEloVitrY2YvazNOcGU3TG9wZ0l0Q21rcDl2UDNOVVJXbGt1aW5GZk5iK0xJdHRXVFdjMHlwRmY1QwpWdklCMmU4RGxWd0lwSHduMFVoMnNQdjZQQUwzMzlTRVZrRm1yNHovcWltZUt6ZktJR3lMbDkwV3IwSUZPeUlkCittc3RoL3p5YzhLbi9tWjJIbFNJS0ZMMmVjYlU5Vk1IWm05T2hCaEhJeTN6YWxYSHlTcVhpTzFPb3kyNURRNG8KVGJZQ3k4WlJ5eWtQbnRac3llanUrUUxoZE1nWHFZR0ZDbEVCVE9zV29SQkZwbW9SVnNnQWl3Nm9JOXo3aktVZgpHbTgwUlg4M1F0Nlk2Q2VWTmFlRkxTVWplZ0lOVFhpYlB6SXVPSDY1UVViaWZYcGloMXZpZmJNZGp5NHp1THB4Ckd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGsxdG9OVGRGQ0dlWDBDZURQNmoKZEY4U1NUdHFpaUNyckIrNk5rMWlveDdTV3AzUTFlQTEzN2NNdU5kc2hZblhiZU14WDBsaStOTVpmQ29DbGdFSApVcUJpeHpFQkJJS2NaRzYycE9OUnRweUI4MlNQbDE1VXBKdTltWWFXeXZsdFhSZjRxdUhCRlBWTEl5ckF5V1NiCmM4Z2hEd3BwNXdhTStDeVlUVUdpV2hKV3pzRHY4QjkwdlhlcGdBaDFtbE1LQ3N6TXlhMVlLYmtSZXMrWmZadVEKbUR5aEtYY2VSM091MW83ZnBkYWg2aTVwY2ZMUXJoNC9BQnFGc2xkRk9CQTJ0SjZjQk5EYjdERDc2OXZBb2VtZApWYnJrandTR1dqaWVWL01aVFVheVIzOEFXeUdlZWg3VUR2dTZLL1hWeXQ2WW81M3NzWFhvODh1RlVnbDQzQjFjCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0RpMDdvRXRxdVFOYkVZV0VHNm8Ka1JDc0w3a2F1ZUVubGhpSWRMSkVuY1B3SWFydUE0bnlUZXBQQTNpalV1U0lzQU5YT2xpdkhZcjUrN21rS0wyUAp0cTFkd2dIL0JMb2Izc1dwMExEUFhNenh0eUtiek56RFg0NHJUMDVnS0wzYVRoa3JxTlhGNWN6MHFxMHNtbXZRCnlGaFd0cW1tMDRGcm1XYzZ6L2pMVjhwRHZ2Tkg2amcramdrcU1jMlVTSEdubzQ5VEh4cG5mUVhqVnRFYm5sMW8KTFA1bkl2Y2t3QUdiZGlra3dMS0dSSk1lbGpxWERnelpkejYrODNycVBsei90SXRYWFpjUU1yWC95Syt5cGZsOQo3ZHNObUVlaDdYMVRLblNYOGZOcFkwSnFSMlc1M2srT3EvbDNhSTlMOGM3TnBsL01pMjA3VXA0bFJDRFlxbGNhCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelBMdEtUY0JWMCsyaS9XL2ZUanIKRmNPYTlmUG9WVDFqK1pKbWtva2k5WGdlVmxheTRXOVRJUGwzamVjLzlaV0dYaDlmdE9DVGdtbWIwYkdBbUl3YgpzSzZRLzBkalgxSzJQQzBJZHdKcFBkd084czhvaWphYlo5UDJRaklKU1ZXbXkyWER0bVVJM2MxbW1ZSzh0ZjVlCjdKMC9USVdOTXhvczdueEx2WmQ1b0pCWFRpK1htVTB3Wi9qZEtmamxDWU8xTDZ6WGpoYlRrRnhuTk1uT0Q3QUgKZXZUU0RQa2xTV0xxUHRnOEdCbUN2V0VEQytNbm1VaVdwR0NiMEdDWTR0aXdaQWIvZ1J3RG0xeHZUZUJneTgyRApHR2NVTTdNL3o5MXdwajE2azY0Zk8wOC9peWVOWHpYbklhNjhRUlZYZ3NRYVFieXJlNUVlYmdtMThIeHhOdXJqCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWVXSi85NGlzcnZZRTZ1WUk5U0gKRElQVUh4Y2ZaT28wRk1LRFJkeU9UTjdDK1h4ODk2SEtidVFIemtOR01rV3grakNhanA4WHZTUWVxU24wYldXUgpad2h4KzJuSURyV0NRNDl4YnF4L2JURUREVWlobWpzUENESkFOVjRCcHgyUVZ6dXczblNHU3hWSFEycWt6V0FCCkdTK2paTlVNTm40VzZsYUhrd3ZxNEFIZUE5YndobVlzRUlLbGdpK2l1Smw2djdEdloyL0IrM2s5R2hwN1pIZ0gKa2tEMkovRGpDc3JQUTI2bzZYNWFlNU13NlVUN0RsekhDN1BJdFFLMXErUUpXc01LZGUyN3JvUHJSWVJjVDJ6SAp2OHhLbTU5T3g2eVcyVHZna1pZRnEyREE0U0RtS1dUQXZRVDlLSzZVTnFLUVZGT293NnhxRzFpc0VZeHJxaXN3Cmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDBqWDRHdlVXSjl0N1dreWQrTXcKaCtYRnM5ZlJOYUJLaFFPTUxzRWlMTlFOVHRwR3lPcmpNelFXMjArcUdqT0lVZGE3L01uN1p4N3VNWHRtUWc5NQpsdUFiaTV5VmdxeDNjT2w2UUN4M3lOSEJISlhoR1p3N2NhQUREZ0pvYXRlU2xrWll1QU9tb1d1clFjdWdldjVqCmFQYnJFV0NyUG1uT2FiOTYwTXRFSXpvRTdRejFscVpzc2NaVk5ER2plbUMzc3BUUXliTk5GbWJwNnVYNzQxa0sKMVd2cGV2bnB0SVd3cHNIOWdYanUyb2VtSkZXRGYzZlhuaUIzcXBkMWo4c2JsUnNqc1ZKUENEMjdIWmtMQnFGaApPZGxOUUhVU3pvM2ZmSWxsZXNDVDF1ZmpmRi93Uml0djFuRHZ6NGgrNGFPdDVzYkM1aFFmVHB2czJDRm9PUnpCCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeW8xcDZYMVBtTmNQUnVQUnhaT0EKYU5PcDN0WVhWYTV2cmw0bGl2VHRBaGhWbXltVlE1NXdzaXFQMU95VXFISUE0bGYwYXhmNDBBNzRvUXV3Nmptcgp0NHNDMTF3VUhsNUFRTjJYT2dSdk01bnNMeWcrZ043b0hxUmI3NFZQQW15cm0wS3FnTHpxVk9qSk1aUG0xM0dCCjVtakpvRTVhQUJwZllvSnI1RC9rZHVYMktmY1JOVnQ2L2VUN29jVk9jdG80SmdZMk5jM1AzNVA3cFA3YlRUSWIKcTYwTTBqYldjcmlPRjhYZlp3R0MzOUNhTE5SYU5kM05xSEZhb3hyTXgyUC8yTEN4WFFBc2ZTWWxRTVdiL1UvTwo5WDQxbSsxdlg3WjJ6V2lodTV0dTd4QTRwMGdSMTZkdS9yb1dyQ0lnaTMrR1Jwc2xuTjBYN0pmZFE1cTNEc0k2Ckl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFBtaHd0SHBFYk1sTHB3TXFJdEsKMGs3ZWdodzd3REQzdWtTRGsxdU9CMksxdFhaRkp5UDVDa1Q2bDdldVI2UlV5cWt0Ynh2UnlFVHdpZjRJZms2OAo4S1ZlYjZiVmhURlpXZWpqWDNUNXJ4MS9vaHJVQ0h0Znk5S0JUMHFwZ3g1Z3ptOHVxbzhrelhDU29pYnRzQWlLCmNOcEgyRlRjamZRSGNaTlZkSXdYMk5GdkhsRHEyb3dWQW1yY2diY0F6cTJ0NGxMWWs5OGsvMnFhUFpXYWw1U2wKTENpTmxIZWVkMjBqOEFkRVFOTFdQODZ4SmsxQVU1TEMzSE54dXBhYitmN0dDNzFJQ3BQbVpVM2lpeVkvbWtBUgpMRTZ6UzU5Ny9JcXZQbHBDVGt3cWNONWFoRTQvWldYQWVReUgrY3VzaDFmZHRWUkZVMUFuVnFOWmVOcnV4SEhEClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFNQUW10OGhQanR5emxLUVRFa2EKTmFKZmJrQ09TMGNidFd3SEtWMzcwSWNMSG5sMWxhZFozZ3c2UUVwS3FqQW0xd2pQbU5iU2cxRm5UVGkrb1dYbgpqYlcvRlZZVEZhY2Vpb3ZSQ2JGMi9kTytMMWFJeWpCdGt4Zy9nMVpJNnprdWZick9UK2xacHk0d2lma2NRWVRSCkZYanZvMXJKeTZGd2dTOFlGU2dWTFdKdkVFU1RqZkRISG9jYUhPL3A3dk5GU1Fsem1PT05kelJ6c2N0R1NrNTQKeUlFNkU1ekhIT3ZYUmRTazV3YXFOR1VpdDV1UjlrS3RRbkpValJLMTdCWkp6K0tLNXBFZFU3ZWQwVFdwVWY1awo3YzdoalZCZHJOKzdEbU94TEp1SEU0TmVlQzNpbGFidEFmc0xpOGYwQVdDa2t2S01FdjZwV25mQzlqREZvUmR3CnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTdyQkorc3NLa0wrcWJhVVkrZXEKbm5ObXYzTHJzcUtoMkVTVG1uQ2VJSzFadkpvTE5SMmsxWDlZNEhUa2dNQ0tkL0lzMnI2elI5K3NhS2RLSitsdQp3Z1V1UTZBaGZZcURabDUweVEwMGI0dUI4cDR1eFEySWc0cjVOK3EzM3hDVzdnZDFEblBaMW9TM2VqVTNmZENnCjhSUHBSeHhnMUZKU3V5YS9UazdmdnRzSWM2TEQ4VmNuWis5R0RLeCtFTTl3ZCtnelZ2N2dyZnUxZ1dINHA0cWYKWjlTdXVvN1lUSFFLb1draHpCeDUrc1lrVzg4Qk5OQlhjTFQ3ZjRLY1AyYnR6a3dWSTVCQlVyRm5iYlFzTWV5RwpQYnBQQS9lRkV1TFBwaENJNTVnQk1qNDJoN1liMjgybXFsaE5CeUM5elgxcjhoMytGN1o1cWpqWlhhaVQvSkFuCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGw4aHZKOU5yTEdjdUtnZ2pIbWwKdjdUc3lFVy9qNnVlcThPU1RKdzAzSDU5NEx4cC9ybktrR25OSmdUdms5VkN3R2pTTFRsazF3Mi83aDA0RVZEUwo0TjBTN21oWTBCRlpxSUNuaEVTU0ZkejR1dG5YMWxYdzFqZlFzeW1UejNYV2xaMnM2ckV2NU9zOWlLS3lRcTlMClUzQy91cFhITG4wVW9wb1c0VG11SzJLQ05UK1JiRjNoT2phUGtZVFlFcTNlWWVkWXVzWGh6b29EaW5IU0svTHAKdjA3UUlPOFZlbkxJeW9CY1NPYXpBREtFWVI4Uk96VlBzR2cxSG5KY3JrUGRtb3hGYm9ZdHViYnZDQ20xVWJZYgpiS2VJM1NneVVVS25xMzlyS1lnMUdMc3F1NnhTKzVoR1lzVXptSUVDd1FWZTlyMWhpdVhmeWhqMkdPbm9Zbm5mCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcllCVUNSdDlaWHllK2JKZE12cWoKWUY1ZlRnVndvY0xjZzNBQzlmM0YvNlZ0WGNlUFFWZkJTMkRNZDJLMHpmZXVsMnBkM2tDR09xeVY1QWk3eXB6Nwo2Q0R0bzFUVU5mK29GOExndU4wNWtnNjJkVnlBZ0RlcDdVaGcxd05WT2dnaUMwQ2s0MGVNaFE2eFBQNHJVTWNJCnVBRVY4cWRVYWtoaW40Z0d1Vkh5Q1ZHMVFpT0JYaG53Y2YyN3FqZ0owSGhqelA2dXEzYzBERzJ1T3QwRm5zUVMKVFUzclNTZ1NTRVYzSE0zMTNFdlRLTXlMckd4cERPcU15em0xWVc0bzZWaXVkZHlvRG5VTFZCTXhpWlVweTQwcQplT0dzYnRzaU5yalBBNHl1Y3lzaUFGUHNFTC9VZktUNGhEWUZrQVJFWGQvcEU2NHBQMld0YjhReVV6cUpVTCtECjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmdDN3Z5NWM2UTh3VHdmRUdLTVgKelE3a3RBdEdTTDlad3lnakdtdDJQZ0gzRHV5WmpFQ2hCaFo0cmN4Z0dONGY0akQ0ajlkTjY0dStwRFRvaWxIKwpjQWNvOVZlNFZhMDg5aWRlajVRNFZvaEs4MTZQK3d5TUU3d0lFVms1SUsxQWVMQkxOMzNGMWo2K0JSaVREOGxzCnBjTFRNWEJSSkxIRVN2OTBCeVVReGZzVWoxUDF2U2VRUWl3aDI0Rk1wYitaQzdWM0lNOFhuYml2THZlTlRQd24KNU8xb3FZQVZJTy9UWG53SXd2eGRYK0RTSzJkVSswbUJFQ2ZIZk1FdnAwdk51WGVPQU5FQ2Z2bXNmanNUU1Y0cgp0bFByWlBQSitHVmdhcWNBYldoV3BETm9NQWNuU2R6K2I5eEtVYUtUYVhMTVlmdVJjQmxlMmlneTBtWUpIZ1VICnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3RlNUYzWDdXM0N3Rlh1ZEVGOHEKenpXS0lYbDE3NHVEdmppeE9ValIweEcyZWZocGE1R2xabU54blRJU2Rtd3NUU09ZdEczMG9XOS90WFN4NGtONwo1U1VZeklJMGhDZyt4SUM5aHk0QldCdUZPS0lCL3BPZUMvb1N3VmlDTVJYMVB4Z1F6cVdPd0RFRGZXWUpjRk81ClkxMVdGemxLQnpqeFkwWElUSmRwOEgzenNTRlJmZWNzUURQOW1hTlZHMGpINmxxaXlTZEQ3Z2tUT0V4dHJxazQKWnRpWjdBVjUxYkI4TzgrYTdGZEt1bWNBbVU0ZU00ZzU2ZHNOZm9vRGF2OGhSNHJQbHo5K3lkNlliaW9LSW0zLwpRZzhrK3RXOFl5NmZ5b0VyU085ZDNReU9zRVpxVWw0aU5YZFMrR2xJWEpSNnV5QWRZVWdBVEY4OXBYSC96SVV4CnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG1UbW1ZcVVJSjVOcU53V3FtdzAKOHV5aUFuYWt4VWxtYXF1WS8yREtCK2w0d0lMQXNBQ1o5WHpnOHZrKzBlR0ptZHRHRTZBc0ZBTnZqSU9Qa0hKcApWbUxBNzV6MUZvbTNQSDg3M2MrVWI3L2tpUk9iNmxCTUF1elNoZ3FFRkRrREJ1L3dHMUxQSmhyWS9XcW9sbHU5CkxiMWR6a3hxYnJyWG5CVksrRVhOMTdCSHNvYkgxZ2lhUWxjSEFySExYSHNYK3JWbUhLUERGRzlEUS95bXJ1dzYKSUE3RHcvTlNKWUtSYzJVVmNQYnZuaXlMZnNOK2ZvclV2OFdaQVNOZnJkRjdPQXl2T25JUGxRNGlSTjZtMTdXcQpJa2M4MHZweWg0NlVta2lPS0R4S0ZsT01tbGRhcEZuV1dwL0JWNW4xYytIUjJHR3dmM0w1UEJOR01xeEFlU2gxCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1ltRnlOYndtMkV6MVNKdGVUZ3AKU3lYaFVBT3NVZm1RemVZR2FoZDJURGltamI1Q3d2cGtYKytzUDRmMGlMSVpDZDBNQ3A5djVOb3FXRW1qcVVkbwprWUhyVXVXYThkMVQ1ZHpIL2o4NllnbTZTTE9xZDkzMlhmVWsrSmZqaEwwM0RlZmZJWFlNTGo0c012dWVIN0E1CndaZFdEbHZCcXlMb1lzSzBzNG9kYlRTR1ZaQzNQZk1IdnJwcDBPYnRVOGh4TU5vQVc3TllqZWZsNm9XWGZObnMKTHcwTEIxZXA2OWRGRUxERjRpOGlOTVJJQnN6WnYvOTVnUnJzVjRnZ0toVEcxNEEyV1dHUzQxL1pPMUpRNjZNSgpDbEZrUjR4R29EOTdrSXE5NmxBQTZzTFpXblpqWkhvYndnOFN1YWpSV0ExNmRzV0VOTzlrQTYrdk01Z3NqaUZ1CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcm81VU5jUHRVNGxmSldnK0FmNUwKbTJ2Snd5Sk9DRlVqOG1yeEVHSEt5N1hRY0VSQkxXWnlGQi9DS3Fka3FUMnR3cXh0RWxkRmJ5a1RheHF3NHVqZApFcVk0ZWROQ1RybkE5dUQyeW42TnZaeUtzK2lacVc2U3MyMENzTy9VUlhvZFdUdzBtQkJ5djZ6NmNUK2N1alJvCjU5SG5DaXBzMElGQUJybkNiamVXYTVpdWdrWWYrR1h2QStRUU1lTlArVkZiK1hZajNUcmJQcERObnd2dTlQNm0Kamxya3hQeE5Ha0VZSkRTdXROT2ZqOVNkanRUU1FOZUw1cFdGRkM2WFZCUTU4NVRmYnJ5OURwTElUSmJVNjFESQpHd2htQ25qOENZVmF2ZW92a3VjNjk0bU1iam50Sm1BdkROSVYxbTYwb3pQZ085ak5oSzFhWEdSMXQrRVozQXVtCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbko0WFoxTVlsVmUzZ0lnYjF1NjMKSGszYW9iZnBmVEdJLzJBdngyZTdnNmlaT0dDRkJvdmZDaGxqQVIwR3RPTTR1MStMM21PcWNrdWluU2xsM3MzSApVSXRtcjRhNUVkUHNZMzRDcmpmTXArSXZXWGZnSlFTYVFqMVNXUitJemtLNmN2T3k2eGJ5cFJxWG81SnlmREZqCmh3eWN1dkF2WFJUR2MrQzlLS3ZCS3QxZzl5Y1U3M3hKRGgzS2VQRUYyWlVqMFFSOWticEZMdG9yQ2RveVBFSFoKRzhHWTlCQUFrWVFDaDU4TFovOEJKOGMrTGpySFJoQ3ZXUlk3QUVYR2F4UHFEdlo1cjRQZ0czN0lueWRZK1ZReQpsM2J4TlVFczZNVncrN1ZWOFVGKzlFS2Q3L0xFU3Fma1BsSzZjSTYyY2ZTWFRzQ2ExQmVmYVByVjFNaWcvWWFoCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWJXNVNLajkwT3ZxTFFuY3B1UzUKbUVWWVBxSmttcHNtVG8yUFMyQmUwSUVJRHZGU09JaFVvaFB3dEpCMW9SMHM0NE1LVlNFSllEeERDMDl2Tzd3TgpZMmZ4aVFpQzB2U1lXd09MbS9BeE9zTkxMVWpkQkZSODdjUUppbk1xa2NQR2k4dkZuY2VwazFQL011cm5XK0JZCktwRmoxcDRXWGl2a0lBUDRtQWdJOVZoUlR4S1ByZHBqT3A1KzJLRHZVdlpVeVRheFgyWnN5bVF5ekFSRXE3dUIKRHBldFM2WXUvUVJNbnVILy9uQStBT2VuU29reUs2b2IvazRLTjJWa08rOCtsUy9MQ0M0NFlpdkpzMzcrZVRILwprc0t1MkVtb25xTDN1WVEvUXo5VkJYdzRKbE5Jc1YyT0F5OG10WGZ5QXFWUy9qS2JZbTBLK3oxQWZpdGxCb0N3CktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdllhbU5zdkhIeWd6TGRiNVhkVmgKM2JPK3BBdExyNzFtNE1ERkFnYStRY05iajJMREJWbU45L1ZCRExtVkdkbnVDOWdzblE4NW9ialNiWGMrZkduQQpQeVAyVXI2R2JCSEs0bDAxUi9EU2V2cEdGVkZDS2hIUFBHbDRBc0dlWDFFeDc4ZE00T2d3VGFrek41UnVPMXdECkYwZmh5Um9qT09xalo4T1JVN0xCcFBpUnpRMUFJcXRvUFhLSWQ2ZkxReFRydHE1VGczWTJZYmZqWS9FSUl4ZlMKRWtSSWtTNDNRRXp6UnU5a0VDVWJqdzJpclIxMThUSHpYVkVLanZONFl2ek5mb3JITWFoRDkrUEY5RkVQZXpSRwpRWDdHYnFnN283MHk1UzZneGpoMDJMcjhQTy90Wkk0aE9sekZTeU5sSmt4bGtMVk9QODNyTWdURjQzUHJTSlN4ClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1diOWxxaHRVSld4czNNQ0pCSHcKanJzU2V4TXZyMkw0bE5mSEs3Nm9DSzhUV0V4d01sT3dtcjhCdzlhTEZiYjNrR1VJaXVuck1zcXNSKzFpSWg2ZQowb2YwS1J1MDlUZzgwMXVacFd1OGxSLzdpdm9OQ29XRlpHdmVGMVR4SWtDY3FLQ2R4eGNTdGFibStCeVFJK0J0CitsYmdnbk9BbFE0bUV1UlNDUHRrOS9EYUx1TVZGZC9QM0lXa3Q1WU5yVDVUa0sxUWpmYk5pNzBZN2RaVXZCeXUKVEFrSGFFUUo3TC9IcU5SVzlBdlFydk1QNkxyV2tvM2xaemphMWdnUGFBNWlUUC9DaDI5b0ZSTnRUWC9XS1ZUbApsa24rTWtIdk9CWEFiWnluSVo1cG1EYVZqZmd4VEhLYTZDa29ESDdlNUJBb3krQ0UxWFFVL0ptWm1nQ09qWSt1ClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3NqdVB0R0FNaE9kckdBRzdNbm0KK3R5eXdpL240YnRXdG1oeGdyT0syTGxFWTR5KzM2ZlgxN2JCWHdIMXNxUGVPK1VNUWtpQ3lubC84MWJWV0hxZwp4cFVHYysvNUo3em1uS0pYdThhdkhQWVFwSTBUUS82RlJOUTVoQm91U3hJRmdlc2ZiMEJtOFVyazZzVGIxTVI3Ck05b3R3V1FuaTJwZ09WRmdkZ3FSbFk0ekVuKzhmS1llSmlxTVRMMHFJTFQzRFpxckhqSTk1d0pnNUV1ay9aNHEKUlZkbmhyV3NGcUI5TVVIdUhDUVpsYXZaamNiVmJXUE9xeDREQ3pUZldFaWFxdDFUdHVNQXVVYVA3UGJ4bTN4Zgo2NzliV2RaRTZlS0Q0SnpHVFJOYjN6a3Q3bWJHNStLeXRmOFdEZ2NiVmdlNlVXSHpNQytwVGhKM1RJZFMyYzR2ClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemJmTi9Gckc2bE1Fcy9QUzY1b1gKUjdKeGFmTytoa0t6Rk9heU0xUllRM3lkL3dWT2orWjhocHY0dlBER0RMeVpPTzNmWEpyRTRIemR6SWUveG01aQpFcjFKT1ZSY2ZxK2Q1Sm9kMDc5U05VUkdDTUtXbnpXUFlBUnI2SFp5Q2NKZ1dWcjdqaEV5dGYvbUhRVWpkWmgvCnVPNTZaMGpBZ3UzUVcwOVVRSjdDUGxTTTFweXJBT0lkVi9NREQzd0txWXlpOVNqTXQzQjBlbndUU1B2S004K2wKQzJoTERBREZLU0pYRlVSRzJKMzY2aGJmeThZRHBCOVplNmxMdnM3bXdqVFBKT0c1TG5mamM0RENIamFqT29pRwpjU3BQdUROWGRHc2J4L01jUFpsNmdZQkRPOTJRaE1XdXI4NDJhanJsRHpCZ3hCcUNzSDVjMitrR0EvTXJVUnJiClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUJhdjY0aVVwa1A3cFhsajliaDgKVjdkY1lIYmNCS3pXTjRxZGRRQU5maXEvN1R6OXQ2bHFFb2pPMGJpUEZKYWVSUkhUTXh4VU50MzRJQko4VkRiVQoxYkN2cmlCYXBFMjQxRzErYXJLdVIvSlVlYlJ0Z2tCL3NLRG5ocE1HWE1PWkU0cUlmalN6Z2V3ZW5teGMvbjMzCmgvaHFiamIwb0o0T1RlcXAyMkJRamI1ekJWSEtac2FvVXB5aWZwL0NQa2U4RjF1MDRyVWU5aGdUU3ZFU01EZUYKY29LdEFsYmk4eUNCZmtvTHJtUzBITWxrOWc3QnN0MGQvSVZkSnFkRXpkRWpqODZwN1hDY0NoR3kxWUc0WkZMTwpkM3drQUhkN3IxMXBXZ0pLOWx4YU1ibS9SYmM3ME16MDgyWnNnTEg1dGEyY0Z4M1Z2c3gxR2dWeFVqK1l6R0FwCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelBIQjB4NThPVldKYUdqT1hGMmoKWTRsNzVrV0JPZktFSSsvK0FORVU4Q0JWRkdNWHhyK1A0RTY2dE9GdnE0OHNpcmY1dHJXZGxTQXVpcmNzeGJKQQozMHVqazVjanplaVBXRVVKRkJNRDl0UXQreEc2cjZhcVEwenhBRU5BcDZrclh4amZ5Z3hhZW4wWDhVZUR3Y29lCitIa2E5a1lRQXV2UXZSUWY3cldacWI2SG43Q2tiWmhOZEMxZGp5ejBlemM4OUtHRkM4TUMwT3phdzV5K2EvcVIKRzFzL2VPTzJUYlNyV0pUalQ3RDNvTmFNWFhEdkp3alQvTk5aNTl6RTkzeDM3QmdXeW1Vdm02VGNlWTlrbFpvMgpBQWlwL1ByZ2kzZXVnZzU4MUdjekFVRldabHZzK1pEMlR1TjE3Mm5Pd3pQUnhuZTB0K1gwUUUrVTM0TEFoS1hwCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenNKZnVDNC9Bc243UVNKcy9hOTIKeHhFWTdZdkJ3clNHa2tTY0o1dktZRSt6UUNlMHpINHl2VnI1aUw3QkpTeDVVMndWcGxDanhGQ0tKdHNqL2IzdQpTK0taT0JFL2hFZkt3RnRpWXZFcHNHODFoZnpLK3U3VGxRZVd2aHZoK3pkMlh0WTF6dUtYN1AxRXA0SXZlSjlyCkI3TUFHNHJuR1E2SGRnOEJtcml1ckZOMEhQOHZHMVNmZzdNelNMWXltVkRpZUFFTzM4N3plWjdVWWlEY3I2VkUKUjBZYWZ0TzFzUFpURUFkeGF2U3YyVWJYQnNGWmRIRk1tWTREcElZS0p5a0VoZXNMUWN2Tm90cTRXajBteEIzUQpBRzFIcjQrekp3UnRYeHpBRVVKdFVkbnJsRXgwWXFxb0dUeE5xRE1rZzdrTGhPUzM1ZktnZEh4UCtTWCtSUmY4Cnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1NUVXZvd3F3VFJFTlA3azVGYUQKZWN6akJsN3M1eXBCekF4bmhuZE5sZjAydGRYOXpqN3NydnFKOHhVeFZWcnhpeUdBeUk1cnNHb2ZTZldyVlpDbApZOEZqQmI5QnN3dEtXYXB2dVhRcmMzS3JWZUovNnovS242R3RoZy81NWFVYkdlSENsT0JaR2E2ZGNXYm9yem1MCk01eldmemFyN3NqcHI5V2FrNi82MGpZTDVJOGtBU2o0b0R5N0lrWFBpT2lNL1NxSS9YZEEwZFJub1BBWkdTSzMKOHpzOFhqSnY3UDhTOHhQN0JkVzFOTE5YSlozRC96U1FpdTZjUm1oTk1sU2dhbmJrRm9zOFVGWkkxSE4xdUlyMQo0amVzRUhBcmhGK29OUUcydkFLMFVYemFsa25HcVQxWGpmK093UDZKaXRYOHlVSHFtbHFERXVXa0NOeW5IWlZpCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWJxd0ROcllCQmJjOG1MVzhPdU8KSDNiaW5HMnM1VkNMYU1sYjBKbDZXc3lPYWtnZnRZUE9FM0t6K0dSekRPSWh4MXd3SE9lWTE3Q2ZXUFpFMzJ3QwpNNGE0a1RRQ1h6SklqNDM0VHh0ZkYrV0VEb3k3bWhYM3pxenJpRkF5SDcxWXoyd1hDeWo4blp2NGNYcG5jVTVuCllsYnNxSkptaEZpN2FsanlJQTJYRjM3T0wrZUEzdkJEd0lEdmhDc0VBMnNTOW05ZmY2UXBCUnErcmlOVW1JWmsKOGJQSk5qYm9xMkdsb0dnUVYwVEcvdXVocW9ITDI5bDhCQlc5L2tjcFQrcDh4NXVGMjcxeGs0MU80Q05jRHd5dApkbTVQL0pNVXNoYm5leXlodklYOXU5bDJwOXlWTWRrVFBnanU1aU1BeFU0NGc0K3ZKN2NRSG1oeFJjUVVueC8vCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHk1OGlCdnJZeTdQMGd6c2dVS3UKSElPQ2hFQTlRNEcxZ1NHYWhPY0ROR1NFWVRCbUlQYVJTdE4wRlZONmMvak5lZENxWGtqbmVlNEtQVEdITC9xOQpDRGh2US83dDB1YnA1N0s1VUcwcnpvTjdqaFFKU0c1QVlwbVQ2dTVkYTU0TmtVdXFMVUF0U1hIT2x0eXlhTVo5ClFkR01VTFY3SFBXRkc2UWpTWit1cW4zUkFmT2ZmT0hUdkJhcGJXL3FOTTlmYnVxYU9hdnphdzA5MjY5N3BsZVUKaXNIYS8vemV2Q09PWE9HRllEdVVHY0xHSi9zMmxDM25GQ29UZktkL3RBb0J5czR3RThZbmRjUnREMkRHVDErNAoyUlMxRHkwSU56Y0ZpSHlEL0x0TTM2ZlBYdmIwNVpmK1dNcFdSNzVwQk9nQkRzVm1PYWhObWV3KzZjRnVaSVRmClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0s0V3g0YUVtZ1hUVC9qOGUxV1kKVzJrNlpmSU1MNXhYRVVWSER3TWEwTTFpUk1lM2h0Smw4cEFLdTUwbkNrYVNxWlp6M0ZIdkdUSzlEQlNWckdUMgpWTzZHdlYrcTdvVllPSU1XK1hnbWNHQ2RvRjNYVmswdk50VHpDZmxvY0F5VGp3b0RIeFBlRVdTL3dHUzlUQ3Y0CjFGdUY4VW9TT3ZRZFd6YVFCOXFubCswb3RtbzBUTmprYVFaQW1sVlNDYU1RclU5bHF0aTJhQnpQdlpCZmw1K2IKWkJBT2tmVDFUZXNQK3BERkg1V1FteGZmWFJrSWE3UWRLMXlSVTZNQ0t0RUY4V1JESUY4VFNqblBrRUVTRVBOVgpuUE51Uzl3QTA4cnhBZEV4Y2pRNWk2SGdVb2RLTzJUbmdJaktqQjNnMWtJazhHMEU1R1Y2RWZ6d3E2Q2xubVRpCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWJUTkFLcVpXQkNDZXA0ZERlaVQKVHZ0dnI3RHJudWwwejlxNEJya3hmZUNsaFMxSldzcXZmY0lUbllUTHRPckhHLzJERGFBVlR2Mi9rL3pOdkM2RQoyK2NaZkZodU9MbmNIRnk3SU11eHVFYncyYkFiU1V1TzlxKzJCelJnbWU3VGF3T3VLNUtLU2ZGQ3d6dVl5UHR0Clp6OXo1U21NNTBqdjZJdTk2M1lrZGRVZWRlV2FXWXdiSkJtUkpkZkV3a2pqdDVTazRnclJXK25XOHEzcGkyT0UKb3lHc3U0QnYzN0t3VGhNL2hnaWYwUkp5OFBNRDVjMzE3Y3VkUXZlanpCdnAvdWZQVkhqWDdSdkQxYW5NL2t2dAo1L1U2d0FiRUtyYnZEQmJlaVZ6alB3citvRGtPaVFJa3BKZVFYaFA2V2luckFTS0tpdmZGYmhqa3h2eXlUZjdkCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk1hTDNWZXlKNjBOSnF1V2NJajAKTnZCS1hka21QbVlRbmNhYTRXell3K3B0T0orRUNlWlB5VlhRbHBaNmtkTFhYUEprbWQ5TUNub1d2dGQ2cUlMYgptUG0zV2xDV2dYVDNIRUFUejZ5dGV5aCtxYkNqbE94L0JIaUpOV2ZXMVFZQUpkNGFDSjJIblIxRWdVL21VQlpiCmlzL1VDNTc1ZnVIOWRFZVVRQzliYVFtSG9xWnBsWmY4UHhQdmM2MG9BcXB5Y2JoWkQ3N0E5TWRoSExWZTV2UFgKYVl3ejJxSHQwRFRxZFFyUjVadDdralkxL291R0orY3VIUGdZUlpLVVd5N3JIcEJZY1o0QzhnaVhiZ1hLWU54cQpoREw4TTdMaUlHcjJHK2trOFJsSWRncjBGaGxlcGtPRVFnRkdlZnI1bGhEOG45WXdRVU1YWWdPSzlPQjQ1aHl3CnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBengxR0tvcVRLcmsxZU1nclM1TVQKSHJWdXdPZnRLZWNpQzBvbUNtb2FNTTFkcUFmK3grUDdTU1F6ZmtTbnZPdVlBV2k3RFh1NFkzaHlpSlVEcDdVbwppL1prcTNoQ0VmMi9iS1BRZVUvcUdha3c1YUlCS09XSzRnaGtTQU9WQmtPcU5XTEJuOXgxdkxVREloZzdwb0ZGCjdkeTV0bTU3YXBjR0o5RlNrM3BIczFIaTZYaHgyL3FXME9icllEdWNNWUloaVhrZTFYRk13ZEtSS1FkODQ0bjAKY0xtT0c0V3RqK0tVdS90TTNxaE9lQUJRUUZaVUJ2MldoR044MlEzbXhkbzZ3UnhMUkV2OFpOWUdSeDYvTEQxVgo4SUs5bGpGZW5qMWFnK0dUd0pLckZKUlo2czdJRnJ4dmp3ckNyTVFCZUJzQUNkWFhBY2hMS0hzdk14MVFrVHM3Cnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0Jlc0dhT1RCTmtBUkgrek5QWEIKanE3Yjk1blduL01QV3hsYVUzaXU1Z1NkUXgyVmdoOXNTYnFmZnQ5eFBINmhPNzJqQzFRaUtXTFE0YXhJVkRPYQp5Z1B4QTExV0xFZ0pWVjJRejA5Mm03aVYxemtnVUtrQ240WFQvVGFhZHJUT3NudmxBaXI5SnVEcWJQa2FJajdtClJoRVVYekx4VTdTS0dBdVFjdDlFQlZRUHRYRU9vUk5qbGRoUUVmRFFZMGJrVHFXdHJBbDlDa3hBQ2tkeE5JckYKOXBDQlZ0b1YwSzVrUFc2M2lnMExxaFRBdFNnajNYb0EwN3VMeEtJRm5Gc01ONk4vbzRVTGtkL3oxN2h2VjRncwpBNWIzdjZWclJmYW96M2xBd3k0aDZzNWRDWitLNkdqMzVOUjg0anQ0YXRzZHZ3SFFoakZIQ3FobkVtUjNKRVdqCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK0lrZUIyZ1I5TER4ME1nd0NRekgKVXdENXpEM2JaUS9PNWRaajk5VjFCS2VkVG1xT2pqUU1PT08yWGEvL0V2bWF1UXM0LzFWekxOMVpncVhIb05uWQowRGZXR0tvUUdlU2p4aUMrUjZFTGMrQzZBbTJDaXhQNC9Mb0hlVFBvWE9LNHFwbGFCV3d4KzhSSGFRQUxkMldyCk9hWC9tNWNYMkhqVlJmWVNhOFF5MFdjYTJlc044ZW1sbitpKzlnSVd0dTJaVGlFQzVucXdmZmMzWnVnb2dBcDEKTXJhNThQaUxRWXpzMGViUWh0cFo4Y0U4ajdLOHNoMzdtUWlJRGpSOGtoS3dBRm1mTW5XdDcwQzA1NmNzaVBBbwptZHRCb0ZUcFFQT05sV0JjTkxVMERCdllrNHVEUHFvenZJK3lEZG1hREFBUFRReTRtRTF0aDlNSUpFQ3RDRHdUCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenBqV0t1Y1NJRmcrdXNiZHI2ekcKWGtNcUFBbGZabVVJdGwwWk5wQUpjeGZyL1NTaDNSQXV1QUZPNXdDUHhOL2EzMWQzb2pSOHI2NzJKRVVhQ3Y4RQpBOElvTGErVVMwelEyMGp5WkMwV3ZXNDlWODR0TVU4dndZNVBjbmNrNkFQdG5zZjI4all2WittUVhvRmRra2g1Ck54N3JuTEJPbHhoeEl1VmxEelcwWmZ3L1pLMnI1QWs0MzYvd1FpdUFZTlQ3MGFmUUVoQi9FUERzemUzZTNQcXgKUXNCRlczT1NnUzZVa3ZNbDRKT2s3SlhrZzJaTlBqOVFDZkZmcVFHV3FEOTJYU2RTRnJ0emQ0Z3VMQmd2QXRnaAptUXh0UTBpNmQzM2dGRFM2RGpOVGpnWUxvTDgzdDlDWXl6T2EveGM3V2FUUjNIZ2h5dkRUSmkrUlhZME42eXAyCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVBaSU5tcUJRbUxHRWpKT3d0VHEKTUxweXBkQTRIZWNsRmFEYVN1TzZUNEIwQU9TVWdQVWdZWmxxZ3UwTkN5N1J4L2FpUyt0a3puaXF0ZVNKNzFMZgpQaTNKbFZjWm0yMnZNZzRTZDZFL2hPV05jcFo4ejNSaTZDZk1TcHBSL2Yxd0xQMkhFZHUyYXZHSEZYcW9jdGFWCmZZMVU5OE1hQmJ6S0pGQW1WNG5hTFB0c253d0oyZDlHM2RmVTVwTTVOTXMwTnQraXQreWtiWmpyNFYrS1VkSXIKWmFuOWJ1Q0QrUXk0Z3g0UGd1emxLc0pLbnYvNUNXcG43RDgyS0lHTTE1WGhFVVNnMzYrLzExQ3AzK0I1Nk11UwpJNXFxbTJWTHdxdmFCbjJrUUZLZlNqSEQ1WURKNEhKeUttWUpTYkFUeXVmbU93QjlTZUZoYzQrZzNkdWJFQVhQCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlNEUTRmTE1sN2lwN211eUhlcUYKQ3ZBK0dGSVEvUDVSUWJlcTVOMllDRnh2UGFhY1QzYVdoR2YzeEgyR2tibEtNMG1zSHZZM2R5UythTG1uN3NjLwoxM0ovdHpubFN0YUd6aDhlSFJETTE3U3NBS2xwcThJejY1blR1K1hDVHY1MGVLdkNESWRZbEVzL2Njelo5UHBBCmZ4cEIvZE1FY1oveGVETmV0VnFsZlJLbmdobUtwZnFWT0ZxL29hcHJod1kxVGxwYlovSDVEd3ZZWXpDdmtwNk0KcXRJUWM4YUIzaEVodUlOdk5OeUsrSThhVkk5U1R5d0NWOC90d21jZ2xXWE1lRU9BdEFyZURmbHVwY0pVZHJrZAoveUJXb1ovQmpxaXQxNmRTTGVLaVJ6Q0I1RjBWaE1ic09XOW9BSS9DUVk1V0tURzljbUYrRXVVQkV2UEl0RXJECjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXNoUi90MWpvYXZYaVRxeHYvcHkKcW5IV1lTUXdyZXZKd1FNMDlPOTlSQXd0dGRJdUppSVBJUXgwVEFyQmRsT0lmeWt2c0JPTEVLdlF3TTIxamZySApkZWVTRnR4a0N1Ykd1WjZoWTNZdXluOE9jaHJIVDFIS2JtNHZRemU3UkxoNitTaFNNekFjWXUrTGJLck9EbFByClBWNGlxa3RVZ1QzdC8wb0VUTVdKVUxzZHpkaVV2T1lVSnBsd25zNGdyOEZLZDd3SEU0eVNZMW5YR1RmTTBZYlcKbWU4S1lDemFOamdhSURnKzg3am00ZFBuV0s4NW03U0RVcmdBTGFGVjdnNmc0S0FiOEpHc2pFdnJmWXFLeVJ5SwpBV1ZwMW9EeFl0TjNKa2pVVGk0T3ZYZGF6NGFtODd3b3dOMHhjdndOZEVRM3NJOXpxajg2WEtHUjduSlhsbjR5CmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2JYcDhHcElmQnlacFowbWduSVgKZ2lyU0h2Z1ZpWit4dC9YNm1rd25EUnZKc0N1bk5tWHUzNjBDYS9VNFVWWTQ3c3ptc1JtNVdTeENsUEphS3NtUgpxZjBWRmZyNHRoMzVwR2xLWlY3ejVXeXZwbFRPbDNqNnkvRWh5bmFvUFByRjRySitmMldqbWVkdFZGSE9lVGtRCnNBcERPUDRJN01IV0xFbk9QNThPb3BjSktocUx0QWFDQ3BhUndoNlJOVFFyUDBVb0RCekhJZTJPQTlLZSsxV2QKVk1SQ1orcTV2bjhhQUVUZ1lLMkhzWFRVU05Jdm1OeXVLNmpWZjJTb05jR0tqZnlYVGNzMFdOeDBaZjdSQ2VPZApEOEdITzMyeFI3aWt0eUJGckQ0NzJkU0RGdnBPOTRTYStXMGJHMmh0aHZHVCs1Y3hqNzE5aS9IRXFaeEZkSlQvCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOTNmaVozNjZLWXFMaUI2ZlB5OXgKWFlDNGJYRWV1M0lKc1U1Y3pxYjh5WllhbjQ3cmE3R0w0OTYwQnpDSUpGL1hPQkVycmJaajJabTdpQmhlY3RwbApPTkEvMHBCUVErZDB4NFA1dFBTWlpUeU9SMDRyTTYydDA1SzI5N0c2bjNGWkxwS0wwcTQvb3hWcGVhMGQzTHZ6CkV5c2lOMnV0b3d6VFF5TXpJY21WVngvUFl0UUNUdVQ1ZEUvMVZJU0dZaExNdXhYZ3VuemdmdTYrRG9DNEV2WUQKaWRkL3NpaXlxRWEvOFdmOUZzTlhrVjBwd1JBUUhlOG1kaWNWTmVlTy9qR1lBQ0cvNWxkMFk1K1BMZ01GNzhHbQpHWGpNcFNQSGd3ZXpudXkzbEhvZDFhL0h2UTQrdHdJcE5YVEF2U1pHTEEwS2JIb2F5YjRKQlFLejU4aEhyRDRlCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0NOZFZZc0tlbTNqS2RUT0dGakkKaFpEUDlXbjVZWU9odGdMbFh4TnhETTFOMlRmSS9KUHNqTkdZRXBqZXlleDVJZjJkL0FRdm9Samc1RVhySTJpVgpQYWpTRmgzZU1PTFQ5OGt2a0FlSkNPRjlQcDNkQzZwdi9GUkZON0tRMEl5MVltak5VT3FWVmtlVTVoYWVnZHkvCmdSSVlaZTBrZDFEZjJORTBmV1JkQ05KZS9BK21IdFpESW9HcFVVN0hvSTFtTldxTWg1NTJMZ0tHTG5iYWx6WkwKNi9qS2hKSmxNTjQzUEN3bUlVaS9ibGFUYXFxWW5pRTNTY29KeWtuSml5RVpZS0VSQzREc1lXcVBNUjdoanpuNAptdCtVMFhidmRhQ2V2YVZwemZsMlovbGlPaXZ3VUd3WW5XaldzVTZybXNBZzd5bEtBcTJiSC82cm5Ba0k5QlhkCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1U2Y0N0ZEdzY1lMMEMwNG1hWWkKV3RUQjNPalFKYmZSYVNZYlRBQnhIaW52YW5aVWkxMFc0MDJUZFdFWWR4amN6YzAyNnE3bmk1a1pPdFU2REsyUQpWMEhBNzBjTFZRUnhxTlVwSWdSaGhmMTlrRWZidE11TG9Cdnd0Q3Y1dlVML3dOdlg3V1J1bXVqcVRIQ1hKb1ZiCjFVMWdtNDZvUkI0ZUNVZ2htUDR6cXF1YTN2VlVzWFc4U2lPWFkrdFd1aENhMk9iV3BKL2lhaXlVYWl1ZWdmUksKbDVtSDQ0Vi9uK0FQbGgraDREcExuZ1kxNkd1VHJoTDl2ZmlXN0FnamV5RzFCUUdoN2QraE8yQVBsMUswbVdUKwp1NEc4QUk4eW9QNndDbU9KMllFWHlRaCtmNHV4OHVMSjl4anJiVnNyMm51SlpBQVRYMFA4Z2tNVHREZ3FJZkF4Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTVDalE4RWJyMk5MSjZrcnZ2b1EKd3BqVnp2RDhzUkFRZUI2TnJyQ3k0ZlhkenF1Z2tlY2JhTldzUE5vVEVtV0UvbHVVQ0RSdDhnOTV2b2FJUDN5SAphejQvQUdlVkVaMEdOQ2Viays0ZkplVXZja1ZneHN3RnBzdTFmbWw2VkFMdkFVNHF5UmNIWExzeGd4dDVUZVJOCmNrSmhMZklJWjJBakZyZHIrTVlGK09XV0cwODF4ZkhRdnJzMlI3bmo1Tzd2cW1PTzlBenNjOFNSNEZKQUwxSWoKRVFVTG1qYjd2eFZnemk4QVcvV0RoU0FyOEhYVy9CaFdWTlNTR1NSems0Yk9lNjBZbnA4dnFXQ3QvQ25LM1pSZApBL1RCN0lqOXRkZWNwd2tKcmxxWjc0N1ZoRFd3azZLNWFJY1Z1UTExbUxTNTBiZ29MUGliVHJpUVJ0Sk9IcTRxCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2Z1Wm1rUTZvaytjSU5rdDdiU2YKbzhSZmJrNjFKQlVJQVIyZDFYd01FWlNmMWZ2VEtnSnZUSzVhMEZFTmhsVWVNNlJlV3UwbzdzOTNLSkRFZlJzdgpSRWhTRE9Pb1dPbysxUU80Q3lRL1N5Rk9lU2NnbHNNaXJzUXhGNTh6c2UzY0pLTHBXVm9nMHlWK2s1d2piNjEzCmVYaFJESWE2QjBkSXRlV21UUjB0SEdHU3RyN2IzZTJxMitsc2RKVmc1N1pXNHVIcE40WGY2bUhYZnZkQ0FvM2kKQ1VaUTFaVndIeHozY1YrVUwyZGF6YmxhNnN3azdTUHMvZVA5ZzVZL0ZkUWpQZDBVN2JwVUlJeko5UVUwL1lFaQp0eFcwclBURmw2OVBkRTBMUEg4QzZoUUJUQzlHdVJOUjZkRUhrcS9yVmhaVHM3NTNBZmhHNjZxQnNuU01mVnlTCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2p0WkI1QUxrNnBjS3JXdkdCUWcKRE1lQU9yU2FLWW1YU1QrTisyL3I1ZFFjckx3ZVNIWXpudVFLTjlQU1hCZGkzUDhjQ3FoQUgrRzJBUzRQZVVKdgpLYU1hcnNTaEVFQWllMU5DNEJBb29zVjF0ZmpRdWtRVTJ6dlR0VlB1cGN2RVNuNVk0ZXV0ZVorUnFQRmRFdVlzCmdCOW1Ec2Z5RUVzUjNPZ3VHL0RWTnFtN1hHZXhPL2dzOGZVbHMyb0dnanh3ZmJQZnZIbFQ5bDlxdy9wRFVxZzQKM0VsMG5vcUwwNEJVMk5mM3d2RmlOVzhsb0tZOEs3aHhERFR3eDlJWHNkdmhUdDNXT3hZdjdCOC9uYVZWVW5jMwphdWIrQXlrcFNLVHkzR2J5czVWVDBUV0Q1VGx0SHZ6MHRkSVBTZXVCUDZDNmV5ODZjWVkwSzV3MFBXOFR2dlMxCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTBMa0ZkRmlOWHpPNWZ1OThRWksKNnlJN3BkbGtGaE4vR2FtT0p1cGZ0NEJnd1g2U1FSSkxxRTVHWTgwRFI0ZTJiZ1hXeHZrOW9KQlFPNGhBT29XRQpVYmZhMW82cVIwR2J0eWV2QTVFK2d3S2NiVTdjVU54TVNrdXRIOURNSStlTDlGZXBKS2RXUFFndWNaTVZmcUtFCkJGRWFnSjQrOWllYjV3a3VSS1JmU3RYamozS2Y5SkpGUTZBTVBydlpyZUNCZDExeVVQclNuZkh5cHloQzBUcFYKK0FwSGpBRVlTUHlTdnZzbjd1YmlHczViV01SSWRZUjVOMWJ0andkT1hjbzg2eDdrS25mZWRWd2p5YkdCb2hoSgpyTm54aWh3bEl1MWx6RFJVeTNmQ1BaQ09DQ0NMTjF6aWZ3MHU5MWxjcVhMTjFOL21KTFhQY0tMbXROeFZZVy9xCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVAzTTZKNDVUOFBvUXlpdlJYYkgKbnc1ZHVRZEplQmZiQTZuSEIvaDg2RXJob2NqL3FBSUs0WU13endZUmlKK2dibEx4YTJQWEpXNmwxVGlNdzRicwpuSE1UWnc2TzVFbVZ2WnoxRm5JdjB6VXFjZmFkQzh2aFByMWdZUHVvK2ZzaitSdWNLc2tWR2Ztb1RFelh6UFBwCkNhMkVKNFRzVmhMWGVaSDRsay9UZEprYXI1bmtZTllQb20vNU5OcHpwUm80VExJOU9MdWNZaDZLcmhmZWhvYmoKUk04OXRma1VVNkJTTFFMeDlray9yTUhKalhhRndFVDRqL2dyL1dJc1FQTFRpZUdKS29jUTYyT2MzaUVUT2tQcwpEU2xjRytVWHNyaDhSYUlaalJYU3lmamNrWUF4REQzb1dLZ1dpcjBLZ2lWOW9KeUNjeTcrbFZjK3VGazdhSUFFCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHFuTzNBbnpyQ3h5cE1hUm1jREkKVTdlaGI2OUFpdzZ4RUdyME9Cem5sQ0pjVG44RW5VdmFXYVY1eEREY3VMS1pEcTc1dE9YaElvR3k5UlFMYXRmTQpMb3RvbEZJMWdlKzVXVkRDTHJLK2J5Y0ZqaXhaRmJjd2JoSStzSmhyUlEzUjdHbTBJbFkrN3RYczNodmF1NklOClp2ZVpRaG0vTUl1dCtWMDRsWXVtUU5UWThHZzh1eVJkcGlYM1NUT09EWVJDVG1sUkJ3RWpYSUUxOWUxQ2daVkoKbUtVYnBwcUtTU0R2d1dUL08wZE1pODkwTnVDeVVaa2xhMnY0WE50cmR2bTNWc0NKU2FCR1NCNTMwWE9SWmdERQpiUTB4eHRNOXc1QlhERUpMVitjdnVMU1oyaGV2eUJSNXJtWWdMUVN3bDlNazBKcnRCNU9wbnhJSVU5aXN4M1VOCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmFNUWdZRm14K2srcWZ2TnYwM0UKakNIbzV1VXZSS0o2STR1MXhhY2tQQmNBdzliZFhITzFTcWk3VmdUWHAyd1lYL2xBZUxWT3diU3lJcDM2STZ2eQpjV3dULzFVMEllY2VPeCtWQm9MeHg4UWt3QjRqbnZSRm9lV3c5bk9PWnhkTVBubktvTXV5TlF4bEt1V2ZqakpZCkVqenUvK3djOGN6Y0NFZVVna0xSUGNtSnNoQ0xVK3NSandQNmI5bm9IbjREQUFxYmVtbCt0WDNrNGllT3NSQmIKMThhMVp0MFdkTEhQZ3V6dEI1UWprYkIwVVQ3MFpnS2xDL2xsRXI4WGNPZEFudk5Bd1pvbmRhVUFrOG1ZMjhadgp4WmhMS0YzTWQyQ3NFVWVDbkFVUFAyaWVHSTdkcGZDVHJYWEM3Mnlmd2Z4WVBzL25XakRHR29uUDBzU2pwaFhWCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkl3Q3N3SFptSzFOSWhXdFc5UzQKMElROExoaU5vUDZqVlMvZ3pyRUdoMXJEZkNnc29uMDFiblFlUXBTeFlmRkxTWWlpNnAzOVdJcVdsZVZqMkJJMQp2bVp1dFRSOFFyQlh3SmxyTlJkblFjSGlsdXRGRDdVa1ZSWE5hbHNTR2FaTTJNaytKWU8rRlBLRGpLYnhWeDVICnJ5Ymc2d1g4UFpRb0Exbjg1c05JN0I1WUZZRlBCYUtPMlk4czVKVVZNSzk4YlhFUjN0blRodEtLc3p2RTlDRVcKTUh5YUE3MU8xYW1BRTMzVFhJZGRPaVBqN2JsNlhIc3JWcVJhTEUwTko1amFhY0pDdWd4T21xem5ESnhQWm1SVgpKVFFCWE1WbEhkemdWWElyVkFGaW1aRTFLMjUrMklqakFzYnZ1VzJRNysvajBvQlpwcHdDRzM1VG9YdUwvSFBZCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkNmSVRNWHZMMVlKNjVtSlNMSUQKa01kbCs2NHY4bThvUGgrMnNwY3RPQlhUNzU5ejUrUkxsbnYvaUU2RjNjZlFySnMySFhFM2Ixd3lKWTBueEgyQQpPMGVGTVlSTHNDY0pKc3hIcnd1Qkk1V0lIdG4vcjhQK2M1VVhOOEZ3cjlycE13NThySHNzbW1PS1JGeTRjUXd0CjZXOEEzOFVDcEFqcyt5Z09kTjJaNG1mbkNUM21GMjRUT1dZYzhVOHFmTE0vR1NFUTBvNzdXSHFHbHB0aVhZS2kKcndDQktsWGlvTlA3VWZKaWFFNzU2VlVweFFlT1lKdHhZTHYrN3FjcDhuR0h6UXVqTGE1U00zclp4bWJCUXo5UApFYW5VL09lUVovTWkzVWNVZVlqWTZHMUxZL3FrdTZnYndoWGNwbEU0d2ZiY0NUbXRqOE5oMmg1UlhHdkF2Vm9iCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmlDRXd1Y3JRdEVnOG56Tmh4WjEKc1ZFV25iU0VBT2ZsVFlHc3F1Z1k2SkNHUGNzSHk2YW1nN0N0U0NJZzJ4ZU1TL3pHdWs0Tk9RYzZyMloySHM1RgpWUDRVWm1VLy9JVGduNC9lbWdIMW9ZZ0J5K2VRalA5eEM3N2hVZ01OdUNmV1ZTSlF0Y215QytTZXpsT3FMeW9jCnl5M0Uwc1Jzem1UV3JOMERUUTNSeXdUU0dDUXFWZ25BRHUvblFzSkluaTVMTWpxQnMxek5PaEJBNk9xNXJiOXEKeVFVWHUzZEJCSnovM3NpbUtiZ3o5YlRzbmczNHFReGdiVTZZSXVvU3Z2WWVTOWh6b1lIRFhmMzRwSWZhUGFIZwpFc2t1dXZZMFg0Mmk1Nm1xeG9Ja1lyeVhVMmQ1dkVZU3hqMzlFUUtsRXJYMWMyWGI1TGVmZUtoMHViRnQrT3NvCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2R2UThQMWVKaS9OWHdsd2FqVU4KMVZLNlNrcFFwUDNFS2RnVmZLRmVHNVc5T01tL1RCYXl1VnpCajhlYVpkM3JNcUdabHM0bC9xRTFrRjdGUHk2RgpUcjNRTkxWVGFRSGRHMFdiT3V6VnZLcXR5dmJ2a0hSL1RkSk1IR0dGUEc0WGMyYWE5UTNIUkx4SjdyWHNBaDR3CjdnelhQeStxd1ZhdHNrYldLRzEzdVZJMFFGa1VNckNwQ3RKdXNxOG9HTk9wbHFLZDhuZllPTDd1S09YL3ZITVoKdnAycUZXWHhCdWhJeDhqR1JMSW1naEdkMXkzWHN6NERhRVU2M1hDNzdnUGhmZnluS3QwWTIvdEFBbUpmZi9ieQovN0taaGw4cDAwRjZ5R0ZxMndrU2x3K0FPTHVpUXZ2ZDdIa2t4VHRCZ2QwOElDM3YrMG5mTFRLOE1BdUFtaFJUCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1ExR0hPcWZqZVdiQWxCMWxCK24KUnRza1NXU3VOcHJVWG9KNEJpbUdLaHpDYkU5K0ZBMWdVdUpwaDZzNGhZNDVGWkJVSzRTcmVCZytDdWhaNFlYbQo0eVRlY3NkR3UyaWZwdjc3TVM1SldtcGFmMG13aTQxSWtjdFg3cC9HLzZWb0RZOHBiNVpCaHgxcXkzcHVmN0MvClQ1WS9Ga09CTEN6SnhyVEhzR2tpNFN6Q04wRDk2V1ZMTGNxNFBFbVQ2ZDZSaFJ3dDBmTFVMdDVkN0phT0dhSUoKVXJiZnMyb0diZ0xsNytZQXg2RnBxY2xwR3A4NjJvSldJUlBJUk1Na0UxNnF4KzVBRTFnc05Za3B0TmN5dDFYUQpDaHdNbXROUzlBaDh0blBSQk42M3dLNkRYT1NBcFA1Vy9GYWFSU1Bxd052djhrN2FCekpUeHhQSW1sbHJleDYwCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0lldXJ6Mng4bnVpYWFIVis5cTEKVFk2U0tDVjJGMDAzUHJqWkYvYkpPUkhIT3gwcVgvOG9IbS8vakNPWm1WUFVucHlja3FiZEo0TlIyM0w2UnVNZwpZMWQzRFVVUVZhdk11dTRpSThBZ09TNnI3ZGN2RkRXV20xY2JrTW1PaktmTVg1emFJVmlHK3dISnEzby82b0pzCnJiTWlxTzBtOVFBT2JEV0dPeHp1TGg1SEtzeEVJTnJuTzFtUDQzL2JvNUNnQWVRRXIxWkpIV1FicmQ0ZVZablMKdWVIbUo3ZnM3bnI5UWNFN2lwOWRPajA5RWpJSFkza3JQYVVJQllKZ3UySUlXNHQ1cy93R1BEb0RtVEw5TVdJbgp3eDNQR1FCNmZCeFV0cThmYnB2bmdJK3ZMWW1uVTUzdEFnSjJHT1U3WU1VRTBTeTYwM3orbjNCQ3lQVUN1VHVsCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlByVlJuT3NSdkNUdzVyL1pMVE0KQVoxUGRGVkU2cXA3UmlPdjV6TXMzT2E2cDlOZkNRQzZHTDlTRjByVy9LUzBYajZWM0tLajZRRlk3amMyeUsrSgpnTVZuTTVKVE8wM1FZRkd2WjZKWXM4Y0JaME5zR0w3VjlESzR2ZmpFZGNoZTZXQTFuZDdvSzY5dVM5elliVzVGCnlFL0NCSVFBZ0NHRllJdGF0aFVuKytmZ1pTb0Rnb01ONE1EbTNIM2pxUExYWnJSYnhUd1V0SUtZTnlUZXE0ZCsKRnN6eVBXYnFWWDJURTJXSURmYVBGOE1MaEpCMlBCKzdDQ3RLK3k1QUFXaXJ4K3prZUtaekMyL1ZtTlBSRTlZUgpCYWRNU1M0TU5RNUNDN2hwUmpadHN5TlJTc1l6R1NrZ1lOOUxuZVdTeUozN0ZmZEdobnFOWmUvMGhSb0ltWGhwCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3d3NzBJRG1oS1IweUZodnpVbDgKajFZRitHZTRNaUZUdk5oYUN6SmZRa2VHWEJleTUyTThJc2psTGU5bmxYQm5sTlBVMnVqV0N4V0k2Ynd5T1BiVgozejB3MlNTcS9LeG5LVXNCWW1HM3dGQmwwOFdVUUpxVkVNNTQyY3I1SDlxV0hUZWtJRENVRTRIallCeC9ST2VXCllSNFRhTGpWMHBGblNJZE9rSjltbGQ1NWRZTHEwVUI0VjJMYjBsT3RqSTNlckt5MzRsNncxNVZOTjQxS1EyZjIKS2hCWUNoRVJMUXJlUENMS2tiTURKbDhBN1VQOStzaXdiZTYxVzZaSVpVUmRRREI2K2FTcHNPQ05sSERiWGpXUwpKbkR5UkQ3L2FvaTZBbytBWmljQXpIS2ZFMmlvMTZKRkMwTzZuY0I5bHNDOGkwdm4rRm9Qall6cjNSQUc0QU41CnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGlmV2NENmxsZmRmU0tPcENJU3EKSE5DZXdPVStMczFEOW9XSG5naDczeVk0NTBuU0tRMjY4cEh5ZWVJeU5SUU9qMm9jdGhNUnVSbGM1OXVlbWRmQgpGeVFjZU4wRGRmSmY4akFkT09HZ3JvbTNmKzdiNWREaFpKUmNKWkZ1aWIwU0NzcDhGaWZ5d0duMHRCbWFZU3g4CjJkRUhEYklJdW0zUVBMYmJodFFSbTd6WW1pbU42b3dMRnRFaFdGa2JEa1FkanU3VlhZTEVGNCs5R3dOTmZOTXcKd1VYaWsrOWpLaWlZSThPVWhYQVJUd3FCODEza0szb3B0ZlVrSjN0VVgvQXZueGJaNFlJVTB0UE13cEJlVVlaTgp0THhNaGliMTM0bEU4SWlMbTZYL3N2UnpyRGdrakJwbm1lLzVzbTF0RVUzVGowOWRReGt2SEd6V2N4K0EzaEVSCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjh5WWVmeFRGU3VMa3FmMUNWRlYKbUVEaWJCNVVkbGk1Q0tVclU5Vzk4ZzdCM0tFdTN0NXpSQnRxMXRrT21aQ3ZEZWdYY1luUENsZDZzMFduOXo1OAptUHlGQUptQm0vai96eFFDMDFmTEVRcVBXVFJRT3hPM0REWEZRQlVId3EwY3V1TkVEQ2RQL2ZPVCsxWUF6b1BsCk5EdVJrc3NGSWdCczRzWXViRHdUTG15UWRnUjZuSTBnb2ZEUy9xdDkrajBJOElUN1FRWU5qMjk0L1A0M3QwcXUKNnYxcHpYZ0NtQ2xKTENBV3VZTVVYbDRPZEk3anNZKzJRcXozeFYrVXJkME52aWU5L1ZOUmtWblMvcmd3TDJkTgpFM1NYdVFuR211RjIzR0ZnREpoOWRJV2ZndXNhamZBS240TEFCZzlQRkhZWmllRjhYYjRRWmFnb1VtdWg0V1NMClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2xlbVJzc09IVVRnM1BtY1hZNHIKNENXNTFFRktxc05KOUxuTitBbzlPUXEzT3NyTFE2c0drUEgrRmNqaGhINkpjV1lZSFoyRkNBK1RlWTVzZ29HVApGZnpoZFkzYURaUHJHRy93akwwTmRUR2EyMlZIQjQ3NThJZ2M1V2NFNEQrYXJLR2tDNUtoYzNOUjg5bmV1Tk1qCjJrUkd3QnpsQjg0cXRSbmpoWjQ4THBFNFdQQ2NuMTl2NDN6eVhDSytOYWJ6ajNHcmpRbmVlVVlkUjIxLzZUcE4KTWZ2UldvNWNlaEhDdkxHRDJZcnpMc2E0M3pEUnJHS25YQ2RCMkFKODZpRjdpSXJQL2ttTEVVYW42Q3U3Qm14WQpsMUp1SzcwVG5lTE9CMTF4WnJtR3NFc2s5aWdId2tOVWwybGt5QmJyUVhESXdqYzVTcEtwRnFibHRRdlFLK2tiCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0VEMCt2YXNVTS95VHZnT3Y5QngKd1dVNStHZTJmcVV0MFJuMDlTcTdvMHU3elNTbVc4UFVpWmpFV1dDeHdoNDZzT2VoWElsRUhncGhXeENPRSt5RwpLblNLbm1WbC9uUHUreTFiTU5kN00wQ05Dem9iS0ZwQU1JQkg5S3YyNThkMVU1MHlJdmJTd3N1ZnU4azFZT1I3CjN5Uzh6dC9KQkkxNUlZejA2MU10cis4eXNxNjNwSlpNQStUbzA2ek9GUUpjM3AyS0VDUk1YUzlKaU52dElhMGsKa0ZKOERSSUF5SWVwRjlVV2l2VW9odmNWYU95d3RUYzRIQ2VTZHBNOHZyL3drWFZUTHMzV0MwWWZuWGZiRGxoWApXYnRzaDI2NytETmR4WnFVNmxvM0JZb2RFTWxkVjNGQUhydzhJd3BXc1NLdzlPQ1VuS2R5MmFBWGR3bHJrOVpUCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjBWTEF2RDdLaUVrOVdXNFVGR1QKMlJjSXcrRURzVi9TbE0rZFYxeHc5NkFsRjZncWg5UE1EUUFNK3p3ZVIyYmhyYjBuNGlrVGhiUXViWU11dUQ1VgpMYmMxYXMvUUxmb05GbWtubHRwdjRBTHI4MFJWOWlCMm9lQXU4cmJSa0JwNHZicm9zNERLV3RvUk1jSXpnRnNsCkwyM1ZvL2dYTUtOWGxUMFRSdTBXYys3dXF5ZHB2VElRakxQeVBwVFNxQlBuTDhHK2JDdXBUVlFSMk9qZXREYlUKZjBQckg2cTFXc0NUU3hrVnIyclFLOGJyL0xKMmxrOW42dUVUTVlZYUk4ekZ5Y2tWY3RHMnErZE1OQytyTEsrbgo0SmV4K1NXNUt4dzJBZnJlaFdwZFEzZ2VQRkRkY0NwUE50M1k4R3B5VW9Sc254N0J0RzFYS0J2dTQ2cEhkWlQ1CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlUxdjR0V21zR3d5WStTUEtUaXMKQU5qY3EvaXhOaTg1MWZBdjFqbDJmbXFkcmhJOU1BOXk4c3ZTa3ZwU1doMUNOd1FvbThGZUNSVEU4MlJhdy9aUApDa0lGT2Vmbm1idy9NbnB4WXFTVGVPTUxmcFZGM2xRNHJBckZSOHE4TlVWSzMwbm1KRjVuanZHYW5icCt4eFNGCmQyMkQ1alpQSUx4dnVxMkd5NlBHQklZNS82R2wwQmdVRHIwZVVjNFMxYWo2UlN4dTlEb1Jjd04wYU45ZWRTa3kKTVQybnRCRURjREpxNDNRM1lhSnpVeXA1WFNjMUx3bWtxZ05mcFQzQnBZd3oyYVJCOHRYT2dSSDYwVnh3eW5XKwpiYXQ4ZmE1cnB4bEYxREdrTnl1SEhTMklhMWJteEdYZDdWd1hWUS8ydUVwSHJwajlubVl4Z09maDhXTmJlNzVKCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnNKUG5KalVobXM5NE51MlNrNm8Ka2h2bkJCNVNzaVQ1enh5SHFqb25uc0NzR09xY0xxdTd6Y1JTUlRhUElXMkc4MzJ2c1YyOHhweFlFcXhadW9JVwpmRW5CZmthaU1KUExTM1FqUkxUTkMyekNrQVl1d2VydVlGSXJmNnJxT2dRbTFETmJLRHVZblFEQkd4VEJmSkNyCjE2ekNPZzE0S2ZPblFrWENJcm1QUnVGY1Jvb29IeSs0c0lFOHRML0U0UVl3OHEvaHgwdlhkZXV2UGI2YkFNem4KdG4vcFNPMndiUHFDQUhXU2tiMytPMTNYZHZGNjBCRlpOUk5CTjhxYk84R2Q0dE5iNGkzZXRwdDJJZXlVZ0FYZApQMksyN2lKek1nbFoySms5VDMrNU4yRE9lN3JWSDZoT2xWSHpQTFR0RlJkTDAyME5rUytLUE1vSVIrVmhPQlJJCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1E0OUlJSjVCbXliT1RnUVhCRVEKWkJWaXBuM1cxTk5FWlRJMXA1dkF6M3NVSU5VZWJvbS9VaWVKbkRjNXY4VGo1VVFpNTJlSEJCbmR5aVdpbVpRaQpyUXFVWVQ5Y0M2KzNwRS9QVWsrNTdaSE1ic05ZQnZ0OFV1WTU0M3IyMmVuNHhKUCs0R2UzZG1DSTRFYnBiTnBtCmpIT1RIa003Ykc3YUtQbGNZeHBEOXVIWWZHaktMdFQ3QjAxMWZrYU5rbEh4bmgyYVpWUFhacHN4djZMNkoyZTIKd1N0U3JPTmRjeE5MZ3ArUzVhWjJPZjdjQ1BXbVlKYXdkWWRmbGxFalpZenJxS3JlRFlHVXBHZ2FWT3lsQWQyZQpnYlhhVUNnVG5QK3lvWTdwV3ZySTR2a0VKS0JsUkRYQ2JwSDR3RnRBUWNqUGlCeWdXV0Z4bnBMRHRMSzQ2bmpXCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFpidGtsL29ESzdhb1loUjdwd3MKUXlVNHQxWUxjcC9GdHdSWnI4U3ZlMEYrcGVUdnlZVkowWmlZY0htMGRzamc1T09nU3FTbzF3TU9xeEpLb1hYVQpMK0ZHWFVadzlOeXU3QWVEM1dUbHd1UWR5MzNtUCswaUdqZ1BiQWVNOEJGWFo4RWlnVzVyZjNuZDU4TndxN0F2Cm94bE5uOElHMHJlRUZsbjl5R1BKUlVSOXVGanAxQzVWcTJ6TStILzN4SnhDazJMUVZMOVVHQTZXd2pFa21IMlEKRTY3OEVLYU1jWk5EL2djQlBaMm9VUzlzOWYyMWNKUmZYVmQwZG1ScU1HbTJYQzc5ajFIa0djWjE3ZWVtREFrYgpTVnJob2lzcDNFRzhoVVFiM2VRUDY2cFlERlMxcjFGb3NyT1RzV242WklGQk9LREpCM1dmNDJ4WjBsNEd0QUhZCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1FXUTR1bk8vUE1QdmNyN0ZjK0QKSHRMT0ZZTDdQMnVlbFlrWGlLbk1nU2h6ajFrZXc0bnZEbUkwU3FKb3JrM0h2aVMzSTBvdzhkMVR6RzVpREsvNQpjbldXYW9QVDBKc1JmM0lmbzJBNktMVmdNcjgrMEh6eWhDamUwc3FqZ3g2cWFIeWNkbzFhVFlKaG1KZGdjcGV4ClRSM0c4bEQ5WEJUR1Vhb3ZWRWw3QWhLWnQzd0U1Vnl3Sy9EakpUb3F5UXdzTitQSzZJcXJvSmUrWVlvcjNRemMKN0JVekdhaSs2b1NlRkVyN0lrdit4QVI4MzNJd1JvVi9BWkxoNjJwUGtYcmxnMm9qcnF6V2VBUnF1NEpFNFpmQgpOWXF3WTRvb2ptWHNKc0JHckdHUldSdDN1a21lVlRpQ0lycE9VSzlDVnVQSlhnMGkzWWZ6QnBjNFMrdHFqeXV0Cm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczJCb0hYSTRTWWhIV28yWUtUdGMKQUhWL0wwa1lhd0UxbnloM3hGVW5IREFTeUhydmN1WEpDaG9wMjloN3lhZGhGeW16VS9jcUtjUW9JOE1IZWw1ZwplTnI4MFJBRzBacHd0eFFYZHZHRFZYS2d5WmZYVTVlVVE1OUVRV3JyaVFOVmk1Q0ErWkY4V0x3N1lXQmozMVlGCjFSWm9UbXRvNXp4ZzNuUHpiZGNHMmJadW1lN2N2Kyt6cDFLWUc1dlF2NWNmWkh4c1J6TW1pT1BIUWE0KzZadUoKWGtNVHg2QjgzckJ4QUhuWGtHNlU5V2ZtV240S0pXcWRtQTFiV1daemZnQklXUnNKMkhRSjQ0UHZYbXQ4aXBwSgpMaXo4TjFtVFJqY1RLYTBOSEFKMXkvTEowRVAvbk1yYm9aRzd1YnE2eE1TaHlaNjQzdEQ0VDdnOFVaUkJVOUorClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjFHOGZ1M3o0K3pnUVBSdnNzMmoKVko3d0ozQW9qb2JmQUQwM0Z6bjV5QXNnWHZCNEFybDhqMlNaWU96Wml5YS9Hc0FjQ2Uvd2VTd0c5b1dJdzJqKwpaK0p4UnlyM1NJV3I4TXRFSHdudndHdWhja3pvYkVlSHNORkJicU1GUkdpdTNzemF2QWVnbSswS0ZlS0lJUEl5ClArSTFiRVVhUGtWNVA1aFZ5S29Zbk85VmxIVXMxVlNJcVd3UG5WbllZOXovaU90b25BUUdIc1ZKQmVPZmFNYmcKZmN6N3JRTnVhSC93blJyblhJNmUvTDE0d2VOWHAxS25pRlRmTUxDY2FhY2YraWFrWGZ0OEMwdm9tT3JXMEZocwpaL2VXNEVHVEgxNVVNajI5YjB3dWtrZ093SWVFYm9ldkdxMUJDVzJpaXA3cHpsUm5hUzl6dmFxMi9uT3NIQW1OCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzVxSUkwTEh5WGN3NlhaMnh2Q0IKQzdjYldOVmpwWSsxdTRFRFpWVWVsSnczOE5vWmZuWTlVUHFMNzR6L1FPTVkzVlRZZzhUWGZjbjQvRG55QjJwRAo4a21sc1g0aGJreFBGdWkra1VIbVJUMXFEQXoybVhZVnRZaW5nellOM0JJM3hwM01WQkVkRldHSks0dEY5VWtoCjFIVnBTd0ZLYzJZOUZzaUhOdkNtbEkwV2lYS3hJY01JQkVhb2dUWTJrUTB6R0RKbC9jWUpRWlNtRU1kV1BBb2cKa05oNXFISUhWSTRIL3dlNmdXVS9HWXp0UXRITHJtT3BqUExhMWhpdkg2L0RFR01Ma1dLNGY5MEtBWVo2STVYTQo4YUpDOGJ6VS9tYis3RFVOam9QbVFsbnh5NWdPbnFhTFo5TFN1WUM3b3NFRjdwV29Edkt0RUM2YXhEczhmTWdRCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM00vMGRXVmkxYjdnZUhwMzZaVXMKSUZYcGRPdGg0MEZOT3NFeUZZTlVhQ0RIM1d4WHVPb2R2b2JqeDVNczF0M1doN2pTOENGU2ZsUkJqWi9SU0hFbgpGQlhpb0J3Y3FJZFo4Tit6eVJISWtaR3ZsUzNFTE1JTjBGeS81Y3cyVER1V1NWK0tkRWtNVE1BYjB3alpQU2JxCmxmRmMzcWREakZYc3hwbktCZjBIUUJRVHJMYkc0Y0xySnRLYldDWW41VWNMTWJmeWJZQWh1QzlKWWpBU3RSNUcKWDAvRlFTMVBNK1Z6MGdWOHFTNEFXbEhwbm5RNDdLTnFVNTQ4OElmQ2VUL1lUcy9pWEpRcGdqSXlEZ0l6QTlregpUVTdkaWl3d293RHE4YWlpKzY4WEovRk93SDNwczJHM0N1cElFcFAzVUtJdDNEZk9qNDlLNGdoRXYrclZieUduCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEpnVHBIdHVKSEMrZUs4ZTVpbGIKR3g5aGdTajd1YWtVbmFpVWpOTktXSjJtcUNuSk1WMjVjMjUyalpMeWZWcXJWZjVJSTc0aTdwQVpHWTJrNjczMAorVVpmNWt4V0wrUUtDcDVYaWdNVE1vRDF5V0s4Qmc4WjR5blhVejlhUEN3bnU1YnZ4N20xeGdtZlJvT2V0SFhECk4zMzdkRjZTN09nRHFIazU3T0pMdUw4RWpsekxNWU03YlN2SjQvSW9QazMzd0pKWHBnZ1B0bXZjaVptN3dtOSsKN2diZldwYmFWck9vRGpVekR6UlBUUHVlbDRpT0hPUW1jTHFJTS9Uc25OZ2JHMWhQRlFlYk1HV3RwREdGd1M1OQpSZDN6Nks5eWlCU0loQWhmdDB5aDdodlpkWUlscm5jOTBCM3VzQzhIay9qeVZEdDVsejhYYkVETmNtRHlDWlVTCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzNXT25JS1VwUUVKNzFlaUhqNFAKaHBrM1hTS3U5TEdwN1F2eEMxMnNDSTc1ZXY4b2xFTEdHbTFiMmQydnpvMmJwTWdWWXRCc245RW1vMXZXK2JUdQowZlZBSVNmNVFUNU9rNnFhSW1BT0dOQ3dHTC9TdHhnTVBPZ3NlMk1RMUs2Qk1GMzRRU3RINVlyRlJGVytIT29WCjZ5ZVltVXh2Zi9QZ1R5dFJrckJaLzZhK2Zqa3FNTk1WZVBYNjJ2cHZRVFhkNGhmQnB6anFZQWNjcFNqRU5oeHcKZ0tmNStqOWlkdWVxNndPQWJ5cG4wN09OWTEyV0lDQ1BGdTRPSEM0QXh0bWV0b2pCL0pFRjFJaTIwOWkvbGhEcAo1alJxbno1dG5OQzRDN0NJdHpyeStiV0tQblNyVENxN3NzalNuTHZORHVHMDNURXZRM1Q2UTdXSGRPZFVFZXo3CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNklnbUd1UU0vQ2ZkM3NsU0RvN2MKY3kzWVlabFlUSUh0NFVOVytQZ0dRdkxkM1dwVGRxWTdNSVk2VXNETyt2TjJZV2NzQzI0Sk5CenlQVmVudU5tQQpvUFJVNENzSk1JbW1uZmhWWkp5ak03d09SN0VNTUNyaVBOMWRkNXQySHo5cmpWdEVjSWpEaklCRnlCQ0lpdEZaCmtuSTZKV2JUVzdyU3lWa1NPaDQ0c1R1TVJOL2N2SlZGZnlUWnM0UU9wMlcwWEE4SDlXRkhXcGJYSE56cXl4eXgKQTdmT3IxZXFHeW04Zmp2UVpSUnBYaHk2TDh3SGNCaDFBSkZBOHpCWTdvRmdTbDZxWnZjcVRxVEt3NUxFZStrNApNcEVVc1JsRVVXTXEwcTB1bFJCbEduOUtkZXVmYWZrNlNhNHFJd3Z5NTdFZkl0cVk3RzBwT3dab25VbVJhUHJKCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3EvSldOR0ltSE9sSSs3T2pnYU8Kdm1uRG1VWEZhTkV2VGtkWWZqZ2d6aGJuTEkzTDljbmVnaHpRYmFTSFc3czlEa0JUNFFLaE1VZDNrSThDVjVVTwowQW0yWkVRR1pUcmo1ek9YRlI1WFZGME9Vb2tIdTBMdVhJWjZHclh1TndFMzc1cFBoc3B4NnlBV0M2ZGlTWVVoCmF4cW1LRkI2bndZRWJTM0pud0tBaU54OWNPL1c3a09PY2FyaFNXaFdCZGd4R3RXMDNyOVQ2Q0FDNmhoY0pCK0UKNnBpN29mS25kbmhiM1RudVFxSnc4TW0zdjBPdUMza2h4d1E0TkhGYWNZUVR2S3J6ODhMUkp2aGpkUHF2SUZsZgpLdjIrSlY0Q0JBMFlQVW5ybVVkUnNiYTVnMXdTb3hCd1RTVWFZekU0bmxRakVQUG9ZaFVvVWczREdSUERMOVlKCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbGlZSHRqdmp1NmJDV3EyNXBYQTcKVWlvdjk1c2FpNVlZOGhodjhMWm1BYVVCT2JmRGIxSEtNWTdoTGZJZUZlREI2VUYvMVNsWFFEQk9DRVliby93VQozTWhnekdxUDlBTVpKZVQ0Y3RIV1hWOHh2enZ5YkxyeEhVdmF3UEV2OU10WjhzL2RTSCs5d0JTWHlseXdLbDVtCjhUQmtscDNTRlMvUmRINm5PTUc3V05XcDMxdEJ0Ung0ZVhtaHV4MjZqanFCTTRGSkxUWlV1Ni90YnlkWXdFKzUKSHdibkNjMEEvc3BRa01YSVZNM3g0ZE1sQlV1MFNHT2JRRzBCQXh0b1N2SXQyMVJEaGtxTGY3Sk9saFljbFZ3TQpTOHkwelFjZlAxSm8rU0RaR1hnS3IyMEJPSnI4eUZFd3NVVTRIejB6Z1pIanVZT2NNZkNURUNTNlQyLzE1T3loClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFFjRDBiR0pibE5BN0R3UE5ZM1IKL3M5VjBYeStmaVBDbWNTeERJbUtpeG1OWWRoMjJ5eE1SOTUwUElNM08wSFZDa2xEUWMrWm5CYktwcjM0bkNJbAo1YUNPUnhuVElrRzlKeVZDdUtWV0VqRGo0MHo1WFJlTTA1NU5uYVMvU1picmFuZDVFS2dCWEtJQmlqL3UraHgzCndHbU1MK01QaGZPS1YrckVFUUdXWHl3TDZFMTc5MUJ2UW0wUkNwVFdOckE2VDVLYk84alFrVWZtQ1prNFRZOWcKOGlNYzJBbjNKN1c4UUJyc25BdFY2WXJyWWM1TkdydkpjbTU4ODR3TGRTdms1YkJOeCt6RHprdk9KUFhENEtzcwo3aVptMjRZaGgrcmg1aU05S3JlUWhLR2RzdUErZ0U4SEl4NnF0cldSQWtwdjB3aXVNSzM3YTVzOUhSaTB6amV1CnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMERuRk5jTncra3pMcnoyOC8ySHIKaTZoVk1SY0E2WE55c3pEYXh6Umt3QkJFTzZRQm8yQmRoNHRDQmxjV05UZDhSaTU2Yy9ROXFzUGJadk1Ec1RMMApSUFI1MVBlK2F5Q1Y5YTVaQXI3WS9lNWZZRUpXYTQ3aEZNRkdqRFp3cmQ5azl1dmNxYlc3WjI3Y1RndTJuTDlnCmo0cFNkbGdmdHVudTltV1grZk81WDV5WndzYmhTYUtNNDVac05uQUxiTDFuenVyM2FBRnNuQ256aVM2aE5nQzIKQTVGSDdrZzFyTEZOemdQNDVNaENJTDZyRjFVbVJrUUV2aWRkRDdjMmV1THVRK2lTU0dud2wzZUptazZTcmtRLwpkaUdIUGd2WTRscVkzdXRxNjMxUklhZ1pKZTJhL2o3Tnk1L2x0c09naWh6ODJCZTRDQVFveEtubHBxaFZ4RXRvClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGNNeTNIZVl6M1p3eGlwUHRHUzIKZGwrRDFYWTJ5dEVEbm9WSnIyRDRlREF1OVFVcjBPdVpwb2xCRkJYTmRwaC9rVk14cC9vUGltVU1mQmtrak1NcwpVeXZ5b3BIamd1Qzdib3FoaEo3dk5BT2lnSW41OTVQRjVDaHU0dmoyM1Nrc1NuRThYNFJ1SGhEWmFqanRmaVE4ClppcS8yMElHaldpNFhReFRNRjBDTGNaSFpCZmovVmZFQnBzMFprcklOSUkrM0o4alJnWXB5T0F0eDlpVzBQa04KeDZFOW1JbmxndVZXRmxqUEZSYnZValNoZEFYQW1kTGVENmNHZlNZQzBLUEF3bGdFMDI2bkdrQWw0cVh6eE1Qbgp3dDlEVFVMSVc1dHpqTVVlUkJOWUljL1k5RUVHclE1UHhJM09KemM3bzh5QnpOR0VKclVodGtXalFGeW0vU01jCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclRnZ0tKMmI2U2lSSWtRbkNCRWEKVjdsTzhSWTZuV2JZLzd3S3lmdUFNUUFQY0lpeExObnhBd3h4eVJPRTZxZ2tlSXpXVEJXdkJ3d2pqUi9CRE05OQpFdCtKd0g4TEkzKzBUYkxTSC9FekFta0k2RTRoa1BXK2h6TU5PN1ZXZFZUMkx3Wm83dFlyY3NqWDFHRU5zcko5CkFJQXJkVFJkNUtELzFXMGM4cEFwdTNEZGZLNXB0dUpTNks3MGViSzZOaVZlSlcyNlJRUG9RZ0RhMVA3VjBUTnQKMjA0SzJ5TmdMKzk5dUhXMjI3QXpFRmJMaks1RUtjVytqSTQ4cEpSTldEZUowcTFld09VRHgrRGZCNmdib3VmZQoxU3U1cXUxUlVDeGEzZ1dZM2h3eHJuQ05MUmJvL1h6L3pMZHhlVnUzQjJxSEtTQmpYMUNmN3hJaWlqS1hyUTIwCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekJYZ1dTaVF6a1RqT3dCZnptWmkKNGZmVkZZN3N6ekc0UEhHOHBsTXAyT1dRWXRCRlY3aWs5ZndxR0RrUnJObGRjZkphOTl3dmpjdTgvbXUwQzUzaQphcExPcHQzSGtoWjAzRVNSNzRzRG54djIzTHE0MWxTMjhvZTV1UGpHNHhmR0VqdE9XVlFDcG9sN2xUaTdQektSCmZZZXc2K3VNMWRjNWkrN29OeU1DaGxuUjk2SEpvMjVsRGlnU0MrVHhUM2JXV1NHOUVabWF0OVUrM1RrUXkybjMKc09LR2wrZGxoKzZBdGUxcko1bkQyTm04THdWVGZDczhOSWF1c2h6cGQ1eHFNK1UwOVBMRDFLK0tLV29uOWQrTgovYUpYMWRKWWNMYWJ3SkRlSlhkYmlDVkRGTHZOVVZCTFk1YWtGS243eHpOb05BSVR1N1p0aGkvcEM5U1JSUWZBCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnFXZHU3U3pzM1JHZ2FwVEVPclkKb0g1bUFJb28rVHlqMDV5MXFRb3RySytpc2p5djhuVTdzallqS1RKYjJ4ZS9ndjdGeEFKZXFUaEpjeWhvVTF0dQpkSldTWkRTZGkzeUsrQW1nTU0yWkdzMGlUaS91cUEzWmR2NGFFWWM3djZNYm51KzhUYmxhZ3gyMVc0MUNFeUdpCnd1R2pTQnRMZWdobjVrUFNVWlMxTXQvUVlrdmNhRlFCeVpUVm0xOFNGVFZrQ2lCUTVGdkVkUDIwSk8wSEIxL0MKSGpjendJL0dMWUhzM3k4b1psQXYzSGdaVTNhbUNrZmFZSU5yWkVpRTNSRDJYQmttQ1hMbHhkTHQ4MkFTYjMyUQpmRzg2MFF4d3BOUzlEemlUNlR2Mk5kRkdnb29BUWtVK3lJZWYwZ3cybk9wbStwRXVjcnZOZkc5bXhmVE9BRkR6Cm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekVCMFFMcmtGTlp3cUxEbTJoS0kKV3JiYUQ2WGtCVk5lL1BocWpRdFJRdVJJL1JNbU1vVFlCZFliVkN2dzNOczJOTWpqQW5GRTJvUXM1eTRoU01COQpLUzVjbXU2QXN2VFRweXdSSzFuYWRvUml0K1JzU0VvWlpSNS95VzFKeTIwWFFWcWhGTzQ2OVJxTUJWUzdBRTRpCldxL1ZJcGtPeGFycFo5QWZyRjdMVGNsMEFRRUpBQjVaMmpYYnNoYjVUODRYcHVCQldLMFd3UWxHYko0L3N0c3YKcVVCOVlRVzIvNHhURG8vM01qS250MCszTmdVVjgyR1krS2N3TERCL1kzNlJxSnFGSktuTU5YS01hQ3cwc3o0QgpsSkNwY2NPb01zYmR5TUN3LzFiV3VPakp5OVJ6cUtjMWtCdVBUM0JLQjVlQjhsd1FYNjNBOFRTbXRxZ0JkRjFLCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXVwd2hDV0k1ZERCamtaK0Y5QVoKc3J6U2FESDg0cXI2R3drUDVINXJGQzBNMU9VbWZNSUhtcmxmNEg3a21qZFZSWEk0QXVWM0EzaUg2VS9qNmdjYQpvVXJkYVp1ZzY4WHpVM1BuQ3QrNFpYenQ4TUc2bVMyNVZyalF3KzhPSjBFdFY2MTM4aGJYVmlkU0R5cVBFNk5WClhBc3VHdGhDaGZCMXJySThVOW52bkhhQWtQWktjRkI5SjFQMy9UTkNqVXRMQ3lIMEdwc0wwL3F2QlA3djI5dUwKdjF5Um1qYnQ3ZWJ1ODNJTWNrMElBSUkzcHhRUHpzZ0ZtUGRhR3gyME91Z3JsdS93bHdWM2Izc1lMaEFCeXRWMQpyano1TTJVcDgxYjZ2WHFKTGdwdFVBQkpZWlBBWm42a1hXK0VSM1l1UCtXS25XRGhmcXh0TEt2YkNFVVcwbytOClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbWx3bFJSMXljOGViclVpWWRoeTUKbHY5US9Mb09oY2NmMVgrN25IWC9tWDZmeEJ6SU1uRzQwWFRvcWJYUmt5V2taL2pnRU9zS2wxSWFDUlIwREYxVQpVNzdyRk1rOXA2b2g2UlVaUUZjWlUrKzlZdE1veHhJRVZEbVRod0Ryb09PdFBLQUJ1S0ZuK3FHcDVqZWRyMExBCmN4OUtSM2J0UWVlSmhOVDllWUJ4VmZEWURFYjREUVRoWVpjU2MzT2d3dmdHZ3F1N243dXdaQTFVQjdCMUN0T1kKWExNV2ViK0hWZktISjZLSHlUSUtSSFp2d01vWDRGeXdZVEYvWXkxNFVSMjhMcnVxV2VwZmhTdmZEaTQ4ZmhicQp2R1ArQWNodHdUVFRIc1FuYkdJM0R4UkNzZnY3UkNvQTg3YzBoOUNKcWpVNkZkTXVRVy9oVE5TVHJ0cUFWemxSCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUNJSkhGZ3lWUWFLZDdQdDlENmsKUW1CRFMwdDJrTitWcFQ1cWUySlNyRG1mbFhnQ0UxZWtFbWpPRTR2TjhhZ1dJQ2poT2ZheVZFeHB5YkJhNGtmNAp2MGNTRUE1eG9URjJDc1NOV2NBTFdyTmVvRjY4RXV2YnZjbkt0OU8vak5pcjZZMTdkSnBXR29GOEl2M29EbHRQCnp3UnZEQ1g3U0JSYXRaS21VdGUwcHc1eFM0SENIb1pJU0ZwYzBjSWduMWdNRElEQnNTU0lVYkpEQzVhdGdKQzkKSllwcmtpaHBXOHR1Q2J2V1RPdEQ2bTZzN1NtNDVVOEF6cFVTcXhsREdDd0RiZ1MxdlFmNlAwdDAvZk10VDRSUQpYdnZyOVozR29yV3B2dm02TWZwcVc2eDAwWm5kTkhIOWtmV0tEK0NEQitub05zRVFuT3FHTmE0N2liWS9kVVpwCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1JGME5tRGZjMUxDZStwRUhKb3QKbzA5cUQzZzdUSEJZV3ZHQzFWcXNIdlRFeWo3Y1UwQjJyUnRVcWkybkJ5WDQ4OTd1TEIyUnhqS2N3SlhLNkwzUwpyd3B3V3cyM0RCUHJLUStmUzgxZ2RvVzlnTGJKZGQ2QkIzbWpkVE9HVGJpSDFrR3dMQ09NQjBMWVN6ckFVREJvCjVra1lZc1dGdytWalhUZlp3TjJRODBEeXRGSUxNOS9Zc2Z4eVg5Yzk5YTVsYWpZM3ZaalNtNnN0V0Y0bFQ1TWsKYU03OWJIWms5cGM5blE4NGVjYU9DbkszWTJQVStPNzJ1L1puMXBEdEZuOXBVS0xkZm9xTFNSemI3SkxPaEFKcwo1WEI5OWFta0dlZmZlaFF4M2xwdWF5bTQ5eC9PajJDaWY1cjB1N0F2NlB0Z2hiMzNnMGc3UFVuTndGZnlRYW5kCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc096S2pSUTIxNXVKZmJKd2lPd28KeXF5eWRzMENnK2daRFQvRTVzcVllSmZYZUdxbjJQYUwxSGFzSUU2MHRtcTcxRndZNzdLR1RRU0V3UzBQdE5RUQpORUVyZlA3ME0weDg2SDhra3g2RVBmRmFnOFhSbHp4QUNRWVRUdlhpRG95ZTVQZzF1NGZHV0p5V2lKSmFxTWEzCmozVVFYeGxIdWt6ZWIwQkdDSVhVVktpa0d1SzFOMlczRHBmbDh1Y0JEak5YNkJzNTh5ZGtwQ25nNHExa2EyWFUKNzBkM0trcEF2ZXZSSVBpVTRTdmRXcC9LNnh3azFPZHIzVUNpWExsc0I1dmNUOThnSjZtRUcvRS8wYXphQUlEYgp4Z3l5bEN1cTROOHNiNWJjaG1aVUxlSWNuVXZpbThWeldPY0ZrSzRhQ2ZraERkNWU4bFlUWTErM3htSWtUV2N1Cm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGpLalg2cHg5MlY5NVpKUnUvMzIKL0FZSTI3Y2hQYUJhUnkzc29tNGIvdW5aanlldHluNEpyL0R4TEZuNUR4NkxNZW5uNWdyVmpkY0pkK0I0VW54cgp5bktML2xjR0Nsdy83T2x2TXUwQUUwOWoxY0s4UXlTMVFoNE1wR0QvK3RmMTdkaXV2NitUdHh3MUR0SmxpZWoyClJPVFJBK2J3N0V1cG1GQjVLcTJiRytuc3hVNEs4Z1NNVUtCRUdyWlVZOVVkVVdvZHIvTEh5R2kzM1ZaSGdONisKN3U0N1BiZVIwT3FFY0U0ZU9PN2NpWDNackI2NU83K2gvbzFSei9KNzZCTzJCTFpDbjBWbmEwUjlGTjBhU0RtRgo1QTQwRWgyL0pEN0I3bnhyQ0U0UWRoS2lzUUQ1SGZleVRUZHAydUJWN1lKV2plOFg2UGJzUk9FYXRWeXJNS2tLCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0RaSS9SL3J0Q1ZXVXhLSG1BbnMKNnZLMG4weVFVVHVRZXR1SndvUGR2SVdzcFBBdW1Rb096aTNSdFNHUnhaWk9lbllab0diRlJkTHFvYzFWeVoybgpnU0x6SElUNGZUVXNRYWQ2am1HVmFBVVdwZ2grK2xwQ1hUbEh5M0NlRjA3VUJZMUtpQmNId1NKd00yWVp2c0wzCm5YS0dBcjdaQVBSaXNibWN4Y29CWkNEaFNGbVR6bEV5dnVGaDNINnZrSERXMFZ0QU16eWlDMnIxSkErbHc5b1kKNE1CZ0xqMzFsRVcva3dEYWNkYTV6bDJ0MlhPR2E0MG1Mb2dlcnZZd2ptZ2p6bGlleVFQMXNnMnQ0dE4za2x1eApkbHRMdnBaaU56MndhWWJWQVU5SHUvVkY2T3BTVWp3WEFSUkRsT2xvR1N0S01FcEl4eFBBbGhHZjllN3NLb3V2Cnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBck9ueWw1VDl3V2tUZWNoZlN4bEEKZnJ2YW9ZQ2FiQ1VzUmtsTlpBNWw1ZEhGbG9NQUl3WlNTWWxKbnFXa2pERGRtQzZLOXdDQThiNWdzSU1tYmtQQQpTellGMFlEVGRlSmVMZW5xMG9Sby9uYUpuRk9tK0hHRkR3MWhwWWo2U0pkYjgvclBWOGlreDV4WjRQSHYvTktCCm1ja0swZHR2MUVvUjQ4Qm9BYlZXRXRRQTVKUEdtOEhkbXJ1cTJTMkUwbkdyNEZ3ZlZvbE5SMmRmZVpxSUIyTmkKUCtuUUdML0xJMnNHeFNWY0dmZS9uU1hxdndYUzFMZ1NRamh4N3BKMXNxRFIwNTdXejV4L3haWmRNNEt3aHl2TAoxOFJ4Ykx2dGl2M0pjNlBRaXdmVGZNeVNMQ3FOOG04amFPdUE5ckYxL1lObTQyNno2RmJkc0RGVkdidU8yWlA2Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlQzNU8rVFBUQThKWWpobzVpV1kKMVVnMHhCVjBRYTNSaE5uaG1BVE02WFduWHFHRmRZUXZuQXc4cWZkd2pDU1k4ZjJDWTV1bFoydFF1YVhlNklKSQpnMStCbmtiQ3p5MjFzWmFlSUdndVlnQlk4RkYvdDNDZDNyQTlqdnZ2TU1wR1FJRlZWVmFkSHphZmxDZDVTdGxiCmtCTUZteUFQcG5xYU8wQnBYY0xRMUg1SkFEa2F6LzhqdVJ4SE4zTkZxOFN6WWxaTmJqMGF5N1Z3VFVNdlk0QnoKbXJBb3NSYlAyZnpZNW01K0RIRVdEdWRmN1RpS1JmUTRFVnZ4QjdoWFZXeTl4ME9xbnNndXZ4N1NKRzRaZCswbQpxdUN3NTl6RGtQRHFHNlZnWW9DcFQ2YjJ0b2JLQVhpYnZ3czFvYktDNVRBaXJYRzBBVlRlWVlkQlJtYUdkWHcyCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWY0SDU2VElObkJKcUtFbk1UN00KVFM5UldpTmtUb0lGR0FERk5wbnYxMEU4Ymh5QmRtR2h5TzR1YkIreFZ3MTV3ekMwS3Nycis2cWhXaURzMU8ydQpxM1phYnVodURTMjBud1VPd1pjcU5qQk1SblNQUkhmZmZkZVM1eEg4ejVFVms4UmN2SWpqOEVKc2ZxN25MMGh0Cm9WakxKY3daN3JnazIwSEJSamM5MVFaMEhianNLNWhiV25mTGpXYmhZMVRWL21nWE4vNzNaRXJ3SHZhU0dXdGIKa3BHLzlkbXZtN2Z4WkFtaTFLcVBXdjVhZjFSQVNLT0V6dWMyY25Gei8rNkxyMS80V1RwM2tPWnFhQ0xPVDJ5aQpYdjMydGtBRHptMDBNWXF4Q0l5VGUvUElHemZzdVZidmFFUFZuSTk1b1Q1L0Q2RXhuYk1zc1I2bkg1WkZacnlhCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWFzNVdKSjNFSWlYL0FyN1FCemMKWkFYUVFmRUNibU5ZYm00Nk9KVXBpbEdxaEs5NVJMcGFva3RYVTdnVkZwa3phakhPOWdtVU5sWmUvK3ppaS9EWgpGaVF2R2R4NXRoZ2I4MklXSDVHdklZUmhjUzBhZTdXNnRsSHlQTTVGaDZPakxPc01PWitaamdyT1I4M3NBSytYCmlmS2U0cnlRZzliV2lzQWREQXJLcFYxMFdjU0hxazR3ZW40b0lGN1cya0puY2h1dG01NGlPV1VTNC8zMmRhRWcKb3BML1FlVTV2RitIUGltOFdrbGY3QUQ1bGhGeHRHNWRCV3QwamZ3YUxJam4vNllOZEIrc3dmNzFQVlBXbTJVdgpleU9TV0Zrek1aeGJOR1ZNR2wyUHdDWGRhN1FtUnBxWDMyeGVvdGVHRXZHOUtJVXRHTzdKWlJ2QTg4NGhxZVlXCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEI4UWlQS29aekdlMDNONXpsMDUKVzFXU0J4aE5BaGR5T0dDNEFUQU1SNWNOOVpyeTdhaFhwQ2ZCaXQ5b3dsUHlvcjVvaGdoeGswVTcrcmpuMmVIVApOSFVBUS9sN3ZVa3Y4WnNIckg3QTFEdmxxbWM3SjN0dTZOM2JGcm9TUy82RlVHenBabUxmME9HbEVrVWJvNXdYCm5zZUg1dSt2RnM5L2Y4ODZBTXU0UnJOcm1ZTUlrd3kzSGttWnNsaFVDV3FDZ05UMmJ0WEo1WEZIaWQwdDlkV0MKbDAwemRDRm9wVjdTSUhFd3BQRjl6WjZnRmE4N2tYK3lheTJTZW11b3pZSzZzdEJZamdaOUJjNG9obElRTE1wNQpUd2wxVUJzVWRDSW1pak9lRHVTSXpCVXZBYWhEVmRWM1k0WlhXREdQYXRjQnRaSzFNT2h4cmt3RmxwYnJ5Mi9oCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2RySmEvYklDTi9NaE1SNXJYaHUKazlTVVFhdjNBUlE5VFh4TjYwVDZkdTF1QkVDNUcvQkFNczZFMUhrR2NUbXUxNnJKY2pvM3B4Z2liWEE5alAwQgpwVmxoVHVQU0xwVUJIaEE5akQ4eEFnYy9rNGFmOEdocEl1Yy9BRU5EYm5JTGliazZpZUU5M2FnNzlUKyt5aGcvCmFVME12MHVZNkpWRFY0Qmk4c3JtUEMwZWZoUlM4NzNEYVdLSUtLU2poMUdxeUJwSHNUT3lGRGRGYU9pcURSRSsKM3VFa3FDQ3FEVGh3Q2FKWU5YZzVrS3RCYVhiZWtyaVpTWWVRenVpaVRxTnBTNW9zR2lQMnFSVytFV0FFSldDcwpkc2JHZkFNSHJ2R3JxYWJuSyt6TDRYTEZhQTRzc1lSOFFvM1R6MHJNQjhvTUZGeUdCSm5IbVRtVFhXdVBlSjhHCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcC9RWjkrOFFTTkU5SzhFckkwbU0KdEJBWkpqWUZUQmVXWk4zb01IbE9BeGE0SVBTaGZnZ2toS1ZBQTd6MGpVcGJwbmRBTUl4RjB0N3ErWklyL2RwMQo3UmJTYzh0VzJCUDIrZHB4TXJteGpMdlZpc0QwUW1rZ29ZOWN3RXAxQnBBbGRnRzN5R0lKazBTN0I1NXFiQ2YzCnNEVGI4R3VIR3VrbkhBM0wwcTM4UVlQSnBGMmcxM3JoUWloMDRiWDNIVDhqWDhDb2VkT0RHUXltaVhuMDI4bnMKbGsvSHJxd21JOCt1ZnM0VWFiNHBEL0hXZEo2UXZGUG1sM1pVTU1HOVNUR2VnUG0yS3ExTjY2YUdlNGVJT2g2SQozL0VXQWNZN1pwNFhyN2VmZlAyRm5GdXFud0twalFiQ0dBc01PNXozSDJGRCtyUFlCZG5MblVnRWoxMkloL29FCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0xuRnAzQ1JtNFQzOE9tQitqVkgKT25QOXkrT1BGeUdqdEpWVWRtcmVoV3JYYlNBd0p5UE9BNVY5WVpQTXpRejBtNEE4YWlFOFMreFdmUkptY1MzSwovMUlNZmw5cE5qQ3JxaVpkSXNNMXlWeHNzczVLcGo2Kzg3N2hoSE9zejNDUjlHUGthaEYzRmZhZmtkQmVzN3hzCjBqUWYvQXFBZ05rUnVMblNLRUQxbS9RRWZoeE1jTllwSkdTMVIya1JkdzBJaFNTSXllMDUya1lLU0FVVzNSTnUKQXlzN3FKWHhxVVhRMDdieXM5V2liT2ZLWlV5SkQ3OU9XZGN6a3FNOWV1UThvMEtkanRYUDZiQ3MwQlNXREdodwpHVmRGdkdockxkRUxUV3FFSHFHQkRoUDRDZ0hGN3dhei9RM2M4ZVlUQVdibEprN1d3NW53NDdnMHVJdHBPd3pDClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3JXSUF1eHo1bUx5SVRzVjA0cmQKTGxLNEg1M0g0bEs5Y21MYXB6RCtxakFHWWhzSllwK0k1MCtHWWwrRitIK1JZRnovOS9KNmpZNUdCWjhBNlBEcAplQkdxSEo5VHh1MTdrVndNZDNtaGg0OTJFL2ZCKzYxL01hTk91UTVJSmJVa09pOVp4N3o5bEFpWXFORFJ3MlpiCmYrT3N0b2dIYVc2Z0t1MDFvVnVCNnpXU0NHUWtKaHBLS2dDVkRTOU1qNkk0aWxNUDd3MTF4ZmVoS2lLT0VqRkUKcUllM2hySWZpWnkzYlVoVG5iVXVudkxiaHVmckh4aGh6WWErOTVPZUlNTkZBaEZtc3Z5NE1hQSs4ZVpWbFFSQQovQ0ttS1IvMnE3OEtKQ0NHVDV0eUh2UUxtZm5JOEo2aGp3aEZxM1RLeTFiZkVwaFVJWlpGWllTYnQ2S2RZMTBOCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3N1aWdXdlBLdGhhb3BBc2Rlc04KN1ZCbmhvZGh5UnZEMnI0RW5HTmxRYWJYZERqYXJ5VlJBbEJFVnJFNWMrNVZHSU5PLzRKMVJ6Q1pteGNlOUxjOQovYzBqdDVqdFh1STAwZmZDQlE3d1FiUEwySk9NRTRiYUZjU2FDSFRraDBZU1Y5STVBanI4UWFaQW9qN0txR1krCjFodC9CcDFJeTdSQ05QWDRGdk5oTDNGWjJheEZSMzZKL0k0cUVxSTJXQXdMdUJzdERWS0F0MFVhZ1JVN05PVzEKTlV3bndWdlQzeVBKZlRjVjdETTMxREhpazk0QkxYM1VTOG9lSWJHSjZscXlEZGh0UDlzejhhdndSTVlzamRzYwpUaUp3aEIrTStQdmt2azIyQis3Z1hpTDY3K2MvMXg4dzk5c0RDSzZNQmdaRzNDRDJTN09ITFF6OEtvWlZoRGxsCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXdUSm5qcWJ3ZnordHZFOUVLV0EKK3hQL0NIU0R5WkhCYW1JZnFvN2pBR3dTNGRjVGV4MmFjUzhJa0Riam11R2lJcUJ2Z1FoUGRiR1V5VU0wL3hPMgpNVkJUTHZtYVlpb2kzTkFSMGowVWRsZlV4dzRaVEhkME5RdjVtYnRFeDZyMm9rV3k4b3kvVkVpT2hCeVhzOElSCnVXLzVvb2p4TGtQQWVCWVBVS1dEay9zM2x4OENGNGkyeERROEZQZFEwQ2tOb1crUzZhQXhubFZFNllJdXhSQ1gKOXM5dEZrTEFmb0JPVmMwNXZ4TmpoUU0xdHpULzh6Z0JRRUVJTVR0L2l3VGJDWmVzYmFyUGNxY1RqdXdmcDhmQQpoRFBTdXpOWmZNdkxmd2xMN2Y3ampZbFhGMkVVcmJ6bUFkRnlWQ1hZbTZ1RUIvZjlCaEdOV2NPRVJNNDU1bVJJCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0I2clZJOW5xQU0rWmFmU3Ryb00KSW1zZWVlWlB6UExvbklRaHArNmZuVitIUzN6MEQ3emRmVGdZclM5ZGlRdksxc2g4VmRlTnllQkxWMGZ1amJ2Wgo1V0x6a1VLUFBoUllVNlRpbithV0RudnhDdkpjOFl0M2NYVmUxTVgzWFd4NXExMkE3YjFuSjV4NEVKMG8waURXCnZpbHpvT3RUWnY2bWkyMlpFOGhCUHJNOUFQYzFiWEpMRTE2bmRSem4rQ0JQNEZpcTdhWE5qdFk1L0t3aUpVeFIKZ0RaeHpvZkwxUUIwSlNuNFo3Z1FLRFljVUsxN3FnMWJwQ25QYzVlc2hsdEpaRms4LzkzMmhjTDlMUkNaYlgxYQpwVjlqcUhsNEdxVFA3MXBseFZLR0xJRjdhSUN6UU05dk1ZU1JidFM3enZaVDQ2N1ZrVFR2ejl4MjcydDFDNm9pCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2I4OUlDL0xaTXB4R1ErTUlrT28KSURPSWgvSjZZUjRndVYwMk1rWWdMS3NrRGtERk1QUkpHdWxlQjVySjVGNGhYU2hRM2NEcVZPUFN2TWVzc2g2awpheFhzMTB4MUszZEI2bjV2U0xjc0YxVW4rNTdrYlNGR08xUEk2MkQ1bGc2YW1zOXowS1hrVlpTcWxGcld0QUxWCmZpejdvRlMvUkpoa2dXTkU5YU1iajdzMkttSmFJN3FETVZHWmZpb1dLSHpUQjBJeVI5RnpnNlFyNTUwbkQ4R20KTlptM3lTdTdzVFo5SGllOEVLVHB2RTVIU3c2KzZwSkltVWZCOHBNSnEyenBrMWVQNkxXSTFtYm44S3VFUnNIcwpGSm9zOUVUVW5IMzVSK3dnd3RWckphWjduYXhMeExyd2dWR0cyUExsWGNxM2lGSlZFdWdWUkF0UkU1OEN0WWNtClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFhkVVdIWi9QVGxGa29CMmJxYisKdFdiMlRLYTU0SVMvTDR2Q1l0dVNiRGlkNmU4SnVVczUwclUrNExMR1pnaXJrRXpSTGtwbVJjTU4vU2V4TGo2cApuWWpXMzRwOENmRDBLMytEMVM3YUltQlNNYkdtVHo3Um4zMjRVUDJXaS8vUHdmZS83cWZKcjdLcHFMbitmUVRhCk9jSlhvanRLckl6dVcxdkpKNkxGeW9wNGdZSVpNbERsU1c4V29jS1dUbHgwUEdPRC9qenAwMll0N0Fwai9nM0YKcWR0QmFIRmRDSUI5MWlvU1J3WVA3aEx0Vng5TjZ4dFN0ZjBCTEI3WHRYTUg0eURRUGNjV0k5bzMzNkwrZS8wYwpPOG5nNDlvRWJKcGVnL2RFelpqTHhDWW5lRitrb1Q0ZjhMU2ZiamdCRkNwenhUQytPa2gyVGtpekJoQTJ6amV3Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMy9jNXRsdktzc1pNWjBCbUJwUXoKRDBQclNqbXY5cDJGeUN2d3pRbkVYdEhNQUNhdnRsR0JrL291bE1JSzFOVndNOGZ2dEdXclM3anNqbW1jQnlheAp1dCtzVWN3ay9CWExXbzA4S1djdFMyaDROOCtjNm14ejlhNWQ4a1ZQY1hZK1p6dndIWDBJbGorQnBlaXprSGx1CmFxOEc1TENNbCtKR2xWM1JVcXdCb25JcFE1b2RUQktuczFSUllLd3JTeHhzb0VqeEFlMTh1Zm5MeWYxNjdBdXgKaFphN2lVSEJWQlpTVGZsWHhnWDdIQVpTaHp6aFQ0Tm1hSWFXVXR2L2p4bjNwZDhxdXBELzJUaFJBVXUxV00yMgpqYkpmMXJOR2tpOGpLRE5ySHhPZ3ZiaC8yR1MrbktjVEtwMlRxMXMrR2NWWWxyYWhlSXM1VmVQOXg4VWg5S3E4CjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1hpT3pEVmU2ZU0xZ3paRHF2NkgKeDJLWVNKaVpFbEZ5SEwrVnVOcFZ2NHdBZGp5allXZjFJN3VJQXhQdUs1eWk3aU1mSjhJcEg0bURTejR1ZWVIRQo0MlcwWTEvNFR0Tlh2WjB0d1ZxZHpnN2cxeFAxT09pbUtCd05CMVhWbXIzN04xRWxpMjFKQUVQMEJPb1JKM0FsCkpaZ0pqMHI3WTBsMVpiNDlCMVF3WHNIUlVGV0czYW1XNW8rajdQY2F4WXVMY3hhcE9MWlA1WUM0OS9nNDFDTkYKSGY3V2tRWmwwQ25ZTUJYaFE5VXFuSkV0RVhMN3ZQQXY5d3UwY2pWZmExamNtOVNOOWkvT2tMMjE1TGZJZzE5VQpaa2dSTWNxOVNKbk9yK0FjZWZQWTVoQ2txMGQyYytqRFpINW1ObmhnbFFvemRPSWQzN2U2VGZQRk5sRWZ3S1NOClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEpTQ0FoR3QrZnZlVWVjRDJLaU0KOEtSaktnczVWVmcvSWlBdzRFcnU1RVZSVGZVWXNJMnFVLzVMM0F3VGlGd0l1ZEs3ejBFU3pEbWFSREFQWkhhVApYZnc1ZXVBRzM0aVZnWlJCYmthVGtpeHVQVlR4WnpaRVFqQjQwcnVpaUdGV2ppeHRsQ0Yxa0E5ZDg5aVVuZHFGCmkxWFMvRHpQanFGaEJrcGxvZWZpalRnbVB4MVUrUkVPVkUwK2tsUjkrQjEvNE5NdlpRTmtYUXk1T2dOSlZWcWQKMHgwNy8zRzZPQ1BqNFpwVjVTbW1OWVFuRzh2OTVSVTYybG1QcVNFamdQaEV6NkUvNWFyUUtJc1hpL2xOcDZMWQpLN0djckpJamMvWDMyb2ZHKytNVWxaSjh4U2xHRzJ0bUxFTDNYOWdrRlh5Ky9VejV1TTIra0VpSEFPTkQxTVpiCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVdvaHFpN21FSHhJekYyemU3VHYKbUFzazNFQkdWeVhBcUpOVTRFRUdGajBKV0o2dTVJcFdQVC9DcU1vaHJYWHFET1lFRERHeThPUFZqM2ZUNDJpbwp3V2hNbDMweUVUUnRZNVdWZnB6dm5HcmUycjkyRVZxNXUzMFdET0JaZzJjTEQ3MVFaZHdqempMbVArUnFQby9HCnNPTHFkVHlBdjFjUEk0WXVqZytJUG1qQlhjSzhXZ0JEQllocnp3bytmbFg5bUMzVkRiS21tWXBDT1VyQS9YQUQKSllIaFhWZDkrVDJ6MGV0VUUyQWw3YlN6S2dFUERLb2p3REJpSkg0TmtONkRIT1ViNFZnQXNOZjlNeEgzMW9hagpYZW14VmxuNlNWNkdJQ29ndDJOQWxoOUNSMW1RcXZDUytoWWJUeUhoaU5tYWNSR0p6azVvc0FKWkY5NlNzenVPCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1VaM0dXMVZZcUU0MFhUTHlzcUoKd3RpeWI2Y0UrWnhOMlRVbTBvRmRmOHIyb0tSSEZKM0ZFZUM3WGlzYWkvbjNRTWNhL2pTUkJmRjRsOHdaYWl2Vgp2UTZUOGNhWERyQzJHNmw0eUV4YzZpK3AvS2lYdnl4ZGhXNVZsQ2lucmJGTzBOZWtHekViRXBaNjFjNy9CaE5pCm5JTEltallTQ0JKL2RqM0tyWnNEWUk2OENqT0tmMkZXNWs0WjltN0JOemJwRkd6ZStaUHljOGNTMFRveEdUVW4KbU5VQW9WNklyV3YxQWNzaStpdk1ZSy9xeVMyNUk5OEsxK09kUnA3SXZpWVdtMFRMS1REN0NNS0l3ZGpWekdBRgptN3FPbGpVZ0s0d2ZVUFVpKy80aFYrd0JnQWwzZ1RBZ1RrMFlBZFJFNWRQSHZxUE1iNVQ3dFZwVWJQaW02Qi9kCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlo4VjFFOG9IUnZCbEFhYkVMZGEKd3Q5TnJ0TVczMTBaSGM3S2srWDAxR2x6cjcraEpzQXhuc1p1cTF2RDFrMm40Z0paUDIwYW43bTYzbkhUNVhRNwp2MmNpSWwzYXZlMzFmSVIvTHFhOVZNYjlnYlEvdFovOWpBRy9HQXRVOVBFTjlDODdzTDVuWEVzZlh0RGs0WHhYCitJNUh6cEdNbStRU2ZOc0xieXVSUSt2dHVVSUVzVVFLNmtqSjBpbDRmMDNyT2hnRURLdDhxVzhCNytXVjRJL3MKVWIxN252a2pOZm5LT2pTYnB2R3kyeEhIVDlnc2J5TzUrZXF4akp4Q2cyMGg4VnJ2Z2NGUXI0Q0lmQ1JSRGg2TgpiNUpJT2NIWkdObE5vcnBLazRkT1lIWFhPb29yU3ZqMlNqUXZNV0FMMDZidW1HOG94NXI0bjNXZ1lXRUt5VituCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUlvODVwOW5sd21jUVo0UDF1Yy8KbkhBS08yVDNKOWNjTlZ5WlNrbG83VmtoSGUyWjBlQzlXNlUvcnZpKytaQjJscGo5VGFXZ1lXbGFEeWo0cllCbQpLd0NPUnlVWDU4UUVpREFvV2pPazlqYUlFV2dndWc1a04vYnJMazFRQjNTazc1a3J5MFNzUDAva0FUb0o0ekJJClp2VW1SMEpZZStWLzVqajVJd0gxRXNNNkJycDl6TGZqalU0QTcxaklQVUo5eHR5WW0wY2pES0Q2Y2JuTGZPelIKR1hUbTF0MGwxZ2hYSzZLUWNwUVV3S2U4RU5mRE1jZCtkMGpxdlhpd3VFU3FyeTdkQjAvcndGM3NmZlVBS1lkVApvRFJHelhiTTFBeUdyV0cxd3luR3Y4WjVSVlYwT250bFJRZTFDak01WmZNRjVUWEN6cEUvaFQzdi8zOEpJWHU1CkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1E0ckZURUcxQUk5ODlNZ2J0OUgKRlk2RG5iSW0vYXJwQ25DTndvQ042WlJ3N0s1MW51KzkyQ3dWc2FoVTk0bU1xbUdPdmNETGpMYWg1MGdIN0hXVgpBeFBTUXkydnZGellnZ2kxeTNOeXhpdkJlRnlvSVlCcFAxZzdTOW95Mi81VnBmcGtJOGVOYi84OGl3Wi93VytECkVjc3d6MENLZFQ3Si9yMHVDdjMvaFBwZTR6ZkVOb0cwWVVQcldTK2FOek1QY0lLMFVOVktZdmRVTEZRQktCZEEKNys1cm1VZDVJVkdtN09rak5mM3lsOFk5VEhWcFp6ZUhRdWFvM2hKSEZyZWM0K3NZZWxuUVFSYnZkVWhtai9VLwp1aGgzOVpvWk9OM09HR3ZrbnZJVUNkM0FISERrcHh4TXlhQzhyclRSMEtMa0tqSnRUWFFzbFZoaHUwbzZxcGNuCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEIzU1QwSGJhbXJJaWs1ME5iZnIKY0VtSUk2UXdINkFGSTRaZXNiZHVRb29qZUFKTUREclZGMFZSSytBZEFNOS9zL2JHVTRRcUJESzU2ZWNzcUFUdgpuUnBvK1UrTUZ0QTBvdjVkT2E3SnUzRWF1b0NMMzY4bTlsSElsbUNTRVVuVjFkdzVaZXUwT2RPZmdIVEkwOXlHCngwNmNKTEdleEFhTjNLZmxIZzBab3ovejhON1NwNEZkalJuYytOUVhnV0xxRlUwSEdTQkZIM1Rta2JWcDJsbFQKTnFCVGZOZHVEUjR5YXZCS2s1QzlhcWJia1RacG1uc01zSmlkUXVZU1lwVXdhQmJWQ1EraUowY2ZLck9QRVdNbgorcUFjcnVKRE9kc2VvNmMxNnFkSW8yN0xCcUtUY2FIT0JWTElsZ0pNUGtkQzE2U3MrMksvak12dGRmS0NTM0ZvCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmp3YlhUR3YwdnZRL2RiMzZJamgKSHJsMlc3MHQzT0JLRjRpcVd1S25jbGJWN2dONFhsOS9ESlhKK2ErclpiZ3RnNW4yRGsveEFJdzd0cWxCc3p5TwpuU1R2ZUlqK2lvK3JkVm84RHRtM3RDSkdvQXBFak94WGF6TDVzaTFhZ2h6ckU4dVpkMy94Tm9BK24yTWtNclVmCnRtaDhGbW5qL1RZQ1JLSUxoc0IzcUZPeFhBNjJlMWFOaHRoT0E3MzF4QmhZWjJsZUZvNEgwb0p6SVVMY3AydXYKQnpPekdCZ1owYnlRU2NZZDRzTlZlWWs0V2NNU2paUmR2ZmR6amR6L25oYUUxU3hXV1dmc2xwNWliSjQwR0xXVwo3R0JNZjh1VVEvZVNqbnNOVWZyZ05ZYTM2ZEJmQktaUlI4eWNpY3R5VGxYNStvazNKUXdkcVI3VXJ6dHZraHFZCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjdZK3lQbHVJanVRQTUzbjh4OUgKN3F5RFZ0UzdIY3UwRDJPL3FrYVJERWkrckg2eVFqd1dOTGM3bzBPN1RudFlPQXc1ZjdFZmtvV3BIUUk2TC9IeQpBMkJlWTRKblBGTWs0ZXhOUjlXMU90aG91alV1c0Q2WXRCNEplNE9ka3RwK1pwbjFuVWhsWk92OGJsamtCYXA1CkF5WFVWOVFVOHU4T1ppTUJZYXNLcW8yb3d4SHFxZWZrOW9oMEttbE41ckw4VGZwSlZlOG9MRWVUUktNWWZiZTMKY295VURnNXJxaHdrclJHMXM3SGlINFNkdWFNMFJsbW9OK0hHOGFmc1FBSlY4Z1k0OC84WGtvemtIRWFkQmpZMApSM0hFa2cxUW1hZ3lMbG5ZQ0l5ZGJSbEJSazY2ZVBDdTZaTzBaUFcrQkFHb1NMYzhxbU1wbE5sRzVjTnRXcFJCCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVhOaWwyYUUyOXg3Z0dCcldFRlUKVVk0K1pSYlJnMXFZWkh0dTV6QW9ValFWU1VwblozQXNKeExlMzQ1TEt4NHVRWC9BMFpzbGdpMWU0NWdmeStOWApoY3JxMmhhQWszbjB3TUlyRGhmdHhOMDFIRmtPVE9SUTMyN2hnTEdjZUxuSFBRWE92SXBkdThENGc5RVRlSmZhCnBXZSt6QkJ1U2k2dmVHUlU4YlNmQWJkMGpIZjlCOWVqSnZjUkN2bndFenJVT3Z2MXFPbXZiM2tWTmFjcjRJVEwKQStwUnFpSnZUTThRY3JwWjhEdkwvZlF0b3A2aDNHQm5oaDkzRDZOc3MzT1J1OXNiVC9NQ01aSGRvcGRXTWlCdwpFTWhXQ2ZjdUdJaklqQ2tGcDhLMlBHL3ZoKzc1VlVqeEZ3YUEzT2tPTjRWTjZOVTlpSU84RjQzMTJ5NVk4Ni9wCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHcwSnB0N1lSaWlZUythN2tDVkkKL0lNOFhrRVRBMDFydXBKWGNQOGJaU2JOTTNhS1ZrNkRnaTRBZWozZWlCYktGemlwaDdML0lWL091dWJ1aUFhLwpUMVB2Z25NdDFpYTRkLzZPekVDUUtWdE1SQlU5aW1PMXpCTmFwOUM2RFF1ZXNJWFBsSzBORHVVbzREZXFVT0JLCnhRc1dXZCtORWRQRkNxYnRocGhkYkNlVVZWOXhhTU1TTjdZNVhTNjRnWHo4Tk00TXZsNU42UjhPOUFPZEJlWUgKeks1bXYrR2xtVlAwbFoyUHZ5SEs0MVczdFcxOCtyeVhZTkN2UUd0N2Iyczh0dEE5SWxacU1RZVZEUjErUVB3MwpnS1BxczVHZDFHSkdrSlN6NkdMUDhvNW5UNTdPakF4NUVoUUorYTRVUmxRYTBMb2RqNm80WjhEZ25aS3lGL0tQCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnk5T2dhWVFkQW1DZ1ZheFJFdGoKZHlrQ2xVSWhlUGFZSkFQTzZtR0dzRWJpbW1mRnRBeDlaaHJsNGkyY3NKME5icmdrZWFZVG41KzR6SEQvdHIrbApLc1g2cGZuTGV5eEE3Yi9nL2pOOWpqY3h3VHA5WnhhTllHSUlScVdocWQ4ZkVSV1hxYlhjYng1OUE5dkFSd2lPCktpSmxMWHJ1K0ppbGhjUVVSOEwyc1d1UWpjbzBUR3VGc2F3Qlg0a0FIMWpLb3N5QThpSDhIcTVHaFgrc3pZc1gKN2RPeW5UT0hsZUUwaTIzRzkxUGhYM1VWRnNPUUhlOFJJS0NiWVg5V1pXc2pMK3Zwd0FuNWJ5NUJIdjBod0RGaQowNmpEaWE0NjRLdHFjVVBkQjRhMEwvdVF1bWpMWXFVbEIybjlQZW1rM2lvekZGcHBBenZsZ0QzMkF3MUx3WnRmCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG9EOXhUNEtQVUZWclVWRGlFeFoKbEoxdFhQeUFrZ2d4UnNGY1JwN3ZnSndCRUY5ekJZcXh6aHY2VytPUFdsU2RiMUZWcTBSZEphcnd3Q0w4YnY4NgprYjVBRkJ3T2tOVlR1bzc0OGt1K20yb3I3K3RBcTIyU0Y4Y3FvZzJ6bzJWRWt6N0oyNnRxNXpFUVA0L2ZSZklxCkgrM2psMmU0VFJWREtNUjg0Q3NnRXVBOGNxYXM1eGVDM29lcWduU294WUlYeE9sU1pjcHZTU0pKaGZ4eUcwL2MKR1UwZlFWY0NFSWY1SVl5UVhYdkJVSWxVMW1tMVZqRmx1M0RNY2RtcEZ6ODJvSndCK2dveHAzZnVjc29JTitGWgpta2V6TUlUN0VseHdOelNGZ2FEUFpobnVkakhqOHduVWpFUGVLSzN4R29hYWh5NDZ2Q2hORksyY0hGQXI1eHk1CnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzNyU1pxeHBNV0lmRWlYVXdNeEwKNjI4Q29ZWHhYK0p2a2phSUVwM2gwT0VXbzg1bi9FWFVudUVYTTI3aUt3V1p2eGdJRndLd0ViOW9LazBINDJ3eApXdDZVMTdtaHJ4SDcvN09BUGZ6Uk1WNGF6ODRWb3lpK0hENksxcXlDYXBsV2tYVG9EVzd2bkFTTlAwbEpNYXZyCkJVckMvL05JOTYrZUlyNnowdElGNUduVkJhclUrY1V3M0RNdGdHczBVbWZNZzQ2STR2S2ZieXl6U0VXb2QyeUIKdEJGaVpnK0g1bk9zaUJNT3llVDA2REFCditaKytQb3BlQThwbTRldmcva1ozZkxKYm4vVUFqdGxTUGQzY2VWdwpxV0IwMVNtZERXV1hCSTFKMlNCWlpDNmluM3Z2NmY5SmZHS2JhSVVrZy9jaU9EV3htYzJUQUwzemNhS1NvUERCCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenNNNnpmd0tkeWF6SldyQ0ZVQUEKV2w4VHNqS3Zoc0VkWDNLVEZDcGVlSkd4RWFkaHY5L2FHak1NY1IrdjZMOVBEWFJvZ2VRMUd6VVk2NCtpVXc0RAprTXA0WlZ4djZYQ3RLaHlEUE8rNnU0dzNCaEo1VlgreWNwanNMSWhEQ056UFNkL2RiejJseW5EUk1aOEpTU3p6CmozeTlybzExT1BvOUFGWFhGY0JLeGpXMEw0VmtXZ05BZWdBOFNXQ2NMTnFSeXhEWGE3clV5R0hLOWFuWHVSMjUKeTJ6R1FPQkh6ZHZmMXUzUFEvWFgxcFMrbitnRFU1dzRmSGMzTnpST2RFZUtEV0ZRa3BEdjdEQ1VSNUtRYUoydwpqTmVjN3c5eUFpN2N6WXltK1UwUms4VVZkN3djWG5mSzJqbCtTZGRzQnlJZHEzcWZCdmgydlI2VnZQMW44V1BvCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTcxYWV1RHgyVXNlNWliMVM5V1oKTUUzRDlGUXkwWG1JNmNWM2lweDFMOEdORnJoSkE4eXNiRFFMY0dodVJRa1p0UmZqd0FjZ2VvR1gzOVFrMStRZQpLZjM4TEFFWXhWcU16OVN3WlRuNkVhRU1NWEZyaWtFWnVldjJZaUY1S2p1cHAyM3R0cUNvcis2Q0pIL2VuRXRaCnJsVWloN0RxRHRLLy9BZ3Y3bkxPQnhad29wVUNOS0RIejQycW5DQk9CVTQrd2VTTnVkcXZMNkdPcHR1T3lBQ00KZmF0UU5GY2Y5Ty95L1lNRE9hRnl0R3I5c2gvS0xGREg2VlZybVhMQm1pVk42dEdFUVRFbldlU0d5MFZtZDlscApCZUtFdUR4QzhORUs3M3l0Tk1tcEhKcWNGMUc3Y2NndW9vRHFZbTZ3eUZLZ25YdDhrQUNETjhqbDgrdE1yaldHCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmJQRFZ2OStGb2FrdmJZNlUvZVEKWGdpRHNuYUZINWtkYnZaejlwNEdLbE9MM2o5ZzhpYkg5RGk3UnY3SmN3RWhPM3oyTEFzT3E3Wk9hK3B1S2RGbwpDc2ZLVG1rQ25idWlhbGdHeVM0MExpdFZoT0NYZXFsbFhwRVhla1hnUHhqNHAwMnh4cXEwZWZpZTR5U2VwaTJ0CmFZcWxGeUQ0cEY3ZXdtYU1KNWl1ck1MM0h4RUtSdDBJeE1kbERmdGRwaHNncmFBdXZldXpLV2hHaWpwMkZaMmEKNTFHUXhMaDdGdTZibmV3dzhieUwrUkd0c1hpSDJvL2VsY0orM2ZZalZmbU1YRnFGNmFncUUrb0NSVk04NE1BUQp5eHJ1UWtxbkpTVWZVQiswTmVZQWExN2RhdWRlUE90TzFSY2o5OWV4QzR4RG02ejBhajR0R2Ixd3QxOE9qd2FHCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnJNTkVSSHF5cEMwVXRYbWIxdDYKUUZjQTZydksxOHBVMHpCZkxDanBaUzVpK0dZM3RrSXVZekY3YVZmOXRxcVQ3WDByVDFiN09ZNXRoZi84NEozUgprS3JDbTU3TldiWXMzS2Z5MWQxbU1zYk5oa2NKSWhFMkxHMEZJQ3VIc0NlR3ZqVm04dlFBaUU2cnREYUFhb3dwCmxGRUZlS1UrVTNVWmN1aldUZkI0cGlWVzBzQTFIY2tIMmpiRjJNSjBOUDFvWWFpamtxdnRMejdOQUoyNTJXcXAKN3UzOXQrK3gxYlJOMmVwb2tIT0MwaGoweUlPUytzZWQ5Q2x1WGg2MXRKN0xRTmNzOHlhejhCV2Q0U1lCYzBOVwpjQWExWk5GUUMrekJtVWdsbHRxcVpTeUR1YUlQaHZZS3h4OFJDU0liMU52YVkvNFF6dXhrTnhRanJzK0N6bVF1Clh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDRLb3JVVDVNQ3NCVFpDSlc4dysKdnl0M0pNRlovdFQ2cks5OC9vNHR0SGYyZ0JqdkdJSE5JVEV4NVVJMjNROFBRdDJNbWdGVDdmakhlV2dPYytzWgpKc3MxSEdLdG1EK1pJeHRhQ2R1Y0ZEOURCOXhCeXNXVlZaZ095QlQ0N0Y1dkdJTEtRS0RaVHk2Nm83YTdPeGJZCnB1cVVwcmFjUmpYY1RwQWJ1MEVjSjBSc0FiL28yR0hGN1J3ZEdxRnoreXBiVEhnQVNVa0R6dzZNNXEwZnhVeHEKY1NaZHFBaXJSSENld1Y1Wng0djM2Qk42Mm91T0xTZnhrYzY2MEE2b1YwRjhhM3JxeXZGV1NpdjRYdS8xVjB2UQpxb2ZMS1R5Ukc3dlNhNitpZGJmbE9UbUs0REZzQ3doMWo3TmxrRHR5dURwZlNoUDNvSFdNaVd2RTVDRWdReFdTCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3BhMnZnVzRLSWlvVTQ4a0MyNFEKci9sdlBwa0w4T2tFWGpzbk9TWnV3ckhnVC9OTU0wUWd2K25mcngrdFM4NlRqTVZ1QnRVT0tacGFzOFE2cnJUdgozeXVXeGNlOTFlSTZyZXZrRWdqNDZ6VjQ3WGFCN3hHOEF0SFVGcWFETjJqTTk0VTU4Zk9Ba1NlQnl0TC93WUlOCkpTNFpyS1RlYjFKV2g5SUd5NmZLcDQ3NjhhbFdIdGxydHJQWHpPeFNwejR0M1hDdVJQT1F6ZkEzQzR3SFlFRysKSGhUQWRrdExJVzlxMTEyaUN3L2FUSUQ5N05ibTZmZFhTV2Fld2V3QTdSdEtQbEx0SUJYSW05d2hrUitFWEdJbApJT293YnRPVHY5QndwSng3eVBHY1dXMDZBM1dobENVeVZTSyszNHZ2dVFoWXdtekM0NkR4aXZJRE9mVExxUlk5CkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN3ZiaSsrcWcxRExRUmtUbFArelgKUE1FYytSckgwMGNQZXE2anZzNlhpcExYZTJiTFhmTk1kRHN2L2dGbzByMEZ6SU9Fa3daVWdzSFgreCs4dE5kQQppMTF5YmlnVlZoTmk0UjVMY0xLSnMydlU1cFMxUXlQVFhJK3RTb2gxUUh3VHpCUmg0R0hVTGR6VDJ0ai9OaklzCjFFb0RnR1pSb2I5b0xKb3h6KzBnQTJPMUdzaXEyYzJmd2Q3emlWSWw1M3BJQ2hLSElSZGNGYklMd3VNOFJNcUwKNnM3WnFGaXFmRzk2MjQ1Z1MybytRN2hJUm1mZlZFNVp6OUJnNEpDM3RQMW52RGZLZHF1cEo1TVhTaTRrUGVwRQo2amZyU1ZtcENycjg0UURkRzF4WVZ2ODJFZGE4bjdKajBiV0RJUkJvcTNUdElTTnk0Z09UeS9qaEhqOWVTM29YCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBa1JuWFVESzhhZlo3RXFEMURQcFgKbExOMEExaXlkVjNvUjh6V3RmQ0Z6SlIwUVlsbURjRUxOVncrV3RNNG9XRFhpTGNUN3J1a0hGL0lWNHljNzljbQplWm5PcFpDUk0yeW5GRUR3cUlHYXB6R1NGMUhaeTQzZ2tJcEVwMm9KSm9Uc2lOU2R6eHBRSEVEVWhMclVtNTRiCnE0Y0FlNStmZnA5OFhPRVEyM0pDNXo3Y3krcEplNTNocTlYbmp0MzVYL29SLzFVYkZjZHlpcWFTbWRwVU5EU0oKbWhGNnJmTHhKb2FRYUpYVlpNbDJDR0orcVkwaGVmbS9xRlhhT0RUN1JJenlxVitGNEdYd2h6Zlord3NCT3N0Wgp2L2VqbnZFNGlnaW44KzJKMGZuTHpsOFdVNVZ2RFFzZGllSllLRTBtNC81am9sZTUyRjlWRUNWNWd5WUVWMmYwCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGRnMTFXUEY4SWNtM1FOODl6VVYKWlk0R1NWYTgrcVhRZXBBNnRhajQ0eFI4R0NYN1J5QnBOQzhBYlNjTTVEYXc5K3JOdXdpdUgwbzZobHljZUcrSApmTVdpVVo1Q1FoNytOWG02TTh1enUxaHloZ0F2YVliTndJblZHVVlkTURLOVpHcDVOMEpQa1l1ZEhFZjk4cFJQClBKUks2WFc1MEQrRlN5V2hlR2xuZ2tsRDNmVzJUZXpFSGkxRGFoYkQrbzVwa0UyNDFRT0ZlV2tTUlk0YzkyR28KZ1dWdlhpVVFKdDNodDdNVzlVV3hCSGdMdEhwdkdGMXdSSHRoVGdMcHBaRkJnR2J2UTgwMjFGdUxucVEyWXdmRAp6QnJjdDEyRUdyUHVvOXNNak5IcEJuMWl6MmZsNm5MWEhXdmZXVEpTZFM1Mk13TXFxV1hLMHlRUTc0RDAvRkNGCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFdiMWEvODFLSy9IbnpXaHhLYzcKOXh0dytxdi9LbGQvT1NwYXYzMkYwSXU1S1l1VmtxZXF5d2ZDYVBwbmUyWXlZVmFMc0FLTS9HcmFCYjNKRkVsSAppK2VuTUk3bDAxTTRqc0JzOEtxNzhUdHBKVVpDRTlzYk1nTm9DbmdkNldDYytDY1gyU045RlFzaW5xSTNjeVpyCmpEaVFLaDZrZjA1TlpJK0RjKzFrZXJFekdyZ29aUmZDdGNNNU55bmwxTmY4STBSMmkzaDAzb3Nnc1RlMHdEQ0wKR2YvN0o1M0dPckZNTkRCNmUxS2Z1TUgwMXlRTnV4Q2ZURXF4blNQMGM2SHRSYTNKeE1WV1gyWDVSWkYvVFNnSwpXY2g5S3ErQ1pBWUFBUWZiWmx1NzFDL1FaTTY3dlJGS0hUa01yT1BZYmNlRnMwQ2JobUljeFpGTURuTXh2R2o2CnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjg3TktPSWFyWnovUkVhS3Nld2MKVzQ0L3pNZkhGM0dLaGgrTDNNTlp2SjhBSzZXT2tGR3dDRDZ2eHllMlRKMlF6OVlSbSsvMzBHNC9WYzJ1MjI5ZQo2MEN3cUdMWTFHdDZvWXA4Yy9PbkVXMFFOVDlXRlkrc0tZczNHZlNhM1JNL3hCcHdDSTR3SkRrMi9FNDdhbE9XCjdCeTJkRXVUV3BOblZrQVlFQ2ladW1kaVNWVDZxL0hnY1hJMGpNRWl3VUJSd3lHSWZIUHZYWFkzZ1Z3QmpGUFAKbkk5VW1rdEVoQ3Z4SWlWbVE1RkdHS01GZE1OUWZOdGIreFJxeXdxTldlYWx3UUJ4dGRaRmFJQnRIbzZjeG1vQQphUXcvbFZKM2ZJU1hEU2NGc2poY3lGYUxVeEplMkdaNGVTZHNHWWlYRFhDRUpZSXA1YW9jNXVDQTU3WjErSXR6ClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWhFK2t1UzlSRENKQ2d4WDg0QmgKd1lkNzlYREtiSDkwVGVKdnJVU0VZN0RNMEZTUk1nZ0RJM21xQ0lXNENsY2xFU0hIcGc0bVhjY3dtRHBmYTU4awpTd3YreWNIdURyRVNHQ05mQS9jOExoS2ZSTmhseXEwNnZYVC9jaEF3MUdENysrL1YyVmZvMitMQUJUK0xYK2pyCkFGRWQ3NnlwcklGaWkxVEovcXRSUER6MUw3UmUrWWhYaWdiU1d4dWwrWThlNVdtVHBualRXcXhZVlVxOFE3UTkKZmRMVmhReWcveEExeDViZmFFS1pTUHJDWUJjY3RXNmhxdkRIdGhVSlJKM0RJdUpPR0dPWXFSSHFndTdEKzNzMQpQUS9JODB6Z0ZMRGx0Rk5XNWsxMDBMWVVnbi8vM1poRXZ1NzFjNzZBc2huanJsdUFtZ3FWRjlGZWdhbWhGY3FTClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkpkYUtUV2IvaVQxSWkxU2NZcmgKM05jNXZtZHordjUxSzNtaXdPYTlKNytDcVIxTVlaSFJRQmx4U1B4M1hNTGQyTVh6VEVPbkxxbDk2RFFzZ2F0TQpvdFdlUE5NL2dreXR2TnFUaTdITFpsVEV2Y3pkOWp5RUhlRHhoMUtXZW1MTTBDbW9PczU1b3JzNGovL3IxT1haCnBsWmUvOEFTRytWcE5oTW1RcEdQY0RvanAyNTF4dUZCRWIrM0hXQ2lFbHZrU0JyMUR6Z056L0grcGxHbWtrS3IKcGZaSHhvVmg0eEpRVC9sb2RWTlRtM212NWVJRHRCRnErbm0vaFlvaDY0TEt1LzJkYWY5c0lIb0lwM1kxQVN6MQowNXVDZUk5RFYyUCtyd3pia1poY3FaQnF5YWVqa3FQNFIrcnJoWWZFc2xLRkUzYmEzZEtPQlBBQ2d0c0N0VWw1CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHNkVjJGbnQvejY1WkUvcjVSQnkKU0xFZkNHZmViVVMyM0VMejd2MnRVOGlsMC8vN1R0L0dSYTNGM1Bnd3VqVFZUaHZnS1VqQnRRdmVKVERRZ0U1cApUSHJsRHlvcisrbUk4N0FVUVhkd2t0RzBpdjJKTDQ5Q29JZloyT090em1HbnhzRUZGZTVhZzRybVdpejNvRWo1CkNVby9rTzBwdEVmR0Y1VyttQ0ZGaUtWWjF2KytnblE0bW15Tk4vNWRpbU12ZlNyYk8yb2I1d0NkaEVtZjU5akoKcm5GeEEyb1d2Mmx5ZGg4UWVpYy9TWC8rNHVwKzVna0NNSmZ6NFV3RjVXVm5QQldRTmQxSE11VEhYc0prR0FLMAp2TU5mL2g1RmpVakwwVGdJOGVuY0wvODJyckQ1dm9FNllHeG1qdFZkMTBWMXhDNHJobWFuMjZQNEVFU1RSY0dNClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnkyem9sblBYOHIvQmZYRUJoT3kKL1hKcWhMY3lhanpaaW9qY1owMXpoVXZuN3BwYk9SeStKZ3IrVWFUSHNPbmFpbzljZWdsa1JjZjdjcWJGMjFPUQpNbWZRamY5aVJ2NHYzMExtYkM4OXUvNGVVMnorQnBhZXVmZkdtK1pET3VpZ2paRVMrdjJ0dnBuZnlTQlNLY2wyClYzQXJ3MUtQaTZCdmpvQ0RrZEtZREFvVHEvWUtDV3gvcHBSa1ZGbUFNdGVLOEl2dmNZdUlrTnoxU2JyRmFhSTAKRy84ay9iN0YrV2IxNk02d0NzTVdqNHBHbXRNcVRLMzB3Uk1Dc0RIZjBJTGM1NFRxMWpGZGxFTkRRWE4wQ0pqOQpHeFV4QXNuT2l2MTRJbHYyKytjTFI0TjgvWDZEVmVET2xkbE11KzUvUTNMM2UraDlkbjlHVnBWcy9JK1g4SVFECkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2JGWmV6bU14dmMvb2l1QjQ3Z1YKRlAyMHkwU291RTdQc1RvRytZL096RzJ0STZSMGxYekJsUlJCd0ZsU0l2S1pwT2U0MzZUaTFtMm80enlrMkhnbAprNkdPVWFaL0lUbktWSVlKeVU3bkpsNUM5c0hDNlVLUXRubUpoaldCakJRMU1mWEpZWEU4cVJPbDZkZEg2aFRFCnRSbTFYNUg5Tk5FelNlTmxDNmx0UEM3TU90em8rdm5LdGFCSUU0d09oenJyZWJ3VTlJMmhVeElHRFdqajVVZDIKWmM0WEpyMGorVU5BL2dqZnhsQncrOTFEcW53dmJ3bWtjb21vUG1oRDJYT2swUWZ0RlZsRUVxT2NDUTVIdVBlMgpSbHBRRGtLRVJFTWE4WEpySEpTWS9NOHYrQXNDdkd5Q0x3VXRqTTNYa2F4UEVMWG00QWdwZTlGK1RzbWZvWkxrCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFhQeVdNd3IxdXZlS0FXZTIrT3YKbzZBMjZkdkZ2UlpsSGRMQmlzQklWMStVQktCSXNxaE5IVVRhSHBCM2ZNT2hsUndWVUVLR3ErMFZNT2lnb1pkVAovWkY3ZTRLU2FjalAvMG9nOVdVZ2pocEQxaWQ5Q1pLWUdaTWxHbEtQeHV1dEEraUpraTBPcmQ3QjF0aHNxVVMxCjJoNUJjR1pTTWZpQnNKam1VbHJpdlBJTmJDZ3ZNa1prZS9HUGJjWFQyWU15ZHpXV0hQV25FSFFTeVBHOFhQZ00KN1NUcVg1a1NZUHN6Rnl3WGpaOTRoQ3JXanRrVWtldUtUeDJtSWVzRVpSTFgwaXFYWlZxbWxPb3NOT3M0RDBuZgorRXExRURzTjlWNnhuemtyMXkyQVJEVkVKYTd4c1kwM0cyZ1JVN1pEM1E1ZytneUxwOEVDeXJrekN6NCs3WmNyClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczYvRDBDRU53VDNoWTI2elA0c1IKYUtZVEFUbjhDR0pMWXlnb0t5T2EwQ29aWmxGY3dQS0UzcktCOW1nZExxYzNuWENoeGUxQ0IzTVVTb3krcmJ3Uwp4RFBRYXU4YTVSN3RvUUovZGxIR2x6OTltL0drZEdNRlhOaXVGYzVWTFE2T29aNjlFYVhKRms5SEgyR2V5ekVwCnB0eForMm9iM01XZS8yNVNudEdSYUgvV1J1MWFxdkx1TFhjYVViRGpDdW1uSEo2TkJZYXVjSWRodHdiOTBUQWYKTlNKQ2ZQZ0dyWkVzL09VUDhpNXZoT3ZWTzJBeWIxYVAwalcvaVI0Uzc3a3NxSHRNRkhGWStEby9mZ21JRnRsUwpHL3VwK0RMbGd2VTZWcTJidVcyaDNNZ0RhV1ZQTCsxTWRiNHB2d2s0aWFUOWNZMkc0ZlZhZ0I2SkRCTlplbDR1CkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTMzNlZseWdpdlpNeGphY2x2cG8Kd0JaYm1YaldDMzlHSnJ0NFhSZ0ZwN3Rwdng3TjJJOGVoOUxUU2VJbi9VQ1FzcEs3N2F6d0haV25paUFlREdRWgp2Vnl3OUFQSkp0NitCVVNvN1MvY0pMUHRkOVIzeG9WdzVJTm9RR2JtaUlieFgyWmNET0ltQmtSc1FxV1pZeU9jCnN6emVmNDNnQldXNHcrenZzTitCemc5OCtTams5VHdTT2dxcUpKTWhEZ2E3cVhaN1pUVk42NEltTEY2VS84c0wKSDRhZFpLRVd4TzA2V1llbFVpaUs5OW8zampOckhvanFCYXlpRTlNSzdpRW9wdnhlY3R3MGl0N1JyeG9EN0tGZgpaOXhLWU5RREhMSHd3NnFic3UzdmgyQWpVbTdPNEZHWDVKdjB1WEkrMGtvL2lDMThNUDJ4aUI3eTJqMDdlU01NCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGlsbEt5RERrZjMxeENXTUlhOWQKZEZqRndxSkJUWnNXWGlSSkFxUll6TFFzeGhHR2xaSkx1ZGtzMG90enFIWEJDNXlmQVkxYWJCSDB1Qkg4UUJCRQpwdmRBUmFlSWdQNHg2ekVCUHBnRUVMdmdxRmdDcEJ0Qlc5NVZBYW9iYThOVVFwWTdDWjlVcTNEeXhHdUpLR0hkCnZrMy92TjZBTjNXUlZNY1FSZ0tscWNmV0ZPb1NSNXN1UVlBZXMzSEllTVJLbGx1UG0rMktHS0hpQkM1aWU0eGQKb1BCY3BjYjZpYVNqUUZFRUFyZEsrTUUvSFI3TVZSZC9EeldKYzVsWmhBVENwWkx3Vkk3Ykc0dlF5emVFKzJMSApEU3VhUFZTd0dhWk56VHpNdGY2bnpER09FZFllZk0xWXZWSEEzTjVsSlMvQm42WGowS2NzUXkxR3laOGxRaExDClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE5JZ3QyeVF6R0MzSzdHdlNPTU0Ka0VVcFNDQVo0cFdxbjlXdm50YnZtK2IzeDVPOFBpYURwZjNBMUlmMzhrdXJnNmtRclNlVTlhMWJVMHdjYnFBVAp2VThDSmduRVdHc3plemFiMmdaRUdBb0o3cDRQdnZ3VmJOWjQ4M2xxMzVqN3ZLaERJc1BwZWlLOUUwdUVLM29pCmNxaUlJaUFNejZjUmxHK1MxRXBCUUVvVkN3ZTB3ZTR3YkZaT25wd3pwcERrenBCbTkrVDJjNVZyRHdyRkNGZFMKV0JOanNLd0txNU1LNnJsVTUxT3lpL2Faa21HQTVqVWt4NVlzdVZmL0dRd0dUaUJPd3FUNE01eXd4NlQ5a1RLaQprdnUzTm9FTTdJTVBBWGpIYVNkNlY5d1NGTkQxNGxjWUVQbWpPQWczRExhRy8zcVBic29JUDFSbHk5VWNEUjQxCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW1meGw5UVFIOFE1TFNBV3Jtb2kKdlVyVUdnRHZ1V0NTcHRscC8xb3RLRnRMQ2VDV0FkWHlBSVVQMVdjb1U2a1ZMYUtpMVpqRHpqMmt3cDhrS1p4YgpDWDlvQld6dnZKTXFsRENBWWZyb210R1o0b0JZSXJ0RStIRlVuU1I5T05aOVdmd1NDOC9PaVM0SHlncDNNTUNwCnY0OWJIaTNneDVDR1RqS3RwUjJJYkNQY1lEdXN1aUtjd2hscDRnVCtVbVRDQTJNQ0EzSUN0MWdRT0Q3dEtWT0sKd3ptYm9GUEMwM2tjU2ZYa3J2eDZJM2VlV1ZkeXNQWGVvcGRXWVAzWXV1V3NlN3dBRDBMay9vbThpVGlPZGJwTgpjYy8rNWpjRDRnSWFGSU5WSVdnZG5qTTFpM2lFcFV3NlY0alhkRVNGZDhCNE1pc3VPYjh6eXpJMHVGR2MvZ3YvClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHhLbHJQWUp0SmlJK3I4VWxGdk0KZmphQ3hua1cyMlhEM3p2THhpNmhGMjVQdVpxNXlBRmpXUWxxWURiaHljUlUzTGRUNkJRRGppd0RvenFYWnlIKwpndFAvQ2dpNWNOb21WaXlXMDNLWFdicGdFcU1WYjlPeVdYWjhtRUJldXprTkxPdnM3VkNVaHRlYko2OVBORUJWClpOL1NDYi84SVhqZ2ZEN0tFVk5Hak1ZbC9mYlhld3V0NlJMczFZTHFCL3dNZUF1V3N5U1lldTVCajA0ditXNEkKeHkvUFpqTXFHTlkxV2VkdjVpRmUvYTd1V2x6am1ZOW5TcGkyUUxOaUw2c2cvTU1aWHJIMlk3NTZLbWFqRTBZNQpma09QSnZkK1IwQ0xZQVZsd25ac1lSUHNIWDRhU3Y3Qy9iQnFjcVlNVnVpUVJQeEg0OFhnNGFENm5PR0NsbTJQClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdktvdjJ0dHFUQVNscGN1OU82KzgKVEhZQ09jY1VJZGd0dWxkb0dDQmNTWms0bExwSlI0TG4vNnZneWdOSWZmRUN4VkhubjVoRld6c1U1NXdEMC8zTgpKN0NhdXk1TG85dDlkTDE0VE91TUhEdGhsaC82M1h3bitZMCtVOWtBUWJPcXh2TDl6eGpSVWw2NFFVdDh1T05hCk5HN2sySWl4bXY1YzkrbUh3UDFocjhMU2d2U080ZXgzanRLWXBONGg0MVFQa3pLazJpejB0WWJnZUFBcm5RTEkKNVU1QUlCUXRsenR3ZGNuditLNnVOVS9ZZVFua1FmTzZ1c1d2U0t3akNPaVcrQmdOMS9pUmJYNStYUHRzZUhSTQpJM2JMZVdrQkhxTVJ2MmVGT2JIZFRPY1pKQnhMWStTY2RVL3ZTZ0QreVVWY2c4T1FIMHdLN0NobHgrOWs5VUlyClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkdTMTdCbVF5UWRVbytXdTdUaWoKZ1A2ZEI1bEFTWHNJakJiTDJ5NDZad2hnQU9iRlY2eStLZkF4TDhkdXd0NEJTMzF4UGE4OGlpYXdCKzR3MW9lawpWdW4vQVlDTHNEclhxM1lqbmNNOFFQTVRQY3hzSTVkM0J3R0Jtdk53OUkvMERjbmRQN0FzZXFmWHZyOHV0VUJLCnFMSE1OQW9xRCtiVUZKN1VURjZQMEdwZDk5am9xQkpDYWFCYnNUWFJOWG8vdFAxVGZWQjAxTVFmTmFLS0dpVXQKUG5BdWZEdkRyT2tzQkNDQld4dGRsSEo4dDllaFpUdUhCZCs3aHMwcjZFd21IbnhnMlBwWExrTjEvNHBGdlZSNwpnbHAwMWJWM21WT3ZSZDhpMHJEdFZRR2tUYkI3UG4rbFpLVU9lUjNaMHI1VC9wcUYwS1g3eFZicVQzUEQrcEhzCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFRPU1ozck9UL09QUHEyMThXcUEKaUp3cmQrZG95MGFQam9jQzlzbFdKQ2t6dHdzTWxjRTNodm54VG51cGxXYzBCQWVpdVB5eVkwQkQ4MnpiOEZHYgpXVnhUdk5MMmNCRnN5QWhGdkJmQWVaemNFZ05ZODNrMm5QdWpVdDNEdmxNbG5BcWhRWXBQRGJtT2tHZS85dllzCmRwZmdNOU5hNmN5dmRWZ09zSGpxTytmQkxIVDc3WGI2eW4wajgzUVNPYm5xR0xHbVNKMEljS2xrK1dhSjNpSWwKMGx6ZFE0WW0xcE03emlqalRhMXMrVG10KzRmMFl2Smd2V3ZSanoyZzZpUGQ1Z0VDR2hvajNhY3FsTWk5NThHeQpTVFVEL3k5c3lwMkxPWlJ5cU5tYzVaalgyWkxXVWJ4THBSNERoSFAwWjh1Z2lkMkUvT1IzZFNqaGZWOWNNbmZoCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2VPN0o5OE14dUFFU0VJN25pOFAKRHpDNEU0eVRwYklNUVhNNk52RVFQUXYwV0g1dUY2bFhCZGxTbUM5VnNFT3lvdSsrRGJTelh6SFdZbWZzYUwxZgpOVzVNb201NS9RdGFHNnFndjloSWxVcFVZZENvemZpQjZ1bHNuWHAyZnNwL29pVCsrcEU4VXd3YkQxbnRva05qCkxqMlVKUXpQMTBJUnBWdjkvRmJVWkx1Nm91d0c2TVdpaG5WdW9qN3FjNnFub3N3U2VNY1pWUStXUkNISHRLNHgKUDB1ZHlhdGdjVUlXeDBnT1QvWXVrSjJpc1VjK3lEdEgvSjJyRExxZkdIZmZ4Y285cmhRV3BYTW1vRUdnYWpzRgpwOHlRRVVoa0xpYklhY1R0MHErZWpjWEwzSEROcURUUWhpZXFudHZCQmIvc29sT3NxSE9XN3k1K1pPMGVsMHlFCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFBDUGFDcllsaVZwQnNsRFU3OVkKR2dtZ0plOVA3cDFSQm1hTVp5Q3lKcDdUN1hNN3BiWXhCc2JRbFNnMUQ3d2NwekpFRkZvNlNicnE1WlNZeHpKWQo1MlZDREdOZzZvenZPK2I3aWtaWWhnSUxqMU8zc29LWnhMc3I1SDZDZXdoUDFqR0d3Qjh2MVRkVjhkeFNNTGRYCkR2ZW5HYlVoOUNxT2hIYzVuaEh1REVpVlgvSDJ4WStkaS9DSGlIYTlwZFFrNFhPMGtIRDlpaTJtNmVwZ0ZoR0EKeHllZzVYdEJkRVpwUksyZGNuaE1ET3QxbUtkTElFVXpOM0JuQlAzbzdZNmtidXA2TlNLVDRabklmaUZVVUIxbgphdEx5UVJKOE9rSVpQcUpCN1FpcUNrOTRTNVJYRktXWXQxSy9velVJUHlPT0d4UmM4QlROY1RuaHRHNEtsR2s5CnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjVadW9NeXVoOE9UbzdxK1pBK1oKMVgzMHRRai9telVyakRQYkUvUWdCUTNKMDhLUy92WFR4VVVMakdPcU1vRHo1eXI0Z2h4MDgzVy9YL082NjduUwpjcU5BOXkyeVRrUzdGR0lEU1ZuVjhGbG95djZ3UFcrWmxZQzBraUVCZnZZYXdxTmFxTWw1L1phL2hTcDBlaVZwCkNkaGJrQ0tyam9qcGNwRkJBRDlxSDBTY0JpRjZFWGdneWZwUll5WXlPb2hlVDRqeGdDR3daWWtPeCsxdDV2bUEKQmgrR2licG1WS1NLa1RpNWZXYnh0RkZEN3RnZldTSWkxNktUZUEvaWlVcFJGdFdqZEY3WElFTE02NnBOeWFoTgpIb0FZYnlQQmlJWU9LMkJQZ1Zhak45ZkdFYXhRbTFpNkRCOUZBWDJuZng5VURCcEtDZjhOajVERmVVQjdMdFdFCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnlKb1FUYjF0emtpSnhEQkFlOUwKSEpCeTZzeG9MbVJuZVRpSzNFTll0VXlvSFhzd1BtbjdYS0hBVlljNEpjSlNGanNSUDcwSnlCblFETCtLbFdtbgpUbGFaaU9sbWtCeEI0RFVFV25zOWkxcnJObFpjQzczVjdPaHlhcVdjYk9IT2xHQThVZVdSTFVBQW96cExkTDIwCmY0ejNVdit5VGVDNU5MSXY5SG5vRTBid2ZlUVR6YnhseUVXRU9tZ1k0NDFlOHNTU09vMWRzYjhTQ05wNHk5UlgKM0JQcTVXVythSTdVYlZZYk91alExbENKYkFWMmFtaFViTEthZE5Vc3JNbk9GKy81dnJudkt3TjJhSzczZ0w4YwpUU2lQZ296SnJlSkJRTkxycjhXOG1mUXNwNUtTT0lGSGJFTHl6MjZsNHpvdG5DM2VYQTdqMXJoSWpMQVVHRHBsCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkpjK0dLalFuQU1QNm1FUXV6RmEKdkUvQmpBSXBwamNwUXI0SjhXR1dIMUplNkUvMlhnV0dWYnU2SUcrdk9Ib3l0RUtBNFhjMEJWZUVWQytRenI3bwpXLzEvZVNpcWVCdnJ1ZlpyV3NuMnNNREErN3VPYVh0Vlh5RnVKem5jZHNHZEJpUG43RHRVeDQ0OGFiRmJnWTNiCjFmTFRxSHk5c2RneEU4ZXlDdVk3N2RFT0JyRmg3TlUzQ2thQWY1TUlHTnIyVENFaHVqYXcxTE9oaXlQdVBpRTIKenFoZTZuRDhQUWl3S3BTUTNuU0ZYd09pbk9JN3FFQnBtWlJBWDVYU2t1Y2UvanhlMGo3dzVWMDhmQ1lLS2RDZQpvc1kwcVU4bnN1QjBLL2R1YWpUQ3l6cnRFMkdVY0ZJY1Q4d2lXWE1SQi9rYm5PbCs5SXFPTGx3bURLZHZrL2h2Ckd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkt4Qy9manhjTUI1dGdvTHlTRnUKWHNmWURhN2JBSlFXaU5yUEdha1ZyL2haa3JEcTFlaWN5KzlVZXpOWkVQZGdTSlZqUUhYMHZ5MmlBUE5rRHE5UApFZXRuZXRnbkpmTDJHeHI1R3dodWczYzRhSEVKc2dtRDZGQlN5cHJzTG9FZTFhREFMNEJJTEZSM0JQKzZSVEUyCnJLVFFNUlM3WDJlOTNFQmQyRzRBb0taRktHdTdtM3JQaTRTMlc4ZDFuLytFT0hQSTZrV1NjZlllbWFyUStra04KdEVpb2JnNHdDajFHdjFCalBENStidGhYMkNBOVBOQ3ZENVRHUlRUMTRLK3dVSURLVDQxR2pYUDRCa08yM3ZQZQo4VDJEL3prbUp1UmJkTGNYMDFKN2hGUzNJQkcwZWp4QWwzWG1HRmtCSmFVamVCZWJHbWRPVnN6N1grMWNoeWRwClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDNmcGhVc2pVRG00OFVzdGxTWm4Kdmp1WnlkbXVFM1puZjB4TEtnZllFV1NMNUhmV1c5R01PRnVUSE5KN2plVldsZ0RmUEVXZWtkK1NDQWNMd0psagphVjIvR0VPdGJoU0ZzVktLTEZDSHN0RlRqOGQ1SjZCZEpseFFBekpxY1Uwc3JLdi94UDJpVXF5aFVMUmxZdzMzCms3T2VjdmJOVnVMdVIxWWJodzRLMzlUbEljMWlCRDdWbldPbTZ0bjBXUnNZTkZ4NkU2SFBpaVFLRGdnNXFIcG8KYm1aMDRnUFQ5aXo3U0tibVJzZkZUVVVNajlaV0NkOG51bEl3bkN6bGlhSWpWcWUwM0pzVXExMklUaFR3RlNKagprWnQ3bG5BVGZvcUw4S3c3b21xeDVmakpOM3dwVEhiZ09QellVZE9XZ2FTN2VjUm5HclNzMG8rN04xSFExRERICjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXYxT2pzQXdsTSs3VTN0UzlRYU0KYUE2RXovUWhlOWZ6d0p4UzRUb3ZEeXNBQk81SEs1NlhpMlVDODVqQWZSalJ5ZlhzREVWSXpVdE1CaTBVSFJjQQo1bWIrTGVZaFAxbnM5ZnpxeDQ3V1NEZmxVdDBwYTVGemxQMzhmQnhGckxBckRsRkJyNUFRSzA2VENJMGN1SEF6CkFXSk56dGxtdEt1L0FXd0RCNDlncDd5TGJyYVU5d1BIbERZZnp3YXFCbFpVWGJBMGpqZUtGMWVIL2ZBSVhaU3IKT1JaR09tZHQyRWQ0dzJPWmxGQUZQbEZPajRCek1uSy8xTytxdDFTb1E0dFowbXVab1hFMlRTOWFza0o1SXIxSgowUy9lRDhsaVUxRkhwYXhXUXlzaWVTbnErTFA1WnNJSFZtU29Jb3UyVlJ3N0k1YWQ2SXJRc2R5RXh1Rm04cmRoClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXZlNXNoYXA5OENsZmgwbk15blgKRTM0VHJlZldmcGVJazBjYVR6V1V5TjdVRGw2ejZSYkRPNU15ME9tUThRNnhDVEhGdXNjWlFSNkNSb2tqa21iOAo3aG9FY0Q4WWc3eG5zOFBsbzFlSnNnMTduYjF5Sk5vd3B3dmd4MDJWOFRRNTlTcHVaK0V2dlZkY0tka3Y3TG1SClptc1N2WmVLU2VmSlpYazRhL3ZPL040MmN5RjdsMVlVWTJKVE1XMExJSm4vVWNUN0lOMVZaYlR0ZjBZa0tUM1UKdWd4OHZUS0RkeFpVY0JNbWxCcTVTMXdJd1A2TVlVbGZJY1VqVWRBRmt0bmdaV2NkK0dHcjBaQWFSeFhBbllLSQo4OTFDV0xUTmVXQUNhaWFTZS9qWTI4YkFMa2xQWHBaK3dYRi9iZG9KSGJNRzl5RHIyYXpScDlxakwvN1FTOFpLCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3Q5Mmp4dGV4bFpXSFpoa3ZTMEEKaXd2R0NyZTFBaFlVQ095RTJUTSthdzUzM29WZlFNY25Yb2x3TUwyZ0ZuNWp6aUR5RW5SSTRMMkxkajhPUGQxRApjZ0ZlRDBNQ2lWaUd0aERVYVZOdWlnc1A3b0FTTS9HeklLMFVXUjVFejZqWUxwaDA5K1Q1SkJ6dWlrOEVYK0IyCjNpUFY3R0pOWHpXZXVveE41ZzVGanNFZHNJVytvZG1CY0lXSjlFOENDaVB2QzFDdWx6ZFU4aUdDNjFOa1VxZTMKZEZLOS9wOUI4dDJ5ZUx0azRvTEh5SzYvN3M2YitxcVdkTURMQTMvSmk5VFZZMWg2cnRQZUFtVzdBZTFiL3N3VQp1cUFJNXduK3Q2aERvbUF6bWNIZFlGMHhQTDJxRHdpNmJ3N2VaT2VIZnR3TzBtaW9GMW5jWno2clJ5aXA1MWRKCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzNXV2lSV05VU1VncE1oTC80a0EKcHJjSFFmM3JmWkxXSUt2ZXlrb0NuWHVOYXMybi93WEJRQ0paV1lyRklLSTdqYkhtRXdQTEpyMGpNOUlEZ2JjbAo0azlTNDBaRkU1VHlvS3N4bjFCc0xTemJ4LzhWZERJcWlRUG5YUkFORGVtYXVoSGN6ZDhPY3ZERTAzbkptRHJVClkyVmJoSXFubUNwcENkN1FPdVZYY05ldG95ckFpd2Y3Q3B0dit4OVZRTVNBZzhkd2RZcjZ1cFFycm1ndmJEcy8KQ0lWMGdobW9sVzRCMXM2MloxVCtqb0p5aThmNy8wZEZHbkQ0UTl3Q0dEYmdGZjMycWpEbHJBYi9uU0ljK1FhYQpXdTZzUEdYMTJaQi9iU2NUSzdlcEIwSEgxNkVCS3UvbzhmdXRFUmZvNWJIV1AvbUxZMVEyT1ptWmxnRlVWOS82Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBek5CMW5CbmtrYnNQQm5Va0wzVkwKSEo3UjMyd256VENUQVYzampNQ2NFLzhodEd0U1UyRThlK1paRkxJdjJ1cm1OMkRIS3RQNXZ2NzMrcVMwdDA2cgpKRnU2ZDFNMlZJR29sSmk3WXNON3hwdFd5UjU3N0dBZU1SeEFIOFlXZjk5Si9udVBSUDRMemtMMEJwb0pNSlAwCndnUWJpUlN1WUs3d3E3ay9KQ0VIUEJTYnNrTjFtU2xYbHhZOUlzcHV6Z0VzQUt6L1ZnYnhQd1g1dW9zRENpWlMKdmVyYW1yZlRmdjNvZWFLR2dIbDhYUmNpUUZMdFlzOWlkZytoMDJVRCtBZVJrK1d2NnBnUzl3clBsdW5Dblo4aQo3NmlNQzlGdHBWTUF5a1NwSEJOSWVBOTdnRGlXVnR5WWMwaWppS2xweHJzdzVWd0RTUWVQQk9nemszRjd5ai8zCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEo2cE9LSG5SSWpXQk9kKy9qUjMKWW9waTg2SlpmNnRyMms2ZTVJTjRoQlkxZGxFVjJhUEVxQURxN0dWcTZDc2IvRGJZYUJDWWs5NXFCVUZDTlRtSgpETHUyMnluMTZRZ0YvU1dmSlMzc3NiRDF6NTdtOEhPZVhTZWRmdkwvYkt4eEFKS0hhczBqcTV4SGVCeFFFTFFrCjRwZGlHaEp6d0Q0OGg5S2J2RzNucHlGUUhPMFVQdjE3VjhtYVBrR1lmUkQ2OFpBY2M5NDRjcWdqWXQ0cTJEN0cKblpGamE5MENPZk5qZUhxZGNxd01hSjdUc1JiRWJaUXgxMEZhaFZydzVrYzF4YVhjak9OR0pxTTVJK3VLdEIwMAo1OEJWa2tYT2pITUZWa09uK3dybXpBMDlhd21qN3FxWXBJMGlPTStoL0R4RG5DbDRSS0VtbmRkSWxSY0IwREIrCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM3QxcjkyWTBaQ3lLaWg3Mk9lWEMKeXZqZ09PdW1KM0I5TTlnZ1Fhb3JnQnBFN0gyUWsvZGJleFptQ2o5d3R1YnVaNURnOWlTREdRZ3MvY1hjMnpEcAp4czk0N1VMOUJheUd5ak45ZG9MV21JTUdHL01KSzg5Ly84Q2NtS0tIem5yUStTcXVMbVdZYVphb0hvS29tZ3I1CmR3WkZVMXlDTVZHVndGMXZ1a3k1bVUwL1ZMMzh0cnNjaXMyME91UTlxS1dJbi9YK0JWSGJRcXdKNUMrQ3FKeXcKUjhlS1NiM29lWitiNHdUYms2YUNPd1FhNWZwaHV4Zmc3QWp5MDcwbXc1QWt4QXdGSi9uZEt5MDJkdHJYUXZpUQp2bmkxVW5RM0N1MHF0UE45eGhLNkg5Wk1SNjFrUzRLTjVZWU00MmN2VjJOSEZhcm9OS3lMUllIL0lxOUN6VEVmClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcURNQi9FYldUYjVJUklkOFRwMDIKN3hGbCtkS245cWl6NTBjR1gxRjBHTTlzWjNTYktzV0ZXamV4OTErOWVrUWFEdE5rVTdxR0R5b0hlTGEvTDlDZQpJVlBWTW11OXhheTJZWnlVQ2JwOXlabjNKZUdlRUw0L3JhNFM4UmtxUWNjbUE1S28rUzE5VWJOK3NuVzladWNiCkkxMnpYMHg1Z0NqOWVtaVcrYi9ESzFMczlXRWk5V3F1cDhCSExLb1AxRVRSU0ZmOUtzNmxqLy93MFVPQmloWWMKbVpOd1pmZVFjVzhWMUhXNkpUdmcxc3ZPbnBqeTJnRnRiTFdHblN1TGJIdmcxbjNSNjJKV29qYjdtM0YyQUxNeQo4bCsyZ0U0elgzTFFlVjh2d3VmU28xZDc4WGpaczI4bUovR1lraTVOSEI3SWMzYjlJUTZncGd6bDJyZDJRVFcyCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1dtTjdDeU9jaU9xZ3U0UDFzZjYKTjVnNTAxemtBOUYzaE5SZ0ZaaE84ZHRxOW1PNTVnNlRnN2JIUWhMVEhzQmJtNE5HM0RNYWJTUGtCS1NjcVJNNAp5QUdUck5xSXdrYm1LclBSMFFnd0pBMUZ1Y1d5b00zd2l0UDllWlozYUM3YVEzUDEyL1cvNXhyaTV1L2sxUVFyCkpQNStTZHlTdmd5Tlp4TDE2NEJHYzRMMitHT0RjMWdhTDY0MllsNVk4Q2hSQTN2cnZ3b0R6LytEQXNaVkJmS2oKV2k4VUFQYTc4TE4vRzk0ZWlrTXliVXhQalN1VkVmZU1WeGFWTFBzUzgxSEZrYnFlRHpwZUt6dkhKNnNGUVN4VApwb2U2b2c1Ymhvc0liSi96OEdtN1d4VHNuMDNOdVNtaG9IYWluVFRuOUVCYmcxZzJRNG9ZTTJDWVJLUFhSTnhXCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGlqQ1psU3hrcS9sNkhsQ0ZmaVYKWU81RDhWNjVwL2lCdWVac0dWNkY1YXdFZ0ttU1Ryd0cweHoxdzJ2aGVyOXJqV1RlYWJuTUc5bnRqMGRSWlAxRgpNWVBsZFFudnVsc3FEaVVWeUVpaWtpM3hTR0hoYURhMXZFWXViV0lwMDk0MUx4Yjh3ZG8zRkEzU2JTMlZPSTltCk05YWFVRnp2aDcwRVNDQW9aYmVFaFFXWTJJNS9SckhKM0xYOHNaWUhxYmpGWWpUcU54WTlxbG1MTnR5dm4xWFEKZlJJYkVUR1dYTzZjMEVjbUk1WDkvK0JGR3Y1TVN0ZHRyQ1lrcXcyNTZaS1MwUVV3N2o2OTNxeHYycFlzRUJnOQpFbms5UDIxTHVna1R4eWhSaS9UK3ZwMDRzWEQ5LzdzQllZOGtLZzlkbEIrSytiLzNKUURkSWdSeEFtdXUyMnBsClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbko4dmlELzY2UzRJZHU3MnhaR1kKMEwrTk16TWJqV1VESU1Nbk90dWkraDJobnNjNjhCOUZiTXNrbVo3TWdCK0NPSGg0SUdnWUFleGE1cXRURm9yRApNUnN5YUdHZzhYVUhiN2lkMkIvYm9FaER2MlF6U0lEc0NvcWltd0ZTbGZMcHM5Z1JIejF4OW9FS043aEF5ZC9YCjhnejBwa3lIRmVHazduT3Z4RW5tZ3FBSEFFSUliODYydFM3Y0srVVNncFUxZUR6TGg5RlFnTEErL202MDFQbVgKRzRzVUlLR3pqK1NzdXJtNU1HTU5XMDBqc3kwVEwwSWJhY2k0Q1ZrTTBTb3JRYUhBVkROYmQ1MzEwMVJpQkkxeApTdTBaRFlrcWZuUUxBeFdDMUNQTlB2ZTR2bUtLcFd4cnB0RnAyd3FpbmFnV01sQ0JqZUFqZzlCOXlhYzQ4UW5ZCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXBXbE52SGE2RFBMUlNyV3dtRC8KSzZjdkoxd3dOSTc5aDVuazNTQzFKNjcvcXJxb0lkQTBqQVRCdGxjL2FoWko0azE5WTBOeVR4RUdqS0Z1bFZzUQpPNmpzRnVZNUllSmxwcDU3NSswY1JDR1A2cmY5cVdkNVkrKzhRRTM5b1c1UW5kd3FHZEp2bTBPaVA0b2RuM1JkClQxU0xYdE16ekcvVUJFYnRwM2piQkhNQUxFUjFBZGNjU3VLNTd6cWc3VmhlWndwUnZDeTdTRCsvaEQxbFNPTFkKTEtjdm1xY0hnSXlFSUdPVGRDU05ZSEVET21rRU9HdVZCc2t3UDIybU1mU0d2NHlTM0VvcGUzL2x4dHZYR2N0ZgozcFFDL29UbkZEZVFFZlR0M2U2SmE3dEFRa3h4eithREtIM3RLbUJuUVQ5dytLdE1oOC9RMDB0bWdwUGJ5TXVMCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1htNFlvYjkybWhWdTJKYTI2b0gKcnVDNTZWVzlPQ2pWckU5UXBpelpEeDdQQ0NhbkYwQlRJMjlvMmwrZ003WFVlKy9PcVJvZTdETHR0aVFUM3hIMApoMUp6c1JoVWNxTkQ0Mm04NkRxVzRQNENhd0g1MnJqelVQb1RFYTk5Z1B6aThpazNsWGZJRksyb0lDWjJ6c2FDCnpIT3VTWEpzZlJiSkt5R3R5VW1pTktOakNiSFl0WkRpbzBkM3htTmpLc2c2SldlZkJYNEJxTzloQ210S0dDRncKaVpmYzBJc1ZWRThsdDBXVVNRejV2QkdSaFo1QUdKMDNCK0pWT29rSThJWkFsOHh4aHJwOTZETDVNVVdsc3B1SQpPQzdOTVRjTkZNQWNXbi9ORWF6RXZRSDVJNHB3L2Q1NWpZb0ZjdC9zMkswYndpL1VmMGRWdGFiTHAzZU1EemVyClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2Rjcmh1SkRVaW9QQVZuUVhZVTYKRGxHQVN0UHFROVRJeHlJVFMwZTRybzA0ZkpweldQSDhud3lPRERmbnVVWGFvMUdaNmo2Y3hSbzhScEJ2WUJ6agpKSXl6TTY0OU1QNU1nTkZucjkzZjd3WmhBRG50THkvdnB5dWo5aEVRMXJyYWVxSXQ4S3VBM2lWeU1PZE8vZVZyClFLQXQ2Q3E1UXQxVGFUL2xyckhYMEx2c2h4SG9LSTNDNTV6K0o0WE42RTRMTi9HcGFLTUJzMTBMNkJQTnNrZVoKcmhoS2JpcmhYTnE2bTJrZ29kMldvMS8xeDNIQ0FvblIxenNMK3prdHV0eWVkUkFkQ2hlL1oyTXU2UHp2WkJVbgpHcTI5aVl2RFZ0Vjh5a0RGOFArNkVHUlJkZUNaRkdBQ3pOMHNxMVpMdkR4MTFYQ3Z3VWtQTkdTdnhEaHRJelhlCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDdjaU9mT0xYZWxUU2tndnFPSm0KMW1MeEt0YTNIY1Q5elRkV1pGRVcrRkYvRk15dVdCYkZoN0xEa1FZTk1UQ2tHSFkvb0lTQ0ZMMVJYUjVsYW5Jago2ME9IREdPSjl1VFYvMW5lcUVTb3FvYTBtbFE3Z2xQajMrY1lZTWQzRGpBUEtLTWg1cDkvOFB2RmNUK3NmcDJkCk52Q0FCWFhORG1mVHE3Z1R2NDNHUmpvUlhVaVBZdlJmSjFpUzIrZzViSzdBMFdVSTkvbmRhQVMwYUc0OFJONGYKd3ZXTVI2RkxVZTNZa00zY3lWbnEvQm04Mmx2S2xiTml6ZWlyNXVQSG95aDBPSzFjZFNIZmFxeHlYUnlDWjVibApSSzF4cTRua1RuU3JpU2FnQW0zdDF5SkFUbEh6OGw0MC9nNDRjUWZVVEF1NkNGUFQ0clRyTW80MjdweGp4aWgzClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTk5RlFDTnNSR2VEbzJCcUVucVAKdlhCRGgrYXdaWWNrd3RMempiSXVlVnZvOGU5ejJJdmJweVJFMEczOXgreSs4ME4xM1hoK1ArVUI0eGJvOGdmbApKd0xIdk9WRWY2VVhLNkdMOWQwWEZvbzA2KzhRSGw0R1Y2eUdqTXFaWTIwUzEwNDB1V3FaWHJpY3hXNUxwdWNuCkxZN1FKWjVTK2M2ZjY1Q1VsalVpNlNZT1VRalZYSWZVUUhsZEtCTTZrbUVFS04rOGtHSU9pQnNiWkpqdGo0ZngKS1IrbmRuZFVMRndXTEEwL0VPUVVLNFJrem1GYi9rK2N2ZHE1cGFPQUNCZ1p5UDArYStFaWkvb254TStlRHBhcwpzNUZKc0NadWZSSFJWT2RDMFpqZElQclJxWjY5MVl1TnhjMGErOTE3RFRSQmdFYnIydkNQaU4vQ2RTSUJrb2MwClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHVqejJ2RVlWZWVNNTFoMFFrekQKVEhOYVBXanZLUDJyYkxQY3I5SitDS25TOW8zd1NhMDcvSnl6eUtDYTJUKzArQWcyMFZpSUw0bnAvYVhWN2ZjUgpUUlZuNFlsYVFaSm90VnFrTEtycHVrYzJNd1Awa1ByNXBud2I0QmpkTFlhdi80dUpPTDI0WjVLNmNwejVrVDg1CkszNlpVS0FGNWZqQlJpTlpBU05KeERrZkUwY2c0TlphOGZRWjFTY2ZtamJDUGl5QVhuVnRhWnhGNWFZazU3RnYKNDYzQXRsbFNSSkZZMDA5MUFEWEs5R2ptdTc3ZWdYR1RxWHJHSFlNWWU3Q1Y3d2JrMkNmMWlwQlkxZFBHb2FGMQp2ZnY3NTBWUVZza1pFZCtCSjFpQ2ljVGxtRGMzZGsrM1RZOVdCaXJTRDBZRnNEWkdxbEUrL1k1cWpHbFdxZDhzClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNXdtVlFIQ3YrVHlaVWZEZUlCVXcKNWZRMG1LL0czcUlteU5OMWpVSHZFWituZ2VicWY4VlNPY2ZjbHNFTVFZOCtBb2hsZ3BDam5NMm1QTzdrZ3MzagpIV2I2VFIxbU9vdjRudUF3QVFyS2VjUXdzWmsxZDBZejlGb255WjJMRm5ESU0wWk1DY0Y3M0RNSkJudzJ3N1JICm9RQjBOeG43WnBwWUJhSkh3Vm4zbndMN3h5Q2ZiRTEzakJGdHlVZFZITlpZSFF1L3VIV3N0eU9MTVRKYUJuWUwKNkUzTkp0NU13LzA1TGRnNTZHQU9JKzVoaEEzSU85K1VzNW5yM2xpSVM0QUNCbDN2alBCOGEySTFEL3BUWDBOYwoveEx1aVI5T0NQV0VaTFRLSGpYWnlBOUNUWmJ5a2sxa1h1ZkNoZ1NQa21hTzIwSGRLTm9WN1RKUTFzSWhpTFVaCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVFrOFpOeHdoR04wL1AwM2h0MDAKT003U25RdWRVanNLUGdGUTduRVJnWGlTMHRmL3JuNXhzTStSSHVYeERKeko3QzBkSGlXWC9QUEdtd2VOcDAxUQorL3J5QlN0OXAwMU5ENk4yVEg0VUpRN0pJVm0rYkFkU2YxSjZ2MnRvM2YzUzkyRjR5ZkxDMEtLclpkQnpJNjA2ClRNZU8vbG04TmYwMGlZUlB1Ynp3bUJ3dVlVNVIvTEthNmMyaSswMkVsYlFLVWdmMmFFKzgvcjFFdWJWZlBKYSsKdTJJNXM0RmZ6aDdyWjNRRXJwVUFDRW8vLy9EK1VTTGN1dWNoRnNqMHpOVFdCUXhLK1dLcGtSM2VLRnMrRkxnMwpTRVp3MUMreWRlVG9MZ0pPOEpwMkRwcFJYTTlPWTN5VW1iSER5WnpHc1ExVXNpNm5rdFROR2dZQ2hkZWFsd2pCCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXBKRlRQQXF4OURVeDBocDFKeUIKK0RQb1crN1BIRE9HU2t6Zk5xbFJ0NzVLekcvQThiY1VielRwYnlCMmpSWis5aWVJVndlTmg3dlhnbjF6TkxscgpScWwzbHNkK0J2V2w0cDlTbXNmUi9rWk9uTlNpTzcvSXBRMi9kdDFrT3VaMS96c2FaUzV3S1RXV2JjZFBteFVrCitJOHcxTWRYdERwZE1rV1A1WVBCWnZMbWpSdXJMWVRrUGI2NEtDZzJmRDVPVUtGb25uQVNmMVZvL2hsYkhjcFEKMFFvN1dxVFV4MkJuSHZ0dVdLV1ZCQ3NQaEszRERRSC95SzJCRjBWVzF0RnNlMGFvRk8rNzRnOHJCU3hvdWlDWgp6V2VEQU5hL2dxOUt1NmVZdEtCcWZ4UXN2MUtGYlpiV0RwZjg4aUxpUW8xNmk0VFM2aVBOQlRCQThJZmJaZkM1CmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN08wMkZWWUM0Q1ZqM2Q0dzJOOGoKcC9xLzNXcDBIcjVSbjdFcDRSeXp4Yi9MV3ZGczJXdXYxVUhnMUZKVkEzR0haMmEzMzlON0dpenNDYVBiVjJzUQpmdzAxcmFiSVRUTUdiOWVuRmt6V0VaUHpmTTZFWnRkb0l2Q3ppSGI1ZVluWVF1NGxxa0hwcXhSenRHRDJoTnovCm85cHJtTTNibDgvK2VvRnpxWERNOThINXZrdDBVT1h5UVJVR1l4RlRDYUI1cXBTNi90NFRWWXd1TlV4SGhGWTAKTTNMMEZSd21VbGFPR2xiSENEZkdTNysvZkhsMkFxbkpOYkxiWDZ2cE5FWE5ETmx3TnlzenpKdkpCT3FCcC93NwpqVjB1KzNmUG1TVXQ2eHlJTzM4SHVMckN2UHJsdU1DMlR3ZlZrS21NTUhha1UxZHZHdXlRSHJqZ2pqZkQvQlpQCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeCtMTG9jeEhrME5kYWdwakxVUksKdTd5amtqWFZ5d2ZjNy9wc1NScUl1Q21aQTlDYzRqZnNKcWZIcTdCMmFRTWVmdUtVWUlsM3BRSDBLa1VWWkkwNwpMcTZnWm1WYTFZMlcxUkIrRmFJODdwaC9BcUZ4Rm5Sa0FPaU1ad2Q3czFNUXlObFF1d1paQVNDUFFxV2dvR3ZLCkRjbk9SQThzL1g5UHdPMkM5Rit4eGV5WnFOQzdzRmtJV1lUQW50ZGZ5b3ZORUwyTzFVMlV6VVVJM25lM3oyRjMKcnRTNkYySW82TEdKSHJsSGt3TnRtU1NHbXlVSEdzZ3hrTHJiMHlReS9YZUdpQU9KV3pkZjNrZlZrR2FLbkQySgo4cmNqVG1XQ3JaWDhlR2hEaWJCeHJlZ3Y0TkJKeXhuZnIvYzNWeVJQMC80cjA5MTBiRXdsVFo2eVNRVVA2aWxSCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVkzcnB0WmtiVDBKWWFaZjNLMFgKOUhhMjV2MmRndnhGVGx5N2JvZ2xXNVB6YUYvYllrN0I1Vkk2VkZDK0pYdStra1hNRzc1UDR4NW1EN2ZHZ0k0YgpHeXFwazVLazFwbUFiTmQ5aitnMXlXWEF2OGhXaFZpNVdhbXdoN013b3Z6dUVrTWtNUzBLVm9xbWlLb3Azc29YCnF3ODVkK3U3MUk5bGFZTjNTMW5wbWNUWGg2YmVYZ0xldE1HcTNrM296WndZbDdoMlh4WURaQlFDcFBsVW9OUU0KTG9BQXZ2d3MvNWwxYlhiRmtsbmg1UFFzcGZadXVJclFwa2hVQ3I0OC9aSFJzT1JpQlBLYnVJZWZ6UDBDMldkbwpTeFBML1J6WEhtSmdQN0w0QUtBeHBHdUV1aU1VZWg4RFJYaGZ2aVNvZVJCM0lEb2xNS2svalB6QU5xN2szckl6CmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2NQS3lpSFE0Z3RMWkRoTHVVdkYKTmJDcmpBWWVodGhqNlBPVGNuM1J2L0RhRW83aHNHbGhXQXFpTG9qaDdDa001RE5UVllPZ0hWM2c0VGxWdVVpcworQVp1Q3dCdnEycStCQStod2QyckE1bXRONnd2UkZQc2ZqVkxNYXpFZXhHaytoYkFoZmVtWCszc0dtRVRiS3RKCmp5NTg1blVmSnh4Qmp2STVzbGxtUExGSXc1WkorNzRLRkVLU0tlS0pXSUhoWmE1YmFVTldLV3ptNFFwNkhqWlMKc1V4Y2NiV2l6SWtMOXdZNm45TFEyblBFZktzUE1ubEZEYnZENHB5VVhLRUFqVnlESkRzRUl2ZUZqTHpGVlg2VApxSnRWd3JkRmx2SXNlak9tWU1uYXlaVVd6NmFrWEdLS3B4dmQwa3ExYWdqb1h5ZzRCd0dkTWFvU0VYZFNBNkg3ClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHRrRWRhMEtkK0JZaTcrVGdXOEcKejMzU3g5M05ydVB3L1MzZEJvYnduekpycmVCSHY1aE5PeERIMW1SbUhhaVhwbHpWK2lkT0MyT1JOdG1WcktpMApuTFhhSjZzVVZOYkdWTDBjS1I2a2RmK1IyQW95Ny9salI5RTU3ZTJSM2ZFTHk3UjJqbStsZUhhQi9YdGUvbk91CmRhK3I0eURjK3YxWTZwc1V2Y004TDRaNU5VVTNZTHVsaHQ0M0tyS3hENmlySzFBcXBwTUU3dWtxNGpQTVl0R2wKaVM4RnYxUHAyQ0dqMU1hREZOL09FOFNkdWQ0WFg5YmgzVGgvZ1JVckpRYlVMVmZDbE1sRWJQcWROVVRUWndwVwpVT1A2TTM3Nlh2aXlOZ2hZSHZQVE5pTTdyMzJzdXp6akV4R3crbzc2M3BweGE1VVJDMmc3czZhbCtBL1VhVVFXCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVdJdVh0ajZIUkxKMXFmZXA1Q3gKUkZPZUtUY0paWUppbGp4N3FNR1VsbWZMYjRjYzBUUTBWNUgwWXB6enI0Y3FRRk1tbVNjUWt0NTdid1l3UVcyWgpNdm44MTA2d1ZmZXpES3pEd1NQN21Yc2ljbW1PSGgvOFF0cmFOUFUzUE9WbkI5Mk9WVCs4Y3cxTklzOFlYKzJ3CkdQK1U1eVRJclkzZEFZQjdIbjRZWHRiSlZwamZLbEpvcGdCMFdiZlZ3NSs1ditJYlR3Vjc1UjNtMnNiVGNEcFQKZlV5UDhVWjY3RWVCME9ZSzFEd3ViS1FvclRZN3prOXVzVWNxV01kanU2YkMrRk1jMGd6ZWxPWWc1b05rVnRvRwp6SHE5bW81cmdseWJWbldkaThtTzRwK055ZWN3Z2t3d1RLdFFYSVBjTmhNRCtnekk1elZBcGRIUXR5bFp4VUN0Cm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVRLY2xJTXhQS09jaUR2NkUwZGcKZVZKQy8yeFVIcEJFSDFKRWxQZm5BSTArNDZjZ08wVE9YQ2l0ZUhTU2FaSHZORmttREVsT0xSdUk5cHdMSW8wcwovSWxOZFdVNGZVdUpLVWxhaFZpeUtkbjIrUGZvYXI4S0laRmw2OXhGaCtaTUZ2R1NTc0ROdW5ZNXF5Z0ZFc255ClgwY1VaeWsxUkllaFhzK0RvSnZVZFgxR1hIRUczbUJoS1JlQWVVRVlaOG5MT3ZyMHNDb3FwVlE0QjR1NS94L2MKdFFxeC9oYmJBdkh4bHhhcDBvYlF5STJCVXFyb1JRUmw1cFZRbnFSN2NmYkMrS1BvWHh4ZmZhTEhBbU1VSU93eQpwY1Mwdzc0NWtFdjEzNnJqRjJmd1hzTHRKeng1WDRVWkt2MVh5aE9JU29BeW9OR1FXYTRibC84RTZKSkdBQkhBCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFdPdnIxMDF1dDFMcFdjRGNtSGQKbTZaR1hRZmhBNWhRa0hKNmVnRXp0VkMveHJKZ0NOOVg4NlJEQk1pZjBxOHlFZ3lKR2VPNWVzWlJuMzNTMUNSTApVYUEvay9zM1RGMTVNQk1pVXNvNWFPMjJDSlB1WXhvVDNWNzNjbkJ2amF1ZnYrdDNWVU42a2dlemJVU1B4Z3liCi90bTV2L3VkNTlxeHhZZGtFY2cxN3JmdEJWQjZ2TU9nMzl2ampWUHRxNWxqd1JpbFkyTlBHendNeWt0eW1QekwKYkpVYllwVXVzUWF3QnFCMzZlOWtWZ0E4MmdRbHNqTFFpUjZWNVJTRWtRSjBSODg1V2hFZU1SNWZ6bE1Ud3hpdQp2Qm5JZEdmVlJSWDFJeFE3dk4yeis0QTYwaDkzZFpLRmV1NU1CYU5YQmhDTzA1UGVqeUpuVUxlOGQ2TzRzS1dtCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDUrZDNMRFdza2Rudk9pd2x5MmoKSVFGK1M3ZHh1S0YvNEtXTG1mSjdwTHRyQkZMRDBla1F1emZObXBTc0pFUEpvN3dNNGI4cUFObzViWjhGV3hEaQpIMnV2Sm9iWWpORHRQbHlyOUpDUDNaaFZtUVVLbkNsVmhGZC8zL0xBTkVURzdscUxnQ1RTK3l0aTA0aDBlb1lzCnU1QkFWU0sxSVRpWEhHdm43Mm9wdTB6T3laUUtnSVEyZVNFZHh0L1VyR0RadlV2VVVVKzR3Y3NoQzZ5ellMLzUKZkp5N3RMbFA4QVR0d3pIRnNCSEJobEJiOXV0cGV5bXpmMWlXd3FaTllYVEQ4RElDeFo3WTBKdDdycFRkVmVuWQpQVzgzZGxrc25mRHFNSTBTSjBacG94eUVpS0xxck9lUzR2NE9IUG9RK2cxSGRESnVUbkV0MFgzc1NPRDNTZU8rCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmlqUW1RSXQwd1NMZFd5aFkwNysKdnRTL3cyUVdqWVpWSE1KdTNjR0N1ZXhjbi9tZlFsWXI3M0JjVG9iNU9VbWdVMFMyS3lSdTUzL0hoaFl2TDJxRApZcTIzRE1mZVVGekdFRUozRmhsa0RUdTc3K3hONzkxOU9jc1Awc09MVEhvTjJTSlQ4eEhkZzdhZHJ3bXFSRkNMCjdrNnVLSGJQYVE5ZDdURHJvVjdtRlp3MjUrNFhGOUJHUHZyTGJ5ZGIvUGQ0c3c4dVowUk16bDd5NHJTajRIamEKUUZLcVJxZVZhRTBMWDN3UGJneGFIYkV5WkhFL0VIWVhXWlpjTUdRSittL3FNNC9xbE0rdHcwdlNzUW1sdHVGZgpSNW16TndmeC9ab0xRL3AyS0QveDNmMGRzUC9jS0E2YlJybk1CakNDakhCVG43VytLc09SSDhrVnBOSzBVeHZnCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGVGUHg3ODFjNmh0VFYrQ0YrdmEKTnpuc29VRjVvK1I2ZTJzYlNEZzJqbG9Ja2h0SmNNRCs0V09oOHdSQUQvL1RkUUh6akVzZDFHRTF1dWsrL0ZwMAphQmh3U0c0ZWN5VUQ3OWtSUy9JY1Rad1NWbjFoNHNuVHRHRFhodjZnWW1UNlVFMnh2T0xsUnFZUE9jaU5zeXdGClltNnRtNGM0OS83UkI4bWtJV3ZISHdXUVBYMHZYbHA5b2FMdWplTFNKSjlROHZTajNacGRrcWlKRE5mM0N1ZEoKZWZLVUwrL2Vhb3d5LzJHaktyb2l2L0hPUmtRM25LREtxWlEyanF4RmMwTWE3WGlBZjBkTzhoWHhPRWszU1VPVgpoWEpMV3pJckhKZ3AyT0kyalBIa2x0NW10cFA5ZXRST00xMlhCUGNWcDNJV2NWb3owOVNpUEZhNHJQdDdsQzExCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW1oRGJFT0JxT1ZnMVI0WVZ4RGYKdDlmdlhqb012WS9HeTBUZ1NOZWhUY1VoSmJldWV3WUl3V1VhV3VlTnJ4RmI2c1FCTjg5eGR3d3pqYkkxWjZ2RgpkMCs0c0J3UWczVklrYUpac1lQQTU4cFIveEJJVzgzQXVrZU9nU2JEMlZCcW1rTkZrNDExZUpqeTJ6Q1YyaHdqCjljRDRoZWt1dzVhUTF0N3JUR3ptWUxXb0ovbzBxWEFoTldvaThGSXBrbVZLTTlBa1lZM1lpQzFDdHVnek15MU8KL1FlZVhickE4WDJtamx6TTZSdXdSYkkwdU9XTzh6YWIvNU9ZSnJBMUhqeXc0LzhKN3BiWXh1aGhQQ1RRNEpmOQpQSkR6NXlJUU9HLzc3RTVFNTI1NWZ3V3FVdjd6Z2pVT2N2aFJTOURDSG9PVWFGNTJIa05HNGVOdWk1S1h5UjkzCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3h2K2E2NHZZSDdnaDVYYm93cTgKQnNpTG9zZlpiSStIQzRMaFU2V2VhMHlOT2pwZmlWSGtQRlhpd0V2azZZMXdidXpFVHRhRjVUOG1laVFGbWV4ZgpGN2FLQVVaRkNDNkFRQ3JGZXRCTlpoQTVOTW1Xc0w5MEk0a0phUXV0cGM4TkZQbHRRZ0h1T0pQeWNqZVh4M3ZBCmZLUzRZMUZma3pROElGajFEcEU2QTF5RUNEV2ZSNjdsMWlvNGdLOW1scTVqVko2bCtKK1JqV3Y4M3k5cWJTU2EKcTVjbUI1Nm0vMHg5dml2cXU0eHFzbE54cFppWE1TbHJ4MU5rZHJvdEE3aWxldS9FckR6RThYallJY2U2S2loWAp0cTVVM3VVUk53OWpPTW1tVzVTa25SS0JHNVk4b2p0cThycFJ6aVNwNUhWbmRIQUFjMDk5eW43YXJIRERuMHJnCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGszV1RCZ1FwWTY2YTdFYXRWMXIKK1Qxdm5Uc2Y2SXJlQ0FJbkZiRmg3ckZiMWppVEdYRVVDN25GeU9IY2x1WUZrTFUzRldTbGN2ank2Y3h0NHpMVwpPV0txM2k3NGlPWUNjdnV5WXpQT1dVaTlHWkVPSEwrbXc0K21KTkZDNG9vR1RIdnlBcE12eHVoZUpaNWw0ZVUwCk9MQTd3ZG1JdVd5M2d0bU9ydDZXV3g0MDZOQUxaam4yZjNyUlBjcmsxaDJYUDkxYTRCOGN6QmpORFpjellXTXoKVjVHT05HcG93YTJUVHFtMXNMMnlWZFhEcEFKSEM0YWg5QWpzRTFTNVF6OFlQdkgyN0o2NU12VXEyQ0Jib0loRAp6TWozWk4zSzlGSTdveW0ydUxTSm0wcWFSVm9HOWZnN0ZxTTNmZUlmZDY1VTgyWkRMVlI3bjBXOUZyNkFDWmFzCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0s4N3o5ak9uQ05SR2xXYy94ZFgKa3oxczloTS9GYzVsZzRLL0IraVJYUjU5c2RjTWU5Q2l3cW15aDMzLzEvZ1FiZlVjeUZuRForN2xvMDRYYWhLYgpLK0grNFZoNFpNYjdXRFZRQW15OGJXL2F1aGJuYmZJV3oxL3BLTldpMnRKVkVUZGwrUlZTdkIxY21tNldCeHJ2Cm0yb0lyWUZrcTl4U3pUS0xhRGFqNGlhMXo5dTZwKzgvRUw0bnRKbVpCT3JUZ284YThWMG8xdXE4Zlk2MGhLVmYKeTAyM3o5R0pJeDUrQ3pPVVlpRVJQUlR1MFlVTUU4QStLU01YaTdSdkVUSVRCWmNDd2xDaFJoSkNZeEpOV29XegpHeXpKaEFiemFLcUlLdjNKR3U3U1FlYThaalZsRFdrZkh1MUJIb1MwV093UEdPTVVzYzhDVWFpTUJtWkRyV205Ckt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkJrOU1iRjZSaUhQUHB3ZGdLRzQKYytwMGZMM2d5eTc5QjhqcDY5b1Q1L3R6Vitad3pJbWRDaGtFV0JYYmdwWW43TnZ6WDF6NUQzaXhkVDRESVhuZwpNalJXNjNFRjRlZ25rSURlUzZaV05BWGZNd3UwSHYyV1Y2a2NaNGdhZU5DaG9sNjg2Q2JCa3JPeFBVWVNid2VuClQ0MTZmNXM5UG44eXNUNzA3YjN4Vmo2WFY0aXFwZ3ZkcTVzNXZpYU9MK2NuQjhFdjA4Z3I2cmZUTEluTDBObysKYmtsQW5tcU1RS0pLSmdVQWFPYmNFUC9HYmRHblJBNTF6VlV0VVdObVU2c2Y0VGJaaGhOOEdZMkdWVkRPT1lYTgpxL0wwaEhYN2szMUlxUVN3azJwTnlXaTk2bWRIWDMrYXN4ZWU0eUc2czRnVmVRcExGTkdmMUhiYVQwMDUxRWVWCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkQySk5aeTFLRFovNEdJTmFiR3UKNk1YaVBGK0tIRTBtTWVWVEdmZ0pwOXJtb1hYc2ZoWmQzRno3eGFSYnVuL1o2Qk9JaTBFZlFlR1N5VEs1eXY3TgozYk9sT1FlM0NnRjdYdzRsZW1SMWF4M0pRUUFZTjVWMXpTcHE2WDlaN3JTejBpY2tVdUZRYitvSm9ETjVub2FnCktEMjJUNkdEMzRZSnRwV1UwTVZUNHgzOENkVHNlT1RDVHFDMllyVmliSWlRVkkwS1Q1K05sczd4dnpSaUt6WEcKMThiUWJzemU2U2xVWkIxczJ6UmlNTGtUSlgxTm40K2FEckdzZEtPOTJKS3FsdS9WcnBjcHNVOFJqRElObXYvegplMXFOMWZHSE1URGExTHF2dks0VTN1RGxmVm1KaDdYMWt0VUx2VnQ4M3NnbUJFdTMyUy90NFdrOTdpN2F2bWZCCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejVGRThnYkxiQzJKb1ZkMjF6L3gKSmcySHh1OWp2UHdCV2dUWmZTRmN0TFNIN3AzRWF3c0dpNE1TNk56MTVFaFVtL3N4cUkrbUZVZk1MZVBPUFZabgo2VDJrdUQwc3VVcS8zY2RKMVdUUGs0cm10dEZYeTlPd285NXZrZDBqWnZDWjlFTjJGOEozQUFpYUtMazI4TWJSCjZkankzYy9EWTU0dG82Y0JSSThaSUxyVDQzSHBBUDd2bWtSWHlJK2lPbEsxMkVKMVduS004aTRVVGZuWTlvUkgKbHFCNGVuM1N6c2V6R1A3WFFlalN2elA5cGJXY0VvckpjRjdsVEpUakNpTUJCWU1IYzJQaVNleERwL0Eza0ZMOQpRNzliVEd2NUIweEhjTGR6RklBK0QxWEJaT2t3QWpZUklRaG0vSjVMaDJucUlSbjJHTUxleVBpL05id3lkSFBvCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3RGVzkzWVFNdTZKVUZoY0Y4TjEKTk9YZllpUnMzOEVTNW1XaWNNWHUrTytLUE9KeFNNbkFUWk9teWt0aUdhRGZ1RlhReGtsWEgvNi9JcWduL1hrbworNDduc2R0UXR2dzNXRXlEeDk5MkVQZE1ZcjJ1bVU1Snh1dHNRYTdWWXh3cWRycnl3elpOQVBOZ3N4L2tPc1V0CjNCMUVNKzNSUVZFaWM0THZ6dEFWVXhQNHdQS0Y2RGxtdHNEWXptdDhlTVVya3Z4elBGc3U4bmZ1OVRZSVMzK0wKNU9GMkZqcU1keXRTeTcwZFpyQTBYQ0N4cGdIeHFPQ2J2b1NLR3JmbUg5dHI3bDZMZThtUU1mVEVid3dBQnZRYwpOcFg4SUZmcHY2dURqOVBSdWg3bFRnZHVmZndPVlhYTUV3b29uYTRRb2RuSkFYUEpldUVlSUliRDUzWlA1YnVOCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzl5UVN5c0dQSzJiVzN0QUFFNW8KOTVZT3Joa3g3Nk9NN0M4aGN4NDlId0p3NFpqamhUQjZBQzNlemlRblJwS005QzBOeVIvamJKbWJGSnhPTzlGVgpTMWhUT2FHMGI5Y0hyYUxKdnNoVnFXeVpNT2JWcUpPdVdTaDhoaEdGRGZZM2JMOWVWanVxUDEwOEh3czdKUlBBClllSUZSa3VNK2dGRVpDY2RNMWZ4cGRuU3I0Q2RUQkRxVFBiN3RHQ3I5MXV2bjhhS0NNLzVoTURYNDJMeldSMisKNTdLOERDV0d6OUJEbTdGUjdPZ0ptWVNWNndVMWtROUloZlZ2ZTcrQzY5VHpzSU1DalNYYTJZZUVZeEJ1OTZYMAo0YUE4K2hFeHlqNTE1NmpERHpmQ3dJV2EvT2VVeWRBMHpmTTJyNC9UVmVhQlcxaDV5bzhYMXljV2J0Mnc0ZDd3CkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTN5TkxicHhBYS9iQzlCNyszWlYKa0RzMVRNcDdreWsxTi9WQmhTb2Q4S1U3U3VtempNeGlka21mZ081ZUxsN09ZdmFuYTFueU0xRm9IOG52ZFNKQwpiQmF1THRjVjJpdkloV0o1c01XN1p6eTFRNlNaQ0MrZU14WnhxT0RTSm5wVFhsWW9HWENSQnB5ZTgrek1EYkhCCi9KNTg4aWNkSjFpcThJd3p0RnUrVFptTWltMzIramhHcHp0Rnh1VW5rZFpDM21ncnFRMklkOXZKVFdLdWtOSngKRVdkZWRrd3Rqc3lVTWROUXhJMU14U0oyMlpoQXQyV2tjTS9HbXM5T2xORVNBelBHc2dqRnZtOWxYeE5EK0p3ZApCOHVOZCtkeStJaEI0ZVhaemUzaGFRNHpGYlJ3Ui9FbWdMVXp4bnZndFlETkRwcHdNbnExTDhVNnp4VUx3Vnp1CnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWJQREd3SGw1RlRwenhMaGJqTXAKMG9aWXVxTDRLU3pRUWZ3QVBzbmVRRHdkYTBVOXlzS2s0U2RPLzA4WSt6ZndkLzdzblpON0RzQ2szM2xoWmF0bApDYnUxNDJuZ1lFYW8ySVhTd21aZ1dvRERUU3FlaXpjejdVaW1QemgzMjVmZnoxVGR3cHJ0U2lUb0Qza21FN0lXCmRxbWZzY2pTK3FpRmdHVERIUGhTcjZRdkF4d1lwYjVWZ0x0U3NVQ3EwNDF6SFRJZjlZNENZbkNJU1RCbmpwSkEKMGJCUlU3ajBwQWpzYnVTdm9RMzR3dU1VV2Y3L0U2MEdabUNVczJXb2s2eFlKZ2JYSXJSNlVncC9KT2MzY29jaApRWldiNjdqTnNMMWZOWEc2emgwK1EvUlAxU29MZzQ3YytSbkRHMGhqV3p3L3BDcjRPMHZ5OXYyMXM0WW41YnJZClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXNFeXNJbGF4VGo3RngrT0d1ZngKR3lrUFZhVThqbmhjTG1BYVlBWjRZMWVjWDBGUkZ3UmQ0czI0QjVxaXJjUDNONk9EWXN3eFVDeUo0RFYxNVlyOApnYXJGOWxaTUpxUXA5RDlPVGx3UlhkZlBjWUZCYmxrellLUmRoTk5qVHloYlp1SStQc2ZIYjFBRjRnSnB4Q2NLCmJQaSt2cTFYVTVZQWhxZEJnNUU4Uzg5d1ZQYUxtN0pNbzc2U3pncnRRYlBuYWZVK09QRVhiUDR0eUZmb3dnS1EKbHRXSW0rb3g2aVR2ZXhrb1FHcDRRcjcwck9hUHNGRjhHVWRUMlJzcFg4K1VaY0QyWFNndzFBMHk0Wk05d3FVaApYcFU2VXhHMUtWTTNWTUpycGI2YktRY1JKdDQyaDJGdzY4YmNGZWRmVVdZWitmcThRcjR2TDFDdjlMdklGWDF0CkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVZzdVhtT0QwL296WVN4VHNickEKNEY4ZXpEYVlZN3F0UGxFQ3h6Z0paYTFiRUhxQ2RZYkpFSko0Y1R6Qk9IM3pmWDQ0bUhLV2RJQU1Yb1FYR0hqQwpKQ3hhMVlnYjYxTjNWanB0ZDljQTlFeVRhelRWeEs2dkc3SXgvbGVzK0J3MDlMbzhLZlY1UjAybHRtWFJrUlZiCkhsUFIwL1Erd1p1MC93TDdKQmJqaEI2UE1ZbGtUQ0RjZzdNVWN1aitFRTBHeTM2azdDMmRiWGF6YlptSHhQYUcKQmxmdmR2U3BhNmkzM05TUWhQODVTUFVBc1E4NjBFL2p6b0xiRjkyRmttM1VKYWJ0cnYvY0pXL3h2Q3RmTE1BMgo4MnA2RjQ5cGhNejFWY0d6NVBkZmlRcW1teUZiRlU4ZEZxRmJOblo3REhHNkVvWkpQZ05QaTNybStrWDh2NmF3Cjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXkrY3M1aW54K1pRSFJnNFJWT0oKalp5QTJHRUhGY09zYURubGV2YlV3dVFZNnpsc1NFWmVYNHF6NzgzdnRXUHU3Y0NlbHVMcFJxellPSVppOVo4SgpTMEZpaGpiejhsK3NrbWpTQ21YTWpuV1doTUszazdSRlR1eXp6ZEhZN0lQNWx6VXVFYXgyTkNVUnZPK2d6bVgwCmROYkhUc0N0amJMWldKSjRQUjFidzE2SEl5QlhlMGU1bDFUS1BBV2tYR3g0K0NndG1yeDlCT0QzczlwYU1MbWwKRUlFY25TUXAyUG9QY3pweGNSejdxK2ZFT2VZdkdQc1pndXhwa2J6TEVmWFRITUhRRDBIczdUd3BPK25CRWo1KwpqYlVGUU9ZNCtkdTAraUxyaWZ2MnF0WFgrckhpTDc2aHlubHFDaHlEMTgyQlFkQllsZG9hMGZDdE1lMzd5VGU1Clh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU9mZ2htTFc1Q2phejg5OUpLWlcKT0xRZW5HRVNkMWdRUWFQU3E4QnNwYU1wRjUyTWtUZTE1Z2ZVbG9NUVZuOENOTGwzTkFEQWlDdEExTURUMkx5Twp6aW5tQmxXanZscGR0MkVuMDlkdGZ0azdjOVExK05iMTdoQTUzVWVnYUNxZXBweVZSS1BhTnIrb1kxZTVlT0ljCjlWVEc0OTlzVzRvREpoWG5OdXFGN2ZreG5ReE04NWtHbUZuV2pNOGc4SHpvQVdYczBTamQ3UjRWWDU0LzlwdmwKajVublNMYmNwdmN2SURtWWhBc2tVS0piNDd0ZG5xMlpWdFRUQmJwSkM5UmpuU09naTRnQ3hLa3VvUkZNbGVMdgpnUjVzbm1BejRBM3YzR2dpV2VYQjl5STYyZHJVNUJaaTkrZzdxOVlYUGhYM0pDQkw3bXRad3d6SjBOTVZ3YVU0Ckl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenJvS0pTZXNnem5TamNxVW10dmcKK1Zpc1UzbElFNDBmMkhWb1MwL3RYUW5jYjhJWDNvdzJPNWdzR2o2bDZlenZzT0JuTHpyMEhvWnhJODJKdThTagp3U3kyYVFOK3RVUVMrZ2RJVEViQjRoVkRoRWQ5cFhBMzIwYmZaOXlsUC9VQldqUFEwQXZNekFGdWZXQVdDR0ZtCnVrdms0cmhVWFp0aUZWMEt5VStiNTlPbVQzaC82Sng3cmFsdlA5RXdrOTlqYVlpNGtYbVJvWmFRalF4SFJkdmcKWUNwV0F0ZmFUUlRhMEljc1FCZWx1RHcvbVgyaE1qRlRSSHBGcnp4NjRSYXNPQUhvR0kyUk5CUVRISm9kdUxuMwp6Y1puUlBIZDcyN1RhTElENGtpZ0lVek5pSXJzNXVxU1V5SXBSTjBOSzJ5aVZUZzlxZHZJLzBsMFpwczVrSWlRClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEJ5cTBmSHNIQnA1Ym5zcmw2Tk0KVjBjTjJlbWpyOVdwVjlGMGNUWC9MTzZ3UUtBWTJkQmkvb2hnbUErN3hSWmhCSlpkNEdvTGNxUm1uYitsdkEzcApkQ3FvRWQyY0tBd3RnL044Yk5XL25jQStLdENLNXVyeEEzbXJwWStYdmYvL3ZlcVlCaFZEdXJieTNkTXdCUkdBClJXK3Z2bS80VXJ2N2gycng2WnVXaDdFS2ZZOEV5ekl5QnZQdHl1b0Nudkw1WVpGb29PL1NEanQ3TUpVNnN0OG8KdVlyUTRPVnRaYmEydmk0dmZMYkIzSmE1dWY1WUxqM2VsWFZNWU55Yk0vSkdaTUdSS0FUTkVsNG1GMDlQWTFhbgplN1hLNEJhWTZLUkJOemttV2N4OXl6Y2t1a1kzZXkrRlpBckZiUmJPN1BxamRLUkROQW1QUnlMYTBKdmZyRzZjClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXlTdVdacnA1YmkvL0pzQ1RhaTkKaDNIQ0VCWFNHOS9KUzByZjhqakRRVlE0VStObHIyckthcmk3WC9YZVN1N1drNXltVnlIQk80cUoxYTBPcTdGMgpkeVI3aFFsVkJYeDgvZkxiVFh3em5PMHoydWwzUEYwZTJHYXpKbkcvOTVmMUJkRU9qdkxnZXF0MkxQS2s4VXNrCk5palhMeUxvbEJyY2VNQ0pTbDB5dzdXZEc1dnYrMEw2bTBsclpFa3V6SDgyNE5mN2FTTXZ0TXF3T0JGdm40aXYKdDRjREo5UmhaR1NYU3BnQ2dvVkdQVzJocHI0aXBIZ3ZZZGd2SVlQclhiQ0dpUENGMkVJUzgvMzZZWHEvL2Zpcgp4YjFOS2RDV0tjcWo4SmMrTzZhMUw4cm5LZ05VNGppV1RVelBhRFRFdEhkWmUzdjkzK21tMDgzVmtWQXFxbzZWClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnlYL3NyeVlXV3EvR1dvdUlwZkMKU1hyeFFGU0dXd1E3QmpFLzdKUVRwMjgwM1QwOEkrZmpwb21lSk1jTVk5MjJkU1RLZVpIa0dnQ3A4c1c4MjUvRQppMVpoemQzSHF5ZE1MbG11eTJNdjR3R3piOHU1QlY4eThpd0NtVkIrWG5QL2FsbTF2dnZPampnNklJR0FkSjlICkFYcXJpZkJxcXNleGs3TVowS2R6dDZ1U2dBM3p6WTg3VC9kcU90YSs2SFVDa2RsOTNBeDBIMlhWY1k1L29seDIKcWZSN05YM1VoNHpIUEFKc0kybHoybGFneGFvVzhCRlc5UFg2NDdqV1FvdUoyRnZFYm9hV1BxVFhRU0t3eDhTVgo3MVp0cVNsQmJyUlZFN2VjOExrbVU0VGZFWllyZHBBUW5WZGV5MGNZWVYvNkxhR0tlYkdzd0NvQnQwcVJVQk1OCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlNRTGp4N2c3bEF5ZWN2MVlGMWsKamF6aFpCTlVJWmxZdGZHd1g4SXNrak5VbGNqKzlPc3dPSXRRbEZjWjZMZzM4R2dJdG14elM4WU1GdktickpMOQpSdHRITncwWE5VSXdIYmVtd0tZUUhUa2xFaGJnSHVGdEx2YldyRW1qNnhmc0dpY1RYYXNDK1pydW1sQzQzT2l1CmVDbEhzcFBCWGpxL2lmWUR5NHdjVHRqQ29WSTZ2V0ltRS9hM2g3REZRTDJPZXErL0dpaFU2MmFQdkV2RHhvVmMKaUIzeFNwNjhpbnRHQ1dQejZSNnliME9XazJUYUN3THpiVkNJSEhCVlpiaFNEWHlMN3JpVXZQMFJuK2N1VVpkaQo2SjRDRGF4ZVhnSDdZeXgvWEMyRTRFQ2lxVncyT0Z4SnlXTU0zTUtiK3hvV2doNXFNZUVnODhwZ1RRczJ6NWNQCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVhaTklKMXpTWXJuQWo2NXk2TG0KMldiMWF5M2JIREk4RStKWndkWFN5U0lOZVpXRWZLOWJKb0tHYkRXbWNObHZnZ0VpU01JT05qNk5sOWVoNndyUgphYUQ1YlNGZ3Z4eE5XUUFNTkYrQ1A0b0lnMklnUXJBMnc3cExmMExGOE5McXNZVktMLzk3YzFuZ3RBYjlXNUdXCkNFTzVXOEZXUGg1TlJNbGdFcVYwbjI0UEJVbjc3Y0ZIc2xhdUxpclBSVTZtVVRaa29rY0FMLzVWWHZ4ZVpFTTAKOWR6R1JwQnI0cmdxTDNOdUJQUWpPVjVTQ3l1RDUrQ1RKU2MwclFaaGpRVmUyZGUzM3VLS3lqTkx0elUvWU9zegpFc1hIREQxb0FvZXE5RDFBaC9qK2x1MWR2QTRSZ0NZUCs3anZiZGpzc0tBbmVDb1hlQkZwdHcyenNJcTFOa3RZCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1ZtQStNYTIzbFRBeXorOG9adTEKK2JCODc3YzdEMGVpd3NLaTNZQ2NHN0VhaVdkaE1Pc0cybCtPZkRoRkZtWW9DbDVFQVowVjZ2YjdLUURpRjBOaQpUS1NvQU5iUUM0a3BsS1ErVldVSG5Denc2cVNBWk82bjNJVlRWK2IvanFSYlBMcHlwVmhHTUhWRnBqWDlGT0ZtCnY1a0pCZ0ZlNkVvd3puUXpmR3Y2NjJ6aXVhRzVtYXFLSjB5a3hWRExMcDNISC9LRWk3QjdIN1ZUcGZpUVViM3oKZmxlQUY3M3ljc0hTNHJnaFRtbWxPOEExVVRKU3JsaFZlNDlORllNRjdjeXZtVVFCeHRWQURDekVQM3R6YVk3NwpUaDRIdnZ2Y2xDRjZudHlhQzIzaXhjR1dWbmhOdnU4Syt1YjhSbS80V21iSEtYV2ZyRzVTbnJqTzFqVWJkQVh2CjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnUwMFo1MW1qZ3R6V1BHdnI2Sm4KRlQxaEp1VGcvOHlaMDc3UGljNkVxOFV3ekxoMFE0bGY2S0VXU05YSmhzOGR0eCsyaWxSM2Jmam9FZGhlb0gvcwpBUDhTQlRud2FqaWV5R2hwRm9zRVVVOFNEWkphTVNNKytQWG4zNlBacGc3Mmd6dWpVZVBFTldXM2VrRW1CNDNNCllNZDNZQ0NpSEluQ04wdXZyYjk5WHNsR3VlWXZROVZNSXdSMTlDNnZqaVFhWWFCajlFWEpJUjlvZUxNeFZNS3kKMVIzbWdDNmtzcVVXVGY1dHFBTC8vNFlmNUQzRk9Wb0hvTFAxR2FoQUxSMVJHa2pERmhLaFFaV3JOWXE0VVNUbgp2SURPT0hDNE96Uy9DVXl5UUVKanNaS1IrMnNVWkZaOEtQWnVUeklGYmRVYmlnZndiMjA0am1kWFlvVWVxc2t4CkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2RBQU15cTNRNnJPQ3hIcHRsS3IKR20vaVoyY3BzdjJrY0JTb1g1aktKV0ZNRER0VWlKT2k3NVFGdzdVMitvYjFBNDdVd3pzWjhYOEZMUUVadlZRMQpsVjIzbjlWYlJRRldqTEc4c1BNd0tyTXMwSGVuaUtVNmQ4UG5sQXFhbDgreWhFSW5qS21YUnREM1pWUGJBY0FzCmk2eDVMTU92Sk9KZnd0dXk4NjBQUnE5YjV3MlRPNVFjVENkeFZVYlJsMml6aGwzK0lZc3RzR05yeWp5N2J1MGcKbUUvSUt3dXN3dTJyQnVaVDBWLzdTMkF1RHliWXF6aWhsNDVSSFFlNm5BUERQUVBFVjl1NmY5eis0NmdNTUt6TwpUVkFyRHN5MDA2QU9xYXVXc09VTUVHelllUFZiQ1hqalMvd0JjV3oydkNmVkNoUWdQdUoyQ3dqK1BWLzBCM3RaCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOFhiVC9Fenh3U0YwZ3VlMmoyWHAKelh0bVlUdDJoUjFkdzBZNXBPVEcreVk2TGJHSEMyUDRVbS9TSWlPNDlTRTdXV0JTR0IxTFlWdjVkbWx0c2NTaAo5ZE4xaXpYd3k0M1JSNk1FYU5DV09aNG1RTXZ1QnlKakJadEdKcit3KzNWaHhTaGNaUkdPMFF1TllEWlc0MWFWCkVIVTF6clJ6WTQvbU9hU1BGRDRpMFhqU2xxVWV5M3p1MjZTQVRTdk82RS9ERitJWHE2Q0gxWVR3YjhPcVQ1UTYKWWNqYUppemtOY0pQMS90NWdpYjErdnBoN2xZTS9WVmdwbk9BbEM4UUNsME5nUERSYnZFMnF5YTkwRjBTVWJKQQpxNGh1cnc4RXFwaUtJMmkvUElqaXZiU01MUmc2Uy9DUHIrRmsycTdRcW5TbXlacFFzR0NjS3Vjc05YdHlGMHZUCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHBCV2xKVEpVM0p1MU5zU0cwaWoKSWVFVTdpdmVSTzFyb01iSFlqOElPTERGQ3k5Rk1LQUpRQTlUWDZGdXlYa01SK1V3ZFJ2Q3lOMmRJZTM2OGc4Mgo3U3JLUEoweW9Rbm1HRHpjakgwdWthR0lEVEQwb2V6WGlQb3Z4VS95YXNWcFJYNzRmeG5hcDNiVU1zdGhOQUl3ClZ3cWNUVnVnRFMzUUhueDlMUEFrL2M0SnJzZk5XamRHRWxEM09rVzNLQjdMYS8xaWJJVmtaQ0htZC9JY25Zd2gKQkFlSSszamc3UG8vZCtjR0FjV3hWNXRpaGRhV2lkcGFLRVErcmp1ckxOT0JNeUVJZmJvaXQ3U1pQK1ZJS05VWgpUMlVnRThlNkVIcTlGdFBmbGNiVWV4NGVkZFc0RDJPN2xoa1AvMG1zUmhTbEJqZUNITHY4L2xPN1VEWWZEU3h3CmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmRCR0Z3bG9qalUvK3cxRDI1VTMKOU84dHZaTmljSjh6ZjhmdEx3RjVRTGpUTWJFQWhBb3llZENoUk1aYnRMMGl5cU8yME1nMGVLck8wK3M1U3N5cwpRcUp2b1hCWnR5WTJ2QXNqbmhrN0lWV3ZuT25vemNPQ0hUYU9GalhCTzFaMjNqdXRoZzJpb0JOSE5VblR6VFlzCkIyeVpDZEJraUhTaUtlazFYLzJzRmIwNnZic29PelZmY3U0aURJVFdZVVZsODdReko4R0JOcm5ONHBzbVN5V0oKMFFOTjZWUExiVVBrc2pPNllSNDMrZUVsTzVkUVVuMVJteFdNOGtSU3BmYlBSUTU0WlZsbzB4WjVJWDQ2dWlPLwpWV0RWNFA4SWxGSkFWYkFVN3lCY083aks3RnI0RFQzOG12SzIxazVKRVBJNndCejNUQ3M5TjR3NE1INis1dzY0Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1YyVzcwS1J3QllKMTFkYkZmTnoKb0xrYUtYNnhjbW1JUmRRdGRNOUxEcFJhdm81aDkyVlFnTkdEMkcxYVlNcXJYZDd5b1VZRTQvUmJUSi9qUmtJRQpKLzcrTmZiQ1cvdHRaT2ZyT1RtZVhXbEVHaktpOHRYVllaSWNMZFF3Rjl0UHM1WVI0VCtVMGhFQ1JsR3I5TERECnZPTFJNc2c2UVk4YzBhTndJeU4yZDVNL3JETWZTL2ZOTjRGNDhaVllmNmhkSkRMMnBIZXBFbVlYcjljRjNjZ1AKcnpVTjVXbHBSRmJaRHdDSTEyMGdKQXdTN3RtSGtPUThHMTJNT0l3Mi9sdnNVUHNmQUMwUm9FaWhQUlltbGlTUQpmd0w1TTI1V014cW1QZjR1WTBiRkFUaGUweURsU2VOSllvaVhLNHh0M3c1dmczekxqS2lCS0dKVHk3VFZqL3RtCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHpERldWd2MvVm1SclJaS1h2YUYKMDJDbG50dUJRV1A5QTBaZ3ByeFNwRVRzNVFOMWp4MDJ2bUFtRDhuK1ZZUitNTjZCVWk0b0M2MTd3eHNjcWdQRgpCN1BHWnAzbExTTU45Q1RMQjcwZlllVytsbCtlaHFId2hWZTZTM3Nhbmxla0pyWC9rMmRCMUp2ZGtlQzB4SUpuClpHWGlxRmZsVEdyM3IvNVhoTEV6My9GU0dLcjVHUmtOWXRLWmJmSXltR2MxQ3REalhhRWdRQkVGeThJRllFM3AKbjNxUmw1d2pnRWVxTUFFc1BCMGxaMEhTY2c5TGJKVmFuWHFUOHJMWDB6cXV2aklqcHp3R0NRUy9ldG1kRVR6dQpBUzlnTnBpa3BzM1grK2dMZjd2S00rWDJhTXBmZ0w0VGRyQ09yNkV5aVlWZnY2SHl2RWZZYWNyeVFTOTdNQ2VoClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlpZYktuZUVPK05uai9ZZGR1ZGMKSGNUSUhNU2U3Z3ppbkVIRnU3QUUzWGFLM2hRaXlXZ2V5bXRoeWRNdk45OHY1STdyeGdWMXBoRmRZcGFRMHlKegpEbUxzMytnODY1MWU5Tm9LS2JBNXo2MGt4anFHOEFTeS91NkFQODA3ZVFQL3pXN3lkSjVWeGdiMU52S1NPd29hCm1Mb2ZLVDBwM1paRVc3U2ViRkJpaExLTnlPS3BQaHJmZHI3Wm51bXpjZ3l2ZE9oalRCMTJDM3ZpU3NlTXduWVEKWk1wSEJCcEE4OU13d0JPMDRHb1ZMb2ZsdnByNE80T1lDajlQM2RIcXVFdEZtWms2SkFSWmJJZnIyN0V5WGZnTQpRcFpMQTV1QTJaanA4M0dyM0Ewd0lUcjg4M0hOUlo0Y0tVZWFmaWV5ZCtoZlhTTFNNZURKOVk4N2poK1g1OVRoCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbStQVnlUemFWYzA2cmJuQUltb0MKU01zaHk4VTZ2N0VVK1dpYnVOKzdKOWJncGxma3B6dTV0TEU5SU1pZlNJbUFLWXNaL0dsTVZndlNPaHVUTUdsLwovMG55MjdVLzNiUVVuQWM4ZitMSCtQT21xZ3FNQkdQV0lLZzhIR2lHd2oxMlhVTGR0UVVnY1pXTHc4QlFEeHJ6ClJKZW10dFM1MitmcFNXbzZnUC8wSE9zSW1Jb2NEMkVHWWlUK202d2NUSXBtQkExMkxYRE5ySHNjNkd6ZWw1NGUKSU5TM1U3VHJYMU02UThJUFNJMGY3TmprQVlDWkJKLzFML3hwOEtoVzNPeDNGeFBPZHI3TkJXVnB2NmRzRWJacAorVVZ4NndsdXJsOXFMMlJPV1hsdWhGS3pHN2NvcjhVS1dMQ3ArQ2hLd0VIVkJBWllKQXJwUHFUK3VsSmZ5dlJYCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUR5b0xSbU5hTnRaM1B5VnhFVTUKVzc4c0NZTEloOW1uYVNDaFZJamdqL21BZm84cjRNMGo0Nlloalp3U1JuS2JIb3FFWW5NelA3T2VkQTd4N2o5WApHOUhJNG5WSlpZZFVTcDlSM2pGRmlmQ0pjNndEVDNTU09ZdENBK0tDWThFV202R01HTXYwQTVQdzhJZVh3WU1zCjJud0N4ZHhCYnN0YndFOStRVzJndTVTT3pVK1pLMzFZVDlXQVpMeE9YdnZMTHVzZ3BJOHQ1TGVqUHFpTG04VWsKSitCQlRLMWdEa1ByODhUTHNIdlBKV2lKajNTUS9zWEtHY3NsTDI4TXVTanlRaVRhaDhVaUVCQWxrUnpmUzRtYworS21TZkI0cnJjbzBFS0FMclFtaEtsMFpyNlg0M21WRVBGMjhUR2lkSVJjemRkbFVOWXBCNitwaStocEFrV1NXCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnpmTVUxVmw5Ukt6ZXpFZkN0ZEEKWThXazFBbG5BbCt0UWJYeVVvV2hEcGd0VUhqWnpMb2trWXBEMU1ITWZxem03dlFjTWYvdUxUaGlRMXcrR05iYwpLVDFQV0lKRkFQeVFwSVc1OG9MMWwvVzVjdEJza3ppTDlEazR6WmJYWk1XaENTMXRyd2dGYklSNzk3SnJJRzFrCitzb1RCSmxYYytmR3RuZ2R6bUgrTzFMM2w4UmpiNkFqMkhSTmZkNFIzcFBpQ3I5NzNqYVlqZXYyS2pTUjhvbEEKZkovVm9XcTVmb3RSaHJpQzJ1WElMY1hYcDZKcDBRc3k5VW1uSThza2ZUNGpQVXE1UU9uU3grUGJqbitWNkJaWApuNHZHOXdzWDlHeGVnU1hvd2JKeWJoSWFrcldyU3VOakw2VHFyUjNUMHNVS2JOUUlMUy9DODZqd0pPL0R6RkQ3CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXJqMjZtNGZ5OGhNT3hEVHArVUsKbUZ3YkdVWFd6R2xhMTFPN2tZM2hzY0pyVmpjNjRCSXEzbzdGcXN3b3VzZmFOTnVObk9lc1cxWFpRVTNYc3BLUQplZTdSZ3VsZ2E5RGs0VUsycENTUlpXUERtcDZLRUhLZVNVV0RSM012ejB0MHBZK2lCNnpLVlc0a3RuSEY0SFpzCnNmd3AycExBb0dmQytLbG9lMStRbjZNVGk3cTFMcHQzSGt5QTRYam1XNmlUV01iRnd3ME5FWmt4Znl4TS9WUnoKZ2w4Vm52TGVvQ2dUelhkRnIrMHp0WG9QM3NZbkxaMUFGUXpBT1VpNVVWR3VzM1AvUjV0SDg1anRCVW1YYWdIbgp4SzhJa0doRzQra1JxZ252MkFCSjdyTGcxN1Z2aTNuSzJmUStObG94bzFoWGhNVGhoT0hRSUtQaHEwcU1xVEFEClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckpzU3BndEFjM0FoT3dQSURVS0gKZGpoZHdoU2FodGQ3aGU4SGJpZkpUbVBCdEx2eVlqU2hnUXdPV0tXdzB5Nk40aG5EekJINnJqQ1BlNzB0NkpvdQpGQWhBRk1QV081S3hQb2VQeEltUW8rOTlUQTlyUjc3Rk9JNGxNWWZKSjhtMUNJQjlxVXhvendFSnF3U1VHUmpDCmdJWlV4S3EwZjFqbzc0THJDMzQyWTdOM2Q3WUJIWlRyZDE0ZVcrM1VmUk1Mb2doV29ETy91WERGZHFjRW5GVksKcVRCU1pWOWpnSkorcDZpS2tmdjk4dElEOU4zWjJZbCtZSFp6bWc2UVBqVkR2b1o5aGFudGIrejJYWjhsYXhvZQo0UFJoZ0p1WmsxaXMrT3MyUnFMQ2dMVjRUakZ1RkEzNERVZEFhVVRYdzVCWk9DbVhkZmJQelZBYXBFa1lPZ2ZsCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUJuK0E5TW1WWVN6cUdtT0JDd0QKU05Cd1luZm0zdFY4V0pPZ0IrZDBDdWhVWmJWUzJhQjVXTURXSHBhKytrZU5OcmQ5Yy9UTXhvWVdFZUw5RnJobgp3TG82MXZRbVdwbjIwaUlxaFRLck9QNk5acHo2Wm1RRjZDVjNhUkhzYWE5OXFRRzdpNG9jQXNkOXk3SHRJZHNFCnRGaGdKOVNocWhDZXEyeDNWSXFOSHg1Wjl1dHNicnNRMElRcW9DWStrRThxanpCYURqbXpOOEpyMmNsem93VDkKdUk4cFJRQWdvYWsxbi9EUUM0eXM4YnlIWVdUMk9Ldk41L0ZtQ2Y5clM0S0s3YS9aem9hT1lHNG9NRXQ5VkNCSApYd0REZHV6VkRXYS84T0dhUlFOSS9zTVdHN3RybUgvdE14R0J4b1p0dm1RU29kcm10UFFtaWdBenVvbTAzY0hkCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN244eUN0Q2pUdXp0eERxeDluZ2UKWVF0QlhzYWdBR0NGNDdnbVFhN1hickZMRzhpbDFSNmtNMG0zVHkvYXNOTzh3L0hxYVFYT0k5R3hsS2svRWlUQwpkZENDcThRSlFrYkJZcFZzRTJ5ckwrN1FjcG1JSHpBRzRmWmc2MTJWaWZ3LzNwMEs0aTRsL3pYSGx2TWJseWM0CkNzNEtrQnhaOStGVFF6eWRFamRkdy9BTFowa20wVC9jblVOd1ZVczM1TmZZODh5aHFRTnY5WXErU0VrYkZvVUkKZkNKTFNmdHJYZDJhaXhhaWNnbW9vZHJTM29mNHczeWxWY1E2TkpGZnVxZC93Y0YwdlhlRWVxdm5SR2hsTnlqdApFM3Z6OWJMWVZEYnYzT3FXRE5wWTl3VzBUTEgwQjJMdGhWMVVxNUtySjNtYXNTUzFMNTZwZWdZTjlNRks3MUt4Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNnYwQ2UyN2ZLaTFqL0NHYVV5WTkKWFlEK2VBWjZqZ0NhZWZncWlXTEZJVjFGRkowS1VrKzlGbnUwSmVxYWdqRmIrMEFnZXpQRVg4cVNqVExFc01NaQptTDdtdkhxaXluTzNzcC9YNTI4c1lHL2FvUzZyRGtkQU0wMEVleG92Ykhva0ZEenVBcTRsMEVJSFFJRi9HaSttCmZ5RTZteFBCQzdjU2xhejg2aHFvZk1aKzVkYXA0djFiL1VuZXNYZm52WjJxeS9SdFc4eHlaNTNBZGJTWm1yenYKckJHa1EwSWl2ZjJId0MvNy9DRDZPSEJJQ3lZK2hGaGxsQ2xxY1hmbnVLbUZ2UVFsOE5FS1E3aHRnYWVOL0pUNwpHK1hXNjhGNFdtTVR2OG95Y1pERkd3NFdVTWZCMTlNNUVScWxlRWovVnFyZ21ZS2dqa1I3U2MxeTBaZkhac1NsCmN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckFyY2xVSHc5UU5rb2lTUkk2bWoKS0MrSU82NDhsNHRsclc1eXRBTDF0UDhvWk9nWFBTS0ZBMzFoS2VwMkVhdTloT2hQTXloTUFDTXh5b1hTWTVCNwpZWEVpdUJqUE1TcmFSbFV2eVd1QkpyQ3htUTQwSFkvWlNGSFkvWUIybTNMS0xzMXdDYkhaVjZ4dHdWNXNXMlpECmQxLzFqMkRlMVEwcEo2RTVoblVJOGNDaXNyVzJ4RjIzWVlaSFRYYmJTRDJpRCtPSWJEQXZHZVoyKzBxR00xclEKTENWdExlMlltYWF5MmVZQm11angrRWxPQTVUZnRyR1JwamRoZmVWWDloM0pmdnl3ODliQ281dmFCdlRRZm5sNQp4OERtZFpoQ2l0QWJHc0JHa1FEdWdTemJrWnVoSU9jVG1ETWZoTTBzTUFnajNUSWkxRzJ5OERXSnlWdGsxZ3gzCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlNHMGdINS84c0lEK0ZSQmpLOGsKVFVmTzNyQ1l3dXBKQzh4Q0FWUVE2MG02NTNmbzJkT1R5UUNIK2laYkowTFQrUFM0ZGVIMnl0NW9aNERuYXZGVworeVl3MGZ3UzFGdGtSMG0zOE5selFJdGNqUE9MQ1A2RWVKZ0RiNk9DalNsQ2hLZ0NNaWp1ZU1Gc3FVWjNsdE1hCmV3TElYSFU2YmRqTzd4ZHBHSGhCcmVtcStyOURTK0tJTHZYNTdROTJ6Ui95WHgvaEhvWXp0dmVIeUFSVm1rSXgKRWZFU3ZDYU1PNzdEQjMyaDJhVmRKckt4RTc5eE90Y0N5NjZuV3dabm1vbmZrRFNEbHJrN2kzTUJ2RGE2blJURQp0UHJZOUR2N0RVeWVuZVV1NnZjUmtpQ0tiVlU3dElLblhqUWJrUGlBejRwTng3bHl3U3ZFR3g3a242dmlkeGNMCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzZ5Y0huZ2huN0lzZTlvblpVRWkKYUNvNkFmRmF4UVhrZGdpVk84YStUQmdPNkpRRXpyL2t6cXJ6bTZvQk1pcU8xL29IVWNPS0Z3cjYrcGpiOURlbQpNaWpPdExhamZ3a09LdElleWxPOUNIQ3JmNVY2QkJwSnhWS25TamdUL3g4UElyUmtmNXhnclN3Qkh6L2krMDFTCjk0MVd3bDFweXVrQkkwSkpOMEVXb0M3TGRMTXJhTUs1Q0Z5OExBUlJaWWM3S0NwYVYxeU4vbXZLeUYzV2xBVG4KUUZrcUk0QXUvRU4rdmgwVXJxNldjK2lyOWx0TUVEVmxZQlM4UUJOamlnbUh4aHFLMjM1eGc1VzRlcmJoaDFLMApoUkt3dWVoZkxFbk9qeFIzdGVwMnV5aWpFZklOeUZBQlREUWlsZndwc0NTbVQrVzhlbVhKUk9oSDRETDVnRmF4Ci93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW8yUTVMdkZQc0xnZWd3dzFvRTUKYnZCb2RrdmFJQUptbDFrWnVPMnVTRGE5R1lBN3VjeGtHWHdBVEYyRjZseDF6QU04V0dVakVWVG8rOUVic3F4WgpEb0NEbk1JWndKcVN2UE5wN29GbE9sMElkVnE4UVAyakJQRGFnczRwUTBFNkIralBOS1RIVnFrbTFHMHJlaGNwCnNYVklOSWJPMXZ3VHMzbXc1UEQzTk1qNHhaek5ZME93b0FDT3Rwak1wQTBvTzRjVDVPK3V1aVY0bnhyWGVxTHMKVVA0eTkrU2lnSFRCQ3dnaEN5YWxPbDFaVFdYMTNJa0ozTnpDUWI5YnROdUF2SlAxaDNjWmFGbzdXWUw5RUlHZgptK3FseTdPSitoSzJzZXozOHQvOGxiR3NNS2gyYjUvRXlBcDdSNjNjWGtTQ09DRHIxN0NoT3V3QmxlTldEcER3CnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFRKQjJud0xIemhOa2JyWjRnWVcKKzNaNjhCVDQxUmVtcHRYaUtKVjBXVm9mQU53U2JaT2gwcGc1ZkFscURybnh6elpJMWdUdVJkNndWWmkyYUUvcwo2UXFReEZaVFdnNmp1UTh6SXZIbjhCdzg1YW1Ea054K0VCOXh5bEZhRlJLa3dkN2lLM0h2ZDUzanhsMTBQVEJICkZML1drVm1sUFIrR3hPVGp6eTZVTktScWlhZVRGc1NEWVJHaUdFZ3cxSDRKSHkxNXRBelg4VmVlWGFJdkRIVlEKNU5RdlBsUHZLeTAyc3lQZ01VVXN3K2dnSGVHZHVLMWtJQ1hwWTFKN0gxTmdWTnladURieGxoajFHWXlPVDVRRwpwWGl0c0JxbHhwdlhheGE2VURmYTNaTzM2VEhqZ2ZCQnBZRUorSms3VFdvVTRnYXZBSDZsNkl5SDVOdjN0Z2Z4CnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjREL1ZWalJ6Q1hBR28zSC9DcUoKWjdTVThsQ0tnOUxhcTlZT3Y2bFZTVzl5bkJ5NURtZjJxQ0NLK3NMQS84SEtSd3k3cEc5OWRXTExxWmFFM3Nqcgp0ZjhFcFEzeStoaWMwaUVSY29zbjkrUk9pQzdIdmFtWTVOT3NhU2hHY3JyVGlqUTVBOXZIL2tnQXVaZVBiY1pOCnhheDBKRFFTQjFwT3dOdEdnRktMTWRXN2RIVGVGZzRMRDBtT3p0c2gzWEd6V1dUMnNLaUVIUmdTbmZZY1hUYzEKdExPcWlaQzVMUksvN1RhUjVONm0yUERldkQ1YTgwNE9VNmxZUDdmajhZVXhZU2gyTG9zRmdIR0lwYmhFYTI2MAoydVNNUmRCT29tZWVpZ053cTUxaHEwRUh2Q0V1NnU4eWt0M1crYUd3d2Yvd3Z5Y1N0VFRhamNhZGI1QlhxOFJ6Cm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNE5yTWtZb0llZW8vQTAxeXR1K00KS2pScGN2eHRiYnExMHVMbFJiS1h6bllpbkZnNkVsY0FHQnM0REZyMkZjK0g0dGZZSHJEU1ZBYUVzVVMzZFlJKwpMaXpBeFl1R0FjZ0pSTnRmNERSNVhTQloxd092SjVpQTQ3QTMxL1BhdlFJUWprVEYxclVMcmdndzg1YlhnVkw1ClBlK2dVVUVvMVhHR3ZZUXd6MVA2NlJIL1c1S0FQVlVDcGE2UGhXTG83NjROMzd3MDJOMlhvR2F5dGFrODI3MnEKKzZqN2REbERoNFpoWG43TnAxRG1tNVd6M1FmMjNrTXFiR3crK3ZoM2ZLajdGdkIzMjBSU2NSK3lqTHRENEVoTwpIQzNCcGhybTFVYmRHKzVpMDI2V2Q3QW1EaFd2T3VBR3plSkY0RW9xMWg2aURRM080NjlqMklaVDExd1lyS0psCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUVsaFp3MzFTYVhENnhLc2FxYWUKYXltdHFCaGJJTmZqci9hQTV6SDM4ZkhkRG9aTzJlaE5XM1ZlSDJhRmJkaWVReFRDNTRidENheTVUUk9nQVhHUwppTVMrUlVRL1BheVZZUnk0cGxGaGtndXo2UkFBNStaK1YzTEdvdzdDQTdxS01EYTFyaEQrazhvL1VtSEUrUXI0CnVyVERrNE9XdjNpdzc3RzBFYzBaWElrM09lT1dWdHA4NmR3T29jZDNibkJPOVU2QmdZdTVCU1ZQVlB1eEdDQ1IKZG5pckRqQ1dEc2xtbGFUNWhMQmR3RTIzdWM4NW5xcFJ1QmwxRCtYN1pkcnVCRVROY1JLM3Nnd1ZNUmh4b050WAprQ3ZSdVNiTG9VTlBLNkU5azlmWUFxNjQxVk04eXR1TUY0TmxRNHJTWXVOcy9mSlh6MkdYMnh3MkoyNVBSeE1nCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU81YXRjc0N2RnkzRlpFU3BBZisKY1dDZDRVSDB5S1pFaDNyTWhJMzU5RTIxQlZXdloxVmRkSjRHaTIwNS8yM2g5ZFZMMlcvU3E0NEdvMTc3UXdpYQpYTVhrMUx5N0o1NG94dkF5alZTWXlnU3lPSUpmUmdXRm5Cb0kxbTY1azExTUg2M0N3dlBsOStEYVNYb3FqeUhMClZPeHEvbVcxYWxhZWRZRGdWd1d4bW11SXhqcTNtbUk0NTdUWE5aQUVNZUJrYUVEMlBhRWk4ckV1RGRWL29JUXkKeHBraWlwbVlGVmRmdExrMGtCUmVmREFlMW8wRkJ1a1pFa0hvSDFDYk5tYm5VUnRub1lkV0J5dnZmZ3krU0ZINwo4TGgvNnhFTUh0aU5URi9HTE1OMmZVWTAxQVg5RXY5UU9URlhlMGN5bVh0TktUUGp2T295bkJJNlZJejU0NDM4CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNW96NjJCeGxSMm9LNG1YUWd4NkEKNjZ4L3VGZnNSRUNlbHVodnd1Z212cXN0eU1iaFVHcTF4Sk5xblVBYXBxbUtOdmtMM011U05pRXg3U2VNZVZ0VQpsK0hZMlhteWJNaFdpOXBPOUdRWDVDTmZHVnZTcjBSdXFuUDkxeFBmdXBrcW9zdFIzL3Vma1RyZGN5MXR0c1d6Cmk0SGZSQkRHWUJiMVhQL1ZmZ3ZBZ1ZidnZ0Sllza1NVR3IyTFQ3d3VpNnFHb2ZYNGNSR2lvMWpIanZKWCtMNmkKS0o5cUJCT055UGNObnk5M2x1U3JHOVFCTDNpMzM3ZDArZEpUN1R4SXdIUHFycDZrNUhCbzVISXJaN1gyVXBJcgoweHlmakFreGlDMXI0VFFGWkRORjUzS2swWmlxK2hiTUFUR1RmZjlhaWg2RzA2NnFKYVBQZzg3SUhNL0YxRkxRCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDZQcnpFNzVNTGhJZURPZjVqVk0KNGxRZFNYb2NvNFROMlFlbGFRNUpOb29jUytDblNZSS9LYVErL1g0TStQL3htbG00c2p1WlpRUWFmbTdUM3NzbwpoVVA5MEN4RzlCK0FDeWJ2QzNpb1JUVTkzMDBLY3NqYmNOa2JmYjhrSFk0My9uVGxCMUJKQjdyQWRRSkViNy9jCnhRUnV0RC9lUmUrcG13Lzdpc2NPQVBySHFjdUoxcnRwWEloOFI3dFhheWtKL1ZLQzB4aGhVUmtPYkRDK1VUbmIKRG1vSWxldlBCdk9DRFpHYjQ1LzNKVzJZRkc1eS95bVNEckZIb09SbTRGT1JLZ2wzYXFUSU1WQzZTc2FySlcxagpiYWZUa1M4bU8rYVFZMkoyaTNhb1ZBVGw1TzJSdzhvbnRLTnBEM3FnWUh6b2FQdkRYall4VlQ3aVlvUExONElrCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVlFSENuTThya0RPaFY5ZkRlTDAKQmxSV3k2a0NCa0RWZitiV0M4WlFFQ2RRNzBPbVpaYk1TazV0bmtZVmJGWGFSZXVZK0t2c1NGV1ZxT1NLTmVwKwpIaktoZnRaRkpGcFJrc0dUVVdSaVdMMWxER2k2b3JQa3MyQlJIUFViUDJhem4zRXBnc1JjazVBOVRTTDVKYWRRCnVGdmZMRXhiMVFYaDhnUFRUdDdHSDdPMjNTMld6SkxTZnFtVUlXeHJ5RG01bDB0T2g3aWd5S05MdkRIaXFDcC8KZk83dkkyMzZPWHBZdFA4NzI4Mk1iMzA0aThjRjV6elQxQ01ETHcxVE9TeFdMS0IwcnpGcWduNFhGOVF4MkJrMAo2bndPRmY4L3JsSCtOb1MwZGE1OUZZTkVXT3JkLy9yRnpSZ2gwZElZNytVTEx2eXpPTDh0M2FMUGNZU1hCaWp0Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0YzRWVYRzk5RkVKMVZlOW9yN0EKR2RIZmZVNEl5QjFSMWc2YThmaTlJbGlsSmlqaWYwNWttUG9Ha0tzZTdzNDAwcG9QTDc1dVZ5Z2VuNDcwT21vcgpXWG5wRzhRUlZaUWZRK1ZJUkZQMnhLYlR1cmVpVDZERzlzS3ZqWGJJOVdzdjV5SWNNRlkwSnY5SzNxeGxuZEM5CnlvclFEcVVIZG0wN0NwdUJ4VWE0bDNhendEYWo3OXk1RXhCVERtYjZSUXF1dSt5cUE2OGRHSUpHMmtNQ1cwOEkKL282Sld4RTRPbER2dE0yNm9MdlVja1dFZHFQQjNxRjFiTnd0bTU0OEllZVZYczN2cEoxOGt3dGpVWTF5OHc5cQpFOFd6UXc4bGVNdlBYZ0Y1dW4vMEUrNEpIWUZ3bHduNHJ4alh5eDdCMzhqc0ozSEdaYm56U2JRKzJuTFQrZFlaCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbVdFZG9zaVJOV094aTBNTEMvdEwKMjgwdndUREhPM2NsaHljUFRCcm05U3ZGSnprWW9zVmFPalJPdkRmNDFnWVQ5L1RaSVl6dEgya1FvcDFmWGNYTQphaEdMMDNkbm9QZTJiZVo3OUFBeUFyRlc0dm5rTkpJMytwcHdyQVJuRG15bEk5ayszYUJZYmJybDlHMmt3aEN2CkI5ckFzUENnREdycDZjK2lJdE41U21WazkydS9PSVdzRGZtejl1Y0gyUi85VXQzV0NPN0lWRlhrNldNVG1OSmwKNVM3UjlFb3dYS1VwNzlJWGJYQ3BjTkt5MmVWTElXTFdYM0V4ckZXT3pPYUt1RmVIKzI3VG1aTmh2bFNibnlHaQplcW9QRDRhZVBPelQwQmJGVzMwanBNMTdkaUdOYjFRcy95Z3ZkN1V4c3NFbjArcUU5UWxFNlNBK2ZjZnhoUmdVCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1RZd1hnMTIvS2Z2NW94bVA4Y2YKQlNkcHNCeVp6TnhNM2JsN3A0dnF1NzJzNE16RElVcDRCbTVrUWJ3Y1cvbWN4eEwwenZUNkx0NWw2QStwd2lpSgpONU8ydEs2MFVFOG1VcnNINDVNRXI2ZVc3RlQ3aHduYitsMldDbHJtTFk4d0Y5cTN1YVRIVnRPb1hRN21XemRNCnAzWEQ1cVF6TUdlb1hGRThITFFtZW8zQ0krOWRlMXN1NEVZWFFvd3k0N3g3R2ZOM1d0bmxVeTBBS3J6WWF1U0MKbmNldkJGUG50L2c2d2EzcTVaZGExR21mNTAweXlXQXY5R2treFFXcm8rNGliU0kzVmNRaFRWUkdIYXpyWHdmZAp6THJ0eis1WnJDekt2UjhTeTIrMWlMYm5kRGlZRGdMMjZpOU1kRXNadXRNaVVFTVpCTnpDSUgzWXlvY3p3WE00CkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBei9Id3Q3YW93T0ZOQXlTNnRvVS8Kdkdidll3RVFhcHJUcTJBYy9zQWt4Z1lsYkllZDRrOFUyT0VZcGtoN1dsSUIrMmE0eWpqdFlPdDlHd0d5QThKUQo1eTAwUFFyOTVhZVV0ZHh0OHovRWZ1enRoR0JyRVRmNElsT2s0ZFlNU2k1czZDcWo4N3AxTVNnSnc2QjdhVWM4Cm5jTTlSbWwvc1JQWm9xa2U0M0ZpMFN0M0RqZGR4UUtUem9jNUJJa3kwRXl6b0FrSWIvZCtYZU42aU81TGtpaEQKaDZOK04xbERiRjNPOG1OdzhReVhpbXdheGZ6QS9FMGpqRkpsYlpPN0orYXFITXgwYW9YRGVacFJCSUVEUUxHOQpaRE42SFd5QkFBOUIwWDRTaGYya1BiKzBvckR3SkdIU2xaQ1g2NTNjd2Nsb0xhK2lXOVk1cUh0R0paNXBLdTdjCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjVrSGQrZmd3K0d2WlE0czR3bFgKTktDMzRlR0l0T3lvaFl5aVJzLzJYT0ZDSDNmQkJkVnlrOUJpOUJUWTcvaSt2OXlSMTlSeDR6akUwQVFsK0t2Ygp5d0p6d05HalowU0swbzVCaTM0VE5QYTZlclY0NGN2RjFiMFZJWk9SVnJNSUhCOHBnNVJiRDY0UWRTVnpVcDRGCkNKMlpTTmFwWlJldHVuOUY0MGt1Sy9ud2RtU3lleTQ5MVk1KytIQUs1TDJTYW80akpBZnp5MmEySjdxdU85a3AKMWpTaWFCZFY3RmxtZG1UZEgxZlEzcjlwRVhuV1ZXTjhzSVY0aEliSDltbmtOMnBIMVdiaGpVd2lsVFR0bXpPZgpNNTRHK2g4ek9CKzFFOEIzMmtNYytuVEJxc05FcFBMSlRsODhYSTFVZzlIb3JhTWREblBBOG1EM04rWDJBcFdoCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdCtwRm14eFBiZDVzdWdGbktIMUIKUG9YbFhNd2tIREg1eUI4RkxCNFhnd2gvOFVVZWFlb3R3LzhmWUlIZWtpL3Y4KzFMV01CS1BZc1ZiRGNKcmY3QQpROFdFdmNlNVhua3ZpZTBNS2ljdWZsMVpobExQQytKNlFhcWdiRGNXZWNOWVdjSVZHRlgvdEUxTTBtY0tCV01oCkdpR3JKdUxucXJiUzV3MkVsc1J6cHZrcXV6RyswL2YrTHF6V0pUa1IzcnRSRXVIdm5hSk5CNzc1S2VqWVNqVmcKNGJKSWxSUzRsVmlSdjZ2SzFNRzRDOThvU0laQ0pJd1JwR3NhelJrcFAvR0NmOTVvSFJ2dHRGVy9CNVp3RjZITwpIYWk2a1R5b0w5NHBEU2czbFQ4TGMybjAwV0xsd2k4OHQ5KzdtL29MVHpKcTJrNkZDeUY2MTB6ckx3ZHpVczRyCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlU2KzhpaGdneGlZd0ljNUFPS1cKWXZQMlBOcCtVelk4Z0RPeGNsZ1Fvc1RjTXVTSXV3Ujh4aW9kMk1Bb1pSTm9IZDAraG9KUjdJLzNISGo4UTNLaApabFZ1VU5ES21OaDl4S3drSHVYYkREeHo5V3Y4bTIzKzE4UitRMWIyRDI1M2dpRzI3RFNkWGFvQkJReG0xMHp3CnJzUTBLTEZKb0Z4a2FJbHVDV2xseUkxbnBGZWtUSHZibUh5eTEvWWZNdHo4Sk9FMlQ2cXdjWmFsajIzMnFJeDcKcjlWQVYrbEtYUnpEdXRDdEMwdzdoZWY3R1VpalZWZXR3SitqUW40Qi9mdTF3MldjTmx1N3BjKzhuWjM1NWdwUApGQnJiRVUrRXdSSnZySjlqRm4xNTFkc3BFZ3JjYjd0MFZyMUtQeERVYWZKeEtScTNhOUw0ZWtsQmhlYUd2dGEzCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnAwRmZhaXphTTlIQnpNOGEyN0YKYitoZjhOTjNoZks1RDN3cStsMUNZS0FLM0NoZWltTWRBS3BWclZjWWYyNUdRTGhYUFRZdlhGbmxkb0tXL3A1aApzeXJRVjFESk81M0YyZWxJV2I0UWt1QlFoWllSTERXUjdUbUFleTV2VnY1NDZERy9jQ3JBWXNaSHVVczU4L0s4CjJkUnVLTExZSGFjRFdSc1NNSzcyT2ZLVEdrZGpMc0JiN1Q2d2t5K1VJakE4OEtXYUJCd1JOZkVoS1F0SXB0V3kKMHU2aXErdzhqVzFWZDNDR1VoaHRkaThVNm4xaW15UzlZNUxIYTNRRVhvVnFwZmVKM1VhZlV5T3IxQ1lYMDRHUgorWmNGWmMxZ2RobkxFc1RBNFh4Y2VnWFVhSlc0TlAzbmFJb3Q2cEdNU01NVG1NZXhsQjNqbW1tSmNCM2MxVlQwCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXZjZmw3aHZ0ZTczNys3R1FHMXUKOWlZa2JienpMY2ZYbjlIemRnaldPQ0NuMmFoV0d5c1dhRTBBWWc2dW9FNVVFZkw3NGkxVEhWdHRIMmRvTFFubgpLZVRERFRWeFUzV1ZhVWN6M1BvbjBZWWthK2RwdjE1d0FEMk83TzJKWk1TSVZka2RGbUJMT2c5VmRlazF2NXh3CnFQQ1dPeWE4RHhydlhsNmtXRjBsRU5OdU8yWUdhMEV2T3R4dERUejV0ekJocFRZVDl0blI5SFNiZWJGYVRiY2UKVVpPZUIyT1N4YiswTUdONTVBLytlbnlSN0FoMWt5dE9BMHBLT3hXblpLUFJ1WVVOTGJlNm5jbnNHNW11M3RRZwo1UGpwdndEcGpDVXl2WXlobDhqRFZQUGxvK0Z3R3JZWDF5VnVEb2tFMEJOQXE5WE5kV1RTL2pLeXJuSGdZcVRuCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3dFd3N0a014STc3S0ZFaTdXWTYKOU9zcFo3dER4REt5RjJUNGdtU2RGV1BhWUxCSGljd1I5cExCdW9IbWtKUWlvVFJsUHRaOWh3SnJMTENQTTdsbwpDdVhCZDlMMWxiZi92Ukc2dEt4SEFoMzBscUdwZWpjaWlKZEQ2clFoZlNSTjhzZlgxTWFDc2ZiSmxFbldHQ3NhCmZjeTgxcE91YitlOWRzelZXeExCaGFoN0hUT2pjWEtyMlZSWUloMGo4bjJyMHVzZzRsSFZLNTF4NUZnZEpBZ24KNTR5RE5mYm1EMXhBN1k3eXZhblBEdlp0V1owVFFkVkxWMmdYZjBWelQxUkI0T1M1dUdnSXFQd0MxSHN5Y095NQp4d0NSTVVkd25wZUUrWTVaL011Qng2TTFDQWU5VVJ0RVk1d2RhS2Y3T0dQalk1ejIrbHI5bTIrUXdtNHh6dHkyCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTdreWp0cDBwdWJlQ05PRkFCWUoKclV5ZVRKWUl0aExNbUJacUhDRFV2bndnQVJrd1pWR3dVdnNsci8rSnJsS0doeW1GK0RLVzBZOUdOMFU4d291egp5VVc4NkliWXFEai9Vc1lxTkRpNjZlUkhKTTZYcER5WVhCTksvbWF6Q2tBS1kwdGxIOE5yUFVTamEyZHFoOWhRCmZ3MEt3RkozU0VFdjJ4em1ZUW83WmFkblEwSG5KQ0o3RXVLb2NUUExDZzhVYW0vS0tRc01uR2ZmMEFNTDEra1kKNzRFRGZvOHZ3QnRGb2RnYllqMEJrYUI2emN0ZW9RbVZycTdsTkoyMjZUVDQrV0Z6ejErbU5VR2RTTFBGcVB1WQpPbzNqbGFCNTQwNHVhUk9ZUWNQcVZWaFVZMFhSTWdWZU1EUE9VSkUxaXBuTVZKbkFCNDlzQzNJOXlnVUFyN1dZClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1QvaVhvakJFVXc2dzIzSFExb3UKWTJZeUZjTm9iSkZXQ3RKNUhFRW5lUk96NnltZVQrWHl2RVg1K01heDRSOTJhSzBNR040K1EyOUtPcllucFgrYwo5WEIrWnBKTmJ4MTM1aWhwRmQzdjZFeXFGM3NmVnhNWUpicTJYMjFuZWcwbjVQR1BEYlpRdHhic0RPdmQwZ0R2ClB1YTFzNWVWRUdqWXM0RE9MNDZSRHJsVHpOT0FWcFY5RzRENUE4WmpEZ3RORGVwZWVKN1pzbnQwbG5xZ1dBeTgKemdSV1EvM3YzdW80bklOSU5BQnFhK0NKVTJUVURtQTB2ZzhLUEQ0UHNvWSt4eE9aSW1zenpySktEVnZReFowSwptVzFpMEQ3cGhML3pDYmorMGlRb2xrK2ZGeU1IMHlOMnBWd2o1T2JzMXloOUR6UHBmWEljZDlQNklEYXRzNE8wCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGRSQlFsQUxBUWRpSGQ2Wmt6bHIKUWl6TXlqWndYRWhSbi9DTU5mWWQ4dzgwSGRJZmdvTHp5Q1ljRThWazJxbWRwRFBRQkJIZXd1TXc3bFN4Z05MSgp0TGhHN1ZUMlJuY3pULzBjalVlRnhQMHBiMEQ2YW95Y0ZDMENmaGxKUXFFbXlzRUtVK0xHVTdGYW52TlZyQTdYCmhDZUVrNTJ3d2lnL3dmNXdHN0xYYWFJSWtWZHRZcGh2Y3ZYU2F3QldHRGcyY21yMDgzSDZ2YnV6dStqa0lVTlEKbDFGampxMGwrYmgxMWZHdHNJQVl5K2N5VWZXK3dUaVY0akpNVkZyaURTMkdYcTdpdGdoejBOT3IvVzczM3JObQoxUlY3WjVBMGtXSGpWcjBKeFZjMG5XM0ZhK2NsMWk1ejkzVWlqUUxQckNqUTJZZUUyOTNjYTdzb0JrUDVzRjRNCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1NVaGxsUlRZditMYzVYZ2NWaDkKcExHOHFFMHZvdXJmZFovaTJ2UWdTVEpnaW1CWUlsWVVmT1VvOXBIaWMyRXlqSmFyeEY3Ykp0Wll4Y3ZjZmxUMgpvWFJ0S2RlbnJrVlBEQ2p2aDd6Y3NRdXV1ZDd1bTVJUEZaK1loa0MrK3lFMDhtQ1RPWjEyMktlOUZHWGxuMlBYCkdnUjBCWUpOeENTb0czZDFiMGdpTVJYQzhEWWtEdC9TYmI0YzVzR2xwajRiMk44d3R1MHg5LzRtUG5KUnFpWlIKdFpYUUJTUHBicElWK2F5U3lGSmpCSUlXWnEyREdnVGJhSHRwckdMb3doWHNlYXdvZk13czU0eURrSFdCZXRiego1VU4vZGVGUWordFhkWXRCRGtOeXJ1cnd6OFZlVWF1THNuaFBpZVFrTy9ORW5ldisvbHdHWkU4V3pvT21IRnRlCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHc2L2JhSVBXazBkUis2T3lCTGQKS3paNDNTNDYrdzlVY3VtTVVJT2I5TjVESUNpTVdqQitTZG9ycVhCaXNScktaNlZMbVVKTDhxdlN3R0UwYmNOSwphV0paZnp3cnF2UlZKN0ZhcU1xeHZqNWdFZTAyNmcxS2dBb21KNFNDL3QzaDRKQkVhZWV3VUYzQnQ3UVdDaUllCnVZc3l1UHo1WFo2YkFxb0gvTXE4bGdhaEQ0L0g0ZmZkeTRFby9qN2VhY3BsNjFveXlQbGpkd1lqRE91MFBVMDYKVC9SQWhCR0FEMG9DdUxpbGZta0wxVWJHQkY5K1d4ZVdDYjJFa0hEbDhsTzh0WGpldWVOcDdGZ1E0Ni9oMFdsZwpCM1A3eDBkblg5RGhXNkl3MnhxaDM4enpDcnlZcVNKQldrZkdISnY1TDVrZUVzTm4yZjlmN0ZnRHV1TlcvME5jClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjZ2TkFBanZCQ1h2TUg1aWlLbUUKRkpnVDVselM3Uk8wL0FrMUg5YmFrT1hQNjZSSUVGOWdoUjQ3VFdCRzFORDljM1ZiSGUrcEpuZW5raWMyY25qZApJSC9NNDZnb3QzQXZFbVRXcC8rMFpWcmluWkY3dXVpaVJoQ2VGT1krcXFJWm1TUXZSSDByVjRnK05JWWs5MTYvClF6UEFWNWlGNDVmQjR0SGptcEQwTEZnK0huK1ZxQmZWS3dudEIrTjh2WkFjMFpqYjlDa014OTNNSzg5REZxQm8KMmc4UGFaSmJjbEp5aUJjeDU0dUhZMTVZZHdUenNqM0ZBMTV3VENDWFoyQ0NDdlRWd1psRVJwZTVTSXBPMUczaApnV3FQWGJpQmplWEYwSThCZXR4c1VOS1FSTzRRTVRWejFHU0pnUk1iQ2VjWkxJNEtoWDd3OFh1RlFYTEtMWjZZCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjUrY0VOQzBiNEdyZEIxQ3JoSkgKZnl1Y0FDV1RTdFJGa1kzTUdiTnJsREQxUTZleGVGYVpQeFVZZTQ0MnVGaDhsbTk4aHh5ZDFidkIzTGpQOGZiagptbDNJWTljQlpOeW9odHdiWWdHNkhIWVdyclBxcm1XeFZ5SEpMa3JiRFJWQWNBemIrTDlPSE44eW02QXpvd3VzCmEzK3pmajBRL09LeXQvZTlTZlduaGhDOVFxK2pRN21KbmplUW9Md0NTWWRGTWd2dVllYjd0OVZCaDNQQmpCd2gKa05jcUk4ckNLL0ZaNXkycWJwMG44eGdSa1JaM0ZUVitLUEp3QUVmNDVaT1J5L2tqRDlLajNQdGxrM3E5a1dlWApDZGhDUWtUWmdOT0d2TTE1anEvakNHeDd5Smd3WTc2aGZ3NWRqdkkvVUFXbzc1bFhETmlxeTJNT05QNkJqVGlSCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1dXNmg2aER3Sk1Lakcyb0Zqd2MKWlhNZStMbnZ3WEVpR1pRNnZ3R2xNaW1oQ0tSQjRPSERQa2doS1ZyazFHeGxQQ2phcWJ4RHcrb3pIcmgxaGJVdQprSjIzYVFtSGRzMUFXUUNJZW9wU0tkeHBLdnVLL0d2ektRZldiZEgwb0ppNnpVeTZMc2tMdys0R2dMVGc4UFd5Cm15KzZXWWNDVUFOWjcvN2FkNnAyT3VkS2pHMEFrcm5tdUNQRDFiMEdVempXQU1QUktxSVBPMkJXVzZHcVZsTDIKdzFEYlZKd3FLSEowRWpnb0QxSktLUVU2NUZPUGpXM1BjcktsaTIwcVVzcXptMU8zTUh3VEYvNlJVenZNSlpkeQpuSW1UL294MmU3R1EvTW1ya1RYaS9PZExKVWFRSWIxUk9vS2o1ZTJpeThaNDlDdDlSYTY5YW9jWEVIcFN6NjVkClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0JuWkJveUpJVmNQWHpSRjRQK1EKMURZbWJ2WmlVVnAyZ1ViREtYOUJ5T1dyNjd1ZWgrTzV1SjQrWEpEQ0lIMm1KRnZ6NUhXaS9maEpqSFUxUVhUdgpMZDBrd3p3OVVZZWNmVFFOY0svSUlLanIwajZBcGViN0orc2EzaXJIdVZSTlpseUVCREFpclJTNHlzck9nMzgvCmhjcGsyRGw4VDNNY0lEMHFlRXFRZVh0d3N2VGZmakp1UFhIOVcxeEdsTnRyNDg3VnlhUEI5NFI2ZUlzeVlKTUYKdlo2NzZYaStIT0luUVFoWk5kd2FWM1gwWGxBSE91MHNRMnFYU2lSY20vNlEzSHBybmxrYk92azY3enJsOHBYdwpLM1gxSVYrYUhGblFQYWZGdEQ3Y3ZuS2tScmNHMGFVTUppQ1IvT1pycjVZOUF6NThzMUtLSXk4SFkvK3pYa3ZrCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDY2blc1c2F3SHAzRUhIZUMxd3IKVE43VlJJbGN6M0FuTnZiZnhVT3YwUm0zZnF0VlZFZzFoYlJYTENCVFhFNUN0am9WQUpTWi82Yml3TUo1VUsrbgpnMFdPMmQ1cWNVQy9LWlRnNFNUcHJLRklESGV0cXc2U0lvdGZSbWpmOHFMbDhhQmh6aVR6a0FvTW0zWHpoWVlkClZVeXJrVjJiVmpBVUZHOEx3emhlK3llYk54dmhJWkxPVDZJeTlQdWwweHJnbHNkTXdIWEtZT0x2bEp3OFZNMHgKcis3M0NQbFo2YnRuTkNVSkhTU0k5b1lvRzE2YlhVb2Flcnk2RWpPTjhFSGpjczkzVUlNOXVXSEV0Qzh4bmV1bwpEMkdMeEpwdFNmZnBWN0htUldOSmEyR1ZBV1ltMkZSVGtmaHN2amVrT0tiSWg5eXZuL2U5NldwSWdqZXB2RDlZCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDIzWFdMSnFOY2dkaVFEMGFkOXkKWGVpY1lXNFlLZWh5RWh6Yy9sUGwzK2hsSk9XS1p5aG9kZFBpOERMWTFNdFhkaVVHanc5dTJHUnhSK2FBVHhPNQpwZTQ2NXM0SlMwS1YwWUNxZGNHTDFJSXJNSnZvL3FLUHRVSlZ1Q2Q1Mk1zK2RSTFBHRVM1VUtoZmpSQ2t0ZUg4CnkydDZ4eFBMTE5OS2FaYS9jMnpmaURpWDUyT1R6Z2VEZGNneWd3SVhOT1JqYlg0NEtZQzQrWkZRaktvUDhsYk4Kc1g5MjJNSlk5R1RwdmNtOWFva3BPSUVmZ2pHUDd3MTQ2MHRnRHZCcXVmMDc5TVRvblVWRkNyWmhGdFZkcDVXcgovV2ljTGREcVphaktwNnZNVUwwNXAzVzE4ZXRkWVpKZkczVHRPeDdTMUx5d2hGaUVHU3JjK2JYQmcvejdXc0o0Cm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2lIQ2djbXRVRFJVRGdTUWNrRmQKTFZpaXI0YzRicVh5Rk1NVWhucnozQWMrUW4xYXpHdGZaS0o0ZWFPL2I5SVRDZ0Q5anNJK3g3ZnlXanl0UzFPVgpEdTkrcDZYeTZwQ0x6QWIybU1mTEF0cGphSi8zN0hTeHc3SGd6L2xRZmRsdXBnNVhHRXZUdDMzbWNQMDdZcXArCkcvMEt6NWxXTjBxaXBoenhGNys0L2hKMFlhMkRrV1dLY0RKL3dCOU9GTnovbzVBSUR1aUVDTmdiTWxFd1J3S3cKdHZmai8yeUZsUlNYbjBkVEFUN0x4SzJ1cXdDRmprb2x6bGR0SHY4L01tNVduUkR5RjhkQXN3VmpBcEZydUlNVQorMXdCTTl2YTRCTE9sK1hFZGZSbUpqM1VvZzhVSE5KZHJiZzRnd2dLdFo0TlpYa3EwT3R1MzNLSUh6b0ZyZkVhCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGowUldRMzNEbi9UWHhHeVRIaGIKNi8vUk1pYUNzSW0zdFY5Sy93d3F5ZERSN3cyNUZiMURndTFtR3JQNlpTTHdDMmsyZm9LZFZMTUhPaGRWRHVXYQpicHhrY3NUeVhna2UvS2tqYVFmVDF1RlNPa0gzb01jbzE4blo5YzJOWmNvRkJ4MmM3RmNYNHl0bXgrVE5kRFlNCjdlbUtsUHJwU09iRUorWEhCTEZ3TzNHcVRLcTZZY25EakhBM04xVUNnQmZkNnhJVGYwaWxkOFJlc05xRk9VNGUKS1owMXhpRWFSdm1GeHE0dHh0U3RYeGhqZzhUdTl0OHRSQ0VFWFFyczFCbFlVT1RqU1VLd2o4c2NWT1VhMGlvMworalVybHZwVWxEOFo4VUIvUEdoYmMzeWZMUFoxU1lKN2d4UzY3K1lMNERUTGM2ZkQ0ODhzZXBrSm0wYmxGY1EvClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0o4OHl1d2wvYzJVaEdiRDFYb1kKWnJWSlpYaVVwN2I5QnRDZEhKTjg0TXE2MUtZd3hOUkRRTVJWd3oyUjF5eXlnblVzOGVnYXd2Q01mRmRKL0lnVgpKTllDRU5iRHdWVWNxK2ZaUis5c1pPUEdUTTR2c1JUVkV3MDhRVnJqZ1JMUjJLZmVORU9xOTY0Z1hLOGpPcE1QCk8yYnhRL2FUTWsrMjBtNXNEbGx6ekswL09VcmdVR2NJdFUyUUpnNVEvS080bDYrWEZtSUF0eXQweUVoQkVLTnYKNUgxcVp1Sld4WmFGL094YWFtQVZEOHc2MFYwT3E3TkpOSmFmRHpDMzdKS2FUUTIzbFY4bUxhNVByaXBBUXRWUApycFY4a0ZyNjNSVUY2bkl5SllKcnVZUklRdWdBSXpFTk0xLzZFVE9kUXJZVXlxeXV1bU50TDEyb3o3dlU0OUIvCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWk4OHlXd0RRUm5wRTRmMzRnN0kKeG5xYkNDWDhKVU44ZVpCVEhlcmZhcDVtdkhPSzB6N1RzZ0xyVDE4dGNwN2Q2UXRycnNiVHFEUmI2NlFOWUNQUgoxZXJqampvWjVseWZRTG1PWFFXbDh3KzBPZW9NWUpwWEJzclhLd1c4bzc5L015WDNKaTR2OHdQbWM1Z0hCMnlpCnhlTURSYm5xbGcyc1JISWtwbGs5NzR5dERGWHVCSzFpdHZua2dKak1WL1l3U3NnbFNKSWswOVdOV1U3NnI5U0cKdDJ0aGRpL1VlZk9ZU1lJbXo5ZExSODU4d2E5K2hVSldmb1RoQXNFdlB4NkdGeTBvMzl4Sm4xN3hxWG10ZmpqdgpjUUYrUkEwYkN6REVMbUdMMU5PelZIZmEvMERwdGpMWUY0ZW1Da09MdkZUV1E0QjBjSzdOR3NTVWpxMVc4VmFFCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGtpbkRUUDhsY1JYVVB4YlladEsKcDU5dzk2Mm9MVGRoTm41aCtPd3FNM3lpMGZIdytnNnk4NHpTZG9aOCtQbG5sbEhISldvNjJicHpPMW1vMGhtKwpwemlGNkUrY2QwVHlJNkg4YkFrRnlhcDYwTTVmRk0zdnVzYTNnTWNmd0hmUGZEZDRJRzN4ekJHZllSMy9FVGxpCjkwY2paVHJjRHFFaW50SlQzUEk3VEE1UUVtbWlYVjhXTW1tMXVIOU5EUG9CaDZCN1A1Zmx5bDZFMnNmNlEwL1cKQ0ZFKytDSWMrS0NnbTZQbUJ0Z292SjVYejFsdkFxTE91bnlrek9WN3p6WTlwaDl3a0xBcDgzWFloaFIvWVpwawozeXpYOTBvSTlMZjB6cmEveTVuWEFCZFF6Ti8yS0Y1VFlDQkg0TkwzSGVuRk9tS3JNYXEzVUczY3NMNmpZMVVlCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBem1mcmplUW56UDg2akloTGcxbEgKMFU0OGJ6eTlJYnFKaVU3NmI3TGsrdk1scjZXeXgwOHJxZzVNakJXWUJXeDVENmc3T3ZtZklvS3Rselg0OWFPbgpkQkt1d21zYTZ0U2FVYVcvU3ROYjJzVEo4YXdkakkvWlhURFR5TFN3MmhoVzZ3WGIyRU9yVmZvTVhJZjhmUzBWCnMwcklYWitIa0FYQ01YbFN0eTdFditEUWNaU3FLMWJFWHYrcEtMREYwQzJndnA4SDJ2UXFLeENpL2lTVTRrWEYKRVAvbVNrTUR2cmdGeFVabThQcXlOYkVoZm9tZkhsWXhuNUh0ckl0M0VwV2hEcGEzeU9URDlkMnZYeTdmNXpuLwpobUZpRXNaMnNtM3hVVUc5ZlNvU2RCOEh4NDhTZmQyL3p6ODhBeTdVeGRQVmtFTFR0NEZjNEwwTzg4eHgrNXZ5Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczRUblpTd0lubVYyWEcyN2xVV28KdEQyQXIwbjZCRDlOcERwVngySHl2b25aTHo3SnV0MzFzUzZJOVgzdDBGc3dQc1VKVE9qb1QxOHd6bitIYmw5WAozNlN5MlNUOTlKb2s3Q1lWbWVKT1NockZDelNmbStJM1lrOU83c2ROT1JzN3huL3ExVG1lZUJZVVltMWZpWkpYCkdUYkpoa1Fpci83QS8zL080cDRhbHVyc0g1c1pPWms0RnZPZFlxRVBMNTE0L0dHSmwxOTBiMjlqQVZQcC9zeTAKM0lZblBER01qMVBYM253d1ZVSzBCMlJMN21tRFRucVdpK2ZITDdNWS9OZUx3eGthSmRIaHh0SUQ2dkpWRWlLbAp3eFNYUXV2MUhteE5qRE5oMi8vZjhlck9McFRqQmhMakxnMENrL1F4YjZUYXB4R0VtZUx5Vk43YWFaU1JGNGVWCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3JjbUk0T0lQYS9TVWVacmdWbG8KcVFUeEhJT3pJQmJlUWZWQVFDSDNaOU5zY1VZbGhWL2EwaWdiTXk0R1VqNDJpeW9zQlhrZDJsRG95T29qTWNnSwprV0VNNVEyejg4RURQU21Cdmx2S2UwOVZIQ0RZMnFHcE9zcWVOZ1l3V3R4WmQxeHpvY04zZGRBVzhPbThLK2dRClUvYXcwL2FxUWVFWkVwdm9BZnhPS0xGaWF6L3NRd1UxUXlsS1lWZmttSkRpWWdBZDI1MUpSbVc4aFJkL2wySVEKMXE1SDJLdFlrb3RTVGdYSjh3cU1SYi90enB6NW4vN0trWENXekh1RHN2UGdzbjFFMXl6WkZNb1hTNjUyMnBtUgpHQnozVk9uRDd0OTg2eTN2TFdDNk4zWVZuSkJ3Mk1FRFNDNWUxd1FzZ1VWUDNVb3hha2xhbWVDeDBESGJWaFlkCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckkrMThxVXU2UXlhOFVqVS8zT0YKTmNHZ040Wno0S3U3Y1M3S2JZY2hObEhYWjF1RlozQjBQNVRkK0tGZVhxR0JNSHhCazl1aUowekYzZnFVMWUxdwpMUnFwRk8yaGFDekp0eXFLQVVaZkRWUVh4a29oaDhxcFAySUkyODlQVWt3WDBzbFdhdGU3dDgza3ZIbytkdUlBCkMzSW9EYVN0aE1EbDY0Um1CaG9oWWcxQnRkOFB3NTNZaWVtSzlJaXlHb0ZPcE9STElkaXUrT21Wd3FDUDJ1RS8Kblp3NHBMK0JHZ25PUmFCam1MUDlwWmxUVlRobXFWN2ZvU1ZKbTJqRGVSQnBKQXNjU2lOd3pqSjZydG1hWVpCbgpxbzROVU85c2oxdmU2dmVINWV3eVR1LzlsOUpzK1ZQOHdscGtqTFpvWTl6d1pOYmFodUxpQ2VHVy9sZlpiMUZpCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUNMMjhRak8yQjlSdURNTWhSWDIKTlFPZzk5QkpCR1ZmOWRJT2xOYWt0cjRxWEQxUlpnSGZPK2MxSFVrZTlsVk9OYWlQcnlSeVFVZDY0dGJkM043LwpIQjd3WE92ZkphSkUxOXZNQVh4WEp5VTZPQ3RXd1FJLzlvZ1FDS1M3V1VTaHJvaitIbkhUT1AwTkxodCtxVjZxCm8raFdJcGxFOEhXdDlqSHkvd00vWWs1bUU4amdlVm5YLzZaMmM2VHlIWExkU2NudDBiZm5LamxLbHZ4RHllNDAKZVFmenlnMHVMMU0rWUU5bEdqV1o1ZUVFNjNrQTBnRU1JQWRHSHFkUnBvaXpFYTRWYUtrL1J5dFByUkVhelMyOAplZ0FQZXd6NVM5M0xYYXB1akJzQVRUSGk2amFqZFZrSUdma21BQVVmaG1XWkp0aGkyV1Jka0oxR3dmTUFJYSs5CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2NvSzFFUWh2ZDZvVmM5dG1xc3AKejU0S21BS0o5bS90SXpFQk5FNUltekhSa2R2QWp4UmF5YklDNHR4cXhXTjV5OTl4SnVLa0N3S2RqY0RsVFhUNwpVOEdmN1JCeUpOMUhUOXJ0RER5L2NLbmpLaEVQRGF4L1htZlFIaEtUSW4xdWIwbG1xamZ1NjEvVUovdXhyK0xwCk5BZWNLL0xFck1mV2JWWjVtOVQzdE1nb3hBaTQxc0RjNzFaU0ZKRjR6QmRKUTZYUDlrOVZKaUtTZXZkMGt5OXIKTTBNdHRjZGNmZzRBL3dwQXR6V0pVS2dyRDJEcW8zTnhMaWN3UCs1VEJOZUpQOU04Sno3eGtGMS9HdnU5azVlOQpaQUtObmlLeFE3YkdwRVVUdHpiTllxbEw3TmN6Q2hEaVNZOW0yeW52REFUcUROTlhqYnlBRUZ0dFpBTEdsSzZ5CmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVZXcUdFR3RYZzhJaDR2aWg0T1MKVkFvTlJNNnQxSEJHb1J2enRZcGdlWlNha1lFMmJ1c0lXdk5NMldnL0FoSnpaTDFpeUJieTFoYVRQQUErWVZTMwpSaFlqOVZpUFZ2cTBwMUlPaWZHQkFnRzhxRktlZUFJRkxtM3FwM3JTRnNINHpHVEFvMExWcVovcnlidTB5S29ICjJZdUxlMng4QzNMeVRBKzBSdFNScmhJNSs3U0VzNCtnMXRFRjRnVUhSc25rNG9FV2poTTEzL2xhOGZzV1p2TmYKV1RoclkrSGMzUHZVQ2NRZ2ZTT1Z5S3lsWmtVTDNiOTZPWS9uQjhMRjhNSTZjbnNib0RCbTFzemhHU0ZHbWkySQpCQjJQR1hrYWZLaXJCa25HRjVwT0tuUnc0TzN0VnE3T1RVb1Vvdk1CZ2FSejRkTitnRDU2VUQ2cEUrWmExMndwClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGRwZjdJRVUzTjBxYmNnQjNLVTcKazIxN3FFaVlZQnJHUUEvZXdkODQzbVJyMERYR1NvMzZSY3UxV0F4LzZITTQ0ei9GY1FCT1Q2QTh6c2xIMkZnZgpwbW9WaDhpNEZvYmxXdlliQi9JVURocmkwNkdlVC9iQXlPK0RDaWRzT3c2RmlvaXhJcEhvaTVQU3FSazAyTUc4CmtvaE10RFZMMG9FVHVhb1V6MkY1WWtpaU1VMkxnRFduZGVqdzFlV2Z5cFhYMmJpWHhvZG03a1JFYzJkb21KajAKeklDdUVOVDk2RHk0YUFjdUh5S1pGVGR3OVpoMHY1ZnVNakkwV09heEhzMXZLYVBMWEFrVjNZOTNoRTlTcGs4YQo2aG1yMFFZaHFGUHROTTB0NkFQN09vSWQyd3lmWmVpZjIrUlZjbDNKWUkvcHBmQzZGWlZUTnhCY0picUFlS1l3CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeURjNHFERFNoZ1hkRFk4VFJQei8KVENuYmM1Y0pORW4zdDdwK2NqSmxhMTl5M1FKUzdFUG5VZjR3Z0oxeUdqMTUrR1BYcDR4ZjhXSVAxNHFtQ0h1SgplL0hDTG5tWWRoQjVMakkyNlhqSE13U0JGOFhoV0VHdlNpYlBjTk5DanVXT1EvdGpKVzBTMHkycG54NERXNDJECmJpcE1VMVZFQ2syelllamloc2I3RHJNd29Ld3c0RG5lM05Yb1lBZkJNWlBHTktjbjRnS3FudVNHRnA2MCtEMlkKUmdPcnlJeENIa0o4L004QWMzM2h6ZFRqbEh0ZWxvZ01aWGNSK29YZmxSWHFOZEp6MjRBT0xTQnVwdFppa1RQRQp4T3Y1M3Z0OWNVWlhKZm1RUEVSczZEblZJT2JqL1JLcHFwKzRzOWpYZ01rSkV2NmlZdVRFTDd6NFFleFA2c2JTCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmVpVnc2bGJiRGZaaTBxU3dwOWEKcXg1MjR2YTNrcUJYL0dvMjJvbWpvKzRKbWFYeUJ5dzlTZHI5MC9zQWxFT0p0K2tFaXYwbEJRTG1xRXA3SWliQQp1cDFqZDZ6UUlXanNmV3JoYkRUaU9sSjhmaFhDUTVja3NGY2F2MWFCU1ZkSktheUNGcEY4MXczd1hJUm9IR0szCjJQQnN6a3dpUG9ibXFRb0VmYWIvL0o2eVVSc3cxcXdoZDhjY0R2VzhFcDNrc3FkcEpQMlJ3WXBPTXRUazliNDgKZW1HSGVsMTZwYmlwak85TjJQMWpobUg1aGhzaVEzZlhjR1JxUTlucXRXWnlVa1lvOEdIUXhhKzBEV0U3ZERiQQp0alh1dDlEdzVONnNndDJEQnZvSmV3WDBTc0VJOHBzTHJRMHNnc0RXZlNacU5mbUZKcnMwMkhHOGR3M3pqT0k2ClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3F4K200bHJtM1JySFBScGpiblMKOGI1ZnN3WkZYRVgrYXFXUWVNSkZNU2Z5cHVNNDdzOXdhbFNVTXhuY1JZYk5MNzZNc1A1T1pBaHBJbGlzUFQrUAo4UFZSNFdFN21zamRaaEx6TEJ2Y08ybU1XSlJneXROWkNwRVNhMU5OdWFqVktLOUdCYTBYZFdZWmhLZnRCZ0NPCjlnQTVOdXRlMWxJSUxIZXpERjMxVEVPZ1l5TzY4WjMvU2FGa1dETHFReTR4eC90cDJVK3ZZaS9tS29BNktSVkQKeFRKOGNNbmNlbXZiOE05MjJxYjQwbCs1WlF1VGtScitkNWs5aDVSV2lmRzBxMmVEYUs0Nm80RUMvYzdjcStFbwp2eUZkQVJ4Mk4yQjRvbTEwVmc0dUNNWDBlSHkxY3BrdkRRK1NqNnZycVhHSUtyNitLZ1paaEFhQXViR2t2amovClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjBxYVNsRUxGMThCUzVLNHR4VU0KZTlrS1pDcVE5UlczeTJHUmEyRDlSbFRXejJCS3ExVDZmTWtBZGU2NkRBVnA2bXdXaWduSnFzTWNtenp4cVk2MgpTTEtsMm02dEVveGYzaEpoeVVZSU5jV0loVlREeE1oN0xhQ3hKTHVLOWFSd3F6WWY0WjdoYTgzck02NHhwOFpRCjRyM01KKzVHeDNkMjlqQVBhN3FmMzF5dFhjUkZnSG9JQUJNZGU0TXZSckNkb3FKRjdac2UybXJlMkFhMlQrcVgKUGZTM3YrRWNjMUl0aU1jWlNTckxZcGZ3c2N3VzN6Ty9pZEtxQlp0ZXEzZlhHZnZIKzRRbVpxTzVrSlc1ZDdVaQpRNmc1ZDZRU2FBR2VqV2Vxb2JOcXM0QVF2Rk5ralNtcm1pY3RNTmFHMDlGVlAzOEVPTFJMWG1CNnp1K1BsYm5ZCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUx0cHJuQS9rZG1QNVM1QlZPN08KMXVaUXhxRjlnNFZjQkRiMUNSZ29uYTB1MXo0WUQ2cUdzWXMzcDlnMUZXc2xudzJqNHRnYUNNNjRRQ0poM3d6YwpjNUNZU0MraDNhMEMzZjgrS1JKTE8wUitrTUpLOUl3YjFSV21FN1hLVWgyclREa1Ftb1VMbzkvbkw2SGgza2tzCk1BbmZkajI0WDZ3bWtBZ2p2ZFNzWjg4UDB5Q0NrUUlNckMweG9jdnF6NGpxY3ZzSVVCUnhGc2t6L2UwV0IwREQKWEtJeDVHdXF2TXhuR0ZDNVJrNjc0RzBhRHdtNnl2U2FMeWNQS050NVlxUWcrV0RBdXZVTWNYU0NYbGdycWpoKwpNRGtLbWNjbThyTWxSQ3pNVWtIMk9UWTBlRzFVcG1DOGd1d1drV1BTSDdnOHB5RzBIWk1LdFIzNXZya1REVDV3Cld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXhsOVhLRXJjUGRsVHQvM0FzSm0KYWZjODB6Qk56NTRXeVM2aE1ZSU14ZVFwU3ZKeWZtQTA2Nm5nQmlEMEZETlE5NkpIa3czWXBCRXBUUmNnQlBNMwpGYnh1Z1IvZE1tL2xqbmhGbmFuckkrQVduYndvVFRnWE9mOWNjUlJuTllVT295ZnJjcHV0VWFUemRoZUpod0ptCmxqbjNNeHlpdms5REpBNlhLa3RwbHZCcTlvVXdwTWxDRE4wU243MWVRYm1ESGRXWE9WdGhySk5ZMDEzKzFiK0oKQTAwOWtYZWN1VitTdXBNbzFHUUs0M2xtTlk2NUdwb1RxTWhBYmF0TEEyTWtiWHQrNDFEdzh2dmdRNzJJaTFQdwpDU2JMSHlwLzVabUxBZTExOUg1UnRQdWJ6amNhaG9UNWxVMkNpUW16ek9iUEJaL1I2RkNJWm1lSDBCSEFFT3JoCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK3JBYTdwNXVqRlFzQlFiM1c3bW4KMFptbTFqSlRHZ0lpS1pKVmd1dWxxdnJrbVViWCsyVFBVSFg3aGU1ck1WMGxwN3RRaTZ6dWFLRDhFSFpuREJ2TwpVdUgySEJIVCt0cnBCQWR6T0ZQUy9BTUhjRkdOcHk4cnFEQ0JtdDB6TzVyazg5RXJuZnQyOTQyMU1uNitWVEVMCmNESjNYenQ0UE1QNjRjL2RVTkZncWo2ZmJYYzFXYU45VXJHek1MR0x3S2ZnNHlMNHhFRzFkTUY4NlNBbmhCcUoKNGhvSDFFYlVkeHNxcXFLeEMwMnlmeGVHVm0rUTMwajN0VFd6ODYveUJqNGhGQUFCRFZZZnpud2I1TUpmck1GRwo0b0xWTWVibHhYMHBBL2dCSlN4c2dncVV2dTBBbStQR0VpSjJEcDhmNEd4RHFCbzRST05sVHdHQUo2MzRsOXEwCjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkhMMW5WZDlWV1JQWUJNOHdvcTYKTVhxVFdITlUzZHFnNjhrdUZkU0Z3UEltbzBuODMyeWU1YXg5TEMxcEhZV1JzeFRKdnRoRDlmR3Ewc0RidWhCRQpMM0lpZkpSMG0zck9UcmNOUkUvRlExeEdtM0NzbVZIWkpqKytLcHZEQWpyUjZvbFpMOXY0UVF1NlBVcWN3dnBCCk1rNytVOVVPT2VxdEU1TW5pQzNpczdUTmVRNnVqR29PU241YUQ5a0hwTytrU0hobmxGWmUzbUwwUFFrY1l5bXAKdUZmTWU4Y1JFWUpTUTI5M0lkQytadll2ellOZkdUdGlJbnpVaHViSExJNWcxMGlyNDJJazVsUmludEhGZ0wyVApnTVlYMmM0SEQxeGtTckJHTHdiQ2t4dmFyeUJTRHA4UmR3V1JrcTRvd0xsbXI3MnNKaTRBZ0plaG4wOE5kRmpUCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGllSFQ5eUdOYU9iN0xRbW1QZWwKQzkwS0M5MEJzRy9ZeGppT0diVTV2UVlGNWlTVDFVYmdmY2E3VUU5aEVEcVFZU0wwbFZnTE1LOTUzQ3psSkhBbApiSEgrN0g2VGVLb1ovd3lOMlkyb2cwcWVUL2lPVEJoMVlJUmtHYTFGWk1IcmNVTndJRnBQSTliVkd5U1VXOHhOCm9SelVCb0FLZmNnZ0RLSXo3djI0YlpZTzhXZjhIdTh0UFIwVXNQRldMNmhNSXUwRE1tSE5SbHFsaFUzckl5bmIKZUVVU3c1R2xNZEpUS25EZmtGMFlFVzVIVFFrTHhGdUxobzJBSDNPY05EeWw4WjhFdzNIRzJSeGNlcktGemE3UwovMi9pLzV3MnFsaHY3dHBwN0ZabzVIbGJqVmtCMVdTU25kRlJrVFF2cVU2N3RqbEVzS2pRMGFCdVdlUlFWYnNQCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnV5d21lM1prKzFpeDlXTmlTRWQKNnB5cmoxdjRycm9UZGZwdVliN1RvV1ZNYmtyWVZ6SDlHbGd3bVdrWDkyNGZQZzl3elNLMmpoa2hFb0R3Tks3cwpLdWEvWFBhVGRTU3RZdERSdnd6M2xmcmRxL2NBSmFrSDVIYUNXTUdMQWdrRTFNSkZqS3h1bHBiUkhwTEtoeXVjCkVoUTVHWW5hOG90TGhrVnhkR0hLaDVaM0hEZ0tnRU5yTUMzV1c2S1phRk5rbitXbUVGSWc4UDA4d2xBdHprQloKMVdUUmVvc0wxSFcyRS9sa2tTMzBMd1B5dFVocWpGMFB6clJNdmN0WjVLdVV4K1hIODNaNUxEUTBjbTA4aUpteQpybWtYU2lsOE9PYm9LNkV3OVFjQ1REcDl4SE5xVExoK3hMSGpqNWpNNFhxdnhBa2JCRXpRdXExcitpdHhzRVdaCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHdJdkxTdzBWdEF4aGlMK0l3bFEKcUMwcXRZZ0MwbFJWT3dGblZJVnRtQVlvc1d1OFJXL29XdzFyZk85NUU3SkxTQXd0b08wNjFNVnJxbDU4b0dSRQp6cHdFZ0ZJT0dhVzFKNGJUVXVZSUFIYTIyTnlzZHRlZWQvRGtRWkFGZ2xmVmZSTWxrenFOYmh2VjVKSDQrcTJsCjUxNkhEd3doeDNVYmJYbU9YWlArYk9YY2pFTG8waWw4bW8xdE93dTdhRklCazloU3hXYUoxaG9nYVBDQWZRRG4KeS9JQXVjMG1RUzU0WkRaYzZZblJCT3NtbjhxZWZYSlRRaStxVkNiMXU1a1hzbWswZGU1UjU2WGk4UjQrU2RtUAo3azQ5RHRmbjhwSzhQb1Z0NEhlZGh3bEZHR0JHa0Nrd3p2c29rbHU2RzVZNTVWZHEzUXl0NGFnWVU2NDMzMFZNCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDk1R1dSVTVTSTJuTFVZT1JhdzEKb3YwdjRDZmVjSUViQ0hab3BSemxDd0NqcCs1dmU2Mk1HTU1qc2d0cXFONC8xTVROb0MvMm5GdFlualNQa25NdAp0bTArZXVGdVVnU3FPTjdPS0l2NzZubUhXbyt3MFMyZzBjdWRMcTJwdWhjNHhkSlprTnlDTU1CQ2hnUDRvYWpTCmllaklvZUNkeGVRKzg4djlDaS82ck9XS3NVeGJPekNBaUtROHJxWVNzeUUrbDhyOHk3c3g2WFVkREdYdjlINDUKUkxYQS8vQVB4N2pJRnAwUDhhNWdtblRYenEvbGdNRERzK1dMTnBLUzJMWXJLUUYxWVRHSm1RT3pvWnI4VExpMgpVQTFvYksveGVKOU1aSzRVcnZwdW1lQjY1MURwSFdJdy9tUFdZejByVzVYL211NkFkMkdKbHdNdnpNZnpNUnlhCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFY0Uzc5OE1LRGV4M0hwM2crUnoKOFpEa1JLU2w5Zkx6a1JURUJhWnFRd3VURlh2Y1ZvZVc4cXhTdW1mUXYvS1RoTGcybkpKd3NHUU1pWXFad090cApoZjdWRllvUDFNZ1FlMUZZbElFZFRyamsrcW9CS0MvVHZWWlVtNER3bk51QmZSbmwvSzdtbG1pOFFBUlRoSU9FCnBYN2V0b2ZZZTBBT1dZS3VveitsWi9aOHBiUjE0RG55SWluM2hFMkRaajdYeTVBQkNrMWV5ZE5FV0U0WVNRSzcKZDVETDk2TmhHTTQvQW9jN3YrdHBGUDhydVN2ajBYVkxPREVWWUFlcnJwYWVuRVI2aTNpQ1p3WTJlOHFQdWMwOApBU212US9wckpHeFIzVXN2L1lrQWxMU2R4WGE4RGQyeG1tWTZBVzRVSUFWQ3VtdEFrRW9jQzJhdGI3ZGtxREc2ClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnV3YjQxKzBOZ2FSWndHUi90NkgKM0ZUUVIvR2RmUjNyZFI2VlhHNGZUU0J4Q1ByVDIxcm1VVytkWmtmRXRCcFJtbmRXQVNoL3ZzdVNSOW81NWl2agpWZGQyUzRGdTYvbCtYdmUxNkswTXY4ekZLMkVkWFMzOFk2Nm9YTGJ4dExvM2MrUDkwbWdkdktrWEJaTU0zNjFOCmdHQlFYR0lRQ2RTYy9KQ2YvaTBkMUh6MWM0Mk5Jcy9CQ0tkdXpWUTFaQlp1N050M3VFN2NMZWdCM0k2bUgzLzgKQ1d2RU9LdFZuVEVxUG5PRTZrZ3JJWnA2SUE2NS9qNWFhaytmUVRkSkk1UDhJN2xrUCtzMVZVWVh1dUpJekx4bwp2endUeHA1SDVQeUM3V1RvcVFYWFVNOVVnZ3hiSU56NnArYnpnd3Q2cTRSenBGNHRIVzJjUVE3K2ZVM0E4YkZBCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUxhUGdSVEZzcXdxMzVJSFpabE4KdC9yWEJmM1c4VXVUM3JiNnBodVIxdE4weHJuUDIyMjRzbkJhMWlUZkFsVCtNME1Gd3cwdzlBUm9Bc1NlcXFaZQpnb2ptdDEzd0puM3JXR1Vxbng3OHJTeE4wOExFRmczZUxFK0pVbUZaR0R6Y3kvTjhKdmpxZWttZVBPcWdCTzkrClBKRzA4dmdZdUN6NXU2bWkrOFhhcFczQTlYMzRxeUxzQyt1bzdoWU53QWw1R3ZLWFJEeDJ1VGp4N084SURNS1EKZWVHNDNBY1FtWmR6aVlPUENVVEEySWpTL2pTbHdkbDd2ZHFkVUxJa0RyZWxteERKQzV0MktZQ25CQmhvVXlkagpSdUVzU0RzY2o4YkJubWRkckFUNGhVdUdlM1JueTlNR0o2bmYzeHVlOTZ0VWxkeWh6bk9vQzNqTU5yWHl0V05PCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2VHL05UVm0wd1JaQ3dySVhIQ3gKRjRBazVzZTZuWXZqRldDTFlMbzRESVhPRit6aTYzem1tc2xsbXRFc3ZtelNYOWd4QlY4V1VOZWd6U3Rka21tQgpYMnRURVQwZW1HSGtUVW9kd1lmS053bTE1UFZuYTVwZTZBWmE4ZUgrRVU4YTJ2ZlR3a1ZRY0k2R3lIRnY0dVBqClFqUGYyUkZjY25OVXkxdUdWenlsejErZGZyTkZ1bXFnR3NFbC9QUzFGMkZHYTY5dVphSVJGWVFBN3NTeHF5QSsKLytKSnhBT1VFdXFTcUh0UmFoYXpxN2FRNHkwcEZhTlNkOFU3TERBbnk5M1k1YTJDRFJYNVMxaFM0MmpZWUpEQwo2UGd6eE5FTW10SXhyTjR2K1FERVVKbVJpRTdyMHk1bjFxZGxTNWdVU2xjdGFzM3RUU2JLTW03eHRFWmpabXdxCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEVwK1NGeHlMRzJERncyaGo1K0YKOFREUElJU01TZUd6VUxLTlg2b0tMcGtybzR1czdJaFIrWElrRmdFRGcrMngwSjR6b1EwYSsxb25NNkhuRXdWaApjM3R1M2tkMkVQNElaeHkrc1NRWmRxUHYzbWhHa1cvcXpVM2toaXVWYVhlN0ZyS1lLZTkxc2ZqTzBuYU9zamZsCk1MbzV5aHpYdmJ5UGc4S3BCbXhsT0tnODQ1UlhqWkNHV0VScGRJaGJKKzRQRjhrd3A5ZlVuTWUra0IyM25zcUMKS29aUnZsQnpMVHRWYW1Ed2JKQ3ZSbmppbFBjMmkrZXRmcE5menJZdzFNajNUV29mVWR4N252c3hKaERKV2RwdwpWMUxyMFRtTWlHU291anQ4cUt3ZlE4K2NQOVBJOG9zVHMvL2UyNGhCdnhnelhVOU1XcUw4OHNpYkVGdGkvc3NWCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGFUZjFjU2VtM3MzTGhUMkdRMFMKcWNqckRpUUhpQU1NTGpTdzY5Wk1RNXU4aFdRV2hoRjJIb1A2eWZtMkNxOFRjK3J1ZmNla1NFaEhZcGpXVGhsSwp2ZmlxcjZ1Y2dySDhaNm1DblY4YU8xSnVXUDYrRk9FTHdabE1uQStWUDU3U3lUQlVyeG5SRmxYYjNEd1k0VzNoClpqMUw5UXAxMHh1alhXUGhjR0JwN3Q5QllDdCtHZjBHZVc2Zmp2aVdMYjlPM1NOWTZVQWZXeEhwcWJyOU1GZWQKcGlIdm5lMk1mczgzbEo3K2RHcG85VjVtUzNyZTc1U2ZFYVhtcnQwZjlmdE9hamkyU3pZK1RHUm5kdnBwTVhJMQpldzZla0tPcmp0akUvTWdyNXZoK0lpUTlzWTJEMngvQVBCbXg1R0s2NGdJSytmRlNJcFBUVUVacWRDUS9kRHhGCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkJ3bXVYNkpOeGd3czdEbUpWRHoKbmhleHd1cDY5VUNLQ0I1c3hNQ09VZGdIMEJ6TEtKbzdhYk5mZlpVZXhuT1kycElTM2NCZjRxVzVBZXNUaFNQUAp3SU5kZGtLRkt1MG5JQUFHSUVqNllsbEFxN0RTU085NkozVldTZ0Rnc1gvZFlWNFVHcjNXcU1Fbkp5UlhZZ25NCkFaV0ZwNUZFTFEvUzlHNkpjYTBTZk9FaTIwdDBEVWVsczZWVXVGdlFucVhjZDNlUnBDZHJ6c0Q2ZlNYTjFHejgKajkvc2g5NTFyV2ZjeTVqeFI3cjlBY3R0MlVxclRNcFZDTjNXMldDQkJsaXRzUG84RXJvUHZzMFFUdmF5MXp4RwpqakEwL3dIK0ZQVjJQWGZYMFRaeUFMMGRhUWFIcFQvNFhXOXE3SklOcUZoM0Y4NURoOGFXTjUxMzRobGJUM2JlCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmZYZ3RGUjFrMy9abjhxY3lxU2oKcEJkOXB1ajRkaUNmK3RTc0JtdnpuQmd1d0Q4S0ZQK1d5cDhqd0JmU3loSnNySktTQjlHRkpCbitqQk5lODBFcQpZWmxmWDNRVGY3OGdTd1N2bDQzZnpoQnUvdlgzb3ZreGFVSmYvK1dLc08vZEtyNC9LbDN3TndTOTVZdkk3ZUhoCmsrSGJUaTVrUVJzeFJkTkIvblZ1VVErY1pPNzhRK05GNHo1R3NUU09GTFN5MXI0aUJxVW1QV0FacmxvVmthbmsKdmh1cGhPK0ZLaWpXS2xnaGE1QUl4dXFBV01EdWZDNjh2dkRlNzd0WGs4Q05oZFZWUnVUWHE0TnNxVnNNb1paeQpwTGFrNDM1bXZJVWJvcGprKytldmhRbW03VUpVSUlSdE1vYkE0QWRObSthL0dQTHU2VlBEQjBTQzRSRTExM3ArCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenh0U2cvMnRua0lMY1piZWVoZE4KRWF4K2Q1VERTSE9Sa2VmRE1hZk5lU3hlL0d5SFQ2amUvUTdnM2ZvcXR3bWE5TmIveTY1eHNjOTVZNEp2RTYvbApybmJ2U2pNcFExVStBNmtKSkpDeVVLeXdFTCtmb1pPUEV4V0h2RWdhK0JBTjlQcHdzSjR4SGcwVWhWVHQ1MGN4ClE2SlVUTTl4NTlyTm1NRnc1cHdIY1p6MVY0cEd5RDhhMHFVK0prWUFYU0JuSktEVUpYNVBmRG00cldaUExRZzkKVHE5NUxwUFRVOUk5dyt0a2lwVkRnUkJ1MmtBdlo2aDIzN1J1aCt1ZGNNTERRUVBBYy9vL3VWWFkyY3ZCd2swMgp6bGs2RGY5M210ZjEzak9DWTY5a0FyVyt6T001YS83VnU0b0tpZE14MWsrMEFQY08ydWpWZzl4TWZyd0p1UmdKClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmVUekJQZW5mekFGZmNjblhFNkcKeXp2WC9YV2UxQUU5aGRBSkJ3YThpRG5jSXFmQkVKS0VqNHpQNXVkUjJTTWNDNk1lYXhhWnYzUHlVRTBvck9aRgpDbWtxdXNsaTBOMjdaMDlzTUF4M25LMFlDVC9YSlRwUXVWZ3k1eEJYSzc4NzJsNkpaL2ZLeHhweER1TlRSa1BlCjJzWVZNMDJpbllKN1RvMExBNHJqNGd4SFBnODZYV2ZBb211YXkvT0Z0THhhdStKS0FQdWpmR2RqS1FKMk9jQmwKNy9ZZEpTbUR3M2R5YjNReFFpK2ErbGFYc3RFd2dtY1FzVmtjdklnWDIrODV5SzBsNEZWYU1ZVW45WndlSkxRYwpDN0g5RGl4RU1qZWlYK3cycytIdzlDZkJzQmpJL2lMZkxJZkVBZmRRZnNhd3Fwbi9IenlnV3FaQVRJdzYvV3hWCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemh1ODJiSDh3SkswejBKREszNHgKQ0JabmdjQkJUTlV2MzF6WVRzb1hOUU9tV0pncDNWRmI0UTFGbnNLMExqMzcvd1FtRnNzemo5Y2xJaVJjckRDQwpZMVN4a1NTN0pFZTFxSzYycU5wVURjcVh3K2o1OXB1V2ZJNW85cUhWWlY4TWgwTnlvQnFvNVliVXk2SS9mOE1wCkFPQnlQZm1xakdyTUhYaSt0QnVzMVV5ZlZPNXdQZ1diell6dHVEeE8vbmpDR2dZcmRmc01XcElqNS9SaHlaTzYKVW16TFpySENLNW5vWTlEd3ZFSUl5VWp3WXkzTi9rMExYRjN1d1EyTytaRGhlRGNvVjVVWUNYSXIvTzlmL2FlQwpLaEZLeDZiM2xaMEZYV2dFVHU3NW13d0o5L2FlcStJeGZ0SHM3dlhNR1krcmdONVJ4c1l2bFFlQzBOaHdGcFRHCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWJJQTZKRTNUVG5nNld0YjBaTEUKVXpqZ2lxcFNxSURneERWaVdraWtjTEFDdmZyNW5CNHY1WmVDNWJoNm1VdEVjRGphdmY2Y01Wd2xuMUt2UGJLUwpZeUJzRHpQY1lFcUw1R0o4bmZ0U0FZL1pORXQ5Y3dWcU0xckxIMm1qc2FGY2xsRnNlbTVnOG5RdzFCdGtubm0zCnhtN3Bqelk3V1c3MHJvak80OURZTzVQNmppQ0t5OWViM1YzMThmQnVaQi9hR2pDV2g3djZqaGFRRTdBMjQwdlEKdy81Skhsc2R0THNmb3NEYng3NHZWenJXWmUyWDhLOU0rWEhaNXRGSlJVV2U4YmdpR2hFWkdRdkFnLzFSd1hpZgpuUWNDSVlVUVFydjBWNTNQY2c2UFNmZ2p3eUI1NFc0dlVSOGJZMlNyRE9qRVFlWnBncnIrZkpEaFM0TDkxaTBxCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeC9QZjZ4VVZiMldqditicDNuVEUKU0tJbHJmbUxiMm5mOW5Gd0x4YlpINjJkUjF6dm03NWorQmhlamhzQzl4Mm1VM2h5d0h3WE5jMllMcGR2RHNZOApNd0lIT0g5N3R0YXFFQlA0L2tJV0M5RGxndXhwV3JobDh5REFHVnhic3YrbWFyVkgyTmhreGJ5WVQ5RUVUVDREClNBZmRESGE4TXE0aTRtc2hlZ2xNM1B3ek9JNE5ka2xzeHltUWVtRVFCeHIxQllNaFNBVFU2SWoyV0tKRjdBVVQKTGhhS3RTdG8rdThmYW40cVMrMWxqNXlxOWtocitCYmlEMHpCbllYaTdjVUs5TmpHVDJaS3JpYmV3cjR6QktGcApLTVpsdGJ5bUp1dTMxQU9vK3p3M0swWmRwbFFIc3c4dFYxbDNRcmpxQmVTcmQzeTVDNmZHS3RzSmdTUWF6cm5vClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlR4anRxa0cyaHloaHNFSEpIVEMKZUN0aFc4UXczQnEydFh3aGZ1QjlQSmJJOHZyYllTMVhvSEFjZ2xMa2VJaW1qTlVGOGtzdEw1MzBMRWV6djdrcQpuM2QyeEFVQzcxK2hNSUFFQjIySHI4YkRnRy9QcEZJNGdrdWI2ekxnNmZBZW5PZ2JzdlJpZStNM2dnMWp6emN1CjI2VGMvb1dxTGVIbHRTckRzN05Rb3FDZW9ZVTd3c3Y4alpvcVFRbmxyRTJhZU82cVF2bVdTOWM4dWRpSGgvTTIKU0F5T0NSWEttQndRSXBZVEZueStrbHRYRldVVGt3aHVpMHVja2YrVnpJWXRQZkd2QUhnMGlVOFYxemZocVk4SgpmMXBNTzZjWDBBOEROV3lodXRXL2lCNEsyekIrb3FqYVRCemkzVUd1ZFEya1Bvb0dBbWJ4T1VMa2gxYzVINGJJCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVEzTFg5Uy95c2RyOUZUVFF6bEwKckVqb1REK3A2ZjZ2aGZCWjBhQVZpR0ZEZ2drdmMwWm1XTE9GTHhGSGwvdENNUzllSE9vVUk4Qzd5amdWeFQ2TAppUlBRWGxYUEc0M21tcXNPSnNrVXkyR2Y0cm9QaE9ESVhzSERGMDVYUUlKYlcxeU1yU3ZXQ1BwSHF4eTIzSXdvCjZKMXVTSktuYmRaYmpNMk42WHdtME5vUlYxSm1ES3gvRDBxeXEzU3BHV2NtUmNSdWNheERaNWhFZkNzV0RJNWMKeGZ2SWNweERSaE5mQkJQVEJIdXNHWHBtTjB3Qk1JMGZlYU9OYWRxdnQwQzE1bEU2SFEycDZiTjBqZ1Q1RzFaOQo2R0IzeXRwRkJLdHRhdFRJMzRGWXc0WXBwREdtMXdYaS9XVGFON3pFRkkrN3k0c3FrR240b0xIeEhzMmVrOFlVCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWQvb05vbDlFWlRVU0w4MVluNS8KRWNNWDVybklYckllbUJCZjBHT1l2eFQ1ODd1ZVFHN0lLSWw2VU5nYk5GWWVwamR0MU94WWRkOE9wakdraWlhSwpJTmVydXZrTlhEU2tnWXZxWnZVSTFMS3psV2QvcDR0RWI4V2RmS3lIS0plQ0Y0YXJ2WmlrS2tjZ2ZYWWN2VWNNClBmaERJcTAzZG5Ta2ZxZ0tMSjB3dVhoYVByaW9RZG93Y0dKWU1mRmlFRmsvU3NFcE1IWlFGejBwNDd6UkpCbXYKWExZSkZrQVJGT0c2U3ZkcE9kWVQwUm5WckVnb0Nnc1lXOWFjaFFHNXBaV0w0M3ZFUGpYYXlhdVZockhFV2oyVQowTUpUY2ltNGloTVdlWjJxSUxBMnArQ3NHd0R3N0t1Rjd0WndBN25zMFlseDFEOCttblJtSkpsdTY5d00vVXpFCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnRIemdqUnF1cWt0Y1JCQU9nTngKTElYQzNzWVhrYkczOHVFVy9IQUkvcEZTSTdzOFRwRUd2MlE0ejF2YUcwRjAyejJ1RTUzdzRqdVVlWTNRdVRUZAozZm5GTDVUQ2pldHBaTEZ6Q2U0Sm85VkJUK1FzaWxJSlhhMnZGdDljTGE1VkNsM21aWGc1RlNuZGFvNTlYTmY1CnRuQkdQK3BGTFd3S0dpLzZ4Q3VoQW1qZzA5RytCRFBSbVk1OFJUN0Q4K0dUK1NlaXJCNGJVNmJROElxTVd4OWoKRGQzKzc5eG12TERQZktoOG5OR1VyNGlzT2F3OVhyOUgxekNRT24ra0FUR2UrNU1qcndWUEwzYWlENWp2M0piUAp0UEpHanNvcUlYd254VE9mUUNpVUR2RVhsL1p0dW1GL1ZhZFliSVFDbHRmQXdYa1pvZW9xc09lSTJzMGZ0STYwCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemc0bkZqNy9LQWYwRThHMVdJVmcKckdackkzZWVLRktFeW1yb0w4cHdhbE84ZnNvam01Y0hVK2VFSGljTXBJSUhwOEhMM2JGL0NJMFgwUkQ2emtodgpmQVZRU1VKUHNmRjdNU081dWpaLzlxWXNaeDhLdllpb2FDb1FOZ0FhZVNyWEdQNHdPRzM4eGVsT1doNEVKWSt6CnY4Y01Kb3dzZk50R1cwZG5aYTdlbjd4djZ5dDFENjRjREFaTFhESEtabndESUd1MUUvS0hGYWhvbzg2dERtNzcKVUt5UkQ1QTNuUU1wQVkyeGxGR1QrM09lVVNGYUZGQmErcjdQMW5zTXMzenNlcWhSRWt4c1BhcDZpMXdGVHRGUQpTcGZEcUdPc3krVlNqZ2hmZ1RJZDYwL0duT2lZUVJPL3BpS2czVXFsbFZWNms5R05aWFNFUDBVZy9YWXlCU0p1CjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmFLb3drQTJSQUpHZjBqdkFwNU4KSmZjQTlHaUNBQkxMcG5PWUFZMXZWQzVPVTdKNFU3MzFIcEdMSmowSlAwU1h0TlhsTUt1bEwvQnhSZ2hZU3d6MgpJckZKbEtHc2pMMDRabWgvN0pSNHR4ZUx6eVZmaVcvendXTGt5Q3FjQTJQMTFwUHZKQk8yNWpuR1dtRURvcWx6CjlpaW1CT3YwNGxkUjRzbE1LcU95bFhVckt2TEVuWlZvazlxNXBZelEzTXZEeWYvbWdGckhBMDI4ZzhidGFubGEKS2lENk5LeGZLcnpoZDRWRG1tQ3pYdlNaaDhWcTJHTGNLSVV6WlhSV3JIdTI4TmxxZGdIYWlzQkp1ZzN0Z2FaSApwUGFrajlWOUhQYW1Nd1pCVmZqejZnN2kyR21Lc0hNREdxQWxwVVFjSThoWmJRaE5iTDB1RHd0VGZxc05TT1IyCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXF5UzQxK3RDU3NldklUelBiM1oKdHRtOUNaUnN5aHRhMWhRYmc0SGJKTG45M0NPV0tueWNsdmo4eUpMdW1QUGR0RitxZ2tUYVRrRHRHNmVOaXlPKworck41UHNodGFXMkttcmZyeSs0cSt0UnlvUUQrVlNTTjRMdmdEdTBOZHR5di9IcFE2dWhNRk9Ed1NBWjNQWTR5CmN1MGVuQ0dvempGc3cxSldoY2YzWkZGTVRiSUxBaGJBTGJTZFRVWXNKeGNqaEdCSmF1blpVTVppcFU4YVRaWSsKcnViZk1XbS9ZVkJVL2RpZUJhKzJySVNLeUw3Z3VKUi84R3lBZFFtOHRBYUJvMmgvZTFHZ2pKVTc5RXlESE9OTwpOMUhJcHpPNjFjUDEvaWlhSThpOVhrZDBxSTI0OGIxeE1OWk5CalhvRDU0YTNMN2xwdzZVVXZnd3BacFp2c3ZXCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2hWdnZkSHRFeGxpQnFGSFdRSVIKQy9wVldCTzJyVldCVkZoRlo0ZG5vRmxwTnZuME5JY25nNTZDMGtJS1JwOGNkM3RHbGNXSmYrWDdRNVNpaXQzYgpCeFNaOFFkZ0tsQmVmUmdmajFVaGU1eEtERzNycXBzU29oOU9SckFlTlpvVWJpMUQxTklxa0pHNWJrQW5zcm5pCnVzNlBkalFmVWhubUE5czQ3MDdxbHg4TUxzNDlrZ1hCNWdQa25Ebm9WMXhTTlozRUVCdEVsTkRyRmFiU3VqckIKQkE0OHBRSWsvYURHSFVvRUF1eWl5aENpMnVaT3dKbHl4UmdOSjdrV0dndmZxa0ZHd29wSnZOc1RCVGlWWHNCYwpUK3o4QlgvK3hHa2FkZzQxY2ErMSs2UElZV3NHMUtwVC90T3lSSlpJaTdjT3RpZmJEQWJPSnNtbnUyd0Nkb21lCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHROd0prOW8zME5LdzB4eEFCREYKdHdUWENBSklaeFJWdlBlZkcvbVZPeThiWVk1YnU5aDVwellHTTk5WnlXYzl6RUF3cWZFV2pudkdRT3pqYTlaegp3NFVoYUZ0S0hPZUVmQWxuL2JNLzV5VGphc3VZSmhmSmlBVCtqai8weXIzbXpGaUYwT1RVUGM2K3hBZkZEWXpRCnp1OEVqWlFSQ3NCbGFFdFl6bkpzM05HYjBSQTcxdmtoVG5tQldtM2ZiR1BpU2k1QmpmbVR0a0NjdktkYmcvNUgKNTBrSEVYdUNYY0N5d2F6K3g4SjhaSERYVDhJZzU5cllxdWpwdVIva1RwTUJDdWdxTUdhb290dDRlNmR1Z1ZyaQpnRS9WSG91ODMyY2hDUHdnV0plKzdFdFd6RkFJZFVxaFFTNmc4em5TTUNTOWZ6TzRycWhodFhvSysvS1J6bFpHCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGh0aWtMcTZWeWRIKy9yWVNvMUoKZjZMbjBtY0xrcW9xcCtTUU5TZXNYQTRKRWQ5S1RXeUVnL1g4RUxwQnNoNW1HSUtOa3lvSjJIeGRWWXJMUXNVMwpERUZlWGJsYkdrcVVjSndwUXdDZ3V3U3RmbkxZaEc4VXZSQmwzTGVGSXFvM1pKelJ3U0FJWHY2cWtkcDRJWTVKCmJncGwzQ2pBbjU3bStaTmd6RkIyRGZQTFNIbHluWndMbDQ2MkxWVHR0TjU5bDhscGhxUW83YVlGWVZRMmFheS8KRytVSk5XbjFkMWlPSFdrSzl3MSsvN3R4aU8rNGg4eCtZR3BUOVMyQktjS0NHUEFBWGtXdHVTaGVaUjVpcWtTRApYOGx6MzM0L1Z1SDdVTXZkKy8yVkRNQUUvU2UwbWVJZUQ1a2tnT0RYY3M4eEhtNUpzamdEWHJuNC94QUFFK1pMCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckxpY0xSRnByT0tqWWdaT2p1RkwKamd1cUJMcDhxYVZTVFYwbC9vaDVrbi9EQzZXazFWbGdjU0IxU3h0UUlGU3NMandCYkpyUTdWN2lXR3E5dGw3dwo0ME1MV25iTGVWZTIremo2bkJwaVpqK3VHYTdlRElPbExCa2JncC9vUDkxcDRyNjBaazRZY3lvYXZsS0FlUkdYCkpTejhwT0xNeHF1dE82cUFnRnQwY1hMOHdDZUhYcGE4a0pnYy9tY1lsNFRYTlVEZ0JRMXNDd0FTN3E1Vm5ZVHIKbDRGdlhEeDcxU001K2wvdnJTa09tbmdBS0FhaDMwRlgwV1AwSkpiU3ErcUZweXVwWER2NExyV0FXaERmZDQrNwpOQUJjT1Q4SExlWGhoZForcW9jeDE1N1pET2gyRGNZdVVHb2s2UENxRVpoSFpNMEFQTjk1OXBPdUdkOU56Rm1iCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUpGREhZaXV3ckNwKzJDMGtsNzQKWXNaUDZSaGZPTmRKWklHVWRwZXo5RkVweEFRc1U3cmU3MXhQcnJJRENpci9EM1kzb2psdk15Y2kxaE9lVkZ1eQpGaURYSWk4ZVg4aFFqQkVySEhHNEJUSWNEbUNyYjZmd3hiaVBYL3kxMFZIZWdYZXM5Uy9PTlI5STNqYnhrOEF0CjdhZEdURWEvSU1wOTRtanJlZGlySmtRaWg0bXF6bkg0aGJUaXpEd2lCY09hL0VDdW9ySzlSSjJsRUMzeVNWMnEKVFUzZ0lXcGlxSHpOSk5MWVpEMEFiMmo1eTdNd0NOVTNOSGM2OVhWNDFtQzJlSnBGU1JnN1lKWHkwdllzMlJjawpBY0JiUGRQYkd4L1Y0RWlzdmt5cGZaVkxCV3FWdGFxdkRoN2R0Z3Z6VDA1b1NpME5KSkVqUVMwbHBvelpjcjA4CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUg1akJqNlF5UFhoT1B4MURRSlkKcVN5Rk05SHZiRnlRRVpBTWZlam5WZnlBTDh1UC9ITS9iaENBRFBubWkrdGVyQVJYUmR2UjFrRTg1amExNVJNaQpmTkxBMXZUalFtQ1h5RnpPVlh3dG1ZNjNnalkvQUlscFYwb3VLZnJrbWd4WDBLZ3BELzVOV0VqRXBRNmpOYnZZCkxlT2ZJYlcxNVd1M0NGdS9BUnRiOWdWYWhrWHlKaXZ3aEZNWTd1aEx2N0FCVmxhWjRHWFIrVFN2Z2EyaXByaFEKNEY1VjhaYld6ZUpzK1VvcnZLSVVFdDRtNWkyRnl6Sm5EVkxNaEN1RUlxZUVJMDZLcGlZSnpGaDRNaDNGcE4wVQpqUWFBbGZtUlJvVGFuVXpzYVdVV3pOcWFrRFV1RWN4L3dhZW12OFRSK3g1T2xXRmdTYnV0Sm5JZWQwV25EY1ZBCmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNytsYTZvU3lObytjS2Fqb3F3Um8KZHZyWkNVakFUc255b1VTQ1RpQmI1Wk9zSjhjWWZjOXJ4WU5Xc2dVb1hEZXJySDJOYyttTnFhbEg3WXFFaFM0LwpvNVp6TXlYb1ExeUpmRnRsdGRwakZhTGQyVlJhcUhZV0lyMG53MDNDZEJJdFlIWG9saHhNeFBRU1NzNXBnamw2Clp5cVBuS2FaeXM4S3pwUm9DZnZTbVVMS21VaGRsczZtNHJBMklqV3IyL0wwbEYrNjd1Y0VCdDhqOGxQTDB5MWYKamV1ZEN2Z3c5Tk5sc0tnNTB2cjNHQjZBZDI0aXZ3Z1Rzb0xENUttUCtRRW9CVVluTHcxaUhVT3FMUjBCV0V1KwpuV1BHVlRVM0MvK2ZtSFVDRnhvREJrakVGWXdUSTJWbVhNUWw4czZjTjZpZlVENXZFdTJJRjR1SnlTeWVFMGRWCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN3dLbFl6eHBaM3JJN2Yzb3JuNWsKVm1vaXMrQ0lMMjlXVzhBODY1bVlCQUJ2N3ppb2V2UHRubHhBM0lYZjVaek9TQlg5RHNPSEdyREFCWFNubUE1Swo4MHlFVWtVckE2bUFNWCt0cDQ2d1dmeld2U0xnYWtvcS9sU3EvL1VabWZKR2VvaGJackhBbUtSV1ptQ0tkMCtICkUwemUrT3JSaktPTDlQTTdkOGtjUm94R2RhS1VBSXRyVlk0TW5mN0dCNHgzdmpjSFk2U2ZxNEJXNytOZjI0bTgKbGF5aDdITG5XNVhMTTc4S0JPd0NldVRRNERBK3lHMlBkWXlQR0I4Wk9VTmJkNHQ0U0VuMjF5dFVrZVozbTQ1aAptQWJvdXBkN2tSaHkzRnlrWUFRUGEreDMrNmh2VkcwT3BlU1hrcytDZG9Nc2MvL1Iybmo0OVVVcTVlNTMrUkVICkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFZMaTVuWXVoTEVpWnpNdDhEbXAKSFJJTkdUOEFLV2xyWFFyVWxuNXo2ZXZDQTAyeHFhOTFyTnBSS09rQU5LeE42NlZCRTY0SVVaRllkMkplSDU3NApITHc1MHdVYXdzbkJpNkZkMWxNTUhQd2ZwODVSeGhRSklBcUQvSWw0SDB3dTFHTjRhVGJJUXJwZ1A5dEU5WHpaCkZtN3FxQjJqWTVXNFlqU1lzZmZ4ZzV0L242dkU1UUFOWWN3ZEFCN05IWlo3TU5sTGRMZnNSUTg3bTVNWWk5c3YKbTZiLzcvV1hUY2wxRGRyb0VLYURkdXRlYXFNdGZhREdiUzQ5NWhUZ2tEaG81RzRqQzlnNzdlc3IxZ1l2c013aQo5clh5RG84YWZjNFBBYzFXMWNJS2I0QTc4M2R1eHR6a1pvRnBQTEFkd3p2K2lKazhLVTBOMVZiUjVwOVlUamFEClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnU4eGMramFzTi9WeDNnL05tKzEKaTNvem1zZnl6WVBMY21aWXVXbGg1UU45NUVPTXlFamhRcHlrNkRDelBSNkdaMmY5c2tWeHNmczVNeXFwdWRrNQpHY2FhS3lMYTlSMUJmQjcyUjF1a3VkeDI0ZU5ZZXF4c2N5NVBRTUdYc2U4SVZvdU5VeXpUK0lEWGdNRHBuV1RZCm8xRmNhYVRzNmhMM0FWYjRYYkgzR001Zjg2N2M4cFNaR2NYQ3B2K3p6MlIzUXlscVY2REV2dGphUlU2MnM2ZzYKRjNEeEdhcmVSWlNhS0hmTGo3UFhCMlBob3R0U1ZCZi9IN1BsU2lvcnljR01hRk9Oc3RUMitFeGxNcmtCTGQ3ZgpCc24rVkFiazdWeXBHY2VISzZTci9ZSGN2QURNSk9BL2lBWmVqdmpVL29UZEdCeXpTSVM2eXJ6MmRjV0JkemxvCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlhLUHlxaDJicUpwMm82dVUvVVUKZTB3KzVSNm1JMUM2d09MT1VkZ1kzOGQyVXVIYUF4K0MyMk1mUHJLck9OTGJWeDU3NzlyUFFmczNRdko3QVF4TwpXRVRmTDJ6REhTaW8zcGVaMXJrVUt1MEFTYlcrM3g3WHMra2grM1N5ZGpXVnJ1NWtCVXdFV1RpZEJQQStNTnZzClFUdU5oOXdUTTFYK3F0ZDhCY3p5MkZMMDRXZjE2SXhOOTlSTzNjL2tvMTJLUjRyek1Bbi9CRFFEZnhaL1V6VDAKYy8rZXZQZXVOREhMalJPLzVYd0JuTXJRdHVUSkpwMFh6TkF1eEZUMUJkU0VDajNOU3BCVEJVZE5pcU1raGtVVApJN21aekVYSlhkSkMvT0x4SDVGQUFuSC80OUVIVGpTNzJBUEQzZ21pcVpqVldVT1FGV01CVytRMVVLK081YXJDCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMThKY3BUaExZeXlxa1ZNeEJtYi8KUzJPQUlWdEdmSkxvdUl5Q2txYStaQSt2bEU3OHpibkZmSmNYWmdzakhQNWxyOFV4aWRRczE4aEJjeWQxUTZEUwpoU1FEeWcrV2hQTnpIV0FzNEphbU9uQVhkYjloV2NDRVBmWHhEbWd5ZHJ2cjdaWmNjaE81d3NlL0h3TTduakdNCmsrdEdRUW5iOHJwbldkaHdCa3h4UHg3R0JqQXVMVGI0L0xqZVFxa3FPalhoRUVoL2dadG1VekUvUFdESkNYazkKamQyOVRqeFVhM1JsTTl1SGFOdFNSanpzV1FVa1k4eFExV2NDaWovenpBOVphbkZKYmx2WlZVNlZDNnBUQnNNVQpHWkZnUlgvdHpsQ0c1aWpmbnRPVmphMVBCR0ZrNW1CRzVVaTA1RU51WWI3b21hQjg3bk80Y1lYdTVCeENNMEd5Ckp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3c2NW1mUnMrTyt4U213ekpqRDYKcFVsUjlPOTNjM3E1cnhwcjhMRDVxMkMxMkRtbXV2MWFhb2s1ei83RVJiL2ZKa29hdFJiSDNkS3h5d2p2c2tXUwo2ZUwrRmt5MVU2d3FUd2YzSEtlZEIvam9yZENlTGg5V2trSzcyRjhscEREUnlSNVF1b3lnWnNVbU0zN2V0VklmCjNCSVdqZFp1NU1uRkNCYTIrZUE2YUVhMFpTKzJYNk5PMGs0SXRUekNPc2NpUERGVnhFK0I5TUkvK0poaTFwdm4KNjlRSDZnVVlRMGlXamhyaHdaMzNXcThnL2REU2loNWRWY3JmQStVMDRtMC9EcGUrMXpzRzF2dWhXN09yald5bAo5YnNxL0NVamJTeTQzZWtqdFJPYW9yVzFPMlVpR1VSLzM1c0JyUGZUN3JWVW9YYTVFTkpERml1SGZMOTFuYjVECkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdytiU3Y1T1hvS1F4VlF4QUd3K0sKWVBoRVp2UFptY0dQZVJkL2Vvb1d0djIzZVJqVkRLdmpqdE1zaTlTZHVNM2ZkRitKU0ZkOE1zM2prVmRUNEI2dgo3RXEvUDlaWG53STZTNzNGSnkwMlRBb1FZVjRiMFlXekg5UWNrYzRhbnNjVC9qSHhTVFZFYnRiRCtoVnpiaWlWCk8xWWpFaW4wR2FmZUJtdmpXQ1llUFF3RVFnczlxdkM4VEhlcXppZG1Qb0MwWUJYYWhwVzVxbFVWRW1rUDR3dm4KZ3NiZEV2MGFyaStjYlNDWHNiQ0YxVzRsUnN1RHVlcVNrdkpHL205QTRJNUJoRnliNGhlMDAzMnFzYlhYbXg3agpIZWtzZEtQakU0SWh2Qms0N3VDZGV0bTJBeFZKZXBPY0Z1Wmc4NE5YWTJ3dWF0Rk5OOTA1NHJPWThYbithL0tYCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNndGRm5ST2NHOW5nWEh4cTU5ZlIKTHdhYXRzamsva3REa0tTTlZIVzRrOTNSSHhRdExkU0tZTFZ1ZlZJWG95V3c3Kzc2SW1WcHFha010QTE0N3RnZgpFa1hoWjRrYituUWUxdmhqV2U3MXkzOXBlT0dWOEhTTTNIM3BSVUx2dlVuZjBCT1B2aXRNczZSWG1CRVZ2c05RCkttRStqYWtKYi9JeEswYzI1SEt0cHRtNm94dTFJbmJqanpJSFlnUHNhYTFDSGdoeUZLQ1RTbUxNR2FJUmpDZ08KY20wd3ZhN0JlVWRVQ3hodXJIc1RlK1RhSXNkODhwWjBoU1VaZCtUQTZyMVQvNTJ6YzBwSENIWDdHVThWNkRyawpxQjdVYzVJb0YyZ2ZHZ1BqbG5EbVdSZC8ydXF3NThWcTRiMm0rcGlaUlZISEJaRXp4ZS9VMlhTQUczaDhmbitkCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2xvc081NFZOU0ZEN3o3TFlwdzUKaDdDV3REd0tselFzNXBpV2I4NHBKRVYwYXV6NWNGVGQ0TktjOE9wRjVkcmdLbFRqa2ZMQ1VaK1VnVGEvWUF2VgpSdFlBVEFPV0ZUd2YwdHk0SC84ZWNmVEhGS2QxbVFweUpabmkwMURRZGRVWW1HQlhuZ0R0aHFPK3FKQTRRenZ4CmVrVVNpN3RmRG5yZy93cGhKQzBNaFZCS2JQcVZyVzE2M0NORE4yZkZmQlB2M24vVzBZbjZidTEwNDJ5OTRxQkEKQktsVnc3Tlc2NHZtN1djdXU0M29Ja25FUzBBM1J4Vi9lKzhXZlVpRnA4eVBkcFM2dlpuQnMvNGFYd1FDY3JiVAo4ZG5hcSsyTTdQMU5ubHdRQm1LeW5pVVF2VUcyNWF4T3N3aEl2TkFoTHN4YU9YU0pIMXVTYWFocm8yc0xHYVROCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemtTUkRvekVvbm9FSWhiOWhXd1EKWkFKWi9UWEI3a1ZDQ3FtVEthNHpHVG1qQ1l3R0JtQTVlbzRoT3h3MzVPR0JrdWVNazFTdkVtZGx3Q3ZmUm9CYwp4U1I1dVJZazdCNTZVekwrd21DdUROcWltbnpqNFpYc05tZFc2WEhod29RRXYveG5qazVsdlNiV042VDVCYjZjCm1EcnNVdTFuT0VxWGZ1NkZZajd5d3lhaVJtUW9WZ0thNTR1YWlHV3lESWJ1cVMrTXlNdU5Ua0xRYzBlM1o5QkUKQXVPM3RUdzhrbDEycGo1SmIzZjVxUVYxNGpPdG5DNEsrbEhrSE9kYVh2YjRZaGRjd1hWQVZZWTgrQTJlWm0wWgpEYkVIaG1YKzBrY2JKeDNqQ2VtY2dtdVhmampNRElTVHlWbzlyVlNpVXVhbzFlWnZIcnowckJOdzh6Wm90N2VxCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdElMTjdIV25BQkszOUl6NEZmOGsKalpRdEVubFVmZ0lTV3lvTUtHS2FhNzgrbGU1UlBjWisrRFUzditNQlEyamlFM2RXcmFQcU9RelFJTTVJcjI2SgpqNytPYnhQdTI4ZC8xUEpwUHpQd3B0UEpSWnRudTBDWUVPRU1tOERsQmMwdlI3Smh0N1E3M09YY3BJNWVvZ1FDCkhhVk5uNnE0Nlo0ZGN0SGMxdDZ3OVFZOUU2WXA2MEVWQXhXaE9NdnYwOVNlVS9ucEV1LzRlWVNhaEgvNnpZRmcKYTVLUE9IdGdRS2JNWlhySHljdnovdm04MWJmejlTT0lUeDlNemh6WUJkYVdOQndQUW9tTndOeTZ5WUNQZWt6dwpxelEyb05XN1M4SDFSVXlxWi9FM1N3aVRWUkpaQTVCVHVEaStIZ0tPNTZpYXBKMExuaDcveW1pM240MmQwRVV5Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnFOWDF1azVNaHYrcEsreG5QaDkKSmU5b0QzTUhmeUFZd2dkTjZMLzRuSVBSVkVmdkliaDZCTGpwQXE5ZVR6UVhRaWxRQmFwTWdyK1FYOEZ2SktuagozUEtIOVdVUDBNSmdRZks4RmNzdy9oNTNzYnpkUzRPVzVTdTRhUkh2ZlJyQUdEd1o3aFJjUVk5NWtNQW5EdEtxClRNbnYyWDVhakdvN25MNktTV1VKM25Ma1ZsT3pEb1o3RXJ4Q1l5Vm5LN0FFQjVEVE13bDRBa3QrNWVIYlk5NXoKL0l2Ym5ZTGhnbDZoVUxlTmF4emtBZi8vK0ZsSVhLS3UvZEU1MnZJaWJzUHdxWllCeUlOdThURXF5WUNiSUtrQQpqSUUvb3dnQTRRZnU2UEhzVU1EcVIvc05OeWRneHNwTWx5ZmhZS0pQdUJ2WXlKMU9nUmgzZVVpVHlPY3h1dDI0CjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd21lTGxyNGtzdFM4VUZRSVpTeDMKR0g5RzhzMzRYcDBKYnpzNnpQUnZjMzJOTkl0ZjlSVTA3bnh3ZnRoaFpsaGg3d2Y1YU9JOHRkL1JZYm5RcFpGaQpOMmNRcXlDNXc0bStXQmE3ODJ3dWJqd1lpTmMrWGh0cjJpMGhEK3NUTHMwbGNvbW5ZNXNkcnQrbm9MMWVPMmpqCklLRHpEVHpoVDY3R3ZNYXUvRjFpc05OOWx4WWJjc29BOTJkWWZpYVYyN3lpY0xSbXRSbjcyczA0d2ltQjhEVzcKWkt2OGhPR0Q3MkwzajRTemNjYTEvNFlPVVErMTdtZlo1cnp5anphZ2ZhODRHM2l4SjNsWjNuTHd6dU0xRUpnSAplTTZDZ2lLajVTajZyNmdHWU43QzVsZzFxZDdDejVZeWg0UUFmbjBycUVnc0c5SUFDNkhwOTV2OGlhR0lvVlZzCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGZTQng4eXl0ZVNMZzVxUm9GMHoKVUM0TDBvKzhZQjhLTlJuZ01Ca1hyaHlOaW5wZWhDTTlXb3d2UG5tR3dpRWltaHRqeEtVYVhoWk5VSnFMemNnTgpadXNoeGJHUVF3MFJMSmZ3b2NpMjlxR3NXTlFOSXF3eUUyUHVCeVNaOEtVeHNiNjkydmZpSzl6ZjRRZWhzWDloCkNPeCtjVHVxZXhaTjlZQTZxRXgrYVhUY2grSzJyK2VpVDY0MkJQMTlaZkovVGREVFJsbXFtd011YVlPSjdiY0IKalBqckR5VWZIRGMvcVJzM0Uzb01mbFZDcWpwRnQ3dmhmZXROelAvZXNmZGlIR25EK0ZvZzZqUVdUam9ZVUlEMgpPUDl4SXVqWXl6R3ZiOHNvL1FOeFBxSWJWLzVES2F4YkRlU29ab0xDd2REOTMwZnJoR3VYbndrM2s0ZzkvR3NJClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmdycDk4VUMvY0hSZ3BMQVpnR1YKUDQ2UkhLNlpDMGpRMnFDMitYNGxzQzdXMUw4aUZ1UVpaUG5sbXRPTHJwN2VNcmpERUNHZEkvUHNHVmVWSWVKMQpYRzJTK1BQVUlZZXIzcG9lLzJpakNKMUhFb2RMZmFXSUFsUzBOQmczTExpSW5iMHpjeEJDdGRlaG85RVBEVmU4Ck1VSXBUb0RONWp4ZVJjNDJISUFpUUJHZ0JvV1h1V3ZWeEgrNmxOVlkvUmdVTWo5OExGTUEwenlPT2Q4aG5NeksKaFJsWFFQbkNZSmE2VVFuK0VOZEdVSi8rcmZxRTRmM3lvSCtiVEptWktzTHc2dklJNUx5SU9qTTAzY1Fuek5MSgoxRlFIMis2dTEzWjdUYi9zeGd4THloNHdFc3hNVE0zS2VKZS9UYVdXYndyakhLUW1RemY4VUZSQkpING95aWNNCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmRDS2pSMUZlQm5rUUp1UG1TcGoKSlJHWittQmlBbTFLRXBpZS9mdnpoMHRBU0VsYmdCSXcrbGxRUi8wekVOK0hVRXJrbVl3VFJTektSbjBNWVRqLworcmFGWU5iTHlmU0lmWWRBa09DNm5nc216aUxLdzVFejlCWlp4WDNNVjI0ZG1WUGl5VVJMS0hST2tueVVGU1dGCnQ4M043c3FjTURhTGdiZzF3d09ENU54VkNDei9uVnBGL3RKTGRpb1JkQ0J4ZEJDbGdNTnlxV3luenIxTGdxK20KY296eThjNmEyNUhJTXY2bDRheUx2WnpkbzY0dnRuTVI5ajJ1akNEQ3hBMWFTQXMxcEc1UUY1c2hGTzBpNzhORApyeVdvSUZ6VVRTdHdLTWZzRUNtWVBraWFPdURVKzlMTS90N3FBcVVZQ001NGNiU1BvcG5lRURWck0vWkRPTUR2CjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNUFkR0NhOXM2eUdBbEhTRVRyaUsKaEZYRHZPdkRKRFNNaFZmS2tDeGl3b3BsL3N2L2Qvd3FiSDZncWp2N09HcUlJZGljYnhSYVo3Qllta05sV1ExbQpYNUVNZXBsTUJreDcxZVhrczNFVWRTOXhQWWcwR0VXalBlQ0N4QW16M2xVWVNXSjQvMzMxSWZlbnUxb0MyMmlhCk56Y1V6M0tpWlN4VDczN3prMzRaL2c5dkNkUzBrY0p6RnpaQXNaTHFBQVpYNE9rZGFTZHNYejdnditLQ0E0QWYKQjZ4RzhhbUZsTDJURitQSzNQZWVPMVVWMVZZMmFwMWdJSktyS2had2JQNXR0U01CT3pxL1ZTU2ZMZEZMTnk0LwpId0tlYjN2VlhuQ0dZRGlzbGJSd2xkWEQ0V0tpWWk5WTBEdmowRW9kelgxTDhUV0xYWDdlWnhGOEZCZ082NjF1CkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXFlMjVUdHAzMEU5MFRobm9lNFAKUnZpTXZkZGhXTnZnKzJpN3NMdXNTVjhPT3lBVjlwNmh3dHpGMytyVmFwV1planhybTFWdFl6Q29zMWJzS2xHZgpkNHdnTjlNK0VYOVQrdlVBZ01keWdYcTR6M1NCNmZrNzFHOGlEZXRabU41eDd5L1ArTUJqNlZoYkNTa1orQnBtCnNuYUZyS1h4QTRDQUduWnZodUl0RGlxcnVrZHVVdVhDTTZHNnhVTHdvNFhONUs4QjFUYit2ZEdwL0IwNGhzMVoKZW9NYnRqYVEzY1VJeFZ5OXBNQ0JmT25vVFdOMVNZS2RuTnR0VU54eS9NMmhCMENRWWFXaWdNazVvTVV2bkUwSApEaldlOCtiRTJFRmlHeCtiWGhiT1Byd0tOZVR6KzhLdHhiQ1l4cW1TZDIvc2VabEkrL2FzOElnZXovcHhhWjlQCnBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEovb0Z1c0pTTGNBZHNBUllIeG0Kb2JJU1p5K3pnNlZmcm85WnEyNkRMN3kzTlJEeisvTjZxYTZxNCsrRGVxM0cyWFgrREg5YmpsTVQzOTd5cGNMZAo1eTNzUysvWmdnall4eFFpckswcC9qZEx1S1Vib1kvaXloUjE2QkpGZ2p4YWR0R1RmaVM3TDdKRkpTeGxvTk8rCkhnbGxzai9uakpIUWg0ZldUR3JkTVByOXI4NDQxVXJiaWdwOEFqN0JqanBwNTR0YnJFWnF5UE93Qk5zUlR2VlIKR1VCZG94aUxGSzBLOEJ0L1MxeVVXc0Q1RUlWSlROc25SS25VUkFZU20wMHRyRjV0UTZwRmdmVlZiVkQrTTM0NAo1TzNnOE5YTm5Ka3laT21IWnB3OXpHUWhLZ2dYRFBWVnJzR2dlbzRFVzFqd1Y2eEJRSVdqMXNxZHlXelJUbkVrCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclhQVGFYYnp1cHlCd1FaTGowYjcKa0VBVklTZ1NQTW5kUzZ3M3FFVUJRMWdIZi84V1RsdThrY2loMDVGTG91NUtSM0R5ZU5qT3FBdFRKTGRBbTYrYwpoWkd6cFNkaTBoYUtHTlBYSVp2VHpGRldVTzdsUXJpSHVxdHZIaFkrWjB2cnpLQVNDYjVhRTJFSm1QTVpwS0hQCmlJb0VHTjVkSUpQOFFXOTc4QTZDMVlMTG5pTi9kcWoyZkMwM0ZOa1dqZ2RBRTZ3SEFRT21SMjRzMzNDL0FoQVQKZGYybmt6ZW9GL3hsMS94TW5UQmJTUnFyVDhTZGxpSmgzZmJCak5DNTU3YnByRXUzaHREVEt3S0tSdWFwbWl4aQo3bWdQb3RKK1ZNalBJMjZoWGZuc1IwdXpLSGtjNmRBWkNORWIybW1UWkMrQ1Zxc3YzazNBU09VZUVnOHRNcTZBCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcW9PV0xkSXpuNmRvVEFxbHVuY1UKQldmcCtBT3FHYVRoSTEyUlNUMTR6YjY2ejVvZUJhV24xd1k1RG9aRjFpanZSbHZMRm5TQXdCeEJNSWVkeWU2YwppQmkrWENuRmd4RldJYStTNG9BUHBmN09obHBKcUpDTTVNM2FnU3M1WDRnZVJrTXhjWkc2dTBGMkRSWVk3SHErCkw5SmRaVEVDWTVPQ2t1V2o0QUFnQWRhRktDQW9XNVhqRXFoVFRsNkR4WmZoTy9QVVArbFNON1k2N1h6VmJYdTcKNmtNSERBVUQ1VzNlWVdnbFEzUTFhTldxRUJ6Ym9ZelpaMVY5SnJ1WVZ2bW5tRlFLK1NibGJVQ2pVNk1wK1BHMQp6WExxT2ZTMWhzdGdxekN0VHFtdm43ZDRWaTN4TGhTaFBjOHRDNG56Sm5FUG1mVXZsNHpkOTlrdWhhb2l4cUJDCnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlJtc3NDN29zdC9ySDFmNlhZa0EKVVNaMmhhUWsxclh4bkYvZEZYck9ORitPWDY5ZTVodE1FVEIrV2xqMi9NeXdOUzdrdDcyRWF1YXQyS2RkS01LQgpDc0trQmJKY2JVd1d1SS9lRmU1QWRpcUs0Z01OQTA3WGpPdTVvdTlOcmxVRTVGRTlsdkFsSUlyVERLYzlLclZ6CjNxcUtWNVk1bFRiS2pWUXNSVXYvNTJEdUxaa2YydmVueGF4UFJIMVVpa0FueC9XNy9IT1lvYzE1UXRvZTNMeXUKcGVtTG1sb1J6RXhpeXArdGMyOWdhakNwZ2lsbTBDM2tLQ2MwcXRlQTV0eGY2b1BmaGhQT1AvZ1gvVnBEZ2gyRAp2REk3NEF0WDREL0ltZkp2ZHFlTUdQZzFGR2l5R0pxcE1adzc4VlBTNldpZFAwQ3ZQdmJSTWcwT3ZqK01uS2l3ClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcE9WS1BRWE41d094QmZuWXdEV1EKdkpQWnNPckpLbDRCemR5aUtzRFFlTnZxTng4RnZPL0I4TGRZZnJnaDJTMmJPOHErRmYya0ErZUlFSVIrUURYcQpFYytvekRVaWt1Q0JrQndqL2xPTjJkN1lPZDZFb25WTG50UC9pTUZVM0RraUhXUnRSYUl1VC9hcWczVngxc1V1CmQ5Ky83bkI0R0FUSXU3SENneDlYYis2Qk1zVzdJM2hDQVFHaEw5ZUg1VHhyY3RKWGFvS0xCaUI4aWw0SUZWRVYKY0RPemp2R2pkVFVJQnZnc0FiNlljckMreWFmSndGY2cwNGNZeGNMZGhpdTQvYTBqWk5NLzJvWEFlMVpUUHROdQpySVZiTTNES2V0NDNtWWxpNmJSeUQwZTJ0T2xuaVA4OTR0dHdkblVyRnRQUVMrWlV2RmNwNVhoMzFVK2tDVlkyCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTRjNDI3S3Q4N0tDQkdQUlVFUmwKYXZHK0k1WWw2VkhNNis1RGhITllocmpyS0ZCMkF4Ui9lYlZXd1J0REJvT1hFeFJYSHFIZWNoT2NnelNSRlQ5RApEbFlNclFjTXZBTFFxRk43RFBIc2dpNkNXNjNTVE5zYmJmaEZCZHRta1RTcEd6ZHNwb0lkTTg0QzlQeCtJV2djCitia1ZONWY3MUwvMy8wQmVtVmhoaldOTVRwVVJEVXJwZ2IrcGwweGpvamliVmY2NWtBWXdYYm01dERIYUtUMEIKWHM5NUh3TXhQWW9jWXd5cXU5TlVRVTFmOC9YWXBaL09lVmFYbUlQMlhkTmlZeExWQ0EvUk04MXpMVmdNN2xUMQpGNm55QkQ0UTkvS3BMOFRYTXcxNWxmeFdOd1lTSzNuOVlWTzZOUm5Zcmo4NFBTQWdGRFlBTkNZM2FjUjB6dnBhCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDBKRmdJMUM5TWh6TXdIcmxvZjgKRkxrdFJ4RndrRGZobmV3d0NqeElXS2c3UEtoQmx6T2N6QTA1OFliS3g5akRCMno5dVVaSmQ3TUZ4Vkc0WGg4bApGK2szb2JXN3FGc1hNWmpSaUoySU1XS085Vk1kZTg4QngyN3laUVlTNnBXZnB6aUtsSWZKa1lqYUwyMHNOYVZHClNrakViQTZHUlNRU01mN1hXa29FV0VCZ0ZjVEw3Z0R2Vzlrb0RFZ3NielFIWVFjek1IYSt4Ykl0eUhkYW1IM2oKSUVxbUhIU1liMEx3WERBS201N0xpcG9ZK1lkNzA2blc3VjZmTHlJMDk4WDRwVm5hemlsdkVFV3JJZ2tJRUx6Vgp3U3Q3aWdNRCtxYi93MFJkMTQrZUdsendBQUxEN1pFYnF0QlBreGFYc2REZnliOVhja1pRMktJdFowOW5sMW1jCit3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1c0ZTMzWFI0VkdsTHRDZ2l4VkkKaldiTFpWeUxJTTFkRjNBQkVyaEc4Y0FQZnBmeGlEV3JTOGpJNVVkQnVFd3FYM1JCSmloTGJ1TWg0NG9kbnhTYwpYS052ZklwQ2trM3pOaVQ5ZmIxRTNrTHhBd2FHSGwzUlhtYWlLSFBXakIzb3Z6OU5tZ1B2RlRJQU95cHBqeXZzCkVadmduQkt5U0JSZkV3MUR6NzJvN3VYY1lsdVEydkt6WnFocUpEMFNQKzBYbE44ZU5SSXlXNGZYcS9RVlcyL2oKRHVVV2EwWDUxODQ4eVVKR05HUE8zbGhOeWZmNTdmYWd6eGdtQ2xSbStWVXVxNXlXSWVRbE9Yemg4N2NDODczagptaWpFZWZ1ajMxUTA4Z0tOV3pib0lRazdMNm5JTzh0L2JqZTdTenh4Y01IMmJhUTdWU05qNTVyRW1KanhuelB0Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmUzZ0Nxcmk5d1dTWC8zVTVSTjEKOTgyL0JOUEFBaFNCZ2FqR0xaOTBOelBiQVI1MEtZR1ViU0Q1UUI1bzBmRjF4dEZUUFV2enNsbXc4dFgwUTlSagpIOWFjUGo0bkNqV3ZSMU1GZXNnYUt5d3FHUkVuQ0g5amVONzMxalRkeFVHN0k3WGhWWkpCaFd2ZnVJRER0Z0VwCjBkOTVuV09Gb0FycVluVVFEV0p1S2lMUkJjdTBGblJnb1NJQ0pNbk0xNVI4ck5qMi9uaFBLZkRSUzJ2M3JWM1oKaGQ3VmNwT3lUR1B3ZWdlMXZJRzlVUWoybmV4NnFlL1RsaytsOURSTDB6Y1JJL002OTA2VWU0SXlSOUNRZEVIRwpyZUlnRTQxZmxkeGFsUWNudzQ3VWtkdFRhNC9rNmhmc1o2L0JnWFhUWThOaGROM09RK3Fxd2xhSkMvejdqbTJNCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNE1XckZjZ2lsZWRVR3lWaDduZ1AKOFRPa3RsaUdCQ0xoVXk4bkUrRGh5anhYK2h4anJsRlFuVElVRlpGVk5NRjNOQllpZ1owSXNCYVAzMlEzME1JMApUYnloamJMWVhFaXN2TXVCS3o2bVRCcDVMdEhkajlOY3graDI4MGFBMVNBQWF1aTRDeHU1TjNXcGJqM3F2bDJYClhvc3ZOd1B0d1djQzcxWS9aTWdVSEN6MVQrZElWRUNVNndWVEZJcGtYSUQ0VEZZNzNJRnlya0czZWd1MWFxTHEKNGEvaFhJVGN0VXo2WHl1U0hicGtTL3pmZmdFQ3I0bXpPZjRMTTZ4b2Z2cGZLMTFydHpSWHRKeTJpYkJJYXcrego2aVZjZE1NM1AzWXVXSXBsNmRxbFNFTzRYVWVEUVVXbnZ2YnZaQ2RuTWNoMzdUTTNxcXJ4Vk90enFURk02cHpwCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDVqRzF0YURlM05wZllFQmIvaUkKUGd5TjZZQ2kvL0JHRFpHMHdIczZmUmZYbnk5SEFTRjJML3hZUHhPSXNtYjR3Nk9rd2JKZVc5bEpRRytzOGk3bgoyT2M2Skh1Kzl5UWk3VDdJSDVLZXdNcUM2aXdhQXZ5WHNVQjNuTUJJUWk0cCtGNGdnSWdGSldPWHh3T0FsY1RMCjYrcFMvY3Jzc3ptWGd0SzR1UGx5OVF5dFphU3NKMWw1VGU4SHp1bEg5eDNCaS9EMkV4ellvOXRnTW9QQlRsZ3YKUStBczN2bG0wN080aHpNQzZtYUZPaDBnd3poTWVpSERVaStqdjR3dDFReVowUkxtL1VTNHhBdFJtUyswTmFNZQpEK0tNdk5Qa1VIL3JrZ1BRZUtzWm1peEJmdWJ6dTN2Z3dWU1pwTCtSY09yYWh2ZTYzZGZjV2RFZjNzektybjNLCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE5DeWlsN3VaKzBhQjBtNTBoSkoKbjN5Yk9GdDlJd1NoekpnU3lTYjFueWFxdkROdWZZSjlrRkNoTkZLOG54OEtBYWVIUjlZa29aYml1TDM5U0lGQgpBWUxPZWZhSjQzWG9rTFZZMTBJL2NLTW9aTDlmUUhIQVVwQVlISGhNZFRwelZEQU0zdFhjd3RITDFSWmNBcE15CmtDWDZ6OXBWVm1UUFBFeEQzUmZSYWFCbTBxbS9xY0U3U0NCVThoaFh2bGJDbFFDL1h6dHpGRG5xdTZuR0ZaSGsKbDNYT0pYcm5qMVZjdlVDWHpaTitSSm8veFVrd3RXYksxYzRKc0JOd3o1Lzl3M212U2l3QlZuUEVWdFBKSzNtdgpUZy90MkIxWVBGN2pRNWpoU1FiUGZHc2dzdjZxSE91TjZOOUJZR1JoamVhYkxzV0RYWTV0WU5xUDJ4T3pzZWZpCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeStWZHFCN2QrUUxlN2luUU55MFgKZ1dqSW16RHJ2Y1k5c0tMVlB3ME5ybWFvbmhLVmtvdEZnMDUvNXdLQWxIeWVndXhpcnQ3ZmVSOWhCWExsL3Z5YQpPc0JKci9EdlhiRTFWREFlVys0VHM0QnltYlZQbVhLV0VwQmtZTEhRN1FTSUdlTkFLSVVKcjZLRjF1UWRHK05zCjdQLzBvNGZnOXRGZ0Z4TWU3d2ZnNWxyRDh6K1ZpRE4wMUUzUWlyeEN6a01DTjJTbEdnSGtYMjh3SWI0L21LR1cKc1dnNTZ0NWJTamJWYVIwUGRJT05ncU9Ta3VzRmYwTTRTTlJQaVdQOWhtUGJzc0R3am51NjAyalNtU2FMRytsYQpKejQ2dzZUY2o0TSthbGhKSWs2U3ZXMFNReFZXRUVTZTRNTzFjVllnRkxHTllBSE45WExoM0VVb0QrSVU2b1A0CnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnJvN2dsYW9CdTI3cDVSMU5hOGkKTnRaVnJuV2NXUkkvL2JLOGg2OEU4TG9vb1d6Mk1GampMZUlTeEpFWjdZcEpWZEdXYWlKU3JpV1poN0F2OEh3ZgpwOHROSm1pbUJsRjNXcXUxSVJhWGRHT1E2UFNDSHM5WGhxb2xvbWNiMUVTN2ZYblVWcDFzL3JDZXFtR0wydFZXClpzaTI5T1ErSG9XdDR5L2huQXlUV05VQmdiU29ubml2VVhMVzI4Y2t5V1dQNUpHWlhvNWJPWnhERjVUNS9JcmwKbS9yUEkrdDJEbnpxVWtVRmNpV21INE9MVCtDSUt5YUx4YTVyS3l5ZjJJbXA0V3c0WFhvMTluQkhPSSttY2tqVwpqMkpuaUlaU0dIeDJXbzhsd1BrMmE4QlkyMjhGcFMrMlM3SGI2UFp0QUNTOFdsZXgwT2FISWkzeWNrOFE1SC95ClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDU4ZUZvTWs4WUZNV2lKVER4Nk0KUkhZZWdlbnJEMG0rV0NQRlA2SmtxLzljcGh0djlUc1cxK0VvVTVoVklsTWs2eDhwTEh1NmNHbThiSHJNdDVaUQpyQUlDdnF1OGl3aWRWSHYzeTY2QUEySEVZZk9xK0FiZHZ4Z0hUS1B6bVUzMXF2dW10bXIwV2Y1TnN6ZWxhbTUvCjdraURkV1dsa3RsVnRPU3plY0pDdVNGQUJqQmpUNWJvVXpMM0xJY1RwTXc0c3VuRU1WaVdnaEc4Q0U5ZHFNaEkKb2p4bTBlTW5jOU9jQ0FNeFVZbHNyWkJMSE1qZitVUi9PdnNNNTNLL2FhYURUdC8rMDErSGFJbExpSnBiUkdSWQpYcUhraDRwbi9xeTZzWHlIcmdmQzIvUU9OSVY0cjVETDlGRStGUWh3STlhNEhjTzR2azJGd2dLWmw3LzlwRDkrCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOE9FMU1vemRNbjZGaTJ3Um9aejUKTVluaEZ1WjdERGx5M1hoTTcxbXNNVHlVRlRiNFE3TFlSZFRLMzZUc2JtVTNJblN0YUpkNXRIcnBDWXNpOUFRaQpZMzdEQTNqNnovYy85bVJrWVlDNCtFdFk0TVQ0UXVWUzA0QVphZnZib0VHcmJsYXVLQ1BRaWRubWEySXBOaEh0CkFNOG01VitGVjFDTGY2MUZXdmZZbXFUWEx4aVhlbjkvLzdiR2tSQ2dCMk5hMHRTZ3pFcWl0OVh2cWFOKzNpSGkKUEVZZmI0Zm9rT0V0VUR1NHRnbjB1a3I1OWdCYkt3ZDNuVTFQK21nRWFHaGpYTWhMWHVyTVkwbmNMNGc1NjNueAp3emQzSGhneXdTbFQ3aTNQSDVQelpxbmlpYzV4aEp1UjJYOUJNYUVzSWZUVzNQdVpJZ0F5bmpqZGxhMFdGdjR3CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmhtRU5yTE5HOUU0MWs2NkZnVm0KQUY4VEsvdThTMXl3ekgwSmtqbnh4RmxET05kdVVta0F5bHBVTXR4YU45Rm1veXBhUEtpaFdUUUx2MTJVT2luRQp2YkdEVDh6VTFwem0rZklTQU1wVG8vTkYxK2duQTJvd3FKRHM1RVVJWElSTWhvNTlzRytnWkZCN2ZoaHZWL0Z5CkFjVis5TU10em01MzhvMnhBKzJNK3FKZXR0TU9Uc2MwYTU0QnFNMHJSSkY0cUxMQ2dOMG8wVDJydnp4S1FmZTcKcm5mSUgwaENSTEFYVmZzak1xU3oxendaYWdvU3N3eDhJZ2I1R1Vwclgxc2thL1YxRXRxZlp5dTdERmtxWHhmTQpobzQ1OVBaQ1BrWGFzb1ZJWXRCQkZpeW1CMjVLaDZld09BQS9KSXByWHdzc2srOEMyUWpwMm91ZUdPTDFzaXBDCjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0VlOGp5S3h3dXBBOFdGVjVidzAKNlNWc0IzSDVXT3U5c0VJZGorSGJOQ0pYYmNGU1Jmam5kTExQWmVlK2tkdjh6QXp6U2hPR2gyRmpDLytHbWlMcwpzT3JvZk5aKzhHdW85dktFSm8vbDR5ZWorR2NPeTRBbXZhREVZNGlwclluNnJ2TExPMmllN2liTkZ5WUo3Rnp1Cnd3dzV2UitpTDlLMUxSSG1WdEFOajh6bnZzS3J3aUhXWWhGMFB5V3NKKzRjTGZjem9DSUp0akRXRWZTaVZNc20KcXhnblVoVlVJSDJES0grbUN3akhWYWVaMFBudXMydzZLWCtaZmJOaG9RREViaStrRFNScGk5THhzYmIrczF1ZApMeU15ZGx6SHdISEx2ZHR6aW1VTDh6ZjllYzY5d1ZYL25aejFMcFYrUUF4SWRGOUgxZDE3MDY2THh1Uk9hbnlmCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0ZFSDBCckhidFdSK0pZeUgxUmQKRTVVQ204d0tmdU1QWEh2RVlWMjJhODFiRHVHZVRjWSt2OWhUemtjbGhtVEpNVENGYjU2Tys0dFFFZHl3N1dLdQowTWkyeUFkdHlSUUU5VXhleGkyTlJ5ZGhZYVVyQkdjMXRHSllGbnBTTkZSeXAxM1dhNU1hbXlKd2NleGZETFNUCk5aSTA5bUdsZ0ZHQTN5cElnYWNONHV0dG5Ic0tkMW5HczY4U0pZdWQ1ZHFUeGtsZkNUNU5RZi9qblJzSzVMdGoKaDJKYVUxZG95L0Nua0xYNmxsR3BUZGdCOE9zdnF5a0swSGV3MVdVaDJhRlIrTUc5SEorRHJzY2NiMDdjNVIxVgpKenBFc0xaVEtYWllmWkNqeWNRUkdKY0tabmdSTlhLQXhjdUh1TFVPenJ0d2kwYUVjOVVPY3ZjSWZOSXBIQ2hMCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGZUUVhTWWNKQkZFcGZNUVJxelkKT0lXeFhERlA1ck1oKzFwSjRlYVBaNkZpQXQrWERlU3hoS2lpQmFyZ2NDdVJ0WEpTckdDMUxzMkJsdFRkT3h2VAp3dnNOZzhjWjNINnJ6TWpvbGozYldaRDluU3AyaDVUUCtGeXFBZjNUY3VaT2VTOHUwM0ZtL1AvWGRUc1NrUzlECmRpbi9pbXNXeTNvYXkzNmltSVk1d1FVYXZuRnRxb2NoamR2RHRaMTdhM2NiQzhxSit5YVdKQ3lCNVBDUndJNm0KZVhzR3RhYnMvbGF1eDd1ekF0SkpvenNTU1hkbVhCL3k4VTZBbzBVOElQUzl1UDNsVTAxMFFNNlFhV1l2NUFuVAp0SWREaTRHbk5PUi9iR2YxempOdTcySG95RmkvVGFocisyZ3VWY2pESXhqVDZYMzJMdWZwMy9xSnByQVdtcndsCkx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG95S0s3eTZmMHVtOVJzVitKSHcKcHlVTjR6NncvUmZ0VVpQc1NTZ3JVOWpvRlNVTE5TdnB3RDUzRVZuM3pDQUFYSzg0UVk5WDI4NHU3OVFXWnJRQQpwNFBuY3loTEFsVzhkcVBCcktHNHpvWkdyckxJYjdESFNoTEdVUW1KRzluNmkwamJyUWc1YmExaXd2RUpFaWwxCi9ld2F0dTc3S2lJM1hBcUlmd3VXMWpwRjlVSUowTWEvSUhhQmZOVjBud2Z5dWdIb2NYUFh0dVNYRmNINFJQbnoKN1BZbldkS29Bb3JGOC91SjJXQ0RYRmRUd1U5Q0t3S3hGRks4aWRpV2ordjd4eld0L3gvVEhCcGtid05wTTBmMgo5WlFpcStRWjd6bmx1c2w4Sm5KaFRaaFlkbXlJRU1RendsOG0yVzdLUjlCRUVPek9od214dGhCN2xtYllBdldiCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHlwenA0SzZrVGZNODd5ODJmeE4KNVFKSjNKNkJkdnNWRDFoWWpIdkJuTzJMUklFOXkvaC9aMmJCQXJuSXMvUHhkT3A4QU9qZ0lUU3dUY3dxUU9Rcgp6TDAwczNOdnB5bHhHU0lDaytRdTBDWm9taDJHaldTTy9EK3ZqTVBCUFNFMU5KVFZiODJ4VFEwVXAzKytySTY3CkZQMGZIREtOdE0vTmc2Y1ByRWltbG9OYTlCZVdJYVZSclF4K01xek11Y3ZmenFmanpweUU2ZHBkVG1YdlZWNUMKTTR1dUtMZ2F2eFlFcnlURS9NTS8wNkdBK0JkTjFTVFdnc0ZTRkd0S05VYi9DZ08wZnhxd1FiMko2dnhnMHBJeQpGWnlkWTlwdzFDVUJYeFIrci9NdVE3K3BRK3RPUzhGckZwNTZ6RGE1V0I1WGFCSUwwMTVzQU82dVBkUHN1S0MyCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDhnaTBuSHNIZE1rY1ZBVm9XWEsKS2ZwNngzcUtma3JVa0FtcUdHU0JsUE44bDIwaE10dVVWUmJTaktaek1VZHNqdUFnR0FSZW5iT1lReGRNVWJtSwpZdlkweVhnK2ZDWU1IL0I1MVV5SzVxbjlXZlBOZEFnd1l1eFlrVjlYZm4yZ0RLWWJwaTlKYjJWWjJoVE1DSHFXCmdkTytlcGd5dE9xWmNTVk9oL3c5MzBXVTNOTVZINmJ1eHZVdWVvY3IzVHpobWhlMFNKRVg1dTY5TkRBN1hJQksKUmpYaHc0R3FFZ1BNN3BKeGJ3dkNrYmhGREtEdjZOMUVJYjRpZWV0bDljWHE5QmlmTFd3L2tRSHNQMHo5SmFHNwpWQ2w4aVlaak40amZ0NWhUSEVNWjNOS3pVV0g1Q29maHNIeWRObCtoUjRiY2Y2WHBPcWU5b3gwaEVFWCt6UDA0Cld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMnpQRFVRMHBlRVFJQUdGMTY0TWYKODduRjB4QlRhVCt2WWlPSy8yYTNFSkIvTVlzV1IzMDRuTy9reFllWUZ0YWpuUDlCSld6RVVaTUdSc0xLUXUzQQplUGhIa21jMko0WUZibDVENmVMeXhFWWxjWW9yajg1WU9uUHA0S0E4WG02L3NuTFY0b2llWFdrczhNTFZDdVMwCjAxRjJZV1F6RnViTDJCWDIxbzd3VG1OSm1ZQ0NMT0gvM1pSNlJDRWExMHVKR0xuY2sySVJsTEtnZ3ZyNlJaUXcKT3Z1OXlBbHV4c3JQd2pCZEJNLzA3c3pVU2RWRzBqTGxWL0FJRDFxOGc4SVQvZndBeXI2YlAwdmlxOUQvcXYwdgoxL3ZMR0tHOWEzNWQreXU1RDh0Y2djd2dpM0xlQ3p3bk1BK25OQWhSNFM3S1NkSmdndFJRN21mQ2FBaTMvMEtLCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1M2MlQvV0pxL2prSjJVVUZ0a0IKbXJwNEVUQVEvK3RqbUgrelRiSFZXWmNVZmFNeklEeUdxeVBsaWZnRGVtbEJFMnNXckcxaTFyK0FEblZUYWlpMgp5c25yaXlrT0dGWXZhdDFjM3E1UC9jbFVFY3dVaUdEcDZpaDZ3aUtsdmpFQ1F5SnpGVWM4Nml1MnYrZEY1d0daCjVVaXNqei9wa3I0akRLd3dkZ2w5WVRiOEVDZHdtaHBTcjdqQzNIdlQrR01yU044cnpQK3NTeTh5djNuNU5mZ2IKQ2c4OFQrNGNsd28vWVlrUmZaLy90V3RVaTRaSUF1eUc1blMvc2NUNW5tT0d1clViYThvSy81T3dOeUp5R2NLVApJZjBGMU1Ea2YvNmVoQ1Zab0Z1NGZRWXVla2t5clh5M3FKejBtRWNqelcrd3IzQjVQUzhCcXdDN1RQaTNiWU04CjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWxBUnorNG84cUJOT09mUHlZT2EKanZMSkhsMDQvOFBTLzBmVUd5NGY3VXA3eU9MdHUrdDJ2Z09vbmhYRmZoM2ZyZllSVTl3cnEzUWRKcWNVbEZLQQpISFd3MEVZamdWSG44aVpPbnVxYm8yMExGbDhIUWg3dk5Kd3BEL3l2NHhZRldKSlFWVkdidzhDKzRoWGtzOXdWClIyOFUzcmtrUlVZMStYM2lGUGE0RnhJKzdwVFVFdS9WQVhha0E2cVg5YjBFTFlWS1pQU2ZFK09GSmFnM3IzZzgKa01LQ0ttaDEwUElVd3FENy81UjA1NFJLcGN5aTdsQmg2VGtab2V3bHZ1bEFraStzT0tVRUlTUGFTM2RSTXZLbAo2WU41dHpjRGpkYVNKTHBvQWlYV1dUd1ZTOHlpMEdOYXluczE5OTlEZnF6UEsxNlZWUUxPM0wwRFhoSW9MNXEzCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVVGajBndjMyVDczT2RVZlRGMWsKeHY4am5WMktYc3ZMWDBOcmtad3VLMXFnbnpyT3J4a1lkQnVWWXBYTG4zb2p0R0ROamc3TXNRMExzWm1Rckl6cgpmR3Q2SE9QZDNpbGIwQlZSb3kvMUJ5Vko5NWZWaklqNXdmYWJydkszU2F5SHpLTGhNV2svK0RQWm80NDNiV01rCjR0M2psakRBU2xOMGllM29iM1pzZG9aUnpNY2NnbDdIUjlTT1dTczNqTlQ1d1l4d3dMRGpOdUROS3ZRQy9UN2QKa1hhMGV0NjM2Uzl1cDc1RnhTLzhITEVuczNyd3hvaHBOMThZb1ZzMEFMd3psN1Y3UG9xL014NHdPYTRxM1JkagpadkwwenlMV0J1bzhqSVJMR3d3ZFRZK3NkR3ROaXZPVUpjdzJFcUdDWllYdm1UczhFbWVPcVkwQ3RBTjBTZ0h4CjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1BmdVAycTZGM0FoTkpSaFFCaWsKa0FOSE82T2I0SzRzMUdnUlgxMlVBZklKK0NUY0oyaDkwdmg5WExVVmp6bnp1QmNOdXBnb2ZPL0RIMzROenJ1dQo5SDRYM1lxRGFHU08vOGNTZjlKWGdLTHREakZYeXRYVTBwN05QV2tPRE1yRUtjRGNUWjIyVVhaNUxvTFRoK3BhCkZBSkVaa3RuMkk3dUVhTFJ5NzNnRnlkckZCM2poc05pdkdDaUVQSEFLVW9yRW1NajBkRUk5MVRrWm03dGhUa28KT0pDM2FlU0loUXQrMHE1VmpQVjY2NlZhNjZHR3ZGWGNJSDh5Q05ueEZyMiticEFGSTB6UmZXcVBMR0hmSExLVQpydkwzUG5BUmlFSFlhbFlUaC9wckxObGkyb2VaS0t6ZE9rV3JabnFUdGhEZTM4Ym5vdzY3TWNpV1U3aTVNZlJYCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEx2UEdGbU56dEloanBtV21yV24KR3l1L01tNFplbkdwV2gyM25WQ0Q5MitjZDd4ZE1jTHA5NHhHVWZDOW1nWExLMWN4ZkNWZk91TXJxeEpxNHdILwpZVkI0TGVVYnZjUnBGMFRYbHBob0t4LzM5NWEyTXRyRGhiZlJwTHg2b0FNaWxNdDZGK1hUSStDckQxWXZsWjJqCnc2dmhveS9EMDl4Znc1d29tRmQwc0x2TlBrVGNTN0Z3ZDZuSHFWdXBrTXE4bWU4QUtxN0kwMExTUS8xMFk0eGUKYXB1TGtPUUpjVzZHWEVuTUVaNy9scXcxTFRzZXhtVnQ0akZzVElMejBib1VCUExmTFZXM1hHZDFWY1V0SWRrSwovWmVxcExFTE1lNWc0Sm1wWEpaWXFUOFFpRjRGTW0zQlVnY2ZTT0VnZFRVUzFXWkUxNWF4SHJLL3dBN1o2UklwCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXhQS3lab1pIUTczK2NwNEhJbzEKMncyM00zbmRHQUNtZFRwRktjTG1SQmFKSlFuME5RT0s5WlJtdzFyTHJPU2N1MHVKVjRIdjNZbzZDMGRpWDVMTgpOR2QwLzFWeDVGT3FqckNuOHNNRG5OKy9qL2plNDdjRDNaTSs0WEhLQjlYSVV0Nkx2eWcxeVg3VFJBckxDMEtvCmN6djBhSzgvSDQzTHpqSXhBUXl0ZW1rOUMvblVnYi9aS0ZVSDZWd3pWWE8rdS9namVmWDVwNWFFYXpvRzBzOCsKM0ZkazNEK0w3U3l1ZWV3cEVFN1NlbFE2THZQSUFTcXk0c2NhV250Q01WZjNoNHZVenBRRHJmRHNrVGY3ZnpOWQp6bjNJeGRWV0o4MmNWSmUrNC9WclhiM1dMdnliY2xWTHUzNWlybWd4UFBtMGd6VTNDYUxONHE2UXVIYld5ZUpDCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTlWWEZSbndRSG9WM1V1SGZhYWEKSFJOa040V1lVZG1Xa2hZRGNHdUVOYXVxNmNSUWtLbk5nL0pjb3JYQmxpc0NQMzhYNTZPaGIxYnNMK0ZzOFNzaApOckYxTTVwTjlta1BUOEFVa0dxbXZSaXFLOWF3YVNwc1YrR1pFV1djZ3lubzhacGpNTGJYN2dDSWdqcEZ2ZWNNCmYxaVJnMHMyT25Ga3VIclJ2STVRcklBTkZNeWJEejBQWkhOWWVNQVhVaFl3NFBFcDVKcDBnSHlMRHJiZGtwNHIKcCtBdVN5eFczL2ZPeHZNR1NteW0xclJpNm9rNGJZOFMvMmFycWdpaUtZNUVxdG9pQ3h5NENpSTlNOFRsblI0VwpPOHBxYVJ5S280dHMvN1ZPVlhhajduRGhWeDY2VWdaUDBhbGw2enVuY0hEZTY4ZXA1RVVUM2lMRDI4N1lodW1kCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkxScGpiNER3bUxlVmhnMVZTMy8KOVZFam5lRW90L2VSZzUvVnRSMVpaV3JDS013dDd2K0x4V3BCeXhqeWxqUDdDd3NEMlNtV2pHU2xiSEVlcXJUTQpNcmRTY2lwNThkSlhKUlJLR0t3SjdUYmpVL0NiVDhydDEzZHR2SC9OYTRFTDdxb0daVHFwNHkvdFVHQmxyemlPCk91bUNBQlZRblBMZ0dISzlsQUVjTWJkYzVhNVBsd0NyUHI1Szl6bmRCU1FqSVRUUnBubTRSQnhiQ1EvVEdzZ0IKWDZydit0U25nQUZteEN5SzFqaSszSkFON0ZtS0VpQVh4bENiNjE4UkNybXdadzBzZE5KRTUvWUVNZEExalM1VApZVHp6V1JYNkxRa3Mwb1NreVc1VHN1QzIyVHFDdElNbHBkaXU4alB3dThhOURKOUJXRjNIbzFiQi8wSGJ6UkNhCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNDVjd2grNlpoZDVzVWVEMmhsRVUKWjRKRzdFOEFpNEttWXluZWpQQlJ6cXNrTUxjSG5vUkMydFFrSEhIdnlDNWFvUWdNR1NZWGVZeU5nUVFlcW40Tgp5ODJUbkZNVDljQ3QrZEVqMzJPeUs1b0VzSGNxeGozUlhhTjF4aUVKd2pMeThhV1B3NWlvMUkxYUtWR1FKblpQCmwwYis5YmNMQmYxczVSYm5zVnRtTUFCWndjancvTnQrZStYaXdxcTJaOS83UjA4M0JtV3VaakFhdFhJb09Xd2UKN1YxNG4yenZrOTZ2RElha2lLdnI4MzVCQ1kwTm9RZnRKSklPUG5pZE0yNXlGOHZOTWh4L2ZQeVpjdWtMS2xaYQpYNUhaQzhWUmNGM2s1QWVucG5TSnZNWS9UMTNIYmJxSitkbWxxZWYyQ0s3UTRqeDVxenRZQURORUEvVHZwSExCCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMndMTS8vbVhqSE12Z2pzNDJseVkKckRxeUpxTUZFeFRYNHhDTUM1bWtpOEJwaHFmNGtLWkJHR2d5V0NPMHVETlJ6WHdPOWZsQ25YQ2h1d0ZkaVRZNwphYVVsVHRjUGVMM2k0V1NwRlpvaXY1ZllsQ040dmUwczNoYVBqYjJtSGtORXRyZyt4Y2RRNGJRS1ZrZGhrdEFHCnBQditjSzBaRkpNZElFanoxbEpjU3JpUkpYS09kV1pSRVNuWCtSS2k5WTB6SjZpL2dVa0VHRGM3MlJUM05adWsKUElRRlBlZXVNcU5LbjlFUUp4K2h4d3B1VUxiSEpsVjlvVURGN0hZNDU1UFVaSjFMV01RNjgxVW5zQnFpSjgzVwo5R242WUxmb0c3d3g2ekxwNDhpdG9UUEJNT1M5dEdrTllSZDJ1dm1WYVJwTGpHT0JMdy9ibkZCOG1lZFdydGVsClVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc05tTGxaTUJQZENZVmVQbUNKY0IKR1NNOE4wcGxvakk1dUJYVUV4MmJrL251ek1ZNldXNnUwbElnVk5PTXZyTlZacFI5bnBKalpCcDF2U0tFRVNPMQpRakdCaXFzZWZXbFQwbGk3QnFOYzNZM29ieEdIcTZ4QmF0cnorSXFoejVUR2pQWERuenVvR2VlTUoxMm1zWlpSCjNNR3kzZGRRTzdSUFdHSzhyUmtVZkZmV0t3K2FHajZKTUdicFdWY1E5blFrZkFHelJwNUxFQkFHMFNtUjJGa00KSnMyZVZtOXFVSU9hUVByS0JrWWNEaEwzejZpMThEajgvdCt0Q0lVU0VSZGJXYVI5anpQYlByTXc1UGtEaDUyRAozdDhINVNaaWRaRnBlM3dZOHFMWGdvY1RGbnMrWkRMb0Y1R3ozdzJqSXUrdVFmOEpRYXhDSXF3cTRXMmJTWXR5CnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1JPVnYzcVZUR1JZbyt0ZVl3M0sKN0VaZE9rZVpaYzdJczNzdjJoVFRUSWpwMElZNjREU3NVYVFnc1E4NzJ0YUdnS1lSc0JOZFRMNk9ENmZuVXVyRwo1ZXlwVWpZYnBjdTZTdVZtaHRxWFlFaE1lOERIL2lyRW44dWt6YVhXczJ3YWRzeWN5b1NORFhrSjhlVnpZUXFIClIyVVlxMmdzVjJUMHNnVlI1MU5KL3hvYkFPOHNqdkVnQkVRUkg0eVRyT0RZUFJEdjBFUFpLRDlhb3ZHaUN1K2kKaEUyTkpLTGlwTjlodlhCa1NmSnZ4bXIwdThZOHpqNmhyT0lCaTZlRVBxRGhYalRQM2FIU0g0WWhybG0weG5qOApBQWZ0YWFUcDc0cGhzNEFHL0J6Z1RXa3ZUc1M3T1hUL0lRZ2JMUmMzTDN4OEwxckpMV3ZKd3J2MFNOSWVHVnE3CmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEt2RUpBZjF4T2dmWFlBU2xJU2EKN08xQTh4VExCUzVLdStBOVZ4cGozbGdEaGROSG9adFRaMENUclYxd1NRZUwyZlFjQmRnVXNObHo3OHpoUDhudwpjNENIRGZ0MWpmbDlnSExLY2Q0THlZcDFNQVlMTGhNdmRuOXFtUFJ2czdLbUU4QTVnTlpvV1p1d3pQY1ZQVTZWClpCMVBCaGlKNG1pNTQ1ckJuSExQVFJ0ODJhT2hxdGJsV2dPQ0hPQUNLVG1lcm5sSTRBaDNuOVBiOU5EeExKRngKVGVLdVI0aDJRb05mdWR6dUZLVy90dlJnL3AxL2ovRGhPLzdSNkw4UmRTdUhpdTRZTlFhMXZFVzRwa200Nk4yeApabjNCK0lNL01zdTNpc1RXY3FRVFB1NHNCeUdVVG4wZXI2Slpza25jNXhnTjhGWTBPZnh1Q2dGRUJrRjR5OGhKCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejQ0YmdST3VYaXBqUitaV3J4VWMKY1VUVTRtWFAxNFIrM2RBWGQ5b3RHU2trWEpoRkxJZXd5NjZ6NHRCNUgrOEhrVTFXbC9IQi9tQ3RDdlFRWlF5NAo0VmhhckYyRWFmcEgrMFltYk5IRVhSTUpaVEtwOUhsL2hCU3VQSWZxK3lYeGJYb3lFOUJNektSaW9tWkJHdzhMCktMMVp0Y2F3bmIxTUF6Uy9waEN4OGFnT2htOWlzRVl6bGNHcXV2YTRnZGpIWnZWMVdjS1h3bENvdGZ4RmtJajYKanN2cFE2bDdTenVoRkNrM2xGOUdWRmNIZVVaYmFXeTdHQVJrRHRMbGtuZno5THFUeFYzaVBBS1Z3OWRPV2N5Mgp6Qm9mbHVKbmIyNi9DamlxRm1oM1pqTitWK1RBQkNoNXhkeVY3TXRCQ1lpMFpabTI4bnVKcElVV2JLNmc5QkVuCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0c1eitxc3BXd2tMdHVnU2szRnAKZS9ScC9kazZEVFBUTzJsckFUV0svR3VkRHJ4M1ZxYXhqL1BnT2txY0pGa1U4Y3Z1N2FKS283Z2s1QWxUY1lEeQpBckNRcEtoaTFRNDBRZzJNQ0NoMGdtN3BBc3VyZWQ3N1haaklHL2FReElQa25wblJYbEdWZFRXMnlNZ3prSGNzCk05Q2ZqQ0hwdEg4ZDcxTkxVditXUUNKR2tUWDR6WEJhSDhjV2lmcHhhWVlXMmFIeUFMTFMvaFhiVmpsNFovNWUKL1QyZVlqNHdVaEViTjhaMXlnc1p0b2JockFTVmVUUXpsMm5BdWx2bmZOeVp4US9XNWVrNVZSc05QUFUzbGlGYQpQVUFuRHlwUStPUlV1b01Oa2R5M2dkRVJYb05NK1ZJRnN6dmlJaDJiWUNHNm9aVWlMV1piUkswNWxVbFZXWHhwCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekEza3N1bnNLVU1LV2hjODlMUXYKU3F0TDRxUkEwb2s2Y21LVWlCVW9SZHdtZlVZZGtMdGt3b2R3UHh3b1U4UDdxSzRtWk9CLzNXZ3lGVFE2b3N0ZgozUVIvcVRpditKUjFZWHltdXFZWUhBYmM5bzRaQy9Ualk1YUFNWmdwcmlzbHZvYy9Udlo2dWJFWitROWJ2UkZSCjdadGpJbWpsSkFEZm9wRnFJckhhNjEzVElXSnhvdDZVZFIzY1AvY0UyeHQrRmJMU3V3bDZkcCtoMkJKRjNFT3gKdnJTaWd6Sm53VkVhc1lST1ZOMnJ2c1hkYk1nMmRrdm1GcWh4cFM1eFN6QUhEVnROZ3BNc3pnTVlMVVlOQ2RBUwoxd2hFdTQ0RDRLQXFHSzNxUmNIbEdZY3B2YmYyVjZIcWFYeTg3eDc5REJ1SVp2UGNKVUpzRzFpeHY4LzExSUQ4CnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHIydCt0eXhNS25EeGJTNkNkdDUKd0Y2Y1puS255YjFpdjdaeG1KbnBWRlkyWHFxQjFRbHduaEtXcE1oTk9jZVZpdmZNRmdMS3ZtbVplMWxFTTBZYQpUQ2lVdURCVDRHNzdLYW9Xc1hMbDgwWVpHUW5SVEpzYUUwb2hNajJ5V2pjSnUwNXMwK2w0Z0hNNzhhMkVGcDZOCkZPWUd0czE3VjhmVExLczZQbGFtbmdqMXoxWjlFanlsK3VIYndGWk5rNC8zTUZaUnpFUzFOOWMrRFdTa2xoQ2YKeVN0ZG9hQUZUanJpalVwbDhKYWw4SU1seXZmNGE2WDVGaHlGVFhENE4xdWExUUlQNDhLYmNCSHBNcE5rcG5IbgpTcmNYTFdObndRMXZSYWJmSTVlbG5wcUVLc0tEcGZsSjJrNEt6ekErODcwZnlWZWhJK2NISUI2MXBaZ2Vxa2hqCjNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTZYQmFvYkdhQTllbGFhQk91aXYKc1ZzMWc1MnB6eXNIRGsrbjhjWDdZOGQvMGhMRGlwdWh1L3VMMW56alhKWWRnQ0Rxc1ZmTXIrSUpLa05UUFRGWgpKbVVwOU16N3dKL1Q4ajVhYmxzVnZDUU10b1poWTVtWWRIbDdodXE3YkhGR21ybnZza2dSSHI2enZScXVUa1BLCmlMNUY4OVY4dGV3a0ZkR0xzdk51a2xYSmhpeVRRU1RvYWkyS1BCV2NuQXBCNGR5OFZ0MTB6c2NJT2tFb21ZTTkKakFwemQrZzU2RTFQZlIyZjhmWEV3UGRtSDJQVC9ock9BZFR2dmtyeTdKMm5Yem9iNy90UUJIdHVRdWFFVnFiQgpJMGVud2VVZDBOdERiQ3pDTEptRUg1dkRPc0c4YlJJVSs1UXg2UFZDd0l2S3ZyLzFGTEpnTzljVzZiVENZK1VyCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzhoZmJaTEJ1YzFpczRsVUErbUEKUVg2WUJZNFVLQmd4UythUStxaXFvWDM5aU1HOG1sdTVwZUZndkdBNVBDVUpxYkdhSmNST0p3Q25ab252WjRJdgpmRlRUNXRvQ3Y5SFJtbFRWRUQ3bVJNWk96YS82dS90QU01TFZ0Q3hwR3BFN2dqWEs0UzZ3UmJNanpCUHdMaU5PCk1tRk91UVpiUFFEa3VIQlNzZUU2RVBBYXloeUNJNldKa1hJZlhZQnpiOXdhYVZTYTlyam40ckpKS00wdCt5c3oKbjVJSHRQK3VtN05TYjVPektET0FHb2pFZCtxRkhpSVh3eHRoM1JZZmRNbUo4S0gySFVqbnh3R1NYNm4zOElMQQp1YUNJSERUempUcjV1QXY0NlFpL09zdlR5TGdnTTZROGdvVXJGek5oZGZ5TXFNc0JvWkhnWUpId2ZhM0tsQkk4CitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkZpSnJoMUdyTTFQU2toTGI1am0KSmROeVdlSWtmM3MrZk9pRmZQMkZzTVNuY0pWT1J4Q2JmU1IrUTU2UVIyT21UTGlQTmExb0JleWwvQkl4WHhUaAo4SCtlc2tqd0xKRDlpM3dVQmRMS3Z0RWFUMGNEbFBsQjRGWUwxVEh4RHh2U0VNY2IzLzQ2Z2lJTm5iWVN5eEJyClVVRXk5Y3JtdjlCSGQ5QVk3b3Fib0RqUG1CZ2c4ZWNXZGN1ek9rNG1lMEl6SGwrZkVETU9Ua2J5YTk4QzQvMU0KWERMb05TdUNKZXA4Wm8yQnhvbkd4dmNFdDNtMEhQbDRGamdhWWVGbFlMZlEvZ2FRR3VtQWVmQmJoTFFCWSt5awpWaUxhWWUvOUkzM0M2eG8wQlUrWGNSMFR1ckd1UFR6OFNmbU5lZTdNakxDSEhZalRHMFB0N3Jrek9ienZLOGhxCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGVodTZtYlNoZzZDbDJQQVM0VWIKczRHMUxpUmtNbDljYmRCVWhRZVhmd2MwS2tEZCt4R3AzNXVveHJoVlducjczSm5vbjNvVTkxUGNnWHhVYy85SgpIaGcwZXJEdnJFVXZtTVhIVElNSjJubjAxY1N0RVRBdU5vV0RaT0NSK0NxcDdKNVd0aitUQUZ5eHdLbjVYYjM1CkZ6b3BJU20yMVU0U0VjbXJnaCszR1Q5dHE1eDBJZkJIVFkzVHppbVpSYlJuTWtVcFhNMTh5cFhyaXgyaXREbXcKUWdtWUNZOGpLRzUxTFNESnFZQVJrNm91SS9qZW1pV3p1UWVFVytiOUo4alh1cEtJYjJMTUNuNXg2YkVEaUdSawpVS0hhUE1RTGk4SEVOOXh6RXF3RWl6bmxqQ0c4WE9KYVNYMzZtOWx6WTVScTNxYmt5Yk9nOVFhbGQ4YXYydUdiCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVV2b0xPWWU5a0lSWVpldDhaNEkKWEs0R2pIWGlZQWoyRzAwbk8zM0lsTFN6VWlndUtxOEhOamRYeFBucHVsOU1CYXV5N2pORml4YnArN1F6S1RaRgpuZk5qYnRvMGtoMzQvcXIvZG9BeTZXbmxwdmhDUy9GNjRkQ09sOVI1eUNWZldlY3d6MTNyeVAzK1pkN1p6V013Cm1nNmRFRTV2OGhibnlTRnQ5Yng5VG9PTFF3YUxJSlhHTDZ5eWRvUllWWUJvUWZ3SWVPYytuVTk4cWhkWnRxMEwKZmo4cU0yeUlRQkdGNGNTUk52RTBmT2kyczI3UU4wWnJzME5XQWl4WDd0SVA3OHFMWjVseWZtLytic0FJRm5vRwpGbHFudzVzWC9oTFZadm11cVFxU1BuZkZBbEJkSWUyT24xMnhxNDBpRG43bW9HdkNPQUE5NUI5NXNtZm9aa08vClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3lXcEZZWEpxSStzRlliZWY1MTUKdDdvRjhhZ24yeGR2bFBjRkpFTGJMK0x3aTNveGFhTmtoeTlFS0hQWCtJOXlVYW9MajRFMHh0SjRISVRTdCs2ZAo0SndQNHFEWTY0ZnFSbThiK09yMFNCeU4veDFXamhyZU1zVDdHNjBZcGh3N042emdtZ2RuWUhvSHZmVTcvWGR3CnJ5YzQyVlRETHM1dTB3TUlqM1BKaXJuR3ROMnJXS1Fud05tN1pvNkY2SGU3TUU3Wk41Wk1WTlh4R2hDRE40K1EKTDc3ZEozTnhQa3dGcXRjSGRnQmNpSit3L3NlVDc2ME9acUlTN2d6d1I3eVlwaXFPRWJGS3VodVlZcXFlS0RJYgp1ZWhkaHVEcUVSay9EeDdBSUlvditSSzJ1UExqY01FcWduMEZMMCtQVmJaek5MZ2dkZCsvMkdLWjMxZk1GSWI1Cmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMjFsVHpEY2I1Y2NwSWVFVjNwYysKQWpyRFpPbGpxWGUvMlhjOEpiNUVnQ1l3eXZYU1NtQWZrdW42eERxb2hxZkgrSnMvNWF0S2ZQNCtTVWZ2MENPMgozYzl1MGtIeTFPVTJuWTJtcDN5ZGdFUmM1K1JQb1YzQlBmMmhyTERmWnZTQ1ZvRFJGRUM3ckRrMXdieDIxYkdTCjEvOXdUajJtQ2lJcTV6UkRGZWREN2l6N2tMeEtCUlcvR09QYWczb0JBd0o0ejBiREhhaXZuUlFDZXhZSnlZYU8KeEJHdlUzMDl3UWhtY253N2EzQTNQOXd5dmp5cW9Jak1SVUZWWUdOcVlYbVlMUVl0SFQrVjBPbm5pR3R6MUl4bgo2dEVWQTNHU0hlQ1lpaldWWG5GMFJRSnZHOSt6dG16Zjlsd0ozWjFaeW5kd1gyWDFlKzY5R3ZHVTJ2Q0lhSlJWCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEMyMjRMelZhdXc4ek1WYmw4Z24KODg4NVp6dVdsSEY3cEduSm9RbVp6N01pa0pEaW9HamUyRXZKOWo4Q0QxMVhvWlpHUkpEUk9WSGtYMkx2ODBlWQpLNm5BSW5XaEc4bTQvd1Ewd0V6NXFrZXd3Z0RNbCtmbHErTS9IWEFvNE0ra0J5SWdtY2Njak51UzM4a0ZSRHRtCnVkd3Z0dE15WVN6TmFUSUFOQmFOS3hOelNWZlFWUGkzMVo1K2ZvQURYZ2xlWHRoSFYzVUNqc3JEOS94SWVKaVEKeHRDd1VkY2ZXak9EZGF3MFRPS2Y5bUpGL1hyL2F5QlE1SkU5ZDA5N09VTHNjTDRKUlRNSElXak13VTE4YjJmZwpFcWM4c1JMRHVmY2tRMFNWT081RHhSaGo1M3lxS3BGcVZacUYxZFExbjdNVitYc0FNSVpRdUJyTGtmOE45dll5CnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1hWYXdmWTJnU24yMzhXbUplUDIKUHlyVDJnbld0cGkrdXVpb3RmdUo0TWtiRTBYWXJxVVRpUUdtb0dhdzNONnVZUFdEU0puc3lyRDcrVHBhTzR2cwp4cGpwTHlhLzNvb1I4THdrQlBEVzltZ25vNDNVTk11UHF1Qm5mZE5vSWJvM0cwVk9OM0k5TnlXaWh0N09GS3NmCnAyQ0Q0Y1kza0VvNTlkSHp2WGtvVnh6N01paEphTUhtU3VGcjBXTUFURjZjajlnVC9oZU9ZYy9EYVZwdEttSUsKTG0wcExwUmxIY1VQWGJtcGEvN0c4dk54TVdaY2UxSzU3Um0rT3pIMStTMWFNaHM5ekR2Q1FDS2dreDZhZ1ZQRQpzc2ZZelh0b1UwSXRIV2tqZzAzZlU5L2hRLzlFeGJNT2pDcm9mdXZVbXBUd2hyWEpaM1BqcW9NZkpVNzJqajgyClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFRjRC9sbWFCYmhveGFaUjQ3OXgKcnZiWUMxK0p1VldCMmlKY0hwVENCUnpNUk4xcjRoVWgrVlpDTUVLK0p2dzBGZGNCUWVRcjdxUkt2WVl3bkZrZApuMUVadXhqY1hFNkZ5VGJya2dhaVovZ0tkL09EY2hnTzFDZXFYd0N2Q0FCcVd1N1YyZHhSNEJnY0RhQWNYSjR6CmZodXNlbGJjTzJSZkFMK3ZzOVZtU1ppSUxFc09pdjVrQTdlanBoY2w1Tjk1eVRNczRWaTN4dVNGbFJxcHJXZnQKK2JORE94eG1rZHVNM2lmYjhpanFySXVFTEhaclRuZUJyTThPRDJsbXZwYW15WlptaTB6UFpEUDN3ZC9uZnNUVAo3Z2VkN1ZkcXJ3QkFTVHZtbnFKVXN0UkMrZkxQcU0wQnRLMWhqcFlrbVVmV0tYUHF0VVJJaXdLamZBMGNiRWRxClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUM1UW9XZzJsRjFlS1pJTllnVDQKSEhJbG1wYURqQ0xSOFBMREtJbHhaQjFPS1pwRG5YRzhwWk82bDNmL3pBOVE3YXB5V1U5a2p4SkVZVUtlai9negpYK2NqT3FBbDZ2N1dFeWt0S0lQRnJ1VHJiQkZiMDZPSklPc04xNnRyZjlKcGk5N2tFczJnaHFsU3Fnbnh4YXhpCmxDRC9XZ00zQktvN0lrakhpRTBNMG1QbXM5ODR4WWxuSndJMmc2d3Q1SkhHNnFxWmZSMEQzUjF2eFRhQ2t1NlUKRzh3c1lOdFEwTEgxL0VodVQ5VnFLYTMyZFp4bzAzaUhxeHhJY1BuMjBTMFlmWEJGREdmWFB6bGZtb2dZVVRLNQorS0RodzRPNmY5dDcweUpwSVg5U3E1Q0xzVmpsWCs1L1c0YWtUeURPZEV1TGFpVkJrT1VWMy9SNHlSMThhZWdxCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXlIdWIrTC9qa0NNcG8wcGVyWHoKc2YrOG5wUGw1dGRNL1FTWk1Xc1FWcnp0NnlQcFdrai9OakRBQ0t4SjQ0dkN0YWF1M3F6ckJjWGxLa0lOK1FyaApkUWUyZ3dRNGNrYUdGMGtrQXl3M0pVbzNITFFYeDBNNSt3d0ZwSmwzdjREY0xadTFUYktvaXVWU25vYTZwcjRaCk40a1ZPa3NUQk0vV01xVWJqeEFOeWRvSVhqQlZ6OUZpbnBqNkdjMjNhUGg5TlVxUUY4cVJPbkJkbW5TQ25QUWoKakJzTkc0bjFCT3JFcjZPc2pwSGFNUStHWCthOXlpRy9USDlCL1ZaM0lyVGZQekV5NURUUnk1dTRhaWRPaGpCLwpadCtjT01CV3drU3VrL2RxSzVFcXNSdmVPdVVIcTNGWVBrV1lCdG82NnFyeTg5RWFjY05PMnhyclVmSHVacnJQClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXB5N1Y5Tlh1NmI4K0srWTZnTVgKbEJicmtrc1N0cDVna3hGVFBLTnRPTUUxNUJQb0tJRkkyUVdkM3Z4SG9Yby84NzRBUlNIL3RzRFJLbXZTWVd3UgplZEhmdnFuUmJIWU4xc3lmMHhVbTRWblNUVkFBOWRqdDJ4c0xZQ01xS2dzeVBLWUhtVU1nb3p3T1JDYXptRHhTClVHSUVuQk05SVR1blBsa1dTQWRlM3RocVdicmlyZ3AzNWZaZDdob1gyNUtGaTRKbWhDbjhZcFBYWklLOWxIV00KOUxpVkFRc0JiVW9jNDBaLzJjQjFMZ0tOd3dibUMzczFIeUFRTExNSGVVeG93aG54OHY5VmlEZ1JOcXBqQkkzTApxVndBQU9pVkJmSExGZkoycUhTc3NiQXptRzhRaWtHdFM5ZHJGNDk3cjlOZFpqWFhQZk1GTnBVSEY5RnBVMXEzCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbi9TMjlKNTh3K2tGK2FMNlF3WjgKdGJiK0ZlNThvenQvT1d3RUNXMmdnR1JpNDlaYVJIWXZDV2hXelNEbFg0VzVGdFRaZFhPUE4rTGkrZExheHQ5NQpPdHRyWGhjWmpPanJVM3FrVk1pckcyTzRYUkFoOG91MS9jcnZ1RWZjb1Z1ektqekI5eVEwZVlJcC91M3Q3Qk1qCnNrbkx4d0JZbkhERFhibEhydEZIejJqSUgyZm5TakVVYWo4ZXhSWGZqOEF2WU9KQjdjSlB0SnZrb20yODljYWkKVzdWYXpMNzZkZk5JdTlvaU1HNnE5eDFVQWRIZUdZNnRaa1o4R3B1bHR1QVI5bkNpbGxXcTF5ekY2TGxIN3c4eQp2cTQya0Z3dDlZbzU3WjFremxvdTFkVjc2ZmRlS29zYWFMQXl5YTBzZWpLOGVNR1luY09YbW1reVZRclRtUUZlCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjVZclZvRFpYL1pha21peEk5ZlMKS2FaL1FsMzdIVkVRb05GcW5LSFBnYnovMTRkSVJyM2QrcU14WXhQRTB1eDB6eHJwMDJBVk5rTTJ5d1p1b2kwMwpFUE1EaVM4SGpiZ29VanBPNVJPUTNJemxseGFiN2tSNlRUbCtWTGlKQmkrZVVIcWVidTdlVFk5M1hCNGd0anN2Ck5EaHN5ZmdaZjBxS016dkQrZXhHUjgvV2s0dEdhbnErb2JxMlUyRUlkalI4cG81eXV6cFZrZjlaTjdyczhvMzYKLzI1WlJmM2sxMW00R2tFS2IyL0ZYdit5Rm9wbFFkVlZmWEhxejgrSnRoZTAySGNRUm5NWm1vQjZGMDkvVTNMRAo4VGNQUXJXa2VadzgxdzRyTHBTV3p0aEgwVDV6ODVCN3VnK3hzTVZIM3VNREtKWVpEbDRITkhheWQ2MVQ2UWZuCm93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdithT1JUUjRMWWZWTWlZL1J6ZTAKd0Rzek1sOHRXbGRHcXdLakU4SHJBK2pDRlZ5S1JUMndrTHFCYld5amwwZFNWQVBGVWFNZkY0c3c3Sk1ubUV1YgpHZG1GdW93VjVnQXZFbWRkeE8xODRtbzk5aGxlNG5MNWRyczBRYXVuMnBNYnE4UThLMHJHdmh5TENkRFpxMU5PClVzd2FmcDF1Q2t3WVAvd284RkM2TDdlMUJMbkJKRnk5bFFqSmt2UnlaK093YjZOSkxZMFRoYlpQYlkyYnNwZGQKWWoyNWdHTGNhUEZiSTNoK3JoODg3bVdDMDh5MTBleEdNZVV6Y2ZkdHRzRzNEdG5vME15SHlaQ3E1bDI5SktQOQpMUSs1b0g2d1IyZXRPY3VZczIrMU5kM2pmVmRpcVJHeXZHVFBSU0pCd2hQMmVNTTM2Mm9sc2lqblhJSDZyNG5oCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkJTTTBtRndNRVpZVVBlWWlwWFYKWTM2S3REVWpmT3pyWHRoR3JZb3MzaEIrVk9xTVRWN3RkTXJ3dGpDT1g1b3IyM1dTV3p5cXUrTUJzZ2F2L2tGRAp5VnBxaDNpKzdLVzVpM1lpYzhHN3UvaWhNRFVub21HSkk1TXAwYXhXZ3E2L1crcjdTaURjdHlzVFFkV2U4RlM1ClpnRHdoTXdiWS90V29LdG5aZVY1L25VUSt3Y1RqVlUrczgzdFp5NUlrVFc4Vjl0N0tLLzFJK1BxR1g3dTVBeWQKQ29vZHlHeXVlcy93U1daeFdvbDY0b2VtSERpR2JYd2FPS2FmYWdaNTVYUm95dHBrUUl1b2hZRWpsN1NaTEU4OQo5V0p5dEdLZWx4eUZ6Nk1yYlFVYTZwTFY5TVBkQUZTTzhDUlVPaHBQSGxaenh5M3NKMTMxUnJGclllTWlneFZxCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdS9NWW54djZRdnVTeGxlanBLa1gKT0V3ODNoNkNMd3JTNjU2SjVLa3RqSmg0bDlGUEdENW5GdUxxa0hnbE4vZEo2T0R1RktrZHh6bnYybjVmMHl5QQpGMWxXSSt3aXFDTzBEeUhWZWJhUTlPdWI1MnhRU0Jmci8rTHBnQnh0VlkxeVQ0b2FOZ3ZrcHNxc3VnTC9kWU5vCmM4YXhoYmJjZ1lXUnU4eExpanZGcTFzRzg1d0Y4L2hnbHB4K1FDeGV3dkdHQ0dTZ1JBNWt2VFFzZXlDZjlEZ0MKc1RoTC9oSU5Bekx5TnpEaGRTcVhrcURDcWFGMXJ1RmtVYlFFdDcwYWhkT09xY0ZyQTRlNU45UXVXNk5wZkhjeQpUakdNbUoyWi9ta1ZPN04vRXJjVEFDcHBwNnVmcW1UdFN3K0t3cHh2a1M3T29saVhLRk9kZDRaYXNLVkNPUzdmCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb25IV2JtekRwRWhLTVNCOG9PeW8KcGZxU0wyUVpvK3c3RHdjc0NObit5OHoyMjhoK29CWUoyQXc3dmx2Y3R2KzZ1RUI1UDdnOHlGanhLdDRNTzRJTwpLZ1oxNTg5ZkdpcnRraDU0aWJwTGV4ZVhYUzc2Y0FZdVluRGlmVEVETUdlYjk1K2ZYanB2TEQ0S1laZUpvSlVKClFCWEYrUk5RZlVDVHVMSWxyY2lScUlqTkJZaDJYMEsxeVVoRC96bk1NdXBTWld4YzFFeFFhTEh3RjV2RUk3Q1UKRm80MVp1N3ZuNWFkcDJjTE16QkM3UHdsYWtiYXpUM0ZxenFWSS9vZmd4YW1GR2Y0YUNuaE5jcTZnNnltQS84ZgpIdHZ4R3dRYlg0ODdaYm5UTzdPQjlqZVRrVE9HRHZ0U2RIY0NWeGFEalhHY2RsUW13UWdyZjZ1Uzc1cUpRa0k2Cm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNTdlUm13cStCR2w2dnFvczE4S2MKNTN3SHFwM2FHVkFNQ0F0ZlVGRmo3S1dLMEl5S3V5NHZuWkZKb25yVm1zbzgyMXAyQW5jSTRzeWs1TGpkUHZHNApodlNWeEVzMlo3Vi9SZzV3dUFpSXl1dWdMOTJ6aHFEUkYwTktya2Nad1FTQVpXbUJESFFBdThSTDdYV0JVZkhyCnNJVEZUREhTd2JYZHIxdVY1SlFFcHpYSnN0NmZnMnNOM0FyUU5CMk41U3VJcmRySlFQT2REZm5aZXFSai90WXcKYjVlbjVVcmFvb1FpekdHZUdURk5CdEJqVkNSRm9vejh6Yk5hakRXVUZqUFNKMVBObUFEaXJENUJxSG5HQ3pjNwpLVnNxZmwzZ0x5VEZSd0VlNERjU0lhY1pQRGNMMTlOanpBRUo0NXlETFVxaSs0a0FKWTJadk56Q1p5a0pSc0JNCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGJpS0RqYkRsc3JVVGJQa1FvZGIKKzcyT3hwK1VZY29HUURoZ0pnZVZDUWJOQmRMaDF3cEVQWkMzcU52dVBqVkIvSW80dmhaNWlabGxibzNicXRSTQpCM0R1Y3NDdzRkN3VxVUtQS2FWMDZib3JvWGx2eUF4aTBjbkdjN3gzTzRSYWV1SXlTaHlZRWVMVWs1dEErYlhjCkowanFvNnVBV2VjaUM2Y3ZCUHBSVVUrMzRxNFJnekxENlBLYWJMTllGa3NKRSs2MDFzUEorZG5PdWYvcTZBUCsKSmtqZG42ZkpJbFd0OGd4UzZ0MWF4aGhDaWEzekp6QzUrMDZBL2VXZGUxNS9xelhHVU5yZmVJL2ROQlZTa0hHbwo0Z1g1OWs3UTFiaXVSSFQrcjVVUU83RE5seTlVa2ZBRXdaN0g3bGZZZkRzUTdydDIwSTF3aTd3T2F1MUZTZ2tQClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2M1U1k1OFI1L1VnV1lXMnJxUTMKNVVIWjhYN2JDSVFXWDRLQ3RqbUo4OUphejlvaFhtTnNsaGFXektkWGhtVTB4VTZqbEpKUWZXTmJDYWFvM3dVSgo5M092V0dGYW5pczJlYlJHcUM5R3lLQ2s5RE5SdDJyaXdNc3RVNGh3U29xYXkxTUI0S05EN0VKakE2d0pqM1h5Cis2Qm10YkpQL2pDdE13Z0xUYkJMNTJLNUZDdi83QUZkMUx4RkNkY0VpR3dLK0dvcS9uc2tmQ1VjOTRGOTdwZmQKZDhkaCtIcVVucUU1bjVuUHV4VXMxNGhSSUgxdTBSelZ3dHVjRTgyR3JpN3ljUE9BMXZqcWxLclRWcFRzYndpRQpUcWUrNnRhR2F6Ymg0cWU2amlmeUcrMDlDcXdTZ2pmNmRhZVpiTmZFZkx6R1dmTURRa1JsWGZGM2UvT25LUlY0CkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1RPVTBxaEMxZUZFdlA2QmxmTEEKVStWcUlpamJvUktqenlCZnM0UVhWYnVVNWh2cFR4ZC9BOFhvdCs1Z3RFNzRET1dQNWFLY3F5L3RVZWJuQi9udwptZVdTejB0QUdvR0hwK3hOYXBrZ2lRd0tIQXdTUGtVdytjT0U3NlgyTit3N0tVbEhDSlBrRXdGMkpFNkFFUVd1CjREbHI4QkV4eVUvYTVhQ2QvNkgzellBWS9hNmxSeFFXanBSSWZWb1VKaDZsYU9SNjhGc04vRHo4Umt4b0JoK0wKL2dwYTNsNll1OXZJb3l3SEpWb3l5bVIzdzVxYW9ZT3AyVGs5M0FCOGVlMjZSTHFVcWpSK0JXS1F4U2t3QzZkQQpib2VjUzkzYkQ4VitrSWVyY3c0YzE1b3VLWDFiazRSL2F0ME1SRXdaUjhhV3B2WTg3Wm5iMFV2MSt4Qm9zR2VOCm1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDZjbmM5KzBXaXk1VU9oSE5PQnIKTG9XTUIrMTAra0hVNzlSUy8xNCtvbjJEN3VFbkZLYmZ2a1M5M2ZtRm9sY2JmSUdBaEpGaStQMmtaV212dHhobgpBc3ZJK2cvcXFwczVWTERwNUxkakhVSVJxdzEyZ3BjWjJ6OUkvQzVyWDZQS3ZCMGNmcHIrNXFvbUh3MWQ5bkdvCk5KNDEvWlZUcmx4T0hjeEx5WThvME5KZzU4V1NZK29FSUdkaGYxU2NwajNFRU1wb09JVDMvelZLSS9Ra0pXS20KYnd2VXRxNzhUUENyODZndTE5eGRwaTlRZW9LeTlUblVjZUNVY3BERDFxMHNPYktXY1ZieHc3aFgvZ0xldVo1SApOWXpKbWxYSUdHRW1yY0JVTHlidXJpampJc2RTbHdHcTZubzhjMy9yUzdwaUlOYlN0c2dzc0hoQ0FLWU9uYkN0ClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2tUNXVJUDR4ZkFxY09jQnBod0YKMVdzUllPNE5jd1o2TGNKY01QeGRQZWhpcXcrSGJicUx6aWd0RURUc2NTKzVwZTJNRURJb0RrWDd6RUpJYUFCRApYK0g5VUZFNzZmNDZGaHgwRVBSd0xBcEtwMEpVYzZBd1UxbnQ2UnJveW4vSGpkUUtNdHFLVnJ3WDJyWUxwRUJVCkI4aDdZS1BVWENBemFyWHZKU29lY2J0ZytQOFBYSEtXS0lXTFQ2U3paeGgveEltdUVRdkxBNUxRZDJBKzFLNW8KOUVZNXY3VUUya1pzNm5QQURmYmVHQjJXU2pTNUtnN0dhWEFHNkEzTXRmL3hSQldBb2NGUEZlWVRwSWZaaHZwbQpQQkhhTU9PR2gyejU4UXZoT29oclpmeFdUUTRrbHRLWlJNeGM0ZDhUQVI2SGF2RlR0bmE1eUljcTFrMk95ZytBClNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUJrbjRFN2k0dE8wOFFtMC9YS1EKL3VuanpZclZNMFNKaHRxRnFsRE13dkpBRGhrQk1BcEo3cWhPYTA3TzB0UlRPQWdqd0dUSGlWOGsyelVTS3FlNgpVVVprVzNGMWZvWDNpSkowNUZTYXdJWGY5Rm9xekhZMHFtNTM3emJwa1duMEVoVXlGemhscjByMXpBOUtGZ2M0CkZwalBPZjBKcFlXQ09TU1BoUnZiVWVTMUxETlMyUEw5cmFoMHlhZXpQLzhLb2E1Zi82ZEVWajBxd04ycm9ZNGcKaEVPa1hsc29aK3RHQnpZeDdIRkxmeU5hS1loeThDV0x5Y3k5NW5xOXdvd0dzMGc3M080ZUZSb0hUT2ZNK1VhOQptNEt5WjZ6MWZ1YXZYbTF6RTE5ak5hUzVmejBqdFk1WVdMVmJiRm1TNUJIditlaDhUWHJoMGtWdzR0V0pzYUF2CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczExeFZtUDFxeTNzVFI2N0ZsYWsKM0V1NnpOOTJhOHNlUERuUEhnTTVxaGd1QUtKeG9oOWd5eG5hVzJKMnRVL2gvQlYvZlA2NENXb0VUTkZIQ1JXWAoyeUtvR1Rja25YcnQ2eTU0RE1NOEhkMWp5TGhHVE4xRjZzR1J1b3k1RlRVNTg4RFdaeHRUQXgxV1ZTQysza1ErCmZIOXNmKzJYbThvQ09zOVoyUENBWFpVdTlpV1JrUExhZ0p0RFNGcjVncERwTGFIMVdXR2RDcjd5WmgrbmpTNTgKc0craGd1b05WTWc2OGQxQUZGa2todVFCVXAwak81dStjUFVlNmNNaTFxTStqMmlYVGRtNWZJNTgrckpNUXhabgpMWDloTXVQTzFHRDc5bWRuTXJaOWN2NGI3bHRrR2FJU3FaL1AxSjF2eVRubnZsTCtvWXFFZFJTMnNLNDdWeERLCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmYzU3MvMytiaGt4UVgwQjNwMzcKTFVzY2E2WTVwT3JveFFRYWl6dk5BN1BzTFNrZWlrWDVSUUhoQlo3SHBOLzNGUmViR2E0aERud2NXWE1pdkorWAoyUGE0SWIwV2c3V1RiR2w5cXVMbUNhOHRvT2V4d0lLQlo4eEF2djlWU3BJVVVta0JROEpBT1dGMGJVdHdYdG5XCmJEVEVyUlZWNWNLN1NkSGRYcXdkRXAwZDQ2ajBKbFFXTzZwM0Mza1FZZUM4TzhoajI2N3MrMkRtQlJWMDQzOFIKNXlxTXp0YTMvV3laUTdUY1FOUXZzRTZ0MlZsdEJtc0hsUjEyZmoxM3V6dFdES01vdE85bmo3VVVWcFlyMnMzOQpqVU9FbXNlS1dZWVZLcHl1U3dCa0RhK1dacnU2THg5SGJ2ZXVNdFdzRlpJMHNHaVVSbFNOb0xjc3hzczFQMjAyCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHZybzNoRm8wN2NYM2NrWlkwbFkKbHg0NFA5NUJQQ3drQUlnMTQ1ZG9XU0xqTldIbFZVUXNMVjhGNWljaHd3SEN6ZmhDUTk2TTR6emFjTzU2NTgrVgovOSt2Q05oZ3FmWXNwUFZFd1BkRG52SU5jSHM5bEFPNURpNHA4V0VTR1lockU2bVJsem4rQ1NlZXVvbGVFa2NCCnovY1pMQWZUeXN3S1gvOGlrTXNINDlzRGc0ZHNUQUYvckNpMFMxTm9lc0pEMlFqQnZMNGJCb0t0M21ISXVLNkMKU1htdjJhR2dNR0l3VmVkeEk5b0dXRmR5ZXUwMU9TbzRJaDlSNEdEL24rSWhWT1JpZ0luYXAyZjJocjJGN2R5TgowZEVzUno3L1JBL2pHQ3lCbUNKTEN2U3VtV2hISEw3TjA1c1lEMTJuaGkvTjU2ZU1sS0ZZc1ZCZGN5VDNoV0dOCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0FvM0pDUlBQcllzelBuN3dpb0IKL0xqb3ZDcUwrVDRaZ1dYbUZZNGg1enlBSGZnaUlkOXIwZXFwSkVWdGsyY0ZUd3JBYk5ScXVOTnd4WjNqdnAvOQpYWGQxOFJLc3hDd1RaQUh2M0k1dEJuWVI3U3B2TktTUzN6Y0loa2FWM3pDVmhRZVN6ZzI4Y3JlV2dIZlhnUFBUCmEwZStQZUZGeUZHc3hnQW5WSlIwWVBFK09nMXE0TllyNXZDdUlSMVQ2elNRRzVoQTRHRUNmbk1SeEdIdFg2RHoKQ05BcVZoZG93MGRFVFV5bzFQL3Q2bG5yME13Y3hGWGZnTWE4akY3cGZacjhVV2ZKZzAwM014RmZ1TVlpbDdvNgpSTlBnZWZMSTRoRGZKNWFrd1JZY1BQdEw2NXZFTTdKQVhNeFpFZ0RBSm5Kd3hvNWJ2MVQ0alY4Uy81cmp3MnRCCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMWlSaS9Ed0tHYTFDKzgvV1BPWFUKQkNuRTU2Y3UrQUNGTk9PN3AybVBjcngrMkZvZDg3M2F1VzVUOVI3VmRuRjF6ckVTYUV1Z0YzZ0xCUnlrRWxVUApianJiQWpZbHJFRGw4YVlEQWVVUnNSMDh3aWNmVFV6UUYzNEM4TkFLUyt1YS9FakZOTENvSXdaandrOHVOTVNNCnR6djEzVDdEODVsR21UMWxVNTFZOVFpOExVS1hpS0MyRlYwdFg4RTlUVXcreHRDakJ6eTRjMUdrUHNzQUxobXIKUmNHUmVUWkdxVU5HMmxYSWZQZ25KUFJLei85WWFRWlg5L0xMcE40aEM0YXdLVUpjdzhhMzlQQlREQXp1dEh6NQpNblZyeGpkc2tLc2xNREhxRUJXV0l1bUtBSm0zQW5zTTYxamFYV2VmSndoZlhQOW9IcDZEOXR6b1NsTHZ6UlhaCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbVAwWjdORDMyRzVMYzF3R0RFVTcKMnZQZC9EOGd3Y2FIbDdpeFRscWRicTJoUzRjMDMraHRSTFpGQ1JzNzRoK3gyNngrS0hGc1g2RlcyYXp2azBsawpiWC9TZWpuRkthcm9tQTJ0M05adDcyanNhUTRVU0JlZG1IRHRjU1BVaUEwRmVyS2pjbUo2eDd1M2xhak14ZFBDCklhWnNJdjBDTXgvUXJNYlZiVnRib3B4Rm84T29aSkFiZ0w4L0Njb3RCeXpnTnhOZjFWWCtuSUg1R2ltRDREMEcKK2ZiTW1aYmxRQ3ZESEtCeXZhaGNJdTBBaFNvL3RlSXh1NkxNellQTGlZU1I4NXpQdzdKNEt5cGc1RlpZbGN0RApubHNGRkJkTVVEeEV2RnpyblgrMU85aEQrRVIxWlBLSG01eWJRTEdNRFRZRUwvMVB2NUJ1S3pOUmliZzg1SHpwClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVQ5b3hWbldBazNjbU5NbFJCWmwKSE5lQkhZVzlBUDVhQzh3YVRtcFozNk5iTVZ6RG1JNUdGVk91Smc4K0RENkxsSVdic2lLcU5QK3hFQ0MxQkU3UwpJL01YZkNPMjN5SHNkY3dTa2pONzFUSms0aFByejVtZ24xMm1HTVpjRE1WV0NHcW1JUGx4RU1DRytSZ2hOYUlmCkdyMnUyc0ltWlBLcUN1aWRsM3FVaU41dGs2dDQ4WjZxM1U4NVhLMlVkRWVRZ3c2K3dka2RJaUMyMFhJMUxrNFAKZ05TYzVBVHphd0psUVU1aWQ3K0VKY2dOMnJQbjBQalh1MnM2bUhSeTBCUjRPWXNzQU1rYmdmQkpadklHemIzMQp5M0d1ME9tOFVsby9ocy9MNmgyOUxwWFRyaEJhQUY5MGNRQnFaL2lrMUoxWlBvMnJtK3N2dCtQYytjb0psRDJ2CnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFEzSFcyd1p5UGRrYnRkaU5SdEYKaGo3V3dJR2tjUXNBUjA1QVAzVTdMRlNlU1pQZzRMTk9RVXlPRC8rQSt2NGRpOHg1bFcwWmU4WGxNRjhOVTh0awpkekRlQnZhZHJSRTBCYXI1MlREN2ZmTVd0bExnNlZGTWJZUUlUYnpHc1FUb3k4ei85L2cvL2dzbW5qMDFNNGliCm5pUGNCakZ2VFRXYzcyZmU1eDNTMDQyK3pRK2llZ28vL2swN0xibkVPRXpYakFFekZXM3RoS3p0aHRtelorb3cKZWVOU1JDWmxxSmVKT2NtME5ieUJ1TG93dURucVg1U05qVGRVZ01lTit6ZTFoWGxRSWNqWXJoajFhUW1abnBOZwpwOGt3Y3R2SEVRM1hKUEVDQjVIeGFGRzdxYW1BYUpOaUNSUSt6VHpDVkxZNW5nQ24ybmx4bWpGUkcvMCsyRno3Clp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG1QbGZ0cGFaaHBDSnFKNDZtQnUKZG1Wb091K0xYTW9hb1grV2hBc1p1UDYwTVU3WllQRG5maHd5R08yaG1QRU9iV3Vra2JwcjlrcTc0aFFIZHNVSQpoTXBPcEpxQ2M0eTBnVlA4UmEzQWg4SkNidm1STTMwbll4L3BCUEs4aWpyNXhySWJGcVhITjNzalppUWpiYlYxCnNzNzhyY2hKMm1aVkFxWEs3azZOSkh6OGJuVHo0OEJadVNEeHRHdmZ6bTMzaWdTeXJTN20rRFNibWZ3VGE4TkEKMisyUm5zMkxQUDFTdUh3QllaSExEQXVtRWN2ZElSdmNEZ08xcXR5YUZwYWlzUHBoSHhycVU4SVJIK2pQTjF0TwpKWitqMjFKOFZUUERXS2Q0MlVlMkxwUkx2YU10OXJpQ1FSa1ZzQzFZQy9tVXVRcktYQlBVWGh2aDY3aGsraGRTCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzcyODRnZTJoSU8zSWFUdUg0MmQKRndtMFlnN1BzYVJCc3EzZHJHdmc4c1FQTzE2MmpsNU02SkYrS21ZYkNncHlsYU9Yb1BTUFcybm1xYWw1NnBXbwpsZVh0VWk1aW9oVGFlSlNZNm9Ja3pab0JyQkNvcEZlVkVXZnRPaFkrM3dlVUoyNyt2UlZqVHkvb05Nb0FXZVpQCmxRdGc5U1Fpb3BFVWVhYWc5amkyQzZKWTRURkpQcXIycHNuQ2xQQmRxL1RxWWhkaEpmVHZSUXAzcDBtNGMxRjYKeDBZbXo0MTlUdEJ0d3NaR0xqZmQvUVBTSE9qazZmYXJOTkNObFd6eEZlY0pJMmFMaXY1bmhjNnZFbVIwSXc0dgpWc2p1emp5R1RGK0RPVXJqWTl3ZGJKL01PekJnOG8yQnViRU10WjFSYWpxd2QyMzh3dDF2dTYxZS9kZUVRdXF3CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMitYVktWT1JCSUJ4bGlzblJkNzgKVy9BeEJNeDQ2d0dhVkRQblloVnZ5SERlUFhLSWxoR29vMWM3NnVqWGxwSFdjQ09wWGV0YWVoR0tSMlNhVW1KOApmcHpVRTVMNHRtc1pWd29NWUd5OVduZkFPa25XUEtIaTRkV0NicW9RUlBVaS9Sc3ZaaEN2bjRBc3B5a29kWTUwCks3bDR3czNrYjhzL1QxMUV1NEZ3TFRGUDc0Yk5lOGtla3JJd09QNGdsWGlZOXZyb2lvRzRGWWxpOWtEUUlyRmYKZUhuYkNXOGNBR29WS01kRlRkOGFpbk9aWUlGQWt4R3Fuc3BpSDZaUnJZL0twWlJ6RUZ3Q1dqSFpKV0taV2EvWQprT3hXMWs0ZXFOekZJUElBUlNjMXlOZFpTdGdXZnZmajZYMTUrUUo4bHFIbCttYXVuOTBJUXBPSHk2dUszcWdTCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFV4aGU5czcxWEVaWTlhRDJTaGEKeU5NYldjZXVxSXRpNFJNcUEzYzd5NHVReGxHUFNYeFVkcUV6Q3F2dGpRaHk2cldUcHhZZFg4ZFBwUFlHVndrMgpiWWFCTjdTVUZHaHh3R0ZEMHJOb0xFT3NDKzBwM3hUYUVUZC9VQzVkUmJyVElPWEVxRGdFMVNnTU5WOEgrZUNLCjl6UlY2clVaOCtGUnJVOXE1VEJtVHZaWE1wNlhJb2VsTng2aXFKbVlZZ3BTNWN6ZzVWdGtKSHVuSUxYQWE5Y2kKZ3l2aFR5cGxMeXVyaTJKbjUwR3ZleTFaZ29DYkxjd29JNzczblo5YjZpb0lwTVRndXNJNk5TTzN4czZrUnJCegpCYnpjbGEzRS9vNk50RHdvMldTaHZod3NBTjA0bW9seXRTM0xudjRiT3I1NW0rb09HY28yeUN6dXl1d3dGNDBVCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHNyaHZBUkJjZ1lXWEJvU0tONnUKSzhzcU9IbnhyY2xrQ2wxeW9mR0tyOU9DVVB0aGVYOHFneG40c0txL1JVMlJqTTJDVnFFY0FCQnMyRkVwYVl5VgphcXBoaUVQd0xLNW5SRkZyQ3hnOTRhQ1h5dEI2ak83S1RXdy9pWjQ2aEtYMVZyOFQ2ekxNRmh6Ui9Tb3E5V29qCkxIM21kT3dVUStReUNPMGdGTU0xcUpDZ0U0Y1ZSY3dUSS9MaVBSQXo2SjNDelV6Z3VRZUxjRFNrVVBaYStKRTQKeXVjWnBsTDZraDc5MVlla2psTFZlQ2pBWWxRYXAzckJXWXdLSm4xQTVad3BCeW1VRDZzTFZpV3NSbU5PalJITwpTREpEdDdBNi9McXdZU2ZUNVZHcE5aeEl1bzJFc3NNaVlOR3A5WjQyZVd0RGptQTF4QW04b1dKWi9TT3phTUJyCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK01URHdBMlJBd0xMZEZpUkhMN1gKRzR4UHd0NEZ2NU1IUVprZTJocDlHeklreUR0UUR3eXRYOTBYMWNpN0dxejFBUXBRZUJObVlhejhDc21mWGJrUQpDK2RyM3RZd0Mrelp1N29oeGRnVi9oOGZVS21tU1d2d00rNmFGLzBBWWRxT3ZzWWVPM08yamtUbjlJa3VKTXlxClVReUh3S0VhbG1WS1prMHpTckNTQjBoTG9BRHJFRW5wZXNRY1Nsa25EcU5BWm9LM0U1SjZ0K2p3UmxYUEpjS0oKY1M2M0JubnJsUEtTMUVnT1p3N1FBbndDblJUblo1TFNYbDE1UjNxRVpHSnNseFpBVXhJcm5XWk45a2p4azZVQwovc0hZSThxUThDcU1NRllZZFFQMWxmdnV1UzhOWVppZm9hSVErVldPM0tBTXJMSUptM2RjdWx0Sjhkd3RGcEpHCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN3RqdXU0MTJwSC83YUMvbTQ2bzYKQjVxVFVxV0w0SC9xT0thWGErcnJhOUlPRlE4NHkvMU1xeVJPTElRVVNTMWZNbFpIZ3g4bkRXbjAzNE1ZYzYxUwpUTTkwejVQSFZqUlAvSUJCc0h4VVV6MUZ2KzhWTnFQRUpWeTJYaWFrNzRySW9Sb2RCRVA3MFQxNThGN1hCSUNlCjlXZkdCUGlzUDAvTFhGNGRiTGRLVmoxMEdXT2U5NklLZ204Y2RyWHJ6dFRJQVNkc2dndVBmSllUZlhXUTZPb20KU0hPNG02cXh3NDd6OHdIUDhEZW5kMXNkTlplQ3RnVVNsT2NDL1FqVDJrcE80MWtOY1VNY2tyRDB4blpxMDVyLwpTSTF2dGM3UjBDbjVwcm5NdWI0dXpNeXpiZFpMZDhqZUxvZnNmeUd6bGdOMTJoL045aXlDSS9iNTRFNkptVGphCkFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFNmR200eHhGU25GZ2NjL1NEb3EKWTJaa2E2MW4zWGJpS2tEVVBMOXFCYW1JYVhZV3ZzalhkeU55MjNUME1oY3dlV0xEWXVucTJXem50c3d0NWFYVgpqd1hHTnJzOHZ6V3I3Zml6REFOVTVhWWkremhqK2grNkV3UkRZN2g1a3AyU1NERHJNOWtNdGlaaksxZjBOMlZHCjZnbHRzYmxGcVdabEZXbmdCVnhzeGVCRHd3RWZBVW5ZNWMwWkxJcUYzQ1JMU0F0N3VNY0JEV2Uydzgra2RGR1IKTS9KSmtEWE1GTFhCeGJKeHpBSkc1aFRyS1hkN3dUSjlONmU1M0w3U3U3TVZZWlFlTGJvNDl0YXRGalRuZ2RxVwpreFMxRnhLRWhwQ3dtWWJJNmhuNVB2WExNbjFNRXA3eDdDNENVTHlKaG9zdlhqSTcxS0g1NFBLU3BUNWplTGU0ClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGlDUk5xWEhWOVZpZ1NHdjd0eGEKQmV2UHJ3TU01dmFBZUVPQzZpQ0ZodlNIV2pFdXBCaEV2YXZHWVJsWnd1S3FSQWdOUTd1clF6Zm5XZWx2SFNyNQpzMWl2cGEra0Z0WE5Ta3NXaUY4QXY2cTJsdWNEVmFuSVZKRDZTSlBpdUNZL25DM1FzbFJxcXpxY2xLaHRKczFwClVoSjA4TDVVNytqVG9yTFp0WUc5SjI0TkRJd3BnY0JsdVM0ZE9BL25CT1hBNmEwQ0tNYWFDUXBndkVEdGxlSjcKa1lGWnc1MS9EQ2FFQTRqWnJNeTk3NnlNa0MwYkhIdVJ1M2tyNjBEVUtrS21kVGVTT0xpdDZBaUpxc2cyRkxRRApPdW5aNnZUM0Rtcyt4N2E2Nnd4cGs3bGN4MTZCcXFOK21RN0lFZmNBaVBCMWhQaXdzdUMxZUp1QkpRdkpqa3lqCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWgxM1BYVXlhcXRPaUxSZmVYdW0KWVBWL2JRc2JpSkhySHV3Wjc3UHlFV0JCWkYwc21rZ25reDlhSThLV0UrSWdJNDlZb1J4RVd2MS94anJBaFFEMAo0bjVhdzNqUFN1QUNCVCtGb2VuRXpuVUs3b0MrZ3IzL01jVXhLeG9qT0RmemJON1RPVVdvVzk5Mks1YUxLNHM5Cit2SXAwclVBdFRPWS9ZcGorWmRtbEVqWmJKS3BkTVUrbGpKOXlWMGdDT2Z4blgxUDh3Q2MxTlBmdEhmS3JKWmgKbk9LSWdjNk0zVlFLbURScitLaUY4Z0p6TlcyL1pJY2JJQ0J0UHluWmhadlRTWnA4dHBkTnRNS29lTE9RZjhwRApKekoxMU1FUHJrUXlGWFhvKzBGMktkTkkySERJblA3UzdwaGdtYVpQampVREJZbXYvMmE0aWcxalJrTGQ3WlNaCll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2ZncE44dkNGZU1YckRRaEE0Z0QKYjJLbFJuNXlTN2VrNGlJOGhIc05KUFpESlFxM0ZwYU1keXZWREd6WXhtOUpFQWFrNnNJSWlXSUdsbjgwOGJEWQpzc1hXeEFKRVdUejZDNTRtQTR1MU5jd0dVSmpya2tPUHZPdTg1UjBJeEJRZnNoVGlVUzVzdks2dkY1SEY5RmdDCi84UmxMNWdDbUFWOWMyZzRSeDV6c3hrbFQ3WjBDVk00dUZDVnl1MzJjWFk4YzFLV1NJOHA2UDVpSVFKQzZaOUUKU21jZWNtQmxvemZrV1hyTU01MEUyVzNDb3YvNjgvMHozcmc4aDdLWHVXWVRibU9FSE11Y050MndwTVNmQzh5VApwNHhqNTBMSDg5L3ZIUzl3Y2R3SVE5N2UxV0M0Z0puaHRZbXRPZGVxdEVUcm5UaU5TbkJzRHBHaU43dll3a3dJClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFlES1BTMEdnbWhLaUJUTmFVbTIKcW9MTjZpNGFwY1lKRHlDMjNNd2NPTHFMWFBZU0Y2N3R1NmVlVjROOUVnVFN4L1ZoNi9tWVgzQnEyYWN0V2RBRwpEdnZTZXNkQlhqbm0zY1ZxR0g2UCtKd2w3bEdlVm9pQzREa3NJTTVZdm5VMVZBcG5KaDArN1E1MU8wYXA1cnI2Ck1WOEJxK2w2R3A1bnZPRHNDbjBuYTZzS1pheHM3akdtV2w4SzBYOFN5WG9CcFpoVG1BczFBTVA5UHpTakhST2QKR3R5K3ZpN2licjdqa0VmMEtXblQrOUo2UWp4Z3pqNUVPbXMvRFllcndiZ1dDSjZETEhuOFBoWXMyMlpoSG0rbgpzVjgvSW15OHowZ25tOXNJVXhiNG81eE1RVDlISytlSGRlRmJvYzVVSXphSnNBeVRPVVEySlNWR29KNW5YNjRzClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDd5alZLTnFBbEx6NDQxZm5JWjYKTjhFcmNGd3BXM00rVVBSRS95NGZOWkxyeEpJYllVSjJHVll3VXhDdHNtMGpMR1AyTzh3bGZYS1RPeEpRSnhwTwpRTWRyQ0paR05PSlJpdi8wQWtLVTRYR1hLUjZVNFppUlhvL0cwdmdVYjdBUkZ0aEowZ2dEVXIrLzVpZWRmZlArClRQUWxGYmVwY2lvc0luM0lLTWpzRGh1VzdjK2QxTE1PWDZCTlhEL0NHTVlOLzFISTBHaVRCU0lTZys4UGlxWlIKeVVqT3ZCTUNlYS9BU0lwNFdVOXpzejJLMEpZbllRWURUdTh4R3hSY2c1RklWSmNHZEpXSURpYmxsTG92dkFmUgpWTlU2OWxLRWFNR3BaWEtaalluK2Y1V0tZcEE4Q2ZmT3dFNkI5bU9GdEVGeUxFUGZpOERjVFhmbjNoQ3k1ajl2Cll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkY4UVcvNjdrb2VXME93c1IwZDEKNmFsZWsrakM5YlpjdWZUaERyaCtpU2RqYzEwNmVJU1F6RmlVTElNbVo1amNFaWtvY2t5V1J6TExXMG13dGtqOQpMMDFxR091SlRiTVhkWktvTFp6azhxMS9pcnN4bFVkZ2g3N2NBK1NsYjVCR1E1QVZJQnNka0owRjM0R01menRsCkZUS05IODFaNGdWRUVkWE1reVhMOVBwYmI4ZGV2Wm5TbTJad1JzQ1R4cDVCRlVUNnJtK2JMemk4RGRqMy9oZ1oKblBxZ2lGR1RxYXE3T0NEem8rZ0V3akNvL1hLV1k3U0VZcnlDS3RGY2ZpZVBoTHlsa0xvbTEwTmQ2Z2RLNndhUApSOXNFMWoxOGpHTWtoSENid1JyQkpWR2hZNWJ4d2ZlbDg1bjdEVUM2L21QekxWYWxoaTlVSDR5WFo5V1d0MU1tCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlJ4dmkrYzJCREl5VlRlRUQzb3QKdHNubmlmMWVYM3p6ck9qVzY3S3p2MCtkb0tFc2JVdUZzdHJOVlBTdSt1TzZvTEdhOVNtQUI3YndOU1VGQVRsawpNQzExMGwzMkloY24zV0JYRTZsOFIxNzVMSWc5azNuWDlYTnFXanBNQk1ZVkxJVU9FYXRjaHhwZE5WNE8wejJtCmF0ZSs5TFVGTVFPMTd2WGtiU0h0NVF5dG9vcXBGdlVxN3VkTnE3clNub3M0L2xtdnZuVWlwTmhvc3FHYkV3bkEKUGM1N1VsdlBQdU41MDk1djl4Y3NpdTBGL2dNaHhVbmt5b2JNVkh4RTZlVlZjUXlSOGFEVVBXZCt4d1NRTXhqeQpXcE1kRnhLK0xKZ01XS1YwaGtucHVPYmNSakJNMXVnRWJrL1hQM0RxT2ZSd0pPTkgzNnJ1RllzNGpjS24xVmJBCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb2xORTJpT0VqYlM5R1VRL2NmUUUKK3BCd3RLZU9wSHZkUkYzQjhPRHJhNkVEUG9SdVdIYmxaZ0F0UFI3UHVIV3N0UXUxNlg2akRyWlJ2eXpqejlGTwpISUJ0UWhKdEtEOEN6OEdxRGIxYTdsV3FpNng3OXhRVnJpSGMyNHhkeUphNE1HbnBoeWhTVm4zaW85VlhtTTFQCnJvZ1dNSGNXUGpNWmg5SUdGOVMwUldSWjVWOFZXWkVoeTZSWDVnVXhvNjVmRUFhd2VBcDB4THpYbGRrUmRVS2sKbE5pbWxraklyNHd4Ui95T2cyL0tPdHE1Tks4dDJoT05qNE1Jc0JvU2NLR3RqZVN2bDRJNVlxN3hSY0tPWWtOUwpNOU5KZzhvYmNaRGlUNXkzUFhRL0kvZzhiZC96ajdVd2FjeHZqSlc5Z1lWblk3bzYrTEtYK3BUaEJHZlRZZVZECnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzE0SGpjTlkvS1FMVllkQjhrSnIKdSsyNGRsZ2Vma25EYjRUMExNeEVIbml4RTlnWFFiZDJhZkp2MTJaci9ta2lFaXk1d3gwQzUyS2xQRk9NVGp0eApIUi8wWmVSaGVZN0hvaWZjMWozOE5hbFR3N0JvbGVMcU1DQVFIQUl1RVh4bUdmQlM3Qzg5bzgxaVptQ1VoTTloCkFWL2NKcUxRQWtTSlpSbG8rTm5QV2ZrVEthT3hVUFpsenhETnI3TmFEZjRKZGhmdFQ0Y2lXa0xGd1UxaUl4c28KN3V0QURuaTlsVmlVUFJKdnJFeHpBNlJPRXpZUW1UWTU5TW5ublo2Q3lJZ2VKV3lSck5TaENxc0Y5NTBXZzJ4eQp5T0JSL1lEenJPZXp1R3MvenkxUzE5R3RFZXhnUUMvNGhoK3ZLS3pUVCtYSWQ5WG1pVHo3aGV0dThQVVdlS2dQCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVRCTXBsRHR2VTNZaVJhSU5oM3IKcnMrU2EySkVKZWpOYnBpQmhKVTh6ZGhxcEFLWTFsY1ZYcXRnN3I5VXVadDdSalhheXoyazAzcnFNeHAxWnVEdApyY3UwQlZIOEFhSXdXcmJ4VWdMV1hMTzVCcktOS0VpRVM4UzJmVThDLzRTODRKZmZ2bVJPNmZmdTRDbTRDL1VSCnhTb0UySjBGQlVvSnp3UXJ0N3FrckJUNWt0K1NibTFWQUVhT0daYjR1bjFMUGhpcm9tRVpxQmJpa3BrZnFHMmkKYmJDKzNuMy8vQ3c3M2tEQnc5MnNLeU1mZTF3RmdnQS9MSHl5Z2ozeHk3MjVIOVBiaHdQdXpVWk84cGtneFA3SQoxKyt5bFoyUFhybkhlVVgvTHdBNnQ4MGFlUWNuaVpqYnJaQUxiemV0OTlCd0dTV1dlSjBaM056aEYya1UxUnlMCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1VKbklvMGc4RjlEaDdzT3QzOXEKM0cwN1A5QUxOWmtQeGZPM00yMlhRUGVNUFVXMHNNVkYycHdFcDcwQ0Zad3RtcGptMWl4djRHc2FqYmx3UUJDUgp0SGNrQ2tHZ253d0ZQOUp1K2pSdUJGaGtKT0xBcGMyRFZib044cGR6OHVoaW1DaVdQbUJvTEh3WXFpMzRjNzRDCktRb1lyUHlqQUlIdFViTEtvOW8vL244VUxzSmdyOE9ZMm1jcXMvczluS01XZEJ1Q3ZyVlJuQUpWbDN6ck5WeloKOEhzYUM5Z1VmTjVXUy8vbmlxM0dXQ2taZjlyZVV6dHIvdkdxck5SbjhCQ0t6VURFS2NCYzhERVU4ZEI0YmpnUApISEE1eGIzcTh0NU15WlRBVjhLOGtCdjdQWURZUGVPRW1OaDNMblNtcjF1WnlMcVlzOXBoUWNYSU5kMnJLOXFBCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmMveFdKTktJbTFoOGFsMkRHdHAKeTBVeHU5SnlnU0l2Y1BhcUFlc1ZkanQrQzhjRVhJSFZQdi9wU2tnN2NaVzhIa25vQ2ttVlcwRlRiWFkrQThqVwpiVGVqWmRQSUtvUUU2dTlSOUtEMEFLbGZ2S09Bb0hiR01Uc0Y3dWY2QThQS3BHRmRndmtqNGpwYzFDSFprTHVQClVBekRCWms4MklBcEVHRzRKQUNhVVd5enF5U3FVS3kwdWhZVkdUTkVUZFlJeDlFQ0NIQ3VhS1AvaHU3UnNURk4KOU1ERUhFT1BJZkg0UzgrWC9NOGphbDNzUURaN3gvdDNNbk5JcEFHV0JvT3luQ1BTenVjdnFjdk9mWnNWb0ZhcgpZcmJWMUFWMDJLekNZS21iZTVlVkNKWVRFWTBzM1locGw2aVlaZHRHcS9HMmF3eEEvUGNtSmczRThMSUh2T1pkClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjhDQ0lpYVVoWW02Y1VMczVad0EKa25FSEVDWGNwenpKekQrNGxSUWI2OE55bEJwM2wxYzlGekFSenJTWHBha3dXOEszMG1nZzh3UFZ3aHBOa2ZESQp6NlZiZEtLUWMyeVdEMnpHTnJQaUh5OXJKZzhSUmJPZjFLQTJUdElmVzNyalJ4Z2NZV0haVzZ2OXlDdVg5eHQzCjRsdWh6c3BsdlY1MnFVeURPYmxkYUhQZVhGZTJVVzVYWEl2Y2JpT05pZjZtMHhHK3AyUnJYc0pkTisvU1hTbjEKRFg4ZWpySU8zeFptZEUxZkJkQVlrTW55UU1BSlUyK09QQnYrSk9ONDF1dDZablJGcWl4ZTZjL0grMDRTSzcxVAp6NWQzbXU3ZGxpa1V5dDVLSEZxaXZOWSs4YTN4cHFEck5Ld0hDVHZTRm91NmpYaFg1b3RQN3Y0LzFjVUxyeFdTClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd05sR2pwTFcvZUhQMVJVWjk1cUsKa29ja3dReEVlZ202ZkcyNWNOOFhjNVZRRlEwYnh3MEVCSjY2TlVrMnFLbmhEakwyYTVKdTdWRVdZOUN0RXNhbgpST3ZjSDhRZm5PcEtHU2FYYURCOXBIMWZUS2Q1NlNpeFAzV09hemllMHRtbzBDR1NDdWpxTDdWUXJvRDZJb2hoCmJvdFRwc3N0RXo5clF5SExWN0ZqZ01GR2JZRVhMRSthTEc4cmxpTEo5dWE1aDJrVFVuMXBNbk93ZTc4Wnl3QmUKaVU1MGRXcmxyU2cxZ2loMmk0ckxvcjFwVzQ1dUFXVUMyWGFXL0N6SW5VWHRwZC9mcG5KZVpKTnlIU3hJdy9rdwpSK3FPOXc5dFZDSGlWdzVEYk42QURRODFZajYwd0diSkNQRURUL01KczZDRXV4b3NFaXcyaHhsdXN3YjZlY253CmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFkwVkl3b1hrK2tvRmV4ZXpyVEQKQzcxWUlMRFJQNStyS1ZhMmRoUXcrU2FJVTRSYnNuTmU0TWx2Vm1rdysyNTlrTm12Z2dMZ0svOWJ2eUJhSWlZRwpOVDJhZy84YXJWdmRiQkRFU3dITjQ5RExIbXNRdW5TNmJIUXNibDlINE1FaHBpNkx2ZlBtQWZXaC9SM29tZkpICjhKS21Zd1VPdlhtaXpCQTRiSk5zMnpMWHA2TWxHREVtYUQ3KzRRVTgvVVJZRnNnQjhWNmMvcTVRbE41MTNJdTcKSmxXOFpyVVhnUTQ1NUcrQisrZnE0MkY2Q0JYQnFvUUg0ajB0cHdNYUdaNDBTV2VEMVlRTkkycEtJNGxQM3MwSApUcHIrZXF2VGVlNGtDdDlyNFE4RlhUeTlrTnEyQzJTY1RReDhkN0U1eXE4SU9idmx4eE9MbW1saU1hYklSMURvCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkxjNVBqdDlUY25sWElRTHdyQVgKYWd2V2VYRGF0WVZmOFZWZVJOUElwZlRKQjd6R2ZxOEw1WkNDWUxRL3d4RTFPcllKc0RQTmlzNmhJekJiN2h2aQp6QVBaNmZzRFBlV1NQaEs4emxkOHVMMnYxZDNiM3lEL1VYQ0IxODdZK1FPbS9Iamd1amxPU3hXeFA5YU9aa29xCnYxT2FNU3BWZXE2MTVPSG9vRmhMKzErcmN3VVRzb2VPUE8xZGx3OTZ5MjkyR1NXZGd0YU9LY0NYWWcrdldwVGUKMStna05lZkU3YjV1MlJNUEhtV3N0MkNCcDdtMzFPN3NPc1dIMmlsTWhlNjVtU1BqZ2E2OUE1UTdLdWQwd2xabgpSRlRMSnV6OHNUeTVFZFhoRGtibkxDTFNxbnp0OGQvZ0Fsc0JrVGFldlJrM3Avb1orOXlPN0Y5YjJYYU15THA2CmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWZnTjFZbS9kcVlDOE1yVWlybmUKTjZYVzJmSG9UM0dnVmxjT1BnQ0tyODhlVzNxbmxRQVNNY2VvUVltU3l5MlRRR0MwMHFhdnBDZGJLYzg1dTZYKwpKN2FvMlQ1K1BSSzczNWZtNlBuOVJDdjVNYnI2clVCbDlaVTlzeGtmQ1U0VWFQU1MwSWNjMWxlSlJGYmNGR25aCjFtU0l4YVB4VWtMY1o0U0U0aGExTDZLMldGSkttUlFlSVl3YUZvTWsxLzg4MzRkREgwaElnRS9RUjlYazFrbksKdlJQSWx6ZmNaYUtONGgvaG5YdGR5bE5GazE0NGZFR3d5Z2NuSzFhUUxrWjhSUDJoVDBnZlBNT21MM2tFZllRKwoybUkvbDVNOW9GRFdBL2pKZjVRVk5CbTBkS3hsZjFaVmpZUWN2WDNxWWlubEZ0YUIvRG1rWVVDYTZYT01pb3pECkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenlqbFZucGUyYVArTVMyR2EvQ3cKbTNaeWJIZVV4NWU0U3E5NmVBRUtWb1hqTGw3TU1YQXJxcXo3VGlQVFlWWWk1NjZURGlNd256Qkdlb2NjcVdhOApsaFhSbkxuZDhNRHloZ1NNUlJXdlVPMU51YzlnRTkvWnJxL09Mc01VUkVKR05ueXBlVUJvRXo1RHM0alduOE9qCndidjk1R0Jkd1M2MGxsVVBWUzBnbmZmQ2lMY214QS95cXQ3bmtXVThWclRUOUJlWDdTM0JoZFlidTNYRDNXelgKc0JpQXhGTTQ0eVFVbmZEV3ZhaTdEdGhUamhaS015ZTNyZkFqdjNIU05EQ3dBei9iL09oVFZ4eWtKZkhFQlBUUwpVSU9nbXRrK0lpaFN5WXBMUkJyUEFwY0lTMUg5NW52VGg0TWtlWHdWN1FBYURCaCtpUTg0Rnl3aGd4SHBKZjJqCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjN5dzE4ZkVKejQ5SmIzVzN3ajYKWWU5Y1VDS1lnRWJHYnlVTXlOL2IxbzdpclIzOWYzR1F1VDZCVmVNaVZCQmxLTCs5TXF0SXFPOWE0VzZITUZTYQpreXE4MjlDNUI2K2hCWFhDN2hsRzlwMTRCMnNVM2hMN01QYW9YcWJ0dHN5NjBQVkVWdFd2TVNMKzhXZW1IWUJrCnpqbHFwelA0cGV2Y1JQZi9yNmdKNUJXQlByUmtRUzB4MCtHMTNKdFhxMEx0SjRFWHRGb0pKNmxzUXA1ZFlPcjcKRG9WZWQ4SGZNZ2NvTWNielRUUmlrdnJkQlhTS2kzaUoyTDlMWHRabFdBOXd4dTZwbnZ5WjdGNXlENDhqN2xlRQprazlpT2xpM0xpeldiNXBvUDNiOFJXc1pNSlhZQ1pxY05jNUR5ZlRqYTJ3ZVNHNnkzc1o2QXN0N05HT0x6a2V0CjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOXBUSmQyZzJ0RDB1cXJMcXJ2WmoKSEkyZUVKVnh5VzJPb3BMUSs5ZFREeXd2WTZpRTZJTEEvSXNuNEpnL2ZOVzRkc0xkRldRa0FWZW8wSEZTR3hXdApHMGxrd2JCck4xOURLZEM5QXQ4aHc4Q3FCcHJaMjNEcmN6RGRBZElENW9RclZUOWN1MTFISEYvQWRrT1ByYTJUCkVnanhvU3ZHQzFsREZ2TDJNK0pNYlpvSjJnY1dnUVVjMXRZKzZBOUFNWEtYbkJSdXFKYkNuTXdzWXhMZXkwSXkKdWZ1SzNKSjdQWk5UdnFEdjZxZmFlR3dUQTVwdnBHWFJySm51YzlrZWljUy82MHpBRzNVbkt3azlOSm1qa040WQpDZnU0NUZJaXRBZU9heGVBUldSZXd3S1BTN2JwL0xVTGE3SnJkOFd3dmlYQXNzd2FTVTFDQVlwK0h4UDJVcHlkCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeERGd1hJYjJ2VjdkR01yN0pVQlMKVmhKeWlDdTlwR0JvQUFSVjFOZXdLRUtnRUVXbkZiazYyTk9YTTByWjZZY0xlNEhOVGNXQ2FPbEwyUDdQamcvcgorNlhlNGFXWFM0Z1N4MTE3WndnOVU4L3lyd3hEY052OURiTFNaaU1WTmZlN1JFNXRCSEVRWW1iN25rTTN0SC9HCnVMOGpoWVdvVnQya1pUaWRKNmIxMEkxWVhWejl5R21MNXdQRU9GTzJySWkyK1J6ZzZHbWFKR2JrbUxRckdpUEsKazhpSDNWWEFVL2VVQklmRmcwTy9McXBJa3NiOFovUnNYcVYrVERvNmRDbk5DVmY0QXhQTzR6Q2FMMkR0Y1dKcgpXbUhyRnhGQ2Q3SGI1Rnl3UzcwQkZPR0p4WDBJQVRWTzhLREpvSXZ5RTIySnJSd2orTTgyamoxZ1p6YzM0ckFjCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTNpK09NWXBWOFoxUFFGMjB3M1YKY1V0citrZS9qUzhXQUJMeWlseU9BL2hvZjR6aHltakE5VkozSCtEcGcwbUY1RXMzbDNYWG94YkRGMXBkbXRrWAp5TWNFVnNIVzFhYUQzSXZEVWtsb1J2RTB4UmZNa20yQ1l1cWhlTWp3WU5sM0RlZjgxbldtTFZWYUVoRHhBMzd1CkdqUTZFZGxxc0ppeXB4aTNyUVFVSm5KL0IxRGJmN2crOWJvaW9PcnRGVTJ1UEdGSWtyUlhlK3VNQUw0eGMya1kKRzgvaERRUGZQYVZBWTAzM2FvTHRmY0tzanI0T3V0SHRoWHVHU1lIYmU2RzdmRFlaNkNHdGpYdUJldWg1UHo0bwovSENEeDAwd21UNHVzeFNMSG5DeGlwSFV0NkRTRUZEcEd1STVxQjNpWUxlbllLZWFGZEcxektIKy8waDI3RmxQCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHFJV3ovUHFYQk5GZFFYcmltblYKdjBNbHFmWDVNNVRsdkw4SWlzWm00M3pIN1FKN1U1QmhMRWJoQUp4cUV3TWpnTkY1WTh6ampWWE95aUVUaGQyUQpNR0VyOGR1b3IxV09VVWVOL0NGS2hTTlJSWVl2V2RBME1zYW11WWp0Qm93ak9DaXZDWjYvT0tyandCL0luZGg3ClRoeFNMTHU0NWZ5NFNrSWlSODcwNXdlWGN1NlIwcUpILzF4T2x4YWRUMTRmaTVWSm54ZGEraG15T09ZUTRVVEcKVXNjbkM3T2VLQmNMdFN1bllQQ3pJTzBDTzlMRFVOWEZDSG1ydmk5K09tMjkxK1J1R0Jid2Y5UEdGdm45anpaWApOMmdhMUx6ODlmUUYwOVozYitKUWhMWHRBZ3QyMU9NTjFUQ2lzSGhyODl0N2FTQ2lYMHI3ay8xdS9DTDFOaTN0CnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenRIS2xkNmxrQlFJbHorQUpNOWYKa05yLzYxb3FVVFcydDd4eVFkckFCRHJDb2o3QVp1UVdQYWhrVExydVdDTjhoNERsR2NCUW1oWmk1TjZEOE9ETgpsVjFEdWI0VGpydWJoSEhkT01jQW1vSUlXNi9UWlVYT2hvTlMrSjRmWGFYdVpOZlQ3MDBpRzlBOXN1NDZ6eFdiCjI3b0o1V0p6L2EwMnN0NzkvNWtSUWU1L2krM2pScFFUSU45bW56aFJTbXVleWVmS3lmd3pUNks2ZldLZTdiTnYKRElUWFJBdzFkYlJZOHg5VEpwcVM1WTMvVXhoNDBTMFJabTZPTzdGcHVERjVRQXlnZUxnVlh4YU1aL3BzY1c4cgpleEljWmpwUU1GWnRuQkFzc2hjZzdYSTdNdmVraDF1MHE0Z3AxWXlaaGVxYmx1eUR2R1dVM1haVmJ0bWUyTTRuClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXVhdHdkWnFwS2FZNXZ1KzVaYUwKci9waGdVOGIyWnJBeHdsbm1qOUxsbmZiNjhiR2txdDVhMHo0YmtTdVRiRzVSdjhKbmc4VjdrWERJNTczM2tzegpUTHpVa01RQmRVdHRXbXZKN05Ea3RXNE9oSEd5VGlEWmFNb0kwV3Znd2twODFiTG1YWXJlTFBpcEhlVWh4c0MyCkx6VzIvdHhRZzBkUUVqMHFaZVU4dWhBZVQ2L1QvUWJ3aVBmdUozMWJaQUJpVFFCNGlraisvcTZjWkdTV2xyY1UKdWs2cGp4NnNjOGVtK1A4NENXN1VwczhCb2o1eTFFYmdhZ21STFhUZFhKek12MERUNkVYY2k3blZYdGJSYWtpbAoyN0Z5RkxUaVRVWXdLQnp4QXNob2hoT2RteUUwTG5HUUZFVWovTTB3SFhxbDBkYTM1VjJwamVZRVk0eU52eEhsCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEVnUFRGM29tbnRnRWI4Z3Riay8KRFJYOWMyMUlrZ09Ma2pWWDB4azNBTk16MUxwVWpLNUFsY0kzd3c3elhUazZrNTFqRHlYU0IxQWdsazJvUW5NUworbW83TVdmNW1xUDBNMmY5d2tmd3BvemVqcHQvbTJ5emd4YnVUWStRNkJIVmZIdDc1aXMzZzUzV096eVozSGU2Ck5ySDRRRlRyUHU4MGZyUVRxanFTWnJMY3ZzbHlSNVRlMWo3N3RzTVNPK0lyYk9RM3M3YnZCVktDMHdDWnBaNGMKR2lWUkdBMEMrcWhZaUJBeU9GQTIwZXp0eGg5dWljaTlDRnhKbE1ieE45Z3BJNStnK29VWmN6QlpDRzQrS3hlQQpGS2lFQTNxUkM5dlFoUE1UUXhFWW1qWEhyWk96dzBubUU2cndKb2w5WkUxbFRxR0Z0LzZDVmlPMGFHVUt0N3ZPCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGFoaFZZMWlTVitZQnNXcWxhNEwKczhSRzRZYjhHQlhMTFV3RUY1NXRyUG9CKy9uamx2RUk3bmVURTVxMjNVbzhTVEVLQWVranY5RHhnYjl4NjFpegpYdGRDUlBPdncxcVcwblBrbTVBTW9ORHR2YkpHR3ZkYzdoMGlHM0xNM1VOdXhQbFF0blYra1pNS25RV0dNa1laCmpQMmxrOFFPY0VIUDlUOVhnVStOdVh3a2RxUEJRVzFSbGxzR3RhYVNiOFZzTEFXRi9vK1lFbWprN20zMW1hVFMKMS85YWJuNkhJaVpNMVFweE5qaGM5dCtsbUNNcStqUjdmUWM2WHVBUVZ3cjFNZUg5aTdYYjA1VXpUc29QbjdKVgo5L2JWZWJhSUJvOVNwY1Rqc3F2b2VSTjVwN0FDTjBJRHVudGxXUGk1dGNDSFlpODZJVDFuMmpkQkNIYnc2WlcrClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEwxeWxWS0t5RFNqNEViUDlPakkKQlNrS2xVelQyVTI2ay96aEJnL3F4MUk3YnZVc3JTMSt4eVNGSU9VczV3dHJ5YWdiSUhpSnd0aE93b3haelFQdgowRVNSdVZ5aElWWExWYXpSMU5kczA3c2VzTVJSNmt5SVE4M0JHZ3VrR0hlU1IwYys2dnFuYnhNaGowWXh6eGhyCk1YY1R2aWRaOUxIUzJJUEp3QW5wTnRmQ21qTm1CWnJsWmxxVitBMlQyaEJHdnVZeExuR3ZYRnp5cEhHM1JvTm4KQ3d4SjBPVUE4Rlh3SW1ZVWdOSmE0WHVud1I2QnhpQVdyYVZYNEJJZDZ3QjV1RG1yNzVLMkdWTTBCaGpZUmxGZQp4LzdxZ1FKbTZ3VlNGdVcxZlhULzYxazhodkxpU094UDV3bU8rdWNCbGU3YXJlOG1yQzljQllpdFh5bjZSc2V5Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVIralRtajRZZDAxVHdmWnVHZXgKZHdsdkQyck5pOUErNUZrSGlNcmlwekJRZzdRMVJxZjl3VXBQU0g5SFMvNDJHeGVHaVdUK1lwRGVjZFpXM3V4UgpJQUprYTJ5QTV1Y0dmYVFSUS9nNkM5Y0xpcjhyVG93cllBd2RvM3ZZMVZGV0FxRXdVdmRBN08vNEdvYTg0RC9XCkkxVGhNSlJSZHozb3owd2x1MGZibDJTUVVYL01YNmpuZDluRkM4V3p2OFMyKzhLeFpBRk9KcDhzQnJQdGNJVi8KQzJjRGtab3M3elo5MkJ0NUw4MGF5QXczZHE3Q1QwNndwVGd1aTYycXVWcjJKR0duRlBjWHNyYXE4RUw5RkpEKwpSR2lnUkVrd2Rnd0pPNU9TcEo2WDI0TjVUbmUwY0RwQ2hGZXBuL1UxUFNZV0UvMzVBZUJlM29pOVBveUxTWE12CkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnBkQzd4K1hRYzhVVnFrSmJLY1gKUDEvQnFmdFIyZ0RERHJZMjdwbWhZQUZ4b0FKVG5ubzBMakttTC8vQUFObHBvdjBuRWg3Sk92dnY2cC9PY201RwpIYVY3QUJsNDd2bVZDMWZjQ01lMC9oaGwrVGhlTTJxWU9OakE0Y1N1dEljS25haU41TFVSalB5SHhZS015K01xCnRSQVp5dXd3RndPdjVZbGk5VXZzbUl4K1U2QktWZmI2dTJuR3VhUG4wTWp2R1N0ekdzTWEyZjkzbXI3RWovbUwKaVJwNkkyMW11Um1DbW9peC9yTElnbmt5TTZEWCt3U0hvM2NCMGM3TWRVL0FZY2pkUVBsMkh2UXlMMDNBU2JQagowRmF4by9PYWhjM3VUYW80bWYzeC9nYWdVS3VNdUlHWGpBQ2ZvY2JleGIyaDROdGo0bkJ0NCt0VVYyMkl6SWN4CmZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUlJWkpKL1ZqSnZ6S1hMczhhSTkKQXgzZFRRcTE3QmxsRWxiY0ZmNHZMZ2txczhlME1xY1Nwa0tpS3pzODJJa0lHRFdmYVI4VW5DK1d2cTJodHhFcQpEY2xHT1pSQkgweW14S2FCcE9PYzJpeDJGUkwzU2dZVnBNeVlZKzNLMkFiYjZrb0htOFBGaVRDdEZJRkVSdzlOCnhVRGdkMXAzVnNuMHRvR2x6ZkpwazFud25yRjVRZHBsNTFBTnRONndvRXM0b0tyQmZVL3FERGN1bW5hMHlPNTgKMWxyaFdJVUU1T0oyZGxKM1E3REJ0NWhPZUsyYUtqYzR0WktVbWVwRTNXQkptRmxyUExJR1lyYjNBZ0lNOERWQQpmQnFLdFVCRk5HeTByTkxPM2hsTVZnY2xYN2oyN2JMdC9pWHRWRHBVbFk1Rko2WWoveE9xUHJBUGdjanN4U0xPCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHRFUzF5RkVvMWxSL3ZSVWJDWDIKTWJLbFZYTlhxRkdWRzVFNGd2SXl0a1YyUFd2Zmk4cVdrTzU4K2hCeTcvTldNSjVPZDk0YkFRMy9iTGF4SGlHZApLMWxpSEcvYk1KNXZpald6eU1lcExDMzNHMDI1YlpVenNvdkY5eWk0YmZxY2VjKzByNjNsaEYrSGRFeFhBSEFmCm5qYWQ5ZG84YU9Oc2RXemh4dmh6d05LY21BZWE3U0tKOXVJZEdHRmJOTzVvQmhzWUtWOXF3T2Q4SnRtdG5tWVgKdk9WQ0ZTU1ZsNmMzUVRhdDYxMElLcUdZMU5JRCtUVnYvUDZaNG15V0RqY1NyMWkzRnZXYWptV2l2cmJtTWVvVwpGcFNWd3lsSXE4MUdrWHBlVFpVYTRFb3B2d0ZIdjNkZHBnemVXSkpnK0lxc256c0dTdFUvakpRSXZHNVVtZEYwCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUJRa0pMcjZLbVJjeUJaN2xWcEcKQ1ZGUXQrbVJBQk1qaWx1ZVVpQUVzbEpiSVl2aEIyL3FFc3FDNlV1UVBrR3l5Z2c0UWdQY1VqVU8zcTBIZUROeAo4MS9KTm84VTdPR2YwT2NiczJxMTJUVllRTXBnV3RoanVRaVFpTUJiT1pUbElVN1RnbHVKbFg4aXFVK0dodkJrCnV3dUxGZGlYZlFTSHVYM1B5N05RRWtiY0RPTUFseXV4OU14RHdFREVHV09vOEJVWE4xR094NG1GblV2NlE2S2kKMERRbCtjaC95K1B4OXVKclpZRWpTWWdSVlFmeUR1WlN4dHRqTlMxT1ZadFg2bU0wSXRnYkhGVTFFcnF1b2NFSApYR1pwZndMaDFqOXlWanZGZTE2NW1IU2ovNXgyeVFKS09nUWhENmhuL0ZUMjFmNkxUQ3JISzM0K2hWamU3S0luCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVYxd2lreFYyQUQ1d2JZNmJOejkKZGdjQ21OcUhvb3VsYWUzMWFpeHc4NWlCTTVwMi9kb2I5RkJrbTZ5eUVObjBPdFZidnMwRDBNdS8vcHFLbGdudwp6S2xuSElyTkVieFlydTFGZ3l2TUhyK1dsNHBhcmxqWlVzMW5NQWJQU05OK2VtSGd1UG5KN0RJT0paNVl5Yk51CmR1RUN4Slh2VkJMRTRQTlN4SWJYZ1lKbGZadERkbGJ3MVFNS3l5djZmUXFHM0hPYU1rNFVMTENVTW9zQ3NTVm4Ka2M3Q3JWYm9RZmVYc1Zqajh4RkNNeDJSV3BxMm1YUCs3YkpuL2NLMDZ3Z2dIdUtRNVBIRkxGRm5Ua1kxdVpPSApkOWV3bXNmdTRBRlRkaUg5NDd5WWRFMElaazZxZG1CNlBlVmtUUEVmdURXaEJybTluVnVDMllyNVVOUTRxTjN4Ckx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGthdnMvVjdPL3NTb3VhVTNaNGgKUG1iUkxWeWdISCtQdnVDWmR6Y0tLSVJUaW1ZMndWSHBORFRSWkxsVUVlanJuaUtSeHI5RnVyczBFUU9qVFB3QQpjbU9kVWpsSmwvUldxRFA5RWp0eFJQSWxCVlpReHQrV25ZQzNkSHA1QXBQWVV4cERHOFNNQU5vV1lsSzBoYVBPCmVBbFZQRjRvQjFLb3R6ckZSc2E0S1ZnR2RvSmpGRnJNYm9oTlhCQnUycDh5T3hKNWN1emR6WVNsMWZwZHIvU3gKTWRSS0ppTHpKTVplOUpOUUZZdWJ5ZmpubnlVNE9KcGxQaytKaDE3NXNHREZocG94NGFQdjB2TmVzY0F0ZGNrYQpJWjM1YXM2NjluRGNBbkFEZVlPYzI4VFRIekl4bHYrcHNKR1Q5MkhnTmFrSGtpTFIwZTJYY2FGUGNDb1F3SEZaCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdFgxV2V6WUlnMDNFWkZUTnBxNkIKd09QOSsyQ0tGYmNWNmZmbUVQUG5uMWZEZ1o1WmJmM1NZNzlmL3Y1ZXlhMFpHVG1aWWF4SVpFTFY3N0IwYjA3UwprUmpUNDVKZ2xsZzJmVXA2SW03ZXVrTDg4N0RLSk1QUkJHRzRydVNlMHNjZnBlMkxkaWY4cERKZkhuQjB3L3RwCnRIazJmdXNkN003dHJ2d09OSHBrVmxvUDVIUHZrdldVcnNSMCtjRmp2UkFzdWZJQVRYQzV5OWw4b3ozam5Oa1YKNVVJL3h0dUJlZFpUUndXVkgxUXJnUWsyYldESjcvdUMyamhod292QmVwNEI3UndqeE83SFAzelFoSUduaS9wYwo4SFF3c0F3Z0I5enhTNGFRZFUrczRtK3VWaUJYMGhqWXBLQ2gyTExmU1U0TGViY3BTODFGQ1dzdEhvczlHV0FkClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFcxcXRhS3AxaDBwTkpsRk9tb24KV3dkZmRFVTZ4dnFCNWZ4NlNGVjNLNTV4L2ZUc3ZreDByeE54RnljaWljb0JFRG5TV3VyU2M2UldPeXFDRjNHVgpCZ2JHdExOYVc3YitGY1ZXU08xNmcwcko5S2tMTU8rRTRtMU1PU21MQ291RjFqdzRVc1BkYTVJVTIzSVZyd3l3ClNaS1RmdFpsVjlTM2VNeXNEdGZWUmVSZDZzVDEzN3RJVFJTZlEzNklnYWhQQjdEUGFmU09iRDl4U0xBRFk5YWMKZVBiSmVtdkNFamF2NE1jWlRlb3V2cnovN0ZHT2NVQ2lwTUZDekVtVFBzUXZ4L0ExVUdraGFla1hjOFVVZUtHSgpNbFpjSUt6dy9TZ2hlem83Z2lzVy80cHplTXp0UWRwN1RabEFzeEtNY0YxeGIxWmhxK3Y1WVlCRGdENVRiOEx4CnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0VrU1RJOS9VZWxDK3U0bHUxNHAKVFlwZGc3TkwxK0cyR3V3YU9yYjNLbE1Bcy9wbXdnYklGUkh6OXByNm5NZmx2RjA0bzhaaklZazFYaVUrbGRMMApXbWlRa1Q2VGRqQ1dOK2RYVUhUZ0xCYnpNTGVoeTA0TFFieEh6L1k5aU9yZU1uc0JKRmxHT0tabktNbEZtRTNICjNUM0NRNlVpa2Y2bldxZ2dTdnhGSGI1aXZMMVdGdEJvSlN6bDVaSUYwanc2SGFDdXdMTFlScWN1SXFyK0Q4VngKSXZSVU9hQkR5Z3ZiOG82UTBFUlduMTBKVW53aVVvZFVYSEZEWi83R2FlYkdIZ2ptUGdpTnYxaUF2T1g4Smt0TgptMkZGTHk1Y25PRWNwUDRkME1wZzFDV3owNlgwTEJ4VHZqV2s1cjRhZkRMbEk0ZmliUzNPQmtoM3ZEa0ZBbTc0CnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOGJDMFdBSzI1Sm9lZHViTHpCVTMKL25EOGZOeUJpSG1tRFYza0NMZExXbm9zdUhiSGpXaTdtalFtN2w1UTJybFVsQWxZdUNXU0QzY3lKbVBoM3YwQwpMUStjaWZIeWVNWEROKzY3ZFVaVHR3TW5SRS8wVm9LRFRJUjc5SnhMV2JHdTUvRkhXQ1BwUTBxSHYxLzdheC9TCnNaemxqWmJyTUEvMVpMbGFpYmI0bEVNWHFNMWVkeXMxM1BscHV5UHk4NzRDWExySlMvT29sVkR5SWZDTHk4V0sKTWtzWlB3cndZbEUzOEM4c2dib1hjaUdZL3kwaURSZG82MjFaWE1BcVZsM1BjbHgyM1l6UmdmMGJINGlSUjN1RQpoS3NCVDVLNWxwRTNQdzdqQnV1TEdMbFhFY0hCK1llcStIMnM0YVNlK2U0aUZRV0FaREhhQ0xsTXpnK2ViSnNDCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenlnOHVWK0NUL1llMEpFYnpXWnQKM2UwQ3pEa1F1dTRPcXZpNmIwWUFQbGt4Um83aU14Y3g0N3ZoeTByZWlEZ1lVSVhHL3hNaVAzR21zMzNkSlpwdQpRSHYyKzdLeE84aXJ3WlllQWl6WjJyYTBkclRSZGNER3hJRVdyRmJkNkNxYUlEeUtnbTc3djVpdWZpUFpneXRkCjBLbWp2bnRwQUsrc1V0LzNidy9XM2p0amJCYXJHU2E1SkJhZDJ0dzJoOXZwdDJ6S0hvelRtYjRHT1J5cUdTOE0KNlpKM1ViSVVmVFpFQUVhdER0Q3JPdTdWQ1A3aE1rcDF2OExkUzM3V0hXbzlnZ1hPQVJxM0thNmNFMzhqbU1RZApnNDNhV0hJMXlQQW9oak1Cd3lwV2U0eU9oaGprOWRpMENzTVJxT3ZlQ1Z4b0huZGVISUEycmhzOUVLMTg0VldSClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWsrL0ZyemlMSXVXQ1V2YytXRmEKNXhzWWxMTHlLTnFYdWtOd0RwdWRVTUlJUmVvT0p1Q1VjbUZKMFlxdVoybnJ2cW8raHB0VHVBWFNyVVRpcHFrLwpxMzBLcU5ReWxsMVMwd1k0cEdVMXFJNTRaVENZcUhXdlJxS3BOUWtwbE56cXpsVHFsQXE2Ykt2RUkzY2ErRG9pClNSSEx4MDNpM0g3UXd6bWpIdW9ncUtPM0Y4QU90OUk5T0xpYnZBKzdGZkRtSWowQUp1TTBteEJ5RFVPQjJQZEwKUC91eU9oem56b0owWGppckRSSGJXSldFbmRKLzlYM0NEdTRVNDFCTjlzNW10U1BBRnV2cUJTZzRpdUZFWnB3cwpyNjhDQ0VVSE9nVjcrL003bEZlMVRlWjk3ZUJFUVh3ZVcrc3VoVlhRZlh3cDl1VjkxZXNJZmUwclBMSFZjSVJ6Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEpkN3ExNUlZMHBMOFNlZ0RUN28KQ2xZUkNGWHoxSDFmdnVZWlMrZHJ0NFh2bS9jV3dETkx5K290RlRYdG1tTVV1YjZKRVZOOXJDWW5GSDEzY29NUgp2V05nTUtGQ013ZGVzL25ZRXNtU0Y4NGZ5cVplSjlMSHZFeXNKdk9Ld0V3Ni9Ka3FGR3BXMXNZekt1aGtBUjNCCmNORy9LZGk4dkd4ZTdWUnBOUCtha0gyQitVTnFNMnNub1lHcVhYZ01BMVllNjdmY20yYnJ0ZVZCY0tjS21tOFUKSW4rWm5OTlBvMXo2aVRMNm9oYktuTVowRjREc3dBcUxzMzVHVzA4Nldnclp0QjhXaDRRYU9vUDVtVjNCcXFCQgpZQXFJTlFZWk1KaTZMQ0w1M3MwVS9LdEVtRFpuRCtia3Z1VGlVYlVxWHFFYWpCcVNwVGZVQ2FFNEloaWlpSWVaCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2lIZVcvWnNPVTNHSDJtT2k2bnAKUnJmTjBTa3haRlpEYkpLeThBZ1J4aTJKR1dPNEpwZ2ZNQWhEa1ZPL3lTTGR5OVNJam9YMHNJbVQwNEdPVkh5cwpOQWtlekdzeFR4OW0xSGRFeVFRbnZWOGFwWnBGYUhtN3VmODZSMG04N1ZWM1cyNE45YldSV2ZrQUI5SkVZU0QzCkVuUDBNdnRwZUJBUmNYV2Z0bWtyVnBkTWV3WlRwZ3VGd3BpSVVua0lMdnhHMFl5a3dyVWppSTBuMkJ1eHFnZmoKZ1RWSkJRRzNxZGh3VW00OEtVY3drclBhN3BpN1pmVmxQT3A2d25idEVlUXQwaVo2NHdrMjRJU2dUVkE3MmFVVAoxWDBEdDlvYi9OTnlsL3JQUk9jQzJvRlhabFlkbCtuMk4vUHZ1RUhrZUY0SGl2emRUK2pFODRzOFJsMzB6Nlk4CnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0M3bldoVUhIOERXMlBFSkM3M2QKV3c1NkNxZi9kaHBBSWZWMjZIL0Q2VnNJVWIzbEs4Nm5PQ0tYb2pNZTFXN2g4ZDZrRE5RenBQdi9oWWFMbVcxbQpvVTFFQVNkdW4yQXZIY3crZ1l2VXVmVGtSdUZSQjZmeXdyei9UTG8xWTIyK2J4cmE3WFQxckFjVGRjMnNLbWplCnBzeWErcWxyY1doOThMejFmUHJUelNiRDh0YnBKR3dRZmt4WDUyME5LZWF6eVpta3NuVTFMYVd2aE9KSHRFcy8KNWtYQXkwajlOOVRUR0hENFd6RTVkaUFGUmtwUnlzamcweDRiNkRPYzJBbXkwNFlKTVM5blBHbE1xYzIzZVhlZwptNEtnY3dQTktoUGFjeU5kKzhSMzlKcFVzK0Z3cFg3KzVrVE45dVZBZFpjREVPb3Znd3JkbVhDU2tQVVRPZ2l0CmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmIwb0k1bkpacXlqR1F6cHVBVUUKZ3lsNW5BUkhHMmJqVWR3SHZoNDNrMmM2Sm5rTkdiRlVnV0RyZzk3ZVd6QjB6Qi9ENnlUdnRaamk1am1RSFZJTAo2U2ZkUWljUGc3aXpNZWVmVERCN01NRDl3V3lOZGdLNkhEMXNPdGN5MEV0a3hCT0RkcS9GV1lTUkQxR1lkOWRjCnhYV0pUZ20vb2pVUXdpdkxFdGxkNDl5UEZaMElEckJvV2NCaXFmRGlpMzFOYXpOR3JnVWR2VW1iemw2QlFySGYKMm9GeHNWRVc0ejd2MC9DTzlDUVdkRXRPNk5wYk9WRjFOVk5leGx0K2JVTWFxc05FZ0JrUXBmQlcrajdMTjFUYQpYeFU5T1B4aVJQT1JHZjBka2hVU0JSSTBSSmZsK0kvVDJBZkk0cWJDbEFJVEpVOUh3SHZUTnNneWlyMXlGOWxSCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1g1WEZ1SFYzYWdoOGI1emxzZHIKa2d1MkxUUURydXFUSzFBbTd5OFVNK04vQlJsS1JCWFJVa0JKZHdiKzZyZzlFSFFWTk5lRW5vWlFrcWtabE1nTwpJQUpjWVdkdGtWb3RaWWZQT1FlaGkyRHpiOUZhUVByS20rVVNnTWU5TjFFaWZKU01CQUZ5Z0FTNnZwcEhoblQ0Cnp5U2hLZmZLc2ErQWFLS09YVWlEdHI3Y0xJNkhLNlkxRTNXNEJjRDlFb1dZU0pGSUQ1ZTNVaXlIV3IrTkZQaUEKZlRiSHc0OHVvK2FjOXN1RDhRV2hWVFBrb1BVSDhSeE1QU3F4elVGWE9aOGZWN2FjQ3ZNVUh1bWFZTGwrendubwpST2RJNlJuUGw3TCtPalhSaTFUNjg2OGxnK2EvdFdJT1FUcE5nWUtCWFdrMEg0Z2RaUCtERXBiU1I5ZWd3TjhJCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUZrd1B4NUxuYVJQa0J1NTdoZEgKUVNiNk9OV0E3RUJoZmpySVpTVlY2ZzJWSktMVjRqZngySy96SU1MRU80czR0V0oySngrbU9NK1JndTM3TG5pZgphVWVNUmlzZ2pXbWNCUGJPQUNsMWpIYzhqTGp5bmx5OXpMNFU3SnhoT0NJRGdCa3BNbjhYMnJrTkdHenM2cVB6Ck1lYTRTamlySlNHSFNKM2hjQmN1bHYzUmZPdnlLL0lrRGJvc1Nwelh6Q0xydzJoQlZaNHlxdExmSGZpaGtTK1MKNU8zSVdnd3FhZDhkaWtDSWV5VHRya1pHejJTYTZlcEpTWjZ5bW5GOGJEenVkMSsvRzlJZStzMHd0WjdoMFVsUApwZ3dBQnpjZUlJYmpBZUpxdjlUMmVMYTJWVE50Z1MxSjRORkJrdm9icndyMWtFb3laL1ZtVmtnZ2Vubm9yeTVLCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVRRQ0VwKzNhOUc1YWNSc1N2M0oKTUxvM2JNK0dsdU0yRFI5RUtvOWk5WVhlMms3c1NNcTJHVzBJdTdCNitYNng1N3JjZjU1a1o2eldjNUQzUkRuOAorbnpPczM1bUhCZ3UxNkFEOFBIZCtkU1RDQ2lBR2loYVFKYlBJdnIvQXA1bGdmSlB2T1ZVL1JFS0pLMFlNT3RDClB5SCthRm5sYis3L2t2OHZobGpvUnVtZmJ6YTJqN3Q0aVFONThjMHcyWjdBekN1TlJGY1dubkU0VG5wTFJqbWMKODIxbXRLU2JxbmQ1K1NBUS9YU3d6UElxNUd5bVowTTY4MEV1aFVhS3BhRUNMZ1pxbHVtaE1jUk9zSGs5WUR3UwpsTUIyVkFib3NxY0lGOTc5ZE0wNWVoNzVyNDljN2JhWWF3Z1pEbnBMSmQvVHJaanA0bk9UM2NrOHZZOFY2TlVhCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBODIxZTNHaGZSak9CMVFIdytmNVkKekJrVnRzM1kwRTZnaWd1dnJQa3Azd3lZSk56VXUzTXJjWnlSbk1SbHJXby85b29OdFJ6YjlQVGwzMEJuUFVCaQpaeVlmRVhHaTlpblB3UGIzaGlPVStoL2tjdUw0MllqRUp4N3o1ZFRYdXpjeGkvSzdETG9tYmxPZ2ZPem9aQU95CkoxTHBhNERpM1J1dG9ieFRuMW11L0k5Mk1GcVU1Q0dTVC9SRkJYMEhqMk5lQmFPOVJZM3JQL2NuZ0RubDlUWU4KYTMySURTcnpnUEZUc3BjejJNOG9iVkZtK3lXdFk0ZUxRYzIrY3h6MjY4Y1c2ak5KTDBvSTBNZjFHcXhyVHgvawpOa3I0ZVczZ2srSE1EdmZSQXFuOGFlTHpWR2gyV1JDczFnUG5VditkQVNySnpNUGVmWFR6SXZsaTJyeVhyQXFGCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMlJPc0dvd3lzeG5xVkZqMVIrVmsKUjhxZmQ1dWtnNjRVZ1oxTXhuekZVZHE3NWFqajh2MDF1bVFrVGp1Wm9SZzlPWHRVS1g5WnFLeXI4TWNkcXA3cQpMc1NoTGZNNy9HWVlRZDdHeVNYcjAxZ0l0dUU4b3dOekNLNDhKUCs1N29sQ3E1a2xPZGJ2M09Wak5iL2JSS1BxCkk5NEZNSmNVYjBETDVuNzk3amZXanhXMEx0VTVxZm41b2t3bmRTZzVsVzF5MFN1WjRUcVBaTDJxUUJnbUlCZ3kKeEt3b3JqZzM4dHdWb0FHeGZIbFVuUytVclZmTy82Qmc1VHBwbnNrVjduZFNsellwV21heWpRRXpXYWVselBPeApkMExORDRSc29rVWFCVW9oYk0vMFBaNW83bUZKTmVHd3haaFdNaitRcjA5OU9VT2pSS1RlRG5iaDEwQVZtSFo2Clh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFUvSXhGbHV4TmNLakh0WVlwVnMKTCtrajZGckZPTFZNbXE4TGdlZmlUNnF6eXlpQ0V1Z0xvUGx2Snd5ZDE1T1hFa3RrQWRNTUR1eTBYSGF3bTJHOQozbnk2TVVMUXA2QlVMZlZVQWY1M2EvMlFvUGdDNDM2YTh4LzMxaUovUy9kKy9JZGVvVzljQ2tkc1AvZ2VvWGI5CjJtNVFQdUZZOFhJcnR6OXN5a3FnRmNGV25Id1lQQWpMampDdG5mYVpFZTZGbUdYZnY4Mzd5VTNCUW1URFJueUQKR3BlK3VKMjVHcTdKeVlRRXlPOElxa0svVlhuRGQ2NWtjNGVoaUQ5VWFCRFBUZUkrMGNtc1dLenB4WlZRcVVWWApzWndOZ2xMaUVib2R1cTBza09QVWxoS2dTTnd4TXA4WVhHZmVlc3hmNHBRUExUUG1vSHlZcTZNME8veGZ4Nk1qCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0g0K21oY1ZleTBCd2tac0RleXcKcGFUaFFjWWxKL2dWczQ4R2hOU1ZlSE91UmR1MlJBSDdJOG9hWGJzajZvZUpZM0tLL3dBU2EzVzAvYkg1c0VqSwpubHZiVXZCWmZTM3RxVENoMWhRK3UzVlNmRlA1TWJnT3BBWUJOTllyUUVteWNCdmJtUUxzQ3doRmFWR2JnbGdrCk16TGtUS21XWUZBa2I0enh2RkpMRE1HNDVKQ0UrelNXcFBVZnZUYWRyTjA2YWRIamJSeE5jZEwySi9YL1hnKzIKd1ZTNy85M0FJSlYyZlQ2Q1d2cWN3YlJlOVhaVy9lRVRUNVM3ZmtBanF3czZralFQT1JnNjFveW1SeExxSk5xTgpFV1NQcG83RCsyOUhJQ1B0QU9IaDNUMWZFR1E5YkVjVlZ3U2ZHL3Bnam5wRDJDbWlsREF0QXViY0ZrSmJkU2hLCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmNyUFRNMGE2ZlhVSHZzZk5BbmMKU3NVaExlazR6MUdyeUNEd1d5UXkyTzBmeFlLMTBMTkNuVU1rVVJaOFRZakRldDM2RjFpSlNydTdwb2RkQjMyVgozNHVEdUdEcHphVUxCYUdNUXRHdEREV0JRVDZ3aEw2ZTdRYTRYQ1NxbHR4U0JEa0pRaG5LVlo5VFpzaG9ud3E3CjFOQlZIeGsxb1U5YUtkY3czVWxMWXJITUVNcDBOV1A5bG52dFJ6eDVNZ05uRHRzT1J2SXlBUnRjZ3VrQ2crZU4KSEM4a0xTOU83WS9TVXJqU1ZkdHVta0g3aUVRZ1JreGsyOG43d2pGS3dQU1NxNTRVWXZUb1lHTG9mbmZmWjJGSApiZnNPU3FQelNUM0plcFRQeWJjaEZhRDVUYmdDOXdRTTZNbWUrc1FxK3BEcFNIZWpwNEJhMkU3K0krcDVIaVBXClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNjJFbFdWd2JOVHFYU1VMTlc3SlMKUHBoYXpFNnd6Uys1a2VwV2V4WFdzN0tOZ2hwV1RTYnRoUGpDODhJWFhMaWQwN2NKVUJLNjlGeXZQZjRuNG82ZwpMejQvSUJvRG54TzlRNmRMMEhoSFprekFWNFJaY1FiSE5OMmhWMVRPWmlSbGthTVVEZU9tRnFyQWlBZWwyRldDCjJCaE0rcmMyRlYrWDNqaFQyakEwUjFPU3ovTUVyaVRlbzM4UllCUHhOeXRqaStEUjhoQW5FS3V2eGVLYVNkR1oKUmVCVlRxVTEvMkFZbUxhTnV1OUI2UXo3c01mZmdwbi8xaW5MRGFDZ1pkbWt5bDV6MEE1TGxlYk4yejQ5WUNXcwp4cGQ0eW9jelpPM2N6ZEpPc3p3U0tFdUEyZXFxVDJoNWxXckl3LzQ1a1ZHTmFhbmtDL1pMbTNBYkRYRm5TUWQyCnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVh3MVRGa01ZVmo0V21pMmpVNTgKT2t0cTYyMDdaVlFxZmRTWEx4MU5lMll1S1FUaEhJeVBuR2ExSDZnTmZIdEIyUkwyb0trM3psWVVRZGs5U3JQaQppNGdCWVd4czNYdHZUUWNOYTQwK0NlUk1nWkhsN2wxMXRsVVpvYms1Wmk0OEt3bGh4ZWNRaW1OTWJwRGtpd0xlCi9zdzZYcGVrYnVIejVySm9BZjJQNFYrbG1vS3pHY1NMdWNibUY5SnlFZ0lkRE9NNSt0Vm1sbFRBNjEwYXdMdm4KOEQwQzhVZkVaYjFvemFPUXR2dW9xU3pyV2drcG9ycVBwTmd5YmlYQkw1YS9GN3hubDZTd0Qzc2lBUmtzaXNheApWK3JuS3VnMnlDemJROW9OeGpIUGdPdDFmR0VFTUlmMWNacUFqR3R4dkNlTGJ6YU9lU3JqUUxMUjBqdkRUdkx2Cmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN09nbTYwak1nejJLWFg2elliak4KZkt6TFc3b3BPYTFOb2VaQ015R081UnVBbW9tbnQ5L25DMzZNU29UelZuR1I0TU9tOFhKNVk3eU9pRjNJV0lLeQozUGdSRlpUMEdpSHc2THUyS2M5ZGhTS21zWEFWQk0yaytHSHhvUzlHT0ZmY2dDeDBUNGEwa3FackNqelFsQ21LCkt1QjR0bGRWT0NWSDZMUVBJOUNYcjF5ZEhWUU45TEp0NGlMRnIrRSs1VUQrSjlDb1U5VzdEWXM5cEJNSXNMS2kKWlpmVUtZTG9naXFTZkVudmpwM0xRMWRNdHdpZ2pMWkE2dDlNSDJJcG12UHVSdEtqNUxoUVg4SWIvWmwxK2MrRwo5akxFVk1paGpzQlkrNHdWejVkWEFCVnkvV2hra2Q1WllEa3RyVnZZVlNNL0dEQUtQY1RsUkY5dTZTYUsrRmpJCmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNVZDQjB1U0hlOHRxcU96Q2FBTk0KdXZXKzM4OHNOMkRTL2Nla0xHSGhERUlMZEUxelUwS3NpRS9wTU1paFhhWkZ1eG5jUVc4eWRFam00UUdsckFEaQo5Njh4ZG9OSFlERVJIUFFDbks3a1pnS1pwUDlYWThkVmVmOEVsSFdVancvdldBdndIblk0cEszRjc4L3F6NWd0CnlEYWJRTTdRY1hON1RiOUxEQTloaXVLbkdpUVNXZTUvWUYzbGRRZUlZakVHMGhTZWlJaU0zS1FBN1NkTmhoNncKMytNNTN1R2JKN2lXWng3TGJ4bzhXSERQaDlvTklwemNyVS83bEhNcHMzekpDT0lMblpwd3pFT2h4K1NHTFBzQwozSmdhY1YvdHBTbmkxb3Z2U3ZCaDdYdlJqUU92dCtpRUdTb0VyWWJPUjliRytjdjVaZ0FOdE8xL1BzUE1uT2tQCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVpRTzNKNlE2QXZYQ2l1bzl4eEcKOGovZ3N4M3JUdWpvb09FRllIRjl2anZUMlJGMDduTWVyTTlxbmd0NUVvbE1INlIwaS82Vy9UOVk4L0JQV1ZtSAp3N1B3MUoyeU5yOG93MDNhOUMzbitHenJuNW1ZL0pJK2lzY2F5eUlRbldNajNBM0lKSTV5dENtUm5aUjBWME9pCkhFYW5sNXY1N2d2S0IweFB1TWZkZURjb0VoVE05ZkE4Wm1ROHBHajFJY2hVdk9uVmlXS3hLRnZORzhTSUpnRnUKMHlQZ1l3bmpVbDRwbnYxRGZxYUxFUVpleks0MDB5bEZwN2lKdDcwMndrelY0c0RTbitoSzRzdlBmelBteXRFdQp5TWp4dGw1VjdIRGdYM3lYOGR3ZE5qWGRnWndScnVCeGZyd2JLR3RqU0RmZmIwYmh4aHVab2V5VlNXSUdydm1xCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb21ORWEyRnRRaTVoeDVwS2g2aS8KWG5UYUZ3b0pLS2Y1K0ZKcjFuNXFQWlNVdGVDb1VYQWc4UnRYN0tCVndNNVA2WXJHaGNzMzY0ZUEvZTM2aWQ4cgpZYTVTUHFTdjFGYmZNb2NjeFlVWjQyYk04bnNPb21VYm1lWW9hVG1hcXNSdE1FT2dZVm56R3FjNFVYeEhtMFV1CjFjZ2Q1QnZzUXh1TnpYcnZMUHJhZFRrRDdpUmEwTkhkV2x4R0gvcURrcTlYZmlxVGlsUkFkSnVMWWZhTmVHV3kKTVllVENjZU9WUjNzNEJad3NRMTMwMk8wQmZHYWxoUGdUbTJ3SXFMYTFwVC8xNW5zUjAyWlJaOXhPNUNabFpHVgpkd3lCUlhhV0ZKVllGU2lkS21ON2s0RnkxT25GaXlsZ3lQRHlnTi93Vi8rVWE5R0djeVJnK3c3cUM5TW91eXFRClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzY3dGR0cmJHMWw3cTBTYXZJVGsKZXdwKzQ4VU14djRhZXU2L1J1SmlTQVNuWXVteFVEQ013M0hMUDFmSmZkWFRpblNIRG9rNUFuTUJ5ZnljSWUvVQpVYU91UDVzQVRVYm04NUkrTVpGMDB4WVlTR2xUOVp3cllOUTFlQkVvYjNqRzBTeXVYa085SEc1UFpPQ0NqOXZmCkpQZjRiMWNWR3FmZlN3RkdXNU0vVEhRVldWcFpjUVVsTFJJRVc2TmhKeGt3bFFOZFJOUVg3cGZUbmJVSVNzVm0KejRjSWFadEtkYzNhYmxZclplWjNEQW43VEN0SDJpbnBhQTZvL2U1OUVUTi9NL1ZIYUx4RThtTEJISVQrY3hCNQpxNXBhZHNkT2lncGtzVTN2elZkRFZFS0RLSjFCRHkvd0gwT295SHFzOW1namw4elJ0OWJXMGY4cEx6aWRTOFhRCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1cvRDV5eWt6ek1abkV6R3lXY3cKRW04MGxEb05zVHlONXQrWitGNWFzczZhYW1UN2lCT2FYckViVnMrcHZYWlhHQTFia2xSWnVHVGl4dUlVdUtNTAp4ekN1Z0hibWZJUy9PQW40VnlETmcvZ2hCRERtN1BSbUpDc0xyR2QySHRVcGMzM1d5anZESndvN2MwTFBpZmVpCmlqQ3pzN0FEMHRMbUp6YnZ4aldNK1laNDNzVXE3ODhiYzlieWszeEo0ZUZIWmJidlIrSE1oOEJFYUNhY0xlTi8KR3VHZHZOeW5Sc1J6cStQSlVINHFkRkd6bjJ1QVhJdllCM2oyeWFzYTR4SU1ReElmYXJKWkh6ZGV3cXJMZTZ4UgppMG9KR1NuUGd1dTBIWXV2Unh3cFVPY3o1a1o3QUpjUWNhOGRqanJBalcrUmxTZVNHWHNtalNBYllUYzJpdXY2Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGNnU0dkeldRN0k1aEN2cUkrTjYKK09tanFzM2NFRUtnbmZad3EvOEo3MFZBeWNSQXdDelQrMElLNTFXM1dkaitsUDR3SVk0M0JVeWtmNUZYaTZ1bQpQc0dRc1FkdGlkVXhMRDBLaWdLTlJJN3VnRmh4YkczK0pOZXhORlFYb0lMSjFNQVpPMnM0OXNyTHozUUVCendkClhFZUd3aVlEQktMRVRZYnFTUENybTE4T0JYNmxrcXNVMHVMcHJ0elkxOCtrYWVrQTlUd0g3Si9qdWwxTTZSVWoKdWNLWkFHUDdRMmVFcU5CT3cwQzhab05POWpQOXBKRG8za1dnSFZaQ2ZaUXZqVUlkbmpBV2NoTmJ0QmhGWWFTUApGSHYrNHpRdUFBdjZEN3J2Vzd4aVlndEVPTmUvMURqbk5zaDUzYjRUeTkxSStsZTJrYllzVzJaekFDQnNrcHNICjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdVlQSllUZHQ2ZTVHZmdIVC9wTWMKOHBtWEVWNTg1N3lsRHZoU1pFeExhQ3p5a0FRQkVxZmhDYXVxdTR0Z3lCUUZERnFBSmdwd2xhZHJQdENoTTNpQQp1TWhDMG5hcytoeU93MTBGdURoZE1wNjg3dkRDWHhPT25Xa2hPVkRmSkJjTEtlMitUMWE2bzAyUHZyVkd3UzNlCnF3blIwV2ZCcTBPY01zWHVkMTBDdXRVaUgzSnJoOSsxeWFENmludlV5RGN3NnoyNWtSemUrcVIxV0F4a3hKOXcKSFhTb0xVOVdsUjFiRGZVNUN6MTBxcHQ3NlFvR0V3TEJNT0Y2aE9RVzNIZzBNZkYxSzU2bUhlZWJ2VVE1QUp6UgpUdXhJQTVSSWx5anNSeU1ESU1kS3BQN2NnU0N3ZFhQcWVQcGFqbk9WbVJjS3grcEloQ0tDc3EyaHJSdnhEWHpVCk93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbWlNa1JoWVF6T1lMbi9lbzhzQjgKcUplY3pCbFJQTTdtWDFuVFk4aVhsYTVaZ3AxYll1YnFZV09ic2U2V0pEb3FWMmxGTGZZVm1iekF2R0lyNG4vUwpIMmE5Z3piWHFveTVXTlZnOVRCeEV0RFdqU0R3RUpUQUxYZU9NMWVuQ2dRWEFCcmVlbHpzbGgwT09RR2Zmcm8vClpQZUVvS08zSlVVMnUvbEJKMzlPeHJuQmlweXhPRUFyTXUrQmViVm5sUG9admh2ejdZc3hJR1o3SVdBSXp1dU8KMW1zclZCckRpUGs1NmFRQTJtSUNsM0RoNWxIb3Z0VzBzMWVTMTVDaWNEclJUU2RpZlMvaFhDQWlnSnIrYUhHeQpGWURwaC9XbEU3eEVhbVM1YTlORHpNOWxrTTVOVW1tV3ZPU08xOTBRL2ZSc3k2UGFSdXNrWkF3L2VYY3JzK251CjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFhYQlF1SXVzYkQyUmh0MXRKamUKZFFJTXVXc29rN3dFWWJ6UVFIT0pKWHRGaW1LdzRqMXZjRnIvYlRjRHBQMzU3UWc1eWRjN2JBaCtFUlNQeVVrNAp2WWx1V1ZMUzJjVmtjMnNxTVJlSW01MW04WkdZdjdMNlU0c1lqVGJPdHdpMEpHcnM1cjVWQjhsYWMrQ3hQU2lOCnBmb1p0UXFIZnN5M1hlZjhwcU5qTXI0VjJhMWtwSmN1dU1sZllLeGx4cXRiTi9BekQ1MGRQVnBiL2w1N2Jpd2gKUlpDZkluRU9rYy9UUnFPRmoyM2xBc1g5OGFVTU9ydWd5Q3J1b3p0U0tYNEZQQkhMWTlaR0RPSHhCQnZVZldtaApHY2dKVW9vREVZbFI3a3BkZ1RGL1M1anU4V3Y3R3FXTEkvcFRQRTdObzdKZ3dVUEZKL05hUm90MnQrYnVzM2FKCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXRBMVlWdFRPTkhHWmIzWllmVysKeSszeTdEVmNQV1JpVDV3RFBQMUxJSXRnaHZCZVVmZGJ6YWo1ZS8xK2hqKzE1V1lidzJLdGdsU09KNjVVZExjQQpWUXNBU2did2RzM09kek5YZ2pGTDdnY3VMc3JSWGV4VVl2WHc2OG42WUIwMVJCVGZoM21iVFRJaHMwaldMaFpGCmhhMiswWllodVRUcy81UTVnRHNHSUx6Tnl4UGZqMGNPMktoUjdPRFlBaVRaRDgzdWozOTU4SS9wRjZLam93Z1UKZTZacmJUYTlJUG1nd1IvQ2VoZEJIcDhNdEhkVldzTkh6MHNqb0xVUWJsN1NlQXIzMUhYTGxPaDBoSEMvLzRwaAp5eVlBVldXRXFxKzM0dVhKVzYvNDVkNTFNaEQ3ZUFPZzlFbWo0NFpuam1ydE15b0dCaS82T0pWYW8zb0JhZHJOCmhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcmp5YmpJcHZUUzR3NXpxaHpKVUEKM3I0Ly9vZVpJblVKblh6NzQrRHVwV0JqWGlJb084L0orblpDcmlCcnpudURjK0hMQTBPMU5IRmI5UU9lcTJuRQp1dHkzanlTNnZ0OXlBSko4UEZEU1RYT3BrY3RCNEdZS0hWTDFSK2lSVDc0dmY2c25ubzFTYzl6M0ZDcEIvVEtHCkk1WXJhRU5oRjZocUlmUElpZTI4UWQ4OFg1UG0zbkxVY2J4VjNYdmprSDVlWDZLUTJTNXZmN2hKMGhCNnZpeE4KZ0lNMHlJdjlzc2tleUxXeHQ0VVFBb0tmUklFUXRVZUx1bEhXSHBlNVVETFJlTlBDV3NnVjJoY1dJRGw2QXcxOQpzUFF5eGh3dERIbnN4RzBWSnM1YTVPdnZWR0NTOURHdkVvbS9MTW1PaG5mK1BjTXhnSjhra05NNGoyKzNTenhECmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVZpN0tDU1NKYmRzK2lvaVJFTWsKS21KMzR4ZjdGUE11WEVFYXBPdzl1SVl4d3hENWZiVWtORDV3TFU0bytOd3psVGpmMkN3WEx6TlQvc1NxN2lpOQo3UFRLL2lsdkUzZkdqRGxLa2pFbW5yTHhLN05Mek4rSkl3Mjl0R005WmFTWDhBNDI0aGVYVFJqYXZRMHZnMEdsCjB6WkVGN0xlR2Jla2hrRm1wb1FUY2VVRERieFFtWHZlQUVQYys1RG05ZHQ2K3VURiswVUlZZ1JkZFJnSGw5aGMKYjdMYnl3bW9GQ1BMTEI0b3dqajFBcEtpVUVESDAzb242eFBpSFRKSGdOSGs4akhIMjFnN01UNk9vdzg5aVNodgpMSm5DOHpPK2RHRHI4bVhqbi9HREFnbTlOa3pscVZ2T25yODk5a05TZFRQZWlPZnhpQXQvNGVCbWlTRXpQaHQ3CkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1VtTWNzU2thVkpnMkpGb3JYMm8KbGJqRUZSZ2syaVlQdkNzeWNFOXhHTlRSRlIzRXJNNFQra3M5OW5pK0ZPVDh2a2ZLOGJNWFZKNFFmMXYyU1NuWgp2cVQwTHU3YS8xbjJxc2pKdnoxVTFBVmJkZzJhY004MnU4ak5TY1pDVzFEWXJwaTFnUjJPeHVhQ0xSNndUS2JiCjlLdGZMbk1UT0xrOEFCNFpYZUNCYUxEV21QSlFMSkNvQ2NkRUhTNmYyTDlOMjhBT1RGMGN2d045UHlzKzVocEYKdFpoSW9laHRwdG5GeGc5VDlFdnRKblFpSUxTUXZEQWtVaDFhZU00emFHY251elJsajNWaCtyc2cvRmJNdUpMcApwVzFKaVYwVEdCbU9qdTBIZWRTZG5lRmhjU0tLSXRpaWxYdk9IUDcyTnhtY2xkakJoanI4d0lVelVEdzNuTXNZCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcDVKWlRwTXlUMUdYTHlYMnBXNy8KdGNObmY3SGordTAvWVpOSlZPd3hTdmNHMlYwU015dm9EUlg5V1VpQm56NXV6QXZqTnIwUTlyK1BkZGtscm5ZRgptV1ZjaXhNaGJMRGF3YmpSVGFWZStmZDMxbm52TnhBT3BkWVhhSm5GR0o1ZDN5dXJVR1ZCZ0dzcjE2Y0FGOHVCClNzUjVoUWRrd2tlWFM4RG1CUzB0TXpQbkpCV01xQWtIazlTMlVnNHNzN25JamtXYksyWUxPUGpPWm1nWmR2VGwKS0NHenhuWG9wQnhpazd3dWdxMlFtdlBmR1N5YVQ4cCtMejFhbzdkaXB5Qmx1WFZrNERUSUY2blRjTEhBbmRobApqYlV1Uzk2U1hnYnRxdjJuazJOSER1em1Tc3dZTHJBUjFVQ3JJaWpPbkhvampsWGdGczlxQTQ3SDZENUJIN2FpCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0FqVHVPVElMZGdKS3BQMng3RWYKL1g4OFNDd2I0V0Njd3ZJZUFwZEhLemtuMmFZWXZONGpxdUdNVWVtQUFFaW1oUjFOY21hSVZDdk12aWI5OTBsVgprSjQzSnp2d2ZZQ1FNaElpQlNsYk1NaS8xaWliYzZMSGhWLzJrRUhXUkxvbXVYd3lwdHRlRUVveUc3bUlIT0E3CkVhcEtKZ1RHb3RLUERHRXArWkszME92bTFCMEZyQXZRVTN0dUJrbEdaRTdBOFRXaUxqOXdmLy9XZktLUzFvc3YKRDAzK2hTV2ZMWndpOFdyam4zMTk5dDNBWGtmT3JtN3JGRTREMkhoMHlUMXE4MG9JV083MXoxaW5XYTVObW94SApUMjFPaTZSOXdRODB6dFI0bno2eGhGcTJXSWJYK05CaUJnMjI0anJTVm1UR3RKM1JUN2ppNmFJSXlKRC9seG90Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHhrc3E5b3lFZmJ6RnBvTGJJRlQKV3V1RUhBM2dXaWpWbWV1eUJmTEhYNUhhWlFBTWJSZlFtZXdTclpJRGlnVE5mNzVnVFRGb0h2U0hpS2JVeG1pWApiODRSYmg5cjFBS3pDY1dhbWI4amM5czdVMjZTZlpCYktyOG1aNnFKc3pBWExVY0lDNnE2aWJUSDJEMVRPd1RXCmRlb3BKNlBVQXA2MkYrMHl3VUk2bldpMmxGV1B4N21ad2h2NTZlcE5kZGFZblk4NnBJd0hoSmZBZmRtZXZNQmwKemN3NVZzYW9lUFV6TkM4aG5UU0FFcDhBN2ZrT2hGTFlmS256djRSd0JnbnVRaGw3cnRpSkdjTnU2ait3amhaWgpsVSs3NEtYZEQ2bEFqODZzaDBGV2dvMVRkNWJxSi9KbnRaOWdvbWxGSU9MTVVObHliNTlRMDNSaTZwa3diRG5CCmNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBa3pCU2xyYmZqRDNYMzU2SFdCUUgKMGRkVXg5VHdBSVlrV1NGcWg4L1Arek5rNWxEUnI0OG11MEFld29XQzBtM2V1ZGxCRWtVYmJuOExVcFB5SURYYwowdHIzeEVwK0phN1lKU1hJWURzNVYwN0pkYVZvZGlZNzI4V21rTGMyTi9DNnA4U3c1eXNsYWtrc0FiR1ZnRkpYCktZRW9yRVBUejhvV3JnM1gweThpb3BLdWJHcitDOHIzaEZ0eTVHYjZrUmFyTjNvN1NiOE1jSU4rbW1KWmZuY2wKSHhqTmhUSDlkZmp5U2J1aEdCU2pya2NQQWdjUzc0YmJVdHRGSE1ybUpPeHVMU1RXbXlvMS9CYnRvZlJuc3dGagpnMjhPWnJneWtEa0ZwRTRzUmw2eS9vS0t3MENyUThCeGh2MWZYSkhrMkVKcmZNcS9YcXo5VndGSkM3NUIxTnBqCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHZpWTJiWDVwSFAzOEFkd0hqemoKK0ZObk5pbDJYSTlRcHJ6YkVCZGZ3dXRIMFR1Wm1pa1dEd1grRjNYRUt3RUc2U3p1QW0rZ1pPaCtLenljeHVUTApHRk9uOXZ5dkYzd1o5L21KT2lJQTlkSlNhMU05VzFjRW1CUW1JZnh6WFU4MjYrYkltV0kxR2pMbGQ1M3huWTRSCmJlUGFDZkVUb0c5MG9pUmpuNHhtVGY5K2pKcFp1VVBBanZpc0lBSnZUUXhHaE40U1NCYmxrR2RVdHF2VWRPaFIKSG9RR1JBV1VMZ3ZZN1JJWVRPaVhXYkYrcHN4QlQ2c09wT2lqLzE3NFNYL3poaEIxS2o3RDRaZGV1amNzaGFuNwpTdEJPNHhUMHYyVG8zalVJSVZKeWhRN1A0TnFJSDNMR0YxbkVBWW52MG9wcEFlVEh6QlFYQUNML3lJZnpOWW5XCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlZKRWdwcnlPWTdyRllza0NYb1AKbkN2ZEc5ZUFaVC9xQnQ5dE9MZVF1cDBqa2REUm9xZlk4bzZ2SGRDZ3VTQWtWWWY4TitlMElaUDNrVnlWV3ROagoyeGZCWFFPMU5LSnc5bEFzQWk1TDRnRVBqS3pDbEtZTG5Xb2hITGRpRWlacEhOZWl1MGZ0bHJqTDZveG5pa3BOCjQzN3ZYeDNyendkYmYxenoyZU12RkhyMHRzbTIwRDJPZUJiTDQvWElXYmRzQUFEaEkzZm9GS28zbERNdGc3NSsKWlVTbExwZHJoUkJJZStVQ0ZOUWp4cHVJZ1JtZ2xhcmdDbUJIbmFhaEw3bmZ3Uzl3Rk5jUnJHLzVrdmN5YXl3TwozVmkxWm1FR0x0SitTcGZ4UktsL051QkYrSzNvZEFGWGNSR1ZDcXJiUnFhOXVBNVRHTXcxQnlxUlZUblJFM1V0Ck13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdG50YUJBd3ZqelFsbmU1VGhMa00KOUlCam5kQXZYMVF1a1VpTVg3a0tteXVzUEVoeXlJVE56S01PdkEzUENRWGdlcjV1VVZHaFVIc2FhYm5zSXBtVAo5MGlTS2FjSWNQbVZGcUpQUCtnM1ZmNE56NlNGSnV5Yk82Q0hFYmRGZzRsTmdGanhFalBoSjZ4VUlHSkJqc1FoCnZVemN5eGtPRTR2amo2ck4xSHFZNFlBQWFlUksxeXhjblYvWnB0eStOazgwR1o2MW1zMys0RVdWeVAyS2pjRm0KeVZXZytheWJJVi9TVDJJSEJ2Uk1XalRkUEdzbWlGQTVxekMxMnh0c1hyV1lBZHdENUh4K3VyNEhLMEVjbWNHcwovNWpYUUpUZk53VlJjN1F0bDJQWS9ZTWQ2RExWQnd6NGdrcVZqUFlVK3B3U3lvMlJhcmJMUHdldzFOK29NR2NaCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG0vTXgzU08wQW0rbThZL216UU0KNU1kamVBUXpuUWtObUFsTnA3NXUwdVdqMzYyRDhVd1MyU3N1eTd0cXlEditBUXdIWDh6cjBDbHB2K1ZkTkhMbgpuc0xvaGhLSWZkZHF4ckl5bGRFdGVkUjdqUGZ3RmNGUnZtOUwwU2Z1ZW4yRVp2N2pzSDFOVy9lYW8vc0VCQ2xECmt6aE9KRGl3TVpITjVDNisra1pOTW5kSGVxNGZJa1VvUHIyYjV5QmFoUkhLdUlINmZOSVQ0M0ZrY2NhckJ0ajcKeFpFdmVKS3kxZ0ZqLzRMUWNNYlBGTTNyTysya2xTdWRXdXR1cnBYeWN0WkZ2dTBpYVlVRkZoSFRmc2tkSGxnbgpValYyblN5ekNkdGpVaE83cFZPVjlWdDVnNnVGQlVZUWR0bk5RL0Y2TFFkMXVoZDNpam1GRXFZaE84cFNQcDB2CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUFDR3A0VFBqRDN6MmwrUzJmRDcKNXJNY0xKaFNoTVA3N1l2MTVicS9jVWRlUTF3aGg1MzRZdjNibjlHcGw5SmlkR1JPZDJiSWRldlAxZGdXRk5GVgpjM0hKc0ZSV2l5VVl4eXFPQkd1QUVPY0txR3RoblYrUTdidmU4L0g4a2dObzVvOHU4THRTNmdhSlg1YmZiamtsCkZjZVJKNStPdWxSV2tBa2RuaGhCRmZFN1BKVm5aU2VlNThETDFiNjlHYnViNTUrS1BoRzhYdW1jVmJDZHIyZHIKRFBTQVgxZDI3U2dMWVF2VmdtcDNxalEwL3J5V3ZtdUNtTDhzaGFCMExxNkcwajNId1RaKzFxR0ljWnVrWWJFQgpOdWNRN1FWVjl3bE1VM0lqZm8wTHNGZVZpSzVDUzVuaXM0WTh1bG12VzNNZkgyamFJaU9aQWhJVW1FdDlCQ2YxCjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjRiMjZOdjlTaG9hcVpMS0R6Zk4KL1h1UFpZOEhyd0RPT2x2WFQyU2ozL2tSaVFkN0ZJOGNSclc1bW9CT09WSkdkaUErMXNvNjRVM0lyQmVaalBTdgp3bEJObHhUTzh1VFhrN2krK3FNdWRDVVpiMklsUlRscTlhU1RObTNPcHJsYVRRYk5JYkpGZER4UklhQUNZQXRaCkFaeUl4aURUZjBONTc3cDRGTDBMWGNyRzZHTjNEUFE1d0xPNENYM0tod3YxYUlyeXNQOG56RkFYYzBKTEdGMDEKZHluMVdMMGcwTGswMjYxUzd5cG9URFFIVzdEMCsyYmhKNTBiVHgxZ2NCRWsvRCtPL3dWMUFKV0FIZHJuQUZFTgp0bzhMbWdYQmFGdjhLUmpEbGVCR2E4czN4YXJQNTFIUWVqelU4aXNZRVZ4aUs0YUQ0Lzd3R3FxUENVcnQxdlVBClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnFHOTJqZ3hQclp2ZS9NUXlkYkQKT2FNdlArRnNqTHZzTnJGcmxraDE2NFpDRUhsbmVwditpbEx2SVNQTXlVSWNMSkt1enYrTmhmZlFxKzFUTVFxbwpFb09Xb1JEUjRrdE9zcmJ1SzdIbm9ZekpIalJUS25Ea2JJbWpGOS92OFEzcldCR25WUkN2dDZxUGg5anl2NDRkCjZ1dmxZT0tlRjJKa2EvejdkMlRvaVFwcS9SeStQNTdoaEt6WFVab3JzNTBTZUVFWDRyRkFQNnVtc1RmbkgxdjMKanluY2MvNTJTWEhwb3ZPZHFYMGc2d2w2SzJreVMwTnJKWHNpSmhpbDJNRkF6VmVUekltMXp4eVdYMmszRjFiYgp2ZDQrTnhYdGhLZGIzMEx3MTdTUGRpWjFjYTMrQWwyWVRSMU1tWjJSblpienBvckZZMDF6K3o0WlQ0QmhkelFjCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdndMbXFFV0NhM0RZZ2YwRHJONHkKdmhud0MyMG5aeWVkMWw3K3FNUGEzNUlrTXZIRG1Oa2l1MkZpZ2craUR5N1JHRkFWSUtaL29DWFlpZU9sRGpkNwpjTmJRR1RxaXphZnJXSjVRUUVIdC8rMnNpQkZkcnQrbUk4d2JrVERHVnFGUFJnTGNMSjZkTCsvWEF0a1g4UG1HCmtjNVFBaDNwOWhqTWFZK2IvYXc0Vi9NZmtkOGZJNTJwcjNsK0VKMU10ZGdQd2wzemtpaUl1Vy9lbnJDc0VRZlYKa3JOdWJYQndFNFpaSU9IVEtGUGMxN0F6LzViRkdxODJ0VWJHMnhtYXJ2enI3L0tMd2RzeTRUUG9tNHJUcXE0Ugoyc3o0MEo0bFNRSHZqRitoR0lzNTN6VnpKbkYvSVRCb0FXaktlTjJrOTZxVjFDZ0VIMXMrUVZSWk1wdjcwZ3E1CmRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1lZd3Y3aWpkVFdMMmFDVkN2ZnQKYjZMbkRrdVdKSnhhQkFFamNaaTdhUmhWYzZrRUJ2NklocGRjdloyZFZXZGtEWWxzZnlaRkNhbE1oRXlyZ045Nwo2MVY0d3NlSEtNcUJrTDhLdFJ4TXpibHp3LzFoOVJWakZWMUJQOWpFcTFGL0h3RHNvMHZtZnVpYm9hNk1jRWV0CktZVlNXRXJRaUV4T3dwYmYvMzVOSnljZ1htcVZDN3N6YUh0QWVRcE1xeTk5d0ovUjNzcWVla2x1ZUZQbnVPUXQKVGM4dEpLT1p5RkFEbm0vUTRmbDJBR0FUdnJVMk1FSkswNkQvbzJtRG1PdGlMRUtVQ08zbXhsYm9iTTZCRTRYUAo5WWhLQWpyK1MxS01acW83S1ZJTnZob0s3cUlVVUh6K3c1em9QZmFMZ2lGVHc3c3dSL0tEZ1RHSGdUT2FqU3NCCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3dWSmk2MzlTeDlUQmlMbWdmTHkKQkZpQ25JOG0wMXB4RU1OYXlNbG1HVUIvOXJFMEh6VWR1RGZKeU9oZ1NGNGFNY0VuUHZsUFlqVGNjTkRHbHVabgp2SkN3SzN1UEtYbmZTcEorU2ErQzlDRm10SUI0Ry9MbnhpZFJhdkp4eWNUbGtTQldzZmpEVFNvVTFvZE1qTmFrClg5Z3c0YTBFQ2M0d3U3bU4wR2FqaWJGS2NCUlZGQkNIdHhYbGlTQjduTE04Y2xCN2loK0kvanNZQWluSlI3MjAKWlhVQXR4S0Uzb2FneTNOOGI4V0E5QlhXVDV0bHU0dmgxdTY2RkYxWFdrOVFqSU9LRlBsRCtjMS9QcytuMXJGawpkdk94Vm41cDgyMUFzbllUU1RLNFg3bG9FZENZQlRMUXphdGpEbVlLZ0gwZ2w3OXk4bWlwTWx6aTZVQVkwOW5sCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjFYZC96QVpDZHB3N0NxeUx1U1UKL3RIZGJXUGcvVzRETHFQVTVUSzBvSVptZ1hxaStWZTNIeVVuN05yVkJKNGNudzY3Qmd1RGkyaW5nN3hlaHZlZAovQUx0Y3Bnbk4vQ1ZUNnB1MUI2Tml3endZR1B3R3NTd2xCYThSQVNNZHovLzRhQ0drSGgrRDJvUW1iZFhSWUk4CloxSXlQS2FPQW1JdG5NVWpMSzQzMUxhcVcrWnUrK1FvY1NQRGJmdU4wdS9PUFdXN0hhSFRjZndmRHJ2cGZsTmYKNzFTaDlKYnh2SUVHeUtKZzlrWThqbTFFMzRmV1ZPQythZlVnSzVMRE04MFUzSEhlRjlRMzMySmFQQ0RRZTZwUApCTUk2YmhBVGxjU2hPUnBtRmkzMkhWRTFzd2JRa05GMnQ4eDY0d294MVBxMStTdmZRUnhpSEU2blJIU093eHV5CjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcU1nazBtOWdPYzA1ZDZsNVV4WGQKaCtKWGJaaS9vN1hqbVJsbXBSa2kxTXlKUmpLZnRocXZDMHZ6cVRRRUFiYk81VTU3bVBQNWpjZDVBeXZNQ0RkRgpWVyt5RzhJaUZIcVU1UVp6QUhwK1ZneVc3VHVnZXpLQ3lCd1VSOGxzajVhaXRLQU9ray9xdFF3b1dnSnFuNTQrCkR2cm1tdlNuNVVuSEE0MFVEbTMxcXU5dDFmUlFTVGE3Z242M0xaU1Fpd2MrQ041ZTYva3BETnRMdVRoV1I2NG0KSDhWekJjNnZKTjV5SFZGSHVENDh4dnJqZ0VIckJBSkRvNTl5UnJ1OVRiTlVPVnd2VW9ZSlp1dnQ0ZlpwQW9maQpqaGRUdTJMb1BzVXBoYTdBUjVJWXBRN1MyV1RqUlRBWmtaTnJZSUgxQ0ZCUFN1NHl0TFVvS1R6bkNaa3NoUkQ0CmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzluNklqYXA3YUdYeWhPQm1LY28KdWkyRDI5ckprMWE3cmdOTkw4b1o4RTNuQmJvWG1iaFFQNGJsdnVwTlh2UFV2M2VjZnNaV2QrK1Z1RzBvTDZubApKNmN6d2hVWGs2aGpVMitjbmtiWjQ5d1N5aXJrMkdIV295aTIyditZbVNud0VqcW9oMExkaTJrc0VLcWd0TGkyCnNsR0NXeE5sMEhldmhMQkFiNGhyeFZmUlYrMlBlYm9XRzRaYThmdEQ5c3E2VlVSaElFTXJaVytaazM2TWNETGwKY2F5UGN5anY1eXpFTFg3T3dsbklJVVdqSXZQT1Q4aU5hS3NtVEtLYVdjN1VTN05RWHRKSnBXSVRuYU5KczZQZwpjSXhUbjc3aGU2dkx5TXFjVTZYTHNpMkZvUUxEcTZzdlJtT0xKVVNMbkJOUWQ5QWtkRmVTNUc2Y1FqR3FwY3ptCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTlvNk9samhPazU0TGUvcFJucm8KSXpEUnRaNEg2MkhDVmNzbVI4ZTgwbFpSMFA2eVJtbzhoRmNPNEFMRlBtNUtTUm5wazNsT2JFOUR5MkxwYzZwVgpHYXpZQ0gzb01SemdCbWJ2MDNROGZWMkxOM2FsaVNxVkFrOTQ3RWs5M2pmemkvM2JhS2dxTU83WE5XT3RzUUF1CnByUzhXT01IdXhPZXRDd3B3ZHN3azFLV2x5M0dCdmpIN3Q1akZ1Tyt0b3hUZWJINkxiYlJndXRtdktmNC9UaUsKK0JCblYzWmxPWjVFaUE5Y3RiREJBWE93UE5jSC9WelZ4aVRDYVRRZ0pibHN4VjJjeUFHVExRWXErcEt2ZDVHUgpoSU1hQ2hyUWNWc0puMCtxSzdSZkpwYkxWR3ZzbEZHQmE4Nlpxa2o0ZERDd1VyR0swZFJOOWFRckFETmpCR2dCClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMExJNzRnWFJldHhpcDZya0F3V3oKMmhsRWtPV1JMTWQyWWphZm1ZL2JtYXdJb0tSVnNTVktCb0ZBb01OUjVjQWY3bXAva213aTNmOFpRTDFuaTFrbgplZzRMMDFpU0pLV3NyV21aWXo4MkRaS2JibHJzRjJJUHpueU4zZGVuZllEQWY2bVhGaFVJd1pFUEpsQWkzWStyCi80RzlWMjVIZjJ4OFRwcHl2Vm0zdVJWMDhPbHZiOHdhelZmSWJNdGk2K3czQkFHL2lGQmhLUXZWNm9HaGwvbmgKL0VvN0t1QlVXTUJidlVvaHdJYWRIcm44bTFKVzZnVnJRS3prNjMzZDFDSnVlY0JmSUhtN05RQ0Z6Tk1uVjYyMQpJUDlHN01EK2hXb2RDekwrSHVrekpCRWZTNnV5d1NoQzRGTUdPZlEwVVE0dG56UW1Rb3hGYkkzekE0UHpjN0hPCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckV0TVdnaEVrN2ZhWkhGdjdCZjYKZSttSGtYNDYxeW5yd2JnMStkWTRRczEzcDdVbkdVTUM4UjFoM1Y5cmtBQUV1a24xQUIrcFFGWStMVGlhWW5JNAoyb2VQWFpKd2FTWG5hY2ZjNE1STExlbFpvazZSY1pnMjQvTWUyTVBSVGs3Y05XY2tvSWZ0bHlZemVHaXFZRHB1ClRqTmZoZFE3a1FSMmRCTVh1OXFTdm5laXdzT1czc1I5RGNid1ZRRXJQbUVIdHZnb215VW8vbmxEUmh4S3l5NlUKUTBVVlN6NmhlQXhsREEzeGppblgrZmtYU00zM3pjTjExbDkvR3o3SG50aVZUL0ZZY0pSWmdTWnJDblF2MVRCWQo0Ni9La01ST0U1cjA4Z2dCMjBMUzIvZnhkdThrR0ZRSHBtKzR5OHVuOUQranBmcU5Gc0pNUUhEWkFEa2dwaFZPCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOG4zMFhrRStqUHNQa040bDlOOVQKVS9ac3I0V3pTK051eDZVb3ArL0VKTXpWS0lWV1VIY05ZWGplb0ora3hBYllVM2pKeVRZTjNzOEpNMW9qcXJ5dgpRTGNKc1BnTDVTYSthYk4ya0pMMENKNlZUcXlIL3dGbmVJMDdpaTdsMkF6K0pydmI3UmkxYVhEaEp0bEFvVWhSCkorNWpqZDhZcG1WZGhOTmk2QXVCZjZUaERORkdEYTNzUWlMYW9DVmxZZkx0eUIzSVlYNWhoMzdrQXhySVpaczYKbGRiYkdoYk04Z1IwbUw1NFFoTDV1b1d1Kzk0ZDNCZ2VOSlU0SW44TTh6WVYweGxhUGN4NXRsbGZIR1VNSmYyQQo2UjVrbmJIbW1OL1RxS1BYOUx6Qk9CQ0NsalhWVXY5WWYzYURhT1l5cE01S2FoL1lmaDhrYVUxTjQzZFNUcEdMCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekplYkFtdTlSTktUSlc5VTZGalcKYlFBNUtsNmcvRWdwMG1vMFA2Y00rTXVpTlBsdVZES0JqMU4rT0s5SGV1RFBpVWVIYWN4QlAyY01SY21SaHR6RAoxd2wwNG9ZMjRNSWlNenA3Z1FWTWNueFU3UzdKbTRIVDBMUEFsby9HZUgvaG12SnJoK0Jubnk4QTd1cUI0N2M4ClZ0b3VBMjh2Z0JsdG05bFVwZnpmWkl3MzY0UnM2Yk1OZzNDQ0I5dXJVRm5oczZ3QUZYa01uVEFzd1JsZmM1amIKNlJmbzVRNXdVaG5VSTlWUUFpdUY0V1NmK0xQbFhZOUhXVFp4a2pCbFZqZDVPaGFvZm1mZmdtM3hpZUxZWFNtNAo5TFl0Q3Q2bitXVHZIQlR1QlJ4ZVovZW96T3I1Qi9NeVVQTmJ1UUFkdUI4SXZCZmpQT282aGtWNlRjSUVISlorCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNmp0enhVRXVndVpjdEhid2Z3QVMKMmtPQ3F1WmdhSkNFcWxZYlV6cEZzWFROYnJIemEzV09lWVNNb2dYVDBVVCtlK1lGdVh0dkVsZkI0V2hqRmZDMwpuWFBIcXVkcWFxQUFNekNMcnN1N0srV2xvQXNaMndKVmYxdEtkOFJFblpBcmNBTUNiSGRYa2tUUGlXeXZqaitLCm00eDNZNlYwT3FlRnV3ZW8wT1p4cklBS1lRdFZKeFk2U1J6VTE5bXVMQkpmdmVXM3VDVWtiUE9ybm1tUjlGQnkKVmQ5aDRaS2lyR0NsSjlVWUZJdUQzSTJsV1JrUmt1dW5MZWQ5NXlCZWRoaUl5QUdOQkJNNTVzUDJPa3l5L0VHMApaRFRyOWFIdkxFdWxsYTU4QUdha0ZlV3dESU9IYm1ZQmx2dnQ5NU5kN2tuUWdZUFRQVUQzTklpTVFZcFdrenNHCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekhzUWdhb0tab1ZSa01tTDlrdGYKVmdaU0ZLbmZWYlJCS2lobXFDeFZMTm5kQmpEQ2RjQm4vZG83S2c1S2dBSmM0RzVtMFF2bXZuMTZEYjc0NGhsWApXVHh1bHlmb1owcGhjZUFoV2dSNXRTejE5WE5vNmdNaEgrb01RMmdxWGsxeWkzVWtYSnZJMmh3QmJzd08rcWd0CmUvLzN4TVhLL0Z5ZDNwUjFOek5mRktOZmt3MzZsdUVLYlJPbmdmbFpnUE4vcXF0MW8wQXBjQ2QvZW1MUzhJZS8KSm5UeFE1dDFPbmt5N213RVEwZGYxOTRwQUl2YlUvMHdiY1JsNlVqUTZ1N0VRemgxbXZiODE3SlBmbWM5bmVkaQptRGI3d3RPTjdvcVR1VmMxT2dhaXJVSE9ZQUc4R2Npdm4xZmxKMGtKbEFKN0pNNkFMTlMxRldFQXhhZ1BEem1WCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelVwOEpQdzA5UGFZZFdhdGdZSHkKRlM5RUlJRmZZd08yZWtBdEFyREt3ajQ4bEJjNklqSm82aWR3WkZYb3NmdFF1R0VyR0hGK0N5UFpIMW5YQXFBZgpIeDZaM0NxUzYrcHUrRFp5eDZvWURIT1pHQWZ1US9ITElUVlpSZ25RWGFtVFZpQkg2ci9IY21PVlJCdjFyem03CmtzQ0FGbTdCUENQRzNjRG12a0JrVEVHd1lqSWYwdzhONTFrVnI1SXIzRnlzcmJKdlF3WFVnajU3T0lCc0p5dmkKeTl4Qll0TXR5emdERytDOGRpdW1PcFVvQ25lNDJPam8rUjhZWWtDYWE2YlN1M1YzeXQ1ZklMalNGcW5HcVFrbApOOE1yb2NzaGZteUZ5amxuQ0dPTUxGVnVjOU4waFJZUllYVFE0ckVFQkJESlVVSnp4MlFWaE5uMUk0Y2k2TFR1CkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1pXekJDS01LbUloWFp0Vk4yZ0IKWk56dHhaV0cvcEpuOHJCUlMyQ2tFRUFuajlGTEJEZVg4Zm5DenNNU2FiUDExWkZVUkkyMkx1K1dWaGNwTzhoUgpGMGlaTjhtdm1iNFd1Wlp5SHZhc2o5cmdZLzhlc2xNM3RMUG84MEE2ditjdGo4d0dYTEhHbGl4eUZTc0RRMlFKClVQTTlnOHo2bEUwcTlyU241VDB2dHJmbElKekkrMjRLOWNsbFpLU2RhOUNsUXkvcDRFZzlhQU1BZU5wbXp0NW8KMUIremZ2VG1jV211VXE5NkM1VU0rZk5qUnNaSFNUSWpiSWpkS256TFV5aWV2TEpZMHlnYlZrT3o3UjZUbmhPcwpKRVB1WmJ3cHp3d3dxa3RhVUI3TTFxM0NlQVlKWm1Pbjd5RHpmMnRsbWVxSGIya28wQUlIR3haT0RSRUdTbTlLCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMUVILzMxVkNkZXFPOHdMR0dUUjIKOTYzdVpPaFVWNE94bDI5NEdMS2hvQ2s5by9RTllabG1oNjN0NHZWcytacklPd0t0QmlRT0NRdzBxbDFlS2VRbwpPWmlNdUtTWHZ4UTQxNFI3R3hhWms2V2c0NHF0SmRhZEwvdXRTUmpUUW1nTmo3ZWFwU1BmMHIzQkg5MkhJSkJVCkRWUEVDL3pCcHhLK2FUN0ZlNk9VUjRUS3c2ME1sNENRWnpOMlVLYWJDK3MzdTdzMWlQYS9oQVlVdVBIb0JXemcKbitTUzRKa1Y2TERoVkpIV2hkcVdzTWFaZUpZYlArVU5DR1I0QmZwNmh2RXBRS0VFWTd1VGt5QXFwVWo5Q1JhNgpPYzZ2dkdielFwUllSSjZqYmdsdU5NWWZ4RU05NXRzdm5NWWVMSllCZ0RKM3Fad1c1bThGNStPWlFLaGdnQzAxCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkhrMDFCQ2hTTWxmbk5oU05Bb0oKWEVEUnQ2THlXYXY0OVN0VTlBK1YwVGE3WXRGSzFrV240eFhkSzFLdjB0TkRubVpsV3MzZjBHMC9TQ09KTi96UgpQcXBJamxiK1AxR0pCaWRlSVlWVGhMZlptVWo3WGlJalNGWXkyOU9OS21wNzVyNnZxNXVkZmszK0V3T012OXBSCnRMb1JiVEwxM2tQODM0V0dxeVIzSjREbWEzdGFZMWk2cUZTdVFUaDkyQURlRCtveTd0ZU9xNWJhZEJYV2VhRE0KbHM4UkNqNXo1dDBseU5ESGhYd2ZmTk5ZVjJTRktmM3FPV21yS1BqbFZyNUlqaEU4OU1SSmVhOXM5OXRZY01SUwpVSG5qS3Vlc1QrTzd3K3o3bUZGNTlvZ3N4djRPYmJwZ1dYYjRac1R4c0tMYlBzTkpSVWR2Y21LbGV2Q1VhbllxCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDVZS0poTm5SN3cxSW9kQTJBR0cKRW5TQ3FDNWZLN3ltbzV6ZXcxQm04OEN6VmI5RXN1R2FwVU1jVVZDaXlRZ2FMSS91WktJQThaek5iNk9lcDc4dgovZnIzMVB0TXR4dW5DOURXT3hPWG94a1l2WWd0QkdkbDNGTVM2TkdqRURDdmpVbGlRaGpaZXZQS0lxOFJsQ2ZjCkRIQVZPbUhkenF3Sk11cm9qQmNUUFNhSHZraUlGM1k5dGVsMms0dXVWUkM5Q1drOTZ3TTBQMGxCOUlZMFlzNmcKVGt2b1h6cmYxaWZwQUI2Z0RFcXRsSzlnWktLZDFDQVJWNkQvOWZIamlQd0tXQXdTOERkZmZnVFdDalArTnpvYQp6eTlzMTB0Si81K1Z2QTkwWWtSSk1pU09EK29UcmxEcmRLVVNDbFZ0eTRuVGVrV1IxZzByQURjUGZOQ3J4NW16CnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcU1mMlBsdWpBNEh5UCtSS1BhRTcKTWkzaFI1MjZtOHFaN0prUzhDYThrYlNtYVFJeHlOWnJBaFluL1psTndFRlhsRDBIUkxlZ1lLZFFqNDB2UGJUUgo0eDkxTXU2YnZndTY1b2VGSDI0RUtMV0tnTDM1MFM5RHlLSVluczJzaVdwVFdPU21SNHgwT3BhMFN3eVZTRjFHCnhKMEJzdjQvc0xyKy9sQmpnOTZESnhhZzlaaDZHL0tabkFCU1RvOElXQ3c0N2NtdFNIZFd3aFFDWWdPK3ZHUHoKeGdKbEU2dXYrUFZxN0RLaGdvNkNpRC9Dd0tkTXJOSk5EM1AvTGJUVWZKTGRucDRDVXBxNmxnL21wMUxvV0Y4bwpRT0hZRzY3b25VeUwyekQyNnp3NXZvL3I0Y0dKc3hZSnRUVG5DRGdyTmtrWDNkUklCbGJ3aXRuQTZ0elZuSVphCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMklHQ3l6bXpXSXhjOHRXRGczU04Kall6KzREUmdUMGNWQTllN2I3VmpZQS9mMm1OZjZwMkxyZnViVXo2akxCN21sRDVESUhqMDl2dmllbTN4bjFzdApBc1RoYlVOUDZ6QmlsWVJjVXNMbHBGczlwczhnK010TmJmUkxzK2sxVGRoa3AvY2VlZTRwTWtIYUFqaXIrM3lVCmtnVFdyN2ZTUkZEM29la2daeXlFRWVvc2ZkYnZjQk5JQW02S2VHOWFSZmZSYVh6QXh6endkK1JLSjRpakVqekYKVEJ4aEdYbnZKVDZCcjhQTjRyYXFNNGpibWtzQmV0Yjh2ZjJQQVhycHFnbU1xUXFXSmZIbkdmTDlSZGxnWnFwUAp0UHNXb29YZlBBak9rdGtpWXlLQTZ6MmVIUURmUUVZVkRZZzAxdE9wR0Q4Vy9BaUhWaHBnNExTUEZmdXFXdjFMCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1JFd3hBd01TUU4zYUlScXFQb2EKVGdwV055a2hOZ2lWNGpYQVFBQUJiSjdHOWkrTllWbEV4cmswY3FDc3lseDFkNGdmUExtN0pBNE5Hb3NDaGZTSgo2UTVlNHNsTVVrU2FXZDlXbm85UTRYb3lQV0RQT3pwSkZwSUtBdzJ5NGE1YlYyVmhFaksxbTA5UVNVV01meDcyClpIWXROQmNsL1J2K0lYSnZHVk83L2FxbDFKZnlnanRtRjR2VHZ0cTN1VGxFVHVBQ2ZxSm14VVRtS25zdnliRHMKVk0wQlhseXl5M1NEc09jZG9lWWsza3p4aVlaLzNKbXJqdGdTaG01MzFGZGNrR0kxeFVwWjJORHhYYVV5bWI1bQp2VEJSaXVSbGZMS3d1Rmk4R28zYk5kUmtzTmZJamFKSDdKQ0JmSmFRWUZVZllCUzZ3a1dEMTNCVGdOSGdIb0VWCmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNWVZc2NhclZucEYxeXd3M212NFAKYk9ERWR5QnU3MllOcFh0cVppOVlEUFVsTnkxdWFpOUp2QlZZR2JkVThMb3RQYnFYS1lXbVNXNTk4cWpIRDlpOQorVUFXTlBQZXF0QVdjT0JRS2UwdEE4T0MvZFd6M21yem1CQ1B4WndpR3p4eHRxVm1mMFdpc0lzUE5vQXJwcWZBCmdPZ08xcUFleURGNmtqMkphN2ZTUGRJMEFtU09mdkRlUUwwbHc1LzlMZjFoZWtLcXhpQXNtdjlqeWxoa2FCU3MKTXpNQzR1RWRkVUd0N3hZQlJDVUJZVGZ2Zi9UYmp0cmZMNWExNHB3RVl6V3ViaUhONEVoMHQ1bkphVFd3Ry8zRQpYQ3FpdVhNZGxGVGp1REtiS041RGtBbmdVVC9BandVZTc0YmpjcHgySlVBNGFITzhlZHZFQ29UU3pUWDBUT0R4CnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDF5U1oxeHRHcnhjTjBKUXJma1EKd1FNeXp0bklnY1htZUh3MTZ1WC9QVnhhUXc4VUxieGx1ZlNWZW92d2xtQ1MwZWNsNjZ5T0U4c0x5ZjdWWU5aNQpwaC8xay9hcWEwcTRrK2tsSEY3SW5nVzFxNDdMZ2IzMUhHVFVaTjMzYjlDVThFZjdZQkFjK0w2ZVpyL1YvZU4rCjE1OTU3VGJxVzIyT0Z2RFc4RWlod01nMXNZb1hvRGk0NmNKaGhpdlNpSVFGK0pwbDZLVVpncEV5ZUpwMjlrSXkKSDdTVktZU1RFUmxrc0gzRjBmVTlOY3VsZ1BXaWhXVEdxd0JDVUhLbU5LV0QwUVcyMDU3d29RWktEZ2tzQWIyNQpuSGxCWUpSNnpMOWg3cDdPbkJGTi9NU3dlbGxqbUN1V0pnbDdOaC9OZ2V1TlZzL2FhbHNLdUtrTUZLMHZmcGRZCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelQ4ZEU1N1pQRzBCUXRyYjNTUm0KbVFuTWVsTjVJbXBvYk5oL1FDcGw1clRTdVEwQ3RZeE42UG0xMkJ3SGNHazBIcDFiVjNUN3Y3a1dLNlhrbjZ4UgpKd3lBRGswUVo2MnJSUmhuV2JJOFkvY0RmQWZ4L2QvV284U1RZQ2JlVkkxNG5HN2xtV2ZSZ09BTDJyYnZzdkx3CnVPNlZ4bmhQQVhNelMrYU5CZGlJdlY1LzZzV0RXWnBSYkplZHlOMk1HT0VPWWVLSDE1VlNtVnVOU1FIbWc1YWEKRVBqUDNGdHpRVlk1SU81T2tPZGovRW9wZzNmSEQxUHdCUEV1TzBUUkZOQWkxWjZETFVveDkzK1BZQktjWER3RwpwRDROb0NMUHhkeVNQQ0ZIWFRnMXlRcnpmaFVsSndZbWVQYkVnM1g4bWd6d1pUU2FXYkZ3TG5JZG1Qd1lhMkJVCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1cyY3RGSUxZcFJRMytad0ZwNGMKN21uVnBxbXA1emtiR0hTN214OXZVUUljWm5pcEpIRVBNS1JjUlZncTZPS0N6Vkd3T2JwSW9FZUwrSCtyUzFCMwo4MEFIUnV2NEZpMlRHUVBBWksvUjdPSCtnZE1aV1YzSzNSeHZHaEZINEhhZGdQZ2RNOHVtUzlYNmhXc0hjRHd3CjFKQ09BQVlWM2VTcFQ2MmhwTkhLY1luRU91dXRMUllrbVNjd0FsazNkN1lZajc1cy9BRzhqQUhzd3lNbUVDbVIKY2lHQjVhWXJscWxHeVVPcUVvWHI3OWNJL2ttQkJleE42ZUJXSENMNHErd1gwZ2ExTnovQkt1STVvM2w0T3hlbQo4Sm4xWmVqQklZcHZRTHBhMnZpMFFHd1I4MjMzQS9URG9TVW4vT213aVlnd0xvOVdWd09WVzFmNUxyaW5iRDFXCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkRvYXVaekZtMXdBc0pSeDhrL0MKSm0wN1FyOU5OZC90bG5kWmU3Ujg4UDNBR25TSmFIdDJveThQQ3NkTnh6TzJNM0lHWmVxY3F5SnlzNW9qdzAyQwoveVFUN3ViWEdZVU0xaEtqWWdtYU5aMEZ4WUJkS2dRbVFOaG42NCtHbElPQjlMdUh6ajFzTkVZd09HRGRueThkCk1wMWZaYUZaeFlMTitmRFJ0c1RGRzFkRUs1d3V0MGpsMmQxUUN2K0NUVEUraVpLTDhmdlpHRUdKbVA0aHJBVE4KK3BFazdBTnQ3SlRGUWZxOEdYSDBqUUg2NVptZi9CNDNNY1FaeWYxbjFOOXdySDBoWENMYmQvS1o2dWd4QVJhVAppNllaY0pFRkdESUo2SUh0aVgwTHVrZVpmbGRvQmQ0VUxoMnN5N1F2ZWxhNVlpMWdKSERhTUJWL2c3SG5KZnBMCmh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcWtaRVpDUWFMdmloY1ArcEdkcVMKRGgrOGQxYzFEK1Baci9wMStqaFJKaG1zZGtMTEZBK1BMUjNZeEs1RVY0VWU1WlNQU2FqWFJSSGdPOU0yU3dVRgpBWGNqbmpzRGx4Skg4RnRIWU8vME0rbHN3VWF3UkRFdW5mYjNEQ0Rsd1JpZDhWWXl6R21DdnBlajBSNm5zL0xoClRndFBVWUtJT3JJTnBlbG1iS2JLTmFic3hSNWhpd20zaWRTdERyME1GRkZnWHVxaXFkWDVFaGltcEt0Kzh2dVAKbW0wai9RWEdlblF0cUNpRGJScDZKTmVtSE9wcktIM2VQMVljR1NmcTBGWVZYWW5xT3BqOWlVNzR4a3gxYnBpNgpKeWtEUlZSM01UVmhsZi9VNUhkb3prN1kyVDZMNkYzQk8vSTJBdjNRMXRmK1FSaXZVWXh4RVFQQUZPL1FYNXBnCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2ZvZXNheTVUTEdZb1V0bXF3NkcKZzB3L3JqUFR3R1JTWnlYam9xZFpacHFXRExzL2VpQVFKSEc3OUh4YVJLTnpWSDhYSlhIVFRkaUtwTFZBUk1lUgptc00yaW5Ea2pVR1pqdVk1T3luWXh0WDZPS0J6S08rV0kwTlVOeXRsWTJmS2tpK0JoOWVzSUZKRUZPTmJ0bHZrClp0Q0VkS2phaWFRM3BXclpnUlBXeWFiTGIrR3ZqRzEzUWpGT3ZRanlhbFFza0pEOFFPTVVwM2I5c2UvMjRIQkUKT0czU3BZR0tNNFVLMlRqWjlNSnB5MW82TDdGSy9MVmhUNWNqR3h0YVVSWHY5RWE5TEUvakIvUjdUcFRLLzVrMQorWFhWSkNwRkg2MXEyUkpWNFRCbnZBamZzcnBIMnJFbUlQTVFDUSs0Zk9zZnNLNmJtUElSbmRFc0JzbDZjT1hMCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTJUWW5YbDFCNzlISzhIZFRscHEKcXNwQzBVWVZOVUdCdkN5NFhnMEFqbTNRdExrQkRFdzB3cXR1WnlGUXloU3FxdFhPQmoyOUs4aDc2aFAvR1ljcwpFOXdUZGFaejloOFVjNWQwYlJaL0IydW84ajA0UjV2REZyY3k3QjBFTWYvYWd6ZFFNcHh3YVUwNDh0U2xuRWExCnpnM0JBaXBOeU9JWkg3OUxsUkZINVl1WDZaaU5ZTE9SdS9TSndCa2dPSTlTbVIrdHRhWHg4cXVjbGxwMWE3d2QKRzQ5Tmo2N2dZc0FFL2lJT3B1SjFTU2NWd01QYnd0SS8rWW9YWnNzdHoycW9OaGRxdTgyVzdkc1lPM2pVMHZoRApCMDAzeW93Q1hUdHBtNGE4NEJiYkdaQVRZekFkZDhwYzlDMU9vb3lHaCtCeVlaMENTQXphSjM4OXROOFhyeVNpCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMC81N1RtZFYzYzA1cjFIcHlXYXoKVks1ZWRPdXMvVTY5alJ3NDVYY1FORmpOSEpWTDlSejBoMTZpTXpsZzlpNFJ5aUtRdXROd1JrMmJkOTgyNG1KOApGZ2VvUWk0eVB2cjJDWnhDRXo1VUZoc2xETWprdXJWeTloQW00eVRVRVRhTW1HNkgvVmVWZG9ZSjdscFIzejNrCm5DVHg0eVVISVlyeC9OVXFRbzhieVN2dkRGRysxalNhOXpnUHRyaHI1V1QwVm5ieHNCcHdHaFZnNzlOTzJXbE0KK2kybVJTeW1UM2Q4Kzlna1Z3ODRDRUZhaFYyRENTNTllSkdVQUwrT09GeG95Rk12USsrQ1lEcFBPWGQzajYrRwp3R3N3dUxibTFBL0hmNVk3bzhrTkZkV0hCVDdBVlJyQ0dUUTRxbzFxUnhVYTk0TEVLdjhPd0ZwNlJzKzdmYU9OClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcENqMHFjMnVwVnkrZmpvbGhJS3kKUGVSV2JmZFhGclFUOURTdTNjcmlYcWVyWUQyUWw4Zno1SU5wcnhlNjZ1R0VRZGVnR2RxRkkySXRkNFNRaHk0WQoyanJlcmZCeldRemNrbUdxZGp5ckloVWtxRzkrOTZYQkJrMlJMN0taMWIrd2JobW01UWVDQ1U2Y3ZrSTFqZmhDCmhrS1hnRkpaalJST0NqSktrZ0NpbnZlU1B1RDNxYlIzblFpVElTL2ZqMmZIMUNBdHg1UUUzQzZEWENGZDVuRngKWDhsMVZVL0l4MllBOVBockk2ZVM5RHpGbWVzN0RpVGlyMlFDdHpvVXhWQThHVDNYYXRsaUYxL3ViYmZMbUZpSQpCb0xTTkI1VVpKLzhDZlhXOTBtcGpYb1N0cUpvZVRSRHRVc2trUVpNQ3dPNFVSd3BIa3EvRnd3VVk0cFh2d2hpCkZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM2tFNE9wN1Q4emluMEF0TjR6aC8KLzdkUm0wbXN2SXlnd3IzWjlWc3dHQ2Jsa3JUQ2lYRFVPZWtXVWc4QVA4bDg2Sm1KenZGZW1wd2FSSDIyS25MeQplQ2FPM2FHNnVaWEVtQkIyQU5SYTFWWlFDQ2l5eEpjU3ZCMkhWSDROd2VJUDZGN1ZmK0NNTWFtclE5UlcyZkpFClBROFZYNmVLeGdJMWJYTWZoOXJoR3JNVlNkQ0gxTXh1eTluN3ovVjYxL1pZOHBkQSs0b3h1NjdyblZlSENtbWEKUmhFTWhWNWlOOFdDdFpPd0lXTU5jNWF3ZHE4bnRRMXBibWdwSklnTER2RkhadnA3Zk9lU1ZEaFpSa1hGY2NVdwprdHBzRkdWWXpJd3Q0bG15WnloTkgzY1gveWthUElJLzJFQVQ5MzZEcE50OUpNVFpLQzd4NmFBK1MwVFdGYitYCk1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkJDM0liUXk3c2pXQXdEcVZKNy8KZGxwSFRSYWFQekt3aEMxNWR1WXAzVUordDFSWk5ldUg4amcxL0RHRlpOcmh0bkNMR0RmRi93YnpaMjQ4ZmVkTwpzVFdYb2VrbmNWbGxzK2pxbU95TUxSaWhsVzZBMTErQ25mNVNXODlucGxHUWJMVUhiUUhRRGVySzhzWHpwVXBKCkt1cmQ2QVUxWWJveUJIMXdQbmRpQ1lGS1Yxb25tZkZGVS9SMnpQVmpwQk55TnZudGNMZmZXVjJkNmE0L0pNMk8KbUU0MlhTOWVsUjlPNnBQczVPQWFQOVE0OHJoMDFIV3I0bWJqcGw4MVFLaERGdUJKRCtNZ01oVUx6V1RBWVVSdQpSL1NwM0xOR0ZOQXhDRDZLUDJDYW5nVU8vWW5vc1NhamE2WllkRk9Hd1pXUWFIb3JFNjFFckgwMEhUMHJHeFN2CkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2pwVEtla3l2MGpTSU53RU80TGsKRUE1UVJzcXNoVkl3S1dMc3ExdER4d0pGZEgyOG4xVFhkbjJ6ZWhySGJBMjBYMzZRTm5uN0Njc1Q2QWFDdW1TWQpNUzJoQU5vMzZRZW1mWDE1QlhITktiYXYwNklVTmVyUUNHM2lIclFZdnNvcWNOcWJNRDZ5Z1Fxbnk2S1A3cWZaCmF0RmlBQnBKMGRkS3ZuMGhGeldPb0UzTy9kRVdadkJMT1NrcGFCSTN6d1ozbldmUTBoRnpvQVVYK3JhVkxIRlEKd0Qvd3FlU2lWMnNLelovQUJVNlFKbEg3QjRFemo0MzlmREZ0bHhDNWhGWlFHdHhQSXVUS0RVQnpxUUJZcXRnbgpUemZqbTQwRXBaY1U3SXBnbHNYYzI2QU5PUkZTaXVwY3BjOVUvWUV2MGREVyt1WFU2Q2lXaXBTSzlTSERLTlNDClJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1Qzc0NLaXJpd0EwMzltcTR3RlAKQmVQN2MvWHNhWU44TnlUaWhnQnJ0bkpQYkllcDJoRkJNdTNPaFpUcTgvYVBkM2NQdUhtaUhxQllncDJYbldIVwpsMGU5M2kvRGkvY0x5ZjcvR3kwZUg5NUZ4TXpESi9oajRLdEZ6ZVZvSmViUEhCT1JHL094bk1zc3RkM3Q4OGZ0CmsrQS94WFFVSGozaytuelZsNk1QeG0yeTBMU0lUTi9BdGdDWVBiRTFWc2xvWnlUaHV2a1VwOWY1K3d3U1l6NVMKdnkxTkJQOFdjK1dRMkdhTjBYODVFcWc0WnV0eHN4RS9xMXZZYjZSaEdCaUhqWTZ0dWtBN1ZBS01BWlpVNFY5VAozeE9GRnAwWHlUd21hQ21GeEF1SVJxRDErR0lWaEtsdjVKclJtUG01OEpjMmZoTFQ0aUNueUp5TWtNT3RqQWtICll3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenRiYngxRjRvRy9rTXVvVkNtUW8KLzBVV1NQSzUzcGFhbUNjNEcyQ08xVkFSY05mMHZkTVh2V3doT08yd2pZQ1hzNG4wSVNGSS96aWV0YWo5empRcAphOEQ2NnAwLzFvVjZmU0xtMEU1bk85SXRtZE9QWloxVzI0SGlKT1BOQjBWK1p1ZVNaT2tRM2hpaUtzZXNKa1BDCm4zanpjSk9rTVJKc3hPSWlhTVpnZWsxMzdKMUdFeklkVEJMT3pQUWFiSGI4ZkhLdWZyclgzTjdERFZUcVJsMjUKR2JZM3czNkNQcXpiVnQ2c09uY0ZmdytBbFVqR2d5ZXJFU1NTWXQ2WXBhOFRtZk9KdGduVm9ZZHF5Wm84amJuTwpFK0dIOHlTTHJCNHMwTmh4aDJZQ05lV1hoVHR3RnpaUkJ5TmRCSWZvVkRPYmM5N2xleXFXWFVVcDg2b3p6SzFmCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDZyVmNtaSsveXV4VWh0bzRaMU0KUnd1Q056dTJ5Ty9XM210a3RWUTR5cGY4L3ZPV2dOV083REozRUR4RWRHcXFKMEJpSDRncXpoQlBBNlpJZC9VWApDbUlUbGNSVXJxNytGaTRUVVFMNmhYRnliQitHV24yMUxUeHpJbkJuTVM1RFBaRFByRjNZVWZ4aVhIQno5VVM4ClFESnI2MWwyU3ZxMDRQZ1pEQmdZbnZBWTE5TEkyTVVvWTROcitxaVc3VjZIMDZYZlBRVzdydWdva0duNUI3aUkKVzY1RzZvSXd5ZndGc1E2azJHcmdPSnExWnlkSVFLMDAyZ2hEQk9zUDJpQzZUK2RZT0F2cVhIcXdKeWZUWStVbQppaFMxZEZpOXBTQ0poYzZpT3ZTa1pQendrU1N1S2gyRHhzdDFuUGQwb0xSd2hPRWU1OW5PM0xnNWV6SHdJVXIxCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWY4K0tnSHVxSTM2Q1Z0N1Mzd1YKeDdsby9iSGNYcmtjR1ZNbDhhTCsyU3FlMS9LYkdwMjBuam5YZmZmNVpCVXRkcDc5ZEN3anFtdjhDZE1xRTBUdwowdCs2YThkMmxIbkVPZnY0dlh5OUNRTE5QdFpQM1hwQlUxUFpDNkFNWHpFdU1ZSHMyL2oxL1hlYzRYUlVwNDBRCm9mcmJLaFI0UVY4N1Z1OXNnRUhtVEpJVGM5cktEdmI1eWEvWlQ0MEZ6U093WjNFRGxBbGVLWlZVT21TY0g1VysKQURHN0xHenNqb0p3cmhtN3pJNXAycDVFa3BjdTFoblBPOHVCcExEYzA2THMzTUQ4aEFhTUo3UVBrMFJqQ3lTcwp5M2IyRm5PaCtTN3NGeXZUcThQMUpacHQ4T3B6NnpEditjZWFic1p6T2k0cWQ4VVd4Ui9EOHFkNnZvSlVGNG8xCndRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1pDODc5cG91anUrODBJZ0RlZ1UKRkt4SHU4YjN3SnpsWGpuQmV3V1M1SXVPWVp0MG0rYUo3TkNQRnBNQ0hQd1grellZZGM4dHNhdngrYkpGZ0d5QQo2Y2VBWkhlMlZKZnQzRGo2a1JLaklKeWpFSDBGZE9TNXk5Y2dOdm01dEd1N1NYUW1vVDk3WkdUbjM2clR4Sm4vCkM0U0RwajRGTmRkcWlsTEluRGNOWDI3V04vSFppZWduSGpkVmZSVWt3WjFVbWtqbVpISisvWFJvRG5YWHBKeW0KQ1hOOTh4emp5ajhjcTJTREs3L1BrdE93bTNxeE9taTl6MHg4MkE1VHV1MktGVWo3NGhuWks2MHV4eXRBU0l4UApuT0xVNkJacCtsSCtZaDFZMjlYcFhHbmtHenJDTW5wc2EzbnZiZHBrK0tiL2ppWnQ3VmJtYUpaTUt6MGZObEhiClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdExwdXdXUnFmWHlPbzV3VmdZNXQKWXp4WFQvcWdLMDl4cFBLS3lhckFORmcyK2N2WG41eFdhVGdoL0dtLy9TdTh2UWM0MDQzL1lzS0Q1K0diTE9GaApTM2dIb3JkdXZBb0xjYkVRNGpXL2JvNmZDR1MydVdwanJFQTFsVHNxR1BwSVRCczNyMGQ0Z25HYlo3YkVjVEpiCmNhL3pDSTUxdDUxYzR3WGZkcXNsYVhtMFRFNE5QNUlsYmdINGdUQlF3NDhrM2g0M1ZuZDFlcU53WGxsbVdNdW8KcmtWdEVjT3JYdmVBTnNqd0Mxam5yVE0xZHMzb2pCTkNCZ0ZFaHZRMjFiQWhneXQ2Z2FwWEtBc2gyY0s1b05KbworTFFvNU1kNmtIRXljUU9tbUhSaGY2aHNkNFFaY3ZMTXZQR3lxSW9wTUx6TnlZY3Rac3RNT0NjUm9MUDl5UVkwCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelJSOVlPTE44eEkwaFZoWnZDZWoKVjRNVi8wUVNuYTdQS1BlYVhFNDdqQldZWmFXNFZVU1JIQWtybi9pMU9GRFdyWGtKai83ZEJwdWpBdlZlQXVubwo0T1BiVkRTbFJtVkxDMXZiQUppeVBnQ2ZUSXJiVzU4WS92YzJyWUQ2aVhQMmJncGg3QVN5SnBDNWtEcWMvWkZtCkFIUzcva1owYlI3ODBpMXBjeEpyai92ZWF5TnlQWGxFeTh3K3AwN0JGbTZxd1VQNEF4QzVZTHZ4cjB6V1ZPQzMKOFh5MmlZeU9oUGZJcHA4bGxLL1hSOXFJQUZ3WDAvM21PVVVWYkQvOXUxUGE4NmRDeGhkOUdEdU9DMTVDNDRoMwplN09obXFlMlFHQ0pkTEV3STE3WEtaRTNmdUhPQWJ3aDFzZElGMDJiYnd1SDBGdHJpSXBJL09uQjdGZ0pFbHZGClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzJHcjdFWUJzVWJycDVHcGE1SW4KYUdHVnZ3VFArL1M3UWxZSFB2bTdBR1ljVEIxQnM3ZWoydTVMT1pkNUxkV3hTKy9ycU01b0EwbDBpcDJxczlXeApwUG9hd20yb2xZaEtVYS9TUUQ2RUJBeHRad3d4SW0yanNoTGM4MFdPaGs2U1AwbWtBTTZtdjJtN0ZqN2k2a3M2CmdKS0xqWDBkMm12WktIMkhiVDEzV0pvdWtJalBzdHpUMGN4cm9GME5USS9STWJ4WDVlZVNKQ05qUGhTaXNvRnUKVDRDalVxeU9TSFlFYTJWcnY0Q3A3bHZ4ZU83VFhTOFBwWk9YQTJSamlYZ2c1N2pLS083bk9iSHFqZUJSbVV5egpYUkpZcXI3V1Bzc1FZQ3JlenRxYlNXcGJDKytwK2xDYm1vODEzaVhXN2duejd1RlFuMmQ4Z04vVTJyN2N0YkZHCjB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc05hUTcrUHkwY1R2TytIaXVrUjgKMThUTjR2R0RUMHR5andHcDAyL0w3ajRGTEw0akhyeHZyV2tvclg5YmJCMW51d215ZmJhUi9HNlBDSzZJdXQ0YQpabmxBT1RCQTNxK2FlTUVqbnZaeTJTckw5MU5kN3piSTU5RTZrbEJ5SVFOV25kbkU1TCtoSVVjQkFHaFQyTHJSCkZ5elUwbnZGa0J5dGR1dEQ4a3JKTUN6dGUxTmdwRUpNRzNCVjlvaGlZS2xhTVRLZGQ1dlBoSGFaL1BybmtuMncKWkhEaW9LT3A2eStpcmJ1N2hxUjYvZmlpZFl0QmpzeEFqRFJzRG1NTU9oRUhkWmNPYzNMa1pqMEtwWEtrNGppUQozb01qTlJOMWxsYzR5dWNrWldrdU5SbWlBQzl3RENjOCtMWkN2L1lTbk1VQW85bGR5dU9ZQXFITTBEVHBXZU9rClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOWs3VU5zQ1VsanprYy9XNXhwbkIKalZpZi9Lc0dWY1dCS0o5ZzFMZGhZSFRnalVTRGgrVHJBdGZlNzN1bDBFd0ViQitreDFpeWdqQUhGcHdPeTRlTwp3VGpWdEdlaHJIRkhwNGpVcEJUa3cydFVIRjhBS1p4ZUplSkZNTzlueWJkeGJQcmdhVUdVelVUbDJxS3Z6SEtSCmNJWW51WlhoOHNteGpLbnRKb2U0Z3R4TUJEVTZpTlFsVFJBZ0NQRGV5UGdmRk9ENCtEQm1DR2h0WHBzNXNkNVUKY29DS0EyaUJ6b0VlSUZkWnBia0lIbVlvVFFBL016cVBqdG4zY0ZxYlJONkNRZ1hjWTJyQzRQeXVLM0pBS1dkdQpuaTA4NXJCdGJsSmg1Z21FU3laNlhxcWphZ2I1T0cwNVZjUVdWVVRwcjcyWFVhN25xb0lZcGVxL0RpQklQWWRVCitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb1lDWE43WE80andscFk5VGdzS0EKdU5IYm1NTWxrWGJ2TEsvZm1EeHdDcFR5YWU2V0liZ3N2TWtuclU3bTJjSWhvNFlmalUzT05ReWNJZ0M1cVc4UQpySitBQTR1emxsK3dyZ2N2WXBTSFJwaktuV3FZVlZGTWdjVHYrTW5TcDI2ZGl3blZpYi9SUWlnWXRrRjM5aTlsClB2UWJBS3pRTGt5a2JFK296S1RobjZiSC81a2txOUhSZ0pBK2VmMjZKOHhaSnBvZjI2N3cyUml4emVnUDdEVXYKMVI4QldZMjNYSFdlMlF1QjRGSEl6NGdEQUJJZmU2ajR3cXhRQzIzQVBYbVhzL2MyL2xaNzhiNWdETHZ1blE1VApDQkpEU0FoNDdVVFc1WTcyWFV2QU1UT0UrMWlLWTVIRzVWYzAyMk0ycHlYbEtVb3FrenpPZHdDc3NHeEVMZjJBCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkRxZ2pacFE4OHlnUldxbDlsek0KUUt3OXJESmZEVHgwTStWZDdHcDhjckNuU0NBZlVOZS9CaVlGdTBiTHp4eCtqTjRoTlEvRlFldS9wVXBicTNVTApMUTJRTVF5cmtiVklRSXBuR1ArekE0K2NVRFFyRFNXM3BIU0dWdHNaVGY0OTNrSitBUW9wbnFtaS81b1Q1SFFBCjBTNmw3bTNneDlUNDJ2SE40OWxIOUYxcEI2L3h3bEJncm1jcDF5cEtUcmUwZkdnTlJyUTRVdC9GR0orSStTMnQKUU1kaUE0NDVqcS9iaWt1emxTcWpFMXVVaWRzRm5wTytUYkgxZXkvSm9DRGpEMmZzWTBzTVZDNkcyMGNtVjREawpoTTB6V0UrZzNNQWhsdnRMTEE0OEExeHRPaDhBVlljb0VkTjh5TDMyd2xpT2l5N0xiVzUxR2t5Z21ueG81UVJXClJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTU2SElQdVJoZXByNHBNTGdmM0kKMi9lNHNQTmRZWjNvNFB4V3lXcFVkampOQ2Y1VjBSbU5hNHhaWXVOdGVqcktoTkNLcWJnYnNaSStrSDRqNFNxNQpUdW41TGkzcTY2M3pxSDEzeHlTa05OZFV1eWZrL1FyaE1EVk5PUUgvMzlDM2hqZUFPVnFRVUJ6NkNaNlV3Unk1Cm9MR3ZPbncvaFZiMEZuajVHaXlvSnFWMThGNlhHY3RYbENlTW5VVEZjZjZoNTdwOUFwQkkzQ0pNMXA2Uy9qb1EKSU5sWnc1am1WZjNSMWxiM1ZKdEV6REJjTVpIUXJ2N0xWYmtIemxNbVFZWUJGVEdOTzcveTNYU0dJK2ZmemtsOQpXcGtmV1JKQXMxcCtQd1lHRXhoTnNlemdZcmpIRzZac1hkZXRqMGp6NTNpRUtDaDFxOVQyWFVQbkw1dlk1dE02CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjZrQWJsYkpvKzFBaTJrN29QMXcKSllVSUxqcDhxSGJqZm9kVExFS2xqcW4zQmdTTkhsdmdvczB2RlowUnZVUXJYN05DeTRzazFjQWhsVnNJUmJwawpUdGo5ZW5GUGFlbjZKUzJ2TDJVeGJ0LzZsVjNERmhSdDNBazJ6TkN1dTU4RlZjL0x3YVVRbzlUNHpqalJyMXRHCjRSUlc0OXl3VUROU0c4Tk1ySDFMQU8vVlhmM1BzcUZ1T0lwZmNTYkFQUTlIYnNFSllCVEtob1ZZSkxtK1VkOS8KRkNza3ovRFdKMUpVc0VISS85WE00YVBQV0V1L1RkaWJwdlJINGtPL0tFLzNOZXVlM0krSWIxQUh6YXhQNzdLSgorVnVCZExCOG1UMVI0ZkxPUGdOVEN2QUJtclo4a3BWc0hvd0tmQWRLRy8raG9ZV25kbFhUOHhBNzUzNXZCM0J2CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclhLOXE2NnptMGZPbi9kMnRwZXQKZFNqRHNRYjhYN21taklhMUlhNk9VWEpyKzJwOVMyand3bEZFZGJ6V2hGaVhEZ1NXbitacHpCbmQ4Y2taZlJkdQorRjNxdWVUZUNTNnZFRnZSdjNmSlFRbkpzdlo5dVdoSGVqeFl4T1M2TEdpbGRUNzdYNExRMFBpYkRyZHRHTTFzCi9YSEVyWjFMUnlUVTVwQ3BCYy9FaTJUYmJoRFFtaDZQU0hUSFFZRTJCR2dXWlY2M2I2MEI0NkREMmwyZWRHdXMKY1ZHWUphcVdCZks2NXhONC9UTEc2a2l1RTJIVUw2Zno1T2xvT2kydDhUeGtyMWRMNlNYcjRVREczS1M3UzlsVgp3am9GVGZvTEdZTXFUQ1BIVTFtZ3dDaFQ4eWFDWFArcEJRK3dYZllaaW5PZDA5UUV6U0t4d2JUL0ZNdVM4Zk1xCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdlJReHlpczZVQmZTUlNwdG82QVgKV3l3c2VYMFBpSk9ZUVNvNzVtd3gvNkhxYXVuZ1ppUGlUZDllaEtmY3drc0c4c2NjTGZKYzhyOUNQV0VDUTV5WgppTUdMU2hsWnNKNkpJL1NkeHNhaFJUZG1UWmNwU2RRZTlNMVVvQys3M1E5VDh3Q2lmMnl3WWU3dFpWb3ZEamhxClZQd3NuZWYvMGY5QUt5WXc2b0tzdUI2V252K2Z3YVJEOWh0TXgzenJ0dGtuVVBmMHZtSXNWUktoQSsvVi9TVXoKQ2RPdkViTVdCZlZsWVdKTlVvSlkwYUZtbk9HaW1vcTl3dE4vaWQ3V2ZGNzhTeXhRYnVXNFVLMWoza3ROM2l6OQoycXhWMmtxRURuU09ZUjkrM0w3QVJrL1RRUnZoRWduV1VYeE5nSWVjMElQVFFxUzd3aVJZSGtBcUVaOWZFU3dBCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTlOMzBQbjZXRlhYbXpIa2lzMjYKMlNPTHVPOTlTeXBVZ3VEaU8zdFViNWRFTHgzTlhkNk9XaFN0Z0FxV1J0VXFnRmg1VGRPQjAzU1h6SEg4RFNrVwpFMVBYSE9KN3h5Z2JPU1YzS0dWVGQzZHlVQ3dWNkJrOE5vTm1zUG5GNlp1N3FSOTlQMmlyMnhOaWcxYTRkVmRBCmVzYVZoRG1lSmpDWkJvMzBuR2F5bEJUNm83UDg0S25iVFlKT3hyMXpMd2piUDBRZ2wyRFNYem1yeGtSOHV3VUgKMmg4bVNEZXBYT1JFKzh5Vy9JK2M4ZUVSMjFOY2dsVFlUN2VTMG1tOFIrbUloUFdtdjZhN1BUWlJtWlRYZW9BYworWXZBZ2pSYmYyVys3NCthOWw1UmtxMENMK3BaL0Zxc2dCb1pqR3d0UWp3TFFVV2FmODM4c0lVdWdhUys2T0JKCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdTBZbmlQMmlFeEVPU2hrL01rTHkKVXVwOEJHVE9VYzQ4NkF3TDNwSFF6VkozSytaWElzRlcyeVl1bmplcFROUTE3d2gzY2lGM0ZWRnhuQ0dvSjVRWgplZ08vRDY3bDRQRjdhTEJ2dTk0ZVc2ZHhKcEZSNzdLZ1AwVXJSbSttRWJ1YXBmNFQ2bktTNmVOUmROMGdEdmFvCjdGRTRSYnc2amo0bGlpRU1PNjV6a2svVGFuc3EzQXhGdHo1L25weHhLWVFldVNLK1Ezc0cwaGo4bHRJcDhJRTUKdEE4K0RtTW5XQUNPMnM5dGpKNzZkWUx1Y0NPVXhNMlVjaEJDYUtxTEVMdHdUaUZhSDJYSytHSlhJUVNKdEtkYQpKMTN3ZnZRM2RSUDZYTElWZ2k3OU9XVlZBYnJzazJUVFZ1UTg1UDl1cmFNSFl6MHk1YUl0UUxucWpSUTV4d3kvCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFFXa3I3U3JiREZ5NktLaVUyY0UKcEVkLzNJbnd2c2NuSiswanAxWC9XSmJ0WEc2YnVTaVpWdnV2Tk5ueUNieFEyQUpNc3JGVWJLcTFHK1QzMVVPUgpZM09jbnJXcTBnL2pRZmZ1K2N0SWNMdXVaY1A1c3NJWEhSSUh6Q0RjdDJhbW9HVVBYc0oxUUlEbUhoUVB6RTVSCnhyRFlOMU56OTg2TTMzK2RjNUpkZGlwWktjMGFGb2ppTjVuVjk4R0ZPdVFmSCtoZ0hwLzdNMGZIVmdaVjFDckwKL0hwOXFsaWFtbDdsdHM0amhyWVUzWUIya2tDcHpnUTFjUnB5S29menFOTmpDU0tJT2dGUCtHV3IvYlprNWVwMApvckYvbWtPVFo3NlFQT3BTNklROS9vZTVDaXJDZnQ1aTlwS1hnWHFQakpZUTFVZEcrY3hFVXBhNStldlBmMGxlCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGhzR3Q2NVRZQ2xKWnhlYkUvYmgKRitUclNQeEx1Q21IQnpDeHk4aHJRR1ZSWTBabUd1MlowZEo4ZmxBdElHSjh0QTVHV2V3YytqVmdWcWRYUE5DTwpuRTM0ZUFMdFgzNitoVVlmcDZBaXl0VkhzUHFIVjV3OU5nemd3OU1GNFFTMThwMXhsYWxDTjhsTjUva2RrZFB5CmxwRUZKdWhacWN1RXFwUzZCOFdzNkZ1UHFWcjZOVHpOOGd5NjVMbk9UTmRIZ2FqZkY1cFhVSFh0WjhtSFZjMzEKUjZ0d08xTTg1T1hBcDBqQ1hBZlRSRUFLWWx4VTEwWnc0Sm1SOXdaOWZhV0l1anN6a1dacFpIV3lYdVZVLy9jZwpPN1gwSVROZnFadHlHM1FaUUJRcXA3Q0tnOUMwZERRcEJIajdOSHZUdHYwdlZJTE1nU2tOVnlVMDJnL3RLKzVKCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUhSZ0RpdDZJampSekZ3UjgzUkgKU3pWYW1RYVBwWFN0Y3RjMVBvUUI1QnQvcVB1V2U2Y0FiaHdpY3RFRkxqT3cvejJSb0FwTFBROWUvZXljTzZsUwo4SjFGT1pKV3BPT1k5NDhFWUora2xRL1NxQlBHMno3bDhzZG9ZTUdkRkJlT1ljN2tUcTNWTjZvcUYxZ0VzdllBCjdja0VnMWtETFNDRTQ5Z0dyam5aN2NRVzhmSlBZKzJhSmFrS2UzSGovcUZYaVpnQytMNndJTzcrOVd6UVNncXUKZTNUN3lzejRXTTMzQ2RLSHo0V1Q0RDgyUGxNS3RoVUFMTUtWeGhMa0Z1RmJMYU9HZWE2Wm5LclVCVlhmeEtiYQprR3owL3B3aTh6ODZ4Y0RBdzhQbEtOQkNYZVRDN2tOQzlpOVRkMmVYUVBkZERoWmkrQkNQNllRZm80elg4U1BGCkl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBclI4a3gxcE50SGx1RGZEQU9nNjQKWmRLTm1OYWZMWmN2d05UbE83Vk5EZGZ6SmdIazFMUFlPRXUyQU5rMVVHVFlJdzBZSWpyaUZodFZ0VDZRWCtBQwpNVnM0WTU4S3Z6aUdPL3dvQ3J6dE9DMkpsVFp1cVIwMjFXV2VKeTl5ZmVmcG9JYW5vRHRyWnNJekFJUDFRdEFCCll3eGtzMVl4NlBycHFOeURJTThabnB1UjR4emxuVWljMWhHaWdBSWVJazh0NW1aZDdab3BjRE5VRlM0VTNEdWQKUWZBSGJ4bDk0N3FjUXBabTdrRnV6RjRnY21iQ2EwbmV1bmFrSkdHbkVucUZqeHppZEF1Y1pDZUVhemU3VXc5cgpOcTg4QmhBNkIycjBkc0ZRbElpS3lTUFRiWElhMkNLM0ZFdCs3RFBYUDlqa0YwYTYxK0t0Z1ZxVlBnZWNpS2FxCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemZPOVQxYmxuWldSeWM3Y1dmc2oKQm5NWFBIOTRIVkRuNS9iTGsrWE1Nek10cDlkWTVDdHRWZE9zMVF0QXg0OGduNVhiREdkeEcyNXgwOFlpcFZXTgpsc3FzT2Rxd3ptRmxKRmVPUjhNSVdDbEJnQWRNeCtOK0RrZk5tSHQ5b2dMajdKazNDL1UreFZXellRalNyM2RvCnRJbmlwUjV4Q25QYWlURzA0ODhQa0ZSUXI2azBzRURkaml6UHhoR2Jiem5nT2JyOFJ4NTJEeEFZWUNPbTd1SEEKUThJL3l3bHdmanE4OXZpaEJXUE9TbnMyczQ3elpIZmtLdjFXSWRQVGhwWDhTcktLSGNRbDQ3ZlRidy84VDdlVwpjU0szc1RoQ2R0ZUtTR0hqckJuTHBPV2Z1amp2aGtXL25wdXJXekl2UW9PWFJXOFU5dTlIZGQ3enBnVG16c1h5CkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUVQc0hGTTBkcVRlM1puOENJSisKdGxkY3hGdWJmbVVCN2w1TVcwUG9WaGlpVkZrTnpPUTVPNzF4SnhjYmVtRDJQK3gzSSszR0ZpODcxRGFJY05rbwpEZWJEVjRUT0tzMEtNQitrSU5mQzV4R3cwY2tCOHROUEE4Ry9nOHROWWdCeTh0TGJyMGQ1SDlUcU4xd3JKcEZSCnFHdUFKZCttRU9MNkI4cnNOUzA0ZjFmOVlXTmZWcUFPM3BFNW5OWU42N3JkSVhydlI5djdoZmpNUDZIdjhmTysKTlJoalpONXRmLzhSNWdxS2pJZXNYbnlSMmIzS0F3Vi81dGN4ak15Ukt1UHZmUHZ6NnZDUm1IeHFtRUZEQXBsVQpPaE92NnFlODRZUTJvdlRzMGJRRStQQU5tWCsrNkIvVVljNjFJeEc4RVRTMlBlSnFCNFljSzdzdjhHQkpsaEc0CnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm1ZTXBjNndsZ1R4aWUzWFZ6Q3gKd25JYjBycE9JbzU0VjJHdG1OWGtOZDBmeUp1R2oxMk9VTFBFMEdnRmZkVlp3MDBDVlhhbEhWKzREVzd3U2pWWQpvVGZqU1cydDdMTjVFbnJ2SFhEY1hRTXlqQmhGRDBQR2Q0d3hkUUsrVzZVSWxQTnJzdndMYmxmSERBdGdJKzhlCkIva1JzVkdlMG0wMEVVMmpVNnZTSVVUdk83d0I2Z2lLNDlzK3RBZ0Vtc2V4dnF3V0lGTGJVYnNsT0ZyM0VPWGUKa0kyWmdGcGtXbDJISlBhT0orZy8xanFIMnc4a0phVEdrSEx2VWdicWFYOGZ1dE0yVkV4cTdYSjVudkVpM1M3cgp3UHlUTHIraDBsR3ZsWHhUa0NPeldWMnBFWDYzMFJ4N3o4N3RmSWxTOU5NeEZaT2ZMbEEyd0tONVpEM2IzcWM1Cm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMklwT2tHM3Y4aWRxdUFjaE5pSW0KdFdzcVIvaUZhWGR3NHl4bGdOY2JoeDBmMlZPVSt1ZWJha3BRdEx1dzZrUVNaU3pYdjk1NE9GaUdmVUs2ZHExUAp3YnU0U21wbmRQRFEwWENVZGJQT0hVWC9BVGMyYjF3NnQ5Q3F0bnh5bnIrTHZSRjU1aExZR3QzbjBJc0tFbEJ1Ci9JbHp0NUhNUHJqc2FlcVY0NzI5eEFHTzMranZ2QmV5bjRSRzRRcDQybzh5aTBKbEM5VU1nYVB5R2xsUjVUM0sKR2NvbXZqREFPTVArUGNGb3BGNTV1L1dHdXNqSVNWTm9XL3FsaEQwWDZCRFpFcjdJczVVbnlBdE9HSHhYVDFpTwpORkZ0T0QrUG5QSUJIUEZOeVFFUnBzWGwwUm84bXkvc2c3KzlxbEdMKy93NExjVEFzRi94bTBxRWxObXNDWVhVCmtRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGd5S3BTc1ZFcFhpbmo4bll2MGwKUzYrY3R3ZGtNdXQwckJ1RnBreDcxRnBVL05VSk5QQVQwWDJwa3FxQWV1QjNaQ3BzT0JqZzlnQm5jVldNS3NYZwpGamFsdUxmUXltL0VMTDJ3ZU9MVk5OVTI3cFFOeHhidFZwWWR1KzB4UzVHdVdOeGJvdHhIb3Yvb2lDY1dHMlhWCm9UOTlRVUtJWlZaYzZwckx3RFZMd2kvbC9GQW45UzRHM2d0U0tuU1FSV3FnSW9OWUd1TFI5Ynprb00rNGZMd2gKVXZmQ2lxZUVVbG52TGlkTWhmV2Z6WWsrbWJRa21uU0FvZnpFQ1NiUCtiaW84SVQ4OXhVMjNqSU5kZFREYkV4NQpneDFxU3NDbUcxSytuU2J6V1B2dmgrcXdiZjZOMmsrSlFBUmU5ZmQ4aFJUdHlJdHQ2ZVZWaU01VkRGV0ZESndGCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczFkbkVLSzNud1RnNDI2cHpBZGEKaE9Ub1hOdDRUaVVub2swUVpiLzM5T0NkMWdwZ0F5QklJczhMWFJNTFVKMG1POHYrQ0tCeG1KZ040TjNnTkdhSQpoL2R6YW9lMzZ4Q2JVcTVWNFhwNjd2dkc0ZXNhR2NLSWxZTDR6MUw0SlVsZ3BaYmkzNVlkM21jWEFzYkZkVXlGCjZKb2k0dmxvOS8rVWhPaGlhU3FWVHl0U0dFTkpoemhFQWhUYW43U2IreVM3a3prYjJoQU5VRkZlTDhBL2lVMVgKWk5xSm5IVHJyRE4zYjd6cDZZZ3YxOXhPWTZOLzBZUmhLcXZVUlY5V2xOVy9YK3JSSzlNazNvTkVZWlBocmFPUwpiODRqOFc3RkdmcW1wUTAyV2ZPa0RtTDJEYVN6U285YklkczNYdkhRL2pWcXNianRDaUthb1hSbHZKOHkyRlFoCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdndTRlpuWmR6cy9PR0RMTEpPRjMKNXUxeEJIcExEcHlhakF4cXIzWVA2MGhYVHppWXlDWUkzK3ZiU1ZPYktSaGVhVkFoY2lOeUFJSmFZV2k0T1lZTgozN3QrWmFwd0NIK2tDVEd3M2kybk8zQ0dLaWtiY0g0UUxQbFcxMG0reTNDRDVOako2VHpGRGZBUEZaL1JZMVIzCmx1b0FUajNOSFMyc2pWVksrTG1TckpnUVNIWWIyV0R2K2V3RFlVcVkyR3M1VUUxTnNDUTdQUElHeFBtbWVZSXQKOGxIcm5qeEl6Qy9TTWxhWlkyUmVJbWJUcklHclZlaUk1cHZWQWFGb3NpeWtCNmxPaXNpNHliczRJbzBzdW53dQpWNWxhcW42UnNEU0hmRVN1MGhFL0NCajF1NTB5QjdvU3lBdVZ4bjV0TW1ZNHhNeEpYd1lkbEJqczJzeUt0SGpzCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXJJVndEZW51UGtsQ29YOXdnWTAKZ05KQWl2NWt3VUVpNWY1NTRnRWEwbGxEdnlGaWZiTHg5K0ZaNzVGd3ZLb0t1WnFpajZzRS9haU5xVnNLNnNCTApuRTd1MW1rNjJIeW1kS0RCZHRnOVBNUGtOb1o0MXh1YllKMnlWU3JhRnB4cUt1WlZLc2d1WHVnYWNKY0YxWFBuCmhwTlIyLzRUMDJUNVJjbjZ4allOVnBwTll0d1VTaDhGRUk3VHc1bEZPUDUxTURDbXFBZ1VsVDlna0lIQ2VPQWIKTy9yTWxnSFpXMDFKNGRzMXMwSE9raHdhdVhCbnFNT1BQbTltUVVkQW0xQm4zLys0d3NEalBPUDkrQ29pV0hlMApVVmVmdUhvcVRCYkpGcnhCcEdFaDMxTHNNMVUvWkxLWWNqUTVQaTdVSlVRaU1qNUwwNjRGbUs1Y3cvTlBxcnRHCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeEI5ZXJiT3E2NUdiNU52cnhMZTgKTVQxUDR5UXNLWU0xZ2ozQmlaYXNSLy9EWDNiS1F2OExtc1QyM3dJRUtUMzFwNUQ4VjRuSW9YZXlOZTd5UDQzQgpEL1RZTzYyYXFvdGV4dGlqQ1lMZ0gvTDc1NWp4RFpkbWpyUG9rN1dSQkowQmMyeWYvQ1FHUUR1a0F0eXBTVk50CmtUV2dVZ3hWK0lkam9lNDlZL2tNY1llS1M3emYxY3VSOTFKUkJRWUpvQkxsZGsyS2VVWkJFNHIvaHY4TnNuL1cKR0dkc1NnbWM2cW96bnhrM2UvUmNVTURNZjRIRTVCeE5uWG9hOU1rQm9ZY1EvNFNxM2tDdTRqdjM4Y0J3VnNtQgoxY21UeXUyMElpRTFpRmhjUXl2WnFDMXRtbmFZVDg3b3VXblUvTUVXeUVRWFQrbWtBSGZiY3hiVzBqZDVnb3RQCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekc0ZHZhTm5jTnEvWEpGenFmdlIKcGJlWm5FNk82bmo0ZjhDVTFSU1RnR0U3Ykd4N1FpNmtUUnZaeFJFNUZQQUVMamFpMzdiU21hc2h3VkNXdjA2MQpsZ1hZVFZyMmQ2aDBZRXR2aUprVnd5NnE4ZUhFM3lyZEVFR1hFRDVkYnlWTlBQaE0yVG4vZVVHMVk2dEc0Y3Q0CnlFRTVmMGxXWi9Ib015eWdTenQwNitxWkNFSWJxdFR0OWhXZSt3V2VzTU5WOGIveWFHeENTUFJFTDV0emxTV2sKV2RicThUWmN1ZzlTTHl5NGl6U0RLNElJUFFSdGg2QkZFRGJPUkJEZUpWSTcrcWhuY0lHWUZaS1MxcFpJVkFBbAo2VkJ6MkJ6dGREbHJyZXdxdDJKWXdHWlAvSmM1ZmNSYUt1NFhJZGZLTWR3K3lJSEU4cXYwM1VFZUIyRFV5cmRaCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3JkcHRMaHZYamxmWEFZTHd1cjUKc0RYK3JqK2J0WStBVkpNTlp3VVk5eVUyVWdVUnpuU3ZZRjQ1ZnU0WVZrWVZJd2pXVGsxUEtQdytKc0xJcE15SgpBNFVqZGdNYXhicndDZ3EvYkwyYllReEN6a1kyRklHd09tMGphdDQrNnQ3WGo1QytEU3ZtNDd0Y1p0cG5teHJ6CmRWRDBNb0VUUmRIOXR2Z3lPMXdLYnRwYnorb0JrVWRSZFV6NnM2NGp2Z3l5ekptODBFRHhjeDd1cEg4NzhLd24KdzNkNFBKRmYyWnZuUUREOG5ucDRoQ3F1UEkxVEMrS05FTTRibFBPN0lsOWlKNEFzeFJyb2ZxSm1YMWlmUHlMWQpkUTdaYVAydGRvbE9QeFRPRXEvenRIMEE1TkdDNW1RWnV0dVJLeGVmVDJZRnh4bkl2Si9TdzgzemFMa1haaTVYCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzBnY2FqNE1sV004WHFockhXWncKTGd6ZUx4Y2tQMjBpL25qWGNYb3hPQUVzdjR2UXMwTkIxaExWRjRkcllFMVFOQ2lSd3hOMkh4bkdFUVJYTnpqRQpvalpVNk1JSzFhN0g1amlxL2JEU2hLVDZKMmVFcU5yaHBXVlprR0ttZ1ltcnF0bm1Da243QTFGTUFWSGdOeFJoCjdkZklqUGVreGZqZ2o0Z01WMVpDdmpLb0s4dTNzMTIrcjJ1dnIrQzVYOTM0eWo1QUlOdzU1TTJRSmgrSlFFNnAKZDBFTUd0SVBPQzROU1FRUSt0MzkwK1lqejI3NVFTZnFlbHNqOEtsNjdvNG4wQmh2VDhqTno5cCtiR1NMYWxpMApDcFdEZlJRTjRMRGhHL3h2bXNiWk1CejhXMHUrRGJwNTUraVpIRUxjcnFYNk5pQSt4TnhHak5Fd0RocHB2TnFVCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXNySUtSenJSSllaY1B1a2RIZ2cKTWUzLy9MOGNtU1hRRjRNUTN0ZDJsNElKOGJlbTk2b2hhdGl4ODVCMnltMEh1NExScm92c2dTdVBFWGV0b05jMApsWVFja0FwZmxPOWtyTTBjdG1ZVTZFUFUxUDloeEp2bThJK0lFUjREcmN4Y2wwTGJ6TE1FQ3J5QUxYcVNCZ3BOCmJBTHFkZVFNdXROWE54WnhkVmppRUJJME9UOE9qcWxYY2xha0loOFVsc0FwbjNYMFM4MTltajE1ODJBcktyVS8KWUEvMHkvUGxFdEM5VnNGekF6YUVOS0lSdjlBazluN3VQVEQ5S0M4OGtzQXlwNE9idWV6aXBxdDRrRlAzRUVyMgpFbkxjcG16YXQxQitYZHNpMUxLWTZRWG9CSmpkcTBKalN3WHBWMFMzeTlyWnlSTUUxc0tjaHlrY0wyTHJlTzNLCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3hXV2ROYVAyWWhqVkJxMkpobkcKRTBKWmhEWTV4S2ZWN2I5aS9PQTBERWhobWxxbWR2dE9UUUpQc3QxZEF6Z0czZC9sUjQrYWRUQm5CSzNnK01mNApYczkvQU5vamtjREpreVRsQkRKTUdxNDJ4bk5YUFpvYWlFZ2ttS252SHBpUmNUNE5wSFVWVGoyMGZuVGZFbVRLClpoNEhyWkR5ck9WRitndmxjQU9ZTm8vNG4xa013Z2doMzJ6YTY1RTFxYTgxZVcxYkkrMjVHaDdCdUM3M2srMVcKQ21YcHV1b3oxeUFlQ3IwZFVOV3VvMjdBSVRSTHEvTVRwdU9DOVRjYW1Ec09GbmI0VHFwMGRqOXluQmVneUsrVQovdUpraDkzaXJDakVHVWlxZFZ4bUZKeDgycllhYzkyR3Y2alhVeHMzRm5HTmZyMHg4ZjUwNmhFcDVPZWFLYkF0CnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcTdGVUNVblREeUNBaXdwSmo3Ny8KTjAxZHdmL29nRWpmaWNnZEY3UjRVUG1TU3VnS3llbmNaMVFKUWM3RG1aVTFnYjFYQ0htUDl0RG9hSEowTG5KWgp2M1Y4eGFYYlBOVFRJZGdiRCtrVURvQkJXMThkMDNmSjhnQmF3NHZSY3phYVlsMmRINzRWdU9xVUhqc0I3TmJjCkJPUGd6Q2lzUXlGRllaQWkzaTZaeC9laU9DTmM1bWY0d25vb0ZUcHczQmcydzVuMWJPTmJqNVVQTUpCZGVBS2sKc0lqcjBGV2ZUK1BjemFSQmVXcGdOMUhYeVc1aGNnNWVJayt2dHJZWnp1ZmNyMEQ1dkd2eU53WmJvemhVRXVvVQpCajJ2SWxsUXpMblRlR0dMc1I4UDZQSUxIUktxT2p6MXBxbm50WmFwSDcyU3NWK0VoZkhvcW1GUk5zVGJWQW83CitRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBK0MyMjd0Smd2bTNIOWZjazlhdnMKcGVRRDdoVytidjdZVEsrWVVHaVkyQndoSmRDTnh3cXlHNW5nM0VtQmJyc29BZGk0TDRhSVRabElkZkRFdEF6Lwp5Mm5qeHN1cXNXejJRZDdYS3NjdTY0WkltUzV6cEQybUpOekFrYVY0ZlFpa2lnenN3UU84aXc1TENON1dwV0NyCm04SGNITE5lZW9ZdXZDWldoRWo3WXN0dzEveTNwYTJWYjBOQnRYcU9nbXFuZExKdXdlakloRWZ5eDdkUU9ocHkKaFhpdWJTTjBBSXpTWGJSTW1WbWoxTkswUUNnQys1NXlJK1VEWTl5eVhHaXhzc0VxR0JTemJzWTVpenFmaVBhVwpCRUlsN0t5L2k1MUw1VnZYNWZSVzhRcWx1UTZaTFROb21TdzJ3c2lHaldkcnpaNy9oNmN0T05abmM2cHp5ellCCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkdmbkxXcERwbjdEakpxajVYZVcKRWxMN0pFdHFOQnpRUVpIRUkzWUdpalVLTHpWdXExS00rWXQzRGhURzNpOVI3akJ1ZFFzZzVxempvQkQ3OTJsQQozNStqL2NmdzBrbTNVVDU2VnltbEdDTXM2YjYrdjg5VWN4Y09vMCtjbnUyQ3JILzVpRnJUamRKQ0V5WlBWOG1qCnpEL2pFdkdlNFZkcjF0ZEdETjZ5Njd3T25IbG1wVDdCR1JsZEwwUHl2STFXYzFiWDhIbzIra3dhR0lHU0ZzaFYKQy91VCtkOVNzVXI2NTFIcDZKd1pKOXAvTGR4Y3E1VkUvaGxnamphMUxJL3NTU3BLL3g3eGxhSWk4OXZ3ejdSUApjNDB4aFlrS1htUWFNa3V2Vk9IQ0pmUWhMRGZoeXYrbDA4TmltR1kyZjBiVjNMMDJqczVWQkRiV2Z0a1hyQlJnCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcVBhMnJBejNQaXZEN3FjbU1PbmIKZVBwbnE0MHdIUytwMWZmU2FrZ08zU3NrZmxubGZzUEZwcmYxVVczUFNTZXpKUC8wanpJei9PTUNLc2drYVJyZAorb2VTRnB1RGxxb0tPdkVmazJpK3E0ZHlJei8zTnpId0pTTjNpTUJuMWN2YUgwOTZ6UWlNWUZhak95WVlpdzFRCmlCc2ptcU4wZEFzeDlWMEtQbkhXcTBCd1U4ZjFlNTBzWTFZVWM2ZDhSOXBCYWZYWHpMR1ptcXpsc1lhYnp6VlEKYU5pTXdSN1lPMHZUbzVtMDh2SWx2MDhqNDVORXltVzBIbjFjbWpvL2VpSVltOGVzQkJjczhNT0dOSGViZnhTNQpCeDM4Qksxbkx1S1lGMnpuTzIvK3pzQmgzb0gwRUx3eUxTS28rdXdZVDRXaDQ1bW9XaFFkbG42S1UrbkI3WEFBCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckJhLy9LUmsyalZiMUxBZjNickkKUnNFY3kweEYzZkppSFlsVThvVUM2cGRMQkl2K1pBdk9PejBRK0JNeDBWRGhwT0FqY00zbVBDNVg1MjB1WFV4VgpmRFRVUW1nbVQvWXl4ZXh2QjEvMzVycTNPQzNOdXh4WUgxSEFiQUZPUzZvakxaeWw1Vmw3bW5BTWRRdjFxQWhPCmxQLzJMUkliZFpqVitYZWxCOUJ4bGRCazB2SzcycDNYTE03emNrYTdRUnFseHpxcnozTXE1ajNZdDVFMGIyWjIKbEV2a2RSV2tFc1pVV29CY0QrOUpXbW4wbXVOQWFLeXdYdHZmM3lua0JDM1FPYkV5Zi90dkp0dkJKbjVobmEzWQpvd2VIZHllTWxjWE83UkxPMFJBcE5PaTQ1eHVGM29ycmkwOTlucnEzNmZhaU1zQTVQZFdEUEpMeTc4aTlBYjV0Cmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUZQOHFqSnQxZ2JlaVpjL09zaVAKSzIxREllY05xTko1SXgzVWJEVml0S05hdlVrR2JoMHBNUFRodzAraTNDaGRvVWdOOVcyNmlNZnh4VWtBcXdueQo2S0U3TkN4VEJnMUJyeWVMTUdPUkRCQWp6ajRlT1MwcHlGdFdWMStKU2tWRzlGWFBDRnB1aWUrNG5RWU41T04xCmo2UEhhbnZDYmp2UVlJOG1TU3djeTdKcE5aRWRMendJY2h1cW50dWg1dHZTWWViRys2OVdmUWZqRC9EalEyTmIKMnMxTmNGSXpxbEx4WDlxVC9kKzNveWtMK2hBVmMzZjcyS2h3dEt6NlEzOTRCODVuZTNOZmlkVWNBQkh2ZUMzWQp1bnhER2RWTzB1eWZ0cFFSKytINS9sdThWUEZ4aFdJZ3A0NkQxTlhvcTBmUldlNDFJeFVFTXIvZURTeUNWU0VYCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk8waklxZTlWZ1p6eUlNZjNIK3YKSWJzcytGbTNNaU1RN29kQTdrVTBqeU5xUDIxTVMyUDdscHBqY21ZL2VrNWlSMlhCYnNCRlJxQ2dkb1B0bjJVLwpGeHdpSktGaHkvQ2I4ZE95QWNZZlBuUmhGTHNLMGNoVE5JS2lFZW5kazRwSFc2K1JEdUJBQVlKTnRJbmYwOVhwCmpJOUVCS2dualNJbmFqRnRCS0M0Z1hJQUkyUWpNdUV1OVN4R1R5bkpjVzdueWd4VWxQNzVaZzByd3BuNFppMDAKUStmb3c2VG14ZGRlVDhvdVRRdnlhaHFHTnBnYnc3UDZGbUI0VUE3MGJ1bTNvUFZ6WEhTYmx0Uzc3bnNuT3NrVwpuTGlNQ21wNWp0RHlTTFoyL3hmUk1LSjcwM3FucVJXVFAzNEZsb0JvclljaDMwN3RHOG9wZkdzL2JJUDhMNGJ5CnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdEtoVktYQ0VoOFpVNnVMYStHTWYKSUlyM29VMUI5eHI3eVROTktBbktnalkxeEVBcDBFQlljRUkveDBoR2RaUkY3MTVqUDdDUEIzcHZvc0ZDRUlEYwpReHJJV2liRU12czFPVS9CYVFnT3FLNTRuMTc2RGZGTE83MUphc05NY09OdXBvSzRuaWR4bk50UHpBNzlBb0xxCjFZRHdXZTZrazRRWG1jRE8xM3VWcjRURzVsUm81a1pIU3NKVWw2OGs0VXZMRDlmendHZHFiWHNDR1NIZnIvcXIKSGxOTnlKR2xqalAyN0VlZXhqc2luRlNTdjJ1VzdUdHlORUc0L2Q2SE5HNWl1N1ZieDhZc1gwWkgwSmUrSmt2WgpvM1hWQkQ2YzVKM0puUDhvekN2T2VkdVhxMHpNZGRxWVV6NmI5QkhaMGtuVk5QR0F5cjR5Q0xzZldRZm1WeFc5CklRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2Z1SlpMdHp6ZlpyWTV6dHhndnQKdVJkMHBHUEErK3BQSW82MnRCcE1MU0RXMFRxM0o3cVFJWS9CMWMrc3E1YVlrVzA1SmtmWGJLQW5nZlRXTjFEMQp3REl6bFpDN3VKTmNuWTBQV1g4Vk5oUzJPZUV1cTl5eDZmQ3RlcmFMcHdBS0tQaGFDajBqMXBCbmM1ZnVJRjRaCkVoL3NkaXh3RnlDT1YxTThzeTZndGFsbEM2VyszSnk0L1FwSFpBQndkWjBZeHpMRzhySm9LMGU1ZmZtamdhbGcKL1k5TUEvZ1I1WElHZldreFFmTmxvaFNHK1FPK21kN3JmQzViMDFTQ05ZYjJRTHMrK3QvWGNMcytVZDNGSHFncQpZTGRwVlNmZVZkdzlPV2dxWkdWQkFNNlB2djBFK2JZbVVyeVNpdmZPTWlPZXNIWHpiVm5uYzdEbWpEZHZaczFGCkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejluaENtSHBhZ2Y5bXZNWkhqWEIKN2ZiM3lxbnFqWFJyZHdoV0E4RjRTVUhlbkI1cGI2Z3BvbTNPNjV4eDM1RUdFVWM1TW1DZXdHcjVrblRMUzBQaQp3eWMwZVlqc1JuU3lWdW5LQXEyY0ltREZoa0NHMGpRSG91LzNqSmp3RURkcnUrdCtnSnp5MHJ0VVZ4NEMwVzNnClFieFZMaWdBaVZRUTJxVXQyOStHUXIyM0lHa0xHQVZOeWl4ZHBtRFh2aVVudWlEdTJORHM2bG9RR1pXMVlGbVoKbGJqdEJicnRsZ0F5cHQ4NUs2VmhtenpjaGdscENpRTFlYXdxY1A2OEVZaDFZQzgxRkRlams5ZHJmNXFqc24zbwpEOE9EK3haY244eGdOaGJOOWJQUjNFUXBkZVR1WDYvTjhHV3IzcEQ3S1FvbGluUTdDVjlhZjc1QUtBMHFRV2lqCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0xHa3g2eExOemtveTJReHl2blkKVkxaODRPRS9IdWdjVjZpbnRVWW1NaXg3bVhFSDBEQzBUY1VHZVZGSEJ4bDRhaU9nbGJTdzRDSE9JeG1OZE1LRwpoSE1RaTNkZk03cDcwY2tuU216QzZXSmV2THVCWkJ3RFdqbnVrZEJlZVRKSllaaStza1hXbXF0dnA3Zzdrcm1XCkN3UVgzRi9EY2NVSnFsQUZIZFM4U2NtSW9lQ2pxNE05TFNaa3dqeDhoT1NUK1orNzRPVVhKd1lWMlMvK294djAKL2RsWEw0ZjdaL1VNeDlWRHpld28wZk9SSHcvODk4RzUyOXY0QW1qWnVpeUN3aHBnREF2N1hLc0VnUm43dzV1SgpxYWZxamZHMTBpTWlpT0kxRDBGbTdmOHhaaDFJd1I3MGRad0IrRXJQWG9WSHVQS0t3TGFJSzZLb2JHTDM5ck15ClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDExZ0JFdmlYdUhNdGlDa0xDY0MKK3YrdDg2N2JraTRYQloyQURyUzVvRHpoK3AzZzFhd0VQVmxkSlBxL2prRTFtRXdsNWdEWmVMUG1kWVIyYXlvWgpvNmhVcHVDOTJLS2pWL0IycHdUU1BDb1Q3Njg4RTNEY1NUclVhWUpQWW5DM2hLWmEvK1RwQVRaTmg4NUZGa1JFCnkwMWQrU01jaHAwZGZkN2JQNHNDdy8xZVJmNDQ3UW9DQUZCTVhTSGpVMnp4WXBvSHUyMFpHaHhXZVYyTHVaakQKWVlRUWtUdXV2Q0ZwTFljSE94MXpnOUxxUkEwTGZKVUhpQ0NQRGRNcytMeENjZU9Ga0tuemoxVlpmWmU5MEx4VgoxemRXaitoM29kRG5DV1hUV2dGeThLRTZDdWJuWmM0MjNSWTIrT3o3VjhoR2cydDdWdzlwTHdzQkdXNGk0ZEhuClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWwxbzhlRk56WU42U0dOMnFHZDAKdEwxL2tMU0RXc1U0NDJKeVFGc0RWOThlSkVidVkvaUJxWVhEZjZ2aE53OURTQ3JLUmk5cXNoV0VONnlQUDdMagpaS01tWU5CTHdqYUJGeFlmbC9LUFR3L0J5U1Z4Mzkwemxxb0hpSFFuQ1NtT20wL3Q5aHFJLzEvekFsSTAvdHorClE0K0pLSDJRcytyNjYwbGpYZFl1TGdkbTA0MC80VnlmeU12bWdQM2JZY2k3LzQ2ZFgwZXBWdDVqWU9xbXJZRVcKZ3RlZm5pUFJwa1MyWkFrM0p3T28zY093SnFmMFlaYWFVMzNwS2llazJyaXlEVWJGb0NqakJKMDRwMXlsNWpNaQpXOW1XVC84ekh1cCtoYjYyOVMvTWVwMk5EQ3NIUThaYzh3NGZ2YXFWMmtOdG1sK0ZHdDdteXd0UXRpWnNUKzZoCjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDZiSVlaSERKYVBaajJ0SjJKQjQKNk9lWHh6ZWFPQitETEpUQVBBa2JURzBEdmFadVZHcFVISUxoYTJ6TU8ybHhTM005dXd4L2RMbE93WDZ0R1VmRApjSys1Y1NINUtoS3d1ZFJZNk56QXpwV0QvSWFHU0I0Wi9TQzZUMmlzU1d1RW5GekRxNTJxNmdKbVN0R2tMcWMxClM2ZWJnemlGMEpUdEJqNGNwN3labGQ0M3phN3pGMHNGN3NyWkdaeTFkQlV6aEpyUGxmeSswZ3BsR3M2R0ZYNUkKNExjRjdvQlVDSVA4NFhYbVRzK0RxTm43MGdrOEtGcUIyMkdmZnlPN0c0NnpRSUo0aVhBd0U2c2dxcjFFeDU1UwphemlleFMvdW9mTnUzTlRwZHZWZnVzQUI2WlVEaCt2eC9jeUFCdVBVYXI0dWsrZmw1MFJpTHoraytScjUzNDE3CmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbkFXMjl0ZjdzSnJmM2kyKytTQjEKdnVkcVdSTWVQYXpXbWg2ckN1VlZOUFYrdWM4YWRmL2V2RkM0VHprSmpJNXFNbmlneEE2cll1VlJsenBPenhrOApCSFB6YklwbVJ4cld5VGZZMnlaUjZkU3RIRC9vN0dFVlJyU1Z6eXlvL0l6VmhYaS9HbUxzQ0FQNXNHQ0VGR0pXClFvR2lIZ0hIQ0oxNlVGa2EwdjhmYnEvZ1VZeTh5TmlVWWJWa0pKb3QzUlBtMEEyVXVUOVpsWXVxNW4ycWZQUUIKMEZkaE9DemcvdWNvb3ArRlpoWjZoQlM4Y1JHM1JkcU40aU1zZHJLY0kwWjM3R3p5RjRLRGZZUTY3VFlZcUplOQpGbmhpamRlUU5pQkRsbXNqQlVqTDlCTVdDTHVSblJ3RXVqN1ZVQ0Z3dVZwTXloanIyQmxHQWJGdFB3TTdDemNkCnZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE14RjFiK1ZpWE1VZkdmbm1FN0oKakJuakkvNWZVYzVjZzA5bEprUVZicnM3VTVYaVc0ZHNoUVZpUkt5dlBpdWMydmNwYjc0T0RvU2drS3lBay9oSgpsYUNHVHJ1M3JVKzJTYmk0cklzdmxLV1JaNlhqd3duQnJOb2h1L1VxdFhoQUdxNlpzWWNPTExuejJCdHd0WVJiClJNQnV2YW5zejQzaTVhNVV6ZGpFWGJwdHM4MWJ5SDRMaGRWMUZvd3VNOUpTNGlGUWk4bmdJMFdxaEVDcU0rSTQKZmg5UldGVnk2VjlodkpwWXhDSGlLWVlhemZTY3VhOW9vb3E0NHA0c0ErcjNUMHFtcU1Vb3FacFVLc1VJQnBZNApwUFNXcnViSUE4dkszdkI1b015SURnRDFIK3Nub1pqd3pXeHZIREoxWGpiQXAzNDI2QUxkT0g5Z0drNCtlTUp6CmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd21jT3NuNU1iQ0tMeTBMRXdLQTgKRDlKRHRXMVpIL0lwTjFNMHJubGRBdHFCRFpzMWZtQ0ZZRzUzdWVyajh4M21UWjhQZ1ZqQisxWDRQWWFTVlByeAp2dEs1UXBlbmhvSFBHaDdaby9HaWIyZUVaeC9BT3FtbjRCa0hmTWRKc0F2MnJlaXdabFNndDNlcnc2aUtDWktoCk9IY2krRW96MDdEeDYwODNWN0VZUVlDNUt5ZXNHczFWdWFXdThWeGpYblpoSnl5bUsyNEdKdlpiZzUybExtWGkKTUJzUFFGUmxrSVFCKytPam9aV0JtTjdxMFVzVlBZNFcrMk5VQ2VCN0tHMjBndVFWMHBkK3c2RGozREhzNWRPdQpQeGlpT1RDUVc3ZElSdVh1dE1jTDhvY0JSMkhqdTMzeGtDdlJKb2FmS3U4dzlKWWVYOG1ZZHpTbzJIMndKdmFLCnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0ZFMGhKdXhYcHoycTZSVWZJSGsKUDBqUXIvNWZoSjh2cS83czZlaWRSejZFdWlaMU8ycWdtT0hpdlZUNGxjbHJzU09adVJ4dkJKcXN2TnBuSlFjcQpXb0RtdllqYVZaQmlyNXliUlhobmFFR3ZVSkRlL2xQOU0zQlhUOWVheDN6SHVxYWpNNGsxdG5YMkJ5eHYwT1dHCkw4VXY3QXZiN1lPODEwVTUzN3A2NnlqMmhRZElxbW4yME9DdjJ0dk5WMEM5allGU0F3U1dxUy9HSEZEYmJ1V1MKbGNnT0VyVXcwYmFvK0hHdVJCek9BSHNyQ052dXR0SEpvc3RnVHhYbVBYNzYyRE9taXBuVzJ1MlRNVEZTbHVhVwpFMktCNVd6dUJ1MEZMRWwramRTMTBvV3VQN1lJbzJDaDBYRHNvOG1RT3dIV0kzVm9Od1JOYzVIVUEyNFFXU2RMCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeG0zVVgvMjFwOUVERXViRnV4ZW0KaW9sMVQxeHV2UU5ZVWtGQm1mZXVMdldBM0FMbE9QaFIxd2NqWUpOZ2tjR01jbE5EYm13UEpMVnBDSzNnNW1RQQpTYW1KU2RSYXcvR0lsQ0dlOElCSUx2WlA1b0hDVUVUSHlGcVpjTUNkNVVQQzBTWDhJSEZRTEpXbTF4aWVLSXJLCkpCaHluMGtEMlIweTFaSkJOUHpadWdCcnEyZDZTM3pzUGwwcGhKYXFpcERKTjJ4ZXM2OGdNRWdab01jVEJ2YjYKc1VoZThUSjA0dzBvT2pMcmJiOGt6OWJUeEhBMW9aTnVzVnRUUXlmdWp0M0FTV2RQSEhySEF0SHpwZFFna1hnUwo4NERCQktwMkx1c0R5aFJFamNLUVY1WXAvZTN5a2cyVGM1ay9ndTRCS2VuNWlDd3FJRWpXc2tmQXZvdVhSa2lJCkRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenkwWHl3RWJ3Q0UxdWdqTWk2bmYKU0p4VWtmT1RKM1I4andNQlZPZEJEbDBrcjQzOEhaMnkrQWU2OUptNi9ITEFCZlBLUmdaMi9BeHZFSURORjhjeAoyWkU2TWRzbTU0MnVtOTM2dDRQL1RvYm1OYVdrMWcxMU5KQ1UrRHhKL0lhbml5ditkbG9jUG0xb215M2pzL241CnhpT1ZrWDNrSkpxZnlmUVdsUDM3c2gzS3hGaENlMmdTTUVTanlieEI1MGN4MXlnNHYxajFPZEtzbkVhV0FtOHoKQVZGU01XaGdKMDF6VTFEckd2NFlwVnBtVGRhTTNvV2JhWlBKRVJmN3d4RW9sdXQ0L3M4RW5YMlBOMFhPZnNoVQpOU2NPQXptbTJQM2RROE9lb3gwNjJhbE9pSThrbXAyMjl6TVhEelc1L1hMQS84eFhMaWwyOGNjOXEzcWozc2FJCm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdW9BYVpHTUNXTjB1YTJYd0w3ZjIKamdtMW5xMzYyRlBmaTQ5QkNscks4aVRyTm1oaHhRL3dCZHZldU9WcjQxS3ZxWWFuRnNYVjhXNUNqdnByUXp0cApSOWRXV3VId1pJREprS25maldEQjFvQkdWalJhdUR6eUtBTHR0bXFDSkxNVmgvQ0w1VjVML0xWd1cyWkhVbW9YCjY5aFg3NzdlKy9zOE05Tjh6eFBGd2Z1YUQwSnprVkVRdmZTYnFmQWl6Vm9yNzk3U1p6c0l6SlNOZ29WSzk5TW0KekJ0bXd3MER1TjhJKzBGd3M1OUE0WTh3U3V5alorRnpWdXVYVEJyMTRUcXdXSjczUGZ0TmpwcFVaQTlGN1B5LwpESUhLc1FHbStNSUVLOFBkaTk1MzhlL1FmMFhmQXMzNG5PeVZ4Nmc4Z1BCZ1pTL1l3RERIQTUrMW04OGdqMHlPCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnZ5VXUxN0xkWEFpRDFFdm0ra0wKL1lLZmNrVmJCNm5ScDZ2NzhLNWo1Qi93aEE2SmRpYnMrQU9tb2ZERlByTGZ2NXNTdWtVRENYV3paS2RsNkh2dwpGUTc5d21KNmx6anZ2RjQ1NzhCSVZzb3RuZkUxaCtqV3JTRDdMTUhTYjREWWVPZ0l0Z2VROEgvSmxpZkQ2NUhXCkp1VjR4TUJ3UkNna2FkcVhqVGtUeDloS3p4ZjNEMXVObFQrNFpwMzFBZ3Q3c0l5NWxJQ3pZT3pBd24xVytrYUYKV2MxcWFJdDBTSldXdkRHUjhNUEt3SGJJR2Q5NlJjTWxmTk5lTkpxK1VUc3FaWnNqc09adEdnZE1GZ3JuUnV2RApwN05GZThrbnIweUFYbXhkVjNGNVpUWTZTR2pEWml6b2xoM0hJVm1tRUVCbE5iQ3psZ2dBbWNBbnN6SlBRRnQ2CjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbFNkV0kzemxwS2Z3SHBpdjJFYnUKVjNHNy9zVmNxemdLMVV2SWU0TTB6Ty9zQmZuaTNoanFSSStYYncvcHRCUWpqVGNXQ2NCNTAxWlFqbmxDd0dqUQpNa25yZGFnbHZOQlkzZGtSNFA3d3hDN1lTVXFMYSsvY0tYNFVRQmNreUZMQ1FiME1xOFNETWpBQlpHUUk4VHd4CjZRVk4xNDB0TlpoVGJUTllETThwVTM2ODFTNUdUWGoxNk9FSHpNajMwQnNrUVlGeDdlNm5DZjVnRGZLVDR0YmgKSjdLTDVwRnlpb2VNYlhIRXlKL3Z5VmF3S29FQThIOWtBeVYwQ3VkMUpJdkVJUnMwdnQvVUZrRUI5TjE5MjM3ZgpodkJRY0gwcHhNclJUQlVSM3haNUVEOUhvWG9TRllJaWN3VTZqVUhIU3RVQ3hscUNhWTdjWFZNRlMwdWNkYkpmCmFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnNDdm8wZzd2SHVpK0dSWjZhNHIKRzhKR3R2TzRRSkRKYk1YTGl4RUtybXd0K3pJYm1FV3FkeFZuRUZlNWVsQitNN2dqVWNNWDhZdnhpNmZHVmZKMQp0c1MxTVBVSU5BN09EL04weWhCOVFBRVBINXJodWpGdERzb0xYSTJDRDg3RHFDalRVYmFBWVVOZlZ4UVdpbUJGCk9OQkFuR0V3YTlHWC92UFZUTlBPRlZtYkhUR1lDMWJ3RCtWQUNrdjI4Z1dXMjZZZ1I1NlFWZm0wSWg0VjZqeloKK2pGSmtZcGJ2UnBid0cvU0hEd1lPa0JOUlYzVDFCWTRXYUE3TkFTZnJ0QWpYbzMvSk85bktvNXVBV3BWYlRTZwpxaEd5Y0JHNWZPUmFYaXp4d05xKzg3aEVpai9vbVZYVVVlV0VHbXhCaGJSQ0tZSEpIa1dxOE0yZVg0d0JMRlZ2CnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBek5uWmxJa0FHTDJWbGl5aUxJMnAKdW5iUmhmYmVtcWJqVlF0TzExblR3RzhhejJabTRHZUtMSXlFWHdiS3BScm5uQXkwVGVqRGlYQkdOOGZ2VE5GYgpremNXZFBKUnRFWFp1ODBpQWVFS3FMOWFTMERIWUNPME1hc3lvV0lkV09DMk0wdVVhZ01aRVFoVmpIR21xMTVjCmZoS3VHZ2krUUhTRURoVE5VT2RweEdmbWVPbVptMWExbzRVbC9ERWdvU0RrZmlJUHlvbVNLcmI0azllWUZJUlkKaWdXRTFGOEpOYjNYTUpPd2VrbzhmdGs1ektSZUxCVmdMTGJvM2NBOFk3cUNLSTdnU0JYS0tsR2NscEwwOVF0Qwowb3ZGRTNlWHRDL3psRXZrdWc1b1NhcVFzVExzOUFyQk96cHQwTmdzT1J1cDRoVFRxK2c5QmIzZmRHNUlRbkpxCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0RhNnZ1bGZzUEdxMHhqY0cxRkoKNEhYQzVtakw3RHV6YlBhMTdYM1AyMDZtd1ZOWnc1RzNtUEcvOGFDMXh3Tmd0SUxFbEc3N1B0c3h6ZTVzbW04YwoyZnprUmU0Wm5HQjR2OUlGb3hyWEpsWmUwY1hBUmlJcnFwMHlWbVlSWDlBRUFubWNlT1NkeW0yQkI5cDZUNHQ3ClltVlJnRFBnQ3kyWVpJZnNuZVhsZUI3alJmSkxCZ0N4eFI5UXFWZFMxRXNDZ3p6NUpzYk56bVhybnJ3Rk5KVWgKbHNzTkY5blVZU0VlSmRCUVNwZitPdjNZNjlENTNLaEQ5QWI0V0ZHNEFJUmpFVSt1QmlCUVJkZ0N2cGRrZklwWApCQzlHVUw2czg2Vitwak01RDlYeHFkUGYzYkJTcXd3ODdseG9uSnd1VHFCdTh2MTF3dmlxTlpmZW5rVzhPcE1BCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelFkekoyVTRTMW1wOXIydWJHRUwKd3B5MVRGQVpjbjg3Z0xvZUdCYUdYMEVYWndOcmhQVkdGL1NvVFpjU0NxaW9BbG1NQ0dvcHVhaGFXQmswLytmZQpmTGQ3RkpWeTNwbWtBL2xja1YrT0Z1bDJjMFMzci9OQU9zdWRFWTlaL0w3NzVxLzU4R1Nocyt1WG5UeFlNNkdNClhFaGRYVVEwRTdLdkpycVZ5N3YvTGg3MHpuWHdSSVNYU2RCZ1JWRm9JQ0l5enBKSjBReGhGNHBLUldIZVN4Z3gKYy9qd1VvY1pscEI3YWgxZnJ0MmU1VkxZK2hybC95aE9Wbk5zU2ExRWxSOEp6MzhrYlZwWlZDdWsyQ1kwRXdYSwpSU1dNOGY1WE56MFAxOEZham1KVHhIcmFHN3pBZEcwbXBjZmZPUWpBVkVqN0p3K3RGOUw1ejVUY21MQTA4eFljCnJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUgycU9uTHozY2hTNUxjU1ZTZTgKYXZrTXVqb29TSm91Zk5BYXc3ZWZFWXovL0dhQ2VTcnYzYkoxNXV0ekJ4MUo5aDdKSlhuYU05b1RwQndmRTlhLwoxSlU2OCtxaXhoTUxWU1FMZlRVOE5sQUcwWjlPWDJnaG8rQUZNMGNRVUE4RVVLZ3NoRGtzTUFkVG1VZFZxWVRJCjNEZEpRSzZzWE1XczBrcjI4RXNTbjBSdmpsTk9IRzF2aG1hMVBYdEFqckxjd3pZSkZ3U2NwekJxNzJkQzhJMmQKWDN3QVpoLzZlRU5pOEY0akdsNytVdmhFUXhhUFRBYzdhYnVZQlYwbDk4RmR1VlFubW81NzFjUmJPdmkyRFVvVQprV0R1YkZEYnVhcXlIbWVpVzB3bWhRR1lkQWFsNW1uQjE3UkdmbTNmWUFzcXZ1cXNTRFAwTE8vKzh2N1ZMcjhxCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0FFa0swb01qY3BWZHBNeGR0bEwKTk5wOXFSYmdJOTEvZjVCWEV0QkxqcUIvVlNhOG9PZE9rMVVDbUZyK204bm80YVliTTRNTnBDK3c3U0Rra2kzbAowVWhhWTlwbHAwMXgrS0lLYmpwN1owRnNxS3JhRW8yMlF3R3daVWZSQkNRMDZsMjk2T3lNTHVML1F1NHRLenc1CityQmpDOE1HRUtwMU9DNy9TNG5udXFoblUzNk1NTUxZU2dvU3VNMng2ZHRHb205Y0ZCQlhia0NMWnp2NFg5bk8KMENIbkdwQi9sMnRLN0x1N2VNT2ovRXluQWJSY3RVWm9DN3VyVXVzSVlRUWh5UDgwbzBRMktiaWU2WFFpa3lXRwo5OFJrb3lpa2hNVXptS21uRml1WVlkcWJVMTZlNWMxdWxuYUpMUUFGTmMyUDVkVG1laHVTZFUySE1DdVh0Ujh1CnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFVNV3NlRkpIaEtobE9PckNjaDEKb3Q3UjgyT2FWVy8xMlRtWXkxQzJzVlp0UUJZYXc5KytjNGZUWStVYXhqb2h5Z3pLVlhnK3JSVlNITmlCcDRhNgpaVHhueGZPdUorY0V5dnhtcHNGQi9FWGx1aEJ5ek9uQkcxQWhaVEdjbStuOEM5K3d4T0hQTGdIVWNBYXI1aHhSCnpvOXRiamdtN2FMQlFSa0pqSklUeFFuSTZ1Z3FPU29RZWNjY0cwelVpaDBVZ3JtMUZWWVVGb2xLbDQ2RXNIaDYKNTRSSUJnYWpRTUY2V1BKNEZ4d0F2a0ZKc2V1OUI0eDdHKzRyODF1OTVseWxkdHNwT1FsSHNSdHVTQTlnRzhXMAo5bU9JQXJBSm1xdi80UHlKM2tRQUN6OExWVDVPYW92c0kzQnB5OFphNmNINVJqQ3J0cTZUNklqYlR3Q05IYko0Cm5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeit3UTRjbXdLQy9pclV3K2dvVkUKKy9RWlFQMkdhTm9MTGdnYzQ1RjNHU3hodHUzelI4Sld4cU5KWWpsVklDckw5bGJXNnZBQXEzRGpwakNNSGF6WgpYRFZRR3VoQmlHcHVPd1RncDJiVEtCcTVKWEZHd0JXSU4wT0xVblplTWZJaXE2WlhYVDU0U1UxMXJMSGtBb2RoClI5eENUV1NtZHVLUE5Uc3A1TXZWRVFnWE1zY2pGYlJtNVMvaXJYTE5oZnhRMkVTWEtGL3dmQmJhNUJDU3lRejkKU25lNjV2N0RVTjVJZVptTW1HNFBsbEpuR2I5bG43TGF5RzlkNDRrN2cwa2pTSnVWTWdwTXhXTC92bmRIaWxEcAphdEF1UzN6dUdHcWFweTVUam5ldloyeXc3S0hGckJGVnlwV2J1WTV3M2hNbCtFQy9wYXh1K2tISzIxUGdaRnk4CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbnRkVjR2Y2llYVM0QkFMdFp2NDYKbVZpVWpkR1AvRjg0bUlhTE1uWXIzb05ONU5oU1RPb2dGMUhiNU9HTXdkZlRwRXhMZ0ZEQXdtOTRPWW9Lc0ZiSwpReHBVWG9OWkl6cWRlVXZSMkdqNWJFVmRSSmt4T2M1WGNVcHg2SDNEcjdNODhXNHdLS0U3Rkh5czRJS2s4NituCkxyaG5HN3dsVzA4WGgwZnNmWEFhZzhhZkt6eCt0R0hLODkrcTg5WDQ1SlpEOWpFb2xtKzlMa3YvRm9aaFhWZXMKUWZzcUdUSkFUaWZSNXhMbjRGMXJUZitTb0ZzU0ltU0lPNm9HSUpwSlNMUHowS1pzZllZZFhoZzEyVUhPZEdMYwpnbzRLN0hIODQ3WnlSb3JXdDdJNlBpQy9sR0oxM2tZRGZQeEdaYU1rT0ppbEEwd3gxMWt6T2hxSGJQVkZ6d2dlClRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGNCUUFsRVhrSlIyVzlMQ2JYM1QKbXp0WUU2M2ltem84OWQ1eU53QjdoYWo3REVoczUzNlJaSzdac3Z1VWNtUW9YdFJ1UHpuWkRKQXFCTkljYkFIbgplL0tPbytNSmFxQzRibTFhL3g2SC9OUUdLOEFSemdXWTUxNnFSWmhwNXh5ZzNqUDhDaHVXWlpPTEx6OE1BcXBnCkl0SW5LN1VldVBwbHlESlhOU2ZuTHYyY2NKYzE1U0plS1Z1Zng0SjVDdU82bkZreHJDeG0yYnF6UVZOd2VkTFoKUDY0K1BMUVE0UG5JbkNGKytaUnBDR3FoZElHRlA1UEY4aStoaXN3VzNETWNoQXR3UGtyR0lEcWFTa1Rlcm5nTAp4alVib2xyb0dtZjB0bnJVbjhiajVnSmRlNTdmOEZzR29RblNLS0ZNNGszUHRoRWJhcTE0NXF5ZWU2VmVxYnVzCkV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWxCYlpPcngvaWlWZXlabFAzY1oKazlIVCs4U25FRFUvT0Z1Z2FOS29BRmovVmJEdG54U3RyMFNRdDBDbEdKb0JKV0VxTS9FVjR3anNQUGFYUzRETQoveEYvRDUweGNkUVNyQzFTKzgyTVc1TWJTNGhsVllPU3h3ZWl2VGZEek5XV2NQQlBQNkJkUTJqaGJzUElRK011CkF4NXlXWTc5R3ZXNmNmOFR4MUZvSXRKNjZmbksrOUZzMnFUaC9qV3JkekpYS3pJTlVQcXVxZXdXeVdzOE80VzcKY2J3N0JXbE9zNzZvdGF4eGVJSkF4Q3JWVUlMdzR5dnA4TTROS3FlcTh3MENaT2pwemJWb0JSc1IwckZGaFQxYwpCUGFqTzZXSjdtbmh4WjV1YWRlRFNpeVhHeVdQZFBZNGtOR1dodVNoT0dTQ2d0WTV3Nk9aNjNNd2dQVmE5Y09GCkd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdStXRXJmRWlVdUl2TjFiOHR0RkMKZnduUnlJMnppUVRTZFpjd2hlbGpwZXd6amFJSWpzMEZLOTJXLzd2WFBqS2hWZC8xc3d6NTVaWWNnV05wWkdGUApaK3RDMVNJMVErYkhDdGNodWpHaDV1ZUVjZ3ZpdHlybGFZY2MvVTZEekVSdkltVnFpeUl4ckJCOGlsVlVENE5WCkZXT3BiN0FSUEZUZGtkVGl1YS9rVmR6TzZhV0JsQkpqTXMvRDg2d0k4bGdsaTBNd0s2dTJFV09Hd0RvWm5uMEwKWlE1a2lNVjRuNC91ZkdTa1JlRkxtei9nUGZsbm1UVERCZFdsZTk1bGxHUmo1Zk9yN29EcnQvck1xdHdUQlQ5UApFbDJXQnhMTUZtL1ZlU3BMc0orRFppL1VHQkU3dFd4L1htK1lobzY0YVZPblA2RHBjK0k2UnN0aEZDaUZHc0J0CmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXVwTGVFQUxFWGlaRnQvVG1ra1kKOEg4Qy9lSmE5YUYvakpPS2FIWlorUHlQZXhFcFVqY1dzYTBnV3RJb0EwMUd2dzIwTkVXMmJvay9sekpZNkEzVgppUG9sd3g5ZFo2Q20wcnFGK1NiVzY4NElMSXovRUpJM3hBeGplbTlPTnQzaHRqUGs4cnpxaVVYOTc5NXVqMHRICjcwUE5RbVpYRUZyazFzdXg0Rm5iYkQrMnhFY25aZ0NMR0ovcHVVTEp5UWhlOUxleG4rR1JZR05FVzM1Zk05c3QKT2dOek5OYmFaODBaaytQRFVTWC9wTXJhOExFbjlxSXU5Rk5TbzNiZ1k5NTN0WGJCcFdXUmpoK2ZRYng0Y2dMdApHcU1BZjdpSzVsYm4yZGU2d2lQeGgzM01ZT3d6NS8vaFZ1QVBXMlhRcEYrYWthSUFxY0dORG8zTWJvQXFCQnM4Cjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkNKMHp1bXF6S1RCT2UzTXhWSzYKRWJIQmVwdlhnOTBmTGtSRkNBemV6Vk9YRDE0RVpsUWZxSTNGTGczNXg4TW1uZ0lEM2ZSNGxHbmJnS2xuNkE2Vgo5QjMxUTVPb2pHczhKbGxyUVNwellWRnJ0aVJrdS9hNExRb2pnSHQvdE14NDM0VzZ4ajdOWFpUVkU3L2IxVW40Cm1ib01yN01zODhvalFmd3JHSHdSR1U5bEkyZndDb3A2Nngvb3pzTjR4OCt3MUtJaE03TS85V1hmK3lBRWh0OUMKVjVRRmtJRmdoa2Ewdm9LRHQyNDJyQ3luZExUQjVISXJ3UDQ4cUx5MTBDRHowdUprTlVNN3BWUDFMSHNvR2Z1MApWRFFHSWtNMTFsYThSZ0NKNFdhc2dsbUR0WThsUmI2ajRoQ3lIYVNDSWxKajBhSDZKRi9iNUsweGxQYktBQTdCCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUlidm1iMkdXWUN2WkRaTFBFNW0KdzNsSysya0pyZTltaHVDcDFrWXhOT2YybXhGTnZiY3VUZ3YzcnJrMGNPU0pPMHR4SHRtQlpXUHVzalA3QitqMgpwb09CZEZ0cElURXNhV05POC95WXNQS0RWaUUwcW1nNmUzTFpESERyYnovTEZNTzFRNDZkMXhrTThvVElwb2N5CmhxODR0K25Vemdrc1V4anFOeTd3c2ZMSGJmRm1DcWp1ZlZob3prV245Q2hpZVcxYjlwNVlEcFJ3cmxudVRYZEEKUmIwM3owK2NmaGVNdi94dGJMcmZmVVJCd0VmY3kwVHluY2lRYzVmYU1MQWxUa2EzenBWZkVwcDkzTjVmVU5vQQp5NmxtWnk5bUdyQ2xWODhzSmFXV0REN0Q3UWYxTVRCYXp4QzdranNNbmxyR3ZhYVRkdDJjQURWeENpblFrZjFPCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2RBTnI2YTRsSVRWc2g4bi9NZisKYUxBY0lHanFBUHd3MUE3a09rU29Gc1JHSkpZS1RRZXlsNmMrY3lRa045M3NRbWdYTVBWMGh4bytIOG1HOVIreApES0JEQ0M4WnQ3UlNXakRINmxJbktZL0NUbnQ0S0FlSXp6RG91RW9BVFBSODJ6RGNZczBqMXFkNGcvSDlybFIvCmNNRHUrL1VGbkRyTzBJYjhRek9JZldZS3ExV1NORGVSL0U3U09WdkNRcjJ6dVdXbmlMUGZNanh4RXJqQ2Znb0oKMDJzK3hVUjNtNlpUSUs0QzJMYVVDR09JVkZkTTB3dUloN3RDTlhhcS95R2h5SmVpUmlQWUIzSEM0K1c4QzdMWgowS2lSRFN6OHU1dXQ2RE5yR1pGM0hyTngrejRiQzQyTEM0a2svSi9SUzFaQWJMWGJId0x4dldnZWxzSDN3T1dxClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBczZzNUF1OFR6MERYcG13ejJabEEKWmc3T0lOdmFReWxnZjdGOE9wMkRIUDd2NjNwK2s2VnBpNnNKck1Od2lCUWhYSkJHODZqOEh2MGw2RkQzKzExYQpPWHgycGo2RVBQMmI1WmM4RDhOL25Ud28wRzZJcTlaVGE0ME1SeGVEN3h0dU5oRW9KV09FZjdXaXlaQWx3ZEMyCnNIblljakRDNFdjMlI5cEk0NDNTZWxwMExBUFd1UzRxQlJXaDlMaGU2V3NYMy9XNlFlZS9qMEh1cW9sTnpjVTMKNnhuVFpnN2hRMncyUjZUNjlISEdEbVI1YzdadnpyWXg3N0NtKzMxRlU2R3E1TjVtT2FOM1JrRWFkYk5MUVFsZwpBeUtVZXora1JvWk5MdFhJS3FpRlQrd2pyM1NhNFR0aFJXVTYvbUphQTRzU0piTkZYVEg0SjZXWjNJOHpaUGFrCmV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFhUVGttdDNMQkhyck9CR0pvMDMKUDFkbGxYNWtTNWJLVGRQTjFIK0k0T1QrRjhDSHBwMHdUcW5LYUJTU1VWc1F1M2duSWdPNmVDZEhMTDJXb1ZmZQoyWWVQVXJJZEFFUExYME1SRVFQU25oZ3I1TXZ4KytPaWtUY0prVktIRThTOWhqblhvVk1CbmdEeWJYMGdnUVlmCnVYZEcxL3piL0V6enY2MGNha1RPTVZ3bEx2NDd1ei9LbVBGTkJBdExEWmJoSEV6TG9tWmZ2cGpIRWRMN0FHMGoKZTZtQWxncTdNUll5d3FIZHptUVZ2aDhXdk44Y1NlNHdFdzZVMGxvVWhQRU1qSTFrWjN5Mzh2ZkdDWlBCR2NkZgpMVVBKTFA3bS8vdGh2RWlxdVhVdURSZ3JTRHlWYUZjM0RhbTRrc1c3REcvZSszdklXQ1VaNTNaZDZOT2Z4R3krCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXdFV2VOMndLNWcvclB6Y3o2cFkKVmxTSERwbGt5ZjVrbFdubG5rNS9mLzBCSmlZSTgrN2gzTHFQUGUyS1FxTjBkTm5LLzQxb1diZ2xzQVhsUlhmZQpuSFJDNXdhR3FQc2NRdlUyb0R2V094S0k0M1pVWTI0VFU5cmFxamhmV1VrSGsyUnczU3N5K1UzNDgvak1BdzVhCmJrZjRPdVhlaTRUeVFNci9Lb2RYbXRVR1pzYUVadjVLNS8zY0pDck1pTkJFZ1Z3a3BHSmI2RVVQUEZpdmhGcjUKSXAzQUdvUW8yOHNncG5ibWlYTU9GanNnUVhobzAxYkUwMnNMaDB2anNHOTV0L2c1R1UzNE85Rzd5aktvNG5uVgpYeWszRkM0Rkl3UlA0NGgzREdiRDhDeC8yNDlHc1luVjkzQkZvYnM2Q25yQ0Q4T1p4TTZwVjAySU1qWjB0bTh0Cmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0JvUEI4Rm5MdVhORTBpTFJMZlMKMnlFSFpsVXJndlVtMFhXY0ZlR3VCR3BrUVN6b05zdUhlWFpWZ0s0dk52Y2oxa2xVYi9EUitjcDNQNTd2RVFjdgpQRTNKaGgwRlZ6Mnc4bUpVY2hzVkpYb2JGdERNdCt6andFVlJmdlBEMjBZaDR6RTd6S21aN003YlMreDZvMGRxCldIbkhTU2RYcFBnejBUeUQ0K210emZBV2hlZ0JLS3JESTd6d1drUE9XOW5ocVkvYUNrNm1zS1dndUpGOTgxR3UKK2hlbUp2MUZORUxiVDB6WmJ6ZGJIa3dWdVpNbHhMVFVKMFJONG8xYUJHQVU2YUdyZTZtaHRYTmEwTGN1VTFJaApPOHczazVJWUpaOXpPMlJlOTVDRUtnZ3pTTUtLS2ZQUDhxWW9LMEZnRElLQXhMUFNEYVVtZjl6ZUtGZ2gwS1B2Ck93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdGtsYlplTTZNempScWxWMlgyVDYKaHJlQnRiZWVXN3A2bDA1YUN4c3dnamE2UEkwM0s4UC9pNzNVRUNwc0U0UFdnbEFSdlljNTVPY24wc0tMYng4cgp6Skd2aHhNZHQ3UDlUUFJadHRsdDBCMmt4T1Z4R0Z6eXNBVk44a1BVaFdCNzczQkppLzNZWGtGTGdXQXRlRkYyCkhxRFVFb3FVYkJETmR1azBuRmx6NmNZRjhXOWJiWWJBeUNETlVsZjlwYURzb2VoWHoyUmNPS1pJcis0QmNXbTgKZ3lxdEJ0cGxwUmhsdkJ4SE1jeVp3dkNzZVJvWXhLT0FoemJML3pkakNSMmJ6YnJITW5tdkJ3ZkJmMUduODBBNApMZi9neWFPVUl3dy9xL2RpdE1zNWphUXU0cmtIQlVzdDBnZ2J3TldtbXcrMDhWdWRiU20wQkRTY1o5UU4za21UCjJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelFycFZSY1JjMCt2S2JwcGdKZGsKYmE5R2dFMURsb1RZTmRHN1JEWmJjM0pHSFRyQWpnNnQzQWlBR0RpZ1dXT1JxdXl0UHd6UytEQXEwTThYWHUrNQpHZTNoOVRGWi81N01zUGo5VVF3d0dqdS9MZUVXd2tZOS9Pd1QxVXRzVVN5MjRnMFVWSW5zSHdGM2lFb0hqampiCmNFSnFBSDAvZ0prWkJZaFhaQ1pPZ0xYRkhlbGZTbFJTUEpwTHpWMEVqb1hUeXUycExja2FpMVdSU2I1c0YrWDEKY2VQQjdXU3NmVzFGRGZEQ3phamVIcTZMQ0ZIZ0xHSUNybG5taElLZWVYaHhHS3hkOUYyWE9EdjMwNVEzeEVzUQpnTlJ2d1UyZ0xoNk9lbE1LeVhnMFRVVFdYUFRtWmtzQ1FTRU9oMEVGVnRZbURneE5lMUJldmZhK2V6UURJWGJoCnB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcU95UzVFSjRHMXBYbklJb01hZm8KL2tTaFQ4N1ZTT2pnT1ZzazlRcGZkUysxaVNCbVRXSVpsQklhd1RFWUVXY3dlMVBIclRCTWdkNEFycEViOFhLWQp6K0pRNE1uZUpGZGxoMUhEajZ3QjVQejlFOE1tWm9jWVdSYlpkRWxzM3Q4VmZsYWUrYkVhUGVQN09UL21XbnEzCnU2VG5zbG9XK2F2MzdkZFlLV3Q4SVJEdG5ySWl4YkdteGRsZU9sZ0FvQi9GMkNPVTNoY0YwWkdJZGFtYloxdnkKbzZTemg2UVljT0tVM09Lekl1azd3TldkMktNWmhKTnRmUzk3anFHaWs4TWFubVUvc1dxc3AxSWpDOVQ1Z3dzUwptQS92T0VSODRmSGFFbENQVzE1VHZTRklpc08rcTRGbWNEVmNCL3FnZ09BNFNQTzBuRHU5aSt2NThtbFQyYVd2CnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeW1lOXYzbWdXSlpXdWIzYWRuV2oKcXZnZy9FV1FpeDRkRysyUVprb1RJcXV3SWFWcGNvQ1BRbXI2VndDT09nc201OHZjS3J1anNSWHp3OGdqa25SNgpNVWl6UldlU0pvbDlkbEFjdndCWm9JOGJKRU5GaXZhajFkd1YwU0JWTDJIR3ZERDd4MHIxTklybkNOSzRUWDRRCm9QZ2VQV3dVWjJSUTU0M3o4KzJWZ2gyN1FZUW5PRXFZWUtqSU1Eb1J6YWszbWdBN1lnYnptZStISkFySDF0ZmIKVmp4eHVETjVCdW5hR2lEa1JzWDVtMkJYU1JEREdFV0FVT2VMQzJPSGlhaVM1YnVSQmVDVkRGMTdTVVdPcmdGQwpiRXJKNVp6WHJyRnpua0RObjJucFpkYlZyZTBkaVlzRXhrbDFEZXMwdTRyamM1WG4vS08wZlc5U0FIRitNVGtJCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBellFTEpOeEpmdkhiblpmbVpMOW4KSHpkeEZEc3pPWDA4K2FwQlRzRXQrVVZqWWMwd21rbkJjZ04yNmk0NWYyS0QvalB6Y0VhQ2hBTDBqR2VOcTZxSQpwZnhLbThRUlg1Qmk1cVo1b2ZvT0NXRmhsUVBYa1ErZVlxUVM2VnA0WFc5blFXZWUzMWVqTHBwaUhYQk5OenNpClhmYmFTZ1BmTUowUjJacUFRMFVsdnhvc0NzVEF0QjN6VG5JS1d4MGdRNDRvZ0dBS0dTUkJRZlZvR2krSUwzNFQKb2d1OHNPNnRuOHp4VmFhOU1sbmpDSHIxcG12ZVNMajZsZHY5enI0Z1NzT1VtaFBLMlN5TnB1dkdESCtGVCt6SwpIa2hsb2g1Mlp4TU52Ry9hQ2k3OWdBZE9Ub2FCWlM5WVdhTVhXUTZNRG9rR1NaSFA2VFFQZG1MdUxWMUl2b3VjCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBejlvK3J6KzhDS0NBQ3pBem9IKzcKMmV6dlFXQ1ZEanJtanNMaGp3eFdHT0JxSGZzOWtLcTZuV3k3d0V3WGRzc1ArSktWaXhuYzlyTEM2ajNoNjduTwowY0x2bFJLdVRPUjhZQTdKLzJzNWlCV0ZuRE9XUE5nKyt6ditUUkVYcUlUYms0WUVnMDgvM0NLTWpaVCtMSVFuCmZ2VUZrbDh6S2NzVW10WVJFY21vak9LZFBSelZzcEhtTVdGejBOTk5rUDJXSzBFajNucU84Wm1ueHROSDlDR0MKSUVTaEpwajF2UCthYkU4anYvWUZ2RzlkeFFKU2N4V3JXUWNFRnZkZXpjL1piYmpTYTZEazVOSDVUd0pEeDQvdAowVzFIOWZHcWRkQjVOZWduOFFyTUw4TnJyeDJuRGZwMXplY0pheS9LTWZ4ZXZRNytRdjMzSG4xM2FLS0M1c01FCmxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNFY0eTZ3c2pUNnN3TUx1T3lFUWoKQzNqa3lRaXIzdWlGczhhb3Y5WUtjV2VkdFh1ZCtBQkhxUmtZRk9ackd0alByN3FUQTNJYUg5ZFlYZmVac0Y4egpONnk4NkxmYXVrRzRwdHpBYzArYTlUNmtIdUcvVWdMMW5ETUQ5N2VBc3dLUEJyNFcybmsxMXpJZ21XNDQ1QStVCkRodjlKOHU5V1E0VjA5WUZLTk9wQlczQUV2bkRZU050RENvR1ltU2MvU3lub2ZvK3JNNzJZSWNzQ1lwTVJLVnYKaDd0M3JwRGdlcHEzZ1pVM0JvcFMrUUtqVmVPdk5oMFNDelBPR0FFZldzMWxPckRpcVZJZG5CVytxckVrN1I2awpuVFhpNEdzR0EzWlBlV2RMMkNYYm1reFpyTlJGZmswZ1NyeHM2L3MzQW1XcnFNVWhUa2tQdWVidm1QWWhkTFRWCkhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTFJUC9oY2dERytnWUtYZ0pjeU4KLzZySDZ3R2JEVVRVSktqVDhDRnRDb1dYTnR6ZnhYRHRtY3ZtcTh5NzJXc3ArOHNhZ2tCL3hPSUQ0TWpPbFZFWQpzVFQ2QnVzZXZoSmRhRUd2d2hBMkdoN0UydWF5Wm1vaEVBb0x4Z2hoOGtJWWlmMGE3TGt4Q3ZZZVhqRVdKcFUvCnFTWmFiYjRPMUs4QThmVEJEVEEwbVRLbUNzZ3RPbytpYXVVb1dBMVJGMHJseVNQY3JDRTdRVTJRMzJkeDNUNFcKR0pQNm96KzBtT0FocFQ1MHBFeWo5eXFlWG5CYWhXVUd0SFNDRks3a1pDVEtwWW5CUExqaW5DYmdNK2JtWmoyaQptejFYUDNJY3dMVnprZnphUXFXc3YxVk5PREQxRDJXWk42Z0xYcUV0SWhEUnNNMGEwMVdnV2l6Z2tCMThSQ2pVCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeW9DKy83TTU2UjlBY04yZ3VpMVUKR2EvSnRybkt4RHJEQzN6bm5sd0ptSnF1NHhHRlFUYnpPV3RFbU13dVJoQzJxVXQ4V1FSOFRpWVBCTXlrMHMyeAo5Sk1zaVRwY1gwNTU4ODNRQjZlaldWdlIvQVk3bUp6RlpOQ1VqbjRRcHJMUThyZGVEdXRwT1pDbEFSbTBZenNuClpRZmkrL3lzdTRVM1Z2amFZZTd1Ryt0c1lrdzExWi9TcWNrWEp0S0xtanNieGh2enR6M01PNlowdEZHVVloVXIKZmlkL1J1VUNsYVlibmlVRlpHM1RMdUplYlFXdkhpb29ZcTV4SS92cXdWKzZjYm5sRzc0alNpSWlUbmFFZ2ZwcAphdlBFa3VOR280SmpCSzdNd3pKazVuZGRjZDl2aXRFWUJCTE1vTkplQjNlem9TSW5LSUg1QVMvM2RCa25CK2pKCnF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHZBZFE2TjlCUVZId2gwSjR3S2cKQlluSUYxT3RCS2hTeHRxTG1ud3dWZjBaV2Jic2Q3UE1pZjJZbnVZVmp6UDZabVMyZmZCUUc3R3N2a1VOcXJxMQpBT0xKVFB5MzhQUzRuS2ZuWFdWby9wRU15eFN0NUl4V3NpSlNFV2Z3TUV4MzZ2emN1cG1icmk4TXFLMG5MKzE5CmVCRHA5WXFMZXlIZzQ1dGJvajMrcko3WDV4WC9NdVZRMUlldXJ1eEZRTXcrVWZkd3NIRnJDR0J3YmZLd3BJaUQKNWlyUmNiQ0tqUkFHb1dmUWkvVmRpUUY2Vm51ZzQrRzVtOTY2QThaRzB5QTJxZ1p2WmZiUDZJRmk0S3hKc3dsZQovYTRMZUZKNnhML1ljRVhVc2oya2M1R0RNYmhYVGhrdlZQZDZGOXFLbXhYa1NrVE5ESlcrdlhoMVpZd2p5c2FxCkh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUxiVmMzR1orOFA5NUpuNS94Nm0KUngrNERqRG5iQU1SZDVwVkdKZUs5a1FNekxLR2Z6WGZHMWt2UDJCNnNmcEZqZERXYXUrbisrRWlwUHh5VmI2NgpoOHd2VVl5M1NnSm5yaHZGSlpKNnBka3BKQjVFS2FRR0xBT0F3OXYzYmhLU3pHLzRRNkxYV2V4MEtpdjZsY291CjNGU0NZVHNCVVBzQWtUR1hUcEdvZi9YcHJrWmgxU2FnbE5VcC9yYVZUOWxVREtCMW5ndlphNERWMzBCODB2RmoKbG50NVdyeG0rU25iRG9GU2ZaZWdSeHZVV2hHVm5Nc3RQWnZFQitiSWJNdDNFQ0RqRDZKcG9BNWxmbXVsUnZQUQpKVjkwVEo4alZsRUNkcjNYS3NEVlFtRVlJVlgweTJRS2tqamhZRnUveVY2WGRXU2lsdzZyRVBLN3RjVlNVeUVUCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVIySFFYN2FyQU54KzJXTVlJU3YKdVljL1ZkT0s2enJyczJUVW1ybGlucFR5Mzh1a0x3VlpzMjVsZDE2MFFvajg1c0pXUDFkRUJGY2xXdzRmQVdHawpMY1g2K2k2VzBONC9rQkxlNjhobDVoRGZmbkJwb1Y2ZitJVmRneTdrYW5Ba2VBV21BRTBPQ05ZVU0zbDNsZ0pSCjBYeXJkTDJKUWtsUFhrR2RDWXFUR3B1NnBVUjdFM2NOQnd5K1d0YTRQTmJFR2YyWldUZjlqbzMrWFdOSVZGeFIKMDZPNlBwTnJ6UGoyL2x5Q3NrQXpMdWo5V2RwUk1DS0tENWhJcGdGREh5dkV4WUk0aWhrMzZMcHJJRGRiZUJvWApTdFc2MWU2NVFyTHdLWkRkRkdYbWFZVlpMRzJkcERkY2gzendoaHYvSk9oUjhWQU0wbU9DV1QwZUNidGhVZDU1CnRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMy9kYkRqbXkzM3crRkFXek9vZnkKZnBpNnN2TWlrMlRJdDdiUDM4RVdvY0RmcmJpOXRPME95bnpsK08vamNBT3FNNnRqakJscEE3cytwQ09QYUZnNwpSNWRuUE1vQXZpd0xKNkV1ci81bzFtcWRnRkdVMksrUzVFRE5zRGNJckpOMGZJOHFHT3NWU0NhRXc1bUtERnYyCnZsREJiZ1RQQ3VNVGtCK2hGSDBUeXlhMnBOS3NBZ1gwTktSUnVPaEtEMGNlcUpURnhjZHlWVDhubmkwUFhwY3IKelRoSk9MOC9DMkJmVUFSRDg3NThsNktXSUhvVm1Fbk15dFhYZVdlb3Z1emFKTVc2QzgyelFHMC9pclhKMC9GQgpBbG9zNVNmQkVTSSszMHhoUm1kTU82a0gwL1JpWkRkTGJ0bzR4MkhUZHVMQ0xWTFllRzY4Ym8yNXRNSVI2SHFyCmF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeUxuTlUrL1RBVnJhR2Y3RXNKYWkKbE9TMVNzbFkyQVAyS2szWnVSVXE1dklLZ0NLeE9XbjU2OXhlWXJvd3VhQ0NpS1VTTzgvcWRDZ0Q1Q21aY0NqWApudmRoVTgwVTJKdVJZUnozdUd1WnR6QWYxRS9lYXJsWFFiSmk1REFZV1lCb2N4aFZINEp3a2hGTjBldjlHSDhICktyY0tQNWQ5all4M3JERnpRVVc3VFdqc0MwWTg4VVVIeGwyajNIQU1lMWxMUjRvOWYrM25EVzFraEJLR0hJWEQKUnRMdkFiVi9ab1cyWDFVWmFEZmFFM1YyQXk2cEFXaFIwNG1KRGdSYTZQcytyYjFOTm9BeUdsOG1FQlhiRmZacwoxbXUxVlJEVnRPUUFjUVpqQzkweXhsVlh2OFlFRDA3aVVhN0ZLdHRlUWEwUWo5SFEycDNSeWxLVWJST2ZQeG1mCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFZzUnlzWnpZYS9pd1hiQTJDdDUKck5jc0NpMWpCYUdLZ251N1drM0FDYmc3U0xBWmpMRW1hTDZmTVFUa1lEa0ViN0dEM2RpL3FmMUdkZUwvMTZEVApkMVczYTN2dWpWUnJEMEpta2poc1c2QkIzYXQxQjlZNjNZOFJxb2tBamRxOTFROFNxRXNhSkNNRnRwdkJOQUdwClE0c2M5UERYOW5COGNFMzM0M3JzQ2xBRkNqZVF1OFFiYjFmOGtQQjJvMjUxSkxyeG1zS3VXZzFnMWRXUjIrZ1QKVXkrU1hnelpIcWFMZmxMYkc0QXl1REZ1bmVBVmxRY3laMG1VY25oeTFacE9pOTQrTDNFRzdjZ0l3MmNERkI1awpWSHozNlJFbDZ1K0gyaFVLOWNQenJpS2lyTXcxeFRodUx5eDNzbTBXc3pWOFI5bEJxUWFmM2d5ZkJJdm1YYlpEClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNzNTb3ZFdndyU1ZPK1h4ZUpvYjMKd2xQZ3Btak9KeGZ5QkxpZjZnbUVUU0RQdm4vRExmZVEvRE11cStNSW1ydzNBdG5JZ1NBT2VnOWdSa0laMmdXZwoxNWZmR0lHb3VXUDZvbGtERnR1S1I0MDYzV0NPZmtZTWtNL2wvVlF4Nnkwcm9oMEd1dXhMd09aWWZnNnpOSktuClFvckh4bWcwUGpveEhoVEVmVzRUMkJqaWlKenhick5hRnQ2Y25CYTgvQmF5OGFrQWtiVUpSL0p5MUhHcFJ6VDkKZzhpZXoyclZCaXBid3hFVHE1TEd3MjVaL3dGV1llTldsMVBQUGR2MG1qbzV4R3Z1ZFBqZ0xPUStwdm9NcWJzNgpacEdVQjU0Sy9VMHhQY25UY3BMUXVxYjI2bmtDUzl4L3B3OVIwV2pRck4xZnVOSXZlNWo2SGRFSjdtU2dHVFZrCmx3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBemxpcExGdC9pZi9NRlpjQWtVSmIKK05hU1pTeDhxQzR4Q0VvQXI0YVJ4Uk9KWmdxZll2UTFmMm10OFluR2FxdG4yOVE0QUxoUlptS2NHSjYxZ1k3UgpPMjIvMjhRWGQrQTRiRVFOSW9SZTN3ekVVdkRHMXo5aHJiSHYrZS9xcTY2cCtRZWRvYUlyN0Yyb05lN0Q1ZTBkClJpSVB5d250TVFjekluQU1pOFJNUGJWLzIvY0dYa2lKVDdsT3d5ajBuQ2N6MGlHM0ZZdGI4OVV4aUFhSXFZb00KeENneTVZWEw1b3dPeGlva2psbVJkRzdLREZCcHhTZDN3aldqaVQ5RjNUcUEzQjlPU1c5SDFhME41TmQ3ay9OWQpleStzNWlzT2FIR0FmWXlqeWo2OFExVXhVMkVaV2tNN2thQ1o5QStuWVF0MzhwL3RGbVY0ZnQ2TzdQbGgyNVhQCi9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1pBMHFUTFhUUVFoMSs1cjlKbWgKQ2puQ2tvT2VpQmxmMENabWFQU2k1MnhBVG1ldjZBaCtJNWpUdGhKNitxQXZEajR5ZTUxR1BPcE9QdXVHL29BZQpWdis0UDMvZ3ZOM09aZ21OR01rQmw1Qk8wWnU0YWFreHVFVlBHb255YjB4VEtKOTdQOWdzZnBGRGtXY21WbW1kCmw2UGN4VE55TnhlWHl0UWRZVFpRY3BDb2tLMDJJTDY0RlBFWFlrbnlqdDdQak9Nd1lGWk1VcWJJc2ptUllQUjYKRFh4WHI2R1NodEVUdkpOTXlCaWhoK25sTTRDQ29OS01pMHM3YmpLMldiUytlNElFbkFmc3lLTUNOMzFaZHdPQQpLN1Fza2tPLytVZGlpNDZvU0VuQUV5eTR5cWRwdVJnam9pOTJySDc4WktnWjRVR0xnN0NpRVY1aXJ5clcyQjlICnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVFoYkxjaGYyblpodml1dHVGTlUKWVIyTTcvMFJNOWNWZlBadTA0VTVScjBsc1hsbFpUeFhqUFIrWDYzcHNNNi91bm0yd2c3SFEwYkNkQVRQcEV3UgpHWElVTCt0YU1mazZiakJ2QTRkZzNQbjBkOFRTQ0xrMkNKRE04SkJxRm1kSVQ2Rzh5czExMGJPVXhjTzZrY2FQCmN4dG1xSWd0YkhzSEppZUVWa0g4b1RWZHc0MElZd0phdWxpZks2NDUvWGwwaW1ieTg0bE84ekZ0MklmY0dDTXgKZVFRbU5ET3BNTll4T2tFblcrTk5zWGxncTgvTzh5SUsyOFFaalVHbWVmRDV0NXExbEZvNktBM1JqNUJSNUNjSQp2NkRmTU9IMkdnSGlQR3lXNGxvUVVySUJMZjB1OHIzeXdMay9WZFY2N3ZXS3NUOXlwV21kanV1SDJzUHFtOWNJCjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekpscWRTREM5TXlWLzNtN1kvQTQKVU8rTklZSE5sTkd6NGJNRXUrOXgyWnA3T0N5cGYvWGN5U21VQXlhUzRZc1hvOXRqRUNVeXZpbjFsRUR1aVl4TgpCS0l2cFRtNUNScUVHRlZocGhNdWtmL1E3WWkwSVp3V3lYVmZuZU5BeFB4ZWhUaG5XNHM2UHdKdkNNekxrWVhXCjhUd0l1M3lNSjZFL0ZhT3RTRnp5K3hsdWdJSThrSGN6aC85VnQ1eVRVUnhHblZvQ2VCT1ZlY3RkbnZlRktxbWgKZUZFZEQ0N0E0NEI1TG14Y2tJdU9HOFc3enV5ajVEd1BmMWl3VkpnbnVOMDEvN2ZZdEFBQitmVkZ5a243UXg1SgpBbC9XUGtLTzA1SjdmQ1hNcnVhMmtFRDdVdmJuOEVxb2VRY0lhd3R5ZE5Jd0p6NVFmazVTMDErMjlVVElTREpoCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzF0ZURyMzkrWlR0T2Vzc2ZKYVIKbE5UQ0ljYldPalBTRnRLakxRMHMxaGV3d1NtemMrL3pZK2lsSnZ3UlAyeUY5V0RpY0JwV3lscFZKNEZtUFI2eAp5SklZQzRPMitZazlJalhIZFNmUzJUTWcvZHd1dEpIR2ovZm11TWhxTWZzNzJLSXdDSVU3MFhCSzk3VlFSQjRvCkk3K1ZUT2dWQnJHRXhNSG0yUVcvNkFVbE5wSkViYUViTVIzNCtGbDR0T2Y0S3ZjcytsUkRtV1JvYjhFbUhPRi8KbWo2c1pyVkFzYlJ6UkNvNXRtaU9pMExYR3Y3WW1HOXF3VXJiUVVna3c2dVBuTlN6cEV3VWdmNVN0clRmWGFsOQpYMVRDbWhMLzdubHZobjNBVU5LdWN0dklrQmI3eFg5K3ZUTWFkY243R3p5c0NaSUtIZHVCbFZhWGlaRHpQVUQrCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1RMWGg2b1AyQ2szNXFmZzQ1RFYKd29kRXYrUlNyalZERGZianorZmhpazA4dTZLb0N5bU5KOFNOcDM4VDRRTGtaSFhNemswMmdZbk10U2FMRjB0WApXTVU5cTNrNWJrRU1ZdkxPcnBuako4d2grTENaR3V1cnVTQVpXMjh1d21sdTNOaWx2YzEzS0tQUE9WWWJ5eW9qCmNvTFQxSGthL0VIakp4V3V1NHByZCtDSEs3L3RiQVFBdnZOMTNEREE0K0tiNE9HSU45RUQ1ZmErT1habDlJd2EKU1czb2ZwZFNjcnNPRUFwNWFDSnJCYzZGVDMreDR6SFMzaVhDQ01KaVc5RzlCWXFvbWM3NkNaQ1djb2twUExTYgpFV3VabjI3WkFxUTRCN3VTdXFzQlRqNTErYmlJdkhiVlBINU9QckJ5MlJ1b1d2Kzd3WkhlYWVXNmJRSzhLamFnCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckFvY2ZLL2pxV3pFSENqM2I1dWsKKzdTd0lZV3F2UWw5ZzBjRno5MnhGUmM1QUdxMGw4Y01lV3dUV0Q5aXB5N1lkOVRsTFBYY0J3L2NlZXhGalVORApmVVMvNlAvUlk1a3ZuQ2NSakR2TG9STEhqeFVxekxFTjFSTWZPODEyQmtOUnp6WlljbExUb0gvUVZXYnlINEpqCkpSVGlGOWRsa2VYdm9zRVdhVFhzQWNZakZlTW9YbUsxMkQvU1BFV0ovdldqMVM0TnlVRFU0Y1c5ZlZiMDcwNjUKZHFnUGRMdUt4TGNhRXNOM1RGZFcxY3oyUTJMVjViWllFM2hiNnhrQzRFdE4yRXgwWTkrNmJIY2JMcXNrYkN3cgpmZlhCbmdMbEJ2UjNtVHVxVFl6WG5XM1FZR1prVVArYkRjZHJnMk5lRjFwR3BFKys3RUdmbWZnSnBFREVjTG5KCjRRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzhNSmZpZXluRW9OT09KblhmTGIKVjFNQVBSQXJwUk9ZTEZ4QTAvOHNEVHR3Y2MzODdXNFA5UWs2YlVxMmM1NTdPRjhZZElPOEdzWTdIMzlpUFBHYwpCUHZRMS9oVmFxYlQ2UWN3RGFTbWt1OFhOdVcrRCs4R3RGUHZaTFpCeVB3UTFKK281RDhaQ2RHWEkveDIzTDdJCmxuSjV1NDBsOU91ZlVESEo0Qm4xMmRzdEhObXZXVGhDanp1MCt2SHpLUjN3bzhidUt4ckMvMkZnTnVmdjVYOTEKT1ROY1dWU05MakcrQ3RFV3VRMnQ2czU3dG13amxvcjZoQksxd3ZZU2h1RlAvWHpKRmxJZ0Z3cVhpa0ppekNJUQpROHFHeFNielowdTR3WE1vYXVxdG9zUnpmUVo0RFZzRUxueEEzV3hqbUhiRzljeGhBUFlhVW9WMUxxNEV5cjdjCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBL1paYlhsM2dBNFpQSlhubnF6TXMKUnZtNXlwYXloY2VqZXVSZDFPWVlHemltVDJxNFFrRGtpT2YvZTd6b21kOFR2d0dwNk05YUttRXRGNUpnU3phZgphcDJqSis3dVEvUUc2ZTNSWVJCN3kraHh3emJhbytkeXRMK083cU5Mc1B0L21kd29veVdrZnN3My9LMkFwQ3RFClV3T3A1bjFxeEthelpDSkFmQUpjR1l6YzZjUGp2cEpHa2hFMkpZMjRCYXZ1NEpEdU5JK3hOTk1Ic085ZmRYTXEKMzFvaTNBVitkRjFrSlpFU2ZOMWk3WGxwNFF6dEJzSGpoNHRRcXZLNWlsenlGV09GM296ZU4zZVJ5SlNzV0pZRQppNGFaUHVPTHliZkYzUHp1RDd1Mk1wSmZsU01obW1HdkEyZ1NuMHZaNHRyRi9FYU9TUCs1V2s4T0hKWUNBNFpMCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBOEFIb3NBelQrRnBlWEExTnNabm4KZHk4cFdDenlCc05CVDFTQzdiOFdpZ0QzYVNaNFNKOXdBa2JkNjVNZXlPTXp5YUczSDZQd05kNk53ZEVuaTJaZApzVzdXTjM3ZW9ENEJnTDVheHVzNG45ZVIyZVluRzlRZmNaTnI5RzNSc0VJbkxXU0I4cUNCcjEzS3ZtczhMR2l0CkkrUFdQZnZwTjZiYTJCWUlkSzZnTVZQTkZrU1FRYnY0UUdCNVpVU0RNZjlzZWhkZ2hSRC9US1dDazk3dGFXUmEKMHpLVjNKYThqN1JSWGErQTRQaFR4OExvUm54VnFPdXJZbXQwa2NOaElST3ZQQ1FpM0JmRFQ2cUt5cmR2RUFlYwpodkdvbVo2aFpHalhHQUMvN1RnclFLNDVwZnkra1NZWWlXRkxncU5vY1UwVFpsejhJZlZaSVRXSHpnMTJjYUg3CktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXJFUFZIVVpqN21PZnQ3dWk2R2gKREg2SUdOSkxaVzJlUndubmh2Vmxtb0hXNVA1QnJBcSsvNXRITW1Vb0pZRHR1eVVQOTlmZW9wZlZnSzZOMzBWcgovOTJra1J2cXhZams1RXZPQUZBVlYwSEd4cjRJQmNQaWJHbXA0WXBnY3FqWHBpcGJUTE4rN0I2QjJLdjZNdGdvCjAvUGM5THBSN0hxbjlvYXdHaTZhajVEbjFTWXREVjlWS0p4VFlNRW1PS2daR2Q1MDJZUERuVlBiOHJBRCtnYlUKZi9nYWkySzQ3QnIxVE5TcjU4S1JmL05pemNJOUZKYVhLQW8yWW9qRnZveGZHcEI0ajlLTXNvUXhKV1BOMzdYRgpjbk9ZMmhWWjlsVXZiR3VpZk5LNlpkbTVqQlMrTm1xQWpWbXBGRFJsZUM5VkZCT1dCOWQ3NFJWcjNLMWROVXh0CjR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjRHU2I2aCtZYktQMWkwL0JtYkcKcHFwTHpuYnd0UXZzb0djL2U3cHNzeDcvV1diL0NsYWFxS1BCOTdtRXNIVjRmRTlKdzRVTU5zejAwZDNJQ2FtcApjRmhoODZ0ZDU5b0dRRG5rNDltUTltNzJ3cHI4YWNYSU5teUZLdDdkMmVjdEd1TysyNkcvUFhsODJoV1psR1dJCnZTV2dqenhidTJQUmJLVXVrNFJ0VXY0OFA3aGlwbittWUt5SVZGZlB4bUIwQWVYYk95TVZOY3pESU9zUW5nR1QKUVN6cFpxYnd6QVdmU3NOUFBCLzhUai9ZV0FYVTRHdjNwcEx4T1dUbTJpcHJtYjM4ZmdQaEdEN1psaVBGL0s3dQpyeS9Rcm1zQytwdUpqV2tpK1ZiWTBZRXZIQUZCSzFFd3pzTHJ2UjlYbE5LNXBEUS8xREd6ZkFwTjU4SDdZa1I4CnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzdMUU5xdG1LMDBWNUcwY0ZsV1kKUzBJVlJkSzd5cmFPcXRsMmF2WTQ1aExzMjFHNyswc2JSOUlIbC9oZjQ0dVZGWlc0Nk4wekI4OVNuNExmS1g2MQo2UEJibS93YisyZnVSNGlreDZpR0l3M3pWN2MxOHdpL0VBN0lZMGI1RzViWVFXMnJhL2hqaWJtWGVKYnFZclBRCjFUM2htOXVhRkJKcUhxNnlXMTloZW1kQzlnaS9VZDJYaVg2K1lReFd3ZC8reStnQzJiUmVSckJFekxyNGNLRjIKdFkwTHFWMEdCWU9DbW9FUmZ2bnlBTVdheUtURUxYQ3BESmFxc2Njb1lNNUF6dStXUk1SZjhMU0I5SE9uT0diUQpjUVZURGtZOWNHK3FqL3JmVG5hZ0RPOHkyeXJTZ1phNDFpMEMwRDZXSWxNWlhoS0xhOTViS3JWdk91WEFLbnJZClR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelBRMXAyODlYRTZoMlRsUm9DeDMKenBqREtBeXRxY3FhdFRuT3BaWWl2dk4yNk5jSlR6Y08zY1JuTWw3bU1zQ3JNZlFBb242b1NhR0FLY1BUaHBuNAo2QlRJMFhDbXF1SWpOMWJ6Zm1lNGJnTGd6TU9QR0FoRjB4a0gyQ0JkQ1R1VGdNNXFGWk14VW1vaDhnTWZzajBGCmJOanRnWWdrUVh1S0RtOVpSajJrUSs2K0pGZisyUjdsTjJqSDJUUkhiV3c5ZW9IeUo0ZHUyQW9JT09lVy9rR3cKdUNGMUlJMlF3NXJTdFJFRXArODFEcWxTb0UyTmdGaGE2VkU4MjBmalF2Mk9EK3ZBM3FhWW01a3VXN3JYbWNEeQpRdTdsYW9zY3Fha3czaks3Mloxd1JpZllldUZGZzIxNUdHK1FJNGpVSmxWbmlUcTZmYjB2UzFDVDBwczVHb3VpClB3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelkyU01sbVlCcm5kWnpnZWlCeEUKeisrKzZxODk4QkRieUNJeTd4OHlGSHJJY2tvZ2JuRjRtamR3T3ZlNE5tUGgzeHlFM3UyaUdlM2pXQW1FWVk3MAo2TG92R05rcVdrZGNvSnZuNXdzZVd4T2ZwVDNzMWlJa0RRRlpMbzFpT21kZFBDSGYyajgyM3JTYjZzUVF3OUcvCnBxOUY2bHJGVUxtWnBRZHdaQ0N3ZVVwK0dVUHN4aThjRnJ3L2lHVE5hazVvU2lCblREcFpMVkt4NDFTT0tmWDcKVmFmYnh4d1E0NUFNY3MrUVIxL2tlaHlJT0t0bnRucHFSV3BOSlFoSzB3SDRmWG5RSmtWUlJJK1BuV0NYNjQrVQpYZ1I4eHhPOFpmbWliTWVNM0RKNGNOb01LWTcrUnZ6OHNZbXZEQTArV1JTVi9SZzR4dHNQQnRIQmRKQ3dlMnlhCmdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGptWjdJVm1qSi9BZEQyZ3A3c3QKK3haWU9zbFAvbGxyMkU4THE5L0hvcG9jTnE0MnNVelpNTlpBMUFhOUJDbWRjTWRiRzI4VXlBTFB5ZU1aNC9WVgpKTlBLUGVvRmxVZWdadndNbjR4Ry9KR3JnSkRtVE4wRHZ4bzBYU0JkMHgrNWdySGxrdnEwQmJUeThVTDd0c3FZCmpUR1RBbVptTjJhWGg1SHA1UFUrdy92dlM1WlBFbzA0REJoUnYrOWJOendhOVhhb1RreEJxbkFhZDREQ3BtLzAKTFZjWGRRVzNtajFHQjdaZFJQWVl6K1drZThuWUV2RzhDdDc0M2FMb2xOeFR0ejdmazlYUGgrVkRnZmpZY3pTLwoyWEJXYmlOREtESlB1MVphSjNpM1M5TENhYy8wYTcrejJOR200b3FEK2RZVGZpbDQ0RFA2MElDWThVRXJ1QnVpCi93SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMVFrZE9hdHQrNFl4dzhHOHluUFEKNnlCUmlmL3R4Ym1DcEIrZ0xMOUhTblJubkZ2Y1dMeitaUlllbUk3eEQ3TW5nL1FXcVhUYmJVOVRqNVhIeVh4bwpaVnFLOEZQbkxIYzFjV3lUb2l2RXg0R0Irem1Jdm9tYzJENHF5NS9pejRZRnFJY0JPY0xyM1BFNk9pNlhCeGExCk9xdlZycjl6a25mRExUeWs1YjV6OHY3Y2xWM3A4Z1NvZW5VRVk5TFQ1UERhM1BpN1RqTm5wdk5YRzFlQlVOR1EKUWtpRlRBcThXbEsyOWNEWE5KWDROaFRyUk1Fc3kzMmtEbDhiWkpGOG96dkxoR0tYUW1nNERBazFML0Nsa1RhSwp2aHJ6ZUdGbEFxOWRHQ2toVXN1cWlMdmVsWjJIWHdpdElqaVJiVExDRERPTG9ESjNoWWVWM0N1UFhtUklYdGcwCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc0NLMW8ycU1aS1FiamhLWjhrQzMKa3lvR0lHWm5OcEI1eFFzNEpJeFF2MnpDVk1uWkVndzVHZVROeVdKMmFHcFM1NnFyQjM0bGxHZWlSSnVJalErVwpSNUJrVXZSWksrMjFxV21LRjU5OVFUSVZxeXQ4dFVncDNJalpFQnhiTnh6aFdqYVVrZkE4L3BkOUx2NUc5c2wzCjVNVmNuUnRURzMwTG5JYlJuc2d4NGdYY09lc2tGS01HUHBOcDdjRG1sRU9TOHVDWlF6TVdpZDBuRkt0Z1R6NlkKQWJrTWFERklrZTlQNEtJYWlpMXU1VHVCby9yOUFLWlVEVHFYVkxXMGtHamZoMExoSUFSWVJ4dnhLSnhaWlhWYQpISFo3N0c4V1l1dHZ5SzRwampFNWlKV0svT25BbGlPeHRRaUY4YUM4c0FhbHJaV3hnZXFGYjZBMjg2TVpQc21BClZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbTNXMUVoMzkwRzk4bXVnWmcvZXEKcUZVbW9aZ0pGOXJGdGhtb0J4Q2EyVTV2MXVsZk1Eb1hhbUt1NzFpWjJRd0I5dkJHWGRIa09WamI5R0ZTYnpLeQpSOTlRYVErMkEzRXFHWUxiRDUzUHlIQWpiZzRaSEg1WWxTUnVESHkxMzZndGNUSFpEM2p1a3J6Snd3NWhKM29zCm01TlRZOUNHc2dNeGRxbElYU3JVSnZyWUc4ZWlxZW03b3Ixa3RIczVRSUg1TXVoQm1vVzhUVmkvSVVLUHN0aXoKczRwU1hJUnYvRlc0SGpWem42SCtCTS9sSmN0bTZzMUtuVDUwWmhCRnE4RzFaNllMM2JkZjdoL2tlbmFGNFdqcgpLcS93L2duQnhjelNVVUxzbzVlYWwyV21WT1J6eVcxTFFSYXdTUnNQMmplQngzc2psc1I0bDVMdzdXeTNyd2xXCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeHRyVGdjNTFoQms0N0pLVDlMN3QKVDFpTFlkQzVhNzlyT0Y4VTA1alJPUmF5RWZlUVFITU5qbkl1WjlHem96aHBYcGJSTEJSczJFR1ExK1hUaUptWQpKQmlZN2VGTlFQaWYzZytxaG1YbGtIbG1DVk40S2FuWjhLcnFGY1o4T1NiN3l1MmlXbGd1bVU5TFFpcUNxYVgwCldXTFc3dUFMdTRyTDExK0hhbXBQM1Rrd2hEVmJTMmtjOFNtVXBwc1YwNGtsUGhsTDFweENKQXBjZUdCRUhKbFQKNEpZUU1kSjArRUFleTl3T1FCdmVlRzdER0FVTnBEMThQN3JWMzB6Q0VISVV0M2h2VDQ5QThiWE9Nd0VwSmF5NgpXZ3VLcy9uT0FwaXRROTdCVnlNUXRsejJJMFQ1cEhKbytmM2g3Nmp4MVd6c0o2bTVKQ01QQjRXeUFST1pMWTZVCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdjB3cnFRdzIyTWdybThrQy9MZS8KT0VIRmRUTmk2aDJtdFJzLzhIS2N1cUFFQ1JnNE1yVHNsSWN3dzB3ZXcwVFBkNGNZWFQySXZoaTUzS0ZONWdlVwpkWEVyM1dOUUdLUkI3ektaMVowaWYvUTJuV2U0R0IySXRZR3BiWjBnbmJrZlIwUnA3QkZhV1Z0Nk9PMk52eVl2CmdaY1VuRURiby9iQTRwZmxXcktyVlBCazVnSVNRcU43bTQ3Q05QdUM1YkpSVms2MHkwM2gxVTAyQTdvU3VUNEoKWk5SN09RY1VmM1Rib3pzUy83V0NqWjB4RHBxQXhkSytib3RaYStadGh2RU9BN0JjNTJqVUlHY0xtc3hUYWFqZAoreEhNaFF5VTF6anBMcUlNNjlvckpOTE83RTQvY0tpT3gwVFhqTFh3TlpOVjVmNkpac29wclFDRUxiUkNBNnZ2CmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN1RnRENVbC84TzZUVVpoTDJTSmQKY0VKSkRYaHFYbmR1M0J5V05aMGp0WUl4WmlqczJ4bitibnVNVFNsb1dnY08yc0RTTUVTNG00ckhOVTlvVWdQagp0aUFqQzgzcVkxcHJSdzJJbzZkRVBla2FGK2FUY0VWZWd2YlRhOU1SdnZxT1Y1eC85VldqcnVsQlRhdWl1djFECjZtV1pXaExGYzEwVUhodnYvcHdFVUJERWtyS1NveEloclQyZXhkT0ZnSlBHNUwrUVBCY1pjSDhQdTJYbUJPQ0EKT0JhNzI0SnZGS1RNeEprcDVzalBBOHJaTGtVdUR0dW9YMzM2ZmhYcFNNMldFS1g4TXZjUStxeVBSdGdQMWgveQpWM1BJRWhVUkd3ZDczQ3RicGlJWlJrMnJsNVpmZklSREdaM201aGdWYUF6Y2xXRW1IVzhndUVxNXBEQi9KbVdGCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBME8zemJBc2c3QUM3SldzOGFZYncKL0I3RHRmbEIreFNHM0Zxd0grK3J3T0I4RTNDVTJuRlFFUDJXZG9jUERIbXJPRE5pNWRMZVRtOE4wVjBNNzNyQQpCMmJhREFNUXhmZ0ozZGhvSnV6NklVNW9NWHFYbVlRWWJ0cVA0VENuU0lpbFpGc2pqam9ScXZQV1d5d28zS0JMCmVoWkc2cm9uNUdGbU44UzJPdVIzd0hHbDFJWDRLWE50eGVyall4dDhiY1hOdmZqTi9ZMVk1bExvWmhMUldpYmwKdjZETFYyQmVtMW5lWVBRTUxhVldReGJmdjcxdG5kb0tDcFRGRHFSUVVjRzdwVEo2YWZabjErT0RRMW1YMjFVSQpkSGgrclVqOWs1Zm5EVnozVzdoeDFCdkEzLzVCWVowdEFQVVJMVi9EY0prRW45UVdEMUNnd29OckhxZDJhVDY4CnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnR0N2ZZcjZ1aGVZREdtY0pDRlkKbjBLRW9XZ3h4T25NYzh0Y0h4b2hpMy81Q0s5YVVycTJvcndScm5tbUtyRURraERJZHZxbngzREs4b0liZG1jRAoxUXJPOGRuYU9YQU1FYzNYVlhCdG5EbWZZWTk4UExVb2ZsWFEyUWFiMUd4YVZVQXFZQk1aZC9nK2lrN1pWWmVkClR0VHFzRHRzbFN2c1Z5ZFlLUEw3UW95dkZpTWtoK0oyejMyTlZ2bW90QjJFVytSRHhlQ1dNVkRLZXJwUGovOTEKZHlaSktvb01ZcnVCK01udTlHb2xqTmlDQkdTd3BHcVk3WTYvZ3ZmL2p3THExRlc5VkorN1czU3EwQ3Y3YWU0Wgo5TjFmV3dKdDkzelkrZHJKVWxSenROb3d6V2xZVlNEN1JsbHIwUHlIRDlPL09nU1hBRFVWUnVPMzBtQmd4SVU5CnVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFFHV1dPdmw3VXcyckYzTy85enkKVkVBamJqcFlVbkxkdkhiTmxIVDd6Sy8zeDZ6N21VSlRnRHZSV1JPakNTeWc5WnhPQmlGQ2M2emplMll4eUR1ZAo3UEVhb0dtOTRKdXV6RDE5SDkrdURkYzZKZmFMSGJQOERRZWdmMStBRjJjcWNnV2o2MEtOTHZQdDJ4K1ZJNkN5ClZ4S1ZPamVUNjYzTk9JczM3Z1JSTVJUK2ZYOVhEZWxlUm5PNC84UGpOL041WGFZZ0ZJaDJkWEpuQk4wa0F4NHoKWGhsclpZdjhBbStGSkpZbE9Ja1pXc01SMmN2YXdmbjZBZDB3elFpa0QwT0ZYbTFGVEZUNitGc1diNko5dTBSQgpXWVp2TjhSdVcwK1pOYy83UTFONTVBRTlZVkRMaVZWdkRPeVAvN05XUis4TVRUZ2JJQzYwQ1Q3ZzlRaFFmTUlHCld3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN3ZVUHoxMU5QNFdDNG1OU3pXaWEKdXZhTWl2d3Y2QlpSalhKZkZXd01EVndudEpmVW1zYm4xUU4zRDlKVE96MEtPRHozc0JNRW94OURRZ2FyMlpiRApqenRTQk5JZ1o4WVpja2tkdnBBUkZha3d6eHBTSXM2cWpYS2Y5YWFDb096QlR1WWE2azhiT0kxYVJ1SWxxNGVyCmJmKzRBMmFWQ2R5WUtEa2JNWHBhOTFXMEFrNFZVV1JjTmFTZzJGeENzQkZKVlc0SG1GZytTKzYzU0lEK0I0dlYKMjZIVjZieHpGTjJIT2lOZGlDRUNqWHhFajR6bWRQMjhsbWlwRXRIallQV1dJZmVwdjIxSUpEcHNhMzhvelNVTQprV1lsZVZUUGNGeEErM0JXOWlvWm51STV1RXUxb1p6R05SZU1KYmdCcVgyaGtBRWZyQlZ2dmRxbzhMQ0NETnVECkF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbng4Zi9ZdmlEUHphYXNRS0llVXEKNjV4eXR6elE1dkczdTIwd3hzWEF6dStSYTV0NnQwbHAvaVV0UlhyVFZsRi9TSnMwb3JLZEM1dzVKekZ3TTdGVgpmcjZiYjRMMFJLd0pOMExiMUdmb1NDSVk3SjZkY0EvT3RINFFTd1FEWWpQbk9xNDlLR29JZ3l1YnFlK1N2czYvCmw5c1NlcEYvQnFsUVZFSi9zM0p3dSs5STdaKytqaFJaNUVwdTFWM29aZGdSZm9LbzRCdzZRUS9IWUtKakFYaVQKOWtWaW1IZW16azRHbVVTZFpKM2ZXNmhzMkR5RnpKNkcvRTRXRFozbE1xZ25ieVBSTW1GeXRZbzhJa280c1lHbgprdmhteGlXVkhSY2lIcUtoQnZMbG5UNlJhODZBWWMzcktTbGE0ZEYrNjRpWlpYZTB6SmlJNDlYOWE3bmxoZWZyCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3c1WE5RYnl5eG9HWFhKOXREeFAKZXRwTnZyRG9IclJIempwMkhXTXRralhiT0FlVTAvanMvSFVyNXlNeHZvYnQ2a0JzU0t4YzFlZytpUGpsZU5ORwp5dE5UV2RyQUYycEVCOS9OMEhVeWwwa1R1SmgxNWNTWEI3a1RXeVRjYWVKNXhlbTBKSTNmMmJQNmo0M0E3YnlaCjhTNm83MzN2VEJzVzVOV0xHMFQzUHpRTDNJbElQa0pPYnN4V3dQU2diKzQ2V1RYTnJYdVdoLzVqNGlLc0VUN1EKdjJhb1phU0FpdFE0YzFWVVZkbEVaaldPRllVMWZwUTdleDZXSGFzM0wxTW1sKzF3UStva2tMTVFqcHFoNWVJMQoxb0RXNlQzTTl4Y2twSWxldER4dnNFTlVwZXYwZTlZL0k5elBOK1p0NUR6YXROWWNqUGdOMTMrSlhBYmJoaUN0ClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcHdKODdHUTh2OEFNZ3dEdFlPenIKMzZjV1BMOW44Z3FUMS9HTjlVVXlxazVrbGZFZll3aVUwVWpTSTU1bFpBSFVtY0oyRWNneVhRTUlzVThMdjMxZgo0bUlsdXdqYTJDQUlZdk5yMHZXMTRtSmw0QnhQNWJjZTBLOVRPQlZIWnd4YzI4QUkyZlphNnFFa1pGVzBpYXpVCk1SNEl5eGdGMlJTY1FrWmhxdWZodkNUVXhXQmxZUk92UFc4bmpMemhvcG5iSGlJU2dtOXpWRUVNTjB4bFpQZFgKRTdzUWVkUkhBcnptcnNxZ1VpbHNMei9PUkZJZHNRdUZuSm9TRTJvNHVza010VGlna1A1NXpVSXpvRlZFN2c4cQpMelNQTzBaZ3BzSVpaR00yYkVyTzVKOXQ5V2dpczJIUk14b0NHdzJZeHRWbnJYTGpPeHFBVzQ4Zkt4SXQ3cURkCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWZCMkdKYnNyb1d2SFR1eGwwK1QKNFhvdkRHcXRJRVliVGN0RTF0Tlo4M25WUjVpYjNRbEl6aTRCMFZjUE5WOHBLcG5XN09vNEI2WDVkNTNXYU1xTwowTlBFNUVCTEVMUlpLenIzNFFaRVltdEhhejQ0OXhIaHMvQXQzZnhFbFo1NEQwbWNqY0l6eEZWbHdGMGgrTmVaCjhsSnBPZEJTTFFEZ2FvdnBWZGc0QVlHY2c4OGFIbEx4QjZUSEcvd3hzMW45aGk5SUg0WUxZUVllUkl4UUNjeU8KbzFqQW9YblRhajBLVG5wUTZmbUVLaE1ScFZrNHUvMk0xaFVQdEV4eDBmczc5WUJGcFZRSitVSys1d1BHaHFTZgpJV083Y1dDSzR0R3Z5STJoWkl5R3dQamVFWGZYZ2k5Q0FJMnl2YzdseXlJbU1mM25XdXFhTlpVQitReDJlbGlmCnNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeXZOMkFPYVRvVDRmTzc1OXd3alcKdDB4dVVMMURJSjVrNzlHZU1ueHdjakJJS1pBbGRpSXpkbFZKOFRoWjdhZlNhNUhRdVpyWHpDNW4rOXZiYStBUQpES3VjZ29xSXAyNERsY0xQZGhtT0o3SWxWVnQ4OEM0M3ZMRFFrVUxWTFZud3NqNENlVklrNFE5MXViRnhPcDl4ClZBOVArOEI0bEFCL29mT3ovUHRIZW0yOFd5WW1Nd3pHZHlKT2w0c3Rta3ZWdlRsTUJFMnI1cXM2RmFjQ1ZtMDEKUVZUMndXamV4OVFZUkhpWUVHVVNETHk0U0YxTXhLb0hKNXdkSkF4SVhrZU1nbVAxV2tTcEJQY3ZaUFVDVXdTTQpDMnBhbnJKZFAvT2xGR1cza243TjRWTWlSZFlYWjA0WWxGT3BHbkl2VGNBSjR0dXRrbXNheTNLaHJRdTE2cUg3ClV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEhEeXZUeldHYldpTHhMOW90dGUKYzhUMzBYYTB5VE8wNlI0ekpWMEhaNTZpWXV1empyMnY1T1l3bzV1VVpEb0xPalJvNmlDbWFZaHdvcUI5ZGJPawpVYVdKN25iZFRWa21hbHk0MGMrdW5HNHJOK0IwR2g5UURZUG1uNzNQd1BKUzdmMmsxQ2lta3ZzOTJFeTh6azdVCnNyTmRHRzExU25UdnFFN0xUVk40bVVBcmdxbUlpQUdiWlA4VVY5U2Ywek90aC8zc2Z1TzBQWmJtTkJyeHNkbmYKRHFaTkNxdmx2U2lTTkNUVXk5UUdVN2VnRmtPZDdZai9GalJhaUE1bEZOTXdUZStMUXRpem41RWNjaXJKTHpvbQpUalFEOW0vZkJkNDlaVVpPZWh0TzhSTGkveW41QitFeit5RktQRnUxeGpvN2JiQ3E1dURPMWJFbFVRWHNtKzRoCkZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmE4Nm1Bc29sdGN2R2dQL2tmaG8KRnppQjMrb0tRUzBBdUVidUlHcno1a1RXS3d5amwrWmRUTW1XWVkwc3dBWmNMMWpZWjBybDZTZjcwUmdFY0lvcApxdjNKeTFkOGJBQjFRMlBUTllmZ0ZMeXJNaWo2MjJhVmdzYWtIMXp1bEFwWThLNjhSRTltMnlzQWJWR2NadVEvCmxISkpaTTNqSUhPME43aFU4SldMc0p1RkU5OTlLa2Z4bWR5aExWckVGNmZzZ3pIenhMVjVzSnRaNi9FQUh0SmkKeEI1a1JsR0xQT1puQTd1UmwzSmVKVHd1ZURLUGpFZjBoUTZlZkpvRng4Z1pSZDFvdzgzL1UyMUtvTUNyV1p4YwpOV0tOT0N3TnpYRGx5Y3h4UjFWbmdNNnE5cWpXSEFkM0dUNHE3cHA4d2lkY0xpL2lIMjVhZlM1bC83U3MrV0JFCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMk9WaVVRRVF0KzVWQ1Z5WFEzMk0KUXVteGJ2VUZ4anAramszZVdmeFhVSnBqc21IdHhlems3R2Z6WDZnNE92bklVV0RJTDdRR0F6RDJkRHdFZmVGNApKNzE0eVF2VDF5d0llblRVK2o5THVKVjVqcDNtZlJGblI5SUFZTHJsbDVLQm1UdGFqeFQ3bENpeXBZcThicUlRCmdiZmROTWxhM21ibnVKMWFETjdwWEVVSXc4T3FBTXRncE1FUUxDbEhmYnNwcDJGdXlPYUpjbzBKdVY1T3JZQzIKQVM2aEhnbGRKR1g4YUNGNnQ3VFY0ck1qQ01GY0k3dmFsa2N1K2w5ZCtCYjlkbHZ0SUdZbzRuUG13K3ZNb3BQVwpkOVlONGowdzhEblBzc3JXaHZkak41RGx4NXlPajRWbnpNSEZ0S0ZsRVRaTlY5OWYxYXZnOEtvVTIvblJ6elgxCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBODVEOSttL1dwMzhOcG1Qb2dib2kKV0YrbU52eXcrc3BDRFN3TnR6TnZmQXBubW5SL2JxbnczaG42K0VVd08vQ3ZJalUyb2ZxdUFPVmVUeHlpWEg1MAoyVVRrcEovQ0dyZVVaV2Y0MHpwNXFvUlArSGJ5STNoQ09uSE1sVitLdnpseTBTM3N6ZFNSUEhhNTd3OVdXTk5sCjcyVzBHbGR6SXYxSXNGZlpCMVJDZFZRNWdWVEQzck9zaWhkZXJSY3c4ZXVIWVNXZ3QrelhpTlE0WWJiSktHS3UKVWpkVzV0UEt6WHhjcUgrTERHenY0TFFzTVAzQWxaR20rTFVBUG16RDhSWHk5WHdRd2ErYXBkMjNGOEhuZFlJegpZdEVNUUk0VmFmWVZURDF2aFhhWUx6SS9lRXRmaWcvZ1dUcnZZVUlTc0NwSHFLejRraU1vKzJJaE1udFBTY3BxCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXd5WkZFc01DbmwxN3FvbVVwUW4KRUcvdHBZellsV0RNTFFjMkZtT1NLR1BwOVdJdlBlUytPZ3htMzFHOWtVVy9HekRMSHFibUltemhoQVJJaXhpSAptOFJEZ3lZTElTWENhODIxOHc2eUZzR09LVnM5cmJMV0t4c2ZuR3R4Nkp4d0ROMlVsT1J4SlBFUlFSbXVqUHExCnVPcCtWaHNzbk1oZmZjYWhYUmFYd3dCcTNyQW1jeEtLdHZaVjR2UGdvM1RSN1ZMbEFwRDFtck5IZHRrTzU0VzgKVnlWMXdOQlNnYU1BNWdWNWc5R0hOWWJUeDFYdVpGZ2pUVmh2OU8rRVRpa0h6OXIzQXZwbFJRYjNPeTQ3bUU5OQpBY1Z4aW9vbzR6S04wR1c3K3pNcVdMWTh1ckV1RFlxaDZZRHAvay9pLy9VK2w2dEJkdmVqNXlTVngrK1pnNVBoCjZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEwydUl5TzZrVWcvem9oK2dQWXAKMm9IQzdZcjRkckh5Z0lPQjhXQ29jcFI2UjFFbmVRamtWdmRtMDNzakZyYUJKZTBUbk1vWFRpbkdZbmY5ek95TAoyT0poQ1E1dCsyeWQwMFhOQWVZUCttR1dQQTNwVGd3MzUrLzJVVmVkdEtnTU5ORFQzQlNyV2JvV3BqbmNJQjFYCmdTRXNHay9URmJBaDZlZ1MySm1yVjdtMmxMS0xJdGt0T1JVNlZ0UVlOUDlIVnpNRnBEcDFWS0RwUWR4NHkwOUsKZUZtak9vamVBTnd1S1Q4cG1rakJ2NDZlR0t5elQyZk1GZE5nWVdWVzE3NFVVMEcrOUU2Y0FhYnNtZWdXL0JNdwpzMjhTdjFhS0QrR1hlZTBxUVBMMFBPN3VQdlo2S2ZONlhYTUZMSjhHRUx3TDBEc1VhVjFqNVV4ZkYvYk5CQWV5Cmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGY5a3N0UDc4YmxXelVicDN1TVMKWVR3VmxsRCtnY2tYekwwSlRKUmg0RXFKVWZMVVlhem1tZGV4Y3dGS25LbXo4VENWdVJjdnFaRzJEM2lNSUNGcApwelM4VDFIdGgrYXArdk4zcGtpcXM5QWNQM0hpMlJock5LRm8zQTNtUE1lNGlUMDZlbkd4ZGZBeHp0KzJpVFVsCmo3S1RhNjRHWkJwRy9tazBiTG5taGpZMWFyQjM5dk51NTNIK3BXMnNGZkFXRUY2cDZOa0tOZnE0RmYyY3YxUnEKMVlwb1RaT3V6Nng2VDdDaHR3MXA5UytrUTIwL0JzSk5zMlpMWDlHbHJ0MkJxQ0czZjB3Q1lTcGtHN1pFNm4zQgozZzZXbmFvaVdteUgyNmx2N2ZNTStlZDkvdHp0bjZxeFlBSWJWZldEZVBhRDJEWlFKLzZZai9IMVJSZTFtL1A0Ckh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkhtV0JIWHk4VjBralY1RTdhQkkKTDBtaDF0eldROFZWMERlamJEeFQxYTQvUzdRS2sya0V6b3NtcDloT3FRaGNWUU5WQ1VSeFE3eVlUVWdwMlpqVAp4OVN4dWUySzRqOHdHOVQvL1FoYWFyUWRhNUhyOVpZNWRFYkhZSktTUkxwOXBqU2tuWnlHTjBhald2M3lldC9TCkRFTGdVRERuckhHeEpHTTE4RHJVbHNDaDJ3NzBOQzhscjhWMFY2b1o2Y3d1elNxYWtyeU9KVHJkMXFEZ0Q1VjAKdk91WkM3blY5bmY3UDV2Q0ptV1c3YXVkNUpPbHVRUnBVYVN4VjROSHM2b2llYlc1aVVpODZZZ1ppVE91SVl2ZwphQmIxM1BGdTFER0lmOHREMlBacGhSUFdZNFhGZjRoMHlPOEYyTW9SMjFtdXdyMEdXdXRKR214cC93KzErSzlMClF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXVDT2hJYjlHcEgvSXQ0N1duVUoKeGVIb0I1bldqVExXbml5Nnk0QnNKUUJnQjVVdDhPOVl2NUFyQ2hTSHFYZUE4eWU1T0J4NVBUSFpJS3V3VTZyZQpQaDBkbDVKaHk1RkE1MlJKTCtMeVBTRjhnRVltMytRWjZpUG4rSDJSbDhiSnRVeEwwT3YvWWNZRlVNbE9CcUUxCmRyckJJbEdxWW9uZ0M5OWo0ZGFSNnlnbmEzbXpFZEZ6dG9FcGMrdmd5QjhpZGxQRFRVUFJSblBZNUE0dWpLd1kKVmUzWndVM0FnSnhpQXk4LzJXWkIzVkJhaXZoYTMzbFJDMnBGdGtiOXYvMjRHTUd6S3BKRS9SOTJza241L2pHQgplTnhBS3J4MHVsQUtGTHdDWDVSVUk4NkFVVFBNUkk4dVVVenptTDVtYlJ6QXVFM1FXcm5zZWF4Tm9WZkZGSDZ1Cml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMDh5YThBUThuclY0T29FaWpMSnEKeFI5aWtWOVpYeElFVHRuaHdMQ00wZ2hKTVNZOWE4QmVTU1N1dVlBZWZpeDV3dzVFaDZOTzB5SHJnem92R0s4aApzYTNsMk03cVg4OWszZDI5MkxHYWRxTitSOEIweU8rRFpCektMZ1U4bFhTM1VmRmF5NFpnSXdxQjdmR08raHVaCkJTcktyLzZUT2FZSDgyNHVBRnU2UFJvcUQ3SlJ5OW5ickJXeS9FMldsc2k1dU9Fb3lrY3NGL3krOW9qemJ6cjQKZE4xZ2VHQjVRcWFDS09Hd0VtQ3RLNnN5SHVhb0dJN3I0Q2xiakdNeC9OODF2SGY5UkwxRkloazRjZTNCRlVkWgo5N0NjM0JGOURReWJZdHdXdzdYSkNOZmJ6YzlJdUpad3dRNzJ1d0RxdWd6bklOcXl3aHBZY2EzZEVDSnBUWWloCjlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNEp2aUdOUEF1Y1Avb1JON3VDL2kKSHBjdXp5OUpFS0l3UE5JYjJOYjNQZjNJM3RBWG56U3VTNEd6QlNJcmR1clN4bWRsVGJja21vOXZabTJwRFdKOApBWWIybmpnejRrOSszY0dtZTdPSFFQMDN2NTNhdjk0bTJicVJiaStWOVlaUDMwR2N2aHNrV2J5K01yaGNWaWdHCjZmbGlzUkZKNGcrcnR6MlRCdVp2Nmp6YXJrdjZYRnBteTBpRWhFcVNvMkdseTFHTTJtRnZBTDJqVW52TGExdWsKOFhMeHp3akQ0d0tkaGRmbGRlQzU3NlZmVWFsbzJlSjNTcDRoWlcvSXFGcnlRUFJxdmM4cE5XOEJXN0pFRU52ZApPeU42QmlxU2xkUjRwcmFOV2NuNHFnYXVEdkdjWUIwYTNrUG5zK3RGTHozZnhGSjEvMkpMZHA4NDljdGdxOTdkCk5RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcUZhcDVDZFA4R29hQy9BZ2czUnkKZXJQWHFvby9LenFvRDZHMURqWE5UMTFna1ZrM0p4djRzeW05Y1hCMUpZZ01rQjJIKzhOeExQQWtDV3VXQkpzdgo2R1pja1JBb2huM29LOUViNzRTZ1lzRTRaNEFHQi8rYlIrTG8xckEyUUpaS0NLRW8vZ3FoV2twdHhoamlmdytaCkt6YUkydE9yaStlQUNrdjZMTTZyZVI3Mis3SllERWd3bGhLaWJwSWx1WURhb2pROUFZZXFWZ3BkK3FhbUNWU2EKa2cvYzJyWG5nN05UZThzckxSMlg3RzJvL2lFNjhtRm50bVM2TGhpZUhZdVlHOUk0cnREcmt0UkorWXZFcjRzZgplMC9EQVV3N2FhS2tZejQ2UVd3UEFwcmg1NmwyRDF6bnpyYjFHSGc1NWFSa1FWZndJd2MvVjlUZWV6cWMxN3Y4ClpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMzRzWHNJTi9wdjltbSs5SUduS1IKcmFLUWNWTmw0ZXBPTDIrMXNPNlpJVXBJbk0xNjFPYnJtaW1DM1FNRkJkMWVXYmxicE9mLzBRRWtlVTJKOERNSgpPYzNLdGVoQW1JcmQ3NmtBcWVydzdUa0Q0UzN3MVRya1ZNNVVobVVwc3R5WXBEd2xCNUVkU1lmd0l6bThKWjhaCndyTjNRMGs4RWUrcXRSbjU3dHpnUGQvRUQvNUVZQnd5MTNYdEU1UytrYkZUSmU2OURhekhGYmxCcU8wVjhwOGIKN1VNYnhwSDBrRklVd1hlQ2M3VjZ4cFJLcnlKem8zYWpqT25SQWh2bTc1R0Z3eXVFaGhLeUZJcUtsalB5bmpHNwpUbXZRMDNKcC9ua2RUcElQeVpwclVkS0RPTEJMR1kxQlAxNHZUbFpuMnNaYVBUQ09aK2FaRHk2dEFFcmdJd2VhCmp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXZLcTdKSlZjcVY1dXdSSWRrUysKYkp4RDhzUmVnbHZQemJnclZGQVBiNFJtQ1hnNnZVYXhBaUdDWkt3UFpVQ0I4NHRvc0c3NytvVG1TcmtxbUVwSAo1NGloSGVPb1VFRG9VQjNSam1haU0zUnhMWnkrelhrMVFKV2lqOHFBWDNnc0FNMVpKVk5YR3h2M01TTFJ2azhlCnBpbW1tRG5nNTBSRk5BdUc1Z1R6d2dDVGh5aHVMdCtkVnA1bmRSeE5MR2JaekRFNC9mYTgvS2R3QTVZVkhWQWEKa2NCb0llQlRmdGRMZVo3N2QwdTVZWW0wU1BPMWZ2ZXpiekV2V2hWMkFheHp2MDYycXJUSm92UEs1NEVzUkxCcQpjcVdYK2lIdTV2VTNBVFBCRjZjTkJ0N1k1Y2xVd0RaZ0RZa0s2ZzloNThJVUpoM290UFpnaGxDVFZEV3ZUOFBwCkxRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeDJKRDdpNHdSanA2V3N4WTdQR3QKYiswNWY5aDRNb2JxeHBZQVA4N3pERDVwamd2OVFyWmNnWlRNRlhJb1pUdU9qV3FXTXFvNmk4eURtVkUzM3hMMQpDMG5ybWFRYkVaWjFnd1BzYzR3WjhyRy85RHVFaDIybWRPZjRkdEc2SG9JL3V3N3RocklGNGNNTExMMjZvQUIwCng0SXRtNDloYmtlQlJuS3Rqa0xhRllETGpXSE9xYUxXZGtPVnR1RFpFR29FaVN5SmI0ZEZJV0kwRU1WNjZzUGcKWjJkUnR4ZEZBaExvTS9jV0NZYS9sWkVadjZRcm1rVjU0dS8rY1hVZS9XSzY5N1d0a3pNakZpZFBNUDl3dDZaRgpZdlllR2dZNFcxVHNiUGJBUk5DdTZoNE9kVmlSZVppY2l1b050Y1ByNHVNS2FjNDdKeE5JWFpKS3V3b0JuOGZECjN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc2xZT1lxWlJhUEhmQ3dSVDJqUjgKNDhMZG1tYnN5ZmtJUnVzRGFjSThENEZBUVhsM0ZFbVRBWGhKQjlQYkdPdHMwM1NnU1I0NWVhQytOTDcrWkZMVwpRYXZIaklTcXRaS2RlbGIzUk9hS1lVTVQ3YjEzQm9QeVR4eWJjM3YyZDFIMVA5U3R5ZlczUXF3RGFnNjkrMThnCktObE1yR0dLUGtDOW9mZTMwQ1o3UmVHbktqelFYZDlGZjZYUkJEUFBTQWtBS09ydVJxVmZmSEFyZFpzbmFPSEUKKzR3THBuN3puaEREQkRWRmlmRSthNFVCREF1cEM1YXNKdkRjRXl5NTdzeEZwRWR0aXV2VUtINko4WjlXUUxVdwpwcDdPNytabWU2eE12L3JnUHJPZk90K3ZqVUVtVVlZYlNJcmZaMkN3ZVBoalkzRVFENGNvb1N5SVNKb1RhbTZOClhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFRUNzRaelFPYzlzdW8ycENFbisKVnVoTkhOMSt6NUtyekNBQW5Ia2hTRXcrc1JIanlHY3JSZ3VEZ1l1U1h2U2R4dnpiOHJCZGVJaXorQXpwV2ordApxVFlHV3l6MFdBeFpNSENDcDYveVA4YmVoblNLTnBKSFB6V3JQNFRSNldweDZwQ3NIbHNpZ2hUWEJLQU5YS2dtClhzRlQybGlaNTkvWDEyVkJzendoRzJobmllekoxQmZQN1ZTYkNSdzFPcFZtbk5UZnFLZ2lkTXFHMXl6cTBYL0MKMmFhb2ViZ1o1ZzhaM1R1SUllZW55MW9ZQWJHVTRialJVL290TURwZnJ4NzVMcVNzTWlMNkR4RWp0QkN3Q2gzMgpWNTVCYWxMTUJsU095ek9FenhqN2J0c3BrUCtQTXdJSjlGUFN6UUNzQkhHTEI4aS9mMFNSMjQrWDVEZzEyb2dJCktRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBby9lYWZQUE81Uy9mT0FnY2E1TlMKTHFrRy9JV3YrdnlBVFZZSitFSVEvK1JUQzl4ZVlSRHBVVHhuR2JTV0VWUWJKa0dzSDdyejhaTERJOWJ3RkYwRwplclJSemdZUEMvR0hKNWRuYll3dUgxdzlkbTdiNzNscHh3d2JhUGFwU2pRRXhJSk5VV2t4dDQ2Wk84WEwrSS9uCkp3bjh5RUJXb3MyRzRHTVI0WTBid0VoQ3hyQ3ZUZFZqK1JFcHlzSzZWb3AxVTJTckMwNW1nWVFhZlE2dnZScjQKaER2SXBCZnhZeit1aDVrRU1kdVlXbWJJSTNiUzdVcmUyaFcvSy9zNlNzTXJPb2k1cXl3MVFtdVdzc1hBcVZQdwpXS2poVFZmSjhvc24rSWRJd3pkSzhtaVlkMVQwTlBBcUROSVoxb0UwUW4wVm1EQW8vek5BYTI2Zmp4WGpJV01PCm9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMHg3QlYwT01Xc09jQXIvNkNTUGMKQzVvZUU1TUFrS0dIbi9sUXc4cFU2RmFGbDM4enQzRmZuK2RCVThWNzRLS3lkN1JIbDhqMHlQVmkzVkFDem1yawpnM0lYd0dqMkJTaGcwZlZqc2pGeHk1VlFVQzRmeVhocnhDUnVyTWRTcmhPUkFuU1F4cVp6NlVLOU1LcmE2d0pvCkc0WWl1TEhqMjd6M1VIU1NrSVRDWFlBcFY4YTRxZkgrcXNtWmowY203c1BMRm9qYTFXYnl6eDZMRU5VR0YvWjEKemVtdVVzb2tLVVBKTGZsMGFDS0dqdENjYjg1US9sTHZ3VHpzQWY1ZGx5NUEwK2RWcFV6YVFtUmMrMHJsd0UxaQpENWlqU3JNdU90RVZnYlFMcnExT3BEbDZIK2kvWkxhR1p1dVFsT2J2NFpXLytIZXRQQk02VHRpZFdUcGYzWjhNCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbUlLSGhFRE1GeUdtaFV1cXl1T0YKNWUvWjdSY0FDbjUyZEMxSWY0aXl3c2s1SVpkYWIwcVBzVHF1Qm5CVk13eGp3SDk4dVY0aHZJWFJEZzVNYVNNegpmNnMwOURodW1qamh4OTJVMlhEd1I3MlV4NFdOZWJuSkgzck9zdEJmS1FlNlNCMlJKYkJyYmM4MjNkK1U4dGs4CkVSMjJuRTZCRzBDTmY2d2NwYUJZaGJIVWE1d3ZRb2EwNmJyYkZWc3oydGZaTVNFcFhQdWtWVEMvU01Sc3drN0UKSlBXRGI5VTNYbGVyamRaV0Z3K3EzRmVRNmEreS93RXhqV05rdHZlUlcwbzg5U090SUNJbHFWQkFidHBhTi8ySQpOR0ZDMUw4YkNSVHhuUVhhWnRJVHQrSUlQeVZRdmd0ZDNpNUpoRkp6T1lZS0wzT0kvSGRLOVFYQjZIQ1RnRDNnCnN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzVKdDBydnR4bytPZjc0QWxsS2UKRDluWCtvZDNQUWlaM2xMZDFvdHVBbzhvbEhqOGFQakZ4OWl2TFlYWFpHaU90dGdVTDRUQ3BuWU5EWjFBVjNtYgpySS81YmhDV1ZyRkVGeFBYOXlFeitqME1hQjVtMjgzOG8vSXU4d2FYeWV6TjdxODJaR1g0cmtBZDZNSjZpWUtNCmdudEN3OTEyOFNFSEFRMW9hbVBBVVdCSzhFeEtOUVhGMVJTVUVtZlRseDJTNFZ3ZlBPRGIxWGgvQ0RZRmJXbzAKOHh0L0hSQ2M4c3I5L0NhVlMyT1hvRG5neXlyNXo3VUVXdU1kVFA1SkZuZlBZaHpnUGFmaGMrdUNpK0U0L1ZXQQpJU0k5dTBUandZUXhsREtUcURvcStCdnRxMmRQaGlDWEpaSmt6MUo1bG9xYWxKYUVTZEI4WTMwMm5Ua2orWjhxCmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0pma2taeG9oTGVVY09URFF5eWgKcVBUQnJyeHNBbUFkYWFFV0hHdmIyR3lQWmF6NWpSWmxuNmtEcjFhQkdMQXdGL0drRHJDTkx2YmJVUXd0ZTVsWQpPMVVML3FPUmhRUi9JS3p4WitsZEl5QkFKZ0Z4MVFqazd2bU9lMXV1dHZBSjAxSXhGMHMzM3ErcUQraUtSSFZCCkNscVNXNEc3TEJBTndieEo1cFJmb01NLy9vQlUvb3h1amI3MTRDUWQxeXZycGhNemIxckJuaUpmTUl6c0VucHUKZC9mNDBOWUZWVGtnWVJOMjVQbWlENW1WSFBzbHVJRnFRNzBaTWxyWkZ4UjhDRHJnbXJZTVVPWmlKTDcyZ1JnMQpPVjhjdHY0Z2NyN3ZzQzk4YzJqdXV3M1VaazEyT1hnZXY2aTZkWEF0RXpHK2ZDWjdTaFpsZnJGd3JRWUtyVUtLCldRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdE5hU3pZVGdTUDhFcHJNZXhSRWkKTG1TZnB0Wm9lcU9maUJsNWNuM1ArSlFJMG81KzRjbytnM1hwUjBQempUcWlXNysySVpIbThrS2pBVlBnWklRVQpud2pseDJLRDI1Ym1GSWl6ZHY4ZnYyN0w4S3pYbUc2eWJRcExhWldWaUYxYlpuOWZYellmdVVtQ2x6YmlZcDE3Cit6YjV1dHowdUprSlVQL094WWU0SUk1MVc0R3NxN0llRTV4UnBCZFN4cktyRXNncHZmZUlzdHZ3QXZRckduRHcKUDd5ME4zT2lSdzJsRFNSVXNqMlRzTUFKSlFHa1doNG5tZ0hYS214Smk3ekZPanc0TGJNdXEyTlZOdzgvd09lZQo5Z0g4SXRSdnhqNmEwQnVqelo0WFN3clhoMURNeXZzUTFmV1hIVEdleUYrTDNZMUw1dlNUVldSZ1VmZk85WHhuCjh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN0N6ZTQvbDhvajNTZGo4djNrcmYKbzlaN1AvQVJidlV1TEpiNHNOZlM4dmczY25ML3hsVHF6NGhtUlMrdVd4MnhUUlNUblBYNXJZMURBYVpRK1dkYwpOdWllTVZJanMrd0tyTDhpMWxjMStXTXpyNmZ3V0NyVDJtUTRubzJ1Q1pEUFRRRTROUEptRU1kbVc2VGYwdVgwCnRtSWpsYnphb3F2Vk5zQkVSVnNWR0pMQ1FVT0hBYWtiSEE1YzBRcU0rdUg0MzFmcHE1UllkTHVtckdGK1BRTGsKUWErTERmV2JIQndZUXVqZTFDMCtGcGZ2OHE2WWFuTmlEZ0tieTdCbWRJME14bmZlN3ZSVFpEZDlWcjIxNC9WUQpXRFpvKzlEN2lSNWdyN21WNHNQU25GeCtqRWIwSENER2VlZ1dlOXN4eTdEb0Q5b3FBdGZEUy9rMGI3YzJIdTlMCkdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbngwU1pvd01iTXhRY1lYZFEzT0cKNEVlV2FkVEJQQjFqRDdMcDRRVFpaaTVxODBvVXVWMk9NT2dZR1F1amtjTDBVRWhVN1VXRThWYTlSbThjWjdlNApNZGl4VXdFZUU3eVNHYmVEbmF1TkpIekxnSGVQL21KSHBHemtQV253U2FJeHVOeHZ5a3NadnVveXQ4VGRhWlRhCjZxaXRqblg4V0xaM2p4RUdDUllyMUFoY3h6OC9Ec2QwbnZ5ZGZGZnVPUEpTTmFOSmkzbzh4RTFlQWRLV2V1aFAKRzQ2S016QnVBdDNlOWY4NDBYYXovdldkZ3hUUjE4WXI4bHpSVUFLRGdJYzdSSnZpQjB6bjRjdGZ2dTBRZ1BaeQo4TCtad25wREpzeWdOWm9mRWx2amRsK1pDUTZIM2hlb3paa1I2d3dsZ05JREh0Z2hUZlpFMUFOVWRwdWUxSThiCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEdvci9qVkorSGt3K1pMb01QanAKYk03a2tDK2JmWkJieWxNU05tNXhPVFo1UTFCcWZQWURkTEF2R1lKRmlLZUk3Rm11cTJORFF1UFVHRnlhZVAxTApiWTZHYU9FRGpZQ1NHb3NESzJ4UmEyL2pYMTBvOHMrbFpBWHk2UjRoYkMxTWtpcE9MVHd4RWJqQlhYV2QwbE1sCjdwR0Z0aTE4Q3cwZVlFYkt4MWQzd3NaY2R0NVg2MUpDQnUzV0FwVlI4NVM1RlBSWHBUQm9oWVNvaTV3eld6MWMKUW1YY0dndTN1UCtFMmhQRU53MHBUUmV3QTFWWThQcmFNWmNuWUluMC8rWUkreTFUUkl2bmVLaHBTS1NzMHEwSgp4VDR0QmNSRFhqeFNXOWcyOXJ4ZmdBaUkyakZYbjc4Uk56MkMzcllraUt5NGlzZHNaNzNUWE42N2c3QVo1TGVCCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb0ZkWm9DQ0NDV2pHUlRFVlJMcUoKY2FhZWNZY1pEb3lDZ292b0tYRzI4U04xT2xNV2pqVDFSWFg5cnV0aXZvbkppQ0ZWV1doQ2Q3MnlBOUZHV1FtNApaMVpMdUZJK0pHYVpWOG1ieFpzeDMvVitPakQ1SlllL2R3RUxtMTNFU2Mwc2JYRC9GWDdvMVY3bi8zSS9qZHpaClNtTEgrVk94OXV5UUtpS0VnTHJhbVNXc3MxdWxMTUtpQ3loZjFYN3hWNlRTZk4zWkVINDdJd1Z3RENUOGJ5WWkKbU9wK2xTZHViVVlRWVlvYkY5KzB4bm13RVdheEd0VG9rdEpwajkzL3VQQU9aRnlkVVpGZXF6QjdjU2orbXQwLwpGMnlwMnVaazQ5WkRMTk5majllaGhZVFF5NmgzaExPM2grNzZ2b21VTm1iaHNCWTZNdmExOHBUZThKZGdZNitRCml3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMTBobTh5Nlk2RlpGdGE4OVVUUXYKenA1TlpOeHRiUFBrdGM4MTF2VWdnYldUSDJrbUE5V1JScjdBOFRCa0tBQlRIYUNtdTM0ZXZCdUhLVTNNRzhubgozckd3M0RLMlpKUUQ4UVhnYmtrQ1FrejluTVRvanFJelRqOEJyYlVXMEhVazc5eHJnekFBSUkwMnNtR2IvYkxWCm5icEY1VnRCbEROdmd2b1FjcGZtNllVQVZQdnRoOTBpSDZwbHlWSDAza3h2akxZc3QrZVZoaEM4elRLWWV0RDgKeUp3c2ROVmtCWDlvcEtaVm0weFlBOUR6MTh5SEtwVlpQZVJzTWV2OHNvYlhSSG5pNHlPb3gvZTFBQnlnK2dIMApDWCtlSk8yekdnZ1hMVHIrU3pNdGtTMWhjSllCUkZvSWhiR1RScFJ3QjNVL245T2tNM2tOK0x1aTJrL0Fsb0U0Ckp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEl1cHViOXNQanhMcW9HRjlLS1AKN0lEKzVwdTdnZHZlQ0k4M05YMGdsQThKeUNmOUpEZFFJLys5YVhuZ01qZnNHWTg3T1ZQQ0ltekdPV1hNK1Z1SwpYam5ETFhqTU5xZnNqbjFzNXFpalltVGhjbTBoMjV5NWx6UklGN2NaMCtCdmxZdVp4MXlBTHl6UlBDMFhLZXRuCnlJK1dNWEdwQi9DL3FyVC9FMUZBaFNyU2FMeVRIZG5zM2lrQVY2ZmtSdzEyZS9BZ0lGeFZOb2VXQXY3aW1taDgKZktFOWtkbXhxSjlJenB1NFFab3QzZnhGRWFxdGtlZjliVnZ6blh4akRyU0FQYXdlenU2VEdxclFONzBZNW1magpBQTdGOWdpUVhpS01NeUJGaTdHZDBGNzVqdmRiK0gvOVVGRmNFLzNYOWNmMDV1WGE0eWxsaThZdExIZW9saWVOCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNkJyMU1PUGhZOUxYZ25HOEFzSUMKRXZLUFlReHJKdGxUQ3AxNUZxNGJNQVd5OHFCSWg5ZUl4dTNUWjMweFFjOHZTcEtlV1pLOHNaNCtHeDhCTldrQwpwTWQzYVUvU1dTLzhVQVVyUGdZMjNWV0hsM3dqZlZSSG1kb3MvT0pSQkxOL1Y2NTNKZy8vTEI4Rm9oNUo0OHkyCjdpVlRNMXFCUGRWNmp6MVg0Um1BUFR2Tml2ZTcyZ2xNb0Z0VWRkc1RVempibGVTb09HazVnZlVIN3QvS2lCTjIKR2FJQUFreUVzUGIzMnJaNWI5THFsL2UyWkIvaXpQa1Fxc1NkRUkwSEo0Z05LdXlGMytoNHlSSkpaSC8xL0E2aApJbUpGNDJVOU9ySFRmMW9TdlJTbGdSbDlORzREM1FTVmoxQXp1b3BYdFpPUVU3dFRRYzBGQXJaRGREZTduK0o5Cnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcCtPTzBNRitsbmVCR2d4NW5Vb2gKQUNPMXpxQkdyTFFydHBFenF6QlhhWlpBN08rQm5GMmhSaHFpT29Gc09RZkxwbHJXem4xODRSVDdzaTlmTThkWApUR2pxN293elo2aHRYa2UzSEtiZTBTMFFyRTkzWnpJS3c2MGI3dGV3a0ZRS0o1Ymx6RG43bm9tMEtRVDhaY2pCCnZzTGY0d3E5VjJqYm55V2NrVW5qUFZoTUU3WlBvT1NJT3QvZUJYMk1nN2FqYkJNbTc5SnpuNmY1VkFmYjVLaEYKNjhJMUlxSWZtWGlwbkxTbnNSbjBnTi9BczZYb2tuRk0xYWtmU3c5YkJROG1Fb2hhcGhVS3NBMHVTcDNBbGsxWAo2UEEwWGUxMDdyamwwS2svbjFvRjVSZVFWeE9ZUkxPL00zTWt1eGZPc2VaSUtrUittTHhybHcrd0pQYTNTNlUzCmZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd3Q0d2sxWEZBbkZDVlZ6SnZkeWkKWlp6Tys5Y2YvZy9raFY4aWQwMFhuU3JGOUJzWVA4UERRTkxLV09Da1hzakQ0UG5GYUt1cmJmT256OXdJMDB6Kwp6elRkUm9kVjBhUWJrekQxN2JvR3psc3ZmL0pTdzIvS0ZUQ29jb0xPMHVOOFZ3ZmlNajFKSFFPV01NUzVqK1puCnpsWlptKzc2WTc0WTdlMFRpVzlrYjNZUGtLMlJiK2h5bXlmTUk0NFJ3UFlCSDUyRFFuTGxYTnRUY1RQNVJoc3cKY2Vka3I3Nkc5WEZadlA5b0ZzaytDMlRYQXA3RE1iNjgxTExZVHNwSHdEVFVzZGhNUzdrd1VuUWRnY21XOEVkLwp3RE5EVlhQZTZGK0NDTUlsMG9sbzVZWTkyd3RxQXJ4cHFvWGxnT1ZvbWUzaStCYUtPY1JvM0JIcUVJajBVMW9MCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWV0ZkNFQzM5d2dsMFRIaktGSjYKSzdSTHRHaENrMmxxZGxIQWVJV2xta0RuNWlnMTZ5bUdMc0p3cGlGNXlsSFAzc0ppVndkbHQra3ZWdWQ4aGFiQwpVN1pBTEJvQTd0dVcyM3lSa1BnUVJFYkUzb2REblhSMFRNV2pFRHJFUkFnTUhCaHdyRUtQKzlaUjZjOVlDbzJ2CitmMjdaQWtqcWZ3QlhOY2FBVm9XOHFoTHM5VjFIRTVhMmM4bXIxMFZPVlV3R3dIVmdaeTI5SFZnQXZSYjBpZ1AKZ1pCczdkaTR0MTZzRjJjTjJsRi85dGp5d2E4aC9QNjByTkcyVW0weUZNN3JLQ3Q3aVJUbmhNQXlETjRqS0lxZgo3bmpRVER5OWhTVVVDSlAxblF4OXJER01tNGlFZXJ3T21yMWZJb1lQbG1Wam53VEIwY2h3dm5nTm1KeC9maHZmCjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM0toRC91RUpMb1N2eHJMOFFlYWEKUVczcjNQQkNzVW45cElTeDI3azQ4UFNTeEMySnE0aHFyaldoZUlRMEhCQjBZRHBBdW93bWFYbCtwbXIzVWEvQgpPTHJNckMyUDg4WHhXcXdZYy9QQVpldy96K1F0SXN6QXIrRndRVFV5cEZhMS82MlBzY2VVVTdsSHo4QWpBT1pMClNqR3kwdEpIM0UvWld2aVFiZkxBbExLTHhhLzJ4WnFJV1ZsYW0rakc2SmIvTlhsTHJSMHl6ZzJ4d0M5a293TGEKdEhJM3FzRWY3Ly9sOFJ2bi9vbTFiMWxKYitvSlFpSVpDaEFmUWgxeGE4YXQ2ejFLa0ZoeGtxa0pIRkN6QWZXTQp6Y1VSNlQremV4NzVPb2puT01VUk9rMmhIb0NIVU1iT2JHWTNDZ1JQTk02bStJVU56bVhkaG0zM3ZZZTR3MkVWCkpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeFhCNExKQWRoV0dNa2RPMWVudHEKaTNxT1dtbE82M05OQ1RhVmk3WDFOb2ltSWk5bmN1RytUMjFRV3BZYVV4M2N3MUt4TVlPMEdCMU1LZnFpaDh6Swp4WmlYdGJNbXUwcm5EL2pyNlh0TlNCMHNrVnNuTmVFS0dVUWpBTGt3d2FkZlMrbjlmK29KZVBXUmw4ZWE4ZjE4ClloTk9EWFhLcnV5OHpGb2U0MitmVSs1dGlZbUdOWWYyVEZLeVZpcS9lU0xzUGVSOEpLS3BRNk56SmszNFk0NWIKeTZUUnk5cWNKRkRqM0dCYjRtZXoyckJwV1ZXMG1aWFhQRG5udm5HMC84T201dmt0VFY1M3NObHFwM1NhNDI0UgpqMDQyUEZ4NmNqZ3FHZXpqeVBaZ0t1WHN0SktwS3g4WlM5bE5jbDA2Q1d5S0ZtajFpbFBOQmMvNVpMR2FHUEIrCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenFKUWw0UHlUMUZRZHBaM3U0Z3QKZlpaYkNNaXZoYlM3RXFIYk4rTWtLaytJd1pKSW0yYmwvdlcxdkpzRGM1VzZVTEQvRmJBZHB6eHRyL2FxMFRzVAprVjl2TzhMY0JxUTBBbTNzTWszc0NFbFcyQjhMQS84b0lXVERNZHBwOE1FMVE1aXlpelMvWW1NcDlCNDVRYU15CndwRjFMSVFBMFFWOFI1eG1BUGtxNE5LbmVKL254dmQ5Vmxja1R1MDhrcXo2dGJQNW95Q3RyUTdNRnhLb0gvRlcKU05XWmc1bVA0ZzdPNS9HdnJEdXYvK1IwcXB1enQ5WnlPVHJsK3FHNlM3SDU2QUJEWHRnWVdBYUVEb1VEUWFnYwpoMVJGTDJ4SEduZGo5R2lVMCs0dXVrclU2a1AvRlBvWGJXbWJoWktsTnZtQzNWZlUrTnFKK2FlYU9VVWhDd2NHCnR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNHlOTEpPZ0Ntcmh0aENRRFZTWjIKNStCN1NQVnpSZXhjWHdtcDBrN2paN2lBejFXSkVNay9CVFFTWTlwS29vVjRYdURxT012RGhrK3IzK2NWM05JUwovNXpXeFlOeXZMOCtkZjZOdTVPY1hmR2E1NjZiNjEybWp5S1RyUnJLUFRQTTFhNk85bEFxMkhKYU81TS9LeU5sCkhkdnJnRmxQT3NTRnZZS3dyckUvSGVYdTVSUmNKZmVPYVJJSGdnWllIa294bVpmbXNSbHJBaGdHRTdUaTVVMHQKV0dOZmFVcGxEMDFuM2xWMHVlaDVIdk50QUJnamtXVUhiK3F1V2orUmJqZXhWMUJNWkZIb1IxclZrN3Azb2U3egp3N3FGSllvcDhUVG5EUi8xd3FJOFErT2hxTzlIaGlPOHVNcWVYZkEvRTNCbHRHaTBCWWw4SFUzYklvZEZ0WlJFCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcjI0VEVDQmMrNU5VYmpYL1kwckYKdW1jbHlycFdjMW9RNmJ3eFIvS2hpQTRCSkJKa0p6Mjg2NU5PMjNLakJmYWQ5YWh6MGIwbXVuVFNOdFl3NXNZYQpxN1JidEVraE5QYXdZZlhPUGFGbEg0Q1hLWlFBQVMzUGlLTWVIbnB5cFg3VDRiOVczd0Y2WitBdzNBOU90VVdFClRvTXJjZ3ZRbDJsTFBPdFNhdDhQeE9Ed25YMmdHQ2UveXZYVk1lemN5VFJFNk93b21RUkVNWjBNb3VwNGxiTWUKYklEeEY0RjVVUHh4NGkvK3ArdURiTmJHVjliRHJRQnlkSGF0N2VCeGZlK0F2R2VRaDlpRUdoQzF0Zm01Q1M2cwo2VEZqMXFkNFFJN3VmUWhTOEZCdmtTM25senJWQ0U1dTJpM3kwaG1kcUhDUDdSbkUrdVdTdEdxbVE4THlHTTdxCnpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeTdtbVhSME9uWVNBRTRrZFBFcnkKRG5vSVhteHUrdkRDOVFsRXNXVUhtRkU1MjhhN1NvOW5maGp4NmRwZ0pnVkdhelFIdVBVTUFrTjNja3NGeWxuOAozVnhEWlVYVko5anpUZEFLVEVLcEtLN2Q5eHVBTVgzMlZ3N3hFNGVQQlJvZzFEZXpRT3lDakNJYWEzbDc3ZXByCnhvdnNIcjAzTDU0NEpPV0ROWGZOWUpGbGs5R1ljZUJETGpoeW1CN2FLRTZmcHFIQ2tFSUZWbktweHNmbnRUamsKV3lsaWcyTTVjYU4ySzY0aWlML0EvVWhpWk54dWlQRGhEeVg0cUUwSWpjbkl1UU9xSTdFY0J6aFVlU2hYU0RXWgp6QnhOc1ZkQmFtTGcyZCtZMHFUbFVDVE1EcHFpNjk2Sy9NM2hLaWV2UlNnSWJVMCsxSnhXM1JxeDZ0SFU0TFFpCnlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmx1WUdiWUw1R1VqSnAzQ0xycFIKd3ZVdzlvVmQ3SEFXekprR01iZDZONlF6YnlBY01VSmExTmRHOFRReGk1TjdDUUY4RXh6VFBQNnZrakhNTzRLaAo4NzRKT2paQ0ExZVNBa1ZpY0dIMHVPT25XS292aUFieTlBWE44Ti9zcnZsSHY3aWdHQVFyVXRPbEFzczdvMDZyCktnbnJtY3dlL21aTVZsMzM0ZHplRlViT1EzUmtNcW9XM1hjWFNnZTVSWjVaQmtKMFhtcE5sVjJZR2FlMHFpTlYKVUlXUWhqMjFEaGZ4a0N0ZkFyNTVvbzBXYmxLMkplRGtzSUs4eElIRmxlZURHSWxucUhBZGRjREdEbVdvNHhiMQo1QklXSUJYTVVKT2loSXZUbjNJZ01wb1VFaUNid2RQeXdWMTY4NjRWeFNvdGV6UWw4RFVqckc5akVlMlhrUCtMCnl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUF5bW1yNStsdnpjeXdKQTVCblAKRUJKUUw0Q050bkFMeVZxcUdtWjlqeURTSG5rekhBUlh1QmRmS2xSODJNcU14UTJJWjFxUWRYbFdiSm5VY3RtSgpxLzNCR1NTWlVuV25tNzlkUDgzTEU0ZGE3cjZNVVJyTmhkVHM3WDlqRThIdXdiRTIyRVIzK1MzSFFJY2VJSERsCkJOUnl4L1ZSZHN1TWExZjZlckptQUlBR3FNaEdpQlVjQUFMZHFSSXhtUEdTSXRObU5zVGE4TEwremVkVGNYNEgKMDlPSVcrMmdQdHlPV3VMZ21wa2ZwZHVQQ1NvTlAxb2YwSTl2SjluQlM1UE9YTXlHcVl0U2VWSzlvUlh0OUl3agpCWGZRaHRlRzdNdUVWMTF0Q1FDdTVuYWQvdmNtbDV2K0c2WVVGZlJQbFR0S0M3U0tmVDJOc3V4QzFLTjVlY0NxCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBelJIaGhsbDZnOUFGNXByMDFWWjIKZENtbG1uT2FuRjltZjU2ZFJxZ3dHZUIwSk00YThhbG5PdmJvSTZ0cUtpeTc3MG1OYWxnOTBGSi96eFhVVnY0KwpaSVJnY0wyY3oyczZ2TW1NOTBzZTVaMWR4TVNheFBGY0xxRXdjbnVPU0pZdUtsM1d1ODBScnNpZDFYRDU3VFMrClVrMHNzZlVSaGlQRmZRN29IRnVuSzgxN3BtWGxOZWlzRzdld0FweDB0MGV1cGE4Nm5ydHVpRWowL2xrMnhjSVYKWm8zUWN4TkF3c3pJbUkzYnF0ODZpdklpdVpudG1VbVdhWVBkcCtlOHYrN01BaWw2ZGswUGN5aXZYTXFjOGNSUgpUYnM4WWtJc1dtS1FOTjYrYkcvVmNicUJPQTRLRVplanE2RDk0dDkxN1BiWFNvTTBzSCtxSkYvcjdraitQUFgxCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1hpSjJHZHlhY1ZpSFkxR3RzZUQKL29qSXUxN3EwMUdtakJ2N3RpYmtMY0ppZW9jTW1tWit3bTVQWTJxNkQvbjQ0QkNkWHE5QzlGQmlmSGRSMms2QwpQOEhkNUU2amYvK0phQVV4bmJWV3JWZzcxMjI1SVlvcERaYlAwaTkzMFg3MmdRYXcvWi9Cc2hES0NJcldzZENXCkFHRzM1T0o3QlFxVExrcGtrWnFVYXNWS2RtaEx4dTJ2UWNiNnV2YUVFTUFHTE5Kd3hCWmZUTElkL0lhbnFoSHEKdWRiQVQ2OVVWREFVY0xzRDl1SVIxcis2TE45UXNSd2hqcGFDZzdVd0R2QWJoTFBDYnJBNGZkWG1HTHFtTU8xOApsb0JSN1BvcHNLK2FnekxuZDJJdUZrQlYxQVRGZzFWUlg0ZDltRnJ3RU9Ocm81SEtWeUxjNklWaDBLRExoMEc4CjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdUF5cEpVeVdyRXM5cUhlT3IvY1gKUUFTLzdOQUFiTkxmL0s4WWJaZ0Z1M0haY09lcmtmcE8yYzQvaG5FcFRrcDAvVG1wU3dFL2NFbVNGSUk3ZnhFZQp4S040Skc4c3VrdHBOZzQ2S3EvR3duMDQrcWJISXFlaWVPWGw5VlVuODBsQ0N4SDZmQVl0enpCVzcvNDBPbUk4ClExTmlvdFZoclhRbFV3TW9xbUdXZ2dXSmdGbDA1dlY5UWp5RHg4QWpzSjNKWmx2Q0l2aWZyb2hiOWIvV3ZUTW0KeW15Z2t5bjNISnN5QW4vVWYxMXRXQVpDNEtzcEZJeHJpdWNkTkowazBsem1JbElSbVZ1cXE1cVRpbDljNklBRgovdnpJUUpJbVZXMW5EUFd5aURPOVhvUmVrcHZ0Z0JsL0w5VlRubFM3YXpRWUdCcXY3allRNU9hM0JGRlhrZFQ1CjZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkdXS2thSzFPZkwybXdHbzZXUjcKaW1XdTNKVG8zU2cvbVRMU25QRTQyRWhuZDEyUzF4em4yK25nUXJJT0Faa1o4TjNoQVpub3pXUFhvRmZMaWl3RwpvQzFuVUkxYVRTdHJjR0xlSllaeGNFUXZaNUszWk94T0o4RWN3dVozMVp2eW1OK040Y3ZrTWdFRm1ZUHM0MHQ5CjIxWnk2V2xnenZFNjdmOVFWN1lIS3dsTTROd21lUHcvNDFOZDF1VHMwUUNaMGJQU2J2MlRBK3BxejdtQUkvbk8KQmxUV3RTOUpSaGxWVlZoYjBMdkdQTEw1KzhGSXQxYi83amUzSW92MXA3dUFSd043NkpKZERlVWVPam80U2pwLwpZenVtZ2hoazdDakt2UU9ZMnlaczlLd2w4SVZPMHEwQW96dFhhS1lZV0FDYXhnMjZ4Q1RPdFc3eVVpSUJ1V2k3CjVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNnYxbkpEV25pNUxYdG9VdXR3U3EKRmhraTNRZ25hbEZINnhyTnYyM0VGZU05VHdnTkRBaXVDWDU0ZGJrTmFtbUpvTXlGeW10YTZVZ1k3NEFnOWdXZgpHOXJLSVBDckZJMHBRMmNkUjB3OFMwVGtDYktza1hWaU42NDVtUFp1UEVodm8wTHFhMy9LN2o0dThvY2Y1bjA0CkhqcWFUdkxTNldFRmFBMHg4L1o4eFpaWkNLa0Mvc3BCeTVtNHRCYlg5Z1ljeXNVUFNWdmpQdEpCaUZraHNSMUkKeHlTbXdvN1ozS3NKbkkvVmI3dXRRUXhrRnBtRU11R0wxK1RZeFVCY1R1cFl5cndyOHpFVjlZQkJCN1BFczRGMwpuNGtnMGU5eHlVVVJYM29uRW5vTCtSejNXK0E4OFdhSldDL1FLb1A5Q2o2Rjdrc2x0d2J3R01ucEFGdWRYNkgyCjJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBenJBRTRyMGUxUGowR3hxUm1QU0wKQWpEVDhqN1RBU3drN3dKM2phcEFKL2h6U2VXOGIzLzBTVytFb3VZbFg3UXJaU1BsdUhpcnJyK3FDWFIzaEptWQpRL1lBNkhmbWhGblloT3pkM1FGNlZGaVM1V3luQWh3R25pQytmajYyZzFDM3Vxampnc2p2STBycFdsc085c3JWCm9SVTZsOVc4TGtVUExyT0Y0RUNsK2l2TXhKbVdVLzZkeHZCVXF2YVR4T2NvUnpPSlRZQXRucTBDdlZ6QnVDNzkKZWRaUUxwYXBEM1NWVEJnRXl3SzRiQjNhY2IvSDJvMFhkQUR3cG9aN3VJYVhxdTZxWEpqc25kSjZocDE1WU5TZgpTRFdoK3pneDJER1o1eUxiMCs2ZFE0bzFnUGxMek4yMSs3TzRiSkZSMmJHeUtvWlRRdE5CVGVqR2pBcWNpRG15CnV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdERETExzSER1OFRCRUgrdkdCNHcKRGFaUHMvTkxtMDloWnV4c3VPaDMvZVQ0UkJJTmlQVjFlSWd3MThIYmV3dnA3Z0pubVFGYTdJWlo0VE56UDNGbAp6bG9CRmx5di9ZdVJjd2FQQ0ZyWEhNeTVDdTZ6Mm1IZVpyRmN4eHA3VU00VHNsNEZBOEsvTGZlZ3d2SCtKUmxRCnpTUlYvYnladVFKZ1R3djBsMFFlRkJRUkYrRWErSEEwand0c2NlVFpCZU8wLzJ2VGZWcE9KK3FLWmJMbFpDOS8KaTBMSWZCU3R3QUdDVUUvMTljS2t5c2xhemxSRjNIb3VRZG9EaGlGMUpHTjl3RmRneFVycUlGbjZOcGtGWnVGZQpqYUZnODN0VWJqc1ArOGo2QWlwK1VYWEhRNFNKcmNGUXQ1d1FtVURjYUUvazlCUGJkUGVWT3Z6WnpRTmhsUzJCCkR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbWJYajBQV0tjaHU0cDNvWktSYjMKbTE0bVpETXExRDZ2aWV1eWJVVDRPdVhaN3lYVzUvSm4vbUNOMzJ3VG9HREExeEliNFEva2d4Y1JCVURXdFdxcQp5bk1yZnhDYVBwUUxpNXVzcDE3V2srNkI0VEx6QUFrMXkwWXBDaURjaEg0QWlZLzdhT2JqME1Ec0pGSGpXUmhnCmdMc2wwR3pGaHdoeXRmN0JzU0R2dWZ2d0twZ0VySTZUVkt6ZEVSbmNHYlNDVVE3bzdHU200Y3pHRklOQi81ZUkKbWJPZk5ZU3Z1Y29tZUp4Z2dUbW03cGdUMlFoMDdsWFB2RHJEZzNhYnpTNi8wWHQ5andSWjFCK240RXJZbTVybgoxbmZFekxnRFhHTy9QS09vd1hKSXBLODh6Y3U5Vkc5eTQ4MXU3YXB1N1hKVW9CN0R6ZG5zaGVQQUdLNWF3ZlUyCkJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdERNWVYrWFIxcFNacGRzWTBUU3cKRXlkREdFWTVaRS9LOEc1bzlQcVRTTGh2QkdtbWVRcDlYT0dEd3BKNWdwUmw2dHhLZlNuT2ZKbFFsazU0YzNUawp2UEhGSWtXYUs2YzBaQ2NxNHJNTWpZRkpidGpTaUd1cE5UN2UySlRPQTNTdzlXRjdXTHZRNlVPNlBtYlpVTHBTCnhFbFhsNjR2Q3J3QWo4aXRQcHAyQVB2Uzg4T0w5Ukd1R1orZG1JME8yV2VIc2dsZjRHeW9acGgxdmkzOFcyQ2MKRFV4d1Z6cUdjVkVnK1U1aWs5UlB2VnhPVUlnUVFaY1U4aWhWL29LVWFuMkJ2K3dsUzFDY0M4Z1lpM2lPR0hLQgp0dFBYVFhnaWxGQ2ZmdjZMMmVPdFJkRGJSRUZIRER1U2VZUlF4ckRLajlHREtvYmVYYWNBakd3TWgvVzJYOVoyCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGV0Y0wzeDJkbzRBQTFHKzBCOUQKSzhMS1k1QjJvZ0J4RUZiRFd4NDZwNXdjbElHRzRLbjZzV2xpMnArUlNScm16dEF3cDN4cFdmVk1PUWM3VEFKUQp1dHRReGxZalR1SUxySDN1Y1pOU0U1Wmp6V1AxcTlkNlRja0FzcHlnMSsvS3NoY1VoNk04dE9Td0ZkN3RZcktaCktNUDc4K1c4RVJZNGhIVTkxYnZBbkhCSzFncE1tWGFzNHN6QzZrcERkaHVRNlEvVEl0bDFBY0dqSGEwbGE0dDgKdGQ2YnZKcnYydExGTGRIOXdBSkJ4Mk9RVXFtTTFaL0FJQ1FJSGZEdGJPNVpabHppMG5KR1Y0RTdobm4xWTBiTAo0c1NjTEQzWFp6VnppM3pLZlBwbWE0dDdYTkUyOEpVSnBVc09kMHpnN0JLTzdHK1RUUVdpRHMxRXB6Y0NubUkyClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmdBU0tDUkUvbEl2eXVGN0d3RkcKY2dtdkhmejhHN20vanFyTFRqU0pSbTRCTXFaQ09UNWx6OTNlT0VMWm11anU2eGdubXBkQlJ3NjR4OE9MRVExbgo0alZid3ZBa2NWTnpIeHY0dUpONWoySktZSVphZzVSRldaMzZuYVNuRGhta3BrSEtTaVArWXNycUJaU3dCVng2CitHdVp1NTVFdVhaazBBMUZHd2N1cHNXM29WK2VnaDZTbHY0b0FvSFE3MGNSUDY2U3dkc3p4OXgyS2tRUktPNkYKMUlaQXZmanIySEtzUDV4cHB3bjdwVENGcDJKb0dqV0MySkdScXJMU05LaHk0TGoreEprME9xbU9oZnVqRnAyVQpGYVYvRDFkMmVzejRIdW16cExLUVhldWFQKzBvK2NOMEZvc3lRVk5WbXpuUmMwVmNqTno0YnV1WGFYa2xtSHovCkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGdBUlMzK2d0RkVkb3BRMTdNOE8KbG1JOXJVSFc3S25kV0NxdVY1YlVJL2NHWk9BLzdNeGpuV0JOQkpFckFIV3RRdXBoR2R2Nmp2MmRuOTlQTDN5bwpUcFhWQTRIMnpRcVlXQlp4YkZTRG1FVWJtNW9vd2x2YUF6OUI2RmZwRE1iV0lEN1QyYWRyUHlWYXc5RUh5a1l0CmQ4ZlpBQzdMWlBXeFZicWI1M0FTL2RMUjI5NVZDZ3VXSWcxdzdQY3BUblp6ZUZ0VWJHczAvbnIxQmp1OTh6TG0KQmVYWGo1MXY0S1ZrVEFsUC8zdzkrSTh5UnplSjlZSFhNanpRcTN5ejNTbGZnWURzSFVwY1pnZHRENVpHR2FuVgpWcWdCV1ZFcm5wMGxuUWdKSWxmUXBod2pUU24rMjV1UnNlMk1XdnpkYjlWWHNzcDB1L1BNY0JlSXRHeGRlN3duCmJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMmF3ZDNqMmR5YWRFT2VpTUs3K1kKd0w4dzBmT0Y4V0gvUXYrV3JNZ3RNODVjS010cEZRUnFGb2NTUGxqQ21GdnY1YjRYdTBoZU4zYnlZakZNTFhVZgoySWRFZlVHOGtJdDI0OWNldnRETDYzWk0yRkk4aFpBR05GNWtWVE9yZ3REMzJMdXhnR09ZSmMxcm94TnJkbHN2CnlCRm5nbXJnR3FtdE1QcEV1SzRTb1R5QWM3RlVEMmtMdmNjVkNHVWt4MXlNU20zQy91S3Bod00vbmxESWw0akoKU0lKUlhJemNNUmlqSHZEcjJKUUJ0VEhwUVdoZy9LSkUxZ1R0V0hOTWcvYUV5ZHYzUkdaeGxMNXlUQkdwS0NpMAp5LzQ0VTVVR1Fmemxya2tCNE1vcHRqRERiWlBNck8rMTJsczBQanovbVpHRDFQRWxGSTdPVzZyMzFQQklsMXJGCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbzU5QzBoZlRDRGFsT245aXBFalIKbmU1ZHZPYVltOFVFSTgzN0tFcC9PQnZqejkxajlIcHcwUlQ4b0p5elZhS2E5dlNPRW9iN0xPemdaaVU3TWI4NwoyZ2UxMUl2ZVdXOWJVTW5xRHVsNkxRUmV0MFhVanI5aWhmODNxSEtPK0dIT3FKRmNpQVUzaGJ6OUJYcm9IR1l5Cllsa0N2cFZVSDZSbi9zYTMwdmJrd0g4NWlJSHFJQk5iVnpFMjZrZnZDaWViaEh5dXhhekxockd1VUJuSFBwSHQKTll0WnJMZkRQVWlwT1JNdjE3eFA0QjVaR0ZyekhCb2NaOGVTRkRTSmFPWWtpQ09KR0JRWW92bVcrMkZYcTNwQgpITXpMTFJLZUFYMGlzZ3dweHl4Snh5enFnb2tRZ1lyYjFRZHREb1EzQmRiZHcwTXJ0c25JNFhlL0t6QS80RCs2Cmd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBb3M0UVB6T3dJWUpEVkdmbXEyNU4KQlBzWWE5aTJ6L252RnNha3NJNjFOYU43N2c4c0l2Qk8wZ2hSSDR1OUxXU2crVDhZL0MwSkRDMzB6R2NIaCszUQo4WXJLN215b05ueUhkdDE1bS93UWxicnFJK0YxMDRIWHBQbExHRFJRcFZicnp0SFpSQWk4WHJhWDZybFcwOUhECnBuWHZJeVNLdnRUSlNZRUJFVEVCbmV4ZzEvSVhSUG9QTlBVZWRDQWpCY1UxSWw1MzU5dEJGanVGL2lXRmpFZ3YKMVAzaThXTGM4YWtONGJEYUVVZDFhZGtSRkdDVDZ1WnBIVkwzM2c3RDNScDJaS1lhWWJZUVVsckVOaGJRdzBDeQpYQytHNjFOci90K0c3SjJib3J0eExlVHEzellmMlRuMWdJUGVqZk45a1gxN2xENVNPcGpqK3hieUJVM0ltVXpUCmt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbmNVN0xXZ0JNcVJ1VDg5TzJrRU8KRk5TU2NPZWF5R0kyQ0s2M0VObmU5Z3p3eFZ2V1NQRHFQWnorQkJKUXBaQ0VvZFVjYmMrd0pXMWc3WVFWSTRFSApwK0VDaml0TmEyVDVpR3l1TDNXTHQyR2I1SjhJM29YdVRXNVpDOTFqY1VIVUQ5R0pldWp0NVQ5R2dRSWVhOUtyCmRFcEpYSWpMbVQ3eDJFNlZZUisrSmE1UDB1WmJGcSsxNUkzeXgwajArcm9pbkxhbkZvY0tLekQ5TWlsQ1lKQ0IKSW1GOEtFVnpGNGtHQzJXbld6Wk1sWWxleHRaVEUyMzNYam4reEJ2L05HSyt5NkVpWWJSQTZWZ3Zxc2NXelZWMgpFYkRkWS9tWFdsb05QRmFxeU5zNGFoMWdEVDhSekRRa0NsZ3Job3dZSXlOVm9aS1ZKdXhScDNXV2hVTEVHemV0CjdRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1oxNVQydHl4Yml4amloKzlrZ1UKV0lMYmRRTHUwZVVBV1BRa3JEa2NmWWh0QkU5Q2JJY3ZtZkpLS1ZyU2dWckFCVC9nMEEyU3M1bDlWWlpsY2o0bwp3WXhtU1JWaDZ1WTNzbVE3NldwdWdscEFPb0oyWUdja2kvd2krZEsvNTc0dWdRV3kvckNMSVg0dWRQQnI3cVFwCmZvdHUxTGNHb2pvcFBTaXBEQVhNR21zaTU5VTV2QzdNMUFoVzVldXVSTHBCYksxOEFwSWVRTUh6UE12akgvay8KK0pBWVpuY0xNZ21Qb21mMnVGeklsVCtISjRBcHRIUERqRUErdVRMT3hCTU9UM3hSeUJEUnkvOVZwa0RvWkFrMgpQSVVwUTZlSHpYM29oeGI5WVJSRkQ1dTJMOHN5VnV5bCtrSlpjWWR5WlphcGJJN05ISW5lcEpodXBhNms5Vk5QCjV3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMm5OcTZ2TnhxdEZnbUpIaWJPcFIKUW9YYnF6VGdSUFhHUHozN0w4ZG95NXN4Yy9YWldFYXg0bWg2Y3BCNkFZTWJVUHVxOVlYZ29odlhmZEN3ejdrQQpOZzBISngrUmU3VGs0YVp2VDJzUzg5MEp3UnNYS3lqNG0xSUJkbDVuMURnUTBSWGJNeks1ZjQ1RXN3a0VDdjF2CmgvM3B0MUM4b29maHdHcDJId0paMXc0SVZ3QzBwditDV28rSFc2cHczcDJyL092MGJXd1BJaUVpZi9mdVdBN2YKNkhaTDJ3WTZXTmRnL0VuRjZITnBYcmxuZ2pXYlZ5YURwQlJQTVBWMXFQQnNCNVkzZFFrRURPOFpKNEoyYnU3Ywp6NEdxZHJldzNBTWxydnhUSUd2UUloZEdsT3Z4R0pUVjB6c3g3MEx5cXdQRVNJU1VkdlZGSUxoSEZxd05IN3lGCnhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2tPbmpxVmZRZktuUUZLTFBzZjYKREprRUgvc1RUNEJva25sS0xBcUVUZzdweCs3TTUwaFdpQ081cnlXcEg0YUV0eU5LWmlCRTFLanZESVJ1MnVOcwpkcEtVb2poUXlReEZFeEVLbkxYNk1KWk85VWQzdjFEQjY4WTJSUjJmaVhmKzNsOHBOZTZTQ3h1ZkZONGxKZ21kCjBzVTBEUENTbHRTcEhRUXpVSEVmVjNYUEFuNlY1SjYyVVVTbDNKK3NEeDFoN25TT2VzZTltQUY5Y2UyNzArbkYKbWZYa1QyRS85RjZNMi9xb21BdE4zZFI2UnhOTEJhb3N2RmNLNkJobTY3T0dUOEEzN0ZOUzBKSW5sbG1mMnBIOQpHR014ODJxc3Q3MGcxdnNjdWVhYnhZSWJ1MVo0dUV3T0l5c2RnQnA5TW5TdFZxYzBwaVMxUm5OZjlDSGFNQ3J5CkJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBM1dnU1YxNTF1TDY2emxYdm03NnUKYTRzMTF2NHZ1UGs1QkwxU2IrbEFQenJtK1NUTWV0R3Y1bWdXSGZ4bzlBcU8xQm8vTFZaZGp0ZTVTaHFMTXE5OApCNkx1Q3ZzZ2ZGQVhmaVVOeGQ4NFhPV1pKTzl0VzEvcGpjRGNPWVBlUkdQcURmaENVamgvcUlDYVNIZ2Y2TExpCnUwL0R6V2V1b0pKZThVay85clFXbXRMMUV2WlZpeGdSOEt1dFVReld0dEljTVF2VVJoRWZUQ0cvRERya1VEbFMKVU1RTlBoNSt0MnV0dE5TSlJ5QnJhakwrSXZwaHNYaytVTnk4aUt1OHlSN3hBMGFLMldMK1JyWXFzUTlKMXZVegoxYnlqSFI4eGM0blJON2VqZTBoQjhsbml2L2xBa1RybTlJYmdxakNWODhaZEVXbVJjaHpKL01Qb2trQlMvNno1CmpRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWUxaW0vN29Ra3J4TnEvZEpIcE4KSHN1N3ZVckNlZWpmaXEraC9YVG9uVnNVckZERzlITlI1bU5yTmhaOU9WZTI2VlRaQ2J5a3pVQ00xMzZjbFhubwpoYmlGcFdDNG9IMER1ZWFnNGdrU0ZMYUwwckt5U0RqRUhtaGxKZ1NpZkJ2SjA5eG0vQ0VyeUZSRzZoK1QzSGc3CmNlc2V6RzY0UHpxTWNtSGtMeit6WUNXb3BjMXc3SitrZ1FKOTEvR0MzK3dkbm9XRGNHdlFsS2NPcko0b0JQSzAKQ21yY1cxc2xUM0FWcWIrNDNPMS9pSXArTjFPeS9KY1pBOXZFUWhYeGxRWnJ2cmFWaFlYSlQvaHI1SG4wbzUvWgo2akVsaHFVR3ZlZGxYNjZpVmxnNEN2aVMyWGRpV1RsRUJ3WTQzd2NZeXNTVWlwVWFmZWNWd2RHVGhkZnY4MlQ2CnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcFV1UVpLMVk4M2FCZ1FOSTRPb3kKQWlUSmU0S28rWlJUSjZaSkY3cW85UXprY1FUcTRzaXlOY2Jwei9HVzQ1VEYrVkFUQ3VURStQN2tESkNCZXYzNgpUYlkyRDlBck9tM3FmMnh2ayt4TGVXS0M3eXBFTGk4aGFZOGh6eGlmeTZqVEpTU05vUFllcEF6MmY1eTBWZC8vCnY0NmhsVkk4MHdSekNqbXNyeUVjdWNwVU83UVl4NmVLRXkwNkdmSW5XVFkxRFk4ejZWUG5IOG5kcnJvbys2MGUKVjdKdlQyYUdUT0RlNDZVa3ErcFVaVnBvUDhvclhXcERjM3ZjVjIrL21pT01SbUJMUU52VTVVcGdPZWkwZ3krVwowQUdwbjJuZXlCQWhvM3U4MWwydTRXa3YxNmVQdkp1VEpLckRTVnR2dUhwcEFuZTZocXNNQ3lSbUlMMEpQemJHCnp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGxEUGNvczlqclFtNU1EcnVpRVgKZ2JtUnRhbTFHVWFjWS9LZngwRVI4VnZwM2FkbTNVUVZ1Y3JweDZQdU91OEZDK0QvaUFmSXBzZlZJbFc0Skc2VgpaZVFhOGNTdjRmMVMvbTNTR2pXUHNFTUxPbFQ2cGdGVGhMQXc1UEdUK1Q2ampLVU1OS1hUa2NvWGhrNjNlYnZLClQzUnozZjQ1ZElCMkNyK0hGYjAxeStPZmcyWXJSWFc3V2ozU1YycHpybWZSTTBzY2htTThxUXhhZEZhMDVnVlAKWmRrQVBuTHlFajBsdXNLb0loODk5R3k0RGVIKzFTSHpmZ2hlMEpQZ1ZpeTluaUlzKzRnendTUXRuaHBoOUI4OQpyd0ZHU3ZINW1iWFBhRjgycGc4K0xrNzhuMHVUOFk4K0dER2R5NkhndzRLQWZvU3BIcks0SXpzcjVYNEsydkJVCnFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXlXZDM3a0Y4OEZGMk5ndW5uNSsKMFJLTUp1ejlMNTBQMGtGY0pEbmc0b1NOb29MUWduTXVkaUtuWU1jcFFQY2VSd25ZK1lOTjAvcGxSbHZDRDJVWAoycGtQSFFKcFZRVUdQWjM0eHBCQk5lSUMvWWMwZ2JDYlJCdG1qRzFCck02QjRNYXpTYzc4RDh3b3NlTTNGalhHClNVYTZhei9kZ1pmNkVxUDNaY3htUk1lSlNWT0FTcEVFYU1ta3JlWDBTbVp4MklSekgyeXprSUg1SEdKajA0MTUKWnprZTBveHk4WWYwMlU5UktJZzRWM3FyTVdQTEcxdmljRWNrbm9JM1RSajhCTW03b01iYlM0bGFUMFpiVWI4cAo3Q1V0Q3R1bi9mMC8zRFJNVGdjV3lPdEcrN0VGSXNYWk9aMTZXZDdvK1pDWWRrWkNrZnk4aDVGMFk1MG9vNjd0CjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEovUnFQcUVTb3FvaHdmRi9oaGUKYUJmMXB4SUhuUm1uR3BqeG9ZdXJGRU5LL2hFR05qTUVuT055WGRlVjRxUzZSbTBmR0s2THFQcWl0WFF0ckkvKwpxWFoxRk1xNm1kY0cvV0NKNktuTnJpSFFpM0x2WHM4UVdwcVBIbS9MM0tlT2JxcjNtY08vOXd3elBPTlYyZVRyCm5pdWVLbXA5S213VDdzRUNFTnRxK1lvU1F6ek5DZkpHbFJXYzNaR1VPRkVvalQ1cSttZ1BMbjFTeTdWSDdjVnoKTUM2UEQwYXlkTytVRlpVOERjSEZLY3F4RVhVMkgzaUJ2ZDNic2FvSWttUXcwY2E3b0F1cDR6WTM5OFN0Y1h2WQpmTVNMUXJ6eGxTOUR6N096YVFWQTdsTUUxUGRPVCtURG5qRHZoVS8vQUpTU3BGMGZaK2JQOTdEWG1LY2JsbWZmCllRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdDRhSmM5Nk91Q2dCYlRXTXAyTksKR01SdEJZY1J0eDR2NytzdjU5UzJpVVhXaE0veXBmSjZIdkdla1BuNzBhTEJqS1dSQWZqaDA3RUkwc2htSWVBZQpGOUxzSnkrYWx2UlRXdzZQTDlaUWg4akhmbVkzN29ZazNtclIrVWZwQnR0TFV5ZldFZjVVajJaN205K3JrQ1lmCmVMRkhDU2UvbENUSXpqU0dQcFV3ZzdDbmF2Y0JRRnJvYkg5VFVxMGpySnl5Nmo1bFZSRGtvS05wSGFYUnZZeU0KcmM1R2k3UXhXY0FhUGtOcW1BalI2UW5tbDdyNlNwVSthbHJMdXdaeEZvNTJjRTFsN1h2bE8xaXk0UDdaMTU4TgpmRXZGMkVwUmtsVmxTN2tNT3VvZEZCTXlxcWhWQm83em04QWZNYTNBcTZDZE1VWTNNUjl5alBwYm5VNjEyM0c2CmR3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBekd3aFg1d21iSHpuY1A3Zmt3LzYKeERMUmM3WENxSitIclZMTC9YWE9majdXVlB6OFFHWjllWlIzZ1lhN1UzTUVweUxid0ZuN1JPWXl5UW1MS25GNQpvYmJaWGc5dUNWUGlMN3d0N0lmam1kVnIwMmpYUW1oR3lGWEF1SC9PMGhORFdzdVAzUE1rS2tHdDdLWlRLR3N4CjZEcG5wTEU3V29sYnR2K0J2MG1MWk54dCtuYzV0SW1FRURsMDZBQkJ5eElwMnBWNmw3WDI4ZXlvNzduVmRuOTYKaG15OS9BUVJpSFVEeXdycVVHWmkxSmhtbWlKSXlhd2hhMlVMckhvbDBFKy9nSU10NkQzTEZMOTVUc0hMSEJPMwo5bEpob2gzRnVseTVIc3NoQmFhUm9jc09SYUhRYTYrNng3NUtuNHhZQmprNW9aNy9UbXQrVjF6QlhubXkza05tCm13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHhoVmwxanQzVDZrTldjeFFFdEUKeUc1ZUJ4Ny8vZmRlRXNQZlVhK1cvTHZFdzFHbFc4d2wwNUJFdHB3ZFhmME1WL2ErM2VpWHErQ05Vc2MrZktrMgp4T1lRR251UkVRRDFEQ3cwd0ZZSXBrd29kcmYyVWc0WkREQ29OQXVDUWpCWU1LMDdhWVpsOEpYeUdQbDJBOUJ2CmpNRU1JaW9JTXV0bzV3Tnd4K1FkblJiQ3JZbHB6Vys4RkZOT2psNjB3QWNLZXhXY1NaMXhPSENXVFBFdEdGQjYKMU0yUmgxbWtpMGY2ZVBjbzdOTGNKNytyWUxRdHlla0F5aTVQanNJOHovTndTVEFpTjRWblcrT3A3ZTFQaEhJOQpGdWRLWWI5VC93ci84TWZzQmN1ZFRLOEM4c2x1bldoUmVvcHQwMEt5VTMyRStNdnpWemdYRWR4T3lucG9GeFlBCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdkpZbHpSdGhKQXRHMUNiNTl4Y3oKUkE5UE1NbURoT1hrNURaQUszdWxTL25sNDIrbGJzd2kzRjN5Tk5ZalBoSVdJZ01NZEdrajVncEJrR1BHWEtadgpmczlUR3lUYjNKY043SS8zcklyQkxWU3pHOEtrT0RWc1FuOVBwTUo4TmtacmVXMDExaFMrdDRKeVNaWklFakhGClo0a2ltM2VsWlNHWVJkNVlFN1YvWW91S1RBWGI5MzhMazNWOU9aSzJtdG5EYjRPU1NZUHlEKzV1VTQ1Z09DcjcKSWgxZk9QMU9lZHNQdkZJS2xScURYb3dyM1dCZXphWW1HQjA0eHN1dmh4cGlvSzRsb1YwOENXSUM3WXRyaklQNQpRa0lFODc1d28rS1FYSVg1VWpRS2V5WFhoRGs5MHdQbkxSdXdDZEk1S1h0Q3ZjdnAwRkMrWDRZelJ0a3YxTU9jClBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdzFOUDlGWWFlOEFDYmRpWGt6MkEKUzNrWlkwWFNhSkl4LzI2cU15MFJTRjRFZ1AzdzZPTlBMUzNCaFRhc2crUTBrOHIxYlg5YkVWVXdtWDlkS0VrQwpLSkRVZHJFNFFuZEYyS00yK1JBb1lURkRsdzVOUkphK1VIaUt4aFk2YnZmZXNtYkR3N0MwaGh6d2FXSnFTV2FZCkd1ZDUzaUxnclZPOXV2RXh1U2E5dCtWZjhoY0JRd3A1Z0FWNmFmcGpvVm5pTlN3M3NMSm8zZEZiZmFUeG41dmsKSGQvS1FCeisxSXZYaG45dm82MEo2RExxVzhFMTJHb1ZQaE1abTIrTG5EQWc3NGY0MGFiYkZpMCt1MlZ4Mk1rdQpZWGNERjZ4NEFKUUJFdnlZM3U5eEFSRlVteWlmTTBFTitURlBKOTBVSHVmcXNUbGhLZXc0QmJxWVpQL0haRURjCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdXd3VTFXVVExOWxaR1JxMTc0d0IKWHF1K1VhcEh3cDhqZnhZU05wU1JEYTlvbzE0cE1heStWL0paeUlCakVyNUVzd2Y3cU10Z0VUc0o4TjBJbnFlQQo2bGs3L1BTblhkelpmQjVPNS9pMWN4Vkg3Qi9vb1dGRFBuZGtHSW5RMkYvc21Ha2E0cnAyUERRV2lua1VnV1dzCmZZRWZ3bXJDNlpNMm1OYVBVS1M3dFlHMnlIU0xKUU5qLzM1eDBvTWk2V29QYXFLYnpTbnpZSTBUYkxENi85Q24KbXJOL1dCQTBSNUV3djhhVlU2dGU3a2xscnNwUUh1OHMvV3RlenRJSnR6RWY1N3BOVi91Z1Zla2wvUUdTTGxaRQp6b1FlT0k0dTk5Q0JzRGNoaS9LV2lnZXE4dytuR1QxVVYvNmtTcWhUNHBTYUxtS2p3Sy9XZzBQbTNISlF6MkRWCnh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdnFPcG95SUVXU1AzaWRYeTNTV0gKWkNCUGE4clhsRno4TU5zYmJDNWJkb2dtbXU2eks3SzRCUzd1emR4OTRYNHRybXpoQ2tvdTBZZlR3MVdjdzBSNQptdEg3cXFQeWZPbVhzRGx6aDhlcnJrRTZGSkNENStVM2ozc0dJN1haWU12VUtZWFJyM0JTcXRjcE5sUFEyUzNzCjdOY1R6UEowNnNkanl6T0QwM0dwOVE0WW1HWmtrNXlhQ0hhUXhMWVZhbUNoN1BNRHJtTERCKzg1b0cyUzc2eUsKazlqd2pscmhIQmJlb2NnYU5Sa1VxZzQ2QmV3a1RoWjYzRmJkR0p2d3U2NGJGY2MwMm9mK29IUHZwYjhkdzJjNgpNZVhOUUY2S29UYk0rdGszNmVhd2dpRmhpM2ZvYy9ScThGTm9XSkJwNndEaVFTQ2J3NHNuQTkvSHk3ckdvNDBmCk13SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEZ2WTJjcEw2Y1MzRktsa1RwOEsKUlMrNWNISHpYMWxTYndJRXdSajUzbW1nTWE3SEV1Z1pIVEhpNW1IeXQvYzVEL3hrdy84bngyRjYveDIxR2ZIWgpnWWlwWmsrWHNBUGtGUUtMblZHS3ViVTF2YmJaWmRlUWxraVVmRWFWWklxTlRuU0xlT0dyWmd5N3VFbFFlaTliCnZBaGZiZGFNNmRySzViZmk5am41SkN5Y1JrQzRWUXA1QThyc3l5WDhnQ0cxdkNRR0VkSm90bCtNdW4xakJtY2gKOVVJRnUwYU1yRHB5YmxId3Y2aFFXME15ZnlLYjdVdGt5WU5SaFdQM0U0QlRFYnpIS20vczF2RW1JKzJRV0pWVwpDY3lRNEo4R2ZWZUFvOVg1YVFGZTFyZUJ6MG1RTHhOeWVkUmdVck9ZMHBwcGMxN0l6V1B1WS9JTjV3ZmhJK3UyCjhRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd2tvUDR1Ulk5NS9mQU5UamU3RFgKMnJTK290YUs3QkZNTERkZEpHMUFsdGRpL29UVUh6QXhEMWNoWUx3SFdXOWg2ZUZITlhoYjBMZDdHU1ZtaTVzWQoxakQzUG9iTTNKRnNqWThldDBoQlZqV0dwMDdMdUJ4cDZnVjBEZjVhZTVEWWpadjJQT0o2RDc0TXlKZG9SN0N0CndBYnZIRjhzNmdhNVFWNnZna05xT0ZJUzMrNTBhakExdXJ0djNnWUpTeFlUSFVkNDBRY1V4Y01TQ3BUSXhWRkIKR1BsL2dKbGlWZEJZeUJSR1dFOG5VdFRrbXVDU3lubzBHamxzNFJ0SS9EMEJJdEVpWVVNbFJWaElNclhabk0ySApFVTdUcFVtVmY2ZGIvZzlNekJ6NXN1Nm5hbWNmcEE3K1M0MHZFVURGOTZ2WTJmakhDUnZtOEtpWEtjdEVuSHVhCkp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNlYrOUpsaE5oOFJUWVNEaWRpSkMKTWNNY2ZFU1ljeXEzd2Nob1I2T2RNeXpWS1owbEdCT1psNk5oL1pUM2hpTWVvUVFDRE1XaTFubytUb0c3emkrdgpKZUFiTXdONXBBaTFMMVpZZ2VnTkNBa3owNDgyTUlWK0VCeEQ1aTV2TkRUeXhmOVU3Sy9jbEF5NUQzaHNHMlRBCndFMW1YR2NDR3l6dkVmTlg0Tk45eTZtOWt2c3lvSVU0ZVc0ZVpFd21sZVovZTIrczFjTDNkbWMwOWg0aWlsRmEKdlZ0TkRBWkJYYjd6MWtjV29kK2k4Yy9DMTlHOHVpeXJnOTltRVVidUxsak4yRllkTzRYMmVzMjlFa2FsT21OOQo4bEMwVnMxdjBtVVMyVjAxTGJyQVFzSlRHMlBiTlBheUZSeHRna3liYi9TMTkyK0NLNjRZQlJ0eHY3emFOQU9vClh3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbm5laU01c2lvZ0orVkdTL3JOa04KcnFpVGxiL051eHIyTmJxZWxqdzBsZHFIeCtwdVBHT2N0Q3BxanA1TW5QbXJ3V0Q5WE1WL1BQdWxMSlBmbWtLRQpKdjFXdG9kbHV6dGVJdmVOM041ZmRDN3VQUzRPR2dOMEZRcUpXYnVRS1Z6R2h0cU8vYVl1UzlhVzI4bUhRQkQzCldhcDRGczB0Ym9XTXBvazRob21xSEZ1WVZrSisvUitLRVFjN0V2UWJkdTRVODYrZFJKMDdxWW9yV0pOWXJjUTgKRERJQkFCeHFmVjZ3MGlXd2RJRXhmejYzK0hsaWpFVVJSVllXeWhycktNQjRjQzQzY3VHUmMvRk1wUUNlL09vSgpBbjB4N09wd2I4SFZCNTJ4Q2c2WVF0dzRseUpuckt1UTYvTkV3VWlnQWZZWi9jN1VLWWQzaElhb25qRUlaVHZCCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdHd1ViswemVvaWo2WkFBLzFjSFoKeVFaSDVIeEYwL0pVNVZobkdCRmtlZk1RdzRUZGR0Z082RW5YMG5TTDJJK0xPcE9jMElYNEdVM2ZLY3ZiZXJXMwpyQTlaNjVGTWk1K1ZzTFhST01Ud0pFYnI0RzJPUnJXVG9DcjR5OGFwdDc3MWJ2QXArMXkrMk5KWEZ2ZkJYcnBRCmVhZGlMQ01PVmIzeFY1eVlqY0JFajN6L3h1N0Ixb2NMekVSMXZMd3hIZ1JoWVhCcHBkTWovUm1pbGx6NDVGWVkKblZDWnFqYVZWK0Z2eVFLTHlBUHp1ZTJqc3dRMFU2eW56SkF3d2RCOC93dmpjY0pQOWY2OEU4ZjlZcWVYZE1QYgplTHBWVkVLUGlOM3dmOWxLbXhSc2N2a0pLaW02Ui96S0loN2tKbURMODJtQVZrQ1FJNVZVVDhUUEJUVmJ2RVRDCjF3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBN2pFb1dTZ3Ewc0JKOUw0dE1YM3YKcjI3dmc4T3ZWM1oyYXJ1Zi9WSE5JWE1sQ2Jjd1dqSkhCVVJmRXRWWjV3aGU1M2JodlZ1Q01pUTlaWHhrS0J1NwpHWVVtL1liQndQMkJzQnMremtVUFVoeWxJcXI1dHd4empVUm12dldhMGVweU9nVEcxVlYyaE16a3VtT1ZVMTVvCnZKeXlta3hLWkpZOU96YUZjZEFHWFNmUWtaYjRzZ0FxT3dtSHVIVjJuSW5KZVBlV3Z1ci9ka3pORjMvZ0lrUk4KcS91YWliRnl5alMvUkwrZ2F6Z1pTajA3SjdMTHJDWEtnZTErWHp1cm54bXE5ZUJkSlRGVHpwbTlZQWlYT2l2VQpBTm5kcFQ5dURLV1Z5RVArT281eXEvQzNoK0FYUlUvc25lam1kbnR4UWFuSlhvSG1XT2h3M2Q3c3puTVNUVEVJClN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbU1oOXYxbWlwSHZ4ZXptZGk1QmsKR0NDbGR3ZThnTWhHYTBpSUtiVFJjVDU1L0tmbUFsa2o1VzllTVJxOTB5NkhCc2dyYjBNNGtraTRwNE9ja2luYgp6ZFBGK2RSK0JYaktvNjU4MjhrYUJsUWtvM3kyWnRxVEdocEVSS25mcmpYa3Q5UVJkQ2hkdGQzUkt4WjVBSnBsCjc0WHROY016TFdkU1pacUdtOUIvQzFlSkEwcjhUbkY0R2tDVDBSUnMrbGRGMmRLNjBhcWpwMnFKN29MdFZvVDcKbFRzRUdZUC84azRkeGFzaDkrb044UTZ6Wnc3M2drbXhKOURhL21TUU5pWmxQanhpdUdVNWcrSHp0UGViVmFRcQo2Q0dXQStQS3l1eDdqQkhka2JKcEhUUmVyeXJrNFlLV2R6Mk9xbTJ4OG9BT21zZ29Ya0pYYjhsU1NVYWtaRmRVCkVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMFFLMXlYRjY0WTNtVm5HaTJ6V0wKWEsyMWlscE5OVWtYUGczVjYvdVVHcWMraHpDRG9rUEh4TURvcXdoNkp1emp3VkJzWXBSRHN3QVpRY1dWcWZMTwpQd2tRaWI0QlgxS0lLSmFxcjJPVVJPdFFId25EN1Jtem1DZFJwcFBrR3A0dlNya2FLR29rK29YMmJSRXhSTVdHClViUkFKYVZvdGw0bkpJOWdjTTg1NkVqY3RCcklnYk14USs4VEZCYTAxUUkvTk9SMGNvdThTNi9ubkZQSytRL3MKK0pUaDdDRERDSlhnRldoV2tFTEpBdnFvaXEwTEZHLzZBdktKS1FTNU5pU2pYNEhwRzc4aEw4TXVoQmJKc3E1MgprUFQ3T3NhdDRHQ0NUK0JjcjdiRi9VMXl1TW54TzVZck1MQnc4WjBWa3FtZUF1Mk9DazluOHZtcWE1RmNpM1ZqCmVRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnJ5V0VReDRyMFgzQldPcUpzMEsKeVd6VnRQdC9RL0xwUFQvczR5eUEzbFFyZDZKS2I1U2lza1RYZjFiZHpTRlQ3YUhUZ25kYXN2ZHNCUlpJcVlTagpXdjZwZmVJT3hpWkd6OEE1S2Z6N3dKKzc2Z2dvVm5qOFh3VGNJV2RrL1BGMXdnMjRGWFVPMUlSZDY1cnRYV09SCmw0MHB2UjFmbFJmNGVWZG9MWE1vYkdzVkp5djhkRVV2QW9KMmo5WmYyNWRSdWd2eEV0NzZYenErU1FiTllzVUcKaWVnNWFxNkxOYUo1ZXN5TmpCTStVZVF3QTdNSlp6enVCdjdwM1dNV2dMbUJwOVMzQ3dnUnVnK09vRFJwRHdKQgplOEo5UjQ0YzRQajhCMkFnUGJCV0FaOUl4RUM0LzREM0k1dnJNRktXVk1qY1RVcUQ4aE1OMGJoNnhhZHBieEpyCmlRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcnFRY0YzWGpKdEo3cWZGWW5yZkMKS2VaWkZqaFg1U2pQcGllSG9tVVF2ZXVObk1xN1RNd1krUkJrTGM1c3NFQjNINmp5VGxwOVhEVGU0dVo1RkYvMwpKbE9IdUdSV210bmQ4dHRDT2luVTJ1VFJIWkg2U3Z2b0lrSFhTbFplRTQ3TkxMb0w2OW1aT25oR2YwTFNxL3p5CmxoeHdZcktZZnBYaHpPSzUwbHk2eERFdEdBVmh0RkViNEE3OTZSMEExeklBSlltOFA2WmlNWU5ZL2tGTHdVYTUKcWt3SzcyaDVna3oxSWQ3bkZHbjFtL0VuMVlvakRlSUxtZDlXakt4ZzhiZ1NXK2g3d081aTJ4UUJNUjFKN2RvegpCc3FIN0VMdm82NkV0L3E4S2toT3B6ek5Ha0ZxQmxQNnMvVFdydk1UZ2lzZmEyWk9WcGV3TDJqUFRPQk5TY2txCk9RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMkxqeFcwbm5mZVd6ejA4cTRZdzEKb1dSSEpsZkV3bWdMekpQbXM3WG12R216U3dqaUpFZWRHY29jampZcWVwZm55TFZYaDZVcTc2aS9wZzRZczM0WQpsditvV0RicERZVlMzMy94eUVsNFp0ODVBOUNlQWlFVXp2NFpEVUw4K3o1UTZWUW9Sb2UrdGEvejFobUVIaGxaCnNMbUJOMUFnLzVRUjdJR0lsMVl3YnZrbDZEY2pOUjdrU1J5cGpvZUM4UmJ6ZXZVQ1BYU21qVDd1RkQ1dytUckcKREFiN0xNOUg0NGZCL1ZyQ2J4L3BtUXZKU2NpeGxjMTZTeFhTRHhqV2s0M09MVWlNb3JlK2VWSk56Yi84MmxmNgpSZk5LUUhnZlZhc040Y0J2SElKUEEwZlcrWDdmalA4VTVYQ0RQejN3dmd0bk1hYUpZMWhVVW1JYmpGT28yZmJrCkt3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMEowUnZSWHB6bE5nSjVIem10MlIKK1dtM1dlYnRBZVJRaENCUlZ6S0RKYjA5T1pLUTIrdFAxZSthTHI5VVF4aHVHaG5jMWhVMUFtWjFnVVJhVjNTRgp1MlZ3eStUVk05V1F5RTA1YUNhT0Jzc05OaGpyUk4wN2txUVVVRWpWb2lJbHU3NFFSb3Nzb1FCMXFUd3NpTmRHCjZYSVFMcFQrZEF6Z0taRThUZTd1WmxYbEg4eE5UL25hTitCQVIwVVFYa05lck1GSEVlcmF0WFZCQkR5aUU4d04KcnlTTTBVbjdnTkMzRDBYRUtKTmV2T3BWUzI5NytDWEF0NGlYWG1OTGZFbDY0Vll3TlNhK3NVeGhyUjl5aTgvSAo4ZzdGdFdXY21xY3dWQU1hdkRIbStPdWU1OC9CZU1nRUJWcnpxWjZZK3BwS0FNaklEQ1lLVWlpSVIzTkJnWUVoCkNRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcXExU202VEd2dmZjRmJuRlJsMFkKZG9jWW0yRUxtV3FDK01abjdvYzhvd2MrZnYrVy9CZ0xESkJHOFJFVlZBZkxGaTlaak05QnlURVA5NEZ0cmY3RAp1Nk5pbisxYitHS01vUjZNSVAvTXBGTkZIeU9nQU1xUElrWEZMNEJWYWlDZW8vUzFnN3F2UTB0YS91bGNKaWR5CjVQZ2xBQzMrRkF6SDYrOHVQcUdUaVJMSGgxSG1DdWpJR3V4bGlUc3VHNTM5VjZjQ3NCMkt6M1pIenZ6ZEk2cjYKUUJrKzJ5VkdZQWUyWHlPOFZFcXdFdVV5RTliN2ZNZjZDTUVQbEhVeTRFdXovTnVqVml4SEdRUW9GQWJXWnJQUQpvWWVNQS93VEl5RXZ0VmMrR0J3M0U4NjNlNWQ4VHhOMXBLelEzQjJqRURGVUo3L2VvZFZxTW00TWFyU3ZSMW1BCmJ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeWlEcE9MbG02R2haTFBHODZvakwKSFEvQ0t3MU9CSUhEaDVHSHpZTUc4bHJoUjdEZGlJd29tdWdwMlJKVktjUlpZTnhYR0ppM2xadWw1K1lMMEk4dAp2eUtWMmc2cWlnZXdUTnY5dEU1RXJlM2hlSVh4TEZ3YktaZnA2VG1nU0tHTUxnZGlUbnhIdndhSWFXdk5USjZpCmJZUWdmaEFxVUlSaUpjVXFnY0tnaVRjV2JKUFcydG5ZU1NmS3NsOC95NWEvNkJyMzBtRVRiS2dLSnBQclpGZ1MKTG1xUldrb1g0WHFiNnRiZ1Z5UzM0UVMxemxybW54aWZyaXUvRmFrK2g5WGZJTlJUK3FOWHcrUXZBcytGdUYyRgpNRVM3eExhTmxHTnFQY3hrV2NWd2VCTVFweTVFdEgxbHVvdFZBaWNNQUhoU2VZV09ZZWRCUUJEczBWVTJTTko2CjBRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcEttcloxeWNLd1ZNNFFTVzFRK3oKYmszNnkwbG1Bb2NncGk0ckxHMnd1RWI5bDJrUVA1UWZMbU5QakpSVTRIZjJyd29QaURXWlhGTWFmNnREbGVvbgpQd1NVMUVMZndnVENKQkpwUWx2THIyNlBLOVBNcXhCUE1XS0c4ZzQxT2lWbGhwT1BHVlEyQ2xNQXhuY0F3U0IxCnNJOWNtRnFNZVVpY0dYeTJRQWJITnRoZkxzWTFwQmNDdEV5SXlnYmpGMmtXK3hqbkNRd2VCSWhmSDQ4UUFUVW8KVzNTLzJ2Y1U3Tjh5d3h6YWRlRmFRMXJERS8yeTdmUzhjc3p4ck9BQ09FSmRPQ255dXhWTDhwMi9Ya2t0YjdVVwpCZFA0blFadjB3ZDRjVVhvOEtMQno5SkN5NlN4VUJZYnQrTllmVHZvSnd1b011OGd0a0MzZzJGYnB4UjN2ZzJyCnd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBNGlRdTRmS3RSb2xaNGh5UFNpRVYKVGM1QlQzZHJQZVNpWmdLa2FWZXp0OGl6NEpSSnVOTW80STJMaG0vN3ZSVTFnSVdUOTBhaCsrYUdEQlk1cUd4Mgp0Q1BZU20zZ0xXR1BRZlh6eUhyL1VObkFWTnJYQUZLd2xpd1ZocEhtTzR1a1BMeEp5K3BVV081YlZZSzYwNjlNCnFjZnB3MjJ2Z2d4YU9qV0FoTEU4K2UreTlGeGFTU3oyMHc1VzVCN0NVQVJ4OFNoUHgzbGNkMkhweTlzOGI4SUEKREU5SzVmWWtWd1VuK3E0bGc3cExlZUphZXFzN2UrWTZMVWVPNkNFK0JpdHZMRHFOSEpvT2Rpc0JrME1ZQkxldwpJOXByUWVqM0VsK2czTFZxbWFBRm1wRVhIZlV6TDRRaHlkUmE3cUZVUG5TanFBTjFGam9ycHZwejVmMXFzUmNMCkN3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMXlzMWs1aVVocytMOXRrenRZSHgKcTBTb1NXdU5ydHV4d0J3V2lpRURQdHd4ZWtRTXBUMURsL2xsTzkrLzhvYXNyajZqTUxnZmRWOW9wZTlTQS9hRQpPS01PdW9VZEVkQlo2SDFRK3ZaekJ3b1JucVdKbjhrWlBpRUJyQ2IrSlJQc0gvRE14L1BMRWd4WEMvYjErencxCmZjYUViL3NqNXhsODJqMzQwOSsyNkJqemNxNlA0RDltRXJnSFZ1UUpEMGc3VjhmUnBscVZVcEpPY1VpK2c4RncKQU5lWXVqQ3lMRHBYZTY4Zmh4bEJLRGV5TDdINnVER3BHbjRCMHFzTktkQk1nVENISHB2WStKR09lSVY0MHBiWApWcVVxYW5EbVU0L292VjVPMjhzOXE1MnJBaExvZ1Bjb2ljemJaUUI5U0w2VDJkNGU3b2hraDBva3ZiZmFzWk5ZCnJRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMG5jL1ZHK3lveWF2NlZPUXM3bncKS1ZZNVZLYzlNdmRJYmRNYWowQ3ppbldJNTN4ZmxYa21XVGJTVnE1aVlPbDFKMXRTeHk5QUxUZW5BbjZ3YldDdwpsM0RISjZMMWt0UTF6VHNwRkt0eXdCNGdqYXhZOHZMNW5tVm9vK2VVdmRCWnRlaDlLczl1SW90VUZZdkI4K1UrCmR1T3RlZ2x1Wm9KUXlXclZiQlArYXcyc3ZXVkllQm5yQWFxaVVSMDh6OEtibnZvUEZqWU4vbjQ5SFloeFozTGcKcFNTdWRTOHFhc2RkQ0dDVGNMT2VreS93OCtyZXQyQnJYV1U0RFE2QkcrSS9RVmxDVnRTU0d0NTV1TnFYVmZENwpMMjY0ZVpDc2MrY3gvNDB6dU1aK0hHTUFVQ1kyYW1KaHBldWh5Q2ZaaHlnMmVmZVdRSFdpMlRtT28zZXplQkNMCm53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc3lDOFFUVi9aTk0rU3FPRzN6anoKQ3lQMlBDeGhlNDdlN2V0TDJCdkd5VnB4SXpYVXRHdWVhZmFNL2ZpVXJwS2lHZ3pTaFBDWk92RjFyZTE3QjY5ZQpzZ2pkc2I1VXdHNDZDWjM1RENDM0pDb2t0WTFzQlBSK3Bya2NNeDNVN01KNERkdkdML3Q3UTd6NHd3VWhLWENICnFTTGRKenluMkZJWkF2QlQreTBZd052bU5JVDVKaS9BK2ZWWGl0V29tZWF6YjZNZ0M4UytHTTZiRUdrcDNxZSsKMFBiRDQzc3V6UFlzWHgvNW1wMFZUcWVhWWxnTHNzMEk2c25sY1MzNi9pVTg0TnIzamJZTUtjZFBhZGpBY2QvZgowaTFxcEx1WC84SGtwOUgyNkhKekdkVGE2b203MDJLQ3lZRnZnZm0xeDlaR0FFWkU4Mmh1TGFwd0lMVVI0YldHClp3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd0Y5T2dmWTh0Nk9YZ2F2dThHM0kKbld5NDFZOWZmeG1FQTMrNERkUWIxdEk0Zk1JTFFyS090V042amQwNDFybythUFNqNEVoNDJYRjBlcFNld1BVeAp6Yi94YXMxbi84YllDRGhsSElUYjIzVWJOV2hIYVZ2UU1tQ1pSYTFJcGIzQk1wSlJTLzJGaGpqMUlhZGtUY3IwClg3dkprYTJSRGEyZ1A5bmhDVkl4YUxCdExUOFVURjJEaUtLZEtYMjlkbkh3dmFFaldtTGpENHhoVzFvak83Q1IKbEQ1YjBMMUNiUTIxeVkvTG5tTlBzeHJpcHV1aTRCZmFVVVgrQWU0TW04UWUxN3ZjZkMwYWpKRVVrV1g4WWFFdApwRXNDcUhzVHJXN0tLeU5uNjFkb3NGZk1zeUh0dVlGWEYrUjRNV1FuQmRvNjdNWlptajU4dVR5Z2FLUUVZYTZkCjl3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBckcyc2NqM3h4U29MNHJwMmRnb0kKNytjQjRsbmM4YWJWS2JCcmp3MVU3U3NkcXNWKyt0WCtwMFplK0oyeGZtV1Y0d0c4cUNCb09QVWhPb1ZFclBzWQpwNy9FYXRiaG9vUm1JOUVMZHZoaXNRWHAyaWVEWjBSUkRFWnBjR1VxWTZPUmxWTUJmT2ZVeDIzRW5aS2lVdzY4ClhXYmR6dUlGTXBnVXZVWkw2cVhhckRKYkpiWngxNVZLZ1NJa0Q3bGg5QlFMbkVFR1hocnpTN3FBdno0bXJCWUEKMWpSb2k5aXhtR0szRkNHOXVWZU9pVEE3L0RxbXJ5SXdpSVJTbDVsZnlOL2UrZ0xkRDV4bnlBVk9WT0p5Um9MdwoyNkpLR1h0ZFNvZ3MxdHpWdnNVblVUMzZhZEpIQjduMFpqUmpEY2hPdUVVNllISnpDQS9qVVdvSjd5VTNCQ3VKCk53SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcS84c21CMm02amVvajVNK3NXVFUKY2JDclhqK1NRaDRLWXM2RjA5RnBPQXl5cnpvNmJNOG1JbTZvUDI4WHhlaWhHOXk2b0prUGlWdytSTDVCQ3Q0MAprc2I0eGZ4VzBaSmdCNU83MlptVUtIeGgvN0JuKzlOMkhOTEpLRjY0LzdjYzhOMU0zZ0RaZmFiRkVhYVJhU243CmF5QzdDOE5aL240ZXRXcmVncERHSldmVHl5RGE5VjRzeHdoUEFoclYxZ1hLUC9UQjQwNXhCRi82c2JEZmpQdWMKdkpkMThhTTZHanFJV3JCc25DeWFpNEkxd3pBQStCZGF6MHBWKy9RTVlOa2JRakR5SkNER29GbjB2YThreWhFeQpTOVc2WCszcndnNVlpS1BpL082eVJNSmJuN2tJaWR4eUJYRU9NY01ua2J0VzZDa04weUxjSGl6N2xqbGpUd3BCCjd3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdWUraSt6aWx5MmF2MXlnQVFTNFcKMkVka3lLUzhaUmR4eXhRaUpjYXJkcEgyTXJBUUxvNlN5UkNKTDBhQ243d1YrcytVVnQ1Tmt2cnByY2VCeHBXTQowWkhmdGxaUlM5bG4zUGh0UWNKTlU1YjAzdTBSODZwbERFYWljSkprY2FpUXZKZjBZMElDQ2NLbjNwQktmY3dNCkplSHE2aFY5Q0FRZHVuSENQS0ZFZW5xNDFIaTh2SGlnVERVZXQzK2M0WVdFVzl0dWxEOFlNVmZCUTh3OTlHWVUKczlUOURSbHNhTzlndkdqNENyTEl5aCthdkFEa25oc0hTUFJOWS9sN1cybi91b3g4MlZmZ2p2blBkalkwSnVGQwovWUxKZjF0UGI2K3pjaXZ2QTJ0YjV1bTNsRHlJRWVoNVNuZEIxVUJxbDZmVHJ2aEdtenRaaWdCOGl2WlJGTElpCjFRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeGxKbGphMm1UTkN6c1AweDYwaTUKdGltMGI3b0NxRGF3L1BXRzlPWHZmRHhRaURyMVNIbE1WMVFTeGxkWmMwNGMwV2kzQkYvL0FqYXpzcHh3RytPYgpCY0RrNkdpcGJCZHNwYVFDellFNVh6dTQ3SDRIN0d5R2hqS3RPbzVrRjIzcTM0SDB3VEs4b01EMHIrVG5jTE8yCklTMHJ0R0Vsdi9VN0JpNjRoMjBoNnZNVkFTRWw0N0ZXYkZubUlOYytVTHJuSGVMWXpEamRQVXhkSisySFdvT2wKbGZBVXZ2M2VKaUJxV2JrTzZZNy9rc3BnSlhYbklLRFgzWlB1WlNzaGVHNGg2SnRtZHhKaFFmU2p2Nk8yNGRBTwpncDJpckcrT3FBWENvNnExd1BhSFA0YzVkMjgzekx2ZzVaUnh1RjhVZFFaV25GUnZmYy9FQjJEc3hEWGtIM2lLClZRSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - }, - { - "operatorKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBc1hBK1kyWXc1aXJHc0phNjI0elcKcnRLVHZqT3VJWTQ0dWdNc2lzbWNSMUVqNDd2S3NxNWhDVjV2NWhRWGowL0MxOHRNbS9VUmRZZU1KTUd1M2s3ZQp2ZFVTUjZjeU9EWTAzVWt0MTRSOXpDMFVoenJhaXg0V2VUUk5tVjZ5MFZGM3UxYmpMVGVZNDJ6Q3ZyVnk4SVQ4CmFZYUlwaEdXNjR1VEczNDNRSjZkaTFjMi9KN3RWeE9jcmxyVDFaRUxDNjhFYjh0QjgwbzlENXVuc25HdmdoKzkKN0FFMER5TkVNd0ZTeS94RG5hRXdUOUlXNjRwblBPZFRIVDZxb0k3MkZoSE9mdHVSZ2c0WEw4ZlhZZ3hMZEY1awp4SksybkZrL1NXWDFybzJrRk1WMnRpOHRZdDhiTVdJZEUzNU9wcll6RXMySWlORWNJR1l6N1N4L0tTNDI3enoxCnZ3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K" - } -] diff --git a/test/helpers/json/validatorKeys.json b/test/helpers/json/validatorKeys.json deleted file mode 100644 index e3b2a544f..000000000 --- a/test/helpers/json/validatorKeys.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "id": 1, - "privateKey": "0x63bc15d14d1460491535700fa2b6ac8873e1ede401cfc46e0c5ce77f00633d29", - "publicKey": "0xa063fa1434f4ae9bb63488cd79e2f76dea59e0e2d6cdec7236c2bb49ffb37da37cb7966be74eca5a171f659fee7bc501" - }, - { - "id": 2, - "privateKey": "0x67a2a67dc439e87566271688576dd430bc2f5a86f5e90850610b21b243490e5f", - "publicKey": "0x821b022611c3cdea28669683ec80a930533633fe7b3489d70fdacf68044661ee2bca1d17d3d095c05f639ebe3108784c" - }, - { - "id": 3, - "privateKey": "0x194a3ffdd039f6ae995a3f2c2de008d770d50f8972c07a477a6e885a353ed4f6", - "publicKey": "0x88ab00343b787f87de60d1e8a552a69ab5fb3525128c53d68e78a3fe2e157bcce75e96a87e8968460087927552a3c891" - }, - { - "id": 4, - "privateKey": "0x52e4bdbdedd85e9e8a77c2f60d083a0693d6f7b2bd717f642a02514528db4278", - "publicKey": "0x9150572051c3496a67207b4caa371dfba34f127318a7aef145ebdba6e0de506c292af31e20831b0c537ab7478508d3e9" - }, - { - "id": 5, - "privateKey": "0x577f673585c58ca8a105c83bf2882e46e1c4e1cfd6d850e5fbe34991da6c2db5", - "publicKey": "0x96a561928f5f54b9d114423543af93ac3c33a30f73797476c1c7f4ee5ab2cea79180c3cea460285f24177e89dcab8d9b" - }, - { - "id": 6, - "privateKey": "0x37dd327f92e790d26cb6e5b8d33076956dcb6126b9636f66e4982a1d3f122541", - "publicKey": "0x93ccd3ae7289abbac58037ac653c1a0b5cc999262da8da8f1a3547aec640267badfdf572aa93db40cab284268a7e76aa" - }, - { - "id": 7, - "privateKey": "0x68fa98ec09059c8e1b2b19e77980ee08a687da0836a0ccf6f662a5bf5340e98a", - "publicKey": "0x8871b2b0d32b6095ec0e55cece80527a813361d4b888e5da23b7c7178ceced7170cba917bb9edb225ebadd0dec649a95" - }, - { - "id": 8, - "privateKey": "0x5ee465c57bd0424b50a60c384627691e7dc3669a9f4cf27dffa375dd1edf7533", - "publicKey": "0x97d81ef3de604273711521bc780ef88ad510b8d3112c96fab917c7a060fa89b3d489a84e1050038f76ddbd2a23315fdd" - }, - { - "id": 9, - "privateKey": "0x696246c682a90413eab2a138f9e1d30aebd580332430b31b6614ee02d8cf97ed", - "publicKey": "0x8941eb35ff2eb3775bb202dc77034704e29f936823fbd615a9224ebb97cfff8a18cb4448c429669e29d6f12c74d8e684" - }, - { - "id": 10, - "privateKey": "0x4bee4207ab3d375085bfb194a333bccc9eb5bb15ff84e134f23626176013b18b", - "publicKey": "0xb2a9e7d7fbad825b30de6b5a9fd8ff5c0604ba0719bd188d5606bc34ac1046dd9dc1aee6b3ca823b1da2897f965ced49" - }, - { - "id": 11, - "privateKey": "0x687d72d8ac9f8ccab3c7145701d651a919c2ef9c65e8508370b82af11fe8bb60", - "publicKey": "0xa750714a3b02a92070995ac3094b16ff8e025fafc156dce53babecf03df4e62dbe4da9edad802bf0afdbd24742312ab2" - }, - { - "id": 12, - "privateKey": "0x040ad8e3dfcda106f9a5772084aebc417ab83632973b609904f07943d75f87e5", - "publicKey": "0xaec659ca36bb2fef6770144adb1411f7348fc7171b6c65dd276dac806c643e180160c1da521b65a5b7ea9da9f0704a30" - } -] \ No newline at end of file diff --git a/test/helpers/keys.ts b/test/helpers/keys.ts new file mode 100644 index 000000000..aa6e78b27 --- /dev/null +++ b/test/helpers/keys.ts @@ -0,0 +1,26 @@ +export function makePublicKey(seed: number): string { + return `0x${seed.toString(16).padStart(96, "0")}`; +} + +export function makePublicKeys(count: number, start = 1): string[] { + return Array.from({ length: count }, (_, i) => makePublicKey(start + i)); +} + +export function makeOperatorKey(seed: number): string { + return `0x${(seed + 1000).toString(16).padStart(96, "0")}`; +} + +export function makeArrayOfKeysAndShares(initialSeed: number, amount: number): { + keys: string[]; + shares: string[]; +} { + const keys: string[] = []; + const shares: string[] = []; + + for (let i = initialSeed; i < amount; i++) { + keys.push(`0x${i.toString(16).padStart(96, "0")}`); + shares.push("0x1234"); + } + + return { keys, shares }; +} diff --git a/test/helpers/migration.ts b/test/helpers/migration.ts new file mode 100644 index 000000000..144514ffe --- /dev/null +++ b/test/helpers/migration.ts @@ -0,0 +1,24 @@ +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { upgradeToStakingVersion } from "../setup/fixtures.ts"; +import { getCurrentClusterState, makePublicKey, registerOperatorsSSV, whitelistAddresses } from "./index.ts"; +import { DEFAULT_SHARES, EMPTY_CLUSTER, TOKEN_REGISTER_AMOUNT } from "../common/constants.ts"; + +export async function setupLegacyClusterAndUpgrade( + connection: NetworkConnection<"generic">, + operatorOwner: HardhatEthersSigner, + clusterOwner: HardhatEthersSigner, + fixtureLoader: () => Promise<{ network: any; views: any; ssvToken: any }>, +) { + const { network, views, ssvToken } = await fixtureLoader(); + await ssvToken.mint(clusterOwner.address, TOKEN_REGISTER_AMOUNT); + await ssvToken.connect(clusterOwner).approve(await network.getAddress(), TOKEN_REGISTER_AMOUNT); + const operatorIds = await registerOperatorsSSV(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(123), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER, + ); + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const { newNetwork, newViews } = await upgradeToStakingVersion(connection, network, views); + return { network, newNetwork, newViews, ssvToken, operatorIds, cluster }; +} diff --git a/test/helpers/multisig.ts b/test/helpers/multisig.ts new file mode 100644 index 000000000..fb0ce037e --- /dev/null +++ b/test/helpers/multisig.ts @@ -0,0 +1,17 @@ +import type { BaseContract, ContractTransactionResponse } from "ethers"; + +export async function deployMultisig(ethers: any): Promise { + const multisig = await ethers.deployContract("MockMultisig"); + await multisig.waitForDeployment(); + return multisig; +} + +export async function multisigExec( + multisig: any, + target: BaseContract, + method: string, + args: any[] = [], +): Promise { + const data = target.interface.encodeFunctionData(method, args); + return multisig.exec(await target.getAddress(), data); +} diff --git a/test/helpers/operator.ts b/test/helpers/operator.ts new file mode 100644 index 000000000..72fc6c369 --- /dev/null +++ b/test/helpers/operator.ts @@ -0,0 +1,142 @@ +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import type { SSVNetwork, SSVNetworkViews } from '../../types/ethers-contracts/index.js'; +import type { Cluster, OperatorTuple } from '../common/types.ts'; +import { BPS_DENOMINATOR, DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, EMPTY_CLUSTER, MINIMAL_OPERATOR_ETH_FEE, MINIMAL_OPERATOR_FEE_SSV, OPERATOR_FEE_PRECISION, } from '../common/constants.ts'; +import { makePublicKey, makeOperatorKey } from './keys.ts'; +import { getCurrentClusterState } from './cluster.ts'; +import { setAccountBalance } from './blocks.ts'; + +export async function registerOperators(network: any, owner: any, count: number): Promise { + const operatorIds: number[] = []; + for (let i = 0; i < count; i += 1) { + const expectedId = await network.connect(owner).registerOperator.staticCall(makeOperatorKey(i + 1), MINIMAL_OPERATOR_ETH_FEE, true); + const tx = await network + .connect(owner) + .registerOperator(makeOperatorKey(i + 1), MINIMAL_OPERATOR_ETH_FEE, true); + await tx.wait(); + operatorIds.push(expectedId); + } + return operatorIds; +} + +export async function registerOperatorsSSV(network: any, owner: any, count: number): Promise { + const operatorIds: number[] = []; + for (let i = 0; i < count; i += 1) { + const expectedId = await network.connect(owner).registerOperator.staticCall(makeOperatorKey(i + 1), MINIMAL_OPERATOR_FEE_SSV, true); + const tx = await network + .connect(owner) + .registerOperator(makeOperatorKey(i + 1), MINIMAL_OPERATOR_FEE_SSV, true); + await tx.wait(); + operatorIds.push(expectedId); + } + return operatorIds; +} + +export async function whitelistAddresses(network: any, signer: HardhatEthersSigner, operators: number[], addresses: string[]): Promise { + const tx = await network.connect(signer).setOperatorsWhitelists(operators, addresses); + await tx.wait(); +} + +type OperatorFeeViews = Pick; +export async function getOperatorFeeBounds(views: OperatorFeeViews, operatorId: bigint): Promise<{ + currentRaw: bigint; + maxOperatorRaw: bigint; + maxAllowedRaw: bigint; +}> { + const currentFee = await views.getOperatorFee(operatorId); + const maxOperatorFee = await views.getMaximumOperatorFee(); + const increaseLimitBps = await views.getOperatorFeeIncreaseLimit(); + const currentRaw = currentFee / OPERATOR_FEE_PRECISION; + const maxOperatorRaw = maxOperatorFee / OPERATOR_FEE_PRECISION; + const maxAllowedRaw = (currentRaw * (BPS_DENOMINATOR + increaseLimitBps) + (BPS_DENOMINATOR - 1n)) / BPS_DENOMINATOR; + return { currentRaw, maxOperatorRaw, maxAllowedRaw }; +} + +export async function getValidOperatorFeeIncrease(views: OperatorFeeViews, operatorId: bigint): Promise { + const { currentRaw, maxOperatorRaw, maxAllowedRaw } = await getOperatorFeeBounds(views, operatorId); + const upperRaw = maxAllowedRaw < maxOperatorRaw ? maxAllowedRaw : maxOperatorRaw; + if (upperRaw <= currentRaw) { + throw new Error("No valid fee increase available for current fork configuration"); + } + return upperRaw * OPERATOR_FEE_PRECISION; +} + +export async function getFeeAboveIncreaseLimit(views: OperatorFeeViews, operatorId: bigint): Promise { + const { maxOperatorRaw, maxAllowedRaw } = await getOperatorFeeBounds(views, operatorId); + const candidateRaw = maxAllowedRaw + 1n; + if (candidateRaw > maxOperatorRaw) { + throw new Error("Cannot construct FeeExceedsIncreaseLimit case without hitting FeeTooHigh first"); + } + return candidateRaw * OPERATOR_FEE_PRECISION; +} + +export async function calculateInitialBurnRate(views: SSVNetworkViews, operatorIds: number[] | bigint[], cluster: Cluster): Promise { + let operatorsFee: bigint = 0n; + const len: number = operatorIds.length; + for (let i: number = 0; i < len; ++i) { + const op: OperatorTuple = await views.getOperatorById(BigInt(operatorIds[i])); + operatorsFee += BigInt(op[1].toString()); + } + const networkFee: bigint = BigInt((await views.getNetworkFee()).toString()); + const vUnits: bigint = BigInt(cluster.validatorCount.toString()) * BPS_DENOMINATOR; + const units: bigint = vUnits / BPS_DENOMINATOR; + return (networkFee + operatorsFee) * units; +} + +export async function seedOperatorWithETHBalance( + networkHelpers: any, + connection: any, + operators: any, + operatorId: number, + ethSnapshotBalance: bigint, +): Promise { + const harnessAddress = await operators.getAddress(); + await networkHelpers.setBalance(harnessAddress, connection.ethers.parseEther("1000")); + await operators.mockSetOperatorBalances(operatorId, Number(ethSnapshotBalance), 0); +} + +export async function registerDefaultCluster(connection: any, network: SSVNetwork, views: SSVNetworkViews, operatorOwner: HardhatEthersSigner, clusterOwner: HardhatEthersSigner): Promise<{ + cluster: Cluster; + validatorKey: string; + operatorIds: number[]; + receiptRegister: any; +}> { + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await setAccountBalance(connection.ethers.provider, clusterOwner.address, (DEFAULT_ETH_REGISTER_VALUE + 10n ** 18n)); + const tx = await network.connect(clusterOwner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE }); + const receiptRegister = await tx.wait(); + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + return { cluster, validatorKey, operatorIds, receiptRegister }; +} + +export async function registerDefaultClusters(connection: any, network: SSVNetwork, operatorIds: number[], operatorOwner: HardhatEthersSigner, n: number): Promise<{ + clusters: Array<{ + owner: HardhatEthersSigner; + cluster: Cluster; + validatorKey: string; + }>; + operatorIds: number[]; +}> { + const allSigners: HardhatEthersSigner[] = await connection.ethers.getSigners(); + const clusterOwners: HardhatEthersSigner[] = allSigners.slice(5, 5 + n); + if (clusterOwners.length < n) { + throw new Error(`Not enough signers available for ${n} clusters`); + } + const ownerAddresses = clusterOwners.map(owner => owner.address); + await whitelistAddresses(network, operatorOwner, operatorIds, ownerAddresses); + const results: Array<{ + owner: HardhatEthersSigner; + cluster: Cluster; + validatorKey: string; + }> = []; + for (let i = 0; i < n; i++) { + const owner = clusterOwners[i]; + const validatorKey = makePublicKey(i + 1); + await network.connect(owner).registerValidator(validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE }); + const cluster = await getCurrentClusterState(connection, network, owner.address, operatorIds); + results.push({ owner, cluster, validatorKey }); + } + return { clusters: results, operatorIds }; +} diff --git a/test/helpers/oracle.ts b/test/helpers/oracle.ts new file mode 100644 index 000000000..5a69c25fc --- /dev/null +++ b/test/helpers/oracle.ts @@ -0,0 +1,175 @@ +import { ethers } from "ethers"; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import type { SSVNetwork } from '../../types/ethers-contracts/index.js'; +import type { Cluster } from '../common/types.ts'; +import { STAKE_AMOUNT } from '../common/constants.ts'; +import { parseClusterFromEvent } from './cluster.ts'; +import { Events } from '../common/events.ts'; + +export function computeClusterId(ownerAddress: string, operatorIds: (number | bigint)[]): string { + return ethers.keccak256(ethers.solidityPacked(["address", "uint64[]"], [ownerAddress, operatorIds])); +} + +export function computeEBRoot(clusterId: string, effectiveBalance: number): string { + const coder = ethers.AbiCoder.defaultAbiCoder(); + const innerHash = ethers.keccak256(coder.encode(["bytes32", "uint32"], [clusterId, effectiveBalance])); + return ethers.keccak256(ethers.solidityPacked(["bytes32"], [innerHash])); +} + +export async function setupOracles(network: any, ssvToken: any, staker: any, oracles: any[]): Promise { + for (let i = 0; i < oracles.length; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); +} + +export async function commitEBRoot(network: any, root: string, blockNum: number, oracles: any[]): Promise { + if (oracles.length < 3) { + throw new Error("commitEBRoot requires at least 3 oracle signers for quorum"); + } + await network.connect(oracles[0]).commitRoot(root, blockNum); + await network.connect(oracles[1]).commitRoot(root, blockNum); + const tx = await network.connect(oracles[2]).commitRoot(root, blockNum); + return tx.wait(); +} + +export function generateMerkleForClusterEB(connection: any, entries: { + clusterId: string; + effectiveBalance: number; +}[]): { + root: string; + proofs: Record; +} { + if (entries.length === 0) { + return { root: connection.ethers.ZeroHash, proofs: {} }; + } + const leafMap = new Map(); + const leaves: string[] = []; + for (const { clusterId, effectiveBalance } of entries) { + const encoded = connection.ethers.AbiCoder.defaultAbiCoder().encode(["bytes32", "uint32"], [clusterId, effectiveBalance]); + const innerHash = connection.ethers.keccak256(encoded); + const leaf = connection.ethers.keccak256(innerHash); + leaves.push(leaf); + leafMap.set(clusterId, leaf); + } + leaves.sort((a, b) => (BigInt(a) < BigInt(b) ? -1 : BigInt(a) > BigInt(b) ? 1 : 0)); + let layer = leaves.slice(); + const layers: string[][] = [layer]; + while (layer.length > 1) { + const nextLayer: string[] = []; + for (let i = 0; i < layer.length; i += 2) { + const left = layer[i]; + const right = i + 1 < layer.length ? layer[i + 1] : left; + const parent = BigInt(left) < BigInt(right) + ? connection.ethers.keccak256(connection.ethers.concat([left, right])) + : connection.ethers.keccak256(connection.ethers.concat([right, left])); + nextLayer.push(parent); + } + layer = nextLayer; + layers.push(layer); + } + const root = layer[0] ?? connection.ethers.ZeroHash; + const proofs: Record = {}; + for (const { clusterId } of entries) { + const leaf = leafMap.get(clusterId)!; + let idx = leaves.indexOf(leaf); + const proof: string[] = []; + for (let level = 0; level < layers.length - 1; level++) { + const isLeft = idx % 2 === 0; + const siblingIdx = isLeft ? idx + 1 : idx - 1; + if (siblingIdx < layers[level].length) { + proof.push(layers[level][siblingIdx]); + } else { + proof.push(layers[level][idx]); + } + idx = Math.floor(idx / 2); + } + proofs[clusterId] = proof; + } + return { root, proofs }; +} + +export function buildEBMerkleForDefaultClusters(connection: any, registered: { + clusters: Array<{ + owner: HardhatEthersSigner; + cluster: Cluster; + validatorKey: string; + }>; + operatorIds: number[]; +}, effectiveBalance: number): { + root: string; + proofsByOwner: Record; +} { + const { clusters, operatorIds } = registered; + const entries = clusters.map(({ owner }) => { + const clusterId = connection.ethers.keccak256(connection.ethers.solidityPacked(["address", "uint64[]"], [owner.address, operatorIds])); + return { clusterId, effectiveBalance }; + }); + const { root, proofs: rawProofs } = generateMerkleForClusterEB(connection, entries); + const proofsByOwner: Record = {}; + clusters.forEach((info, i) => { + const clusterId = entries[i].clusterId; + proofsByOwner[info.owner.address] = { + proof: rawProofs[clusterId], + cluster: info.cluster, + clusterId, + }; + }); + return { root, proofsByOwner }; +} + +export async function mockEBAndUpdate(clusters: any, ownerAddress: string, operatorIds: bigint[], cluster: any, effectiveBalance: number, blockNum: number): Promise<{ + cluster: Cluster; + block: bigint; +}> { + const clusterId = computeClusterId(ownerAddress, operatorIds); + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + const tx = await clusters.updateClusterBalance(blockNum, ownerAddress, operatorIds, cluster, effectiveBalance, []); + const receipt = await tx.wait(); + return { + cluster: parseClusterFromEvent(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED), + block: BigInt(receipt!.blockNumber), + }; +} + +export async function updateClusterBalancesForDefaultClusters(network: SSVNetwork, registered: { + clusters: Array<{ + owner: HardhatEthersSigner; + cluster: Cluster; + validatorKey: string; + }>; + operatorIds: number[]; +}, merkleData: { + root: string; + proofsByOwner: Record; +}, blockNum: number, effectiveBalance: number, selectedOwners?: string[]): Promise { + const ownersToUpdate = selectedOwners ?? Object.keys(merkleData.proofsByOwner); + const operatorIdsBigInt = registered.operatorIds.map(id => BigInt(id)); + for (const ownerAddr of ownersToUpdate) { + const { proof, cluster } = merkleData.proofsByOwner[ownerAddr]; + const clusterStruct = { + validatorCount: Number(cluster.validatorCount), + networkFeeIndex: BigInt(cluster.networkFeeIndex), + index: BigInt(cluster.index), + active: cluster.active, + balance: BigInt(cluster.balance), + }; + const tx = await network.updateClusterBalance(blockNum, ownerAddr, operatorIdsBigInt, clusterStruct, effectiveBalance, proof); + await tx.wait(); + } +} diff --git a/test/helpers/staking.ts b/test/helpers/staking.ts new file mode 100644 index 000000000..a3c0d7cc7 --- /dev/null +++ b/test/helpers/staking.ts @@ -0,0 +1,4 @@ +export async function approveAndStake(staking: any, ssvToken: any, amount: bigint): Promise { + await ssvToken.approve(await staking.getAddress(), amount); + await staking.stake(amount); +} diff --git a/test/helpers/types.ts b/test/helpers/types.ts deleted file mode 100644 index cec7864be..000000000 --- a/test/helpers/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -export type Validator = { - id: number; - privateKey: string; - publicKey: string; -}; - -export type Operator = { - id: number; - operatorKey: string; - publicKey: string; -}; - -export type SSVConfig = { - initialVersion: string, - operatorMaxFeeIncrease: number, - declareOperatorFeePeriod: number, - executeOperatorFeePeriod: number, - minimalOperatorFee: BigInt, - minimalBlocksBeforeLiquidation: number, - minimumLiquidationCollateral: number, - validatorsPerOperatorLimit: number, - maximumOperatorFee: BigInt, -}; - -export type Cluster = { - validatorCount: number, - networkFeeIndex: number, - index: number, - active: bool, - balance: BigInt -} - -export enum SSVModules { - SSVOperators, - SSVClusters, - SSVDAO, - SSVViews -}; diff --git a/test/helpers/utils/test.ts b/test/helpers/utils/test.ts deleted file mode 100644 index 72ed83f89..000000000 --- a/test/helpers/utils/test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from 'chai'; -import { publicClient } from '../contract-helpers'; - -interface Event { - contract: any; - eventName: string; -} - -interface EventAssertion extends Event { - eventLength?: number; - argNames?: string[]; - argValuesList?: any[][]; -} - -export async function assertEvent(tx: Promise, eventAssertions: EventAssertion[], unemittedEvent?: Event) { - const hash = await tx; - await publicClient.waitForTransactionReceipt({ hash }); - - if (unemittedEvent) { - const events = await unemittedEvent.contract.getEvents[unemittedEvent.eventName](); - expect(events.length).to.equal(0); - } - for (const assertion of eventAssertions) { - const events = await assertion.contract.getEvents[assertion.eventName](); - if (assertion.eventLength) { - expect(events.length).to.equal(assertion.eventLength); - } - - if (assertion.argNames && assertion.argValuesList) { - for (let i = 0; i < events.length; i++) { - for (let j = 0; j < assertion.argNames.length; j++) { - expect(events[i].args[assertion.argNames[j]]).to.deep.equal(assertion.argValuesList[i][j]); - } - } - } - } -} - -export async function assertPostTxEvent(eventAssertions: EventAssertion[], unemittedEvent?: Event) { - if (unemittedEvent) { - const events = await unemittedEvent.contract.getEvents[unemittedEvent.eventName](); - expect(events.length).to.equal(0); - } - for (const assertion of eventAssertions) { - const events = await assertion.contract.getEvents[assertion.eventName](); - if (assertion.eventLength) { - expect(events.length).to.equal(assertion.eventLength); - } - - if (assertion.argNames && assertion.argValuesList) { - for (let i = 0; i < events.length; i++) { - for (let j = 0; j < assertion.argNames.length; j++) { - expect(events[i].args[assertion.argNames[j]]).to.deep.equal(assertion.argValuesList[i][j]); - } - } - } - } -} diff --git a/test/helpers/v1-gas-report.json b/test/helpers/v1-gas-report.json new file mode 100644 index 000000000..3c85be815 --- /dev/null +++ b/test/helpers/v1-gas-report.json @@ -0,0 +1,642 @@ +{ + "timestamp": "2026-01-20T13:04:35.178Z", + "entries": [ + { + "name": "BULK_EXIT_10_VALIDATOR_10", + "maxLimit": 152500, + "min": 149421, + "max": 149421, + "average": 149421, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_EXIT_10_VALIDATOR_13", + "maxLimit": 165500, + "min": 162341, + "max": 162341, + "average": 162341, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_EXIT_10_VALIDATOR_4", + "maxLimit": 126200, + "min": 123585, + "max": 123585, + "average": 123585, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_EXIT_10_VALIDATOR_7", + "maxLimit": 139500, + "min": 136504, + "max": 136504, + "average": 136504, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4", + "maxLimit": 830000, + "min": 829413, + "max": 829413, + "average": 829413, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_10", + "maxLimit": 1430500, + "min": 1427849, + "max": 1427849, + "average": 1427849, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_13", + "maxLimit": 1740000, + "min": 1738195, + "max": 1738195, + "average": 1738195, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4", + "maxLimit": 818700, + "min": 816931, + "max": 816931, + "average": 816931, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_7", + "maxLimit": 1126500, + "min": 1124236, + "max": 1124236, + "average": 1124236, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_NEW_STATE_10", + "maxLimit": 1447000, + "min": 1445085, + "max": 1445085, + "average": 1445085, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_NEW_STATE_13", + "maxLimit": 1757000, + "min": 1754752, + "max": 1754752, + "average": 1754752, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_NEW_STATE_4", + "maxLimit": 835500, + "min": 833576, + "max": 833576, + "average": 833576, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REGISTER_10_VALIDATOR_NEW_STATE_7", + "maxLimit": 1143000, + "min": 1141231, + "max": 1141231, + "average": 1141231, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REMOVE_10_VALIDATOR_10", + "maxLimit": 292500, + "min": 290843, + "max": 290843, + "average": 290843, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REMOVE_10_VALIDATOR_13", + "maxLimit": 343000, + "min": 341479, + "max": 341479, + "average": 341479, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REMOVE_10_VALIDATOR_4", + "maxLimit": 191500, + "min": 190056, + "max": 190056, + "average": 190056, + "txCount": 1, + "withinLimit": true + }, + { + "name": "BULK_REMOVE_10_VALIDATOR_7", + "maxLimit": 241700, + "min": 240194, + "max": 240194, + "average": 240194, + "txCount": 1, + "withinLimit": true + }, + { + "name": "CANCEL_OPERATOR_FEE", + "maxLimit": 41900, + "min": 40821, + "max": 40821, + "average": 40821, + "txCount": 1, + "withinLimit": true + }, + { + "name": "CHANGE_LIQUIDATION_THRESHOLD_PERIOD", + "maxLimit": 41000, + "min": 40252, + "max": 40252, + "average": 40252, + "txCount": 1, + "withinLimit": true + }, + { + "name": "CHANGE_MINIMUM_COLLATERAL", + "maxLimit": 41200, + "min": 40403, + "max": 40403, + "average": 40403, + "txCount": 1, + "withinLimit": true + }, + { + "name": "DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD", + "maxLimit": 40900, + "min": 40231, + "max": 40231, + "average": 40231, + "txCount": 1, + "withinLimit": true + }, + { + "name": "DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD", + "maxLimit": 41000, + "min": 40198, + "max": 40198, + "average": 40198, + "txCount": 1, + "withinLimit": true + }, + { + "name": "DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT", + "maxLimit": 38200, + "min": 37376, + "max": 37376, + "average": 37376, + "txCount": 1, + "withinLimit": true + }, + { + "name": "DAO_UPDATE_OPERATOR_MAX_FEE", + "maxLimit": 40300, + "min": 40231, + "max": 40231, + "average": 40231, + "txCount": 1, + "withinLimit": true + }, + { + "name": "DECLARE_OPERATOR_FEE", + "maxLimit": 70000, + "min": 69658, + "max": 69658, + "average": 69658, + "txCount": 3, + "withinLimit": true + }, + { + "name": "DEPOSIT", + "maxLimit": 77500, + "min": 69566, + "max": 69566, + "average": 69566, + "txCount": 2, + "withinLimit": true + }, + { + "name": "EXECUTE_OPERATOR_FEE", + "maxLimit": 52000, + "min": 51473, + "max": 51473, + "average": 51473, + "txCount": 1, + "withinLimit": true + }, + { + "name": "LIQUIDATE_CLUSTER_10", + "maxLimit": 212000, + "min": 211792, + "max": 211792, + "average": 211792, + "txCount": 1, + "withinLimit": true + }, + { + "name": "LIQUIDATE_CLUSTER_13", + "maxLimit": 253000, + "min": 252763, + "max": 252763, + "average": 252763, + "txCount": 1, + "withinLimit": true + }, + { + "name": "LIQUIDATE_CLUSTER_4", + "maxLimit": 130500, + "min": 109897, + "max": 129851, + "average": 127276, + "txCount": 13, + "withinLimit": true + }, + { + "name": "LIQUIDATE_CLUSTER_7", + "maxLimit": 171000, + "min": 170821, + "max": 170821, + "average": 170821, + "txCount": 1, + "withinLimit": true + }, + { + "name": "NETWORK_FEE_CHANGE", + "maxLimit": 45800, + "min": 45024, + "max": 45024, + "average": 45024, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REACTIVATE_CLUSTER", + "maxLimit": 121500, + "min": 121427, + "max": 121427, + "average": 121427, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REDUCE_OPERATOR_FEE", + "maxLimit": 51900, + "min": 51142, + "max": 51142, + "average": 51142, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_OPERATOR", + "maxLimit": 137000, + "min": 115909, + "max": 136460, + "average": 116346, + "txCount": 12215, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_EXISTING_CLUSTER", + "maxLimit": 202000, + "min": 179753, + "max": 201580, + "average": 194304, + "txCount": 3, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_EXISTING_CLUSTER_10", + "maxLimit": 342700, + "min": 342200, + "max": 342248, + "average": 342224, + "txCount": 2, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_EXISTING_CLUSTER_13", + "maxLimit": 413700, + "min": 412851, + "max": 412899, + "average": 412875, + "txCount": 2, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4", + "maxLimit": 204500, + "min": 201592, + "max": 201592, + "average": 201592, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_EXISTING_CLUSTER_7", + "maxLimit": 272500, + "min": 272063, + "max": 272075, + "average": 272069, + "txCount": 2, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_NEW_STATE", + "maxLimit": 236000, + "min": 218327, + "max": 226021, + "average": 223169, + "txCount": 59, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4", + "maxLimit": 221000, + "min": 220721, + "max": 220721, + "average": 220721, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4", + "maxLimit": 231000, + "min": 230890, + "max": 230890, + "average": 230890, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_NEW_STATE_10", + "maxLimit": 359500, + "min": 358837, + "max": 358969, + "average": 358919, + "txCount": 5, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_NEW_STATE_13", + "maxLimit": 430500, + "min": 429523, + "max": 429583, + "average": 429549, + "txCount": 5, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4", + "maxLimit": 221500, + "min": 218327, + "max": 218327, + "average": 218327, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_NEW_STATE_7", + "maxLimit": 289000, + "min": 288737, + "max": 288857, + "average": 288804, + "txCount": 5, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_WITHOUT_DEPOSIT", + "maxLimit": 180600, + "min": 179849, + "max": 179849, + "average": 179849, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_WITHOUT_DEPOSIT_10", + "maxLimit": 322200, + "min": 320492, + "max": 320492, + "average": 320492, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_WITHOUT_DEPOSIT_13", + "maxLimit": 393300, + "min": 390939, + "max": 390939, + "average": 390939, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REGISTER_VALIDATOR_WITHOUT_DEPOSIT_7", + "maxLimit": 251600, + "min": 250284, + "max": 250284, + "average": 250284, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10", + "maxLimit": 168000, + "min": 144991, + "max": 144991, + "average": 144991, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REMOVE_OPERATOR", + "maxLimit": 70500, + "min": 70272, + "max": 70272, + "average": 70272, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REMOVE_OPERATOR_WHITELISTING_CONTRACT", + "maxLimit": 43000, + "min": 42193, + "max": 42193, + "average": 42193, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REMOVE_OPERATOR_WHITELISTING_CONTRACT_10", + "maxLimit": 130000, + "min": 129251, + "max": 129251, + "average": 129251, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REMOVE_OPERATOR_WITH_WITHDRAW", + "maxLimit": 70500, + "min": 70272, + "max": 70272, + "average": 70272, + "txCount": 2, + "withinLimit": true + }, + { + "name": "REMOVE_VALIDATOR", + "maxLimit": 114000, + "min": 51223, + "max": 113866, + "average": 103430, + "txCount": 7, + "withinLimit": true + }, + { + "name": "REMOVE_VALIDATOR_10", + "maxLimit": 197000, + "min": 196720, + "max": 196720, + "average": 196720, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REMOVE_VALIDATOR_13", + "maxLimit": 238500, + "min": 238150, + "max": 238150, + "average": 238150, + "txCount": 1, + "withinLimit": true + }, + { + "name": "REMOVE_VALIDATOR_7", + "maxLimit": 155500, + "min": 155293, + "max": 155293, + "average": 155293, + "txCount": 1, + "withinLimit": true + }, + { + "name": "SET_MULTIPLE_OPERATOR_WHITELIST_10_10", + "maxLimit": 381000, + "min": 380595, + "max": 380595, + "average": 380595, + "txCount": 1, + "withinLimit": true + }, + { + "name": "SET_OPERATOR_WHITELISTING_CONTRACT", + "maxLimit": 70000, + "min": 57719, + "max": 69863, + "average": 63791, + "txCount": 2, + "withinLimit": true + }, + { + "name": "SET_OPERATOR_WHITELISTING_CONTRACT_10", + "maxLimit": 375000, + "min": 340576, + "max": 340576, + "average": 340576, + "txCount": 1, + "withinLimit": true + }, + { + "name": "SET_OPERATORS_PRIVATE_10", + "maxLimit": 313000, + "min": 312019, + "max": 312019, + "average": 312019, + "txCount": 1, + "withinLimit": true + }, + { + "name": "SET_OPERATORS_PUBLIC_10", + "maxLimit": 114000, + "min": 112814, + "max": 112814, + "average": 112814, + "txCount": 1, + "withinLimit": true + }, + { + "name": "UPDATE_OPERATOR_WHITELISTING_CONTRACT", + "maxLimit": 70000, + "min": null, + "max": null, + "average": null, + "txCount": 0, + "withinLimit": true + }, + { + "name": "VALIDATOR_EXIT", + "maxLimit": 43000, + "min": 42660, + "max": 42660, + "average": 42660, + "txCount": 1, + "withinLimit": true + }, + { + "name": "WITHDRAW_CLUSTER_BALANCE", + "maxLimit": 95000, + "min": 94863, + "max": 94863, + "average": 94863, + "txCount": 1, + "withinLimit": true + }, + { + "name": "WITHDRAW_NETWORK_EARNINGS", + "maxLimit": 62500, + "min": 62424, + "max": 62424, + "average": 62424, + "txCount": 1, + "withinLimit": true + }, + { + "name": "WITHDRAW_OPERATOR_BALANCE", + "maxLimit": 64900, + "min": 64586, + "max": 64838, + "average": 64712, + "txCount": 2, + "withinLimit": true + } + ], + "summary": { + "totalOperations": 70, + "operationsWithData": 69, + "allWithinLimits": true + }, + "commit": "4116763", + "branch": "main" +} \ No newline at end of file diff --git a/test/integration/SSVNetwork.test.ts b/test/integration/SSVNetwork.test.ts new file mode 100644 index 000000000..cda9ea897 --- /dev/null +++ b/test/integration/SSVNetwork.test.ts @@ -0,0 +1,3582 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullFixture } from '../setup/fixtures.ts'; +import type { NetworkHelpersType, OperatorTuple, UnstakeRequest } from '../common/types.ts'; +import { + addValidatorsToCluster, buildEBMerkleForDefaultClusters, + calculateInitialBurnRate, + getCurrentClusterState, makeArrayOfKeysAndShares, + makeOperatorKey, + makePublicKey, registerDefaultCluster, registerDefaultClusters, + registerOperators, setupTestContext, updateClusterBalancesForDefaultClusters, + whitelistAddresses, +} from '../common/helpers.ts'; +import { + CLUSTER_VERSION_ETH, + DECLARE_OPERATOR_FEE_PERIOD, + DEFAULT_ETH_EB_PER_VALIDATOR, + DEFAULT_ETH_REGISTER_VALUE, DEFAULT_ORACLES_IDS, + DEFAULT_SHARES, DEFAULT_UNSTAKE_COOLDOWN, + EMPTY_CLUSTER, + EXECUTE_OPERATOR_FEE_PERIOD, + MAXIMUM_OPERATORS_FEE, + MINIMAL_LIQUIDATION_THRESHOLD, + MINIMAL_OPERATOR_ETH_FEE, + MINIMUM_BLOCKS_BEFORE_LIQUIDATION, + MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, NETWORK_FEE, + OPERATOR_FEE_PRECISION, OPERATOR_MAX_FEE_INCREASE, SMALL_ETH_REGISTER_VALUE, STAKE_AMOUNT, VALIDATORS_PER_OPERATOR_LIMIT, +} from '../common/constants.ts'; +import { Events } from '../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../common/errors.js'; +import { deployContract } from '../../scripts/common/helpers.js'; +import { ContractTransactionResponse } from 'ethers'; +import { trackGasFromReceipt, GasGroup } from '../helpers/gas-usage.ts'; + +describe("SSVNetwork full integration tests", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let randomUser: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, randomUser] } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Constructor, initializer and upgrades", async function () { + it("Configures SSVNetwork correctly", async function () { + const { network, views, cssvToken, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + expect(await network.getAddress()).to.be.properAddress; + expect(await views.getAddress()).to.be.properAddress; + expect(await cssvToken.getAddress()).to.be.properAddress; + expect(await ssvToken.getAddress()).to.be.properAddress; + + expect(await views.getVersion()).to.be.equal("v2.0.0"); + + const version = await network.getVersion(); + expect(version).to.be.a("string").and.not.empty; + + expect(await views.getMinimumLiquidationCollateral()).to.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL); + expect(await views.getValidatorsPerOperatorLimit()).to.equal(VALIDATORS_PER_OPERATOR_LIMIT); + expect(await views.getOperatorFeePeriods()).to.deep.equal([DECLARE_OPERATOR_FEE_PERIOD, EXECUTE_OPERATOR_FEE_PERIOD]); + expect(await views.getOperatorFeeIncreaseLimit()).to.equal(OPERATOR_MAX_FEE_INCREASE); + expect(await views.getActiveOracleIds()).to.deep.equal(DEFAULT_ORACLES_IDS); + expect(await views.getQuorumBps()).to.equal(7500n); + + expect(await views.getNetworkFee()).to.equal(NETWORK_FEE); + expect(await views.getNetworkFeeSSV()).to.equal(NETWORK_FEE); + expect(await views.getMaximumOperatorFee()).to.equal(MAXIMUM_OPERATORS_FEE); + expect(await views.getMinimumOperatorEthFee()).to.equal(MINIMAL_OPERATOR_ETH_FEE); + + expect(await views.cooldownDuration()).to.equal(DEFAULT_UNSTAKE_COOLDOWN); + + expect(await views.getNetworkEarnings()).to.equal(0n); + expect(await views.getNetworkEarningsSSV()).to.equal(0n); + expect(await views.getNetworkValidatorsCount()).to.equal(0); + expect(await views.totalStaked()).to.equal(0n); + }); + + it("Calling initializeSSVStaking again reverts with already-initialized error", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const upgradeFactory = await connection.ethers.getContractFactory("SSVNetworkSSVStakingUpgrade"); + const upgradeNetwork = upgradeFactory.attach(await network.getAddress()); + + await expect( + upgradeNetwork.initializeSSVStaking(DEFAULT_UNSTAKE_COOLDOWN, [1, 2, 3, 4], 7500) + ).to.be.revertedWith("Initializable: contract is already initialized"); + }); + }); + + describe("Function 'registerOperator()'", async function () { + it("Creates new operator and emits correct event", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + const tx = await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_OPERATOR]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_ADDED).withArgs(expectedId, operatorOwner.address, operatorKey, MINIMAL_OPERATOR_ETH_FEE) + .and.to.emit(network, Events.OPERATOR_PRIVACY_STATUS_UPDATED).withArgs([expectedId], true); + + expect(await views.getOperatorFee(expectedId)).to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + expect(await views.getOperatorFeeSSV(expectedId)).to.be.equal(0); + expect(await views.getOperatorDeclaredFee(expectedId)).to.be.deep.equal([false, 0n, 0n, 0n]); + expect(await views.getOperatorById(expectedId)).to.be.deep.equal([ + operatorOwner.address, + MINIMAL_OPERATOR_ETH_FEE, + 0, + connection.ethers.ZeroAddress, + true, + true + ]); + expect(await views.getOperatorByIdSSV(expectedId)).to.be.deep.equal([ + operatorOwner.address, + 0, + 0, + connection.ethers.ZeroAddress, + true, + true + ]); + }); + + it("Is reverted with 'FeeTooLow' if the provided fee is less than minimal allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE - 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeTooHigh' if the provided fee is higher than maximum allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MAXIMUM_OPERATORS_FEE + 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + + it("Is reverted with 'OperatorAlreadyExists' if the public key is already registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_ALREADY_EXISTS); + }); + }); + + describe("Function 'removeOperator()'", async function (){ + it("Deactivates the operator and emits correct event", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + const tx = await network.removeOperator(expectedId); + + expect(tx) + .to.emit(network, Events.OPERATOR_REMOVED) + .withArgs(expectedId) + + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR]); + + const operator: OperatorTuple = await views.getOperatorById(expectedId) + expect(operator[5]).to.be.equal(false) + expect(await views.getOperatorFee(expectedId)).to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator with passed id is not registered", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.removeOperator(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.connect(randomUser).removeOperator(expectedId)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setOperatorsWhitelists()'", async function () { + it("Whitelists addresses and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + expect(await network.setOperatorsWhitelists([expectedId], [clusterOwner])) + .to.emit(network, Events.OPERATOR_MULTIPLE_WHITELIST_UPDATED) + .withArgs([expectedId], [clusterOwner]); + + expect(await views.getWhitelistedOperators([expectedId], clusterOwner)).to.be.deep.equal([1n]); + }); + + it("Whitelists multiple operators for multiple addresses", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 10); + const whitelistAddresses = Array(10).fill(clusterOwner.address); + + const tx = await network.setOperatorsWhitelists(operatorIds, whitelistAddresses); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.setOperatorsWhitelists([], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH) + }); + + it("Is reverted with 'InvalidWhitelistAddressesLength' if the array of addresses is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.setOperatorsWhitelists([123], [])) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELIST_ADDRESSES_LENGTH) + }); + + it("Is reverted with 'ZeroAddressNotAllowed' if one of addresses is zero address", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.setOperatorsWhitelists([expectedId], [connection.ethers.ZeroAddress])) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS_NOT_ALLOWED) + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.setOperatorsWhitelists([123], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.connect(randomUser).setOperatorsWhitelists([expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicate", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.setOperatorsWhitelists([expectedId, expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network,operatorOwner, 3); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.setOperatorsWhitelists(operatorIds, [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + }); + + describe("Function 'removeOperatorsWhitelists()'", async function(){ + it("Removes addresses from the whitelist and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.setOperatorsWhitelists([expectedId], [clusterOwner]) + + expect(await network.removeOperatorsWhitelists([expectedId], [clusterOwner])) + .to.emit(network, Events.OPERATOR_MULTIPLE_WHITELIST_REMOVED) + .withArgs([expectedId], [clusterOwner]); + + expect(await views.getWhitelistedOperators([expectedId], clusterOwner)).to.be.deep.equal([]); + }); + + it("Removes multiple operators for multiple addresses", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 10); + const whitelistAddresses = Array(10).fill(clusterOwner.address); + + await network.setOperatorsWhitelists(operatorIds, whitelistAddresses); + + const tx = await network.removeOperatorsWhitelists(operatorIds, whitelistAddresses); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.removeOperatorsWhitelists([], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH) + }); + + it("Is reverted with 'InvalidWhitelistAddressesLength' if the array of addresses is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.removeOperatorsWhitelists([123], [])) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELIST_ADDRESSES_LENGTH) + }); + + it("Is reverted with 'ZeroAddressNotAllowed' if one of addresses is zero address", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.removeOperatorsWhitelists([expectedId], [connection.ethers.ZeroAddress])) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS_NOT_ALLOWED) + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.removeOperatorsWhitelists([123], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.connect(randomUser).removeOperatorsWhitelists([expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicate", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.removeOperatorsWhitelists([expectedId, expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network,operatorOwner, 3); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.removeOperatorsWhitelists(operatorIds, [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + }); + + describe("Function 'setOperatorsWhitelistingContract()'", async function () { + it("Registers whitelisting contract, emits correct event and allows to whitelist addresses via contract", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network,operatorOwner, 3); + const { contract: whiteListingContract, address: contractAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + const tx = await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT]); + + expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, contractAddress); + + expect(await views.isWhitelistingContract(contractAddress)).to.be.equal(true); + + await whiteListingContract.addWhitelistedAddress(clusterOwner); + + expect(await views.isAddressWhitelistedInWhitelistingContract(clusterOwner, operatorIds[0], contractAddress)) + .to.be.equal(true); + }); + + it("Updates whitelisting contract for operators", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 3); + const { contract: firstContract, address: firstAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + const { contract: secondContract, address: secondAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + await network.setOperatorsWhitelistingContract(operatorIds, firstContract); + + const tx = await network.setOperatorsWhitelistingContract(operatorIds, secondContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.UPDATE_OPERATOR_WHITELISTING_CONTRACT]); + + await expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, secondAddress); + + expect(firstAddress).to.not.equal(secondAddress); + }); + + it("Registers whitelisting contract for 10 operators", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 10); + const { contract: whiteListingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + const tx = await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10]); + }); + + it("Is reverted with 'InvalidWhitelistingContract' if the contract does not support required interface", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "SSVOperatorsWhitelist"); + const operatorIds = await registerOperators(network,operatorOwner, 3); + + expect(network.setOperatorsWhitelistingContract(operatorIds, contractAddress)) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELISTING_CONTRACT) + .withArgs(contractAddress); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' is the array of operators is empty", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + + await expect(network.setOperatorsWhitelistingContract([], contractAddress)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + + await expect(network.setOperatorsWhitelistingContract([12345n], contractAddress)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + const operatorIds = await registerOperators(network,operatorOwner, 3); + + await expect(network.connect(randomUser).setOperatorsWhitelistingContract(operatorIds, contractAddress)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address) + }); + }); + + describe("Function 'removeOperatorsWhitelistingContract()'", async function(){ + it("Removes whitelisting address and emits correct event", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network,operatorOwner, 3); + const { contract: whiteListingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + + const tx = await network.removeOperatorsWhitelistingContract(operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT]); + + expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, connection.ethers.ZeroAddress); + }); + + it("Removes whitelisting contract for 10 operators", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 10); + const { contract: whiteListingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + + const tx = await network.removeOperatorsWhitelistingContract(operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.removeOperatorsWhitelistingContract([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.removeOperatorsWhitelistingContract([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).removeOperatorsWhitelistingContract(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address) + }); + }); + + describe("Function 'setOperatorsPrivateUnchecked()'", async function() { + it("Changes privacy status and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + expect(await network.setOperatorsPrivateUnchecked(operatorIds)) + .to.emit(network, Events.OPERATORS_PRIVACY_STATUS_UPDATED) + .withArgs(operatorIds, true); + + const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); + expect(operator[4]).to.be.equal(true); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.setOperatorsPrivateUnchecked([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.setOperatorsPrivateUnchecked([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.connect(randomUser).setOperatorsPrivateUnchecked(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner); + }); + }); + + describe("Function 'setOperatorsPublicUnchecked()'", async function () { + it("Changes privacy status and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + expect(await network.setOperatorsPublicUnchecked(operatorIds)) + .to.emit(network, Events.OPERATORS_PRIVACY_STATUS_UPDATED) + .withArgs(operatorIds, false); + + const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); + expect(operator[4]).to.be.equal(false); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.setOperatorsPublicUnchecked([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.setOperatorsPublicUnchecked([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.connect(randomUser).setOperatorsPublicUnchecked(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner); + }); + }); + + describe("Function 'declareOperatorFee()'", async function() { + it("Declares new fee and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee: bigint = MINIMAL_OPERATOR_ETH_FEE * 2n; + + const tx: ContractTransactionResponse = await network.declareOperatorFee(operatorIds[0], newFee) + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DECLARE_OPERATOR_FEE]); + const block = await tx.getBlock(); + + const expectedBegin = BigInt(block!.timestamp) + DECLARE_OPERATOR_FEE_PERIOD; + const expectedEnd = expectedBegin + EXECUTE_OPERATOR_FEE_PERIOD; + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_DECLARED) + .withArgs(operatorOwner.address, operatorIds[0], tx.blockNumber, newFee); + expect(await views.getOperatorDeclaredFee(operatorIds[0])) + .to.be.deep.equal([ + true, + newFee, + expectedBegin, + expectedEnd + ]); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await expect(network.declareOperatorFee(12345n, newFee)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await expect(network.connect(randomUser).declareOperatorFee(operatorIds[0], newFee)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner); + }); + + it("Is reverted with 'FeeTooLow' is the passed fee is less than minimal", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE - 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'SameFeeChangeNotAllowed' is the passed value is the same as current one", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.SAME_FEE_CHANGE_NOW_ALLOWED); + }); + + it("Is reverted with 'SameFeeChangeNotAllowed' is the passed value is the same as current one", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.SAME_FEE_CHANGE_NOW_ALLOWED); + }); + + it("Is reverted with 'FeeTooHigh' if the new fee is higher than allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.declareOperatorFee(operatorIds[0], MAXIMUM_OPERATORS_FEE + 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + + it("Is reverted with 'FeeExceedsIncreaseLimit' if the new fee exceeds the allowed limit", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const exceedingFee = MINIMAL_OPERATOR_ETH_FEE * 3n; + + await expect(network.declareOperatorFee(operatorIds[0], exceedingFee)) + .to.be.revertedWithCustomError(network, Errors.FEE_EXCEEDS_INCREASE_LIMIT); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' if operators current fee is zero", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, 0, true); + + await expect(network.declareOperatorFee(expectedId, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + }); + + describe("Function 'cancelDeclaredOperatorFee()'", async function(){ + it("Cancels declared fee and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n) + + const tx = await network.cancelDeclaredOperatorFee(operatorIds[0]); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CANCEL_OPERATOR_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_DECLARATION_CANCELLED) + .withArgs(operatorOwner, operatorIds[0]); + + expect(await views.getOperatorDeclaredFee(operatorIds[0])) + .to.be.deep.equal([ + false, + 0n, + 0n, + 0n + ]); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.cancelDeclaredOperatorFee(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n) + + await expect(network.connect(randomUser).cancelDeclaredOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner); + }); + + it("Is reverted with 'NoFeeDeclared' if no declarations were done before", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.cancelDeclaredOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + }); + + describe("Function 'executeOperatorFee()'", async function() { + it("Updates operator fee according to a declared one and emits the correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n) + + await connection.networkHelpers.time.increase(EXECUTE_OPERATOR_FEE_PERIOD + 1n); + await connection.networkHelpers.mine(); + + const tx = await network.executeOperatorFee(operatorIds[0]); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.EXECUTE_OPERATOR_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_EXECUTED); + + expect(await views.getOperatorFee(operatorIds[0])).to.be.equal(MINIMAL_OPERATOR_ETH_FEE * 2n); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.executeOperatorFee(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n) + + await expect(network.connect(randomUser).executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'NoFeeDeclared' if no declarations were done before", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + + it("Is reverted with 'ApprovalNotWithinTimeframe' if execution period is not started or ended", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n) + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + + await connection.networkHelpers.time.increase(EXECUTE_OPERATOR_FEE_PERIOD * 2n); + await connection.networkHelpers.mine(); + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + }); + + it("Is reverted with 'FeeTooHigh' if the maximum fee changed during the execution period", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n) + await network.updateMaximumOperatorFee(MINIMAL_OPERATOR_ETH_FEE); + + await connection.networkHelpers.time.increase(EXECUTE_OPERATOR_FEE_PERIOD + 1n); + await connection.networkHelpers.mine(); + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + }); + + describe("Function 'updateMaximumOperatorFee()'", async function(){ + it("Updates maximum fee and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const tx = await network.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE * 2n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_MAXIMUM_FEE_UPDATED); + + expect(await views.getMaximumOperatorFee()) + .to.be.equal(MAXIMUM_OPERATORS_FEE * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if the caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("Reverts when new maximum fee is below the configured minimum fee", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.updateMaximumOperatorFee(MINIMAL_OPERATOR_ETH_FEE - OPERATOR_FEE_PRECISION)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_FEE_RANGE); + }); + }); + + describe("Function 'updateMinimumOperatorEthFee()'", async function() { + it("Updates minimum fee and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const newMinFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + const tx = await network.updateMinimumOperatorEthFee(newMinFee); + + await expect(tx) + .to.emit(network, Events.MINIMUM_OPERATOR_ETH_FEE_UPDATED) + .withArgs(newMinFee); + + expect(await views.getMinimumOperatorEthFee()).to.equal(newMinFee); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if the caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateMinimumOperatorEthFee(MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("When minimum is raised, registerOperator with fee below minimum reverts with FeeTooLow", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const raisedMinFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + await network.updateMinimumOperatorEthFee(raisedMinFee); + + await expect( + network.registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false) + ).to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + + await expect( + network.registerOperator(makeOperatorKey(1), raisedMinFee, false) + ).to.emit(network, Events.OPERATOR_ADDED); + }); + + it("Reverts when new minimum fee exceeds the configured maximum fee", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.updateMinimumOperatorEthFee(MAXIMUM_OPERATORS_FEE + OPERATOR_FEE_PRECISION)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_FEE_RANGE); + }); + }); + + describe("Function 'updateUnstakeCooldownDuration()'", async function() { + it("Changes cooldown period and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(await network.updateUnstakeCooldownDuration(DEFAULT_UNSTAKE_COOLDOWN + 1n)) + .to.emit(network, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + expect(await views.cooldownDuration()).to.be.equal(DEFAULT_UNSTAKE_COOLDOWN + 1n); + }); + + it("Is reverted if the caller is not the error", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateUnstakeCooldownDuration(DEFAULT_UNSTAKE_COOLDOWN + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateMinBlocksBetweenUpdates()'", async function() { + it("Updates the EB update cooldown blocks and emits correct event", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const newMinBlocks = 7200n; + + await expect(network.updateMinBlocksBetweenUpdates(newMinBlocks)) + .to.emit(network, Events.MIN_BLOCKS_BETWEEN_UPDATES_UPDATED) + .withArgs(newMinBlocks); + }); + + it("Is reverted if the caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateMinBlocksBetweenUpdates(7200n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateQuorumBps()'", async function() { + it("Changes quorum and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(await network.updateQuorumBps(10000n)) + .to.emit(network, Events.QUORUM_UPDATED) + .withArgs(10000n); + + expect(await views.getQuorumBps()).to.be.equal(10000n); + }); + + it("Is reverted if the caller is not the error", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateQuorumBps(10000n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'reduceOperatorFee()'", async function(){ + it("Decreases fee and emits the correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + const tx = await network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REDUCE_OPERATOR_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_EXECUTED); + + expect(await views.getOperatorFee(operatorId)) + .to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + }); + + it("Is reverted with 'OperatorDoesNotExist' if the operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.reduceOperatorFee(12345n, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.connect(randomUser).reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'FeeTooLow' if the passed fee is less than minimum allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE - 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' if caller is trying to increase the fee", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE * 3n)) + .to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + }); + + describe("Function 'withdrawOperatorEarnings()'", async function(){ + it("Withdraws operators earnings, update balances and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const earningsPeriod = 100n; + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + + expect(expectedEarnings).to.be.equal(earnings); + + const tx = await network.withdrawOperatorEarnings(operatorIds[0], earnings); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.WITHDRAW_OPERATOR_BALANCE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], earnings); + + expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.withdrawOperatorEarnings(12345n, 9999n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).withdrawOperatorEarnings(operatorIds[0], 9999n)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'InsufficientBalance' if the amount is less than operator earnings", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.withdrawOperatorEarnings(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function 'withdrawAllOperatorEarnings()'", async function(){ + it("Withdraws all operators earnings, update balances and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const earningsPeriod = 100n; + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + + expect(expectedEarnings).to.be.equal(earnings); + + await expect(await network.withdrawAllOperatorEarnings(operatorIds[0])) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], earnings + MINIMAL_OPERATOR_ETH_FEE); + + expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.withdrawAllOperatorEarnings(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).withdrawAllOperatorEarnings(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'withdrawAllVersionOperatorEarnings()'", async function() { + it("Withdraws all operators earnings and emits correct events", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const earningsPeriod = 100n; + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + + expect(expectedEarnings).to.be.equal(earnings); + + await expect(await network.withdrawAllVersionOperatorEarnings(operatorIds[0])) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], earnings + MINIMAL_OPERATOR_ETH_FEE); + + expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.withdrawAllVersionOperatorEarnings(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).withdrawAllVersionOperatorEarnings(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setFeeRecipientAddress()'", async function(){ + it("Emits the correct event with the correct input data", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).setFeeRecipientAddress(clusterOwner.address)) + .to.emit(network, Events.FEE_RECIPIENT_ADDRESS_UPDATED) + .withArgs(randomUser.address, clusterOwner.address); + }); + }); + + describe("Function 'updateOperatorFeeIncreaseLimit()'", async function(){ + it("Changes fee increase limit and emits the correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const tx = await network.updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_INCREASE_LIMIT_UPDATED) + .withArgs(OPERATOR_MAX_FEE_INCREASE); + + expect(await views.getOperatorFeeIncreaseLimit()).to.be.equal(OPERATOR_MAX_FEE_INCREASE); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("Reverts when fee increase limit exceeds 100%", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_FEE_INCREASE_LIMIT); + }); + }); + + describe("Function 'updateDeclareOperatorFeePeriod()'", async function() { + it("Changes the fee declare period and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const tx = await network.updateDeclareOperatorFeePeriod(DECLARE_OPERATOR_FEE_PERIOD + 1n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]); + + await expect(tx) + .to.emit(network, Events.DECLARE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + expect(await views.getOperatorFeePeriods()) + .to.be.deep.equal([DECLARE_OPERATOR_FEE_PERIOD + 1n, EXECUTE_OPERATOR_FEE_PERIOD]); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateDeclareOperatorFeePeriod(DECLARE_OPERATOR_FEE_PERIOD + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateExecuteOperatorFeePeriod()'", async function(){ + it("Changes the fee execute period and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const tx = await network.updateExecuteOperatorFeePeriod(EXECUTE_OPERATOR_FEE_PERIOD + 1n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]); + + await expect(tx) + .to.emit(network, Events.EXECUTE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(EXECUTE_OPERATOR_FEE_PERIOD + 1n); + + expect(await views.getOperatorFeePeriods()) + .to.be.deep.equal([DECLARE_OPERATOR_FEE_PERIOD , EXECUTE_OPERATOR_FEE_PERIOD + 1n]); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateExecuteOperatorFeePeriod(EXECUTE_OPERATOR_FEE_PERIOD + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateLiquidationThresholdPeriod()'", async function(){ + it("Changes the period and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const tx = await network.updateLiquidationThresholdPeriod(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]); + + await expect(tx) + .to.emit(network, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED) + .withArgs(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n); + + expect(await views.getLiquidationThresholdPeriod()) + .to.be.equal(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n); + }); + + it("Is reverted 'NewBlockPeriodIsBelowMinimum' if the passed threshold is less than minimum allowed", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.updateLiquidationThresholdPeriod(MINIMAL_LIQUIDATION_THRESHOLD - 1n)) + .to.be.revertedWithCustomError(network, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateLiquidationThresholdPeriod(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateLiquidationThresholdPeriodSSV()'", async function(){ + it("Changes the period and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.updateLiquidationThresholdPeriodSSV(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n)) + .to.emit(network, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED_SSV) + .withArgs(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n); + + expect(await views.getLiquidationThresholdPeriodSSV()) + .to.be.equal(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n); + }); + + it("Is reverted 'NewBlockPeriodIsBelowMinimum' if the passed threshold is less than minimum allowed", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.updateLiquidationThresholdPeriodSSV(MINIMAL_LIQUIDATION_THRESHOLD - 1n)) + .to.be.revertedWithCustomError(network, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateLiquidationThresholdPeriodSSV(MINIMUM_BLOCKS_BEFORE_LIQUIDATION + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateMinimumLiquidationCollateral()'", async function(){ + it("Changes collateral and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const tx = await network.updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_MINIMUM_COLLATERAL]); + + await expect(tx) + .to.emit(network, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED) + .withArgs(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + + expect(await views.getMinimumLiquidationCollateral()) + .to.be.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateMinimumLiquidationCollateralSSV()'", async function(){ + it("Changes collateral and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.updateMinimumLiquidationCollateralSSV(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.emit(network, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED_SSV) + .withArgs(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + + expect(await views.getMinimumLiquidationCollateralSSV()) + .to.be.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateMinimumLiquidationCollateralSSV(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateNetworkFee()'", async function() { + it("Updates network fee and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const currentFee = await views.getNetworkFee(); + const newFee = currentFee * 2n; + + const tx = await network.updateNetworkFee(newFee); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.NETWORK_FEE_CHANGE]); + + await expect(tx) + .to.emit(network, Events.NETWORK_FEE_UPDATED) + .withArgs(currentFee, newFee); + + expect(await views.getNetworkFee()).to.equal(newFee); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateNetworkFee(1000n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateNetworkFeeSSV()'", async function() { + it("Updates network fee SSV and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const currentFee = await views.getNetworkFeeSSV(); + const newFee = currentFee * 2n; + + await expect(network.updateNetworkFeeSSV(newFee)) + .to.emit(network, Events.NETWORK_FEE_UPDATED_SSV) + .withArgs(currentFee, newFee); + + expect(await views.getNetworkFeeSSV()).to.equal(newFee); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).updateNetworkFeeSSV(1000n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'withdrawNetworkSSVEarnings()'", async function() { + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).withdrawNetworkSSVEarnings(1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'registerValidator()'", async function () { + it("For a new cluster, creates it with a passed validator and emits correct event", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const tx = await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE]); + + await expect(tx).to.emit(network, Events.VALIDATOR_ADDED); + + const expectedCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + expect(await views.getValidator(clusterOwner, validatorKey)).to.equal(true); + expect(await views.isLiquidatable(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + expect(await views.isLiquidated(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + expect(await views.getBurnRate(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(await calculateInitialBurnRate(views, operatorIds, expectedCluster)); + expect(await views.getBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(await views.getEffectiveBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_EB_PER_VALIDATOR); + expect(await views.getClusterAssetType(clusterOwner, operatorIds)) + .to.be.equal(CLUSTER_VERSION_ETH); + await expect(views.isLiquidatableSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBalanceSSV(clusterOwner, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Registers a validator for a new ETH cluster using whitelisting contract", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + const { contract: whitelistingContract, address: whitelistingContractAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + await whitelistingContract.addWhitelistedAddress(clusterOwner.address); + await network.setOperatorsWhitelistingContract(operatorIds, whitelistingContractAddress); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTING_CONTRACT_4]); + }); + + it("Registers a validator for a new ETH cluster with one whitelisted operator", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsPublicUnchecked([operatorIds[1], operatorIds[2], operatorIds[3]]); + await network.setOperatorsWhitelists([operatorIds[0]], [clusterOwner.address]); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]); + }); + + it("Registers a validator for a new ETH cluster with four whitelisted operators", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4]); + }); + + it("Registers a validator into an existing ETH cluster with four whitelisted operators", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const existingCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4]); + }); + + it("Registers a validator for a new ETH cluster with one whitelisting contract operator", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsPublicUnchecked([operatorIds[1], operatorIds[2], operatorIds[3]]); + + const { contract: whitelistingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + await whitelistingContract.addWhitelistedAddress(clusterOwner.address); + await network.setOperatorsWhitelistingContract([operatorIds[0]], whitelistingContract); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4]); + }); + + it("Registers a validator into an existing ETH cluster", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const existingCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_ETH_CLUSTER]); + }); + + it("Registers a validator into a prefunded ETH cluster with zero additional deposit", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE * 2n } + ); + + const existingCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + existingCluster, + { value: 0 } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT]); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is removed", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(operatorOwner).removeOperator(operatorIds[2]); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is removed for an existing cluster", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const existingCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + await network.connect(operatorOwner).removeOperator(operatorIds[2]); + + await expect(network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the amount of operators is not the allowed one", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 5); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidPublicKeyLength' if the public key is not 48 bytes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const invalidLengthPublicKey = makePublicKey(1) + "11"; + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).registerValidator( + invalidLengthPublicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'ValidatorAlreadyRegistered' if the public key is already registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_ALREADY_REGISTERED) + .withArgs(validatorKey, clusterOwner.address); + }); + + it("Is reverted with 'IncorrectClusterState' for the new cluster is the cluster data is not consisting from zeroes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const invalidCluster = { ...EMPTY_CLUSTER, validatorCount: 123n }; + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + invalidCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicates", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + operatorIds.pop(); + operatorIds.unshift(operatorIds[0]); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'CallerNotWhitelistedWithData' if one of operators did not whitelist the caller", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_WHITELISTED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'ExceedValidatorLimitWithData' if one of operators will exceed the network limit", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const { address: upgradeImplAddr } = await deployContract(connection.ethers, "SSVNetworkValidatorsPerOperatorUpgrade"); + const factory = await connection.ethers.getContractFactory("SSVNetworkValidatorsPerOperatorUpgrade"); + const initData = factory.interface.encodeFunctionData("initializev2", [0]); + await network.upgradeToAndCall(upgradeImplAddr, initData); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_VALIDATORS_LIMIT_EXCEEDED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'InsufficientBalance' if msg value is not enough to cover the validator", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: 0 } + )) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function bulkRegisterValidator()", async function() { + it("Registers bulk of validators, creates a new cluster with the expected data and emits correct events", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const tx = await network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await tx.wait(); + + for (let i = 0; i < keys.length; i++) { + const expectedCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + expect(await views.getValidator(clusterOwner, keys[i])).to.equal(true); + expect(await views.isLiquidatable(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + expect(await views.isLiquidated(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + expect(await views.getBurnRate(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(await calculateInitialBurnRate(views, operatorIds, expectedCluster)); + expect(await views.getBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(await views.getEffectiveBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_EB_PER_VALIDATOR * BigInt(keys.length)); + expect(await views.getClusterAssetType(clusterOwner, operatorIds)) + .to.be.equal(CLUSTER_VERSION_ETH); + + await expect(views.isLiquidatableSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBalanceSSV(clusterOwner, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + } + }); + + it("Registers bulk of validators into an existing cluster with one whitelisting contract operator", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsPublicUnchecked([operatorIds[1], operatorIds[2], operatorIds[3]]); + + const { contract: whitelistingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + await whitelistingContract.addWhitelistedAddress(clusterOwner.address); + await network.setOperatorsWhitelistingContract([operatorIds[0]], whitelistingContract); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const existingCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + const keys = Array.from({ length: 10 }, (_, i) => makePublicKey(i + 2)); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the amount of operators is not the allowed one", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 5); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidPublicKeyLength' if one of public keys is not 48 bytes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + + const invalidLengthPublicKey = makePublicKey(1) + "11"; + keys.shift(); + keys.unshift(invalidLengthPublicKey); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'ValidatorAlreadyRegistered' if one of public keys is already registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + keys[7], + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_ALREADY_REGISTERED) + .withArgs(keys[7], clusterOwner.address); + }); + + it("Is reverted with 'IncorrectClusterState' for the new cluster is the cluster data is not consisting from zeroes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const invalidCluster = { ...EMPTY_CLUSTER, validatorCount: 123n }; + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + invalidCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicates", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + operatorIds.pop(); + operatorIds.unshift(operatorIds[0]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'CallerNotWhitelistedWithData' if one of operators did not whitelist the caller", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_WHITELISTED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'ExceedValidatorLimitWithData' if one of operators will exceed the network limit", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const { address: upgradeImplAddr } = await deployContract(connection.ethers, "SSVNetworkValidatorsPerOperatorUpgrade"); + const factory = await connection.ethers.getContractFactory("SSVNetworkValidatorsPerOperatorUpgrade"); + const initData = factory.interface.encodeFunctionData("initializev2", [0]); + await network.upgradeToAndCall(upgradeImplAddr, initData); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_VALIDATORS_LIMIT_EXCEEDED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'InsufficientBalance' if msg value is not enough to cover new validators", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: 0 } + )) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is removed", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(operatorOwner).removeOperator(operatorIds[2]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is removed for an existing cluster", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const existingCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + await network.connect(operatorOwner).removeOperator(operatorIds[2]); + + const {keys, shares} = makeArrayOfKeysAndShares(2, 10); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + }); + + it("Is reverted with 'EmptyPublicKeysList' if the array of public keys is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + [], + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.EMPTY_PUBLIC_KEYS_LIST); + }); + + it("Is reverted with 'PublicKeysSharesLengthMismatch' if the array of keys and array of shares have different length", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: 0 } + )) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + describe("Function 'removeValidator()'", async function() { + it("Removes validator and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const tx = await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_VALIDATOR]); + + await expect(tx) + .to.emit(network, Events.VALIDATOR_REMOVED); + + const clusterAfter = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + expect(clusterAfter.validatorCount).to.equal(0n); + expect(clusterAfter.active).to.equal(true); + expect(await views.getValidator(clusterOwner.address, validatorKey)).to.be.equal(false); + }); + + it("Is reverted with 'ClusterDoesNotExists' if the cluster with this owner and operators does not exist", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(randomUser).removeValidator(validatorKey, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the cluster data is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ValidatorDoesNotExist' if the validator was never registered", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const incorrectValidator: string = validatorKey + "11"; + + await expect(network.connect(clusterOwner).removeValidator(incorrectValidator, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reveted with 'ValidatorDoesNotExist' if validator is already removed", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const updatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect(network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, updatedCluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function 'bulkRemoveValidator()'", async function() { + it("Removes validators and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + const { keys, shares } = makeArrayOfKeysAndShares(2, 10); + const populatedCluster = await addValidatorsToCluster( + connection, network, keys, shares, clusterOwner, operatorIds, cluster + ); + + const tx = await network.connect(clusterOwner).bulkRemoveValidator(keys, operatorIds, populatedCluster); + await tx.wait(); + const clusterAfter = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + for (let i = 0; i < keys.length; i++) { + await expect(tx).to.emit(network, Events.VALIDATOR_REMOVED); + expect(await views.getValidator(clusterOwner.address, keys[i])).to.be.equal(false); + } + + expect(clusterAfter.validatorCount).to.equal(cluster.validatorCount); + expect(clusterAfter.active).to.equal(true); + }); + + it("Is reverted with 'ClusterDoesNotExists' if the cluster with this owner and operators does not exist", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(randomUser).bulkRemoveValidator([validatorKey], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the cluster data is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(clusterOwner).bulkRemoveValidator([validatorKey], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ValidatorDoesNotExist' if the validator was never registered", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const incorrectValidator: string = validatorKey + "11"; + + await expect(network.connect(clusterOwner).bulkRemoveValidator([incorrectValidator], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reveted with 'ValidatorDoesNotExist' if validator is already removed", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const updatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect(network.connect(clusterOwner).bulkRemoveValidator([validatorKey], operatorIds, updatedCluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function 'liquidate()'", async function() { + it("If called by cluster owner - liquidates cluster, refunds caller and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: SMALL_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + const networkAddress = await network.getAddress(); + const callerBalanceBefore = await connection.ethers.provider.getBalance(clusterOwner.address); + const contractBalanceBefore = await connection.ethers.provider.getBalance(networkAddress); + + const tx = await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + await expect(tx).to.emit(network, Events.CLUSTER_LIQUIDATED); + const receipt = await tx.wait(); + const gasCost = receipt!.gasUsed * (receipt!.effectiveGasPrice ?? receipt!.gasPrice); + + const callerBalanceAfter = await connection.ethers.provider.getBalance(clusterOwner.address); + const contractBalanceAfter = await connection.ethers.provider.getBalance(networkAddress); + + const payout = contractBalanceBefore - contractBalanceAfter; + expect(payout).to.be.greaterThan(0n); + expect(callerBalanceAfter - callerBalanceBefore + gasCost).to.equal(payout); + + const newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(await views.isLiquidated(clusterOwner.address, operatorIds, newClusterState)).to.be.equal(true); + }); + + it("If called not by a cluster owner, liquidates cluster, refunds the caller and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: SMALL_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.updateLiquidationThresholdPeriod(1000000000); + + const networkAddress = await network.getAddress(); + const callerBalanceBefore = await connection.ethers.provider.getBalance(randomUser.address); + const contractBalanceBefore = await connection.ethers.provider.getBalance(networkAddress); + + const tx = await network.connect(randomUser).liquidate(clusterOwner.address, operatorIds, cluster); + await expect(tx).to.emit(network, Events.CLUSTER_LIQUIDATED); + const receipt = await tx.wait(); + const gasCost = receipt!.gasUsed * (receipt!.effectiveGasPrice ?? receipt!.gasPrice); + + const callerBalanceAfter = await connection.ethers.provider.getBalance(randomUser.address); + const contractBalanceAfter = await connection.ethers.provider.getBalance(networkAddress); + + const payout = contractBalanceBefore - contractBalanceAfter; + expect(payout).to.be.greaterThan(0n); + expect(callerBalanceAfter - callerBalanceBefore + gasCost).to.equal(payout); + + const newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(await views.isLiquidated(clusterOwner.address, operatorIds, newClusterState)).to.be.equal(true); + }); + + it("Is reverted with 'ClusterDoesNotExists' if the cluster was not created yet", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(randomUser).liquidate(randomUser.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the provided cluster state is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(randomUser).liquidate(clusterOwner.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterIsLiquidated' if cluster is already liquidated", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + const newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect(network.connect(randomUser).liquidate(clusterOwner.address, operatorIds, newClusterState)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_IS_LIQUIDATED); + }); + + it("Is reverted with 'ClusterNotLiquidatable' if called not by cluster owner and cluster is not yet liquidatable", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(randomUser).liquidate(clusterOwner.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_NOT_LIQUIDATABLE); + }); + }); + + describe("Function 'reactivate()'", async function() { + it("Reactivates the cluster and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + let newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect(await network.connect(clusterOwner).reactivate(operatorIds, newClusterState, {value: DEFAULT_ETH_REGISTER_VALUE})) + .to.emit(network, Events.CLUSTER_REACTIVATED); + newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + expect(await views.isLiquidated(clusterOwner.address, operatorIds, newClusterState)).to.be.equal(false); + }); + + it("Is reverted with 'ClusterDoesNotExists' if such cluster was not yet registered", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(randomUser).reactivate(operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the provided cluster state is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(clusterOwner).reactivate(operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterAlreadyEnabled' if cluster is not liquidated", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(clusterOwner).reactivate(operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_ALREADY_ENABLED); + }); + + it("Is reverted with 'InsufficientBalance' if the amount is not enough to cover runway", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + let newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect(network.connect(clusterOwner).reactivate(operatorIds, newClusterState, {value: 1})) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function 'deposit()'", async function() { + it("Increases cluster balance and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const balanceBefore = await views.getBalance(clusterOwner.address, operatorIds, cluster); + + await expect(await network.deposit(clusterOwner.address, operatorIds, cluster, {value: DEFAULT_ETH_REGISTER_VALUE})) + .to.emit(network, Events.CLUSTER_DEPOSITED); + + const newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter = await views.getBalance(clusterOwner.address, operatorIds, newClusterState); + + expect(balanceAfter).to.be.greaterThan(balanceBefore); + }); + + it("Is reverted with 'ClusterDoesNotExists' if such cluster was not yet registered", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.deposit(randomUser.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the provided cluster state is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(clusterOwner).deposit(clusterOwner.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + }); + + describe("Function 'withdraw()'", async function() { + it("Withdraws from cluster balance and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const callerBalanceBefore = await connection.ethers.provider.getBalance(clusterOwner.address); + + await expect(await network.connect(clusterOwner).withdraw(operatorIds, SMALL_ETH_REGISTER_VALUE, cluster)) + .to.emit(network, Events.CLUSTER_WITHDRAWN); + + const callerBalanceAfter = await connection.ethers.provider.getBalance(clusterOwner.address); + const newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + expect(callerBalanceAfter).to.be.greaterThan(callerBalanceBefore); + expect(BigInt(newClusterState.balance)).to.be.lessThan(BigInt(cluster.balance)); + }); + + it("Is reverted with 'ClusterDoesNotExists' if such cluster was not yet registered", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.withdraw(operatorIds, SMALL_ETH_REGISTER_VALUE, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the provided cluster state is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(clusterOwner).withdraw(operatorIds, SMALL_ETH_REGISTER_VALUE, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'InsufficientBalance' when withdrawing from a liquidated cluster with zero balance", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + const newClusterState = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await expect(network.connect(clusterOwner).withdraw(operatorIds, SMALL_ETH_REGISTER_VALUE, newClusterState)) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Is reverted with 'InsufficientBalance' if the amount is bigger than cluster balance", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {cluster, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(clusterOwner).withdraw(operatorIds, DEFAULT_ETH_REGISTER_VALUE + 1n, cluster)) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function 'exitValidator()'", async function() { + it("Emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(clusterOwner).exitValidator(validatorKey, operatorIds)) + .to.emit(network, Events.VALIDATOR_EXITED) + .withArgs(clusterOwner.address, operatorIds, validatorKey) + }); + + it("Is reverted with 'ValidatorDoesNotExist' if the key does not exist or belong to a caller", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(clusterOwner).exitValidator(makePublicKey(123), operatorIds)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + + await expect(network.connect(randomUser).exitValidator(validatorKey, operatorIds)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function 'bulkExitValidator()'", async function() { + it("Emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(clusterOwner).bulkExitValidator([validatorKey], operatorIds)) + .to.emit(network, Events.VALIDATOR_EXITED) + .withArgs(clusterOwner.address, operatorIds, validatorKey) + }); + + it("Is reverted with 'ValidatorDoesNotExist' if the key does not exist or belong to a caller", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const {validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(clusterOwner).bulkExitValidator([makePublicKey(123)], operatorIds)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + + await expect(network.connect(randomUser).bulkExitValidator([validatorKey], operatorIds)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function stake()", async function() { + it("Stakes SSV, mints CSSV to the staker and creates delegation weight", async function() { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + + const tx = await network.connect(randomUser).stake(STAKE_AMOUNT); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.STAKE_SSV]); + + await expect(tx) + .to.emit(network, Events.STAKED) + .withArgs(randomUser.address, STAKE_AMOUNT); + + expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + }); + + it("Is reverted with 'StakeTooLow' if the amount to stake is smaller than minimum allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.stake(1)) + .to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + + it("Is reverted with 'StakeTooLow' is caller is trying to stake 0 SSV", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.stake(0)) + .to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + }); + + describe("Function requestUnstake()", async function() { + it("For full amount, creates unstake request, burns CSSV and removes delegation", async function () { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken + .connect(randomUser) + .approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + + const tx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REQUEST_UNSTAKE]); + + const block = await tx.getBlock(); + const expectedUnlockTime = + BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + await expect(tx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT, expectedUnlockTime); + + const requests: UnstakeRequest[] = + await views.pendingUnstake(randomUser.address); + + expect(requests.length).to.equal(1); + expect(requests[0].amount).to.equal(STAKE_AMOUNT); + expect(requests[0].unlockTime).to.equal(expectedUnlockTime); + + expect(await cssvToken.balanceOf(randomUser.address)).to.equal(0); + expect(await views.stakedBalanceOf(randomUser.address)).to.equal(0); + }); + + it("For partial amount, creates unstake request, burns CSSV and removes delegation", async function(){ + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken + .connect(randomUser) + .approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + const tx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT / 2n); + await tx.wait(); + const block = await tx.getBlock(); + + const firstUnlockTime = + BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + await expect(tx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT / 2n, firstUnlockTime); + + let requests: UnstakeRequest[] = + await views.pendingUnstake(randomUser.address); + + expect(requests.length).to.equal(1); + expect(requests[0].amount).to.equal(STAKE_AMOUNT / 2n); + expect(requests[0].unlockTime).to.equal(firstUnlockTime); + const secondTx = await network + .connect(randomUser) + .requestUnstake(STAKE_AMOUNT / 2n); + await secondTx.wait(); + const secondBlock = await secondTx.getBlock(); + + const secondUnlockTime = + BigInt(secondBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + await expect(secondTx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT / 2n, secondUnlockTime); + + requests = await views.pendingUnstake(randomUser.address); + + expect(requests.length).to.equal(2); + expect(requests[0].amount).to.equal(STAKE_AMOUNT / 2n); + expect(requests[0].unlockTime).to.equal(firstUnlockTime); + expect(requests[1].amount).to.equal(STAKE_AMOUNT / 2n); + expect(requests[1].unlockTime).to.equal(secondUnlockTime); + }); + + it("Is reverted with 'MaxRequestsAmountReached' if more than 10 pending requests", async function() { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + + const smallAmount = STAKE_AMOUNT / 20000n; + + for (let i = 0; i < 2000; i++) { + await network.connect(randomUser).requestUnstake(smallAmount); + } + + await expect(network.connect(randomUser).requestUnstake(smallAmount)) + .to.be.revertedWithCustomError(network, Errors.MAX_REQUESTS_AMOUNT_REACHED); + }); + + it("Is reverted with 'ZeroAmount' if caller is trying to request 0 SSV", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.requestUnstake(0)) + .to.be.revertedWithCustomError(network, Errors.ZERO_AMOUNT); + }); + + it("Is reverted with 'UnstakeAmountExceedsBalance' if caller is trying to request more SSV than they staked", async function(){ + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT) + + await expect(network.connect(randomUser).requestUnstake(STAKE_AMOUNT + 1n)) + .to.be.revertedWithCustomError(network, Errors.UNSTAKE_AMOUNT_EXCEEDS_BALANCE); + }); + }); + + describe("Function 'withdrawUnlocked()'", async function(){ + it("Withdraws SSV and emits correct event", async function() { + const { network, views, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT) + await network.connect(randomUser).requestUnstake(STAKE_AMOUNT); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await networkHelpers.mine(); + + const tx = await network.connect(randomUser).withdrawUnlocked(); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.WITHDRAW_UNSTAKE]); + + await expect(tx) + .to.emit(network, Events.UNSTAKE_WITHDRAWN) + .withArgs(randomUser.address, STAKE_AMOUNT); + + expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(0); + expect(await ssvToken.balanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(0); + }); + + it("Is reverted with 'NothingToWithdraw()' if nor rewards were unfrozen yet", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).withdrawUnlocked()) + .to.be.revertedWithCustomError(network, Errors.NOTHING_TO_WITHDRAW); + }); + }); + + describe("Function 'claimEthRewards()'", async function() { + it("Claims ETH rewards and emits correct event", async function() { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + + await network.connect(randomUser).stake(STAKE_AMOUNT); + + const oracles = (await connection.ethers.getSigners()).slice(10, 14); + + await network.replaceOracle(1, oracles[0].address); + await network.replaceOracle(2, oracles[1].address); + await network.replaceOracle(3, oracles[2].address); + await network.replaceOracle(4, oracles[3].address); + + for (let i = 1; i <= oracles.length; i++) { + expect(await views.getOracle(i)).to.be.equal(oracles[i-1]); + expect(await views.getOracleWeight(i)).to.be.equal(STAKE_AMOUNT / BigInt(oracles.length)); + } + + const operatorIds = await registerOperators(network, operatorOwner, 4) + const clusters = await registerDefaultClusters( + connection, + network, + operatorIds, + operatorOwner, + 8 + ); + const merkleData = buildEBMerkleForDefaultClusters(connection, clusters, 33); + + const block = await connection.ethers.provider.getBlock('latest'); + const blockNum = block!.number + + for (let i = 0; i < 3; i++) { + await network.connect(oracles[i]).commitRoot(merkleData.root, blockNum); + } + expect(await views.getCommittedRoot(blockNum)).to.be.equal(merkleData.root); + + await updateClusterBalancesForDefaultClusters( + network, + clusters, + merkleData, + blockNum, + 33 + ); + + expect(await views.previewClaimableEth(randomUser.address)).to.not.be.equal(0); + expect(await views.stakingEthPoolBalance()).to.be.equal(0); + + const userBalanceBefore = await connection.ethers.provider.getBalance(randomUser.address); + + await expect(network.connect(randomUser).claimEthRewards()) + .to.emit(network, Events.REWARDS_CLAIMED); + + const userBalanceAfter = await connection.ethers.provider.getBalance(randomUser.address); + expect(userBalanceAfter).to.be.greaterThan(userBalanceBefore); + }); + + it("Is reverted with 'NothingToClaim()' if no rewards were accumulated yet", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).claimEthRewards()) + .to.be.revertedWithCustomError(network, Errors.NOTHING_TO_CLAIM); + }); + }); + + describe("Function 'syncFees()'", async function() { + it("Syncs fees and emits correct event if the data is relevant", async function() { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + + await network.connect(randomUser).stake(STAKE_AMOUNT); + + const oracles = (await connection.ethers.getSigners()).slice(10, 14); + + await network.replaceOracle(1, oracles[0].address); + await network.replaceOracle(2, oracles[1].address); + await network.replaceOracle(3, oracles[2].address); + await network.replaceOracle(4, oracles[3].address); + + const operatorIds = await registerOperators(network, operatorOwner, 4) + const clusters = await registerDefaultClusters( + connection, + network, + operatorIds, + operatorOwner, + 8 + ); + const merkleData = buildEBMerkleForDefaultClusters(connection, clusters, 33); + + const block = await connection.ethers.provider.getBlock('latest'); + const blockNum = block!.number + + for (let i = 0; i < 3; i++) { + await network.connect(oracles[i]).commitRoot(merkleData.root, blockNum); + } + + await updateClusterBalancesForDefaultClusters( + network, + clusters, + merkleData, + blockNum, + 33 + ); + + await expect(network.syncFees()) + .to.emit(network, Events.FEES_SYNCED); + }); + + it("Does not emit event if no relevant changes happened", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.syncFees()) + .to.not.emit(network, Events.FEES_SYNCED); + }) + }); + + describe("Function 'rescueERC20()'", async function() { + it("Withdraws tokens and emits correct event", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const randomToken = await connection.ethers.deployContract("MockToken"); + await randomToken.waitForDeployment(); + const tokenAddress = await randomToken.getAddress(); + + await randomToken.mint(await network.getAddress(), 123); + + await expect(network.rescueERC20(tokenAddress, randomUser.address, 123)) + .to.emit(network, Events.ERC20_RESCUED) + .withArgs(tokenAddress, randomUser.address, 123); + + expect(await randomToken.balanceOf(randomUser.address)).to.be.equal(123); + }); + + it("Withdraws non-standard ERC20 tokens that do not return a value", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const nonStandardToken = await connection.ethers.deployContract("NonStandardERC20Mock"); + await nonStandardToken.waitForDeployment(); + const tokenAddress = await nonStandardToken.getAddress(); + + await nonStandardToken.mint(await network.getAddress(), 123); + + await expect(network.rescueERC20(tokenAddress, randomUser.address, 123)) + .to.emit(network, Events.ERC20_RESCUED) + .withArgs(tokenAddress, randomUser.address, 123); + + expect(await nonStandardToken.balanceOf(randomUser.address)).to.be.equal(123); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if the caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).rescueERC20(randomUser.address, randomUser.address, 123)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("Is reverted with 'ZeroAddress()' if token address is zero", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.rescueERC20(connection.ethers.ZeroAddress, randomUser.address, 123)) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS); + }); + + it("Is reverted with 'ZeroAddress()' if receiver address is zero", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.rescueERC20(randomUser.address, connection.ethers.ZeroAddress, 123)) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS); + }); + + it("Is reverted with 'InvalidToken()' if trying to rescue ssv token", async function() { + const { network, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.rescueERC20(await ssvToken.getAddress(), randomUser.address, 123)) + .to.be.revertedWithCustomError(network, Errors.INVALID_TOKEN); + }); + + it("Is reverted with 'InvalidToken()' if trying to rescue cssv token", async function() { + const { network, cssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.rescueERC20(await cssvToken.getAddress(), randomUser.address, 123)) + .to.be.revertedWithCustomError(network, Errors.INVALID_TOKEN); + }); + + it("Is reverted with 'ZeroAmount()' if trying to rescue zero tokens", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.rescueERC20(randomUser.address, randomUser.address, 0)) + .to.be.revertedWithCustomError(network, Errors.ZERO_AMOUNT); + }); + }); + + describe("Function 'onCSSVTransfer()'", async function() { + it("Syncs fees and emits correct events", async function() { + const { network, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + + await network.connect(randomUser).stake(STAKE_AMOUNT); + + const oracles = (await connection.ethers.getSigners()).slice(10, 14); + + await network.replaceOracle(1, oracles[0].address); + await network.replaceOracle(2, oracles[1].address); + await network.replaceOracle(3, oracles[2].address); + await network.replaceOracle(4, oracles[3].address); + + const operatorIds = await registerOperators(network, operatorOwner, 4) + const clusters = await registerDefaultClusters( + connection, + network, + operatorIds, + operatorOwner, + 8 + ); + const merkleData = buildEBMerkleForDefaultClusters(connection, clusters, 33); + + const block = await connection.ethers.provider.getBlock('latest'); + const blockNum = block!.number + + for (let i = 0; i < 3; i++) { + await network.connect(oracles[i]).commitRoot(merkleData.root, blockNum); + } + + await updateClusterBalancesForDefaultClusters( + network, + clusters, + merkleData, + blockNum, + 33 + ); + + const tx = await cssvToken.connect(randomUser).transfer(operatorOwner.address, STAKE_AMOUNT); + await tx.wait(); + + await expect(tx) + .to.emit(network, Events.FEES_SYNCED) + }); + + it("Is reverted with 'NotCSSV()' if the caller is not CSSV token", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).onCSSVTransfer(randomUser.address, randomUser.address, 123)) + .to.be.revertedWithCustomError(network, Errors.NOT_CSSV); + }); + }); + + describe("Reentrancy Guard Tests", async function () { + it("Prevents reentrancy in 'liquidate()'", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + const Malicious = await connection.ethers.getContractFactory("MaliciousLiquidate"); + const malicious = await Malicious.deploy(await network.getAddress()); + await malicious.waitForDeployment(); + + await whitelistAddresses(network, operatorOwner, operatorIds, [await malicious.getAddress()]); + + await malicious.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: SMALL_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState(connection, network, await malicious.getAddress(), operatorIds); + + await connection.networkHelpers.mine(9999); + + await malicious.setParams(operatorIds, cluster); + await expect(malicious.attack()).to.be.revertedWithCustomError(network, Errors.ETH_TRANSFER_FAILED); + }); + + it("Prevents reentrancy in 'withdraw()'", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + const Malicious = await connection.ethers.getContractFactory("MaliciousWithdraw"); + const malicious = await Malicious.deploy(await network.getAddress()); + await malicious.waitForDeployment(); + + await whitelistAddresses(network, operatorOwner, operatorIds, [await malicious.getAddress()]); + + await malicious.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: SMALL_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState(connection, network, await malicious.getAddress(), operatorIds); + + await malicious.setParams(operatorIds, cluster); + await expect(malicious.attack()).to.be.revertedWithCustomError(network, Errors.ETH_TRANSFER_FAILED); + }); + + it("Prevents reentrancy in 'withdrawAllOperatorEarnings()'", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const Malicious = await connection.ethers.getContractFactory("MaliciousWithdrawAllOperatorEarnings"); + const malicious = await Malicious.deploy(await network.getAddress()); + await malicious.waitForDeployment(); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 3); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const operatorKey = makeOperatorKey(123); + const expectedId = await malicious.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await malicious.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await malicious.setOperatorsWhitelists([expectedId], [clusterOwner]); + const earningsPeriod = 100n; + operatorIds.push(Number(expectedId)); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(earningsPeriod); + + await malicious.setParams(expectedId); + await expect(malicious.attack()).to.be.revertedWithCustomError(network, Errors.ETH_TRANSFER_FAILED); + }); + + it("Prevents reentrancy in 'MaliciousWithdrawAllVersionOperatorEarnings()'", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const Malicious = await connection.ethers.getContractFactory("MaliciousWithdrawAllVersionOperatorEarnings"); + const malicious = await Malicious.deploy(await network.getAddress()); + await malicious.waitForDeployment(); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 3); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const operatorKey = makeOperatorKey(123); + const expectedId = await malicious.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await malicious.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await malicious.setOperatorsWhitelists([expectedId], [clusterOwner]); + const earningsPeriod = 100n; + operatorIds.push(Number(expectedId)); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(earningsPeriod); + + await malicious.setParams(expectedId); + await expect(malicious.attack()).to.be.revertedWithCustomError(network, Errors.ETH_TRANSFER_FAILED); + }); + + it("Prevents reentrancy in 'withdrawOperatorEarnings()'", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const Malicious = await connection.ethers.getContractFactory("MaliciousWithdrawOperatorEarnings"); + const malicious = await Malicious.deploy(await network.getAddress()); + await malicious.waitForDeployment(); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 3); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const operatorKey = makeOperatorKey(123); + const expectedId = await malicious.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await malicious.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await malicious.setOperatorsWhitelists([expectedId], [clusterOwner]); + const earningsPeriod = 100n; + operatorIds.push(Number(expectedId)); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(earningsPeriod); + + await malicious.setParams(expectedId, MINIMAL_OPERATOR_ETH_FEE); + await expect(malicious.attack()).to.be.revertedWithCustomError(network, Errors.ETH_TRANSFER_FAILED); + }); + + it("Prevents reentrancy in 'claimEthRewards()'", async function () { + const { network, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const malicious = await connection.ethers.deployContract( + "MaliciousClaimEthRewards", + [await network.getAddress()] + ); + await malicious.waitForDeployment(); + + await ssvToken.mint(randomUser.address, STAKE_AMOUNT); + await ssvToken.connect(randomUser).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + await cssvToken.connect(randomUser).transfer(await malicious.getAddress(), STAKE_AMOUNT); + + const oracles = (await connection.ethers.getSigners()).slice(10, 14); + await network.replaceOracle(1, oracles[0].address); + await network.replaceOracle(2, oracles[1].address); + await network.replaceOracle(3, oracles[2].address); + await network.replaceOracle(4, oracles[3].address); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + const clusters = await registerDefaultClusters(connection, network, operatorIds, operatorOwner, 8); + const merkleData = buildEBMerkleForDefaultClusters(connection, clusters, 33); + + const block = await connection.ethers.provider.getBlock("latest"); + const blockNum = block!.number; + + for (let i = 0; i < 3; i++) { + await network.connect(oracles[i]).commitRoot(merkleData.root, blockNum); + } + await updateClusterBalancesForDefaultClusters(network, clusters, merkleData, blockNum, 33); + + await expect(malicious.attack()).to.be.revertedWithCustomError(network, Errors.ETH_TRANSFER_FAILED); + }); + + it("Prevents reentrancy in 'reactivate()'", async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + const Malicious = await connection.ethers.getContractFactory("MaliciousReactivate"); + const malicious = await Malicious.deploy(await network.getAddress()); + await malicious.waitForDeployment(); + + await whitelistAddresses(network, operatorOwner, operatorIds, [await malicious.getAddress()]); + + await malicious.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState(connection, network, await malicious.getAddress(), operatorIds); + + await malicious.setParams(operatorIds, cluster); + await malicious.setReactivateParams(operatorIds, cluster); + await expect(malicious.attack()).to.be.revertedWithCustomError(network, Errors.ETH_TRANSFER_FAILED); + }); + + }); +}); diff --git a/test/integration/SSVNetwork/clusters.test.ts b/test/integration/SSVNetwork/clusters.test.ts new file mode 100644 index 000000000..df53e2de8 --- /dev/null +++ b/test/integration/SSVNetwork/clusters.test.ts @@ -0,0 +1,1111 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import { + registerOperators, + whitelistAddresses, + makePublicKey, + getCurrentClusterState, + registerDefaultCluster, + computeEBRoot, + computeClusterId, + setupTestContext, +} from '../../common/helpers.ts'; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + DEFAULT_ETH_REGISTER_VALUE, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE, +} from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../../common/errors.js'; +import { ethers } from 'ethers'; + +/** + * Enhanced Integration Tests for SSVNetwork Clusters + * + * These tests focus on: + * 1. Balance delta assertions for every ETH-moving operation (deposit, withdraw, liquidate) + * 2. Boundary testing (liquidation thresholds, minimum collateral) + * 3. Multi-block simulation with exact expected values for balance burn + * 4. Basic invariant checks (cluster balance + operator earnings + network fees = deposited) + * 5. Combined scenarios verifying full cluster lifecycle economics + */ +describe("SSVNetwork Integration - Clusters (Enhanced)", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, liquidator] } = await setupTestContext()); + + for (const signer of [operatorOwner, clusterOwner, liquidator]) { + await connection.ethers.provider.send("hardhat_setBalance", [signer.address, "0x3635c9adc5dea00000"]); + } + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Balance Delta Assertions", async function() { + + it("deposit: verifies exact ETH transfer from depositor to contract", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + await connection.ethers.provider.send("hardhat_setBalance", [clusterOwner.address, "0x3635c9adc5dea00000"]); + + const depositAmount = connection.ethers.parseEther("5"); + const balanceBefore = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const contractBalanceBefore = await connection.ethers.provider.getBalance(await network.getAddress()); + const depositorBalanceBefore = await connection.ethers.provider.getBalance(clusterOwner.address); + const blockBefore = await connection.ethers.provider.getBlockNumber(); + + const tx = await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + cluster, + { value: depositAmount } + ); + const receipt = await tx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const blockAfter = receipt!.blockNumber; + + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter = await views.getBalance(clusterOwner.address, operatorIds, clusterAfter); + const contractBalanceAfter = await connection.ethers.provider.getBalance(await network.getAddress()); + const depositorBalanceAfter = await connection.ethers.provider.getBalance(clusterOwner.address); + const blocksDelta = BigInt(blockAfter - blockBefore); + const burnRatePerBlock = (MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE; + const expectedBurn = blocksDelta * burnRatePerBlock; + const expectedBalance = balanceBefore + depositAmount - expectedBurn; + + expect(balanceAfter).to.equal(expectedBalance); + expect(contractBalanceAfter - contractBalanceBefore).to.equal(depositAmount); + expect(depositorBalanceBefore - depositorBalanceAfter).to.equal(depositAmount + gasUsed); + }); + + it("withdraw: verifies exact ETH transfer from contract to owner", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds, receiptRegister } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + const balanceBefore = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const withdrawAmount = balanceBefore / 2n; + + const contractBalanceBefore = await connection.ethers.provider.getBalance(await network.getAddress()); + const ownerBalanceBefore = await connection.ethers.provider.getBalance(clusterOwner.address); + const blockRegister = receiptRegister.blockNumber; + + const tx = await network.connect(clusterOwner).withdraw(operatorIds, withdrawAmount, cluster); + const receipt = await tx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const blockWithdraw = receipt!.blockNumber; + + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter = await views.getBalance(clusterOwner.address, operatorIds, clusterAfter); + const contractBalanceAfter = await connection.ethers.provider.getBalance(await network.getAddress()); + const ownerBalanceAfter = await connection.ethers.provider.getBalance(clusterOwner.address); + expect(contractBalanceBefore - contractBalanceAfter).to.equal(withdrawAmount); + expect(ownerBalanceAfter + gasUsed - ownerBalanceBefore).to.equal(withdrawAmount); + const blocksDelta = BigInt(blockWithdraw - blockRegister); + const burnRatePerBlock = (MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE; + const expectedBurn = blocksDelta * burnRatePerBlock; + const expectedBalanceDecrease = withdrawAmount + expectedBurn; + + expect(balanceBefore - balanceAfter).to.equal(expectedBalanceDecrease); + }); + + it("liquidate: liquidator receives remaining cluster balance", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const highNetworkFee = NETWORK_FEE * 100n; + await network.updateNetworkFee(highNetworkFee); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const txRegister = await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receiptRegister = await txRegister.wait(); + const blockRegister = receiptRegister!.blockNumber; + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + let isLiquidatable = false; + let attempts = 0; + while (!isLiquidatable && attempts < 20) { + await connection.networkHelpers.mine(100000); + isLiquidatable = await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster); + attempts++; + } + expect(isLiquidatable).to.be.true; + const liquidatorBalanceBefore = await connection.ethers.provider.getBalance(liquidator.address); + const contractBalanceBefore = await connection.ethers.provider.getBalance(await network.getAddress()); + + const tx = await network.connect(liquidator).liquidate( + clusterOwner.address, + operatorIds, + currentCluster + ); + const receipt = await tx.wait(); + const gasUsed = receipt!.gasUsed * receipt!.gasPrice; + const blockLiquidate = receipt!.blockNumber; + + const liquidatorBalanceAfter = await connection.ethers.provider.getBalance(liquidator.address); + const contractBalanceAfter = await connection.ethers.provider.getBalance(await network.getAddress()); + const blocksDelta = BigInt(blockLiquidate - blockRegister); + const burnRatePerBlock = (MINIMAL_OPERATOR_ETH_FEE * 4n) + highNetworkFee; + const totalFees = blocksDelta * burnRatePerBlock; + const expectedRemainingBalance = DEFAULT_ETH_REGISTER_VALUE - totalFees; + const actualLiquidatorReward = expectedRemainingBalance > 0n ? expectedRemainingBalance : 0n; + const liquidatorGain = liquidatorBalanceAfter + gasUsed - liquidatorBalanceBefore; + expect(liquidatorGain).to.equal(actualLiquidatorReward); + expect(contractBalanceBefore - contractBalanceAfter).to.equal(actualLiquidatorReward); + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfter.active).to.equal(false); + expect(await views.isLiquidated(clusterOwner.address, operatorIds, clusterAfter)).to.equal(true); + }); + }); + + describe("Multi-Block Simulation - Cluster Balance Burn", async function() { + + it("Cluster balance decreases exactly by burn rate per block", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const initialBalance = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + const expectedBurnRatePerBlock = (MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE; + const checkpoints = [10n, 50n, 100n, 200n]; + let totalBlocksMined = 0n; + + for (const blocks of checkpoints) { + await connection.networkHelpers.mine(blocks); + totalBlocksMined += blocks; + + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + const expectedBalance = initialBalance - (totalBlocksMined * expectedBurnRatePerBlock); + + expect(currentBalance).to.equal( + expectedBalance, + `Balance mismatch at block ${totalBlocksMined}` + ); + } + }); + + it("Burn rate scales with validator count", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter1Validator = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + await connection.networkHelpers.mine(100n); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter100Blocks = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + + const burnRateWith1Validator = balanceAfter1Validator - balanceAfter100Blocks; + const expectedBurnRate1 = 100n * ((MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE); + expect(burnRateWith1Validator).to.equal(expectedBurnRate1); + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + currentCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter2Validators = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + await connection.networkHelpers.mine(100n); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter2Val100Blocks = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + + const burnRateWith2Validators = balanceAfter2Validators - balanceAfter2Val100Blocks; + const expectedBurnRate2 = 100n * ((MINIMAL_OPERATOR_ETH_FEE * 4n * 2n) + (NETWORK_FEE * 2n)); + expect(burnRateWith2Validators).to.equal(expectedBurnRate2); + expect(burnRateWith2Validators).to.equal(burnRateWith1Validator * 2n); + }); + + it("removeValidator settles exact fee deduction from cluster balance", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const clusterAfterReg = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const burnRatePerBlock = (MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE; + const blocksToMine = 100n; + + await connection.networkHelpers.mine(blocksToMine); + + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, clusterAfterReg); + const clusterAfterRemove = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + const totalFeeDeducted = (blocksToMine + 1n) * burnRatePerBlock; + const expectedBalance = DEFAULT_ETH_REGISTER_VALUE - totalFeeDeducted; + + const remainingBalance = await views.getBalance(clusterOwner.address, operatorIds, clusterAfterRemove); + expect(remainingBalance).to.equal(expectedBalance); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + }); + }); + + describe("Invariant Checks - Balance Conservation", async function() { + + it("Invariant: Deposited = ClusterBalance + OperatorEarnings + NetworkEarnings", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const depositAmount = DEFAULT_ETH_REGISTER_VALUE; + const networkEarningsBefore = await views.getNetworkEarnings(); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositAmount } + ); + const blocks = 500n; + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await connection.networkHelpers.mine(blocks); + const clusterBalance = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + + let totalOperatorEarnings = 0n; + for (const opId of operatorIds) { + totalOperatorEarnings += await views.getOperatorEarnings(opId); + } + + const networkEarningsAfter = await views.getNetworkEarnings(); + const networkEarningsDelta = networkEarningsAfter - networkEarningsBefore; + const totalAccounted = clusterBalance + totalOperatorEarnings + networkEarningsDelta; + expect(totalAccounted).to.equal(depositAmount, "Balance invariant violated: total accounted must equal deposited"); + }); + + it("Invariant: Withdrawal reduces cluster balance exactly", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds, receiptRegister } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + const balanceBefore = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const withdrawAmount = connection.ethers.parseEther("1"); + const blockRegister = receiptRegister.blockNumber; + + const tx = await network.connect(clusterOwner).withdraw(operatorIds, withdrawAmount, cluster); + const receipt = await tx.wait(); + const blockWithdraw = receipt!.blockNumber; + + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter = await views.getBalance(clusterOwner.address, operatorIds, clusterAfter); + const blocksDelta = BigInt(blockWithdraw - blockRegister); + const burnRatePerBlock = (MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE; + const expectedBurn = blocksDelta * burnRatePerBlock; + const expectedBalanceDecrease = withdrawAmount + expectedBurn; + + expect(balanceBefore - balanceAfter).to.equal(expectedBalanceDecrease); + }); + + it("Invariant: Deposit increases cluster balance exactly", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds, receiptRegister } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + await connection.ethers.provider.send("hardhat_setBalance", [clusterOwner.address, "0x3635c9adc5dea00000"]); + const balanceBefore = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const depositAmount = connection.ethers.parseEther("5"); + const blockRegister = receiptRegister.blockNumber; + + const tx = await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + cluster, + { value: depositAmount } + ); + const receipt = await tx.wait(); + const blockDeposit = receipt!.blockNumber; + + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfter = await views.getBalance(clusterOwner.address, operatorIds, clusterAfter); + const blocksDelta = BigInt(blockDeposit - blockRegister); + const burnRatePerBlock = (MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE; + const expectedBurn = blocksDelta * burnRatePerBlock; + const expectedBalanceIncrease = depositAmount - expectedBurn; + + expect(balanceAfter - balanceBefore).to.equal(expectedBalanceIncrease); + }); + }); + + describe("Liquidation Boundary Tests", async function() { + + it("Cluster is not liquidatable just above threshold", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const isLiquidatable = await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster); + expect(isLiquidatable).to.equal(false); + await expect( + network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, currentCluster) + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_NOT_LIQUIDATABLE); + }); + + it("Owner can self-liquidate even when not underfunded", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + const isLiquidatable = await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster); + expect(isLiquidatable).to.equal(false); + + const networkAddress = await network.getAddress(); + const ownerBalanceBefore = await connection.ethers.provider.getBalance(clusterOwner.address); + const contractBalanceBefore = await connection.ethers.provider.getBalance(networkAddress); + + const tx = await network.connect(clusterOwner).liquidate( + clusterOwner.address, + operatorIds, + currentCluster + ); + await expect(tx).to.emit(network, Events.CLUSTER_LIQUIDATED); + const receipt = await tx.wait(); + const gasCost = receipt!.gasUsed * (receipt!.effectiveGasPrice ?? receipt!.gasPrice); + + const ownerBalanceAfter = await connection.ethers.provider.getBalance(clusterOwner.address); + const contractBalanceAfter = await connection.ethers.provider.getBalance(networkAddress); + + const payout = contractBalanceBefore - contractBalanceAfter; + expect(payout).to.be.greaterThan(0n); + expect(ownerBalanceAfter - ownerBalanceBefore + gasCost).to.equal(payout); + + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfter.active).to.equal(false); + }); + + it("Reactivation requires sufficient balance to avoid immediate liquidation", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await network.updateNetworkFee(NETWORK_FEE * 100n); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + let attempts = 0; + while (!(await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster)) && attempts < 20) { + await connection.networkHelpers.mine(100000); + attempts++; + } + await network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, currentCluster); + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(liquidatedCluster.active).to.equal(false); + await expect( + network.connect(clusterOwner).reactivate( + operatorIds, + liquidatedCluster, + { value: 1n } + ) + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + const tx = await network.connect(clusterOwner).reactivate( + operatorIds, + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await expect(tx).to.emit(network, Events.CLUSTER_REACTIVATED); + + const reactivatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(reactivatedCluster.active).to.equal(true); + }); + }); + + describe("Combined Scenarios - Full Lifecycle Economics", async function() { + + it("Full lifecycle: register → operate → withdraw → deposit → liquidate → reactivate", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await network.updateNetworkFee(NETWORK_FEE * 100n); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(currentCluster.active).to.equal(true); + const initialBalance = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + await connection.networkHelpers.mine(100n); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfterOperation = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + expect(balanceAfterOperation).to.be.lessThan(initialBalance); + const operatorEarnings = await views.getOperatorEarnings(operatorIds[0]); + expect(operatorEarnings).to.be.greaterThan(0n); + const withdrawAmount = connection.ethers.parseEther("0.1"); + await network.connect(clusterOwner).withdraw(operatorIds, withdrawAmount, currentCluster); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfterWithdraw = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + expect(balanceAfterWithdraw).to.be.lessThan(balanceAfterOperation); + await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + currentCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfterDeposit = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + expect(balanceAfterDeposit).to.be.greaterThan(balanceAfterWithdraw); + let attempts = 0; + while (!(await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster)) && attempts < 30) { + await connection.networkHelpers.mine(100000); + attempts++; + } + expect(await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster)).to.be.true; + + await network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, currentCluster); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(currentCluster.active).to.equal(false); + await network.connect(clusterOwner).reactivate( + operatorIds, + currentCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(currentCluster.active).to.equal(true); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, currentCluster); + currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(currentCluster.validatorCount).to.equal(0n); + const finalBalance = await views.getBalance(clusterOwner.address, operatorIds, currentCluster); + expect(finalBalance).to.be.greaterThanOrEqual(0n); + }); + + it("Third-party deposit doesn't affect owner's ability to withdraw", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + const balanceBeforeThirdParty = await views.getBalance(clusterOwner.address, operatorIds, cluster); + await network.connect(liquidator).deposit( + clusterOwner.address, + operatorIds, + cluster, + { value: connection.ethers.parseEther("2") } + ); + + const clusterAfterDeposit = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const balanceAfterThirdParty = await views.getBalance(clusterOwner.address, operatorIds, clusterAfterDeposit); + expect(balanceAfterThirdParty).to.be.greaterThan(balanceBeforeThirdParty); + const withdrawAmount = balanceAfterThirdParty / 2n; + const tx = await network.connect(clusterOwner).withdraw(operatorIds, withdrawAmount, clusterAfterDeposit); + await expect(tx).to.emit(network, Events.CLUSTER_WITHDRAWN); + }); + }); + + describe("Edge Cases and Error Conditions", async function() { + + it("Cannot withdraw more than available balance", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + const balance = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const excessiveAmount = balance * 2n; + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, excessiveAmount, cluster) + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Cannot withdraw if it would make cluster liquidatable", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + const balance = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const excessiveAmount = balance - 1n; + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, excessiveAmount, cluster) + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Deposit with stale cluster state reverts", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + const staleCluster = { ...cluster, balance: cluster.balance + 1n }; + + await expect( + network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + staleCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ) + ).to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Cannot reactivate an already active cluster", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + await expect( + network.connect(clusterOwner).reactivate( + operatorIds, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ) + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_ALREADY_ENABLED); + }); + + it("Cannot operate on non-existent cluster", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect( + network.deposit( + clusterOwner.address, + operatorIds, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ) + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, 1000n, EMPTY_CLUSTER) + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("updateClusterBalance succeeds on a liquidated cluster, emits ClusterBalanceUpdated with cluster still inactive", async function() { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const activeCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(activeCluster.active).to.equal(true); + + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, activeCluster); + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(liquidatedCluster.active).to.equal(false); + + await network.updateQuorumBps(1000); + await network.replaceOracle(1, operatorOwner.address); + await network.updateMinBlocksBetweenUpdates(1); + + const stakeAmount = ethers.parseEther("10"); + await ssvToken.mint(clusterOwner.address, stakeAmount); + await ssvToken.connect(clusterOwner).approve(await network.getAddress(), stakeAmount); + await network.connect(clusterOwner).stake(stakeAmount); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 33; + const ebRoot = computeEBRoot(clusterId, effectiveBalance); + + const blockNum = await connection.ethers.provider.getBlockNumber(); + + await network.connect(operatorOwner).commitRoot(ebRoot, blockNum); + + const tx = await network.updateClusterBalance( + blockNum, clusterOwner.address, operatorIds, liquidatedCluster, effectiveBalance, [] + ); + const receipt = await tx.wait(); + await expect(tx).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + + const clusterAfterUpdate = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds) + expect(clusterAfterUpdate).to.not.be.null; + expect(clusterAfterUpdate!.active).to.equal(false); + expect(clusterAfterUpdate!.balance).to.equal(0n); + + const effectiveBalance2 = 64; + const ebRoot2 = computeEBRoot(clusterId, effectiveBalance2); + + const blockNum2 = await connection.ethers.provider.getBlockNumber(); + await network.connect(operatorOwner).commitRoot(ebRoot2, blockNum2); + const tx2 = await network.updateClusterBalance( + blockNum2, clusterOwner.address, operatorIds, liquidatedCluster, effectiveBalance2, [] + ); + await expect(tx2).to.emit(network, Events.CLUSTER_BALANCE_UPDATED); + + const finalCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(finalCluster.active).to.equal(false); + }); + + it("Is reverted with 'InsufficientBalance' when withdrawing from a liquidated cluster with zero balance", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await network.updateNetworkFee(NETWORK_FEE * 100n); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + let attempts = 0; + while (!(await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster)) && attempts < 20) { + await connection.networkHelpers.mine(100000); + attempts++; + } + + await network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, currentCluster); + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, 1n, liquidatedCluster) + ).to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Allows deposit to liquidated cluster and subsequent withdrawal without reactivation", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await network.updateNetworkFee(NETWORK_FEE * 100n); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const activeCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(activeCluster.active).to.equal(true); + expect(activeCluster.validatorCount).to.equal(1n); + let currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + let attempts = 0; + while (!(await views.isLiquidatable(clusterOwner.address, operatorIds, currentCluster)) && attempts < 20) { + await connection.networkHelpers.mine(100000); + attempts++; + } + expect(attempts).to.be.lessThan(20, "Cluster should have become liquidatable"); + const networkBalanceBefore = await connection.ethers.provider.getBalance(await network.getAddress()); + + await network.connect(liquidator).liquidate(clusterOwner.address, operatorIds, currentCluster); + + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(liquidatedCluster.active).to.equal(false); + expect(liquidatedCluster.balance).to.equal(0n); + expect(liquidatedCluster.validatorCount).to.equal(1n); + const depositAmount = connection.ethers.parseEther("5"); + const ownerBalanceBeforeDeposit = await connection.ethers.provider.getBalance(clusterOwner.address); + + const depositTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + const depositReceipt = await depositTx.wait(); + const depositGasCost = depositReceipt!.gasUsed * depositReceipt!.gasPrice; + + const clusterAfterDeposit = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfterDeposit.active).to.equal(false); + expect(clusterAfterDeposit.balance).to.equal(depositAmount); + expect(clusterAfterDeposit.validatorCount).to.equal(1n); + const networkBalanceAfterDeposit = await connection.ethers.provider.getBalance(await network.getAddress()); + expect(networkBalanceAfterDeposit - networkBalanceBefore).to.equal(depositAmount); + const ownerBalanceAfterDeposit = await connection.ethers.provider.getBalance(clusterOwner.address); + expect(ownerBalanceBeforeDeposit - ownerBalanceAfterDeposit).to.equal(depositAmount + depositGasCost); + const ownerBalanceBeforeWithdraw = await connection.ethers.provider.getBalance(clusterOwner.address); + const networkBalanceBeforeWithdraw = await connection.ethers.provider.getBalance(await network.getAddress()); + + const withdrawAmount = depositAmount; + const withdrawTx = await network.connect(clusterOwner).withdraw( + operatorIds, + withdrawAmount, + clusterAfterDeposit + ); + const withdrawReceipt = await withdrawTx.wait(); + const withdrawGasCost = withdrawReceipt!.gasUsed * withdrawReceipt!.gasPrice; + + await expect(withdrawTx) + .to.emit(network, Events.CLUSTER_WITHDRAWN) + .withArgs( + clusterOwner.address, + operatorIds, + withdrawAmount, + [1n, 0n, 0n, false, 0n] + ); + const clusterAfterWithdraw = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfterWithdraw.active).to.equal(false); + expect(clusterAfterWithdraw.balance).to.equal(0n); + expect(clusterAfterWithdraw.validatorCount).to.equal(1n); + const ownerBalanceAfterWithdraw = await connection.ethers.provider.getBalance(clusterOwner.address); + const ownerBalanceDelta = ownerBalanceAfterWithdraw - ownerBalanceBeforeWithdraw; + expect(ownerBalanceDelta).to.equal(withdrawAmount - withdrawGasCost); + const networkBalanceAfterWithdraw = await connection.ethers.provider.getBalance(await network.getAddress()); + expect(networkBalanceBeforeWithdraw - networkBalanceAfterWithdraw).to.equal(withdrawAmount); + const ownerBalanceFinal = await connection.ethers.provider.getBalance(clusterOwner.address); + const totalGasSpent = depositGasCost + withdrawGasCost; + expect(ownerBalanceFinal).to.equal(ownerBalanceBeforeDeposit - totalGasSpent); + }); + + it("Reverts withdraw from liquidated cluster when using stale pre-deposit cluster state", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const activeCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, activeCluster); + + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(liquidatedCluster.active).to.equal(false); + expect(liquidatedCluster.balance).to.equal(0n); + + const depositAmount = connection.ethers.parseEther("1"); + await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + + await expect( + network.connect(clusterOwner).withdraw(operatorIds, depositAmount, liquidatedCluster) + ).to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Does not change operator or DAO earnings when withdrawing from a liquidated cluster with pre-existing earnings", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const sumOperatorEarnings = async (operatorIds: number[]) => { + let total = 0n; + for (const opId of operatorIds) { + total += await views.getOperatorEarnings(opId); + } + return total; + }; + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const activeCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await connection.networkHelpers.mine(100); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, activeCluster); + + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(liquidatedCluster.active).to.equal(false); + + const operatorEarningsBefore = await sumOperatorEarnings(operatorIds); + const daoEarningsBefore = await views.getNetworkEarnings(); + + const depositAmount = connection.ethers.parseEther("2"); + await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + + const clusterAfterDeposit = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).withdraw(operatorIds, depositAmount, clusterAfterDeposit); + + const operatorEarningsAfter = await sumOperatorEarnings(operatorIds); + const daoEarningsAfter = await views.getNetworkEarnings(); + + expect(operatorEarningsAfter).to.equal(operatorEarningsBefore); + expect(daoEarningsAfter).to.equal(daoEarningsBefore); + }); + + it("Maintains global ETH accounting invariant after liquidated cluster withdrawal", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const calculateInvariant = async () => { + const contractBalance = await connection.ethers.provider.getBalance(await network.getAddress()); + const clusterBalance = BigInt((await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds)).balance); + let totalOperatorEarnings = 0n; + for (let i = 0; i < operatorIds.length; i++) { + const earnings = await views.getOperatorEarnings(operatorIds[i]); + totalOperatorEarnings += earnings; + } + const daoBalance = await views.getNetworkEarnings(); + const stakingBalance = await views.stakingEthPoolBalance(); + + const expectedBalance = clusterBalance + totalOperatorEarnings + daoBalance + stakingBalance; + + return { contractBalance, expectedBalance, clusterBalance, totalOperatorEarnings, daoBalance, stakingBalance }; + }; + await network.updateNetworkFee(NETWORK_FEE * 100n); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + let invariant = await calculateInvariant(); + expect(invariant.contractBalance).to.equal(invariant.expectedBalance); + const clusterBeforeLiquidation = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, clusterBeforeLiquidation); + invariant = await calculateInvariant(); + expect(invariant.contractBalance).to.equal(invariant.expectedBalance); + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const depositAmount = connection.ethers.parseEther("5"); + + await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + invariant = await calculateInvariant(); + expect(invariant.contractBalance).to.equal(invariant.expectedBalance); + const clusterAfterDeposit = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const partialWithdraw = connection.ethers.parseEther("3"); + + await network.connect(clusterOwner).withdraw(operatorIds, partialWithdraw, clusterAfterDeposit); + invariant = await calculateInvariant(); + expect(invariant.contractBalance).to.equal(invariant.expectedBalance); + const clusterAfterPartialWithdraw = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const remainingWithdraw = connection.ethers.parseEther("2"); + + await network.connect(clusterOwner).withdraw(operatorIds, remainingWithdraw, clusterAfterPartialWithdraw); + invariant = await calculateInvariant(); + expect(invariant.contractBalance).to.equal(invariant.expectedBalance); + const finalCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(finalCluster.balance).to.equal(0n); + }); + + it("Allows withdrawal from liquidated cluster even if one operator was removed", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await network.updateNetworkFee(NETWORK_FEE * 100n); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const activeCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(activeCluster.active).to.equal(true); + expect(activeCluster.validatorCount).to.equal(1n); + await network.connect(operatorOwner).removeOperator(operatorIds[0]); + const removedOperatorDetails = await views.getOperatorById(operatorIds[0]); + expect(removedOperatorDetails[0]).to.not.equal(connection.ethers.ZeroAddress); + const currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, currentCluster); + + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(liquidatedCluster.active).to.equal(false); + const depositAmount = connection.ethers.parseEther("4"); + + const depositTx = await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + await depositTx.wait(); + + const clusterAfterDeposit = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfterDeposit.balance).to.equal(depositAmount); + const ownerBalanceBefore = await connection.ethers.provider.getBalance(clusterOwner.address); + + const withdrawTx = await network.connect(clusterOwner).withdraw( + operatorIds, + depositAmount, + clusterAfterDeposit + ); + const withdrawReceipt = await withdrawTx.wait(); + const withdrawGasCost = withdrawReceipt!.gasUsed * withdrawReceipt!.gasPrice; + const clusterAfterWithdraw = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfterWithdraw.balance).to.equal(0n); + expect(clusterAfterWithdraw.active).to.equal(false); + const ownerBalanceAfter = await connection.ethers.provider.getBalance(clusterOwner.address); + expect(ownerBalanceAfter - ownerBalanceBefore).to.equal(depositAmount - withdrawGasCost); + }); + + it("Allows reactivation after partial withdrawal from liquidated cluster", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await network.updateNetworkFee(NETWORK_FEE * 100n); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, currentCluster); + + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(liquidatedCluster.active).to.equal(false); + const depositAmount = connection.ethers.parseEther("10"); + + await network.connect(clusterOwner).deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + + const clusterAfterDeposit = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfterDeposit.balance).to.equal(depositAmount); + const partialWithdrawAmount = connection.ethers.parseEther("3"); + + await network.connect(clusterOwner).withdraw( + operatorIds, + partialWithdrawAmount, + clusterAfterDeposit + ); + + const clusterAfterPartialWithdraw = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(clusterAfterPartialWithdraw.balance).to.equal(depositAmount - partialWithdrawAmount); + expect(clusterAfterPartialWithdraw.active).to.equal(false); + const reactivationDeposit = connection.ethers.parseEther("3"); + + const reactivateTx = await network.connect(clusterOwner).reactivate( + operatorIds, + clusterAfterPartialWithdraw, + { value: reactivationDeposit } + ); + + await expect(reactivateTx) + .to.emit(network, Events.CLUSTER_REACTIVATED); + const reactivatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(reactivatedCluster.active).to.equal(true); + expect(reactivatedCluster.validatorCount).to.equal(1n); + expect(reactivatedCluster.balance).to.equal( + depositAmount - partialWithdrawAmount + reactivationDeposit + ); + const isLiquidatable = await views.isLiquidatable(clusterOwner.address, operatorIds, reactivatedCluster); + expect(isLiquidatable).to.equal(false); + }); + }); +}); diff --git a/test/integration/SSVNetwork/commitRootUpdateClusterBalance.test.ts b/test/integration/SSVNetwork/commitRootUpdateClusterBalance.test.ts new file mode 100644 index 000000000..86c3f701e --- /dev/null +++ b/test/integration/SSVNetwork/commitRootUpdateClusterBalance.test.ts @@ -0,0 +1,243 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { getTestConnection } from "../../setup/connection.ts"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { Cluster, NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + whitelistAddresses, + makePublicKey, + parseClusterFromEvent, + generateMerkleForClusterEB, +} from "../../common/helpers.ts"; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, + STAKE_AMOUNT, + BPS_DENOMINATOR, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; + +describe("ITEST-1 Integration: commitRoot -> updateClusterBalance E2E", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwnerA: HardhatEthersSigner; + let clusterOwnerB: HardhatEthersSigner; + let staker: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + const signers = await connection.ethers.getSigners(); + operatorOwner = signers[1]; + clusterOwnerA = signers[2]; + clusterOwnerB = signers[3]; + staker = signers[4]; + [oracle1, oracle2, oracle3, oracle4] = signers.slice(10, 14); + }); + + const deployFixture = async () => ssvNetworkFullFixture(connection); + + const getClusterId = (ownerAddress: string, operatorIds: number[]): string => { + return ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [ownerAddress, operatorIds.map(BigInt)]) + ); + }; + + const toClusterArg = (cluster: Cluster) => ({ + validatorCount: Number(cluster.validatorCount), + networkFeeIndex: BigInt(cluster.networkFeeIndex), + index: BigInt(cluster.index), + active: cluster.active, + balance: BigInt(cluster.balance), + }); + + const setupOraclesAndStake = async (network: any, ssvToken: any): Promise => { + const oracles = [oracle1, oracle2, oracle3, oracle4]; + for (let i = 0; i < oracles.length; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + return oracles; + }; + + const commitRootWithThreeOracles = async ( + network: any, + oracles: HardhatEthersSigner[], + root: string, + blockNum: number + ) => { + const tx1 = await network.connect(oracles[0]).commitRoot(root, blockNum); + await expect(tx1).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + + const tx2 = await network.connect(oracles[1]).commitRoot(root, blockNum); + await expect(tx2).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED); + + const tx3 = await network.connect(oracles[2]).commitRoot(root, blockNum); + await expect(tx3).to.emit(network, Events.ROOT_COMMITTED).withArgs(root, blockNum); + }; + + const registerOneValidatorCluster = async ( + network: any, + owner: HardhatEthersSigner, + operatorIds: number[], + validatorSeed: number + ): Promise<{ cluster: Cluster; registerBlock: bigint }> => { + const tx = await network.connect(owner).registerValidator( + makePublicKey(validatorSeed), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + return { + cluster: parseClusterFromEvent(network, receipt, Events.VALIDATOR_ADDED), + registerBlock: BigInt(receipt.blockNumber), + }; + }; + + it("3 oracles commit root, then updateClusterBalance applies EB=64 and doubles post-update fee accrual", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFixture); + const oracles = await setupOraclesAndStake(network, ssvToken); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwnerA.address]); + + const { cluster } = await registerOneValidatorCluster(network, clusterOwnerA, operatorIds, 1); + + const clusterId = getClusterId(clusterOwnerA.address, operatorIds); + const { root, proofs } = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance: 64 }]); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + await commitRootWithThreeOracles(network, oracles, root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(root); + + const updateTx = await network.updateClusterBalance( + blockNum, + clusterOwnerA.address, + operatorIds.map(BigInt), + toClusterArg(cluster), + 64, + proofs[clusterId] + ); + const updateReceipt = await updateTx.wait(); + const clusterAfterUpdate = parseClusterFromEvent(network, updateReceipt, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfterUpdate.active).to.equal(true); + + const blocksToMine = 40; + const earningsBefore = await views.getOperatorEarnings(operatorIds[0]); + await networkHelpers.mine(blocksToMine); + const earningsAfter = await views.getOperatorEarnings(operatorIds[0]); + + const actualDelta = earningsAfter - earningsBefore; + const expectedPostUpdateDelta = BigInt(blocksToMine) * MINIMAL_OPERATOR_ETH_FEE * 2n; + const expectedBaselineDelta = BigInt(blocksToMine) * MINIMAL_OPERATOR_ETH_FEE; + + expect(expectedPostUpdateDelta).to.equal(expectedBaselineDelta * 2n); + expect(actualDelta).to.equal(expectedPostUpdateDelta); + }); + + it("Two clusters update from the same committed root and settle independently per-cluster", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFixture); + const oracles = await setupOraclesAndStake(network, ssvToken); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses( + network, + operatorOwner, + operatorIds, + [clusterOwnerA.address, clusterOwnerB.address] + ); + + const { cluster: clusterA } = await registerOneValidatorCluster(network, clusterOwnerA, operatorIds, 1); + const { cluster: clusterB } = await registerOneValidatorCluster(network, clusterOwnerB, operatorIds, 2); + + const clusterIdA = getClusterId(clusterOwnerA.address, operatorIds); + const clusterIdB = getClusterId(clusterOwnerB.address, operatorIds); + + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId: clusterIdA, effectiveBalance: 32 }, + { clusterId: clusterIdB, effectiveBalance: 64 }, + ]); + + const blockNum = await connection.ethers.provider.getBlockNumber(); + await commitRootWithThreeOracles(network, oracles, root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(root); + + const updateTxA = await network.updateClusterBalance( + blockNum, + clusterOwnerA.address, + operatorIds.map(BigInt), + toClusterArg(clusterA), + 32, + proofs[clusterIdA] + ); + const updateReceiptA = await updateTxA.wait(); + const clusterAAfterUpdate = parseClusterFromEvent(network, updateReceiptA, Events.CLUSTER_BALANCE_UPDATED); + + const updateTxB = await network.updateClusterBalance( + blockNum, + clusterOwnerB.address, + operatorIds.map(BigInt), + toClusterArg(clusterB), + 64, + proofs[clusterIdB] + ); + const updateReceiptB = await updateTxB.wait(); + const clusterBAfterUpdate = parseClusterFromEvent(network, updateReceiptB, Events.CLUSTER_BALANCE_UPDATED); + + const blockBeforeAccrual = await connection.ethers.provider.getBlockNumber(); + const earningsBefore = await views.getOperatorEarnings(operatorIds[0]); + const balanceABefore = await views.getBalance( + clusterOwnerA.address, + operatorIds, + toClusterArg(clusterAAfterUpdate) + ); + const balanceBBefore = await views.getBalance( + clusterOwnerB.address, + operatorIds, + toClusterArg(clusterBAfterUpdate) + ); + + const blocksToMine = 25; + await networkHelpers.mine(blocksToMine); + const blockAfterAccrual = await connection.ethers.provider.getBlockNumber(); + + const earningsAfter = await views.getOperatorEarnings(operatorIds[0]); + const balanceAAfter = await views.getBalance( + clusterOwnerA.address, + operatorIds, + toClusterArg(clusterAAfterUpdate) + ); + const balanceBAfter = await views.getBalance( + clusterOwnerB.address, + operatorIds, + toClusterArg(clusterBAfterUpdate) + ); + + const blocksDelta = BigInt(blockAfterAccrual - blockBeforeAccrual); + const combinedExpectedEarningsDelta = blocksDelta * MINIMAL_OPERATOR_ETH_FEE * 3n; + expect(earningsAfter - earningsBefore).to.equal(combinedExpectedEarningsDelta); + + const feeRatePerBlockAtDefaultVUnits = (4n * MINIMAL_OPERATOR_ETH_FEE) + (await views.getNetworkFee()); + const expectedBalanceDeltaA = blocksDelta * feeRatePerBlockAtDefaultVUnits; + const expectedBalanceDeltaB = (blocksDelta * feeRatePerBlockAtDefaultVUnits * 20_000n) / BPS_DENOMINATOR; + + expect(balanceABefore - balanceAAfter).to.equal(expectedBalanceDeltaA); + expect(balanceBBefore - balanceBAfter).to.equal(expectedBalanceDeltaB); + }); +}); diff --git a/test/integration/SSVNetwork/dao.test.ts b/test/integration/SSVNetwork/dao.test.ts new file mode 100644 index 000000000..2ae494c60 --- /dev/null +++ b/test/integration/SSVNetwork/dao.test.ts @@ -0,0 +1,215 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import { STAKE_AMOUNT } from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../../common/errors.js'; +import { ethers } from 'ethers'; +import { setupTestContext } from '../../common/helpers.ts'; + +describe("SSVNetwork Integration - DAO Oracle Quorum", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let staker: HardhatEthersSigner; + let oracles: HardhatEthersSigner[]; + + const numberOfOracles = 4n; + + before(async function () { + let signers: HardhatEthersSigner[]; + ({ connection, networkHelpers, signers } = await setupTestContext()); + staker = signers[2]; + oracles = signers.slice(10, 14); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + + const setupOraclesAndStake = async (network: any, ssvToken: any) => { + for (let i = 0; i < oracles.length; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + return { weight: STAKE_AMOUNT / numberOfOracles }; + }; + + describe("Oracle Quorum — 100% threshold (quorumBps = 10000)", async function () { + it("First three oracle votes emit WeightedRootProposed; fourth vote commits the root", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { weight } = await setupOraclesAndStake(network, ssvToken); + + await network.updateQuorumBps(10000); + expect(await views.getQuorumBps()).to.equal(10000n); + + const root = ethers.keccak256(ethers.toUtf8Bytes("100pct-quorum")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const threshold = STAKE_AMOUNT; + + const tx1 = await network.connect(oracles[0]).commitRoot(root, blockNum); + await expect(tx1).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight, threshold, 1, oracles[0].address); + expect(await views.getCommittedRoot(blockNum)).to.equal(ethers.ZeroHash); + + const tx2 = await network.connect(oracles[1]).commitRoot(root, blockNum); + await expect(tx2).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 2n, threshold, 2, oracles[1].address); + expect(await views.getCommittedRoot(blockNum)).to.equal(ethers.ZeroHash); + + const tx3 = await network.connect(oracles[2]).commitRoot(root, blockNum); + await expect(tx3).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 3n, threshold, 3, oracles[2].address); + expect(await views.getCommittedRoot(blockNum)).to.equal(ethers.ZeroHash); + + const tx4 = await network.connect(oracles[3]).commitRoot(root, blockNum); + await expect(tx4).to.emit(network, Events.ROOT_COMMITTED).withArgs(root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(root); + }); + }); + + describe("Oracle Quorum — 1 bps minimum threshold (quorumBps = 1)", async function () { + it("Single oracle vote immediately commits root when quorumBps is 1", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await setupOraclesAndStake(network, ssvToken); + + await network.updateQuorumBps(1); + expect(await views.getQuorumBps()).to.equal(1n); + + const root = ethers.keccak256(ethers.toUtf8Bytes("1bps-quorum")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const tx = await network.connect(oracles[0]).commitRoot(root, blockNum); + await expect(tx).to.emit(network, Events.ROOT_COMMITTED).withArgs(root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(root); + }); + }); + + describe("Oracle Quorum — oracle replaced between votes", async function () { + it("Pre-replacement vote still counts; old oracle loses rights, new oracle gets AlreadyVoted for reused slot", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await setupOraclesAndStake(network, ssvToken); + + const root = ethers.keccak256(ethers.toUtf8Bytes("mid-replace")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const newOracle = (await connection.ethers.getSigners())[7]; + + await network.connect(oracles[0]).commitRoot(root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(ethers.ZeroHash); + + await network.replaceOracle(1, newOracle.address); + expect(await views.getOracle(1)).to.equal(newOracle.address); + + await expect(network.connect(oracles[0]).commitRoot(root, blockNum)) + .to.be.revertedWithCustomError(network, Errors.NOT_ORACLE); + + await expect(network.connect(newOracle).commitRoot(root, blockNum)) + .to.be.revertedWithCustomError(network, Errors.ALREADY_VOTED); + + await network.connect(oracles[1]).commitRoot(root, blockNum); + const finalTx = await network.connect(oracles[2]).commitRoot(root, blockNum); + await expect(finalTx).to.emit(network, Events.ROOT_COMMITTED).withArgs(root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(root); + }); + }); + + describe("Oracle Quorum — completely new address replaces oracle slot", async function () { + it("New oracle is blocked from the in-flight vote but has full slot ownership for subsequent blocks", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { weight } = await setupOraclesAndStake(network, ssvToken); + + const brandNewOracle = (await connection.ethers.getSigners())[8]; + + const root = ethers.keccak256(ethers.toUtf8Bytes("brand-new-replacement")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const threshold = (STAKE_AMOUNT * 7500n) / 10000n; + + await network.connect(oracles[0]).commitRoot(root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(ethers.ZeroHash); + + await network.replaceOracle(1, brandNewOracle.address); + expect(await views.getOracle(1)).to.equal(brandNewOracle.address); + + await expect(network.connect(oracles[0]).commitRoot(root, blockNum)) + .to.be.revertedWithCustomError(network, Errors.NOT_ORACLE); + + await expect(network.connect(brandNewOracle).commitRoot(root, blockNum)) + .to.be.revertedWithCustomError(network, Errors.ALREADY_VOTED); + + const root2 = ethers.keccak256(ethers.toUtf8Bytes("brand-new-replacement-round2")); + const blockNum2 = await connection.ethers.provider.getBlockNumber(); + + const tx = await network.connect(brandNewOracle).commitRoot(root2, blockNum2); + await expect(tx).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root2, blockNum2, weight, threshold, 1, brandNewOracle.address); + + await network.connect(oracles[1]).commitRoot(root2, blockNum2); + const finalTx = await network.connect(oracles[2]).commitRoot(root2, blockNum2); + await expect(finalTx).to.emit(network, Events.ROOT_COMMITTED).withArgs(root2, blockNum2); + expect(await views.getCommittedRoot(blockNum2)).to.equal(root2); + }); + }); + + describe("Oracle Quorum — quorumBps changed between votes", async function () { + it("Lowering quorumBps between two votes causes the second vote to cross the new, lower threshold", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { weight } = await setupOraclesAndStake(network, ssvToken); + + const root = ethers.keccak256(ethers.toUtf8Bytes("mid-quorum-change")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const initialThreshold = (STAKE_AMOUNT * 7500n) / 10000n; // 75% + + const tx1 = await network.connect(oracles[0]).commitRoot(root, blockNum); + await expect(tx1).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight, initialThreshold, 1, oracles[0].address); + expect(await views.getCommittedRoot(blockNum)).to.equal(ethers.ZeroHash); + + await network.updateQuorumBps(5000); + expect(await views.getQuorumBps()).to.equal(5000n); + + const tx2 = await network.connect(oracles[1]).commitRoot(root, blockNum); + await expect(tx2).to.emit(network, Events.ROOT_COMMITTED).withArgs(root, blockNum); + expect(await views.getCommittedRoot(blockNum)).to.equal(root); + }); + }); + + describe("Oracle Quorum — conflicting roots for same block", async function () { + it("First root to reach quorum is committed; further votes on the losing root revert with StaleBlockNumber", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { weight } = await setupOraclesAndStake(network, ssvToken); + + await network.updateQuorumBps(5000); // 50% + + const rootA = ethers.keccak256(ethers.toUtf8Bytes("rootA")); + const rootB = ethers.keccak256(ethers.toUtf8Bytes("rootB")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const threshold = (STAKE_AMOUNT * 5000n) / 10000n; + + const txA1 = await network.connect(oracles[0]).commitRoot(rootA, blockNum); + await expect(txA1).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootA, blockNum, weight, threshold, 1, oracles[0].address); + + const txB2 = await network.connect(oracles[1]).commitRoot(rootB, blockNum); + await expect(txB2).to.emit(network, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootB, blockNum, weight, threshold, 2, oracles[1].address); + + expect(await views.getCommittedRoot(blockNum)).to.equal(ethers.ZeroHash); + + const txA3 = await network.connect(oracles[2]).commitRoot(rootA, blockNum); + await expect(txA3).to.emit(network, Events.ROOT_COMMITTED).withArgs(rootA, blockNum); + + expect(await views.getCommittedRoot(blockNum)).to.equal(rootA); + + await expect(network.connect(oracles[3]).commitRoot(rootB, blockNum)) + .to.be.revertedWithCustomError(network, Errors.STALE_BLOCK_NUMBER); + }); + }); +}); diff --git a/test/integration/SSVNetwork/ebDecreaseScenarios.test.ts b/test/integration/SSVNetwork/ebDecreaseScenarios.test.ts new file mode 100644 index 000000000..0008efb25 --- /dev/null +++ b/test/integration/SSVNetwork/ebDecreaseScenarios.test.ts @@ -0,0 +1,174 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + whitelistAddresses, + makePublicKey, + createCluster, + getCurrentClusterState, + parseClusterFromEvent, + computeEBRoot, + computeClusterId, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + DEFAULT_ETH_REGISTER_VALUE, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE, + STAKE_AMOUNT, + BPS_DENOMINATOR, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; + +const FEE_PER_BLOCK_BASELINE = 4n * MINIMAL_OPERATOR_ETH_FEE + NETWORK_FEE; + +const FEE_PER_BLOCK_64ETH = 2n * FEE_PER_BLOCK_BASELINE; + +describe("TEST-6 Integration: EB decrease via oracle commitRoot pipeline", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let deployer: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [deployer, operatorOwner, clusterOwner, oracle1, oracle2, oracle3, oracle4] } = await setupTestContext()); + }); + + const deployFixture = async () => ssvNetworkFullFixture(connection); + + const setupOracles = async (network: any, ssvToken: any) => { + await network.replaceOracle(1, oracle1.address); + await network.replaceOracle(2, oracle2.address); + await network.replaceOracle(3, oracle3.address); + await network.replaceOracle(4, oracle4.address); + + await ssvToken.mint(deployer.address, STAKE_AMOUNT); + await ssvToken.connect(deployer).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(deployer).stake(STAKE_AMOUNT); + }; + + const commitEBRoot = async (network: any, root: string, blockNum: number) => { + await network.connect(oracle1).commitRoot(root, blockNum); + await network.connect(oracle2).commitRoot(root, blockNum); + const tx = await network.connect(oracle3).commitRoot(root, blockNum); + return tx.wait(); + }; + + it("EB update via oracle commitRoot: RootCommitted emitted, exact fees settled at baseline rate", async function () { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network + .connect(clusterOwner) + .registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { + value: DEFAULT_ETH_REGISTER_VALUE, + }); + const clusterAfterReg = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + await setupOracles(network, ssvToken); + + await networkHelpers.mine(5); + const ebBlockNum = (await connection.ethers.provider.getBlockNumber()) - 1; + + const root64 = computeEBRoot(clusterId, 64); + const commitReceipt = await commitEBRoot(network, root64, ebBlockNum); + const updateTx = await network.updateClusterBalance( + ebBlockNum, + clusterOwner.address, + operatorIds, + clusterAfterReg, + 64, + [], + ); + const updateReceipt = await updateTx.wait(); + + const clusterAfterUpdate = parseClusterFromEvent( + network, updateReceipt, Events.CLUSTER_BALANCE_UPDATED + ); + + expect(clusterAfterUpdate.active).to.equal(true); + expect(clusterAfterUpdate.validatorCount).to.equal(1n); + const expectedFeesPaid = 16n * FEE_PER_BLOCK_BASELINE; + expect(clusterAfterUpdate.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE - expectedFeesPaid); + }); + + it("EB decrease (64→32 ETH): fees for 14 blocks charged at double baseline rate", async function () { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network + .connect(clusterOwner) + .registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { + value: DEFAULT_ETH_REGISTER_VALUE, + }); + const clusterAfterReg = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + await setupOracles(network, ssvToken); + + await networkHelpers.mine(5); + const block1 = (await connection.ethers.provider.getBlockNumber()) - 1; + const root64 = computeEBRoot(clusterId, 64); + + await commitEBRoot(network, root64, block1); + + const update1Tx = await network.updateClusterBalance( + block1, clusterOwner.address, operatorIds, clusterAfterReg, 64, [], + ); + const update1Receipt = await update1Tx.wait(); + const blockUpdate1 = update1Receipt!.blockNumber; + + const clusterAt64 = parseClusterFromEvent(network, update1Receipt, Events.CLUSTER_BALANCE_UPDATED); + + expect(clusterAt64.active).to.equal(true); + expect(clusterAt64.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE - 16n * FEE_PER_BLOCK_BASELINE); + const vUnits64 = 20_000n; + + await networkHelpers.mine(10); + + const block2 = (await connection.ethers.provider.getBlockNumber()) - 1; + const root32 = computeEBRoot(clusterId, 32); + + await commitEBRoot(network, root32, block2); + const update2Tx = await network.updateClusterBalance( + block2, clusterOwner.address, operatorIds, clusterAt64, 32, [], + ); + const update2Receipt = await update2Tx.wait(); + const blockUpdate2 = update2Receipt!.blockNumber; + const clusterAt32 = parseClusterFromEvent(network, update2Receipt, Events.CLUSTER_BALANCE_UPDATED); + + expect(clusterAt32.active).to.equal(true); + expect(clusterAt32.validatorCount).to.equal(1n); + const blocksDelta = BigInt(blockUpdate2 - blockUpdate1); + const packedOpFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const packedNetworkFee = NETWORK_FEE / ETH_DEDUCTED_DIGITS; + const totalPackedFeeRate = (4n * packedOpFee + packedNetworkFee); + const expectedFees = ((blocksDelta * totalPackedFeeRate * vUnits64) / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + expect(clusterAt32.balance).to.equal(clusterAt64.balance - expectedFees); + }); +}); diff --git a/test/integration/SSVNetwork/ebOperatorEarnings.test.ts b/test/integration/SSVNetwork/ebOperatorEarnings.test.ts new file mode 100644 index 000000000..1f0851e88 --- /dev/null +++ b/test/integration/SSVNetwork/ebOperatorEarnings.test.ts @@ -0,0 +1,272 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { + registerOperators, + registerDefaultCluster, + registerDefaultClusters, + generateMerkleForClusterEB, + computeClusterId, + setupTestContext, +} from '../../common/helpers.ts'; +import { + MINIMAL_OPERATOR_ETH_FEE, + ETH_DEDUCTED_DIGITS, + BPS_DENOMINATOR, + STAKE_AMOUNT, +} from '../../common/constants.ts'; +import { Errors } from "../../common/errors.ts"; + +const BLOCKS_TO_MINE = 100; + +describe("SSVNetwork Integration tests - EB-Weighted Operator Earnings", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner] } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => ssvNetworkFullFixture(connection); + + const setupOracles = async (network: any, ssvToken: any): Promise => { + const allSigners = await connection.ethers.getSigners(); + const staker = allSigners[2]; + const oracles = allSigners.slice(10, 14); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + for (let i = 0; i < 4; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + return oracles; + }; + + const commitRoot = async ( + network: any, + oracles: HardhatEthersSigner[], + root: string, + blockNum: number, + ): Promise => { + for (let i = 0; i < 3; i++) { + await network.connect(oracles[i]).commitRoot(root, blockNum); + } + }; + + const toClusterArg = (cluster: any) => ({ + validatorCount: Number(cluster.validatorCount), + networkFeeIndex: cluster.networkFeeIndex, + index: cluster.index, + active: cluster.active, + balance: cluster.balance, + }); + + it("getOperatorEarnings reflects EB=64 uplift (2× vs baseline) after updateClusterBalance", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + + const oracles = await setupOracles(network, ssvToken); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + const blockNum = (await connection.ethers.provider.getBlock('latest'))!.number; + await commitRoot(network, oracles, root, blockNum); + + await network.updateClusterBalance( + blockNum, clusterOwner.address, operatorIds.map(BigInt), + toClusterArg(cluster), 64, proofs[clusterId] + ); + + const earningsBefore = await views.getOperatorEarnings(operatorIds[0]); + + await networkHelpers.mine(BLOCKS_TO_MINE); + const earningsAfter = await views.getOperatorEarnings(operatorIds[0]); + + const expectedDelta = BigInt(BLOCKS_TO_MINE) * packedFee * 20000n / BPS_DENOMINATOR * ETH_DEDUCTED_DIGITS; + expect(expectedDelta).to.equal(BigInt(BLOCKS_TO_MINE) * MINIMAL_OPERATOR_ETH_FEE * 2n); + expect(earningsAfter - earningsBefore).to.equal(expectedDelta); + + expect(earningsAfter - earningsBefore).to.be.greaterThan(BigInt(BLOCKS_TO_MINE) * MINIMAL_OPERATOR_ETH_FEE); + }); + + it("getOperatorEarnings scales with combined vUnits from two clusters at EB=32 and EB=64", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const packedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + + const oracles = await setupOracles(network, ssvToken); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + const registered = await registerDefaultClusters(connection, network, operatorIds, operatorOwner, 2); + const [clusterInfo1, clusterInfo2] = registered.clusters; + + const clusterId1 = computeClusterId(clusterInfo1.owner.address, operatorIds); + const clusterId2 = computeClusterId(clusterInfo2.owner.address, operatorIds); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId: clusterId1, effectiveBalance: 32 }, + { clusterId: clusterId2, effectiveBalance: 64 }, + ]); + + const blockNum = (await connection.ethers.provider.getBlock('latest'))!.number; + await commitRoot(network, oracles, root, blockNum); + + await network.updateClusterBalance( + blockNum, clusterInfo1.owner.address, operatorIds.map(BigInt), + toClusterArg(clusterInfo1.cluster), 32, proofs[clusterId1] + ); + + await network.updateClusterBalance( + blockNum, clusterInfo2.owner.address, operatorIds.map(BigInt), + toClusterArg(clusterInfo2.cluster), 64, proofs[clusterId2] + ); + const earningsBefore = await views.getOperatorEarnings(operatorIds[0]); + + await networkHelpers.mine(BLOCKS_TO_MINE); + const earningsAfter = await views.getOperatorEarnings(operatorIds[0]); + + const expectedDelta = BigInt(BLOCKS_TO_MINE) * packedFee * 30000n / BPS_DENOMINATOR * ETH_DEDUCTED_DIGITS; + expect(expectedDelta).to.equal(BigInt(BLOCKS_TO_MINE) * MINIMAL_OPERATOR_ETH_FEE * 3n); + expect(earningsAfter - earningsBefore).to.equal(expectedDelta); + + expect(earningsAfter - earningsBefore).to.be.gt(BigInt(BLOCKS_TO_MINE) * MINIMAL_OPERATOR_ETH_FEE * 2n); + }); + + it("withdrawAllOperatorEarnings transfers exact EB-weighted ETH after EB=64 accrual", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const oracles = await setupOracles(network, ssvToken); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + const blockNum = (await connection.ethers.provider.getBlock('latest'))!.number; + await commitRoot(network, oracles, root, blockNum); + await network.updateClusterBalance( + blockNum, clusterOwner.address, operatorIds.map(BigInt), + toClusterArg(cluster), 64, proofs[clusterId] + ); + + await networkHelpers.mine(BLOCKS_TO_MINE); + + const earningsBeforeWithdraw = await views.getOperatorEarnings(operatorIds[0]); + + const networkAddress = await network.getAddress(); + const networkEthBefore = await connection.ethers.provider.getBalance(networkAddress); + + await network.connect(operatorOwner).withdrawAllOperatorEarnings(operatorIds[0]); + + const networkEthAfter = await connection.ethers.provider.getBalance(networkAddress); + const withdrawn = networkEthBefore - networkEthAfter; + + const oneBlockEB64 = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS * 2n * ETH_DEDUCTED_DIGITS; + expect(withdrawn).to.equal(earningsBeforeWithdraw + oneBlockEB64); + + expect(await views.getOperatorEarnings(operatorIds[0])).to.equal(0n); + expect(withdrawn).to.be.gte(BigInt(BLOCKS_TO_MINE) * MINIMAL_OPERATOR_ETH_FEE * 2n); + }); + + it("withdrawOperatorEarnings on EB=64 cluster uses explicit-EB weighted accrual", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const oracles = await setupOracles(network, ssvToken); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + const operatorId = operatorIds[0]; + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { root, proofs } = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance: 64 }]); + const blockNum = (await connection.ethers.provider.getBlock("latest"))!.number; + await commitRoot(network, oracles, root, blockNum); + await network.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds.map(BigInt), + toClusterArg(cluster), + 64, + proofs[clusterId] + ); + + await networkHelpers.mine(BLOCKS_TO_MINE); + const earningsBeforeWithdraw = await views.getOperatorEarnings(operatorId); + + await network.connect(operatorOwner).withdrawOperatorEarnings(operatorId, earningsBeforeWithdraw); + + const remainingAfterWithdraw = await views.getOperatorEarnings(operatorId); + const oneBlockAtEb64 = MINIMAL_OPERATOR_ETH_FEE * 2n; + expect(remainingAfterWithdraw).to.equal(oneBlockAtEb64); + }); + + it("withdrawOperatorEarnings reverts after removing operator from explicit-EB cluster", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const oracles = await setupOracles(network, ssvToken); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + const operatorId = operatorIds[0]; + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { root, proofs } = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance: 64 }]); + const blockNum = (await connection.ethers.provider.getBlock("latest"))!.number; + await commitRoot(network, oracles, root, blockNum); + await network.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds.map(BigInt), + toClusterArg(cluster), + 64, + proofs[clusterId] + ); + + await network.connect(operatorOwner).removeOperator(operatorId); + await expect( + network.connect(operatorOwner).withdrawOperatorEarnings(operatorId, ETH_DEDUCTED_DIGITS) + ).to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("withdrawOperatorEarnings reflects higher post-update accrual at EB=128", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const oracles = await setupOracles(network, ssvToken); + + const { cluster, operatorIds } = await registerDefaultCluster( + connection, network, views, operatorOwner, clusterOwner + ); + const operatorId = operatorIds[0]; + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { root, proofs } = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance: 128 }]); + const blockNum = (await connection.ethers.provider.getBlock("latest"))!.number; + await commitRoot(network, oracles, root, blockNum); + await network.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds.map(BigInt), + toClusterArg(cluster), + 128, + proofs[clusterId] + ); + + await networkHelpers.mine(BLOCKS_TO_MINE); + const earningsBeforeWithdraw = await views.getOperatorEarnings(operatorId); + + await network.connect(operatorOwner).withdrawOperatorEarnings(operatorId, earningsBeforeWithdraw); + + const remainingAfterWithdraw = await views.getOperatorEarnings(operatorId); + const oneBlockAtEb128 = MINIMAL_OPERATOR_ETH_FEE * 4n; + expect(remainingAfterWithdraw).to.equal(oneBlockAtEb128); + }); +}); diff --git a/test/integration/SSVNetwork/legacy-ssv.test.ts b/test/integration/SSVNetwork/legacy-ssv.test.ts new file mode 100644 index 000000000..e8973aa91 --- /dev/null +++ b/test/integration/SSVNetwork/legacy-ssv.test.ts @@ -0,0 +1,296 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + getCurrentClusterState, + makeOperatorKey, + makePublicKey, + registerOperators, + whitelistAddresses, + setupTestContext, +} from "../../common/helpers.ts"; +import { + CLUSTER_VERSION_ETH, + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { Errors } from "../../common/errors.ts"; + +/** + * Legacy SSV Accounting Integration Tests + * + * These tests verify the separation between legacy SSV token-based accounting + * and the new ETH accounting system. + * + * Key focus areas: + * - SSV vs ETH cluster/operator differentiation (SSV getters return 0 for ETH clusters) + * - SSV-specific DAO functions (updateNetworkFeeSSV, withdrawNetworkSSVEarnings) + * - Independence of SSV and ETH fee systems + * + * Note: ETH cluster economics (balance burn, deposits, withdrawals, liquidation) + * are tested in clusters.test.ts. ETH operator earnings are tested in operators.test.ts. + */ +describe("SSVNetwork Integration - Legacy SSV Accounting", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let randomUser: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, randomUser] } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + describe("SSV vs ETH Cluster Differentiation", function () { + it("ETH cluster has correct version and zero SSV balance/burn rate", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + expect(await views.getClusterAssetType(clusterOwner, operatorIds)).to.equal(CLUSTER_VERSION_ETH); + expect(await views.getBalance(clusterOwner, operatorIds, cluster)).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(await views.getBurnRate(clusterOwner, operatorIds, cluster)).to.be.greaterThan(0n); + await expect(views.getBalanceSSV(clusterOwner, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.isLiquidatableSSV(clusterOwner.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + + }); + + it("Operator registered via ETH cluster has ETH fee but zero SSV fee", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + expect(await views.getOperatorFee(operatorId)).to.equal(MINIMAL_OPERATOR_ETH_FEE); + expect(await views.getOperatorFeeSSV(operatorId)).to.equal(0n); + const opDetails = await views.getOperatorById(operatorId); + expect(opDetails[1]).to.equal(MINIMAL_OPERATOR_ETH_FEE); + const opDetailsSSV = await views.getOperatorByIdSSV(operatorId); + expect(opDetailsSSV[1]).to.equal(0n); + }); + + it("ETH cluster operators have zero SSV earnings", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await connection.networkHelpers.mine(100); + const ethEarnings = await views.getOperatorEarnings(operatorIds[0]); + expect(ethEarnings).to.be.greaterThan(0n); + const ssvEarnings = await views.getOperatorEarningsSSV(operatorIds[0]); + expect(ssvEarnings).to.equal(0n); + }); + }); + describe("Network Fee Earnings - SSV vs ETH Independence", function () { + it("Initial network earnings are zero for both SSV and ETH", async function () { + const { views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + expect(await views.getNetworkEarnings()).to.equal(0n); + expect(await views.getNetworkEarningsSSV()).to.equal(0n); + }); + + it("Network fee is configured for both SSV and ETH", async function () { + const { views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + expect(await views.getNetworkFee()).to.equal(NETWORK_FEE); + expect(await views.getNetworkFeeSSV()).to.equal(NETWORK_FEE); + }); + + it("ETH cluster activity increases ETH network earnings only, SSV unchanged", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const ethEarningsBefore = await views.getNetworkEarnings(); + const ssvEarningsBefore = await views.getNetworkEarningsSSV(); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await connection.networkHelpers.mine(100); + + const ethEarningsAfter = await views.getNetworkEarnings(); + const ssvEarningsAfter = await views.getNetworkEarningsSSV(); + expect(ethEarningsAfter).to.be.greaterThan(ethEarningsBefore); + expect(ssvEarningsAfter).to.equal(ssvEarningsBefore); + }); + + it("updateNetworkFeeSSV changes SSV network fee independently of ETH fee", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const initialSSVFee = await views.getNetworkFeeSSV(); + const initialETHFee = await views.getNetworkFee(); + + const newSSVFee = initialSSVFee * 2n; + const tx = await network.updateNetworkFeeSSV(newSSVFee); + + await expect(tx) + .to.emit(network, Events.NETWORK_FEE_UPDATED_SSV) + .withArgs(initialSSVFee, newSSVFee); + + expect(await views.getNetworkFeeSSV()).to.equal(newSSVFee); + expect(await views.getNetworkFee()).to.equal(initialETHFee); + }); + + it("updateNetworkFee changes ETH network fee independently of SSV fee", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const initialSSVFee = await views.getNetworkFeeSSV(); + const initialETHFee = await views.getNetworkFee(); + + const newETHFee = initialETHFee * 2n; + const tx = await network.updateNetworkFee(newETHFee); + + await expect(tx) + .to.emit(network, Events.NETWORK_FEE_UPDATED) + .withArgs(initialETHFee, newETHFee); + + expect(await views.getNetworkFee()).to.equal(newETHFee); + expect(await views.getNetworkFeeSSV()).to.equal(initialSSVFee); + }); + }); + describe("SSV-Specific DAO Functions", function () { + it("withdrawNetworkSSVEarnings requires owner permission", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.connect(randomUser).withdrawNetworkSSVEarnings(1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("updateNetworkFeeSSV requires owner permission", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const newFee = (await views.getNetworkFeeSSV()) * 2n; + + await expect(network.connect(randomUser).updateNetworkFeeSSV(newFee)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("getMinimumLiquidationCollateralSSV is callable and returns a value", async function () { + const { views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const ssvCollateral = await views.getMinimumLiquidationCollateralSSV(); + const ethCollateral = await views.getMinimumLiquidationCollateral(); + expect(ssvCollateral).to.be.greaterThanOrEqual(0n); + expect(ethCollateral).to.be.greaterThan(0n); + }); + }); + describe("SSV Operator Earnings Functions", function () { + it("withdrawOperatorEarningsSSV reverts with InsufficientBalance when no SSV earnings", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(100); + const precisionSafeAmount = 10_000_000n; + await expect(network.connect(operatorOwner).withdrawOperatorEarningsSSV(operatorIds[0], precisionSafeAmount)) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("withdrawAllOperatorEarningsSSV reverts with InsufficientBalance when no SSV earnings", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await expect(network.connect(operatorOwner).withdrawAllOperatorEarningsSSV(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("withdrawOperatorEarningsSSV requires operator ownership", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.connect(randomUser).withdrawOperatorEarningsSSV(operatorIds[0], 1n)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER); + }); + }); + describe("Liquidation Version Checks", function () { + it("liquidateSSV reverts for ETH clusters with IncorrectClusterVersion", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + await expect(network.liquidateSSV(clusterOwner.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + }); + }); +}); diff --git a/test/integration/SSVNetwork/migrationMultipleEBUpdates.test.ts b/test/integration/SSVNetwork/migrationMultipleEBUpdates.test.ts new file mode 100644 index 000000000..8cdb006a3 --- /dev/null +++ b/test/integration/SSVNetwork/migrationMultipleEBUpdates.test.ts @@ -0,0 +1,189 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { getTestConnection } from "../../setup/connection.ts"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { createCluster, makePublicKey, parseClusterFromEvent, extractEventArgs } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; + +describe("ITEST-2 Integration: migration with multiple EB updates", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [clusterOwner] = await connection.ethers.getSigners(); + }); + + const deployFixture = async () => ssvClustersHarnessFixture(connection); + + const getClusterId = (ownerAddress: string, operatorIds: bigint[]): string => { + return ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [ownerAddress, operatorIds]) + ); + }; + + const getEBRoot = (clusterId: string, effectiveBalance: number): string => { + const coder = ethers.AbiCoder.defaultAbiCoder(); + const innerHash = ethers.keccak256( + coder.encode(["bytes32", "uint32"], [clusterId, effectiveBalance]) + ); + return ethers.keccak256(ethers.solidityPacked(["bytes32"], [innerHash])); + }; + + it("Migrate after multiple EB updates uses the latest EB snapshot", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const ssvCluster = createCluster({ + validatorCount: 1n, + index: 0n, + networkFeeIndex: 0n, + balance: 0n, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(1), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + const clusterId = getClusterId(clusterOwner.address, operatorIds); + + const root64 = getEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root64); + const updateTx1 = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + ssvCluster, + 64, + [] + ); + const updateReceipt1 = await updateTx1.wait(); + const clusterAfterUpdate1 = parseClusterFromEvent(clusters, updateReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + const root96 = getEBRoot(clusterId, 96); + await clusters.mockSetEBRoot(2, root96); + const updateTx2 = await clusters.updateClusterBalance( + 2, + clusterOwner.address, + operatorIds, + clusterAfterUpdate1, + 96, + [] + ); + const updateReceipt2 = await updateTx2.wait(); + const clusterAfterUpdate2 = parseClusterFromEvent(clusters, updateReceipt2, Events.CLUSTER_BALANCE_UPDATED); + + const latestVUnits = (96n * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(latestVUnits); + + const migrateTx = await clusters.migrateClusterToETH(operatorIds, clusterAfterUpdate2, { + value: DEFAULT_ETH_REGISTER_VALUE, + }); + const migrateReceipt = await migrateTx.wait(); + const migrationEvent = extractEventArgs(clusters, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + const migratedCluster = parseClusterFromEvent(clusters, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + + expect(migrationEvent.effectiveBalance).to.equal(96); + expect(migratedCluster.validatorCount).to.equal(1n); + expect(migratedCluster.active).to.equal(true); + expect(migratedCluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + + const baseline = 1n * BPS_DENOMINATOR; + const expectedDeviation = latestVUnits - baseline; + + expect(await clusters.getDaoTotalEthVUnits()).to.equal(latestVUnits); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedDeviation); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(latestVUnits); + } + }); + + it("EB set then validators added: migration uses updated vUnits for new validator count", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const clusterWithOneValidator = createCluster({ + validatorCount: 1n, + index: 0n, + networkFeeIndex: 0n, + balance: 0n, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(1), + operatorIds, + clusterOwner.address, + clusterWithOneValidator + ); + + const clusterId = getClusterId(clusterOwner.address, operatorIds); + + const root64 = getEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root64); + await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + clusterWithOneValidator, + 64, + [] + ); + + const clusterWithTwoValidators = createCluster({ + validatorCount: 2n, + index: 0n, + networkFeeIndex: 0n, + balance: 0n, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(2), + operatorIds, + clusterOwner.address, + clusterWithTwoValidators + ); + + const root96 = getEBRoot(clusterId, 96); + await clusters.mockSetEBRoot(2, root96); + const updateTx2 = await clusters.updateClusterBalance( + 2, + clusterOwner.address, + operatorIds, + clusterWithTwoValidators, + 96, + [] + ); + const updateReceipt2 = await updateTx2.wait(); + const clusterAfterSecondUpdate = parseClusterFromEvent(clusters, updateReceipt2, Events.CLUSTER_BALANCE_UPDATED); + + const expectedVUnits = (96n * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + + const migrateTx = await clusters.migrateClusterToETH(operatorIds, clusterAfterSecondUpdate, { + value: DEFAULT_ETH_REGISTER_VALUE, + }); + const migrateReceipt = await migrateTx.wait(); + const migrationEvent = extractEventArgs(clusters, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + const migratedCluster = parseClusterFromEvent(clusters, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + + expect(migrationEvent.effectiveBalance).to.equal(96); + expect(migratedCluster.validatorCount).to.equal(2n); + expect(migratedCluster.active).to.equal(true); + expect(migratedCluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + + const expectedBaseline = 2n * BPS_DENOMINATOR; + const expectedDeviation = expectedVUnits - expectedBaseline; + + expect(await clusters.getDaoTotalEthVUnits()).to.equal(expectedVUnits); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedDeviation); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(expectedVUnits); + } + }); +}); diff --git a/test/integration/SSVNetwork/operators.test.ts b/test/integration/SSVNetwork/operators.test.ts new file mode 100644 index 000000000..f2844bee9 --- /dev/null +++ b/test/integration/SSVNetwork/operators.test.ts @@ -0,0 +1,639 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import { + makeOperatorKey, + registerOperators, + whitelistAddresses, + makePublicKey, + getCurrentClusterState, + registerDefaultCluster, + setupTestContext, +} from '../../common/helpers.ts'; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + DEFAULT_ETH_REGISTER_VALUE, + DECLARE_OPERATOR_FEE_PERIOD, + EXECUTE_OPERATOR_FEE_PERIOD, + MAXIMUM_OPERATORS_FEE, + MINIMAL_OPERATOR_ETH_FEE, + OPERATOR_MAX_FEE_INCREASE, + NETWORK_FEE, +} from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../../common/errors.js'; +import { deployContract } from '../../../scripts/common/helpers.js'; +import { trackGasFromReceipt, GasGroup } from '../../helpers/gas-usage.ts'; +import { expectETHDelta, expectETHDeltas } from '../../helpers/balance.ts'; + +/** + * Enhanced Integration Tests for SSVNetwork Operators + * + * These tests focus on: + * 1. Balance delta assertions for every ETH-moving operation + * 2. Boundary testing (min/max values, just below/above thresholds) + * 3. Multi-block simulation with exact expected values + * 4. Basic invariant checks (operator balances, DAO balance, cluster balance) + * 5. Combined scenarios verifying cluster, operator, and network fee distribution + */ +describe("SSVNetwork Integration - Operators (Enhanced)", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let randomUser: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, randomUser] } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Balance Delta Assertions", async function() { + + it("withdrawOperatorEarnings: verifies exact ETH transfer to operator owner", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const earningsPeriod = 100n; + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const actualEarnings = await views.getOperatorEarnings(operatorIds[0]); + expect(actualEarnings).to.equal(expectedEarnings, "Operator earnings mismatch after mining"); + await expectETHDeltas(connection.ethers.provider, + () => network.withdrawOperatorEarnings(operatorIds[0], actualEarnings), + [ + { address: operatorOwner.address, expectedDelta: actualEarnings, accountForGas: true }, + { address: await network.getAddress(), expectedDelta: -actualEarnings }, + ]); + const earningsAfter = await views.getOperatorEarnings(operatorIds[0]); + expect(earningsAfter).to.equal(MINIMAL_OPERATOR_ETH_FEE, "Remaining earnings should equal 1 block fee"); + }); + + it("withdrawAllOperatorEarnings: verifies complete balance drain with exact amounts", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const earningsPeriod = 50n; + await connection.networkHelpers.mine(earningsPeriod); + + const earningsBefore = await views.getOperatorEarnings(operatorIds[0]); + const expectedWithdrawn = earningsBefore + MINIMAL_OPERATOR_ETH_FEE; + + await expectETHDelta(connection.ethers.provider, operatorOwner.address, + () => network.withdrawAllOperatorEarnings(operatorIds[0]), + expectedWithdrawn, { accountForGas: true }); + expect(await views.getOperatorEarnings(operatorIds[0])).to.equal(0n); + }); + }); + + describe("Boundary Tests - Operator Fees", async function() { + + it("registerOperator: succeeds at exact minimum fee", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true)) + .to.emit(network, Events.OPERATOR_ADDED); + + expect(await views.getOperatorFee(1n)).to.equal(MINIMAL_OPERATOR_ETH_FEE); + }); + + it("registerOperator: reverts at just below minimum fee", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE - 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("registerOperator: succeeds at exact maximum fee", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MAXIMUM_OPERATORS_FEE, true)) + .to.emit(network, Events.OPERATOR_ADDED); + + expect(await views.getOperatorFee(1n)).to.equal(MAXIMUM_OPERATORS_FEE); + }); + + it("registerOperator: reverts at just above maximum fee", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MAXIMUM_OPERATORS_FEE + 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + + it("registerOperator: succeeds with zero fee (special case)", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, 0n, true)) + .to.emit(network, Events.OPERATOR_ADDED); + + expect(await views.getOperatorFee(1n)).to.equal(0n); + }); + + it("declareOperatorFee: succeeds at exact max allowed increase", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + const startingFee = MINIMAL_OPERATOR_ETH_FEE * 10n; + await network.registerOperator(operatorKey, startingFee, true); + const maxAllowedFee = (startingFee * (10000n + OPERATOR_MAX_FEE_INCREASE)) / 10000n; + const DEDUCTED_DIGITS = 10_000_000n; + const precisionSafeFee = (maxAllowedFee / DEDUCTED_DIGITS) * DEDUCTED_DIGITS; + + await expect(network.declareOperatorFee(1n, precisionSafeFee)) + .to.emit(network, Events.OPERATOR_FEE_DECLARED); + }); + + it("declareOperatorFee: reverts at just above max allowed increase", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + const currentFee = MINIMAL_OPERATOR_ETH_FEE; + const exceedingFee = currentFee * 3n; + + await expect(network.declareOperatorFee(1n, exceedingFee)) + .to.be.revertedWithCustomError(network, Errors.FEE_EXCEEDS_INCREASE_LIMIT); + }); + + it("reduceOperatorFee: succeeds reducing to exact minimum fee", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.reduceOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE)) + .to.emit(network, Events.OPERATOR_FEE_EXECUTED); + + expect(await views.getOperatorFee(1n)).to.equal(MINIMAL_OPERATOR_ETH_FEE); + }); + + it("reduceOperatorFee: reverts when reducing below minimum fee", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorKey = makeOperatorKey(1); + + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.reduceOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE - 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + }); + + describe("Multi-Block Simulation - Operator Earnings Accrual", async function() { + + it("Operator earnings accrue correctly over multiple block periods", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const checkpoints = [10n, 50n, 100n, 500n]; + let totalBlocksMined = 0n; + + for (const blocks of checkpoints) { + await connection.networkHelpers.mine(blocks); + totalBlocksMined += blocks; + + const expectedEarnings = totalBlocksMined * MINIMAL_OPERATOR_ETH_FEE; + const actualEarnings = await views.getOperatorEarnings(operatorIds[0]); + + expect(actualEarnings).to.equal( + expectedEarnings, + `Earnings mismatch at block ${totalBlocksMined}` + ); + } + }); + + it("All 4 operators earn equally from a single validator cluster", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const blocks = 200n; + await connection.networkHelpers.mine(blocks); + + const expectedPerOperator = blocks * MINIMAL_OPERATOR_ETH_FEE; + for (let i = 0; i < 4; i++) { + const earnings = await views.getOperatorEarnings(operatorIds[i]); + expect(earnings).to.equal(expectedPerOperator, `Operator ${i} earnings mismatch`); + } + }); + + it("Operator earnings scale with validator count", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const blocks1 = 100n; + await connection.networkHelpers.mine(blocks1); + const earningsAfter1Validator = await views.getOperatorEarnings(operatorIds[0]); + expect(earningsAfter1Validator).to.equal(blocks1 * MINIMAL_OPERATOR_ETH_FEE); + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const blocks2 = 100n; + await connection.networkHelpers.mine(blocks2); + const expectedTotal = earningsAfter1Validator + MINIMAL_OPERATOR_ETH_FEE + (blocks2 * MINIMAL_OPERATOR_ETH_FEE * 2n); + const actualEarnings = await views.getOperatorEarnings(operatorIds[0]); + + expect(actualEarnings).to.equal(expectedTotal, "Earnings should scale with validator count"); + }); + + it("removeValidator triggers exact settlement of operator earnings", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const blocksToMine = 100n; + await connection.networkHelpers.mine(blocksToMine); + + const currentCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, currentCluster); + const expectedEarningsPerOperator = (blocksToMine + 1n) * MINIMAL_OPERATOR_ETH_FEE; + + for (const opId of operatorIds) { + const earnings = await views.getOperatorEarnings(opId); + expect(earnings).to.equal(expectedEarningsPerOperator); + } + }); + }); + + describe("Invariant Checks - Operator Balance Consistency", async function() { + + it("Invariant: Total operator earnings <= Cluster balance drained", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const depositAmount = DEFAULT_ETH_REGISTER_VALUE; + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositAmount } + ); + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + const blocks = 500n; + await connection.networkHelpers.mine(blocks); + let totalOperatorEarnings = 0n; + for (const opId of operatorIds) { + totalOperatorEarnings += await views.getOperatorEarnings(opId); + } + const clusterBalance = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const networkEarnings = await views.getNetworkEarnings(); + const totalAccounted = clusterBalance + totalOperatorEarnings + networkEarnings; + const difference = depositAmount > totalAccounted + ? depositAmount - totalAccounted + : totalAccounted - depositAmount; + + expect(difference).to.be.lessThanOrEqual( + 100n, + "Balance invariant violated: funds not properly accounted" + ); + }); + + it("Invariant: Withdrawing all operator earnings zeros out balance", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(100n); + await network.withdrawAllOperatorEarnings(operatorIds[0]); + expect(await views.getOperatorEarnings(operatorIds[0])).to.equal(0n); + }); + + it("Invariant: Removing validator stops operator fee accrual", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(50n); + const earningsBeforeRemoval = await views.getOperatorEarnings(operatorIds[0]); + let cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + await connection.networkHelpers.mine(100n); + const earningsAfterRemoval = await views.getOperatorEarnings(operatorIds[0]); + expect(earningsAfterRemoval).to.equal( + earningsBeforeRemoval + MINIMAL_OPERATOR_ETH_FEE, + "Operator should not earn fees after validator removal" + ); + }); + }); + + describe("Combined Scenarios - Full Fee Distribution", async function() { + + it("Full accounting: cluster deposit -> operator earnings -> network fees", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const depositAmount = DEFAULT_ETH_REGISTER_VALUE; + const networkFeeBefore = await views.getNetworkEarnings(); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: depositAmount } + ); + + const blocks = 100n; + await connection.networkHelpers.mine(blocks); + const expectedOperatorEarningsPerOp = blocks * MINIMAL_OPERATOR_ETH_FEE; + const expectedTotalOperatorEarnings = expectedOperatorEarningsPerOp * 4n; + const expectedNetworkFeeEarnings = blocks * NETWORK_FEE; + for (const opId of operatorIds) { + const earnings = await views.getOperatorEarnings(opId); + expect(earnings).to.equal(expectedOperatorEarningsPerOp, `Operator ${opId} earnings incorrect`); + } + const networkFeeAfter = await views.getNetworkEarnings(); + expect(networkFeeAfter - networkFeeBefore).to.equal( + expectedNetworkFeeEarnings, + "Network fee earnings incorrect" + ); + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const clusterBalance = await views.getBalance(clusterOwner.address, operatorIds, cluster); + + const expectedBurnRate = (MINIMAL_OPERATOR_ETH_FEE * 4n) + NETWORK_FEE; + const expectedClusterBalance = depositAmount - (blocks * expectedBurnRate); + + expect(clusterBalance).to.equal(expectedClusterBalance, "Cluster balance incorrect after burn"); + }); + + it("Operator withdrawal doesn't affect other operators' balances", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(100n); + const earningsBefore: bigint[] = []; + for (const opId of operatorIds) { + earningsBefore.push(await views.getOperatorEarnings(opId)); + } + await network.withdrawOperatorEarnings(operatorIds[0], earningsBefore[0]); + for (let i = 1; i < operatorIds.length; i++) { + const earningsAfter = await views.getOperatorEarnings(operatorIds[i]); + expect(earningsAfter).to.equal( + earningsBefore[i] + MINIMAL_OPERATOR_ETH_FEE, + `Operator ${i} balance incorrectly affected by operator 0's withdrawal` + ); + } + }); + + it("Fee change via declare->execute workflow with precise timing", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const oldFee = MINIMAL_OPERATOR_ETH_FEE; + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + const declareTx = await network.declareOperatorFee(operatorIds[0], newFee); + const declareBlock = await declareTx.getBlock(); + + await expect(declareTx) + .to.emit(network, Events.OPERATOR_FEE_DECLARED) + .withArgs(operatorOwner.address, operatorIds[0], declareBlock!.number, newFee); + const pendingFee = await views.getOperatorDeclaredFee(operatorIds[0]); + expect(pendingFee[0]).to.equal(true, "Fee change should be active"); + expect(pendingFee[1]).to.equal(newFee, "Pending fee value incorrect"); + await connection.networkHelpers.time.increase(DECLARE_OPERATOR_FEE_PERIOD + 1n); + await connection.networkHelpers.mine(); + const executeTx = await network.executeOperatorFee(operatorIds[0]); + await expect(executeTx).to.emit(network, Events.OPERATOR_FEE_EXECUTED); + expect(await views.getOperatorFee(operatorIds[0])).to.equal(newFee); + const blocksBefore = 50n; + const earningsBefore = await views.getOperatorEarnings(operatorIds[0]); + + await connection.networkHelpers.mine(blocksBefore); + + const earningsAfter = await views.getOperatorEarnings(operatorIds[0]); + const expectedIncrease = blocksBefore * newFee; + + expect(earningsAfter - earningsBefore).to.equal( + expectedIncrease, + "Earnings should accrue at new fee rate" + ); + }); + }); + + describe("Edge Cases and Error Conditions", async function() { + + it("Cannot withdraw more than available earnings", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(10n); + + const currentEarnings = await views.getOperatorEarnings(operatorIds[0]); + const excessiveAmount = currentEarnings * 2n; + + await expect(network.withdrawOperatorEarnings(operatorIds[0], excessiveAmount)) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + + it("Operator with zero fee earns nothing", async function() { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const op1Key = makeOperatorKey(1); + const op2Key = makeOperatorKey(2); + const op3Key = makeOperatorKey(3); + const op4Key = makeOperatorKey(4); + + await network.registerOperator(op1Key, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(op2Key, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(op3Key, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(op4Key, 0n, true); + + const operatorIds = [1n, 2n, 3n, 4n]; + await network.setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(100n); + for (let i = 0; i < 3; i++) { + const earnings = await views.getOperatorEarnings(operatorIds[i]); + expect(earnings).to.be.greaterThan(0n, `Operator ${i+1} should have earnings`); + } + const zeroFeeEarnings = await views.getOperatorEarnings(operatorIds[3]); + expect(zeroFeeEarnings).to.equal(0n, "Zero-fee operator should have no earnings"); + }); + + it("executeOperatorFee reverts before declare period ends", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n); + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + }); + + it("executeOperatorFee reverts after execute period expires", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE * 2n); + await connection.networkHelpers.time.increase( + DECLARE_OPERATOR_FEE_PERIOD + EXECUTE_OPERATOR_FEE_PERIOD + 100n + ); + await connection.networkHelpers.mine(); + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + }); + + it("Cannot increase fee from zero (must use reduceOperatorFee)", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + await network.registerOperator(operatorKey, 0n, true); + + await expect(network.declareOperatorFee(1n, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + + it("Removed operator cannot have earnings withdrawn", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 1); + await network.removeOperator(operatorIds[0]); + + await expect(network.withdrawOperatorEarnings(operatorIds[0], 1n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + }); +}); diff --git a/test/integration/SSVNetwork/removedOperatorExplicitEB.test.ts b/test/integration/SSVNetwork/removedOperatorExplicitEB.test.ts new file mode 100644 index 000000000..dd1811222 --- /dev/null +++ b/test/integration/SSVNetwork/removedOperatorExplicitEB.test.ts @@ -0,0 +1,188 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; + +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { + registerOperators, + whitelistAddresses, + makePublicKey, + getCurrentClusterState, + computeEBRoot, + computeClusterId, + setupOracles, + setupTestContext, +} from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + DEFAULT_ETH_REGISTER_VALUE, + NETWORK_FEE, +} from "../../common/constants.ts"; + +/** + * These tests intentionally describe the expected legitimate behavior. + * On the current code, they fail and demonstrate the removed-operator / explicit-EB bug. + */ +describe("Known Issue Repro: removed operator bricks explicit-EB maintenance paths", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let deployer: HardhatEthersSigner; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + + before(async function () { + ({ + connection, + networkHelpers, + signers: [deployer, operatorOwner, clusterOwner, liquidator, oracle1, oracle2, oracle3, oracle4], + } = await setupTestContext()); + }); + + const deployFixture = async () => ssvNetworkFullFixture(connection); + + async function prepareScenario(highNetworkFee = false) { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFixture); + + if (highNetworkFee) { + await network.updateNetworkFee(NETWORK_FEE * 100n); + } + + await setupOracles(network, ssvToken, deployer, [oracle1, oracle2, oracle3, oracle4]); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await ( + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ) + ).wait(); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const clusterAfterRegister = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + await networkHelpers.mine(1n); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const root64 = computeEBRoot(clusterId, 64); + + await (await network.connect(oracle1).commitRoot(root64, blockNum)).wait(); + await (await network.connect(oracle2).commitRoot(root64, blockNum)).wait(); + await (await network.connect(oracle3).commitRoot(root64, blockNum)).wait(); + + await ( + await network.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + clusterAfterRegister, + 64, + [] + ) + ).wait(); + + const clusterAfterEB = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + await (await network.connect(operatorOwner).removeOperator(operatorIds[0])).wait(); + + return { + network, + views, + operatorIds, + clusterId, + clusterAfterEB, + }; + } + + it("allows the owner to self-liquidate after an operator removal", async function () { + const { network, operatorIds, clusterAfterEB } = await prepareScenario(); + + await network.connect(clusterOwner).liquidate( + clusterOwner.address, + operatorIds, + clusterAfterEB + ); + }); + + it("allows an EB decrease after an operator removal", async function () { + const { network, operatorIds, clusterId, clusterAfterEB } = await prepareScenario(); + + await networkHelpers.mine(1n); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const root32 = computeEBRoot(clusterId, 32); + + await (await network.connect(oracle1).commitRoot(root32, blockNum)).wait(); + await (await network.connect(oracle2).commitRoot(root32, blockNum)).wait(); + await (await network.connect(oracle3).commitRoot(root32, blockNum)).wait(); + + await network.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + clusterAfterEB, + 32, + [] + ); + }); + + it("allows the last validator to be removed cleanly after an operator removal", async function () { + const { network, operatorIds, clusterAfterEB } = await prepareScenario(); + + await network.connect(clusterOwner).removeValidator( + makePublicKey(1), + operatorIds, + clusterAfterEB + ); + }); + + it("allows third-party liquidation once the cluster becomes objectively liquidatable", async function () { + const { network, views, operatorIds, clusterAfterEB } = await prepareScenario(true); + + let liquidatable = await views.isLiquidatable( + clusterOwner.address, + operatorIds, + clusterAfterEB + ); + let attempts = 0; + + while (!liquidatable && attempts < 20) { + await networkHelpers.mine(100000n); + liquidatable = await views.isLiquidatable( + clusterOwner.address, + operatorIds, + clusterAfterEB + ); + attempts++; + } + + expect(liquidatable).to.equal(true); + + await network.connect(liquidator).liquidate( + clusterOwner.address, + operatorIds, + clusterAfterEB + ); + }); +}); diff --git a/test/integration/SSVNetwork/staking.test.ts b/test/integration/SSVNetwork/staking.test.ts new file mode 100644 index 000000000..266bc9c88 --- /dev/null +++ b/test/integration/SSVNetwork/staking.test.ts @@ -0,0 +1,943 @@ +import { expect } from "chai"; +import { anyValue } from "@nomicfoundation/hardhat-ethers-chai-matchers/withArgs"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType, UnstakeRequest } from '../../common/types.ts'; +import { + registerOperators, + whitelistAddresses, + makePublicKey, + getCurrentClusterState, + setupTestContext, +} from '../../common/helpers.ts'; +import { computeClusterId, generateMerkleForClusterEB, commitEBRoot } from '../../helpers/oracle.ts'; +import { + DEFAULT_SHARES, + EMPTY_CLUSTER, + DEFAULT_ETH_REGISTER_VALUE, + STAKE_AMOUNT, + DEFAULT_UNSTAKE_COOLDOWN, + DEFAULT_ORACLES_IDS, + NETWORK_FEE, +} from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../../common/errors.js'; +import { deployMultisig, multisigExec } from '../../helpers/multisig.ts'; + +/** + * Enhanced Integration Tests for SSVNetwork Staking + * + * These tests focus on: + * 1. Balance delta assertions for SSV/cSSV token movements + * 2. Reward accrual from cluster ETH inflow (deposits, registrations, reactivations) + * 3. Multi-block simulation for staking rewards distribution + * 4. Invariant checks for staking/rewards balance consistency + * 5. Combined scenarios: stake → earn rewards → claim → unstake + * + * Key insight: The source of staking rewards is the network fee portion of ETH + * that flows in from cluster owners when they: + * - Register validators (deposit ETH) + * - Deposit ETH into clusters + * - Reactivate clusters (deposit ETH) + */ +describe("SSVNetwork Integration - Staking (Enhanced)", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let staker: HardhatEthersSigner; + let staker2: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner, staker, staker2] } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("Balance Delta Assertions - Token Movements", async function() { + + it("stake: SSV transferred from staker to contract, cSSV minted 1:1", async function() { + const { network, views, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + + const stakerSsvBefore = await ssvToken.balanceOf(staker.address); + const contractSsvBefore = await ssvToken.balanceOf(await network.getAddress()); + const cssvSupplyBefore = await cssvToken.totalSupply(); + const stakerCssvBefore = await cssvToken.balanceOf(staker.address); + + const tx = await network.connect(staker).stake(STAKE_AMOUNT); + await tx.wait(); + + const stakerSsvAfter = await ssvToken.balanceOf(staker.address); + const contractSsvAfter = await ssvToken.balanceOf(await network.getAddress()); + const cssvSupplyAfter = await cssvToken.totalSupply(); + const stakerCssvAfter = await cssvToken.balanceOf(staker.address); + expect(stakerSsvBefore - stakerSsvAfter).to.equal(STAKE_AMOUNT); + expect(contractSsvAfter - contractSsvBefore).to.equal(STAKE_AMOUNT); + expect(cssvSupplyAfter - cssvSupplyBefore).to.equal(STAKE_AMOUNT); + expect(stakerCssvAfter - stakerCssvBefore).to.equal(STAKE_AMOUNT); + expect(await views.stakedBalanceOf(staker.address)).to.equal(STAKE_AMOUNT); + }); + + it("requestUnstake: cSSV burned, delegation removed", async function() { + const { network, views, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const cssvSupplyBefore = await cssvToken.totalSupply(); + const stakerCssvBefore = await cssvToken.balanceOf(staker.address); + const stakedBefore = await views.stakedBalanceOf(staker.address); + + const unstakeAmount = STAKE_AMOUNT / 2n; + const tx = await network.connect(staker).requestUnstake(unstakeAmount); + const block = await tx.getBlock(); + + const cssvSupplyAfter = await cssvToken.totalSupply(); + const stakerCssvAfter = await cssvToken.balanceOf(staker.address); + const stakedAfter = await views.stakedBalanceOf(staker.address); + expect(cssvSupplyBefore - cssvSupplyAfter).to.equal(unstakeAmount); + expect(stakerCssvBefore - stakerCssvAfter).to.equal(unstakeAmount); + expect(stakedBefore - stakedAfter).to.equal(unstakeAmount); + const requests: UnstakeRequest[] = await views.pendingUnstake(staker.address); + expect(requests[0].amount).to.equal(unstakeAmount); + expect(requests[0].unlockTime).to.equal(BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + }); + + it("withdrawUnlocked: SSV returned to staker after cooldown", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + await network.connect(staker).requestUnstake(STAKE_AMOUNT); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await networkHelpers.mine(); + + const stakerSsvBefore = await ssvToken.balanceOf(staker.address); + const contractSsvBefore = await ssvToken.balanceOf(await network.getAddress()); + + const tx = await network.connect(staker).withdrawUnlocked(); + await expect(tx).to.emit(network, Events.UNSTAKE_WITHDRAWN).withArgs(staker.address, STAKE_AMOUNT); + + const stakerSsvAfter = await ssvToken.balanceOf(staker.address); + const contractSsvAfter = await ssvToken.balanceOf(await network.getAddress()); + expect(stakerSsvAfter - stakerSsvBefore).to.equal(STAKE_AMOUNT); + expect(contractSsvBefore - contractSsvAfter).to.equal(STAKE_AMOUNT); + const requests: UnstakeRequest[] = await views.pendingUnstake(staker.address); + expect(requests.length).to.equal(0); + }); + }); + + describe("Reward Accrual from Cluster ETH Inflow", async function() { + + it("Network fees from validator registration flow to staking rewards pool", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const networkEarningsBefore = await views.getNetworkEarnings(); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await connection.networkHelpers.mine(100n); + + const networkEarningsAfter = await views.getNetworkEarnings(); + expect(networkEarningsAfter).to.be.greaterThan(networkEarningsBefore); + const expectedNetworkEarnings = 100n * NETWORK_FEE; + expect(networkEarningsAfter - networkEarningsBefore).to.equal(expectedNetworkEarnings); + }); + + it("Multiple cluster deposits increase reward pool proportionally", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const networkEarningsAfter1 = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(50n); + + const networkEarningsAfter50Blocks = await views.getNetworkEarnings(); + const earningsFrom1Validator = networkEarningsAfter50Blocks - networkEarningsAfter1; + let cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const networkEarningsAfter2Validators = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(50n); + + const networkEarningsAfter2Val50Blocks = await views.getNetworkEarnings(); + const earningsFrom2Validators = networkEarningsAfter2Val50Blocks - networkEarningsAfter2Validators; + expect(earningsFrom2Validators).to.equal(earningsFrom1Validator * 2n); + }); + + it('EB=64 cluster contributes exactly 2x network-fee rewards vs EB=32', async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture( + deployFullSSVNetworkFixture, + ); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(11), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const blocksPerPhase = 100n; + const before32 = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(blocksPerPhase); + const after32 = await views.getNetworkEarnings(); + const eb32Delta = after32 - before32; + + const allSigners = await connection.ethers.getSigners(); + const oracles = allSigners.slice(10, 14); + for (let i = 0; i < 4; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const eb64 = 64; + const ebBlock = Number(await connection.ethers.provider.getBlockNumber()); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: eb64 }, + ]); + + await commitEBRoot(network, root, ebBlock, oracles); + + const cluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + + await network.updateClusterBalance( + ebBlock, + clusterOwner.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(cluster.validatorCount), + networkFeeIndex: BigInt(cluster.networkFeeIndex), + index: BigInt(cluster.index), + active: cluster.active, + balance: BigInt(cluster.balance), + }, + eb64, + proofs[clusterId], + ); + + const before64 = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(blocksPerPhase); + const after64 = await views.getNetworkEarnings(); + const eb64Delta = after64 - before64; + + expect(eb64Delta).to.equal(eb32Delta * 2n); + }); + + it('Multiple clusters with different EBs accrue cumulative EB-weighted staking fees', async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture( + deployFullSSVNetworkFixture, + ); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const clusterOwner2 = staker2; + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [ + clusterOwner.address, + clusterOwner2.address, + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(21), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + await network.connect(clusterOwner2).registerValidator( + makePublicKey(22), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + + const blocks = 120n; + await network.connect(staker).syncFees(); + const phaseAStart = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(blocks); + const phaseAEnd = await views.getNetworkEarnings(); + const phaseASyncTx = await network.connect(staker).syncFees(); + const phaseAReceipt = await phaseASyncTx.wait(); + const phaseAViewDelta = phaseAEnd - phaseAStart; + const phaseAFees: bigint = phaseAReceipt!.logs + .map((log) => network.interface.parseLog(log)) + .find((e) => e?.name === Events.FEES_SYNCED)!.args.newFeesWei; + await expect(phaseASyncTx).to.emit(network, Events.FEES_SYNCED).withArgs(phaseAFees, anyValue); + + const allSigners = await connection.ethers.getSigners(); + const oracles = allSigners.slice(10, 14); + for (let i = 0; i < 4; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + + const clusterId1 = computeClusterId(clusterOwner.address, operatorIds); + const clusterId2 = computeClusterId(clusterOwner2.address, operatorIds); + const eb32 = 32; + const eb64 = 64; + const ebBlock = Number(await connection.ethers.provider.getBlockNumber()); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId: clusterId1, effectiveBalance: eb32 }, + { clusterId: clusterId2, effectiveBalance: eb64 }, + ]); + + await commitEBRoot(network, root, ebBlock, oracles); + + const cluster1 = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds, + ); + await network.updateClusterBalance( + ebBlock, + clusterOwner.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(cluster1.validatorCount), + networkFeeIndex: BigInt(cluster1.networkFeeIndex), + index: BigInt(cluster1.index), + active: cluster1.active, + balance: BigInt(cluster1.balance), + }, + eb32, + proofs[clusterId1], + ); + + const cluster2 = await getCurrentClusterState( + connection, + network, + clusterOwner2.address, + operatorIds, + ); + await network.connect(clusterOwner2).updateClusterBalance( + ebBlock, + clusterOwner2.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(cluster2.validatorCount), + networkFeeIndex: BigInt(cluster2.networkFeeIndex), + index: BigInt(cluster2.index), + active: cluster2.active, + balance: BigInt(cluster2.balance), + }, + eb64, + proofs[clusterId2], + ); + + // Settle transition-period fees (replaceOracle + commitRoot + updateClusterBalance blocks) + // so phase B window starts at a clean checkpoint with only 30k vUnits active. + await network.connect(staker).syncFees(); + + const phaseBStart = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(blocks); + const phaseBEnd = await views.getNetworkEarnings(); + const phaseBSyncTx = await network.connect(staker).syncFees(); + const phaseBReceipt = await phaseBSyncTx.wait(); + const phaseBViewDelta = phaseBEnd - phaseBStart; + const phaseBFees: bigint = phaseBReceipt!.logs + .map((log) => network.interface.parseLog(log)) + .find((e) => e?.name === Events.FEES_SYNCED)!.args.newFeesWei; + await expect(phaseBSyncTx).to.emit(network, Events.FEES_SYNCED).withArgs(phaseBFees, anyValue); + + // Two implicit 32-EB clusters (20k vUnits total) should become 32+64 EB (30k vUnits): + // fee rate scales from 2x to 3x, so over equal blocks the fee deltas scale 3:2. + expect(phaseBViewDelta * 2n).to.equal(phaseAViewDelta * 3n); + expect(phaseBFees * 2n).to.equal(phaseAFees * 3n) + }); + }); + + describe("Staking Rewards Distribution", async function() { + it("Multiple stakers share rewards proportionally", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + await ssvToken.mint(staker2.address, STAKE_AMOUNT); + await ssvToken.connect(staker2).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker2).stake(STAKE_AMOUNT); + expect(await views.stakedBalanceOf(staker.address)).to.equal(STAKE_AMOUNT); + expect(await views.stakedBalanceOf(staker2.address)).to.equal(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(101), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(100n); + + const claimableA = await views.previewClaimableEth(staker.address); + const claimableB = await views.previewClaimableEth(staker2.address); + + expect(claimableA).to.be.greaterThan(0n); + expect(claimableA).to.equal(claimableB); + }); + }); + + describe("Invariant Checks - Staking Consistency", async function() { + + it("Invariant: cSSV totalSupply always equals total staked across all users", async function() { + const { network, views, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + expect(await cssvToken.totalSupply()).to.equal(0n); + expect(await views.totalStaked()).to.equal(0n); + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + expect(await cssvToken.totalSupply()).to.equal(await views.totalStaked()); + await ssvToken.mint(staker2.address, STAKE_AMOUNT * 2n); + await ssvToken.connect(staker2).approve(await network.getAddress(), STAKE_AMOUNT * 2n); + await network.connect(staker2).stake(STAKE_AMOUNT * 2n); + + expect(await cssvToken.totalSupply()).to.equal(await views.totalStaked()); + expect(await cssvToken.totalSupply()).to.equal(STAKE_AMOUNT * 3n); + await network.connect(staker).requestUnstake(STAKE_AMOUNT / 2n); + + expect(await cssvToken.totalSupply()).to.equal(await views.totalStaked()); + expect(await cssvToken.totalSupply()).to.equal(STAKE_AMOUNT * 3n - STAKE_AMOUNT / 2n); + }); + + it("Invariant: Sum of individual staked balances equals totalStaked", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + await ssvToken.mint(staker2.address, STAKE_AMOUNT * 2n); + await ssvToken.connect(staker2).approve(await network.getAddress(), STAKE_AMOUNT * 2n); + await network.connect(staker2).stake(STAKE_AMOUNT * 2n); + + const staker1Balance = await views.stakedBalanceOf(staker.address); + const staker2Balance = await views.stakedBalanceOf(staker2.address); + const totalStaked = await views.totalStaked(); + + expect(staker1Balance + staker2Balance).to.equal(totalStaked); + }); + + it("Invariant: Unstake request + staked balance = original stake", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const unstakeAmount = STAKE_AMOUNT / 3n; + await network.connect(staker).requestUnstake(unstakeAmount); + + const stakedBalance = await views.stakedBalanceOf(staker.address); + + const requests: UnstakeRequest[] = await views.pendingUnstake(staker.address); + const pendingAmount = requests.reduce( + (sum: bigint, r: { amount: bigint }) => sum + r.amount, + 0n + ); + + expect(stakedBalance + pendingAmount).to.equal(STAKE_AMOUNT); + }); + }); + + describe("Combined Scenarios - Full Staking Lifecycle", async function() { + + it("Full lifecycle: stake → cluster activity → unstake → withdraw", async function() { + const { network, views, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + expect(await views.stakedBalanceOf(staker.address)).to.equal(STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await connection.networkHelpers.mine(200n); + const networkEarnings = await views.getNetworkEarnings(); + expect(networkEarnings).to.be.greaterThan(0n); + const unstakeAmount = STAKE_AMOUNT / 2n; + await network.connect(staker).requestUnstake(unstakeAmount); + + expect(await views.stakedBalanceOf(staker.address)).to.equal(STAKE_AMOUNT - unstakeAmount); + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT - unstakeAmount); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await networkHelpers.mine(); + const stakerSsvBefore = await ssvToken.balanceOf(staker.address); + await network.connect(staker).withdrawUnlocked(); + const stakerSsvAfter = await ssvToken.balanceOf(staker.address); + + expect(stakerSsvAfter - stakerSsvBefore).to.equal(unstakeAmount); + expect(await views.stakedBalanceOf(staker.address)).to.equal(STAKE_AMOUNT - unstakeAmount); + }); + + it("Multiple unstake requests processed in order", async function () { + const { network, views, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + const amount1 = STAKE_AMOUNT / 4n; + const amount2 = STAKE_AMOUNT / 4n; + const amount3 = STAKE_AMOUNT / 4n; + + await network.connect(staker).requestUnstake(amount1); + await networkHelpers.time.increase(100n); + await network.connect(staker).requestUnstake(amount2); + await networkHelpers.time.increase(100n); + await network.connect(staker).requestUnstake(amount3); + const requests: UnstakeRequest[] = await views.pendingUnstake(staker.address); + + expect(requests.length).to.equal(3); + expect(requests[0].amount).to.equal(amount1); + expect(requests[1].amount).to.equal(amount2); + expect(requests[2].amount).to.equal(amount3); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await networkHelpers.mine(); + const stakerSsvBefore = await ssvToken.balanceOf(staker.address); + await network.connect(staker).withdrawUnlocked(); + const stakerSsvAfter = await ssvToken.balanceOf(staker.address); + + expect(stakerSsvAfter - stakerSsvBefore).to.equal( + amount1 + amount2 + amount3 + ); + const requestsAfter: UnstakeRequest[] = await views.pendingUnstake(staker.address); + expect(requestsAfter.length).to.equal(0); + }); + }); + + describe("Edge Cases and Error Conditions", async function() { + + it("Cannot stake zero amount", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.stake(0)).to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + + it("Cannot stake below minimum stake amount", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect(network.stake(1)).to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + + it("Cannot unstake more than staked balance", async function() { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + await expect( + network.connect(staker).requestUnstake(STAKE_AMOUNT + 1n) + ).to.be.revertedWithCustomError(network, Errors.UNSTAKE_AMOUNT_EXCEEDS_BALANCE); + }); + + it("Cannot unstake zero amount", async function() { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + await expect( + network.connect(staker).requestUnstake(0) + ).to.be.revertedWithCustomError(network, Errors.ZERO_AMOUNT); + }); + + it("Withdraws full amount one year after maturity", async function() { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + await network.connect(staker).requestUnstake(STAKE_AMOUNT); + + const oneYear = 365n * 24n * 60n * 60n; + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + oneYear); + + const balanceBefore = await ssvToken.balanceOf(staker.address); + const tx = await network.connect(staker).withdrawUnlocked(); + await expect(tx) + .to.emit(network, Events.UNSTAKE_WITHDRAWN) + .withArgs(staker.address, STAKE_AMOUNT); + + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + }); + + it("Does not change cSSV supply on withdrawal", async function() { + const { network, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + await network.connect(staker).requestUnstake(STAKE_AMOUNT); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const supplyBefore = await cssvToken.totalSupply(); + await network.connect(staker).withdrawUnlocked(); + const supplyAfter = await cssvToken.totalSupply(); + + expect(supplyAfter).to.equal(supplyBefore); + }); + + it("Cannot withdraw before cooldown expires", async function() { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + await network.connect(staker).requestUnstake(STAKE_AMOUNT); + await expect( + network.connect(staker).withdrawUnlocked() + ).to.be.revertedWithCustomError(network, Errors.NOTHING_TO_WITHDRAW); + }); + + it("Cannot withdraw with no pending requests", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect( + network.connect(staker).withdrawUnlocked() + ).to.be.revertedWithCustomError(network, Errors.NOTHING_TO_WITHDRAW); + }); + + it("Cannot exceed maximum unstake requests (10)", async function() { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const smallAmount = STAKE_AMOUNT / 20000n; + for (let i = 0; i < 2000; i++) { + await network.connect(staker).requestUnstake(smallAmount); + } + await expect( + network.connect(staker).requestUnstake(smallAmount) + ).to.be.revertedWithCustomError(network, Errors.MAX_REQUESTS_AMOUNT_REACHED); + }); + + it("Cannot unstake when caller has no cSSV", async function() { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await expect( + network.connect(staker).requestUnstake(1n) + ).to.be.revertedWithCustomError(network, Errors.UNSTAKE_AMOUNT_EXCEEDS_BALANCE); + }); + + it("Cooldown duration change only affects new unstake requests", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const firstAmount = STAKE_AMOUNT / 4n; + const firstTx = await network.connect(staker).requestUnstake(firstAmount); + const firstBlock = await firstTx.getBlock(); + const expectedFirstUnlock = BigInt(firstBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + const requestsBefore: UnstakeRequest[] = await views.pendingUnstake(staker.address); + expect(requestsBefore[0].unlockTime).to.equal(expectedFirstUnlock); + + const newCooldown = DEFAULT_UNSTAKE_COOLDOWN * 3n; + await network.updateUnstakeCooldownDuration(newCooldown); + + const requestsAfterChange: UnstakeRequest[] = await views.pendingUnstake(staker.address); + expect(requestsAfterChange[0].unlockTime).to.equal(expectedFirstUnlock); + + const secondAmount = STAKE_AMOUNT / 4n; + const secondTx = await network.connect(staker).requestUnstake(secondAmount); + const secondBlock = await secondTx.getBlock(); + const expectedSecondUnlock = BigInt(secondBlock!.timestamp) + newCooldown; + + const requestsAfterSecond: UnstakeRequest[] = await views.pendingUnstake(staker.address); + expect(requestsAfterSecond[0].unlockTime).to.equal(expectedFirstUnlock); + expect(requestsAfterSecond[1].unlockTime).to.equal(expectedSecondUnlock); + }); + + it("Cannot claim rewards when no rewards accrued", async function() { + const { network, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + await expect( + network.connect(staker).claimEthRewards() + ).to.be.revertedWithCustomError(network, Errors.NOTHING_TO_CLAIM); + }); + }); + + describe("Explicit EB staking revenue checks", async function() { + it("liquidating an explicit EB=64 cluster stops further staking revenue accrual", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(9101), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const allSigners = await connection.ethers.getSigners(); + const oracles = allSigners.slice(10, 14); + for (let i = 0; i < 4; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebBlock = Number(await connection.ethers.provider.getBlockNumber()); + const { root, proofs } = generateMerkleForClusterEB(connection, [ + { clusterId, effectiveBalance: 64 }, + ]); + await commitEBRoot(network, root, ebBlock, oracles); + + const clusterBeforeUpdate = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.updateClusterBalance( + ebBlock, + clusterOwner.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(clusterBeforeUpdate.validatorCount), + networkFeeIndex: BigInt(clusterBeforeUpdate.networkFeeIndex), + index: BigInt(clusterBeforeUpdate.index), + active: clusterBeforeUpdate.active, + balance: BigInt(clusterBeforeUpdate.balance), + }, + 64, + proofs[clusterId], + ); + + const earningsBefore = await views.getNetworkEarnings(); + const blocksPerPhase = 100n; + await connection.networkHelpers.mine(blocksPerPhase); + const earningsBeforeLiquidation = await views.getNetworkEarnings(); + const preLiquidationDelta = earningsBeforeLiquidation - earningsBefore; + const expectedPreLiquidationDelta = blocksPerPhase * NETWORK_FEE * 2n; + expect(preLiquidationDelta).to.equal(expectedPreLiquidationDelta); + + const clusterBeforeLiquidation = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).liquidate( + clusterOwner.address, + operatorIds, + { + validatorCount: Number(clusterBeforeLiquidation.validatorCount), + networkFeeIndex: BigInt(clusterBeforeLiquidation.networkFeeIndex), + index: BigInt(clusterBeforeLiquidation.index), + active: clusterBeforeLiquidation.active, + balance: BigInt(clusterBeforeLiquidation.balance), + }, + ); + + const earningsImmediatelyAfterLiquidation = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(100n); + const earningsAfterLiquidation = await views.getNetworkEarnings(); + const postLiquidationDelta = earningsAfterLiquidation - earningsImmediatelyAfterLiquidation; + expect(postLiquidationDelta).to.equal(0n); + }); + + it("staking revenue doubles when explicit EB increases from 64 to 128", async function() { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + await ssvToken.mint(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator( + makePublicKey(9102), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const allSigners = await connection.ethers.getSigners(); + const oracles = allSigners.slice(10, 14); + for (let i = 0; i < 4; i++) { + await network.replaceOracle(i + 1, oracles[i].address); + } + + await network.updateMinBlocksBetweenUpdates(1n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const eb64Block = Number(await connection.ethers.provider.getBlockNumber()); + const merkle64 = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance: 64 }]); + await commitEBRoot(network, merkle64.root, eb64Block, oracles); + const clusterBefore64 = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.updateClusterBalance( + eb64Block, + clusterOwner.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(clusterBefore64.validatorCount), + networkFeeIndex: BigInt(clusterBefore64.networkFeeIndex), + index: BigInt(clusterBefore64.index), + active: clusterBefore64.active, + balance: BigInt(clusterBefore64.balance), + }, + 64, + merkle64.proofs[clusterId], + ); + + const earningsBefore64 = await views.getNetworkEarnings(); + const blocksPerPhase = 100n; + await connection.networkHelpers.mine(blocksPerPhase); + const earningsAfter64 = await views.getNetworkEarnings(); + const delta64 = earningsAfter64 - earningsBefore64; + const expectedDelta64 = blocksPerPhase * NETWORK_FEE * 2n; + expect(delta64).to.equal(expectedDelta64); + + await connection.networkHelpers.mine(1n); + const eb128Block = Number(await connection.ethers.provider.getBlockNumber()); + const merkle128 = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance: 128 }]); + await commitEBRoot(network, merkle128.root, eb128Block, oracles); + const clusterBefore128 = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.updateClusterBalance( + eb128Block, + clusterOwner.address, + operatorIds.map((id) => BigInt(id)), + { + validatorCount: Number(clusterBefore128.validatorCount), + networkFeeIndex: BigInt(clusterBefore128.networkFeeIndex), + index: BigInt(clusterBefore128.index), + active: clusterBefore128.active, + balance: BigInt(clusterBefore128.balance), + }, + 128, + merkle128.proofs[clusterId], + ); + + const earningsBefore128 = await views.getNetworkEarnings(); + await connection.networkHelpers.mine(blocksPerPhase); + const earningsAfter128 = await views.getNetworkEarnings(); + const delta128 = earningsAfter128 - earningsBefore128; + + const expectedDelta128 = blocksPerPhase * NETWORK_FEE * 4n; + expect(delta128).to.equal(expectedDelta128); + expect(delta128).to.equal(delta64 * 2n); + }); + }); + + describe("Multisig Accounts", async function() { + + it("Multisig contract stakes SSV tokens", async function() { + const { network, views, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const networkAddress = await network.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [networkAddress, STAKE_AMOUNT]); + + const ssvBefore = await ssvToken.balanceOf(multisigAddress); + const contractSsvBefore = await ssvToken.balanceOf(networkAddress); + + const tx = await multisigExec(multisig, network, "stake", [STAKE_AMOUNT]); + + await expect(tx) + .to.emit(network, Events.STAKED) + .withArgs(multisigAddress, STAKE_AMOUNT); + + expect(await ssvToken.balanceOf(multisigAddress)).to.equal(ssvBefore - STAKE_AMOUNT); + expect(await ssvToken.balanceOf(networkAddress)).to.equal(contractSsvBefore + STAKE_AMOUNT); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT); + expect(await views.stakedBalanceOf(multisigAddress)).to.equal(STAKE_AMOUNT); + }); + + it("Multisig stakes multiple times", async function() { + const { network, views, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const networkAddress = await network.getAddress(); + + const totalAmount = STAKE_AMOUNT * 3n; + await ssvToken.mint(multisigAddress, totalAmount); + await multisigExec(multisig, ssvToken, "approve", [networkAddress, totalAmount]); + + await multisigExec(multisig, network, "stake", [STAKE_AMOUNT]); + expect(await views.stakedBalanceOf(multisigAddress)).to.equal(STAKE_AMOUNT); + + await multisigExec(multisig, network, "stake", [STAKE_AMOUNT]); + expect(await views.stakedBalanceOf(multisigAddress)).to.equal(STAKE_AMOUNT * 2n); + + await multisigExec(multisig, network, "stake", [STAKE_AMOUNT]); + expect(await views.stakedBalanceOf(multisigAddress)).to.equal(STAKE_AMOUNT * 3n); + + expect(await ssvToken.balanceOf(multisigAddress)).to.equal(0n); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(totalAmount); + }); + }); +}); diff --git a/test/integration/SSVNetworkPreMigration.test.ts b/test/integration/SSVNetworkPreMigration.test.ts new file mode 100644 index 000000000..9b803aa3e --- /dev/null +++ b/test/integration/SSVNetworkPreMigration.test.ts @@ -0,0 +1,224 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullPreUpgradeFixture, upgradeToStakingVersion } from '../setup/fixtures.ts'; +import type { NetworkHelpersType, OperatorSSV } from '../common/types.ts'; +import { CLUSTER_VERSION_ETH, CLUSTER_VERSION_SSV, DEFAULT_ETH_REGISTER_VALUE, DEFAULT_OPERATOR_ETH_FEE, DEFAULT_SHARES, EMPTY_CLUSTER, MAXIMUM_OPERATORS_FEE, MINIMAL_OPERATOR_ETH_FEE, MINIMAL_OPERATOR_FEE_SSV, MINIMUM_BLOCKS_BEFORE_LIQUIDATION, MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, NETWORK_FEE, OPERATOR_MAX_FEE_INCREASE, SMALL_ETH_REGISTER_VALUE, TOKEN_REGISTER_AMOUNT, VALIDATORS_PER_OPERATOR_LIMIT, } from '../common/constants.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { getCurrentClusterState, makePublicKey, registerOperators, registerOperatorsSSV, setupLegacyClusterAndUpgrade, setupTestContext, whitelistAddresses, } from '../helpers/index.js'; +import type { ISSVViewsTypes } from '../../types/ethers-contracts/contracts/SSVNetworkViews.js'; +import { Errors } from '../common/errors.js'; +import { Events } from '../common/events.js'; + +describe("SSVNetwork full integration tests with performing an upgrade on a legacy artifact", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner] } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullPreUpgradeFixture(connection); + }; + + const loadLegacyCluster = () => + setupLegacyClusterAndUpgrade(connection, operatorOwner, clusterOwner, () => + networkHelpers.loadFixture(deployFullSSVNetworkFixture), + ); + + describe("Legacy setup configuration", async function () { + it("Configures SSVNetwork and SSVNetworkViews correctly", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + expect(await network.getVersion()).to.be.equal("v1.2.0"); + expect(await views.ssvNetwork()).to.be.equal(await network.getAddress()); + expect(await views.getNetworkFee()).to.be.equal(NETWORK_FEE); + expect(await views.getNetworkEarnings()).to.be.equal(0); + expect(await views.getOperatorFeeIncreaseLimit()).to.be.equal(OPERATOR_MAX_FEE_INCREASE); + expect(await views.getMaximumOperatorFee()).to.be.equal(MAXIMUM_OPERATORS_FEE); + expect(await views.getLiquidationThresholdPeriod()).to.be.equal(MINIMUM_BLOCKS_BEFORE_LIQUIDATION); + expect(await views.getMinimumLiquidationCollateral()).to.be.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL); + expect(await views.getValidatorsPerOperatorLimit()).to.be.equal(VALIDATORS_PER_OPERATOR_LIMIT); + }); + }); + + describe("Restrictions for legacy ssv clusters", async function () { + it("'registerValidator()' is reverted with 'IncorrectClusterVersion' if trying to register validators to a legacy cluster", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + await expect(newNetwork.connect(clusterOwner).registerValidator(makePublicKey(322), operatorIds, DEFAULT_SHARES, cluster)).to.be.revertedWithCustomError(newNetwork, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("'bulkRegisterValidator()' is reverted with 'IncorrectClusterVersion' if trying to register validators to a legacy cluster", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + await expect(newNetwork.connect(clusterOwner).bulkRegisterValidator([makePublicKey(322)], operatorIds, [DEFAULT_SHARES], cluster)).to.be.revertedWithCustomError(newNetwork, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("'removeValidator()' succeeds on legacy SSV clusters (BUG-12 fix)", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + const removeTx = await newNetwork.connect(clusterOwner).removeValidator(makePublicKey(123), operatorIds, cluster); + await removeTx.wait(); + }); + + it("'bulkRemoveValidator()' succeeds on legacy SSV clusters (BUG-12 fix)", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + const bulkRemoveTx = await newNetwork.connect(clusterOwner).bulkRemoveValidator([makePublicKey(123)], operatorIds, cluster); + await bulkRemoveTx.wait(); + }); + + it("'liquidate()' is reverted with 'IncorrectClusterVersion' if trying to liquidate legacy ssv cluster with an ETH-based function", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + await expect(newNetwork.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster)).to.be.revertedWithCustomError(newNetwork, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("'reactivate()' is reverted with 'IncorrectClusterVersion' if trying to reactivate a legacy ssv cluster", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + await expect(newNetwork.connect(clusterOwner).reactivate(operatorIds, cluster)).to.be.revertedWithCustomError(newNetwork, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("withdraw() is reverted with 'IncorrectClusterVersion' is trying to withdraw from a legacy ssv cluster", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + await expect(newNetwork.connect(clusterOwner).withdraw(operatorIds, TOKEN_REGISTER_AMOUNT, cluster)).to.be.revertedWithCustomError(newNetwork, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("deposit() is reverted with 'IncorrectClusterVersion' is trying to deposit to a legacy ssv cluster", async function () { + const { newNetwork, operatorIds, cluster } = await loadLegacyCluster(); + await expect(newNetwork.connect(clusterOwner).deposit(clusterOwner.address, operatorIds, cluster, { value: SMALL_ETH_REGISTER_VALUE })).to.be.revertedWithCustomError(newNetwork, Errors.INCORRECT_CLUSTER_VERSION); + }); + }); + + describe("Function 'migrateClusterToETH'", async function () { + it("Executes as expected and emits correct event", async function () { + const { network, newNetwork, newViews, ssvToken, operatorIds, cluster } = await loadLegacyCluster(); + const clusterBalance = await newViews.getBalanceSSV(clusterOwner.address, operatorIds, cluster); + const burnRateSSV = await newViews.getBurnRateSSV(clusterOwner.address, operatorIds, cluster); + const expectedClusterBalance = clusterBalance - burnRateSSV; + const tx = await newNetwork.connect(clusterOwner).migrateClusterToETH(operatorIds, cluster, { value: SMALL_ETH_REGISTER_VALUE }); + await tx.wait(); + await expect(tx) + .to.changeTokenBalances(connection.ethers, ssvToken, [newNetwork, clusterOwner], [-expectedClusterBalance, expectedClusterBalance]); + await expect(tx).to.emit(newNetwork, Events.CLUSTER_MIGRATED_TO_ETH); + const clusterAfter = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(await newViews.getClusterAssetType(clusterOwner.address, operatorIds)).to.be.equal(CLUSTER_VERSION_ETH); + await expect(newViews.getBalanceSSV(clusterOwner.address, operatorIds, clusterAfter)).to.be.revertedWithCustomError(newViews, Errors.INCORRECT_CLUSTER_VERSION); + expect(await newViews.getBalance(clusterOwner.address, operatorIds, clusterAfter)).to.be.equal(SMALL_ETH_REGISTER_VALUE); + expect(clusterAfter.active).to.be.equal(true); + }); + + it("Migrates a liquidated cluster, emits correct events and reactivates cluster", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await ssvToken.mint(clusterOwner.address, TOKEN_REGISTER_AMOUNT); + await ssvToken.connect(clusterOwner).approve(await network.getAddress(), TOKEN_REGISTER_AMOUNT); + const operatorIds = await registerOperatorsSSV(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator(makePublicKey(123), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER); + let cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + const { newNetwork, newViews } = await upgradeToStakingVersion(connection, network, views); + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const tx = await newNetwork.connect(clusterOwner).migrateClusterToETH(operatorIds, cluster, { value: SMALL_ETH_REGISTER_VALUE }); + await tx.wait(); + await expect(tx).to.emit(newNetwork, Events.CLUSTER_MIGRATED_TO_ETH); + await expect(tx).to.emit(newNetwork, Events.CLUSTER_REACTIVATED); + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(await newViews.getClusterAssetType(clusterOwner.address, operatorIds)).to.be.equal(CLUSTER_VERSION_ETH); + await expect(newViews.getBalanceSSV(clusterOwner.address, operatorIds, cluster)).to.be.revertedWithCustomError(newViews, Errors.INCORRECT_CLUSTER_VERSION); + expect(await newViews.getBalance(clusterOwner.address, operatorIds, cluster)).to.be.equal(SMALL_ETH_REGISTER_VALUE); + expect(cluster.active).to.be.equal(true); + }); + }); + + describe("Legacy operators migration", async function () { + it("Migrates operators to the eth version after migration of operator's cluster", async function () { + const { newNetwork, newViews, operatorIds, cluster } = await loadLegacyCluster(); + expect(await newNetwork.getVersion()).to.be.equal("v2.0.0"); + expect(await newViews.getClusterAssetType(clusterOwner.address, operatorIds)).to.be.equal(CLUSTER_VERSION_SSV); + for (let i = 0; i < operatorIds.length; i++) { + const opSSV: OperatorSSV = await newViews.getOperatorByIdSSV(operatorIds[i]); + const opEth: ISSVViewsTypes.OperatorDataStructOutput = await newViews.getOperatorById(operatorIds[i]); + expect(opSSV.validatorCount).to.be.equal(1); + expect(opEth.validatorCount).to.be.equal(0); + expect(opSSV.fee).to.be.equal(MINIMAL_OPERATOR_FEE_SSV); + expect(opEth.fee).to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + expect(opSSV.isActive).to.be.equal(true); + expect(opEth.isActive).to.be.equal(true); + } + await newNetwork.connect(clusterOwner).migrateClusterToETH(operatorIds, cluster, { value: SMALL_ETH_REGISTER_VALUE }); + expect(await newViews.getClusterAssetType(clusterOwner.address, operatorIds)).to.be.equal(CLUSTER_VERSION_ETH); + for (let i = 0; i < operatorIds.length; i++) { + const opSSV: OperatorSSV = await newViews.getOperatorByIdSSV(operatorIds[i]); + const opEth: ISSVViewsTypes.OperatorDataStructOutput = await newViews.getOperatorById(operatorIds[i]); + expect(opSSV.validatorCount).to.be.equal(0); + expect(opEth.validatorCount).to.be.equal(1); + expect(opSSV.fee).to.be.equal(MINIMAL_OPERATOR_FEE_SSV); + expect(opEth.fee).to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + expect(opSSV.isActive).to.be.equal(true); + expect(opEth.isActive).to.be.equal(true); + } + }); + }); + + describe("Sanity", async function () { + it("Migrates operators to ETH after registering an ETH validator and applies correct fees", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const operatorIds = await registerOperatorsSSV(network, operatorOwner, 4); + const { newNetwork, newViews } = await upgradeToStakingVersion(connection, network, views); + for (let i = 0; i < operatorIds.length; i++) { + const opSSVFee = await newViews.getOperatorFeeSSV(operatorIds[i]); + const opEthFee = await newViews.getOperatorFee(operatorIds[i]); + expect(opSSVFee).to.be.equal(MINIMAL_OPERATOR_FEE_SSV); + expect(opEthFee).to.be.equal(DEFAULT_OPERATOR_ETH_FEE); + } + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await newNetwork.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE }); + await networkHelpers.mine(999); + for (let i = 0; i < operatorIds.length; i++) { + const opSSVFee = await newViews.getOperatorFeeSSV(operatorIds[i]); + const opEthFee = await newViews.getOperatorFee(operatorIds[i]); + expect(opSSVFee).to.be.equal(MINIMAL_OPERATOR_FEE_SSV); + expect(opEthFee).to.be.equal(DEFAULT_OPERATOR_ETH_FEE); + expect(await newViews.getOperatorEarnings(operatorIds[i])).to.be.equal(DEFAULT_OPERATOR_ETH_FEE * 999n); + } + }); + + it("Reactivates liquidated cluster during migration, emits event and applies correct fees", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + await ssvToken.mint(clusterOwner.address, TOKEN_REGISTER_AMOUNT); + await ssvToken.connect(clusterOwner).approve(await network.getAddress(), TOKEN_REGISTER_AMOUNT); + const operatorIds = await registerOperatorsSSV(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(clusterOwner).registerValidator(makePublicKey(123), operatorIds, DEFAULT_SHARES, TOKEN_REGISTER_AMOUNT, EMPTY_CLUSTER); + let cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + const { newNetwork, newViews } = await upgradeToStakingVersion(connection, network, views); + expect(await newViews.isLiquidated(clusterOwner.address, operatorIds, cluster)).to.be.equal(true); + const tx = await newNetwork.connect(clusterOwner) + .migrateClusterToETH(operatorIds, cluster, { value: DEFAULT_ETH_REGISTER_VALUE }); + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(tx).to.emit(newNetwork, Events.CLUSTER_REACTIVATED); + expect(await newViews.isLiquidated(clusterOwner.address, operatorIds, cluster)).to.be.equal(false); + await networkHelpers.mine(999); + for (let i = 0; i < operatorIds.length; i++) { + const opEthFee = await newViews.getOperatorFee(operatorIds[i]); + expect(opEthFee).to.be.equal(DEFAULT_OPERATOR_ETH_FEE); + expect(await newViews.getOperatorEarnings(operatorIds[i])).to.be.equal(DEFAULT_OPERATOR_ETH_FEE * 999n); + } + }); + + it("Allows to remove all validators from a liquidated cluster", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { newNetwork, newViews } = await upgradeToStakingVersion(connection, network, views); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await newNetwork.connect(clusterOwner).registerValidator(makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE }); + let cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await newNetwork.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(await newViews.isLiquidated(clusterOwner.address, operatorIds, cluster)).to.be.equal(true); + await newNetwork.connect(clusterOwner).removeValidator(makePublicKey(1), operatorIds, cluster); + cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + expect(cluster.validatorCount).to.be.deep.equal(0); + }); + }); +}); diff --git a/test/liquidate/liquidate.ts b/test/liquidate/liquidate.ts deleted file mode 100644 index 5f29f258f..000000000 --- a/test/liquidate/liquidate.ts +++ /dev/null @@ -1,378 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; - -import { expect } from 'chai'; - -let ssvNetwork: any, ssvViews: any, ssvToken: any, minDepositAmount: BigInt, firstCluster: Cluster; - -// Declare globals -describe('Liquidate Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - minDepositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation + 10) * CONFIG.minimalOperatorFee * 4n; - - // cold register - await coldRegisterValidator(); - - // first validator - firstCluster = ( - await bulkRegisterValidators( - 4, - 1, - DEFAULT_OPERATOR_IDS[4], - minDepositAmount, - { validatorCount: 0, networkFeeIndex: 0, index: 0, balance: 0n, active: true }, - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ) - ).args; - }); - - it('Liquidate a cluster via liquidation threshold emits "ClusterLiquidated"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - - await assertEvent( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [ - { - contract: ssvNetwork, - eventName: 'ClusterLiquidated', - }, - { - contract: ssvToken, - eventName: 'Transfer', - argNames: ['from', 'to', 'value'], - argValuesList: [ - [ - ssvNetwork.address, - owners[0].account.address, - minDepositAmount - CONFIG.minimalOperatorFee * 4n * BigInt(CONFIG.minimalBlocksBeforeLiquidation + 1), - ], - ], - }, - ], - ); - }); - - it('Liquidate a cluster via minimum liquidation collateral emits "ClusterLiquidated"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation - 2); - - await assertEvent( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [ - { - contract: ssvNetwork, - eventName: 'ClusterLiquidated', - }, - { - contract: ssvToken, - eventName: 'Transfer', - argNames: ['from', 'to', 'value'], - argValuesList: [ - [ - ssvNetwork.address, - owners[0].account.address, - minDepositAmount - CONFIG.minimalOperatorFee * 4n * BigInt(CONFIG.minimalBlocksBeforeLiquidation + 1 - 2), - ], - ], - }, - ], - ); - }); - - it('Liquidate a cluster after liquidation period emits "ClusterLiquidated"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - - await assertEvent( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [ - { - contract: ssvNetwork, - eventName: 'ClusterLiquidated', - }, - ], - { - contract: ssvToken, - eventName: 'Transfer', - }, - ); - }); - - it('Liquidatable with removed operator', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - await ssvNetwork.write.removeOperator([1]); - expect( - await ssvViews.read.isLiquidatable([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidatable with removed operator after liquidation period', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - await ssvNetwork.write.removeOperator([1]); - expect( - await ssvViews.read.isLiquidatable([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidate validator with removed operator in a cluster', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - await ssvNetwork.write.removeOperator([1]); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - expect( - await ssvViews.read.isLiquidatable([updatedCluster.owner, updatedCluster.operatorIds, updatedCluster.cluster]), - ).to.be.equals(false); - }); - - it('Liquidate and register validator in a disabled cluster reverts "ClusterIsLiquidated"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - await mine(CONFIG.minimalBlocksBeforeLiquidation); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { - account: owners[1].account, - }); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - updatedCluster.operatorIds, - await DataGenerator.shares(1, 2, updatedCluster.operatorIds), - minDepositAmount * 2n, - updatedCluster.cluster, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('IncorrectClusterState'); - }); - - it('Liquidate cluster (4 operators) and check isLiquidated true', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, updatedCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidate cluster (7 operators) and check isLiquidated true', async () => { - const depositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation + 10) * (CONFIG.minimalOperatorFee * 7n); - - const cluster = await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[7], depositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - firstCluster = cluster.args; - - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_7], - ); - firstCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidate cluster (10 operators) and check isLiquidated true', async () => { - const depositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation + 10) * (CONFIG.minimalOperatorFee * 10n); - - const cluster = await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[10], depositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - firstCluster = cluster.args; - - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_10], - ); - firstCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidate cluster (13 operators) and check isLiquidated true', async () => { - const depositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation + 10) * (CONFIG.minimalOperatorFee * 13n); - - const cluster = await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[13], depositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - firstCluster = cluster.args; - - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_13], - ); - firstCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidate a non liquidatable cluster that I own', async () => { - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster], { - account: owners[4].account, - }), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, updatedCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidate cluster that I own', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster], { - account: owners[4].account, - }), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, updatedCluster.cluster]), - ).to.equal(true); - }); - - it('Liquidate cluster that I own after liquidation period', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster], { - account: owners[4].account, - }), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, updatedCluster.cluster]), - ).to.equal(true); - }); - - it('Get if the cluster is liquidatable', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - expect( - await ssvViews.read.isLiquidatable([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(true); - }); - - it('Get if the cluster is liquidatable after liquidation period', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - expect( - await ssvViews.read.isLiquidatable([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(true); - }); - - it('Get if the cluster is not liquidatable', async () => { - expect( - await ssvViews.read.isLiquidatable([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(false); - }); - - it('Liquidate a cluster that is not liquidatable reverts "ClusterNotLiquidatable"', async () => { - await expect( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.be.rejectedWith('ClusterNotLiquidatable'); - expect( - await ssvViews.read.isLiquidatable([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ).to.equal(false); - }); - - it('Liquidate a cluster that is not liquidatable reverts "IncorrectClusterState"', async () => { - await expect( - ssvNetwork.write.liquidate([ - firstCluster.owner, - firstCluster.operatorIds, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]), - ).to.be.rejectedWith('IncorrectClusterState'); - }); - - it('Liquidate already liquidated cluster reverts "ClusterIsLiquidated"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - await expect( - ssvNetwork.write.liquidate([firstCluster.owner, updatedCluster.operatorIds, updatedCluster.cluster]), - ).to.be.rejectedWith('ClusterIsLiquidated'); - }); - - it('Is liquidated reverts "ClusterDoesNotExists"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster], { - account: owners[4].account, - }), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - await expect( - ssvViews.read.isLiquidated([owners[1].account.address, firstCluster.operatorIds, updatedCluster.cluster]), - ).to.be.rejectedWith('ClusterDoesNotExists'); - }); -}); diff --git a/test/liquidate/liquidated-cluster.ts b/test/liquidate/liquidated-cluster.ts deleted file mode 100644 index 2f62f236c..000000000 --- a/test/liquidate/liquidated-cluster.ts +++ /dev/null @@ -1,219 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - bulkRegisterValidators, - deposit, - liquidate, - withdraw, - reactivate, - removeValidator, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; - -import { expect } from 'chai'; - -let ssvNetwork: any, - ssvViews: any, - ssvToken: any, - minDepositAmount: BigInt, - firstCluster: Cluster, - burnPerBlock: BigInt, - networkFee: BigInt; - -// Declare globals -describe('Liquidate Cluster Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - networkFee = CONFIG.minimalOperatorFee; - burnPerBlock = CONFIG.minimalOperatorFee * 4n + networkFee; - minDepositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation) * burnPerBlock; - - await ssvNetwork.write.updateNetworkFee([networkFee]); - - // first validator - firstCluster = ( - await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[4], minDepositAmount * 2n, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - }); - - it('Liquidate -> deposit -> reactivate', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - - let clusterEventData = await liquidate(firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster); - - expect( - await ssvViews.read.isLiquidated([firstCluster.owner, firstCluster.operatorIds, clusterEventData.cluster]), - ).to.equal(true); - - clusterEventData = await deposit( - 1, - firstCluster.owner, - firstCluster.operatorIds, - minDepositAmount, - clusterEventData.cluster, - ); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - - await assertEvent( - ssvNetwork.write.reactivate([clusterEventData.operatorIds, minDepositAmount, clusterEventData.cluster], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterReactivated', - }, - ], - ); - }); - - it('RegisterValidator -> liquidate -> removeValidator -> deposit -> withdraw', async () => { - let clusterEventData = await bulkRegisterValidators( - 1, - 2, - DEFAULT_OPERATOR_IDS[4], - minDepositAmount, - firstCluster.cluster, - ); - - await mine(CONFIG.minimalBlocksBeforeLiquidation); - - clusterEventData.args = await liquidate( - clusterEventData.args.owner, - clusterEventData.args.operatorIds, - clusterEventData.args.cluster, - ); - await expect(clusterEventData.args.cluster.balance).to.be.equals(0); - - clusterEventData.args = await removeValidator( - 1, - DataGenerator.publicKey(1), - clusterEventData.args.operatorIds, - clusterEventData.args.cluster, - ); - - clusterEventData.args = await deposit( - 1, - clusterEventData.args.owner, - clusterEventData.args.operatorIds, - minDepositAmount, - clusterEventData.args.cluster, - ); - await expect(clusterEventData.args.cluster.balance).to.be.equals(minDepositAmount); // shrink - - await expect( - ssvNetwork.write.withdraw([clusterEventData.args.operatorIds, minDepositAmount, clusterEventData.args.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('ClusterIsLiquidated'); - }); - - it('Withdraw -> liquidate -> deposit -> reactivate', async () => { - await mine(2); - - const withdrawAmount: BigInt = 20000000n; - let clusterEventData = await withdraw(1, firstCluster.operatorIds, withdrawAmount, firstCluster.cluster); - expect( - await ssvViews.read.getBalance([ - owners[1].account.address, - clusterEventData.operatorIds, - clusterEventData.cluster, - ]), - ).to.be.equal(minDepositAmount * 2n - withdrawAmount - burnPerBlock * 3n); - - await mine(CONFIG.minimalBlocksBeforeLiquidation - 2); - - clusterEventData = await liquidate(clusterEventData.owner, clusterEventData.operatorIds, clusterEventData.cluster); - await expect( - ssvViews.read.getBalance([owners[1].account.address, clusterEventData.operatorIds, clusterEventData.cluster]), - ).to.be.rejectedWith('ClusterIsLiquidated'); - - clusterEventData = await deposit( - 1, - clusterEventData.owner, - clusterEventData.operatorIds, - minDepositAmount, - clusterEventData.cluster, - ); - - clusterEventData = await reactivate(1, clusterEventData.operatorIds, minDepositAmount, clusterEventData.cluster); - expect( - await ssvViews.read.getBalance([ - owners[1].account.address, - clusterEventData.operatorIds, - clusterEventData.cluster, - ]), - ).to.be.equal(minDepositAmount * 2n); - - await mine(2); - expect( - await ssvViews.read.getBalance([ - owners[1].account.address, - clusterEventData.operatorIds, - clusterEventData.cluster, - ]), - ).to.be.equal(minDepositAmount * 2n - burnPerBlock * 2n); - }); - - it('Remove validator -> withdraw -> try liquidate reverts "ClusterNotLiquidatable"', async () => { - let clusterEventData = ( - await bulkRegisterValidators(2, 1, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - - await mine(CONFIG.minimalBlocksBeforeLiquidation - 10); - - const remove = await trackGas( - ssvNetwork.write.removeValidator( - [DataGenerator.publicKey(2), clusterEventData.operatorIds, clusterEventData.cluster], - { account: owners[2].account }, - ), - ); - clusterEventData = remove.eventsByName.ValidatorRemoved[0].args; - - let balance = await ssvViews.read.getBalance([ - owners[2].account.address, - clusterEventData.operatorIds, - clusterEventData.cluster, - ]); - - clusterEventData = await withdraw( - 2, - clusterEventData.operatorIds, - (balance - BigInt(CONFIG.minimumLiquidationCollateral)) * (101n / 100n), - clusterEventData.cluster, - ); - - await expect( - ssvNetwork.write.liquidate([clusterEventData.owner, clusterEventData.operatorIds, clusterEventData.cluster]), - ).to.be.rejectedWith('ClusterNotLiquidatable'); - }); -}); diff --git a/test/liquidate/reactivate.ts b/test/liquidate/reactivate.ts deleted file mode 100644 index 0e3471576..000000000 --- a/test/liquidate/reactivate.ts +++ /dev/null @@ -1,161 +0,0 @@ -// Declare imports -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - deposit, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; - -import { expect } from 'chai'; - -let ssvNetwork: any, ssvToken: any, minDepositAmount: BigInt, firstCluster: Cluster; - -// Declare globals -describe('Reactivate Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvToken = metadata.ssvToken; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - minDepositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation + 10) * CONFIG.minimalOperatorFee * 4n; - - // Register validators - // cold register - await coldRegisterValidator(); - - // first validator - firstCluster = ( - await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - }); - - it('Reactivate a disabled cluster emits "ClusterReactivated"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[1].account, - }); - - await assertEvent( - ssvNetwork.write.reactivate([updatedCluster.operatorIds, minDepositAmount, updatedCluster.cluster], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterReactivated', - }, - ], - ); - }); - - it('Reactivate a cluster with a removed operator in the cluster', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - await ssvNetwork.write.removeOperator([1]); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[1].account, - }); - await trackGas( - ssvNetwork.write.reactivate([updatedCluster.operatorIds, minDepositAmount, updatedCluster.cluster], { - account: owners[1].account, - }), - [GasGroup.REACTIVATE_CLUSTER], - ); - }); - - it('Reactivate an enabled cluster reverts "ClusterAlreadyEnabled"', async () => { - await expect( - ssvNetwork.write.reactivate([firstCluster.operatorIds, minDepositAmount, firstCluster.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('ClusterAlreadyEnabled'); - }); - - it('Reactivate a cluster when the amount is not enough reverts "InsufficientBalance"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - await expect( - ssvNetwork.write.reactivate([updatedCluster.operatorIds, CONFIG.minimalOperatorFee, updatedCluster.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Reactivate a liquidated cluster after making a deposit', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ); - let clusterData = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[1].account, - }); - - clusterData = await deposit(1, firstCluster.owner, firstCluster.operatorIds, minDepositAmount, clusterData.cluster); - - await assertEvent( - ssvNetwork.write.reactivate([firstCluster.operatorIds, 0, clusterData.cluster], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ClusterReactivated', - }, - ], - ); - }); - - it('Reactivate a cluster after liquidation period when the amount is not enough reverts "InsufficientBalance"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[1].account, - }); - await expect( - ssvNetwork.write.reactivate([updatedCluster.operatorIds, CONFIG.minimalOperatorFee, updatedCluster.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('InsufficientBalance'); - }); -}); diff --git a/test/operators/external-whitelist.ts b/test/operators/external-whitelist.ts deleted file mode 100644 index 250f66922..000000000 --- a/test/operators/external-whitelist.ts +++ /dev/null @@ -1,86 +0,0 @@ -import hre from 'hardhat'; - -import { assertEvent } from '../helpers/utils/test'; -const { expect } = require('chai'); - -describe('BasicWhitelisting', () => { - let basicWhitelisting: any, owners: any; - - beforeEach(async () => { - owners = await hre.viem.getWalletClients(); - - basicWhitelisting = await hre.viem.deployContract('BasicWhitelisting'); - }); - - describe('Deployment', async () => { - it('Should set the right owner', async () => { - expect(await basicWhitelisting.read.owner()).to.deep.equal(owners[0].account.address); - }); - }); - - describe('Whitelisting', async () => { - it('Should whitelist an address', async () => { - const addr1 = owners[2].account.address; - - await basicWhitelisting.write.addWhitelistedAddress([addr1]); - expect(await basicWhitelisting.read.isWhitelisted([addr1, 0])).to.be.true; - }); - - it('Should remove an address from whitelist', async () => { - const addr1 = owners[2].account.address; - - await basicWhitelisting.write.addWhitelistedAddress([addr1]); - await basicWhitelisting.write.removeWhitelistedAddress([addr1]); - expect(await basicWhitelisting.read.isWhitelisted([addr1, 0])).to.be.false; - }); - - it('Should emit AddressWhitelisted event', async () => { - const addr1 = owners[2].account.address; - - await assertEvent(basicWhitelisting.write.addWhitelistedAddress([addr1]), [ - { - contract: basicWhitelisting, - eventName: 'AddressWhitelisted', - argNames: ['account'], - argValuesList: [[addr1]], - }, - ]); - }); - - it('Should emit AddressRemovedFromWhitelist event', async () => { - const addr1 = owners[2].account.address; - - await basicWhitelisting.write.addWhitelistedAddress([addr1]); - - await assertEvent(basicWhitelisting.write.removeWhitelistedAddress([addr1]), [ - { - contract: basicWhitelisting, - eventName: 'AddressRemovedFromWhitelist', - argNames: ['account'], - argValuesList: [[addr1]], - }, - ]); - }); - - it('Should only allow the owner to whitelist addresses', async () => { - const addr1 = owners[2].account.address; - - await expect( - basicWhitelisting.write.addWhitelistedAddress([addr1], { - account: owners[1].account, - }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); - - it('Should only allow the owner to remove addresses from whitelist', async () => { - const addr1 = owners[2].account.address; - - await basicWhitelisting.write.addWhitelistedAddress([addr1]); - await expect( - basicWhitelisting.write.removeWhitelistedAddress([addr1], { - account: owners[1].account, - }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); - }); -}); diff --git a/test/operators/others.ts b/test/operators/others.ts deleted file mode 100644 index c4af54020..000000000 --- a/test/operators/others.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Declare imports -import { owners, initializeContract, registerOperators, DataGenerator, CONFIG } from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { ethers } from 'hardhat'; -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any; - -describe('Others Operator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - }); - - it('Add fee recipient address emits "FeeRecipientAddressUpdated"', async () => { - await assertEvent( - ssvNetwork.write.setFeeRecipientAddress([owners[2].account.address], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'FeeRecipientAddressUpdated', - argNames: ['owner', 'recipientAddress'], - argValuesList: [[owners[1].account.address, owners[2].account.address]], - }, - ], - ); - }); - - it('Get the maximum number of validators per operator', async () => { - expect(await ssvViews.read.getValidatorsPerOperatorLimit()).to.equal(CONFIG.validatorsPerOperatorLimit); - }); -}); diff --git a/test/operators/register.ts b/test/operators/register.ts deleted file mode 100644 index 99fa82c6c..000000000 --- a/test/operators/register.ts +++ /dev/null @@ -1,196 +0,0 @@ -// Declare imports -import { owners, initializeContract, DataGenerator, CONFIG } from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { ethers } from 'hardhat'; -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any; - -describe('Register Operator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - }); - - it('Register operator emits "OperatorAdded" and "OperatorPrivacyStatusUpdated" if setPrivate is true', async () => { - const publicKey = DataGenerator.publicKey(0); - - await assertEvent( - ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorAdded', - argNames: ['operatorId', 'owner', 'publicKey', 'fee'], - argValuesList: [[1, owners[1].account.address, publicKey, CONFIG.minimalOperatorFee]], - }, - { - contract: ssvNetwork, - eventName: 'OperatorPrivacyStatusUpdated', - argNames: ['operatorIds', 'toPrivate'], - argValuesList: [[[1], true]], - }, - ], - ); - }); - - it('Register operator emits "OperatorAdded" and "OperatorPrivacyStatusUpdated" if setPrivate is false', async () => { - const publicKey = DataGenerator.publicKey(0); - - await assertEvent( - ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorAdded', - argNames: ['operatorId', 'owner', 'publicKey', 'fee'], - argValuesList: [[1, owners[1].account.address, publicKey, CONFIG.minimalOperatorFee]], - }, - { - contract: ssvNetwork, - eventName: 'OperatorPrivacyStatusUpdated', - argNames: ['operatorIds', 'toPrivate'], - argValuesList: [[[1], false]], - }, - ], - ); - }); - - it('Register operator gas limits', async () => { - await trackGas( - ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }), - [GasGroup.REGISTER_OPERATOR], - ); - - await trackGas( - ssvNetwork.write.registerOperator([DataGenerator.publicKey(1), CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }), - [GasGroup.REGISTER_OPERATOR], - ); - }); - - it('Get operator by id with setPrivate false', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - false, // isPrivate - true, // active - ]); - }); - - it('Get operator by id with setPrivate true', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - true, // isPrivate - true, // active - ]); - }); - - it('Get non-existent operator by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([5])).to.deep.equal([ - ethers.ZeroAddress, // owner - 0, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - false, // isPrivate - false, // active - ]); - }); - - it('Get operator removed by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }); - await ssvNetwork.write.removeOperator([1], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - 0, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - false, // isPrivate - false, // active - ]); - }); - - it('Register an operator with a fee thats too low reverts "FeeTooLow", setPrivate false', async () => { - await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), '10', false])).to.be.rejectedWith( - 'FeeTooLow', - ); - }); - - it('Register an operator with a fee thats too low reverts "FeeTooLow", setPrivate true', async () => { - await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), '10', true])).to.be.rejectedWith( - 'FeeTooLow', - ); - }); - - it('Register an operator with a fee thats too high reverts "FeeTooHigh", setPrivate false', async () => { - await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), 2e14, false])).to.be.rejectedWith( - 'FeeTooHigh', - ); - }); - - it('Register an operator with a fee thats too high reverts "FeeTooHigh", setPrivate false', async () => { - await expect(ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), 2e14, true])).to.be.rejectedWith( - 'FeeTooHigh', - ); - }); - - it('Register same operator twice reverts "OperatorAlreadyExists", setPrivate false', async () => { - const publicKey = DataGenerator.publicKey(1); - await ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }); - - await expect( - ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }), - ).to.be.rejectedWith('OperatorAlreadyExists'); - }); - - it('Register same operator twice reverts "OperatorAlreadyExists", setPrivate true', async () => { - const publicKey = DataGenerator.publicKey(1); - await ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }); - - await expect( - ssvNetwork.write.registerOperator([publicKey, CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }), - ).to.be.rejectedWith('OperatorAlreadyExists'); - }); -}); diff --git a/test/operators/remove.ts b/test/operators/remove.ts deleted file mode 100644 index 2648621d3..000000000 --- a/test/operators/remove.ts +++ /dev/null @@ -1,132 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - bulkRegisterValidators, - coldRegisterValidator, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { ethers } from 'hardhat'; -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any; - -describe('Remove Operator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - // Register a validator - // cold register - await coldRegisterValidator(); - }); - - it('Remove operator emits "OperatorRemoved"', async () => { - await assertEvent(ssvNetwork.write.removeOperator([1]), [ - { - contract: ssvNetwork, - eventName: 'OperatorRemoved', - argNames: ['operatorId'], - argValuesList: [[1]], - }, - ]); - }); - - it('Remove private operator emits "OperatorRemoved"', async () => { - const result = await trackGas( - ssvNetwork.write.registerOperator([DataGenerator.publicKey(22), CONFIG.minimalOperatorFee, true]), - ); - const { operatorId } = result.eventsByName.OperatorAdded[0].args; - - await ssvNetwork.write.setOperatorsWhitelists([[operatorId], [owners[2].account.address]]); - - await assertEvent(ssvNetwork.write.removeOperator([operatorId]), [ - { - contract: ssvNetwork, - eventName: 'OperatorRemoved', - argNames: ['operatorId'], - argValuesList: [[operatorId]], - }, - ]); - - expect(await ssvViews.read.getOperatorById([operatorId])).to.deep.equal([ - owners[0].account.address, // owner - 0, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - true, // isPrivate - false, // active - ]); - }); - - it('Remove operator gas limits', async () => { - await trackGas(ssvNetwork.write.removeOperator([1]), [GasGroup.REMOVE_OPERATOR]); - }); - - it('Remove operator with a balance emits "OperatorWithdrawn"', async () => { - await bulkRegisterValidators( - 4, - 1, - DEFAULT_OPERATOR_IDS[4], - BigInt(CONFIG.minimalBlocksBeforeLiquidation) * CONFIG.minimalOperatorFee * 4n, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ); - - await assertEvent(ssvNetwork.write.removeOperator([1]), [ - { - contract: ssvNetwork, - eventName: 'OperatorRemoved', - argNames: ['operatorId'], - argValuesList: [[1]], - }, - ]); - }); - - it('Remove operator with a balance gas limits', async () => { - await bulkRegisterValidators( - 4, - 1, - DEFAULT_OPERATOR_IDS[4], - BigInt(CONFIG.minimalBlocksBeforeLiquidation) * CONFIG.minimalOperatorFee * 4n, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ); - await trackGas(ssvNetwork.write.removeOperator([1]), [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]); - }); - - it('Remove operator I do not own reverts "CallerNotOwnerWithData"', async () => { - await expect( - ssvNetwork.write.removeOperator([1], { - account: owners[1].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Remove same operator twice reverts "OperatorDoesNotExist"', async () => { - await ssvNetwork.write.removeOperator([1]); - await expect(ssvNetwork.write.removeOperator([1])).to.be.rejectedWith('OperatorDoesNotExist'); - }); -}); diff --git a/test/operators/update-fee.ts b/test/operators/update-fee.ts deleted file mode 100644 index 556ef0ae2..000000000 --- a/test/operators/update-fee.ts +++ /dev/null @@ -1,481 +0,0 @@ -// Declare imports -import { owners, initializeContract, registerOperators, DataGenerator, CONFIG } from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { time } from '@nomicfoundation/hardhat-network-helpers'; - -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, initialFee: BigInt; - -describe('Operator Fee Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - - initialFee = CONFIG.minimalOperatorFee * 10n; - await registerOperators(2, 1, initialFee); - }); - - it('Declare fee emits "OperatorFeeDeclared"', async () => { - await assertEvent( - ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeDeclared', - }, - ], - ); - }); - - it('Declare fee gas limits"', async () => { - await trackGas( - ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }), - [GasGroup.DECLARE_OPERATOR_FEE], - ); - }); - - it('Declare fee with zero value emits "OperatorFeeDeclared"', async () => { - await assertEvent( - ssvNetwork.write.declareOperatorFee([1, 0], { - account: owners[2].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeDeclared', - }, - ], - ); - }); - - it('Declare a lower fee gas limits', async () => { - await trackGas( - ssvNetwork.write.declareOperatorFee([1, initialFee - initialFee / 10n], { - account: owners[2].account, - }), - [GasGroup.DECLARE_OPERATOR_FEE], - ); - }); - - it('Declare a higher fee gas limit', async () => { - await trackGas( - ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }), - [GasGroup.DECLARE_OPERATOR_FEE], - ); - }); - - it('Cancel declared fee emits "OperatorFeeDeclarationCancelled"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await assertEvent( - ssvNetwork.write.cancelDeclaredOperatorFee([1], { - account: owners[2].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeDeclarationCancelled', - }, - ], - ); - }); - - it('Cancel declared fee gas limits', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - await trackGas( - ssvNetwork.write.cancelDeclaredOperatorFee([1], { - account: owners[2].account, - }), - [GasGroup.CANCEL_OPERATOR_FEE], - ); - }); - - it('Execute declared fee emits "OperatorFeeExecuted"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await time.increase(CONFIG.declareOperatorFeePeriod); - - await assertEvent( - ssvNetwork.write.executeOperatorFee([1], { - account: owners[2].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeExecuted', - }, - ], - ); - }); - - it('Execute declared fee gas limits', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await time.increase(CONFIG.declareOperatorFeePeriod); - await trackGas( - ssvNetwork.write.executeOperatorFee([1], { - account: owners[2].account, - }), - [GasGroup.EXECUTE_OPERATOR_FEE], - ); - }); - - it('Get operator fee', async () => { - expect(await ssvViews.read.getOperatorFee([1])).to.equal(initialFee); - }); - - it('Get fee from operator that does not exist returns 0', async () => { - expect(await ssvViews.read.getOperatorFee([12])).to.equal(0); - }); - - it('Get operator maximum fee limit', async () => { - expect(await ssvViews.read.getMaximumOperatorFee()).to.equal(CONFIG.maximumOperatorFee); - }); - - it('Declare fee of operator I do not own reverts "CallerNotOwnerWithData"', async () => { - await expect( - ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { account: owners[1].account }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Declare fee with a wrong Publickey reverts "OperatorDoesNotExist"', async () => { - await expect( - ssvNetwork.write.declareOperatorFee([12, initialFee + initialFee / 10n], { account: owners[1].account }), - ).to.be.rejectedWith('OperatorDoesNotExist'); - }); - - it('Declare fee when previously set to zero reverts "FeeIncreaseNotAllowed"', async () => { - await ssvNetwork.write.declareOperatorFee([1, 0], { - account: owners[2].account, - }); - await time.increase(CONFIG.declareOperatorFeePeriod); - await ssvNetwork.write.executeOperatorFee([1], { - account: owners[2].account, - }); - - await expect( - ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { account: owners[2].account }), - ).to.be.rejectedWith('FeeIncreaseNotAllowed'); - }); - - it('Declare same fee value as actual reverts "SameFeeChangeNotAllowed"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee / 10n], { - account: owners[2].account, - }); - await time.increase(CONFIG.declareOperatorFeePeriod); - - await ssvNetwork.write.executeOperatorFee([1], { - account: owners[2].account, - }); - await expect( - ssvNetwork.write.declareOperatorFee([1, initialFee / 10n], { account: owners[2].account }), - ).to.be.rejectedWith('SameFeeChangeNotAllowed'); - }); - - it('Declare fee after registering an operator with zero fee reverts "FeeIncreaseNotAllowed"', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(2), 0, false], { - account: owners[2].account, - }); - - await expect( - ssvNetwork.write.declareOperatorFee([2, initialFee + initialFee / 10n], { account: owners[2].account }), - ).to.be.rejectedWith('FeeIncreaseNotAllowed'); - }); - - it('Declare fee above the operators max fee increase limit reverts "FeeExceedsIncreaseLimit"', async () => { - await expect( - ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 5n], { account: owners[2].account }), - ).to.be.rejectedWith('FeeExceedsIncreaseLimit'); - }); - - it('Declare fee above the operators max fee limit reverts "FeeTooHigh"', async () => { - await expect(ssvNetwork.write.declareOperatorFee([1, 2e14], { account: owners[2].account })).to.be.rejectedWith( - 'FeeTooHigh', - ); - }); - - it('Declare fee too high reverts "FeeTooHigh" -> DAO updates limit -> declare fee emits "OperatorFeeDeclared"', async () => { - const maxOperatorFee = 8e14; - await ssvNetwork.write.updateMaximumOperatorFee([maxOperatorFee]); - - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(10), maxOperatorFee, false], { - account: owners[3].account, - }); - - const newOperatorFee = maxOperatorFee + maxOperatorFee / 10; - - await expect( - ssvNetwork.write.declareOperatorFee([2, newOperatorFee], { - account: owners[3].account, - }), - ).to.be.rejectedWith('FeeTooHigh'); - - await assertEvent(ssvNetwork.write.updateMaximumOperatorFee([newOperatorFee]), [ - { - contract: ssvNetwork, - eventName: 'OperatorMaximumFeeUpdated', - argNames: ['maxFee'], - argValuesList: [[newOperatorFee]], - }, - ]); - - await assertEvent(ssvNetwork.write.declareOperatorFee([2, newOperatorFee], { account: owners[3].account }), [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeDeclared', - }, - ]); - }); - - it('Cancel declared fee without a pending request reverts "NoFeeDeclared"', async () => { - await expect( - ssvNetwork.write.cancelDeclaredOperatorFee([1], { - account: owners[2].account, - }), - ).to.be.rejectedWith('NoFeeDeclared'); - }); - - it('Cancel declared fee of an operator I do not own reverts "CallerNotOwnerWithData"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await expect(ssvNetwork.write.cancelDeclaredOperatorFee([1], { account: owners[1].account })).to.be.rejectedWith( - 'CallerNotOwnerWithData', - ); - }); - - it('Execute declared fee of an operator I do not own reverts "CallerNotOwnerWithData"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await expect(ssvNetwork.write.executeOperatorFee([1], { account: owners[1].account })).to.be.rejectedWith( - 'CallerNotOwnerWithData', - ); - }); - - it('Execute declared fee without a pending request reverts "NoFeeDeclared"', async () => { - await expect(ssvNetwork.write.executeOperatorFee([1], { account: owners[2].account })).to.be.rejectedWith( - 'NoFeeDeclared', - ); - }); - - it('Execute declared fee too early reverts "ApprovalNotWithinTimeframe"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await time.increase(CONFIG.declareOperatorFeePeriod - 10); - await expect(ssvNetwork.write.executeOperatorFee([1], { account: owners[2].account })).to.be.rejectedWith( - 'ApprovalNotWithinTimeframe', - ); - }); - - it('Execute declared fee too late reverts "ApprovalNotWithinTimeframe"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await time.increase(CONFIG.declareOperatorFeePeriod + CONFIG.executeOperatorFeePeriod + 1); - await expect(ssvNetwork.write.executeOperatorFee([1], { account: owners[2].account })).to.be.rejectedWith( - 'ApprovalNotWithinTimeframe', - ); - }); - - it('Reduce fee emits "OperatorFeeExecuted"', async () => { - await assertEvent(ssvNetwork.write.reduceOperatorFee([1, initialFee / 2n], { account: owners[2].account }), [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeExecuted', - }, - ]); - - expect(await ssvViews.read.getOperatorFee([1])).to.equal(initialFee / 2n); - - await assertEvent(ssvNetwork.write.reduceOperatorFee([1, 0], { account: owners[2].account }), [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeExecuted', - }, - ]); - expect(await ssvViews.read.getOperatorFee([1])).to.equal(0); - }); - - it('Reduce fee emits "OperatorFeeExecuted"', async () => { - await trackGas(ssvNetwork.write.reduceOperatorFee([1, initialFee / 2n], { account: owners[2].account }), [ - GasGroup.REDUCE_OPERATOR_FEE, - ]); - }); - - it('Reduce fee with a fee thats too low reverts "FeeTooLow"', async () => { - await expect(ssvNetwork.write.reduceOperatorFee([1, 10e6], { account: owners[2].account })).to.be.rejectedWith( - 'FeeTooLow', - ); - }); - - it('Reduce fee with an increased value reverts "FeeIncreaseNotAllowed"', async () => { - await expect( - ssvNetwork.write.reduceOperatorFee([1, initialFee * 2n], { account: owners[2].account }), - ).to.be.rejectedWith('FeeIncreaseNotAllowed'); - }); - - it('Reduce fee after declaring a fee change', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - await assertEvent(ssvNetwork.write.reduceOperatorFee([1, initialFee / 2n], { account: owners[2].account }), [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeExecuted', - }, - ]); - - expect(await ssvViews.read.getOperatorFee([1])).to.equal(initialFee / 2n); - const [isFeeDeclared] = await ssvViews.read.getOperatorDeclaredFee([1]); - expect(isFeeDeclared).to.equal(false); - }); - - it('Reduce maximum fee limit after declaring a fee change reverts "FeeTooHigh', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - await ssvNetwork.write.updateMaximumOperatorFee([1000]); - - await time.increase(CONFIG.declareOperatorFeePeriod); - - await expect(ssvNetwork.write.executeOperatorFee([1], { account: owners[2].account })).to.be.rejectedWith( - 'FeeTooHigh', - ); - }); - - //Dao - it('DAO increase the fee emits "OperatorFeeIncreaseLimitUpdated"', async () => { - await assertEvent(ssvNetwork.write.updateOperatorFeeIncreaseLimit([1000]), [ - { - contract: ssvNetwork, - eventName: 'OperatorFeeIncreaseLimitUpdated', - }, - ]); - }); - - it('DAO update the maximum operator fee emits "OperatorMaximumFeeUpdated"', async () => { - const newMaxFee = 2e10; - - await assertEvent(ssvNetwork.write.updateMaximumOperatorFee([newMaxFee]), [ - { - contract: ssvNetwork, - eventName: 'OperatorMaximumFeeUpdated', - argNames: ['maxFee'], - argValuesList: [[newMaxFee]], - }, - ]); - }); - - it('DAO increase the fee gas limits"', async () => { - await trackGas(ssvNetwork.write.updateOperatorFeeIncreaseLimit([1000]), [ - GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT, - ]); - }); - - it('DAO update the declare fee period emits "DeclareOperatorFeePeriodUpdated"', async () => { - await assertEvent(ssvNetwork.write.updateDeclareOperatorFeePeriod([1200]), [ - { - contract: ssvNetwork, - eventName: 'DeclareOperatorFeePeriodUpdated', - }, - ]); - }); - - it('DAO update the declare fee period gas limits"', async () => { - await trackGas(ssvNetwork.write.updateDeclareOperatorFeePeriod([1200]), [ - GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD, - ]); - }); - - it('DAO update the execute fee period emits "ExecuteOperatorFeePeriodUpdated"', async () => { - await assertEvent(ssvNetwork.write.updateExecuteOperatorFeePeriod([1200]), [ - { - contract: ssvNetwork, - eventName: 'ExecuteOperatorFeePeriodUpdated', - }, - ]); - }); - - it('DAO update the execute fee period gas limits', async () => { - await trackGas(ssvNetwork.write.updateExecuteOperatorFeePeriod([1200]), [ - GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD, - ]); - }); - - it('DAO update the maximum fee for operators using SSV gas limits', async () => { - await trackGas(ssvNetwork.write.updateMaximumOperatorFee([2e10]), [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]); - }); - - it('DAO get fee increase limit', async () => { - expect(await ssvViews.read.getOperatorFeeIncreaseLimit()).to.equal(CONFIG.operatorMaxFeeIncrease); - }); - - it('DAO get declared fee', async () => { - const newFee = initialFee + initialFee / 10n; - await ssvNetwork.write.declareOperatorFee([1, newFee], { account: owners[2].account }); - - const [_, feeDeclaredInContract] = await ssvViews.read.getOperatorDeclaredFee([1]); - expect(feeDeclaredInContract).to.equal(newFee); - }); - - it('DAO get declared and execute fee periods', async () => { - expect(await ssvViews.read.getOperatorFeePeriods()).to.deep.equal([ - CONFIG.declareOperatorFeePeriod, - CONFIG.executeOperatorFeePeriod, - ]); - }); - - it('Increase fee from an address thats not the DAO reverts "caller is not the owner"', async () => { - await expect( - ssvNetwork.write.updateOperatorFeeIncreaseLimit([1000], { account: owners[1].account }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); - - it('Update the declare fee period from an address thats not the DAO reverts "caller is not the owner"', async () => { - await expect( - ssvNetwork.write.updateDeclareOperatorFeePeriod([1200], { account: owners[1].account }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); - - it('Update the execute fee period from an address thats not the DAO reverts "caller is not the owner"', async () => { - await expect( - ssvNetwork.write.updateExecuteOperatorFeePeriod([1200], { account: owners[1].account }), - ).to.be.rejectedWith('Ownable: caller is not the owner'); - }); - - it('DAO declared fee without a pending request reverts "NoFeeDeclared"', async () => { - await ssvNetwork.write.declareOperatorFee([1, initialFee + initialFee / 10n], { - account: owners[2].account, - }); - - const [isFeeDeclared] = await ssvViews.read.getOperatorDeclaredFee([2]); - expect(isFeeDeclared).to.equal(false); - }); -}); diff --git a/test/operators/whitelist.ts b/test/operators/whitelist.ts deleted file mode 100644 index 7d7ff410f..000000000 --- a/test/operators/whitelist.ts +++ /dev/null @@ -1,1052 +0,0 @@ -// Declare imports -import hre from 'hardhat'; - -import { owners, initializeContract, registerOperators, DataGenerator, CONFIG } from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { ethers } from 'hardhat'; -import { expect } from 'chai'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; - -// Declare globals -let ssvNetwork: any, ssvViews: any, ssvToken: any, mockWhitelistingContract: any, mockWhitelistingContractAddress: any; -const OPERATOR_IDS_10 = Array.from({ length: 10 }, (_, i) => i + 1); - -describe('Whitelisting Operator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - mockWhitelistingContract = await hre.viem.deployContract('MockWhitelistingContract', [[]], { - client: owners[0].client, - }); - mockWhitelistingContractAddress = await mockWhitelistingContract.address; - }); - - /* GAS LIMITS */ - it('Set operator whitelisting contract (1 operator) gas limits', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }); - await trackGas( - ssvNetwork.write.setOperatorsWhitelistingContract([[1], mockWhitelistingContractAddress], { - account: owners[1].account, - }), - [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT], - ); - }); - - it('Update operator whitelisting contract (1 operator) gas limits', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }); - - const fakeWhitelistingContract = await hre.viem.deployContract( - 'FakeWhitelistingContract', - [await ssvNetwork.address], - { - client: owners[0].client, - }, - ); - - ssvNetwork.write.setOperatorsWhitelistingContract([[1], await fakeWhitelistingContract.address], { - account: owners[1].account, - }); - - await trackGas( - ssvNetwork.write.setOperatorsWhitelistingContract([[1], mockWhitelistingContractAddress], { - account: owners[1].account, - }), - [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT], - ); - }); - - it('Set operator whitelisting contract (10 operators) gas limits', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await trackGas( - ssvNetwork.write.setOperatorsWhitelistingContract([OPERATOR_IDS_10, mockWhitelistingContractAddress], { - account: owners[1].account, - }), - [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10], - ); - }); - - it('Remove operator whitelisting contract (1 operator) gas limits', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, true], { - account: owners[1].account, - }); - - await ssvNetwork.write.setOperatorsWhitelistingContract([[1], mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await trackGas( - ssvNetwork.write.removeOperatorsWhitelistingContract([[1]], { - account: owners[1].account, - }), - [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT], - ); - }); - - it('Remove operator whitelisting contract (10 operators) gas limits', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsWhitelistingContract([OPERATOR_IDS_10, mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await trackGas( - ssvNetwork.write.removeOperatorsWhitelistingContract([OPERATOR_IDS_10], { - account: owners[1].account, - }), - [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10], - ); - }); - - it('Set 10 whitelist addresses (EOAs) for 10 operators gas limits', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await trackGas( - ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }), - [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10], - ); - }); - - it('Remove 10 whitelist addresses (EOAs) for 10 operators gas limits', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }); - - await trackGas( - ssvNetwork.write.removeOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }), - [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10], - ); - }); - - it('Set operators private (10 operators) gas limits', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await trackGas( - ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }), - [GasGroup.SET_OPERATORS_PRIVATE_10], - ); - }); - - it('Set operators public (10 operators) gas limits', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }); - - await trackGas( - ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }), - [GasGroup.SET_OPERATORS_PUBLIC_10], - ); - }); - - /* EVENTS */ - it('Set operator whitelisting contract (10 operators) emits "OperatorWhitelistingContractUpdated"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await assertEvent( - ssvNetwork.write.setOperatorsWhitelistingContract([OPERATOR_IDS_10, mockWhitelistingContractAddress], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorWhitelistingContractUpdated', - argNames: ['operatorIds', 'whitelistingContract'], - argValuesList: [[OPERATOR_IDS_10, mockWhitelistingContractAddress]], - }, - ], - ); - }); - - it('Remove operator whitelisting contract (10 operators) emits "OperatorWhitelistingContractUpdated"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsWhitelistingContract([OPERATOR_IDS_10, mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await assertEvent( - ssvNetwork.write.removeOperatorsWhitelistingContract([OPERATOR_IDS_10], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorWhitelistingContractUpdated', - argNames: ['operatorIds', 'whitelistingContract'], - argValuesList: [[OPERATOR_IDS_10, ethers.ZeroAddress]], - }, - ], - ); - }); - - it('Set 10 whitelist addresses (EOAs) for 10 operators emits "OperatorMultipleWhitelistUpdated"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await assertEvent( - ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorMultipleWhitelistUpdated', - argNames: ['operatorIds', 'whitelistAddresses'], - argValuesList: [[OPERATOR_IDS_10, whitelistAddresses]], - }, - ], - ); - }); - - it('Remove 10 whitelist addresses (EOAs) for 10 operators emits "OperatorMultipleWhitelistRemoved"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }); - - await assertEvent( - ssvNetwork.write.removeOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorMultipleWhitelistRemoved', - argNames: ['operatorIds', 'whitelistAddresses'], - argValuesList: [[OPERATOR_IDS_10, whitelistAddresses]], - }, - ], - ); - }); - - it('Set operators private (10 operators) emits "OperatorPrivacyStatusUpdated"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await assertEvent( - ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorPrivacyStatusUpdated', - argNames: ['operatorIds', 'toPrivate'], - argValuesList: [[OPERATOR_IDS_10, true]], - }, - ], - ); - }); - - it('Set operators public (10 operators) emits "OperatorPrivacyStatusUpdated"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await assertEvent( - ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorPrivacyStatusUpdated', - argNames: ['operatorIds', 'toPrivate'], - argValuesList: [[OPERATOR_IDS_10, false]], - }, - ], - ); - }); - - /* REVERTS */ - it('Set operator whitelisted address (EOA) in non-existing operator reverts "OperatorDoesNotExist"', async () => { - await expect(ssvNetwork.write.setOperatorsWhitelists([[1], [owners[2].account.address]])).to.be.rejectedWith( - 'OperatorDoesNotExist', - ); - }); - - it('Set multiple operator whitelisted addresses (zero address) reverts "ZeroAddressNotAllowed"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await expect( - ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, [ethers.ZeroAddress]], { - account: owners[1].account, - }), - ).to.be.rejectedWith('ZeroAddressNotAllowed'); - }); - - it('Non-owner sets multiple operator whitelisted addresses (EOA) reverts "CallerNotOwnerWithData"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await expect( - ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Set multiple operator whitelisted addresses (EOA) with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await expect(ssvNetwork.write.setOperatorsWhitelists([[], whitelistAddresses])).to.be.rejectedWith( - 'InvalidOperatorIdsLength', - ); - }); - - it('Set multiple operator whitelisted addresses (EOA) with empty addresses IDs reverts "InvalidWhitelistAddressesLength"', async () => { - await expect(ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, []])).to.be.rejectedWith( - 'InvalidWhitelistAddressesLength', - ); - }); - - it('Set multiple operator whitelisted addresses (EOA) passing unsorted operator IDs reverts "UnsortedOperatorsList"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - const unsortedOperatorIds = [1, 3, 2, 4, 5]; - await expect( - ssvNetwork.write.setOperatorsWhitelists([unsortedOperatorIds, whitelistAddresses], { - account: owners[1].account, - }), - ).to.be.rejectedWith('UnsortedOperatorsList'); - }); - - it('Set multiple operator whitelisted addresses (EOA) passing a whitelisting contract reverts "AddressIsWhitelistingContract"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - whitelistAddresses.push(mockWhitelistingContractAddress); - - await expect( - ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }), - ).to.be.rejectedWith('AddressIsWhitelistingContract', mockWhitelistingContractAddress); - }); - - it('Non-owner removes multiple operator whitelisted addresses (EOA) reverts "CallerNotOwnerWithData"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }); - - await expect( - ssvNetwork.write.removeOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Remove multiple operator whitelisted addresses (EOA) passing unsorted operator IDs reverts "UnsortedOperatorsList"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - const unsortedOperatorIds = [1, 3, 2, 4, 5]; - - await expect( - ssvNetwork.write.removeOperatorsWhitelists([unsortedOperatorIds, whitelistAddresses], { - account: owners[1].account, - }), - ).to.be.rejectedWith('UnsortedOperatorsList'); - }); - - it('Remove multiple operator whitelisted addresses (EOA) with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await expect(ssvNetwork.write.removeOperatorsWhitelists([[], whitelistAddresses])).to.be.rejectedWith( - 'InvalidOperatorIdsLength', - ); - }); - - it('Remove multiple operator whitelisted addresses (EOA) with empty addresses IDs reverts "InvalidWhitelistAddressesLength"', async () => { - await expect(ssvNetwork.write.removeOperatorsWhitelists([OPERATOR_IDS_10, []])).to.be.rejectedWith( - 'InvalidWhitelistAddressesLength', - ); - }); - - it('Remove multiple operator whitelisted addresses (EOA) passing a whitelisting contract reverts "AddressIsWhitelistingContract"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - whitelistAddresses.push(mockWhitelistingContractAddress); - - await expect( - ssvNetwork.write.removeOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }), - ).to.not.be.rejectedWith('AddressIsWhitelistingContract', mockWhitelistingContractAddress); - }); - - it('Set operator whitelisting contract with an EOA reverts "InvalidWhitelistingContract"', async () => { - await expect( - ssvNetwork.write.setOperatorsWhitelistingContract([OPERATOR_IDS_10, owners[2].account.address]), - ).to.be.rejectedWith('InvalidWhitelistingContract'); - }); - - it('Set operator whitelisting contract with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { - await expect( - ssvNetwork.write.setOperatorsWhitelistingContract([[], mockWhitelistingContractAddress]), - ).to.be.rejectedWith('InvalidOperatorIdsLength'); - }); - - it('Non-owner sets operator whitelisting contract reverts "CallerNotOwnerWithData"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await expect( - ssvNetwork.write.setOperatorsWhitelistingContract([OPERATOR_IDS_10, mockWhitelistingContractAddress], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Sets operator whitelisting contract for a non-existing operator reverts "OperatorDoesNotExist"', async () => { - await expect( - ssvNetwork.write.setOperatorsWhitelistingContract([OPERATOR_IDS_10, mockWhitelistingContractAddress]), - ).to.be.rejectedWith('OperatorDoesNotExist'); - }); - - it('Remove operator whitelisting contract with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { - await expect(ssvNetwork.write.removeOperatorsWhitelistingContract([[]])).to.be.rejectedWith( - 'InvalidOperatorIdsLength', - ); - }); - - it('Non-owner removes operator whitelisting contract reverts "CallerNotOwnerWithData"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await expect( - ssvNetwork.write.removeOperatorsWhitelistingContract([OPERATOR_IDS_10], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Set operators private with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { - await expect(ssvNetwork.write.setOperatorsPrivateUnchecked([[]])).to.be.rejectedWith('InvalidOperatorIdsLength'); - }); - - it('Set operators public with empty operator IDs reverts "InvalidOperatorIdsLength"', async () => { - await expect(ssvNetwork.write.setOperatorsPublicUnchecked([[]])).to.be.rejectedWith('InvalidOperatorIdsLength'); - }); - - it('Non-owner set operators private reverts "CallerNotOwnerWithData"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await expect( - ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Non-owner set operators public reverts "CallerNotOwnerWithData"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await expect( - ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { - account: owners[2].account, - }), - ).to.be.rejectedWith('CallerNotOwnerWithData'); - }); - - it('Whitelist accounts passing repeated operator IDs reverts "OperatorsListNotUnique"', async () => { - // register 10 operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await expect( - ssvNetwork.write.setOperatorsWhitelists( - [ - [2, 2, 2, 2, 4, 4, 4, 4, 6, 6, 6, 6, 8, 8, 8, 8], - [owners[4].account.address, owners[5].account.address], - ], - { - account: owners[1].account, - }, - ), - ).to.be.rejectedWith('OperatorsListNotUnique'); - }); - - it('Remove whitelist addresses passing repeated operator IDs reverts "OperatorsListNotUnique"', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }); - - await expect( - ssvNetwork.write.removeOperatorsWhitelists( - [[2, 2, 2, 2, 4, 4, 4, 4, 6, 6, 6, 6, 8, 8, 8, 8], whitelistAddresses], - { - account: owners[1].account, - }, - ), - ).to.be.rejectedWith('OperatorsListNotUnique'); - }); - - /* LOGIC */ - - it('Get whitelisted address for no operators returns empty list', async () => { - expect(await ssvViews.read.getWhitelistedOperators([[], owners[1].account.address])).to.be.deep.equal([]); - }); - - it('Get whitelisted zero address for operators returns empty list', async () => { - expect(await ssvViews.read.getWhitelistedOperators([[1, 2], ethers.ZeroAddress])).to.be.deep.equal([]); - }); - - it('Get whitelisted address for operators returns the whitelisted operators (only SSV whitelisting module)', async () => { - const whitelistAddress = owners[4].account.address; - - // Register 1000 operators to have 4 bitmap blocks - await registerOperators(1, 1000, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsWhitelists([[100, 200, 300, 400, 500, 600, 700, 800], [whitelistAddress]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[100, 200], whitelistAddress])).to.be.deep.equal([100, 200]); - expect(await ssvViews.read.getWhitelistedOperators([[200, 400, 600, 800], whitelistAddress])).to.be.deep.equal([ - 200, 400, 600, 800, - ]); - expect( - await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 200, 320, 400, 512, 715, 800, 905], whitelistAddress]), - ).to.be.deep.equal([200, 400, 800]); - expect( - await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 320, 512, 715, 905], whitelistAddress]), - ).to.be.deep.equal([]); - }); - - it('Get whitelisted address for operators returns the whitelisted operators (only externally whitelisted)', async () => { - const whitelistAddress = owners[4].account.address; - - // Register 1000 operators to have 4 bitmap blocks - await registerOperators(1, 1000, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsWhitelistingContract( - [[100, 200, 300, 400, 500, 600, 700, 800], mockWhitelistingContractAddress], - { - account: owners[1].account, - }, - ); - - await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); - - expect(await ssvViews.read.getWhitelistedOperators([[200, 400, 600, 800], whitelistAddress])).to.be.deep.equal([ - 200, 400, 600, 800, - ]); - expect( - await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 200, 320, 400, 512, 715, 800, 905], whitelistAddress]), - ).to.be.deep.equal([200, 400, 800]); - expect( - await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 320, 512, 715, 905], whitelistAddress]), - ).to.be.deep.equal([]); - }); - - it('Get whitelisted address for operators returns the whitelisted operators (internally and externally whitelisted)', async () => { - const whitelistAddress = owners[4].account.address; - - // Register 1000 operators to have 4 bitmap blocks - await registerOperators(1, 1000, CONFIG.minimalOperatorFee); - - // Whitelist using external whitelisting contract - await ssvNetwork.write.setOperatorsWhitelistingContract([[100, 400, 700, 800], mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[200, 300, 500, 600], [whitelistAddress]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[200, 400, 600, 800], whitelistAddress])).to.be.deep.equal([ - 200, 400, 600, 800, - ]); - expect( - await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 200, 320, 400, 512, 715, 800, 905], whitelistAddress]), - ).to.be.deep.equal([200, 400, 800]); - expect( - await ssvViews.read.getWhitelistedOperators([[1, 60, 150, 320, 512, 715, 905], whitelistAddress]), - ).to.be.deep.equal([]); - }); - - it('Get whitelisted address for a single operator whitelisted both internally and externally', async () => { - const whitelistAddress = owners[4].account.address; - - // Register operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - // Whitelist using external whitelisting contract - await ssvNetwork.write.setOperatorsWhitelistingContract([[1], mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[1], [whitelistAddress]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[1], whitelistAddress])).to.be.deep.equal([1]); - }); - - it('Get whitelisted address for overlapping internal and external whitelisting', async () => { - const whitelistAddress = owners[4].account.address; - - // Register operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - // Whitelist using external whitelisting contract - await ssvNetwork.write.setOperatorsWhitelistingContract([[1, 2, 3], mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await mockWhitelistingContract.write.setWhitelistedAddress([whitelistAddress]); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[2, 3, 4], [whitelistAddress]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4], whitelistAddress])).to.be.deep.equal([ - 1, 2, 3, 4, - ]); - }); - - it('Get whitelisted address for a list containing non-whitelisted operators', async () => { - const whitelistAddress = owners[4].account.address; - - // Register operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[2, 4, 6], [whitelistAddress]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8], whitelistAddress])).to.be.deep.equal([ - 2, 4, 6, - ]); - }); - - it('Get whitelisted address for non-existent operator IDs', async () => { - const whitelistAddress = owners[4].account.address; - - // Register operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[2, 4, 6], [whitelistAddress]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[11, 12, 13], whitelistAddress])).to.be.deep.equal([]); - }); - - it('Get whitelisted address for mixed whitelisted and non-whitelisted addresses', async () => { - const whitelistAddress1 = owners[4].account.address; - const whitelistAddress2 = owners[5].account.address; - - // Register operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[2, 4, 6], [whitelistAddress1]], { - account: owners[1].account, - }); - - await ssvNetwork.write.setOperatorsWhitelists([[3, 5, 7], [whitelistAddress2]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8], whitelistAddress1])).to.be.deep.equal( - [2, 4, 6], - ); - expect(await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8], whitelistAddress2])).to.be.deep.equal( - [3, 5, 7], - ); - }); - - it('Get whitelisted address for unsorted operators', async () => { - const whitelistAddress = owners[4].account.address; - - // Register operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[2, 4, 6], [whitelistAddress]], { - account: owners[1].account, - }); - - await expect(ssvViews.read.getWhitelistedOperators([[6, 2, 4], whitelistAddress])).to.be.rejectedWith( - 'UnsortedOperatorsList', - ); - }); - - it('Get whitelisted address for duplicate operator IDs', async () => { - const whitelistAddress = owners[4].account.address; - - // Register operators - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - // Whitelist using SSV whitelisting module - await ssvNetwork.write.setOperatorsWhitelists([[2, 4, 6], [whitelistAddress]], { - account: owners[1].account, - }); - - await expect(ssvViews.read.getWhitelistedOperators([[2, 2, 4, 6, 6], whitelistAddress])).to.be.rejectedWith( - 'OperatorsListNotUnique', - ); - }); - - (process.env.SOLIDITY_COVERAGE ? it.skip : it)( - 'Get whitelisted address for a large number of operator IDs', - async () => { - const whitelistAddress = owners[4].account.address; - - // Register a large number of operators - const largeNumber = 3000; - await registerOperators(1, largeNumber, CONFIG.minimalOperatorFee); - - let operatorIds = []; - for (let i = 1; i <= largeNumber; i++) { - operatorIds.push(i); - } - - await ssvNetwork.write.setOperatorsWhitelists([operatorIds, [whitelistAddress]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getWhitelistedOperators([operatorIds, whitelistAddress])).to.be.deep.equal( - operatorIds, - ); - }, - ); - - it('Get private operator by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }); - - await ssvNetwork.write.setOperatorsWhitelistingContract([[1], mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([[1]], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 0, // validatorCount - mockWhitelistingContractAddress, // whitelisting contract address - true, // isPrivate - true, // active - ]); - }); - - it('Get removed private operator by id', async () => { - await ssvNetwork.write.registerOperator([DataGenerator.publicKey(0), CONFIG.minimalOperatorFee, false], { - account: owners[1].account, - }); - - await ssvNetwork.write.setOperatorsWhitelistingContract([[1], mockWhitelistingContractAddress], { - account: owners[1].account, - }); - - await ssvNetwork.write.removeOperator([1], { - account: owners[1].account, - }); - - expect(await ssvViews.read.getOperatorById([1])).to.deep.equal([ - owners[1].account.address, // owner - 0, // fee - 0, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - false, // isPrivate - false, // active - ]); - }); - - it('Check if an address is a whitelisting contract', async () => { - // whitelisting contract - expect(await ssvViews.read.isWhitelistingContract([mockWhitelistingContractAddress])).to.be.true; - // EOA - expect(await ssvViews.read.isWhitelistingContract([owners[1].account.address])).to.be.false; - // generic contract - expect(await ssvViews.read.isWhitelistingContract([ssvViews.address])).to.be.false; - }); - - it('Set operators private (10 operators)', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }); - - for (let i = 0; i < OPERATOR_IDS_10.length; i++) { - const operatorData = await ssvViews.read.getOperatorById([OPERATOR_IDS_10[i]]); - expect(operatorData[4]).to.be.true; - } - }); - - it('Set operators private (10 operators)', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }); - - await ssvNetwork.write.setOperatorsPublicUnchecked([OPERATOR_IDS_10], { - account: owners[1].account, - }); - - for (let i = 0; i < OPERATOR_IDS_10.length; i++) { - const operatorData = await ssvViews.read.getOperatorById([OPERATOR_IDS_10[i]]); - expect(operatorData[4]).to.be.false; - } - }); - - it('Check account is whitelisted in a whitelisting contract', async () => { - await mockWhitelistingContract.write.setWhitelistedAddress([owners[4].account.address]); - - expect( - await ssvViews.read.isAddressWhitelistedInWhitelistingContract([ - owners[4].account.address, - 0, - mockWhitelistingContractAddress, - ]), - ).to.be.true; - }); - - it('Check account is not whitelisted in a whitelisting contract', async () => { - await mockWhitelistingContract.write.setWhitelistedAddress([owners[4].account.address]); - - expect( - await ssvViews.read.isAddressWhitelistedInWhitelistingContract([ - owners[2].account.address, - 0, - mockWhitelistingContractAddress, - ]), - ).to.be.false; - }); - - it('Check address(0) account in a whitelisting contract', async () => { - await mockWhitelistingContract.write.setWhitelistedAddress([owners[4].account.address]); - - expect( - await ssvViews.read.isAddressWhitelistedInWhitelistingContract([ - ethers.ZeroAddress, - 0, - mockWhitelistingContractAddress, - ]), - ).to.be.false; - }); - - it('Check account in an address(0) contract', async () => { - expect( - await ssvViews.read.isAddressWhitelistedInWhitelistingContract([ - owners[2].account.address, - 0, - ethers.ZeroAddress, - ]), - ).to.be.false; - }); - - it('Set multiple whitelisted addresses to one operator', async () => { - await registerOperators(1, 1, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await assertEvent( - ssvNetwork.write.setOperatorsWhitelists([[1], whitelistAddresses], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorMultipleWhitelistUpdated', - argNames: ['operatorIds', 'whitelistAddresses'], - argValuesList: [[[1], whitelistAddresses]], - }, - ], - ); - - for (let i = 0; i < whitelistAddresses.length; i++) { - expect( - await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], whitelistAddresses[i]]), - ).to.be.deep.equal([1]); - } - - expect( - await ssvViews.read.getWhitelistedOperators([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], owners[11].account.address]), - ).to.be.deep.equal([]); - }); - - it('Set 10 whitelist addresses (EOAs) for 10 operators', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - const whitelistAddresses = owners.slice(0, 10).map(owner => owner.account.address); - - await assertEvent( - ssvNetwork.write.setOperatorsWhitelists([OPERATOR_IDS_10, whitelistAddresses], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorMultipleWhitelistUpdated', - argNames: ['operatorIds', 'whitelistAddresses'], - argValuesList: [[OPERATOR_IDS_10, whitelistAddresses]], - }, - ], - ); - - for (let i = 0; i < whitelistAddresses.length; i++) { - expect(await ssvViews.read.getWhitelistedOperators([OPERATOR_IDS_10, whitelistAddresses[i]])).to.be.deep.equal( - OPERATOR_IDS_10, - ); - expect(await ssvViews.read.getWhitelistedOperators([[500], whitelistAddresses[i]])).to.be.deep.equal([]); - } - }); - - it('Set 1 whitelist addresses for 1 operator', async () => { - await registerOperators(1, 10, CONFIG.minimalOperatorFee); - - await assertEvent( - ssvNetwork.write.setOperatorsWhitelists([[2], [owners[3].account.address]], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'OperatorMultipleWhitelistUpdated', - argNames: ['operatorIds', 'whitelistAddresses'], - argValuesList: [[[2], [owners[3].account.address]]], - }, - ], - ); - - expect(await ssvViews.read.getWhitelistedOperators([OPERATOR_IDS_10, owners[3].account.address])).to.be.deep.equal([ - 2, - ]); - expect(await ssvViews.read.getWhitelistedOperators([OPERATOR_IDS_10, owners[2].account.address])).to.be.deep.equal( - [], - ); - }); - - it('Custom test: Operators balances sync', async () => { - // owners[2] -> operators' owner - // owners[3] -> whitelisted address for all 4 operators - - // create 4 operators with a fee - const operatorIds = await registerOperators(2, 4, CONFIG.minimalOperatorFee); - - // set operators private - ssvNetwork.write.setOperatorsPrivateUnchecked([operatorIds], { - account: owners[2].account, - }); - - // whitelist owners[3] address for all operators - await ssvNetwork.write.setOperatorsWhitelists([operatorIds, [owners[3].account.address]], { - account: owners[2].account, - }); - - // owners[3] registers a validator - const minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * CONFIG.minimalOperatorFee * 4n; - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - operatorIds, - await DataGenerator.shares(2, 1, operatorIds), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - ); - - const firstCluster = eventsByName.ValidatorAdded[0].args; - - // liquidate the cluster - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - // withdraw all operators' earnings - for (let i = 0; i < operatorIds.length; i++) { - await ssvNetwork.write.withdrawAllOperatorEarnings([operatorIds[i]], { - account: owners[2].account, - }); - } - - // de-whitelist owners[3] address for all operators - await ssvNetwork.write.removeOperatorsWhitelists([operatorIds, [owners[3].account.address]], { - account: owners[2].account, - }); - - // check operators' balance is 0 after few blocks - await mine(1000); - for (let i = 0; i < operatorIds.length; i++) { - expect(await ssvViews.read.getOperatorEarnings([operatorIds[i]])).to.equal(0); - } - - // reactivate the cluster - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - await ssvNetwork.write.reactivate([updatedCluster.operatorIds, minDepositAmount, updatedCluster.cluster], { - account: owners[3].account, - }); - - // all operators have have the right balance - await mine(100); - for (let i = 0; i < operatorIds.length; i++) { - expect(await ssvViews.read.getOperatorEarnings([operatorIds[i]])).to.equal(CONFIG.minimalOperatorFee * 100n); - } - }); -}); diff --git a/test/sanity/balances.ts b/test/sanity/balances.ts deleted file mode 100644 index 8a6c83773..000000000 --- a/test/sanity/balances.ts +++ /dev/null @@ -1,576 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { trackGas } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; - -let ssvNetwork: any, - ssvViews: any, - ssvToken: any, - cluster1: any, - minDepositAmount: BigInt, - burnPerBlock: BigInt, - networkFee: BigInt, - initNetworkFeeBalance: BigInt; - -// Declare globals -describe('Balance Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - networkFee = CONFIG.minimalOperatorFee; - burnPerBlock = CONFIG.minimalOperatorFee * 4n + networkFee; - minDepositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation) * burnPerBlock; - - // Set network fee - await ssvNetwork.write.updateNetworkFee([networkFee]); - - // Register validators - // cold register - await coldRegisterValidator(); - - cluster1 = ( - await bulkRegisterValidators(4, 1, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - - initNetworkFeeBalance = await ssvViews.read.getNetworkEarnings(); - }); - - it('Check cluster balance with removing operator', async () => { - const operatorIds = cluster1.operatorIds; - const cluster = cluster1.cluster; - let prevBalance: any; - - for (let i = 1; i <= 13; i++) { - await ssvNetwork.write.removeOperator([i]); - let balance = await ssvViews.read.getBalance([owners[4].account.address, operatorIds, cluster]); - let networkFee = await ssvViews.read.getNetworkFee(); - if (i > 4) { - expect(prevBalance - balance).to.equal(networkFee); - } - prevBalance = balance; - } - }); - - it('Check cluster balance after removing operator, progress blocks and confirm', async () => { - const operatorIds = cluster1.operatorIds; - const cluster = cluster1.cluster; - const owner = cluster1.owner; - - // get difference of account balance between blocks before removing operator - let balance1 = await ssvViews.read.getBalance([owners[4].account.address, operatorIds, cluster]); - await mine(1); - let balance2 = await ssvViews.read.getBalance([owners[4].account.address, operatorIds, cluster]); - - await ssvNetwork.write.removeOperator([1]); - - // get difference of account balance between blocks after removing operator - let balance3 = await ssvViews.read.getBalance([owners[4].account.address, operatorIds, cluster]); - await mine(1); - let balance4 = await ssvViews.read.getBalance([owners[4].account.address, operatorIds, cluster]); - - // check the reducing the balance after removing operator (only 3 operators) - expect(balance1 - balance2).to.be.greaterThan(balance3 - balance4); - - // try to register a new validator in the new cluster with the same operator Ids, check revert - const newOperatorIds = operatorIds.map((id: any) => id); - await expect( - bulkRegisterValidators(1, 1, newOperatorIds, minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }), - ).to.be.rejectedWith('OperatorDoesNotExist'); - - // try to remove the validator again and check the operator removed is skipped - const removed = await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), operatorIds, cluster], { - account: owners[4].account, - }), - ); - const cluster2 = removed.eventsByName.ValidatorRemoved[0]; - - // try to liquidate - const liquidated = await trackGas( - ssvNetwork.write.liquidate([owner, operatorIds, cluster2.args.cluster], { account: owners[4].account }), - ); - const cluster3 = liquidated.eventsByName.ClusterLiquidated[0]; - - await expect( - ssvViews.read.getBalance([owners[4].account.address, operatorIds, cluster3.args.cluster]), - ).to.be.rejectedWith('ClusterIsLiquidated'); - }); - - it('Check cluster balance in three blocks, one after the other', async () => { - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock); - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 2n); - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 3n); - }); - - it('Check cluster balance in two and twelve blocks, after network fee updates', async () => { - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock); - const newBurnPerBlock = burnPerBlock + networkFee; - await ssvNetwork.write.updateNetworkFee([networkFee * 2n]); - - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 2n - newBurnPerBlock); - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 2n - newBurnPerBlock * 2n); - await mine(10); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 2n - newBurnPerBlock * 12n); - }); - - it('Check DAO earnings in three blocks, one after the other', async () => { - await mine(1); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal(networkFee * 2n); - await mine(1); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal(networkFee * 4n); - await mine(1); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal(networkFee * 6n); - }); - - it('Check DAO earnings in two and twelve blocks, after network fee updates', async () => { - await mine(1); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal(networkFee * 2n); - const newNetworkFee = networkFee * 2n; - await ssvNetwork.write.updateNetworkFee([newNetworkFee]); - await mine(1); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal( - networkFee * 4n + newNetworkFee * 2n, - ); - await mine(1); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal( - networkFee * 4n + newNetworkFee * 4n, - ); - await mine(10); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal( - networkFee * 4n + newNetworkFee * 24n, - ); - }); - - it('Check operators earnings in three blocks, one after the other', async () => { - await mine(1); - - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 2n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([2])).to.equal( - CONFIG.minimalOperatorFee * 2n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([3])).to.equal( - CONFIG.minimalOperatorFee * 2n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([4])).to.equal( - CONFIG.minimalOperatorFee * 2n + CONFIG.minimalOperatorFee * 2n, - ); - await mine(1); - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 4n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([2])).to.equal( - CONFIG.minimalOperatorFee * 4n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([3])).to.equal( - CONFIG.minimalOperatorFee * 4n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([4])).to.equal( - CONFIG.minimalOperatorFee * 4n + CONFIG.minimalOperatorFee * 2n, - ); - await mine(1); - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 6n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([2])).to.equal( - CONFIG.minimalOperatorFee * 6n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([3])).to.equal( - CONFIG.minimalOperatorFee * 6n + CONFIG.minimalOperatorFee * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([4])).to.equal( - CONFIG.minimalOperatorFee * 6n + CONFIG.minimalOperatorFee * 2n, - ); - }); - - it('Check cluster balance with removed operator', async () => { - await ssvNetwork.write.removeOperator([1]); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).not.equals(0); - }); - - it('Check cluster balance with not enough balance', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.be.equals(0); - }); - - it('Check cluster balance in a non liquidated cluster', async () => { - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock); - }); - - it('Check cluster balance in a liquidated cluster reverts "ClusterIsLiquidated"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation - 1); - - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([cluster1.owner, cluster1.operatorIds, cluster1.cluster], { - account: owners[4].account, - }), - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - expect( - await ssvViews.read.isLiquidated([updatedCluster.owner, updatedCluster.operatorIds, updatedCluster.cluster]), - ).to.equal(true); - await expect( - ssvViews.read.getBalance([owners[4].account.address, updatedCluster.operatorIds, updatedCluster.cluster]), - ).to.be.rejectedWith('ClusterIsLiquidated'); - }); - - it('Check operator earnings, cluster balances and network earnings', async () => { - // 2 exisiting clusters - // update network fee - // register a new validator with some shared operators - // update network fee - - // progress blocks in the process - await mine(1); - - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 3n + CONFIG.minimalOperatorFee, - ); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal(networkFee * 2n); - - const newNetworkFee = networkFee * 2n; - await ssvNetwork.write.updateNetworkFee([newNetworkFee]); - - const newBurnPerBlock = CONFIG.minimalOperatorFee * 4n + newNetworkFee; - await mine(1); - - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 2n - newBurnPerBlock); - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal( - networkFee * 4n + newNetworkFee * 2n, - ); - - const minDep2 = minDepositAmount * 2n; - - const cluster2 = await bulkRegisterValidators(4, 1, [3, 4, 5, 6], minDep2, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await mine(2); - - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 8n + CONFIG.minimalOperatorFee * 8n, - ); - expect(await ssvViews.read.getOperatorEarnings([3])).to.equal( - CONFIG.minimalOperatorFee * 8n + CONFIG.minimalOperatorFee * 8n + CONFIG.minimalOperatorFee * 2n, - ); - - expect(await ssvViews.read.getOperatorEarnings([5])).to.equal(CONFIG.minimalOperatorFee * 2n); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 2n - newBurnPerBlock * 5n); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster2.args.operatorIds, cluster2.args.cluster]), - ).to.equal(minDep2 - newBurnPerBlock * 2n); - - // cold cluster + cluster1 * networkFee (4) + (cold cluster + cluster1 * newNetworkFee (5 + 5)) + cluster2 * newNetworkFee (2) - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal( - networkFee * 4n + newNetworkFee * 5n + newNetworkFee * 4n + newNetworkFee * 3n, - ); - - await ssvNetwork.write.updateNetworkFee([networkFee]); - await mine(4); - - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock * 2n - newBurnPerBlock * 6n - burnPerBlock * 4n); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster2.args.operatorIds, cluster2.args.cluster]), - ).to.equal(minDep2 - newBurnPerBlock * 3n - burnPerBlock * 4n); - - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - CONFIG.minimalOperatorFee * 14n + CONFIG.minimalOperatorFee * 12n, - ); - expect(await ssvViews.read.getOperatorEarnings([3])).to.equal( - CONFIG.minimalOperatorFee * 14n + CONFIG.minimalOperatorFee * 12n + CONFIG.minimalOperatorFee * 7n, - ); - expect(await ssvViews.read.getOperatorEarnings([5])).to.equal( - CONFIG.minimalOperatorFee * 2n + CONFIG.minimalOperatorFee * 5n, - ); - - // cold cluster + cluster1 * networkFee (4) + (cold cluster + cluster1 * newNetworkFee (6 + 6)) + cluster2 * newNetworkFee (3) + (cold cluster + cluster1 + cluster2 * networkFee (4 + 4 + 4)) - expect((await ssvViews.read.getNetworkEarnings()) - initNetworkFeeBalance).to.equal( - networkFee * 4n + newNetworkFee * 6n + newNetworkFee * 6n + newNetworkFee * 3n + networkFee * 12n, - ); - }); - - it('Check cluster balance after withdraw and deposit', async () => { - await mine(1); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { - account: owners[4].account, - }); - let validator2 = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(3), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(4, 3, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount * 2n, - cluster1.cluster, - ], - { - account: owners[4].account, - }, - ), - ); - let cluster2 = validator2.eventsByName.ValidatorAdded[0]; - - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster2.args.operatorIds, cluster2.args.cluster]), - ).to.equal(minDepositAmount * 3n - burnPerBlock * 3n); - - validator2 = await trackGas( - ssvNetwork.write.withdraw([cluster2.args.operatorIds, CONFIG.minimalOperatorFee, cluster2.args.cluster], { - account: owners[4].account, - }), - ); - cluster2 = validator2.eventsByName.ClusterWithdrawn[0]; - - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster2.args.operatorIds, cluster2.args.cluster]), - ).to.equal(minDepositAmount * 3n - burnPerBlock * 4n - burnPerBlock - CONFIG.minimalOperatorFee); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[4].account, - }); - validator2 = await trackGas( - ssvNetwork.write.deposit( - [owners[4].account.address, cluster2.args.operatorIds, CONFIG.minimalOperatorFee, cluster2.args.cluster], - { - account: owners[4].account, - }, - ), - ); - cluster2 = validator2.eventsByName.ClusterDeposited[0]; - await mine(2); - - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster2.args.operatorIds, cluster2.args.cluster]), - ).to.equal( - minDepositAmount * 3n - - burnPerBlock * 8n - - burnPerBlock * 5n - - CONFIG.minimalOperatorFee + - CONFIG.minimalOperatorFee, - ); - }); - - it('Check cluster and operators balance after 10 validators bulk registration and removal', async () => { - const clusterDeposit = minDepositAmount * 10n; - - // Register 10 validators in a cluster - const { args, pks } = await bulkRegisterValidators(2, 10, [5, 6, 7, 8], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await mine(2); - - // check cluster balance - expect(await ssvViews.read.getBalance([owners[2].account.address, args.operatorIds, args.cluster])).to.equal( - clusterDeposit - burnPerBlock * 10n * 2n, - ); - - // check operators' earnings - expect(await ssvViews.read.getOperatorEarnings([5])).to.equal(CONFIG.minimalOperatorFee * 10n * 2n); - expect(await ssvViews.read.getOperatorEarnings([6])).to.equal(CONFIG.minimalOperatorFee * 10n * 2n); - expect(await ssvViews.read.getOperatorEarnings([7])).to.equal(CONFIG.minimalOperatorFee * 10n * 2n); - expect(await ssvViews.read.getOperatorEarnings([8])).to.equal(CONFIG.minimalOperatorFee * 10n * 2n); - - // bulk remove 5 validators - const result = await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks.slice(0, 5), args.operatorIds, args.cluster], { - account: owners[2].account, - }), - ); - - const removed = result.eventsByName.ValidatorRemoved[0].args; - - await mine(2); - - // check cluster balance - expect(await ssvViews.read.getBalance([owners[2].account.address, removed.operatorIds, removed.cluster])).to.equal( - clusterDeposit - burnPerBlock * 10n * 3n - burnPerBlock * 5n * 2n, - ); - - // check operators' earnings - expect(await ssvViews.read.getOperatorEarnings([5])).to.equal( - CONFIG.minimalOperatorFee * 10n * 3n + CONFIG.minimalOperatorFee * 5n * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([6])).to.equal( - CONFIG.minimalOperatorFee * 10n * 3n + CONFIG.minimalOperatorFee * 5n * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([7])).to.equal( - CONFIG.minimalOperatorFee * 10n * 3n + CONFIG.minimalOperatorFee * 5n * 2n, - ); - expect(await ssvViews.read.getOperatorEarnings([8])).to.equal( - CONFIG.minimalOperatorFee * 10n * 3n + CONFIG.minimalOperatorFee * 5n * 2n, - ); - }); - - it('Remove validators from a liquidated cluster', async () => { - const clusterDeposit = minDepositAmount * 10n; - // 3 operators cluster burnPerBlock - const newBurnPerBlock = CONFIG.minimalOperatorFee * 3n + networkFee; - - // register 10 validators - const { args, pks } = await bulkRegisterValidators(2, 10, [5, 6, 7, 8], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await mine(2); - - // remove one operator - await ssvNetwork.write.removeOperator([8]); - - await mine(2); - - // bulk remove 10 validators - const result = await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - ); - const removed = result.eventsByName.ValidatorRemoved[0].args; - - await mine(2); - - // check operators' balances - expect(await ssvViews.read.getOperatorEarnings([5])).to.equal(CONFIG.minimalOperatorFee * 10n * 6n); - expect(await ssvViews.read.getOperatorEarnings([6])).to.equal(CONFIG.minimalOperatorFee * 10n * 6n); - expect(await ssvViews.read.getOperatorEarnings([7])).to.equal(CONFIG.minimalOperatorFee * 10n * 6n); - expect(await ssvViews.read.getOperatorEarnings([8])).to.equal(0); - - // check cluster balance - expect(await ssvViews.read.getBalance([owners[2].account.address, removed.operatorIds, removed.cluster])).to.equal( - clusterDeposit - burnPerBlock * 10n * 3n - newBurnPerBlock * 10n * 3n, - ); - }); -}); - -describe('Balance Tests (reduce fee)', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee * 2n); - - networkFee = CONFIG.minimalOperatorFee; - burnPerBlock = CONFIG.minimalOperatorFee * 2n * 4n + networkFee; - minDepositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation) * burnPerBlock; - - // Set network fee - await ssvNetwork.write.updateNetworkFee([networkFee]); - - // Register validators - // cold register - await coldRegisterValidator(); - - cluster1 = ( - await bulkRegisterValidators(4, 1, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - }); - - it('Check operator earnings and cluster balance when reducing operator fee"', async () => { - const prevOperatorFee = CONFIG.minimalOperatorFee * 2n; - const newFee = CONFIG.minimalOperatorFee; - await ssvNetwork.write.reduceOperatorFee([1, newFee]); - - await mine(2); - - expect(await ssvViews.read.getOperatorEarnings([1])).to.equal( - prevOperatorFee * 4n + (prevOperatorFee + newFee * 2n), - ); - expect( - await ssvViews.read.getBalance([owners[4].account.address, cluster1.operatorIds, cluster1.cluster]), - ).to.equal(minDepositAmount - burnPerBlock - (prevOperatorFee * 3n + networkFee) * 2n - newFee * 2n); - }); -}); diff --git a/test/sanity/effective-balance.ts b/test/sanity/effective-balance.ts new file mode 100644 index 000000000..50c5493a0 --- /dev/null +++ b/test/sanity/effective-balance.ts @@ -0,0 +1,38 @@ +import hre from 'hardhat'; +import { expect } from 'chai'; + +describe('Effective Balance Roundtrip Tests', () => { + let testContract: any; + + before(async () => { + const { ethers } = await hre.network.connect(); + const factory = await ethers.getContractFactory('EffectiveBalanceTest'); + testContract = await factory.deploy(); + await testContract.waitForDeployment(); + }); + + describe('Roundtrip conversion', () => { + const testCases = [ + { effectiveBalance: 0, expectedVUnits: 0n, description: '0 ETH (edge case)' }, + { effectiveBalance: 1, expectedVUnits: 313n, description: '1 ETH (minimum)' }, + { effectiveBalance: 31, expectedVUnits: 9688n, description: '31 ETH (below 1 validator)' }, + { effectiveBalance: 32, expectedVUnits: 10000n, description: '32 ETH (1 validator, exact)' }, + { effectiveBalance: 33, expectedVUnits: 10313n, description: '33 ETH (ceiling)' }, + { effectiveBalance: 63, expectedVUnits: 19688n, description: '63 ETH' }, + { effectiveBalance: 64, expectedVUnits: 20000n, description: '64 ETH (2 validators, exact)' }, + { effectiveBalance: 100, expectedVUnits: 31250n, description: '100 ETH' }, + { effectiveBalance: 515, expectedVUnits: 160938n, description: '515 ETH (ceiling)' }, + { effectiveBalance: 1000, expectedVUnits: 312500n, description: '1000 ETH' }, + { effectiveBalance: 2048, expectedVUnits: 640000n, description: '2048 ETH (max per validator)' }, + ]; + + for (const { effectiveBalance, expectedVUnits, description } of testCases) { + it(`${description}`, async () => { + const [vUnits, result, success] = await testContract.testRoundtrip(effectiveBalance); + expect(success).to.be.true; + expect(result).to.equal(effectiveBalance); + expect(vUnits).to.equal(expectedVUnits); + }); + } + }); +}); diff --git a/test/sanity/migration-removed-operator-eth-index.test.ts b/test/sanity/migration-removed-operator-eth-index.test.ts new file mode 100644 index 000000000..3f2e7dca4 --- /dev/null +++ b/test/sanity/migration-removed-operator-eth-index.test.ts @@ -0,0 +1,491 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { + setupTestContext, + createCluster, + makePublicKey, + parseClusterFromEvent, +} from "../common/helpers.ts"; +import { DEFAULT_SHARES, ETH_DEDUCTED_DIGITS } from "../common/constants.ts"; +import { Events } from "../common/events.ts"; +import { ethers } from "ethers"; + +/** + * updateClusterOperatorsMigration skips removed operator's frozen ETH index. + * + * When an operator served ETH clusters (building up ethSnapshot.index), then was removed + * (freezing the index), and then an SSV cluster containing that operator migrates to ETH, + * the migration function must include the frozen ethSnapshot.index in cumulativeIndexETH. + * + * Otherwise, the first post-migration cluster operation sees a phantom delta equal to the + * frozen index, causing a one-time overcharge on the cluster. + */ + +const OPERATOR_FEE = 10_000_000_000n; // 10 gwei — divisible by ETH_DEDUCTED_DIGITS +const PACKED_FEE = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; // 100_000 +const MIGRATION_DEPOSIT = ethers.parseEther("5"); +const ETH_CLUSTER_DEPOSIT = ethers.parseEther("10"); +const BLOCKS_TO_ACCRUE = 100n; + +describe("Migration with removed operator frozen ETH index", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let ethClusterOwner: HardhatEthersSigner; // separate owner for the ETH cluster that builds indices + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, ethClusterOwner] } = await setupTestContext()); + }); + + async function deploy() { + return ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE); + } + + async function deployZeroFee() { + return ssvClustersHarnessFixture(connection, 4, 0n); + } + + /** + * Shared setup: builds operator ETH indices via an ETH cluster, then removes one operator. + * Returns the frozen ethSnapshot.index and the SSV cluster ready for migration. + */ + async function setupRemovedDualOperator() { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deploy); + + // Zero network fee to isolate operator fee accounting + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + // --- Phase 1: Build up operator ETH indices via an ETH cluster --- + // Use ethClusterOwner so the ETH cluster hash doesn't collide with the SSV cluster + const regTx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: ETH_CLUSTER_DEPOSIT }, + ); + const regReceipt = await regTx.wait(); + const ethCluster = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + // Mine blocks so indices accumulate on next snapshot update + await networkHelpers.mine(Number(BLOCKS_TO_ACCRUE)); + + // Register a second validator to trigger updateSnapshot for all operators + const reg2Tx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + ethCluster, + { value: ETH_CLUSTER_DEPOSIT }, + ); + await reg2Tx.wait(); + + // Verify all operators now have non-zero ethSnapshot.index + for (const opId of operatorIds) { + const [index] = await clusters.getOperatorEthSnapshot(opId); + expect(index).to.be.greaterThan(0n, `operator ${opId} should have non-zero ethSnapshot.index`); + } + + // --- Phase 2: Set up SSV cluster with same operators --- + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + await clusters.mockRegisterSSVValidator( + makePublicKey(100), + operatorIds, + clusterOwner.address, + ssvCluster, + ); + + // --- Phase 3: Remove one operator (freezes index, zeros block/fee/balance) --- + const removedOpId = operatorIds[0]; + + // Read index BEFORE removal (will be updated by mockRemoveOperatorAndPayout) + const [indexBeforeRemoval] = await clusters.getOperatorEthSnapshot(removedOpId); + + await clusters.mockRemoveOperatorAndPayout(removedOpId, clusterOwner.address); + + // Verify: index preserved, block zeroed + const [frozenIndex, frozenBlock] = await clusters.getOperatorEthSnapshot(removedOpId); + expect(frozenBlock).to.equal(0n, "removed operator ethSnapshot.block must be 0"); + expect(frozenIndex).to.be.greaterThanOrEqual( + indexBeforeRemoval, + "frozen index must be >= pre-removal index (updated by mockRemoveOperatorAndPayout)", + ); + expect(frozenIndex).to.be.greaterThan(0n, "frozen index must be non-zero for this test"); + + // Also verify SSV snapshot zeroed + const [, ssvBlock] = await clusters.getOperatorSnapshot(removedOpId); + expect(ssvBlock).to.equal(0n, "removed operator snapshot.block must be 0"); + + return { clusters, operatorIds, ssvCluster, removedOpId, frozenIndex }; + } + + it("Migration cluster.index includes removed operator's frozen ethSnapshot.index", async function () { + const { clusters, operatorIds, ssvCluster, frozenIndex } = + await setupRemovedDualOperator(); + + // Migrate + const migrateTx = await clusters.migrateClusterToETH(operatorIds, ssvCluster, { + value: MIGRATION_DEPOSIT, + }); + const migrateReceipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent( + clusters, + migrateReceipt, + Events.CLUSTER_MIGRATED_TO_ETH, + ); + + // After migration, active operators' ethSnapshot.index may have been updated + // by updateSnapshotSt. Read the post-migration values. + let expectedCumulativeIndex = 0n; + for (let i = 0; i < operatorIds.length; i++) { + const [index] = await clusters.getOperatorEthSnapshot(operatorIds[i]); + expectedCumulativeIndex += index; + } + + // The cluster.index MUST equal the sum of all operators' ethSnapshot.index, + // including the removed operator's frozen value. + expect(clusterAfterMigration.index).to.equal( + expectedCumulativeIndex, + "cluster.index must include removed operator's frozen ethSnapshot.index", + ); + + // Specifically, verify the removed operator's frozen index is part of the sum + expect(expectedCumulativeIndex).to.be.greaterThanOrEqual( + frozenIndex, + "cumulative index must contain the frozen index", + ); + }); + + it("No phantom fee charge on first post-migration operation", async function () { + const { clusters, operatorIds, ssvCluster, removedOpId, frozenIndex } = + await setupRemovedDualOperator(); + + // Migrate + const migrateTx = await clusters.migrateClusterToETH(operatorIds, ssvCluster, { + value: MIGRATION_DEPOSIT, + }); + const migrateReceipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent( + clusters, + migrateReceipt, + Events.CLUSTER_MIGRATED_TO_ETH, + ); + const migrationBlock = BigInt(migrateReceipt!.blockNumber); + + // Mine blocks, then withdraw 1 wei to trigger fee settlement + const BLOCKS_AFTER = 50n; + await networkHelpers.mine(Number(BLOCKS_AFTER)); + + const withdrawTx = await clusters.withdraw(operatorIds, 1n, clusterAfterMigration); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent( + clusters, + withdrawReceipt, + Events.CLUSTER_WITHDRAWN, + ); + const withdrawBlock = BigInt(withdrawReceipt!.blockNumber); + const blocksDiff = withdrawBlock - migrationBlock; + + // Expected fees: only 3 active operators charge fees. Removed operator contributes zero growth. + // vUnits = 1 validator * BPS_DENOMINATOR = 10_000 (implicit EB) + // fee per active operator per block = PACKED_FEE (in the index) + // total index growth = 3 * blocksDiff * PACKED_FEE + // operatorFeeUnits = totalIndexGrowth * vUnits / BPS_DENOMINATOR = 3 * blocksDiff * PACKED_FEE + // totalFees = operatorFeeUnits * ETH_DEDUCTED_DIGITS + const numActiveOperators = BigInt(operatorIds.length) - 1n; // 3 + const expectedFees = numActiveOperators * blocksDiff * PACKED_FEE * ETH_DEDUCTED_DIGITS; + const expectedBalance = MIGRATION_DEPOSIT - expectedFees - 1n; // -1 for the withdrawn wei + + expect(clusterAfterWithdraw.balance).to.equal( + expectedBalance, + `Balance mismatch: cluster was overcharged. ` + + `If the removed operator's frozen index (${frozenIndex}) caused a phantom charge, ` + + `the balance would be lower than expected by ${frozenIndex * ETH_DEDUCTED_DIGITS} wei.`, + ); + }); + + it("Migration with multiple removed operators preserves all frozen indices", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deploy); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + // Build up ETH indices (use ethClusterOwner to avoid hash collision) + const regTx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: ETH_CLUSTER_DEPOSIT }, + ); + const regReceipt = await regTx.wait(); + const ethCluster = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + await networkHelpers.mine(Number(BLOCKS_TO_ACCRUE)); + + const reg2Tx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + ethCluster, + { value: ETH_CLUSTER_DEPOSIT }, + ); + await reg2Tx.wait(); + + // Set up SSV cluster + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + await clusters.mockRegisterSSVValidator( + makePublicKey(100), + operatorIds, + clusterOwner.address, + ssvCluster, + ); + + // Remove TWO operators + const removedOp1 = operatorIds[0]; + const removedOp2 = operatorIds[1]; + await clusters.mockRemoveOperatorAndPayout(removedOp1, clusterOwner.address); + await clusters.mockRemoveOperatorAndPayout(removedOp2, clusterOwner.address); + + const [frozenIndex1] = await clusters.getOperatorEthSnapshot(removedOp1); + const [frozenIndex2] = await clusters.getOperatorEthSnapshot(removedOp2); + expect(frozenIndex1).to.be.greaterThan(0n); + expect(frozenIndex2).to.be.greaterThan(0n); + + // Migrate + const migrateTx = await clusters.migrateClusterToETH(operatorIds, ssvCluster, { + value: MIGRATION_DEPOSIT, + }); + const migrateReceipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent( + clusters, + migrateReceipt, + Events.CLUSTER_MIGRATED_TO_ETH, + ); + + // Verify cluster.index includes both frozen indices + let expectedCumulativeIndex = 0n; + for (const opId of operatorIds) { + const [index] = await clusters.getOperatorEthSnapshot(opId); + expectedCumulativeIndex += index; + } + + expect(clusterAfterMigration.index).to.equal( + expectedCumulativeIndex, + "cluster.index must include both removed operators' frozen ethSnapshot.index", + ); + + // Verify no phantom charge: withdraw after mining + await networkHelpers.mine(20); + + const withdrawTx = await clusters.withdraw(operatorIds, 1n, clusterAfterMigration); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent( + clusters, + withdrawReceipt, + Events.CLUSTER_WITHDRAWN, + ); + const migrationBlock = BigInt(migrateReceipt!.blockNumber); + const withdrawBlock = BigInt(withdrawReceipt!.blockNumber); + const blocksDiff = withdrawBlock - migrationBlock; + + // Only 2 active operators charge fees + const numActive = BigInt(operatorIds.length) - 2n; + const expectedFees = numActive * blocksDiff * PACKED_FEE * ETH_DEDUCTED_DIGITS; + const expectedBalance = MIGRATION_DEPOSIT - expectedFees - 1n; + + expect(clusterAfterWithdraw.balance).to.equal( + expectedBalance, + "Two removed operators must not cause phantom fee charges", + ); + }); + + it("Removed SSV-only operator (zero ETH index) causes no issue on migration", async function () { + // Convert harness operators into legacy SSV-only operators: + // snapshot.block > 0, ethSnapshot.block == 0, ethSnapshot.index == 0 + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployZeroFee); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + for (const opId of operatorIds) { + await clusters.mockSetOperatorLegacySSV(opId, 0n); + const [, ssvBlock] = await clusters.getOperatorSnapshot(opId); + const [ethIndex, ethBlock] = await clusters.getOperatorEthSnapshot(opId); + expect(ssvBlock).to.be.greaterThan(0n, "legacy SSV operator must keep snapshot.block"); + expect(ethBlock).to.equal(0n, "legacy SSV operator must have ethSnapshot.block == 0"); + expect(ethIndex).to.equal(0n, "legacy SSV operator must start with zero ethSnapshot.index"); + } + + // Set up SSV cluster directly (no ETH activity needed) + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + await clusters.mockRegisterSSVValidator( + makePublicKey(1), + operatorIds, + clusterOwner.address, + ssvCluster, + ); + + // Remove operator — ethSnapshot.index should be 0 + const removedOpId = operatorIds[0]; + await clusters.mockRemoveOperator(removedOpId); + const [frozenIndex] = await clusters.getOperatorEthSnapshot(removedOpId); + expect(frozenIndex).to.equal(0n, "SSV-only operator should have zero ethSnapshot.index"); + + // Migrate — should work and produce correct cluster.index + const migrateTx = await clusters.migrateClusterToETH(operatorIds, ssvCluster, { + value: MIGRATION_DEPOSIT, + }); + const migrateReceipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent( + clusters, + migrateReceipt, + Events.CLUSTER_MIGRATED_TO_ETH, + ); + + expect(clusterAfterMigration.active).to.equal(true); + expect(clusterAfterMigration.balance).to.equal(MIGRATION_DEPOSIT); + + // cluster.index should equal sum of all operators' indices (all zero for fee=0) + let expectedIndex = 0n; + for (const opId of operatorIds) { + const [index] = await clusters.getOperatorEthSnapshot(opId); + expectedIndex += index; + } + expect(clusterAfterMigration.index).to.equal(expectedIndex); + }); + + it("Liquidated SSV cluster migration with removed dual operator is correctly accounted", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deploy); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + // Build ETH indices via an ETH cluster (use ethClusterOwner to avoid hash collision) + const regTx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: ETH_CLUSTER_DEPOSIT }, + ); + const regReceipt = await regTx.wait(); + const ethCluster = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + await networkHelpers.mine(Number(BLOCKS_TO_ACCRUE)); + + // Trigger snapshot update + const reg2Tx = await clusters.connect(ethClusterOwner).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + ethCluster, + { value: ETH_CLUSTER_DEPOSIT }, + ); + await reg2Tx.wait(); + + // Set up an active SSV cluster first, then liquidate it through the real flow + const activeSsvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + await clusters.mockRegisterSSVValidator( + makePublicKey(100), + operatorIds, + clusterOwner.address, + activeSsvCluster, + ); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, activeSsvCluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedSsvCluster = parseClusterFromEvent( + clusters, + liquidateReceipt, + Events.CLUSTER_LIQUIDATED, + ); + expect(liquidatedSsvCluster.active).to.equal(false, "liquidateSSV must produce an inactive SSV cluster"); + + // Remove one operator after liquidation so migration exercises the + // already-liquidated branch together with a removed operator. + const removedOpId = operatorIds[0]; + await clusters.mockRemoveOperatorAndPayout(removedOpId, clusterOwner.address); + + const [frozenIndex] = await clusters.getOperatorEthSnapshot(removedOpId); + expect(frozenIndex).to.be.greaterThan(0n); + + // Migrate the liquidated SSV cluster (migration also reactivates) + const migrateTx = await clusters.migrateClusterToETH(operatorIds, liquidatedSsvCluster, { + value: MIGRATION_DEPOSIT, + }); + const migrateReceipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent( + clusters, + migrateReceipt, + Events.CLUSTER_MIGRATED_TO_ETH, + ); + + // Cluster must be active after migration + expect(clusterAfterMigration.active).to.equal(true); + expect(clusterAfterMigration.balance).to.equal(MIGRATION_DEPOSIT); + + // cluster.index must include the removed operator's frozen index + let expectedCumulativeIndex = 0n; + for (const opId of operatorIds) { + const [index] = await clusters.getOperatorEthSnapshot(opId); + expectedCumulativeIndex += index; + } + expect(clusterAfterMigration.index).to.equal(expectedCumulativeIndex); + + // Post-migration: verify no phantom charge + await networkHelpers.mine(30); + + const withdrawTx = await clusters.withdraw(operatorIds, 1n, clusterAfterMigration); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent( + clusters, + withdrawReceipt, + Events.CLUSTER_WITHDRAWN, + ); + const migrationBlock = BigInt(migrateReceipt!.blockNumber); + const withdrawBlock = BigInt(withdrawReceipt!.blockNumber); + const blocksDiff = withdrawBlock - migrationBlock; + + const numActive = BigInt(operatorIds.length) - 1n; + const expectedFees = numActive * blocksDiff * PACKED_FEE * ETH_DEDUCTED_DIGITS; + const expectedBalance = MIGRATION_DEPOSIT - expectedFees - 1n; + + expect(clusterAfterWithdraw.balance).to.equal( + expectedBalance, + "Liquidated SSV cluster migration with removed operator must not cause phantom charge", + ); + }); +}); diff --git a/test/sanity/operator-views-consistency.test.ts b/test/sanity/operator-views-consistency.test.ts new file mode 100644 index 000000000..ac5a3514b --- /dev/null +++ b/test/sanity/operator-views-consistency.test.ts @@ -0,0 +1,126 @@ +import type { NetworkConnection } from 'hardhat/types/network'; +import type { NetworkHelpersType } from '../common/types.js'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { getTestConnection } from '../setup/connection.js'; +import { ssvNetworkFullFixture } from '../setup/fixtures.js'; +import { + makePublicKey, + makeOperatorKey, + registerOperators, + whitelistAddresses, + getCurrentClusterState, +} from '../common/helpers.js'; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + MINIMAL_OPERATOR_ETH_FEE, +} from '../common/constants.js'; +import { expect } from 'chai'; + +describe("Operator views consistency sanity tests", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [operatorOwner, clusterOwner] = await connection.ethers.getSigners(); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + describe("getOperatorById vs getOperatorByIdSSV isActive consistency", () => { + it("ETH-only operator: both views report isActive = true", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.connect(operatorOwner).registerOperator.staticCall( + operatorKey, MINIMAL_OPERATOR_ETH_FEE, false + ); + await network.connect(operatorOwner).registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, false); + + const ethView = await views.getOperatorById(operatorId); + const ssvView = await views.getOperatorByIdSSV(operatorId); + + expect(ethView.isActive).to.equal(true); + expect(ssvView.isActive).to.equal(true); + }); + + it("Removed operator: both views report isActive = false", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 1); + const operatorId = operatorIds[0]; + + await network.connect(operatorOwner).removeOperator(operatorId); + + const ethView = await views.getOperatorById(operatorId); + const ssvView = await views.getOperatorByIdSSV(operatorId); + + expect(ethView.isActive).to.equal(false); + expect(ssvView.isActive).to.equal(false); + }); + + it("Operator used in ETH cluster: both views report isActive = true", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(50); + + for (const opId of operatorIds) { + const ethView = await views.getOperatorById(opId); + const ssvView = await views.getOperatorByIdSSV(opId); + expect(ethView.isActive).to.equal(true); + expect(ssvView.isActive).to.equal(true); + } + }); + + it("Removed operator with prior ETH cluster usage: both views report isActive = false", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await connection.networkHelpers.mine(10); + + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + await network.connect(clusterOwner).removeValidator(makePublicKey(1), operatorIds, cluster); + + await network.connect(operatorOwner).removeOperator(operatorIds[0]); + + const ethView = await views.getOperatorById(operatorIds[0]); + const ssvView = await views.getOperatorByIdSSV(operatorIds[0]); + + expect(ethView.isActive).to.equal(false); + expect(ssvView.isActive).to.equal(false); + }); + }); + +}); diff --git a/test/sanity/precision-governance-boundaries.test.ts b/test/sanity/precision-governance-boundaries.test.ts new file mode 100644 index 000000000..841d455f7 --- /dev/null +++ b/test/sanity/precision-governance-boundaries.test.ts @@ -0,0 +1,271 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { + setupTestContext, + computeClusterId, + computeEBRoot, + createCluster, + makePublicKey, + parseClusterFromEvent, +} from "../common/helpers.ts"; +import { DEFAULT_SHARES, DEFAULT_ETH_REGISTER_VALUE, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../common/constants.ts"; +import { Events } from "../common/events.ts"; +import { Errors } from "../common/errors.ts"; +import { calcClusterBurn } from "../helpers/index.ts"; + +const OPERATOR_FEE_RAW = 100_000n; +const OPERATOR_FEE_WEI = OPERATOR_FEE_RAW * ETH_DEDUCTED_DIGITS; + +describe("precision and governance boundaries", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator] } = await setupTestContext()); + }); + + const deployWithFee = async () => ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE_WEI); + const deployWithZeroFee = async () => ssvClustersHarnessFixture(connection, 4, 0n); + + async function registerOne(clusters: any, operatorIds: bigint[], publicKeySeed: number) { + const tx = await clusters.registerValidator( + makePublicKey(publicKeySeed), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + return { + cluster: parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED), + block: BigInt(receipt!.blockNumber), + }; + } + + async function registerMany(clusters: any, operatorIds: bigint[], count: number, firstSeed: number) { + let cluster = createCluster(); + for (let i = 0; i < count; i++) { + const tx = await clusters.registerValidator( + makePublicKey(firstSeed + i), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + cluster = parseClusterFromEvent(clusters, await tx.wait(), Events.VALIDATOR_ADDED); + } + return cluster; + } + + async function updateEB(clusters: any, operatorIds: bigint[], cluster: any, effectiveBalance: number, blockNum: number) { + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await clusters.mockSetEBRoot(blockNum, computeEBRoot(clusterId, effectiveBalance)); + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const receipt = await tx.wait(); + return { + cluster: parseClusterFromEvent(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED), + block: BigInt(receipt!.blockNumber), + }; + } + + it("explicit EB=32 with removed operator liquidates without underflow", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithFee); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const { cluster } = await registerOne(clusters, operatorIds, 2001); + const { cluster: clusterAfter32 } = await updateEB(clusters, operatorIds, cluster, 32, 1); + await clusters.mockRemoveOperator(operatorIds[0]); + + await expect( + clusters.liquidate(clusterOwner.address, operatorIds, clusterAfter32) + ).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + }); + + it("EB=33 lifecycle keeps exact minimal non-default vUnits", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithFee); + await clusters.mockEthNetworkFee(0n); + + const { cluster } = await registerOne(clusters, operatorIds, 2002); + const { cluster: clusterAfter33, block: blockAfter33 } = await updateEB(clusters, operatorIds, cluster, 33, 1); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const expectedVUnits = (33n * BPS_DENOMINATOR + 31n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + + await networkHelpers.mine(10); + const withdrawTx = await clusters.withdraw(operatorIds, 0n, clusterAfter33); + const withdrawReceipt = await withdrawTx.wait(); + const blockAfterWithdraw = BigInt(withdrawReceipt!.blockNumber); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const expectedBurn = calcClusterBurn({ + blockDiff: blockAfterWithdraw - blockAfter33, + numOperators: 4n, + ethFee: OPERATOR_FEE_RAW, + networkFee: 0n, + effectiveVUnits: expectedVUnits, + }); + expect(clusterAfter33.balance - clusterAfterWithdraw.balance).to.equal(expectedBurn); + }); + + it("same-EB update has zero vUnit delta", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithFee); + await clusters.mockEthNetworkFee(0n); + await clusters.mockSetMinBlocksBetweenUpdates(1); + + const { cluster } = await registerOne(clusters, operatorIds, 2003); + const { cluster: clusterAfter64 } = await updateEB(clusters, operatorIds, cluster, 64, 1); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const vUnitsBefore = await clusters.getClusterVUnits(clusterId); + const opVUnitsBefore = await clusters.getOperatorEthVUnits(operatorIds[0]); + await networkHelpers.mine(1); + + await updateEB(clusters, operatorIds, clusterAfter64, 64, 2); + const vUnitsAfter = await clusters.getClusterVUnits(clusterId); + const opVUnitsAfter = await clusters.getOperatorEthVUnits(operatorIds[0]); + + expect(vUnitsAfter).to.equal(vUnitsBefore); + expect(opVUnitsAfter).to.equal(opVUnitsBefore); + }); + + it("governance parameter changes move explicit-EB liquidation boundaries deterministically", async function () { + const { clusters: clustersFee, operatorIds: operatorIdsFee } = await networkHelpers.loadFixture(deployWithZeroFee); + const { cluster: feeCluster } = await registerOne(clustersFee, operatorIdsFee, 2004); + const { cluster: feeClusterAfter64 } = await updateEB(clustersFee, operatorIdsFee, feeCluster, 64, 1); + await clustersFee.mockMinimumBlocksBeforeLiquidation(1n); + await clustersFee.mockMinimumLiquidationCollateral(0n); + await clustersFee.mockEthNetworkFee(1n); + await expect( + clustersFee.connect(liquidator).liquidate(clusterOwner.address, operatorIdsFee, feeClusterAfter64) + ).to.be.revertedWithCustomError(clustersFee, Errors.CLUSTER_NOT_LIQUIDATABLE); + await clustersFee.mockEthNetworkFee(1_000_000_000_000_000n); + await expect( + clustersFee.connect(liquidator).liquidate(clusterOwner.address, operatorIdsFee, feeClusterAfter64) + ).to.emit(clustersFee, Events.CLUSTER_LIQUIDATED); + + const { clusters: clustersCollateral, operatorIds: operatorIdsCollateral } = await networkHelpers.loadFixture(deployWithZeroFee); + const { cluster: collateralCluster } = await registerOne(clustersCollateral, operatorIdsCollateral, 2005); + const { cluster: collateralClusterAfter64 } = await updateEB(clustersCollateral, operatorIdsCollateral, collateralCluster, 64, 1); + await clustersCollateral.mockEthNetworkFee(0n); + await clustersCollateral.mockMinimumBlocksBeforeLiquidation(1n); + await clustersCollateral.mockMinimumLiquidationCollateral(collateralClusterAfter64.balance + 1n); + await expect( + clustersCollateral.connect(liquidator).liquidate(clusterOwner.address, operatorIdsCollateral, collateralClusterAfter64) + ).to.emit(clustersCollateral, Events.CLUSTER_LIQUIDATED); + + const { clusters: clustersPeriod, operatorIds: operatorIdsPeriod } = await networkHelpers.loadFixture(deployWithZeroFee); + const { cluster: periodCluster } = await registerOne(clustersPeriod, operatorIdsPeriod, 2006); + const { cluster: periodClusterAfter64 } = await updateEB(clustersPeriod, operatorIdsPeriod, periodCluster, 64, 1); + await clustersPeriod.mockEthNetworkFee(1_000_000_000_000_000n); + await clustersPeriod.mockMinimumLiquidationCollateral(0n); + await clustersPeriod.mockMinimumBlocksBeforeLiquidation(1_000_000_000n); + await expect( + clustersPeriod.connect(liquidator).liquidate(clusterOwner.address, operatorIdsPeriod, periodClusterAfter64) + ).to.emit(clustersPeriod, Events.CLUSTER_LIQUIDATED); + }); + + it("maximum deviation decrease (2048 -> 32) after operator removal is safe and exact", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithZeroFee); + await clusters.mockEthNetworkFee(0n); + + const { cluster } = await registerOne(clusters, operatorIds, 2005); + const { cluster: clusterAfter2048 } = await updateEB(clusters, operatorIds, cluster, 2048, 1); + await clusters.mockRemoveOperator(operatorIds[0]); + const { cluster: clusterAfter32 } = await updateEB(clusters, operatorIds, clusterAfter2048, 32, 2); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(clusterAfter32.active).to.equal(true); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + }); + + it("non-round vUnits accrual is exact across EB 33 -> 65 transition", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithFee); + await clusters.mockEthNetworkFee(0n); + + const { cluster } = await registerOne(clusters, operatorIds, 2006); + const { cluster: clusterAfter33, block: blockAfter33 } = await updateEB(clusters, operatorIds, cluster, 33, 1); + + await networkHelpers.mine(10); + const { cluster: clusterAfter65, block: blockAfter65 } = await updateEB(clusters, operatorIds, clusterAfter33, 65, 2); + + await networkHelpers.mine(10); + const withdrawTx = await clusters.withdraw(operatorIds, 0n, clusterAfter65); + const withdrawReceipt = await withdrawTx.wait(); + const blockAfterWithdraw = BigInt(withdrawReceipt!.blockNumber); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const vUnits33 = (33n * BPS_DENOMINATOR + 31n) / 32n; + const vUnits65 = (65n * BPS_DENOMINATOR + 31n) / 32n; + const expectedBurn = calcClusterBurn({ + blockDiff: blockAfter65 - blockAfter33, + numOperators: 4n, + ethFee: OPERATOR_FEE_RAW, + networkFee: 0n, + effectiveVUnits: vUnits33, + }) + calcClusterBurn({ + blockDiff: blockAfterWithdraw - blockAfter65, + numOperators: 4n, + ethFee: OPERATOR_FEE_RAW, + networkFee: 0n, + effectiveVUnits: vUnits65, + }); + + expect(clusterAfter33.balance - clusterAfterWithdraw.balance).to.equal(expectedBurn); + }); + + it("EB=225 with 7 validators yields exact per-operator rounded deviation", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithZeroFee); + await clusters.mockEthNetworkFee(0n); + + const clusterWithSevenValidators = await registerMany(clusters, operatorIds, 7, 3000); + const { cluster: clusterAfter225 } = await updateEB(clusters, operatorIds, clusterWithSevenValidators, 225, 1); + expect(clusterAfter225.validatorCount).to.equal(7n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const expectedVUnits = (225n * BPS_DENOMINATOR + 31n) / 32n; + const baseline = 7n * BPS_DENOMINATOR; + const expectedDeviation = expectedVUnits - baseline; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedDeviation); + } + }); + + it("withdrawing exact max after settlement leaves zero residual dust", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithZeroFee); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const { cluster } = await registerOne(clusters, operatorIds, 2007); + const { cluster: clusterAfter64 } = await updateEB(clusters, operatorIds, cluster, 64, 1); + + await networkHelpers.mine(5); + const settleTx = await clusters.withdraw(operatorIds, 0n, clusterAfter64); + const settleReceipt = await settleTx.wait(); + const clusterAfterSettle = parseClusterFromEvent(clusters, settleReceipt, Events.CLUSTER_WITHDRAWN); + const exactMaxWithdraw = clusterAfterSettle.balance; + + const maxWithdrawTx = await clusters.withdraw(operatorIds, exactMaxWithdraw, clusterAfterSettle); + const maxWithdrawReceipt = await maxWithdrawTx.wait(); + const clusterAfterMaxWithdraw = parseClusterFromEvent(clusters, maxWithdrawReceipt, Events.CLUSTER_WITHDRAWN); + expect(clusterAfterMaxWithdraw.balance).to.equal(0n); + }); +}); diff --git a/test/sanity/removed-operator-with-deviated-cluster.test.ts b/test/sanity/removed-operator-with-deviated-cluster.test.ts new file mode 100644 index 000000000..54af92f02 --- /dev/null +++ b/test/sanity/removed-operator-with-deviated-cluster.test.ts @@ -0,0 +1,1177 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { + setupTestContext, + computeClusterId, + computeEBRoot, + createCluster, + makePublicKey, + parseClusterFromEvent, + registerAndParseCluster, + assertOperatorVUnits, +} from "../common/helpers.ts"; +import { DEFAULT_SHARES, DEFAULT_ETH_REGISTER_VALUE, BPS_DENOMINATOR } from "../common/constants.ts"; +import { Events } from "../common/events.ts"; +import { Errors } from "../common/errors.ts"; + +const OPERATOR_FEE = 10_000_000_000n; +const OPERATOR_COUNTS = [4, 7, 10, 13]; + +describe("'removeOperator()' deletes operatorEthVUnits and does not affect clusters", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator] } = await setupTestContext()); + }); + + async function deploy4() { return ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE); } + async function deploy7() { return ssvClustersHarnessFixture(connection, 7, OPERATOR_FEE); } + async function deploy10() { return ssvClustersHarnessFixture(connection, 10, OPERATOR_FEE); } + async function deploy13() { return ssvClustersHarnessFixture(connection, 13, OPERATOR_FEE); } + const fixtures: Record Promise> = { 4: deploy4, 7: deploy7, 10: deploy10, 13: deploy13 }; + + function runTestSuite(operatorCount: number) { + const loadFixtureForCount = () => networkHelpers.loadFixture(fixtures[operatorCount]); + + it("liquidation does not revert after operator removal when cluster has EB deviation", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const depositValue = 5_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + clusterAfterReg, + 64, + [], + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + + const expectedDeviation = 20000n - BPS_DENOMINATOR; + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation, 20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + + await networkHelpers.mine(200); + + await expect( + clusters.connect(liquidator).liquidate(clusterOwner.address, operatorIds, clusterAfterEB), + ).to.not.revert(connection.ethers); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("'updateClusterBalance()' with previous deviation and EB decrease does not revert after operator removal", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + clusterAfterReg, + 64, + [], + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB64 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + const root2 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(2, root2); + + await expect( + clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB64, 32, []), + ).to.not.revert(connection.ethers); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(BPS_DENOMINATOR); + }); + + it("'bulkRemoveValidator()' (emptying cluster) does not revert after operator removal", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + clusterAfterReg, + 64, + [], + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + await expect( + clusters.removeValidator(makePublicKey(1), operatorIds, clusterAfterEB), + ).to.not.revert(connection.ethers); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("auto liquidation via 'updateClusterBalance()' does not revert after operator removal", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + const networkFee = 100_000n; + await clusters.mockEthNetworkFee(networkFee); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const depositValue = (BigInt(operatorIds.length) + 1n) * 3_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + clusterAfterReg, + 64, + [], + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB64 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(clusterAfterEB64.active).to.equal(true); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + const removedOpId = operatorIds[0]; + await clusters.mockRemoveOperator(removedOpId); + + await networkHelpers.mine(140); + + const root2 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(2, root2); + + const autoLiqTx = await clusters.updateClusterBalance( + 2, clusterOwner.address, operatorIds, clusterAfterEB64, 32, [], + ); + const autoLiqReceipt = await autoLiqTx.wait(); + const clusterAfterAutoLiq = parseClusterFromEvent(clusters, autoLiqReceipt, Events.CLUSTER_LIQUIDATED); + expect(clusterAfterAutoLiq.active).to.equal(false); + + expect(await clusters.getOperatorEthVUnits(removedOpId)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("'updateClusterBalance()' with EB increase does not re-add deviation to removed operator", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + clusterAfterReg, + 64, + [], + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB64 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + const root2 = computeEBRoot(clusterId, 128); + await clusters.mockSetEBRoot(2, root2); + + await expect( + clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB64, 128, []), + ).to.not.revert(connection.ethers); + + const expectedVUnits = 40000n; + const expectedDeviation = expectedVUnits - BPS_DENOMINATOR; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(expectedDeviation); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(expectedVUnits); + }); + + it("liquidating two clusters with common removed operator cleans up correctly and does not revert", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const depositValue = 5_000_000_000_000n; + + const clusterIdA = computeClusterId(clusterOwner.address, operatorIds); + const regTxA = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceiptA = await regTxA.wait(); + const clusterA = parseClusterFromEvent(clusters, regReceiptA, Events.VALIDATOR_ADDED); + + const clusterIdB = computeClusterId(liquidator.address, operatorIds); + const regTxB = await clusters.connect(liquidator).registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceiptB = await regTxB.wait(); + const clusterB = parseClusterFromEvent(clusters, regReceiptB, Events.VALIDATOR_ADDED); + + const rootA = computeEBRoot(clusterIdA, 64); + await clusters.mockSetEBRoot(1, rootA); + const ebTxA = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterA, 64, [], + ); + const clusterAAfterEB = parseClusterFromEvent(clusters, await ebTxA.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const rootB = computeEBRoot(clusterIdB, 64); + await clusters.mockSetEBRoot(2, rootB); + const ebTxB = await clusters.connect(liquidator).updateClusterBalance( + 2, liquidator.address, operatorIds, clusterB, 64, [], + ); + const clusterBAfterEB = parseClusterFromEvent(clusters, await ebTxB.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const deviationPerCluster = 20000n - BPS_DENOMINATOR; + for (const opId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(deviationPerCluster * 2n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n * 2n); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + + await networkHelpers.mine(200); + + await clusters.connect(liquidator).liquidate(clusterOwner.address, operatorIds, clusterAAfterEB); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(deviationPerCluster); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + await clusters.connect(clusterOwner).liquidate(liquidator.address, operatorIds, clusterBAfterEB); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("'reactivate()' with EB deviation does not add deviation to a removed operator", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const depositValue = 5_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 64, [], + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const expectedDeviation = 20000n - BPS_DENOMINATOR; + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + await networkHelpers.mine(200); + + const liqTx = await clusters.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, clusterAfterEB, + ); + const clusterAfterLiq = parseClusterFromEvent(clusters, await liqTx.wait(), Events.CLUSTER_LIQUIDATED); + expect(clusterAfterLiq.active).to.equal(false); + + await assertOperatorVUnits(clusters, operatorIds, 0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + const reactivateTx = await clusters.reactivate( + operatorIds, clusterAfterLiq, { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const clusterAfterReactivate = parseClusterFromEvent( + clusters, await reactivateTx.wait(), Events.CLUSTER_REACTIVATED, + ); + expect(clusterAfterReactivate.active).to.equal(true); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(expectedDeviation); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + }); + + it("'migrateClusterToETH()' with EB deviation does not write deviation to removed operator", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + await clusters.mockRegisterSSVValidator( + makePublicKey(10), operatorIds, clusterOwner.address, ssvCluster, + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const vUnitsEB64 = 20000n; + await clusters.mockSetClusterVUnits(clusterId, vUnitsEB64); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, ssvCluster, { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const clusterAfterMigration = parseClusterFromEvent( + clusters, await migrateTx.wait(), Events.CLUSTER_MIGRATED_TO_ETH, + ); + expect(clusterAfterMigration.active).to.equal(true); + + const expectedDeviation = vUnitsEB64 - BPS_DENOMINATOR; + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(expectedDeviation); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(vUnitsEB64); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(vUnitsEB64); + }); + + it("'migrateClusterToETH()' with removed operator skips removed operator's snapshot and fee accumulation", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + await clusters.mockRegisterSSVValidator( + makePublicKey(10), operatorIds, clusterOwner.address, ssvCluster, + ); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + await networkHelpers.mine(50); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, ssvCluster, { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const clusterAfterMigration = parseClusterFromEvent( + clusters, await migrateTx.wait(), Events.CLUSTER_MIGRATED_TO_ETH, + ); + expect(clusterAfterMigration.active).to.equal(true); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(BPS_DENOMINATOR); + }); + + it("R-11: liquidation does not revert after removing 2 operators from explicit EB cluster", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const depositValue = 5_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 64, [], + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const expectedDeviation = 20000n - BPS_DENOMINATOR; + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation, 20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + // Remove 2 operators — larger underflow surface + const removedOp1 = operatorIds[0]; + const removedOp2 = operatorIds[1]; + await clusters.mockRemoveOperator(removedOp1); + await clusters.mockRemoveOperator(removedOp2); + + expect(await clusters.getOperatorEthVUnits(removedOp1)).to.equal(0n); + expect(await clusters.getOperatorEthVUnits(removedOp2)).to.equal(0n); + + await networkHelpers.mine(200); + + await expect( + clusters.connect(liquidator).liquidate(clusterOwner.address, operatorIds, clusterAfterEB), + ).to.not.revert(connection.ethers); + + // Both removed operators stay at 0, survivors clean up + expect(await clusters.getOperatorEthVUnits(removedOp1)).to.equal(0n); + expect(await clusters.getOperatorEthVUnits(removedOp2)).to.equal(0n); + for (const opId of operatorIds.slice(2)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("EC-03: maximum EB (2048) + removed operator + liquidate does not underflow", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + // Use high network fee but zero operator fee impact to isolate the deviation math + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + // Large deposit to survive EB=2048 burn rate (640000 vUnits × 64x fees) + const depositValue = 500_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + // Maximum EB: 2048 ETH → 640,000 vUnits → deviation = 630,000 per operator + const root1 = computeEBRoot(clusterId, 2048); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 2048, [], + ); + const ebReceipt = await ebTx.wait(); + + // Check if auto-liquidation happened (cluster may be insolvent at 64x burn rate) + let clusterAfterEB; + try { + clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + } catch { + // Auto-liquidated during EB update — deviation was cleaned up in the same tx + clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_LIQUIDATED); + // Even if auto-liquidated, verify removed op invariant holds + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + return; + } + + const maxVUnits = 640000n; + const maxDeviation = maxVUnits - BPS_DENOMINATOR; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(maxVUnits); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(maxVUnits); + // Check each operator individually to get better error messages + for (const opId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(maxDeviation); + } + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + + await networkHelpers.mine(200); + + // Liquidation must not underflow despite 630,000 deviation on dead slot + await expect( + clusters.connect(liquidator).liquidate(clusterOwner.address, operatorIds, clusterAfterEB), + ).to.not.revert(connection.ethers); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("RI-04: implicit EB cluster → remove operator → first oracle EB update skips dead operator", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + // Register at implicit EB (no oracle update yet) + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + // All operators at 0 deviation (implicit EB) + for (const opId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(BPS_DENOMINATOR); + + // Remove operator BEFORE any oracle update + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + // First oracle EB update — deviation write must skip removed operator + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + await expect( + clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 64, []), + ).to.not.revert(connection.ethers); + + const expectedDeviation = 20000n - BPS_DENOMINATOR; + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const opId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(expectedDeviation); + } + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + }); + + it("R-13: withdraw succeeds after operator removal on explicit EB cluster (balance settlement correctness)", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 64, [], + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + await networkHelpers.mine(50); + + // Withdraw a small amount — triggers fee settlement + liquidation check with removed operator + const withdrawAmount = 100_000n; + const withdrawTx = await clusters.withdraw(operatorIds, withdrawAmount, clusterAfterEB); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + // Cluster still active after small withdrawal + expect(clusterAfterWithdraw.active).to.equal(true); + // Removed operator stays clean + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + }); + + it("R-11 variant: removing 2 operators + EB decrease does not underflow", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 64, [], + ); + const clusterAfterEB64 = parseClusterFromEvent(clusters, await ebTx1.wait(), Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + + // Remove 2 operators + const removedOp1 = operatorIds[0]; + const removedOp2 = operatorIds[1]; + await clusters.mockRemoveOperator(removedOp1); + await clusters.mockRemoveOperator(removedOp2); + + // EB decrease from 64 → 32: subtracts deviation from each active operator + const root2 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(2, root2); + + await expect( + clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB64, 32, []), + ).to.not.revert(connection.ethers); + + expect(await clusters.getOperatorEthVUnits(removedOp1)).to.equal(0n); + expect(await clusters.getOperatorEthVUnits(removedOp2)).to.equal(0n); + for (const opId of operatorIds.slice(2)) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(BPS_DENOMINATOR); + }); + + it("R-07: registerValidator reverts with OperatorDoesNotExist after operator removal on explicit EB cluster", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + // Set explicit EB=64 + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 64, [], + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + // Attempting to register a second validator must revert — removed operator no longer exists + await expect( + clusters.registerValidator( + makePublicKey(99), + operatorIds, + DEFAULT_SHARES, + clusterAfterEB, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ), + ).to.be.revertedWithCustomError(clusters, Errors.OPERATOR_DOES_NOT_EXIST); + + // vUnits unchanged — revert was atomic + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + }); + + it("R-10: explicit EB=32 (zero deviation) + removed operator + self-liquidate does not revert", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const depositValue = 5_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + // Explicit EB=32 — same as default, so deviation = 0 per operator + const root1 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 32, [], + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + // Explicit EB=32 means vUnits = 10000, deviation = 0 + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + for (const opId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + await networkHelpers.mine(200); + + // Self-liquidate — deviation is 0, so no subtraction at all, but guard must still hold + await expect( + clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB), + ).to.not.revert(connection.ethers); + + for (const opId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("EC-05: all operators removed from explicit EB cluster → self-liquidate does not revert", async function () { + const { clusters, operatorIds } = await loadFixtureForCount(); + + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(10n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const depositValue = 5_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 64, [], + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const expectedDeviation = 20000n - BPS_DENOMINATOR; + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation, 20000n); + + // Remove ALL operators + for (const opId of operatorIds) { + await clusters.mockRemoveOperator(opId); + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + + await networkHelpers.mine(200); + + // Self-liquidation must not revert even with all operators removed + await expect( + clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB), + ).to.not.revert(connection.ethers); + + for (const opId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(opId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + } + + for (const operatorCount of OPERATOR_COUNTS) { + describe(`with ${operatorCount} operators`, function () { + runTestSuite(operatorCount); + }); + } + + describe("Cross-cluster removed-operator propagation (4 operators)", function () { + const loadFixtureFor4 = () => networkHelpers.loadFixture(deploy4); + + async function registerSingleValidatorCluster( + clusters: any, + owner: HardhatEthersSigner, + operatorIds: bigint[], + publicKeySeed: number, + depositValue = 5_000_000_000_000n, + ) { + const registerTx = await clusters.connect(owner).registerValidator( + makePublicKey(publicKeySeed), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue }, + ); + return parseClusterFromEvent(clusters, await registerTx.wait(), Events.VALIDATOR_ADDED); + } + + async function updateClusterEB( + clusters: any, + owner: HardhatEthersSigner, + operatorIds: bigint[], + cluster: any, + blockNum: number, + effectiveBalance: number, + ) { + const clusterId = computeClusterId(owner.address, operatorIds); + await clusters.mockSetEBRoot(blockNum, computeEBRoot(clusterId, effectiveBalance)); + const updateTx = await clusters.connect(owner).updateClusterBalance( + blockNum, + owner.address, + operatorIds, + cluster, + effectiveBalance, + [], + ); + return parseClusterFromEvent(clusters, await updateTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + } + + it("shared operator removal does not corrupt multi-cluster explicit-EB totals", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterA = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8101); + const clusterB = await registerSingleValidatorCluster(clusters, liquidator, operatorIds, 8102); + + await updateClusterEB(clusters, clusterOwner, operatorIds, clusterA, 1, 64); + await updateClusterEB(clusters, liquidator, operatorIds, clusterB, 2, 64); + + const removedOperator = operatorIds[0]; + const expectedDeviationPerLiveOperator = 20000n; + expect(await clusters.getDaoTotalEthVUnits()).to.equal(40000n); + await clusters.mockRemoveOperator(removedOperator); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedDeviationPerLiveOperator); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(40000n); + }); + + it("liquidating explicit cluster after shared removal preserves implicit-only accounting", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterA = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8201); + const clusterBImplicit = await registerSingleValidatorCluster(clusters, liquidator, operatorIds, 8202); + const clusterAAfterEB64 = await updateClusterEB(clusters, clusterOwner, operatorIds, clusterA, 1, 64); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + await clusters.liquidate(clusterOwner.address, operatorIds, clusterAAfterEB64); + + expect(clusterBImplicit.active).to.equal(true); + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(BPS_DENOMINATOR); + }); + + it("EB decrease on one explicit cluster after shared removal updates only surviving operator slots", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterA = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8301); + const clusterB = await registerSingleValidatorCluster(clusters, liquidator, operatorIds, 8302); + const clusterAAfterEB64 = await updateClusterEB(clusters, clusterOwner, operatorIds, clusterA, 1, 64); + await updateClusterEB(clusters, liquidator, operatorIds, clusterB, 2, 64); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + await updateClusterEB(clusters, clusterOwner, operatorIds, clusterAAfterEB64, 3, 32); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(30000n); + }); + + it("removing the last validator on second explicit cluster after shared removal cleans only that cluster", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterA = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8401); + const clusterB = await registerSingleValidatorCluster(clusters, liquidator, operatorIds, 8402); + await updateClusterEB(clusters, clusterOwner, operatorIds, clusterA, 1, 64); + const clusterBAfterEB64 = await updateClusterEB(clusters, liquidator, operatorIds, clusterB, 2, 64); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + await clusters.connect(liquidator).removeValidator( + makePublicKey(8402), + operatorIds, + clusterBAfterEB64, + ); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + }); + + it("mixed EB increase/decrease after one shared-operator removal keeps per-operator totals exact", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterA = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8501); + const clusterB = await registerSingleValidatorCluster(clusters, liquidator, operatorIds, 8502); + const clusterAAfterEB64 = await updateClusterEB(clusters, clusterOwner, operatorIds, clusterA, 1, 64); + const clusterBAfterEB128 = await updateClusterEB(clusters, liquidator, operatorIds, clusterB, 2, 128); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + + await updateClusterEB(clusters, clusterOwner, operatorIds, clusterAAfterEB64, 3, 128); + await updateClusterEB(clusters, liquidator, operatorIds, clusterBAfterEB128, 4, 32); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(30000n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(50000n); + }); + + it("multiple explicit-EB clusters liquidated end at daoTotalEthVUnits == 0", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterA = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8601); + const clusterB = await registerSingleValidatorCluster(clusters, liquidator, operatorIds, 8602); + const clusterAAfterEB64 = await updateClusterEB(clusters, clusterOwner, operatorIds, clusterA, 1, 64); + const clusterBAfterEB128 = await updateClusterEB(clusters, liquidator, operatorIds, clusterB, 2, 128); + + expect(await clusters.getDaoTotalEthVUnits()).to.equal(60000n); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(40000n); + } + + await clusters.liquidate(clusterOwner.address, operatorIds, clusterAAfterEB64); + await clusters.connect(liquidator).liquidate(liquidator.address, operatorIds, clusterBAfterEB128); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("shared operators across explicit clusters accumulate and clean exactly", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterA = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8701); + const clusterB = await registerSingleValidatorCluster(clusters, liquidator, operatorIds, 8702); + const clusterAAfter64 = await updateClusterEB(clusters, clusterOwner, operatorIds, clusterA, 1, 64); + const clusterBAfter64 = await updateClusterEB(clusters, liquidator, operatorIds, clusterB, 2, 64); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(20000n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(40000n); + + const clusterB64to128 = await updateClusterEB(clusters, liquidator, operatorIds, clusterBAfter64, 3, 128); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(40000n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(60000n); + + await clusters.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, clusterAAfter64); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(30000n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(40000n); + + await clusters.connect(liquidator).liquidate(liquidator.address, operatorIds, clusterB64to128); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("removeOperator clears operator slot but leaves DAO total unchanged through deposit", async function () { + const { clusters, operatorIds } = await loadFixtureFor4(); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const cluster = await registerSingleValidatorCluster(clusters, clusterOwner, operatorIds, 8801); + const clusterAfter64 = await updateClusterEB(clusters, clusterOwner, operatorIds, cluster, 1, 64); + + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + const depositAmount = DEFAULT_ETH_REGISTER_VALUE / 2n; + const depositTx = await clusters.deposit( + clusterOwner.address, + operatorIds, + clusterAfter64, + { value: depositAmount } + ); + parseClusterFromEvent(clusters, await depositTx.wait(), Events.CLUSTER_DEPOSITED); + + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + }); + }); +}); diff --git a/test/sanity/removed-operator.test.ts b/test/sanity/removed-operator.test.ts new file mode 100644 index 000000000..48ef8eedc --- /dev/null +++ b/test/sanity/removed-operator.test.ts @@ -0,0 +1,62 @@ +import type { NetworkConnection } from 'hardhat/types/network'; +import type { NetworkHelpersType } from '../common/types.js'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { ssvNetworkFullFixture } from '../setup/fixtures.js'; +import { getCurrentClusterState, makePublicKey, registerOperators, setupTestContext, whitelistAddresses } from '../common/helpers.js'; +import { + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + SMALL_ETH_REGISTER_VALUE, +} from '../common/constants.js'; +import { expect } from 'chai'; +import { Events } from '../common/events.js'; + +describe("Cluster with a removed operator sanity test", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner, clusterOwner] } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + it("Allows to liquidate cluster with a previously removed operator", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: SMALL_ETH_REGISTER_VALUE } + ); + + const expectedCluster = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + await networkHelpers.mine(100); + await network.connect(operatorOwner).removeOperator(operatorIds[2]); + await networkHelpers.mine(999999999999); + + expect(await views.isLiquidatable(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(true); + + expect(await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, expectedCluster)) + .to.emit(network, Events.CLUSTER_LIQUIDATED); + }); +}); \ No newline at end of file diff --git a/test/sanity/replace-oracle-invalid-id.test.ts b/test/sanity/replace-oracle-invalid-id.test.ts new file mode 100644 index 000000000..4680a83d4 --- /dev/null +++ b/test/sanity/replace-oracle-invalid-id.test.ts @@ -0,0 +1,47 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { getTestConnection } from "../setup/connection.ts"; +import { ssvDAOHarnessFixture } from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { Errors } from "../common/errors.ts"; + +const MAX_DELEGATION_SLOTS = 4; + +describe("replaceOracle() - InvalidOracleId boundary sanity", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let newOracle: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [, newOracle] = await connection.ethers.getSigners(); + }); + + const deployDAOFixture = async () => ssvDAOHarnessFixture(connection); + + it("Accepts oracleId equal to MAX_DELEGATION_SLOTS (boundary)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.replaceOracle(MAX_DELEGATION_SLOTS, newOracle.address); + }); + + it("Is reverted with 'InvalidOracleId' when oracleId is MAX_DELEGATION_SLOTS + 1", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.replaceOracle(MAX_DELEGATION_SLOTS + 1, newOracle.address)).to.be.revertedWithCustomError( + dao, + Errors.INVALID_ORACLE_ID, + ); + }); + + it("Is reverted with 'InvalidOracleId' when oracleId is far above MAX_DELEGATION_SLOTS", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.replaceOracle(100, newOracle.address)).to.be.revertedWithCustomError( + dao, + Errors.INVALID_ORACLE_ID, + ); + }); +}); diff --git a/test/sanity/ssv-staking-dust.test.ts b/test/sanity/ssv-staking-dust.test.ts new file mode 100644 index 000000000..eba89d69d --- /dev/null +++ b/test/sanity/ssv-staking-dust.test.ts @@ -0,0 +1,137 @@ +import type { NetworkConnection } from 'hardhat/types/network'; +import type { NetworkHelpersType } from '../common/types.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { ssvNetworkFullFixture } from '../setup/fixtures.ts'; +import { + registerOperators, + registerDefaultClusters, + buildEBMerkleForDefaultClusters, + updateClusterBalancesForDefaultClusters, + commitEBRoot, + getCurrentClusterState, + setAccountBalance, + setupOracles, + setupTestContext, +} from '../common/helpers.ts'; +import { expect } from 'chai'; + +describe("Dust check in the ssv staking module", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let operatorOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [operatorOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => ssvNetworkFullFixture(connection); + + it("Should not leave any dust after all participants withdraw their funds", async function () { + const { network, views, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployFixture); + + const networkAddress = await network.getAddress(); + + await ssvToken.mint(networkAddress, connection.ethers.parseEther("100")); + await setAccountBalance(connection.ethers.provider, networkAddress, connection.ethers.parseEther("100")); + + const allSigners = await connection.ethers.getSigners(); + const staker = allSigners[2]; + const oracles = allSigners.slice(16, 20); + + await setupOracles(network, ssvToken, staker, oracles); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + + const registered = await registerDefaultClusters(connection, network, operatorIds, operatorOwner, 10); + + const effectiveBalance = 64; + const merkleData = buildEBMerkleForDefaultClusters(connection, registered, effectiveBalance); + + const blockNum = (await connection.ethers.provider.getBlock('latest'))!.number; + + await commitEBRoot(network, merkleData.root, blockNum, oracles); + + await updateClusterBalancesForDefaultClusters(network, registered, merkleData, blockNum, effectiveBalance); + + const stakers = [allSigners[1], allSigners[3], allSigners[4], allSigners[15], allSigners[16]]; + for (let i = 0; i < stakers.length; i++) { + const amount = connection.ethers.parseEther((Math.floor(Math.random() * 1000) + 1).toString()); + await ssvToken.mint(stakers[i].address, amount); + await ssvToken.connect(stakers[i]).approve(networkAddress, amount); + await network.connect(stakers[i]).stake(amount); + } + + const clusterStates = []; + for (const { owner } of registered.clusters) { + clusterStates.push(await getCurrentClusterState(connection, network, owner.address, operatorIds)); + } + + await networkHelpers.mine(100); + + for (const id of operatorIds) { + await network.connect(operatorOwner).withdrawAllOperatorEarnings(id); + } + + const currentNetworkFee = await views.getNetworkFee(); + await network.updateNetworkFee(currentNetworkFee * 2n); + + await networkHelpers.mine(100); + + for (const id of operatorIds) { + await network.connect(operatorOwner).withdrawAllOperatorEarnings(id); + } + + await networkHelpers.mine(100); + + for (let i = 0; i < registered.clusters.length; i++) { + const { owner } = registered.clusters[i]; + await network.connect(owner).liquidate(owner.address, operatorIds, clusterStates[i]); + } + + await networkHelpers.mine(100); + + for (const id of operatorIds) { + await network.connect(operatorOwner).withdrawAllOperatorEarnings(id); + } + + await networkHelpers.mine(100); + + for (const s of stakers) { + await network.connect(s).claimEthRewards(); + const cssvBalance = await cssvToken.balanceOf(s.address); + await network.connect(s).requestUnstake(cssvBalance); + } + + const cooldown = 7 * 24 * 60 * 60 + 1; + await connection.ethers.provider.send("evm_increaseTime", [cooldown]); + await connection.ethers.provider.send("evm_mine", []); + + for (const s of stakers) { + await network.connect(s).withdrawUnlocked(); + } + + const allStakers = [...stakers, staker]; + const unstakers = []; + for (const s of allStakers) { + const cssvBalance = await cssvToken.balanceOf(s.address); + if (cssvBalance > 0n) { + await network.connect(s).claimEthRewards(); + await network.connect(s).requestUnstake(cssvBalance); + unstakers.push(s); + } + } + + await connection.ethers.provider.send("evm_increaseTime", [cooldown]); + await connection.ethers.provider.send("evm_mine", []); + + for (const s of unstakers) { + await network.connect(s).withdrawUnlocked(); + } + + const contractEth = await connection.ethers.provider.getBalance(networkAddress); + const contractSsv = await ssvToken.balanceOf(networkAddress); + + expect(contractEth).to.be.closeTo(connection.ethers.parseEther("100"), connection.ethers.parseEther("0.0000001")); + expect(contractSsv).to.equal(connection.ethers.parseEther("100")); + }); +}); diff --git a/test/sanity/ssv2-frozen-supply-quorum.test.ts b/test/sanity/ssv2-frozen-supply-quorum.test.ts new file mode 100644 index 000000000..54fc7ad59 --- /dev/null +++ b/test/sanity/ssv2-frozen-supply-quorum.test.ts @@ -0,0 +1,113 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { getTestConnection } from "../setup/connection.js"; +import { ssvDAOHarnessFixture } from "../setup/fixtures.js"; +import type { NetworkHelpersType } from "../common/types.js"; +import { Events } from "../common/events.js"; +import { ethers } from "ethers"; + +const totalSupply = ethers.parseEther("1000"); +const numberOfOracles = 4n; + +describe("SSV-2: commitRoot freezes cSSV supply on first vote", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + + const getCommitmentKey = (blockNum: number | bigint, merkleRoot: string) => { + return ethers.keccak256( + ethers.solidityPacked(["uint64", "bytes32"], [blockNum, merkleRoot]) + ); + }; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [owner, oracle1, oracle2, oracle3, oracle4] = await connection.ethers.getSigners(); + }); + + const deployFixture = async () => { + const { dao, cssv } = await ssvDAOHarnessFixture(connection); + await dao.mockSetOracle(1, oracle1.address); + await dao.mockSetOracle(2, oracle2.address); + await dao.mockSetOracle(3, oracle3.address); + await dao.mockSetOracle(4, oracle4.address); + await dao.mockupdateQuorumBps(7500); + return { dao, cssv }; + }; + + it("Freezes supply at first vote and cleans up on commit", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployFixture); + await cssv.mint(owner.address, totalSupply); + + const root = ethers.keccak256(ethers.toUtf8Bytes("root")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(0n); + + await dao.connect(oracle1).commitRoot(root, blockNum); + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(totalSupply); + + await dao.connect(oracle2).commitRoot(root, blockNum); + await dao.connect(oracle3).commitRoot(root, blockNum); + + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(0n); + expect(await dao.getEBRoot(blockNum)).to.equal(root); + }); + + it("Supply increase between votes does not block quorum (liveness attack blocked)", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployFixture); + await cssv.mint(owner.address, totalSupply); + + const root = ethers.keccak256(ethers.toUtf8Bytes("root")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + const frozenWeight = totalSupply / numberOfOracles; + const frozenThreshold = (totalSupply * 7500n) / 10000n; + + await dao.connect(oracle1).commitRoot(root, blockNum); + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(totalSupply); + + await cssv.mint(owner.address, totalSupply); + + const tx2 = await dao.connect(oracle2).commitRoot(root, blockNum); + await expect(tx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, frozenWeight * 2n, frozenThreshold, 2, oracle2.address); + + const tx3 = await dao.connect(oracle3).commitRoot(root, blockNum); + await expect(tx3).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(0n); + }); + + it("Supply decrease between votes does not bypass quorum (safety attack blocked)", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployFixture); + await cssv.mint(owner.address, totalSupply); + + const root = ethers.keccak256(ethers.toUtf8Bytes("supply-decrease")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + const frozenWeight = totalSupply / numberOfOracles; + const frozenThreshold = (totalSupply * 7500n) / 10000n; + + await dao.connect(oracle1).commitRoot(root, blockNum); + + await cssv.burn(owner.address, totalSupply - 10n); + + const tx2 = await dao.connect(oracle2).commitRoot(root, blockNum); + await expect(tx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, frozenWeight * 2n, frozenThreshold, 2, oracle2.address); + + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(totalSupply); + }); +}); diff --git a/test/sanity/ssv3-stale-vunits-liquidation.test.ts b/test/sanity/ssv3-stale-vunits-liquidation.test.ts new file mode 100644 index 000000000..7eea3a905 --- /dev/null +++ b/test/sanity/ssv3-stale-vunits-liquidation.test.ts @@ -0,0 +1,125 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { getTestConnection } from '../setup/connection.js'; +import { ssvValidatorsHarnessFixture } from '../setup/fixtures.js'; +import type { NetworkHelpersType } from '../common/types.js'; +import { createCluster, makePublicKey, parseClusterFromEvent } from '../common/helpers.js'; +import { DEFAULT_SHARES, ETH_DEDUCTED_DIGITS, BPS_DENOMINATOR } from '../common/constants.js'; +import { Events } from '../common/events.js'; +import { Errors } from '../common/errors.js'; +import { ethers } from "ethers"; + +const OPERATOR_FEE = ETH_DEDUCTED_DIGITS; + +const MINIMUM_BLOCKS = 1000n; +const START_V_UNITS = 2n * BPS_DENOMINATOR; + +describe("SSV-3: bulkRegisterValidator uses post-registration vUnits for liquidation check", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [clusterOwner] = await connection.ethers.getSigners(); + }); + + const getClusterId = (ownerAddress: string, operatorIds: bigint[]): string => { + return ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [ownerAddress, operatorIds]) + ); + }; + + const deployWithFeeAndParams = async () => { + const result = await ssvValidatorsHarnessFixture(connection, 4, OPERATOR_FEE); + const { validators } = result; + + await validators.mockEthNetworkFee(0n); + await validators.mockMinimumBlocksBeforeLiquidation(MINIMUM_BLOCKS); + await validators.mockMinimumLiquidationCollateral(0n); + + return result; + }; + + it("Reverts with InsufficientBalance when deposit covers old vUnits but not post-registration vUnits", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deployWithFeeAndParams); + + const initialDeposit = 1_000_000_000n; + const regTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: initialDeposit } + ); + const regReceipt = await regTx.wait(); + const existingCluster = parseClusterFromEvent(validators, regReceipt, Events.VALIDATOR_ADDED); + + const clusterId = getClusterId(clusterOwner.address, operatorIds); + await validators.mockSetClusterVUnits(clusterId, START_V_UNITS); + + await expect( + validators.bulkRegisterValidator( + [makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES], + existingCluster, + { value: 0n } + ) + ).to.be.revertedWithCustomError(validators, Errors.INSUFFICIENT_BALANCE); + }); + + it("Succeeds when deposit is sufficient for post-registration vUnits", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deployWithFeeAndParams); + + const initialDeposit = 2_000_000_000n; + const regTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: initialDeposit } + ); + const regReceipt = await regTx.wait(); + const existingCluster = parseClusterFromEvent(validators, regReceipt, Events.VALIDATOR_ADDED); + + const clusterId = getClusterId(clusterOwner.address, operatorIds); + await validators.mockSetClusterVUnits(clusterId, START_V_UNITS); + + await expect( + validators.bulkRegisterValidator( + [makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES], + existingCluster, + { value: 0n } + ) + ).to.emit(validators, Events.VALIDATOR_ADDED); + }); + + it("Implicit EB clusters (vUnits == 0 in storage) are unaffected", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deployWithFeeAndParams); + + const initialDeposit = 2_000_000_000n; + const regTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: initialDeposit } + ); + const regReceipt = await regTx.wait(); + const existingCluster = parseClusterFromEvent(validators, regReceipt, Events.VALIDATOR_ADDED); + + await expect( + validators.bulkRegisterValidator( + [makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES], + existingCluster, + { value: 0n } + ) + ).to.emit(validators, Events.VALIDATOR_ADDED); + }); +}); diff --git a/test/sanity/stale-snapshot-replay-matrix.test.ts b/test/sanity/stale-snapshot-replay-matrix.test.ts new file mode 100644 index 000000000..c3b66b2df --- /dev/null +++ b/test/sanity/stale-snapshot-replay-matrix.test.ts @@ -0,0 +1,163 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { + setupTestContext, + computeClusterId, + computeEBRoot, + createCluster, + makePublicKey, + parseClusterFromEvent, +} from "../common/helpers.ts"; +import { DEFAULT_SHARES, DEFAULT_ETH_REGISTER_VALUE, ETH_DEDUCTED_DIGITS } from "../common/constants.ts"; +import { Events } from "../common/events.ts"; +import { Errors } from "../common/errors.ts"; + +const OPERATOR_FEE_WEI = 100_000n * ETH_DEDUCTED_DIGITS; + +describe("stale snapshot replay matrix", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE_WEI); + + async function registerValidator(clusters: any, operatorIds: bigint[], pubKeySeed: number, cluster = createCluster()) { + const tx = await clusters.connect(clusterOwner).registerValidator( + makePublicKey(pubKeySeed), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + return parseClusterFromEvent(clusters, await tx.wait(), Events.VALIDATOR_ADDED); + } + + async function updateEB(clusters: any, operatorIds: bigint[], cluster: any, effectiveBalance: number, blockNum: number) { + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await clusters.mockSetEBRoot(blockNum, computeEBRoot(clusterId, effectiveBalance)); + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + return parseClusterFromEvent(clusters, await tx.wait(), Events.CLUSTER_BALANCE_UPDATED); + } + + it("stale caller-supplied cluster is rejected on repeated updateClusterBalance", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + await clusters.mockEthNetworkFee(0n); + + const clusterAfterRegister = await registerValidator(clusters, operatorIds, 1001); + const staleCluster = { ...clusterAfterRegister }; + const clusterAfter64 = await updateEB(clusters, operatorIds, clusterAfterRegister, 64, 1); + expect(clusterAfter64.active).to.equal(true); + + await clusters.mockSetEBRoot(2, computeEBRoot(computeClusterId(clusterOwner.address, operatorIds), 128)); + await expect( + clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, staleCluster, 128, []) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("liquidation rejects stale pre-mutation cluster snapshot", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerValidator(clusters, operatorIds, 1002); + const clusterAfter64 = await updateEB(clusters, operatorIds, clusterAfterRegister, 64, 1); + const stalePreMutation = { ...clusterAfter64 }; + + const depositTx = await clusters.deposit(clusterOwner.address, operatorIds, clusterAfter64, { value: DEFAULT_ETH_REGISTER_VALUE }); + parseClusterFromEvent(clusters, await depositTx.wait(), Events.CLUSTER_DEPOSITED); + + await expect( + clusters.liquidate(clusterOwner.address, operatorIds, stalePreMutation) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("removeValidator rejects stale snapshot after a successful fresh removal", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + await clusters.mockEthNetworkFee(0n); + + const clusterAfterReg1 = await registerValidator(clusters, operatorIds, 1003); + const clusterAfterReg2 = await registerValidator(clusters, operatorIds, 1004, clusterAfterReg1); + const staleBeforeFirstRemove = { ...clusterAfterReg2 }; + + const removeFirstTx = await clusters.removeValidator(makePublicKey(1003), operatorIds, clusterAfterReg2); + parseClusterFromEvent(clusters, await removeFirstTx.wait(), Events.VALIDATOR_REMOVED); + + await expect( + clusters.removeValidator(makePublicKey(1004), operatorIds, staleBeforeFirstRemove) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("reactivate rejects stale active snapshot after liquidation", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerValidator(clusters, operatorIds, 1005); + const clusterAfter64 = await updateEB(clusters, operatorIds, clusterAfterRegister, 64, 1); + const staleActiveCluster = { ...clusterAfter64 }; + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfter64); + parseClusterFromEvent(clusters, await liquidateTx.wait(), Events.CLUSTER_LIQUIDATED); + + await expect( + clusters.reactivate(operatorIds, staleActiveCluster, { value: DEFAULT_ETH_REGISTER_VALUE }) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("removeValidator rejects stale EB=64 snapshot after EB=128 update", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + await clusters.mockEthNetworkFee(0n); + + const validatorPubKey = makePublicKey(1006); + const registerTx = await clusters.registerValidator( + validatorPubKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const clusterAfterRegister = parseClusterFromEvent(clusters, await registerTx.wait(), Events.VALIDATOR_ADDED); + + const clusterAfter64 = await updateEB(clusters, operatorIds, clusterAfterRegister, 64, 1); + const staleEb64Cluster = { ...clusterAfter64 }; + await updateEB(clusters, operatorIds, clusterAfter64, 128, 2); + + await expect( + clusters.removeValidator(validatorPubKey, operatorIds, staleEb64Cluster) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("liquidate replay with stale pre-liquidation snapshot fails cleanly", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerValidator(clusters, operatorIds, 1007); + const clusterAfter64 = await updateEB(clusters, operatorIds, clusterAfterRegister, 64, 1); + const stalePreLiquidation = { ...clusterAfter64 }; + + const firstLiqTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfter64); + parseClusterFromEvent(clusters, await firstLiqTx.wait(), Events.CLUSTER_LIQUIDATED); + + await expect( + clusters.liquidate(clusterOwner.address, operatorIds, stalePreLiquidation) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); +}); diff --git a/test/sanity/vunits-cluster-size-matrix.test.ts b/test/sanity/vunits-cluster-size-matrix.test.ts new file mode 100644 index 000000000..a21c54049 --- /dev/null +++ b/test/sanity/vunits-cluster-size-matrix.test.ts @@ -0,0 +1,409 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { + setupTestContext, + computeClusterId, + computeEBRoot, + createCluster, + makePublicKey, + parseClusterFromEvent, +} from "../common/helpers.ts"; +import { DEFAULT_SHARES, DEFAULT_ETH_REGISTER_VALUE, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../common/constants.ts"; +import { Events } from "../common/events.ts"; +import { calcClusterBurn, defaultVUnits } from "../helpers/index.ts"; + +const OPERATOR_FEE_RAW = 100_000n; +const OPERATOR_FEE_WEI = OPERATOR_FEE_RAW * ETH_DEDUCTED_DIGITS; + +describe("vUnits cluster-size matrix coverage", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let ownerA: HardhatEthersSigner; + let ownerB: HardhatEthersSigner; + let ownerC: HardhatEthersSigner; + let ownerD: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + [ownerA, ownerB, ownerC, ownerD] = await connection.ethers.getSigners(); + }); + + const deployWith13Operators = async () => ssvClustersHarnessFixture(connection, 13, OPERATOR_FEE_WEI); + + const ops = (operatorIds: bigint[], size: 4 | 7 | 10 | 13) => operatorIds.slice(0, size); + + async function registerSingleValidator( + clusters: any, + owner: HardhatEthersSigner, + operatorIds: bigint[], + publicKeySeed: number, + cluster = createCluster(), + depositValue = DEFAULT_ETH_REGISTER_VALUE, + ) { + const tx = await clusters.connect(owner).registerValidator( + makePublicKey(publicKeySeed), + operatorIds, + DEFAULT_SHARES, + cluster, + { value: depositValue } + ); + return parseClusterFromEvent(clusters, await tx.wait(), Events.VALIDATOR_ADDED); + } + + async function updateEB( + clusters: any, + owner: HardhatEthersSigner, + operatorIds: bigint[], + cluster: any, + effectiveBalance: number, + blockNum: number, + ) { + const clusterId = computeClusterId(owner.address, operatorIds); + await clusters.mockSetEBRoot(blockNum, computeEBRoot(clusterId, effectiveBalance)); + const tx = await clusters.connect(owner).updateClusterBalance( + blockNum, + owner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + return parseClusterFromEvent(clusters, await tx.wait(), Events.CLUSTER_BALANCE_UPDATED); + } + + it("fee accrual scales with 7/10/13 operators", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + + for (const size of [7, 10, 13] as const) { + const operatorSet = ops(operatorIds, size); + const regTx = await clusters.connect(ownerA).registerValidator( + makePublicKey(1000 + size), + operatorSet, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + const regBlock = BigInt(regReceipt!.blockNumber); + + await networkHelpers.mine(12); + const withdrawTx = await clusters.connect(ownerA).withdraw(operatorSet, 0n, clusterAfterReg); + const withdrawReceipt = await withdrawTx.wait(); + const withdrawBlock = BigInt(withdrawReceipt!.blockNumber); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const expectedBurn = calcClusterBurn({ + blockDiff: withdrawBlock - regBlock, + numOperators: BigInt(size), + ethFee: OPERATOR_FEE_RAW, + networkFee: 0n, + effectiveVUnits: defaultVUnits(1n), + }); + + expect(clusterAfterReg.balance - clusterAfterWithdraw.balance).to.equal(expectedBurn); + } + }); + + it("per-operator counts and EB=64 deviation distribution are exact for 7/13 operators", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + + const operatorSet13 = ops(operatorIds, 13); + const cluster13 = await registerSingleValidator(clusters, ownerA, operatorSet13, 2013); + for (const operatorId of operatorSet13) { + expect(await clusters.getOperatorEthValidatorCount(operatorId)).to.equal(1n); + } + + const operatorSet7 = ops(operatorIds, 7); + const cluster7 = await registerSingleValidator(clusters, ownerB, operatorSet7, 2007); + + await updateEB(clusters, ownerB, operatorSet7, cluster7, 64, 1); + await updateEB(clusters, ownerA, operatorSet13, cluster13, 64, 2); + + for (const operatorId of operatorSet7) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(20000n); + } + for (const operatorId of operatorSet13.slice(7)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + }); + + it("EB transitions on 7/13 operators apply exact per-operator deltas", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + + const operatorSet7 = ops(operatorIds, 7); + const cluster7 = await registerSingleValidator(clusters, ownerA, operatorSet7, 3007); + const cluster7After64 = await updateEB(clusters, ownerA, operatorSet7, cluster7, 64, 1); + await updateEB(clusters, ownerA, operatorSet7, cluster7After64, 32, 2); + for (const operatorId of operatorSet7) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + + const operatorSet13 = ops(operatorIds, 13); + const cluster13 = await registerSingleValidator(clusters, ownerB, operatorSet13, 3013); + const cluster13After64 = await updateEB(clusters, ownerB, operatorSet13, cluster13, 64, 3); + await updateEB(clusters, ownerB, operatorSet13, cluster13After64, 128, 4); + for (const operatorId of operatorSet13) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(30000n); + } + }); + + it("explicit-EB liquidation/reactivation restores deviations for 7/13 operators", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const operatorSet7 = ops(operatorIds, 7); + const cluster7 = await registerSingleValidator(clusters, ownerA, operatorSet7, 4007); + const cluster7After64 = await updateEB(clusters, ownerA, operatorSet7, cluster7, 64, 1); + const liqTx7 = await clusters.connect(ownerA).liquidate(ownerA.address, operatorSet7, cluster7After64); + const liqCluster7 = parseClusterFromEvent(clusters, await liqTx7.wait(), Events.CLUSTER_LIQUIDATED); + await clusters.connect(ownerA).reactivate(operatorSet7, liqCluster7, { value: DEFAULT_ETH_REGISTER_VALUE }); + for (const operatorId of operatorSet7) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + + const { clusters: clusters2, operatorIds: operatorIds2 } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters2.mockEthNetworkFee(0n); + await clusters2.mockMinimumBlocksBeforeLiquidation(1n); + await clusters2.mockMinimumLiquidationCollateral(0n); + const operatorSet13 = ops(operatorIds2, 13); + const cluster13 = await registerSingleValidator(clusters2, ownerB, operatorSet13, 4013); + const cluster13After64 = await updateEB(clusters2, ownerB, operatorSet13, cluster13, 64, 1); + const liqTx13 = await clusters2.connect(ownerB).liquidate(ownerB.address, operatorSet13, cluster13After64); + const liqCluster13 = parseClusterFromEvent(clusters2, await liqTx13.wait(), Events.CLUSTER_LIQUIDATED); + await clusters2.connect(ownerB).reactivate(operatorSet13, liqCluster13, { value: DEFAULT_ETH_REGISTER_VALUE }); + for (const operatorId of operatorSet13) { + expect(await clusters2.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + }); + + it("post-liquidation operator removals are skipped on reactivation for 7/13 operators", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const operatorSet7 = ops(operatorIds, 7); + const cluster7 = await registerSingleValidator(clusters, ownerA, operatorSet7, 5007); + const cluster7After64 = await updateEB(clusters, ownerA, operatorSet7, cluster7, 64, 1); + const liqTx7 = await clusters.connect(ownerA).liquidate(ownerA.address, operatorSet7, cluster7After64); + const liqCluster7 = parseClusterFromEvent(clusters, await liqTx7.wait(), Events.CLUSTER_LIQUIDATED); + await clusters.mockRemoveOperator(operatorSet7[0]); + await clusters.mockRemoveOperator(operatorSet7[1]); + await clusters.connect(ownerA).reactivate(operatorSet7, liqCluster7, { value: DEFAULT_ETH_REGISTER_VALUE }); + expect(await clusters.getOperatorEthVUnits(operatorSet7[0])).to.equal(0n); + expect(await clusters.getOperatorEthVUnits(operatorSet7[1])).to.equal(0n); + + const { clusters: clusters2, operatorIds: operatorIds2 } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters2.mockEthNetworkFee(0n); + await clusters2.mockMinimumBlocksBeforeLiquidation(1n); + await clusters2.mockMinimumLiquidationCollateral(0n); + const operatorSet13 = ops(operatorIds2, 13); + const cluster13 = await registerSingleValidator(clusters2, ownerB, operatorSet13, 5013); + const cluster13After64 = await updateEB(clusters2, ownerB, operatorSet13, cluster13, 64, 1); + const liqTx13 = await clusters2.connect(ownerB).liquidate(ownerB.address, operatorSet13, cluster13After64); + const liqCluster13 = parseClusterFromEvent(clusters2, await liqTx13.wait(), Events.CLUSTER_LIQUIDATED); + for (const removedId of operatorSet13.slice(0, 6)) { + await clusters2.mockRemoveOperator(removedId); + } + await clusters2.connect(ownerB).reactivate(operatorSet13, liqCluster13, { value: DEFAULT_ETH_REGISTER_VALUE }); + for (const removedId of operatorSet13.slice(0, 6)) { + expect(await clusters2.getOperatorEthVUnits(removedId)).to.equal(0n); + } + }); + + it("migration with explicit EB=64 works for 7/13 operators", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const operatorSet7 = ops(operatorIds, 7); + await clusters.mockRegisterSSVValidator(makePublicKey(6007), operatorSet7, ownerA.address, ssvCluster); + const clusterId7 = computeClusterId(ownerA.address, operatorSet7); + await clusters.mockSetClusterVUnits(clusterId7, 20000n); + await clusters.connect(ownerA).migrateClusterToETH(operatorSet7, ssvCluster, { value: DEFAULT_ETH_REGISTER_VALUE }); + for (const operatorId of operatorSet7) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + + const { clusters: clusters2, operatorIds: operatorIds2 } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters2.mockEthNetworkFee(0n); + const operatorSet13 = ops(operatorIds2, 13); + await clusters2.mockRegisterSSVValidator(makePublicKey(6013), operatorSet13, ownerB.address, ssvCluster); + const clusterId13 = computeClusterId(ownerB.address, operatorSet13); + await clusters2.mockSetClusterVUnits(clusterId13, 20000n); + await clusters2.connect(ownerB).migrateClusterToETH(operatorSet13, ssvCluster, { value: DEFAULT_ETH_REGISTER_VALUE }); + for (const operatorId of operatorSet13) { + expect(await clusters2.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + }); + + it("mixed-size shared-operator interactions keep per-cluster accounting isolated", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const operatorSet4 = ops(operatorIds, 4); + const operatorSet7 = ops(operatorIds, 7); + const operatorSet13 = ops(operatorIds, 13); + + const cluster4 = await registerSingleValidator(clusters, ownerA, operatorSet4, 7004); + const cluster13 = await registerSingleValidator(clusters, ownerB, operatorSet13, 7013); + const cluster7 = await registerSingleValidator(clusters, ownerC, operatorSet7, 7007); + const cluster7ForLiquidation = await registerSingleValidator(clusters, ownerD, operatorSet7, 7107); + const cluster4After64 = await updateEB(clusters, ownerA, operatorSet4, cluster4, 64, 1); + const cluster13After64 = await updateEB(clusters, ownerB, operatorSet13, cluster13, 64, 2); + const cluster7After128 = await updateEB(clusters, ownerC, operatorSet7, cluster7, 128, 3); + const cluster7ForLiquidationAfter64 = await updateEB(clusters, ownerD, operatorSet7, cluster7ForLiquidation, 64, 4); + + const eb64Deviation = 10_000n; + const eb128Deviation = 30_000n; + const expectedSharedOperatorVUnitsBeforeLiquidation = + eb64Deviation + eb64Deviation + eb128Deviation + eb64Deviation; + const expectedSharedOperatorVUnitsAfterCluster4Liquidation = + expectedSharedOperatorVUnitsBeforeLiquidation - eb64Deviation; + const expectedNonCluster4OperatorVUnitsAfterCluster4Liquidation = + eb64Deviation + eb128Deviation + eb64Deviation; + + await clusters.mockRemoveOperator(operatorSet4[0]); + expect(await clusters.getOperatorEthVUnits(operatorSet4[1])).to.equal(expectedSharedOperatorVUnitsBeforeLiquidation); + expect(await clusters.getOperatorEthVUnits(operatorSet13[12])).to.equal(eb64Deviation); + + await clusters.connect(ownerA).liquidate(ownerA.address, operatorSet4, cluster4After64); + for (const operatorId of operatorSet7.slice(1, 4)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedSharedOperatorVUnitsAfterCluster4Liquidation); + } + for (const operatorId of operatorSet7.slice(4)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedNonCluster4OperatorVUnitsAfterCluster4Liquidation); + } + + await clusters.connect(ownerD).liquidate(ownerD.address, operatorSet7, cluster7ForLiquidationAfter64); + await clusters.connect(ownerB).liquidate(ownerB.address, operatorSet13, cluster13After64); + await clusters.connect(ownerC).liquidate(ownerC.address, operatorSet7, cluster7After128); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("DAO invariant holds across 4/7/10/13 clusters with and without removals", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const operatorSet4 = ops(operatorIds, 4); + const operatorSet7 = ops(operatorIds, 7); + const operatorSet10 = ops(operatorIds, 10); + const operatorSet13 = ops(operatorIds, 13); + + const cluster4 = await updateEB( + clusters, + ownerA, + operatorSet4, + await registerSingleValidator(clusters, ownerA, operatorSet4, 8004), + 64, + 1 + ); + const cluster7 = await updateEB( + clusters, + ownerB, + operatorSet7, + await registerSingleValidator(clusters, ownerB, operatorSet7, 8007), + 64, + 2 + ); + const cluster10 = await updateEB( + clusters, + ownerC, + operatorSet10, + await registerSingleValidator(clusters, ownerC, operatorSet10, 8010), + 64, + 3 + ); + const cluster13 = await updateEB( + clusters, + ownerD, + operatorSet13, + await registerSingleValidator(clusters, ownerD, operatorSet13, 8013), + 64, + 4 + ); + + expect(await clusters.getDaoTotalEthVUnits()).to.equal(80000n); + + await clusters.connect(ownerA).liquidate(ownerA.address, operatorSet4, cluster4); + await clusters.connect(ownerB).liquidate(ownerB.address, operatorSet7, cluster7); + await clusters.connect(ownerC).liquidate(ownerC.address, operatorSet10, cluster10); + await clusters.connect(ownerD).liquidate(ownerD.address, operatorSet13, cluster13); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + + const { clusters: clusters2, operatorIds: operatorIds2 } = await networkHelpers.loadFixture(deployWith13Operators); + await clusters2.mockEthNetworkFee(0n); + await clusters2.mockMinimumBlocksBeforeLiquidation(1n); + await clusters2.mockMinimumLiquidationCollateral(0n); + + const operatorSet4b = ops(operatorIds2, 4); + const operatorSet7b = ops(operatorIds2, 7); + const operatorSet10b = ops(operatorIds2, 10); + const operatorSet13b = ops(operatorIds2, 13); + + const cluster4b = await updateEB( + clusters2, + ownerA, + operatorSet4b, + await registerSingleValidator(clusters2, ownerA, operatorSet4b, 8104), + 64, + 1 + ); + const cluster7b = await updateEB( + clusters2, + ownerB, + operatorSet7b, + await registerSingleValidator(clusters2, ownerB, operatorSet7b, 8107), + 64, + 2 + ); + const cluster10b = await updateEB( + clusters2, + ownerC, + operatorSet10b, + await registerSingleValidator(clusters2, ownerC, operatorSet10b, 8110), + 64, + 3 + ); + const cluster13b = await updateEB( + clusters2, + ownerD, + operatorSet13b, + await registerSingleValidator(clusters2, ownerD, operatorSet13b, 8113), + 64, + 4 + ); + + await clusters2.mockRemoveOperator(operatorSet4b[0]); + await clusters2.mockRemoveOperator(operatorSet7b[4]); + await clusters2.mockRemoveOperator(operatorSet10b[7]); + await clusters2.mockRemoveOperator(operatorSet13b[10]); + + await clusters2.connect(ownerA).liquidate(ownerA.address, operatorSet4b, cluster4b); + await clusters2.connect(ownerB).liquidate(ownerB.address, operatorSet7b, cluster7b); + await clusters2.connect(ownerC).liquidate(ownerC.address, operatorSet10b, cluster10b); + await clusters2.connect(ownerD).liquidate(ownerD.address, operatorSet13b, cluster13b); + + expect(await clusters2.getDaoTotalEthVUnits()).to.equal(0n); + }); +}); diff --git a/test/setup/artifacts/SSVClustersLegacy.json b/test/setup/artifacts/SSVClustersLegacy.json new file mode 100644 index 000000000..c96d30abe --- /dev/null +++ b/test/setup/artifacts/SSVClustersLegacy.json @@ -0,0 +1,1116 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "SSVClusters", + "sourceName": "contracts/modules/SSVClusters.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterReactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "shares", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorRemoved", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "bulkExitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes[]", + "name": "sharesData", + "type": "bytes[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRegisterValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRemoveValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "exitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "liquidate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "reactivate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes", + "name": "sharesData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "registerValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "removeValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60808060405234610016576133c5908161001b8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c806306e8fb9c146100b457806312b3fc19146100af57806322f18bf5146100aa57806332afd02f146100a55780633877322b146100a05780635aed11421461009b5780635fec6dd014610096578063686e682c14610091578063bc26e7e51461008c5763bf0f2fb214610087575f80fd5b6110cc565b610fd9565b610d9a565b610bd5565b610998565b610895565b610740565b610649565b6103b5565b3461013057610120366003190112610130576001600160401b03600435818111610130576100e6903690600401610134565b90602435838111610130576100ff903690600401610251565b6044359384116101305761011a61012e943690600401610134565b9161012436610279565b9460643594611331565b005b5f80fd5b9181601f84011215610130578235916001600160401b038311610130576020838186019501011161013057565b634e487b7160e01b5f52604160045260245ffd5b60a081019081106001600160401b0382111761019057604052565b610161565b606081019081106001600160401b0382111761019057604052565b90601f801991011681019081106001600160401b0382111761019057604052565b6001600160401b0381116101905760051b60200190565b6001600160401b0381160361013057565b9291610204826101d1565b9161021260405193846101b0565b829481845260208094019160051b810192831161013057905b8282106102385750505050565b8380918335610246816101e8565b81520191019061022b565b9080601f830112156101305781602061026c933591016101f9565b90565b8015150361013057565b60a0906083190112610130576040519061029282610175565b8160843563ffffffff8116810361013057815260a4356102b1816101e8565b602082015260c4356102c2816101e8565b604082015260e4356102d38161026f565b6060820152608061010435910152565b60a090604319011261013057604051906102fc82610175565b8160443563ffffffff8116810361013057815260643561031b816101e8565b602082015260843561032c816101e8565b604082015260a43561033d8161026f565b6060820152608060c435910152565b60a0906063190112610130576040519061036582610175565b8160643563ffffffff81168103610130578152608435610384816101e8565b602082015260a435610395816101e8565b604082015260c4356103a68161026f565b6060820152608060e435910152565b346101305760e0366003190112610130576001600160401b03600435818111610130576103e6903690600401610134565b909160243590811161013057610400903690600401610251565b9061040a366102e3565b9261041683338661255b565b61041f846125e7565b604051602081019061044581610437338989876118e6565b03601f1981018352826101b0565b5190209061047a825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b5480156105aa576001191603610588579461055a915f6104e37fccf4370403e5fbbde0cd3f13426479dcd8a5916b05db424b7a2c04978cf8ce6e97985f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b55606082015161055f575b610511610507610502845163ffffffff1690565b61191a565b63ffffffff168352565b61054b61051d83612cf7565b915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f2090565b5560405193849333978561192c565b0390a2005b61057b61056b88612616565b50610574612939565b908461297e565b6105836129a4565b6104ee565b50506105a66040519283926311260f2760e31b845260048401611909565b0390fd5b60046040517fe51315d2000000000000000000000000000000000000000000000000000000008152fd5b9291926001600160401b03821161019057604051916105fd601f8201601f1916602001846101b0565b829481845281830111610130578281602093845f960137010152565b9181601f84011215610130578235916001600160401b038311610130576020808501948460051b01011161013057565b3461013057610120366003190112610130576001600160401b036004358181116101305736602382011215610130578060040135602491610689826101d1565b9261069760405194856101b0565b8284526020926024602086019160051b840101923684116101305760248101915b848310610709578787602435828111610130576106d9903690600401610251565b90604435928311610130576106f561012e933690600401610619565b906106ff36610279565b9360643593611a83565b8235888111610130578201366043820112156101305786916107358392369060448982013591016105d4565b8152019201916106b8565b34610130576040806003193601126101305760046001600160401b03813581811161013057610773903690600401610619565b90916024359081116101305761078d903690600401610619565b91909281156105aa576107a96107a43685876101f9565b6125e7565b945f5b8381106107b557005b61082061081c886108138b6104376107e36107d1888c8c6119be565b935192839160208301953391876118e6565b5190205f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b54600119161490565b1590565b61086f5780867fb4b20ffb2eb1f020be3df600b2287914f50c07003526d3a9d89a9dd12351828c61085460019488886119be565b906108668d519283928c339785611c19565b0390a2016107ac565b61087e6105a691858a956119be565b9093519384936311260f2760e31b85528401611909565b34610130576040366003190112610130576001600160401b03600435818111610130576108c6903690600401610134565b91602435908111610130576108df903690600401610619565b60409291925160208101906108fa81610437338988876118e6565b5190205f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205261094460405f20546109396107a43685886101f9565b600119909116141590565b61097c5761055a7fb4b20ffb2eb1f020be3df600b2287914f50c07003526d3a9d89a9dd12351828c9394604051938493339785611c19565b6040516311260f2760e31b8152806105a6868560048401611909565b346101305760e03660031901126101305760046001600160401b038135818111610130576109ca903690600401610619565b9091602435908111610130576109e4903690600401610251565b906109ee366102e3565b9381156105aa57610a0083338761255b565b90610a0a846125e7565b5f915f915b858310610ada57505050610a3f610a4991610a2d6060890151151590565b610aaf575b875163ffffffff16611c55565b63ffffffff168652565b610a5561051d86612cf7565b555f5b818110610a6157005b80857fccf4370403e5fbbde0cd3f13426479dcd8a5916b05db424b7a2c04978cf8ce6e610a9160019486896119be565b610aa660409492945192839233968b8561192c565b0390a201610a58565b610acc610abc8288612704565b50610ac5612939565b908a61297e565b610ad581612a99565b610a32565b909192610ae884878a6119be565b610437610b03604093845192839160208301953391876118e6565b51902090610b3f61081c85610813855f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b610b8b57506001915f610b7c610b82935f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b55611c40565b93019190610a0f565b846105a661087e888b8e6119be565b9060e0600319830112610130576004356001600160401b0381116101305782610bc591600401610619565b9290929161026c602435926102e3565b3461013057610be336610b9a565b9091610bfa610bf33683876101f9565b338461255b565b9260608301610c098151151590565b610d7057610c52610c62610cec92610c38610c28885163ffffffff1690565b610c3336898d6101f9565b6127e6565b93909160808901610c4a8882516112c8565b905260019052565b6001600160401b03166040870152565b610c7d610c6d612939565b6001600160401b03166020870152565b610c93610c8e865163ffffffff1690565b612b6a565b5f805160206133708339815191525460801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c169187612d97565b610d46577fc803f8c01343fcdaf32068f4c283951623ef2b3fa0c547551931356f456b685993610d1e61051d85612cf7565b5580610d37575b5061055a604051928392339684611c6b565b610d409061248c565b5f610d25565b60046040517ff4d678b8000000000000000000000000000000000000000000000000000000008152fd5b60046040517f3babafd2000000000000000000000000000000000000000000000000000000008152fd5b3461013057610da836610b9a565b91610dbe610db73683876101f9565b338561255b565b610dc784612e17565b5f60608501610dd68151151590565b610ede575b608086019085825110610d465781610df787610dff9451611cf2565b905251151590565b9081610ec0575b81610e5c575b50610d46578361055a91610e4361051d7f39d1320bbda24947e77f3560661323384aa0a1cb9d5e040e617e5cbf50b6dbe097612cf7565b55610e4e8433612e4c565b604051938493339785611cff565b5f8051602061337083398151915254610eba925060801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c169188612d97565b5f610e0c565b905063ffffffff610ed5865163ffffffff1690565b16151590610e06565b5f806001600160401b03804316905b878b818510610f13575050505050610f0e90610f07612939565b908861297e565b610ddb565b94610fb4610f6c610f36610f3188610fba96600198999a9d9b611c86565b611c96565b6001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b91610fae600284015487610fa4610f9d610f8c63ffffffff85168d611ca0565b975460201c6001600160401b031690565b8097611cb9565b9160201c16611cd7565b90611cd7565b95611cd7565b95019190610eed565b600435906001600160a01b038216820361013057565b346101305761010036600319011261013057610ff3610fc3565b6024356001600160401b03811161013057611012903690600401610619565b604492919235916110223661034c565b926110386110313685886101f9565b838661255b565b9360808101918251948186018096116110c7577f2bac1912f2481d12f0df08647c06bee174967c62d3a03cbc078eb215dc1bd9a2966001600160a01b039661055a955261108484612cf7565b905f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f20556110b98261248c565b604051958695169785611cff565b6112a6565b346101305760e0366003190112610130576110e5610fc3565b6001600160401b039060243582811161013057611106903690600401610619565b90611110366102e3565b61112561111e3685856101f9565b858361255b565b9061112f81612e17565b611164611153611143835163ffffffff1690565b61114e3688886101f9565b612704565b919061115d612939565b9084612ecc565b6001600160a01b035f961696338814159182611244575b505061121a577f1fce24c373e07f89214e9187598635036111dbb363e99f4ce498488cdc66e6889461055a926111bd6111b8845163ffffffff1690565b612a99565b6080830180518061120d575b50505f60408401525f60208401525f60608401526111e961051d84612cf7565b55806111fd575b5060405193849384611c6b565b6112079033612e4c565b5f6111f0565b5f91935092525f806111c9565b60046040517f60300a8d000000000000000000000000000000000000000000000000000000008152fd5b5f805160206133708339815191525461129f935061081c92919060801c6001600160401b03167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165492808460801c169360401c169186612d97565b5f8061117b565b634e487b7160e01b5f52601160045260245ffd5b90600182018092116110c757565b919082018092116110c757565b9081518082526020808093019301915f5b8281106112f4575050505090565b83516001600160401b0316855293810193928101926001016112e6565b908060209392818452848401375f828201840152601f01601f1916010190565b94919392909261134085611d63565b6113548561134f36878a6105d4565b611e3a565b61135e8588611f88565b60808089018051908682018092116110c7575286515f9182919082905f19825b8c8582106114fc57505050505050906113a36114279261139c612939565b908c61297e565b6113ab612c61565b6113ce6113c46113bf8c5163ffffffff1690565b6120b8565b63ffffffff168b52565b5f805160206133708339815191525460801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c16918c612d97565b610d46577f48a3ea0796746043948f6341d17ff8200937b99262a0b48c2663b951ed7114e5966114e59561149e956114909361146561051d8d612cf7565b55806114ed575b5061148260405198610100808b528a01906112d5565b9188830360208a0152611311565b918583036040870152611311565b9360608301906080809163ffffffff815116845260208101516001600160401b03809116602086015260408201511660408501526060810151151560608501520151910152565b8033930390a2565b6114f69061248c565b5f61146c565b61150982611516926119a5565b516001600160401b031690565b958d86611522846112ba565b1061186e575b5061156b611566886001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b612f59565b908482019861158c6115838b5163ffffffff90511690565b63ffffffff1690565b15611844576060830151611681575b6115a4836131af565b6115b56113bf845163ffffffff1690565b63ffffffff81811685525f80516020613370833981519152546115dc9060601c8216611583565b911611611660579161165561161e61165a93610fae6001979661160e602091610fae838901516001600160401b031690565b9e5101516001600160401b031690565b996001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b612fdc565b0161137e565b60405163639f585160e01b81526001600160401b038a166004820152602490fd5b66ffffffffffffff8960081c168581036117eb575b50600160ff8a161b871661159b576116ee6116e18a6001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b546001600160a01b031690565b6001600160a01b0381169081156117ca5733820361170e575b505061159b565b61081c61171a91613224565b90811561174f575b5061172e575f80611707565b604051635bfa94ff60e11b81526001600160401b038a166004820152602490fd5b6040516320c18e6b60e21b81523360048201526001600160401b038c16602482015260209250908290829060449082905afa9182156117c5575f92611798575b5050155f611722565b6117b79250803d106117be575b6117af81836101b0565b81019061246c565b5f8061178f565b503d6117a5565b612481565b604051635bfa94ff60e11b81526001600160401b038c166004820152602490fd5b80975061183b91955061182e336001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b905f5260205260405f2090565b5495935f611696565b60046040517f961e3e8c000000000000000000000000000000000000000000000000000000008152fd5b61188761150961189392611881866112ba565b906119a5565b6001600160401b031690565b6001600160401b038816908111156118b657600460405163dd020e2560e01b8152fd5b6118ca8f61150961188791611881876112ba565b146118d5578d611528565b600460405163a5a1ff5d60e01b8152fd5b826014949392823701906bffffffffffffffffffffffff199060601b1681520190565b91602061026c938181520191611311565b63ffffffff1680156110c7575f190190565b94939161194b9061198f9461148260409460e08a5260e08a01906112d5565b9401906080809163ffffffff815116845260208101516001600160401b03809116602086015260408201511660408501526060810151151560608501520151910152565b565b634e487b7160e01b5f52603260045260245ffd5b80518210156119b95760209160051b010190565b611991565b91908110156119b95760051b81013590601e19813603018212156101305701908135916001600160401b038311610130576020018236038113610130579190565b5f5b838110611a105750505f910152565b8181015183820152602001611a01565b90602091611a39815180928185528580860191016119ff565b601f01601f1916010190565b94939161198f93611a7561194b92611a67606095610100808c528b01906112d5565b9089820360208b0152611a20565b908782036040890152611a20565b94929093948051958615611ba857828703611b7e57611aa186611d63565b5f5b878110611b615750611ad8611ab88787611f88565b60808701611ac78482516112c8565b905263ffffffff89169088886120e5565b80611b52575b505f5b868110611af15750505050505050565b80867f48a3ea0796746043948f6341d17ff8200937b99262a0b48c2663b951ed7114e5611b20600194866119a5565b51611b36611b2f85898b6119be565b36916105d4565b90611b498a604051938493339785611a45565b0390a201611ae1565b611b5b9061248c565b5f611ade565b80611b7888611b72600194876119a5565b51611e3a565b01611aa3565b60046040517f9ad467b8000000000000000000000000000000000000000000000000000000008152fd5b60046040517fdf83e679000000000000000000000000000000000000000000000000000000008152fd5b9190808252602080920192915f5b828110611bee575050505090565b9091929382806001926001600160401b038835611c0a816101e8565b16815201950193929101611be0565b9290611c329061026c9593604086526040860191611bd2565b926020818503910152611311565b63ffffffff8091169081146110c75760010190565b63ffffffff91821690821603919082116110c757565b93929061194b60209161198f9460c0885260c0880191611bd2565b91908110156119b95760051b0190565b3561026c816101e8565b6001600160401b0391821690821603919082116110c757565b9190916001600160401b03808094169116029182169182036110c757565b9190916001600160401b03808094169116019182116110c757565b919082039182116110c757565b611d1a60409261198f9597969460e0845260e0840191611bd2565b95602082015201906080809163ffffffff815116845260208101516001600160401b03809116602086015260408201511660408501526060810151151560608501520151910152565b5160048110908115611db8575b8115611da8575b50611d7e57565b60046040517f38186224000000000000000000000000000000000000000000000000000000008152fd5b600191506003900614155f611d77565b600d81119150611d70565b602090611dd960149493828151948592016119ff565b01906bffffffffffffffffffffffff199060601b1681520190565b90602061026c928181520190611a20565b80516020809201915f5b828110611e1d575050505090565b83516001600160401b031685529381019392810192600101611e0f565b906030825103611f3a576040516020810181611e57338684611dc3565b0391611e6b601f19938481018352826101b0565b51902092611ea0845f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b54611f045750611f0191600191611ecf6040519182611ec3602082018096611e05565b039081018352826101b0565b51902017915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b55565b6105a6906040519182917f388e799900000000000000000000000000000000000000000000000000000000835260048301611df4565b60046040517f637297a4000000000000000000000000000000000000000000000000000000008152fd5b60149061026c93926bffffffffffffffffffffffff199060601b1681520190611e05565b9190604051611fa08161043760208201943386611f64565b51902091611fd5835f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f2090565b5480612090575063ffffffff611fef825163ffffffff1690565b161590811591612069575b8115612042575b8115612034575b8115612027575b5061201657565b60046040516312e04c8760e01b8152fd5b606001511590505f61200f565b608081015115159150612008565b90506001600160401b0361206060408301516001600160401b031690565b16151590612001565b90506001600160401b0361208760208301516001600160401b031690565b16151590611ffa565b61209982612cf7565b146120af5760046040516312e04c8760e01b8152fd5b61198f90612e17565b90600163ffffffff809316019182116110c757565b91909163ffffffff808094169116019182116110c757565b91925f80928051905f195f905f5b8481106121a4575050505050612131612194939261211661213b93610f07612939565b61211f81612b6a565b855163ffffffff166120cd565b6120cd565b63ffffffff168452565b5f805160206133708339815191525460801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c169185612d97565b610d465761051d611f0191612cf7565b6121b161150982866119a5565b95856121bc836112ba565b10612403575b6121ff611566886001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b9060808201996122186115838c5163ffffffff90511690565b156118445760608301516122b1575b612230836131af565b6122428a61212c855163ffffffff1690565b63ffffffff81811685525f80516020613370833981519152546122699060601c8216611583565b911611611660579161165561161e6122ab93610fae6001979661229b602091610fae838901516001600160401b031690565b9f5101516001600160401b031690565b016120f3565b66ffffffffffffff8960081c168581036123b7575b50600160ff8a161b8616612227576123116116e18a6001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b6001600160a01b0381169081156117ca57338203612331575b5050612227565b61081c61233d91613224565b908115612351575b5061172e575f8061232a565b6040516320c18e6b60e21b81523360048201526001600160401b038c16602482015260209250908290829060449082905afa9182156117c5575f9261239a575b5050155f612345565b6123b09250803d106117be576117af81836101b0565b5f80612391565b8096506123fa91955061182e336001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b5494935f6122c6565b61241b611887611509612415856112ba565b886119a5565b6001600160401b0388169081111561243e57600460405163dd020e2560e01b8152fd5b612456611887611509612450866112ba565b896119a5565b036121c257600460405163a5a1ff5d60e01b8152fd5b90816020910312610130575161026c8161026f565b6040513d5f823e3d90fd5b60205f9160646001600160a01b037fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b54169160405194859384927f23b872dd00000000000000000000000000000000000000000000000000000000845233600485015230602485015260448401525af19081156117c5575f9161253c575b501561251257565b60046040517f045c4b02000000000000000000000000000000000000000000000000000000008152fd5b612555915060203d6020116117be576117af81836101b0565b5f61250a565b92919061043761257961258293604051928391602083019586611f64565b51902092612cf7565b825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f205480155f146125e05760046040517f185e2b16000000000000000000000000000000000000000000000000000000008152fd5b0361201657565b6040519061260b826125fd602082018094611e05565b03601f1981018452836101b0565b905190206001191690565b5f915f9180515f915b81831061262b57505050565b9091946001600160401b0361264087846119a5565b51165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f20600281019063ffffffff808354166126a3575b50505460019161269a9160201c6001600160401b0316610fae565b9501919061261f565b6126b18294929893986130a5565b835481165f19019081116110c7576126f961269a93610fae866126e9610fae956001999063ffffffff1663ffffffff19825416179055565b5460201c6001600160401b031690565b97925081935061267f565b915f925f928151905f925b82841061271c5750505050565b909192956001600160401b038061273389856119a5565b51165f5260207fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a815260405f20600281019063ffffffff80835416612792575b505091600193916127889354901c1690611cd7565b960192919061270f565b84929a61278895836127d06127ba8c6127da966127b260019d9b996130a5565b845416611c55565b825463ffffffff191663ffffffff909116178255565b54841c1690611cd7565b99919381939550612773565b915f925f928151905f925b8284106127fe5750505050565b9091929561280f61150988846119a5565b612849816001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b600281019161285f611583845463ffffffff1690565b612889575b50505460019161287f9160201c6001600160401b0316610fae565b96019291906127f1565b612895829993996130a5565b6128a78661212c845463ffffffff1690565b825463ffffffff191663ffffffff821617835563ffffffff6128e16115835f805160206133708339815191525463ffffffff9060601c1690565b911611612916575091610fae61290b61287f93610fae600196546001600160401b039060201c1690565b989250819350612864565b60405163639f585160e01b81526001600160401b03919091166004820152602490fd5b5f805160206133708339815191525463ffffffff81164303904382116110c75761297561026c926001600160401b03808460801c169116611cb9565b9060c01c611cd7565b919060209161298e818386612ecc565b6001600160401b03809216604085015216910152565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa168054906001600160401b03915f805160206133708339815191529182549163ffffffff94612a23612a1b612a0f612a03898860401c16854316611ca0565b848860801c1690611cb9565b888760201c1690611cb9565b828416611cd7565b67ffffffffffffffff1990921691161790556bffffffff000000000000000019164360401b63ffffffff60401b16179081905560201c81165f19019081116110c75761198f905f805160206133708339815191529067ffffffff0000000082549160201b169067ffffffff000000001916179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169081549067ffffffff00000000612b506001600160401b03925f805160206133708339815191529586549563ffffffff95612b1d612a1b612b11612b058a8c60401c16854316611ca0565b848c60801c1690611cb9565b898b60201c1690611cb9565b16906001600160401b03191617905563ffffffff60401b4360401b16938463ffffffff60401b1987161760201c16611c55565b60201b16916bffffffffffffffff00000000191617179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1690612c028254916001600160401b035f80516020613370833981519152805463ffffffff9687968794612bce612a1b612a0f612a03898860401c16854316611ca0565b16906001600160401b03191617905563ffffffff60401b4360401b169063ffffffff60401b19161780915560201c166120cd565b5f80516020613370833981519152805467ffffffff000000001916602083901b67ffffffff00000000161790551611612c3757565b60046040517f91aa3017000000000000000000000000000000000000000000000000000000008152fd5b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa168054612c026001600160401b035f80516020613370833981519152805463ffffffff9586958694612cc3612a1b612a0f612a03898860401c16854316611ca0565b16906001600160401b03191617905563ffffffff60401b4360401b169063ffffffff60401b19161780915560201c166120b8565b805190602081015160408201519160606080820151910151151591604051937fffffffff00000000000000000000000000000000000000000000000000000000602086019660e01b1686527fffffffffffffffff000000000000000000000000000000000000000000000000809260c01b16602486015260c01b16602c840152603483015260f81b605482015260358152612d9181610195565b51902090565b9493909291925f9563ffffffff80825116612db457505050505050565b909192939495965060808201958651612dd66001600160401b03809716613197565b11612e0b57612e0795612def612df592612dfe96611cd7565b90611cb9565b91511690611cb9565b92519216613197565b1190565b50505050505050600190565b6060015115612e2257565b60046040517f95a0cf33000000000000000000000000000000000000000000000000000000008152fd5b60209060446001600160a01b03915f837fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b541660405196879586947fa9059cbb00000000000000000000000000000000000000000000000000000000865216600485015260248401525af19081156117c5575f9161253c57501561251257565b9190612f2190612f1c612eee6001600160401b03948560208801511690611ca0565b612f12612f0463ffffffff928389511690611cb9565b938660408901511690611ca0565b9086511690611cb9565b611cd7565b16906080612f2e83613197565b91019182518092115f14612f435750505f9052565b612f4c90613197565b81039081116110c7579052565b90604051612f6681610175565b809280549163ffffffff9283811682526001600160401b0390818160201c16602084015260601c604083015260ff600184015416151560608301526040519360608501858110838211176101905760809460029160405201549081168552818160201c16602086015260601c1660408401520152565b81516020808401516040808601516bffffffffffffffff0000000092841b831663ffffffff95861617606091821b6bffffffffffffffffffffffff19161786558087015160018701805460ff9215159290921660ff1990921691909117905560809096015180516002909601805482860151929093015173ffffffffffffffff000000000000000000000000981b979097167fffffffffffffffffffffffff000000000000000000000000000000000000000090921695909416949094179290911b1617179055565b61198f9063ffffffff61318261313d6130f36131318443169560028101958654916131296130ff6130d88486168c611c55565b946001600160401b039788968688875460201c169116611cb9565b95869160201c16611cd7565b89546bffffffffffffffff00000000191660209190911b6bffffffffffffffff0000000016178955565b541690611cb9565b90845460601c16611cd7565b82547fffffffffffffffffffffffff0000000000000000ffffffffffffffffffffffff1660609190911b73ffffffffffffffff00000000000000000000000016178255565b9063ffffffff1663ffffffff19825416179055565b9062989680918281029281840414901517156110c757565b63ffffffff908143169161320a60808301926131cf838551511686611c55565b926131eb6001600160401b039482866020860151169116611cb9565b916020865101856131ff8582845116611cd7565b169052511690611cb9565b9061321d60408451019282845116611cd7565b1690525152565b604051906020808301815f6301ffc9a760e01b958684528660248201526024815261324e81610195565b51617530938685fa933d5f519086613309575b50856132ff575b5084613285575b5050508161327b575090565b61026c9150613314565b839450905f9183946040518581019283527fffffffff000000000000000000000000000000000000000000000000000000006024820152602481526132c981610195565b5192fa5f5190913d836132f4575b5050816132ea575b5015905f808061326f565b905015155f6132df565b101591505f806132d7565b151594505f613268565b84111595505f613261565b5f602091604051838101906301ffc9a760e01b82526320c18e6b60e21b60248201526024815261334381610195565b5191617530fa5f513d82613363575b508161335c575090565b9050151590565b6020111591505f61335256fe0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15a26469706673582212203d5e290ba2efa0608d3e97006423781c2a89ce98b75babedc4d4434684fbb60a64736f6c63430008180033", + "deployedBytecode": "0x60806040526004361015610011575f80fd5b5f3560e01c806306e8fb9c146100b457806312b3fc19146100af57806322f18bf5146100aa57806332afd02f146100a55780633877322b146100a05780635aed11421461009b5780635fec6dd014610096578063686e682c14610091578063bc26e7e51461008c5763bf0f2fb214610087575f80fd5b6110cc565b610fd9565b610d9a565b610bd5565b610998565b610895565b610740565b610649565b6103b5565b3461013057610120366003190112610130576001600160401b03600435818111610130576100e6903690600401610134565b90602435838111610130576100ff903690600401610251565b6044359384116101305761011a61012e943690600401610134565b9161012436610279565b9460643594611331565b005b5f80fd5b9181601f84011215610130578235916001600160401b038311610130576020838186019501011161013057565b634e487b7160e01b5f52604160045260245ffd5b60a081019081106001600160401b0382111761019057604052565b610161565b606081019081106001600160401b0382111761019057604052565b90601f801991011681019081106001600160401b0382111761019057604052565b6001600160401b0381116101905760051b60200190565b6001600160401b0381160361013057565b9291610204826101d1565b9161021260405193846101b0565b829481845260208094019160051b810192831161013057905b8282106102385750505050565b8380918335610246816101e8565b81520191019061022b565b9080601f830112156101305781602061026c933591016101f9565b90565b8015150361013057565b60a0906083190112610130576040519061029282610175565b8160843563ffffffff8116810361013057815260a4356102b1816101e8565b602082015260c4356102c2816101e8565b604082015260e4356102d38161026f565b6060820152608061010435910152565b60a090604319011261013057604051906102fc82610175565b8160443563ffffffff8116810361013057815260643561031b816101e8565b602082015260843561032c816101e8565b604082015260a43561033d8161026f565b6060820152608060c435910152565b60a0906063190112610130576040519061036582610175565b8160643563ffffffff81168103610130578152608435610384816101e8565b602082015260a435610395816101e8565b604082015260c4356103a68161026f565b6060820152608060e435910152565b346101305760e0366003190112610130576001600160401b03600435818111610130576103e6903690600401610134565b909160243590811161013057610400903690600401610251565b9061040a366102e3565b9261041683338661255b565b61041f846125e7565b604051602081019061044581610437338989876118e6565b03601f1981018352826101b0565b5190209061047a825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b5480156105aa576001191603610588579461055a915f6104e37fccf4370403e5fbbde0cd3f13426479dcd8a5916b05db424b7a2c04978cf8ce6e97985f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b55606082015161055f575b610511610507610502845163ffffffff1690565b61191a565b63ffffffff168352565b61054b61051d83612cf7565b915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f2090565b5560405193849333978561192c565b0390a2005b61057b61056b88612616565b50610574612939565b908461297e565b6105836129a4565b6104ee565b50506105a66040519283926311260f2760e31b845260048401611909565b0390fd5b60046040517fe51315d2000000000000000000000000000000000000000000000000000000008152fd5b9291926001600160401b03821161019057604051916105fd601f8201601f1916602001846101b0565b829481845281830111610130578281602093845f960137010152565b9181601f84011215610130578235916001600160401b038311610130576020808501948460051b01011161013057565b3461013057610120366003190112610130576001600160401b036004358181116101305736602382011215610130578060040135602491610689826101d1565b9261069760405194856101b0565b8284526020926024602086019160051b840101923684116101305760248101915b848310610709578787602435828111610130576106d9903690600401610251565b90604435928311610130576106f561012e933690600401610619565b906106ff36610279565b9360643593611a83565b8235888111610130578201366043820112156101305786916107358392369060448982013591016105d4565b8152019201916106b8565b34610130576040806003193601126101305760046001600160401b03813581811161013057610773903690600401610619565b90916024359081116101305761078d903690600401610619565b91909281156105aa576107a96107a43685876101f9565b6125e7565b945f5b8381106107b557005b61082061081c886108138b6104376107e36107d1888c8c6119be565b935192839160208301953391876118e6565b5190205f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b54600119161490565b1590565b61086f5780867fb4b20ffb2eb1f020be3df600b2287914f50c07003526d3a9d89a9dd12351828c61085460019488886119be565b906108668d519283928c339785611c19565b0390a2016107ac565b61087e6105a691858a956119be565b9093519384936311260f2760e31b85528401611909565b34610130576040366003190112610130576001600160401b03600435818111610130576108c6903690600401610134565b91602435908111610130576108df903690600401610619565b60409291925160208101906108fa81610437338988876118e6565b5190205f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205261094460405f20546109396107a43685886101f9565b600119909116141590565b61097c5761055a7fb4b20ffb2eb1f020be3df600b2287914f50c07003526d3a9d89a9dd12351828c9394604051938493339785611c19565b6040516311260f2760e31b8152806105a6868560048401611909565b346101305760e03660031901126101305760046001600160401b038135818111610130576109ca903690600401610619565b9091602435908111610130576109e4903690600401610251565b906109ee366102e3565b9381156105aa57610a0083338761255b565b90610a0a846125e7565b5f915f915b858310610ada57505050610a3f610a4991610a2d6060890151151590565b610aaf575b875163ffffffff16611c55565b63ffffffff168652565b610a5561051d86612cf7565b555f5b818110610a6157005b80857fccf4370403e5fbbde0cd3f13426479dcd8a5916b05db424b7a2c04978cf8ce6e610a9160019486896119be565b610aa660409492945192839233968b8561192c565b0390a201610a58565b610acc610abc8288612704565b50610ac5612939565b908a61297e565b610ad581612a99565b610a32565b909192610ae884878a6119be565b610437610b03604093845192839160208301953391876118e6565b51902090610b3f61081c85610813855f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b610b8b57506001915f610b7c610b82935f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b55611c40565b93019190610a0f565b846105a661087e888b8e6119be565b9060e0600319830112610130576004356001600160401b0381116101305782610bc591600401610619565b9290929161026c602435926102e3565b3461013057610be336610b9a565b9091610bfa610bf33683876101f9565b338461255b565b9260608301610c098151151590565b610d7057610c52610c62610cec92610c38610c28885163ffffffff1690565b610c3336898d6101f9565b6127e6565b93909160808901610c4a8882516112c8565b905260019052565b6001600160401b03166040870152565b610c7d610c6d612939565b6001600160401b03166020870152565b610c93610c8e865163ffffffff1690565b612b6a565b5f805160206133708339815191525460801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c169187612d97565b610d46577fc803f8c01343fcdaf32068f4c283951623ef2b3fa0c547551931356f456b685993610d1e61051d85612cf7565b5580610d37575b5061055a604051928392339684611c6b565b610d409061248c565b5f610d25565b60046040517ff4d678b8000000000000000000000000000000000000000000000000000000008152fd5b60046040517f3babafd2000000000000000000000000000000000000000000000000000000008152fd5b3461013057610da836610b9a565b91610dbe610db73683876101f9565b338561255b565b610dc784612e17565b5f60608501610dd68151151590565b610ede575b608086019085825110610d465781610df787610dff9451611cf2565b905251151590565b9081610ec0575b81610e5c575b50610d46578361055a91610e4361051d7f39d1320bbda24947e77f3560661323384aa0a1cb9d5e040e617e5cbf50b6dbe097612cf7565b55610e4e8433612e4c565b604051938493339785611cff565b5f8051602061337083398151915254610eba925060801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c169188612d97565b5f610e0c565b905063ffffffff610ed5865163ffffffff1690565b16151590610e06565b5f806001600160401b03804316905b878b818510610f13575050505050610f0e90610f07612939565b908861297e565b610ddb565b94610fb4610f6c610f36610f3188610fba96600198999a9d9b611c86565b611c96565b6001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b91610fae600284015487610fa4610f9d610f8c63ffffffff85168d611ca0565b975460201c6001600160401b031690565b8097611cb9565b9160201c16611cd7565b90611cd7565b95611cd7565b95019190610eed565b600435906001600160a01b038216820361013057565b346101305761010036600319011261013057610ff3610fc3565b6024356001600160401b03811161013057611012903690600401610619565b604492919235916110223661034c565b926110386110313685886101f9565b838661255b565b9360808101918251948186018096116110c7577f2bac1912f2481d12f0df08647c06bee174967c62d3a03cbc078eb215dc1bd9a2966001600160a01b039661055a955261108484612cf7565b905f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f20556110b98261248c565b604051958695169785611cff565b6112a6565b346101305760e0366003190112610130576110e5610fc3565b6001600160401b039060243582811161013057611106903690600401610619565b90611110366102e3565b61112561111e3685856101f9565b858361255b565b9061112f81612e17565b611164611153611143835163ffffffff1690565b61114e3688886101f9565b612704565b919061115d612939565b9084612ecc565b6001600160a01b035f961696338814159182611244575b505061121a577f1fce24c373e07f89214e9187598635036111dbb363e99f4ce498488cdc66e6889461055a926111bd6111b8845163ffffffff1690565b612a99565b6080830180518061120d575b50505f60408401525f60208401525f60608401526111e961051d84612cf7565b55806111fd575b5060405193849384611c6b565b6112079033612e4c565b5f6111f0565b5f91935092525f806111c9565b60046040517f60300a8d000000000000000000000000000000000000000000000000000000008152fd5b5f805160206133708339815191525461129f935061081c92919060801c6001600160401b03167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165492808460801c169360401c169186612d97565b5f8061117b565b634e487b7160e01b5f52601160045260245ffd5b90600182018092116110c757565b919082018092116110c757565b9081518082526020808093019301915f5b8281106112f4575050505090565b83516001600160401b0316855293810193928101926001016112e6565b908060209392818452848401375f828201840152601f01601f1916010190565b94919392909261134085611d63565b6113548561134f36878a6105d4565b611e3a565b61135e8588611f88565b60808089018051908682018092116110c7575286515f9182919082905f19825b8c8582106114fc57505050505050906113a36114279261139c612939565b908c61297e565b6113ab612c61565b6113ce6113c46113bf8c5163ffffffff1690565b6120b8565b63ffffffff168b52565b5f805160206133708339815191525460801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c16918c612d97565b610d46577f48a3ea0796746043948f6341d17ff8200937b99262a0b48c2663b951ed7114e5966114e59561149e956114909361146561051d8d612cf7565b55806114ed575b5061148260405198610100808b528a01906112d5565b9188830360208a0152611311565b918583036040870152611311565b9360608301906080809163ffffffff815116845260208101516001600160401b03809116602086015260408201511660408501526060810151151560608501520151910152565b8033930390a2565b6114f69061248c565b5f61146c565b61150982611516926119a5565b516001600160401b031690565b958d86611522846112ba565b1061186e575b5061156b611566886001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b612f59565b908482019861158c6115838b5163ffffffff90511690565b63ffffffff1690565b15611844576060830151611681575b6115a4836131af565b6115b56113bf845163ffffffff1690565b63ffffffff81811685525f80516020613370833981519152546115dc9060601c8216611583565b911611611660579161165561161e61165a93610fae6001979661160e602091610fae838901516001600160401b031690565b9e5101516001600160401b031690565b996001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b612fdc565b0161137e565b60405163639f585160e01b81526001600160401b038a166004820152602490fd5b66ffffffffffffff8960081c168581036117eb575b50600160ff8a161b871661159b576116ee6116e18a6001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b546001600160a01b031690565b6001600160a01b0381169081156117ca5733820361170e575b505061159b565b61081c61171a91613224565b90811561174f575b5061172e575f80611707565b604051635bfa94ff60e11b81526001600160401b038a166004820152602490fd5b6040516320c18e6b60e21b81523360048201526001600160401b038c16602482015260209250908290829060449082905afa9182156117c5575f92611798575b5050155f611722565b6117b79250803d106117be575b6117af81836101b0565b81019061246c565b5f8061178f565b503d6117a5565b612481565b604051635bfa94ff60e11b81526001600160401b038c166004820152602490fd5b80975061183b91955061182e336001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b905f5260205260405f2090565b5495935f611696565b60046040517f961e3e8c000000000000000000000000000000000000000000000000000000008152fd5b61188761150961189392611881866112ba565b906119a5565b6001600160401b031690565b6001600160401b038816908111156118b657600460405163dd020e2560e01b8152fd5b6118ca8f61150961188791611881876112ba565b146118d5578d611528565b600460405163a5a1ff5d60e01b8152fd5b826014949392823701906bffffffffffffffffffffffff199060601b1681520190565b91602061026c938181520191611311565b63ffffffff1680156110c7575f190190565b94939161194b9061198f9461148260409460e08a5260e08a01906112d5565b9401906080809163ffffffff815116845260208101516001600160401b03809116602086015260408201511660408501526060810151151560608501520151910152565b565b634e487b7160e01b5f52603260045260245ffd5b80518210156119b95760209160051b010190565b611991565b91908110156119b95760051b81013590601e19813603018212156101305701908135916001600160401b038311610130576020018236038113610130579190565b5f5b838110611a105750505f910152565b8181015183820152602001611a01565b90602091611a39815180928185528580860191016119ff565b601f01601f1916010190565b94939161198f93611a7561194b92611a67606095610100808c528b01906112d5565b9089820360208b0152611a20565b908782036040890152611a20565b94929093948051958615611ba857828703611b7e57611aa186611d63565b5f5b878110611b615750611ad8611ab88787611f88565b60808701611ac78482516112c8565b905263ffffffff89169088886120e5565b80611b52575b505f5b868110611af15750505050505050565b80867f48a3ea0796746043948f6341d17ff8200937b99262a0b48c2663b951ed7114e5611b20600194866119a5565b51611b36611b2f85898b6119be565b36916105d4565b90611b498a604051938493339785611a45565b0390a201611ae1565b611b5b9061248c565b5f611ade565b80611b7888611b72600194876119a5565b51611e3a565b01611aa3565b60046040517f9ad467b8000000000000000000000000000000000000000000000000000000008152fd5b60046040517fdf83e679000000000000000000000000000000000000000000000000000000008152fd5b9190808252602080920192915f5b828110611bee575050505090565b9091929382806001926001600160401b038835611c0a816101e8565b16815201950193929101611be0565b9290611c329061026c9593604086526040860191611bd2565b926020818503910152611311565b63ffffffff8091169081146110c75760010190565b63ffffffff91821690821603919082116110c757565b93929061194b60209161198f9460c0885260c0880191611bd2565b91908110156119b95760051b0190565b3561026c816101e8565b6001600160401b0391821690821603919082116110c757565b9190916001600160401b03808094169116029182169182036110c757565b9190916001600160401b03808094169116019182116110c757565b919082039182116110c757565b611d1a60409261198f9597969460e0845260e0840191611bd2565b95602082015201906080809163ffffffff815116845260208101516001600160401b03809116602086015260408201511660408501526060810151151560608501520151910152565b5160048110908115611db8575b8115611da8575b50611d7e57565b60046040517f38186224000000000000000000000000000000000000000000000000000000008152fd5b600191506003900614155f611d77565b600d81119150611d70565b602090611dd960149493828151948592016119ff565b01906bffffffffffffffffffffffff199060601b1681520190565b90602061026c928181520190611a20565b80516020809201915f5b828110611e1d575050505090565b83516001600160401b031685529381019392810192600101611e0f565b906030825103611f3a576040516020810181611e57338684611dc3565b0391611e6b601f19938481018352826101b0565b51902092611ea0845f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b54611f045750611f0191600191611ecf6040519182611ec3602082018096611e05565b039081018352826101b0565b51902017915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f2090565b55565b6105a6906040519182917f388e799900000000000000000000000000000000000000000000000000000000835260048301611df4565b60046040517f637297a4000000000000000000000000000000000000000000000000000000008152fd5b60149061026c93926bffffffffffffffffffffffff199060601b1681520190611e05565b9190604051611fa08161043760208201943386611f64565b51902091611fd5835f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f2090565b5480612090575063ffffffff611fef825163ffffffff1690565b161590811591612069575b8115612042575b8115612034575b8115612027575b5061201657565b60046040516312e04c8760e01b8152fd5b606001511590505f61200f565b608081015115159150612008565b90506001600160401b0361206060408301516001600160401b031690565b16151590612001565b90506001600160401b0361208760208301516001600160401b031690565b16151590611ffa565b61209982612cf7565b146120af5760046040516312e04c8760e01b8152fd5b61198f90612e17565b90600163ffffffff809316019182116110c757565b91909163ffffffff808094169116019182116110c757565b91925f80928051905f195f905f5b8481106121a4575050505050612131612194939261211661213b93610f07612939565b61211f81612b6a565b855163ffffffff166120cd565b6120cd565b63ffffffff168452565b5f805160206133708339815191525460801c6001600160401b0316907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1654916001600160401b03808460801c169360401c169185612d97565b610d465761051d611f0191612cf7565b6121b161150982866119a5565b95856121bc836112ba565b10612403575b6121ff611566886001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b9060808201996122186115838c5163ffffffff90511690565b156118445760608301516122b1575b612230836131af565b6122428a61212c855163ffffffff1690565b63ffffffff81811685525f80516020613370833981519152546122699060601c8216611583565b911611611660579161165561161e6122ab93610fae6001979661229b602091610fae838901516001600160401b031690565b9f5101516001600160401b031690565b016120f3565b66ffffffffffffff8960081c168581036123b7575b50600160ff8a161b8616612227576123116116e18a6001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b6001600160a01b0381169081156117ca57338203612331575b5050612227565b61081c61233d91613224565b908115612351575b5061172e575f8061232a565b6040516320c18e6b60e21b81523360048201526001600160401b038c16602482015260209250908290829060449082905afa9182156117c5575f9261239a575b5050155f612345565b6123b09250803d106117be576117af81836101b0565b5f80612391565b8096506123fa91955061182e336001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b5494935f6122c6565b61241b611887611509612415856112ba565b886119a5565b6001600160401b0388169081111561243e57600460405163dd020e2560e01b8152fd5b612456611887611509612450866112ba565b896119a5565b036121c257600460405163a5a1ff5d60e01b8152fd5b90816020910312610130575161026c8161026f565b6040513d5f823e3d90fd5b60205f9160646001600160a01b037fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b54169160405194859384927f23b872dd00000000000000000000000000000000000000000000000000000000845233600485015230602485015260448401525af19081156117c5575f9161253c575b501561251257565b60046040517f045c4b02000000000000000000000000000000000000000000000000000000008152fd5b612555915060203d6020116117be576117af81836101b0565b5f61250a565b92919061043761257961258293604051928391602083019586611f64565b51902092612cf7565b825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f205480155f146125e05760046040517f185e2b16000000000000000000000000000000000000000000000000000000008152fd5b0361201657565b6040519061260b826125fd602082018094611e05565b03601f1981018452836101b0565b905190206001191690565b5f915f9180515f915b81831061262b57505050565b9091946001600160401b0361264087846119a5565b51165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f20600281019063ffffffff808354166126a3575b50505460019161269a9160201c6001600160401b0316610fae565b9501919061261f565b6126b18294929893986130a5565b835481165f19019081116110c7576126f961269a93610fae866126e9610fae956001999063ffffffff1663ffffffff19825416179055565b5460201c6001600160401b031690565b97925081935061267f565b915f925f928151905f925b82841061271c5750505050565b909192956001600160401b038061273389856119a5565b51165f5260207fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a815260405f20600281019063ffffffff80835416612792575b505091600193916127889354901c1690611cd7565b960192919061270f565b84929a61278895836127d06127ba8c6127da966127b260019d9b996130a5565b845416611c55565b825463ffffffff191663ffffffff909116178255565b54841c1690611cd7565b99919381939550612773565b915f925f928151905f925b8284106127fe5750505050565b9091929561280f61150988846119a5565b612849816001600160401b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b600281019161285f611583845463ffffffff1690565b612889575b50505460019161287f9160201c6001600160401b0316610fae565b96019291906127f1565b612895829993996130a5565b6128a78661212c845463ffffffff1690565b825463ffffffff191663ffffffff821617835563ffffffff6128e16115835f805160206133708339815191525463ffffffff9060601c1690565b911611612916575091610fae61290b61287f93610fae600196546001600160401b039060201c1690565b989250819350612864565b60405163639f585160e01b81526001600160401b03919091166004820152602490fd5b5f805160206133708339815191525463ffffffff81164303904382116110c75761297561026c926001600160401b03808460801c169116611cb9565b9060c01c611cd7565b919060209161298e818386612ecc565b6001600160401b03809216604085015216910152565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa168054906001600160401b03915f805160206133708339815191529182549163ffffffff94612a23612a1b612a0f612a03898860401c16854316611ca0565b848860801c1690611cb9565b888760201c1690611cb9565b828416611cd7565b67ffffffffffffffff1990921691161790556bffffffff000000000000000019164360401b63ffffffff60401b16179081905560201c81165f19019081116110c75761198f905f805160206133708339815191529067ffffffff0000000082549160201b169067ffffffff000000001916179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169081549067ffffffff00000000612b506001600160401b03925f805160206133708339815191529586549563ffffffff95612b1d612a1b612b11612b058a8c60401c16854316611ca0565b848c60801c1690611cb9565b898b60201c1690611cb9565b16906001600160401b03191617905563ffffffff60401b4360401b16938463ffffffff60401b1987161760201c16611c55565b60201b16916bffffffffffffffff00000000191617179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1690612c028254916001600160401b035f80516020613370833981519152805463ffffffff9687968794612bce612a1b612a0f612a03898860401c16854316611ca0565b16906001600160401b03191617905563ffffffff60401b4360401b169063ffffffff60401b19161780915560201c166120cd565b5f80516020613370833981519152805467ffffffff000000001916602083901b67ffffffff00000000161790551611612c3757565b60046040517f91aa3017000000000000000000000000000000000000000000000000000000008152fd5b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa168054612c026001600160401b035f80516020613370833981519152805463ffffffff9586958694612cc3612a1b612a0f612a03898860401c16854316611ca0565b16906001600160401b03191617905563ffffffff60401b4360401b169063ffffffff60401b19161780915560201c166120b8565b805190602081015160408201519160606080820151910151151591604051937fffffffff00000000000000000000000000000000000000000000000000000000602086019660e01b1686527fffffffffffffffff000000000000000000000000000000000000000000000000809260c01b16602486015260c01b16602c840152603483015260f81b605482015260358152612d9181610195565b51902090565b9493909291925f9563ffffffff80825116612db457505050505050565b909192939495965060808201958651612dd66001600160401b03809716613197565b11612e0b57612e0795612def612df592612dfe96611cd7565b90611cb9565b91511690611cb9565b92519216613197565b1190565b50505050505050600190565b6060015115612e2257565b60046040517f95a0cf33000000000000000000000000000000000000000000000000000000008152fd5b60209060446001600160a01b03915f837fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b541660405196879586947fa9059cbb00000000000000000000000000000000000000000000000000000000865216600485015260248401525af19081156117c5575f9161253c57501561251257565b9190612f2190612f1c612eee6001600160401b03948560208801511690611ca0565b612f12612f0463ffffffff928389511690611cb9565b938660408901511690611ca0565b9086511690611cb9565b611cd7565b16906080612f2e83613197565b91019182518092115f14612f435750505f9052565b612f4c90613197565b81039081116110c7579052565b90604051612f6681610175565b809280549163ffffffff9283811682526001600160401b0390818160201c16602084015260601c604083015260ff600184015416151560608301526040519360608501858110838211176101905760809460029160405201549081168552818160201c16602086015260601c1660408401520152565b81516020808401516040808601516bffffffffffffffff0000000092841b831663ffffffff95861617606091821b6bffffffffffffffffffffffff19161786558087015160018701805460ff9215159290921660ff1990921691909117905560809096015180516002909601805482860151929093015173ffffffffffffffff000000000000000000000000981b979097167fffffffffffffffffffffffff000000000000000000000000000000000000000090921695909416949094179290911b1617179055565b61198f9063ffffffff61318261313d6130f36131318443169560028101958654916131296130ff6130d88486168c611c55565b946001600160401b039788968688875460201c169116611cb9565b95869160201c16611cd7565b89546bffffffffffffffff00000000191660209190911b6bffffffffffffffff0000000016178955565b541690611cb9565b90845460601c16611cd7565b82547fffffffffffffffffffffffff0000000000000000ffffffffffffffffffffffff1660609190911b73ffffffffffffffff00000000000000000000000016178255565b9063ffffffff1663ffffffff19825416179055565b9062989680918281029281840414901517156110c757565b63ffffffff908143169161320a60808301926131cf838551511686611c55565b926131eb6001600160401b039482866020860151169116611cb9565b916020865101856131ff8582845116611cd7565b169052511690611cb9565b9061321d60408451019282845116611cd7565b1690525152565b604051906020808301815f6301ffc9a760e01b958684528660248201526024815261324e81610195565b51617530938685fa933d5f519086613309575b50856132ff575b5084613285575b5050508161327b575090565b61026c9150613314565b839450905f9183946040518581019283527fffffffff000000000000000000000000000000000000000000000000000000006024820152602481526132c981610195565b5192fa5f5190913d836132f4575b5050816132ea575b5015905f808061326f565b905015155f6132df565b101591505f806132d7565b151594505f613268565b84111595505f613261565b5f602091604051838101906301ffc9a760e01b82526320c18e6b60e21b60248201526024815261334381610195565b5191617530fa5f513d82613363575b508161335c575090565b9050151590565b6020111591505f61335256fe0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15a26469706673582212203d5e290ba2efa0608d3e97006423781c2a89ce98b75babedc4d4434684fbb60a64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/modules/SSVClusters.sol", + "buildInfoId": "solc-0_8_24-dc3260967516f45b660c2554111e2a58f5f3b385" +} \ No newline at end of file diff --git a/test/setup/artifacts/SSVDAOLegacy.json b/test/setup/artifacts/SSVDAOLegacy.json new file mode 100644 index 000000000..482aeb220 --- /dev/null +++ b/test/setup/artifacts/SSVDAOLegacy.json @@ -0,0 +1,508 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "SSVDAO", + "sourceName": "contracts/modules/SSVDAO.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "DeclareOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "ExecuteOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "LiquidationThresholdPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "MinimumLiquidationCollateralUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "NetworkEarningsWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "NetworkFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "OperatorFeeIncreaseLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "maxFee", + "type": "uint64" + } + ], + "name": "OperatorMaximumFeeUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateDeclareOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateExecuteOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blocks", + "type": "uint64" + } + ], + "name": "updateLiquidationThresholdPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "maxFee", + "type": "uint64" + } + ], + "name": "updateMaximumOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "updateMinimumLiquidationCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "updateNetworkFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "percentage", + "type": "uint64" + } + ], + "name": "updateOperatorFeeIncreaseLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawNetworkEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080806040523461001657610a31908161001b8239f35b5f80fdfe604060808152600480361015610013575f80fd5b5f3560e01c80631f1f9fd5146106a85780633631983f146106075780636512447d1461053357806379e3e4e41461047a578063b4c9c408146103d6578063d2231741146101a6578063e39c6744146100fa5763eb60802214610073575f80fd5b346100f65760203660031901126100f657359067ffffffffffffffff82168092036100f6577f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17805467ffffffffffffffff191683179055519081527ff6b8a2b45d0a60381de51a7b980c4660d9e5b82db6e07a4d342bfc17a6ff96bf90602090a1005b5f80fd5b50346100f65760203660031901126100f6573567ffffffffffffffff81168082036100f6577f38552bed8df52ac76c5de6da688eafcda7d7b070f6c987f391a07dd69986d783926020927f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17907fffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffff67ffffffffffffffff60801b83549260801b16911617905551908152a1005b5090346100f6576020806003193601126100f6578235906101c68261087a565b6101ce61096a565b9067ffffffffffffffff91828116838316116103ae5782916101ef9161084c565b167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169067ffffffffffffffff198254161790557f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1580546bffffffff000000000000000043871b16906bffffffff000000000000000019161790555f8273ffffffffffffffffffffffffffffffffffffffff7fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b541660448751809481937fa9059cbb000000000000000000000000000000000000000000000000000000008352338c8401528960248401525af19182156103a4575f92610348575b505015610320577f370342c3bb9245e20bffe6dced02ba2fceca979701f881d5adc72d838e83f1c5935082519182523390820152a1005b5050517f045c4b02000000000000000000000000000000000000000000000000000000008152fd5b90915082903d841161039c575b601f8201601f191683019081118382101761038957839183918752810103126100f6575180151581036100f6575f806102e9565b604187634e487b7160e01b5f525260245ffd5b3d9150610355565b85513d5f823e3d90fd5b8686517ff4d678b8000000000000000000000000000000000000000000000000000000008152fd5b50346100f65760203660031901126100f6577fd363ab4392efaf967a89d8616cba1ff0c6f05a04c2f214671be365f0fab059609160209135906104188261087a565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa16907fffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffff67ffffffffffffffff60801b83549260801b16911617905551908152a1005b50346100f65760203660031901126100f6573567ffffffffffffffff81168082036100f6577f5fbd75d987b37490f91aa1909db948e7ff14c6ffb495b2f8e0b2334da9b192f1926020927f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169077ffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffffffff00000000000000000000000000000000000000000000000083549260c01b16911617905551908152a1005b5090346100f65760203660031901126100f657813567ffffffffffffffff8116928382036100f657620189c084106105e0577f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1680546fffffffffffffffff0000000000000000191683851b6fffffffffffffffff00000000000000001617905582518481527f42af14411036d7a50e5e92daf825781450fc8fac8fb65cbdb04720ff08efb84f90602090a1005b82517f6e6c9cac000000000000000000000000000000000000000000000000000000008152fd5b50346100f65760203660031901126100f6573567ffffffffffffffff81168082036100f6577f2fff7e5a48a4befc2c2be4d77e141f6d97907798977ce452429ec55c2658a342926020927f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17906fffffffffffffffff0000000000000000196fffffffffffffffff0000000000000000835492851b16911617905551908152a1005b5090346100f65760203660031901126100f65781357f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa159081549167ffffffffffffffff92838160801c1693806106fc61096a565b167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169067ffffffffffffffff1982541617905563ffffffff906bffffffff000000000000000043881b1690816bffffffff00000000000000001985161790838516430390438211610839579161079e6107a792847fffffffffffffffff000000000000000000000000000000000000000000000000958460801c16911661092f565b9060c01c61094e565b60c01b16906fffffffff00000000ffffffff0000000067ffffffffffffffff60801b6107d28861087a565b60801b1694161717904316171790556298968091828102928184041490151715610826577f8f49a76c5d617bd72673d92d3a019ff8f04f204536aae7a3d10e7ca85603f3cc935082519182526020820152a1005b601184634e487b7160e01b5f525260245ffd5b60118b634e487b7160e01b5f525260245ffd5b67ffffffffffffffff918216908216039190821161086657565b634e487b7160e01b5f52601160045260245ffd5b6a98968000000000000000008110156108eb57629896808082066108a75767ffffffffffffffff91041690565b606460405162461bcd60e51b815260206004820152601660248201527f4d617820707265636973696f6e206578636565646564000000000000000000006044820152fd5b606460405162461bcd60e51b815260206004820152601260248201527f4d61782076616c756520657863656564656400000000000000000000000000006044820152fd5b91909167ffffffffffffffff8080941691160291821691820361086657565b91909167ffffffffffffffff8080941691160191821161086657565b6109f867ffffffffffffffff6109f2817f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165416917f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1554906109e763ffffffff916109db838560401c1682431661084c565b908460801c169061092f565b9160201c169061092f565b9061094e565b9056fea26469706673582212202fc1bf69a1064c557b42f4a3acbd7d6ebb1172e42d7d5f1b75c60415edd1a95264736f6c63430008180033", + "deployedBytecode": "0x604060808152600480361015610013575f80fd5b5f3560e01c80631f1f9fd5146106a85780633631983f146106075780636512447d1461053357806379e3e4e41461047a578063b4c9c408146103d6578063d2231741146101a6578063e39c6744146100fa5763eb60802214610073575f80fd5b346100f65760203660031901126100f657359067ffffffffffffffff82168092036100f6577f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17805467ffffffffffffffff191683179055519081527ff6b8a2b45d0a60381de51a7b980c4660d9e5b82db6e07a4d342bfc17a6ff96bf90602090a1005b5f80fd5b50346100f65760203660031901126100f6573567ffffffffffffffff81168082036100f6577f38552bed8df52ac76c5de6da688eafcda7d7b070f6c987f391a07dd69986d783926020927f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17907fffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffff67ffffffffffffffff60801b83549260801b16911617905551908152a1005b5090346100f6576020806003193601126100f6578235906101c68261087a565b6101ce61096a565b9067ffffffffffffffff91828116838316116103ae5782916101ef9161084c565b167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169067ffffffffffffffff198254161790557f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1580546bffffffff000000000000000043871b16906bffffffff000000000000000019161790555f8273ffffffffffffffffffffffffffffffffffffffff7fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b541660448751809481937fa9059cbb000000000000000000000000000000000000000000000000000000008352338c8401528960248401525af19182156103a4575f92610348575b505015610320577f370342c3bb9245e20bffe6dced02ba2fceca979701f881d5adc72d838e83f1c5935082519182523390820152a1005b5050517f045c4b02000000000000000000000000000000000000000000000000000000008152fd5b90915082903d841161039c575b601f8201601f191683019081118382101761038957839183918752810103126100f6575180151581036100f6575f806102e9565b604187634e487b7160e01b5f525260245ffd5b3d9150610355565b85513d5f823e3d90fd5b8686517ff4d678b8000000000000000000000000000000000000000000000000000000008152fd5b50346100f65760203660031901126100f6577fd363ab4392efaf967a89d8616cba1ff0c6f05a04c2f214671be365f0fab059609160209135906104188261087a565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa16907fffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffff67ffffffffffffffff60801b83549260801b16911617905551908152a1005b50346100f65760203660031901126100f6573567ffffffffffffffff81168082036100f6577f5fbd75d987b37490f91aa1909db948e7ff14c6ffb495b2f8e0b2334da9b192f1926020927f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169077ffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffffffff00000000000000000000000000000000000000000000000083549260c01b16911617905551908152a1005b5090346100f65760203660031901126100f657813567ffffffffffffffff8116928382036100f657620189c084106105e0577f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1680546fffffffffffffffff0000000000000000191683851b6fffffffffffffffff00000000000000001617905582518481527f42af14411036d7a50e5e92daf825781450fc8fac8fb65cbdb04720ff08efb84f90602090a1005b82517f6e6c9cac000000000000000000000000000000000000000000000000000000008152fd5b50346100f65760203660031901126100f6573567ffffffffffffffff81168082036100f6577f2fff7e5a48a4befc2c2be4d77e141f6d97907798977ce452429ec55c2658a342926020927f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17906fffffffffffffffff0000000000000000196fffffffffffffffff0000000000000000835492851b16911617905551908152a1005b5090346100f65760203660031901126100f65781357f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa159081549167ffffffffffffffff92838160801c1693806106fc61096a565b167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169067ffffffffffffffff1982541617905563ffffffff906bffffffff000000000000000043881b1690816bffffffff00000000000000001985161790838516430390438211610839579161079e6107a792847fffffffffffffffff000000000000000000000000000000000000000000000000958460801c16911661092f565b9060c01c61094e565b60c01b16906fffffffff00000000ffffffff0000000067ffffffffffffffff60801b6107d28861087a565b60801b1694161717904316171790556298968091828102928184041490151715610826577f8f49a76c5d617bd72673d92d3a019ff8f04f204536aae7a3d10e7ca85603f3cc935082519182526020820152a1005b601184634e487b7160e01b5f525260245ffd5b60118b634e487b7160e01b5f525260245ffd5b67ffffffffffffffff918216908216039190821161086657565b634e487b7160e01b5f52601160045260245ffd5b6a98968000000000000000008110156108eb57629896808082066108a75767ffffffffffffffff91041690565b606460405162461bcd60e51b815260206004820152601660248201527f4d617820707265636973696f6e206578636565646564000000000000000000006044820152fd5b606460405162461bcd60e51b815260206004820152601260248201527f4d61782076616c756520657863656564656400000000000000000000000000006044820152fd5b91909167ffffffffffffffff8080941691160291821691820361086657565b91909167ffffffffffffffff8080941691160191821161086657565b6109f867ffffffffffffffff6109f2817f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165416917f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1554906109e763ffffffff916109db838560401c1682431661084c565b908460801c169061092f565b9160201c169061092f565b9061094e565b9056fea26469706673582212202fc1bf69a1064c557b42f4a3acbd7d6ebb1172e42d7d5f1b75c60415edd1a95264736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/modules/SSVDAO.sol", + "buildInfoId": "solc-0_8_24-dc3260967516f45b660c2554111e2a58f5f3b385" +} \ No newline at end of file diff --git a/test/setup/artifacts/SSVNetworkLegacy.json b/test/setup/artifacts/SSVNetworkLegacy.json new file mode 100644 index 000000000..a6adc6a7a --- /dev/null +++ b/test/setup/artifacts/SSVNetworkLegacy.json @@ -0,0 +1,2156 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "SSVNetwork", + "sourceName": "contracts/SSVNetwork.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterDeposited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterReactivated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ClusterWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "DeclareOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "ExecuteOperatorFeePeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipientAddress", + "type": "address" + } + ], + "name": "FeeRecipientAddressUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "LiquidationThresholdPeriodUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "MinimumLiquidationCollateralUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "enum SSVModules", + "name": "moduleId", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "address", + "name": "moduleAddress", + "type": "address" + } + ], + "name": "ModuleUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "NetworkEarningsWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "NetworkFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorFeeDeclarationCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeDeclared", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "value", + "type": "uint64" + } + ], + "name": "OperatorFeeIncreaseLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "maxFee", + "type": "uint64" + } + ], + "name": "OperatorMaximumFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bool", + "name": "toPrivate", + "type": "bool" + } + ], + "name": "OperatorPrivacyStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "OperatorWhitelistingContractUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "OperatorWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "shares", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorExited", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "ValidatorRemoved", + "type": "event" + }, + { + "stateMutability": "nonpayable", + "type": "fallback" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "bulkExitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes[]", + "name": "sharesData", + "type": "bytes[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRegisterValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "publicKeys", + "type": "bytes[]" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "bulkRemoveValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "cancelDeclaredOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "declareOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "executeOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "exitValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getVersion", + "outputs": [ + { + "internalType": "string", + "name": "version", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token_", + "type": "address" + }, + { + "internalType": "contract ISSVOperators", + "name": "ssvOperators_", + "type": "address" + }, + { + "internalType": "contract ISSVClusters", + "name": "ssvClusters_", + "type": "address" + }, + { + "internalType": "contract ISSVDAO", + "name": "ssvDAO_", + "type": "address" + }, + { + "internalType": "contract ISSVViews", + "name": "ssvViews_", + "type": "address" + }, + { + "internalType": "uint64", + "name": "minimumBlocksBeforeLiquidation_", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "minimumLiquidationCollateral_", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validatorsPerOperatorLimit_", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "declareOperatorFeePeriod_", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "executeOperatorFeePeriod_", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "operatorMaxFeeIncrease_", + "type": "uint64" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "liquidate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "reactivate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "reduceOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "setPrivate", + "type": "bool" + } + ], + "name": "registerOperator", + "outputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "bytes", + "name": "sharesData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "registerValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "removeOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "removeOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "removeOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "removeValidator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipientAddress", + "type": "address" + } + ], + "name": "setFeeRecipientAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPrivateUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPublicUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "contract ISSVWhitelistingContract", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "setOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "setOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateDeclareOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timeInSeconds", + "type": "uint64" + } + ], + "name": "updateExecuteOperatorFeePeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "blocks", + "type": "uint64" + } + ], + "name": "updateLiquidationThresholdPeriod", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "maxFee", + "type": "uint64" + } + ], + "name": "updateMaximumOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "updateMinimumLiquidationCollateral", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum SSVModules", + "name": "moduleId", + "type": "uint8" + }, + { + "internalType": "address", + "name": "moduleAddress", + "type": "address" + } + ], + "name": "updateModule", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "updateNetworkFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "percentage", + "type": "uint64" + } + ], + "name": "updateOperatorFeeIncreaseLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawNetworkEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60a080604052346100e157306080525f549060ff8260081c1661008f575060ff80821603610055575b6040516120e190816100e682396080518181816108fb01528181610c1d0152818161147c015261174e0152f35b60ff90811916175f557f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498602060405160ff8152a15f610028565b62461bcd60e51b815260206004820152602760248201527f496e697469616c697a61626c653a20636f6e747261637420697320696e697469604482015266616c697a696e6760c81b6064820152608490fd5b5f80fdfe60806040526004361015610019575b3415611bf6575b5f80fd5b5f3560e01c806306e8fb9c146102925780630d8e6e2c1461028d57806312b3fc1914610288578063190d82e4146102335780631f1f9fd51461021a57806322f18bf51461028357806323d68a6d1461023d5780632e168e0e1461023d57806332afd02f1461027e57806335f63767146102335780633631983f146102065780633659cfe6146102795780633877322b146102745780634ad00e54146102425780634b2fd45e146102605780634bc93b641461023d5780634f1ef2861461026f57806352d1902d1461026a5780635aed1142146102655780635d06ecb4146102605780635fec6dd01461025b5780636512447d14610206578063686e682c1461025b5780636a31cf1d14610256578063715018a61461025157806379ba50971461024c57806379e3e4e4146102065780637dc24d5214610247578063822124c1146102425780638932cee01461023d5780638da5cb5b14610238578063b317c35f14610233578063b4c9c4081461021a578063bc26e7e51461022e578063bf0f2fb214610229578063c626c3c614610224578063c9bbc9fa1461021f578063d22317411461021a578063dbcdc2cc14610215578063e30c397814610210578063e39c674414610206578063e3e324b01461020b578063eb608022146102065763f2fde38b0361000e57611237565b61089f565b611140565b61111a565b6110c6565b610723565b611084565b610fe7565b610fab565b610f08565b610704565b610ee2565b6107e6565b610b40565b610e9f565b610e0a565b610da8565b610d93565b610d0a565b610b55565b610cd8565b610c03565b610b88565b610a84565b6108d2565b61084a565b61077f565b6106ad565b610641565b610541565b9181601f840112156100155782359167ffffffffffffffff8311610015576020838186019501011161001557565b9181601f840112156100155782359167ffffffffffffffff8311610015576020808501948460051b01011161001557565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761032657604052565b6102f6565b6020810190811067ffffffffffffffff82111761032657604052565b6060810190811067ffffffffffffffff82111761032657604052565b90601f8019910116810190811067ffffffffffffffff82111761032657604052565b6044359063ffffffff8216820361001557565b6004359067ffffffffffffffff8216820361001557565b610104359067ffffffffffffffff8216820361001557565b610124359067ffffffffffffffff8216820361001557565b610144359067ffffffffffffffff8216820361001557565b6084359067ffffffffffffffff8216820361001557565b6064359067ffffffffffffffff8216820361001557565b60a4359067ffffffffffffffff8216820361001557565b60e43590811515820361001557565b60a43590811515820361001557565b60c43590811515820361001557565b60a090608319011261001557604051906104828261030a565b8160843563ffffffff8116810361001557815267ffffffffffffffff60a435818116810361001557602083015260c43590811681036100155760408201526104c861043c565b6060820152608061010435910152565b60a090604319011261001557604051906104f18261030a565b8160443563ffffffff8116810361001557815260643567ffffffffffffffff811681036100155760208201526105256103f7565b604082015261053261044b565b6060820152608060c435910152565b34610015576101203660031901126100155767ffffffffffffffff60043581811161001557610574903690600401610297565b50506024358181116100155761058e9036906004016102c5565b5050604435908111610015576105a8903690600401610297565b50506105b336610469565b5060015f525f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed5b5416611c37565b602080825282518183018190529093925f5b82811061062d57505060409293505f838284010152601f8019910116010190565b81810186015184820160400152850161060c565b34610015575f36600319011261001557604051604081019080821067ffffffffffffffff831117610326576106a991604052600681527f76312e322e3000000000000000000000000000000000000000000000000000006020820152604051918291826105fa565b0390f35b346100155760e03660031901126100155767ffffffffffffffff600435818111610015576106df903690600401610297565b5050602435908111610015576106f99036906004016102c5565b50506105b3366104d8565b346100155760403660031901126100155761071d610398565b50611305565b346100155760203660031901126100155761073c611c54565b60025f525f8051602061208c8339815191526020526001600160a01b037f22d7db5ca5cd9f397c046990f780ecc0cee9b69ff4f64a0786fd367cb45a9a396105f3565b34610015576101203660031901126100155767ffffffffffffffff600435818111610015576107b29036906004016102c5565b5050602435818111610015576107cc9036906004016102c5565b5050604435908111610015576105a89036906004016102c5565b346100155760203660031901126100155761071d610398565b60406003198201126100155767ffffffffffffffff91600435838111610015578261082c916004016102c5565b9390939260243591821161001557610846916004016102c5565b9091565b3461001557610858366107ff565b505060015f5250505f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed6105f3565b34610015576020366003190112610015576108b8610398565b5061073c611c54565b6001600160a01b0381160361001557565b34610015576020366003190112610015576004356108ef816108c1565b6001600160a01b0390817f0000000000000000000000000000000000000000000000000000000000000000169161092883301415611349565b6109577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc9382855416146113ba565b61095f611c54565b6040519061096c8261032b565b5f82527f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff16156109a75750506109a59150611f34565b005b6020600491604094939451928380926352d1902d60e01b825286165afa5f9181610a53575b50610a405760405162461bcd60e51b815260206004820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152608490fd5b0390fd5b6109a593610a4e9114611cbb565b611dec565b610a7691925060203d602011610a7d575b610a6e8183610363565b810190611cac565b905f6109cc565b503d610a64565b346100155760403660031901126100155767ffffffffffffffff60043581811161001557610ab6903690600401610297565b505060243590811161001557610ad09036906004016102c5565b505060015f525f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed6105f3565b6020600319820112610015576004359067ffffffffffffffff821161001557610846916004016102c5565b3461001557610b4e36610b15565b5050611305565b3461001557610b63366107ff565b5050505061142b565b67ffffffffffffffff811161032657601f01601f191660200190565b604036600319011261001557600435610ba0816108c1565b6024359067ffffffffffffffff8211610015573660238301121561001557816004013590610bcd82610b6c565b91610bdb6040519384610363565b8083523660248286010111610015576020815f9260246109a597018387013784010152611470565b34610015575f366003190112610015576001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163003610c6e576040517f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8152602090f35b608460405162461bcd60e51b815260206004820152603860248201527f555550535570677261646561626c653a206d757374206e6f742062652063616c60448201527f6c6564207468726f7567682064656c656761746563616c6c00000000000000006064820152fd5b346100155760e03660031901126100155767ffffffffffffffff600435818111610015576106df9036906004016102c5565b346100155760e03660031901126100155760043567ffffffffffffffff811161001557610d3b9036906004016102c5565b505060a036604319011261001557604051610d558161030a565b610d5d610385565b8152610d6761040e565b6020820152610d746103f7565b6040820152610d8161044b565b6060820152608060c4359101526112c2565b3461001557610da136610b15565b505061142b565b34610015575f36600319011261001557610dc0611c54565b5f6001600160a01b036001600160a01b03198060c9541660c955609754908116609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a3005b34610015575f36600319011261001557336001600160a01b0360c9541603610e35576109a533611d2c565b608460405162461bcd60e51b815260206004820152602960248201527f4f776e61626c6532537465703a2063616c6c6572206973206e6f74207468652060448201527f6e6577206f776e657200000000000000000000000000000000000000000000006064820152fd5b346100155760403660031901126100155760043567ffffffffffffffff811161001557610ed09036906004016102c5565b5050610edd6024356108c1565b61142b565b34610015575f3660031901126100155760206001600160a01b0360975416604051908152f35b346100155761010036600319011261001557610f256004356108c1565b67ffffffffffffffff60243581811161001557610f469036906004016102c5565b505060a03660631901126100155760405190610f618261030a565b60643563ffffffff811681036100155782526084359081168103610015576020820152610f8c610425565b6040820152610f9961045a565b6060820152608060e4359101526112c2565b346100155760e036600319011261001557610fc76004356108c1565b60243567ffffffffffffffff8111610015576106f99036906004016102c5565b346100155761016036600319011261001557600435611005816108c1565b602435611011816108c1565b60443561101d816108c1565b606435611029816108c1565b60843593611036856108c1565b60a4359067ffffffffffffffff821682036100155760e4359163ffffffff83168303610015576109a5966110686103af565b946110716103c7565b9661107a6103df565b9860c435956115e1565b346100155760603660031901126100155760043567ffffffffffffffff8111610015576110b5903690600401610297565b505060443580151514611305575f80fd5b34610015576020366003190112610015576004356110e3816108c1565b6001600160a01b03604051911681527f259235c230d57def1521657e7c7951d3b385e76193378bc87ef6b56bc2ec354860203392a2005b34610015575f3660031901126100155760206001600160a01b0360c95416604051908152f35b3461001557604036600319011261001557600435600581101561001557602435611169816108c1565b611171611c54565b61117a81611fe1565b15611203576111fe7ffdf54bf052398eb41c923eb1bd596351c5e72b99959d1ca529a7f13c0a2503d791835f525f8051602061208c8339815191526020526111db8160405f20906001600160a01b03166001600160a01b0319825416179055565b6111e4846112a4565b6040516001600160a01b0390911681529081906020820190565b0390a2005b60248260ff604051917f208bb85d000000000000000000000000000000000000000000000000000000008352166004820152fd5b3461001557602036600319011261001557600435611254816108c1565b61125c611c54565b6001600160a01b0380911690816001600160a01b031960c954161760c955609754167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e227005f80a3005b600511156112ae57565b634e487b7160e01b5f52602160045260245ffd5b60015f525f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed6105f3565b5f80525f8051602061208c8339815191526020527f2cdb7a1c6ad37bb9b0925403a9829caec6f24337bf74435442ec8aa10bcd1b25546001600160a01b0316611c37565b1561135057565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f64656c656761746563616c6c00000000000000000000000000000000000000006064820152fd5b156113c157565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f6163746976652070726f787900000000000000000000000000000000000000006064820152fd5b60045f525f8051602061208c8339815191526020527fd6ef2100888bc9cc088775bc677f689e8e9e29ab42f4b1daf23655d4e4e41e97546001600160a01b0316611c37565b6001600160a01b0391827f000000000000000000000000000000000000000000000000000000000000000016926114a984301415611349565b6114d87f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc9482865416146113ba565b6114e0611c54565b7f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff16156115185750506115169150611f34565b565b6020600491604094939451928380926352d1902d60e01b825286165afa5f91816115c0575b506115ad5760405162461bcd60e51b815260206004820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152608490fd5b611516936115bb9114611cbb565b611ee2565b6115da91925060203d602011610a7d57610a6e8183610363565b905f61153d565b989694929099979593915f549a60ff8c60081c1615809c819d61170e575b81156116ee575b50156116845761162a9a8c611621600160ff195f5416175f55565b61166d5761171c565b61163057565b61163e61ff00195f54165f55565b604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb384740249890602090a1565b61167f61010061ff00195f5416175f55565b61171c565b608460405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152fd5b303b15915081611700575b505f611606565b6001915060ff16145f6116f9565b600160ff82161091506115ff565b611812611918946118a06118dc9361196b999895969e9d9b9c9a9e6118646001600160a01b038099819782956117a4847f00000000000000000000000000000000000000000000000000000000000000001661177a81301415611349565b857f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5416146113ba565b6117bd60ff5f5460081c166117b881611d7b565b611d7b565b6117c633611d2c565b6117d660ff5f5460081c16611d7b565b6001600160a01b037fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b91166001600160a01b0319825416179055565b5f80525f8051602061208c833981519152602052167f2cdb7a1c6ad37bb9b0925403a9829caec6f24337bf74435442ec8aa10bcd1b255b906001600160a01b03166001600160a01b0319825416179055565b60015f525f8051602061208c833981519152602052167f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed611849565b60025f525f8051602061208c833981519152602052167f22d7db5ca5cd9f397c046990f780ecc0cee9b69ff4f64a0786fd367cb45a9a39611849565b60035f525f8051602061208c833981519152602052167f4c491f594fde295748e6cbe50b70b22eac7ad67882df72950176ca2461652d60611849565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa16906fffffffffffffffff0000000000000000196fffffffffffffffff000000000000000083549260401b169116179055565b6a9896800000000000000000851015611bb2576298968090818606611b6e57611a73611b1b94611a1167ffffffffffffffff611add956115169a04167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa16907fffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffff77ffffffffffffffff0000000000000000000000000000000083549260801b169116179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15907fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff6fffffffff00000000000000000000000083549260601b169116179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169077ffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffffffff00000000000000000000000000000000000000000000000083549260c01b169116179055565b67ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17911667ffffffffffffffff19825416179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17906fffffffffffffffff0000000000000000196fffffffffffffffff000000000000000083549260401b169116179055565b606460405162461bcd60e51b815260206004820152601660248201527f4d617820707265636973696f6e206578636565646564000000000000000000006044820152fd5b606460405162461bcd60e51b815260206004820152601260248201527f4d61782076616c756520657863656564656400000000000000000000000000006044820152fd5b60035f525f8051602061208c8339815191526020526001600160a01b037f4c491f594fde295748e6cbe50b70b22eac7ad67882df72950176ca2461652d6054165b5f8091368280378136915af43d5f803e15611c50573d5ff35b3d5ffd5b6001600160a01b03609754163303611c6857565b606460405162461bcd60e51b815260206004820152602060248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152fd5b90816020910312610015575190565b15611cc257565b608460405162461bcd60e51b815260206004820152602960248201527f45524331393637557067726164653a20756e737570706f727465642070726f7860448201527f6961626c655555494400000000000000000000000000000000000000000000006064820152fd5b6001600160a01b0319908160c9541660c9556097546001600160a01b038092168093821617609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b15611d8257565b608460405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152fd5b90611df682611f34565b6001600160a01b0382167fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2805115801590611edb575b611e37575050565b611ed0915f8060405193611e4a85610347565b602785527f416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c60208601527f206661696c6564000000000000000000000000000000000000000000000000006040860152602081519101845af43d15611ed3573d91611eb483610b6c565b92611ec26040519485610363565b83523d5f602085013e611ffb565b50565b606091611ffb565b505f611e2f565b90611eec82611f34565b6001600160a01b0382167fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2805115801590611f2c57611e37575050565b506001611e2f565b803b15611f77576001600160a01b037f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc91166001600160a01b0319825416179055565b608460405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e7472616374000000000000000000000000000000000000006064820152fd5b6001600160a01b03811615611ff6573b151590565b505f90565b9192901561205c575081511561200f575090565b3b156120185790565b606460405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b82519091501561206f5750805190602001fd5b610a3c9060405191829162461bcd60e51b8352600483016105fa56fed56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed107a2646970667358221220ee3985e7be0ec787e88c104d4be8942fa9409ef5fa0dab267f54c883c3bf598764736f6c63430008180033", + "deployedBytecode": "0x60806040526004361015610019575b3415611bf6575b5f80fd5b5f3560e01c806306e8fb9c146102925780630d8e6e2c1461028d57806312b3fc1914610288578063190d82e4146102335780631f1f9fd51461021a57806322f18bf51461028357806323d68a6d1461023d5780632e168e0e1461023d57806332afd02f1461027e57806335f63767146102335780633631983f146102065780633659cfe6146102795780633877322b146102745780634ad00e54146102425780634b2fd45e146102605780634bc93b641461023d5780634f1ef2861461026f57806352d1902d1461026a5780635aed1142146102655780635d06ecb4146102605780635fec6dd01461025b5780636512447d14610206578063686e682c1461025b5780636a31cf1d14610256578063715018a61461025157806379ba50971461024c57806379e3e4e4146102065780637dc24d5214610247578063822124c1146102425780638932cee01461023d5780638da5cb5b14610238578063b317c35f14610233578063b4c9c4081461021a578063bc26e7e51461022e578063bf0f2fb214610229578063c626c3c614610224578063c9bbc9fa1461021f578063d22317411461021a578063dbcdc2cc14610215578063e30c397814610210578063e39c674414610206578063e3e324b01461020b578063eb608022146102065763f2fde38b0361000e57611237565b61089f565b611140565b61111a565b6110c6565b610723565b611084565b610fe7565b610fab565b610f08565b610704565b610ee2565b6107e6565b610b40565b610e9f565b610e0a565b610da8565b610d93565b610d0a565b610b55565b610cd8565b610c03565b610b88565b610a84565b6108d2565b61084a565b61077f565b6106ad565b610641565b610541565b9181601f840112156100155782359167ffffffffffffffff8311610015576020838186019501011161001557565b9181601f840112156100155782359167ffffffffffffffff8311610015576020808501948460051b01011161001557565b634e487b7160e01b5f52604160045260245ffd5b60a0810190811067ffffffffffffffff82111761032657604052565b6102f6565b6020810190811067ffffffffffffffff82111761032657604052565b6060810190811067ffffffffffffffff82111761032657604052565b90601f8019910116810190811067ffffffffffffffff82111761032657604052565b6044359063ffffffff8216820361001557565b6004359067ffffffffffffffff8216820361001557565b610104359067ffffffffffffffff8216820361001557565b610124359067ffffffffffffffff8216820361001557565b610144359067ffffffffffffffff8216820361001557565b6084359067ffffffffffffffff8216820361001557565b6064359067ffffffffffffffff8216820361001557565b60a4359067ffffffffffffffff8216820361001557565b60e43590811515820361001557565b60a43590811515820361001557565b60c43590811515820361001557565b60a090608319011261001557604051906104828261030a565b8160843563ffffffff8116810361001557815267ffffffffffffffff60a435818116810361001557602083015260c43590811681036100155760408201526104c861043c565b6060820152608061010435910152565b60a090604319011261001557604051906104f18261030a565b8160443563ffffffff8116810361001557815260643567ffffffffffffffff811681036100155760208201526105256103f7565b604082015261053261044b565b6060820152608060c435910152565b34610015576101203660031901126100155767ffffffffffffffff60043581811161001557610574903690600401610297565b50506024358181116100155761058e9036906004016102c5565b5050604435908111610015576105a8903690600401610297565b50506105b336610469565b5060015f525f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed5b5416611c37565b602080825282518183018190529093925f5b82811061062d57505060409293505f838284010152601f8019910116010190565b81810186015184820160400152850161060c565b34610015575f36600319011261001557604051604081019080821067ffffffffffffffff831117610326576106a991604052600681527f76312e322e3000000000000000000000000000000000000000000000000000006020820152604051918291826105fa565b0390f35b346100155760e03660031901126100155767ffffffffffffffff600435818111610015576106df903690600401610297565b5050602435908111610015576106f99036906004016102c5565b50506105b3366104d8565b346100155760403660031901126100155761071d610398565b50611305565b346100155760203660031901126100155761073c611c54565b60025f525f8051602061208c8339815191526020526001600160a01b037f22d7db5ca5cd9f397c046990f780ecc0cee9b69ff4f64a0786fd367cb45a9a396105f3565b34610015576101203660031901126100155767ffffffffffffffff600435818111610015576107b29036906004016102c5565b5050602435818111610015576107cc9036906004016102c5565b5050604435908111610015576105a89036906004016102c5565b346100155760203660031901126100155761071d610398565b60406003198201126100155767ffffffffffffffff91600435838111610015578261082c916004016102c5565b9390939260243591821161001557610846916004016102c5565b9091565b3461001557610858366107ff565b505060015f5250505f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed6105f3565b34610015576020366003190112610015576108b8610398565b5061073c611c54565b6001600160a01b0381160361001557565b34610015576020366003190112610015576004356108ef816108c1565b6001600160a01b0390817f0000000000000000000000000000000000000000000000000000000000000000169161092883301415611349565b6109577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc9382855416146113ba565b61095f611c54565b6040519061096c8261032b565b5f82527f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff16156109a75750506109a59150611f34565b005b6020600491604094939451928380926352d1902d60e01b825286165afa5f9181610a53575b50610a405760405162461bcd60e51b815260206004820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152608490fd5b0390fd5b6109a593610a4e9114611cbb565b611dec565b610a7691925060203d602011610a7d575b610a6e8183610363565b810190611cac565b905f6109cc565b503d610a64565b346100155760403660031901126100155767ffffffffffffffff60043581811161001557610ab6903690600401610297565b505060243590811161001557610ad09036906004016102c5565b505060015f525f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed6105f3565b6020600319820112610015576004359067ffffffffffffffff821161001557610846916004016102c5565b3461001557610b4e36610b15565b5050611305565b3461001557610b63366107ff565b5050505061142b565b67ffffffffffffffff811161032657601f01601f191660200190565b604036600319011261001557600435610ba0816108c1565b6024359067ffffffffffffffff8211610015573660238301121561001557816004013590610bcd82610b6c565b91610bdb6040519384610363565b8083523660248286010111610015576020815f9260246109a597018387013784010152611470565b34610015575f366003190112610015576001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163003610c6e576040517f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8152602090f35b608460405162461bcd60e51b815260206004820152603860248201527f555550535570677261646561626c653a206d757374206e6f742062652063616c60448201527f6c6564207468726f7567682064656c656761746563616c6c00000000000000006064820152fd5b346100155760e03660031901126100155767ffffffffffffffff600435818111610015576106df9036906004016102c5565b346100155760e03660031901126100155760043567ffffffffffffffff811161001557610d3b9036906004016102c5565b505060a036604319011261001557604051610d558161030a565b610d5d610385565b8152610d6761040e565b6020820152610d746103f7565b6040820152610d8161044b565b6060820152608060c4359101526112c2565b3461001557610da136610b15565b505061142b565b34610015575f36600319011261001557610dc0611c54565b5f6001600160a01b036001600160a01b03198060c9541660c955609754908116609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a3005b34610015575f36600319011261001557336001600160a01b0360c9541603610e35576109a533611d2c565b608460405162461bcd60e51b815260206004820152602960248201527f4f776e61626c6532537465703a2063616c6c6572206973206e6f74207468652060448201527f6e6577206f776e657200000000000000000000000000000000000000000000006064820152fd5b346100155760403660031901126100155760043567ffffffffffffffff811161001557610ed09036906004016102c5565b5050610edd6024356108c1565b61142b565b34610015575f3660031901126100155760206001600160a01b0360975416604051908152f35b346100155761010036600319011261001557610f256004356108c1565b67ffffffffffffffff60243581811161001557610f469036906004016102c5565b505060a03660631901126100155760405190610f618261030a565b60643563ffffffff811681036100155782526084359081168103610015576020820152610f8c610425565b6040820152610f9961045a565b6060820152608060e4359101526112c2565b346100155760e036600319011261001557610fc76004356108c1565b60243567ffffffffffffffff8111610015576106f99036906004016102c5565b346100155761016036600319011261001557600435611005816108c1565b602435611011816108c1565b60443561101d816108c1565b606435611029816108c1565b60843593611036856108c1565b60a4359067ffffffffffffffff821682036100155760e4359163ffffffff83168303610015576109a5966110686103af565b946110716103c7565b9661107a6103df565b9860c435956115e1565b346100155760603660031901126100155760043567ffffffffffffffff8111610015576110b5903690600401610297565b505060443580151514611305575f80fd5b34610015576020366003190112610015576004356110e3816108c1565b6001600160a01b03604051911681527f259235c230d57def1521657e7c7951d3b385e76193378bc87ef6b56bc2ec354860203392a2005b34610015575f3660031901126100155760206001600160a01b0360c95416604051908152f35b3461001557604036600319011261001557600435600581101561001557602435611169816108c1565b611171611c54565b61117a81611fe1565b15611203576111fe7ffdf54bf052398eb41c923eb1bd596351c5e72b99959d1ca529a7f13c0a2503d791835f525f8051602061208c8339815191526020526111db8160405f20906001600160a01b03166001600160a01b0319825416179055565b6111e4846112a4565b6040516001600160a01b0390911681529081906020820190565b0390a2005b60248260ff604051917f208bb85d000000000000000000000000000000000000000000000000000000008352166004820152fd5b3461001557602036600319011261001557600435611254816108c1565b61125c611c54565b6001600160a01b0380911690816001600160a01b031960c954161760c955609754167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e227005f80a3005b600511156112ae57565b634e487b7160e01b5f52602160045260245ffd5b60015f525f8051602061208c8339815191526020526001600160a01b037f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed6105f3565b5f80525f8051602061208c8339815191526020527f2cdb7a1c6ad37bb9b0925403a9829caec6f24337bf74435442ec8aa10bcd1b25546001600160a01b0316611c37565b1561135057565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f64656c656761746563616c6c00000000000000000000000000000000000000006064820152fd5b156113c157565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f6163746976652070726f787900000000000000000000000000000000000000006064820152fd5b60045f525f8051602061208c8339815191526020527fd6ef2100888bc9cc088775bc677f689e8e9e29ab42f4b1daf23655d4e4e41e97546001600160a01b0316611c37565b6001600160a01b0391827f000000000000000000000000000000000000000000000000000000000000000016926114a984301415611349565b6114d87f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc9482865416146113ba565b6114e0611c54565b7f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff16156115185750506115169150611f34565b565b6020600491604094939451928380926352d1902d60e01b825286165afa5f91816115c0575b506115ad5760405162461bcd60e51b815260206004820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152608490fd5b611516936115bb9114611cbb565b611ee2565b6115da91925060203d602011610a7d57610a6e8183610363565b905f61153d565b989694929099979593915f549a60ff8c60081c1615809c819d61170e575b81156116ee575b50156116845761162a9a8c611621600160ff195f5416175f55565b61166d5761171c565b61163057565b61163e61ff00195f54165f55565b604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb384740249890602090a1565b61167f61010061ff00195f5416175f55565b61171c565b608460405162461bcd60e51b815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152fd5b303b15915081611700575b505f611606565b6001915060ff16145f6116f9565b600160ff82161091506115ff565b611812611918946118a06118dc9361196b999895969e9d9b9c9a9e6118646001600160a01b038099819782956117a4847f00000000000000000000000000000000000000000000000000000000000000001661177a81301415611349565b857f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5416146113ba565b6117bd60ff5f5460081c166117b881611d7b565b611d7b565b6117c633611d2c565b6117d660ff5f5460081c16611d7b565b6001600160a01b037fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b91166001600160a01b0319825416179055565b5f80525f8051602061208c833981519152602052167f2cdb7a1c6ad37bb9b0925403a9829caec6f24337bf74435442ec8aa10bcd1b255b906001600160a01b03166001600160a01b0319825416179055565b60015f525f8051602061208c833981519152602052167f9d576197d12ecb32276a0e3ac1e9819a88be30ffacef2ca11d34efaca02bbeed611849565b60025f525f8051602061208c833981519152602052167f22d7db5ca5cd9f397c046990f780ecc0cee9b69ff4f64a0786fd367cb45a9a39611849565b60035f525f8051602061208c833981519152602052167f4c491f594fde295748e6cbe50b70b22eac7ad67882df72950176ca2461652d60611849565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa16906fffffffffffffffff0000000000000000196fffffffffffffffff000000000000000083549260401b169116179055565b6a9896800000000000000000851015611bb2576298968090818606611b6e57611a73611b1b94611a1167ffffffffffffffff611add956115169a04167f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa16907fffffffffffffffff0000000000000000ffffffffffffffffffffffffffffffff77ffffffffffffffff0000000000000000000000000000000083549260801b169116179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa15907fffffffffffffffffffffffffffffffff00000000ffffffffffffffffffffffff6fffffffff00000000000000000000000083549260601b169116179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa169077ffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffffffff00000000000000000000000000000000000000000000000083549260c01b169116179055565b67ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17911667ffffffffffffffff19825416179055565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17906fffffffffffffffff0000000000000000196fffffffffffffffff000000000000000083549260401b169116179055565b606460405162461bcd60e51b815260206004820152601660248201527f4d617820707265636973696f6e206578636565646564000000000000000000006044820152fd5b606460405162461bcd60e51b815260206004820152601260248201527f4d61782076616c756520657863656564656400000000000000000000000000006044820152fd5b60035f525f8051602061208c8339815191526020526001600160a01b037f4c491f594fde295748e6cbe50b70b22eac7ad67882df72950176ca2461652d6054165b5f8091368280378136915af43d5f803e15611c50573d5ff35b3d5ffd5b6001600160a01b03609754163303611c6857565b606460405162461bcd60e51b815260206004820152602060248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152fd5b90816020910312610015575190565b15611cc257565b608460405162461bcd60e51b815260206004820152602960248201527f45524331393637557067726164653a20756e737570706f727465642070726f7860448201527f6961626c655555494400000000000000000000000000000000000000000000006064820152fd5b6001600160a01b0319908160c9541660c9556097546001600160a01b038092168093821617609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b15611d8257565b608460405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152fd5b90611df682611f34565b6001600160a01b0382167fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2805115801590611edb575b611e37575050565b611ed0915f8060405193611e4a85610347565b602785527f416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c60208601527f206661696c6564000000000000000000000000000000000000000000000000006040860152602081519101845af43d15611ed3573d91611eb483610b6c565b92611ec26040519485610363565b83523d5f602085013e611ffb565b50565b606091611ffb565b505f611e2f565b90611eec82611f34565b6001600160a01b0382167fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a2805115801590611f2c57611e37575050565b506001611e2f565b803b15611f77576001600160a01b037f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc91166001600160a01b0319825416179055565b608460405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e7472616374000000000000000000000000000000000000006064820152fd5b6001600160a01b03811615611ff6573b151590565b505f90565b9192901561205c575081511561200f575090565b3b156120185790565b606460405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b82519091501561206f5750805190602001fd5b610a3c9060405191829162461bcd60e51b8352600483016105fa56fed56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed107a2646970667358221220ee3985e7be0ec787e88c104d4be8942fa9409ef5fa0dab267f54c883c3bf598764736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": { + "794": [ + { + "length": 32, + "start": 2299 + }, + { + "length": 32, + "start": 3101 + }, + { + "length": 32, + "start": 5244 + }, + { + "length": 32, + "start": 5966 + } + ] + }, + "inputSourceName": "project/contracts/SSVNetwork.sol", + "buildInfoId": "solc-0_8_24-dc3260967516f45b660c2554111e2a58f5f3b385" +} \ No newline at end of file diff --git a/test/setup/artifacts/SSVNetworkViewsLegacy.json b/test/setup/artifacts/SSVNetworkViewsLegacy.json new file mode 100644 index 000000000..329f3e4cb --- /dev/null +++ b/test/setup/artifacts/SSVNetworkViewsLegacy.json @@ -0,0 +1,1102 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "SSVNetworkViews", + "sourceName": "contracts/SSVNetworkViews.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBurnRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidationThresholdPeriod", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaximumOperatorFee", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumLiquidationCollateral", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkValidatorsCount", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorById", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "", + "type": "uint32" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorDeclaredFee", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "name": "getOperatorEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeeIncreaseLimit", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeePeriods", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "getValidator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidatorsPerOperatorLimit", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address", + "name": "whitelistedAddress", + "type": "address" + } + ], + "name": "getWhitelistedOperators", + "outputs": [ + { + "internalType": "uint64[]", + "name": "whitelistedOperatorIds", + "type": "uint64[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract ISSVViews", + "name": "ssvNetwork_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addressToCheck", + "type": "address" + }, + { + "internalType": "uint256", + "name": "operatorId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "isAddressWhitelistedInWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "isWhitelisted", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidated", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "isWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "ssvNetwork", + "outputs": [ + { + "internalType": "contract ISSVViews", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } + ], + "bytecode": "0x60a080604052346100e157306080525f549060ff8260081c1661008f575060ff80821603610055575b604051611fe790816100e6823960805181818161055b01528181610f90015281816110c401526115060152f35b60ff90811916175f557f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498602060405160ff8152a15f610028565b62461bcd60e51b815260206004820152602760248201527f496e697469616c697a61626c653a20636f6e747261637420697320696e697469604482015266616c697a696e6760c81b6064820152608490fd5b5f80fdfe6080604081815260049182361015610015575f80fd5b5f3560e01c90816303b3d436146118b2575080630d8e6e2c146117ca57806310d04858146117a357806314cb9d7b1461173c57806316cff008146116e45780633659cfe6146114dc5780633e2ec1601461140757806346e6d917146113745780634f1ef2861461104757806352d1902d14610f745780635ba3d62a14610f1357806368465f7d14610ea85780636d0db0e414610e2e578063715018a614610dcc578063777915cb14610d6b57806379ba509714610cd45780638da5cb5b14610cad5780639040f7c314610c425780639568f9d914610b985780639ad3c74514610aee578063a694695b14610a3c578063a9cf9eec146108ae578063bac69e6f14610801578063be3f058e146106be578063c4d66de8146104fb578063ca162e5e146104a3578063df02ef7f146103e7578063e30c3978146103c0578063e6d2834d146102fe578063eb8ecfa71461028b578063f2fde38b146102225763fc0438301461017f575f80fd5b3461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927ffc0438300000000000000000000000000000000000000000000000000000000082525afa908115610219575f916101e0575b6020925051908152f35b90506020823d602011610211575b816101fb60209383611a87565b8101031261020d5760209151906101d6565b5f80fd5b3d91506101ee565b513d5f823e3d90fd5b3461020d57602036600319011261020d5761023b611a10565b610243611d58565b6001600160a01b0380911690816001600160a01b031960c954161760c955609754167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e227005f80a3005b503461020d57602061029c36611aa9565b919290956102e36001600160a01b0360fb5416938751988996879586957feb8ecfa70000000000000000000000000000000000000000000000000000000087528601611bf7565b03915afa908115610219575f916101e0576020925051908152f35b50903461020d575f36600319011261020d57816001600160a01b0360fb54168151928380927fe6d2834d0000000000000000000000000000000000000000000000000000000082525afa9081156103b6575f905f92610372575b5082519167ffffffffffffffff8092168352166020820152f35b809250838092503d83116103af575b61038b8183611a87565b8101031261020d576103a860206103a183611b89565b9201611b89565b905f610358565b503d610381565b82513d5f823e3d90fd5b503461020d575f36600319011261020d576020906001600160a01b0360c954169051908152f35b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927fdf02ef7f0000000000000000000000000000000000000000000000000000000082525afa918215610499575f92610453575b60208367ffffffffffffffff845191168152f35b91506020823d602011610491575b8161046e60209383611a87565b8101031261020d5767ffffffffffffffff61048a602093611b89565b925061043f565b3d9150610461565b50513d5f823e3d90fd5b503461020d5760206104b436611aa9565b919290956102e36001600160a01b0360fb5416938751988996879586957fca162e5e0000000000000000000000000000000000000000000000000000000087528601611bf7565b50903461020d57602036600319011261020d5780356001600160a01b0380821680920361020d575f5460ff8160081c1615938480956106b1575b801561069a575b15610631575060ff1981166001175f556105b191908461061f575b50807f0000000000000000000000000000000000000000000000000000000000000000169061058882301415611c62565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc541614611cd3565b6105ca60ff5f5460081c166105c581611dff565b611dff565b6105d333611db0565b6001600160a01b031960fb54161760fb556105ea57005b60207f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989161ff00195f54165f555160018152a1005b61ffff1916610101175f908155610557565b608490602087519162461bcd60e51b8352820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152fd5b50303b15801561053c5750600160ff83161461053c565b50600160ff831610610535565b50903461020d57602036600319011261020d576106d96119ac565b9060c067ffffffffffffffff60246001600160a01b03948560fb5416875195869485937fbe3f058e00000000000000000000000000000000000000000000000000000000855216908301525afa9081156107f7575f925f80955f915f945f9661076e575b5060c09763ffffffff91858451991689526020890152169086015216606084015215156080830152151560a0820152f35b96505093505093505060c0823d60c0116107ef575b8161079060c09383611a87565b8101031261020d5760c0926107a483611d44565b906020840151936107b6848201611b9e565b9363ffffffff6107c860608401611d44565b6107e060a06107d960808701611b7c565b9501611b7c565b9597969093959691509761073d565b3d9150610783565b83513d5f823e3d90fd5b50903461020d576020918260031936011261020d578261081f611a10565b60246001600160a01b03918260fb5416855196879485937fbac69e6f00000000000000000000000000000000000000000000000000000000855216908301525afa918215610499575f92610877575b50519015158152f35b9091508281813d83116108a7575b61088f8183611a87565b8101031261020d576108a090611b7c565b905f61086e565b503d610885565b503461020d578060031936011261020d5767ffffffffffffffff91803583811161020d576108df9036908301611a26565b9190602435926001600160a01b039081851680950361020d575f9261093e9260fb5416918751968794859384937fa9cf9eec0000000000000000000000000000000000000000000000000000000085528b8a8601526044850191611baf565b90602483015203915afa9182156107f7575f92610996575b5050815191602090602080850191818652845180935285019301915f5b8281106109805785850386f35b8351871685529381019392810192600101610973565b9091503d805f833e6109a88183611a87565b8101602091828183031261020d5780519086821161020d570181601f8201121561020d57805193868511610a2957508360051b908551946109eb85840187611a87565b8552838086019282010192831161020d578301905b828210610a1257505050505f80610956565b838091610a1e84611b89565b815201910190610a00565b604190634e487b7160e01b5f525260245ffd5b503461020d576020610a4d36611aa9565b91929095610a946001600160a01b0360fb5416938751988996879586957fa694695b0000000000000000000000000000000000000000000000000000000087528601611bf7565b03915afa908115610219575f91610ab2575b60209250519015158152f35b90506020823d602011610ae6575b81610acd60209383611a87565b8101031261020d57610ae0602092611b7c565b90610aa6565b3d9150610ac0565b50903461020d576020918260031936011261020d578267ffffffffffffffff6024610b176119ac565b6001600160a01b0360fb5416855196879485937f9ad3c74500000000000000000000000000000000000000000000000000000000855216908301525afa918215610499575f92610b69575b5051908152f35b9091508281813d8311610b91575b610b818183611a87565b8101031261020d5751905f610b62565b503d610b77565b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f9568f9d90000000000000000000000000000000000000000000000000000000082525afa918215610499575f92610c00575b60208363ffffffff845191168152f35b91506020823d602011610c3a575b81610c1b60209383611a87565b8101031261020d5763ffffffff610c33602093611b9e565b9250610bf0565b3d9150610c0e565b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f9040f7c30000000000000000000000000000000000000000000000000000000082525afa918215610499575f926104535760208367ffffffffffffffff845191168152f35b503461020d575f36600319011261020d576020906001600160a01b03609754169051908152f35b50903461020d575f36600319011261020d57336001600160a01b0360c9541603610d0357610d0133611db0565b005b6020608492519162461bcd60e51b8352820152602960248201527f4f776e61626c6532537465703a2063616c6c6572206973206e6f74207468652060448201527f6e6577206f776e657200000000000000000000000000000000000000000000006064820152fd5b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f777915cb0000000000000000000000000000000000000000000000000000000082525afa908115610219575f916101e0576020925051908152f35b3461020d575f36600319011261020d57610de4611d58565b5f6001600160a01b036001600160a01b03198060c9541660c955609754908116609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a3005b50903461020d576020918260031936011261020d578267ffffffffffffffff6024610e576119ac565b6001600160a01b0360fb5416855196879485937f6d0db0e400000000000000000000000000000000000000000000000000000000855216908301525afa918215610499575f92610b69575051908152f35b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f68465f7d0000000000000000000000000000000000000000000000000000000082525afa918215610499575f926104535760208367ffffffffffffffff845191168152f35b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f5ba3d62a0000000000000000000000000000000000000000000000000000000082525afa908115610219575f916101e0576020925051908152f35b50903461020d575f36600319011261020d576001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163003610fdf57602082517f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8152f35b6020608492519162461bcd60e51b8352820152603860248201527f555550535570677261646561626c653a206d757374206e6f742062652063616c60448201527f6c6564207468726f7567682064656c656761746563616c6c00000000000000006064820152fd5b50908160031936011261020d5761105c611a10565b906024359267ffffffffffffffff841161020d573660238501121561020d57838201359061108982611b60565b61109582519182611a87565b82815260209283820196366024838301011161020d57815f9260248793018a37830101526001600160a01b03807f000000000000000000000000000000000000000000000000000000000000000016906110f182301415611c62565b6111207f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc928284541614611cd3565b611128611d58565b7f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff161561116257505050505050610d019150611e70565b869293949596169084516352d1902d60e01b815286818981865afa5f9181611345575b506111f2576084888888519162461bcd60e51b8352820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152fd5b9691929396036112dd575061120682611e70565b7fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a28351158015906112d5575b61123b57005b5f80610d019684519661124d88611a57565b602788527f416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c878901527f206661696c656400000000000000000000000000000000000000000000000000868901525190845af4913d156112cb573d6112bd6112b482611b60565b92519283611a87565b81525f81943d92013e611f1d565b5060609250611f1d565b506001611235565b6084908585519162461bcd60e51b8352820152602960248201527f45524331393637557067726164653a20756e737570706f727465642070726f7860448201527f6961626c655555494400000000000000000000000000000000000000000000006064820152fd5b9091508781813d831161136d575b61135d8183611a87565b8101031261020d5751905f611185565b503d611353565b503461020d57606036600319011261020d5761138e611a10565b6044356001600160a01b039081811680910361020d576020926064918360fb541690865197889586947f46e6d9170000000000000000000000000000000000000000000000000000000086521690840152602435602484015260448301525afa908115610219575f91610ab25760209250519015158152f35b503461020d578060031936011261020d57611420611a10565b6024359267ffffffffffffffff80851161020d573660238601121561020d578482013590811161020d57366024828701011161020d5760209260646001600160a01b03918260fb54169380602489519a8b98899788967f3e2ec16000000000000000000000000000000000000000000000000000000000885216908601528a8286015282604486015201848401375f828201840152601f01601f191681010301915afa908115610219575f91610ab25760209250519015158152f35b50903461020d576020908160031936011261020d576114f9611a10565b916001600160a01b0393847f00000000000000000000000000000000000000000000000000000000000000001661153281301415611c62565b6115617f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc918783541614611cd3565b611569611d58565b8151908382019682881067ffffffffffffffff8911176116d1578784525f83527f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff16156115c357505050505050610d019150611e70565b869293949596169084516352d1902d60e01b815286818981865afa5f91816116a2575b50611653576084888888519162461bcd60e51b8352820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152fd5b9691929396036112dd575061166782611e70565b7fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a283511580159061169b5761123b57005b505f611235565b9091508781813d83116116ca575b6116ba8183611a87565b8101031261020d5751905f6115e6565b503d6116b0565b604186634e487b7160e01b5f525260245ffd5b503461020d5760206116f536611aa9565b91929095610a946001600160a01b0360fb5416938751988996879586957f16cff0080000000000000000000000000000000000000000000000000000000087528601611bf7565b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f14cb9d7b0000000000000000000000000000000000000000000000000000000082525afa918215610499575f92610c005760208363ffffffff845191168152f35b503461020d575f36600319011261020d576020906001600160a01b0360fb54169051908152f35b503461020d575f36600319011261020d575f6001600160a01b0360fb54168251938480927f0d8e6e2c0000000000000000000000000000000000000000000000000000000082525afa908115610219575f91611834575b611830925051918291826119e4565b0390f35b90503d805f843e6118458184611a87565b82019160208184031261020d5780519067ffffffffffffffff821161020d57019180601f8401121561020d5782519261187d84611b60565b9161188a84519384611a87565b8483526020858301011161020d57611830936118ac91602080850191016119c3565b90611821565b82843461020d57602036600319011261020d576080836024816118d36119ac565b946001600160a01b0360fb5416907f03b3d43600000000000000000000000000000000000000000000000000000000835267ffffffffffffffff809716908301525afa80156103b6575f925f80955f93611947575b5060809584918351961515875260208701521690840152166060820152f35b9550509250506080833d6080116119a4575b8161196660809383611a87565b8101031261020d578261197a608094611b7c565b92826020830151946119996060611992858701611b89565b9501611b89565b939690959150611928565b3d9150611959565b6004359067ffffffffffffffff8216820361020d57565b5f5b8381106119d45750505f910152565b81810151838201526020016119c5565b60409160208252611a0481518092816020860152602086860191016119c3565b601f01601f1916010190565b600435906001600160a01b038216820361020d57565b9181601f8401121561020d5782359167ffffffffffffffff831161020d576020808501948460051b01011161020d57565b6060810190811067ffffffffffffffff821117611a7357604052565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff821117611a7357604052565b9060e060031983011261020d576004356001600160a01b038116810361020d57916024359167ffffffffffffffff80841161020d57611aed8360a095600401611a26565b90949093604319011261020d576040519060a0820182811082821117611a735760405260443563ffffffff8116810361020d578252606435818116810361020d576020830152608435908116810361020d57604082015260a435801515810361020d57606082015260c435608082015290565b67ffffffffffffffff8111611a7357601f01601f191660200190565b5190811515820361020d57565b519067ffffffffffffffff8216820361020d57565b519063ffffffff8216820361020d57565b9190808252602080920192915f5b828110611bcb575050505090565b9091929384359067ffffffffffffffff821680920361020d579081528201938201929190600101611bbd565b9260c092611c21916001600160a01b0360809498979816865260e0602087015260e0860191611baf565b9463ffffffff8151166040850152602081015167ffffffffffffffff8091166060860152604082015116828501526060810151151560a08501520151910152565b15611c6957565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f64656c656761746563616c6c00000000000000000000000000000000000000006064820152fd5b15611cda57565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f6163746976652070726f787900000000000000000000000000000000000000006064820152fd5b51906001600160a01b038216820361020d57565b6001600160a01b03609754163303611d6c57565b606460405162461bcd60e51b815260206004820152602060248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152fd5b6001600160a01b0319908160c9541660c9556097546001600160a01b038092168093821617609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b15611e0657565b608460405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152fd5b803b15611eb3576001600160a01b037f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc91166001600160a01b0319825416179055565b608460405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e7472616374000000000000000000000000000000000000006064820152fd5b91929015611f7e5750815115611f31575090565b3b15611f3a5790565b606460405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b825190915015611f915750805190602001fd5b611fad9060405191829162461bcd60e51b8352600483016119e4565b0390fdfea2646970667358221220f2591923882cfd014b5b268b869f66a411499ed9b9c14855716b80c5a80a543564736f6c63430008180033", + "deployedBytecode": "0x6080604081815260049182361015610015575f80fd5b5f3560e01c90816303b3d436146118b2575080630d8e6e2c146117ca57806310d04858146117a357806314cb9d7b1461173c57806316cff008146116e45780633659cfe6146114dc5780633e2ec1601461140757806346e6d917146113745780634f1ef2861461104757806352d1902d14610f745780635ba3d62a14610f1357806368465f7d14610ea85780636d0db0e414610e2e578063715018a614610dcc578063777915cb14610d6b57806379ba509714610cd45780638da5cb5b14610cad5780639040f7c314610c425780639568f9d914610b985780639ad3c74514610aee578063a694695b14610a3c578063a9cf9eec146108ae578063bac69e6f14610801578063be3f058e146106be578063c4d66de8146104fb578063ca162e5e146104a3578063df02ef7f146103e7578063e30c3978146103c0578063e6d2834d146102fe578063eb8ecfa71461028b578063f2fde38b146102225763fc0438301461017f575f80fd5b3461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927ffc0438300000000000000000000000000000000000000000000000000000000082525afa908115610219575f916101e0575b6020925051908152f35b90506020823d602011610211575b816101fb60209383611a87565b8101031261020d5760209151906101d6565b5f80fd5b3d91506101ee565b513d5f823e3d90fd5b3461020d57602036600319011261020d5761023b611a10565b610243611d58565b6001600160a01b0380911690816001600160a01b031960c954161760c955609754167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e227005f80a3005b503461020d57602061029c36611aa9565b919290956102e36001600160a01b0360fb5416938751988996879586957feb8ecfa70000000000000000000000000000000000000000000000000000000087528601611bf7565b03915afa908115610219575f916101e0576020925051908152f35b50903461020d575f36600319011261020d57816001600160a01b0360fb54168151928380927fe6d2834d0000000000000000000000000000000000000000000000000000000082525afa9081156103b6575f905f92610372575b5082519167ffffffffffffffff8092168352166020820152f35b809250838092503d83116103af575b61038b8183611a87565b8101031261020d576103a860206103a183611b89565b9201611b89565b905f610358565b503d610381565b82513d5f823e3d90fd5b503461020d575f36600319011261020d576020906001600160a01b0360c954169051908152f35b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927fdf02ef7f0000000000000000000000000000000000000000000000000000000082525afa918215610499575f92610453575b60208367ffffffffffffffff845191168152f35b91506020823d602011610491575b8161046e60209383611a87565b8101031261020d5767ffffffffffffffff61048a602093611b89565b925061043f565b3d9150610461565b50513d5f823e3d90fd5b503461020d5760206104b436611aa9565b919290956102e36001600160a01b0360fb5416938751988996879586957fca162e5e0000000000000000000000000000000000000000000000000000000087528601611bf7565b50903461020d57602036600319011261020d5780356001600160a01b0380821680920361020d575f5460ff8160081c1615938480956106b1575b801561069a575b15610631575060ff1981166001175f556105b191908461061f575b50807f0000000000000000000000000000000000000000000000000000000000000000169061058882301415611c62565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc541614611cd3565b6105ca60ff5f5460081c166105c581611dff565b611dff565b6105d333611db0565b6001600160a01b031960fb54161760fb556105ea57005b60207f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989161ff00195f54165f555160018152a1005b61ffff1916610101175f908155610557565b608490602087519162461bcd60e51b8352820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152fd5b50303b15801561053c5750600160ff83161461053c565b50600160ff831610610535565b50903461020d57602036600319011261020d576106d96119ac565b9060c067ffffffffffffffff60246001600160a01b03948560fb5416875195869485937fbe3f058e00000000000000000000000000000000000000000000000000000000855216908301525afa9081156107f7575f925f80955f915f945f9661076e575b5060c09763ffffffff91858451991689526020890152169086015216606084015215156080830152151560a0820152f35b96505093505093505060c0823d60c0116107ef575b8161079060c09383611a87565b8101031261020d5760c0926107a483611d44565b906020840151936107b6848201611b9e565b9363ffffffff6107c860608401611d44565b6107e060a06107d960808701611b7c565b9501611b7c565b9597969093959691509761073d565b3d9150610783565b83513d5f823e3d90fd5b50903461020d576020918260031936011261020d578261081f611a10565b60246001600160a01b03918260fb5416855196879485937fbac69e6f00000000000000000000000000000000000000000000000000000000855216908301525afa918215610499575f92610877575b50519015158152f35b9091508281813d83116108a7575b61088f8183611a87565b8101031261020d576108a090611b7c565b905f61086e565b503d610885565b503461020d578060031936011261020d5767ffffffffffffffff91803583811161020d576108df9036908301611a26565b9190602435926001600160a01b039081851680950361020d575f9261093e9260fb5416918751968794859384937fa9cf9eec0000000000000000000000000000000000000000000000000000000085528b8a8601526044850191611baf565b90602483015203915afa9182156107f7575f92610996575b5050815191602090602080850191818652845180935285019301915f5b8281106109805785850386f35b8351871685529381019392810192600101610973565b9091503d805f833e6109a88183611a87565b8101602091828183031261020d5780519086821161020d570181601f8201121561020d57805193868511610a2957508360051b908551946109eb85840187611a87565b8552838086019282010192831161020d578301905b828210610a1257505050505f80610956565b838091610a1e84611b89565b815201910190610a00565b604190634e487b7160e01b5f525260245ffd5b503461020d576020610a4d36611aa9565b91929095610a946001600160a01b0360fb5416938751988996879586957fa694695b0000000000000000000000000000000000000000000000000000000087528601611bf7565b03915afa908115610219575f91610ab2575b60209250519015158152f35b90506020823d602011610ae6575b81610acd60209383611a87565b8101031261020d57610ae0602092611b7c565b90610aa6565b3d9150610ac0565b50903461020d576020918260031936011261020d578267ffffffffffffffff6024610b176119ac565b6001600160a01b0360fb5416855196879485937f9ad3c74500000000000000000000000000000000000000000000000000000000855216908301525afa918215610499575f92610b69575b5051908152f35b9091508281813d8311610b91575b610b818183611a87565b8101031261020d5751905f610b62565b503d610b77565b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f9568f9d90000000000000000000000000000000000000000000000000000000082525afa918215610499575f92610c00575b60208363ffffffff845191168152f35b91506020823d602011610c3a575b81610c1b60209383611a87565b8101031261020d5763ffffffff610c33602093611b9e565b9250610bf0565b3d9150610c0e565b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f9040f7c30000000000000000000000000000000000000000000000000000000082525afa918215610499575f926104535760208367ffffffffffffffff845191168152f35b503461020d575f36600319011261020d576020906001600160a01b03609754169051908152f35b50903461020d575f36600319011261020d57336001600160a01b0360c9541603610d0357610d0133611db0565b005b6020608492519162461bcd60e51b8352820152602960248201527f4f776e61626c6532537465703a2063616c6c6572206973206e6f74207468652060448201527f6e6577206f776e657200000000000000000000000000000000000000000000006064820152fd5b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f777915cb0000000000000000000000000000000000000000000000000000000082525afa908115610219575f916101e0576020925051908152f35b3461020d575f36600319011261020d57610de4611d58565b5f6001600160a01b036001600160a01b03198060c9541660c955609754908116609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08280a3005b50903461020d576020918260031936011261020d578267ffffffffffffffff6024610e576119ac565b6001600160a01b0360fb5416855196879485937f6d0db0e400000000000000000000000000000000000000000000000000000000855216908301525afa918215610499575f92610b69575051908152f35b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f68465f7d0000000000000000000000000000000000000000000000000000000082525afa918215610499575f926104535760208367ffffffffffffffff845191168152f35b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f5ba3d62a0000000000000000000000000000000000000000000000000000000082525afa908115610219575f916101e0576020925051908152f35b50903461020d575f36600319011261020d576001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000163003610fdf57602082517f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8152f35b6020608492519162461bcd60e51b8352820152603860248201527f555550535570677261646561626c653a206d757374206e6f742062652063616c60448201527f6c6564207468726f7567682064656c656761746563616c6c00000000000000006064820152fd5b50908160031936011261020d5761105c611a10565b906024359267ffffffffffffffff841161020d573660238501121561020d57838201359061108982611b60565b61109582519182611a87565b82815260209283820196366024838301011161020d57815f9260248793018a37830101526001600160a01b03807f000000000000000000000000000000000000000000000000000000000000000016906110f182301415611c62565b6111207f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc928284541614611cd3565b611128611d58565b7f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff161561116257505050505050610d019150611e70565b869293949596169084516352d1902d60e01b815286818981865afa5f9181611345575b506111f2576084888888519162461bcd60e51b8352820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152fd5b9691929396036112dd575061120682611e70565b7fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a28351158015906112d5575b61123b57005b5f80610d019684519661124d88611a57565b602788527f416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c878901527f206661696c656400000000000000000000000000000000000000000000000000868901525190845af4913d156112cb573d6112bd6112b482611b60565b92519283611a87565b81525f81943d92013e611f1d565b5060609250611f1d565b506001611235565b6084908585519162461bcd60e51b8352820152602960248201527f45524331393637557067726164653a20756e737570706f727465642070726f7860448201527f6961626c655555494400000000000000000000000000000000000000000000006064820152fd5b9091508781813d831161136d575b61135d8183611a87565b8101031261020d5751905f611185565b503d611353565b503461020d57606036600319011261020d5761138e611a10565b6044356001600160a01b039081811680910361020d576020926064918360fb541690865197889586947f46e6d9170000000000000000000000000000000000000000000000000000000086521690840152602435602484015260448301525afa908115610219575f91610ab25760209250519015158152f35b503461020d578060031936011261020d57611420611a10565b6024359267ffffffffffffffff80851161020d573660238601121561020d578482013590811161020d57366024828701011161020d5760209260646001600160a01b03918260fb54169380602489519a8b98899788967f3e2ec16000000000000000000000000000000000000000000000000000000000885216908601528a8286015282604486015201848401375f828201840152601f01601f191681010301915afa908115610219575f91610ab25760209250519015158152f35b50903461020d576020908160031936011261020d576114f9611a10565b916001600160a01b0393847f00000000000000000000000000000000000000000000000000000000000000001661153281301415611c62565b6115617f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc918783541614611cd3565b611569611d58565b8151908382019682881067ffffffffffffffff8911176116d1578784525f83527f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff16156115c357505050505050610d019150611e70565b869293949596169084516352d1902d60e01b815286818981865afa5f91816116a2575b50611653576084888888519162461bcd60e51b8352820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f7420555550530000000000000000000000000000000000006064820152fd5b9691929396036112dd575061166782611e70565b7fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a283511580159061169b5761123b57005b505f611235565b9091508781813d83116116ca575b6116ba8183611a87565b8101031261020d5751905f6115e6565b503d6116b0565b604186634e487b7160e01b5f525260245ffd5b503461020d5760206116f536611aa9565b91929095610a946001600160a01b0360fb5416938751988996879586957f16cff0080000000000000000000000000000000000000000000000000000000087528601611bf7565b503461020d575f36600319011261020d5760206001600160a01b0360fb54168251938480927f14cb9d7b0000000000000000000000000000000000000000000000000000000082525afa918215610499575f92610c005760208363ffffffff845191168152f35b503461020d575f36600319011261020d576020906001600160a01b0360fb54169051908152f35b503461020d575f36600319011261020d575f6001600160a01b0360fb54168251938480927f0d8e6e2c0000000000000000000000000000000000000000000000000000000082525afa908115610219575f91611834575b611830925051918291826119e4565b0390f35b90503d805f843e6118458184611a87565b82019160208184031261020d5780519067ffffffffffffffff821161020d57019180601f8401121561020d5782519261187d84611b60565b9161188a84519384611a87565b8483526020858301011161020d57611830936118ac91602080850191016119c3565b90611821565b82843461020d57602036600319011261020d576080836024816118d36119ac565b946001600160a01b0360fb5416907f03b3d43600000000000000000000000000000000000000000000000000000000835267ffffffffffffffff809716908301525afa80156103b6575f925f80955f93611947575b5060809584918351961515875260208701521690840152166060820152f35b9550509250506080833d6080116119a4575b8161196660809383611a87565b8101031261020d578261197a608094611b7c565b92826020830151946119996060611992858701611b89565b9501611b89565b939690959150611928565b3d9150611959565b6004359067ffffffffffffffff8216820361020d57565b5f5b8381106119d45750505f910152565b81810151838201526020016119c5565b60409160208252611a0481518092816020860152602086860191016119c3565b601f01601f1916010190565b600435906001600160a01b038216820361020d57565b9181601f8401121561020d5782359167ffffffffffffffff831161020d576020808501948460051b01011161020d57565b6060810190811067ffffffffffffffff821117611a7357604052565b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff821117611a7357604052565b9060e060031983011261020d576004356001600160a01b038116810361020d57916024359167ffffffffffffffff80841161020d57611aed8360a095600401611a26565b90949093604319011261020d576040519060a0820182811082821117611a735760405260443563ffffffff8116810361020d578252606435818116810361020d576020830152608435908116810361020d57604082015260a435801515810361020d57606082015260c435608082015290565b67ffffffffffffffff8111611a7357601f01601f191660200190565b5190811515820361020d57565b519067ffffffffffffffff8216820361020d57565b519063ffffffff8216820361020d57565b9190808252602080920192915f5b828110611bcb575050505090565b9091929384359067ffffffffffffffff821680920361020d579081528201938201929190600101611bbd565b9260c092611c21916001600160a01b0360809498979816865260e0602087015260e0860191611baf565b9463ffffffff8151166040850152602081015167ffffffffffffffff8091166060860152604082015116828501526060810151151560a08501520151910152565b15611c6957565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f64656c656761746563616c6c00000000000000000000000000000000000000006064820152fd5b15611cda57565b608460405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201527f6163746976652070726f787900000000000000000000000000000000000000006064820152fd5b51906001600160a01b038216820361020d57565b6001600160a01b03609754163303611d6c57565b606460405162461bcd60e51b815260206004820152602060248201527f4f776e61626c653a2063616c6c6572206973206e6f7420746865206f776e65726044820152fd5b6001600160a01b0319908160c9541660c9556097546001600160a01b038092168093821617609755167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e05f80a3565b15611e0657565b608460405162461bcd60e51b815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152fd5b803b15611eb3576001600160a01b037f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc91166001600160a01b0319825416179055565b608460405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e7472616374000000000000000000000000000000000000006064820152fd5b91929015611f7e5750815115611f31575090565b3b15611f3a5790565b606460405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152fd5b825190915015611f915750805190602001fd5b611fad9060405191829162461bcd60e51b8352600483016119e4565b0390fdfea2646970667358221220f2591923882cfd014b5b268b869f66a411499ed9b9c14855716b80c5a80a543564736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": { + "794": [ + { + "length": 32, + "start": 1371 + }, + { + "length": 32, + "start": 3984 + }, + { + "length": 32, + "start": 4292 + }, + { + "length": 32, + "start": 5382 + } + ] + }, + "inputSourceName": "project/contracts/SSVNetworkViews.sol", + "buildInfoId": "solc-0_8_24-dc3260967516f45b660c2554111e2a58f5f3b385" +} \ No newline at end of file diff --git a/test/setup/artifacts/SSVOperatorsLegacy.json b/test/setup/artifacts/SSVOperatorsLegacy.json new file mode 100644 index 000000000..dc3890d39 --- /dev/null +++ b/test/setup/artifacts/SSVOperatorsLegacy.json @@ -0,0 +1,637 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "SSVOperators", + "sourceName": "contracts/modules/SSVOperators.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipientAddress", + "type": "address" + } + ], + "name": "FeeRecipientAddressUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorFeeDeclarationCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeDeclared", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OperatorFeeExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "bool", + "name": "toPrivate", + "type": "bool" + } + ], + "name": "OperatorPrivacyStatusUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "OperatorRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "OperatorWithdrawn", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "cancelDeclaredOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "declareOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "executeOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "reduceOperatorFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "setPrivate", + "type": "bool" + } + ], + "name": "registerOperator", + "outputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "removeOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPrivateUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "setOperatorsPublicUnchecked", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "withdrawAllOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "withdrawOperatorEarnings", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60808060405234610016576119b8908161001b8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063190d82e41461130257806323d68a6d1461123d5780632e168e0e1461104f57806335f6376714610ef75780634ad00e5414610e015780634bc93b6414610c23578063822124c114610b005780638932cee01461085b578063b317c35f146105905763c9bbc9fa14610087575f80fd5b3461058c57606036600319011261058c5760043567ffffffffffffffff811161058c573660238201121561058c5767ffffffffffffffff81600401351161058c57602481019036602482600401358301011161058c5760443590811515820361058c5760243515158061057d575b6105535767ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175460801c16602435116105295760405161014b6004830135601f01601f191660200182611598565b60048201358082526020820191908583375f602084600401358301015251902091825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10660205267ffffffffffffffff60405f2054166104ff577fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10c92600184540180945563ffffffff60408051916101e383611560565b80431683525f60208401525f828401526101fe602435611702565b6002835161020b8161157c565b5f815267ffffffffffffffff60208201931683526102ee85820133815260608301908a151582526080840198895267ffffffffffffffff8d165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a6020526102b967ffffffffffffffff888a5f209651169763ffffffff199889885416178755511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b516bffffffffffffffffffffffff84549181199060601b169116178355511515600183019060ff801983541691151516179055565b019351918251169084541617835561033967ffffffffffffffff60208301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b1691161790555f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10660205260405f2067ffffffffffffffff841667ffffffffffffffff19825416179055604051936040850185811067ffffffffffffffff8211176104eb5760405260018552602085019260203685378551156104d75767ffffffffffffffff851684526040805181815260048301359181018290529260608401375f606082600401358401015260243560208301527fd839f31c14bd632f424e307b36abff63ca33684f77f28e35dc13718ef338f7f4339260608167ffffffffffffffff891694601f8019916004013501168101030190a360405193604085019060408652518091526060850192905f5b8181106104b7576020867f7cae2703330c3f53308fb0fe3a9143f335997ba7e059b9ac8e4417ed8fbddbd3898089891515868301520390a167ffffffffffffffff60405191168152f35b825167ffffffffffffffff1685526020948501949092019160010161046d565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52604160045260245ffd5b60046040517f289c9494000000000000000000000000000000000000000000000000000000008152fd5b60046040517fcd4e6167000000000000000000000000000000000000000000000000000000008152fd5b60046040517f732f9413000000000000000000000000000000000000000000000000000000008152fd5b50633b9aca00602435106100f5565b5f80fd5b3461058c57604036600319011261058c576105a96114e7565b6024359067ffffffffffffffff80911691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a916020928084526105f96105f460405f206115ba565b61166c565b8215158061084e575b610553577f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175490828260801c16841161052957855f5284528160405f2054851c16918061064e85611702565b16928084036106815760046040517fc81272f8000000000000000000000000000000000000000000000000000000008152fd5b83151580610846575b61081c57612710828460401c168101838111610808576106ad849391849261164d565b16041683116107de576106fe81421692826106f67f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165460c01c6106f08188611631565b96611631565b911690611631565b906040519361070c85611560565b84528086850193168352806040850192168252865f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed109865260405f209351167fffffffffffffffff00000000000000000000000000000000000000000000000077ffffffffffffffff000000000000000000000000000000006fffffffffffffffff00000000000000008654955160401b16935160801b1693161717179055604051914383528201527f796204296f2eb56d7432fa85961e9750d0cb21741873ebf7077e28263e32735860403392a3005b60046040517f958065d9000000000000000000000000000000000000000000000000000000008152fd5b634e487b7160e01b5f52601160045260245ffd5b60046040517f410a2b6c000000000000000000000000000000000000000000000000000000008152fd5b50801561068a565b50633b9aca008310610602565b3461058c5760208060031936011261058c5767ffffffffffffffff90816108806114e7565b1691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a908183526108b660405f206115ba565b6108bf8161166c565b845f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10980855260405f2091604051926108f884611560565b548481168452848160401c168785019080825286604087019360801c16835215610ad65785809151164210918215610ac9575b5050610a9f5761093d8484511661182d565b847f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175460801c1610610529576040610a6d95610978836117b7565b845186168389019081525f8a81529189529082902083519151838501516bffffffffffffffffffffffff19606091821b1663ffffffff948516928a1660201b6bffffffffffffffff00000000169290921791909117825584015160018201805460ff191691151560ff1691909117905563ffffffff19906080906002019401519182511690845416178355610a3786898301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b16911617905584525f6040812055511661182d565b604051914383528201527f513e931ff778ed01e676d55880d8db185c29b0094546ff2b3e9f5b6920d16bef60403392a3005b60046040517f97e4b518000000000000000000000000000000000000000000000000000000008152fd5b511642119050848961092b565b60046040517f1d226c30000000000000000000000000000000000000000000000000000000008152fd5b3461058c57610b0e36611513565b8015610bf9575f5b818110610b94575060405190806040830160408452526060820192905f5b818110610b6a57600160208501527f7cae2703330c3f53308fb0fe3a9143f335997ba7e059b9ac8e4417ed8fbddbd384860385a1005b90919360019067ffffffffffffffff610b82876114fe565b16815260209081019501929101610b34565b8060051b8301359067ffffffffffffffff821680920361058c576001915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f20610be76105f4826115ba565b8201805460ff19168317905501610b16565b60046040517f38186224000000000000000000000000000000000000000000000000000000008152fd5b3461058c57602036600319011261058c57610c3c6114e7565b67ffffffffffffffff808216805f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a80602052610c7c60405f206115ba565b610c858161166c565b610c8e816117b7565b60808101805160400151909390851615610dd7578351604001805186169590935f9260409583610d90951690525f52602052835f2092600263ffffffff94610d516060878551169463ffffffff199586855416178455610d198760208301511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b898101516bffffffffffffffffffffffff855491811990851b16911617845501511515600183019060ff801983541691151516179055565b019551938451169086541617855560208301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b169116179055629896809081810291818304149015171561080857610dd59161184f565b005b60046040517ff4d678b8000000000000000000000000000000000000000000000000000000008152fd5b3461058c57610e0f36611513565b8015610bf9575f5b818110610e94575060405190806040830160408452526060820192905f5b818110610e6a575f60208501527f7cae2703330c3f53308fb0fe3a9143f335997ba7e059b9ac8e4417ed8fbddbd384860385a1005b90919360019067ffffffffffffffff610e82876114fe565b16815260209081019501929101610e35565b8060051b8301359067ffffffffffffffff821680920361058c576001915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a6020528160405f20610ee86105f4826115ba565b01805460ff1916905501610e17565b3461058c57604036600319011261058c57610f106114e7565b60243567ffffffffffffffff80831691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a9081602052610f5560405f206115ba565b90610f5f8261166c565b610f68826117b7565b610f7181611702565b9015808061103c575b156110145750508260406080830151015116935b608082019360408551019381808651169716809703928284116108085760409583610d90951690525f52602052835f2092600263ffffffff94610d516060878551169463ffffffff199586855416178455610d198760208301511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b1580611026575b15610dd75793610f8e565b508360406080840151015116848216111561101b565b5084604060808501510151161515610f7a565b3461058c5760208060031936011261058c576110696114e7565b67ffffffffffffffff7fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10881831693845f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a9283825260406110cc815f206115ba565b6110d58161166c565b6110de816117b7565b61119960808201938451975f82868b01511699525f85875101525f8452868401905f82528b5f5287526002855f2061115b606063ffffffff97610d19878a8351169763ffffffff199889885416178755511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b0195519384511690865416178555858301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b1691161790555260405f207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558015801561121b575b837f0e0ba6c2b04de36d6d509ec5bd155c43a9fe862f8052096dd54f3902a74cca3e5f80a2005b62989680808302928304141715610808576112359161184f565b8180806111f4565b3461058c57602036600319011261058c5767ffffffffffffffff806112606114e7565b1690815f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a6020526112986105f460405f206115ba565b815f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed109908160205260405f205460401c1615610ad657815f526020525f6040812055337f5055fa347441172447637c015e80a3ee748b9382212ceb5dca5a3683298fd6f35f80a3005b3461058c57604036600319011261058c5761131b6114e7565b6024359067ffffffffffffffff80911691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a9160209280845261136360405f206115ba565b9161136d8361166c565b831515806114da575b6105535761138384611702565b85840191808084511692169182101561081c57604093611458926113a6876117b7565b84525f89815290885284902085519351868601516bffffffffffffffffffffffff19606091821b1663ffffffff96871692851660201b6bffffffffffffffff00000000169290921791909117825586015160018201805460ff191691151560ff1691909117905563ffffffff19906080906002019601519384511690865416178555868301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b1691161790557fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10982525f6040812055604051914383528201527f513e931ff778ed01e676d55880d8db185c29b0094546ff2b3e9f5b6920d16bef60403392a3005b50633b9aca008410611376565b6004359067ffffffffffffffff8216820361058c57565b359067ffffffffffffffff8216820361058c57565b90602060031983011261058c5760043567ffffffffffffffff9283821161058c578060238301121561058c57816004013593841161058c5760248460051b8301011161058c576024019190565b6060810190811067ffffffffffffffff8211176104eb57604052565b60a0810190811067ffffffffffffffff8211176104eb57604052565b90601f8019910116810190811067ffffffffffffffff8211176104eb57604052565b90604051916115c88361157c565b608083825463ffffffff808216835267ffffffffffffffff91828160201c16602085015260601c604084015260ff6001860154161515606084015260026040519561161287611560565b01549081168552818160201c16602086015260601c1660408401520152565b91909167ffffffffffffffff8080941691160191821161080857565b91909167ffffffffffffffff8080941691160291821691820361080857565b63ffffffff60808201515116156116d8576040015173ffffffffffffffffffffffffffffffffffffffff163381036116a15750565b604490604051907f8907fc650000000000000000000000000000000000000000000000000000000082523360048301526024820152fd5b60046040517f961e3e8c000000000000000000000000000000000000000000000000000000008152fd5b6a9896800000000000000000811015611773576298968080820661172f5767ffffffffffffffff91041690565b606460405162461bcd60e51b815260206004820152601660248201527f4d617820707265636973696f6e206578636565646564000000000000000000006044820152fd5b606460405162461bcd60e51b815260206004820152601260248201527f4d61782076616c756520657863656564656400000000000000000000000000006044820152fd5b63ffffffff804316916080810191808351511684039181831161080857611813916117f467ffffffffffffffff948286602086015116911661164d565b916020865101856118088582845116611631565b16905251169061164d565b9061182660408451019282845116611631565b1690525152565b67ffffffffffffffff1662989680908181029181830414901517156108085790565b5f602073ffffffffffffffffffffffffffffffffffffffff7fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b54166044604051809481937fa9059cbb0000000000000000000000000000000000000000000000000000000083523360048401528860248401525af1908115611977575f9161193c575b50156119125767ffffffffffffffff9060405192835216907f178bf78bdd8914b8483d640b4a4f84e20943b5eb6b639b7474286364c7651d6060203392a3565b60046040517f045c4b02000000000000000000000000000000000000000000000000000000008152fd5b90506020813d60201161196f575b8161195760209383611598565b8101031261058c5751801515810361058c575f6118d2565b3d915061194a565b6040513d5f823e3d90fdfea264697066735822122000b64969c0da1e8b83df27cfe4b18d82ec63eb6f8f900b4186a8a4ca166b069264736f6c63430008180033", + "deployedBytecode": "0x60806040526004361015610011575f80fd5b5f3560e01c8063190d82e41461130257806323d68a6d1461123d5780632e168e0e1461104f57806335f6376714610ef75780634ad00e5414610e015780634bc93b6414610c23578063822124c114610b005780638932cee01461085b578063b317c35f146105905763c9bbc9fa14610087575f80fd5b3461058c57606036600319011261058c5760043567ffffffffffffffff811161058c573660238201121561058c5767ffffffffffffffff81600401351161058c57602481019036602482600401358301011161058c5760443590811515820361058c5760243515158061057d575b6105535767ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175460801c16602435116105295760405161014b6004830135601f01601f191660200182611598565b60048201358082526020820191908583375f602084600401358301015251902091825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10660205267ffffffffffffffff60405f2054166104ff577fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10c92600184540180945563ffffffff60408051916101e383611560565b80431683525f60208401525f828401526101fe602435611702565b6002835161020b8161157c565b5f815267ffffffffffffffff60208201931683526102ee85820133815260608301908a151582526080840198895267ffffffffffffffff8d165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a6020526102b967ffffffffffffffff888a5f209651169763ffffffff199889885416178755511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b516bffffffffffffffffffffffff84549181199060601b169116178355511515600183019060ff801983541691151516179055565b019351918251169084541617835561033967ffffffffffffffff60208301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b1691161790555f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10660205260405f2067ffffffffffffffff841667ffffffffffffffff19825416179055604051936040850185811067ffffffffffffffff8211176104eb5760405260018552602085019260203685378551156104d75767ffffffffffffffff851684526040805181815260048301359181018290529260608401375f606082600401358401015260243560208301527fd839f31c14bd632f424e307b36abff63ca33684f77f28e35dc13718ef338f7f4339260608167ffffffffffffffff891694601f8019916004013501168101030190a360405193604085019060408652518091526060850192905f5b8181106104b7576020867f7cae2703330c3f53308fb0fe3a9143f335997ba7e059b9ac8e4417ed8fbddbd3898089891515868301520390a167ffffffffffffffff60405191168152f35b825167ffffffffffffffff1685526020948501949092019160010161046d565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52604160045260245ffd5b60046040517f289c9494000000000000000000000000000000000000000000000000000000008152fd5b60046040517fcd4e6167000000000000000000000000000000000000000000000000000000008152fd5b60046040517f732f9413000000000000000000000000000000000000000000000000000000008152fd5b50633b9aca00602435106100f5565b5f80fd5b3461058c57604036600319011261058c576105a96114e7565b6024359067ffffffffffffffff80911691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a916020928084526105f96105f460405f206115ba565b61166c565b8215158061084e575b610553577f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175490828260801c16841161052957855f5284528160405f2054851c16918061064e85611702565b16928084036106815760046040517fc81272f8000000000000000000000000000000000000000000000000000000008152fd5b83151580610846575b61081c57612710828460401c168101838111610808576106ad849391849261164d565b16041683116107de576106fe81421692826106f67f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165460c01c6106f08188611631565b96611631565b911690611631565b906040519361070c85611560565b84528086850193168352806040850192168252865f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed109865260405f209351167fffffffffffffffff00000000000000000000000000000000000000000000000077ffffffffffffffff000000000000000000000000000000006fffffffffffffffff00000000000000008654955160401b16935160801b1693161717179055604051914383528201527f796204296f2eb56d7432fa85961e9750d0cb21741873ebf7077e28263e32735860403392a3005b60046040517f958065d9000000000000000000000000000000000000000000000000000000008152fd5b634e487b7160e01b5f52601160045260245ffd5b60046040517f410a2b6c000000000000000000000000000000000000000000000000000000008152fd5b50801561068a565b50633b9aca008310610602565b3461058c5760208060031936011261058c5767ffffffffffffffff90816108806114e7565b1691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a908183526108b660405f206115ba565b6108bf8161166c565b845f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10980855260405f2091604051926108f884611560565b548481168452848160401c168785019080825286604087019360801c16835215610ad65785809151164210918215610ac9575b5050610a9f5761093d8484511661182d565b847f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175460801c1610610529576040610a6d95610978836117b7565b845186168389019081525f8a81529189529082902083519151838501516bffffffffffffffffffffffff19606091821b1663ffffffff948516928a1660201b6bffffffffffffffff00000000169290921791909117825584015160018201805460ff191691151560ff1691909117905563ffffffff19906080906002019401519182511690845416178355610a3786898301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b16911617905584525f6040812055511661182d565b604051914383528201527f513e931ff778ed01e676d55880d8db185c29b0094546ff2b3e9f5b6920d16bef60403392a3005b60046040517f97e4b518000000000000000000000000000000000000000000000000000000008152fd5b511642119050848961092b565b60046040517f1d226c30000000000000000000000000000000000000000000000000000000008152fd5b3461058c57610b0e36611513565b8015610bf9575f5b818110610b94575060405190806040830160408452526060820192905f5b818110610b6a57600160208501527f7cae2703330c3f53308fb0fe3a9143f335997ba7e059b9ac8e4417ed8fbddbd384860385a1005b90919360019067ffffffffffffffff610b82876114fe565b16815260209081019501929101610b34565b8060051b8301359067ffffffffffffffff821680920361058c576001915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f20610be76105f4826115ba565b8201805460ff19168317905501610b16565b60046040517f38186224000000000000000000000000000000000000000000000000000000008152fd5b3461058c57602036600319011261058c57610c3c6114e7565b67ffffffffffffffff808216805f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a80602052610c7c60405f206115ba565b610c858161166c565b610c8e816117b7565b60808101805160400151909390851615610dd7578351604001805186169590935f9260409583610d90951690525f52602052835f2092600263ffffffff94610d516060878551169463ffffffff199586855416178455610d198760208301511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b898101516bffffffffffffffffffffffff855491811990851b16911617845501511515600183019060ff801983541691151516179055565b019551938451169086541617855560208301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b169116179055629896809081810291818304149015171561080857610dd59161184f565b005b60046040517ff4d678b8000000000000000000000000000000000000000000000000000000008152fd5b3461058c57610e0f36611513565b8015610bf9575f5b818110610e94575060405190806040830160408452526060820192905f5b818110610e6a575f60208501527f7cae2703330c3f53308fb0fe3a9143f335997ba7e059b9ac8e4417ed8fbddbd384860385a1005b90919360019067ffffffffffffffff610e82876114fe565b16815260209081019501929101610e35565b8060051b8301359067ffffffffffffffff821680920361058c576001915f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a6020528160405f20610ee86105f4826115ba565b01805460ff1916905501610e17565b3461058c57604036600319011261058c57610f106114e7565b60243567ffffffffffffffff80831691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a9081602052610f5560405f206115ba565b90610f5f8261166c565b610f68826117b7565b610f7181611702565b9015808061103c575b156110145750508260406080830151015116935b608082019360408551019381808651169716809703928284116108085760409583610d90951690525f52602052835f2092600263ffffffff94610d516060878551169463ffffffff199586855416178455610d198760208301511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b1580611026575b15610dd75793610f8e565b508360406080840151015116848216111561101b565b5084604060808501510151161515610f7a565b3461058c5760208060031936011261058c576110696114e7565b67ffffffffffffffff7fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10881831693845f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a9283825260406110cc815f206115ba565b6110d58161166c565b6110de816117b7565b61119960808201938451975f82868b01511699525f85875101525f8452868401905f82528b5f5287526002855f2061115b606063ffffffff97610d19878a8351169763ffffffff199889885416178755511685906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b0195519384511690865416178555858301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b1691161790555260405f207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558015801561121b575b837f0e0ba6c2b04de36d6d509ec5bd155c43a9fe862f8052096dd54f3902a74cca3e5f80a2005b62989680808302928304141715610808576112359161184f565b8180806111f4565b3461058c57602036600319011261058c5767ffffffffffffffff806112606114e7565b1690815f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a6020526112986105f460405f206115ba565b815f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed109908160205260405f205460401c1615610ad657815f526020525f6040812055337f5055fa347441172447637c015e80a3ee748b9382212ceb5dca5a3683298fd6f35f80a3005b3461058c57604036600319011261058c5761131b6114e7565b6024359067ffffffffffffffff80911691825f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a9160209280845261136360405f206115ba565b9161136d8361166c565b831515806114da575b6105535761138384611702565b85840191808084511692169182101561081c57604093611458926113a6876117b7565b84525f89815290885284902085519351868601516bffffffffffffffffffffffff19606091821b1663ffffffff96871692851660201b6bffffffffffffffff00000000169290921791909117825586015160018201805460ff191691151560ff1691909117905563ffffffff19906080906002019601519384511690865416178555868301511684906bffffffffffffffff0000000082549160201b16906bffffffffffffffff000000001916179055565b015167ffffffffffffffff60601b1967ffffffffffffffff60601b83549260601b1691161790557fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10982525f6040812055604051914383528201527f513e931ff778ed01e676d55880d8db185c29b0094546ff2b3e9f5b6920d16bef60403392a3005b50633b9aca008410611376565b6004359067ffffffffffffffff8216820361058c57565b359067ffffffffffffffff8216820361058c57565b90602060031983011261058c5760043567ffffffffffffffff9283821161058c578060238301121561058c57816004013593841161058c5760248460051b8301011161058c576024019190565b6060810190811067ffffffffffffffff8211176104eb57604052565b60a0810190811067ffffffffffffffff8211176104eb57604052565b90601f8019910116810190811067ffffffffffffffff8211176104eb57604052565b90604051916115c88361157c565b608083825463ffffffff808216835267ffffffffffffffff91828160201c16602085015260601c604084015260ff6001860154161515606084015260026040519561161287611560565b01549081168552818160201c16602086015260601c1660408401520152565b91909167ffffffffffffffff8080941691160191821161080857565b91909167ffffffffffffffff8080941691160291821691820361080857565b63ffffffff60808201515116156116d8576040015173ffffffffffffffffffffffffffffffffffffffff163381036116a15750565b604490604051907f8907fc650000000000000000000000000000000000000000000000000000000082523360048301526024820152fd5b60046040517f961e3e8c000000000000000000000000000000000000000000000000000000008152fd5b6a9896800000000000000000811015611773576298968080820661172f5767ffffffffffffffff91041690565b606460405162461bcd60e51b815260206004820152601660248201527f4d617820707265636973696f6e206578636565646564000000000000000000006044820152fd5b606460405162461bcd60e51b815260206004820152601260248201527f4d61782076616c756520657863656564656400000000000000000000000000006044820152fd5b63ffffffff804316916080810191808351511684039181831161080857611813916117f467ffffffffffffffff948286602086015116911661164d565b916020865101856118088582845116611631565b16905251169061164d565b9061182660408451019282845116611631565b1690525152565b67ffffffffffffffff1662989680908181029181830414901517156108085790565b5f602073ffffffffffffffffffffffffffffffffffffffff7fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10b54166044604051809481937fa9059cbb0000000000000000000000000000000000000000000000000000000083523360048401528860248401525af1908115611977575f9161193c575b50156119125767ffffffffffffffff9060405192835216907f178bf78bdd8914b8483d640b4a4f84e20943b5eb6b639b7474286364c7651d6060203392a3565b60046040517f045c4b02000000000000000000000000000000000000000000000000000000008152fd5b90506020813d60201161196f575b8161195760209383611598565b8101031261058c5751801515810361058c575f6118d2565b3d915061194a565b6040513d5f823e3d90fdfea264697066735822122000b64969c0da1e8b83df27cfe4b18d82ec63eb6f8f900b4186a8a4ca166b069264736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/modules/SSVOperators.sol", + "buildInfoId": "solc-0_8_24-dc3260967516f45b660c2554111e2a58f5f3b385" +} \ No newline at end of file diff --git a/test/setup/artifacts/SSVOperatorsWhitelistLegacy.json b/test/setup/artifacts/SSVOperatorsWhitelistLegacy.json new file mode 100644 index 000000000..e367a6c8e --- /dev/null +++ b/test/setup/artifacts/SSVOperatorsWhitelistLegacy.json @@ -0,0 +1,412 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "SSVOperatorsWhitelist", + "sourceName": "contracts/modules/SSVOperatorsWhitelist.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "OperatorMultipleWhitelistUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "indexed": false, + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "OperatorWhitelistingContractUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + } + ], + "name": "removeOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "removeOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "contract ISSVWhitelistingContract", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "setOperatorsWhitelistingContract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address[]", + "name": "whitelistAddresses", + "type": "address[]" + } + ], + "name": "setOperatorsWhitelists", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x6080806040523461001657610e1b908161001b8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c80634b2fd45e146100545780635d06ecb41461004f5780636a31cf1d1461004a57637dc24d5214610045575f80fd5b6104b4565b61036c565b610207565b3461018757610062366101bc565b929083156101765761007383610998565b5061007e8383610ab2565b61008982518261096d565b905f5b8781106100cb576040517f589a71ef5bb37432c8ce279a4afc32783592f1764c6fcb07e3c437e80c80ab2e90806100c68b898c8c85610785565b0390a1005b6100de6100d9828a88610809565b61097a565b6100e781610c47565b825b8481106100fa57505060010161008c565b8061011061010a86600194610952565b88610984565b518061011e575b50016100e9565b6101698261015c866001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b905f5260205260405f2090565b901981541690558b610117565b6332ecd8b760e21b60805260046080fd5b5f80fd5b9181601f840112156101875782359167ffffffffffffffff8311610187576020808501948460051b01011161018757565b60406003198201126101875767ffffffffffffffff9160043583811161018757826101e99160040161018b565b93909392602435918211610187576102039160040161018b565b9091565b3461018757610215366101bc565b809391931561035b5761022783610998565b506102328383610ab2565b9061023e81518361096d565b5f5b84811061027a576040517f3d5869fa1ed68d6b7b5e2a1f44df8e1e7edd8ea7a6cc240e45c72e2eb352396290806100c6888c8c8c85610785565b6102886100d982878b610809565b61029181610c47565b61029a81610c81565b61031f57845b8381106102b1575050600101610240565b806102c76102c188600194610952565b87610984565b51806102d5575b50016102a0565b6103138261015c866001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b9081541790555f6102ce565b6040517f71cadba70000000000000000000000000000000000000000000000000000000081526001600160a01b03919091166004820152602490fd5b60046040516332ecd8b760e21b8152fd5b346101875760203660031901126101875760043567ffffffffffffffff81116101875761039d90369060040161018b565b6103a681610998565b5f5b8181106103e0576040517ff41d8ca981ff900f6db7f71d7e2ae866eae8e4327d23e5c692c13a6c43b39c3d90806100c68688836108e6565b8061049d6104806103f46001948789610809565b356103fe8161072b565b61044961044461043f8367ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b61085b565b6109cb565b67ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff198154169055565b016103a8565b6001600160a01b0381160361018757565b346101875760403660031901126101875760043567ffffffffffffffff8111610187576104e590369060040161018b565b602435916104f2836104a3565b6001600160a01b038093169261050e61050a85610c81565b1590565b6106f15761051b83610998565b905f5b828110610559575050506100c67ff41d8ca981ff900f6db7f71d7e2ae866eae8e4327d23e5c692c13a6c43b39c3d9360405193849384610903565b806106728761064a6105766105716001968b8b610809565b610819565b6105b761044461043f8367ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b6106026105f58267ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b546001600160a01b031690565b8781161515806106df575b610678575b5067ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b0161051e565b866106d261015c92610699859060ff66ffffffffffffff8360081c16921690565b9490916001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b911b81541790555f610612565b506106ec61050a82610c81565b61060d565b6040517f886e6a030000000000000000000000000000000000000000000000000000000081526001600160a01b0385166004820152602490fd5b67ffffffffffffffff81160361018757565b9190808252602080920192915f5b828110610759575050505090565b90919293828060019267ffffffffffffffff88356107768161072b565b1681520195019392910161074b565b92939160209161079d9160408652604086019161073d565b8193828183039101528281520192915f5b8281106107bc575050505090565b9091929382806001926001600160a01b0388356107d8816104a3565b168152019501939291016107ae565b634e487b7160e01b5f52603260045260245ffd5b90156108045790565b6107e7565b91908110156108045760051b0190565b356108238161072b565b90565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff82111761085657604052565b610826565b6040805190929167ffffffffffffffff9160a0810183811182821017610856578552809482549363ffffffff948581168452818160201c16602085015260601c8284015260ff60018501541615156060840152815194606086018681108382111761085657608095600291855201549081168652818160201c16602087015260601c16908401520152565b92916108fe5f9260209260408752604087019161073d565b930152565b916109246020926001600160a01b039296959660408652604086019161073d565b9416910152565b634e487b7160e01b5f52601160045260245ffd5b5f1981019190821161094d57565b61092b565b9190820391821161094d57565b906001820180921161094d57565b9190820180921161094d57565b35610823816104a3565b80518210156108045760209160051b010190565b9081156109a157565b60046040517f38186224000000000000000000000000000000000000000000000000000000008152fd5b63ffffffff6080820151511615610a2a57604001516001600160a01b03163381036109f35750565b604490604051907f8907fc650000000000000000000000000000000000000000000000000000000082523360048301526024820152fd5b60046040517f961e3e8c000000000000000000000000000000000000000000000000000000008152fd5b67ffffffffffffffff81116108565760051b60200190565b90610a7682610a54565b60405190601f1990601f018116820167ffffffffffffffff81118382101761085657604052838252610aa88294610a54565b0190602036910137565b9091610ae3610ad6610ac761057186866107fb565b60081c66ffffffffffffff1690565b67ffffffffffffffff1690565b91610b19610b14610b0f85610b0a610ad6610ac7610571610b038c61093f565b8c8a610809565b610952565b61095f565b610a6c565b935f90815b818310610b2b5750505050565b610b39610571848487610809565b90610b7b61044461043f8467ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b83151580610c30575b610bc9575060019060ff8116610bc083610bab8a66ffffffffffffff600887901c16610952565b921b610bb7838c610984565b5117918a610984565b52920191610b1e565b67ffffffffffffffff809116911614610c065760046040517fdd020e25000000000000000000000000000000000000000000000000000000008152fd5b60046040517fa5a1ff5d000000000000000000000000000000000000000000000000000000008152fd5b5067ffffffffffffffff8082169083161115610b84565b6001600160a01b031615610c5757565b60046040517f8579befe000000000000000000000000000000000000000000000000000000008152fd5b604051906020808301815f6301ffc9a760e01b9586845286602482015260248152610cab8161083a565b51617530938685fa933d5f519086610d66575b5085610d5c575b5084610ce2575b50505081610cd8575090565b6108239150610d71565b839450905f9183946040518581019283527fffffffff00000000000000000000000000000000000000000000000000000000602482015260248152610d268161083a565b5192fa5f5190913d83610d51575b505081610d47575b5015905f8080610ccc565b905015155f610d3c565b101591505f80610d34565b151594505f610cc5565b84111595505f610cbe565b5f602091604051838101906301ffc9a760e01b82527f830639ac00000000000000000000000000000000000000000000000000000000602482015260248152610db98161083a565b5191617530fa5f513d82610dd9575b5081610dd2575090565b9050151590565b6020111591505f610dc856fea26469706673582212204d674a17922996f3e134dc3346a026b33f9e53c005c10ba3e0c67e851882e46864736f6c63430008180033", + "deployedBytecode": "0x60806040526004361015610011575f80fd5b5f3560e01c80634b2fd45e146100545780635d06ecb41461004f5780636a31cf1d1461004a57637dc24d5214610045575f80fd5b6104b4565b61036c565b610207565b3461018757610062366101bc565b929083156101765761007383610998565b5061007e8383610ab2565b61008982518261096d565b905f5b8781106100cb576040517f589a71ef5bb37432c8ce279a4afc32783592f1764c6fcb07e3c437e80c80ab2e90806100c68b898c8c85610785565b0390a1005b6100de6100d9828a88610809565b61097a565b6100e781610c47565b825b8481106100fa57505060010161008c565b8061011061010a86600194610952565b88610984565b518061011e575b50016100e9565b6101698261015c866001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b905f5260205260405f2090565b901981541690558b610117565b6332ecd8b760e21b60805260046080fd5b5f80fd5b9181601f840112156101875782359167ffffffffffffffff8311610187576020808501948460051b01011161018757565b60406003198201126101875767ffffffffffffffff9160043583811161018757826101e99160040161018b565b93909392602435918211610187576102039160040161018b565b9091565b3461018757610215366101bc565b809391931561035b5761022783610998565b506102328383610ab2565b9061023e81518361096d565b5f5b84811061027a576040517f3d5869fa1ed68d6b7b5e2a1f44df8e1e7edd8ea7a6cc240e45c72e2eb352396290806100c6888c8c8c85610785565b6102886100d982878b610809565b61029181610c47565b61029a81610c81565b61031f57845b8381106102b1575050600101610240565b806102c76102c188600194610952565b87610984565b51806102d5575b50016102a0565b6103138261015c866001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b9081541790555f6102ce565b6040517f71cadba70000000000000000000000000000000000000000000000000000000081526001600160a01b03919091166004820152602490fd5b60046040516332ecd8b760e21b8152fd5b346101875760203660031901126101875760043567ffffffffffffffff81116101875761039d90369060040161018b565b6103a681610998565b5f5b8181106103e0576040517ff41d8ca981ff900f6db7f71d7e2ae866eae8e4327d23e5c692c13a6c43b39c3d90806100c68688836108e6565b8061049d6104806103f46001948789610809565b356103fe8161072b565b61044961044461043f8367ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b61085b565b6109cb565b67ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b73ffffffffffffffffffffffffffffffffffffffff198154169055565b016103a8565b6001600160a01b0381160361018757565b346101875760403660031901126101875760043567ffffffffffffffff8111610187576104e590369060040161018b565b602435916104f2836104a3565b6001600160a01b038093169261050e61050a85610c81565b1590565b6106f15761051b83610998565b905f5b828110610559575050506100c67ff41d8ca981ff900f6db7f71d7e2ae866eae8e4327d23e5c692c13a6c43b39c3d9360405193849384610903565b806106728761064a6105766105716001968b8b610809565b610819565b6105b761044461043f8367ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b6106026105f58267ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b546001600160a01b031690565b8781161515806106df575b610678575b5067ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b906001600160a01b031673ffffffffffffffffffffffffffffffffffffffff19825416179055565b0161051e565b866106d261015c92610699859060ff66ffffffffffffff8360081c16921690565b9490916001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b911b81541790555f610612565b506106ec61050a82610c81565b61060d565b6040517f886e6a030000000000000000000000000000000000000000000000000000000081526001600160a01b0385166004820152602490fd5b67ffffffffffffffff81160361018757565b9190808252602080920192915f5b828110610759575050505090565b90919293828060019267ffffffffffffffff88356107768161072b565b1681520195019392910161074b565b92939160209161079d9160408652604086019161073d565b8193828183039101528281520192915f5b8281106107bc575050505090565b9091929382806001926001600160a01b0388356107d8816104a3565b168152019501939291016107ae565b634e487b7160e01b5f52603260045260245ffd5b90156108045790565b6107e7565b91908110156108045760051b0190565b356108238161072b565b90565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff82111761085657604052565b610826565b6040805190929167ffffffffffffffff9160a0810183811182821017610856578552809482549363ffffffff948581168452818160201c16602085015260601c8284015260ff60018501541615156060840152815194606086018681108382111761085657608095600291855201549081168652818160201c16602087015260601c16908401520152565b92916108fe5f9260209260408752604087019161073d565b930152565b916109246020926001600160a01b039296959660408652604086019161073d565b9416910152565b634e487b7160e01b5f52601160045260245ffd5b5f1981019190821161094d57565b61092b565b9190820391821161094d57565b906001820180921161094d57565b9190820180921161094d57565b35610823816104a3565b80518210156108045760209160051b010190565b9081156109a157565b60046040517f38186224000000000000000000000000000000000000000000000000000000008152fd5b63ffffffff6080820151511615610a2a57604001516001600160a01b03163381036109f35750565b604490604051907f8907fc650000000000000000000000000000000000000000000000000000000082523360048301526024820152fd5b60046040517f961e3e8c000000000000000000000000000000000000000000000000000000008152fd5b67ffffffffffffffff81116108565760051b60200190565b90610a7682610a54565b60405190601f1990601f018116820167ffffffffffffffff81118382101761085657604052838252610aa88294610a54565b0190602036910137565b9091610ae3610ad6610ac761057186866107fb565b60081c66ffffffffffffff1690565b67ffffffffffffffff1690565b91610b19610b14610b0f85610b0a610ad6610ac7610571610b038c61093f565b8c8a610809565b610952565b61095f565b610a6c565b935f90815b818310610b2b5750505050565b610b39610571848487610809565b90610b7b61044461043f8467ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b83151580610c30575b610bc9575060019060ff8116610bc083610bab8a66ffffffffffffff600887901c16610952565b921b610bb7838c610984565b5117918a610984565b52920191610b1e565b67ffffffffffffffff809116911614610c065760046040517fdd020e25000000000000000000000000000000000000000000000000000000008152fd5b60046040517fa5a1ff5d000000000000000000000000000000000000000000000000000000008152fd5b5067ffffffffffffffff8082169083161115610b84565b6001600160a01b031615610c5757565b60046040517f8579befe000000000000000000000000000000000000000000000000000000008152fd5b604051906020808301815f6301ffc9a760e01b9586845286602482015260248152610cab8161083a565b51617530938685fa933d5f519086610d66575b5085610d5c575b5084610ce2575b50505081610cd8575090565b6108239150610d71565b839450905f9183946040518581019283527fffffffff00000000000000000000000000000000000000000000000000000000602482015260248152610d268161083a565b5192fa5f5190913d83610d51575b505081610d47575b5015905f8080610ccc565b905015155f610d3c565b101591505f80610d34565b151594505f610cc5565b84111595505f610cbe565b5f602091604051838101906301ffc9a760e01b82527f830639ac00000000000000000000000000000000000000000000000000000000602482015260248152610db98161083a565b5191617530fa5f513d82610dd9575b5081610dd2575090565b9050151590565b6020111591505f610dc856fea26469706673582212204d674a17922996f3e134dc3346a026b33f9e53c005c10ba3e0c67e851882e46864736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/modules/SSVOperatorsWhitelist.sol", + "buildInfoId": "solc-0_8_24-dc3260967516f45b660c2554111e2a58f5f3b385" +} \ No newline at end of file diff --git a/test/setup/artifacts/SSVViewsLegacy.json b/test/setup/artifacts/SSVViewsLegacy.json new file mode 100644 index 000000000..4bd032d98 --- /dev/null +++ b/test/setup/artifacts/SSVViewsLegacy.json @@ -0,0 +1,859 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "SSVViews", + "sourceName": "contracts/modules/SSVViews.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "AddressIsWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "ApprovalNotWithinTimeframe", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "CallerNotOwnerWithData", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotWhitelisted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "CallerNotWhitelistedWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterAlreadyEnabled", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterDoesNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterIsLiquidated", + "type": "error" + }, + { + "inputs": [], + "name": "ClusterNotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPublicKeysList", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimit", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "ExceedValidatorLimitWithData", + "type": "error" + }, + { + "inputs": [], + "name": "FeeExceedsIncreaseLimit", + "type": "error" + }, + { + "inputs": [], + "name": "FeeIncreaseNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "FeeTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectClusterState", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectValidatorState", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "IncorrectValidatorStateWithData", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOperatorIdsLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKeyLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidWhitelistAddressesLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "InvalidWhitelistingContract", + "type": "error" + }, + { + "inputs": [], + "name": "MaxValueExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "NewBlockPeriodIsBelowMinimum", + "type": "error" + }, + { + "inputs": [], + "name": "NoFeeDeclared", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorAlreadyExists", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorsListNotUnique", + "type": "error" + }, + { + "inputs": [], + "name": "PublicKeysSharesLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "SameFeeChangeNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "TargetModuleDoesNotExist", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "moduleId", + "type": "uint8" + } + ], + "name": "TargetModuleDoesNotExistWithData", + "type": "error" + }, + { + "inputs": [], + "name": "TokenTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsortedOperatorsList", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorAlreadyExists", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "ValidatorAlreadyExistsWithData", + "type": "error" + }, + { + "inputs": [], + "name": "ValidatorDoesNotExist", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddressNotAllowed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "getBurnRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidationThresholdPeriod", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMaximumOperatorFee", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMinimumLiquidationCollateral", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNetworkValidatorsCount", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorById", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fee", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "address", + "name": "whitelistedAddress", + "type": "address" + }, + { + "internalType": "bool", + "name": "isPrivate", + "type": "bool" + }, + { + "internalType": "bool", + "name": "isActive", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorDeclaredFee", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "id", + "type": "uint64" + } + ], + "name": "getOperatorEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "operatorId", + "type": "uint64" + } + ], + "name": "getOperatorFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeeIncreaseLimit", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOperatorFeePeriods", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + } + ], + "name": "getValidator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getValidatorsPerOperatorLimit", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getVersion", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "internalType": "address", + "name": "addressToCheck", + "type": "address" + } + ], + "name": "getWhitelistedOperators", + "outputs": [ + { + "internalType": "uint64[]", + "name": "whitelistedOperatorIds", + "type": "uint64[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addressToCheck", + "type": "address" + }, + { + "internalType": "uint256", + "name": "operatorId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "whitelistingContract", + "type": "address" + } + ], + "name": "isAddressWhitelistedInWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "clusterOwner", + "type": "address" + }, + { + "internalType": "uint64[]", + "name": "operatorIds", + "type": "uint64[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "validatorCount", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "networkFeeIndex", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "index", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "active", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "internalType": "struct ISSVNetworkCore.Cluster", + "name": "cluster", + "type": "tuple" + } + ], + "name": "isLiquidated", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "isWhitelistingContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x6080806040523461001657611c8d908161001b8239f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c806303b3d436146101745780630d8e6e2c1461016f57806314cb9d7b1461016a57806316cff008146101655780633e2ec1601461016057806346e6d9171461015b5780635ba3d62a1461015657806368465f7d146101515780636d0db0e41461014c578063777915cb146101475780639040f7c3146101425780639568f9d91461013d5780639ad3c74514610138578063a694695b14610133578063a9cf9eec1461012e578063bac69e6f14610129578063be3f058e14610124578063ca162e5e1461011f578063df02ef7f1461011a578063e6d2834d14610115578063eb8ecfa7146101105763fc0438301461010b575f80fd5b610d28565b610c3d565b610bce565b610b85565b610a7d565b610984565b610961565b6108de565b6108a9565b610872565b61082e565b6107e5565b610743565b610656565b61060d565b6105bc565b61057f565b61050d565b6104e7565b61030b565b610279565b61018f565b67ffffffffffffffff81160361018b57565b5f80fd5b3461018b57602036600319011261018b576004356101ac81610179565b67ffffffffffffffff8091165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10960205260405f2090610275604051926101f3846103c1565b5461023f610230610221858416808852868560401c1696604060208a0199898b52019560801c1685526115dd565b955167ffffffffffffffff1690565b915167ffffffffffffffff1690565b9060405194859415158592909493606092608085019615158552602085015267ffffffffffffffff809216604085015216910152565b0390f35b3461018b575f36600319011261018b576040805190610297826103e2565b600682526020907f76312e322e300000000000000000000000000000000000000000000000000000602084015260405191602083528351918260208501525f5b8381106102f85784604081865f838284010152601f80199101168101030190f35b85810183015185820183015282016102d7565b3461018b575f36600319011261018b57602063ffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460601c16604051908152f35b600435906001600160a01b038216820361018b57565b602435906001600160a01b038216820361018b57565b9181601f8401121561018b5782359167ffffffffffffffff831161018b576020808501948460051b01011161018b57565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff8211176103dd57604052565b6103ad565b6040810190811067ffffffffffffffff8211176103dd57604052565b60a0810190811067ffffffffffffffff8211176103dd57604052565b90601f8019910116810190811067ffffffffffffffff8211176103dd57604052565b8015150361018b57565b9060e060031983011261018b5761045b610350565b916024359167ffffffffffffffff831161018b5761047e8260a09460040161037c565b90939092604319011261018b57604051610497816103fe565b60443563ffffffff8116810361018b5781526064356104b581610179565b60208201526084356104c681610179565b604082015260a4356104d78161043c565b606082015260c435608082015290565b3461018b5760206105036104fa36610446565b92919091610f1d565b6040519015158152f35b3461018b57604036600319011261018b57610526610350565b60243567ffffffffffffffff80821161018b573660238301121561018b57816004013590811161018b57366024828401011161018b5761027592602461056d9301906110c8565b60405190151581529081906020820190565b3461018b57606036600319011261018b57610598610350565b604435906001600160a01b038216820361018b576020916105039160243590611169565b3461018b575f36600319011261018b57602061060567ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165460801c166115dd565b604051908152f35b3461018b575f36600319011261018b5760207f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175467ffffffffffffffff6040519160401c168152f35b3461018b57602036600319011261018b5761027561073361072e60406106b860043561068181610179565b67ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b61071e61071160028451936106cc856103fe565b805463ffffffff8116865267ffffffffffffffff8160201c16602087015260601c8686015261070b610702600183015460ff1690565b15156060870152565b01610e20565b916080810192835261187a565b51015167ffffffffffffffff1690565b6115dd565b6040519081529081906020820190565b3461018b575f36600319011261018b57602061060561072e67ffffffffffffffff6107df817f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165416917f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1554906107d563ffffffff916107c9838560401c16824316610ec3565b908460801c1690610ee2565b91871c1690610ee2565b90610f01565b3461018b575f36600319011261018b5760207f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165467ffffffffffffffff6040519160401c168152f35b3461018b575f36600319011261018b5760207f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155463ffffffff60405191831c168152f35b3461018b57602036600319011261018b57602061060567ffffffffffffffff6108a060043561068181610179565b54831c166115dd565b3461018b57602060606108cb6108d26108c136610446565b9491903691610d89565b90836115ff565b50015115604051908152f35b3461018b57604036600319011261018b5767ffffffffffffffff60043581811161018b5761091361092191369060040161037c565b61091b610366565b91611290565b6040519060208083016020845282518091526020604085019301915f5b82811061094b5785850386f35b835187168552938101939281019260010161093e565b3461018b57602036600319011261018b57602061050361097f610350565b611b0c565b3461018b57602036600319011261018b5760c06004356109a381610179565b6109de8167ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b8054906109f767ffffffffffffffff8360201c166115dd565b926001600160a01b03610a4063ffffffff9267ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b54169080600260ff60018601541694015416151593604051958160601c8752602087015216604085015260608401521515608083015260a0820152f35b3461018b57610aa4610a8e36610446565b92610a9d949194368685610d89565b90846115ff565b505f925f5b818110610b135761027561073361072e86610b0d610b04610af98b67ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460801c1690610f01565b925163ffffffff1690565b63ffffffff1690565b90610ee2565b610b31610b2c610681610b27848688610e03565b610e13565b610e59565b610b54610b4860408301516001600160a01b031690565b6001600160a01b031690565b610b62575b50600101610aa9565b600191956107df6020610b7e93015167ffffffffffffffff1690565b9490610b59565b3461018b575f36600319011261018b57602067ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175460801c16604051908152f35b3461018b575f36600319011261018b5760407f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165460c01c67ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17541682519182526020820152f35b3461018b57610c64610c4e36610446565b93909192610c5d368486610d89565b90856115ff565b50610c6e83611a37565b5f915f9167ffffffffffffffff4316905b808410610cb157610275608087610c9f88610c9861170a565b9083611763565b01516040519081529081906020820190565b90919293610d1e6001916107df610cd2610b2c610681610b278b898c610e03565b6107df608082015191610b0d6020610d0e610d08610b04610cfd8489015167ffffffffffffffff1690565b975163ffffffff1690565b8c610ec3565b92015167ffffffffffffffff1690565b9401929190610c7f565b3461018b575f36600319011261018b57602061060567ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460801c166115dd565b67ffffffffffffffff81116103dd5760051b60200190565b9291610d9482610d71565b91610da2604051938461041a565b829481845260208094019160051b810192831161018b57905b828210610dc85750505050565b8380918335610dd681610179565b815201910190610dbb565b634e487b7160e01b5f52603260045260245ffd5b9015610dfe5790565b610de1565b9190811015610dfe5760051b0190565b35610e1d81610179565b90565b90604051610e2d816103c1565b604081935463ffffffff8116835267ffffffffffffffff90818160201c16602085015260601c16910152565b90604051610e66816103fe565b6080610eaa60028395805463ffffffff8116865267ffffffffffffffff8160201c16602087015260601c604086015260ff6001820154161515606086015201610e20565b910152565b634e487b7160e01b5f52601160045260245ffd5b67ffffffffffffffff9182169082160391908211610edd57565b610eaf565b91909167ffffffffffffffff80809416911602918216918203610edd57565b91909167ffffffffffffffff80809416911601918211610edd57565b610f3090949391946108cb368588610d89565b50610f45610f416060830151151590565b1590565b6110c1575f935f905f9067ffffffffffffffff94854316905b808410610fe45750505050610f80610e1d9495610f7961170a565b9084611763565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460801c67ffffffffffffffff16907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165493808560801c169460401c16926117f1565b90919293610ff3858386610e03565b610ffc90610e13565b6110379067ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b61104090610e59565b9860808a015160208082015161105d9067ffffffffffffffff1690565b915161106f9063ffffffff1687610ec3565b9b019a8b516110859067ffffffffffffffff1690565b61108e91610ee2565b61109791610f01565b6110a091610f01565b985167ffffffffffffffff166110b591610f01565b93600101929190610f5e565b505f925050565b909160346111079160405193818592602084019788378201906bffffffffffffffffffffffff199060601b16602082015203601481018452018261041a565b5190205f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f205480156111445760018091161490565b505f90565b9081602091031261018b5751610e1d8161043c565b6040513d5f823e3d90fd5b909161117481611b0c565b1580156111f5575b6111ee5760446020926001600160a01b03809360405196879586946320c18e6b60e21b86521660048501526024840152165afa9081156111e9575f916111c0575090565b610e1d915060203d6020116111e2575b6111da818361041a565b810190611149565b503d6111d0565b61115e565b5050505f90565b506001600160a01b0382161561117c565b9061121082610d71565b61121d604051918261041a565b828152809261122e601f1991610d71565b0190602036910137565b9060018201809211610edd57565b91908201809211610edd57565b5f19810191908211610edd57565b91908203918211610edd57565b8051821015610dfe5760209160051b010190565b5f198114610edd5760010190565b929192811580156115b4575b6115ac57925f936112ad83826118f0565b6112b8859295611206565b946112c4815183611246565b91805b8381106114c657505050508584526112de81611206565b9586935f955f935f945b8186106112f9575050505050505052565b909192939495969761130f610b2788858b610e03565b9086831080611489575b1561135f579161134b6113509261133c8561133660019791611282565b9d61126e565b9067ffffffffffffffff169052565b611282565b955b01939291908996956112e8565b98906113af6113a28299949967ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b546001600160a01b031690565b6001600160a01b0380821690881681149182156113fd575b50506113d8575b5050600190611352565b6113f49061133c8b6113ee600196959d91611282565b9c61126e565b9050895f6113ce565b611408919250611b0c565b9081611417575b505f806113c7565b6040516320c18e6b60e21b81526001600160a01b038916600482015267ffffffffffffffff841660248201526020945091508390829060449082905afa9081156111e9578d935f9261146c575b50505f61140f565b6114829250803d106111e2576111da818361041a565b5f80611464565b506114b56114a861149a858861126e565b5167ffffffffffffffff1690565b67ffffffffffffffff1690565b67ffffffffffffffff831614611319565b6114de6114d883839c9798999c611261565b8461126e565b51806114f3575b5060010198959493986112c7565b61153e826115318d6001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b905f5260205260405f2090565b54168160081b5f5b61010081106115565750506114e5565b8a6001821b841661156b575b50600101611546565b976115909061133c6115806114a88587611246565b9161158a81611282565b9b61126e565b88881461159d578a611562565b50989a50505050505050505050565b506060925050565b506001600160a01b0384161561129c565b906298968091828102928184041490151715610edd57565b67ffffffffffffffff166298968090818102918183041490151715610edd5790565b9192906040516020808201926bffffffffffffffffffffffff199060601b1683526034820160208751919701915f5b8281106116ec57505050506116518161165a94959603601f19810183528261041a565b51902092611a6c565b61168b835f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f2090565b54806116bb5760046040517f185e2b16000000000000000000000000000000000000000000000000000000008152fd5b036116c257565b60046040517f12e04c87000000000000000000000000000000000000000000000000000000008152fd5b835167ffffffffffffffff168952978101979281019260010161162e565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155463ffffffff8116430390438211610edd5761175a610e1d9267ffffffffffffffff808460801c169116610ee2565b9060c01c610f01565b91906117b9906117b461178667ffffffffffffffff948560208801511690610ec3565b6117aa61179c63ffffffff928389511690610ee2565b938660408901511690610ec3565b9086511690610ee2565b610f01565b169060806117c6836115c5565b91019182518092115f146117db5750505f9052565b6117e4906115c5565b8103908111610edd579052565b9493909291925f9563ffffffff8082511661180e57505050505050565b90919293949596506080820195865167ffffffffffffffff809616906298968091828102928184041490151715610edd571061186e5761186a95610b0d6118589261186196610f01565b91511690610ee2565b925192166115c5565b1190565b50505050505050600190565b63ffffffff8043169160808101918083515116840391818311610edd576118d6916118b767ffffffffffffffff9482866020860151169116610ee2565b916020865101856118cb8582845116610f01565b169052511690610ee2565b906118e960408451019282845116610f01565b1690525152565b90916119146114a8611905610b278686610df5565b60081c66ffffffffffffff1690565b9161194a6119456119408561193b6114a8611905610b276119348c611253565b8c8a610e03565b611261565b611238565b611206565b935f90815b81831061195c5750505050565b61196a610b27848487610e03565b9083151580611a20575b6119b9575060019060ff81166119b08361199b8a66ffffffffffffff600887901c16611261565b921b6119a7838c61126e565b5117918a61126e565b5292019161194f565b67ffffffffffffffff8091169116146119f65760046040517fdd020e25000000000000000000000000000000000000000000000000000000008152fd5b60046040517fa5a1ff5d000000000000000000000000000000000000000000000000000000008152fd5b5067ffffffffffffffff8082169083161115611974565b6060015115611a4257565b60046040517f95a0cf33000000000000000000000000000000000000000000000000000000008152fd5b805190602081015160408201519160606080820151910151151591604051937fffffffff00000000000000000000000000000000000000000000000000000000602086019660e01b1686527fffffffffffffffff000000000000000000000000000000000000000000000000809260c01b16602486015260c01b16602c840152603483015260f81b605482015260358152611b06816103c1565b51902090565b604051906020808301815f6301ffc9a760e01b9586845286602482015260248152611b36816103c1565b51617530938685fa933d5f519086611bf1575b5085611be7575b5084611b6d575b50505081611b63575090565b610e1d9150611bfc565b839450905f9183946040518581019283527fffffffff00000000000000000000000000000000000000000000000000000000602482015260248152611bb1816103c1565b5192fa5f5190913d83611bdc575b505081611bd2575b5015905f8080611b57565b905015155f611bc7565b101591505f80611bbf565b151594505f611b50565b84111595505f611b49565b5f602091604051838101906301ffc9a760e01b82526320c18e6b60e21b602482015260248152611c2b816103c1565b5191617530fa5f513d82611c4b575b5081611c44575090565b9050151590565b6020111591505f611c3a56fea264697066735822122002ac68be9b0df16cb1ffe2459231b85b8d6488b52743ca8b1c3e3538dba0367464736f6c63430008180033", + "deployedBytecode": "0x60806040526004361015610011575f80fd5b5f3560e01c806303b3d436146101745780630d8e6e2c1461016f57806314cb9d7b1461016a57806316cff008146101655780633e2ec1601461016057806346e6d9171461015b5780635ba3d62a1461015657806368465f7d146101515780636d0db0e41461014c578063777915cb146101475780639040f7c3146101425780639568f9d91461013d5780639ad3c74514610138578063a694695b14610133578063a9cf9eec1461012e578063bac69e6f14610129578063be3f058e14610124578063ca162e5e1461011f578063df02ef7f1461011a578063e6d2834d14610115578063eb8ecfa7146101105763fc0438301461010b575f80fd5b610d28565b610c3d565b610bce565b610b85565b610a7d565b610984565b610961565b6108de565b6108a9565b610872565b61082e565b6107e5565b610743565b610656565b61060d565b6105bc565b61057f565b61050d565b6104e7565b61030b565b610279565b61018f565b67ffffffffffffffff81160361018b57565b5f80fd5b3461018b57602036600319011261018b576004356101ac81610179565b67ffffffffffffffff8091165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10960205260405f2090610275604051926101f3846103c1565b5461023f610230610221858416808852868560401c1696604060208a0199898b52019560801c1685526115dd565b955167ffffffffffffffff1690565b915167ffffffffffffffff1690565b9060405194859415158592909493606092608085019615158552602085015267ffffffffffffffff809216604085015216910152565b0390f35b3461018b575f36600319011261018b576040805190610297826103e2565b600682526020907f76312e322e300000000000000000000000000000000000000000000000000000602084015260405191602083528351918260208501525f5b8381106102f85784604081865f838284010152601f80199101168101030190f35b85810183015185820183015282016102d7565b3461018b575f36600319011261018b57602063ffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460601c16604051908152f35b600435906001600160a01b038216820361018b57565b602435906001600160a01b038216820361018b57565b9181601f8401121561018b5782359167ffffffffffffffff831161018b576020808501948460051b01011161018b57565b634e487b7160e01b5f52604160045260245ffd5b6060810190811067ffffffffffffffff8211176103dd57604052565b6103ad565b6040810190811067ffffffffffffffff8211176103dd57604052565b60a0810190811067ffffffffffffffff8211176103dd57604052565b90601f8019910116810190811067ffffffffffffffff8211176103dd57604052565b8015150361018b57565b9060e060031983011261018b5761045b610350565b916024359167ffffffffffffffff831161018b5761047e8260a09460040161037c565b90939092604319011261018b57604051610497816103fe565b60443563ffffffff8116810361018b5781526064356104b581610179565b60208201526084356104c681610179565b604082015260a4356104d78161043c565b606082015260c435608082015290565b3461018b5760206105036104fa36610446565b92919091610f1d565b6040519015158152f35b3461018b57604036600319011261018b57610526610350565b60243567ffffffffffffffff80821161018b573660238301121561018b57816004013590811161018b57366024828401011161018b5761027592602461056d9301906110c8565b60405190151581529081906020820190565b3461018b57606036600319011261018b57610598610350565b604435906001600160a01b038216820361018b576020916105039160243590611169565b3461018b575f36600319011261018b57602061060567ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165460801c166115dd565b604051908152f35b3461018b575f36600319011261018b5760207f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175467ffffffffffffffff6040519160401c168152f35b3461018b57602036600319011261018b5761027561073361072e60406106b860043561068181610179565b67ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b61071e61071160028451936106cc856103fe565b805463ffffffff8116865267ffffffffffffffff8160201c16602087015260601c8686015261070b610702600183015460ff1690565b15156060870152565b01610e20565b916080810192835261187a565b51015167ffffffffffffffff1690565b6115dd565b6040519081529081906020820190565b3461018b575f36600319011261018b57602061060561072e67ffffffffffffffff6107df817f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165416917f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa1554906107d563ffffffff916107c9838560401c16824316610ec3565b908460801c1690610ee2565b91871c1690610ee2565b90610f01565b3461018b575f36600319011261018b5760207f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165467ffffffffffffffff6040519160401c168152f35b3461018b575f36600319011261018b5760207f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155463ffffffff60405191831c168152f35b3461018b57602036600319011261018b57602061060567ffffffffffffffff6108a060043561068181610179565b54831c166115dd565b3461018b57602060606108cb6108d26108c136610446565b9491903691610d89565b90836115ff565b50015115604051908152f35b3461018b57604036600319011261018b5767ffffffffffffffff60043581811161018b5761091361092191369060040161037c565b61091b610366565b91611290565b6040519060208083016020845282518091526020604085019301915f5b82811061094b5785850386f35b835187168552938101939281019260010161093e565b3461018b57602036600319011261018b57602061050361097f610350565b611b0c565b3461018b57602036600319011261018b5760c06004356109a381610179565b6109de8167ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b8054906109f767ffffffffffffffff8360201c166115dd565b926001600160a01b03610a4063ffffffff9267ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b54169080600260ff60018601541694015416151593604051958160601c8752602087015216604085015260608401521515608083015260a0820152f35b3461018b57610aa4610a8e36610446565b92610a9d949194368685610d89565b90846115ff565b505f925f5b818110610b135761027561073361072e86610b0d610b04610af98b67ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460801c1690610f01565b925163ffffffff1690565b63ffffffff1690565b90610ee2565b610b31610b2c610681610b27848688610e03565b610e13565b610e59565b610b54610b4860408301516001600160a01b031690565b6001600160a01b031690565b610b62575b50600101610aa9565b600191956107df6020610b7e93015167ffffffffffffffff1690565b9490610b59565b3461018b575f36600319011261018b57602067ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa175460801c16604051908152f35b3461018b575f36600319011261018b5760407f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165460c01c67ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa17541682519182526020820152f35b3461018b57610c64610c4e36610446565b93909192610c5d368486610d89565b90856115ff565b50610c6e83611a37565b5f915f9167ffffffffffffffff4316905b808410610cb157610275608087610c9f88610c9861170a565b9083611763565b01516040519081529081906020820190565b90919293610d1e6001916107df610cd2610b2c610681610b278b898c610e03565b6107df608082015191610b0d6020610d0e610d08610b04610cfd8489015167ffffffffffffffff1690565b975163ffffffff1690565b8c610ec3565b92015167ffffffffffffffff1690565b9401929190610c7f565b3461018b575f36600319011261018b57602061060567ffffffffffffffff7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460801c166115dd565b67ffffffffffffffff81116103dd5760051b60200190565b9291610d9482610d71565b91610da2604051938461041a565b829481845260208094019160051b810192831161018b57905b828210610dc85750505050565b8380918335610dd681610179565b815201910190610dbb565b634e487b7160e01b5f52603260045260245ffd5b9015610dfe5790565b610de1565b9190811015610dfe5760051b0190565b35610e1d81610179565b90565b90604051610e2d816103c1565b604081935463ffffffff8116835267ffffffffffffffff90818160201c16602085015260601c16910152565b90604051610e66816103fe565b6080610eaa60028395805463ffffffff8116865267ffffffffffffffff8160201c16602087015260601c604086015260ff6001820154161515606086015201610e20565b910152565b634e487b7160e01b5f52601160045260245ffd5b67ffffffffffffffff9182169082160391908211610edd57565b610eaf565b91909167ffffffffffffffff80809416911602918216918203610edd57565b91909167ffffffffffffffff80809416911601918211610edd57565b610f3090949391946108cb368588610d89565b50610f45610f416060830151151590565b1590565b6110c1575f935f905f9067ffffffffffffffff94854316905b808410610fe45750505050610f80610e1d9495610f7961170a565b9084611763565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155460801c67ffffffffffffffff16907f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa165493808560801c169460401c16926117f1565b90919293610ff3858386610e03565b610ffc90610e13565b6110379067ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10a60205260405f2090565b61104090610e59565b9860808a015160208082015161105d9067ffffffffffffffff1690565b915161106f9063ffffffff1687610ec3565b9b019a8b516110859067ffffffffffffffff1690565b61108e91610ee2565b61109791610f01565b6110a091610f01565b985167ffffffffffffffff166110b591610f01565b93600101929190610f5e565b505f925050565b909160346111079160405193818592602084019788378201906bffffffffffffffffffffffff199060601b16602082015203601481018452018261041a565b5190205f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10460205260405f205480156111445760018091161490565b505f90565b9081602091031261018b5751610e1d8161043c565b6040513d5f823e3d90fd5b909161117481611b0c565b1580156111f5575b6111ee5760446020926001600160a01b03809360405196879586946320c18e6b60e21b86521660048501526024840152165afa9081156111e9575f916111c0575090565b610e1d915060203d6020116111e2575b6111da818361041a565b810190611149565b503d6111d0565b61115e565b5050505f90565b506001600160a01b0382161561117c565b9061121082610d71565b61121d604051918261041a565b828152809261122e601f1991610d71565b0190602036910137565b9060018201809211610edd57565b91908201809211610edd57565b5f19810191908211610edd57565b91908203918211610edd57565b8051821015610dfe5760209160051b010190565b5f198114610edd5760010190565b929192811580156115b4575b6115ac57925f936112ad83826118f0565b6112b8859295611206565b946112c4815183611246565b91805b8381106114c657505050508584526112de81611206565b9586935f955f935f945b8186106112f9575050505050505052565b909192939495969761130f610b2788858b610e03565b9086831080611489575b1561135f579161134b6113509261133c8561133660019791611282565b9d61126e565b9067ffffffffffffffff169052565b611282565b955b01939291908996956112e8565b98906113af6113a28299949967ffffffffffffffff165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10860205260405f2090565b546001600160a01b031690565b6001600160a01b0380821690881681149182156113fd575b50506113d8575b5050600190611352565b6113f49061133c8b6113ee600196959d91611282565b9c61126e565b9050895f6113ce565b611408919250611b0c565b9081611417575b505f806113c7565b6040516320c18e6b60e21b81526001600160a01b038916600482015267ffffffffffffffff841660248201526020945091508390829060449082905afa9081156111e9578d935f9261146c575b50505f61140f565b6114829250803d106111e2576111da818361041a565b5f80611464565b506114b56114a861149a858861126e565b5167ffffffffffffffff1690565b67ffffffffffffffff1690565b67ffffffffffffffff831614611319565b6114de6114d883839c9798999c611261565b8461126e565b51806114f3575b5060010198959493986112c7565b61153e826115318d6001600160a01b03165f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10d60205260405f2090565b905f5260205260405f2090565b54168160081b5f5b61010081106115565750506114e5565b8a6001821b841661156b575b50600101611546565b976115909061133c6115806114a88587611246565b9161158a81611282565b9b61126e565b88881461159d578a611562565b50989a50505050505050505050565b506060925050565b506001600160a01b0384161561129c565b906298968091828102928184041490151715610edd57565b67ffffffffffffffff166298968090818102918183041490151715610edd5790565b9192906040516020808201926bffffffffffffffffffffffff199060601b1683526034820160208751919701915f5b8281106116ec57505050506116518161165a94959603601f19810183528261041a565b51902092611a6c565b61168b835f527fd56c4f4aab8ca22f9fde432777379f436593c6027698a6995e2daea890bed10560205260405f2090565b54806116bb5760046040517f185e2b16000000000000000000000000000000000000000000000000000000008152fd5b036116c257565b60046040517f12e04c87000000000000000000000000000000000000000000000000000000008152fd5b835167ffffffffffffffff168952978101979281019260010161162e565b7f0f1d85405047bdb6b0a60e27531f52a1f7a948613527b9b83a7552558207aa155463ffffffff8116430390438211610edd5761175a610e1d9267ffffffffffffffff808460801c169116610ee2565b9060c01c610f01565b91906117b9906117b461178667ffffffffffffffff948560208801511690610ec3565b6117aa61179c63ffffffff928389511690610ee2565b938660408901511690610ec3565b9086511690610ee2565b610f01565b169060806117c6836115c5565b91019182518092115f146117db5750505f9052565b6117e4906115c5565b8103908111610edd579052565b9493909291925f9563ffffffff8082511661180e57505050505050565b90919293949596506080820195865167ffffffffffffffff809616906298968091828102928184041490151715610edd571061186e5761186a95610b0d6118589261186196610f01565b91511690610ee2565b925192166115c5565b1190565b50505050505050600190565b63ffffffff8043169160808101918083515116840391818311610edd576118d6916118b767ffffffffffffffff9482866020860151169116610ee2565b916020865101856118cb8582845116610f01565b169052511690610ee2565b906118e960408451019282845116610f01565b1690525152565b90916119146114a8611905610b278686610df5565b60081c66ffffffffffffff1690565b9161194a6119456119408561193b6114a8611905610b276119348c611253565b8c8a610e03565b611261565b611238565b611206565b935f90815b81831061195c5750505050565b61196a610b27848487610e03565b9083151580611a20575b6119b9575060019060ff81166119b08361199b8a66ffffffffffffff600887901c16611261565b921b6119a7838c61126e565b5117918a61126e565b5292019161194f565b67ffffffffffffffff8091169116146119f65760046040517fdd020e25000000000000000000000000000000000000000000000000000000008152fd5b60046040517fa5a1ff5d000000000000000000000000000000000000000000000000000000008152fd5b5067ffffffffffffffff8082169083161115611974565b6060015115611a4257565b60046040517f95a0cf33000000000000000000000000000000000000000000000000000000008152fd5b805190602081015160408201519160606080820151910151151591604051937fffffffff00000000000000000000000000000000000000000000000000000000602086019660e01b1686527fffffffffffffffff000000000000000000000000000000000000000000000000809260c01b16602486015260c01b16602c840152603483015260f81b605482015260358152611b06816103c1565b51902090565b604051906020808301815f6301ffc9a760e01b9586845286602482015260248152611b36816103c1565b51617530938685fa933d5f519086611bf1575b5085611be7575b5084611b6d575b50505081611b63575090565b610e1d9150611bfc565b839450905f9183946040518581019283527fffffffff00000000000000000000000000000000000000000000000000000000602482015260248152611bb1816103c1565b5192fa5f5190913d83611bdc575b505081611bd2575b5015905f8080611b57565b905015155f611bc7565b101591505f80611bbf565b151594505f611b50565b84111595505f611b49565b5f602091604051838101906301ffc9a760e01b82526320c18e6b60e21b602482015260248152611c2b816103c1565b5191617530fa5f513d82611c4b575b5081611c44575090565b9050151590565b6020111591505f611c3a56fea264697066735822122002ac68be9b0df16cb1ffe2459231b85b8d6488b52743ca8b1c3e3538dba0367464736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/modules/SSVViews.sol", + "buildInfoId": "solc-0_8_24-dc3260967516f45b660c2554111e2a58f5f3b385" +} \ No newline at end of file diff --git a/test/setup/connection.ts b/test/setup/connection.ts new file mode 100644 index 000000000..d711335d0 --- /dev/null +++ b/test/setup/connection.ts @@ -0,0 +1,18 @@ +import hre from "hardhat"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../common/types.ts"; +export { getForkedConnection } from "./fork.ts"; + +export async function getTestConnection(): Promise<{ + connection: NetworkConnection<"generic">; + ethers: NetworkConnection<"generic">["ethers"]; + networkHelpers: NetworkHelpersType; +}> { + const connection = await hre.network.connect("hardhat"); + + return { + connection, + ethers: connection.ethers, + networkHelpers: connection.networkHelpers, + }; +} \ No newline at end of file diff --git a/test/setup/deploy.ts b/test/setup/deploy.ts new file mode 100644 index 000000000..091f2600f --- /dev/null +++ b/test/setup/deploy.ts @@ -0,0 +1,37 @@ +import type { NetworkConnection } from "hardhat/types/network"; +import { Contract } from "ethers"; +import { SSVModules } from '../common/types.ts'; +import { SSV_MODULE_CONTRACTS } from '../common/constants.ts'; +import { getHarnessName } from '../common/helpers.ts'; + +export async function deployToken( + connection: NetworkConnection<"generic"> +): Promise { + const { ethers } = connection; + const [deployer] = await ethers.getSigners(); + + return ethers.deployContract( + "ERC20Mock", + ["SSV", "SSV", deployer.address, ethers.parseEther("1000000")] + ); +} + +export async function deployModule( + connection: NetworkConnection<"generic">, + module: SSVModules +): Promise { + return connection.ethers.deployContract( + SSV_MODULE_CONTRACTS[module] + ); +} + +export async function deployHarnessModule( + connection: NetworkConnection<"generic">, + module: SSVModules, + args: unknown[] = [] +): Promise { + return connection.ethers.deployContract( + getHarnessName(module), + args + ); +} \ No newline at end of file diff --git a/test/setup/fixtures.ts b/test/setup/fixtures.ts new file mode 100644 index 000000000..97f9149bd --- /dev/null +++ b/test/setup/fixtures.ts @@ -0,0 +1,705 @@ +import type { NetworkConnection } from "hardhat/types/network"; +import { SSVClustersHarness, SSVValidatorsHarness, SSVOperatorsHarness, SSVDAOHarness, SSVStakingHarness } from '../../types/ethers-contracts/index.js'; +import { deployHarnessModule } from './deploy.ts'; +import { SSVModules } from '../common/types.ts'; +import { makeOperatorKey } from '../common/helpers.ts'; +import { + getDeployer, + deployContract, + deployProxy, + attachModule, + upgradeProxy, +} from "../../scripts/common/helpers.ts"; +import { CSSVToken, SSVNetwork, SSVNetworkViews, SSVToken } from '../../types/ethers-contracts/index.js'; +import { + DECLARE_OPERATOR_FEE_PERIOD, EXECUTE_OPERATOR_FEE_PERIOD, + DEFAULT_UNSTAKE_COOLDOWN, + MAXIMUM_OPERATORS_FEE, MINIMAL_LIQUIDATION_THRESHOLD, + MINIMUM_BLOCKS_BEFORE_LIQUIDATION, + MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, + MINIMAL_OPERATOR_ETH_FEE, + NETWORK_FEE, NETWORK_FEE_ETH, OPERATOR_MAX_FEE_INCREASE, VALIDATORS_PER_OPERATOR_LIMIT, +} from '../common/constants.js'; +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { ForkConfig } from '../test-forked/v2.0.0/config.ts'; +import { ethers } from 'ethers'; +import legacyNetworkArtifact from './artifacts/SSVNetworkLegacy.json' assert { type: 'json' }; +import legacySSVNetworkViewsArtifact from "./artifacts/SSVNetworkViewsLegacy.json" assert { type: 'json' }; +import legacyClustersArtifact from "./artifacts/SSVClustersLegacy.json" assert { type: "json" }; +import legacyOperatorsArtifact from "./artifacts/SSVOperatorsLegacy.json" assert { type: "json" }; +import legacyDAOLegacyArtifact from "./artifacts/SSVDAOLegacy.json" assert { type: "json" }; +import legacyOperatorsWhitelistArtifact from "./artifacts/SSVOperatorsWhitelistLegacy.json" assert { type: "json" }; +import legacyViewsModuleArtifact from "./artifacts/SSVViewsLegacy.json" assert { type: "json" }; + +export async function ssvClustersHarnessFixture( + connection: NetworkConnection<"generic">, + operatorCount = 4, + operatorFee = 0n +): Promise<{ + clusters: SSVClustersHarness; + operatorIds: bigint[]; +}> { + const clusters = await deployHarnessModule( + connection, + SSVModules.SSVClusters + ); + await clusters.waitForDeployment(); + + await clusters.mockValidatorsPerOperatorLimit(3000); + + const [owner] = await connection.ethers.getSigners(); + + const operatorIds: bigint[] = []; + + for (let i = 0; i < operatorCount; i++) { + const operatorKey = makeOperatorKey(i); + + const operatorId: bigint = + await clusters.mockOperator.staticCall( + operatorKey, + owner.address, + operatorFee, + false + ); + + await clusters.mockOperator( + operatorKey, + owner.address, + operatorFee, + false + ); + + operatorIds.push(operatorId); + } + + return { + clusters, + operatorIds, + }; +} + +export async function ssvValidatorsHarnessFixture( + connection: NetworkConnection<"generic">, + operatorCount = 4, + operatorFee = 0n +): Promise<{ + validators: SSVValidatorsHarness; + operatorIds: bigint[]; +}> { + const validators = await deployHarnessModule( + connection, + SSVModules.SSVValidators + ); + await validators.waitForDeployment(); + + await validators.mockValidatorsPerOperatorLimit(3000); + + const [owner] = await connection.ethers.getSigners(); + + const operatorIds: bigint[] = []; + + for (let i = 0; i < operatorCount; i++) { + const operatorKey = makeOperatorKey(i); + + const operatorId: bigint = + await validators.mockOperator.staticCall( + operatorKey, + owner.address, + operatorFee, + false + ); + + await validators.mockOperator( + operatorKey, + owner.address, + operatorFee, + false + ); + + operatorIds.push(operatorId); + } + + return { + validators, + operatorIds, + }; +} + +export const getValidatorsHarnessFixture = ( + connection: NetworkConnection<"generic">, + operatorCount: number +) => + async function validatorsHarnessFixtureWithOperators() { + return ssvValidatorsHarnessFixture(connection, operatorCount); + }; + +export const getClustersHarnessFixture = ( + connection: NetworkConnection<"generic">, + operatorCount: number +) => + async function clustersHarnessFixtureWithOperators() { + return ssvClustersHarnessFixture(connection, operatorCount); + }; + + +export async function ssvOperatorsHarnessFixture( + connection: NetworkConnection<"generic">, + operatorMaxFee = MAXIMUM_OPERATORS_FEE, + declarePeriod = DECLARE_OPERATOR_FEE_PERIOD, + executePeriod = EXECUTE_OPERATOR_FEE_PERIOD, + maxFeeIncrease = OPERATOR_MAX_FEE_INCREASE, + upgradeTimestamp = 0n +): Promise<{ operators: SSVOperatorsHarness; }> { + const operators = await deployHarnessModule(connection, SSVModules.SSVOperators, [upgradeTimestamp]); + await operators.waitForDeployment(); + + await operators.mockSetOperatorMaxFee(Number(operatorMaxFee)); + await operators.mockSetFeePeriods(Number(declarePeriod), Number(executePeriod)); + await operators.mockSetOperatorMaxFeeIncrease(Number(maxFeeIncrease)); + + return { operators }; +} + +export async function ssvDAOHarnessFixture( + connection: NetworkConnection<"generic"> +): Promise<{ dao: SSVDAOHarness; cssv: any }> { + const { contract: cssv, address: cssvTokenAddress } = await deployContract(connection.ethers, "MockCSSV"); + const dao = await deployHarnessModule(connection, SSVModules.SSVDAO, [cssvTokenAddress]); + await dao.waitForDeployment(); + + return { dao, cssv }; +} + +export async function ssvStakingHarnessFixture( + connection: NetworkConnection<"generic">, + cooldownDuration = DEFAULT_UNSTAKE_COOLDOWN +): Promise<{ + staking: SSVStakingHarness; + ssvToken: SSVToken; + cssvToken: CSSVToken; +}> { + const { contract: cssvToken, address: cssvTokenAddress } = await deployContract(connection.ethers, "MockCSSV") + + const staking = await deployHarnessModule(connection, SSVModules.SSVStaking, [cssvTokenAddress]); + await staking.waitForDeployment(); + + const [deployer] = await connection.ethers.getSigners(); + + const ssvToken = await connection.ethers.deployContract("MockToken"); + await ssvToken.waitForDeployment(); + + await ssvToken.mint(deployer.address, connection.ethers.parseEther("1000000")); + + await staking.mockSetToken(await ssvToken.getAddress()); + await staking.mockSetCooldownDuration(cooldownDuration); + + await staking.mockSetDefaultOracleIds([1, 2, 3, 4]); + + return { + staking, + ssvToken, + cssvToken, + }; +} + +const QUORUM_BPS = 7500; +const DEFAULT_ORACLE_IDS = [1, 2, 3, 4]; + +const params = { + minimumBlocksBeforeLiquidation: MINIMUM_BLOCKS_BEFORE_LIQUIDATION, + minimumLiquidationCollateral: MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, + validatorsPerOperatorLimit: VALIDATORS_PER_OPERATOR_LIMIT, + declareOperatorFeePeriod: DECLARE_OPERATOR_FEE_PERIOD, + executeOperatorFeePeriod: EXECUTE_OPERATOR_FEE_PERIOD, + operatorMaxFeeIncrease: OPERATOR_MAX_FEE_INCREASE, + defaultOracleIds: DEFAULT_ORACLE_IDS, + quorumBps: QUORUM_BPS, +}; + +export async function ssvNetworkFullFixture( + connection: NetworkConnection<"generic"> +): Promise<{ + network: SSVNetwork; + views: SSVNetworkViews; + cssvToken: CSSVToken; + ssvToken: SSVToken; + modules: { [key: string]: string }; +}> { + const deployer = await getDeployer(connection.ethers); + + const { contract: ssvToken } = await deployContract(connection.ethers, "SSVToken"); + + const { address: networkImplAddr } = await deployContract(connection.ethers, "SSVNetwork"); + + const networkFactory = await connection.ethers.getContractFactory("SSVNetwork"); + const networkInitData = networkFactory.interface.encodeFunctionData("initialize", [ + await ssvToken.getAddress(), + ethers.ZeroAddress, + ethers.ZeroAddress, + ethers.ZeroAddress, + ethers.ZeroAddress, + params, + ]); + + const { address: networkProxyAddr } = await deployProxy( + connection.ethers, + deployer, + networkImplAddr, + networkInitData + ); + + const { contract: cssvToken } = await deployContract(connection.ethers, "CSSVToken", [networkProxyAddr]); + + const moduleNames = [ + "SSVClusters", + "SSVOperatorsWhitelist", + "SSVValidators", + ]; + const moduleAddresses: { [key: string]: string } = {}; + + const { address: ssvOperatorsAddr } = await deployContract(connection.ethers, "SSVOperators", [0]); + moduleAddresses["SSVOperators"] = ssvOperatorsAddr; + + const { address: ssvDaoAddr } = await deployContract(connection.ethers, "SSVDAO", [await cssvToken.getAddress()]); + moduleAddresses["SSVDAO"] = ssvDaoAddr; + + const { address: ssvViewsAddr } = await deployContract(connection.ethers, "SSVViews", [await cssvToken.getAddress()]); + moduleAddresses["SSVViews"] = ssvViewsAddr; + + const { address: ssvStakingAddr } = await deployContract(connection.ethers, "SSVStaking", [await cssvToken.getAddress()]); + moduleAddresses["SSVStaking"] = ssvStakingAddr; + + for (const mod of moduleNames) { + const { address } = await deployContract(connection.ethers, mod); + moduleAddresses[mod] = address; + } + + const network = networkFactory.attach(networkProxyAddr); + + await attachModule(connection.ethers, networkProxyAddr, "SSVOperatorsWhitelist", moduleAddresses["SSVOperatorsWhitelist"]); + await attachModule(connection.ethers, networkProxyAddr, "SSVStaking", moduleAddresses["SSVStaking"]); + await attachModule(connection.ethers, networkProxyAddr, "SSVValidators", moduleAddresses["SSVValidators"]); + await attachModule(connection.ethers, networkProxyAddr, "SSVViews", moduleAddresses["SSVViews"]); + await attachModule(connection.ethers, networkProxyAddr, "SSVDAO", moduleAddresses["SSVDAO"]); + await attachModule(connection.ethers, networkProxyAddr, "SSVOperators", moduleAddresses["SSVOperators"]); + await attachModule(connection.ethers, networkProxyAddr, "SSVClusters", moduleAddresses["SSVClusters"]); + + + const { address: viewsImplAddr } = await deployContract(connection.ethers, "SSVNetworkViews"); + + const viewsFactory = await connection.ethers.getContractFactory("SSVNetworkViews"); + const viewsInitData = viewsFactory.interface.encodeFunctionData("initialize", [networkProxyAddr]); + + const { address: viewsProxyAddr } = await deployProxy( + connection.ethers, + deployer, + viewsImplAddr, + viewsInitData + ); + + const views = viewsFactory.attach(viewsProxyAddr); + + const { address: upgradeImplAddr } = await deployContract(connection.ethers, "SSVNetworkSSVStakingUpgrade"); + + const cooldown = DEFAULT_UNSTAKE_COOLDOWN; + + await upgradeProxy( + connection.ethers, + deployer, + networkProxyAddr, + upgradeImplAddr, + "SSVNetworkSSVStakingUpgrade", + "initializeSSVStaking(uint64,uint32[4],uint16)", + [ + cooldown, + DEFAULT_ORACLE_IDS, + QUORUM_BPS, + ] + ); + + await network.updateNetworkFeeSSV(NETWORK_FEE); + await network.updateNetworkFee(NETWORK_FEE); + await network.updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL); + await network.updateLiquidationThresholdPeriod(MINIMAL_LIQUIDATION_THRESHOLD); + await network.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE); + await network.updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE); + await network.updateMinimumOperatorEthFee(MINIMAL_OPERATOR_ETH_FEE); + + return { + network, + views, + cssvToken, + ssvToken, + modules: moduleAddresses, + }; +} + +export async function ssvNetworkFullForkedFixture( + connection: NetworkConnection<"generic"> +): Promise<{ + network: SSVNetwork; + views: SSVNetworkViews; + cssvToken: CSSVToken; + ssvToken: SSVToken; + modules: { [key: string]: string }; + daoSigner: HardhatEthersSigner +}> { + const ethers = connection.ethers; + const useDeployedState = process.env.FORK_USE_DEPLOYED_STATE === "true"; + const strictDeployedState = process.env.FORK_STRICT_DEPLOYED_STATE === "true"; + const allowDeployedFallback = process.env.FORK_ALLOW_DEPLOYED_FALLBACK !== "false"; + + await ethers.provider.send("hardhat_impersonateAccount", [ForkConfig.DAO_ADDRESS]); + const daoSigner = await ethers.getSigner(ForkConfig.DAO_ADDRESS); + await ethers.provider.send("hardhat_setBalance", [ForkConfig.DAO_ADDRESS, "0x" + (BigInt(1e18) * 100n).toString(16)]); + + const runInTestUpgradePath = async () => { + const { contract: cssvToken } = await deployContract(ethers, "CSSVToken", [ForkConfig.SSV_NETWORK_ADDRESS]); + const modules: { [key: string]: string } = {}; + + const { address: ssvOperatorsAddr } = await deployContract(ethers, "SSVOperators", [0]); + modules["SSVOperators"] = ssvOperatorsAddr; + + const { address: ssvClustersAddr } = await deployContract(ethers, "SSVClusters"); + modules["SSVClusters"] = ssvClustersAddr; + + const { address: ssvDaoAddr } = await deployContract(ethers, "SSVDAO", [await cssvToken.getAddress()]); + modules["SSVDAO"] = ssvDaoAddr; + + const { address: ssvViewsAddr } = await deployContract(ethers, "SSVViews", [await cssvToken.getAddress()]); + modules["SSVViews"] = ssvViewsAddr; + + const { address: ssvOperatorsWhitelistAddr } = await deployContract(ethers, "SSVOperatorsWhitelist"); + modules["SSVOperatorsWhitelist"] = ssvOperatorsWhitelistAddr; + + const { address: ssvStakingAddr } = await deployContract(ethers, "SSVStaking", [await cssvToken.getAddress()]); + modules["SSVStaking"] = ssvStakingAddr; + + const { address: ssvValidatorsAddr } = await deployContract(ethers, "SSVValidators"); + modules["SSVValidators"] = ssvValidatorsAddr; + + const { address: networkImplAddr } = await deployContract(ethers, "SSVNetwork"); + const { address: stakingUpgradeImplAddr } = await deployContract(ethers, "SSVNetworkSSVStakingUpgrade"); + const { address: viewsImplAddr } = await deployContract(ethers, "SSVNetworkViews"); + + const networkFactory = await ethers.getContractFactory("SSVNetwork"); + const network = networkFactory.attach(ForkConfig.SSV_NETWORK_ADDRESS); + const daoNetwork = network.connect(daoSigner); + + const cooldown = DEFAULT_UNSTAKE_COOLDOWN; + const upgradeFactory = await ethers.getContractFactory("SSVNetworkSSVStakingUpgrade"); + const initData = upgradeFactory.interface.encodeFunctionData( + "initializeSSVStaking(uint64,uint32[4],uint16)", + [cooldown, DEFAULT_ORACLE_IDS, QUORUM_BPS] + ); + + try { + await (await daoNetwork.upgradeToAndCall(stakingUpgradeImplAddr, initData)).wait(); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + if (!message.includes("Initializable: contract is already initialized")) { + throw err; + } + console.warn( + "[FORK] initializeSSVStaking already executed on this proxy; continuing with non-init upgrade path." + ); + await (await daoNetwork.upgradeTo(stakingUpgradeImplAddr)).wait(); + } + await (await daoNetwork.upgradeTo(networkImplAddr)).wait(); + + const viewsFactory = await ethers.getContractFactory("SSVNetworkViews"); + const views = viewsFactory.attach(ForkConfig.SSV_NETWORK_VIEWS); + const daoViews = views.connect(daoSigner); + await (await daoViews.upgradeTo(viewsImplAddr)).wait(); + + for (const [moduleName, moduleAddress] of Object.entries(modules)) { + const moduleEnumKey = moduleName as keyof typeof SSVModules; + if (SSVModules[moduleEnumKey] === undefined) { + throw new Error(`Invalid module: ${moduleName}`); + } + const tx = await daoNetwork.updateModule(SSVModules[moduleEnumKey], moduleAddress); + await tx.wait(); + } + + const ssvTokenFactory = await ethers.getContractFactory("SSVToken"); + const ssvToken = ssvTokenFactory.attach(ForkConfig.SSV_TOKEN); + + await (await daoNetwork.updateNetworkFeeSSV(NETWORK_FEE)).wait(); + await (await daoNetwork.updateNetworkFee(NETWORK_FEE_ETH)).wait(); + await (await daoNetwork.updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL)).wait(); + await (await daoNetwork.updateMinimumLiquidationCollateralSSV(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL)).wait(); + await (await daoNetwork.updateLiquidationThresholdPeriod(MINIMUM_BLOCKS_BEFORE_LIQUIDATION)).wait(); + await (await daoNetwork.updateLiquidationThresholdPeriodSSV(MINIMUM_BLOCKS_BEFORE_LIQUIDATION)).wait(); + await (await daoNetwork.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE)).wait(); + await (await daoNetwork.updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE)).wait(); + await (await daoNetwork.updateMinimumOperatorEthFee(MINIMAL_OPERATOR_ETH_FEE)).wait(); + await (await daoNetwork.updateDeclareOperatorFeePeriod(DECLARE_OPERATOR_FEE_PERIOD)).wait(); + await (await daoNetwork.updateExecuteOperatorFeePeriod(EXECUTE_OPERATOR_FEE_PERIOD)).wait(); + + return { network, views, cssvToken, ssvToken, modules, daoSigner }; + }; + + if (!useDeployedState) { + return runInTestUpgradePath(); + } + + if (!ForkConfig.CSSV_TOKEN) { + throw new Error( + "FORK_USE_DEPLOYED_STATE=true requires cssvToken in FORK_CONFIG_PATH or FORK_CSSV_TOKEN env var" + ); + } + + const networkFactory = await ethers.getContractFactory("SSVNetwork"); + const network = networkFactory.attach(ForkConfig.SSV_NETWORK_ADDRESS); + + const viewsFactory = await ethers.getContractFactory("SSVNetworkViews"); + const views = viewsFactory.attach(ForkConfig.SSV_NETWORK_VIEWS); + + const cssvTokenFactory = await ethers.getContractFactory("CSSVToken"); + const cssvToken = cssvTokenFactory.attach(ForkConfig.CSSV_TOKEN); + + const ssvTokenFactory = await ethers.getContractFactory("SSVToken"); + const ssvToken = ssvTokenFactory.attach(ForkConfig.SSV_TOKEN); + + try { + await views.getVersion(); + await views.getNetworkFee(); + await views.getActiveOracleIds(); + } catch (err) { + if (strictDeployedState || !allowDeployedFallback) { + throw new Error( + "FORK_USE_DEPLOYED_STATE=true but deployed instances are not readable via SSVNetworkViews. " + + "Re-run `just upgrade-test-fork` against the same local Anvil endpoint and ensure no stale FORK_BLOCK_NUMBER.", + { cause: err as Error } + ); + } + + console.warn( + "[FORK] Deployed state is unreadable via SSVNetworkViews; falling back to in-test upgrade path. " + + "Set FORK_STRICT_DEPLOYED_STATE=true to enforce strict mode." + ); + return runInTestUpgradePath(); + } + + const modules: { [key: string]: string } = { ...ForkConfig.MODULES }; + return { network, views, cssvToken, ssvToken, modules, daoSigner }; +} + +export async function ssvNetworkFullPreUpgradeFixture( + connection: NetworkConnection<"generic"> +): Promise<{ + network: any; + views: any; + ssvToken: SSVToken; +}> { + const deployer = await getDeployer(connection.ethers); + + const { contract: ssvToken } = await deployContract( + connection.ethers, + "SSVToken" + ); + + const oldNetworkFactory = + await connection.ethers.getContractFactoryFromArtifact( + legacyNetworkArtifact + ); + + const legacyNetworkImpl = await oldNetworkFactory.deploy(); + await legacyNetworkImpl.waitForDeployment(); + + const networkInitData = oldNetworkFactory.interface.encodeFunctionData( + "initialize", + [ + await ssvToken.getAddress(), + ethers.ZeroAddress, + ethers.ZeroAddress, + ethers.ZeroAddress, + ethers.ZeroAddress, + params.minimumBlocksBeforeLiquidation, + params.minimumLiquidationCollateral, + params.validatorsPerOperatorLimit, + params.declareOperatorFeePeriod, + params.executeOperatorFeePeriod, + params.operatorMaxFeeIncrease, + ] + ); + + const { address: networkProxyAddr } = await deployProxy( + connection.ethers, + deployer, + await legacyNetworkImpl.getAddress(), + networkInitData + ); + + const legacyModules = { + SSVOperators: legacyOperatorsArtifact, + SSVClusters: legacyClustersArtifact, + SSVDAO: legacyDAOLegacyArtifact, + SSVViews: legacyViewsModuleArtifact, + SSVOperatorsWhitelist: legacyOperatorsWhitelistArtifact, + }; + + const moduleAddresses: Record = {}; + + for (const [moduleName, artifact] of Object.entries(legacyModules)) { + const factory = + await connection.ethers.getContractFactoryFromArtifact(artifact); + + const impl = await factory.deploy(); + await impl.waitForDeployment(); + + moduleAddresses[moduleName] = await impl.getAddress(); + } + + const network = oldNetworkFactory.attach(networkProxyAddr); + + for (const [moduleName, moduleAddress] of Object.entries(moduleAddresses)) { + await attachModule( + connection.ethers, + networkProxyAddr, + moduleName, + moduleAddress + ); + } + + const oldViewsFactory = + await connection.ethers.getContractFactoryFromArtifact( + legacySSVNetworkViewsArtifact + ); + + const legacyViewsImpl = await oldViewsFactory.deploy(); + await legacyViewsImpl.waitForDeployment(); + + const viewsInitData = oldViewsFactory.interface.encodeFunctionData( + "initialize", + [networkProxyAddr] + ); + + const { address: viewsProxyAddr } = await deployProxy( + connection.ethers, + deployer, + await legacyViewsImpl.getAddress(), + viewsInitData + ); + + const views = oldViewsFactory.attach(viewsProxyAddr); + + await (await network.updateNetworkFee(NETWORK_FEE)).wait(); + await (await network.updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL)).wait(); + await (await network.updateLiquidationThresholdPeriod(MINIMUM_BLOCKS_BEFORE_LIQUIDATION)).wait(); + await (await network.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE)).wait(); + await (await network.updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE)).wait(); + + return { + network, + views, + ssvToken, + }; +} + +export async function upgradeToStakingVersion( + connection: any, + network: any, + views: any, +): Promise<{ + cssv: any; + newNetwork: SSVNetwork; + newViews: SSVNetworkViews; +}> { + const deployer = await getDeployer(connection.ethers); + const networkAddress = await network.getAddress(); + + const { contract: cssv, address: cssvTokenAddress } = + await deployContract(connection.ethers, "CSSVToken", [networkAddress]); + + const latestBlock = await connection.ethers.provider.getBlock("latest"); + const upgradeBlockNum = latestBlock.number; + + const { address: upgradeImplAddr } = + await deployContract(connection.ethers, "SSVNetworkSSVStakingUpgrade"); + + await upgradeProxy( + connection.ethers, + deployer, + networkAddress, + upgradeImplAddr, + "SSVNetworkSSVStakingUpgrade", + "initializeSSVStaking(uint64,uint32[4],uint16)", + [DEFAULT_UNSTAKE_COOLDOWN, DEFAULT_ORACLE_IDS, QUORUM_BPS] + ); + + const networkFactory = + await connection.ethers.getContractFactory("SSVNetwork"); + const upgradedNetwork = networkFactory.attach(networkAddress); + + const moduleNames = [ + "SSVClusters", + "SSVOperatorsWhitelist", + "SSVValidators", + ]; + const moduleAddresses: Record = {}; + + const { address: ssvOperatorsAddr } = + await deployContract(connection.ethers, "SSVOperators", [upgradeBlockNum]); + moduleAddresses["SSVOperators"] = ssvOperatorsAddr; + + const { address: ssvDaoAddr } = + await deployContract(connection.ethers, "SSVDAO", [cssvTokenAddress]); + moduleAddresses["SSVDAO"] = ssvDaoAddr; + + const { address: ssvViewsAddr } = + await deployContract(connection.ethers, "SSVViews", [cssvTokenAddress]); + moduleAddresses["SSVViews"] = ssvViewsAddr; + + const { address: ssvStakingAddr } = + await deployContract(connection.ethers, "SSVStaking", [cssvTokenAddress]); + moduleAddresses["SSVStaking"] = ssvStakingAddr; + + for (const mod of moduleNames) { + const { address } = await deployContract(connection.ethers, mod); + moduleAddresses[mod] = address; + } + + for (const [name, addr] of Object.entries(moduleAddresses)) { + await attachModule(connection.ethers, networkAddress, name, addr); + } + + const { address: newViewsImpl } = + await deployContract(connection.ethers, "SSVNetworkViews"); + + await views.upgradeTo(newViewsImpl); + + const viewsFactory = + await connection.ethers.getContractFactory("SSVNetworkViews"); + const upgradedViews = viewsFactory.attach(await views.getAddress()); + + await (await upgradedNetwork.updateNetworkFeeSSV(NETWORK_FEE)).wait(); + await (await upgradedNetwork.updateNetworkFee(NETWORK_FEE_ETH)).wait(); + await (await upgradedNetwork.updateMinimumLiquidationCollateral( + MINIMUM_LIQUIDATION_PERIOD_COLLATERAL + )).wait(); + await (await upgradedNetwork.updateMinimumLiquidationCollateralSSV( + MINIMUM_LIQUIDATION_PERIOD_COLLATERAL + )).wait(); + await (await upgradedNetwork.updateLiquidationThresholdPeriod( + MINIMUM_BLOCKS_BEFORE_LIQUIDATION + )).wait(); + await (await upgradedNetwork.updateLiquidationThresholdPeriodSSV( + MINIMUM_BLOCKS_BEFORE_LIQUIDATION + )).wait(); + await (await upgradedNetwork.updateMaximumOperatorFee( + MAXIMUM_OPERATORS_FEE + )).wait(); + await (await upgradedNetwork.updateOperatorFeeIncreaseLimit( + OPERATOR_MAX_FEE_INCREASE + )).wait(); + await (await upgradedNetwork.updateMinimumOperatorEthFee( + MINIMAL_OPERATOR_ETH_FEE + )).wait(); + + return { + cssv, + newNetwork: upgradedNetwork, + newViews: upgradedViews, + }; +} diff --git a/test/setup/fork.ts b/test/setup/fork.ts new file mode 100644 index 000000000..aba774cde --- /dev/null +++ b/test/setup/fork.ts @@ -0,0 +1,18 @@ +import hre from "hardhat"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../common/types.ts"; + +export async function getForkedConnection(): Promise<{ + connection: NetworkConnection<"generic">; + ethers: NetworkConnection<"generic">["ethers"]; + networkHelpers: NetworkHelpersType; +}> { + const selectedForkNetwork = process.env.FORK_TEST_NETWORK ?? "hardhat_forked"; + const connection = await hre.network.connect(selectedForkNetwork as any); + + return { + connection, + ethers: connection.ethers, + networkHelpers: connection.networkHelpers, + }; +} diff --git a/test/test-forked/v2.0.0/config.ts b/test/test-forked/v2.0.0/config.ts new file mode 100644 index 000000000..82b50767e --- /dev/null +++ b/test/test-forked/v2.0.0/config.ts @@ -0,0 +1,62 @@ +import fs from "node:fs"; +import path from "node:path"; + +type ForkConfigFile = { + owner?: string; + daoAddress?: string; + ssvNetworkProxy?: string; + ssvNetworkAddress?: string; + ssvNetworkViews?: string; + ssvToken?: string; + cssvToken?: string; + modules?: Record; +}; + +const DEFAULT_FORK_CONFIG = { + SSV_NETWORK_ADDRESS: "0xDD9BC35aE942eF0cFa76930954a156B3fF30a4E1", + SSV_NETWORK_VIEWS: "0xafE830B6Ee262ba11cce5F32fDCd760FFE6a66e4", + SSV_TOKEN: "0x9D65fF81a3c488d585bBfb0Bfe3c7707c7917f54", + DAO_ADDRESS: "0xb35096b074fdb9bBac63E3AdaE0Bbde512B2E6b6", +} as const; + +function loadForkConfigFile(): ForkConfigFile { + const configPathFromEnv = process.env.FORK_CONFIG_PATH; + if (!configPathFromEnv) { + return {}; + } + + const resolvedPath = path.resolve(configPathFromEnv); + if (!fs.existsSync(resolvedPath)) { + throw new Error(`FORK_CONFIG_PATH does not exist: ${resolvedPath}`); + } + + const raw = fs.readFileSync(resolvedPath, "utf8"); + return JSON.parse(raw) as ForkConfigFile; +} + +const fileConfig = loadForkConfigFile(); + +export const ForkConfig = { + SSV_NETWORK_ADDRESS: + process.env.FORK_SSV_NETWORK_ADDRESS ?? + fileConfig.ssvNetworkProxy ?? + fileConfig.ssvNetworkAddress ?? + DEFAULT_FORK_CONFIG.SSV_NETWORK_ADDRESS, + SSV_NETWORK_VIEWS: + process.env.FORK_SSV_NETWORK_VIEWS ?? + fileConfig.ssvNetworkViews ?? + DEFAULT_FORK_CONFIG.SSV_NETWORK_VIEWS, + SSV_TOKEN: + process.env.FORK_SSV_TOKEN ?? + fileConfig.ssvToken ?? + DEFAULT_FORK_CONFIG.SSV_TOKEN, + CSSV_TOKEN: + process.env.FORK_CSSV_TOKEN ?? + fileConfig.cssvToken, + DAO_ADDRESS: + process.env.FORK_DAO_ADDRESS ?? + fileConfig.daoAddress ?? + fileConfig.owner ?? + DEFAULT_FORK_CONFIG.DAO_ADDRESS, + MODULES: fileConfig.modules ?? {}, +} as const; diff --git a/test/test-forked/v2.0.0/fullIntegrationForked.test.ts b/test/test-forked/v2.0.0/fullIntegrationForked.test.ts new file mode 100644 index 000000000..106f1cc82 --- /dev/null +++ b/test/test-forked/v2.0.0/fullIntegrationForked.test.ts @@ -0,0 +1,2544 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvNetworkFullForkedFixture } from '../../setup/fixtures.ts'; +import type { NetworkHelpersType, OperatorTuple, UnstakeRequest } from '../../common/types.ts'; +import { + calculateInitialBurnRate, + getCurrentClusterState, makeArrayOfKeysAndShares, + getFeeAboveIncreaseLimit, + getValidOperatorFeeIncrease, + makeOperatorKey, + makePublicKey, registerDefaultCluster, + registerOperators, + whitelistAddresses, +} from '../../common/helpers.ts'; +import { + CLUSTER_VERSION_ETH, + DECLARE_OPERATOR_FEE_PERIOD, + DEFAULT_ETH_EB_PER_VALIDATOR, + DEFAULT_ETH_REGISTER_VALUE, DEFAULT_ORACLES_IDS, + DEFAULT_SHARES, DEFAULT_UNSTAKE_COOLDOWN, + EMPTY_CLUSTER, + EXECUTE_OPERATOR_FEE_PERIOD, + MAXIMUM_OPERATORS_FEE, + MINIMAL_LIQUIDATION_THRESHOLD, + MINIMAL_OPERATOR_ETH_FEE, + MINIMUM_BLOCKS_BEFORE_LIQUIDATION, + MINIMUM_LIQUIDATION_PERIOD_COLLATERAL, NETWORK_FEE, + OPERATOR_MAX_FEE_INCREASE, OPERATOR_FEE_PRECISION, + STAKE_AMOUNT, VALIDATORS_PER_OPERATOR_LIMIT, +} from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../../common/errors.ts'; +import { deployContract } from '../../../scripts/common/helpers.ts'; +import { ContractTransactionResponse } from 'ethers'; +import { trackGasFromReceipt, GasGroup } from '../../helpers/gas-usage.ts'; +import { getForkedConnection } from '../../setup/fork.ts'; +import { ForkConfig } from './config.ts'; + +const RUN_FORK = process.env.RUN_FORK === 'true'; +const suite = RUN_FORK ? describe : describe.skip; + +suite("SSVNetwork full integration tests made on forked contract", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + let randomUser: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getForkedConnection()); + [operatorOwner, clusterOwner, randomUser] = await connection.ethers.getSigners(); + + for (const signer of [operatorOwner, clusterOwner, randomUser]) { + await connection.ethers.provider.send("hardhat_impersonateAccount", [signer.address]); + await connection.ethers.provider.send("hardhat_setBalance", [signer.address, "0x56bc75e2d63100000"]); + } + + operatorOwner = await connection.ethers.getSigner(operatorOwner.address); + clusterOwner = await connection.ethers.getSigner(clusterOwner.address); + randomUser = await connection.ethers.getSigner(randomUser.address); + }); + + const deployFullSSVNetworkForkFixture = async () => { + return ssvNetworkFullForkedFixture(connection); + }; + + describe("Constructor, initializer and upgrades", async function () { + it("Configures SSVNetwork correctly", async function () { + const { network, views, cssvToken, ssvToken } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(await network.getAddress()).to.be.equal(ForkConfig.SSV_NETWORK_ADDRESS); + await expect(await views.getAddress()).to.be.equal(ForkConfig.SSV_NETWORK_VIEWS); + await expect(await ssvToken.getAddress()).to.be.equal(ForkConfig.SSV_TOKEN); + + const version = await network.getVersion(); + await expect(version).to.be.a("string").and.not.empty; + + await expect(await views.getMinimumLiquidationCollateralSSV()).to.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL); + await expect(await views.getValidatorsPerOperatorLimit()).to.equal(VALIDATORS_PER_OPERATOR_LIMIT); + await expect(await views.getOperatorFeePeriods()).to.deep.equal([DECLARE_OPERATOR_FEE_PERIOD, EXECUTE_OPERATOR_FEE_PERIOD]); + await expect(await views.getOperatorFeeIncreaseLimit()).to.equal(OPERATOR_MAX_FEE_INCREASE); + await expect(await views.getActiveOracleIds()).to.deep.equal(DEFAULT_ORACLES_IDS); + + await expect(await views.getNetworkFeeSSV()).to.equal(NETWORK_FEE); + + await expect(await views.cooldownDuration()).to.equal(DEFAULT_UNSTAKE_COOLDOWN); + + await expect(await views.getNetworkEarnings()).to.equal(0n); + await expect(await views.totalStaked()).to.equal(0n); + }); + }); + + describe("Function 'registerOperator()'", async function () { + it("Creates new operator and emits correct event", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + const tx = await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_OPERATOR]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_ADDED).withArgs(expectedId, operatorOwner.address, operatorKey, MINIMAL_OPERATOR_ETH_FEE) + .and.to.emit(network, Events.OPERATOR_PRIVACY_STATUS_UPDATED).withArgs([expectedId], true); + + await expect(await views.getOperatorFee(expectedId)).to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + await expect(await views.getOperatorFeeSSV(expectedId)).to.be.equal(0); + await expect(await views.getOperatorDeclaredFee(expectedId)).to.be.deep.equal([false, 0n, 0n, 0n]); + await expect(await views.getOperatorById(expectedId)).to.be.deep.equal([ + operatorOwner.address, + MINIMAL_OPERATOR_ETH_FEE, + 0, + connection.ethers.ZeroAddress, + true, + true + ]); + await expect(await views.getOperatorByIdSSV(expectedId)).to.be.deep.equal([ + operatorOwner.address, + 0, + 0, + connection.ethers.ZeroAddress, + true, + true + ]); + }); + + it("Is reverted with 'FeeTooLow' if the provided fee is less than minimal allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE - 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeTooHigh' if the provided fee is higher than maximum allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + + await expect(network.registerOperator(operatorKey, MAXIMUM_OPERATORS_FEE + 1n, true)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + + it("Is reverted with 'OperatorAlreadyExists' if the public key is already registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_ALREADY_EXISTS); + }); + }); + + describe("Function 'removeOperator()'", async function (){ + it("Deactivates the operator and emits correct event", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + const tx = await network.removeOperator(expectedId); + + await expect(tx) + .to.emit(network, Events.OPERATOR_REMOVED) + .withArgs(expectedId) + + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR]); + + const operator: OperatorTuple = await views.getOperatorById(expectedId) + + await expect(operator[5]).to.be.equal(false) + await expect(await views.getOperatorFee(expectedId)).to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator with passed id is not registered", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.removeOperator(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.connect(randomUser).removeOperator(expectedId)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setOperatorsWhitelists()'", async function () { + it("Whitelists addresses and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(await network.setOperatorsWhitelists([expectedId], [clusterOwner])) + .to.emit(network, Events.OPERATOR_MULTIPLE_WHITELIST_UPDATED) + .withArgs([expectedId], [clusterOwner]); + + await expect(await views.getWhitelistedOperators([expectedId], clusterOwner)).to.be.deep.equal([expectedId]); //true + }); + + it("Whitelists multiple operators for multiple addresses", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 10); + const whitelistAddresses = Array(10).fill(clusterOwner.address); + + const tx = await network.setOperatorsWhitelists(operatorIds, whitelistAddresses); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_MULTIPLE_OPERATOR_WHITELIST_10_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.setOperatorsWhitelists([], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH) + }); + + it("Is reverted with 'InvalidWhitelistAddressesLength' if the array of addresses is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.setOperatorsWhitelists([123], [])) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELIST_ADDRESSES_LENGTH) + }); + + it("Is reverted with 'ZeroAddressNotAllowed' if one of addresses is zero address", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.setOperatorsWhitelists([expectedId], [connection.ethers.ZeroAddress])) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS_NOT_ALLOWED) + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.setOperatorsWhitelists([123456789], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.connect(randomUser).setOperatorsWhitelists([expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicate", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.setOperatorsWhitelists([expectedId, expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorIds = await registerOperators(network,operatorOwner, 3); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.setOperatorsWhitelists(operatorIds, [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + }); + + describe("Function 'removeOperatorsWhitelists()'", async function(){ + it("Removes addresses from the whitelist and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.setOperatorsWhitelists([expectedId], [clusterOwner]) + + await expect(await network.removeOperatorsWhitelists([expectedId], [clusterOwner])) + .to.emit(network, Events.OPERATOR_MULTIPLE_WHITELIST_REMOVED) + .withArgs([expectedId], [clusterOwner]); + + await expect(await views.getWhitelistedOperators([expectedId], clusterOwner)).to.be.deep.equal([]); //false + }); + + it("Removes multiple operators for multiple addresses", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 10); + const whitelistAddresses = Array(10).fill(clusterOwner.address); + + await network.setOperatorsWhitelists(operatorIds, whitelistAddresses); + + const tx = await network.removeOperatorsWhitelists(operatorIds, whitelistAddresses); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_MULTIPLE_OPERATOR_WHITELIST_10_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.removeOperatorsWhitelists([], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH) + }); + + it("Is reverted with 'InvalidWhitelistAddressesLength' if the array of addresses is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.removeOperatorsWhitelists([123], [])) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELIST_ADDRESSES_LENGTH) + }); + + it("Is reverted with 'ZeroAddressNotAllowed' if one of addresses is zero address", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.removeOperatorsWhitelists([expectedId], [connection.ethers.ZeroAddress])) + .to.be.revertedWithCustomError(network, Errors.ZERO_ADDRESS_NOT_ALLOWED) + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.removeOperatorsWhitelists([123456789], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.connect(randomUser).removeOperatorsWhitelists([expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicate", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + + await expect(network.removeOperatorsWhitelists([expectedId, expectedId], [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorIds = await registerOperators(network,operatorOwner, 3); + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.removeOperatorsWhitelists(operatorIds, [clusterOwner])) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + }); + + describe("Function 'setOperatorsWhitelistingContract()'", async function () { + it("Registers whitelisting contract, emits correct event and allows to whitelist addresses via contract", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network,operatorOwner, 3); + const { contract: whiteListingContract, address: contractAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + const tx = await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT]); + + await expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, contractAddress); + + await expect(await views.isWhitelistingContract(contractAddress)).to.be.equal(true); + + await whiteListingContract.addWhitelistedAddress(clusterOwner); + + await expect(await views.isAddressWhitelistedInWhitelistingContract(clusterOwner, operatorIds[0], contractAddress)) + .to.be.equal(true); + }); + + it("Updates whitelisting contract for operators", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 3); + const { contract: firstContract, address: firstAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + const { contract: secondContract, address: secondAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + await network.setOperatorsWhitelistingContract(operatorIds, firstContract); + + const tx = await network.setOperatorsWhitelistingContract(operatorIds, secondContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.UPDATE_OPERATOR_WHITELISTING_CONTRACT]); + + await expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, secondAddress); + + await expect(firstAddress).to.not.equal(secondAddress); + }); + + it("Registers whitelisting contract for 10 operators", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 10); + const { contract: whiteListingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + const tx = await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_OPERATOR_WHITELISTING_CONTRACT_10]); + }); + + it("Is reverted with 'InvalidWhitelistingContract' if the contract does not support required interface", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "SSVOperatorsWhitelist"); + const operatorIds = await registerOperators(network,operatorOwner, 3); + + await expect(network.setOperatorsWhitelistingContract(operatorIds, contractAddress)) + .to.be.revertedWithCustomError(network, Errors.INVALID_WHITELISTING_CONTRACT) + .withArgs(contractAddress); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' is the array of operators is empty", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + + await expect(network.setOperatorsWhitelistingContract([], contractAddress)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + + await expect(network.setOperatorsWhitelistingContract([12345n], contractAddress)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is the the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const { address: contractAddress } = await deployContract(connection.ethers, "BasicWhitelisting"); + const operatorIds = await registerOperators(network,operatorOwner, 3); + + await expect(network.connect(randomUser).setOperatorsWhitelistingContract(operatorIds, contractAddress)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address) + }); + }); + + describe("Function 'removeOperatorsWhitelistingContract()'", async function(){ + it("Removes whitelisting address and emits correct event", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network,operatorOwner, 3); + const { contract: whiteListingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + + const tx = await network.removeOperatorsWhitelistingContract(operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT]); + + await expect(tx) + .to.emit(network, Events.OPERATORS_WHITELISTING_CONTRACT_UPDATED) + .withArgs(operatorIds, connection.ethers.ZeroAddress); + }); + + it("Removes whitelisting contract for 10 operators", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 10); + const { contract: whiteListingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + + await network.setOperatorsWhitelistingContract(operatorIds, whiteListingContract); + + const tx = await network.removeOperatorsWhitelistingContract(operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR_WHITELISTING_CONTRACT_10]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.removeOperatorsWhitelistingContract([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.removeOperatorsWhitelistingContract([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).removeOperatorsWhitelistingContract(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address) + }); + }); + + describe("Function 'setOperatorsPrivateUnchecked()'", async function() { + it("Changes privacy status and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(await network.setOperatorsPrivateUnchecked(operatorIds)) + .to.emit(network, Events.OPERATORS_PRIVACY_STATUS_UPDATED) + .withArgs(operatorIds, true); + + const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); + await expect(operator[4]).to.be.equal(true); //isPrivate + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.setOperatorsPrivateUnchecked([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.setOperatorsPrivateUnchecked([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.connect(randomUser).setOperatorsPrivateUnchecked(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setOperatorsPublicUnchecked()'", async function () { + it("Changes privacy status and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(await network.setOperatorsPublicUnchecked(operatorIds)) + .to.emit(network, Events.OPERATORS_PRIVACY_STATUS_UPDATED) + .withArgs(operatorIds, false); + + const operator: OperatorTuple = await views.getOperatorById(operatorIds[0]); + await expect(operator[4]).to.be.equal(false); //isPrivate + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the array of operators is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.setOperatorsPublicUnchecked([])) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'OperatorDoesNotExist' if one of operators is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.setOperatorsPublicUnchecked([12345n])) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.connect(randomUser).setOperatorsPublicUnchecked(operatorIds)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'declareOperatorFee()'", async function() { + it("Declares new fee and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const [declarePeriod, executePeriod] = await views.getOperatorFeePeriods(); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + + const tx: ContractTransactionResponse = await network.declareOperatorFee(operatorIds[0], newFee) + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DECLARE_OPERATOR_FEE]); + const block = await tx.getBlock(); + + const expectedBegin = BigInt(block!.timestamp) + declarePeriod; + const expectedEnd = expectedBegin + executePeriod; + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_DECLARED) + .withArgs(operatorOwner.address, operatorIds[0], tx.blockNumber, newFee); + + await expect(await views.getOperatorDeclaredFee(operatorIds[0])) + .to.be.deep.equal([ + true, // isActive + newFee, // declaredFee + expectedBegin, + expectedEnd + ]); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await expect(network.declareOperatorFee(12345n, newFee)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + + await expect(network.connect(randomUser).declareOperatorFee(operatorIds[0], newFee)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'FeeTooLow' is the passed fee is less than minimal", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE - 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'SameFeeChangeNotAllowed' is the passed value is the same as current one", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.declareOperatorFee(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.SAME_FEE_CHANGE_NOW_ALLOWED); + }); + + it("Is reverted with 'FeeTooHigh' if the new fee is higher than allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.declareOperatorFee(operatorIds[0], MAXIMUM_OPERATORS_FEE + 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + + it("Is reverted with 'FeeExceedsIncreaseLimit' if the new fee exceeds the allowed limit", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const exceedingFee = await getFeeAboveIncreaseLimit(views, operatorIds[0]); + + await expect(network.declareOperatorFee(operatorIds[0], exceedingFee)) + .to.be.revertedWithCustomError(network, Errors.FEE_EXCEEDS_INCREASE_LIMIT); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' if operators current fee is zero", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorKey = makeOperatorKey(1); + const expectedId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, 0, true); + + await expect(network.declareOperatorFee(expectedId, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + }); + + describe("Function 'cancelDeclaredOperatorFee()'", async function(){ + it("Cancels declared fee and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee) + + const tx = await network.cancelDeclaredOperatorFee(operatorIds[0]); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CANCEL_OPERATOR_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_DECLARATION_CANCELLED) + .withArgs(operatorOwner, operatorIds[0]); + + await expect(await views.getOperatorDeclaredFee(operatorIds[0])) + .to.be.deep.equal([ + false, // isActive + 0n, + 0n, + 0n + ]); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.cancelDeclaredOperatorFee(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee) + + await expect(network.connect(randomUser).cancelDeclaredOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'NoFeeDeclared' if no declarations were done before", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.cancelDeclaredOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + }); + + describe("Function 'executeOperatorFee()'", async function() { + it("Updates operator fee according to a declared one and emits the correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee) + const [isActive, declaredFee, begin, end] = await views.getOperatorDeclaredFee(operatorIds[0]); + + await connection.networkHelpers.time.increaseTo(begin + 1n); + await connection.networkHelpers.mine(); + + const tx = await network.executeOperatorFee(operatorIds[0]); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.EXECUTE_OPERATOR_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_EXECUTED); + + await expect(await views.getOperatorFee(operatorIds[0])).to.be.equal(newFee); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.executeOperatorFee(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee) + + await expect(network.connect(randomUser).executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'NoFeeDeclared' if no declarations were done before", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.NO_FEE_DECLARED); + }); + + it("Is reverted with 'ApprovalNotWithinTimeframe' if execution period is not started or ended", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.declareOperatorFee(operatorIds[0], newFee) + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + + const [declarePeriod, executePeriod] = await views.getOperatorFeePeriods(); + const [isActive, declaredFee, begin, end] = await views.getOperatorDeclaredFee(operatorIds[0]); + + await connection.networkHelpers.time.increaseTo(end + 1n); + await connection.networkHelpers.mine(); + + await expect(network.executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.APPROVAL_NOT_WITHIN_TIMEFRAME); + }); + + it("Is reverted with 'FeeTooHigh' if the maximum fee changed during the execution period", async function(){ + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 1); + const newFee = await getValidOperatorFeeIncrease(views, operatorIds[0]); + await network.connect(operatorOwner).declareOperatorFee(operatorIds[0], newFee); + + const [isActive, declaredFee, begin, end] = await views.getOperatorDeclaredFee(operatorIds[0]); + + await network.connect(daoSigner).updateMaximumOperatorFee(newFee - OPERATOR_FEE_PRECISION); + + await connection.networkHelpers.time.increaseTo(begin + 1n); + await connection.networkHelpers.mine(); + + await expect(network.connect(operatorOwner).executeOperatorFee(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_HIGH); + }); + }); + + describe("Function 'updateMaximumOperatorFee()'", async function(){ + it("Updates maximum fee and emits correct event", async function() { + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const tx = await network.connect(daoSigner) + .updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE * 2n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_MAXIMUM_FEE_UPDATED); + + await expect(await views.getMaximumOperatorFee()) + .to.be.equal(MAXIMUM_OPERATORS_FEE * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if the caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(randomUser).updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("Reverts when new maximum fee is below the configured minimum fee", async function() { + const { network, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(daoSigner).updateMaximumOperatorFee(MINIMAL_OPERATOR_ETH_FEE - OPERATOR_FEE_PRECISION)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_FEE_RANGE); + }); + }); + + describe("Function 'reduceOperatorFee()'", async function(){ + it("Decreases fee and emits the correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + const tx = await network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REDUCE_OPERATOR_FEE]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_EXECUTED); + + await expect(await views.getOperatorFee(operatorId)) + .to.be.equal(MINIMAL_OPERATOR_ETH_FEE); + }); + + it("Is reverted with 'OperatorDoesNotExist' if the operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.reduceOperatorFee(12345n, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if the caller is not the operator owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.connect(randomUser).reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'FeeTooLow' if the passed fee is less than minimum allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE - 1n)) + .to.be.revertedWithCustomError(network, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' if caller is trying to increase the fee", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorKey = makeOperatorKey(1); + const operatorId = await network.registerOperator.staticCall(operatorKey, MINIMAL_OPERATOR_ETH_FEE, true); + await network.registerOperator(operatorKey, MINIMAL_OPERATOR_ETH_FEE * 2n, true); + + await expect(network.reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE * 3n)) + .to.be.revertedWithCustomError(network, Errors.FEE_INCREASE_NOT_ALLOWED); + }); + }); + + describe("Function 'withdrawOperatorEarnings()'", async function(){ + it("Withdraws operators earnings, update balances and emits correct event", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + const registrationDeposit = requiredDeposit + DEFAULT_ETH_REGISTER_VALUE; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (registrationDeposit + 10n ** 18n).toString(16), + ]); + + const earningsPeriod = 100n; + await network.connect(clusterOwner).registerValidator( + validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: registrationDeposit } + ); + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + await expect(expectedEarnings).to.be.equal(earnings); + const withdrawAmount = earnings + MINIMAL_OPERATOR_ETH_FEE; + const tx = await network.connect(operatorOwner).withdrawOperatorEarnings(operatorIds[0], withdrawAmount); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.WITHDRAW_OPERATOR_BALANCE]); + await expect(tx) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], withdrawAmount); + await expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0n); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.withdrawOperatorEarnings(12345n, 9999n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).withdrawOperatorEarnings(operatorIds[0], 9999n)) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + + it("Is reverted with 'InsufficientBalance' if the amount is less than operator earnings", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + // no validators no earnings rn + await expect(network.withdrawOperatorEarnings(operatorIds[0], MINIMAL_OPERATOR_ETH_FEE)) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function 'withdrawAllOperatorEarnings()'", async function(){ + it("Withdraws all operators earnings, update balances and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + const registrationDeposit = requiredDeposit + DEFAULT_ETH_REGISTER_VALUE; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (registrationDeposit + 10n ** 18n).toString(16), + ]); + + const earningsPeriod = 100n; + await network.connect(clusterOwner).registerValidator( + validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: registrationDeposit } + ); + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + await expect(expectedEarnings).to.be.equal(earnings); + await expect(network.connect(operatorOwner).withdrawAllOperatorEarnings(operatorIds[0])) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], earnings + MINIMAL_OPERATOR_ETH_FEE); // 1 block passed + await expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.withdrawAllOperatorEarnings(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).withdrawAllOperatorEarnings(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'withdrawAllVersionOperatorEarnings()'", async function() { + it("Withdraws all operators earnings and emits correct events", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + const registrationDeposit = requiredDeposit + DEFAULT_ETH_REGISTER_VALUE; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (registrationDeposit + 10n ** 18n).toString(16), + ]); + + const earningsPeriod = 100n; + await network.connect(clusterOwner).registerValidator( + validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: registrationDeposit } + ); + await connection.networkHelpers.mine(earningsPeriod); + const expectedEarnings = earningsPeriod * MINIMAL_OPERATOR_ETH_FEE; + const earnings: bigint = await views.getOperatorEarnings(operatorIds[0]); + await expect(expectedEarnings).to.be.equal(earnings); + await expect(network.connect(operatorOwner).withdrawAllVersionOperatorEarnings(operatorIds[0])) + .to.emit(network, Events.OPERATOR_WITHDRAWN) + .withArgs(operatorOwner.address, operatorIds[0], earnings + MINIMAL_OPERATOR_ETH_FEE); // 1 block passed + await expect(await views.getOperatorEarnings(operatorIds[0])) + .to.be.equal(0); + }); + + it("Is reverted with 'OperatorDoesNotExist' if operator is not registered", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.withdrawAllVersionOperatorEarnings(12345n)) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller is not the operator owner", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(randomUser).withdrawAllVersionOperatorEarnings(operatorIds[0])) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_OWNER) + .withArgs(randomUser.address, operatorOwner.address); + }); + }); + + describe("Function 'setFeeRecipientAddress()'", async function(){ + it("Emits the correct event with the correct input data", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(randomUser).setFeeRecipientAddress(clusterOwner.address)) + .to.emit(network, Events.FEE_RECIPIENT_ADDRESS_UPDATED) + .withArgs(randomUser.address, clusterOwner.address); + }); + }); + + describe("Function 'updateOperatorFeeIncreaseLimit()'", async function(){ + it("Changes fee increase limit and emits the correct event", async function(){ + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const tx = await network.connect(daoSigner) + .updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]); + + await expect(tx) + .to.emit(network, Events.OPERATOR_FEE_INCREASE_LIMIT_UPDATED) + .withArgs(OPERATOR_MAX_FEE_INCREASE); + + await expect(await views.getOperatorFeeIncreaseLimit()).to.be.equal(OPERATOR_MAX_FEE_INCREASE); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(randomUser).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + + it("Reverts when fee increase limit exceeds 100%", async function() { + const { network, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(daoSigner).updateOperatorFeeIncreaseLimit(OPERATOR_MAX_FEE_INCREASE + 1n)) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_FEE_INCREASE_LIMIT); + }); + }); + + describe("Function 'updateDeclareOperatorFeePeriod()'", async function() { + it("Changes the fee declare period and emits correct event", async function(){ + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const tx = await network.connect(daoSigner) + .updateDeclareOperatorFeePeriod(DECLARE_OPERATOR_FEE_PERIOD + 1n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]); + + await expect(tx) + .to.emit(network, Events.DECLARE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await expect(await views.getOperatorFeePeriods()) + .to.be.deep.equal([DECLARE_OPERATOR_FEE_PERIOD + 1n, EXECUTE_OPERATOR_FEE_PERIOD]); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(randomUser).updateDeclareOperatorFeePeriod(DECLARE_OPERATOR_FEE_PERIOD + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateExecuteOperatorFeePeriod()'", async function(){ + it("Changes the fee execute period and emits correct event", async function() { + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const [initialDeclarePeriod, initialExecutePeriod] = await views.getOperatorFeePeriods(); + const newExecutePeriod = initialExecutePeriod + 1n; + + const tx = await network.connect(daoSigner) + .updateExecuteOperatorFeePeriod(newExecutePeriod); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]); + + await expect(tx) + .to.emit(network, Events.EXECUTE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(newExecutePeriod); + await expect(await views.getOperatorFeePeriods()) + .to.be.deep.equal([initialDeclarePeriod, newExecutePeriod]); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(randomUser).updateExecuteOperatorFeePeriod(EXECUTE_OPERATOR_FEE_PERIOD + 1n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateLiquidationThresholdPeriod()'", async function(){ + it("Changes the period and emits correct event", async function(){ + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + + const tx = await network.connect(daoSigner) + .updateLiquidationThresholdPeriod(newThreshold); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]); + + await expect(tx) + .to.emit(network, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED) + .withArgs(newThreshold); + + await expect(await views.getLiquidationThresholdPeriod()) + .to.be.equal(newThreshold); + }); + + it("Is reverted 'NewBlockPeriodIsBelowMinimum' if the passed threshold is less than minimum allowed", async function(){ + const { network, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(daoSigner).updateLiquidationThresholdPeriod(MINIMAL_LIQUIDATION_THRESHOLD - 1n)) + .to.be.revertedWithCustomError(network, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + + await expect(network.connect(randomUser).updateLiquidationThresholdPeriod(newThreshold)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateLiquidationThresholdPeriodSSV()'", async function(){ + it("Changes the period and emits correct event", async function(){ + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + + await expect(network.connect(daoSigner).updateLiquidationThresholdPeriodSSV(newThreshold)) + .to.emit(network, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED_SSV) + .withArgs(newThreshold); + + await expect(await views.getLiquidationThresholdPeriodSSV()) + .to.be.equal(newThreshold); + }); + + it("Is reverted 'NewBlockPeriodIsBelowMinimum' if the passed threshold is less than minimum allowed", async function(){ + const { network, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(daoSigner).updateLiquidationThresholdPeriodSSV(MINIMAL_LIQUIDATION_THRESHOLD - 1n)) + .to.be.revertedWithCustomError(network, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const newThreshold = MINIMAL_LIQUIDATION_THRESHOLD + 1n; + + await expect(network.connect(randomUser).updateLiquidationThresholdPeriodSSV(newThreshold)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateMinimumLiquidationCollateral()'", async function(){ + it("Changes collateral and emits correct event", async function(){ + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const tx = await network.connect(daoSigner) + .updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_MINIMUM_COLLATERAL]); + + await expect(tx) + .to.emit(network, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED) + .withArgs(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + + await expect(await views.getMinimumLiquidationCollateral()) + .to.be.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(randomUser).updateMinimumLiquidationCollateral(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'updateMinimumLiquidationCollateralSSV()'", async function(){ + it("Changes collateral and emits correct event", async function(){ + const { network, views, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(daoSigner).updateMinimumLiquidationCollateralSSV(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.emit(network, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED_SSV) + .withArgs(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + + await expect(await views.getMinimumLiquidationCollateralSSV()) + .to.be.equal(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n); + }); + + it("Is reverted with 'Ownable: caller is not the owner' if caller is not the owner", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.connect(randomUser).updateMinimumLiquidationCollateralSSV(MINIMUM_LIQUIDATION_PERIOD_COLLATERAL * 2n)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + }); + + describe("Function 'registerValidator()'", async function () { + it("For a new cluster, creates it with a passed validator and emits correct event", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredDeposit + 10n ** 18n).toString(16), + ]); + + const tx = await network.connect(clusterOwner).registerValidator( + validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE]); + await expect(tx).to.emit(network, Events.VALIDATOR_ADDED); + + const expectedCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + await expect(await views.getValidator(clusterOwner, validatorKey)).to.equal(true); + await expect(await views.isLiquidatable(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.isLiquidated(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.getBurnRate(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(await calculateInitialBurnRate(views, operatorIds, expectedCluster)); + await expect(await views.getBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(requiredDeposit); + await expect(await views.getEffectiveBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_EB_PER_VALIDATOR); + await expect(await views.getClusterAssetType(clusterOwner, operatorIds)) + .to.be.equal(CLUSTER_VERSION_ETH); + + // ssv legacy getters + await expect(views.isLiquidatableSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBalanceSSV(clusterOwner, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Registers a validator for a new ETH cluster using whitelisting contract", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + const { contract: whitelistingContract, address: whitelistingContractAddress } = + await deployContract(connection.ethers, "BasicWhitelisting"); + await whitelistingContract.addWhitelistedAddress(clusterOwner.address); + await network.setOperatorsWhitelistingContract(operatorIds, whitelistingContractAddress); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredDeposit + 10n ** 18n).toString(16), + ]); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTING_CONTRACT_4]); + }); + + it("Registers a validator for a new ETH cluster with one whitelisted operator", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsPublicUnchecked([operatorIds[1], operatorIds[2], operatorIds[3]]); + await network.setOperatorsWhitelists([operatorIds[0]], [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredDeposit + 10n ** 18n).toString(16), + ]); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]); + }); + + it("Registers a validator for a new ETH cluster with four whitelisted operators", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + + // Calc min deposit to avoid liquidation + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredDeposit + 10n ** 18n).toString(16), + ]); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: requiredDeposit } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4]); + }); + + it("Registers a validator into an existing ETH cluster with four whitelisted operators", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForOne = perValidatorBurn * 1n * minBlocks; + const requiredForOne = thresholdForOne > minCollateral ? thresholdForOne : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredForOne + 10n ** 18n).toString(16), + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredForOne } + ); + const existingCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, existingCluster); + const newValidatorCount = BigInt(existingCluster.validatorCount) + 1n; + const newThreshold = perValidatorBurn * newValidatorCount * minBlocks; + const newRequired = newThreshold > minCollateral ? newThreshold : minCollateral; + let additionalDeposit = newRequired > currentBalance ? newRequired - currentBalance : 0n; + additionalDeposit += perValidatorBurn * 2n; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (additionalDeposit + 10n ** 18n).toString(16), + ]); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, existingCluster, { value: additionalDeposit } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4]); + }); + + it("Registers a validator into an existing ETH cluster", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForOne = perValidatorBurn * 1n * minBlocks; + const requiredForOne = thresholdForOne > minCollateral ? thresholdForOne : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredForOne + 10n ** 18n).toString(16), + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredForOne } + ); + const existingCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, existingCluster); + const newValidatorCount = BigInt(existingCluster.validatorCount) + 1n; + const newThreshold = perValidatorBurn * newValidatorCount * minBlocks; + const newRequired = newThreshold > minCollateral ? newThreshold : minCollateral; + let additionalDeposit = newRequired > currentBalance ? newRequired - currentBalance : 0n; + additionalDeposit += perValidatorBurn * 2n; // Buffer + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (additionalDeposit + 10n ** 18n).toString(16), + ]); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, existingCluster, { value: additionalDeposit } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_ETH_CLUSTER]); + }); + + it("Registers a validator into a prefunded ETH cluster with zero additional deposit", async function () { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForTwo = perValidatorBurn * 2n * minBlocks; + const requiredForTwo = thresholdForTwo > minCollateral ? thresholdForTwo : minCollateral; + const initialDeposit = requiredForTwo + perValidatorBurn * 2n; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (initialDeposit + 10n ** 18n).toString(16), + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: initialDeposit } + ); + const existingCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + const tx = await network.connect(clusterOwner).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, existingCluster, { value: 0 } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the amount of operators is not the allowed one", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 5); // 5 operators for invalid cluster size + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidPublicKeyLength' if the public key is not 48 bytes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const invalidLengthPublicKey = makePublicKey(1) + "11"; + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).registerValidator( + invalidLengthPublicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'ValidatorAlreadyRegistered' if the public key is already registered", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredDeposit * 2n + 10n ** 18n).toString(16), + ]); + + await network.connect(clusterOwner).registerValidator( + validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit } + ); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit } + )) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_ALREADY_REGISTERED) + .withArgs(validatorKey, clusterOwner.address); + }); + + it("Is reverted with 'IncorrectClusterState' for the new cluster is the cluster data is not consisting from zeroes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const invalidCluster = { ...EMPTY_CLUSTER, validatorCount: 123n }; + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + invalidCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicates", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + operatorIds.pop(); + operatorIds.unshift(operatorIds[0]); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'CallerNotWhitelistedWithData' if one of operators did not whitelist the caller", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_WHITELISTED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'ExceedValidatorLimitWithData' if one of operators will exceed the network limit", async function () { + const { network, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const { address: upgradeImplAddr } = await deployContract(connection.ethers, "SSVNetworkValidatorsPerOperatorUpgrade"); + const factory = await connection.ethers.getContractFactory("SSVNetworkValidatorsPerOperatorUpgrade"); + const initData = factory.interface.encodeFunctionData("initializev2", [0]); + await network.connect(daoSigner).upgradeToAndCall(upgradeImplAddr, initData); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_VALIDATORS_LIMIT_EXCEEDED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'InsufficientBalance' if msg value is not enough to cover the validator", async function () { + const { network, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const validatorKey = makePublicKey(1); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(daoSigner).updateMinimumLiquidationCollateral(100_000n); + + await expect(network.connect(clusterOwner).registerValidator( + validatorKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: 0 } + )) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + describe("Function bulkRegisterValidator()", async function() { + it("Registers bulk of validators, creates a new cluster with the expected data and emits correct events", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const numValidators = BigInt(keys.length); + const threshold = burnRate * numValidators * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredDeposit + 10n ** 18n).toString(16), + ]); + + const tx = await network.connect(clusterOwner).bulkRegisterValidator( + keys, operatorIds, shares, EMPTY_CLUSTER, { value: requiredDeposit } + ); + await tx.wait(); + + const expectedCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + for (let i = 0; i < keys.length; i++) { + await expect(await views.getValidator(clusterOwner, keys[i])).to.equal(true); + } + + await expect(await views.isLiquidatable(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.isLiquidated(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(false); + await expect(await views.getBurnRate(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.equal(await calculateInitialBurnRate(views, operatorIds, expectedCluster)); + await expect(await views.getBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(requiredDeposit); + await expect(await views.getEffectiveBalance(clusterOwner, operatorIds, expectedCluster)) + .to.be.equal(DEFAULT_ETH_EB_PER_VALIDATOR * numValidators); + await expect(await views.getClusterAssetType(clusterOwner, operatorIds)) + .to.be.equal(CLUSTER_VERSION_ETH); + + await expect(views.isLiquidatableSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner.address, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBalanceSSV(clusterOwner, operatorIds, expectedCluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Registers bulk of validators into an existing cluster with one whitelisting contract operator", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await network.setOperatorsPublicUnchecked([operatorIds[1], operatorIds[2], operatorIds[3]]); + const { contract: whitelistingContract } = + await deployContract(connection.ethers, "BasicWhitelisting"); + await whitelistingContract.addWhitelistedAddress(clusterOwner.address); + await network.setOperatorsWhitelistingContract([operatorIds[0]], whitelistingContract); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const perValidatorBurn = sumOpFees + networkFee; + const thresholdForOne = perValidatorBurn * 1n * minBlocks; + const requiredForOne = thresholdForOne > minCollateral ? thresholdForOne : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredForOne + 10n ** 18n).toString(16), + ]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredForOne } + ); + const existingCluster = await getCurrentClusterState( + connection, network, clusterOwner.address, operatorIds + ); + + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, existingCluster); + const newValidatorCount = BigInt(existingCluster.validatorCount) + 10n; + const newThreshold = perValidatorBurn * newValidatorCount * minBlocks; + const newRequired = newThreshold > minCollateral ? newThreshold : minCollateral; + let additionalDeposit = newRequired > currentBalance ? newRequired - currentBalance : 0n; + additionalDeposit += perValidatorBurn * 2n; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (additionalDeposit + 10n ** 18n).toString(16), + ]); + + const keys = Array.from({ length: 10 }, (_, i) => makePublicKey(i + 2)); + const shares = Array(10).fill(DEFAULT_SHARES); + const tx = await network.connect(clusterOwner).bulkRegisterValidator( + keys, operatorIds, shares, existingCluster, { value: additionalDeposit } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4]); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the amount of operators is not the allowed one", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 5); // 5 operators for invalid cluster size + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'InvalidPublicKeyLength' if one of public keys is not 48 bytes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + + const invalidLengthPublicKey = makePublicKey(1) + "11"; + keys.shift(); + keys.unshift(invalidLengthPublicKey); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'ValidatorAlreadyRegistered' if one of public keys is already registered", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const networkFee = await views.getNetworkFee(); + const minBlocks = await views.getLiquidationThresholdPeriod(); + const minCollateral = await views.getMinimumLiquidationCollateral(); + let sumOpFees = 0n; + for (const id of operatorIds) { + sumOpFees += await views.getOperatorFee(id); + } + const burnRate = sumOpFees + networkFee; + const threshold = burnRate * minBlocks; + const requiredDeposit = threshold > minCollateral ? threshold : minCollateral; + + await connection.ethers.provider.send("hardhat_setBalance", [ + clusterOwner.address, + "0x" + (requiredDeposit + 10n ** 18n).toString(16), + ]); + + await network.connect(clusterOwner).registerValidator( + keys[7], operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: requiredDeposit } + ); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, operatorIds, shares, EMPTY_CLUSTER, { value: requiredDeposit } + )) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_ALREADY_REGISTERED) + .withArgs(keys[7], clusterOwner.address); + }); + + it("Is reverted with 'IncorrectClusterState' for the new cluster is the cluster data is not consisting from zeroes", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const invalidCluster = { ...EMPTY_CLUSTER, validatorCount: 123n }; + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + invalidCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'UnsortedOperatorsList' if operators are not sorted in increasing order", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const lastOp = operatorIds.pop(); + operatorIds.unshift(lastOp!); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the array of operators has any duplicates", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + operatorIds.pop(); + operatorIds.unshift(operatorIds[0]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'CallerNotWhitelistedWithData' if one of operators did not whitelist the caller", async function () { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.CALLER_NOT_WHITELISTED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'ExceedValidatorLimitWithData' if one of operators will exceed the network limit", async function () { + const { network, daoSigner} = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const { address: upgradeImplAddr } = await deployContract(connection.ethers, "SSVNetworkValidatorsPerOperatorUpgrade"); + const factory = await connection.ethers.getContractFactory("SSVNetworkValidatorsPerOperatorUpgrade"); + const initData = factory.interface.encodeFunctionData("initializev2", [0]); + await network.connect(daoSigner).upgradeToAndCall(upgradeImplAddr, initData); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.OPERATOR_VALIDATORS_LIMIT_EXCEEDED) + .withArgs(operatorIds[0]); + }); + + it("Is reverted with 'InsufficientBalance' if msg value is not enough to cover new validators", async function () { + const { network, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + await network.connect(daoSigner).updateMinimumLiquidationCollateral(100_000n); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + shares, + EMPTY_CLUSTER, + { value: 0 } + )) + .to.be.revertedWithCustomError(network, Errors.INSUFFICIENT_BALANCE); + }); + }); + + it("Is reverted with 'EmptyPublicKeysList' if the array of public keys is empty", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + [], + operatorIds, + shares, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.EMPTY_PUBLIC_KEYS_LIST); + }); + + it("Is reverted with 'PublicKeysSharesLengthMismatch' if the array of keys and array of shares have different length", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {keys, shares} = makeArrayOfKeysAndShares(1, 10); + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + const sharesWithMismatch = shares.slice(0, shares.length - 1); + + await expect(network.connect(clusterOwner).bulkRegisterValidator( + keys, + operatorIds, + sharesWithMismatch, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )) + .to.be.revertedWithCustomError(network, Errors.PUBLIC_KEYS_SHARES_LENGTH_MISMATCH); + }); + + describe("Function 'removeValidator()'", async function() { + it("Removes validator and emits correct event", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const tx = await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_VALIDATOR]); + + await expect(tx) + .to.emit(network, Events.VALIDATOR_REMOVED); + + const clusterAfter = await getCurrentClusterState( + connection, + network, + clusterOwner.address, + operatorIds + ); + + await expect(clusterAfter.validatorCount).to.equal(0n); + await expect(clusterAfter.active).to.equal(true); + await expect(await views.getValidator(clusterOwner.address, validatorKey)).to.be.equal(false); + }); + + it("Is reverted with 'ClusterDoesNotExists' if the cluster with this owner and operators does not exist", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(randomUser).removeValidator(validatorKey, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the cluster data is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ValidatorDoesNotExist' if the validator was never registered", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const incorrectValidator: string = validatorKey + "11"; + + await expect(network.connect(clusterOwner).removeValidator(incorrectValidator, operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reveted with 'ValidatorDoesNotExist' if validator is already removed", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const updatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect(network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, updatedCluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function 'bulkRemoveValidator()'", async function(){ + it("Is reverted with 'ClusterDoesNotExists' if the cluster with this owner and operators does not exist", async function(){ + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + await expect(network.connect(randomUser).bulkRemoveValidator([validatorKey], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' if the cluster data is incorrect", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + cluster.validatorCount += 1n; + + await expect(network.connect(clusterOwner).bulkRemoveValidator([validatorKey], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'IncorrectValidatorStateWithData' if the validator was never registered", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + + const incorrectValidator: string = validatorKey + "11"; + + await expect(network.connect(clusterOwner).bulkRemoveValidator([incorrectValidator], operatorIds, cluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reveted with 'ValidatorDoesNotExist' if validator is already removed", async function() { + const { network, views } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + const {cluster, validatorKey, operatorIds} = + await registerDefaultCluster(connection, network, views, operatorOwner, clusterOwner); + await network.connect(clusterOwner).removeValidator(validatorKey, operatorIds, cluster); + const updatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + await expect(network.connect(clusterOwner).bulkRemoveValidator([validatorKey], operatorIds, updatedCluster)) + .to.be.revertedWithCustomError(network, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + }); + + describe("Function stake()", async function() { + it("Stakes SSV, mints CSSV to the staker and creates delegation weight", async function() { + const { network, views, ssvToken, cssvToken, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + + const tx = await network.connect(randomUser).stake(STAKE_AMOUNT); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.STAKE_SSV]); + + await expect(tx) + .to.emit(network, Events.STAKED) + .withArgs(randomUser.address, STAKE_AMOUNT); + + await expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + await expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + }); + + it("Is reverted with 'StakeTooLow' if the amount to stake is smaller than minimum allowed", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.stake(1)) + .to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + + it("Is reverted with 'StakeTooLow' is caller is trying to stake 0 SSV", async function(){ + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.stake(0)) + .to.be.revertedWithCustomError(network, Errors.STAKE_TOO_LOW); + }); + }); + + describe("Function requestUnstake()", async function() { + it("For full amount, creates unstake request, burns CSSV and removes delegation", async function(){ + const { network, views, ssvToken, cssvToken, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT) + + const tx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REQUEST_UNSTAKE]); + const block = await tx.getBlock(); + + await expect(tx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT, BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN) + + const requests: UnstakeRequest[] = + await views.pendingUnstake(randomUser.address); + await expect(requests.length).to.be.equal(1); + await expect(requests[0].amount).to.be.equal(STAKE_AMOUNT); + await expect(requests[0].unlockTime).to.be.equal(BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + + await expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(0); + await expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(0); + }); + + it("For partial amount, creates unstake request, burns CSSV and removes delegation", async function(){ + const { network, views, ssvToken, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT) + + const tx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT / 2n); + await tx.wait(); + const block = await tx.getBlock(); + + await expect(tx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT / 2n, BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN) + + let requests: UnstakeRequest[] = + await views.pendingUnstake(randomUser.address); + await expect(requests.length).to.be.equal(1); + await expect(requests[0].amount).to.be.equal(STAKE_AMOUNT / 2n); + await expect(requests[0].unlockTime).to.be.equal(BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + + const secondTx = await network.connect(randomUser).requestUnstake(STAKE_AMOUNT / 2n); + await secondTx.wait(); + const secondBlock = await secondTx.getBlock(); + + await expect(secondTx) + .to.emit(network, Events.UNSTAKE_REQUESTED) + .withArgs(randomUser.address, STAKE_AMOUNT / 2n, BigInt(secondBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + + requests = await views.pendingUnstake(randomUser.address); + await expect(requests.length).to.be.equal(2); + await expect(requests[0].amount).to.be.equal(STAKE_AMOUNT / 2n); + await expect(requests[0].unlockTime).to.be.equal(BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + await expect(requests[1].amount).to.be.equal(STAKE_AMOUNT / 2n); + await expect(requests[1].unlockTime).to.be.equal(BigInt(secondBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN); + }); + + it("Is reverted with 'MaxRequestsAmountReached' if more than 2000 pending requests", async function() { + const { network, ssvToken, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT); + + const smallAmount = STAKE_AMOUNT / 2001n; + + for (let i = 0; i < 2000; i++) { + await network.connect(randomUser).requestUnstake(smallAmount); + } + + await expect(network.connect(randomUser).requestUnstake(smallAmount)) + .to.be.revertedWithCustomError(network, Errors.MAX_REQUESTS_AMOUNT_REACHED); + }); + + it("Is reverted with 'ZeroAmount' if caller is trying to request 0 SSV", async function() { + const { network } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await expect(network.requestUnstake(0)) + .to.be.revertedWithCustomError(network, Errors.ZERO_AMOUNT); + }); + + it("Is reverted with 'UnstakeAmountExceedsBalance' if caller is trying to request more SSV than they staked", async function(){ + const { network, ssvToken, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT) + + await expect(network.connect(randomUser).requestUnstake(STAKE_AMOUNT + 1n)) + .to.be.revertedWithCustomError(network, Errors.UNSTAKE_AMOUNT_EXCEEDS_BALANCE); + }); + }); + + describe("Function 'withdrawUnlocked()'", async function(){ + it("Withdraws SSV and emits correct event", async function() { + const { network, views, ssvToken, cssvToken, daoSigner } = + await networkHelpers.loadFixture(deployFullSSVNetworkForkFixture); + + await ssvToken.connect(randomUser).approve(await network.getAddress(), connection.ethers.MaxUint256); + await ssvToken.connect(daoSigner).mint(randomUser.address, STAKE_AMOUNT); + await network.connect(randomUser).stake(STAKE_AMOUNT) + await network.connect(randomUser).requestUnstake(STAKE_AMOUNT); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await networkHelpers.mine(); + + const tx = await network.connect(randomUser).withdrawUnlocked(); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.WITHDRAW_UNSTAKE]); + + await expect(tx) + .to.emit(network, Events.UNSTAKE_WITHDRAWN) + .withArgs(randomUser.address, STAKE_AMOUNT); + + await expect(await cssvToken.balanceOf(randomUser.address)).to.be.equal(0); + await expect(await ssvToken.balanceOf(randomUser.address)).to.be.equal(STAKE_AMOUNT); + await expect(await views.stakedBalanceOf(randomUser.address)).to.be.equal(0); + }); + }); +}); diff --git a/test/unit/SSVClusters/README.md b/test/unit/SSVClusters/README.md new file mode 100644 index 000000000..4624beec7 --- /dev/null +++ b/test/unit/SSVClusters/README.md @@ -0,0 +1,23 @@ +## SSVClusters Unit Tests + +This directory contains unit tests for the SSVClusters module, which handles validator registration and cluster management in the SSV Network. + +### Running Tests + +- Run all unit tests under this suite: `npx hardhat test test/unit/SSVClusters/*.test.ts` +- Or use the helper script from repo root: `./test/unit/SSVClusters/run-tests.sh` + +### Test Coverage + +The tests cover: +- Valid validator registration +- Validation error cases (empty public keys, length mismatches, invalid operator IDs, etc.) +- Duplicate registration prevention +- Multiple validator registration in existing clusters +- Validator removal (single/bulk) +- Cluster liquidation, reactivation, deposits, withdrawals +- Exit flows (single/bulk) +- Cluster balance updates (error path) +- ETH migration (error paths) + +Hardhat will build artifacts on demand; make sure dependencies are installed before running (`npm install`). diff --git a/test/unit/SSVClusters/deposit.test.ts b/test/unit/SSVClusters/deposit.test.ts new file mode 100644 index 000000000..4892e0c00 --- /dev/null +++ b/test/unit/SSVClusters/deposit.test.ts @@ -0,0 +1,243 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, createCluster, makePublicKey, parseClusterFromEvent, registerAndParseCluster } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGas, trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { expectContractETHDelta } from "../../helpers/balance.ts"; +import { ethers } from "ethers"; + +describe("SSVClusters function `deposit()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, otherAccount] } = await setupTestContext()); + }); + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return defaultClustersFixture(connection); + }; + + it("Deposits into an existing cluster, updates balance and emits correct event", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeDeposit = await registerAndParseCluster(clusters, operatorIds); + + const depositAmount = 1n; + + const rawReceipt = await expectContractETHDelta( + connection.ethers.provider, + await clusters.getAddress(), + () => clusters.deposit( + clusterOwner.address, + operatorIds, + clusterBeforeDeposit, + { value: depositAmount } + ), + depositAmount, + ); + const depositReceipt = await trackGasFromReceipt(rawReceipt, [GasGroup.DEPOSIT]); + const clusterAfterDeposit = parseClusterFromEvent(clusters, depositReceipt, Events.CLUSTER_DEPOSITED); + + expect(depositReceipt.eventsByName[Events.CLUSTER_DEPOSITED]).to.have.lengthOf(1); + expect(clusterAfterDeposit.balance).to.equal(clusterBeforeDeposit.balance + depositAmount); + }); + + it("Does not change operatorEthVUnits or stored cluster EB snapshot when depositing", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeDeposit = await registerAndParseCluster(clusters, operatorIds); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await clusters.mockSetClusterVUnits(clusterId, 7n * BPS_DENOMINATOR); + + const beforeClusterVUnits = await clusters.getClusterVUnits(clusterId); + const beforeOperatorVUnits = await Promise.all(operatorIds.map((id) => clusters.getOperatorEthVUnits(id))); + + const depositAmount = 3n; + const depositReceipt = await trackGas( + clusters.deposit( + clusterOwner.address, + operatorIds, + clusterBeforeDeposit, + { value: depositAmount } + ), + [GasGroup.DEPOSIT] + ); + const clusterAfterDeposit = parseClusterFromEvent(clusters, depositReceipt, Events.CLUSTER_DEPOSITED); + + expect(clusterAfterDeposit.balance).to.equal(clusterBeforeDeposit.balance + depositAmount); + + const afterClusterVUnits = await clusters.getClusterVUnits(clusterId); + const afterOperatorVUnits = await Promise.all(operatorIds.map((id) => clusters.getOperatorEthVUnits(id))); + + expect(afterClusterVUnits).to.equal(beforeClusterVUnits); + expect(afterOperatorVUnits).to.deep.equal(beforeOperatorVUnits); + }); + + it("Allows a third party to deposit to an existing cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeDeposit = await registerAndParseCluster(clusters, operatorIds); + + const depositAmount = 2n; + + const rawReceipt = await expectContractETHDelta( + connection.ethers.provider, + await clusters.getAddress(), + () => clusters.connect(otherAccount).deposit( + clusterOwner.address, + operatorIds, + clusterBeforeDeposit, + { value: depositAmount } + ), + depositAmount, + ); + const depositReceipt = await trackGasFromReceipt(rawReceipt, [GasGroup.DEPOSIT]); + const clusterAfterDeposit = parseClusterFromEvent(clusters, depositReceipt, Events.CLUSTER_DEPOSITED); + + expect(depositReceipt.eventsByName[Events.CLUSTER_DEPOSITED]).to.have.lengthOf(1); + expect(clusterAfterDeposit.balance).to.equal(clusterBeforeDeposit.balance + depositAmount); + }); + + it("Accumulates contract ETH balance by the sum of multiple deposits", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockCurrentNetworkFeeIndex(0n); + for (const operatorId of operatorIds) { + await clusters.mockSetOperatorFee(operatorId, 0n); + } + + const clusterBeforeDeposits = await registerAndParseCluster(clusters, operatorIds); + const harnessAddress = await clusters.getAddress(); + + const deposit1 = ethers.parseEther("0.01"); + const depositReceipt1 = await expectContractETHDelta( + connection.ethers.provider, + harnessAddress, + () => clusters.deposit( + clusterOwner.address, + operatorIds, + clusterBeforeDeposits, + { value: deposit1 } + ), + deposit1, + ); + await trackGasFromReceipt(depositReceipt1, [GasGroup.DEPOSIT]); + const clusterAfterDeposit1 = parseClusterFromEvent(clusters, depositReceipt1, Events.CLUSTER_DEPOSITED); + + const deposit2 = ethers.parseEther("0.02"); + const depositReceipt2 = await expectContractETHDelta( + connection.ethers.provider, + harnessAddress, + () => clusters.connect(otherAccount).deposit( + clusterOwner.address, + operatorIds, + clusterAfterDeposit1, + { value: deposit2 } + ), + deposit2, + ); + await trackGasFromReceipt(depositReceipt2, [GasGroup.DEPOSIT]); + const clusterAfterDeposit2 = parseClusterFromEvent(clusters, depositReceipt2, Events.CLUSTER_DEPOSITED); + + expect(clusterAfterDeposit2.balance).to.equal(clusterBeforeDeposits.balance + deposit1 + deposit2); + }); + + it("Is reverted with 'IncorrectClusterState' when provided cluster data is stale or mismatched", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeDeposit = await registerAndParseCluster(clusters, operatorIds); + + const mismatchedCluster = { + ...clusterBeforeDeposit, + balance: clusterBeforeDeposit.balance + 1n, + }; + + await expect(clusters.deposit( + clusterOwner.address, + operatorIds, + mismatchedCluster, + { value: 1n } + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterDoesNotExists' when attempting to deposit into a missing cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await expect(clusters.deposit( + clusterOwner.address, + operatorIds, + createCluster(), + { value: 1n } + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Deposit into zero-validator cluster accrues no fees over elapsed blocks", async function () { + const deployWithFee = async () => ssvClustersHarnessFixture(connection, 4, 10_000_000_000n); + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithFee); + + const publicKey = makePublicKey(1); + const registerTx = await clusters.registerValidator( + publicKey, operatorIds, DEFAULT_SHARES, createCluster(), { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await clusters.removeValidator(publicKey, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + const balanceAtRemoval = clusterAfterRemove.balance; + + await networkHelpers.mine(100); + + const depositAmount = ethers.parseEther("0.5"); + const depositTx = await clusters.deposit( + clusterOwner.address, operatorIds, clusterAfterRemove, { value: depositAmount } + ); + const depositReceipt = await depositTx.wait(); + const clusterAfterDeposit = parseClusterFromEvent(clusters, depositReceipt, Events.CLUSTER_DEPOSITED); + + expect(clusterAfterDeposit.balance).to.equal(balanceAtRemoval + depositAmount); + }); + + it("Does not change contract balance when deposit reverts", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeDeposit = await registerAndParseCluster(clusters, operatorIds); + + const mismatchedCluster = { + ...clusterBeforeDeposit, + balance: clusterBeforeDeposit.balance + 1n, + }; + + await expectContractETHDelta( + connection.ethers.provider, + await clusters.getAddress(), + async () => { + await expect( + clusters.deposit(clusterOwner.address, operatorIds, mismatchedCluster, { value: 1n }) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }, + 0n, + ); + }); +}); diff --git a/test/unit/SSVClusters/ebAutoLiquidation.test.ts b/test/unit/SSVClusters/ebAutoLiquidation.test.ts new file mode 100644 index 000000000..e55592826 --- /dev/null +++ b/test/unit/SSVClusters/ebAutoLiquidation.test.ts @@ -0,0 +1,207 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, computeEBRoot, createCluster, makePublicKey, parseClusterFromEvent, registerAndParseCluster } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { ethers } from "ethers"; +const OPERATOR_FEE = 10_000_000_000n; + +describe("EB auto-liquidation on updateClusterBalance", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator] } = await setupTestContext()); + }); + + const deployClustersWithFee = async () => { + return ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE); + }; + + const deployClustersWithFeeAndEightOperators = async () => { + return ssvClustersHarnessFixture(connection, 8, OPERATOR_FEE); + }; + + + + it("Auto-liquidates cluster when EB increase makes it insolvent at new rate", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + + const minBlocksBeforeLiq = 100n; + await clusters.mockMinimumBlocksBeforeLiquidation(minBlocksBeforeLiq); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("0.0001")); + + expect(clusterAfterReg.active).to.equal(true); + expect(clusterAfterReg.balance).to.be.gt(0n); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebBlockNum1 = 1; + const initialEB = 32; + const root1 = computeEBRoot(clusterId, initialEB); + await clusters.mockSetEBRoot(ebBlockNum1, root1); + + const ebTx1 = await clusters.updateClusterBalance( + ebBlockNum1, + clusterOwner.address, + operatorIds, + clusterAfterReg, + initialEB, + [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB32 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfterEB32.active).to.equal(true); + const vUnitsAfterEB32 = await clusters.getClusterVUnits(clusterId); + expect(vUnitsAfterEB32).to.equal(BPS_DENOMINATOR); + await expect( + clusters.connect(liquidator).liquidate( + clusterOwner.address, + operatorIds, + clusterAfterEB32 + ) + ).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + const ebBlockNum2 = 2; + const newEB = 2048; + const root2 = computeEBRoot(clusterId, newEB); + await clusters.mockSetEBRoot(ebBlockNum2, root2); + + const ebTx2 = await clusters.updateClusterBalance( + ebBlockNum2, + clusterOwner.address, + operatorIds, + clusterAfterEB32, + newEB, + [] + ); + const ebReceipt2 = await ebTx2.wait(); + const clusterAfterEB2048 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_LIQUIDATED); + expect(clusterAfterEB2048.active).to.equal(false, + "Auto-liquidation should fire when EB increase makes cluster insolvent at new rate"); + expect(clusterAfterEB2048.balance).to.equal(0n); + }); + + it("Does NOT auto-liquidate when cluster is solvent at new EB rate", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + await clusters.mockEthNetworkFee(100_000n); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("1")); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root1 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 32, []); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB32 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + const root2 = computeEBRoot(clusterId, 2048); + await clusters.mockSetEBRoot(2, root2); + + const ebTx2 = await clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB32, 2048, []); + const ebReceipt2 = await ebTx2.wait(); + const clusterAfterEB2048 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_BALANCE_UPDATED); + + expect(clusterAfterEB2048.active).to.equal(true, + "Cluster with sufficient balance should NOT be auto-liquidated"); + const vUnits = await clusters.getClusterVUnits(clusterId); + expect(vUnits).to.equal(640000n); + await expect( + clusters.connect(liquidator).liquidate(clusterOwner.address, operatorIds, clusterAfterEB2048) + ).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + }); + + it("Auto-liquidates when cluster is already insolvent at old rate", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + await clusters.mockEthNetworkFee(100_000n); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("0.0001")); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root1 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 32, []); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB32 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + await networkHelpers.mine(2500); + const root2 = computeEBRoot(clusterId, 2048); + await clusters.mockSetEBRoot(2, root2); + + const ebTx2 = await clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB32, 2048, []); + const ebReceipt2 = await ebTx2.wait(); + + const clusterAfterEB2048 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_LIQUIDATED); + expect(clusterAfterEB2048.active).to.equal(false, + "Auto-liquidation correctly fires when cluster is insolvent"); + }); + + it("Blocks reentrant guarded calls during updateClusterBalance auto-liquidation callback", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFeeAndEightOperators); + + const Malicious = await connection.ethers.getContractFactory("MaliciousUpdateClusterBalance"); + const malicious = await Malicious.deploy(await clusters.getAddress()); + await malicious.waitForDeployment(); + + const liquidationOps = operatorIds.slice(0, 4); + const withdrawOps = operatorIds.slice(4, 8); + const maliciousAddress = await malicious.getAddress(); + + await clusters.mockEthNetworkFee(100_000n); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const regLiquidationTx = await malicious.registerValidator( + makePublicKey(1), + liquidationOps, + DEFAULT_SHARES, + createCluster(), + { value: ethers.parseEther("0.0001") } + ); + const regLiquidationReceipt = await regLiquidationTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(clusters, regLiquidationReceipt, Events.VALIDATOR_ADDED); + + const liquidationClusterId = computeClusterId(maliciousAddress, liquidationOps); + await clusters.mockSetEBRoot(1, computeEBRoot(liquidationClusterId, 32)); + + const ebTx1 = await clusters.updateClusterBalance( + 1, + maliciousAddress, + liquidationOps, + clusterAfterRegister, + 32, + [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB32 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + const regWithdrawTx = await malicious.registerValidator( + makePublicKey(2), + withdrawOps, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const regWithdrawReceipt = await regWithdrawTx.wait(); + const clusterForWithdraw = parseClusterFromEvent(clusters, regWithdrawReceipt, Events.VALIDATOR_ADDED); + + await malicious.setReentryParams(withdrawOps, 0n, clusterForWithdraw); + await clusters.mockSetEBRoot(2, computeEBRoot(liquidationClusterId, 2048)); + await malicious.setLiquidationParams(2, liquidationOps, clusterAfterEB32, 2048, []); + + const attackTx = await malicious.attack(); + const attackReceipt = await attackTx.wait(); + const clusterAfterAttack = parseClusterFromEvent(clusters, attackReceipt, Events.CLUSTER_LIQUIDATED); + + expect(clusterAfterAttack.active).to.equal(false); + expect(await malicious.attemptedReenter()).to.equal(true); + expect(await malicious.reenterSucceeded()).to.equal(false); + }); +}); diff --git a/test/unit/SSVClusters/ebDecreaseScenarios.test.ts b/test/unit/SSVClusters/ebDecreaseScenarios.test.ts new file mode 100644 index 000000000..4f934a0cc --- /dev/null +++ b/test/unit/SSVClusters/ebDecreaseScenarios.test.ts @@ -0,0 +1,192 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, computeEBRoot, createCluster, makePublicKey, parseClusterFromEvent, registerAndParseCluster, assertOperatorVUnits } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { ethers } from "ethers"; + +const OPERATOR_FEE = 10_000_000_000n; + +describe("EB decrease scenarios", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator] } = await setupTestContext()); + }); + + const deployClustersWithFee = async () => { + return ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE); + }; + + + + it("EB decrease from 64 to 32 ETH reduces vUnits, clears deviation, settles fees at old rate", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + + await clusters.mockEthNetworkFee(0n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 64, []); + const ebReceipt1 = await ebTx1.wait(); + const blockEB64 = ebReceipt1!.blockNumber; + const clusterAfterEB64 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + const expectedVUnits64 = ((64n * BPS_DENOMINATOR) + 31n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits64); + + const expectedDeviation64 = expectedVUnits64 - BPS_DENOMINATOR; + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation64, expectedVUnits64); + + await networkHelpers.mine(100); + + const balanceAfterEB64 = clusterAfterEB64.balance; + + const root2 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(2, root2); + + const ebTx2 = await clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB64, 32, []); + const ebReceipt2 = await ebTx2.wait(); + const blockEB32 = ebReceipt2!.blockNumber; + const clusterAfterEB32 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + + await assertOperatorVUnits(clusters, operatorIds, 0n, BPS_DENOMINATOR); + const blocksDelta = BigInt(blockEB32 - blockEB64); + const vUnits64 = 20000n; + const ETH_DEDUCTED_DIGITS = 100_000n; + const packedOpFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const totalPackedFeeRate = 4n * packedOpFee; + const expectedFees = ((blocksDelta * totalPackedFeeRate * vUnits64) / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + const feesDeducted = balanceAfterEB64 - clusterAfterEB32.balance; + expect(feesDeducted).to.equal(expectedFees); + + expect(clusterAfterEB32.active).to.equal(true); + }); + + it("EB decrease below 32 ETH per validator reverts with EBBelowMinimum", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + const root1 = computeEBRoot(clusterId, 128); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 128, []); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfter128 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.be.gt(BPS_DENOMINATOR); + + const belowMinEB = 31; + const root2 = computeEBRoot(clusterId, belowMinEB); + await clusters.mockSetEBRoot(2, root2); + + await expect( + clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfter128, belowMinEB, []) + ).to.be.revertedWithCustomError(clusters, Errors.EB_BELOW_MINIMUM); + }); + + it("EB decrease auto-liquidates cluster when balance falls below new lower threshold", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const depositValue = 12_000_000_000_000n; + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue } + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + expect(clusterAfterReg.active).to.equal(true); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 64, []); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB64 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(clusterAfterEB64.active).to.equal(true); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + + await expect( + clusters.connect(liquidator).liquidate(clusterOwner.address, operatorIds, clusterAfterEB64) + ).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + + await networkHelpers.mine(70); + + const root2 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(2, root2); + + const ebTx2 = await clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB64, 32, []); + const ebReceipt2 = await ebTx2.wait(); + + const clusterAfterEB32 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_LIQUIDATED); + expect(clusterAfterEB32.active).to.equal(false); + expect(clusterAfterEB32.balance).to.equal(0n); + }); + + it("EB decrease correctly decrements operator deviation and daoTotalEthVUnits", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumLiquidationCollateral(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds); + + await assertOperatorVUnits(clusters, operatorIds, 0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(BPS_DENOMINATOR); + + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + + const ebTx1 = await clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 64, []); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB64 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + const expectedDeviation = 20000n - BPS_DENOMINATOR; + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation, 20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + const root2 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(2, root2); + + const ebTx2 = await clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB64, 32, []); + const ebReceipt2 = await ebTx2.wait(); + parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_BALANCE_UPDATED); + + await assertOperatorVUnits(clusters, operatorIds, 0n, BPS_DENOMINATOR); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(BPS_DENOMINATOR); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + }); +}); diff --git a/test/unit/SSVClusters/ebSettlement.test.ts b/test/unit/SSVClusters/ebSettlement.test.ts new file mode 100644 index 000000000..d060dd9f0 --- /dev/null +++ b/test/unit/SSVClusters/ebSettlement.test.ts @@ -0,0 +1,303 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, computeEBRoot, createCluster, makePublicKey, parseClusterFromEvent, registerAndParseCluster } from "../../common/helpers.ts"; +import { DEFAULT_SHARES, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; +const OPERATOR_FEE = 10_000_000_000n; + +describe("EB-aware fee settlement on registration and removal", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + const deployClustersWithFee = async () => { + return ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE); + }; + + + + it("Registration settles fees using EB-weighted vUnits, not flat validatorCount", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const cluster1 = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("100")); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebBlockNum = 1; + const effectiveBalance = 1000; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(ebBlockNum, root); + + const ebTx = await clusters.updateClusterBalance( + ebBlockNum, + clusterOwner.address, + operatorIds, + cluster1, + effectiveBalance, + [] + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + const expectedVUnits = ((BigInt(effectiveBalance) * BPS_DENOMINATOR) + 31n) / 32n; + expect(clusterVUnits).to.equal(expectedVUnits); + const balanceBeforeMine = clusterAfterEB.balance; + const blockBeforeMine = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(100); + const blockAfterMine = await connection.ethers.provider.getBlockNumber(); + const blocksMined = blockAfterMine - blockBeforeMine; + const regTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterEB, + { value: 0n } + ); + const receipt2 = await regTx2.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + const balanceAfterReg = clusterAfterReg.balance; + const feeDeducted = balanceBeforeMine - balanceAfterReg; + const packedOpFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const vUnitsMultiplier = expectedVUnits / BPS_DENOMINATOR; + const expectedEBFee = 4n * packedOpFee * BigInt(blocksMined + 1) * vUnitsMultiplier * ETH_DEDUCTED_DIGITS; + const flatUsageExpanded = 4n * packedOpFee * BigInt(blocksMined + 1) * 1n * ETH_DEDUCTED_DIGITS; + expect(feeDeducted).to.be.gt(0n, "Fee should have been deducted"); + expect(feeDeducted).to.be.approximately( + expectedEBFee, + expectedEBFee / 100n, + "EB-weighted fee settlement should match expected calculation" + ); + expect(feeDeducted).to.be.gt( + flatUsageExpanded * 10n, + "EB-weighted fee settlement should charge significantly more than flat validatorCount" + ); + }); + + it("Removal settles fees using EB-weighted vUnits", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const cluster1 = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("100")); + + const regTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster1, + { value: 0n } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebBlockNum = 1; + const effectiveBalance = 500; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(ebBlockNum, root); + + const ebTx = await clusters.updateClusterBalance( + ebBlockNum, + clusterOwner.address, + operatorIds, + cluster2, + effectiveBalance, + [] + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + const expectedVUnits = ((BigInt(effectiveBalance) * BPS_DENOMINATOR) + 31n) / 32n; + expect(clusterVUnits).to.equal(expectedVUnits); + + const balanceBeforeMine = clusterAfterEB.balance; + const blockBeforeMine = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(100); + const blockAfterMine = await connection.ethers.provider.getBlockNumber(); + const blocksMined = blockAfterMine - blockBeforeMine; + const removeTx = await clusters.removeValidator( + makePublicKey(1), + operatorIds, + clusterAfterEB + ); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + const feeDeducted = balanceBeforeMine - clusterAfterRemove.balance; + expect(feeDeducted).to.be.gt(0n, "Fee should have been deducted on removal"); + const packedOpFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const vUnitsMultiplier = expectedVUnits / BPS_DENOMINATOR; + const expectedEBFee = 4n * packedOpFee * BigInt(blocksMined + 1) * vUnitsMultiplier * ETH_DEDUCTED_DIGITS; + const flatUsageExpanded = 4n * packedOpFee * BigInt(blocksMined + 1) * 2n * ETH_DEDUCTED_DIGITS; + expect(feeDeducted).to.be.approximately( + expectedEBFee, + expectedEBFee / 10n, + "EB-weighted fee settlement on removal should match expected calculation" + ); + expect(feeDeducted).to.be.gt( + flatUsageExpanded * 5n, + "EB-weighted fee settlement on removal should charge more than flat validatorCount" + ); + }); + + describe("Edge Cases for EB Settlement", async () => { + it("Uses baseline vUnits when EB = 0 (no EB set)", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const cluster1 = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("100")); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + expect(clusterVUnits).to.equal(0n, "vUnits should be 0 when EB is not set"); + + const balanceBeforeMine = cluster1.balance; + await networkHelpers.mine(50); + + const regTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster1, + { value: 0n } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const feeDeducted = balanceBeforeMine - cluster2.balance; + const packedOpFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const expectedBaselineFee = 4n * packedOpFee * 51n * 1n * ETH_DEDUCTED_DIGITS; + + expect(feeDeducted).to.be.approximately( + expectedBaselineFee, + expectedBaselineFee / 10n, + "Should use baseline vUnits when EB = 0" + ); + }); + + it("Handles EB exactly at baseline (32 ETH)", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const cluster1 = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("100")); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebBlockNum = 1; + const effectiveBalance = 32; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(ebBlockNum, root); + + const ebTx = await clusters.updateClusterBalance( + ebBlockNum, + clusterOwner.address, + operatorIds, + cluster1, + effectiveBalance, + [] + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + const expectedVUnits = 1n * BPS_DENOMINATOR; + expect(clusterVUnits).to.equal(expectedVUnits); + + const balanceBeforeMine = clusterAfterEB.balance; + await networkHelpers.mine(50); + + const regTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterEB, + { value: 0n } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const feeDeducted = balanceBeforeMine - cluster2.balance; + const packedOpFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const expectedBaselineFee = 4n * packedOpFee * 51n * 1n * ETH_DEDUCTED_DIGITS; + + expect(feeDeducted).to.be.approximately( + expectedBaselineFee, + expectedBaselineFee / 100n, + "EB at baseline should charge same as baseline calculation" + ); + }); + + it("Handles very high EB values (stress test)", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const cluster1 = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("1000")); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebBlockNum = 1; + const effectiveBalance = 1000; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(ebBlockNum, root); + + const ebTx = await clusters.updateClusterBalance( + ebBlockNum, + clusterOwner.address, + operatorIds, + cluster1, + effectiveBalance, + [] + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + const expectedVUnits = ((BigInt(effectiveBalance) * BPS_DENOMINATOR) + 31n) / 32n; + expect(clusterVUnits).to.equal(expectedVUnits); + + const balanceBeforeMine = clusterAfterEB.balance; + await networkHelpers.mine(10); + + const regTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterEB, + { value: 0n } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const feeDeducted = balanceBeforeMine - cluster2.balance; + const packedOpFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const vUnitsMultiplier = expectedVUnits / BPS_DENOMINATOR; + + expect(feeDeducted).to.be.gt(0n, "High EB should still deduct fees"); + const baselineFee = 4n * packedOpFee * 11n * 1n * ETH_DEDUCTED_DIGITS; + expect(feeDeducted).to.be.gt( + baselineFee * 10n, + "High EB should result in proportionally higher fees" + ); + expect(feeDeducted).to.be.lt( + balanceBeforeMine, + "Fees deducted should not exceed total balance" + ); + }); + + it("Handles zero validator count edge case", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const ebBlockNum = 1; + const effectiveBalance = 1000; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(ebBlockNum, root); + const emptyCluster = createCluster(); + emptyCluster.validatorCount = 0; + try { + const ebTx = await clusters.updateClusterBalance( + ebBlockNum, + clusterOwner.address, + operatorIds, + emptyCluster, + effectiveBalance, + [] + ); + const ebReceipt = await ebTx.wait(); + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + expect(clusterVUnits).to.equal(0n, "Cluster with 0 validators should have 0 vUnits"); + } catch (error) { + expect(error.message).to.include("revert", "Should handle 0 validator case gracefully"); + } + }); + }); +}); \ No newline at end of file diff --git a/test/unit/SSVClusters/ebWeightedOperatorEarnings.test.ts b/test/unit/SSVClusters/ebWeightedOperatorEarnings.test.ts new file mode 100644 index 000000000..3d977c3ed --- /dev/null +++ b/test/unit/SSVClusters/ebWeightedOperatorEarnings.test.ts @@ -0,0 +1,422 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, computeEBRoot, createCluster, makePublicKey, parseClusterFromEvent } from "../../common/helpers.ts"; +import { DEFAULT_SHARES, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; + +const OPERATOR_FEE = 10_000_000_000n; + +describe("EB-weighted operator earnings (Consolidated)", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner1: HardhatEthersSigner; + let clusterOwner2: HardhatEthersSigner; + let clusterOwner3: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner1, clusterOwner2, clusterOwner3] } = await setupTestContext()); + }); + + const deployClustersWithFee = async () => { + return ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE); + }; + + const deployClustersWithZeroFee = async () => { + return ssvClustersHarnessFixture(connection, 4, 0n); + }; + + + + describe("Accumulation", async () => { + it("operator earns proportionally from two clusters with EB=32 and EB=64", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const deposit = ethers.parseEther("100"); + + const regTx1 = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt1 = await regTx1.wait(); + const cluster1 = parseClusterFromEvent(clusters, receipt1, Events.VALIDATOR_ADDED); + + const regTx2 = await clusters.connect(clusterOwner2).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const clusterId1 = computeClusterId(clusterOwner1.address, operatorIds); + const root1 = computeEBRoot(clusterId1, 32); + await clusters.mockSetEBRoot(1, root1); + const ebTx1 = await clusters.connect(clusterOwner1).updateClusterBalance( + 1, clusterOwner1.address, operatorIds, cluster1, 32, [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB1 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + const clusterId2 = computeClusterId(clusterOwner2.address, operatorIds); + const root2 = computeEBRoot(clusterId2, 64); + await clusters.mockSetEBRoot(2, root2); + await clusters.connect(clusterOwner2).updateClusterBalance( + 2, clusterOwner2.address, operatorIds, cluster2, 64, [] + ); + + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(30000n); + const [, , balanceBefore] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + const blockBeforeMine = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(100); + const blocksMined = (await connection.ethers.provider.getBlockNumber()) - blockBeforeMine; + + await clusters.connect(clusterOwner1).removeValidator(makePublicKey(1), operatorIds, clusterAfterEB1); + + const [, , balanceAfter] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + const earned = balanceAfter - balanceBefore; + + const blocksDelta = BigInt(blocksMined + 1); + const expected = packedFee * blocksDelta * 30000n / BPS_DENOMINATOR; + expect(earned).to.equal(expected); + + const flatBaseline = packedFee * blocksDelta * 20000n / BPS_DENOMINATOR; + }); + + it("earnings split correctly at fee change boundary with EB-weighted vUnits", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const deposit = ethers.parseEther("100"); + + const regTx = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner1.address, operatorIds); + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + const ebTx1 = await clusters.connect(clusterOwner1).updateClusterBalance( + 1, clusterOwner1.address, operatorIds, clusterAfterReg, 64, [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB1 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(20000n); + + const [, snapshotBlock1, balancePhase1Start] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + await networkHelpers.mine(50); + + const root2 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(2, root2); + const ebTx2 = await clusters.connect(clusterOwner1).updateClusterBalance( + 2, clusterOwner1.address, operatorIds, clusterAfterEB1, 64, [] + ); + const ebReceipt2 = await ebTx2.wait(); + const clusterAfterEB2 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_BALANCE_UPDATED); + + const [, snapshotBlock2, balancePhase1End] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + const phase1Blocks = BigInt(snapshotBlock2) - BigInt(snapshotBlock1); + const expectedPhase1Delta = packedFee * phase1Blocks * 20000n / BPS_DENOMINATOR; + expect(balancePhase1End - balancePhase1Start).to.equal(expectedPhase1Delta); + + const NEW_OPERATOR_FEE = 5_000_000_000n; + const newPackedFee = NEW_OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + await clusters.mockSetOperatorFee(operatorIds[0], NEW_OPERATOR_FEE); + + await networkHelpers.mine(50); + + const root3 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(3, root3); + await clusters.connect(clusterOwner1).updateClusterBalance( + 3, clusterOwner1.address, operatorIds, clusterAfterEB2, 64, [] + ); + + const settledBlock3 = await connection.ethers.provider.getBlockNumber(); + const [, , balancePhase2End] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + const phase2Blocks = BigInt(settledBlock3) - BigInt(snapshotBlock2); + const expectedPhase2Delta = newPackedFee * phase2Blocks * 20000n / BPS_DENOMINATOR; + expect(balancePhase2End - balancePhase1End).to.equal(expectedPhase2Delta); + }); + + it("operator snapshot balance equals expected EB-weighted ETH after settlement", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const deposit = ethers.parseEther("100"); + + const regTx = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner1.address, operatorIds); + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + const ebTx1 = await clusters.connect(clusterOwner1).updateClusterBalance( + 1, clusterOwner1.address, operatorIds, clusterAfterReg, 64, [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB1 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + const [, snapshotBlock1, balanceAtSnapshot] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + await networkHelpers.mine(100); + const root2 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(2, root2); + await clusters.connect(clusterOwner1).updateClusterBalance( + 2, clusterOwner1.address, operatorIds, clusterAfterEB1, 64, [] + ); + + const harnessAddress = await clusters.getAddress(); + const harnessEthBefore = await connection.ethers.provider.getBalance(harnessAddress); + await clusters.connect(clusterOwner1).mockWithdrawAllEthEarnings(operatorIds[0]); + const withdrawalBlock = await connection.ethers.provider.getBlockNumber(); + const harnessEthAfter = await connection.ethers.provider.getBalance(harnessAddress); + + const totalBlocksDelta = BigInt(withdrawalBlock) - BigInt(snapshotBlock1); + const newEarningsPacked = packedFee * totalBlocksDelta * 20000n / BPS_DENOMINATOR; + const expectedETH = (balanceAtSnapshot + newEarningsPacked) * ETH_DEDUCTED_DIGITS; + expect(harnessEthBefore - harnessEthAfter).to.equal(expectedETH); + + const [, , balanceAfterWithdraw] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + expect(balanceAfterWithdraw).to.equal(0n); + }); + }); + + describe("Edge Cases", async () => { + it("operator with zero fee earns nothing despite EB > 32", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithZeroFee); + const deposit = ethers.parseEther("100"); + + const regTx = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt = await regTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner1.address, operatorIds); + const root = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root); + const ebTx1 = await clusters.connect(clusterOwner1).updateClusterBalance( + 1, clusterOwner1.address, operatorIds, cluster, 64, [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(20000n); + + const [, , balanceBefore] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + await networkHelpers.mine(100); + + const root2 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(2, root2); + const ebTx2 = await clusters.connect(clusterOwner1).updateClusterBalance( + 2, clusterOwner1.address, operatorIds, clusterAfterEB, 64, [] + ); + await ebTx2.wait(); + + const [, , balanceAfter] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + expect(balanceAfter).to.equal(balanceBefore); + expect(balanceAfter).to.equal(0n); + }); + + it("operator earnings cap at maximum EB (2048 ETH per validator)", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const deposit = ethers.parseEther("100"); + + const regTx = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt = await regTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + const clusterId = computeClusterId(clusterOwner1.address, operatorIds); + const root = computeEBRoot(clusterId, 2048); + await clusters.mockSetEBRoot(1, root); + const ebTx = await clusters.connect(clusterOwner1).updateClusterBalance( + 1, clusterOwner1.address, operatorIds, cluster, 2048, [] + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + const maxVUnits = 640_000n; + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(maxVUnits); + + const [, snapshotBlock, balanceBefore] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + await networkHelpers.mine(50); + + await clusters.connect(clusterOwner1).removeValidator(makePublicKey(1), operatorIds, clusterAfterEB); + const removeBlock = await connection.ethers.provider.getBlockNumber(); + + const [, , balanceAfter] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + const blocksDelta = BigInt(removeBlock) - BigInt(snapshotBlock); + const expectedEarnings = packedFee * blocksDelta * maxVUnits / BPS_DENOMINATOR; + + expect(balanceAfter - balanceBefore).to.equal(expectedEarnings); + expect(balanceAfter - balanceBefore).to.equal(packedFee * blocksDelta * 64n); + }); + + it("operator earnings reflect multi-validator cluster with EB > 32", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const deposit = ethers.parseEther("100"); + const regTx1 = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt1 = await regTx1.wait(); + const cluster1 = parseClusterFromEvent(clusters, receipt1, Events.VALIDATOR_ADDED); + + const regTx2 = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, cluster1, { value: deposit } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const regTx3 = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(3), operatorIds, DEFAULT_SHARES, cluster2, { value: deposit } + ); + const receipt3 = await regTx3.wait(); + const cluster3 = parseClusterFromEvent(clusters, receipt3, Events.VALIDATOR_ADDED); + + expect(cluster3.validatorCount).to.equal(3); + const clusterId = computeClusterId(clusterOwner1.address, operatorIds); + const root = computeEBRoot(clusterId, 144); + await clusters.mockSetEBRoot(1, root); + const ebTx = await clusters.connect(clusterOwner1).updateClusterBalance( + 1, clusterOwner1.address, operatorIds, cluster3, 144, [] + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + + const expectedVUnits = 45_000n; + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(expectedVUnits); + + const [, snapshotBlock, balanceBefore] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + await networkHelpers.mine(80); + + await clusters.connect(clusterOwner1).removeValidator(makePublicKey(1), operatorIds, clusterAfterEB); + const removeBlock = await connection.ethers.provider.getBlockNumber(); + + const [, , balanceAfter] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + const blocksDelta = BigInt(removeBlock) - BigInt(snapshotBlock); + const expectedEarnings = packedFee * blocksDelta * expectedVUnits / BPS_DENOMINATOR; + + expect(balanceAfter - balanceBefore).to.equal(expectedEarnings); + }); + + it("operator earnings adjust correctly when EB decreases", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const deposit = ethers.parseEther("100"); + + const regTx = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt = await regTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + const clusterId = computeClusterId(clusterOwner1.address, operatorIds); + const root1 = computeEBRoot(clusterId, 64); + await clusters.mockSetEBRoot(1, root1); + const ebTx1 = await clusters.connect(clusterOwner1).updateClusterBalance( + 1, clusterOwner1.address, operatorIds, cluster, 64, [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB1 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(20000n); + + const [, snapshotBlock1, balancePhase1Start] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + await networkHelpers.mine(50); + const root2 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(2, root2); + const ebTx2 = await clusters.connect(clusterOwner1).updateClusterBalance( + 2, clusterOwner1.address, operatorIds, clusterAfterEB1, 32, [] + ); + const ebReceipt2 = await ebTx2.wait(); + const clusterAfterEB2 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_BALANCE_UPDATED); + + const [, snapshotBlock2, balancePhase1End] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + const phase1Blocks = BigInt(snapshotBlock2) - BigInt(snapshotBlock1); + const expectedPhase1 = packedFee * phase1Blocks * 20000n / BPS_DENOMINATOR; + expect(balancePhase1End - balancePhase1Start).to.equal(expectedPhase1); + + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(10000n); + + await networkHelpers.mine(50); + + await clusters.connect(clusterOwner1).removeValidator(makePublicKey(1), operatorIds, clusterAfterEB2); + const removeBlock = await connection.ethers.provider.getBlockNumber(); + + const [, , balanceFinal] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + const phase2Blocks = BigInt(removeBlock) - BigInt(snapshotBlock2); + const expectedPhase2 = packedFee * phase2Blocks * 10000n / BPS_DENOMINATOR; + expect(balanceFinal - balancePhase1End).to.equal(expectedPhase2); + expect(expectedPhase2).to.be.lessThan(expectedPhase1); + }); + + it("operator earns from mixed implicit and explicit EB clusters correctly", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const deposit = ethers.parseEther("100"); + const regTx1 = await clusters.connect(clusterOwner1).registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt1 = await regTx1.wait(); + const cluster1 = parseClusterFromEvent(clusters, receipt1, Events.VALIDATOR_ADDED); + const regTx2 = await clusters.connect(clusterOwner2).registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const clusterId2 = computeClusterId(clusterOwner2.address, operatorIds); + const root2 = computeEBRoot(clusterId2, 64); + await clusters.mockSetEBRoot(1, root2); + await clusters.connect(clusterOwner2).updateClusterBalance( + 1, clusterOwner2.address, operatorIds, cluster2, 64, [] + ); + const regTx3 = await clusters.connect(clusterOwner3).registerValidator( + makePublicKey(3), operatorIds, DEFAULT_SHARES, createCluster(), { value: deposit } + ); + const receipt3 = await regTx3.wait(); + const cluster3 = parseClusterFromEvent(clusters, receipt3, Events.VALIDATOR_ADDED); + + const clusterId3 = computeClusterId(clusterOwner3.address, operatorIds); + const root3 = computeEBRoot(clusterId3, 32); + await clusters.mockSetEBRoot(2, root3); + await clusters.connect(clusterOwner3).updateClusterBalance( + 2, clusterOwner3.address, operatorIds, cluster3, 32, [] + ); + const expectedTotalVUnits = 40_000n; + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(expectedTotalVUnits); + + const [, snapshotBlock, balanceBefore] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + await networkHelpers.mine(60); + const removeTx = await clusters.connect(clusterOwner1).removeValidator( + makePublicKey(1), operatorIds, cluster1 + ); + const removeReceipt = await removeTx.wait(); + const settleBlock = removeReceipt!.blockNumber; + + const [, , balanceAfter] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + + const blocksDelta = BigInt(settleBlock) - BigInt(snapshotBlock); + const expectedEarnings = packedFee * blocksDelta * expectedTotalVUnits / BPS_DENOMINATOR; + + expect(balanceAfter - balanceBefore).to.equal(expectedEarnings); + expect(balanceAfter - balanceBefore).to.equal(packedFee * blocksDelta * 4n); + }); + }); +}); diff --git a/test/unit/SSVClusters/feeChangeEBInteraction.test.ts b/test/unit/SSVClusters/feeChangeEBInteraction.test.ts new file mode 100644 index 000000000..69db776a5 --- /dev/null +++ b/test/unit/SSVClusters/feeChangeEBInteraction.test.ts @@ -0,0 +1,321 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { Cluster, NetworkHelpersType } from "../../common/types.ts"; +import { + setupTestContext, + generateMerkleForClusterEB, + makeOperatorKey, + makePublicKey, + parseClusterFromEvent, +} from "../../common/helpers.ts"; +import { + DECLARE_OPERATOR_FEE_PERIOD, + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + EMPTY_CLUSTER, + ETH_DEDUCTED_DIGITS, + MINIMAL_OPERATOR_ETH_FEE, + STAKE_AMOUNT, + BPS_DENOMINATOR, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; + +describe("Operator fee change + EB burn rate interaction", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const EB_64 = 64; + + const deployNetworkForFeeEbFixture = async () => { + const { network, ssvToken } = await ssvNetworkFullFixture(connection); + const [deployer, operatorOwner, clusterOwner, oracle1, oracle2, oracle3, oracle4, staker] = + await connection.ethers.getSigners(); + + await network.connect(deployer).replaceOracle(1, oracle1.address); + await network.connect(deployer).replaceOracle(2, oracle2.address); + await network.connect(deployer).replaceOracle(3, oracle3.address); + await network.connect(deployer).replaceOracle(4, oracle4.address); + + await ssvToken.transfer(staker.address, STAKE_AMOUNT); + await ssvToken.connect(staker).approve(await network.getAddress(), STAKE_AMOUNT); + await network.connect(staker).stake(STAKE_AMOUNT); + await network.connect(deployer).updateNetworkFee(0n); + + return { + network, + operatorOwner, + clusterOwner, + oracles: [oracle1, oracle2, oracle3] as HardhatEthersSigner[], + }; + }; + + const registerOperatorsWithFee = async ( + network: any, + owner: HardhatEthersSigner, + fee: bigint, + count = 4 + ): Promise => { + const operatorIds: bigint[] = []; + for (let i = 0; i < count; i += 1) { + const operatorId = await network + .connect(owner) + .registerOperator.staticCall(makeOperatorKey(i + 1), fee, true); + await network.connect(owner).registerOperator(makeOperatorKey(i + 1), fee, true); + operatorIds.push(operatorId); + } + return operatorIds; + }; + + const clusterIdFor = (ownerAddress: string, operatorIds: bigint[]): string => + ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [ownerAddress, operatorIds]) + ); + + const commitRootForCluster = async ( + network: any, + oracles: HardhatEthersSigner[], + clusterId: string, + effectiveBalance: number + ): Promise<{ blockNum: number; proof: string[] }> => { + const { root, proofs } = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance }]); + + await networkHelpers.mine(1); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + await network.connect(oracles[0]).commitRoot(root, blockNum); + await network.connect(oracles[1]).commitRoot(root, blockNum); + await network.connect(oracles[2]).commitRoot(root, blockNum); + + return { blockNum, proof: proofs[clusterId] }; + }; + + const settleClusterAtEB = async ( + network: any, + oracles: HardhatEthersSigner[], + clusterOwner: HardhatEthersSigner, + operatorIds: bigint[], + cluster: Cluster, + effectiveBalance: number + ): Promise<{ cluster: Cluster; blockNumber: bigint }> => { + const clusterId = clusterIdFor(clusterOwner.address, operatorIds); + const { blockNum, proof } = await commitRootForCluster(network, oracles, clusterId, effectiveBalance); + + const tx = await network.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + proof + ); + const receipt = await tx.wait(); + + return { + cluster: parseClusterFromEvent(network, receipt, Events.CLUSTER_BALANCE_UPDATED), + blockNumber: BigInt(receipt!.blockNumber), + }; + }; + + const registerAndSetEb64Cluster = async ( + network: any, + operatorOwner: HardhatEthersSigner, + clusterOwner: HardhatEthersSigner, + oracles: HardhatEthersSigner[], + operatorFee: bigint + ): Promise<{ operatorIds: bigint[]; cluster: Cluster; settledBlock: bigint }> => { + const operatorIds = await registerOperatorsWithFee(network, operatorOwner, operatorFee); + await network.connect(operatorOwner).setOperatorsWhitelists(operatorIds, [clusterOwner.address]); + + const registerTx = await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(network, registerReceipt, Events.VALIDATOR_ADDED); + + const settled = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + clusterAfterRegister, + EB_64 + ); + + return { operatorIds, cluster: settled.cluster, settledBlock: settled.blockNumber }; + }; + + it("Operator fee increase on EB=64 cluster doubles burn rate", async function () { + const { network, operatorOwner, clusterOwner, oracles } = + await networkHelpers.loadFixture(deployNetworkForFeeEbFixture); + + const { operatorIds, cluster: clusterAtEb64 } = await registerAndSetEb64Cluster( + network, + operatorOwner, + clusterOwner, + oracles, + MINIMAL_OPERATOR_ETH_FEE + ); + + const windowBlocks = 120n; + + await networkHelpers.mine(windowBlocks); + const beforeIncrease = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + clusterAtEb64, + EB_64 + ); + const deductionBeforeIncrease = clusterAtEb64.balance - beforeIncrease.cluster.balance; + + const doubledFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + for (const operatorId of operatorIds) { + await network.connect(operatorOwner).declareOperatorFee(operatorId, doubledFee); + } + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + for (const operatorId of operatorIds) { + await network.connect(operatorOwner).executeOperatorFee(operatorId); + } + + const afterIncreaseSettle = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + beforeIncrease.cluster, + EB_64 + ); + + await networkHelpers.mine(windowBlocks); + const afterIncreaseWindow = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + afterIncreaseSettle.cluster, + EB_64 + ); + const deductionAfterIncrease = + afterIncreaseSettle.cluster.balance - afterIncreaseWindow.cluster.balance; + + expect(deductionAfterIncrease).to.equal(deductionBeforeIncrease * 2n); + }); + + it("Operator fee reduction on EB-weighted cluster lowers burn proportionally", async function () { + const { network, operatorOwner, clusterOwner, oracles } = + await networkHelpers.loadFixture(deployNetworkForFeeEbFixture); + + const highFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + const { operatorIds, cluster: clusterAtEb64 } = await registerAndSetEb64Cluster( + network, + operatorOwner, + clusterOwner, + oracles, + highFee + ); + + const windowBlocks = 120n; + + await networkHelpers.mine(windowBlocks); + const highFeeWindow = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + clusterAtEb64, + EB_64 + ); + const highFeeDeduction = clusterAtEb64.balance - highFeeWindow.cluster.balance; + + for (const operatorId of operatorIds) { + await network.connect(operatorOwner).reduceOperatorFee(operatorId, MINIMAL_OPERATOR_ETH_FEE); + } + + const afterReduceSettle = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + highFeeWindow.cluster, + EB_64 + ); + + await networkHelpers.mine(windowBlocks); + const lowFeeWindow = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + afterReduceSettle.cluster, + EB_64 + ); + const lowFeeDeduction = afterReduceSettle.cluster.balance - lowFeeWindow.cluster.balance; + + expect(highFeeDeduction).to.equal(lowFeeDeduction * 2n); + }); + + it("Fee execution boundary with EB=64 applies old and new rates to correct block ranges", async function () { + const { network, operatorOwner, clusterOwner, oracles } = + await networkHelpers.loadFixture(deployNetworkForFeeEbFixture); + + const { operatorIds, cluster: clusterAtEb64, settledBlock } = await registerAndSetEb64Cluster( + network, + operatorOwner, + clusterOwner, + oracles, + MINIMAL_OPERATOR_ETH_FEE + ); + + const targetOperator = operatorIds[0]; + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await network.connect(operatorOwner).declareOperatorFee(targetOperator, newFee); + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + const executeTx = await network.connect(operatorOwner).executeOperatorFee(targetOperator); + const executeReceipt = await executeTx.wait(); + const executeBlock = BigInt(executeReceipt!.blockNumber); + + await networkHelpers.mine(120n); + + const finalSettlement = await settleClusterAtEB( + network, + oracles, + clusterOwner, + operatorIds, + clusterAtEb64, + EB_64 + ); + const finalBlock = finalSettlement.blockNumber; + + const oldPackedFee = MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS; + const newPackedFee = newFee / ETH_DEDUCTED_DIGITS; + + const oldSpanBlocks = executeBlock - settledBlock; + const newSpanBlocks = finalBlock - executeBlock; + + const expectedIndexDelta = + oldSpanBlocks * (oldPackedFee * 4n) + + newSpanBlocks * (oldPackedFee * 3n + newPackedFee); + + const expectedDeduction = + ((expectedIndexDelta * 20_000n) / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + const actualDeduction = clusterAtEb64.balance - finalSettlement.cluster.balance; + expect(actualDeduction).to.equal(expectedDeduction); + }); +}); diff --git a/test/unit/SSVClusters/legacySSVAccounting.test.ts b/test/unit/SSVClusters/legacySSVAccounting.test.ts new file mode 100644 index 000000000..2ab9c599d --- /dev/null +++ b/test/unit/SSVClusters/legacySSVAccounting.test.ts @@ -0,0 +1,206 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { getTestConnection } from "../../setup/connection.ts"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { Cluster, NetworkHelpersType } from "../../common/types.ts"; +import { createCluster, makePublicKeys, parseClusterFromEvent } from "../../common/helpers.ts"; +import { DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; + +type Snapshot = { + block: bigint; + index: bigint; +}; + +describe("SSVClusters legacy SSV accounting", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [clusterOwner] = await connection.ethers.getSigners(); + }); + + const deployLegacySSVFixture = async (operatorFeeRaw: bigint, networkFeeRaw: bigint) => { + const { clusters, operatorIds } = await ssvClustersHarnessFixture(connection); + const operatorFeeUnpacked = operatorFeeRaw * DEDUCTED_DIGITS; + + for (const operatorId of operatorIds) { + await clusters.mockOperatorSSVFee(operatorId, operatorFeeUnpacked); + } + + await clusters.mockSSVNetworkFee(networkFeeRaw); + const networkFeeIndexTx = await clusters.mockCurrentNetworkFeeIndexSSV(0n); + const networkFeeIndexReceipt = await networkFeeIndexTx.wait(); + + return { + clusters, + operatorIds, + networkFeeIndexBlock: BigInt(networkFeeIndexReceipt!.blockNumber), + }; + }; + + const deployOperatorFeeFixture = async () => deployLegacySSVFixture(2_000n, 0n); + const deployNetworkFeeFixture = async () => deployLegacySSVFixture(0n, 75n); + + const createLegacySSVCluster = (overrides: Partial = {}): Cluster => + createCluster({ + validatorCount: 2n, + index: 0n, + networkFeeIndex: 0n, + balance: ethers.parseEther("100"), + ...overrides, + }); + + const captureSnapshots = async (clusters: any, operatorIds: bigint[]): Promise => + Promise.all( + operatorIds.map(async (operatorId) => { + const [index, blockNumber] = await clusters.getOperatorSnapshot(operatorId); + return { + block: BigInt(blockNumber), + index: BigInt(index), + }; + }) + ); + + const calculateClusterIndex = (snapshots: Snapshot[], currentBlock: bigint, operatorFeeRaw: bigint): bigint => + snapshots.reduce( + (sum, snapshot) => sum + snapshot.index + (currentBlock - snapshot.block) * operatorFeeRaw, + 0n + ); + + const calculateNetworkFeeIndex = ( + currentBlock: bigint, + feeIndexBlock: bigint, + networkFeeRaw: bigint + ): bigint => (currentBlock - feeIndexBlock) * networkFeeRaw; + + const calculateSettledFees = ( + cluster: Cluster, + currentClusterIndex: bigint, + currentNetworkFeeIndex: bigint + ): bigint => + ( + (currentClusterIndex - cluster.index) * BigInt(cluster.validatorCount) + + (currentNetworkFeeIndex - cluster.networkFeeIndex) * BigInt(cluster.validatorCount) + ) * DEDUCTED_DIGITS; + + it("removeValidator settles accrued legacy SSV operator fees before decrementing validator count", async function () { + const operatorFeeRaw = 2_000n; + const { clusters, operatorIds, networkFeeIndexBlock } = + await networkHelpers.loadFixture(deployOperatorFeeFixture); + + const [publicKey1, publicKey2] = makePublicKeys(2); + const cluster = createLegacySSVCluster({ validatorCount: 2n }); + + await clusters.mockRegisterSSVValidator(publicKey1, operatorIds, clusterOwner.address, cluster); + await clusters.mockRegisterSSVValidator(publicKey2, operatorIds, clusterOwner.address, cluster); + + const snapshots = await captureSnapshots(clusters, operatorIds); + + await networkHelpers.mine(25); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey1, operatorIds, cluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + const removeBlock = BigInt(removeReceipt!.blockNumber); + + const expectedClusterIndex = calculateClusterIndex(snapshots, removeBlock, operatorFeeRaw); + const expectedNetworkFeeIndex = calculateNetworkFeeIndex(removeBlock, networkFeeIndexBlock, 0n); + const expectedFees = calculateSettledFees(cluster, expectedClusterIndex, expectedNetworkFeeIndex); + + expect(clusterAfterRemove.validatorCount).to.equal(1n); + expect(clusterAfterRemove.index).to.equal(expectedClusterIndex); + expect(clusterAfterRemove.networkFeeIndex).to.equal(expectedNetworkFeeIndex); + expect(clusterAfterRemove.balance).to.equal(cluster.balance - expectedFees); + expect(expectedFees).to.equal(expectedClusterIndex * BigInt(cluster.validatorCount) * DEDUCTED_DIGITS); + expect(expectedFees % DEDUCTED_DIGITS).to.equal(0n); + }); + + it("removeValidator settles legacy SSV fees identically when a pending ETH fee change request exists", async function () { + const operatorFeeRaw = 2_000n; + const { clusters, operatorIds, networkFeeIndexBlock } = + await networkHelpers.loadFixture(deployOperatorFeeFixture); + + const [publicKey1, publicKey2] = makePublicKeys(2, 21); + const cluster = createLegacySSVCluster({ validatorCount: 2n }); + + await clusters.mockRegisterSSVValidator(publicKey1, operatorIds, clusterOwner.address, cluster); + await clusters.mockRegisterSSVValidator(publicKey2, operatorIds, clusterOwner.address, cluster); + + // Inject a pending ETH fee change request on each operator (declared, within approval window) + const now = BigInt(await networkHelpers.time.latest()); + for (const operatorId of operatorIds) { + await clusters.mockSetOperatorFeeChangeRequest( + operatorId, + 99_999n, // large pending ETH fee — must NOT affect SSV settlement + now + 1n, // approvalBeginTime (in the future, so pending) + now + 86400n, // approvalEndTime + ); + } + + const snapshots = await captureSnapshots(clusters, operatorIds); + + await networkHelpers.mine(30); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey1, operatorIds, cluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + const removeBlock = BigInt(removeReceipt!.blockNumber); + + // Expected values use only the SSV fee — identical formula to the operator-fee-only test + const expectedClusterIndex = calculateClusterIndex(snapshots, removeBlock, operatorFeeRaw); + const expectedNetworkFeeIndex = calculateNetworkFeeIndex(removeBlock, networkFeeIndexBlock, 0n); + const expectedFees = calculateSettledFees(cluster, expectedClusterIndex, expectedNetworkFeeIndex); + + expect(clusterAfterRemove.validatorCount).to.equal(1n); + expect(clusterAfterRemove.index).to.equal(expectedClusterIndex); + expect(clusterAfterRemove.networkFeeIndex).to.equal(expectedNetworkFeeIndex); + expect(clusterAfterRemove.balance).to.equal(cluster.balance - expectedFees); + // The pending ETH fee (99_999) had zero effect — fees match the SSV-only formula exactly + expect(expectedFees).to.equal(expectedClusterIndex * BigInt(cluster.validatorCount) * DEDUCTED_DIGITS); + }); + + it("bulkRemoveValidator settles legacy SSV network fees on active clusters", async function () { + const networkFeeRaw = 75n; + const { clusters, operatorIds, networkFeeIndexBlock } = + await networkHelpers.loadFixture(deployNetworkFeeFixture); + + const publicKeys = makePublicKeys(3, 11); + const cluster = createLegacySSVCluster({ + validatorCount: 3n, + balance: ethers.parseEther("60"), + }); + + for (const publicKey of publicKeys) { + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + } + + const snapshots = await captureSnapshots(clusters, operatorIds); + + await networkHelpers.mine(40); + + const removeTx = await clusters.connect(clusterOwner).bulkRemoveValidator( + [publicKeys[0], publicKeys[1]], + operatorIds, + cluster + ); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + const removeBlock = BigInt(removeReceipt!.blockNumber); + + const expectedClusterIndex = calculateClusterIndex(snapshots, removeBlock, 0n); + const expectedNetworkFeeIndex = calculateNetworkFeeIndex(removeBlock, networkFeeIndexBlock, networkFeeRaw); + const expectedFees = calculateSettledFees(cluster, expectedClusterIndex, expectedNetworkFeeIndex); + + expect(expectedClusterIndex).to.equal(0n); + expect(clusterAfterRemove.validatorCount).to.equal(1n); + expect(clusterAfterRemove.index).to.equal(0n); + expect(clusterAfterRemove.networkFeeIndex).to.equal(expectedNetworkFeeIndex); + expect(clusterAfterRemove.balance).to.equal(cluster.balance - expectedFees); + expect(expectedFees).to.equal(expectedNetworkFeeIndex * BigInt(cluster.validatorCount) * DEDUCTED_DIGITS); + }); +}); diff --git a/test/unit/SSVClusters/liquidate.test.ts b/test/unit/SSVClusters/liquidate.test.ts new file mode 100644 index 000000000..c561943ee --- /dev/null +++ b/test/unit/SSVClusters/liquidate.test.ts @@ -0,0 +1,330 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { getClustersHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, createCluster, makePublicKey, parseClusterFromEvent, registerAndParseCluster, registerAndLiquidate, assertOperatorVUnits } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { expectETHDelta, expectETHDeltas, expectContractETHDelta } from "../../helpers/balance.ts"; +import { ethers } from "ethers"; + +describe("SSVClusters function `liquidate()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + let deployClustersWith7Operators!: ReturnType; + let deployClustersWith10Operators!: ReturnType; + let deployClustersWith13Operators!: ReturnType; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, otherAccount] } = await setupTestContext()); + + deployClustersWith7Operators = getClustersHarnessFixture(connection, 7); + deployClustersWith10Operators = getClustersHarnessFixture(connection, 10); + deployClustersWith13Operators = getClustersHarnessFixture(connection, 13); + }); + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return defaultClustersFixture(connection); + }; + + + it("Allows the cluster owner to liquidate and emits correct event", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await clusters.mockCurrentNetworkFeeIndex(2000n); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister); + const liquidateReceipt = await liquidateTx.wait(); + await trackGasFromReceipt(liquidateReceipt, [GasGroup.LIQUIDATE_CLUSTER_4]); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + expect(clusterAfterLiquidation.active).to.equal(false); + expect(clusterAfterLiquidation.balance).to.equal(0n); + }); + + it("Transfers remaining cluster ETH balance to the liquidator", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + const harnessAddress = await clusters.getAddress(); + const harnessBalance = await connection.ethers.provider.getBalance(harnessAddress); + const minCollateral = harnessBalance / ETH_DEDUCTED_DIGITS + 1n; + await clusters.mockMinimumLiquidationCollateral(minCollateral); + + await expectETHDeltas(connection.ethers.provider, + () => clusters.connect(otherAccount).liquidate(clusterOwner.address, operatorIds, clusterAfterRegister), + [ + { address: otherAccount.address, expectedDelta: DEFAULT_ETH_REGISTER_VALUE, accountForGas: true }, + { address: harnessAddress, expectedDelta: -DEFAULT_ETH_REGISTER_VALUE }, + ]); + }); + + it("Transfers no ETH when cluster remaining balance is zero after fee accrual", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + const drainFeeIndex = DEFAULT_ETH_REGISTER_VALUE / ETH_DEDUCTED_DIGITS; + await clusters.mockCurrentNetworkFeeIndex(drainFeeIndex); + + await expectContractETHDelta(connection.ethers.provider, await clusters.getAddress(), + () => clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister), + 0n); + }); + + it("Self-liquidation returns remaining ETH balance to the cluster owner", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await expectETHDelta(connection.ethers.provider, clusterOwner.address, + () => clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister), + DEFAULT_ETH_REGISTER_VALUE, { accountForGas: true }); + }); + + it("Updates operatorEthVUnits on liquidation even when cluster EB snapshot is not set", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await assertOperatorVUnits(clusters, operatorIds, 0n, BPS_DENOMINATOR); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister); + await liquidateTx.wait(); + + await assertOperatorVUnits(clusters, operatorIds, 0n, 0n); + }); + + it("Uses stored cluster EB snapshot vUnits when present when updating operatorEthVUnits on liquidation", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfter1 = await registerAndParseCluster(clusters, operatorIds); + + const registerTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfter1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt2 = await registerTx2.wait(); + const clusterAfter2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const registerTx3 = await clusters.registerValidator( + makePublicKey(3), + operatorIds, + DEFAULT_SHARES, + clusterAfter2, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt3 = await registerTx3.wait(); + const clusterAfter3 = parseClusterFromEvent(clusters, receipt3, Events.VALIDATOR_ADDED); + + await assertOperatorVUnits(clusters, operatorIds, 0n, 3n * BPS_DENOMINATOR); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const explicitVUnits = 5n * BPS_DENOMINATOR; + const baseline = 3n * BPS_DENOMINATOR; + const deviation = explicitVUnits - baseline; + await clusters.mockSetClusterVUnits(clusterId, explicitVUnits); + await clusters.mockSetDaoTotalEthVUnits(explicitVUnits); + for (const operatorId of operatorIds) { + await clusters.mockSetOperatorEthVUnits(operatorId, deviation); + } + + const beforeSnapshotVUnits = await clusters.getClusterVUnits(clusterId); + expect(beforeSnapshotVUnits).to.equal(explicitVUnits); + + await assertOperatorVUnits(clusters, operatorIds, deviation, explicitVUnits); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfter3); + await liquidateTx.wait(); + await assertOperatorVUnits(clusters, operatorIds, 0n, 0n); + + const afterSnapshotVUnits = await clusters.getClusterVUnits(clusterId); + expect(afterSnapshotVUnits).to.equal(explicitVUnits); + }); + + it("Allows the cluster owner to liquidate with 7 operators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await clusters.mockCurrentNetworkFeeIndex(2000n); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister); + const liquidateReceipt = await liquidateTx.wait(); + await trackGasFromReceipt(liquidateReceipt, [GasGroup.LIQUIDATE_CLUSTER_7]); + }); + + it("Allows the cluster owner to liquidate with 10 operators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await clusters.mockCurrentNetworkFeeIndex(2000n); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister); + const liquidateReceipt = await liquidateTx.wait(); + await trackGasFromReceipt(liquidateReceipt, [GasGroup.LIQUIDATE_CLUSTER_10]); + }); + + it("Allows the cluster owner to liquidate with 13 operators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await clusters.mockCurrentNetworkFeeIndex(2000n); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister); + const liquidateReceipt = await liquidateTx.wait(); + await trackGasFromReceipt(liquidateReceipt, [GasGroup.LIQUIDATE_CLUSTER_13]); + }); + + it("Allows a third party to liquidate when the cluster is liquidatable", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockCurrentNetworkFeeIndex(1000n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await clusters.mockCurrentNetworkFeeIndex(2000n); + await clusters.mockMinimumLiquidationCollateral(DEFAULT_ETH_REGISTER_VALUE + 1n); + + const liquidateTx = await clusters.connect(otherAccount).liquidate( + clusterOwner.address, + operatorIds, + clusterAfterRegister + ); + const liquidateReceipt = await liquidateTx.wait(); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + expect(clusterAfterLiquidation.active).to.equal(false); + expect(clusterAfterLiquidation.balance).to.equal(0n); + }); + + it("Allows a third party to liquidate when liquidation threshold units exceed uint64", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + const maxUint64 = (1n << 64n) - 1n; + const rate = 1n << 20n; + const minimumBlocksBeforeLiquidation = (1n << 44n) + 1n; + const thresholdUnits = minimumBlocksBeforeLiquidation * rate; + const wrappedThresholdUnits = thresholdUnits & maxUint64; + + expect(thresholdUnits).to.be.greaterThan(maxUint64); + expect(wrappedThresholdUnits * ETH_DEDUCTED_DIGITS).to.be.lessThan(clusterAfterRegister.balance); + + await clusters.mockMinimumLiquidationCollateral(0n); + await clusters.mockEthNetworkFee(rate); + await clusters.mockMinimumBlocksBeforeLiquidation(minimumBlocksBeforeLiquidation); + + const liquidateTx = await clusters.connect(otherAccount).liquidate( + clusterOwner.address, + operatorIds, + clusterAfterRegister + ); + const liquidateReceipt = await liquidateTx.wait(); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + expect(clusterAfterLiquidation.active).to.equal(false); + expect(clusterAfterLiquidation.balance).to.equal(0n); + }); + + it("Is reverted with 'ClusterNotLiquidatable' when a third party tries to liquidate a healthy cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await expect(clusters.connect(otherAccount).liquidate( + clusterOwner.address, + operatorIds, + clusterAfterRegister + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + }); + + it("Is reverted with 'ClusterIsLiquidated' when liquidating an already liquidated cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const { clusterAfterLiquidation } = await registerAndLiquidate(clusters, clusterOwner.address, operatorIds); + + await expect(clusters.liquidate( + clusterOwner.address, + operatorIds, + clusterAfterLiquidation + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_IS_LIQUIDATED); + }); + + it("Is reverted with 'IncorrectClusterState' when provided cluster data is stale or mismatched", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + const mismatchedCluster = { + ...clusterAfterRegister, + balance: clusterAfterRegister.balance + 1n, + }; + + await expect(clusters.liquidate( + clusterOwner.address, + operatorIds, + mismatchedCluster + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterDoesNotExists' when attempting to liquidate a missing cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await expect(clusters.liquidate( + clusterOwner.address, + operatorIds, + createCluster() + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_DOES_NOT_EXIST); + }); +}); diff --git a/test/unit/SSVClusters/liquidateSSV.test.ts b/test/unit/SSVClusters/liquidateSSV.test.ts new file mode 100644 index 000000000..772a8058d --- /dev/null +++ b/test/unit/SSVClusters/liquidateSSV.test.ts @@ -0,0 +1,351 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { getClustersHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, createCluster, makePublicKey } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, EMPTY_CLUSTER, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { ethers } from "ethers"; + +type ClusterType = typeof EMPTY_CLUSTER; + +const createSSVCluster = (overrides: Partial = {}): ClusterType => ({ + ...EMPTY_CLUSTER, + validatorCount: 1n, + active: true, + balance: 10_000_000_000_000_000_000n, + ...overrides, +}); + +describe("SSVClusters function `liquidateSSV()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + let deployClustersWith7Operators!: ReturnType; + let deployClustersWith10Operators!: ReturnType; + let deployClustersWith13Operators!: ReturnType; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, otherAccount] } = await setupTestContext()); + + deployClustersWith7Operators = getClustersHarnessFixture(connection, 7); + deployClustersWith10Operators = getClustersHarnessFixture(connection, 10); + deployClustersWith13Operators = getClustersHarnessFixture(connection, 13); + }); + + const setupSSVClustersFixture = async (fixture: { clusters: any, operatorIds: bigint[] }) => { + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + + const { clusters } = fixture; + + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + + await mockToken.mint(harnessAddress, connection.ethers.parseEther("1000")); + await clusters.mockSetToken(tokenAddress); + + await networkHelpers.setBalance(harnessAddress, connection.ethers.parseEther("1000")); + + return { ...fixture, mockToken }; + }; + + + const deploySSVClustersFixture = async () => { + const fixture = await defaultClustersFixture(connection); + return setupSSVClustersFixture(fixture); + }; + + const deploySSVClustersWith7OperatorsFixture = async () => { + const fixture = await deployClustersWith7Operators(); + return setupSSVClustersFixture(fixture); + }; + + const deploySSVClustersWith10OperatorsFixture = async () => { + const fixture = await deployClustersWith10Operators(); + return setupSSVClustersFixture(fixture); + }; + + const deploySSVClustersWith13OperatorsFixture = async () => { + const fixture = await deployClustersWith13Operators(); + return setupSSVClustersFixture(fixture); + }; + + const createSSVClusterWithTokenBalance = (balance: bigint, overrides: Partial = {}): ClusterType => ({ + ...EMPTY_CLUSTER, + validatorCount: 1n, + active: true, + balance, + ...overrides, + }); + + it("Allows the cluster owner to liquidate SSV cluster and emits correct event", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster({ networkFeeIndex: 1000n }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(2000n); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + const receipt = await liquidateTx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.LIQUIDATE_CLUSTER_SSV_4]); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + }); + + it("Transfers remaining SSV token balance in the cluster to the liquidator", async function () { + const { clusters, operatorIds, mockToken } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const clusterBalance = connection.ethers.parseEther("1"); + const currentNetworkFeeIndexSSV = 2000n; + const cluster = createSSVClusterWithTokenBalance(clusterBalance, { networkFeeIndex: currentNetworkFeeIndexSSV }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(currentNetworkFeeIndexSSV); + + const minCollateral = clusterBalance / 10_000_000n + 1n; + await clusters.mockMinimumLiquidationCollateralSSV(minCollateral); + + const liquidatorBalanceBefore = await mockToken.balanceOf(otherAccount.address); + const harnessBalanceBefore = await mockToken.balanceOf(await clusters.getAddress()); + expect(harnessBalanceBefore).to.be.greaterThanOrEqual(clusterBalance); + + await clusters.connect(otherAccount).liquidateSSV(clusterOwner.address, operatorIds, cluster); + + const liquidatorBalanceAfter = await mockToken.balanceOf(otherAccount.address); + const harnessBalanceAfter = await mockToken.balanceOf(await clusters.getAddress()); + + expect(liquidatorBalanceAfter - liquidatorBalanceBefore).to.equal(clusterBalance); + expect(harnessBalanceBefore - harnessBalanceAfter).to.equal(clusterBalance); + }); + + it("Transfers no SSV when cluster remaining balance is zero after fee accrual", async function () { + const { clusters, operatorIds, mockToken } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const clusterBalance = 1_000_000_000n; + const cluster = createSSVClusterWithTokenBalance(clusterBalance); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(clusterBalance); + + const liquidatorTokenBefore = await mockToken.balanceOf(clusterOwner.address); + + await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + + const liquidatorTokenAfter = await mockToken.balanceOf(clusterOwner.address); + + expect(liquidatorTokenAfter).to.equal(liquidatorTokenBefore); + }); + + it("SSV self-liquidation returns remaining SSV balance to the cluster owner", async function () { + const { clusters, operatorIds, mockToken } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const clusterBalance = connection.ethers.parseEther("1"); + const currentSSVFeeIndex = 2000n; + const cluster = createSSVClusterWithTokenBalance(clusterBalance, { networkFeeIndex: currentSSVFeeIndex }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(currentSSVFeeIndex); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + + await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + + expect(ownerTokenAfter - ownerTokenBefore).to.equal(clusterBalance); + }); + + it("Does not change operatorEthVUnits or stored cluster EB snapshot when liquidating an SSV cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + await clusters.connect(otherAccount).registerValidator( + makePublicKey(999), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await clusters.mockSetClusterVUnits(clusterId, 7n * BPS_DENOMINATOR); + + const beforeClusterVUnits = await clusters.getClusterVUnits(clusterId); + const beforeOperatorVUnits = await Promise.all(operatorIds.map((id) => clusters.getOperatorEthVUnits(id))); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster({ networkFeeIndex: 1000n }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(2000n); + + await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + + const afterClusterVUnits = await clusters.getClusterVUnits(clusterId); + const afterOperatorVUnits = await Promise.all(operatorIds.map((id) => clusters.getOperatorEthVUnits(id))); + + expect(afterClusterVUnits).to.equal(beforeClusterVUnits); + expect(afterOperatorVUnits).to.deep.equal(beforeOperatorVUnits); + }); + + it("Allows the cluster owner to liquidate SSV cluster with 7 operators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersWith7OperatorsFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster({ networkFeeIndex: 1000n }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(2000n); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + const receipt = await liquidateTx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.LIQUIDATE_CLUSTER_SSV_7]); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + }); + + it("Allows the cluster owner to liquidate SSV cluster with 10 operators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersWith10OperatorsFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster({ networkFeeIndex: 1000n }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(2000n); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + const receipt = await liquidateTx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.LIQUIDATE_CLUSTER_SSV_10]); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + }); + + it("Allows the cluster owner to liquidate SSV cluster with 13 operators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersWith13OperatorsFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster({ networkFeeIndex: 1000n }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(100n); + await clusters.mockCurrentNetworkFeeIndexSSV(2000n); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + const receipt = await liquidateTx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.LIQUIDATE_CLUSTER_SSV_13]); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + }); + + it("Allows a third party to liquidate SSV cluster when liquidatable", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster({ networkFeeIndex: 500000n, balance: 1n }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + await clusters.mockCurrentNetworkFeeIndex(1000n); + await clusters.mockCurrentNetworkFeeIndexSSV(600000n); + await clusters.mockMinimumLiquidationCollateralSSV(1000n); + + const liquidateTx = await clusters.connect(otherAccount).liquidateSSV( + clusterOwner.address, + operatorIds, + cluster + ); + + await expect(liquidateTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + }); + + it("Is reverted with 'ClusterNotLiquidatable' when a third party tries to liquidate a healthy SSV cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster(); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + + await expect(clusters.connect(otherAccount).liquidateSSV( + clusterOwner.address, + operatorIds, + cluster + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + }); + + it("Is reverted with 'ClusterIsLiquidated' when liquidating an already liquidated SSV cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster(); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + + await clusters.liquidateSSV(clusterOwner.address, operatorIds, cluster); + + const liquidatedCluster = { ...cluster, active: false, balance: 0n, index: 0n, networkFeeIndex: 0n }; + + await expect(clusters.liquidateSSV( + clusterOwner.address, + operatorIds, + liquidatedCluster + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_IS_LIQUIDATED); + }); + + it("Is reverted with 'IncorrectClusterState' when provided cluster data is stale or mismatched", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + const publicKey = makePublicKey(1); + const cluster = createSSVCluster(); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, cluster); + + const mismatchedCluster = { ...cluster, balance: cluster.balance + 1n }; + + await expect(clusters.liquidateSSV( + clusterOwner.address, + operatorIds, + mismatchedCluster + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterDoesNotExists' when attempting to liquidate a missing SSV cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersFixture); + + await expect(clusters.liquidateSSV( + clusterOwner.address, + operatorIds, + createSSVCluster() + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_DOES_NOT_EXIST); + }); +}); diff --git a/test/unit/SSVClusters/migrateClusterToETH.test.ts b/test/unit/SSVClusters/migrateClusterToETH.test.ts new file mode 100644 index 000000000..1cf5c74e4 --- /dev/null +++ b/test/unit/SSVClusters/migrateClusterToETH.test.ts @@ -0,0 +1,1510 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, computeEBRoot, extractEventArgs, getCurrentClusterState, makePublicKey, parseClusterFromEvent } from '../../common/helpers.ts'; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_OPERATOR_ETH_FEE, DEFAULT_SHARES, EMPTY_CLUSTER, BPS_DENOMINATOR, DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { ethers } from "ethers"; + +describe("SSVClusters function `migrateClusterToETH()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let anotherOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, anotherOwner] } = await setupTestContext()); + }); + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return defaultClustersFixture(connection); + }; + + + it("Migrates an existing SSV cluster to ETH and emits the expected event", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.MIGRATE_CLUSTER_TO_ETH]); + const clusterAfterMigration = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + await expect(migrateTx).to.emit(clusters, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfterMigration.active).to.equal(true); + expect(clusterAfterMigration.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(clusterAfterMigration.validatorCount).to.equal(ssvCluster.validatorCount); + + expect(eventArgs.ethDeposited).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(eventArgs.ssvRefunded).to.equal(0n); + expect(eventArgs.effectiveBalance).to.equal(32); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await clusters.getClusterHash(clusterId)).to.not.equal(ethers.ZeroHash); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthValidatorCount(operatorId)).to.equal(1n); + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(BPS_DENOMINATOR); + } + + await expect(clusters.migrateClusterToETH( + operatorIds, + clusterAfterMigration + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Emits OperatorFeeExecuted for each legacy SSV operator when migrating to ETH", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + for (const operatorId of operatorIds) { + await clusters.mockSetOperatorLegacySSV(operatorId, 1); + } + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(2); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const expectedBlock = BigInt(receipt!.blockNumber); + + for (const operatorId of operatorIds) { + await expect(migrateTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED) + .withArgs(clusterOwner.address, operatorId, expectedBlock, DEFAULT_OPERATOR_ETH_FEE); + } + }); + + it("Does not emit duplicate OperatorFeeExecuted when operator already initialized with ETH defaults", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + // Set operators as legacy SSV operators (will receive default ETH fee on first ETH operation) + for (const operatorId of operatorIds) { + await clusters.mockSetOperatorLegacySSV(operatorId, 1); + } + + // First ETH operation: registerValidator triggers ensureETHDefaults, emits OperatorFeeExecuted + const firstTx = await clusters.registerValidator( + makePublicKey(100), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const firstReceipt = await firstTx.wait(); + + // Verify OperatorFeeExecuted IS emitted on first ETH operation + for (const operatorId of operatorIds) { + await expect(firstTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED) + .withArgs(clusterOwner.address, operatorId, BigInt(firstReceipt!.blockNumber), DEFAULT_OPERATOR_ETH_FEE); + } + + // Verify operators are now ETH-initialized + for (const operatorId of operatorIds) { + const ethSnapshot = await clusters.getOperatorEthSnapshot(operatorId); + expect(ethSnapshot.blockNumber).to.be.greaterThan(0); + // getOperatorEthFee returns packed value, so we expect DEFAULT_OPERATOR_ETH_FEE / 100_000 + const ethFee = await clusters.getOperatorEthFee(operatorId); + expect(ethFee).to.equal(DEFAULT_OPERATOR_ETH_FEE / 100_000n); + } + + // Second ETH operation: registerValidator again, ensureETHDefaults should NOT emit event + const cluster1 = parseClusterFromEvent(clusters, firstReceipt, Events.VALIDATOR_ADDED); + + const secondTx = await clusters.registerValidator( + makePublicKey(200), + operatorIds, + DEFAULT_SHARES, + cluster1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const secondReceipt = await secondTx.wait(); + + // Verify OperatorFeeExecuted is NOT emitted on second ETH operation (idempotency) + const feeExecutedEvents = secondReceipt?.logs + .map(log => { + try { + return clusters.interface.parseLog(log); + } catch { + return null; + } + }) + .filter(parsed => parsed?.name === Events.OPERATOR_FEE_EXECUTED); + + expect(feeExecutedEvents).to.have.length(0); + + // Verify second registration still succeeded + await expect(secondTx).to.emit(clusters, Events.VALIDATOR_ADDED); + const clusterAfter = parseClusterFromEvent(clusters, secondReceipt, Events.VALIDATOR_ADDED); + expect(clusterAfter.active).to.equal(true); + expect(clusterAfter.validatorCount).to.equal(2n); + }); + + it("Refunds SSV token balance to the owner when migrating an active SSV cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const ssvBalance = connection.ethers.parseEther("1"); + await mockToken.mint(harnessAddress, ssvBalance); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: ssvBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenBefore = await mockToken.balanceOf(harnessAddress); + expect(harnessTokenBefore).to.be.greaterThanOrEqual(ssvBalance); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + expect(eventArgs.ethDeposited).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(eventArgs.ssvRefunded).to.equal(ssvBalance); + expect(eventArgs.effectiveBalance).to.equal(32); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(ssvBalance); + expect(harnessTokenBefore - harnessTokenAfter).to.equal(ssvBalance); + }); + + it("Uses stored EB snapshot vUnits during migration when present", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await clusters.mockSetClusterVUnits(clusterId, 12_000n); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + expect(eventArgs.effectiveBalance).to.equal(38); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(2_000n); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(12_000n); + } + }); + + it("Is reverted with 'InsufficientBalance' when ETH top-up is too low", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + await clusters.mockMinimumLiquidationCollateral(DEFAULT_ETH_REGISTER_VALUE + 1n); + + await expect(clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + }); + + it("Is reverted with 'IncorrectClusterVersion' when migrating an ETH cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const registerTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + { ...EMPTY_CLUSTER }, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const ethCluster = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + await expect(clusters.migrateClusterToETH( + operatorIds, + ethCluster + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Is reverted with 'ClusterDoesNotExists' when migrating a missing cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await expect(clusters.migrateClusterToETH( + operatorIds, + { ...EMPTY_CLUSTER } + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Validates full migration accounting correctness from SSV cluster to ETH cluster after time passes", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + await clusters.mockMinimumLiquidationCollateral(1000000n); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + const validatorCount = 10n; + const ssvBalance = connection.ethers.parseEther("5"); + await mockToken.mint(harnessAddress, ssvBalance); + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: ssvBalance, + active: true, + }; + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + const ssvNetworkFee = 5n; + await clusters.mockSSVNetworkFee(ssvNetworkFee); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + const operatorSSVFee = DEDUCTED_DIGITS * 3n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + } + const ethNetworkFee = 1770n; + await clusters.mockEthNetworkFee(ethNetworkFee); + await clusters.mockCurrentNetworkFeeIndex(0n); + const operatorSnapshots = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorSnapshot(opId); + const fee = await clusters.getOperatorSSVFee(opId); + operatorSnapshots.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + const networkFeeIndexBefore = await clusters.getCurrentNetworkFeeIndexSSV(); + const readBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + const blocksToMine = 100; + await networkHelpers.mine(blocksToMine); + const ownerSSVBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessSSVBefore = await mockToken.balanceOf(harnessAddress); + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const migrationBlock = BigInt(receipt!.blockNumber); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + await expect(migrateTx).to.emit(clusters, Events.CLUSTER_MIGRATED_TO_ETH); + + expect(eventArgs.ethDeposited).to.equal(DEFAULT_ETH_REGISTER_VALUE); + const blocksElapsed = migrationBlock - readBlock; + let expectedCumulativeIndex = 0n; + for (const snap of operatorSnapshots) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeIndex += snap.index + blockDiff * snap.fee; + } + const expectedNetworkFeeIndex = networkFeeIndexBefore + blocksElapsed * ssvNetworkFee; + const operatorUsagePacked = (expectedCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + const totalUnpackedUsage = (operatorUsagePacked + networkUsagePacked) * DEDUCTED_DIGITS; + const expectedRefund = ssvBalance > totalUnpackedUsage ? ssvBalance - totalUnpackedUsage : 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + const ownerSSVAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessSSVAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerSSVAfter - ownerSSVBefore).to.equal(expectedRefund); + expect(harnessSSVBefore - harnessSSVAfter).to.equal(expectedRefund); + const ethCluster = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(ethCluster.active).to.equal(true); + expect(ethCluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(ethCluster.validatorCount).to.equal(validatorCount); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await clusters.getClusterHash(clusterId)).to.not.equal(ethers.ZeroHash); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthValidatorCount(operatorId)).to.equal(validatorCount); + } + }); + + it("Correctly updates SSV snapshot and settles fees for already-ETH operators during migration", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const dummyPublicKey = makePublicKey(999); + await clusters.connect(anotherOwner).registerValidator( + dummyPublicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const ssvNetworkFee = 1000000n; + await clusters.mockSSVNetworkFee(ssvNetworkFee); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const ethNetworkFee = 1770n; + await clusters.mockEthNetworkFee(ethNetworkFee); + await clusters.mockCurrentNetworkFeeIndex(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const ssvBalance = connection.ethers.parseEther("10"); + await mockToken.mint(harnessAddress, ssvBalance); + + const validatorCount = 4n; + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0, + index: 0, + balance: ssvBalance, + active: true, + }; + + const ssvPublicKey = makePublicKey(1000); + await clusters.mockRegisterSSVValidator(ssvPublicKey, operatorIds, clusterOwner.address, ssvCluster); + const opSnapshotsBefore = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorSnapshot(opId); + const fee = await clusters.getOperatorSSVFee(opId); + opSnapshotsBefore.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + const networkFeeIndexSSVBefore = await clusters.getCurrentNetworkFeeIndexSSV(); + const readBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + + const blocksToMine = 750; + await networkHelpers.mine(blocksToMine); + + const ownerSSVBefore = await mockToken.balanceOf(clusterOwner.address); + + const migrateTx = await clusters.connect(clusterOwner).migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const migrationBlock = BigInt(receipt!.blockNumber); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + await expect(migrateTx).to.emit(clusters, Events.CLUSTER_MIGRATED_TO_ETH); + expect(eventArgs.ethDeposited).to.equal(DEFAULT_ETH_REGISTER_VALUE); + let expectedCumulativeIndex = 0n; + for (const snap of opSnapshotsBefore) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeIndex += snap.index + blockDiff * snap.fee; + } + const blocksElapsed = migrationBlock - readBlock; + const expectedNetworkFeeIndex = networkFeeIndexSSVBefore + blocksElapsed * ssvNetworkFee; + const opUsagePacked = (expectedCumulativeIndex - BigInt(ssvCluster.index)) * validatorCount; + const netUsagePacked = (expectedNetworkFeeIndex - BigInt(ssvCluster.networkFeeIndex)) * validatorCount; + const totalUnpackedUsage = (opUsagePacked + netUsagePacked) * DEDUCTED_DIGITS; + const expectedRefund = ssvBalance > totalUnpackedUsage ? ssvBalance - totalUnpackedUsage : 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + + const ownerSSVAfter = await mockToken.balanceOf(clusterOwner.address); + expect(ownerSSVAfter - ownerSSVBefore).to.equal(expectedRefund); + }); + + describe("updateClusterOperatorsMigration specific tests", async function () { + it("Preserves SSV snapshot state before validator count reduction", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const ssvNetworkFee = 1000000n; + await clusters.mockSSVNetworkFee(ssvNetworkFee); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + const validatorCount = 5n; + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + await networkHelpers.mine(50); + const operatorStatesBefore = []; + for (const operatorId of operatorIds) { + const snapshot = await clusters.getOperatorSnapshot(operatorId); + const validatorCount = await clusters.getOperatorValidatorCount(operatorId); + operatorStatesBefore.push({ + operatorId, + snapshotIndex: snapshot.index, + validatorCount: validatorCount + }); + } + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await migrateTx.wait(); + for (let i = 0; i < operatorIds.length; i++) { + const stateBefore = operatorStatesBefore[i]; + const snapshotAfter = await clusters.getOperatorSnapshot(stateBefore.operatorId); + expect(snapshotAfter.index).to.be.greaterThanOrEqual(stateBefore.snapshotIndex); + const ssvValidatorCountAfter = await clusters.getOperatorValidatorCount(stateBefore.operatorId); + expect(ssvValidatorCountAfter).to.equal(stateBefore.validatorCount - validatorCount); + } + }); + + it("Correctly handles mixed operator states during migration", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const ethPublicKey = makePublicKey(100); + await clusters.connect(anotherOwner).registerValidator( + ethPublicKey, + operatorIds.slice(0, 4), + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const ssvNetworkFee = 1000000n; + await clusters.mockSSVNetworkFee(ssvNetworkFee); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + const validatorCount = 3n; + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const ssvPublicKey = makePublicKey(200); + await clusters.mockRegisterSSVValidator(ssvPublicKey, operatorIds, clusterOwner.address, ssvCluster); + await networkHelpers.mine(25); + const mixedStatesBefore = []; + for (const operatorId of operatorIds) { + const ethSnapshot = await clusters.getOperatorEthSnapshot(operatorId); + const ssvSnapshot = await clusters.getOperatorSnapshot(operatorId); + const ethValidatorCount = await clusters.getOperatorEthValidatorCount(operatorId); + const ssvValidatorCount = await clusters.getOperatorValidatorCount(operatorId); + + mixedStatesBefore.push({ + operatorId, + wasEthOperator: ethSnapshot.blockNumber > 0, + ethValidatorCount: ethValidatorCount || 0n, + ssvValidatorCount: ssvValidatorCount || 0n, + ssvIndex: ssvSnapshot.index, + ethIndex: ethSnapshot.index + }); + } + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await migrateTx.wait(); + for (let i = 0; i < operatorIds.length; i++) { + const stateBefore = mixedStatesBefore[i]; + const ssvSnapshotAfter = await clusters.getOperatorSnapshot(stateBefore.operatorId); + const ethSnapshotAfter = await clusters.getOperatorEthSnapshot(stateBefore.operatorId); + expect(ssvSnapshotAfter.index).to.be.greaterThanOrEqual(stateBefore.ssvIndex); + if (stateBefore.wasEthOperator) { + if (stateBefore.ethIndex > 0) { + expect(ethSnapshotAfter.index).to.be.greaterThan(stateBefore.ethIndex); + } + const ethValidatorCountAfter = await clusters.getOperatorEthValidatorCount(stateBefore.operatorId); + expect(ethValidatorCountAfter).to.equal(stateBefore.ethValidatorCount + validatorCount); + } else { + const ethSnapshotAfterBlock = ethSnapshotAfter.blockNumber || 0; + expect(ethSnapshotAfterBlock).to.be.greaterThanOrEqual(0); + const ethValidatorCountAfter = await clusters.getOperatorEthValidatorCount(stateBefore.operatorId); + if (stateBefore.ethValidatorCount === 0n) { + expect(ethValidatorCountAfter).to.equal(validatorCount); + } else { + expect(ethValidatorCountAfter).to.equal(stateBefore.ethValidatorCount + validatorCount); + } + } + const ssvValidatorCountAfter = await clusters.getOperatorValidatorCount(stateBefore.operatorId); + expect(ssvValidatorCountAfter).to.equal(stateBefore.ssvValidatorCount - validatorCount); + } + }); + + it("Accumulates SSV indices correctly for all operators during migration", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const ssvNetworkFee = 2000000n; + await clusters.mockSSVNetworkFee(ssvNetworkFee); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + const validatorCount = 2n; + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + await networkHelpers.mine(100); + const indicesBefore = []; + for (const operatorId of operatorIds) { + const snapshot = await clusters.getOperatorSnapshot(operatorId); + indicesBefore.push(snapshot.index); + } + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await migrateTx.wait(); + for (let i = 0; i < operatorIds.length; i++) { + const snapshotAfter = await clusters.getOperatorSnapshot(operatorIds[i]); + expect(snapshotAfter.index).to.be.greaterThanOrEqual(indicesBefore[i]); + } + expect(migrateTx).to.not.be.null; + }); + + it("Handles liquidated cluster migration correctly", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const validatorCount = 3n; + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, ssvCluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.be.false; + const validatorCountsBefore = []; + for (const operatorId of operatorIds) { + const ssvCount = await clusters.getOperatorValidatorCount(operatorId); + const ethCount = await clusters.getOperatorEthValidatorCount(operatorId); + validatorCountsBefore.push({ ssvCount, ethCount }); + } + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await migrateTx.wait(); + for (let i = 0; i < operatorIds.length; i++) { + const countsBefore = validatorCountsBefore[i]; + const ssvCountAfter = await clusters.getOperatorValidatorCount(operatorIds[i]); + const ethCountAfter = await clusters.getOperatorEthValidatorCount(operatorIds[i]); + expect(ssvCountAfter).to.equal(countsBefore.ssvCount); + expect(ethCountAfter).to.equal(liquidatedCluster.validatorCount); + } + }); + }); + + describe("Migration balance accounting verification", async function () { + it("Exact SSV refund after 1000 blocks — independently calculated", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const operatorSSVFee = DEDUCTED_DIGITS * 5n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + } + + const ssvNetworkFeeRaw = 3n; + await clusters.mockSSVNetworkFee(ssvNetworkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const validatorCount = 1n; + const initialBalance = connection.ethers.parseEther("100"); + await mockToken.mint(harnessAddress, initialBalance); + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: initialBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const operatorSnapshots = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorSnapshot(opId); + const fee = await clusters.getOperatorSSVFee(opId); + operatorSnapshots.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + const networkFeeIndexBefore = await clusters.getCurrentNetworkFeeIndexSSV(); + const readBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + + await networkHelpers.mine(1000); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenBefore = await mockToken.balanceOf(harnessAddress); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const migrationBlock = BigInt(receipt!.blockNumber); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + const blocksElapsed = migrationBlock - readBlock; + + let expectedCumulativeIndex = 0n; + for (const snap of operatorSnapshots) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeIndex += snap.index + blockDiff * snap.fee; + } + + const expectedNetworkFeeIndex = networkFeeIndexBefore + blocksElapsed * ssvNetworkFeeRaw; + + const operatorUsagePacked = (expectedCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + const totalPackedUsage = operatorUsagePacked + networkUsagePacked; + const totalUnpackedUsage = totalPackedUsage * DEDUCTED_DIGITS; + + const expectedRefund = initialBalance > totalUnpackedUsage + ? initialBalance - totalUnpackedUsage + : 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(expectedRefund); + expect(harnessTokenBefore - harnessTokenAfter).to.equal(expectedRefund); + const feesCharged = initialBalance - expectedRefund; + expect(feesCharged).to.equal(totalUnpackedUsage); + }); + + it("Migration with partial SSV balance remaining — exact token transfer", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const operatorSSVFee = DEDUCTED_DIGITS * 20n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + } + + const ssvNetworkFeeRaw = 10n; + await clusters.mockSSVNetworkFee(ssvNetworkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const validatorCount = 4n; + const initialBalance = connection.ethers.parseEther("5"); + await mockToken.mint(harnessAddress, initialBalance); + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: initialBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const operatorSnapshots = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorSnapshot(opId); + const fee = await clusters.getOperatorSSVFee(opId); + operatorSnapshots.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + const networkFeeIndexBefore = await clusters.getCurrentNetworkFeeIndexSSV(); + const readBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + + await networkHelpers.mine(500); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenBefore = await mockToken.balanceOf(harnessAddress); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const migrationBlock = BigInt(receipt!.blockNumber); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + const blocksElapsed = migrationBlock - readBlock; + + let expectedCumulativeIndex = 0n; + for (const snap of operatorSnapshots) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeIndex += snap.index + blockDiff * snap.fee; + } + + const expectedNetworkFeeIndex = networkFeeIndexBefore + blocksElapsed * ssvNetworkFeeRaw; + + const operatorUsagePacked = (expectedCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + const totalPackedUsage = operatorUsagePacked + networkUsagePacked; + const totalUnpackedUsage = totalPackedUsage * DEDUCTED_DIGITS; + + const expectedRefund = initialBalance > totalUnpackedUsage + ? initialBalance - totalUnpackedUsage + : 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(expectedRefund); + expect(harnessTokenBefore - harnessTokenAfter).to.equal(expectedRefund); + const feesCharged = initialBalance - expectedRefund; + expect(feesCharged).to.equal(totalUnpackedUsage); + }); + + it("Migration with dual SSV/ETH fees — ETH side correctly initialized", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const operatorSSVFee = DEDUCTED_DIGITS * 3n; + const operatorETHFee = 1_770_000_000n; + + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + await clusters.mockSetOperatorFee(opId, operatorETHFee); + } + + const ssvNetworkFeeRaw = 2n; + await clusters.mockSSVNetworkFee(ssvNetworkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + await clusters.mockCurrentNetworkFeeIndex(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const validatorCount = 2n; + const initialBalance = connection.ethers.parseEther("50"); + await mockToken.mint(harnessAddress, initialBalance); + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: initialBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const ssvSnapshots = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorSnapshot(opId); + const fee = await clusters.getOperatorSSVFee(opId); + ssvSnapshots.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + + const ethSnapshots = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorEthSnapshot(opId); + const fee = await clusters.getOperatorEthFee(opId); + ethSnapshots.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + + const networkFeeIndexSSVBefore = await clusters.getCurrentNetworkFeeIndexSSV(); + const readBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + + await networkHelpers.mine(200); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const migrationBlock = BigInt(receipt!.blockNumber); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + const ethCluster = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + const blocksElapsed = migrationBlock - readBlock; + + let expectedCumulativeSSVIndex = 0n; + for (const snap of ssvSnapshots) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeSSVIndex += snap.index + blockDiff * snap.fee; + } + + const expectedNetworkFeeIndexSSV = networkFeeIndexSSVBefore + blocksElapsed * ssvNetworkFeeRaw; + + const operatorUsagePacked = (expectedCumulativeSSVIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndexSSV - ssvCluster.networkFeeIndex) * validatorCount; + const totalPackedUsage = operatorUsagePacked + networkUsagePacked; + const totalUnpackedUsage = totalPackedUsage * DEDUCTED_DIGITS; + + const expectedRefund = initialBalance > totalUnpackedUsage + ? initialBalance - totalUnpackedUsage + : 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(expectedRefund); + + expect(ethCluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(ethCluster.active).to.equal(true); + expect(ethCluster.validatorCount).to.equal(validatorCount); + + let expectedCumulativeETHIndex = 0n; + for (const snap of ethSnapshots) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeETHIndex += snap.index + blockDiff * snap.fee; + } + + expect(ethCluster.index).to.equal(expectedCumulativeETHIndex); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthValidatorCount(operatorId)).to.equal(validatorCount); + } + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorValidatorCount(operatorId)).to.equal(0); + } + }); + + it("Zero SSV balance migration — exact refund calculation", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const operatorSSVFee = DEDUCTED_DIGITS * 2n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + } + + const ssvNetworkFeeRaw = 1n; + await clusters.mockSSVNetworkFee(ssvNetworkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + const validatorCount = 2n; + const initialBalance = 0n; + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: initialBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenBefore = await mockToken.balanceOf(harnessAddress); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + const expectedRefund = 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(expectedRefund); + expect(harnessTokenBefore - harnessTokenAfter).to.equal(expectedRefund); + const ethCluster = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(ethCluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(ethCluster.active).to.equal(true); + expect(ethCluster.validatorCount).to.equal(validatorCount); + }); + + it("Liquidated cluster migration — exact zero refund verification", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const operatorSSVFee = DEDUCTED_DIGITS * 10n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + } + + const ssvNetworkFeeRaw = 5n; + await clusters.mockSSVNetworkFee(ssvNetworkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const validatorCount = 3n; + const initialBalance = connection.ethers.parseEther("1"); + await mockToken.mint(harnessAddress, initialBalance); + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: initialBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, ssvCluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.be.false; + expect(liquidatedCluster.balance).to.equal(0n); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenBefore = await mockToken.balanceOf(harnessAddress); + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + const expectedRefund = 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(expectedRefund); + expect(harnessTokenBefore - harnessTokenAfter).to.equal(expectedRefund); + const ethCluster = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(ethCluster.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + expect(ethCluster.active).to.equal(true); + expect(ethCluster.validatorCount).to.equal(validatorCount); + }); + + it("Maximum precision SSV balance — exact refund with non-round values", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const operatorSSVFee = DEDUCTED_DIGITS * 7n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + } + + const ssvNetworkFeeRaw = 11n; + await clusters.mockSSVNetworkFee(ssvNetworkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const validatorCount = 7n; + const initialBalance = 123_456_780_000_000_000n; + + await mockToken.mint(harnessAddress, initialBalance); + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: initialBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const operatorSnapshots = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorSnapshot(opId); + const fee = await clusters.getOperatorSSVFee(opId); + operatorSnapshots.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + const networkFeeIndexBefore = await clusters.getCurrentNetworkFeeIndexSSV(); + const readBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + + await networkHelpers.mine(317); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenBefore = await mockToken.balanceOf(harnessAddress); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const migrationBlock = BigInt(receipt!.blockNumber); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + const blocksElapsed = migrationBlock - readBlock; + let expectedCumulativeIndex = 0n; + for (const snap of operatorSnapshots) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeIndex += snap.index + blockDiff * snap.fee; + } + + const expectedNetworkFeeIndex = networkFeeIndexBefore + blocksElapsed * ssvNetworkFeeRaw; + + const operatorUsagePacked = (expectedCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + const totalPackedUsage = operatorUsagePacked + networkUsagePacked; + const totalUnpackedUsage = totalPackedUsage * DEDUCTED_DIGITS; + + const expectedRefund = initialBalance > totalUnpackedUsage + ? initialBalance - totalUnpackedUsage + : 0n; + + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(expectedRefund); + expect(harnessTokenBefore - harnessTokenAfter).to.equal(expectedRefund); + const feesCharged = initialBalance - expectedRefund; + expect(feesCharged).to.equal(totalUnpackedUsage); + }); + + it("Fee integer truncation — totalUnpackedUsage is always a multiple of DEDUCTED_DIGITS", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const operatorSSVFee = DEDUCTED_DIGITS * 3n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, operatorSSVFee); + } + + const ssvNetworkFeeRaw = 13n; + await clusters.mockSSVNetworkFee(ssvNetworkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + const tokenAddress = await mockToken.getAddress(); + const harnessAddress = await clusters.getAddress(); + await clusters.mockSetToken(tokenAddress); + + const validatorCount = 5n; + const initialBalance = connection.ethers.parseEther("50"); + await mockToken.mint(harnessAddress, initialBalance); + + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: initialBalance, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const operatorSnapshots = []; + for (const opId of operatorIds) { + const snap = await clusters.getOperatorSnapshot(opId); + const fee = await clusters.getOperatorSSVFee(opId); + operatorSnapshots.push({ block: BigInt(snap.blockNumber), index: snap.index, fee }); + } + const networkFeeIndexBefore = await clusters.getCurrentNetworkFeeIndexSSV(); + const readBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + + await networkHelpers.mine(97); + + const ownerTokenBefore = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenBefore = await mockToken.balanceOf(harnessAddress); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const migrationBlock = BigInt(receipt!.blockNumber); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + + const blocksElapsed = migrationBlock - readBlock; + + let expectedCumulativeIndex = 0n; + for (const snap of operatorSnapshots) { + const blockDiff = migrationBlock - snap.block; + expectedCumulativeIndex += snap.index + blockDiff * snap.fee; + } + + const expectedNetworkFeeIndex = networkFeeIndexBefore + blocksElapsed * ssvNetworkFeeRaw; + const operatorUsagePacked = (expectedCumulativeIndex - ssvCluster.index) * validatorCount; + const networkUsagePacked = (expectedNetworkFeeIndex - ssvCluster.networkFeeIndex) * validatorCount; + const totalPackedUsage = operatorUsagePacked + networkUsagePacked; + const totalUnpackedUsage = totalPackedUsage * DEDUCTED_DIGITS; + + const expectedRefund = initialBalance > totalUnpackedUsage + ? initialBalance - totalUnpackedUsage + : 0n; + expect(eventArgs.ssvRefunded).to.equal(expectedRefund); + + const ownerTokenAfter = await mockToken.balanceOf(clusterOwner.address); + const harnessTokenAfter = await mockToken.balanceOf(harnessAddress); + expect(ownerTokenAfter - ownerTokenBefore).to.equal(expectedRefund); + expect(harnessTokenBefore - harnessTokenAfter).to.equal(expectedRefund); + const feesCharged = initialBalance - expectedRefund; + expect(feesCharged % DEDUCTED_DIGITS).to.equal(0n); + expect(totalPackedUsage % DEDUCTED_DIGITS).to.not.equal(0n); + }); + }); + + describe("Post-migration EB updates", async function () { + it("first ETH-side updateClusterBalance after migration applies explicit EB", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + await clusters.mockRegisterSSVValidator(makePublicKey(7001), operatorIds, clusterOwner.address, ssvCluster); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const migratedCluster = parseClusterFromEvent(clusters, await migrateTx.wait(), Events.CLUSTER_MIGRATED_TO_ETH); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + await clusters.mockSetEBRoot(1, computeEBRoot(clusterId, 64)); + + const updateTx = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + migratedCluster, + 64, + [] + ); + const updatedCluster = parseClusterFromEvent(clusters, await updateTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + expect(updatedCluster.active).to.equal(true); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(20000n); + } + }); + + it("removed operator remains skipped in first post-migration EB update", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + await clusters.mockRegisterSSVValidator(makePublicKey(7002), operatorIds, clusterOwner.address, ssvCluster); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const migratedCluster = parseClusterFromEvent(clusters, await migrateTx.wait(), Events.CLUSTER_MIGRATED_TO_ETH); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const removedOperator = operatorIds[0]; + await clusters.mockRemoveOperator(removedOperator); + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + + await clusters.mockSetEBRoot(1, computeEBRoot(clusterId, 64)); + const updateTx = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + migratedCluster, + 64, + [] + ); + const updatedCluster = parseClusterFromEvent(clusters, await updateTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + expect(updatedCluster.active).to.equal(true); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(20000n); + expect(await clusters.getOperatorEthVUnits(removedOperator)).to.equal(0n); + for (const operatorId of operatorIds.slice(1)) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(10000n); + } + }); + + it("migrateClusterToETH rejects stale caller-supplied SSV cluster state", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const staleCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + await clusters.mockRegisterSSVValidator(makePublicKey(7010), operatorIds, clusterOwner.address, staleCluster); + + const freshCluster = { + validatorCount: 2n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + await clusters.mockRegisterSSVValidator(makePublicKey(7011), operatorIds, clusterOwner.address, freshCluster); + + await expect( + clusters.migrateClusterToETH(operatorIds, staleCluster, { value: DEFAULT_ETH_REGISTER_VALUE }) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + + await expect( + clusters.migrateClusterToETH(operatorIds, freshCluster, { value: DEFAULT_ETH_REGISTER_VALUE }) + ).to.emit(clusters, Events.CLUSTER_MIGRATED_TO_ETH); + }); + }); + + describe("Removed Operators Security Check", async () => { + it("Skips removed operators during migration without reviving them", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const validatorCount = 2n; + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + const operatorToRemove = operatorIds[0]; + await clusters.mockRemoveOperator(operatorToRemove); + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfterMigration.active).to.equal(true); + expect(clusterAfterMigration.validatorCount).to.equal(ssvCluster.validatorCount); + for (let i = 1; i < operatorIds.length; i++) { + const operatorId = operatorIds[i]; + const ethValidatorCount = await clusters.getOperatorEthValidatorCount(operatorId); + expect(ethValidatorCount).to.equal(validatorCount); + } + const removedOperatorCount = await clusters.getOperatorEthValidatorCount(operatorToRemove); + expect(removedOperatorCount).to.be.greaterThanOrEqual(0n); + }); + + it("Handles migration with all operators removed gracefully", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const ssvCluster = { + validatorCount: 2n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + for (const operatorId of operatorIds) { + await clusters.mockRemoveOperator(operatorId); + } + try { + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfterMigration.active).to.equal(true); + for (const operatorId of operatorIds) { + const ethValidatorCount = await clusters.getOperatorEthValidatorCount(operatorId); + expect(ethValidatorCount).to.equal(0n); + } + } catch (error) { + expect((error as Error).message).to.include("revert"); + } + }); + + it("Prevents silent revival of removed operators with zero fees", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + const operatorToRemove = operatorIds[0]; + await clusters.mockRemoveOperator(operatorToRemove); + await clusters.mockSetOperatorFee(operatorToRemove, 0n); + const ethFeeBefore = await clusters.getOperatorEthFee(operatorToRemove); + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await migrateTx.wait(); + const ethFeeAfter = await clusters.getOperatorEthFee(operatorToRemove); + expect(ethFeeAfter).to.equal(ethFeeBefore); + const ethValidatorCount = await clusters.getOperatorEthValidatorCount(operatorToRemove); + expect(ethValidatorCount).to.be.greaterThanOrEqual(0n); + }); + + it("Maintains operator count integrity with mixed valid/removed operators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const validatorCount = 3n; + const ssvCluster = { + validatorCount: validatorCount, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + const removedOperators = []; + const validOperators = []; + + for (let i = 0; i < operatorIds.length; i += 2) { + await clusters.mockRemoveOperator(operatorIds[i]); + removedOperators.push(operatorIds[i]); + } + + for (let i = 1; i < operatorIds.length; i += 2) { + validOperators.push(operatorIds[i]); + } + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + ssvCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfterMigration.active).to.equal(true); + expect(clusterAfterMigration.validatorCount).to.equal(ssvCluster.validatorCount); + for (const operatorId of validOperators) { + const ethValidatorCount = await clusters.getOperatorEthValidatorCount(operatorId); + expect(ethValidatorCount).to.equal(validatorCount); + } + for (const operatorId of removedOperators) { + const ethValidatorCount = await clusters.getOperatorEthValidatorCount(operatorId); + expect(ethValidatorCount).to.be.greaterThanOrEqual(0n); + } + }); + }); +}); diff --git a/test/unit/SSVClusters/networkFeeImpact.test.ts b/test/unit/SSVClusters/networkFeeImpact.test.ts new file mode 100644 index 000000000..8ae4f8404 --- /dev/null +++ b/test/unit/SSVClusters/networkFeeImpact.test.ts @@ -0,0 +1,169 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, makePublicKey, parseClusterFromEvent } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, EMPTY_CLUSTER, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; + +describe("Network fee update impact on active clusters", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + const deployFixture = async () => defaultClustersFixture(connection); + + + it("Increase ETH network fee cluster burns faster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployFixture); + + const fee1 = 1_000n; + await clusters.mockEthNetworkFee(fee1); + await clusters.mockCurrentNetworkFeeIndex(0n); + + const registerTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + { ...EMPTY_CLUSTER }, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerReceipt = await registerTx.wait(); + let cluster = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + const blocksPerPeriod = 500; + await networkHelpers.mine(blocksPerPeriod); + + const w1Tx = await clusters.withdraw(operatorIds, 0n, cluster); + const w1Receipt = await w1Tx.wait(); + const clusterAfterP1 = parseClusterFromEvent(clusters, w1Receipt, Events.CLUSTER_WITHDRAWN); + const burnP1 = cluster.balance - clusterAfterP1.balance; + + const currentIndex = await clusters.getCurrentNetworkFeeIndex(); + await clusters.mockCurrentNetworkFeeIndex(currentIndex); + const fee2 = 3_000n; + await clusters.mockEthNetworkFee(fee2); + + await networkHelpers.mine(blocksPerPeriod); + + const w2Tx = await clusters.withdraw(operatorIds, 0n, clusterAfterP1); + const w2Receipt = await w2Tx.wait(); + const clusterAfterP2 = parseClusterFromEvent(clusters, w2Receipt, Events.CLUSTER_WITHDRAWN); + const burnP2 = clusterAfterP1.balance - clusterAfterP2.balance; + + expect(burnP2).to.be.greaterThan(burnP1); + + const registerBlock = BigInt(registerReceipt!.blockNumber); + const w1Block = BigInt(w1Receipt!.blockNumber); + const p1Blocks = w1Block - registerBlock; + const expectedBurnP1 = p1Blocks * fee1 * ETH_DEDUCTED_DIGITS; + expect(burnP1).to.equal(expectedBurnP1); + + expect(burnP2).to.be.greaterThan(0n); + expect(clusterAfterP2.balance).to.be.greaterThan(0n); + }); + + it("Decrease ETH network fee cluster burn rate decreases", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployFixture); + + const fee1 = 3_000n; + await clusters.mockEthNetworkFee(fee1); + await clusters.mockCurrentNetworkFeeIndex(0n); + + const registerTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + { ...EMPTY_CLUSTER }, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerReceipt = await registerTx.wait(); + let cluster = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + const blocksPerPeriod = 500; + await networkHelpers.mine(blocksPerPeriod); + + const w1Tx = await clusters.withdraw(operatorIds, 0n, cluster); + const w1Receipt = await w1Tx.wait(); + const clusterAfterP1 = parseClusterFromEvent(clusters, w1Receipt, Events.CLUSTER_WITHDRAWN); + const burnP1 = cluster.balance - clusterAfterP1.balance; + + const currentIndex = await clusters.getCurrentNetworkFeeIndex(); + await clusters.mockCurrentNetworkFeeIndex(currentIndex); + const fee2 = 1_000n; + await clusters.mockEthNetworkFee(fee2); + + await networkHelpers.mine(blocksPerPeriod); + + const w2Tx = await clusters.withdraw(operatorIds, 0n, clusterAfterP1); + const w2Receipt = await w2Tx.wait(); + const clusterAfterP2 = parseClusterFromEvent(clusters, w2Receipt, Events.CLUSTER_WITHDRAWN); + const burnP2 = clusterAfterP1.balance - clusterAfterP2.balance; + + expect(burnP2).to.be.lessThan(burnP1); + + const registerBlock = BigInt(registerReceipt!.blockNumber); + const w1Block = BigInt(w1Receipt!.blockNumber); + const p1Blocks = w1Block - registerBlock; + const expectedBurnP1 = p1Blocks * fee1 * ETH_DEDUCTED_DIGITS; + expect(burnP1).to.equal(expectedBurnP1); + + expect(burnP2).to.be.greaterThan(0n); + expect(clusterAfterP2.balance).to.be.greaterThan(0n); + }); + + it("Network fee with EB-weighted cluster vUnit scaling applied", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployFixture); + + const fee = 2_000n; + await clusters.mockEthNetworkFee(fee); + await clusters.mockCurrentNetworkFeeIndex(0n); + + const registerTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + { ...EMPTY_CLUSTER }, + { value: DEFAULT_ETH_REGISTER_VALUE }, + ); + const registerReceipt = await registerTx.wait(); + let cluster = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const explicitVUnits = 15_000n; + await clusters.mockSetClusterVUnits(clusterId, explicitVUnits); + + const blocksToMine = 500; + await networkHelpers.mine(blocksToMine); + + const w1Tx = await clusters.withdraw(operatorIds, 0n, cluster); + const w1Receipt = await w1Tx.wait(); + const clusterAfterW1 = parseClusterFromEvent(clusters, w1Receipt, Events.CLUSTER_WITHDRAWN); + const actualBurn = cluster.balance - clusterAfterW1.balance; + + const registerBlock = BigInt(registerReceipt!.blockNumber); + const w1Block = BigInt(w1Receipt!.blockNumber); + const totalBlocks = w1Block - registerBlock; + + const networkFeeIndexDelta = totalBlocks * fee; + const scaledUnits = (networkFeeIndexDelta * explicitVUnits) / BPS_DENOMINATOR; + const expectedBurn = scaledUnits * ETH_DEDUCTED_DIGITS; + expect(actualBurn).to.equal(expectedBurn); + + const defaultVUnits = BPS_DENOMINATOR; + const defaultScaledUnits = (networkFeeIndexDelta * defaultVUnits) / BPS_DENOMINATOR; + const defaultBurn = defaultScaledUnits * ETH_DEDUCTED_DIGITS; + + expect(actualBurn).to.be.greaterThan(defaultBurn); + expect(actualBurn * defaultVUnits).to.equal(defaultBurn * explicitVUnits); + }); +}); diff --git a/test/unit/SSVClusters/operatorFeeEBInteraction.test.ts b/test/unit/SSVClusters/operatorFeeEBInteraction.test.ts new file mode 100644 index 000000000..7a96a3b66 --- /dev/null +++ b/test/unit/SSVClusters/operatorFeeEBInteraction.test.ts @@ -0,0 +1,524 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, makePublicKey, parseClusterFromEvent, registerAndParseCluster } from "../../common/helpers.ts"; +import { DEFAULT_SHARES, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS, MINIMAL_OPERATOR_ETH_FEE } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { mockEBAndUpdate } from "../../helpers/oracle.ts"; +import { ethers } from "ethers"; + +const INITIAL_FEE = MINIMAL_OPERATOR_ETH_FEE; +const DOUBLED_FEE = MINIMAL_OPERATOR_ETH_FEE * 2n; +const TRIPLED_FEE = MINIMAL_OPERATOR_ETH_FEE * 3n; + +describe("Operator fee change + EB burn rate interaction", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator] } = await setupTestContext()); + }); + + + const deployWithInitialFee = async () => ssvClustersHarnessFixture(connection, 4, INITIAL_FEE); + const deployWithDoubledFee = async () => ssvClustersHarnessFixture(connection, 4, DOUBLED_FEE); + + const getOperatorSnapshotWei = async (clusters: any, operatorId: bigint) => { + const [, snapshotBlock, operatorEarnings] = await clusters.getOperatorEthSnapshot(operatorId); + return { + snapshotBlock: BigInt(snapshotBlock), + earningsWei: operatorEarnings * ETH_DEDUCTED_DIGITS, + }; + }; + + it("Fee increase with EB=64 cluster → burn rate doubles", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("100")); + + const { cluster: clusterAfterEB, block: ebBlock } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 64, 1); + const expectedVUnits = (64n * BPS_DENOMINATOR + 31n) / 32n; + expect(expectedVUnits).to.equal(20000n); + + await networkHelpers.mine(500); + const w1Tx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + await expect(w1Tx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const w1Receipt = await w1Tx.wait(); + const clusterAfterP1 = parseClusterFromEvent(clusters, w1Receipt, Events.CLUSTER_WITHDRAWN); + const w1Block = BigInt(w1Receipt!.blockNumber); + const burnP1 = clusterAfterEB.balance - clusterAfterP1.balance; + const snapBeforeFeeExec = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, DOUBLED_FEE); + await expect(fcTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + const snapAfterFeeExec = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + await networkHelpers.mine(500); + const w2Tx = await clusters.withdraw(operatorIds, 0n, clusterAfterP1); + await expect(w2Tx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const w2Receipt = await w2Tx.wait(); + const clusterAfterP2 = parseClusterFromEvent(clusters, w2Receipt, Events.CLUSTER_WITHDRAWN); + const w2Block = BigInt(w2Receipt!.blockNumber); + const burnP2 = clusterAfterP1.balance - clusterAfterP2.balance; + + const settleTx = await clusters.mockExecuteAllOperatorFees([operatorIds[0]], DOUBLED_FEE); + await expect(settleTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const settleReceipt = await settleTx.wait(); + const settleBlock = BigInt(settleReceipt!.blockNumber); + const snapAfterSettle = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const packedDoubled = DOUBLED_FEE / ETH_DEDUCTED_DIGITS; + const numOps = BigInt(operatorIds.length); + + const p1Blocks = w1Block - ebBlock; + const expectedBurnP1 = (numOps * p1Blocks * packedInitial * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + const expectedEarningsAtFeeExec = ((fcBlock - snapBeforeFeeExec.snapshotBlock) * packedInitial * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + const transitionBlocks = fcBlock - w1Block; + const p2NewFeeBlocks = w2Block - fcBlock; + const idxOpP2 = numOps * (transitionBlocks * packedInitial + p2NewFeeBlocks * packedDoubled); + const expectedBurnP2 = (idxOpP2 * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + const expectedEarningsAtSettle = ((settleBlock - snapAfterFeeExec.snapshotBlock) * packedDoubled * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + expect(burnP1).to.equal(expectedBurnP1); + expect(burnP2).to.equal(expectedBurnP2); + expect(burnP2).to.be.greaterThan(burnP1); + expect(snapAfterFeeExec.earningsWei - snapBeforeFeeExec.earningsWei).to.equal(expectedEarningsAtFeeExec); + expect(snapAfterSettle.earningsWei - snapAfterFeeExec.earningsWei).to.equal(expectedEarningsAtSettle); + }); + + it("Fee reduction with EB=128 cluster → savings reflected", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithDoubledFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("100")); + + const { cluster: clusterAfterEB, block: ebBlock } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 128, 1); + const expectedVUnits = (128n * BPS_DENOMINATOR + 31n) / 32n; + expect(expectedVUnits).to.equal(40000n); + + await networkHelpers.mine(500); + const w1Tx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + await expect(w1Tx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const w1Receipt = await w1Tx.wait(); + const clusterAfterP1 = parseClusterFromEvent(clusters, w1Receipt, Events.CLUSTER_WITHDRAWN); + const w1Block = BigInt(w1Receipt!.blockNumber); + const burnP1 = clusterAfterEB.balance - clusterAfterP1.balance; + const snapBeforeFeeExec = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, INITIAL_FEE); + await expect(fcTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + const snapAfterFeeExec = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + await networkHelpers.mine(500); + const w2Tx = await clusters.withdraw(operatorIds, 0n, clusterAfterP1); + await expect(w2Tx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const w2Receipt = await w2Tx.wait(); + const clusterAfterP2 = parseClusterFromEvent(clusters, w2Receipt, Events.CLUSTER_WITHDRAWN); + const w2Block = BigInt(w2Receipt!.blockNumber); + const burnP2 = clusterAfterP1.balance - clusterAfterP2.balance; + + const settleTx = await clusters.mockExecuteAllOperatorFees([operatorIds[0]], INITIAL_FEE); + await expect(settleTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const settleReceipt = await settleTx.wait(); + const settleBlock = BigInt(settleReceipt!.blockNumber); + const snapAfterSettle = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + const packedDoubled = DOUBLED_FEE / ETH_DEDUCTED_DIGITS; + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const numOps = BigInt(operatorIds.length); + + const p1Blocks = w1Block - ebBlock; + const expectedBurnP1 = (numOps * p1Blocks * packedDoubled * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + const expectedEarningsAtFeeExec = ((fcBlock - snapBeforeFeeExec.snapshotBlock) * packedDoubled * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + const transitionBlocks = fcBlock - w1Block; + const p2NewFeeBlocks = w2Block - fcBlock; + const idxOpP2 = numOps * (transitionBlocks * packedDoubled + p2NewFeeBlocks * packedInitial); + const expectedBurnP2 = (idxOpP2 * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + const expectedEarningsAtSettle = ((settleBlock - snapAfterFeeExec.snapshotBlock) * packedInitial * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + expect(burnP1).to.equal(expectedBurnP1); + expect(burnP2).to.equal(expectedBurnP2); + expect(burnP2).to.be.lessThan(burnP1); + expect(snapAfterFeeExec.earningsWei - snapBeforeFeeExec.earningsWei).to.equal(expectedEarningsAtFeeExec); + expect(snapAfterSettle.earningsWei - snapAfterFeeExec.earningsWei).to.equal(expectedEarningsAtSettle); + }); + + it("Fee change boundary accounting — total burn = sum of both rate periods", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("100")); + + const { cluster: clusterAfterEB, block: ebBlock } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 96, 1); + const expectedVUnits = (96n * BPS_DENOMINATOR + 31n) / 32n; + expect(expectedVUnits).to.equal(30000n); + + await networkHelpers.mine(200); + + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, TRIPLED_FEE); + await expect(fcTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + + await networkHelpers.mine(300); + + const wTx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + await expect(wTx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const wReceipt = await wTx.wait(); + const clusterAfterW = parseClusterFromEvent(clusters, wReceipt, Events.CLUSTER_WITHDRAWN); + const wBlock = BigInt(wReceipt!.blockNumber); + const totalBurn = clusterAfterEB.balance - clusterAfterW.balance; + + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const packedTripled = TRIPLED_FEE / ETH_DEDUCTED_DIGITS; + const numOps = BigInt(operatorIds.length); + + const preChangeBlocks = fcBlock - ebBlock; + const postChangeBlocks = wBlock - fcBlock; + const idxOp = numOps * (preChangeBlocks * packedInitial + postChangeBlocks * packedTripled); + const expectedBurn = (idxOp * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + expect(totalBurn).to.equal(expectedBurn); + expect(totalBurn).to.be.greaterThan(0n); + expect(clusterAfterW.balance).to.be.greaterThan(0n); + + const totalBlocks = wBlock - ebBlock; + const burnIfAllOld = (numOps * totalBlocks * packedInitial * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + const burnIfAllNew = (numOps * totalBlocks * packedTripled * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + expect(totalBurn).to.be.greaterThan(burnIfAllOld); + expect(totalBurn).to.be.lessThan(burnIfAllNew); + }); + + it("Fee change with EB=0 (implicit vUnits mode) settles with baseline vUnits", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const regTx = await clusters.registerValidator( + makePublicKey(2), operatorIds, DEFAULT_SHARES, createCluster(), { value: ethers.parseEther("50") }, + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + const regBlock = BigInt(regReceipt!.blockNumber); + + await networkHelpers.mine(100); + const snapBeforeFeeExec = await getOperatorSnapshotWei(clusters, operatorIds[0]); + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, DOUBLED_FEE); + await expect(fcTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + const snapAfterFeeExec = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + await networkHelpers.mine(100); + const wTx = await clusters.withdraw(operatorIds, 0n, clusterAfterReg); + await expect(wTx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const wReceipt = await wTx.wait(); + const clusterAfterW = parseClusterFromEvent(clusters, wReceipt, Events.CLUSTER_WITHDRAWN); + const wBlock = BigInt(wReceipt!.blockNumber); + + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const packedDoubled = DOUBLED_FEE / ETH_DEDUCTED_DIGITS; + const baselineVUnits = 10_000n; + const numOps = BigInt(operatorIds.length); + + const expectedBurn = ( + numOps * ( + (fcBlock - regBlock) * packedInitial + + (wBlock - fcBlock) * packedDoubled + ) * baselineVUnits / BPS_DENOMINATOR + ) * ETH_DEDUCTED_DIGITS; + expect(clusterAfterReg.balance - clusterAfterW.balance).to.equal(expectedBurn); + + const expectedFeeExecDelta = ((fcBlock - snapBeforeFeeExec.snapshotBlock) * packedInitial * baselineVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + expect(snapAfterFeeExec.earningsWei - snapBeforeFeeExec.earningsWei).to.equal(expectedFeeExecDelta); + + const settleTx = await clusters.mockExecuteAllOperatorFees([operatorIds[0]], DOUBLED_FEE); + await expect(settleTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const settleReceipt = await settleTx.wait(); + const settleBlock = BigInt(settleReceipt!.blockNumber); + const snapAfterSettle = await getOperatorSnapshotWei(clusters, operatorIds[0]); + const expectedSettleDelta = ((settleBlock - snapAfterFeeExec.snapshotBlock) * packedDoubled * baselineVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + expect(snapAfterSettle.earningsWei - snapAfterFeeExec.earningsWei).to.equal(expectedSettleDelta); + }); + + it("Fee change with removed operators skips removed entries and settles active operators", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 3, ethers.parseEther("60")); + + const { cluster: clusterAfterEB } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 64, 1); + + await networkHelpers.mine(40); + await clusters.mockRemoveOperator(operatorIds[0]); + + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, TRIPLED_FEE); + await expect(fcTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + await fcTx.wait(); + + await networkHelpers.mine(40); + const wTx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + await expect(wTx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + await wTx.wait(); + + const [, removedBlock, removedBalance] = await clusters.getOperatorEthSnapshot(operatorIds[0]); + expect(removedBlock).to.equal(0); + expect(removedBalance).to.equal(0n); + const activeOperatorSnapshot = await getOperatorSnapshotWei(clusters, operatorIds[1]); + expect(activeOperatorSnapshot.earningsWei).to.be.greaterThan(0n); + }); + + it("Fee change can make cluster immediately liquidatable at max EB", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 4, 5_000_000_000_000n); + + const { cluster: clusterAfterEB } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 2048, 1); + await clusters.mockExecuteAllOperatorFees(operatorIds, TRIPLED_FEE); + await networkHelpers.mine(2); + + const liqTx = await clusters.connect(liquidator).liquidate( + clusterOwner.address, + operatorIds, + clusterAfterEB, + ); + await expect(liqTx).to.emit(clusters, Events.CLUSTER_LIQUIDATED); + }); + + it("Multiple fee changes in quick succession preserve exact accounting", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 5, ethers.parseEther("80")); + const { cluster: clusterAfterEB, block: ebBlock } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 96, 1); + const vUnits = 30_000n; + + await networkHelpers.mine(10); + const fc1Tx = await clusters.mockExecuteAllOperatorFees(operatorIds, DOUBLED_FEE); + await expect(fc1Tx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const fc1Receipt = await fc1Tx.wait(); + const fc1Block = BigInt(fc1Receipt!.blockNumber); + + const fc2Tx = await clusters.mockExecuteAllOperatorFees(operatorIds, TRIPLED_FEE); + await expect(fc2Tx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const fc2Receipt = await fc2Tx.wait(); + const fc2Block = BigInt(fc2Receipt!.blockNumber); + + await networkHelpers.mine(20); + const wTx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + await expect(wTx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const wReceipt = await wTx.wait(); + const wBlock = BigInt(wReceipt!.blockNumber); + const clusterAfterW = parseClusterFromEvent(clusters, wReceipt, Events.CLUSTER_WITHDRAWN); + + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const packedDoubled = DOUBLED_FEE / ETH_DEDUCTED_DIGITS; + const packedTripled = TRIPLED_FEE / ETH_DEDUCTED_DIGITS; + const numOps = BigInt(operatorIds.length); + + const expectedBurn = ( + numOps * ( + (fc1Block - ebBlock) * packedInitial + + (fc2Block - fc1Block) * packedDoubled + + (wBlock - fc2Block) * packedTripled + ) * vUnits / BPS_DENOMINATOR + ) * ETH_DEDUCTED_DIGITS; + expect(clusterAfterEB.balance - clusterAfterW.balance).to.equal(expectedBurn); + + const snapAfterFc2 = await getOperatorSnapshotWei(clusters, operatorIds[0]); + const settleTx = await clusters.mockExecuteAllOperatorFees([operatorIds[0]], TRIPLED_FEE); + await expect(settleTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const settleReceipt = await settleTx.wait(); + const settleBlock = BigInt(settleReceipt!.blockNumber); + const snapAfterSettle = await getOperatorSnapshotWei(clusters, operatorIds[0]); + const expectedSettleDelta = ((settleBlock - snapAfterFc2.snapshotBlock) * packedTripled * vUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + expect(snapAfterSettle.earningsWei - snapAfterFc2.earningsWei).to.equal(expectedSettleDelta); + }); + + it("Fee change with max EB (2048 ETH/validator) uses capped vUnits in settlement", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 6, ethers.parseEther("120")); + const { cluster: clusterAfterEB, block: ebBlock } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 2048, 1); + + const maxVUnits = 640_000n; + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(maxVUnits); + + await networkHelpers.mine(20); + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, DOUBLED_FEE); + await expect(fcTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + + await networkHelpers.mine(20); + const wTx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + await expect(wTx).to.emit(clusters, Events.CLUSTER_WITHDRAWN); + const wReceipt = await wTx.wait(); + const wBlock = BigInt(wReceipt!.blockNumber); + const clusterAfterW = parseClusterFromEvent(clusters, wReceipt, Events.CLUSTER_WITHDRAWN); + + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const packedDoubled = DOUBLED_FEE / ETH_DEDUCTED_DIGITS; + const numOps = BigInt(operatorIds.length); + + const expectedBurn = ( + numOps * ( + (fcBlock - ebBlock) * packedInitial + + (wBlock - fcBlock) * packedDoubled + ) * maxVUnits / BPS_DENOMINATOR + ) * ETH_DEDUCTED_DIGITS; + expect(clusterAfterEB.balance - clusterAfterW.balance).to.equal(expectedBurn); + + const snapAfterFc = await getOperatorSnapshotWei(clusters, operatorIds[0]); + const settleTx = await clusters.mockExecuteAllOperatorFees([operatorIds[0]], DOUBLED_FEE); + await expect(settleTx).to.emit(clusters, Events.OPERATOR_FEE_EXECUTED); + const settleReceipt = await settleTx.wait(); + const settleBlock = BigInt(settleReceipt!.blockNumber); + const snapAfterSettle = await getOperatorSnapshotWei(clusters, operatorIds[0]); + const expectedSettleDelta = ((settleBlock - snapAfterFc.snapshotBlock) * packedDoubled * maxVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + expect(snapAfterSettle.earningsWei - snapAfterFc.earningsWei).to.equal(expectedSettleDelta); + }); + + it("Operator fee change with network fee accounting → both fees correctly deducted", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 10, ethers.parseEther("100")); + + const { cluster: clusterAfterEB, block: ebBlock } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 64, 1); + const vUnits = 20_000n; + + await networkHelpers.mine(100); + + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, DOUBLED_FEE); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + + const oldNetworkFeeIndex = clusterAfterEB.networkFeeIndex; + + await networkHelpers.mine(100); + + const wTx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + const wReceipt = await wTx.wait(); + const wBlock = BigInt(wReceipt!.blockNumber); + const clusterAfterW = parseClusterFromEvent(clusters, wReceipt, Events.CLUSTER_WITHDRAWN); + + const numOps = BigInt(operatorIds.length); + const packedInitialOp = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const packedDoubledOp = DOUBLED_FEE / ETH_DEDUCTED_DIGITS; + + const p1Blocks = fcBlock - ebBlock; + const p2Blocks = wBlock - fcBlock; + const idxOp = numOps * (p1Blocks * packedInitialOp + p2Blocks * packedDoubledOp); + const operatorFeeUnits = (idxOp * vUnits) / BPS_DENOMINATOR; + + const currentNetworkFeeIndex = clusterAfterW.networkFeeIndex; + const idxNet = currentNetworkFeeIndex - oldNetworkFeeIndex; + const networkFeeUnits = (idxNet * vUnits) / BPS_DENOMINATOR; + + const expectedBurn = (operatorFeeUnits + networkFeeUnits) * ETH_DEDUCTED_DIGITS; + const actualBurn = clusterAfterEB.balance - clusterAfterW.balance; + + expect(actualBurn).to.equal(expectedBurn); + expect(operatorFeeUnits).to.be.greaterThan(0n); + }); + + it("EB update between fee change execution updates vUnits for earnings calculation", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const clusterAfterReg = await registerAndParseCluster(clusters, operatorIds, 11, ethers.parseEther("100")); + + await networkHelpers.mine(50); + + await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterReg, 96, 1); + const vUnitsAfterEB = 30_000n; + + await networkHelpers.mine(50); + + const snap2 = await getOperatorSnapshotWei(clusters, operatorIds[0]); + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, DOUBLED_FEE); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + const snap3 = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const blocksDelta = fcBlock - snap2.snapshotBlock; + const expectedDelta = (blocksDelta * packedInitial * vUnitsAfterEB / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + expect(snap3.earningsWei - snap2.earningsWei).to.equal(expectedDelta); + + await networkHelpers.mine(50); + + const settleTx = await clusters.mockExecuteAllOperatorFees([operatorIds[0]], DOUBLED_FEE); + const settleReceipt = await settleTx.wait(); + const settleBlock = BigInt(settleReceipt!.blockNumber); + const snap4 = await getOperatorSnapshotWei(clusters, operatorIds[0]); + + const packedDoubled = DOUBLED_FEE / ETH_DEDUCTED_DIGITS; + const blocksDelta2 = settleBlock - snap3.snapshotBlock; + const expectedDelta2 = (blocksDelta2 * packedDoubled * vUnitsAfterEB / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + expect(snap4.earningsWei - snap3.earningsWei).to.equal(expectedDelta2); + }); + + it("Fee change on 4-validator cluster with EB=128 (avg 32 ETH/validator)", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployWithInitialFee); + + const depositValue = ethers.parseEther("50"); + let cluster = await registerAndParseCluster(clusters, operatorIds, 20, depositValue); + + for (let i = 1; i < 4; i++) { + const regTx = await clusters.registerValidator( + makePublicKey(20 + i), operatorIds, DEFAULT_SHARES, cluster, { value: depositValue }, + ); + const regReceipt = await regTx.wait(); + cluster = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + } + + expect(cluster.validatorCount).to.equal(4); + + const { cluster: clusterAfterEB, block: ebBlock } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, cluster, 128, 1); + const expectedVUnits = (128n * BPS_DENOMINATOR + 31n) / 32n; + expect(expectedVUnits).to.equal(40_000n); + + await networkHelpers.mine(100); + + const fcTx = await clusters.mockExecuteAllOperatorFees(operatorIds, TRIPLED_FEE); + const fcReceipt = await fcTx.wait(); + const fcBlock = BigInt(fcReceipt!.blockNumber); + + await networkHelpers.mine(100); + + const wTx = await clusters.withdraw(operatorIds, 0n, clusterAfterEB); + const wReceipt = await wTx.wait(); + const wBlock = BigInt(wReceipt!.blockNumber); + const clusterAfterW = parseClusterFromEvent(clusters, wReceipt, Events.CLUSTER_WITHDRAWN); + + const numOps = BigInt(operatorIds.length); + const packedInitial = INITIAL_FEE / ETH_DEDUCTED_DIGITS; + const packedTripled = TRIPLED_FEE / ETH_DEDUCTED_DIGITS; + + const p1Blocks = fcBlock - ebBlock; + const p2Blocks = wBlock - fcBlock; + const idxOp = numOps * (p1Blocks * packedInitial + p2Blocks * packedTripled); + const expectedBurnOp = (idxOp * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + const idxNet = clusterAfterW.networkFeeIndex - clusterAfterEB.networkFeeIndex; + const expectedBurnNet = (idxNet * expectedVUnits / BPS_DENOMINATOR) * ETH_DEDUCTED_DIGITS; + + const expectedTotalBurn = expectedBurnOp + expectedBurnNet; + const actualBurn = clusterAfterEB.balance - clusterAfterW.balance; + + expect(actualBurn).to.equal(expectedTotalBurn); + expect(cluster.validatorCount).to.equal(4); + expect(clusterAfterW.validatorCount).to.equal(4); + }); +}); diff --git a/test/unit/SSVClusters/reactivate.test.ts b/test/unit/SSVClusters/reactivate.test.ts new file mode 100644 index 000000000..be4d17577 --- /dev/null +++ b/test/unit/SSVClusters/reactivate.test.ts @@ -0,0 +1,656 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, createCluster, makePublicKey, parseClusterFromEvent, registerAndParseCluster, registerAndLiquidate, assertOperatorVUnits, calcLiquidationThreshold } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { mockEBAndUpdate } from "../../helpers/oracle.ts"; +import { ethers } from "ethers"; + +describe("SSVClusters function `reactivate()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, otherAccount] } = await setupTestContext()); + }); + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return defaultClustersFixture(connection); + }; + + + const getOperatorEthEarnings = async (clusters: any, operatorId: bigint): Promise => { + const [, , balance] = await clusters.getOperatorEthSnapshot(operatorId); + return balance; + }; + + + it("Reactivates a liquidated cluster with sufficient balance and emits correct event", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const { clusterAfterLiquidation } = await registerAndLiquidate(clusters, clusterOwner.address, operatorIds); + + const reactivateTx = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const reactivateReceipt = await reactivateTx.wait(); + await trackGasFromReceipt(reactivateReceipt, [GasGroup.REACTIVATE_CLUSTER]); + const clusterAfterReactivate = parseClusterFromEvent(clusters, reactivateReceipt, Events.CLUSTER_REACTIVATED); + + await expect(reactivateTx).to.emit(clusters, Events.CLUSTER_REACTIVATED); + expect(clusterAfterReactivate.active).to.equal(true); + expect(clusterAfterReactivate.validatorCount).to.equal(clusterAfterLiquidation.validatorCount); + }); + + it("Keeps operator deviation at zero when reactivating without EB snapshot", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const { clusterAfterLiquidation } = await registerAndLiquidate(clusters, clusterOwner.address, operatorIds); + + await assertOperatorVUnits(clusters, operatorIds, 0n); + + const reactivateTx = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await reactivateTx.wait(); + + const baselineVUnits = clusterAfterLiquidation.validatorCount * BPS_DENOMINATOR; + await assertOperatorVUnits(clusters, operatorIds, 0n, baselineVUnits); + }); + + it("Is reverted with 'ClusterAlreadyEnabled' when trying to reactivate an active cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + + await expect(clusters.reactivate( + operatorIds, + clusterAfterRegister, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_ALREADY_ENABLED); + }); + + it("Is reverted with 'ClusterDoesNotExists' when a non-owner tries to reactivate a cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const { clusterAfterLiquidation } = await registerAndLiquidate(clusters, clusterOwner.address, operatorIds); + + await expect(clusters.connect(otherAccount).reactivate( + operatorIds, + clusterAfterLiquidation, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' when provided cluster data is stale or mismatched", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const { clusterAfterLiquidation } = await registerAndLiquidate(clusters, clusterOwner.address, operatorIds); + + const mismatchedCluster = { + ...clusterAfterLiquidation, + balance: clusterAfterLiquidation.balance + 1n, + }; + + await expect(clusters.reactivate( + operatorIds, + mismatchedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'InsufficientBalance' when reactivation deposit is too low", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const { clusterAfterLiquidation } = await registerAndLiquidate(clusters, clusterOwner.address, operatorIds); + await clusters.mockMinimumLiquidationCollateral(DEFAULT_ETH_REGISTER_VALUE + 1_000_000_000n); + + await expect(clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + }); + + it("Is reverted with 'InsufficientBalance' when reactivation deposit does not cover runway", async function () { + const operatorFee = 5_000_000_000n; + const deployFixture = async () => ssvClustersHarnessFixture(connection, 4, operatorFee); + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + await clusters.mockMinimumLiquidationCollateral(0n); + + const { clusterAfterLiquidation } = await registerAndLiquidate(clusters, clusterOwner.address, operatorIds); + await clusters.mockMinimumBlocksBeforeLiquidation(1_000_000_000n); + + await expect(clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + + const reactivateTx = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: ethers.parseEther("30") } + ); + await reactivateTx.wait(); + }); + + it("Scales reactivation solvency threshold with EB=64 (2x baseline vUnits)", async function () { + const operatorFeePacked = 100_000n; + const operatorFee = operatorFeePacked * ETH_DEDUCTED_DIGITS; + const networkFeePacked = 100_000n; + const minimumBlocksBeforeLiquidation = 100n; + const deployFixture = async () => ssvClustersHarnessFixture(connection, 4, operatorFee); + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + await clusters.mockEthNetworkFee(networkFeePacked); + await clusters.mockMinimumBlocksBeforeLiquidation(minimumBlocksBeforeLiquidation); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { cluster: clusterAfterEB64 } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 64, 1); + + const vUnitsAt64 = await clusters.getClusterVUnits(clusterId); + expect(vUnitsAt64).to.equal(2n * BPS_DENOMINATOR); + + await clusters.mockMinimumLiquidationCollateral(DEFAULT_ETH_REGISTER_VALUE + 1n); + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB64); + const liquidateReceipt = await liquidateTx.wait(); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + await clusters.mockMinimumLiquidationCollateral(0n); + + const baselineThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation, + numOperators: BigInt(operatorIds.length), + ethFee: operatorFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: BPS_DENOMINATOR, + }); + const thresholdAt64 = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation, + numOperators: BigInt(operatorIds.length), + ethFee: operatorFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnitsAt64, + }); + expect(thresholdAt64).to.equal(baselineThreshold * 2n); + + await expect( + clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: baselineThreshold } + ) + ).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + + await expect( + clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: thresholdAt64 } + ) + ).to.emit(clusters, Events.CLUSTER_REACTIVATED); + }); + + it("Enforces a much higher reactivation threshold when EB=2048", async function () { + const operatorFeePacked = 100_000n; + const operatorFee = operatorFeePacked * ETH_DEDUCTED_DIGITS; + const networkFeePacked = 100_000n; + const minimumBlocksBeforeLiquidation = 100n; + const deployFixture = async () => ssvClustersHarnessFixture(connection, 4, operatorFee); + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + await clusters.mockEthNetworkFee(networkFeePacked); + await clusters.mockMinimumBlocksBeforeLiquidation(minimumBlocksBeforeLiquidation); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { cluster: clusterAfterEB2048 } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 2048, 1); + + const vUnitsAt2048 = await clusters.getClusterVUnits(clusterId); + expect(vUnitsAt2048).to.equal(64n * BPS_DENOMINATOR); + + await clusters.mockMinimumLiquidationCollateral(DEFAULT_ETH_REGISTER_VALUE + 1n); + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB2048); + const liquidateReceipt = await liquidateTx.wait(); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + await clusters.mockMinimumLiquidationCollateral(0n); + + const baselineThreshold = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation, + numOperators: BigInt(operatorIds.length), + ethFee: operatorFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: BPS_DENOMINATOR, + }); + const thresholdAt2048 = calcLiquidationThreshold({ + minimumBlocksBeforeLiquidation, + numOperators: BigInt(operatorIds.length), + ethFee: operatorFeePacked, + networkFee: networkFeePacked, + effectiveVUnits: vUnitsAt2048, + }); + expect(thresholdAt2048).to.equal(baselineThreshold * 64n); + + await expect( + clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: thresholdAt2048 - 1n } + ) + ).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + + await expect( + clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: thresholdAt2048 } + ) + ).to.emit(clusters, Events.CLUSTER_REACTIVATED); + }); + + it("Migrates a liquidated SSV cluster to ETH without requiring an EB snapshot", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, ssvCluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const migrateReceipt = await migrateTx.wait(); + const clusterAfterMigration = parseClusterFromEvent(clusters, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + + await expect(migrateTx).to.emit(clusters, Events.CLUSTER_MIGRATED_TO_ETH); + expect(clusterAfterMigration.active).to.equal(true); + expect(clusterAfterMigration.balance).to.equal(DEFAULT_ETH_REGISTER_VALUE); + + await assertOperatorVUnits(clusters, operatorIds, 0n, 10_000n); + }); + + it("Migrates a liquidated SSV cluster to ETH using the stored EB snapshot when present", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + + const publicKey = makePublicKey(1); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await clusters.mockSetClusterVUnits(clusterId, 12_000n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(12_000n); + + const liquidateTx = await clusters.liquidateSSV(clusterOwner.address, operatorIds, ssvCluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + const migrateTx = await clusters.migrateClusterToETH( + operatorIds, + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await migrateTx.wait(); + + await assertOperatorVUnits(clusters, operatorIds, 2_000n, 12_000n); + }); + + it("Maintains daoTotalEthVUnits consistency through liquidation/reactivation", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const cluster = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("10")); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 1000; + await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, cluster, effectiveBalance, 1); + const initialDaoVUnits = await clusters.getDaoTotalEthVUnits(); + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + const baselineVUnits = cluster.validatorCount * BPS_DENOMINATOR; + const expectedDeviation = clusterVUnits > baselineVUnits ? clusterVUnits - baselineVUnits : 0n; + const totalDeviationToSubtract = expectedDeviation * BigInt(operatorIds.length); + const expectedAfterLiquidation = totalDeviationToSubtract > initialDaoVUnits ? 0n : initialDaoVUnits - totalDeviationToSubtract; + const liquidateTx = await clusters.liquidate( + clusterOwner.address, + operatorIds, + cluster + ); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + const afterLiquidation = await clusters.getDaoTotalEthVUnits(); + expect(afterLiquidation).to.equal(expectedAfterLiquidation); + const reactivateTx = await clusters.reactivate( + operatorIds, + liquidatedCluster, + { value: ethers.parseEther("20") } + ); + await reactivateTx.wait(); + const afterReactivation = await clusters.getDaoTotalEthVUnits(); + expect(afterReactivation).to.equal(initialDaoVUnits); + const finalClusterVUnits = await clusters.getClusterVUnits(clusterId); + expect(finalClusterVUnits).to.equal(clusterVUnits); + const ebSnapshotAfterReactivation = await clusters.getClusterVUnits(clusterId); + let expectedVUnits = ((BigInt(effectiveBalance) * BPS_DENOMINATOR) + 31n) / 32n; + expect(ebSnapshotAfterReactivation).to.equal(expectedVUnits); + expectedVUnits = ((BigInt(effectiveBalance) * BPS_DENOMINATOR) + 31n) / 32n; + expect(finalClusterVUnits).to.equal(expectedVUnits, "EB vUnits should match original effective balance calculation"); + const finalBaselineVUnits = liquidatedCluster.validatorCount * BPS_DENOMINATOR; + const finalDeviation = finalClusterVUnits > finalBaselineVUnits ? finalClusterVUnits - finalBaselineVUnits : 0n; + expect(finalDeviation).to.equal(expectedDeviation, "Deviation should be preserved through liquidation/reactivation"); + await assertOperatorVUnits(clusters, operatorIds, finalDeviation); + }); + + it("Maintains accounting consistency across multiple liquidation/reactivation cycles", async function () { + const operatorFee = 5_000_000_000n; + const deployFixture = async () => ssvClustersHarnessFixture(connection, 4, operatorFee); + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("10")); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { cluster: clusterAfterEB } = await mockEBAndUpdate(clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 96, 1); + + const clusterVUnits = await clusters.getClusterVUnits(clusterId); + const baselineVUnits = clusterAfterEB.validatorCount * BPS_DENOMINATOR; + const expectedDeviation = clusterVUnits - baselineVUnits; + const initialDaoVUnits = await clusters.getDaoTotalEthVUnits(); + + expect(clusterVUnits).to.equal(3n * BPS_DENOMINATOR); + expect(expectedDeviation).to.equal(2n * BPS_DENOMINATOR); + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation); + + await networkHelpers.mine(200); + const liquidateTx1 = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB); + const liquidateReceipt1 = await liquidateTx1.wait(); + const clusterAfterLiquidation1 = parseClusterFromEvent(clusters, liquidateReceipt1, Events.CLUSTER_LIQUIDATED); + + expect(clusterAfterLiquidation1.active).to.equal(false); + expect(clusterAfterLiquidation1.balance).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + await assertOperatorVUnits(clusters, operatorIds, 0n); + + const cycle1Deposit = ethers.parseEther("3"); + const reactivateTx1 = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation1, + { value: cycle1Deposit } + ); + const reactivateReceipt1 = await reactivateTx1.wait(); + const clusterAfterReactivation1 = parseClusterFromEvent(clusters, reactivateReceipt1, Events.CLUSTER_REACTIVATED); + + expect(clusterAfterReactivation1.active).to.equal(true); + expect(clusterAfterReactivation1.balance).to.equal(cycle1Deposit); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(initialDaoVUnits); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(clusterVUnits); + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation); + + await networkHelpers.mine(200); + const liquidateTx2 = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterReactivation1); + const liquidateReceipt2 = await liquidateTx2.wait(); + const clusterAfterLiquidation2 = parseClusterFromEvent(clusters, liquidateReceipt2, Events.CLUSTER_LIQUIDATED); + + expect(clusterAfterLiquidation2.active).to.equal(false); + expect(clusterAfterLiquidation2.balance).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + await assertOperatorVUnits(clusters, operatorIds, 0n); + + const cycle2Deposit = ethers.parseEther("7"); + const reactivateTx2 = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation2, + { value: cycle2Deposit } + ); + const reactivateReceipt2 = await reactivateTx2.wait(); + const clusterAfterReactivation2 = parseClusterFromEvent(clusters, reactivateReceipt2, Events.CLUSTER_REACTIVATED); + + expect(clusterAfterReactivation2.active).to.equal(true); + expect(clusterAfterReactivation2.balance).to.equal(cycle2Deposit); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(initialDaoVUnits); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(clusterVUnits); + await assertOperatorVUnits(clusters, operatorIds, expectedDeviation); + }); + + it("Accrues operator earnings across cycles without double-counting", async function () { + const operatorFee = 10_000_000_000n; + const activeBlocksPerCycle = 120; + const deployFixture = async () => ssvClustersHarnessFixture(connection, 4, operatorFee); + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployFixture); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds, 1, ethers.parseEther("10")); + const trackedOperator = operatorIds[0]; + const initialEarnings = await getOperatorEthEarnings(clusters, trackedOperator); + + await networkHelpers.mine(activeBlocksPerCycle); + const liquidateTx1 = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterRegister); + const liquidateReceipt1 = await liquidateTx1.wait(); + const clusterAfterLiquidation1 = parseClusterFromEvent(clusters, liquidateReceipt1, Events.CLUSTER_LIQUIDATED); + const earningsAfterLiquidation1 = await getOperatorEthEarnings(clusters, trackedOperator); + const cycle1Increment = earningsAfterLiquidation1 - initialEarnings; + expect(cycle1Increment).to.be.gt(0n); + + await networkHelpers.mine(300); + const reactivateTx1 = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation1, + { value: ethers.parseEther("4") } + ); + const reactivateReceipt1 = await reactivateTx1.wait(); + const clusterAfterReactivation1 = parseClusterFromEvent(clusters, reactivateReceipt1, Events.CLUSTER_REACTIVATED); + const earningsAfterReactivation1 = await getOperatorEthEarnings(clusters, trackedOperator); + expect(earningsAfterReactivation1).to.equal(earningsAfterLiquidation1); + + await networkHelpers.mine(activeBlocksPerCycle); + const liquidateTx2 = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterReactivation1); + const liquidateReceipt2 = await liquidateTx2.wait(); + const clusterAfterLiquidation2 = parseClusterFromEvent(clusters, liquidateReceipt2, Events.CLUSTER_LIQUIDATED); + const earningsAfterLiquidation2 = await getOperatorEthEarnings(clusters, trackedOperator); + const cycle2Increment = earningsAfterLiquidation2 - earningsAfterReactivation1; + + expect(cycle2Increment).to.equal(cycle1Increment); + expect(earningsAfterLiquidation2 - initialEarnings).to.equal(cycle1Increment + cycle2Increment); + + await networkHelpers.mine(200); + const reactivateTx2 = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation2, + { value: ethers.parseEther("5") } + ); + await reactivateTx2.wait(); + const earningsAfterReactivation2 = await getOperatorEthEarnings(clusters, trackedOperator); + expect(earningsAfterReactivation2).to.equal(earningsAfterLiquidation2); + }); + + it("EB=64 liquidate/reactivate then EB=128 update uses reactivated snapshot safely", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const { cluster: clusterAfterEB64 } = await mockEBAndUpdate( + clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 64, 1 + ); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB64); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, await liquidateTx.wait(), Events.CLUSTER_LIQUIDATED); + + const reactivateTx = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const clusterAfterReactivation = parseClusterFromEvent(clusters, await reactivateTx.wait(), Events.CLUSTER_REACTIVATED); + + const { cluster: clusterAfterEB128 } = await mockEBAndUpdate( + clusters, clusterOwner.address, operatorIds, clusterAfterReactivation, 128, 2 + ); + + expect(clusterAfterEB128.active).to.equal(true); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(40000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(40000n); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(30000n); + } + }); + + it("validator removal works after EB=64 liquidate/reactivate cycle", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const validatorPublicKey = makePublicKey(9001); + const registerTx = await clusters.registerValidator( + validatorPublicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const clusterAfterRegister = parseClusterFromEvent(clusters, await registerTx.wait(), Events.VALIDATOR_ADDED); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const { cluster: clusterAfterEB64 } = await mockEBAndUpdate( + clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 64, 1 + ); + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB64); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, await liquidateTx.wait(), Events.CLUSTER_LIQUIDATED); + const reactivateTx = await clusters.reactivate( + operatorIds, + clusterAfterLiquidation, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const clusterAfterReactivation = parseClusterFromEvent(clusters, await reactivateTx.wait(), Events.CLUSTER_REACTIVATED); + + const removeTx = await clusters.removeValidator(validatorPublicKey, operatorIds, clusterAfterReactivation); + const clusterAfterRemove = parseClusterFromEvent(clusters, await removeTx.wait(), Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("deposit before reactivation on explicit-EB cluster preserves deposited balance", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + const { cluster: clusterAfterEB64 } = await mockEBAndUpdate( + clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 64, 1 + ); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB64); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, await liquidateTx.wait(), Events.CLUSTER_LIQUIDATED); + + const preReactivateDeposit = ethers.parseEther("2"); + const depositTx = await clusters.deposit( + clusterOwner.address, + operatorIds, + clusterAfterLiquidation, + { value: preReactivateDeposit } + ); + const clusterAfterDeposit = parseClusterFromEvent(clusters, await depositTx.wait(), Events.CLUSTER_DEPOSITED); + + const reactivateValue = DEFAULT_ETH_REGISTER_VALUE; + const reactivateTx = await clusters.reactivate( + operatorIds, + clusterAfterDeposit, + { value: reactivateValue } + ); + const clusterAfterReactivation = parseClusterFromEvent(clusters, await reactivateTx.wait(), Events.CLUSTER_REACTIVATED); + + expect(clusterAfterReactivation.active).to.equal(true); + expect(clusterAfterReactivation.balance).to.equal(clusterAfterDeposit.balance + reactivateValue); + }); + + it("daoTotalEthVUnits is decremented exactly on explicit-EB liquidation", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + const { cluster: clusterAfterEB64 } = await mockEBAndUpdate( + clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 64, 1 + ); + + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB64); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); + + it("daoTotalEthVUnits is restored exactly on explicit-EB reactivation", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0n); + await clusters.mockMinimumBlocksBeforeLiquidation(1n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const clusterAfterRegister = await registerAndParseCluster(clusters, operatorIds); + const { cluster: clusterAfterEB64 } = await mockEBAndUpdate( + clusters, clusterOwner.address, operatorIds, clusterAfterRegister, 64, 1 + ); + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB64); + const clusterAfterLiquidation = parseClusterFromEvent(clusters, await liquidateTx.wait(), Events.CLUSTER_LIQUIDATED); + + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + await clusters.reactivate(operatorIds, clusterAfterLiquidation, { value: DEFAULT_ETH_REGISTER_VALUE }); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + }); +}); diff --git a/test/unit/SSVClusters/removedOperatorImpact.test.ts b/test/unit/SSVClusters/removedOperatorImpact.test.ts new file mode 100644 index 000000000..25c613407 --- /dev/null +++ b/test/unit/SSVClusters/removedOperatorImpact.test.ts @@ -0,0 +1,161 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, makePublicKey, parseClusterFromEvent } from "../../common/helpers.ts"; +import { DEFAULT_SHARES, DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; + +const ETH_OPERATOR_FEE = 10_000_000_000n; +const SSV_OPERATOR_FEE = DEDUCTED_DIGITS; + +describe("Removed operator impact on active cluster accounting", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + const deployClustersWithEthFee = async () => { + return ssvClustersHarnessFixture(connection, 4, ETH_OPERATOR_FEE); + }; + + const deployClusters = async () => { + return ssvClustersHarnessFixture(connection, 4); + }; + + it("excludes removed operator fees from ETH cluster settlement and freezes removed operator ETH earnings", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithEthFee); + + await clusters.mockEthNetworkFee(0); + + const registerTx = await clusters.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: connection.ethers.parseEther("100") } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + const removedOperatorId = operatorIds[2]; + await clusters.mockRemoveOperator(removedOperatorId); + + const activeOperatorIds = operatorIds.filter((id) => id !== removedOperatorId); + + const ethSnapshotsBefore = new Map(); + for (const operatorId of operatorIds) { + const [, blockNumber, balance] = await clusters.getOperatorEthSnapshot(operatorId); + ethSnapshotsBefore.set(operatorId, { + blockNumber: BigInt(blockNumber), + balance: BigInt(balance), + }); + } + + await networkHelpers.mine(50); + + const removeTx = await clusters.connect(clusterOwner).removeValidator( + makePublicKey(1), + operatorIds, + clusterAfterRegister + ); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + const receiptBlock = BigInt(removeReceipt!.blockNumber); + const packedEthFee = ETH_OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + + let totalOperatorFeeRaw = 0n; + for (const operatorId of activeOperatorIds) { + const before = ethSnapshotsBefore.get(operatorId)!; + const [, blockAfter, balanceAfter] = await clusters.getOperatorEthSnapshot(operatorId); + + const expectedDeltaRaw = packedEthFee * (receiptBlock - before.blockNumber); + const actualDeltaRaw = BigInt(balanceAfter) - before.balance; + + expect(actualDeltaRaw).to.equal(expectedDeltaRaw); + expect(BigInt(blockAfter)).to.equal(receiptBlock); + + totalOperatorFeeRaw += expectedDeltaRaw; + } + + const removedBefore = ethSnapshotsBefore.get(removedOperatorId)!; + const [, removedBlockAfter, removedBalanceAfter] = await clusters.getOperatorEthSnapshot(removedOperatorId); + expect(BigInt(removedBlockAfter)).to.equal(removedBefore.blockNumber); + expect(BigInt(removedBalanceAfter)).to.equal(removedBefore.balance); + expect(BigInt(removedBalanceAfter)).to.equal(0n); + + const expectedClusterFeeDeduction = totalOperatorFeeRaw * ETH_DEDUCTED_DIGITS; + expect(clusterAfterRegister.balance - clusterAfterRemove.balance).to.equal(expectedClusterFeeDeduction); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + }); + + it("freezes removed operator SSV earnings while active operators continue earning on SSV cluster settlement", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClusters); + + await clusters.mockSSVNetworkFee(0); + await clusters.mockCurrentNetworkFeeIndexSSV(0); + + const ssvCluster = createCluster({ + validatorCount: 1n, + balance: 0n, + }); + + await clusters.mockRegisterSSVValidator( + makePublicKey(100), + operatorIds, + clusterOwner.address, + ssvCluster + ); + + for (const operatorId of operatorIds) { + await clusters.mockOperatorSSVFee(operatorId, SSV_OPERATOR_FEE); + } + + const removedOperatorId = operatorIds[1]; + await clusters.mockRemoveOperator(removedOperatorId); + + const ssvSnapshotsBefore = new Map(); + for (const operatorId of operatorIds) { + const [, blockNumber, balance] = await clusters.getOperatorSnapshot(operatorId); + ssvSnapshotsBefore.set(operatorId, { + blockNumber: BigInt(blockNumber), + balance: BigInt(balance), + }); + } + + await networkHelpers.mine(30); + + const liquidateTx = await clusters.connect(clusterOwner).liquidateSSV( + clusterOwner.address, + operatorIds, + ssvCluster + ); + const liquidateReceipt = await liquidateTx.wait(); + const receiptBlock = BigInt(liquidateReceipt!.blockNumber); + const packedSsvFee = SSV_OPERATOR_FEE / DEDUCTED_DIGITS; + + const activeOperatorIds = operatorIds.filter((id) => id !== removedOperatorId); + for (const operatorId of activeOperatorIds) { + const before = ssvSnapshotsBefore.get(operatorId)!; + const [, blockAfter, balanceAfter] = await clusters.getOperatorSnapshot(operatorId); + + const expectedDeltaRaw = packedSsvFee * (receiptBlock - before.blockNumber); + const actualDeltaRaw = BigInt(balanceAfter) - before.balance; + + expect(actualDeltaRaw).to.equal(expectedDeltaRaw); + expect(actualDeltaRaw).to.be.greaterThan(0n); + expect(BigInt(blockAfter)).to.equal(receiptBlock); + } + + const removedBefore = ssvSnapshotsBefore.get(removedOperatorId)!; + const [, removedBlockAfter, removedBalanceAfter] = await clusters.getOperatorSnapshot(removedOperatorId); + expect(BigInt(removedBlockAfter)).to.equal(removedBefore.blockNumber); + expect(BigInt(removedBalanceAfter)).to.equal(removedBefore.balance); + expect(BigInt(removedBalanceAfter)).to.equal(0n); + }); +}); diff --git a/test/unit/SSVClusters/run-tests.sh b/test/unit/SSVClusters/run-tests.sh new file mode 100755 index 000000000..fd5f9abd1 --- /dev/null +++ b/test/unit/SSVClusters/run-tests.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Move to repo root +cd "$(dirname "${BASH_SOURCE[0]}")/../../.." + +pattern="test/unit/SSVClusters/*.test.ts" + +npx hardhat test $pattern "$@" diff --git a/test/unit/SSVClusters/updateClusterBalance.test.ts b/test/unit/SSVClusters/updateClusterBalance.test.ts new file mode 100644 index 000000000..5d9b5a223 --- /dev/null +++ b/test/unit/SSVClusters/updateClusterBalance.test.ts @@ -0,0 +1,931 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture, getClustersHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, extractEventArgs, makePublicKey, parseClusterFromEvent, registerAndParseCluster } from "../../common/helpers.ts"; +import { computeClusterId, computeEBRoot } from "../../helpers/oracle.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { ethers } from "ethers"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +const OPERATOR_FEE = 10_000_000_000n; + +type ClusterType = ReturnType; + +describe("SSVClusters function `updateClusterBalance()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + let deployClustersWith13Operators!: ReturnType; + let deployClustersWith13OperatorsAutoLiq!: () => Promise<{ clusters: any; operatorIds: bigint[] }>; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, otherAccount] } = await setupTestContext()); + deployClustersWith13Operators = getClustersHarnessFixture(connection, 13); + deployClustersWith13OperatorsAutoLiq = () => ssvClustersHarnessFixture(connection, 13, OPERATOR_FEE); + }); + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return defaultClustersFixture(connection); + }; + + + + it("Is reverted with 'RootNotFound' when EB root is missing for the provided block", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + + await expect(clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + cluster, + 32, + [] + )).to.be.revertedWithCustomError(clusters, Errors.ROOT_NOT_FOUND); + }); + + it("Is reverted with 'MustUseLatestRoot' when provided root is not the latest committed root", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const staleBlockNum = 1; + const latestBlockNum = 2; + const staleEffectiveBalance = 32; + const latestEffectiveBalance = 33; + + await clusters.mockSetEBRoot(staleBlockNum, computeEBRoot(clusterId, staleEffectiveBalance)); + await clusters.mockSetEBRoot(latestBlockNum, computeEBRoot(clusterId, latestEffectiveBalance)); + + await expect(clusters.updateClusterBalance( + staleBlockNum, + clusterOwner.address, + operatorIds, + cluster, + staleEffectiveBalance, + [] + )).to.be.revertedWithCustomError(clusters, Errors.MUST_USE_LATEST_ROOT); + }); + + it("Updates cluster balance when proof is valid", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + + const blockNum = 1; + const effectiveBalance = 32; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root = computeEBRoot(clusterId, effectiveBalance); + + await clusters.mockSetEBRoot(blockNum, root); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(BPS_DENOMINATOR); + } + + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.UPDATE_CLUSTER_BALANCE]); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(eventArgs.owner).to.equal(clusterOwner.address); + expect(eventArgs.operatorIds).to.deep.equal(operatorIds); + expect(eventArgs.blockNum).to.equal(BigInt(blockNum)); + expect(eventArgs.effectiveBalance).to.equal(effectiveBalance); + + const clusterAfter = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfter.active).to.equal(true); + expect(clusterAfter.validatorCount).to.equal(cluster.validatorCount); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(BPS_DENOMINATOR); + } + }); + + it("Updates operator ETH vUnits when effective balance changes", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + + const blockNum = 1; + const effectiveBalance = 33; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + const vUnitsPerValidator = 32n; + const newVUnits = ((BigInt(effectiveBalance) * BPS_DENOMINATOR) + vUnitsPerValidator - 1n) / vUnitsPerValidator; + + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const receipt = await tx.wait(); + + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(eventArgs.effectiveBalance).to.equal(effectiveBalance); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(newVUnits); + for (const operatorId of operatorIds) { + const deviation = newVUnits - BPS_DENOMINATOR; + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(deviation); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(newVUnits); + } + }); + + it("Is reverted with 'InvalidProof' when merkle proof is invalid", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + + const blockNum = 1; + const effectiveBalance = 32; + + await clusters.mockSetEBRoot(blockNum, ethers.keccak256("0x1234")); + + await expect(clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + )).to.be.revertedWithCustomError(clusters, Errors.INVALID_PROOF); + }); + + it("Is reverted with 'EBExceedsMaximum' when effective balance exceeds 2048 ETH per validator", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + + const blockNum = 1; + const effectiveBalance = 2049; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + await expect(clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + )).to.be.revertedWithCustomError(clusters, Errors.EB_EXCEEDS_MAXIMUM); + }); + + it("Accepts EB at exactly maximum (2048 ETH per 1 validator) and produces 640000 vUnits", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const blockNum = 1; + const effectiveBalance = 2048; + + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const receipt = await tx.wait(); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(eventArgs.effectiveBalance).to.equal(effectiveBalance); + + const expectedVUnits = 640_000n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + + const expectedDeviation = 630_000n; + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedDeviation); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(expectedVUnits); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(expectedVUnits); + }); + + it("Accepts EB at exactly maximum for 2-validator cluster (4096 ETH) and produces 1,280,000 vUnits", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfter1 = await registerAndParseCluster(clusters, operatorIds); + + const registerTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfter1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt2 = await registerTx2.wait(); + const clusterWith2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const blockNum = 1; + const effectiveBalance = 4096; + + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + clusterWith2, + effectiveBalance, + [] + ); + await tx.wait(); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + + const expectedVUnits = 1_280_000n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + + const expectedDeviation = 1_260_000n; + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedDeviation); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(expectedVUnits); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(expectedVUnits); + }); + + it("Is reverted with 'EBExceedsMaximum' when EB exceeds 2048 ETH per validator for a 2-validator cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfter1 = await registerAndParseCluster(clusters, operatorIds); + + const registerTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfter1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt2 = await registerTx2.wait(); + const clusterWith2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const blockNum = 1; + const effectiveBalance = 4097; + + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + await expect(clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + clusterWith2, + effectiveBalance, + [] + )).to.be.revertedWithCustomError(clusters, Errors.EB_EXCEEDS_MAXIMUM); + }); + + it("Is reverted with 'StaleUpdate' when blockNum is not increasing", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + + const blockNum = 1; + const effectiveBalance = 32; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + const tx1 = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const receipt1 = await tx1.wait(); + const clusterAfter1 = parseClusterFromEvent(clusters, receipt1, Events.CLUSTER_BALANCE_UPDATED); + + await expect(clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + clusterAfter1, + effectiveBalance, + [] + )).to.be.revertedWithCustomError(clusters, Errors.STALE_UPDATE); + }); + + it("Is reverted with 'UpdateTooFrequent' when a second EB update is within the cooldown window", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 32; + + await clusters.mockSetMinBlocksBetweenUpdates(5); + await clusters.mockSetEBRoot(1, computeEBRoot(clusterId, effectiveBalance)); + + const tx1 = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const receipt1 = await tx1.wait(); + const clusterAfter1 = parseClusterFromEvent(clusters, receipt1, Events.CLUSTER_BALANCE_UPDATED); + + await clusters.mockSetEBRoot(2, computeEBRoot(clusterId, effectiveBalance)); + + await expect(clusters.updateClusterBalance( + 2, + clusterOwner.address, + operatorIds, + clusterAfter1, + effectiveBalance, + [] + )).to.be.revertedWithCustomError(clusters, Errors.UPDATE_TOO_FREQUENT); + }); + + it("Allows a second EB update after the cooldown window passes", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 32; + + await clusters.mockSetMinBlocksBetweenUpdates(3); + await clusters.mockSetEBRoot(1, computeEBRoot(clusterId, effectiveBalance)); + + const tx1 = await clusters.updateClusterBalance( + 1, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const receipt1 = await tx1.wait(); + const clusterAfter1 = parseClusterFromEvent(clusters, receipt1, Events.CLUSTER_BALANCE_UPDATED); + + await networkHelpers.mine(3); + + await clusters.mockSetEBRoot(2, computeEBRoot(clusterId, effectiveBalance)); + + const tx2 = await clusters.updateClusterBalance( + 2, + clusterOwner.address, + operatorIds, + clusterAfter1, + effectiveBalance, + [] + ); + const receipt2 = await tx2.wait(); + + await expect(tx2).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const eventArgs = extractEventArgs(clusters, receipt2, Events.CLUSTER_BALANCE_UPDATED); + expect(eventArgs.blockNum).to.equal(2n); + expect(eventArgs.effectiveBalance).to.equal(effectiveBalance); + }); + + it("Updates only EB snapshot for SSV clusters (no ETH operator vUnits accounting)", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + await clusters.mockRegisterSSVValidator(makePublicKey(1), operatorIds, clusterOwner.address, ssvCluster); + + const blockNum = 1; + const effectiveBalance = 32; + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getClusterHash(clusterId)).to.equal(ethers.ZeroHash); + + await (await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + ssvCluster, + effectiveBalance, + [] + )).wait(); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(BPS_DENOMINATOR); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getClusterHash(clusterId)).to.equal(ethers.ZeroHash); + }); + + it("Succeeds on a liquidated cluster: updates EB snapshot but skips fee settlement and vUnit updates", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, cluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.equal(false); + expect(liquidatedCluster.balance).to.equal(0n); + + const operatorVUnitsBefore = await clusters.getOperatorEthVUnits(operatorIds[0]); + expect(operatorVUnitsBefore).to.equal(0n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + + const blockNum = 1; + const effectiveBalance = 33; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + liquidatedCluster, + effectiveBalance, + [] + ); + const receipt = await tx.wait(); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(eventArgs.effectiveBalance).to.equal(effectiveBalance); + + const clusterAfter = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfter.active).to.equal(false); + expect(clusterAfter.balance).to.equal(0n); + + const expectedVUnits = (BigInt(effectiveBalance) * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(operatorVUnitsBefore); + }); + + it("EB update on insolvent liquidated cluster does not corrupt operator or DAO vUnit accounting", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const blockNum1 = 1; + const effectiveBalance1 = 64; + const root1 = computeEBRoot(clusterId, effectiveBalance1); + await clusters.mockSetEBRoot(blockNum1, root1); + + const tx1 = await clusters.updateClusterBalance( + blockNum1, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance1, + [] + ); + const receipt1 = await tx1.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, receipt1, Events.CLUSTER_BALANCE_UPDATED); + const deviationAfterEBUpdate = 10000n; + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(deviationAfterEBUpdate); + } + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.equal(false); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + const daoVUnitsAfterLiquidation = await clusters.getDaoTotalEthVUnits(); + const blockNum2 = 2; + const effectiveBalance2 = 128; + const root2 = computeEBRoot(clusterId, effectiveBalance2); + await clusters.mockSetEBRoot(blockNum2, root2); + + const tx2 = await clusters.updateClusterBalance( + blockNum2, + clusterOwner.address, + operatorIds, + liquidatedCluster, + effectiveBalance2, + [] + ); + const receipt2 = await tx2.wait(); + await expect(tx2).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const clusterAfterUpdate = parseClusterFromEvent(clusters, receipt2, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfterUpdate.active).to.equal(false); + expect(clusterAfterUpdate.balance).to.equal(0n); + const expectedVUnits2 = (BigInt(effectiveBalance2) * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits2); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(daoVUnitsAfterLiquidation); + }); + + it("Is reverted with 'EBBelowMinimum' when effective balance is below 32 ETH per validator", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterAfter1 = await registerAndParseCluster(clusters, operatorIds); + + const registerTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfter1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt2 = await registerTx2.wait(); + const clusterAfter2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const blockNum = 1; + const effectiveBalance = 60; + + const clusterId = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwner.address, operatorIds]) + ); + const coder = ethers.AbiCoder.defaultAbiCoder(); + const innerHash = ethers.keccak256(coder.encode(["bytes32", "uint32"], [clusterId, effectiveBalance])); + const root = ethers.keccak256(ethers.solidityPacked(["bytes32"], [innerHash])); + await clusters.mockSetEBRoot(blockNum, root); + + await expect(clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + clusterAfter2, + effectiveBalance, + [] + )).to.be.revertedWithCustomError(clusters, Errors.EB_BELOW_MINIMUM); + }); + + it("Multi-validator liquidated cluster: EB update preserves per-validator vUnit accounting", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const clusterAfter1 = await registerAndParseCluster(clusters, operatorIds); + + const registerTx2 = await clusters.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfter1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt2 = await registerTx2.wait(); + const clusterWith2Validators = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterWith2Validators); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.equal(false); + expect(liquidatedCluster.validatorCount).to.equal(2n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const blockNum = 1; + const effectiveBalance = 66; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + liquidatedCluster, + effectiveBalance, + [] + ); + const receipt = await tx.wait(); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const clusterAfter = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfter.active).to.equal(false); + expect(clusterAfter.balance).to.equal(0n); + expect(clusterAfter.validatorCount).to.equal(2n); + const expectedVUnits = (BigInt(effectiveBalance) * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + }); + + it("EB decrease on liquidated cluster: updates snapshot without corrupting accounting", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const blockNum1 = 1; + const effectiveBalance1 = 64; + const root1 = computeEBRoot(clusterId, effectiveBalance1); + await clusters.mockSetEBRoot(blockNum1, root1); + + const tx1 = await clusters.updateClusterBalance( + blockNum1, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance1, + [] + ); + const receipt1 = await tx1.wait(); + const clusterAfterEBIncrease = parseClusterFromEvent(clusters, receipt1, Events.CLUSTER_BALANCE_UPDATED); + const deviationAfterIncrease = 10000n; + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(deviationAfterIncrease); + } + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEBIncrease); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.equal(false); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + const daoVUnitsAfterLiquidation = await clusters.getDaoTotalEthVUnits(); + const blockNum2 = 2; + const effectiveBalance2 = 40; + const root2 = computeEBRoot(clusterId, effectiveBalance2); + await clusters.mockSetEBRoot(blockNum2, root2); + + const tx2 = await clusters.updateClusterBalance( + blockNum2, + clusterOwner.address, + operatorIds, + liquidatedCluster, + effectiveBalance2, + [] + ); + const receipt2 = await tx2.wait(); + + await expect(tx2).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const clusterAfterDecrease = parseClusterFromEvent(clusters, receipt2, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfterDecrease.active).to.equal(false); + expect(clusterAfterDecrease.balance).to.equal(0n); + const expectedVUnits2 = (BigInt(effectiveBalance2) * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits2); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(daoVUnitsAfterLiquidation); + }); + + it("Liquidated cluster with implicit EB: first updateClusterBalance transitions to explicit tracking", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, cluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + expect(liquidatedCluster.active).to.equal(false); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + const blockNum = 1; + const effectiveBalance = 35; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + const tx = await clusters.updateClusterBalance( + blockNum, + clusterOwner.address, + operatorIds, + liquidatedCluster, + effectiveBalance, + [] + ); + const receipt = await tx.wait(); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const clusterAfter = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfter.active).to.equal(false); + expect(clusterAfter.balance).to.equal(0n); + const expectedVUnits = (BigInt(effectiveBalance) * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + }); + + it("EB update with effectiveBalance = 0 on zero-validator cluster succeeds without modifying vUnit state", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const removeTx = await clusters.removeValidator(makePublicKey(1), operatorIds, cluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + + const daoVUnitsBefore = await clusters.getDaoTotalEthVUnits(); + + const blockNum = 1; + const root = computeEBRoot(clusterId, 0); + await clusters.mockSetEBRoot(blockNum, root); + + const tx = await clusters.updateClusterBalance( + blockNum, clusterOwner.address, operatorIds, clusterAfterRemove, 0, [] + ); + const receipt = await tx.wait(); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(eventArgs.effectiveBalance).to.equal(0); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(daoVUnitsBefore); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + }); + + it("Oracle EB report effectiveBalance = 0 on active zero-validator cluster resets explicit EB to implicit-EB sentinel", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + await clusters.mockEthNetworkFee(0n); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const blockNum1 = 1; + const effectiveBalance1 = 64; + const root1 = computeEBRoot(clusterId, effectiveBalance1); + await clusters.mockSetEBRoot(blockNum1, root1); + + const ebTx1 = await clusters.updateClusterBalance( + blockNum1, clusterOwner.address, operatorIds, cluster, effectiveBalance1, [] + ); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + const expectedVUnits = (64n * BPS_DENOMINATOR + 32n - 1n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + + const removeTx = await clusters.removeValidator(makePublicKey(1), operatorIds, clusterAfterEB); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + const daoVUnitsAfterRemove = await clusters.getDaoTotalEthVUnits(); + + const blockNum2 = 2; + const root2 = computeEBRoot(clusterId, 0); + await clusters.mockSetEBRoot(blockNum2, root2); + + const tx = await clusters.updateClusterBalance( + blockNum2, clusterOwner.address, operatorIds, clusterAfterRemove, 0, [] + ); + const receipt = await tx.wait(); + + await expect(tx).to.emit(clusters, Events.CLUSTER_BALANCE_UPDATED); + const eventArgs = extractEventArgs(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(eventArgs.effectiveBalance).to.equal(0); + + const clusterAfterEB0 = parseClusterFromEvent(clusters, receipt, Events.CLUSTER_BALANCE_UPDATED); + expect(clusterAfterEB0.active).to.equal(true); + expect(clusterAfterEB0.validatorCount).to.equal(0n); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(daoVUnitsAfterRemove); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + }); + + it("Updates vUnit accounting correctly for 13 operators at maximum EB (2048 ETH per validator)", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const effectiveBalance = 2048; + const blockNum = 1; + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + + await clusters.updateClusterBalance(blockNum, clusterOwner.address, operatorIds, cluster, effectiveBalance, []); + + const expectedVUnits = (BigInt(effectiveBalance) * BPS_DENOMINATOR + 32n - 1n) / 32n; + const expectedDeviation = expectedVUnits - BPS_DENOMINATOR; + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(expectedVUnits); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(expectedDeviation); + expect(await clusters.getEffectiveOperatorVUnits(operatorId)).to.equal(expectedVUnits); + } + }); + it("Auto-liquidates cluster with 13 operators when EB increase to maximum makes it insolvent", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13OperatorsAutoLiq); + + await clusters.mockEthNetworkFee(100_000n); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const depositValue = ethers.parseEther("0.0001"); + const regTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue } + ); + const regReceipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(clusters, regReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const root1 = computeEBRoot(clusterId, 32); + await clusters.mockSetEBRoot(1, root1); + const ebTx1 = await clusters.updateClusterBalance(1, clusterOwner.address, operatorIds, clusterAfterReg, 32, []); + const ebReceipt1 = await ebTx1.wait(); + const clusterAfterEB32 = parseClusterFromEvent(clusters, ebReceipt1, Events.CLUSTER_BALANCE_UPDATED); + + expect(clusterAfterEB32.active).to.equal(true); + await expect( + clusters.connect(otherAccount).liquidate(clusterOwner.address, operatorIds, clusterAfterEB32) + ).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + + const root2 = computeEBRoot(clusterId, 2048); + await clusters.mockSetEBRoot(2, root2); + const ebTx2 = await clusters.updateClusterBalance(2, clusterOwner.address, operatorIds, clusterAfterEB32, 2048, []); + const ebReceipt2 = await ebTx2.wait(); + const clusterAfterEB2048 = parseClusterFromEvent(clusters, ebReceipt2, Events.CLUSTER_LIQUIDATED); + + expect(clusterAfterEB2048.active).to.equal(false); + expect(clusterAfterEB2048.balance).to.equal(0n); + + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); +}); diff --git a/test/unit/SSVClusters/withdraw.test.ts b/test/unit/SSVClusters/withdraw.test.ts new file mode 100644 index 000000000..325a199ae --- /dev/null +++ b/test/unit/SSVClusters/withdraw.test.ts @@ -0,0 +1,395 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultClustersFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, computeClusterId, computeEBRoot, createCluster, extractEventArgs, makePublicKey, parseClusterFromEvent, registerAndParseCluster } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, ETH_DEDUCTED_DIGITS, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { expectETHDeltas } from "../../helpers/balance.ts"; +import { ethers } from "ethers"; + +describe("SSVClusters function `withdraw()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let otherAccount: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, otherAccount] } = await setupTestContext()); + }); + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return defaultClustersFixture(connection); + }; + + const deploySSVClustersWithLowFeesFixture = async () => { + return ssvClustersHarnessFixture(connection, 4, 100_000n); + }; + + + + it("Withdraws from an existing cluster, updates balance and emits correct event", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0); + await clusters.mockCurrentNetworkFeeIndex(0); + + const clusterBeforeWithdraw = await registerAndParseCluster(clusters, operatorIds); + const withdrawAmount = 1n; + + const harnessAddress = await clusters.getAddress(); + + const { receipt: withdrawReceipt } = await expectETHDeltas(connection.ethers.provider, + () => clusters.withdraw(operatorIds, withdrawAmount, clusterBeforeWithdraw), + [ + { address: clusterOwner.address, expectedDelta: withdrawAmount, accountForGas: true }, + { address: harnessAddress, expectedDelta: -withdrawAmount }, + ]); + await trackGasFromReceipt(withdrawReceipt, [GasGroup.WITHDRAW_CLUSTER_BALANCE]); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + const eventArgs = extractEventArgs(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + expect(eventArgs.owner).to.equal(clusterOwner.address); + expect(eventArgs.operatorIds).to.deep.equal(operatorIds); + expect(eventArgs.value).to.equal(withdrawAmount); + + expect(clusterAfterWithdraw.balance).to.equal(clusterBeforeWithdraw.balance - withdrawAmount); + + await expect(clusters.withdraw(operatorIds, 1n, clusterBeforeWithdraw)) + .to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'InsufficientBalance' when withdrawal would make the cluster liquidatable", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0); + await clusters.mockCurrentNetworkFeeIndex(0); + + const clusterBeforeWithdraw = await registerAndParseCluster(clusters, operatorIds); + await clusters.mockMinimumLiquidationCollateral(clusterBeforeWithdraw.balance); + + await expect(clusters.withdraw( + operatorIds, + 1n, + clusterBeforeWithdraw + )).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + }); + + it("Settles full fees when usageUnits exceeds uint64", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersWithLowFeesFixture); + + const clusterBeforeWithdraw = await registerAndParseCluster(clusters, operatorIds); + + await connection.ethers.provider.send("evm_mine", []); + + const maxUint64 = (1n << 64n) - 1n; + await clusters.mockEthNetworkFee(0n); + await clusters.mockCurrentNetworkFeeIndex(maxUint64); + await clusters.mockMinimumBlocksBeforeLiquidation(0n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const withdrawTx = await clusters.withdraw(operatorIds, 0n, clusterBeforeWithdraw); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const units = clusterBeforeWithdraw.validatorCount * BPS_DENOMINATOR; + const idxOp = clusterAfterWithdraw.index - clusterBeforeWithdraw.index; + const idxNet = maxUint64 - clusterBeforeWithdraw.networkFeeIndex; + const usageUnits = (idxOp * units) / BPS_DENOMINATOR + (idxNet * units) / BPS_DENOMINATOR; + const wrappedUsageUnits = usageUnits & maxUint64; + const overflowUnits = usageUnits >> 64n; + const expectedUsageFromWrapped = wrappedUsageUnits + (overflowUnits << 64n); + const expectedBalanceIfUint64Truncated = clusterBeforeWithdraw.balance - wrappedUsageUnits * 100_000n; + + expect(overflowUnits).to.equal(1n); + expect(usageUnits).to.equal(expectedUsageFromWrapped); + expect(expectedBalanceIfUint64Truncated).to.not.equal(clusterAfterWithdraw.balance); + expect(clusterAfterWithdraw.balance).to.equal(0n); + }); + + it("Is reverted with 'IncorrectClusterVersion' when withdrawing from an SSV cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: true, + }; + await clusters.mockRegisterSSVValidator(makePublicKey(1), operatorIds, clusterOwner.address, ssvCluster); + + await expect(clusters.withdraw( + operatorIds, + 1n, + ssvCluster + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Is reverted with 'InsufficientBalance' when withdrawing more than the cluster balance", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeWithdraw = await registerAndParseCluster(clusters, operatorIds); + const excessiveAmount = clusterBeforeWithdraw.balance + 1n; + + await expect(clusters.withdraw( + operatorIds, + excessiveAmount, + clusterBeforeWithdraw + )).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + }); + + it("Is reverted with 'IncorrectClusterState' when provided cluster data is stale or mismatched", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeWithdraw = await registerAndParseCluster(clusters, operatorIds); + + const mismatchedCluster = { + ...clusterBeforeWithdraw, + balance: clusterBeforeWithdraw.balance + 1n, + }; + + await expect(clusters.withdraw( + operatorIds, + 1n, + mismatchedCluster + )).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterDoesNotExists' when a non-owner tries to withdraw", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const clusterBeforeWithdraw = await registerAndParseCluster(clusters, operatorIds); + + await expect(clusters.connect(otherAccount).withdraw( + operatorIds, + 1n, + clusterBeforeWithdraw + )).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Withdraws deposited funds from a liquidated cluster without reactivating", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0); + await clusters.mockCurrentNetworkFeeIndex(0); + await registerAndParseCluster(clusters, operatorIds); + await clusters.mockSetClusterLiquidated(clusterOwner.address, operatorIds); + + const liquidatedCluster = { + validatorCount: 0n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: false, + }; + const depositAmount = ethers.parseEther("0.1"); + const depositTx = await clusters.deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + const depositReceipt = await depositTx.wait(); + const clusterAfterDeposit = parseClusterFromEvent(clusters, depositReceipt, Events.CLUSTER_DEPOSITED); + const harnessAddress = await clusters.getAddress(); + + const { receipt: withdrawReceipt } = await expectETHDeltas(connection.ethers.provider, + () => clusters.withdraw(operatorIds, depositAmount, clusterAfterDeposit), + [ + { address: clusterOwner.address, expectedDelta: depositAmount, accountForGas: true }, + { address: harnessAddress, expectedDelta: -depositAmount }, + ]); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + expect(clusterAfterWithdraw.balance).to.equal(0n); + expect(clusterAfterWithdraw.active).to.equal(false); + }); + + it("Withdraws full balance from a liquidated cluster that received multiple deposits", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(0); + await clusters.mockCurrentNetworkFeeIndex(0); + await registerAndParseCluster(clusters, operatorIds); + await clusters.mockSetClusterLiquidated(clusterOwner.address, operatorIds); + + const liquidatedCluster = { + validatorCount: 0n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: false, + }; + const deposit1 = ethers.parseEther("0.05"); + const deposit2 = ethers.parseEther("0.03"); + + const depositTx1 = await clusters.deposit(clusterOwner.address, operatorIds, liquidatedCluster, { value: deposit1 }); + const receipt1 = await depositTx1.wait(); + const clusterAfterDeposit1 = parseClusterFromEvent(clusters, receipt1, Events.CLUSTER_DEPOSITED); + + const depositTx2 = await clusters.deposit(clusterOwner.address, operatorIds, clusterAfterDeposit1, { value: deposit2 }); + const receipt2 = await depositTx2.wait(); + const clusterAfterDeposit2 = parseClusterFromEvent(clusters, receipt2, Events.CLUSTER_DEPOSITED); + + expect(clusterAfterDeposit2.balance).to.equal(deposit1 + deposit2); + const withdrawTx = await clusters.withdraw(operatorIds, deposit1 + deposit2, clusterAfterDeposit2); + const withdrawReceipt: any = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + expect(clusterAfterWithdraw.balance).to.equal(0n); + expect(clusterAfterWithdraw.active).to.equal(false); + await expect( + clusters.withdraw(operatorIds, 1n, clusterAfterWithdraw) + ).to.be.revertedWithCustomError(clusters, Errors.INSUFFICIENT_BALANCE); + }); + + it("Withdraws from a liquidated zero-validator cluster even with non-zero liquidation settings", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockEthNetworkFee(123_000n); + await clusters.mockMinimumBlocksBeforeLiquidation(50_190n); + await clusters.mockMinimumLiquidationCollateral(94_000n); + await clusters.mockCurrentNetworkFeeIndex(777n); + + await registerAndParseCluster(clusters, operatorIds); + await clusters.mockSetClusterLiquidated(clusterOwner.address, operatorIds); + + const liquidatedCluster = { + validatorCount: 0n, + networkFeeIndex: 0n, + index: 0n, + balance: 0n, + active: false, + }; + + const depositAmount = ethers.parseEther("0.02"); + const depositTx = await clusters.deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + const depositReceipt = await depositTx.wait(); + const clusterAfterDeposit = parseClusterFromEvent(clusters, depositReceipt, Events.CLUSTER_DEPOSITED); + + const withdrawTx = await clusters.withdraw(operatorIds, depositAmount, clusterAfterDeposit); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + expect(clusterAfterWithdraw.active).to.equal(false); + expect(clusterAfterWithdraw.validatorCount).to.equal(0n); + expect(clusterAfterWithdraw.balance).to.equal(0n); + }); + + it("Cluster balance becomes 0 when accumulated fees exceed the remaining balance (no underflow)", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + await clusters.mockMinimumBlocksBeforeLiquidation(0n); + await clusters.mockMinimumLiquidationCollateral(0n); + await clusters.mockEthNetworkFee(0n); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + + const indexToDrainBalance = DEFAULT_ETH_REGISTER_VALUE / ETH_DEDUCTED_DIGITS + 1n; + await clusters.mockCurrentNetworkFeeIndex(indexToDrainBalance); + + const withdrawTx = await clusters.withdraw(operatorIds, 0n, cluster); + const withdrawReceipt: any = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + expect(clusterAfterWithdraw.balance).to.equal(0n); + }); + + it("Withdraws from a liquidated cluster after explicit EB update", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const cluster = await registerAndParseCluster(clusters, operatorIds); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 160; + const ebBlockNum = 1; + + await clusters.mockSetEBRoot(ebBlockNum, computeEBRoot(clusterId, effectiveBalance)); + const ebTx = await clusters.updateClusterBalance( + ebBlockNum, + clusterOwner.address, + operatorIds, + cluster, + effectiveBalance, + [] + ); + const ebReceipt = await ebTx.wait(); + const clusterAfterEB = parseClusterFromEvent(clusters, ebReceipt, Events.CLUSTER_BALANCE_UPDATED); + + const liquidateTx = await clusters.liquidate(clusterOwner.address, operatorIds, clusterAfterEB); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + expect(liquidatedCluster.active).to.equal(false); + expect(liquidatedCluster.validatorCount).to.equal(clusterAfterEB.validatorCount); + + const depositAmount = ethers.parseEther("0.03"); + const depositTx = await clusters.deposit( + clusterOwner.address, + operatorIds, + liquidatedCluster, + { value: depositAmount } + ); + const depositReceipt = await depositTx.wait(); + const clusterAfterDeposit = parseClusterFromEvent(clusters, depositReceipt, Events.CLUSTER_DEPOSITED); + + const withdrawTx = await clusters.withdraw(operatorIds, depositAmount, clusterAfterDeposit); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + expect(clusterAfterWithdraw.active).to.equal(false); + expect(clusterAfterWithdraw.validatorCount).to.equal(clusterAfterEB.validatorCount); + expect(clusterAfterWithdraw.balance).to.equal(0n); + }); + + it("Zero-validator cluster allows full balance withdrawal without fee deduction", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersWithLowFeesFixture); + + await clusters.mockEthNetworkFee(100_000n); + + const publicKey = makePublicKey(1); + const registerTx = await clusters.registerValidator( + publicKey, operatorIds, DEFAULT_SHARES, createCluster(), { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await clusters.removeValidator(publicKey, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + + await networkHelpers.mine(100); + + const fullBalance = clusterAfterRemove.balance; + const withdrawTx = await clusters.withdraw(operatorIds, fullBalance, clusterAfterRemove); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterWithdraw = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + expect(clusterAfterWithdraw.balance).to.equal(0n); + expect(clusterAfterWithdraw.active).to.equal(true); + }); +}); diff --git a/test/unit/SSVDAO/accessControl.test.ts b/test/unit/SSVDAO/accessControl.test.ts new file mode 100644 index 000000000..8e5ab1c11 --- /dev/null +++ b/test/unit/SSVDAO/accessControl.test.ts @@ -0,0 +1,117 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { MINIMAL_LIQUIDATION_THRESHOLD } from "../../common/constants.ts"; +import { Errors } from "../../common/errors.ts"; +import { setupTestContext } from "../../common/helpers.ts"; + +describe("SSVDAO governance access control (via SSVNetwork)", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let signers: HardhatEthersSigner[]; + + before(async function () { + ({ connection, networkHelpers, signers } = await setupTestContext()); + }); + + const deployFullSSVNetworkFixture = async () => { + return ssvNetworkFullFixture(connection); + }; + + const getNonOwner = async (network: any): Promise => { + const ownerAddress = await network.owner(); + const nonOwner = signers.find((signer) => signer.address !== ownerAddress); + if (!nonOwner) { + throw new Error("Failed to find a non-owner signer for access control tests"); + } + return nonOwner; + }; + + const governanceCalls: Array<{ + fnName: string; + invoke: (network: any, nonOwner: HardhatEthersSigner) => Promise; + }> = [ + { + fnName: "updateNetworkFee", + invoke: (network, nonOwner) => network.connect(nonOwner).updateNetworkFee(0n), + }, + { + fnName: "updateNetworkFeeSSV", + invoke: (network, nonOwner) => network.connect(nonOwner).updateNetworkFeeSSV(0n), + }, + { + fnName: "withdrawNetworkSSVEarnings", + invoke: (network, nonOwner) => network.connect(nonOwner).withdrawNetworkSSVEarnings(0n), + }, + { + fnName: "updateOperatorFeeIncreaseLimit", + invoke: (network, nonOwner) => network.connect(nonOwner).updateOperatorFeeIncreaseLimit(0n), + }, + { + fnName: "updateDeclareOperatorFeePeriod", + invoke: (network, nonOwner) => network.connect(nonOwner).updateDeclareOperatorFeePeriod(0n), + }, + { + fnName: "updateExecuteOperatorFeePeriod", + invoke: (network, nonOwner) => network.connect(nonOwner).updateExecuteOperatorFeePeriod(0n), + }, + { + fnName: "updateLiquidationThresholdPeriod", + invoke: (network, nonOwner) => + network.connect(nonOwner).updateLiquidationThresholdPeriod(MINIMAL_LIQUIDATION_THRESHOLD), + }, + { + fnName: "updateLiquidationThresholdPeriodSSV", + invoke: (network, nonOwner) => + network.connect(nonOwner).updateLiquidationThresholdPeriodSSV(MINIMAL_LIQUIDATION_THRESHOLD), + }, + { + fnName: "updateMinimumLiquidationCollateral", + invoke: (network, nonOwner) => network.connect(nonOwner).updateMinimumLiquidationCollateral(0n), + }, + { + fnName: "updateMinimumLiquidationCollateralSSV", + invoke: (network, nonOwner) => network.connect(nonOwner).updateMinimumLiquidationCollateralSSV(0n), + }, + { + fnName: "updateMaximumOperatorFee", + invoke: (network, nonOwner) => network.connect(nonOwner).updateMaximumOperatorFee(0n), + }, + { + fnName: "updateMinimumOperatorEthFee", + invoke: (network, nonOwner) => network.connect(nonOwner).updateMinimumOperatorEthFee(0n), + }, + { + fnName: "updateUnstakeCooldownDuration", + invoke: (network, nonOwner) => network.connect(nonOwner).updateUnstakeCooldownDuration(0n), + }, + { + fnName: "replaceOracle", + invoke: (network, nonOwner) => network.connect(nonOwner).replaceOracle(1, nonOwner.address), + }, + { + fnName: "updateQuorumBps", + invoke: (network, nonOwner) => network.connect(nonOwner).updateQuorumBps(0), + }, + { + fnName: "updateModule", + invoke: (network, nonOwner) => network.connect(nonOwner).updateModule(0, nonOwner.address), + }, + { + fnName: "rescueERC20", + invoke: (network, nonOwner) => network.connect(nonOwner).rescueERC20(nonOwner.address, nonOwner.address, 0n), + }, + ]; + + for (const testCase of governanceCalls) { + it(`reverts for non-owner on ${testCase.fnName}()`, async function () { + const { network } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const nonOwner = await getNonOwner(network); + + await expect(testCase.invoke(network, nonOwner)) + .to.be.revertedWith(Errors.OWNABLE_CALLER_NOT_OWNER); + }); + } +}); diff --git a/test/unit/SSVDAO/commitRoot.test.ts b/test/unit/SSVDAO/commitRoot.test.ts new file mode 100644 index 000000000..6ca210f19 --- /dev/null +++ b/test/unit/SSVDAO/commitRoot.test.ts @@ -0,0 +1,603 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { ethers } from "ethers"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `commitRoot()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let nonOracle: HardhatEthersSigner; + + const totalSupply = ethers.parseEther("1000"); + const truncatingSupply = 1_000_000_002n; + const truncatedSupply = 1_000_000_000n; + const numberOfOracles = 4n; + + before(async function () { + ({ connection, networkHelpers, signers: [owner, oracle1, oracle2, oracle3, oracle4, nonOracle] } = await setupTestContext()); + }); + + const deployDAOWithOraclesFixture = async () => { + const { dao, cssv } = await defaultDAOFixture(connection); + + await dao.mockSetOracle(1, oracle1.address); + await dao.mockSetOracle(2, oracle2.address); + await dao.mockSetOracle(3, oracle3.address); + await dao.mockupdateQuorumBps(7500); + + return { dao, cssv }; + }; + + const deployDAOWithFourOraclesFixture = async () => { + const { dao, cssv } = await defaultDAOFixture(connection); + + await dao.mockSetOracle(1, oracle1.address); + await dao.mockSetOracle(2, oracle2.address); + await dao.mockSetOracle(3, oracle3.address); + await dao.mockSetOracle(4, oracle4.address); + await dao.mockupdateQuorumBps(7500); + + return { dao, cssv }; + }; + + const getCommitmentKey = (blockNum: number | bigint, merkleRoot: string) => { + return ethers.keccak256( + ethers.solidityPacked(["uint64", "bytes32"], [blockNum, merkleRoot]) + ); + }; + + it("Is reverted with 'NotOracle' when caller is not an oracle", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + + await expect(dao.connect(nonOracle).commitRoot(merkleRoot, currentBlock)) + .to.be.revertedWithCustomError(dao, Errors.NOT_ORACLE); + }); + + it("Is reverted with 'StaleBlockNumber' when block number is not greater than last committed", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + + await dao.mockSetLatestCommittedBlock(100); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + + await expect(dao.connect(oracle1).commitRoot(merkleRoot, 100)) + .to.be.revertedWithCustomError(dao, Errors.STALE_BLOCK_NUMBER); + + await expect(dao.connect(oracle1).commitRoot(merkleRoot, 50)) + .to.be.revertedWithCustomError(dao, Errors.STALE_BLOCK_NUMBER); + }); + + it("Is reverted with 'FutureBlockNumber' when block number is in the future", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + const futureBlock = currentBlock + 1000; + + await expect(dao.connect(oracle1).commitRoot(merkleRoot, futureBlock)) + .to.be.revertedWithCustomError(dao, Errors.FUTURE_BLOCK_NUMBER); + }); + + it("Is reverted with 'ZeroCSSVSupply' when no cSSV supply exists", async function() { + const { dao } = + await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + + await expect(dao.connect(oracle1).commitRoot(merkleRoot, currentBlock)) + .to.be.revertedWithCustomError(dao, Errors.ZERO_CSSV_SUPPLY); + }); + + it("Is reverted with 'AlreadyVoted' when oracle tries to vote twice", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + + await dao.connect(oracle1).commitRoot(merkleRoot, currentBlock); + + await expect(dao.connect(oracle1).commitRoot(merkleRoot, currentBlock)) + .to.be.revertedWithCustomError(dao, Errors.ALREADY_VOTED); + }); + + it("Emits WeightedRootProposed when quorum is not reached", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + const threshold = (totalSupply * 7500n) / 10000n; + + const tx = await dao.connect(oracle1).commitRoot(merkleRoot, currentBlock); + + await expect(tx) + .to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(merkleRoot, currentBlock, (totalSupply / numberOfOracles), threshold, 1, oracle1.address); + + const commitmentKey = getCommitmentKey(currentBlock, merkleRoot); + expect(await dao.hasOracleVoted(commitmentKey, 1)).to.equal(true); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(totalSupply / numberOfOracles); + expect(await dao.getEBRoot(currentBlock)).to.equal(ethers.ZeroHash); + }); + + it("Emits WeightedRootProposed repeatedly and accumulates weight when quorum is still not reached", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const threshold = (totalSupply * 7500n) / 10000n; + const commitmentKey = getCommitmentKey(blockNum, merkleRoot); + + const tx1 = await dao.connect(oracle1).commitRoot(merkleRoot, blockNum); + await expect(tx1) + .to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(merkleRoot, blockNum, totalSupply / numberOfOracles, threshold, 1, oracle1.address); + + const tx2 = await dao.connect(oracle2).commitRoot(merkleRoot, blockNum); + await expect(tx2) + .to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(merkleRoot, blockNum, (totalSupply / numberOfOracles) * 2n, threshold, 2, oracle2.address); + + expect(await dao.hasOracleVoted(commitmentKey, 1)).to.equal(true); + expect(await dao.hasOracleVoted(commitmentKey, 2)).to.equal(true); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + expect(await dao.getLatestCommittedBlock()).to.equal(0n); + }); + + it("Commits root and emits RootCommitted when quorum is reached", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + await dao.mockupdateQuorumBps(5000); + + await dao.connect(oracle1).commitRoot(merkleRoot, currentBlock); + + const tx = await dao.connect(oracle2).commitRoot(merkleRoot, currentBlock); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.COMMIT_ROOT]); + + const threshold = (totalSupply * 5000n) / 10000n; + const weight = totalSupply / numberOfOracles; + + await expect(tx) + .to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(merkleRoot, currentBlock, weight * 2n, threshold, 2, oracle2.address); + await expect(tx) + .to.emit(dao, Events.ROOT_COMMITTED) + .withArgs(merkleRoot, currentBlock); + + const storedRoot = await dao.getEBRoot(currentBlock); + expect(storedRoot).to.equal(merkleRoot); + + const latestBlock = await dao.getLatestCommittedBlock(); + expect(latestBlock).to.equal(currentBlock); + }); + + it("Commits on the third vote at 75% quorum even when totalSupply is not divisible by 4", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithFourOraclesFixture); + await cssv.mint(owner.address, truncatingSupply); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("truncation-regression")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + await dao.connect(oracle1).commitRoot(merkleRoot, blockNum); + await dao.connect(oracle2).commitRoot(merkleRoot, blockNum); + + const tx = await dao.connect(oracle3).commitRoot(merkleRoot, blockNum); + + await expect(tx) + .to.emit(dao, Events.ROOT_COMMITTED) + .withArgs(merkleRoot, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(merkleRoot); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + }); + + it("Stores truncated frozen supply and emits quorum based on the stored voting supply", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithFourOraclesFixture); + await cssv.mint(owner.address, truncatingSupply); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("truncated-storage")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, merkleRoot); + + const tx = await dao.connect(oracle1).commitRoot(merkleRoot, blockNum); + + await expect(tx) + .to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(merkleRoot, blockNum, truncatedSupply / numberOfOracles, (truncatedSupply * 7500n) / 10000n, 1, oracle1.address); + + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(truncatedSupply); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(truncatedSupply / numberOfOracles); + }); + + it("Requires all 4 oracle votes at 100% quorum even when totalSupply is not divisible by 4", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithFourOraclesFixture); + await cssv.mint(owner.address, truncatingSupply); + + await dao.mockupdateQuorumBps(10000); + + const root = ethers.keccak256(ethers.toUtf8Bytes("100-quorum-truncation")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + const weight = truncatedSupply / numberOfOracles; + const threshold = truncatedSupply; + + const tx1 = await dao.connect(oracle1).commitRoot(root, blockNum); + await expect(tx1).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight, threshold, 1, oracle1.address); + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(truncatedSupply); + + const tx2 = await dao.connect(oracle2).commitRoot(root, blockNum); + await expect(tx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 2n, threshold, 2, oracle2.address); + + const tx3 = await dao.connect(oracle3).commitRoot(root, blockNum); + await expect(tx3).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 3n, threshold, 3, oracle3.address); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + + const tx4 = await dao.connect(oracle4).commitRoot(root, blockNum); + await expect(tx4).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(0n); + }); + + it("Does not commit on the third vote at 80% quorum when totalSupply is not divisible by 4", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithFourOraclesFixture); + await cssv.mint(owner.address, truncatingSupply); + + await dao.mockupdateQuorumBps(8000); + + const root = ethers.keccak256(ethers.toUtf8Bytes("80-quorum-truncation")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + const weight = truncatedSupply / numberOfOracles; + const threshold = (truncatedSupply * 8000n) / 10000n; + + await dao.connect(oracle1).commitRoot(root, blockNum); + await dao.connect(oracle2).commitRoot(root, blockNum); + + const tx3 = await dao.connect(oracle3).commitRoot(root, blockNum); + await expect(tx3).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 3n, threshold, 3, oracle3.address); + + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + expect(await dao.getRoundFrozenSupply(commitmentKey)).to.equal(truncatedSupply); + + const tx4 = await dao.connect(oracle4).commitRoot(root, blockNum); + await expect(tx4).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(0n); + }); + + it("Commits root on the first vote when accumulated weight meets the quorum threshold", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + await dao.mockupdateQuorumBps(100); + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const commitmentKey = getCommitmentKey(blockNum, merkleRoot); + const threshold = (totalSupply * 100n) / 10000n; + const weight = totalSupply / numberOfOracles; + + const tx = await dao.connect(oracle1).commitRoot(merkleRoot, blockNum); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.COMMIT_ROOT]); + + await expect(tx) + .to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(merkleRoot, blockNum, weight, threshold, 1, oracle1.address); + await expect(tx) + .to.emit(dao, Events.ROOT_COMMITTED) + .withArgs(merkleRoot, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(merkleRoot); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(0n); + expect(await dao.hasOracleVoted(commitmentKey, 1)).to.equal(true); + }); + + it("Is reverted with 'StaleBlockNumber' when trying to propose the same block after it was committed", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + await dao.mockupdateQuorumBps(5000); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + await dao.connect(oracle1).commitRoot(merkleRoot, blockNum); + await dao.connect(oracle2).commitRoot(merkleRoot, blockNum); + + await expect(dao.connect(oracle3).commitRoot(merkleRoot, blockNum)) + .to.be.revertedWithCustomError(dao, Errors.STALE_BLOCK_NUMBER); + }); + + it("Accumulates weight across multiple oracle votes", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("test")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + const oracleWeight = totalSupply / numberOfOracles; + + await dao.connect(oracle1).commitRoot(merkleRoot, currentBlock); + + const commitmentKey = ethers.keccak256( + ethers.solidityPacked(["uint64", "bytes32"], [currentBlock, merkleRoot]) + ); + const weight1 = await dao.getRootCommitmentWeight(commitmentKey); + expect(weight1).to.equal(oracleWeight); + + await dao.connect(oracle2).commitRoot(merkleRoot, currentBlock); + + const weight2 = await dao.getRootCommitmentWeight(commitmentKey); + expect(weight2).to.equal(oracleWeight * 2n); + }); + + it("Is reverted with 'InsufficientCSSVSupply' when totalSupply is below the oracle count", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithFourOraclesFixture); + await cssv.mint(owner.address, 3n); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("below-oracle-count")); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + + await expect(dao.connect(oracle1).commitRoot(merkleRoot, currentBlock)) + .to.be.revertedWithCustomError(dao, Errors.INSUFFICIENT_CSSV_SUPPLY); + }); + + it("Requires all 4 oracle votes when quorumBps is 10000 (100%)", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithFourOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + await dao.mockupdateQuorumBps(10000); + + const root = ethers.keccak256(ethers.toUtf8Bytes("100-quorum")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + const weight = totalSupply / numberOfOracles; + const threshold = totalSupply; + + const tx1 = await dao.connect(oracle1).commitRoot(root, blockNum); + await expect(tx1).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight, threshold, 1, oracle1.address); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + + const tx2 = await dao.connect(oracle2).commitRoot(root, blockNum); + await expect(tx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 2n, threshold, 2, oracle2.address); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + + const tx3 = await dao.connect(oracle3).commitRoot(root, blockNum); + await expect(tx3).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 3n, threshold, 3, oracle3.address); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + expect(await dao.getLatestCommittedBlock()).to.equal(0n); + + const tx4 = await dao.connect(oracle4).commitRoot(root, blockNum); + await expect(tx4).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 4n, threshold, 4, oracle4.address); + await expect(tx4).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(0n); + }); + + it("Single oracle vote commits root when quorumBps is 1 (1 bps = 0.01%)", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + await dao.mockupdateQuorumBps(1); + + const root = ethers.keccak256(ethers.toUtf8Bytes("1-quorum")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + const weight = totalSupply / numberOfOracles; + const threshold = (totalSupply * 1n) / 10000n; + + const tx = await dao.connect(oracle1).commitRoot(root, blockNum); + await expect(tx).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight, threshold, 1, oracle1.address); + await expect(tx).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(0n); + expect(await dao.hasOracleVoted(commitmentKey, 1)).to.equal(true); + }); + + it("Oracle replaced mid-vote: old oracle loses voting rights, new oracle gets AlreadyVoted for reused slot", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + const root = ethers.keccak256(ethers.toUtf8Bytes("mid-replace")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const commitmentKey = getCommitmentKey(blockNum, root); + + const weight = totalSupply / numberOfOracles; + + await dao.connect(oracle1).commitRoot(root, blockNum); + expect(await dao.hasOracleVoted(commitmentKey, 1)).to.equal(true); + expect(await dao.getRootCommitmentWeight(commitmentKey)).to.equal(weight); + + await dao.replaceOracle(1, oracle4.address); + expect(await dao.getOracleId(oracle1.address)).to.equal(0n); + expect(await dao.getOracleId(oracle4.address)).to.equal(1n); + + await expect(dao.connect(oracle1).commitRoot(root, blockNum)) + .to.be.revertedWithCustomError(dao, Errors.NOT_ORACLE); + + await expect(dao.connect(oracle4).commitRoot(root, blockNum)) + .to.be.revertedWithCustomError(dao, Errors.ALREADY_VOTED); + + await dao.connect(oracle2).commitRoot(root, blockNum); + const threshold = (totalSupply * 7500n) / 10000n; + + const finalTx = await dao.connect(oracle3).commitRoot(root, blockNum); + await expect(finalTx).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 3n, threshold, 3, oracle3.address); + await expect(finalTx).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + }); + + it("Oracle replaced with a completely new address: new oracle inherits the slot and can vote on subsequent blocks", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + const brandNewOracle = (await connection.ethers.getSigners())[6]; + + const root = ethers.keccak256(ethers.toUtf8Bytes("brand-new-replacement")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + const weight = totalSupply / numberOfOracles; + const threshold = (totalSupply * 7500n) / 10000n; + + await dao.connect(oracle1).commitRoot(root, blockNum); + + await dao.replaceOracle(1, brandNewOracle.address); + expect(await dao.getOracleAddress(1)).to.equal(brandNewOracle.address); + expect(await dao.getOracleId(brandNewOracle.address)).to.equal(1n); + expect(await dao.getOracleId(oracle1.address)).to.equal(0n); + + await expect(dao.connect(brandNewOracle).commitRoot(root, blockNum)) + .to.be.revertedWithCustomError(dao, Errors.ALREADY_VOTED); + + const root2 = ethers.keccak256(ethers.toUtf8Bytes("brand-new-replacement-round2")); + const blockNum2 = await connection.ethers.provider.getBlockNumber(); + const commitmentKey2 = getCommitmentKey(blockNum2, root2); + + const tx = await dao.connect(brandNewOracle).commitRoot(root2, blockNum2); + await expect(tx).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root2, blockNum2, weight, threshold, 1, brandNewOracle.address); + expect(await dao.hasOracleVoted(commitmentKey2, 1)).to.equal(true); + + await dao.connect(oracle2).commitRoot(root2, blockNum2); + const finalTx2 = await dao.connect(oracle3).commitRoot(root2, blockNum2); + await expect(finalTx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root2, blockNum2, weight * 3n, threshold, 3, oracle3.address); + await expect(finalTx2).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root2, blockNum2); + expect(await dao.getEBRoot(blockNum2)).to.equal(root2); + }); + + it("Lowering quorumBps between votes causes the next vote to evaluate against the new threshold", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + const root = ethers.keccak256(ethers.toUtf8Bytes("mid-quorum-change")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const weight = totalSupply / numberOfOracles; + const initialThreshold = (totalSupply * 7500n) / 10000n; + const tx1 = await dao.connect(oracle1).commitRoot(root, blockNum); + await expect(tx1).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight, initialThreshold, 1, oracle1.address); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + await dao.mockupdateQuorumBps(5000); + const newThreshold = (totalSupply * 5000n) / 10000n; + const tx2 = await dao.connect(oracle2).commitRoot(root, blockNum); + await expect(tx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 2n, newThreshold, 2, oracle2.address); + await expect(tx2).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + }); + + it("Raising quorumBps between votes requires additional votes to reach new threshold", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + await dao.mockupdateQuorumBps(5000); + + const root = ethers.keccak256(ethers.toUtf8Bytes("mid-quorum-raise")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const weight = totalSupply / numberOfOracles; + const initialThreshold = (totalSupply * 5000n) / 10000n; + const tx1 = await dao.connect(oracle1).commitRoot(root, blockNum); + await expect(tx1).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight, initialThreshold, 1, oracle1.address); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + await dao.mockupdateQuorumBps(7500); + + const newThreshold = (totalSupply * 7500n) / 10000n; + const tx2 = await dao.connect(oracle2).commitRoot(root, blockNum); + await expect(tx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 2n, newThreshold, 2, oracle2.address); + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + const tx3 = await dao.connect(oracle3).commitRoot(root, blockNum); + await expect(tx3).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(root, blockNum, weight * 3n, newThreshold, 3, oracle3.address); + await expect(tx3).to.emit(dao, Events.ROOT_COMMITTED).withArgs(root, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(root); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + }); + + it("Conflicting roots for same block: first root to reach quorum is committed, further votes on the losing root revert", async function () { + const { dao, cssv } = await networkHelpers.loadFixture(deployDAOWithOraclesFixture); + await cssv.mint(owner.address, totalSupply); + + await dao.mockupdateQuorumBps(5000); + + const rootA = ethers.keccak256(ethers.toUtf8Bytes("rootA")); + const rootB = ethers.keccak256(ethers.toUtf8Bytes("rootB")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const weight = totalSupply / numberOfOracles; + const threshold = (totalSupply * 5000n) / 10000n; + + const txA1 = await dao.connect(oracle1).commitRoot(rootA, blockNum); + await expect(txA1).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootA, blockNum, weight, threshold, 1, oracle1.address); + + const txB2 = await dao.connect(oracle2).commitRoot(rootB, blockNum); + await expect(txB2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootB, blockNum, weight, threshold, 2, oracle2.address); + + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + + const txA3 = await dao.connect(oracle3).commitRoot(rootA, blockNum); + await expect(txA3).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED) + .withArgs(rootA, blockNum, weight * 2n, threshold, 3, oracle3.address); + await expect(txA3).to.emit(dao, Events.ROOT_COMMITTED).withArgs(rootA, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(rootA); + expect(await dao.getLatestCommittedBlock()).to.equal(blockNum); + + await expect(dao.connect(oracle1).commitRoot(rootB, blockNum)) + .to.be.revertedWithCustomError(dao, Errors.STALE_BLOCK_NUMBER); + }); +}); diff --git a/test/unit/SSVDAO/replaceOracle.test.ts b/test/unit/SSVDAO/replaceOracle.test.ts new file mode 100644 index 000000000..ce2ec84d7 --- /dev/null +++ b/test/unit/SSVDAO/replaceOracle.test.ts @@ -0,0 +1,111 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { ethers } from "ethers"; + +describe("SSVDAO function `replaceOracle()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + let oldOracle: HardhatEthersSigner; + let newOracle: HardhatEthersSigner; + let otherOracle: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner, oldOracle, newOracle, otherOracle] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Replaces an oracle and emits OracleReplaced event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.mockSetOracle(1, oldOracle.address); + + const tx = await dao.replaceOracle(1, newOracle.address); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REPLACE_ORACLE]); + + await expect(tx) + .to.emit(dao, Events.ORACLE_REPLACED) + .withArgs(1, oldOracle.address, newOracle.address); + }); + + it("Updates oracle address in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.mockSetOracle(1, oldOracle.address); + + await dao.replaceOracle(1, newOracle.address); + + const storedOracle = await dao.getOracleAddress(1); + expect(storedOracle).to.equal(newOracle.address); + }); + + it("Updates reverse mapping (oracleIdOf) correctly", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.mockSetOracle(1, oldOracle.address); + + await dao.replaceOracle(1, newOracle.address); + + const oldOracleId = await dao.getOracleId(oldOracle.address); + expect(oldOracleId).to.equal(0); + + const newOracleId = await dao.getOracleId(newOracle.address); + expect(newOracleId).to.equal(1); + }); + + it("Is reverted with 'InvalidOracleId' when oracle ID is zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.replaceOracle(0, newOracle.address)) + .to.be.revertedWithCustomError(dao, Errors.INVALID_ORACLE_ID); + }); + + it("Is reverted with 'ZeroAddress' when new oracle address is zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.replaceOracle(1, ethers.ZeroAddress)) + .to.be.revertedWithCustomError(dao, Errors.ZERO_ADDRESS); + }); + + it("Is reverted with 'OracleAlreadyAssigned' when new oracle is already assigned to another ID", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.mockSetOracle(1, oldOracle.address); + await dao.mockSetOracle(2, otherOracle.address); + + await expect(dao.replaceOracle(1, otherOracle.address)) + .to.be.revertedWithCustomError(dao, Errors.ORACLE_ALREADY_ASSIGNED); + }); + + it("Is reverted with 'SameOracleAddressNotAllowed' when replacing with same address", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.mockSetOracle(1, oldOracle.address); + + await expect(dao.replaceOracle(1, oldOracle.address)) + .to.be.revertedWithCustomError(dao, Errors.SAME_ORACLE_ADDRESS_NOT_ALLOWED); + }); + + it("Can replace an oracle with ID that had no previous address", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const tx = await dao.replaceOracle(1, newOracle.address); + + await expect(tx) + .to.emit(dao, Events.ORACLE_REPLACED) + .withArgs(1, ethers.ZeroAddress, newOracle.address); + + const storedOracle = await dao.getOracleAddress(1); + expect(storedOracle).to.equal(newOracle.address); + }); +}); diff --git a/test/unit/SSVDAO/run-tests.sh b/test/unit/SSVDAO/run-tests.sh new file mode 100755 index 000000000..90220c8e3 --- /dev/null +++ b/test/unit/SSVDAO/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Run all SSVDAO unit tests +npx hardhat test test/unit/SSVDAO/*.test.ts + diff --git a/test/unit/SSVDAO/setMinBlocksBetweenUpdates.test.ts b/test/unit/SSVDAO/setMinBlocksBetweenUpdates.test.ts new file mode 100644 index 000000000..c2dde58a6 --- /dev/null +++ b/test/unit/SSVDAO/setMinBlocksBetweenUpdates.test.ts @@ -0,0 +1,53 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { getTestConnection } from "../../setup/connection.ts"; +import { ssvDAOHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; + +describe("SSVDAO function `updateMinBlocksBetweenUpdates()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + }); + + const deployDAOFixture = async () => ssvDAOHarnessFixture(connection); + + it("Sets EB update cooldown blocks and emits MinBlocksBetweenUpdatesUpdated event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newMinBlocks = 7200n; + + const tx = await dao.updateMinBlocksBetweenUpdates(newMinBlocks); + + await expect(tx) + .to.emit(dao, Events.MIN_BLOCKS_BETWEEN_UPDATES_UPDATED) + .withArgs(newMinBlocks); + + expect(await dao.getMinBlocksBetweenUpdates()).to.equal(newMinBlocks); + }); + + it("Can update EB update cooldown blocks from one non-zero value to another and go back to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateMinBlocksBetweenUpdates(7200n); + let tx = await dao.updateMinBlocksBetweenUpdates(3600n); + + await expect(tx) + .to.emit(dao, Events.MIN_BLOCKS_BETWEEN_UPDATES_UPDATED) + .withArgs(3600n); + + expect(await dao.getMinBlocksBetweenUpdates()).to.equal(3600n); + + tx = await dao.updateMinBlocksBetweenUpdates(0n); + + await expect(tx) + .to.emit(dao, Events.MIN_BLOCKS_BETWEEN_UPDATES_UPDATED) + .withArgs(0n); + + expect(await dao.getMinBlocksBetweenUpdates()).to.equal(0n); + }); +}); diff --git a/test/unit/SSVDAO/setQuorumBps.test.ts b/test/unit/SSVDAO/setQuorumBps.test.ts new file mode 100644 index 000000000..a51a6cd01 --- /dev/null +++ b/test/unit/SSVDAO/setQuorumBps.test.ts @@ -0,0 +1,108 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { Errors } from "../../common/errors.ts"; + +describe("SSVDAO function `updateQuorumBps()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Sets quorum basis points and emits QuorumUpdated event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newQuorum = 7500n; + + const tx = await dao.updateQuorumBps(newQuorum); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_QUORUM]); + + await expect(tx) + .to.emit(dao, Events.QUORUM_UPDATED) + .withArgs(newQuorum); + }); + + it("Stores the new quorum in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newQuorum = 6000n; + + await dao.updateQuorumBps(newQuorum); + + const storedQuorum = await dao.getQuorumBps(); + expect(storedQuorum).to.equal(newQuorum); + }); + + it("Can set quorum to 100% (10000 bps)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const maxQuorum = 10000n; + + const tx = await dao.updateQuorumBps(maxQuorum); + + await expect(tx) + .to.emit(dao, Events.QUORUM_UPDATED) + .withArgs(maxQuorum); + + const storedQuorum = await dao.getQuorumBps(); + expect(storedQuorum).to.equal(maxQuorum); + }); + + it("Is reverted when quorum is 0", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.updateQuorumBps(0n)) + .to.be.revertedWithCustomError(dao, Errors.INVALID_QUORUM); + }); + + it("Can set quorum to 1 bps (minimum)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const tx = await dao.updateQuorumBps(1n); + + await expect(tx) + .to.emit(dao, Events.QUORUM_UPDATED) + .withArgs(1n); + + const storedQuorum = await dao.getQuorumBps(); + expect(storedQuorum).to.equal(1n); + }); + + it("Is reverted when quorum exceeds 10000 bps", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const invalidQuorum = 10001n; + + await expect(dao.updateQuorumBps(invalidQuorum)) + .to.be.revertedWithCustomError(dao, Errors.INVALID_QUORUM); + }); + + it("Can update quorum from one value to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstQuorum = 5000n; + const secondQuorum = 8000n; + + await dao.updateQuorumBps(firstQuorum); + const tx = await dao.updateQuorumBps(secondQuorum); + + await expect(tx) + .to.emit(dao, Events.QUORUM_UPDATED) + .withArgs(secondQuorum); + + const storedQuorum = await dao.getQuorumBps(); + expect(storedQuorum).to.equal(secondQuorum); + }); +}); diff --git a/test/unit/SSVDAO/setUnstakeCooldownDuration.test.ts b/test/unit/SSVDAO/setUnstakeCooldownDuration.test.ts new file mode 100644 index 000000000..833ab0343 --- /dev/null +++ b/test/unit/SSVDAO/setUnstakeCooldownDuration.test.ts @@ -0,0 +1,92 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `updateUnstakeCooldownDuration()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Sets unstake cooldown duration and emits CooldownDurationUpdated event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newDuration = 604800n; + + const tx = await dao.updateUnstakeCooldownDuration(newDuration); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.SET_UNSTAKE_COOLDOWN]); + + await expect(tx) + .to.emit(dao, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(newDuration); + }); + + it("Stores the new cooldown duration in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newDuration = 86400n; + + await dao.updateUnstakeCooldownDuration(newDuration); + + const storedDuration = await dao.getCooldownDuration(); + expect(storedDuration).to.equal(newDuration); + }); + + it("Can set cooldown duration to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateUnstakeCooldownDuration(86400n); + const tx = await dao.updateUnstakeCooldownDuration(0n); + + await expect(tx) + .to.emit(dao, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(0n); + + const storedDuration = await dao.getCooldownDuration(); + expect(storedDuration).to.equal(0n); + }); + + it("Can set high cooldown duration", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const highDuration = 2592000n; + + const tx = await dao.updateUnstakeCooldownDuration(highDuration); + + await expect(tx) + .to.emit(dao, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(highDuration); + + const storedDuration = await dao.getCooldownDuration(); + expect(storedDuration).to.equal(highDuration); + }); + + it("Can update cooldown duration from one value to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstDuration = 86400n; + const secondDuration = 172800n; + + await dao.updateUnstakeCooldownDuration(firstDuration); + const tx = await dao.updateUnstakeCooldownDuration(secondDuration); + + await expect(tx) + .to.emit(dao, Events.COOLDOWN_DURATION_UPDATED) + .withArgs(secondDuration); + + const storedDuration = await dao.getCooldownDuration(); + expect(storedDuration).to.equal(secondDuration); + }); +}); diff --git a/test/unit/SSVDAO/updateDeclareOperatorFeePeriod.test.ts b/test/unit/SSVDAO/updateDeclareOperatorFeePeriod.test.ts new file mode 100644 index 000000000..769bb139e --- /dev/null +++ b/test/unit/SSVDAO/updateDeclareOperatorFeePeriod.test.ts @@ -0,0 +1,77 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `updateDeclareOperatorFeePeriod()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the declare operator fee period and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = 604800n; + + const tx = await dao.updateDeclareOperatorFeePeriod(newPeriod); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_DECLARE_OPERATOR_FEE_PERIOD]); + + await expect(tx) + .to.emit(dao, Events.DECLARE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(newPeriod); + }); + + it("Stores the new declare operator fee period in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = 86400n; + + await dao.updateDeclareOperatorFeePeriod(newPeriod); + + const storedPeriod = await dao.getDeclareOperatorFeePeriod(); + expect(storedPeriod).to.equal(newPeriod); + }); + + it("Can set declare period to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateDeclareOperatorFeePeriod(86400n); + const tx = await dao.updateDeclareOperatorFeePeriod(0n); + + await expect(tx) + .to.emit(dao, Events.DECLARE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(0n); + + const storedPeriod = await dao.getDeclareOperatorFeePeriod(); + expect(storedPeriod).to.equal(0n); + }); + + it("Can update from one period to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstPeriod = 86400n; + const secondPeriod = 172800n; + + await dao.updateDeclareOperatorFeePeriod(firstPeriod); + const tx = await dao.updateDeclareOperatorFeePeriod(secondPeriod); + + await expect(tx) + .to.emit(dao, Events.DECLARE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(secondPeriod); + + const storedPeriod = await dao.getDeclareOperatorFeePeriod(); + expect(storedPeriod).to.equal(secondPeriod); + }); +}); diff --git a/test/unit/SSVDAO/updateExecuteOperatorFeePeriod.test.ts b/test/unit/SSVDAO/updateExecuteOperatorFeePeriod.test.ts new file mode 100644 index 000000000..f762241b2 --- /dev/null +++ b/test/unit/SSVDAO/updateExecuteOperatorFeePeriod.test.ts @@ -0,0 +1,77 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `updateExecuteOperatorFeePeriod()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the execute operator fee period and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = 604800n; + + const tx = await dao.updateExecuteOperatorFeePeriod(newPeriod); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_EXECUTE_OPERATOR_FEE_PERIOD]); + + await expect(tx) + .to.emit(dao, Events.EXECUTE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(newPeriod); + }); + + it("Stores the new execute operator fee period in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = 86400n; + + await dao.updateExecuteOperatorFeePeriod(newPeriod); + + const storedPeriod = await dao.getExecuteOperatorFeePeriod(); + expect(storedPeriod).to.equal(newPeriod); + }); + + it("Can set execute period to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateExecuteOperatorFeePeriod(86400n); + const tx = await dao.updateExecuteOperatorFeePeriod(0n); + + await expect(tx) + .to.emit(dao, Events.EXECUTE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(0n); + + const storedPeriod = await dao.getExecuteOperatorFeePeriod(); + expect(storedPeriod).to.equal(0n); + }); + + it("Can update from one period to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstPeriod = 86400n; + const secondPeriod = 259200n; + + await dao.updateExecuteOperatorFeePeriod(firstPeriod); + const tx = await dao.updateExecuteOperatorFeePeriod(secondPeriod); + + await expect(tx) + .to.emit(dao, Events.EXECUTE_OPERATOR_FEE_PERIOD_UPDATED) + .withArgs(secondPeriod); + + const storedPeriod = await dao.getExecuteOperatorFeePeriod(); + expect(storedPeriod).to.equal(secondPeriod); + }); +}); diff --git a/test/unit/SSVDAO/updateLiquidationThresholdPeriod.test.ts b/test/unit/SSVDAO/updateLiquidationThresholdPeriod.test.ts new file mode 100644 index 000000000..24cd16662 --- /dev/null +++ b/test/unit/SSVDAO/updateLiquidationThresholdPeriod.test.ts @@ -0,0 +1,132 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { MINIMAL_LIQUIDATION_THRESHOLD } from "../../common/constants.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `updateLiquidationThresholdPeriod()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the liquidation threshold period and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = MINIMAL_LIQUIDATION_THRESHOLD + 1000n; + + const tx = await dao.updateLiquidationThresholdPeriod(newPeriod); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_LIQUIDATION_THRESHOLD_PERIOD]); + + await expect(tx) + .to.emit(dao, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED) + .withArgs(newPeriod); + }); + + it("Stores the new liquidation threshold period in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = 200000n; + + await dao.updateLiquidationThresholdPeriod(newPeriod); + + const storedPeriod = await dao.getMinimumBlocksBeforeLiquidation(); + expect(storedPeriod).to.equal(newPeriod); + }); + + it("Is reverted with 'NewBlockPeriodIsBelowMinimum' when period is below minimum", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const belowMinimum = MINIMAL_LIQUIDATION_THRESHOLD - 1n; + + await expect(dao.updateLiquidationThresholdPeriod(belowMinimum)) + .to.be.revertedWithCustomError(dao, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); + + it("Accepts exactly the minimum threshold", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const tx = await dao.updateLiquidationThresholdPeriod(MINIMAL_LIQUIDATION_THRESHOLD); + + await expect(tx) + .to.emit(dao, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED) + .withArgs(MINIMAL_LIQUIDATION_THRESHOLD); + + const storedPeriod = await dao.getMinimumBlocksBeforeLiquidation(); + expect(storedPeriod).to.equal(MINIMAL_LIQUIDATION_THRESHOLD); + }); + + it("Can update from one period to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstPeriod = MINIMAL_LIQUIDATION_THRESHOLD + 100n; + const secondPeriod = MINIMAL_LIQUIDATION_THRESHOLD + 200n; + + await dao.updateLiquidationThresholdPeriod(firstPeriod); + const tx = await dao.updateLiquidationThresholdPeriod(secondPeriod); + + await expect(tx) + .to.emit(dao, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED) + .withArgs(secondPeriod); + + const storedPeriod = await dao.getMinimumBlocksBeforeLiquidation(); + expect(storedPeriod).to.equal(secondPeriod); + }); +}); + +describe("SSVDAO function `updateLiquidationThresholdPeriodSSV()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the SSV liquidation threshold period and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = MINIMAL_LIQUIDATION_THRESHOLD + 1000n; + + const tx = await dao.updateLiquidationThresholdPeriodSSV(newPeriod); + + await expect(tx) + .to.emit(dao, Events.LIQUIDATION_THRESHOLD_PERIOD_UPDATED_SSV) + .withArgs(newPeriod); + }); + + it("Stores the new SSV liquidation threshold period in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newPeriod = 200000n; + + await dao.updateLiquidationThresholdPeriodSSV(newPeriod); + + const storedPeriod = await dao.getMinimumBlocksBeforeLiquidationSSV(); + expect(storedPeriod).to.equal(newPeriod); + }); + + it("Is reverted with 'NewBlockPeriodIsBelowMinimum' when period is below minimum", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const belowMinimum = MINIMAL_LIQUIDATION_THRESHOLD - 1n; + + await expect(dao.updateLiquidationThresholdPeriodSSV(belowMinimum)) + .to.be.revertedWithCustomError(dao, Errors.NEW_BLOCK_PERIOD_IS_BELOW_MINIMUM); + }); +}); diff --git a/test/unit/SSVDAO/updateMaximumOperatorFee.test.ts b/test/unit/SSVDAO/updateMaximumOperatorFee.test.ts new file mode 100644 index 000000000..7472dc2da --- /dev/null +++ b/test/unit/SSVDAO/updateMaximumOperatorFee.test.ts @@ -0,0 +1,89 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { MAXIMUM_OPERATORS_FEE, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `updateMaximumOperatorFee()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the maximum operator fee and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newMaxFee = 100_000_000_000n; + + const tx = await dao.updateMaximumOperatorFee(newMaxFee); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_MAX_FEE]); + + await expect(tx) + .to.emit(dao, Events.OPERATOR_MAXIMUM_FEE_UPDATED) + .withArgs(newMaxFee); + }); + + it("Stores the new maximum operator fee in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newMaxFee = 50_000_000_000n; + + await dao.updateMaximumOperatorFee(newMaxFee); + + const storedMaxFee = await dao.getOperatorMaxFee(); + expect(storedMaxFee * ETH_DEDUCTED_DIGITS).to.equal(newMaxFee); + }); + + it("Can set maximum operator fee to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateMaximumOperatorFee(100_000_000_000n); + const tx = await dao.updateMaximumOperatorFee(0n); + + await expect(tx) + .to.emit(dao, Events.OPERATOR_MAXIMUM_FEE_UPDATED) + .withArgs(0n); + + const storedMaxFee = await dao.getOperatorMaxFee(); + expect(storedMaxFee).to.equal(0n); + }); + + it("Can update from one max fee to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstMaxFee = 50_000_000_000n; + const secondMaxFee = MAXIMUM_OPERATORS_FEE; + + await dao.updateMaximumOperatorFee(firstMaxFee); + const tx = await dao.updateMaximumOperatorFee(secondMaxFee); + + await expect(tx) + .to.emit(dao, Events.OPERATOR_MAXIMUM_FEE_UPDATED) + .withArgs(secondMaxFee); + + const storedMaxFee = await dao.getOperatorMaxFee(); + expect(storedMaxFee * ETH_DEDUCTED_DIGITS).to.equal(secondMaxFee); + }); + + it("Reverts when the new maximum fee is below the configured minimum fee", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const currentMaxFee = MAXIMUM_OPERATORS_FEE; + const currentMinFee = 10_000_000_000n; + + await dao.updateMaximumOperatorFee(currentMaxFee); + await dao.updateMinimumOperatorEthFee(currentMinFee); + + await expect(dao.updateMaximumOperatorFee(currentMinFee - ETH_DEDUCTED_DIGITS)) + .to.be.revertedWithCustomError(dao, Errors.INVALID_OPERATOR_FEE_RANGE); + }); +}); diff --git a/test/unit/SSVDAO/updateMinimumLiquidationCollateral.test.ts b/test/unit/SSVDAO/updateMinimumLiquidationCollateral.test.ts new file mode 100644 index 000000000..d1e1bb9fd --- /dev/null +++ b/test/unit/SSVDAO/updateMinimumLiquidationCollateral.test.ts @@ -0,0 +1,138 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from '../../common/errors.js'; +import { ethers } from "ethers"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; + +describe("SSVDAO function `updateMinimumLiquidationCollateral()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the minimum liquidation collateral and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newCollateral = ethers.parseEther("1"); + + const tx = await dao.updateMinimumLiquidationCollateral(newCollateral); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.CHANGE_MINIMUM_COLLATERAL]); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED) + .withArgs(newCollateral); + }); + + it("Is reverted when collateral is not a multiple of 1e7 (shrink precision)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.updateMinimumLiquidationCollateral(1n)) + .to.be.revertedWithCustomError(dao, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Stores the new minimum liquidation collateral in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newCollateral = ethers.parseEther("2"); + + await dao.updateMinimumLiquidationCollateral(newCollateral); + + const storedCollateral = await dao.getMinimumLiquidationCollateral(); + expect(storedCollateral).to.equal(newCollateral / ETH_DEDUCTED_DIGITS); + }); + + it("Can set minimum liquidation collateral to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateMinimumLiquidationCollateral(ethers.parseEther("1")); + const tx = await dao.updateMinimumLiquidationCollateral(0n); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED) + .withArgs(0n); + + const storedCollateral = await dao.getMinimumLiquidationCollateral(); + expect(storedCollateral).to.equal(0n); + }); + + it("Can update from one collateral to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstCollateral = ethers.parseEther("1"); + const secondCollateral = ethers.parseEther("5"); + + await dao.updateMinimumLiquidationCollateral(firstCollateral); + const tx = await dao.updateMinimumLiquidationCollateral(secondCollateral); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED) + .withArgs(secondCollateral); + }); +}); + +describe("SSVDAO function `updateMinimumLiquidationCollateralSSV()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the SSV minimum liquidation collateral and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newCollateral = ethers.parseEther("1"); + + const tx = await dao.updateMinimumLiquidationCollateralSSV(newCollateral); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED_SSV) + .withArgs(newCollateral); + }); + + it("Is reverted when SSV collateral is not a multiple of 1e7 (shrink precision)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.updateMinimumLiquidationCollateralSSV(1n)) + .to.be.revertedWithCustomError(dao, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Stores the new SSV minimum liquidation collateral in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newCollateral = ethers.parseEther("3"); + + await dao.updateMinimumLiquidationCollateralSSV(newCollateral); + + const storedCollateral = await dao.getMinimumLiquidationCollateralSSV(); + expect(storedCollateral).to.equal(newCollateral / 10_000_000n); + }); + + it("Can set SSV minimum liquidation collateral to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateMinimumLiquidationCollateralSSV(ethers.parseEther("1")); + const tx = await dao.updateMinimumLiquidationCollateralSSV(0n); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_LIQUIDATION_COLLATERAL_UPDATED_SSV) + .withArgs(0n); + }); +}); diff --git a/test/unit/SSVDAO/updateMinimumOperatorEthFee.test.ts b/test/unit/SSVDAO/updateMinimumOperatorEthFee.test.ts new file mode 100644 index 000000000..0bc886716 --- /dev/null +++ b/test/unit/SSVDAO/updateMinimumOperatorEthFee.test.ts @@ -0,0 +1,90 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { MINIMAL_OPERATOR_ETH_FEE, ETH_DEDUCTED_DIGITS, MAXIMUM_OPERATORS_FEE } from "../../common/constants.ts"; +import { setupTestContext } from "../../common/helpers.ts"; + +describe("SSVDAO function `updateMinimumOperatorEthFee()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the minimum operator ETH fee and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newMinFee = MINIMAL_OPERATOR_ETH_FEE; + + await dao.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE); + const tx = await dao.updateMinimumOperatorEthFee(newMinFee); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_OPERATOR_ETH_FEE_UPDATED) + .withArgs(newMinFee); + }); + + it("Stores the new minimum operator ETH fee in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newMinFee = 1000_000_000n; + + await dao.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE); + await dao.updateMinimumOperatorEthFee(newMinFee); + + const storedMinFee = await dao.getMinimumOperatorEthFee(); + expect(storedMinFee * ETH_DEDUCTED_DIGITS).to.equal(newMinFee); + }); + + it("Can set minimum operator ETH fee to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE); + await dao.updateMinimumOperatorEthFee(MINIMAL_OPERATOR_ETH_FEE); + const tx = await dao.updateMinimumOperatorEthFee(0n); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_OPERATOR_ETH_FEE_UPDATED) + .withArgs(0n); + + const storedMinFee = await dao.getMinimumOperatorEthFee(); + expect(storedMinFee).to.equal(0n); + }); + + it("Can update from one min fee to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstMinFee = 500_000_000n; + const secondMinFee = MINIMAL_OPERATOR_ETH_FEE; + + await dao.updateMaximumOperatorFee(MAXIMUM_OPERATORS_FEE); + await dao.updateMinimumOperatorEthFee(firstMinFee); + const tx = await dao.updateMinimumOperatorEthFee(secondMinFee); + + await expect(tx) + .to.emit(dao, Events.MINIMUM_OPERATOR_ETH_FEE_UPDATED) + .withArgs(secondMinFee); + + const storedMinFee = await dao.getMinimumOperatorEthFee(); + expect(storedMinFee * ETH_DEDUCTED_DIGITS).to.equal(secondMinFee); + }); + + it("Reverts when the new minimum fee exceeds the configured maximum fee", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const currentMaxFee = 10_000_000_000n; + await dao.updateMaximumOperatorFee(currentMaxFee); + + await expect(dao.updateMinimumOperatorEthFee(currentMaxFee + ETH_DEDUCTED_DIGITS)) + .to.be.revertedWithCustomError(dao, Errors.INVALID_OPERATOR_FEE_RANGE); + }); +}); diff --git a/test/unit/SSVDAO/updateNetworkFee.test.ts b/test/unit/SSVDAO/updateNetworkFee.test.ts new file mode 100644 index 000000000..1bb3bfa8b --- /dev/null +++ b/test/unit/SSVDAO/updateNetworkFee.test.ts @@ -0,0 +1,90 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from '../../common/errors.js'; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; + + +describe("SSVDAO function `updateNetworkFee()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the network fee and emits NetworkFeeUpdated event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const initialFee = 0n; + const newFee = 1_000_000_000n; + + const tx = await dao.updateNetworkFee(newFee); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.NETWORK_FEE_CHANGE]); + + await expect(tx) + .to.emit(dao, Events.NETWORK_FEE_UPDATED) + .withArgs(initialFee, newFee); + + const storedFee = await dao.getNetworkFee(); + expect(storedFee).to.equal(newFee / ETH_DEDUCTED_DIGITS); + }); + + it("Is reverted when fee is not a multiple of 1e5 (shrink precision)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.updateNetworkFee(1n)) + .to.be.revertedWithCustomError(dao, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Stores the new network fee in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newFee = 2_000_000_000n; + + await dao.updateNetworkFee(newFee); + + const storedFee = await dao.getNetworkFee(); + expect(storedFee).to.equal(newFee / ETH_DEDUCTED_DIGITS); + }); + + it("Updates the network fee from a non-zero value", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstFee = 1_000_000_000n; + const secondFee = 2_000_000_000n; + + await dao.updateNetworkFee(firstFee); + const tx = await dao.updateNetworkFee(secondFee); + + await expect(tx) + .to.emit(dao, Events.NETWORK_FEE_UPDATED) + .withArgs(firstFee, secondFee); + }); + + it("Can set network fee to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstFee = 1_000_000_000n; + await dao.updateNetworkFee(firstFee); + + const tx = await dao.updateNetworkFee(0n); + + await expect(tx) + .to.emit(dao, Events.NETWORK_FEE_UPDATED) + .withArgs(firstFee, 0n); + + const storedFee = await dao.getNetworkFee(); + expect(storedFee).to.equal(0n); + }); +}); diff --git a/test/unit/SSVDAO/updateNetworkFeeSSV.test.ts b/test/unit/SSVDAO/updateNetworkFeeSSV.test.ts new file mode 100644 index 000000000..b687c357d --- /dev/null +++ b/test/unit/SSVDAO/updateNetworkFeeSSV.test.ts @@ -0,0 +1,89 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { Errors } from '../../common/errors.js'; +import { DEDUCTED_DIGITS } from "../../common/constants.ts"; + +describe("SSVDAO function `updateNetworkFeeSSV()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the SSV network fee and emits NetworkFeeUpdated event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const initialFee = 0n; + const newFee = 1_000_000_000n; + + const tx = await dao.updateNetworkFeeSSV(newFee); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.NETWORK_FEE_CHANGE_SSV]); + + await expect(tx) + .to.emit(dao, Events.NETWORK_FEE_UPDATED_SSV) + .withArgs(initialFee, newFee); + + const storedFee = await dao.getNetworkFeeSSV(); + expect(storedFee).to.equal(newFee / DEDUCTED_DIGITS); + }); + + it("Is reverted when fee is not a multiple of 1e7 (shrink precision)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.updateNetworkFeeSSV(1n)) + .to.be.revertedWithCustomError(dao, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Stores the new SSV network fee in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newFee = 2_000_000_000n; + + await dao.updateNetworkFeeSSV(newFee); + + const storedFee = await dao.getNetworkFeeSSV(); + expect(storedFee).to.equal(newFee / DEDUCTED_DIGITS); + }); + + it("Updates the SSV network fee from a non-zero value", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstFee = 1_000_000_000n; + const secondFee = 3_000_000_000n; + + await dao.updateNetworkFeeSSV(firstFee); + const tx = await dao.updateNetworkFeeSSV(secondFee); + + await expect(tx) + .to.emit(dao, Events.NETWORK_FEE_UPDATED_SSV) + .withArgs(firstFee, secondFee); + }); + + it("Can set SSV network fee to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstFee = 1_000_000_000n; + await dao.updateNetworkFeeSSV(firstFee); + + const tx = await dao.updateNetworkFeeSSV(0n); + + await expect(tx) + .to.emit(dao, Events.NETWORK_FEE_UPDATED_SSV) + .withArgs(firstFee, 0n); + + const storedFee = await dao.getNetworkFeeSSV(); + expect(storedFee).to.equal(0n); + }); +}); diff --git a/test/unit/SSVDAO/updateOperatorFeeIncreaseLimit.test.ts b/test/unit/SSVDAO/updateOperatorFeeIncreaseLimit.test.ts new file mode 100644 index 000000000..50daf07a8 --- /dev/null +++ b/test/unit/SSVDAO/updateOperatorFeeIncreaseLimit.test.ts @@ -0,0 +1,100 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `updateOperatorFeeIncreaseLimit()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOFixture = async () => defaultDAOFixture(connection); + + it("Updates the operator fee increase limit and emits event", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newLimit = 1000n; + + const tx = await dao.updateOperatorFeeIncreaseLimit(newLimit); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.DAO_UPDATE_OPERATOR_FEE_INCREASE_LIMIT]); + + await expect(tx) + .to.emit(dao, Events.OPERATOR_FEE_INCREASE_LIMIT_UPDATED) + .withArgs(newLimit); + }); + + it("Can update operator fee increase limit from one value to another", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const firstLimit = 1000n; + const secondLimit = 2000n; + + await dao.updateOperatorFeeIncreaseLimit(firstLimit); + const tx = await dao.updateOperatorFeeIncreaseLimit(secondLimit); + + await expect(tx) + .to.emit(dao, Events.OPERATOR_FEE_INCREASE_LIMIT_UPDATED) + .withArgs(secondLimit); + + const storedLimit = await dao.getOperatorMaxFeeIncrease(); + expect(storedLimit).to.equal(secondLimit); + }); + + it("Stores the new operator fee increase limit in storage", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const newLimit = 1500n; + + await dao.updateOperatorFeeIncreaseLimit(newLimit); + + const storedLimit = await dao.getOperatorMaxFeeIncrease(); + expect(storedLimit).to.equal(newLimit); + }); + + it("Can set operator fee increase limit to zero", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await dao.updateOperatorFeeIncreaseLimit(1000n); + const tx = await dao.updateOperatorFeeIncreaseLimit(0n); + + await expect(tx) + .to.emit(dao, Events.OPERATOR_FEE_INCREASE_LIMIT_UPDATED) + .withArgs(0n); + + const storedLimit = await dao.getOperatorMaxFeeIncrease(); + expect(storedLimit).to.equal(0n); + }); + + it("Can set high operator fee increase limit", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + const highLimit = 10000n; + + const tx = await dao.updateOperatorFeeIncreaseLimit(highLimit); + + await expect(tx) + .to.emit(dao, Events.OPERATOR_FEE_INCREASE_LIMIT_UPDATED) + .withArgs(highLimit); + + const storedLimit = await dao.getOperatorMaxFeeIncrease(); + expect(storedLimit).to.equal(highLimit); + }); + + it("Reverts when operator fee increase limit exceeds 100%", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + + await expect(dao.updateOperatorFeeIncreaseLimit(10001n)) + .to.be.revertedWithCustomError(dao, Errors.INVALID_OPERATOR_FEE_INCREASE_LIMIT); + }); +}); diff --git a/test/unit/SSVDAO/withdrawNetworkSSVEarnings.test.ts b/test/unit/SSVDAO/withdrawNetworkSSVEarnings.test.ts new file mode 100644 index 000000000..205a9630c --- /dev/null +++ b/test/unit/SSVDAO/withdrawNetworkSSVEarnings.test.ts @@ -0,0 +1,103 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultDAOFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVDAO function `withdrawNetworkSSVEarnings()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployDAOWithTokenFixture = async () => { + const { dao } = await defaultDAOFixture(connection); + + const mockToken = await connection.ethers.deployContract("MockToken", []); + await mockToken.waitForDeployment(); + + await dao.mockSetToken(await mockToken.getAddress()); + + const daoBalance = 1000n; + await dao.mockSetDaoBalance(daoBalance); + + await mockToken.mint(await dao.getAddress(), daoBalance * 10_000_000n); + + return { dao, mockToken, daoBalance }; + }; + + it("Is reverted with 'InsufficientBalance' when trying to withdraw more than available", async function () { + const { dao } = await defaultDAOFixture(connection); + + await dao.mockSetDaoBalance(100n); + + const withdrawAmount = 200n * 10_000_000n; + + await expect(dao.withdrawNetworkSSVEarnings(withdrawAmount)) + .to.be.revertedWithCustomError(dao, Errors.INSUFFICIENT_BALANCE); + }); + + it("Is reverted when amount is not a multiple of 1e7 (shrink precision)", async function () { + const { dao } = await defaultDAOFixture(connection); + + await expect(dao.withdrawNetworkSSVEarnings(1n)) + .to.be.revertedWithCustomError(dao, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Withdraws network SSV earnings and emits NetworkEarningsWithdrawn event", async function () { + const { dao, mockToken, daoBalance } = await networkHelpers.loadFixture(deployDAOWithTokenFixture); + + const withdrawAmount = 500n * 10_000_000n; + + const ownerBalanceBefore = await mockToken.balanceOf(owner.address); + const daoTokenBalanceBefore = await mockToken.balanceOf(await dao.getAddress()); + + const tx = await dao.withdrawNetworkSSVEarnings(withdrawAmount); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.WITHDRAW_NETWORK_SSV_EARNINGS]); + + await expect(tx) + .to.emit(dao, Events.NETWORK_EARNINGS_WITHDRAWN) + .withArgs(withdrawAmount, owner.address); + + const ownerBalanceAfter = await mockToken.balanceOf(owner.address); + const daoTokenBalanceAfter = await mockToken.balanceOf(await dao.getAddress()); + + expect(ownerBalanceAfter - ownerBalanceBefore).to.equal(withdrawAmount); + expect(daoTokenBalanceBefore - daoTokenBalanceAfter).to.equal(withdrawAmount); + }); + + it("Updates DAO balance after withdrawal", async function () { + const { dao, daoBalance } = await networkHelpers.loadFixture(deployDAOWithTokenFixture); + + const withdrawAmount = 500n * 10_000_000n; + + await dao.withdrawNetworkSSVEarnings(withdrawAmount); + + const newBalance = await dao.getDaoBalance(); + expect(newBalance).to.equal(daoBalance - 500n); + }); + + it("Can withdraw all available earnings", async function () { + const { dao, daoBalance } = await networkHelpers.loadFixture(deployDAOWithTokenFixture); + + const withdrawAmount = daoBalance * 10_000_000n; + + const tx = await dao.withdrawNetworkSSVEarnings(withdrawAmount); + + await expect(tx) + .to.emit(dao, Events.NETWORK_EARNINGS_WITHDRAWN) + .withArgs(withdrawAmount, owner.address); + + const newBalance = await dao.getDaoBalance(); + expect(newBalance).to.equal(0n); + }); +}); diff --git a/test/unit/SSVOperators/cancelDeclaredOperatorFee.test.ts b/test/unit/SSVOperators/cancelDeclaredOperatorFee.test.ts new file mode 100644 index 000000000..a00ca0b6f --- /dev/null +++ b/test/unit/SSVOperators/cancelDeclaredOperatorFee.test.ts @@ -0,0 +1,64 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + MINIMAL_OPERATOR_ETH_FEE, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators function `cancelDeclaredOperatorFee()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + + it("Cancels declared fee and emits expected event", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await trackGas( + operators.declareOperatorFee(1, MINIMAL_OPERATOR_ETH_FEE * 2n), + [GasGroup.DECLARE_OPERATOR_FEE] + ); + + await expect( + trackGas( + operators.cancelDeclaredOperatorFee(1), + [GasGroup.CANCEL_OPERATOR_FEE] + ) + ).to.emit( + operators, + Events.OPERATOR_FEE_DECLARATION_CANCELLED + ); + + const request = await operators.getOperatorFeeChangeRequest(1); + expect(request.fee).to.equal(0n); + expect(request.approvalBeginTime).to.equal(0n); + expect(request.approvalEndTime).to.equal(0n); + }); + + it("Is reverted with 'NoFeeDeclared' when canceling without a declaration", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.cancelDeclaredOperatorFee(1)).to.be.revertedWithCustomError( + operators, + Errors.NO_FEE_DECLARED + ); + }); +}); diff --git a/test/unit/SSVOperators/declareOperatorFee.test.ts b/test/unit/SSVOperators/declareOperatorFee.test.ts new file mode 100644 index 000000000..789337efc --- /dev/null +++ b/test/unit/SSVOperators/declareOperatorFee.test.ts @@ -0,0 +1,163 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvOperatorsHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + DEFAULT_OPERATOR_ETH_FEE, + ETH_DEDUCTED_DIGITS, + MINIMAL_OPERATOR_ETH_FEE, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators function `declareOperatorFee()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + const deployOperatorsWithTightMaxFee = async () => + ssvOperatorsHarnessFixture(connection, MINIMAL_OPERATOR_ETH_FEE); + + it("Declares operator fee within allowed limits and emits event", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + const operatorId = 1; + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await expect( + trackGas( + operators.declareOperatorFee(operatorId, newFee), + [GasGroup.DECLARE_OPERATOR_FEE] + ) + ).to.emit(operators, Events.OPERATOR_FEE_DECLARED); + + const request = await operators.getOperatorFeeChangeRequest(operatorId); + expect(request.fee).to.equal(BigInt(newFee) / ETH_DEDUCTED_DIGITS); + expect(request.approvalBeginTime).to.be.greaterThan(0); + expect(request.approvalEndTime).to.be.greaterThan(request.approvalBeginTime); + }); + + it("Is reverted with 'FeeTooLow' when declaring below minimal fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await operators.mockSetMinimumOperatorEthFee(20_000_000); + await expect(operators.declareOperatorFee(1, 10_000_000)).to.be.revertedWithCustomError(operators, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeTooHigh' when declaring above max fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsWithTightMaxFee); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.declareOperatorFee(1, Number(MINIMAL_OPERATOR_ETH_FEE * 2n))).to.be.revertedWithCustomError( + operators, + Errors.FEE_TOO_HIGH + ); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' when starting from zero fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), 0, false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.declareOperatorFee(1, Number(MINIMAL_OPERATOR_ETH_FEE))).to.be.revertedWithCustomError( + operators, + Errors.FEE_INCREASE_NOT_ALLOWED + ); + }); + + it("Is reverted with 'SameFeeChangeNotAllowed' when declaring same fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.declareOperatorFee(1, Number(MINIMAL_OPERATOR_ETH_FEE))).to.be.revertedWithCustomError( + operators, + Errors.SAME_FEE_CHANGE_NOT_ALLOWED + ); + }); + + it("Is reverted with 'FeeExceedsIncreaseLimit' when increasing fee beyond allowed percentage", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE); + await operators.registerOperator(makeOperatorKey(1), initialFee, false); + const newFee = initialFee * 3; + + await expect(operators.declareOperatorFee(1, newFee)).to.be.revertedWithCustomError( + operators, + Errors.FEE_EXCEEDS_INCREASE_LIMIT + ); + }); + + it("Is reverted with 'MaxPrecisionExceeded' when declared fee is not aligned to ETH_DEDUCTED_DIGITS", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.declareOperatorFee(1, 1n)) + .to.be.revertedWithCustomError(operators, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Emits OperatorFeeExecuted when defaulting legacy SSV operator to ETH fee on declare", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + const operatorId = 1; + await operators.mockSetOperatorLegacySSV(operatorId, 1); + + const newFee = DEFAULT_OPERATOR_ETH_FEE + DEFAULT_OPERATOR_ETH_FEE / 2n; // 1.5× = 2_655_000_000n + + const tx = await operators.declareOperatorFee(operatorId, newFee); + const receipt = await tx.wait(); + const expectedBlock = BigInt(receipt!.blockNumber); + + await expect(tx).to.emit(operators, Events.OPERATOR_FEE_EXECUTED) + .withArgs(owner.address, operatorId, expectedBlock, DEFAULT_OPERATOR_ETH_FEE); + + await expect(tx).to.emit(operators, Events.OPERATOR_FEE_DECLARED) + .withArgs(owner.address, operatorId, expectedBlock, newFee); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to declare fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [_, other] = await connection.ethers.getSigners(); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + + await expect(operators.connect(other).declareOperatorFee(1, Number(MINIMAL_OPERATOR_ETH_FEE) * 2)) + .to.be.revertedWithCustomError(operators, Errors.CALLER_NOT_OWNER); + }); +}); diff --git a/test/unit/SSVOperators/executeOperatorFee.test.ts b/test/unit/SSVOperators/executeOperatorFee.test.ts new file mode 100644 index 000000000..fbafa5832 --- /dev/null +++ b/test/unit/SSVOperators/executeOperatorFee.test.ts @@ -0,0 +1,213 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { ssvOperatorsHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + DECLARE_OPERATOR_FEE_PERIOD, ETH_DEDUCTED_DIGITS, EXECUTE_OPERATOR_FEE_PERIOD, + MAXIMUM_OPERATORS_FEE, + MINIMAL_OPERATOR_ETH_FEE, OPERATOR_MAX_FEE_INCREASE, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators function `executeOperatorFee()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + const deployOperatorsWithDelay = async () => defaultOperatorsFixture(connection); + + it("Executes declared fee and emits event", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await trackGas( + operators.declareOperatorFee(1, MINIMAL_OPERATOR_ETH_FEE * 2n), + [GasGroup.DECLARE_OPERATOR_FEE] + ); + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await expect( + trackGas( + operators.executeOperatorFee(1), + [GasGroup.EXECUTE_OPERATOR_FEE] + ) + ).to.emit(operators, Events.OPERATOR_FEE_EXECUTED); + }); + + it("Is reverted with 'NoFeeDeclared' when executing without a declaration", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.executeOperatorFee(1)).to.be.revertedWithCustomError( + operators, + Errors.NO_FEE_DECLARED + ); + }); + + it("Is reverted with 'ApprovalNotWithinTimeframe' when executing too early or too late", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsWithDelay); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await trackGas( + operators.declareOperatorFee(1, MINIMAL_OPERATOR_ETH_FEE * 2n), + [GasGroup.DECLARE_OPERATOR_FEE] + ); + + await expect(operators.executeOperatorFee(1)).to.be.revertedWithCustomError( + operators, + Errors.APPROVAL_NOT_WITHIN_TIMEFRAME + ); + await networkHelpers.time.increase(250); + + await expect(operators.executeOperatorFee(1)).to.be.revertedWithCustomError( + operators, + Errors.APPROVAL_NOT_WITHIN_TIMEFRAME + ); + }); + + it("Updates operator fee and clears request after execution", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE); + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await operators.registerOperator(makeOperatorKey(1), initialFee, false); + await operators.declareOperatorFee(1, newFee); + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await operators.executeOperatorFee(1); + + const op = await operators.getOperator(1); + expect(op.ethFee).to.equal(BigInt(newFee) / ETH_DEDUCTED_DIGITS); + + const request = await operators.getOperatorFeeChangeRequest(1); + expect(request.approvalBeginTime).to.equal(0); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to execute fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [_, other] = await connection.ethers.getSigners(); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.declareOperatorFee(1, MINIMAL_OPERATOR_ETH_FEE * 2n); + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await expect(operators.connect(other).executeOperatorFee(1)) + .to.be.revertedWithCustomError(operators, Errors.CALLER_NOT_OWNER); + }); + + it("Is reverted with 'FeeTooHigh' if DAO lowers max fee below declared amount before execution", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE); + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await operators.registerOperator(makeOperatorKey(1), initialFee, false); + await operators.declareOperatorFee(1, newFee); + await operators.mockSetOperatorMaxFee(Number(MINIMAL_OPERATOR_ETH_FEE)); + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await expect(operators.executeOperatorFee(1)).to.be.revertedWithCustomError( + operators, + Errors.FEE_TOO_HIGH + ); + }); + + it("Is reverted with 'LegacyOperatorFeeDeclarationInvalid' when executing a pre-upgrade fee declaration", async function () { + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + const upgradeTimestamp = currentTime + 1000n; + + const operators = (await ssvOperatorsHarnessFixture( + connection, + MAXIMUM_OPERATORS_FEE, + DECLARE_OPERATOR_FEE_PERIOD, + EXECUTE_OPERATOR_FEE_PERIOD, + OPERATOR_MAX_FEE_INCREASE, + upgradeTimestamp + )).operators; + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + + const legacyApprovalBeginTime = upgradeTimestamp - 500n; + const legacyApprovalEndTime = upgradeTimestamp + 500n; + const newFee = 2n; + + await operators.mockSetOperatorFeeChangeRequest( + 1, + newFee, + legacyApprovalBeginTime, + legacyApprovalEndTime + ); + + await expect(operators.executeOperatorFee(1)).to.be.revertedWithCustomError( + operators, + Errors.LEGACY_OPERATOR_FEE_DECLARATION_INVALID + ); + }); + + it("executeOperatorFee reverts when governance minimum rises above declared fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const declaredFee = MINIMAL_OPERATOR_ETH_FEE + ETH_DEDUCTED_DIGITS; + const raisedMinimum = declaredFee + ETH_DEDUCTED_DIGITS; + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.declareOperatorFee(1, declaredFee); + await operators.mockSetMinimumOperatorEthFee(Number(raisedMinimum)); + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await expect(operators.executeOperatorFee(1)).to.be.revertedWithCustomError( + operators, + Errors.FEE_TOO_LOW + ); + + const request = await operators.getOperatorFeeChangeRequest(1); + expect(request.fee).to.equal(declaredFee / ETH_DEDUCTED_DIGITS); + expect(request.approvalBeginTime).to.not.equal(0n); + expect(request.approvalEndTime).to.not.equal(0n); + + const operator = await operators.getOperator(1); + expect(operator.ethFee).to.equal(MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS); + }); + + it("[F-08] executeOperatorFee succeeds when declared fee equals newly raised minimum", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const declaredFee = MINIMAL_OPERATOR_ETH_FEE + ETH_DEDUCTED_DIGITS * 3n; + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.declareOperatorFee(1, declaredFee); + await operators.mockSetMinimumOperatorEthFee(Number(declaredFee)); + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await expect(operators.executeOperatorFee(1)) + .to.emit(operators, Events.OPERATOR_FEE_EXECUTED); + + const operator = await operators.getOperator(1); + expect(operator.ethFee).to.equal(declaredFee / ETH_DEDUCTED_DIGITS); + + const request = await operators.getOperatorFeeChangeRequest(1); + expect(request.approvalBeginTime).to.equal(0n); + expect(request.approvalEndTime).to.equal(0n); + }); +}); diff --git a/test/unit/SSVOperators/operatorPrivacy.test.ts b/test/unit/SSVOperators/operatorPrivacy.test.ts new file mode 100644 index 000000000..226ee323e --- /dev/null +++ b/test/unit/SSVOperators/operatorPrivacy.test.ts @@ -0,0 +1,83 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + MINIMAL_OPERATOR_ETH_FEE, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; +import { Errors } from "../../common/errors.ts"; + +describe("SSVOperators privacy helpers", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + + it("Updates privacy status via unchecked helpers", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect( + trackGas( + operators.setOperatorsPrivateUnchecked([1]), + [GasGroup.SET_OPERATORS_PRIVATE_10] + ) + ).to.emit( + operators, + Events.OPERATOR_PRIVACY_STATUS_UPDATED + ).withArgs([1n], true); + + await expect( + trackGas( + operators.setOperatorsPublicUnchecked([1]), + [GasGroup.SET_OPERATORS_PUBLIC_10] + ) + ).to.emit( + operators, + Events.OPERATOR_PRIVACY_STATUS_UPDATED + ).withArgs([1n], false); + }); + + it("Updates privacy status for a batch of operators", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.registerOperator(makeOperatorKey(2), Number(MINIMAL_OPERATOR_ETH_FEE), false); + + const ids = [1n, 2n]; + await expect(operators.setOperatorsPrivateUnchecked(ids)) + .to.emit(operators, Events.OPERATOR_PRIVACY_STATUS_UPDATED) + .withArgs(ids, true); + + const op1 = await operators.getOperator(1); + const op2 = await operators.getOperator(2); + expect(op1.whitelisted).to.be.true; + expect(op2.whitelisted).to.be.true; + await expect(operators.setOperatorsPublicUnchecked(ids)) + .to.emit(operators, Events.OPERATOR_PRIVACY_STATUS_UPDATED) + .withArgs(ids, false); + + const op1Public = await operators.getOperator(1); + expect(op1Public.whitelisted).to.be.false; + }); + + it("Is reverted with 'CallerNotOwnerWithData' if caller does not own all operators in batch", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner, other] = await connection.ethers.getSigners(); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.connect(other).registerOperator(makeOperatorKey(2), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await expect(operators.setOperatorsPrivateUnchecked([1n, 2n])) + .to.be.revertedWithCustomError(operators, Errors.CALLER_NOT_OWNER); + }); +}); diff --git a/test/unit/SSVOperators/reduceOperatorFee.test.ts b/test/unit/SSVOperators/reduceOperatorFee.test.ts new file mode 100644 index 000000000..f9965e4aa --- /dev/null +++ b/test/unit/SSVOperators/reduceOperatorFee.test.ts @@ -0,0 +1,158 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + ETH_DEDUCTED_DIGITS, + MINIMAL_OPERATOR_ETH_FEE, +} from '../../common/constants.ts'; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators function `reduceOperatorFee()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + const LEGACY_REDUCED_FEE = 1_000_000_000n; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + + it("Reduces operator fee and emits execution event", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE * 2n); + await trackGas( + operators.registerOperator(makeOperatorKey(1), initialFee, false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect( + trackGas( + operators.reduceOperatorFee(1, Number(MINIMAL_OPERATOR_ETH_FEE)), + [GasGroup.REDUCE_OPERATOR_FEE] + ) + ).to.emit( + operators, + Events.OPERATOR_FEE_EXECUTED + ); + }); + + it("Is reverted with 'FeeIncreaseNotAllowed' when reducing to the same or higher fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE * 2n); + await trackGas( + operators.registerOperator(makeOperatorKey(1), initialFee, false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.reduceOperatorFee(1, initialFee)).to.be.revertedWithCustomError( + operators, + Errors.FEE_INCREASE_NOT_ALLOWED + ); + }); + + it("Clears pending fee declaration when reducing fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE * 2n); + const declaredFee = Number(MINIMAL_OPERATOR_ETH_FEE * 3n); + const reducedFee = Number(MINIMAL_OPERATOR_ETH_FEE); + + await operators.registerOperator(makeOperatorKey(1), initialFee, false); + await operators.declareOperatorFee(1, declaredFee); + let request = await operators.getOperatorFeeChangeRequest(1); + expect(request.approvalBeginTime).to.be.gt(0); + await operators.reduceOperatorFee(1, reducedFee); + request = await operators.getOperatorFeeChangeRequest(1); + expect(request.approvalBeginTime).to.equal(0); + const op = await operators.getOperator(1); + expect(op.ethFee).to.equal(BigInt(reducedFee) / ETH_DEDUCTED_DIGITS); + }); + + it("Is reverted with 'FeeTooLow' when reducing below minimal allowed fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE * 2n); + + await operators.registerOperator(makeOperatorKey(1), initialFee, false); + + await operators.mockSetMinimumOperatorEthFee(Number(MINIMAL_OPERATOR_ETH_FEE)); + await expect(operators.reduceOperatorFee(1, 10_000_000)).to.be.revertedWithCustomError( + operators, + Errors.FEE_TOO_LOW + ); + }); + + it("Is reverted with 'MaxPrecisionExceeded' when reduced fee is not aligned to ETH_DEDUCTED_DIGITS", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE * 2n); + + await operators.registerOperator(makeOperatorKey(1), initialFee, false); + + await expect(operators.reduceOperatorFee(1, 1n)) + .to.be.revertedWithCustomError(operators, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to reduce fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [_, other] = await connection.ethers.getSigners(); + const initialFee = Number(MINIMAL_OPERATOR_ETH_FEE * 2n); + + await operators.registerOperator(makeOperatorKey(1), initialFee, false); + + await expect(operators.connect(other).reduceOperatorFee(1, Number(MINIMAL_OPERATOR_ETH_FEE))) + .to.be.revertedWithCustomError(operators, Errors.CALLER_NOT_OWNER); + }); + + it("Initializes legacy ETH snapshot and reduces fee for SSV legacy operator", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE * 2n, false); + await operators.mockSetOperatorLegacySSV(1, 1); + + const before = await operators.getOperator(1); + expect(before.ethSnapshot.block).to.equal(0); + expect(before.ethFee).to.equal(0n); + expect(before.fee).to.equal(1n); + + const tx = await operators.reduceOperatorFee(1, LEGACY_REDUCED_FEE); + const receipt = await tx.wait(); + + const after = await operators.getOperator(1); + expect(after.ethSnapshot.block).to.be.gt(0); + expect(after.ethFee).to.equal(LEGACY_REDUCED_FEE / ETH_DEDUCTED_DIGITS); + + const feeExecutedLogs = receipt?.logs.filter((log: any) => { + try { + const parsed = operators.interface.parseLog(log); + return parsed?.name === Events.OPERATOR_FEE_EXECUTED; + } catch { + return false; + } + }) ?? []; + expect(feeExecutedLogs.length).to.equal(2); + }); + + it("Keeps explicit zero fee after legacy initialization marker is set", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE * 2n, false); + await operators.mockSetOperatorLegacySSV(1, 1); + + await operators.reduceOperatorFee(1, 0n); + + const afterFirstReduce = await operators.getOperator(1); + expect(afterFirstReduce.ethSnapshot.block).to.be.gt(0); + expect(afterFirstReduce.ethFee).to.equal(0n); + + await expect(operators.reduceOperatorFee(1, 0n)) + .to.be.revertedWithCustomError(operators, Errors.FEE_INCREASE_NOT_ALLOWED); + + const afterSecondAttempt = await operators.getOperator(1); + expect(afterSecondAttempt.ethFee).to.equal(0n); + }); +}); diff --git a/test/unit/SSVOperators/reentrancy.test.ts b/test/unit/SSVOperators/reentrancy.test.ts new file mode 100644 index 000000000..994804c0b --- /dev/null +++ b/test/unit/SSVOperators/reentrancy.test.ts @@ -0,0 +1,82 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + MINIMAL_OPERATOR_ETH_FEE, + DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS, +} from '../../common/constants.ts'; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators reentrancy guard", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + + it("Blocks reentrancy during ETH earnings withdrawal", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const attacker = await connection.ethers.deployContract( + "OperatorEarningsReentrancy", + [await operators.getAddress()] + ); + await attacker.waitForDeployment(); + + await trackGas( + attacker.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + const operatorId = await attacker.operatorId(); + + await networkHelpers.setBalance(await operators.getAddress(), connection.ethers.parseEther("10")); + await operators.mockSetOperatorBalances(Number(operatorId), 5, 0); + + const withdrawAmount = 2n * ETH_DEDUCTED_DIGITS; + const reenterAmount = 1n * ETH_DEDUCTED_DIGITS; + + await attacker.setReenterAmount(reenterAmount); + await trackGas( + attacker.triggerWithdraw(withdrawAmount), + [GasGroup.WITHDRAW_OPERATOR_BALANCE] + ); + + expect(await attacker.reentered()).to.equal(true); + expect(await attacker.reenterSucceeded()).to.equal(false); + + const operatorAfter = await operators.getOperator(operatorId); + expect(operatorAfter.ethSnapshot.balance).to.equal(3n); + }); + + it("Blocks reentrancy during SSV earnings withdrawal", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const token = await connection.ethers.deployContract("ReentrantTokenMock"); + await token.waitForDeployment(); + await operators.mockSetToken(await token.getAddress()); + const attacker = await connection.ethers.deployContract( + "OperatorEarningsReentrancySSV", + [await operators.getAddress(), await token.getAddress()] + ); + await attacker.waitForDeployment(); + await trackGas( + attacker.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + const operatorId = await attacker.operatorId(); + await token.mint(await operators.getAddress(), connection.ethers.parseEther("100")); + await operators.mockSetOperatorLegacySSV(Number(operatorId), 1n); + await operators.mockSetOperatorBalances(Number(operatorId), 0, 5n); + const withdrawAmount = 2n * DEDUCTED_DIGITS; + const reenterAmount = 1n * DEDUCTED_DIGITS; + + await attacker.setReenterAmount(reenterAmount); + await attacker.triggerWithdraw(withdrawAmount); + }); +}); diff --git a/test/unit/SSVOperators/registerOperator.test.ts b/test/unit/SSVOperators/registerOperator.test.ts new file mode 100644 index 000000000..1baf8dee6 --- /dev/null +++ b/test/unit/SSVOperators/registerOperator.test.ts @@ -0,0 +1,150 @@ +import { expect } from "chai"; +import { ethers } from "ethers"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvOperatorsHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { MAXIMUM_OPERATORS_FEE, MINIMAL_OPERATOR_ETH_FEE } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; +import { ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; + +describe("SSVOperators function `registerOperator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner] } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => ssvOperatorsHarnessFixture(connection); + + it("Registers an operator with valid params and emits expected events", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const publicKey = makeOperatorKey(1); + const fee = MINIMAL_OPERATOR_ETH_FEE; + + const tx = await trackGas( + operators.registerOperator(publicKey, fee, true), + [GasGroup.REGISTER_OPERATOR] + ); + await expect(tx).to.emit(operators, Events.OPERATOR_ADDED).withArgs(1n, owner.address, publicKey, fee); + await expect(tx).to.emit(operators, Events.OPERATOR_PRIVACY_STATUS_UPDATED).withArgs([1n], true); + }); + + it("Is reverted with 'FeeTooLow' when provided fee is below minimal allowed", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.mockSetMinimumOperatorEthFee(Number(MINIMAL_OPERATOR_ETH_FEE)); + + await expect(operators.registerOperator( + makeOperatorKey(1), + 1n, + false + )).to.be.revertedWithCustomError(operators, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with 'FeeTooHigh' when provided fee exceeds max operator fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await expect(operators.registerOperator( + makeOperatorKey(1), + MAXIMUM_OPERATORS_FEE + 1n, + false + )).to.be.revertedWithCustomError(operators, Errors.FEE_TOO_HIGH); + }); + + it("Is reverted with 'OperatorAlreadyExists' when registering duplicate public key", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const publicKey = makeOperatorKey(1); + await trackGas( + operators.registerOperator(publicKey, MINIMAL_OPERATOR_ETH_FEE, false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.registerOperator( + publicKey, + MINIMAL_OPERATOR_ETH_FEE, + false + )).to.be.revertedWithCustomError(operators, Errors.OPERATOR_ALREADY_EXISTS); + }); + + it("Stores operator data in storage", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const publicKey = makeOperatorKey(1); + await trackGas( + operators.registerOperator(publicKey, MINIMAL_OPERATOR_ETH_FEE, true), + [GasGroup.REGISTER_OPERATOR] + ); + + const operatorData = await operators.getOperator(1); + + expect(operatorData.owner).to.equal(owner.address); + expect(operatorData.ethFee).to.equal(MINIMAL_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS); + expect(operatorData.whitelisted).to.equal(true); + expect(operatorData.ethSnapshot.block).to.be.greaterThan(0); + }); + + it("Registers an operator with 0 fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const publicKey = makeOperatorKey(1); + + await expect(operators.registerOperator(publicKey, 0n, false)) + .to.emit(operators, Events.OPERATOR_ADDED) + .withArgs(1n, owner.address, publicKey, 0n); + + const operatorData = await operators.getOperator(1); + expect(operatorData.ethFee).to.equal(0n); + }); + + it("Registers an operator with exact max fee", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const publicKey = makeOperatorKey(1); + + await expect(operators.registerOperator(publicKey, MAXIMUM_OPERATORS_FEE, false)) + .to.emit(operators, Events.OPERATOR_ADDED) + .withArgs(1n, owner.address, publicKey, MAXIMUM_OPERATORS_FEE); + }); + + it("Registers a public operator (whitelisted = false)", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const publicKey = makeOperatorKey(1); + + await expect(operators.registerOperator(publicKey, MINIMAL_OPERATOR_ETH_FEE, false)) + .to.emit(operators, Events.OPERATOR_PRIVACY_STATUS_UPDATED) + .withArgs([1n], false); + + const operatorData = await operators.getOperator(1); + expect(operatorData.whitelisted).to.equal(false); + }); + + it("Is reverted with 'MaxPrecisionExceeded' when fee is not aligned to ETH_DEDUCTED_DIGITS", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await expect(operators.registerOperator( + makeOperatorKey(1), + 1n, + false + )).to.be.revertedWithCustomError(operators, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Increments operator ID correctly for multiple registrations", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.registerOperator(makeOperatorKey(1), MINIMAL_OPERATOR_ETH_FEE, false); + await operators.registerOperator(makeOperatorKey(2), MINIMAL_OPERATOR_ETH_FEE, false); + + const op1 = await operators.getOperator(1); + const op2 = await operators.getOperator(2); + + expect(op1.owner).to.not.equal(ethers.ZeroAddress); + expect(op2.owner).to.not.equal(ethers.ZeroAddress); + }); +}); diff --git a/test/unit/SSVOperators/removeOperator.test.ts b/test/unit/SSVOperators/removeOperator.test.ts new file mode 100644 index 000000000..6ab43fc7d --- /dev/null +++ b/test/unit/SSVOperators/removeOperator.test.ts @@ -0,0 +1,233 @@ +import { expect } from "chai"; +import { ethers } from "ethers"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvOperatorsHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { DECLARE_OPERATOR_FEE_PERIOD, DEFAULT_OPERATOR_ETH_FEE, ETH_DEDUCTED_DIGITS, EXECUTE_OPERATOR_FEE_PERIOD, MINIMAL_OPERATOR_ETH_FEE } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGas, trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators function `removeOperator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + let other: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner, other] } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => ssvOperatorsHarnessFixture(connection); + + it("Removes operator successfully and emits expected event", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect( + trackGas( + operators.removeOperator(1), + [GasGroup.REMOVE_OPERATOR] + ) + ).to.emit(operators, Events.OPERATOR_REMOVED).withArgs(1n); + + const operatorData = await operators.getOperator(1); + expect(operatorData.ethFee).to.equal(0n); + expect(await operators.getOperatorWhitelist(1)).to.equal(ethers.ZeroAddress); + }); + + it("Removes operator with a balance and withdraws", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + + await operators.mockSetOperatorBalances(1, 1n, 0n); + + const operatorsAddress = await operators.getAddress(); + await connection.ethers.provider.send("hardhat_setBalance", [ + operatorsAddress, + `0x${(10_000_000n).toString(16)}`, + ]); + + const tx = await operators.removeOperator(1); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]); + }); + + it("Removes operator with SSV balance and withdraws", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const token = await connection.ethers.deployContract("MockToken"); + await token.waitForDeployment(); + + await operators.mockSetToken(await token.getAddress()); + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.mockSetOperatorLegacySSV(1, 1n); + await operators.mockSetOperatorBalances(1, 0n, 100n); + await token.mint(await operators.getAddress(), ethers.parseEther("1000")); + + const before = await token.balanceOf(owner.address); + await operators.removeOperator(1); + const after = await token.balanceOf(owner.address); + + expect(after).to.be.gt(before); + }); + + it("Removes a legacy SSV-only operator without initializing ETH state", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const token = await connection.ethers.deployContract("MockToken"); + await token.waitForDeployment(); + + await operators.mockSetToken(await token.getAddress()); + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.mockSetOperatorLegacySSV(1, 12n); + await operators.mockSetOperatorBalances(1, 0n, 100n); + await token.mint(await operators.getAddress(), ethers.parseEther("1000")); + + const before = await operators.getOperator(1); + expect(before.snapshot.block).to.be.greaterThan(0n); + expect(before.ethSnapshot.block).to.equal(0n); + expect(before.ethFee).to.equal(0n); + + const ownerBalanceBefore = await token.balanceOf(owner.address); + await operators.removeOperator(1); + + const ownerBalanceAfter = await token.balanceOf(owner.address); + expect(ownerBalanceAfter).to.be.gt(ownerBalanceBefore); + + const after = await operators.getOperator(1); + expect(after.snapshot.block).to.equal(0n); + expect(after.ethSnapshot.block).to.equal(0n); + expect(after.fee).to.equal(0n); + expect(after.ethFee).to.equal(0n); + }); + + it("Verifies operator state after removal (fees reset, owner persists)", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), true); + + await operators.removeOperator(1); + + const op = await operators.getOperator(1); + expect(op.ethFee).to.equal(0n); + expect(op.fee).to.equal(0n); + expect(op.validatorCount).to.equal(0n); + expect(op.owner).to.equal(owner.address); + expect(await operators.getOperatorWhitelist(1)).to.equal(ethers.ZeroAddress); + }); + + it("Clears a pending fee change request when removing an operator", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const operatorId = 1n; + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + + const declareTx = await operators.declareOperatorFee(operatorId, Number(newFee)); + const declareReceipt = await declareTx.wait(); + const declareBlock = await connection.ethers.provider.getBlock(declareReceipt!.blockNumber); + if (declareBlock === null) { + throw new Error("declareOperatorFee block not found"); + } + + const requestBeforeRemoval = await operators.getOperatorFeeChangeRequest(operatorId); + expect(requestBeforeRemoval.fee).to.equal(newFee / ETH_DEDUCTED_DIGITS); + expect(requestBeforeRemoval.approvalBeginTime).to.equal(BigInt(declareBlock.timestamp) + DECLARE_OPERATOR_FEE_PERIOD); + expect(requestBeforeRemoval.approvalEndTime).to.equal( + BigInt(declareBlock.timestamp) + DECLARE_OPERATOR_FEE_PERIOD + EXECUTE_OPERATOR_FEE_PERIOD + ); + + await operators.removeOperator(operatorId); + + const requestAfterRemoval = await operators.getOperatorFeeChangeRequest(operatorId); + expect(requestAfterRemoval.fee).to.equal(0n); + expect(requestAfterRemoval.approvalBeginTime).to.equal(0n); + expect(requestAfterRemoval.approvalEndTime).to.equal(0n); + }); + + it("Blocks executeOperatorFee with OperatorDoesNotExist after removal clears both snapshots", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const operatorId = 1n; + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.declareOperatorFee(operatorId, Number(newFee)); + await operators.removeOperator(operatorId); + + // Advance past the declare period so we'd be in the executable window + await networkHelpers.time.increase(Number(DECLARE_OPERATOR_FEE_PERIOD) + 1); + + // checkOwner() sees snapshot.block == 0 && ethSnapshot.block == 0 → OperatorDoesNotExist + // (the cleared fee change request provides defense-in-depth, but checkOwner fires first) + await expect( + operators.executeOperatorFee(operatorId) + ).to.be.revertedWithCustomError(operators, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Blocks cancelDeclaredOperatorFee with OperatorDoesNotExist after removal clears both snapshots", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const operatorId = 1n; + const newFee = MINIMAL_OPERATOR_ETH_FEE * 2n; + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.declareOperatorFee(operatorId, Number(newFee)); + await operators.removeOperator(operatorId); + + // checkOwner() sees snapshot.block == 0 && ethSnapshot.block == 0 → OperatorDoesNotExist + // (the cleared fee change request provides defense-in-depth, but checkOwner fires first) + await expect( + operators.cancelDeclaredOperatorFee(operatorId) + ).to.be.revertedWithCustomError(operators, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Cannot register the same public key after removal", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const key = makeOperatorKey(1); + + await operators.registerOperator(key, Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.removeOperator(1); + + await expect( + operators.registerOperator(key, Number(MINIMAL_OPERATOR_ETH_FEE), false) + ).to.be.revertedWithCustomError(operators, Errors.OPERATOR_ALREADY_EXISTS); + }); + + it("Clears operatorEthVUnits when removing an operator", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + + await operators.mockSetOperatorEthVUnits(1, 5000n); + expect(await operators.getOperatorEthVUnits(1)).to.equal(5000n); + + const operatorsAddress = await operators.getAddress(); + await connection.ethers.provider.send("hardhat_setBalance", [ + operatorsAddress, + `0x${ethers.parseEther("1").toString(16)}`, + ]); + + await operators.removeOperator(1); + + expect(await operators.getOperatorEthVUnits(1)).to.equal(0n); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to remove operator", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.connect(other).removeOperator(1)).to.be.revertedWithCustomError( + operators, + Errors.CALLER_NOT_OWNER + ); + }); +}); diff --git a/test/unit/SSVOperators/run-tests.sh b/test/unit/SSVOperators/run-tests.sh new file mode 100755 index 000000000..03a87838c --- /dev/null +++ b/test/unit/SSVOperators/run-tests.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Move to repo root +cd "$(dirname "${BASH_SOURCE[0]}")/../../.." + +pattern="test/unit/SSVOperators/*.test.ts" + +npx hardhat test $pattern "$@" diff --git a/test/unit/SSVOperators/withdrawAllVersionOperatorEarnings.test.ts b/test/unit/SSVOperators/withdrawAllVersionOperatorEarnings.test.ts new file mode 100644 index 000000000..1b2207afd --- /dev/null +++ b/test/unit/SSVOperators/withdrawAllVersionOperatorEarnings.test.ts @@ -0,0 +1,199 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { ethers } from "ethers"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + DECLARE_OPERATOR_FEE_PERIOD, + MINIMAL_OPERATOR_ETH_FEE, +} from '../../common/constants.ts'; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators function `withdrawAllVersionOperatorEarnings()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + + it("Withdraws both ETH and SSV earnings and resets balances", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await trackGas( + operators.declareOperatorFee(1, MINIMAL_OPERATOR_ETH_FEE * 2n), + [GasGroup.DECLARE_OPERATOR_FEE] + ); + + await networkHelpers.mine(DECLARE_OPERATOR_FEE_PERIOD + 1n); + + await trackGas( + operators.executeOperatorFee(1), + [GasGroup.EXECUTE_OPERATOR_FEE] + ); + await operators.mockSetOperatorBalances(1, 2, 0); + const harnessAddress = await operators.getAddress(); + await networkHelpers.setBalance(harnessAddress, connection.ethers.parseEther("1")); + + await expect( + trackGas( + operators.withdrawAllVersionOperatorEarnings(1), + [GasGroup.WITHDRAW_OPERATOR_BALANCE_ALL_VERSIONS] + ) + ).to.emit( + operators, + Events.OPERATOR_WITHDRAWN + ); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.ethSnapshot.balance).to.equal(0n); + expect(operatorAfter.snapshot.balance).to.equal(0n); + }); + + it("Withdraws both ETH and SSV earnings when both have balances", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner] = await connection.ethers.getSigners(); + const token = await connection.ethers.deployContract("MockToken"); + await token.waitForDeployment(); + await operators.mockSetToken(await token.getAddress()); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + const harnessAddress = await operators.getAddress(); + await networkHelpers.setBalance(harnessAddress, connection.ethers.parseEther("1")); + await token.mint(harnessAddress, connection.ethers.parseEther("100")); + + // Initialize the legacy SSV side so the all-version withdrawal has both paths active. + const operatorBefore = await operators.getOperator(1); + await operators.mockSetOperator(1, { + validatorCount: operatorBefore.validatorCount, + fee: 0n, + owner: operatorBefore.owner, + whitelisted: operatorBefore.whitelisted, + snapshot: { + block: operatorBefore.ethSnapshot.block, + index: 0n, + balance: 0n, + }, + ethValidatorCount: operatorBefore.ethValidatorCount, + ethFee: operatorBefore.ethFee, + ethSnapshot: { + block: operatorBefore.ethSnapshot.block, + index: operatorBefore.ethSnapshot.index, + balance: operatorBefore.ethSnapshot.balance, + }, + }); + + // Simulate both ETH and SSV balances + const ethBalance = 2n; + const ssvBalance = 3n; + await operators.mockSetOperatorBalances(1, Number(ethBalance), Number(ssvBalance)); + + const ownerSsvBalanceBefore = await token.balanceOf(owner.address); + + await expect( + trackGas( + operators.withdrawAllVersionOperatorEarnings(1), + [GasGroup.WITHDRAW_OPERATOR_BALANCE_ALL_VERSIONS] + ) + ).to.emit(operators, Events.OPERATOR_WITHDRAWN); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.ethSnapshot.balance).to.equal(0n); + expect(operatorAfter.snapshot.balance).to.equal(0n); + + const ownerSsvBalanceAfter = await token.balanceOf(owner.address); + expect(ownerSsvBalanceAfter).to.be.gt(ownerSsvBalanceBefore); + }); + + it("Succeeds when withdrawing with zero balances (no-op)", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await operators.mockSetOperatorBalances(1, 0, 0); + await operators.withdrawAllVersionOperatorEarnings(1); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.ethSnapshot.balance).to.equal(0n); + expect(operatorAfter.snapshot.balance).to.equal(0n); + }); + + it("Does not initialize ETH snapshot for a legacy SSV-only operator", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + + // Seed a legacy SSV-only operator: SSV fee set, ETH state unset. + await operators.mockSetOperatorLegacySSV(1, 12n); + + const operatorBefore = await operators.getOperator(1); + expect(operatorBefore.fee).to.equal(12n); + expect(operatorBefore.ethFee).to.equal(0n); + expect(operatorBefore.ethSnapshot.block).to.equal(0n); + + await operators.withdrawAllVersionOperatorEarnings(1); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.fee).to.equal(12n); + expect(operatorAfter.ethFee).to.equal(0n); + expect(operatorAfter.snapshot.block).to.be.greaterThan(0n); + expect(operatorAfter.ethSnapshot.block).to.equal(0n); + }); + + it("Pays out SSV balance for a legacy SSV-only operator without initializing ETH snapshot", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner] = await connection.ethers.getSigners(); + + const token = await connection.ethers.deployContract("MockToken"); + await token.waitForDeployment(); + await operators.mockSetToken(await token.getAddress()); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await operators.mockSetOperatorLegacySSV(1, 12n); + + // Seed a non-zero SSV balance and fund the contract with tokens + await operators.mockSetOperatorBalances(1, 0n, 50n); + await token.mint(await operators.getAddress(), ethers.parseEther("100")); + + const ownerSsvBefore = await token.balanceOf(owner.address); + await operators.withdrawAllVersionOperatorEarnings(1); + const ownerSsvAfter = await token.balanceOf(owner.address); + + expect(ownerSsvAfter).to.be.gt(ownerSsvBefore); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.snapshot.balance).to.equal(0n); + expect(operatorAfter.ethSnapshot.block).to.equal(0n); + expect(operatorAfter.ethFee).to.equal(0n); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to withdraw", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + const [, other] = await connection.ethers.getSigners(); + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.connect(other).withdrawAllVersionOperatorEarnings(1)).to.be.revertedWithCustomError( + operators, + Errors.CALLER_NOT_OWNER + ); + }); +}); diff --git a/test/unit/SSVOperators/withdrawOperatorEarnings.test.ts b/test/unit/SSVOperators/withdrawOperatorEarnings.test.ts new file mode 100644 index 000000000..994507054 --- /dev/null +++ b/test/unit/SSVOperators/withdrawOperatorEarnings.test.ts @@ -0,0 +1,162 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, seedOperatorWithETHBalance, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + MINIMAL_OPERATOR_ETH_FEE, + ETH_DEDUCTED_DIGITS +} from '../../common/constants.ts'; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators ETH earnings withdrawals", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + + + it("withdrawOperatorEarnings withdraws specific amount and emits event", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner] = await connection.ethers.getSigners(); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await seedOperatorWithETHBalance(networkHelpers, connection, operators, 1, 5n); + + const amount = 2n * ETH_DEDUCTED_DIGITS; + + await expect( + trackGas( + operators.withdrawOperatorEarnings(1, amount), + [GasGroup.WITHDRAW_OPERATOR_BALANCE] + ) + ) + .to.emit(operators, Events.OPERATOR_WITHDRAWN) + .withArgs(owner.address, 1, amount); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.ethSnapshot.balance).to.equal(3n); + }); + + it("Succeeds when withdrawing zero amount", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await seedOperatorWithETHBalance(networkHelpers, connection, operators, 1, 5n); + await operators.withdrawOperatorEarnings(1, 0n); + }); + + it("withdrawAllOperatorEarnings withdraws full balance and resets snapshot", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner] = await connection.ethers.getSigners(); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await seedOperatorWithETHBalance(networkHelpers, connection, operators, 1, 4n); + + const expectedAmount = 4n * ETH_DEDUCTED_DIGITS; + + await expect( + trackGas( + operators.withdrawAllOperatorEarnings(1), + [GasGroup.WITHDRAW_OPERATOR_BALANCE] + ) + ) + .to.emit(operators, Events.OPERATOR_WITHDRAWN) + .withArgs(owner.address, 1, expectedAmount); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.ethSnapshot.balance).to.equal(0n); + }); + + it("Is reverted with 'InsufficientBalance' when withdrawing more than ETH snapshot balance", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.withdrawOperatorEarnings(1, ETH_DEDUCTED_DIGITS)).to.be.revertedWithCustomError( + operators, + Errors.INSUFFICIENT_BALANCE + ); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to withdraw ETH earnings", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [, other] = await connection.ethers.getSigners(); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await seedOperatorWithETHBalance(networkHelpers, connection, operators, 1, 1n); + + await expect(operators.connect(other).withdrawOperatorEarnings(1, ETH_DEDUCTED_DIGITS)).to.be.revertedWithCustomError( + operators, + Errors.CALLER_NOT_OWNER + ); + }); + + it("Is reverted with 'MaxPrecisionExceeded' when ETH withdrawal amount is not aligned to ETH_DEDUCTED_DIGITS", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await seedOperatorWithETHBalance(networkHelpers, connection, operators, 1, 5n); + + await expect(operators.withdrawOperatorEarnings(1, 1n)) + .to.be.revertedWithCustomError(operators, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to withdraw all ETH earnings", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [, other] = await connection.ethers.getSigners(); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await seedOperatorWithETHBalance(networkHelpers, connection, operators, 1, 1n); + + await expect(operators.connect(other).withdrawAllOperatorEarnings(1)).to.be.revertedWithCustomError( + operators, + Errors.CALLER_NOT_OWNER + ); + }); + + it("Withdraws exactly 1 * ETH_DEDUCTED_DIGITS (minimum non-zero precision unit) and zeroes balance", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner] = await connection.ethers.getSigners(); + + await operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false); + await seedOperatorWithETHBalance(networkHelpers, connection, operators, 1, 1n); + + const amount = 1n * ETH_DEDUCTED_DIGITS; + + await expect(operators.withdrawOperatorEarnings(1, amount)) + .to.emit(operators, Events.OPERATOR_WITHDRAWN) + .withArgs(owner.address, 1, amount); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.ethSnapshot.balance).to.equal(0n); + }); +}); + diff --git a/test/unit/SSVOperators/withdrawOperatorEarningsSSV.test.ts b/test/unit/SSVOperators/withdrawOperatorEarningsSSV.test.ts new file mode 100644 index 000000000..08b1fc182 --- /dev/null +++ b/test/unit/SSVOperators/withdrawOperatorEarningsSSV.test.ts @@ -0,0 +1,144 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { makeOperatorKey, setupTestContext } from "../../common/helpers.ts"; +import { defaultOperatorsFixture } from "../../helpers/fixture-presets.ts"; +import { + MINIMAL_OPERATOR_ETH_FEE, + DEDUCTED_DIGITS, ETH_DEDUCTED_DIGITS, +} from '../../common/constants.ts'; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVOperators SSV earnings withdrawals", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployOperatorsFixture = async () => defaultOperatorsFixture(connection); + + const seedOperatorWithSSVBalance = async (operators: any, operatorId: number, ssvSnapshotBalance: bigint) => { + const token = await connection.ethers.deployContract("MockToken"); + await token.waitForDeployment(); + await operators.mockSetToken(await token.getAddress()); + + const harnessAddress = await operators.getAddress(); + await token.mint(harnessAddress, connection.ethers.parseEther("1000")); + + await operators.mockSetOperatorBalances(operatorId, 0, Number(ssvSnapshotBalance)); + }; + + it("withdrawOperatorEarningsSSV withdraws specific amount and emits event", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner] = await connection.ethers.getSigners(); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await operators.mockSetOperatorLegacySSV(1, 1); + await seedOperatorWithSSVBalance(operators, 1, 5n); + + const amount = 2n * DEDUCTED_DIGITS; + + await expect( + trackGas( + operators.withdrawOperatorEarningsSSV(1, amount), + [GasGroup.WITHDRAW_OPERATOR_BALANCE] + ) + ) + .to.emit(operators, Events.OPERATOR_WITHDRAWN_SSV) + .withArgs(owner.address, 1, amount); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.snapshot.balance).to.equal(3n); + }); + + it("Succeeds when withdrawing zero amount", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await operators.mockSetOperatorLegacySSV(1, 1); + await seedOperatorWithSSVBalance(operators, 1, 5n); + await operators.withdrawOperatorEarningsSSV(1, 0n); + }); + + it("withdrawAllOperatorEarningsSSV withdraws full balance and resets snapshot", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [owner] = await connection.ethers.getSigners(); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await operators.mockSetOperatorLegacySSV(1, 1); + await seedOperatorWithSSVBalance(operators, 1, 4n); + + const expectedAmount = 4n * DEDUCTED_DIGITS; + + await expect( + trackGas( + operators.withdrawAllOperatorEarningsSSV(1), + [GasGroup.WITHDRAW_OPERATOR_BALANCE] + ) + ) + .to.emit(operators, Events.OPERATOR_WITHDRAWN_SSV) + .withArgs(owner.address, 1, expectedAmount); + + const operatorAfter = await operators.getOperator(1); + expect(operatorAfter.snapshot.balance).to.equal(0n); + }); + + it("Is reverted with 'InsufficientBalance' when withdrawing more than SSV snapshot balance", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await expect(operators.withdrawOperatorEarningsSSV(1, DEDUCTED_DIGITS)).to.be.revertedWithCustomError( + operators, + Errors.INSUFFICIENT_BALANCE + ); + }); + + it("Is reverted with 'MaxPrecisionExceeded' when SSV withdrawal amount is not aligned to DEDUCTED_DIGITS", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + + await operators.mockSetOperatorLegacySSV(1, 1); + await seedOperatorWithSSVBalance(operators, 1, 5n); + + await expect(operators.withdrawOperatorEarningsSSV(1, 1n)) + .to.be.revertedWithCustomError(operators, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Is reverted with 'CallerNotOwnerWithData' when non-owner tries to withdraw SSV earnings", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + const [, other] = await connection.ethers.getSigners(); + + await trackGas( + operators.registerOperator(makeOperatorKey(1), Number(MINIMAL_OPERATOR_ETH_FEE), false), + [GasGroup.REGISTER_OPERATOR] + ); + await seedOperatorWithSSVBalance(operators, 1, 1n); + + await expect(operators.connect(other).withdrawOperatorEarningsSSV(1, DEDUCTED_DIGITS)).to.be.revertedWithCustomError( + operators, + Errors.CALLER_NOT_OWNER + ); + }); +}); diff --git a/test/unit/SSVStaking/README.md b/test/unit/SSVStaking/README.md new file mode 100644 index 000000000..ff9e24d39 --- /dev/null +++ b/test/unit/SSVStaking/README.md @@ -0,0 +1,73 @@ +## SSVStaking Unit Tests + +This directory contains unit tests for the SSVStaking module, which handles SSV token staking, cSSV minting, and ETH rewards distribution in the SSV Network. + +### Running Tests + +- Run all unit tests under this suite: `npx hardhat test test/unit/SSVStaking/*.test.ts` +- Or use the helper script from repo root: `./test/unit/SSVStaking/run-tests.sh` + +### Test Coverage + +The tests cover: + +#### stake.test.ts +- Successful staking of SSV tokens with cSSV minting and event emission +- User index updates after staking +- Delegation creation to default oracles with oracle weight distribution +- Zero amount validation +- Minimum stake amount validation +- Multiple stakes from same user +- SSV token transfer verification +- **Storage checks**: Delegation data (oracle IDs and amounts) stored correctly + +#### requestUnstake.test.ts +- Successful unstake request with cSSV burning and event emission +- Withdrawal request creation with correct unlock time +- Proportional delegation removal +- Zero amount validation +- Cooldown active error (only one pending withdrawal allowed) +- Unstake amount exceeds balance validation +- Full balance unstaking +- **Storage checks**: Withdrawal request (amount and unlock time) stored correctly + +#### withdrawUnlocked.test.ts +- Successful withdrawal after cooldown period with event emission +- Withdrawal request clearing after withdrawal +- Nothing to withdraw validation +- Cooldown not finished validation +- Partial cooldown validation +- Exact unlock time withdrawal +- **Storage checks**: Withdrawal request cleared from storage after successful withdrawal + +#### claimEthRewards.test.ts +- Successful ETH rewards claiming with event emission +- Accrued balance reduction after claiming +- Nothing to claim validation (no rewards) +- Nothing to claim validation (amount too small) +- Insufficient balance validation +- Fee syncing before claiming +- **Storage checks**: Updated accrued balance stored correctly after claiming + +#### syncFees.test.ts +- Staking pool balance update with event emission +- accEthPerShare update with new fees +- No change when no new fees +- No change when total staked is zero +- DAO balance syncing +- Multiple sync calls +- **Storage checks**: Updated pool balance and accEthPerShare stored correctly + +#### rescueERC20.test.ts +- Successful rescue of accidentally sent ERC20 tokens with event emission +- Correct amount transfer to recipient +- Zero address validation (token) +- Zero address validation (recipient) +- Invalid token validation (SSV not rescuable) +- Invalid token validation (cSSV not rescuable) +- Zero amount validation +- Partial token rescue + +### Dependencies + +Hardhat will build artifacts on demand; make sure dependencies are installed before running (`npm install`). diff --git a/test/unit/SSVStaking/claimEthRewards.test.ts b/test/unit/SSVStaking/claimEthRewards.test.ts new file mode 100644 index 000000000..b9f2fa3e8 --- /dev/null +++ b/test/unit/SSVStaking/claimEthRewards.test.ts @@ -0,0 +1,616 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { ssvStakingHarnessFixture } from "../../setup/fixtures.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { STAKE_AMOUNT, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVStaking function `claimEthRewards()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let staker: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [staker] } = await setupTestContext()); + }); + + const stakeAndAccrueRewards = async () => { + const { staking, ssvToken, cssvToken } = await defaultStakingFixture(connection); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const rewardAmount = 10_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0); + await staking.mockSetEthDaoBalance(rewardAmount); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("1"), + }); + + return { staking, ssvToken, cssvToken, rewardAmount }; + }; + + it("Claims accrued ETH rewards and emits RewardsClaimed event with correct args", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndAccrueRewards); + + const accruedAmount = connection.ethers.parseEther("0.1"); + await staking.mockSetUserAccrued(staker.address, accruedAmount); + const expectedPayout = accruedAmount - (accruedAmount % ETH_DEDUCTED_DIGITS); + const expectedPayoutShrunk = expectedPayout / ETH_DEDUCTED_DIGITS; + await staking.mockSetStakingEthPoolBalance(expectedPayoutShrunk + 1_000_000n); + await staking.mockSetEthDaoBalance(expectedPayoutShrunk + 1_000_000n); + + const ethBalanceBefore = await connection.ethers.provider.getBalance(staker.address); + const poolBalanceBefore = await staking.getStakingEthPoolBalance(); + const daoBalanceBefore = await staking.getEthDaoBalance(); + + const tx = await trackGas( + staking.claimEthRewards(), + [GasGroup.CLAIM_ETH_REWARDS, GasGroup.SYNC_FEES] + ); + + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(staker.address, expectedPayout); + const ethBalanceAfter = await connection.ethers.provider.getBalance(staker.address); + const gasUsed = BigInt(tx.gasUsed) * BigInt(tx.gasPrice); + expect(ethBalanceAfter + gasUsed - ethBalanceBefore).to.equal(expectedPayout); + const poolBalanceAfter = await staking.getStakingEthPoolBalance(); + const daoBalanceAfter = await staking.getEthDaoBalance(); + expect(poolBalanceBefore - poolBalanceAfter).to.equal(expectedPayoutShrunk); + expect(daoBalanceBefore - daoBalanceAfter).to.equal(expectedPayoutShrunk); + }); + + it("Keeps remainder in accrued when user still holds cSSV (precision handling)", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndAccrueRewards); + const accruedAmount = 123_456_789n; + await staking.mockSetUserAccrued(staker.address, accruedAmount); + await staking.mockSetStakingEthPoolBalance(100_000_000_000n); + await staking.mockSetEthDaoBalance(100_000_000_000n); + + await trackGas( + staking.claimEthRewards(), + [GasGroup.CLAIM_ETH_REWARDS, GasGroup.SYNC_FEES] + ); + + const expectedRemainder = accruedAmount % ETH_DEDUCTED_DIGITS; + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(expectedRemainder); + }); + + it("Zeros dust in accrued when user holds 0 cSSV after full transfer (SEC-16b)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + const [, receiver] = await connection.ethers.getSigners(); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + await cssvToken.transfer(receiver.address, STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(0n); + + const accruedWithDust = 1_234_599_999n; // payout = 1_234_500_000, dust = 99_999 + const packedPayout = accruedWithDust / ETH_DEDUCTED_DIGITS; // 12345 + await staking.mockSetUserAccrued(staker.address, accruedWithDust); + await staking.mockSetStakingEthPoolBalance(packedPayout + 1_000_000n); + await staking.mockSetEthDaoBalance(packedPayout + 1_000_000n); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ to: stakingAddress, value: connection.ethers.parseEther("1") }); + + await staking.claimEthRewards(); + + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + }); + + it("Keeps remainder when user still holds cSSV despite dust (SEC-16b no false positive)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT); + + const accruedWithDust = 1_234_599_999n; + const packedPayout = accruedWithDust / ETH_DEDUCTED_DIGITS; + await staking.mockSetUserAccrued(staker.address, accruedWithDust); + await staking.mockSetStakingEthPoolBalance(packedPayout + 1_000_000n); + await staking.mockSetEthDaoBalance(packedPayout + 1_000_000n); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ to: stakingAddress, value: connection.ethers.parseEther("1") }); + + await staking.claimEthRewards(); + + const expectedRemainder = accruedWithDust % ETH_DEDUCTED_DIGITS; + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(expectedRemainder); + }); + + it("Is reverted with 'NothingToClaim' when there are no rewards", async function () { + const { staking, ssvToken } = await defaultStakingFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + await expect(staking.claimEthRewards()).to.be.revertedWithCustomError( + staking, + Errors.NOTHING_TO_CLAIM + ); + }); + + it("Is reverted with 'NothingToClaim' when accrued amount is too small to payout", async function () { + const { staking, ssvToken } = await defaultStakingFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(0n); + + const tinyAmount = 99_999n; + await staking.mockSetUserAccrued(staker.address, tinyAmount); + + await expect(staking.claimEthRewards()).to.be.revertedWithCustomError( + staking, + Errors.NOTHING_TO_CLAIM + ); + }); + + it("Is reverted with 'InsufficientBalance' when staking pool has insufficient balance", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndAccrueRewards); + + const accruedAmount = connection.ethers.parseEther("0.1"); + await staking.mockSetUserAccrued(staker.address, accruedAmount); + await staking.mockSetStakingEthPoolBalance(1n); + await staking.mockSetEthDaoBalance(1n); + + await expect(staking.claimEthRewards()).to.be.revertedWithCustomError( + staking, + Errors.INSUFFICIENT_BALANCE + ); + }); + + it("Syncs fees before claiming", async function () { + const { staking, ssvToken } = await defaultStakingFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("1"), + }); + const newFees = 1_000n; + await staking.mockSetUserAccrued(staker.address, 0n); + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + + const tx = await trackGas( + staking.claimEthRewards(), + [GasGroup.CLAIM_ETH_REWARDS, GasGroup.SYNC_FEES] + ); + + await expect(tx).to.emit(staking, Events.FEES_SYNCED); + }); + + it("Stores updated accrued balance in storage after claiming", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndAccrueRewards); + + const accruedBefore = connection.ethers.parseEther("0.1"); + await staking.mockSetUserAccrued(staker.address, accruedBefore); + const packedPayout = accruedBefore / ETH_DEDUCTED_DIGITS; + await staking.mockSetStakingEthPoolBalance(packedPayout + 1n); + await staking.mockSetEthDaoBalance(packedPayout + 1n); + + await staking.claimEthRewards(); + + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + }); + + it("Is reverted with 'InsufficientBalance' when ethDaoBalance is insufficient", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndAccrueRewards); + + const accruedAmount = connection.ethers.parseEther("0.1"); + await staking.mockSetUserAccrued(staker.address, accruedAmount); + await staking.mockSetStakingEthPoolBalance(100_000_000_000n); + await staking.mockSetEthDaoBalance(1n); + + await expect(staking.claimEthRewards()).to.be.revertedWithCustomError( + staking, + Errors.INSUFFICIENT_BALANCE + ); + }); + + it("Allows multiple claims as rewards continue to accrue", async function () { + const { staking, ssvToken } = await defaultStakingFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("10"), + }); + const firstAccrued = 100_000_000n; + await staking.mockSetUserAccrued(staker.address, firstAccrued * ETH_DEDUCTED_DIGITS); + await staking.mockSetStakingEthPoolBalance(firstAccrued); + await staking.mockSetEthDaoBalance(firstAccrued); + + const tx1 = await staking.claimEthRewards(); + await expect(tx1).to.emit(staking, Events.REWARDS_CLAIMED); + const secondAccrued = 200_000_000n; + await staking.mockSetUserAccrued(staker.address, secondAccrued * ETH_DEDUCTED_DIGITS); + await staking.mockSetStakingEthPoolBalance(secondAccrued); + await staking.mockSetEthDaoBalance(secondAccrued); + const tx2 = await staking.claimEthRewards(); + await expect(tx2).to.emit(staking, Events.REWARDS_CLAIMED); + }); + + it("Processes only the first claim when two claims are mined in the same block", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndAccrueRewards); + + const accruedAmount = 200_000_000n; + await staking.mockSetUserAccrued(staker.address, accruedAmount * ETH_DEDUCTED_DIGITS); + await staking.mockSetStakingEthPoolBalance(accruedAmount + 10n); + await staking.mockSetEthDaoBalance(accruedAmount + 10n); + + const provider = connection.ethers.provider; + const startingBalance = await provider.getBalance(staker.address); + const pendingNonce = await provider.getTransactionCount(staker.address, "pending"); + const claimData = staking.interface.encodeFunctionData("claimEthRewards"); + const stakingAddress = await staking.getAddress(); + + await provider.send("evm_setAutomine", [false]); + + try { + const firstClaim = await staker.sendTransaction({ + to: stakingAddress, + data: claimData, + nonce: pendingNonce, + gasLimit: 1_000_000n, + }); + const secondClaim = await staker.sendTransaction({ + to: stakingAddress, + data: claimData, + nonce: pendingNonce + 1, + gasLimit: 1_000_000n, + }); + + await provider.send("evm_mine", []); + + const firstReceipt = await firstClaim.wait(); + const secondReceipt = await provider.getTransactionReceipt(secondClaim.hash); + + expect(firstReceipt!.status).to.equal(1); + expect(secondReceipt!.status).to.equal(0); + + const gasUsedFirst = BigInt(firstReceipt!.gasUsed) * BigInt(firstReceipt!.gasPrice); + const gasUsedSecond = BigInt(secondReceipt!.gasUsed) * BigInt(secondReceipt!.gasPrice); + const balanceAfter = await provider.getBalance(staker.address); + expect(balanceAfter + gasUsedFirst + gasUsedSecond - startingBalance).to.equal( + accruedAmount * ETH_DEDUCTED_DIGITS + ); + + expect(await staking.getUserAccrued(staker.address)).to.equal(0n); + expect(await staking.getStakingEthPoolBalance()).to.equal(10n); + expect(await staking.getEthDaoBalance()).to.equal(10n); + } finally { + await provider.send("evm_setAutomine", [true]); + } + }); + + it("Settles pending rewards before claiming", async function () { + const { staking, ssvToken } = await defaultStakingFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("10"), + }); + const newFees = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + + const userIndexBefore = await staking.getUserIndex(staker.address); + const expectedIndexDelta = + (newFees * ETH_DEDUCTED_DIGITS * 1_000_000_000_000_000_000n) / STAKE_AMOUNT; + const tx = await staking.claimEthRewards(); + + await expect(tx).to.emit(staking, Events.REWARDS_CLAIMED); + + const userIndexAfter = await staking.getUserIndex(staker.address); + expect(userIndexAfter).to.equal(userIndexBefore + expectedIndexDelta); + }); + + it("Does not affect other users' accrued balances", async function () { + const { staking, ssvToken } = await defaultStakingFixture(connection); + const [, otherUser] = await connection.ethers.getSigners(); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + await ssvToken.transfer(otherUser.address, STAKE_AMOUNT); + await ssvToken.connect(otherUser).approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.connect(otherUser).stake(STAKE_AMOUNT); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("10"), + }); + const stakerAccrued = 100_000_000_000n; + const otherAccrued = 200_000_000_000n; + await staking.mockSetUserAccrued(staker.address, stakerAccrued); + await staking.mockSetUserAccrued(otherUser.address, otherAccrued); + await staking.mockSetStakingEthPoolBalance(50_000_000_000n); + await staking.mockSetEthDaoBalance(50_000_000_000n); + await staking.claimEthRewards(); + const otherAccruedAfter = await staking.getUserAccrued(otherUser.address); + expect(otherAccruedAfter).to.equal(otherAccrued); + }); + + it("Zeros dust when user unstakes all cSSV then claims (SEC-16b unstake flow)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + // Accrue sub-dust rewards + const dustAmount = 50_000n; // Sub-ETH_DEDUCTED_DIGITS + await staking.mockSetUserAccrued(staker.address, dustAmount); + + // Unstake ALL cSSV + await staking.requestUnstake(STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(0n); + + // Claim should succeed with zero payout and zero dust (SEC-16b fix) + const tx = await staking.claimEthRewards(); + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(staker.address, 0n); // Zero payout event + + // Verify dust was zeroed + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + }); + + it("Zeros exactly 99,999 wei dust (max) when bal == 0 after full unstake (SEC-16b boundary)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const maxDust = ETH_DEDUCTED_DIGITS - 1n; // 99,999 wei + await staking.mockSetUserAccrued(staker.address, maxDust); + + // Unstake ALL cSSV + await staking.requestUnstake(STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(0n); + + // Claim should succeed with zero payout (SEC-16b fix) + const tx = await staking.claimEthRewards(); + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(staker.address, 0n); + + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + }); + + it("Keeps remainder when user unstakes partial cSSV and claims (SEC-16b partial unstake)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + // Unstake HALF cSSV + await staking.requestUnstake(STAKE_AMOUNT / 2n); + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT / 2n); + + // Accrue 150K wei (payout = 100K, remainder = 50K) + const accruedWithRemainder = 150_000n; + const expectedPayout = 100_000n; + const expectedRemainder = 50_000n; + + await staking.mockSetUserAccrued(staker.address, accruedWithRemainder); + await staking.mockSetStakingEthPoolBalance(expectedPayout / ETH_DEDUCTED_DIGITS + 1n); + await staking.mockSetEthDaoBalance(expectedPayout / ETH_DEDUCTED_DIGITS + 1n); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ to: stakingAddress, value: connection.ethers.parseEther("1") }); + + await staking.claimEthRewards(); + + // Should keep remainder (bal > 0) + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(expectedRemainder); + }); + + it("Zeros dust after multiple partial cSSV transfers resulting in bal == 0 (SEC-16b multi-transfer)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + const [, receiver] = await connection.ethers.getSigners(); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + // Transfer 25% cSSV + await cssvToken.transfer(receiver.address, STAKE_AMOUNT / 4n); + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT * 3n / 4n); + + // Transfer another 25% cSSV + await cssvToken.transfer(receiver.address, STAKE_AMOUNT / 4n); + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT / 2n); + + // Transfer remaining 50% cSSV + await cssvToken.transfer(receiver.address, STAKE_AMOUNT / 2n); + expect(await cssvToken.balanceOf(staker.address)).to.equal(0n); + + // Set sub-dust accrued + await staking.mockSetUserAccrued(staker.address, 75_000n); + + // Claim should succeed with zero payout (SEC-16b fix) + const tx = await staking.claimEthRewards(); + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(staker.address, 0n); + + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + }); + + it("Zeros exactly 99,999 wei remainder when bal == 0 but keeps it when bal > 0 (SEC-16b max dust)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + const [, receiver] = await connection.ethers.getSigners(); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + // Scenario 1: User has cSSV, accrues 199,999 wei (payout = 100K, remainder = 99,999) + const accruedAmount = 199_999n; + const expectedPayout = 100_000n; + const maxDust = ETH_DEDUCTED_DIGITS - 1n; // 99,999 wei + + await staking.mockSetUserAccrued(staker.address, accruedAmount); + await staking.mockSetStakingEthPoolBalance(expectedPayout / ETH_DEDUCTED_DIGITS + 1n); + await staking.mockSetEthDaoBalance(expectedPayout / ETH_DEDUCTED_DIGITS + 1n); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ to: stakingAddress, value: connection.ethers.parseEther("1") }); + + // Claim while bal > 0 + await staking.claimEthRewards(); + + // Should keep max dust (bal > 0) + let accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(maxDust); + + // Scenario 2: User transfers all cSSV, then claims with max dust + await cssvToken.transfer(receiver.address, STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(0n); + + // Try to claim max dust with bal == 0 (should succeed with zero payout - SEC-16b fix) + const tx2 = await staking.claimEthRewards(); + await expect(tx2) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(staker.address, 0n); + + accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + }); + + it("Allows new accrual after dust was zeroed when user receives cSSV back (SEC-16b re-stake)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + const [, receiver] = await connection.ethers.getSigners(); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + // Transfer all cSSV → dust zeroed scenario + await cssvToken.transfer(receiver.address, STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(0n); + + await staking.mockSetUserAccrued(staker.address, 50_000n); + + // Claim should succeed with zero payout (SEC-16b fix) + const firstClaim = await staking.claimEthRewards(); + await expect(firstClaim) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(staker.address, 0n); + expect(await staking.getUserAccrued(staker.address)).to.equal(0n); + + // Receive cSSV back + await cssvToken.connect(receiver).transfer(staker.address, STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT); + + // Accrue new rewards + const newReward = 200_000_000n; // 200M wei + await staking.mockSetUserAccrued(staker.address, newReward); + await staking.mockSetStakingEthPoolBalance(newReward / ETH_DEDUCTED_DIGITS); + await staking.mockSetEthDaoBalance(newReward / ETH_DEDUCTED_DIGITS); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ to: stakingAddress, value: connection.ethers.parseEther("1") }); + + // Claim should work (bal > 0, new rewards) + const tx = await staking.claimEthRewards(); + await expect(tx).to.emit(staking, Events.REWARDS_CLAIMED); + + // Verify new accrual worked correctly (no interference from previous forfeit) + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); // All claimed (200M is exact multiple) + }); + + it("Storage state is clean after dust is zeroed (SEC-16b view consistency)", async function () { + const { staking, ssvToken, cssvToken } = await ssvStakingHarnessFixture(connection); + const [, receiver] = await connection.ethers.getSigners(); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + // Get initial user index after stake + const initialIndex = await staking.getUserIndex(staker.address); + + await cssvToken.transfer(receiver.address, STAKE_AMOUNT); + await staking.mockSetUserAccrued(staker.address, 50_000n); + + // Before claim: accrued should show dust + const accruedBefore = await staking.getUserAccrued(staker.address); + expect(accruedBefore).to.equal(50_000n); + + // Claim should succeed with zero payout (SEC-16b fix) + const tx = await staking.claimEthRewards(); + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(staker.address, 0n); + + // After dust zeroed, accrued storage should be 0 + // This ensures view functions like previewClaimableEth return 0 + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + + // User index should still be synced (not reset) + const userIndexAfter = await staking.getUserIndex(staker.address); + expect(userIndexAfter).to.equal(initialIndex); // Index preserved, not reset to 0 + }); + + it("Handles exact ETH_DEDUCTED_DIGITS boundary (100,000 wei) correctly (SEC-16b boundary)", async function () { + const { staking, ssvToken } = await ssvStakingHarnessFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const exactBoundary = ETH_DEDUCTED_DIGITS; // 100,000 wei + await staking.mockSetUserAccrued(staker.address, exactBoundary); + await staking.mockSetStakingEthPoolBalance(1n); // 1 packed unit + await staking.mockSetEthDaoBalance(1n); + + const stakingAddress = await staking.getAddress(); + await staker.sendTransaction({ to: stakingAddress, value: connection.ethers.parseEther("1") }); + + await staking.claimEthRewards(); + + // Should have 0 remainder (exact payout, no dust) + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(accruedAfter).to.equal(0n); + }); +}); diff --git a/test/unit/SSVStaking/onCSSVTransfer.test.ts b/test/unit/SSVStaking/onCSSVTransfer.test.ts new file mode 100644 index 000000000..615ccd65b --- /dev/null +++ b/test/unit/SSVStaking/onCSSVTransfer.test.ts @@ -0,0 +1,395 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { Errors } from "../../common/errors.ts"; +import { Events } from "../../common/events.ts"; + +const PRECISION = 10n ** 18n; +const MIN_STAKE = 1_000_000_000n; + +describe("SSVStaking function `onCSSVTransfer()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let staker: HardhatEthersSigner; + let receiver: HardhatEthersSigner; + let thirdUser: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [staker, receiver, thirdUser] } = await setupTestContext()); + }); + + const deployStakingFixture = async () => defaultStakingFixture(connection); + + async function impersonate(address: string) { + await connection.ethers.provider.send("hardhat_impersonateAccount", [address]); + await connection.ethers.provider.send("hardhat_setBalance", [address, "0x1000000000000000000"]); + return connection.ethers.getSigner(address); + } + + async function freezeSync(staking: any) { + await staking.mockSetDaoTotalEthVUnits(0n); + await staking.mockSetEthNetworkFee(0n); + } + + async function stakeFor(staking: any, ssvToken: any, user: HardhatEthersSigner, amount: bigint) { + await ssvToken.connect(user).approve(await staking.getAddress(), amount); + await staking.connect(user).stake(amount); + } + + async function simulateCssvTransfer( + staking: any, + cssvToken: any, + cssvSigner: any, + fromSigner: HardhatEthersSigner, + to: string, + amount: bigint + ) { + await staking.connect(cssvSigner).onCSSVTransfer(fromSigner.address, to, amount); + await cssvToken.connect(fromSigner).transfer(to, amount); + } + + it("Is reverted with 'NotCSSV' when caller is not the cSSV token", async function () { + const { staking } = await networkHelpers.loadFixture(deployStakingFixture); + + await expect( + staking.onCSSVTransfer(staker.address, receiver.address, 1n) + ).to.be.revertedWithCustomError(staking, Errors.NOT_CSSV); + }); + + it("Settles rewards for sender and receiver and updates user indexes", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const cssvAddress = await cssvToken.getAddress(); + const cssvSigner = await impersonate(cssvAddress); + await staking.mockSetDaoTotalEthVUnits(0n); + await staking.mockSetEthNetworkFee(0n); + + const accEthPerShare = 2n * PRECISION; + await staking.mockSetAccEthPerShare(accEthPerShare); + await staking.mockSetUserIndex(staker.address, PRECISION); + await staking.mockSetUserIndex(receiver.address, PRECISION); + + const stakerBalance = 100n; + const receiverBalance = 200n; + await cssvToken.mint(staker.address, stakerBalance); + await cssvToken.mint(receiver.address, receiverBalance); + + const tx = await staking.connect(cssvSigner).onCSSVTransfer( + staker.address, + receiver.address, + 1n + ); + + const stakerAccrued = await staking.getUserAccrued(staker.address); + const receiverAccrued = await staking.getUserAccrued(receiver.address); + expect(stakerAccrued).to.equal(stakerBalance); + expect(receiverAccrued).to.equal(receiverBalance); + + const stakerIndex = await staking.getUserIndex(staker.address); + const receiverIndex = await staking.getUserIndex(receiver.address); + expect(stakerIndex).to.equal(accEthPerShare); + expect(receiverIndex).to.equal(accEthPerShare); + + await expect(tx) + .to.emit(staking, Events.REWARDS_SETTLED) + .withArgs(staker.address, stakerBalance, stakerBalance, accEthPerShare); + await expect(tx) + .to.emit(staking, Events.REWARDS_SETTLED) + .withArgs(receiver.address, receiverBalance, receiverBalance, accEthPerShare); + }); + + it("Distributes rewards proportionally across 3 stakers with different balances", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const amountA = MIN_STAKE; + const amountB = 2n * MIN_STAKE; + const amountC = 7n * MIN_STAKE; + + await ssvToken.transfer(receiver.address, amountB); + await ssvToken.transfer(thirdUser.address, amountC); + + await stakeFor(staking, ssvToken, staker, amountA); + await stakeFor(staking, ssvToken, receiver, amountB); + await stakeFor(staking, ssvToken, thirdUser, amountC); + + await freezeSync(staking); + const accEthPerShare = 5n * PRECISION; + await staking.mockSetAccEthPerShare(accEthPerShare); + + const cssvSigner = await impersonate(await cssvToken.getAddress()); + await staking.connect(cssvSigner).onCSSVTransfer(staker.address, receiver.address, 0n); + await staking.connect(cssvSigner).onCSSVTransfer(thirdUser.address, staker.address, 0n); + + const accruedA = await staking.getUserAccrued(staker.address); + const accruedB = await staking.getUserAccrued(receiver.address); + const accruedC = await staking.getUserAccrued(thirdUser.address); + + expect(accruedA).to.equal(amountA * 5n); + expect(accruedB).to.equal(amountB * 5n); + expect(accruedC).to.equal(amountC * 5n); + }); + + it("Allocates rewards by staking time (A early, B late)", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const amountA = 4n * MIN_STAKE; + const amountB = 6n * MIN_STAKE; + + await ssvToken.transfer(receiver.address, amountB); + + await stakeFor(staking, ssvToken, staker, amountA); + + await freezeSync(staking); + await staking.mockSetAccEthPerShare(2n * PRECISION); + + await stakeFor(staking, ssvToken, receiver, amountB); + await staking.mockSetAccEthPerShare(5n * PRECISION); + + const cssvSigner = await impersonate(await cssvToken.getAddress()); + await staking.connect(cssvSigner).onCSSVTransfer(staker.address, receiver.address, 0n); + + const accruedA = await staking.getUserAccrued(staker.address); + const accruedB = await staking.getUserAccrued(receiver.address); + + expect(accruedA).to.equal(amountA * 5n); + expect(accruedB).to.equal(amountB * 3n); + }); + + it("Settles A->B transfer rewards and applies higher future rate to receiver balance", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const amountA = 10n * MIN_STAKE; + const transferAmount = 4n * MIN_STAKE; + + await stakeFor(staking, ssvToken, staker, amountA); + + await freezeSync(staking); + await staking.mockSetAccEthPerShare(2n * PRECISION); + const cssvSigner = await impersonate(await cssvToken.getAddress()); + + await simulateCssvTransfer(staking, cssvToken, cssvSigner, staker, receiver.address, transferAmount); + + await staking.mockSetAccEthPerShare(5n * PRECISION); + await staking.connect(cssvSigner).onCSSVTransfer(staker.address, receiver.address, 0n); + + const accruedA = await staking.getUserAccrued(staker.address); + const accruedB = await staking.getUserAccrued(receiver.address); + + const expectedA = (amountA * 2n) + ((amountA - transferAmount) * 3n); + const expectedB = transferAmount * 3n; + + expect(accruedA).to.equal(expectedA); + expect(accruedB).to.equal(expectedB); + }); + + it("Accrues future rewards for a receiver holding exactly 1 wei cSSV", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const amountA = MIN_STAKE; + const transferAmount = 1n; + + await stakeFor(staking, ssvToken, staker, amountA); + + await freezeSync(staking); + const cssvSigner = await impersonate(await cssvToken.getAddress()); + + await staking.mockSetAccEthPerShare(2n * PRECISION); + await simulateCssvTransfer(staking, cssvToken, cssvSigner, staker, receiver.address, transferAmount); + + expect(await cssvToken.balanceOf(receiver.address)).to.equal(transferAmount); + expect(await staking.getUserAccrued(receiver.address)).to.equal(0n); + + await staking.mockSetAccEthPerShare(3n * PRECISION); + await staking.connect(cssvSigner).onCSSVTransfer(staker.address, receiver.address, 0n); + + const accruedA = await staking.getUserAccrued(staker.address); + const accruedB = await staking.getUserAccrued(receiver.address); + + expect(accruedA).to.equal((amountA * 2n) + (amountA - transferAmount)); + expect(accruedB).to.equal(1n); + }); + + it("Handles sequential transfer chain A->B->C with correct per-period reward accumulation", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const amountA = 9n * MIN_STAKE; + const transferAB = 3n * MIN_STAKE; + const transferBC = 2n * MIN_STAKE; + + await stakeFor(staking, ssvToken, staker, amountA); + + await freezeSync(staking); + const cssvSigner = await impersonate(await cssvToken.getAddress()); + await staking.mockSetAccEthPerShare(2n * PRECISION); + await simulateCssvTransfer(staking, cssvToken, cssvSigner, staker, receiver.address, transferAB); + + await staking.mockSetAccEthPerShare(5n * PRECISION); + await simulateCssvTransfer(staking, cssvToken, cssvSigner, receiver, thirdUser.address, transferBC); + + await staking.mockSetAccEthPerShare(9n * PRECISION); + await staking.connect(cssvSigner).onCSSVTransfer(staker.address, receiver.address, 0n); + await staking.connect(cssvSigner).onCSSVTransfer(thirdUser.address, staker.address, 0n); + + const accruedA = await staking.getUserAccrued(staker.address); + const accruedB = await staking.getUserAccrued(receiver.address); + const accruedC = await staking.getUserAccrued(thirdUser.address); + + const expectedA = (9n * MIN_STAKE * 2n) + (6n * MIN_STAKE * 3n) + (6n * MIN_STAKE * 4n); + const expectedB = (3n * MIN_STAKE * 3n) + (1n * MIN_STAKE * 4n); + const expectedC = 2n * MIN_STAKE * 4n; + + expect(accruedA).to.equal(expectedA); + expect(accruedB).to.equal(expectedB); + expect(accruedC).to.equal(expectedC); + }); + + it("Settles transfer after fee accrual for A=100, B=200 using pendingReward formula", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const cssvSigner = await impersonate(await cssvToken.getAddress()); + await freezeSync(staking); + + const amountA = 100n; + const amountB = 200n; + const transferAmount = 50n; + const userIndexA = 1n * PRECISION; + const userIndexB = 1n * PRECISION; + const accEthPerShare = 3n * PRECISION; + + await cssvToken.mint(staker.address, amountA); + await cssvToken.mint(receiver.address, amountB); + await staking.mockSetUserIndex(staker.address, userIndexA); + await staking.mockSetUserIndex(receiver.address, userIndexB); + await staking.mockSetAccEthPerShare(accEthPerShare); + + await simulateCssvTransfer(staking, cssvToken, cssvSigner, staker, receiver.address, transferAmount); + + const expectedAccruedA = (amountA * (accEthPerShare - userIndexA)) / PRECISION; + const expectedAccruedB = (amountB * (accEthPerShare - userIndexB)) / PRECISION; + + expect(await staking.getUserAccrued(staker.address)).to.equal(expectedAccruedA); + expect(await staking.getUserAccrued(receiver.address)).to.equal(expectedAccruedB); + expect(await staking.getUserIndex(staker.address)).to.equal(accEthPerShare); + expect(await staking.getUserIndex(receiver.address)).to.equal(accEthPerShare); + + expect(await cssvToken.balanceOf(staker.address)).to.equal(amountA - transferAmount); + expect(await cssvToken.balanceOf(receiver.address)).to.equal(amountB + transferAmount); + }); + + it("Handles multi-transfer sequence A->B->C with exact settlement at each phase", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const cssvSigner = await impersonate(await cssvToken.getAddress()); + await freezeSync(staking); + + const amountA = 300n; + const amountB = 200n; + const amountC = 100n; + const transferAB = 60n; + const transferBC = 40n; + + await cssvToken.mint(staker.address, amountA); + await cssvToken.mint(receiver.address, amountB); + await cssvToken.mint(thirdUser.address, amountC); + await staking.mockSetUserIndex(staker.address, 1n * PRECISION); + await staking.mockSetUserIndex(receiver.address, 1n * PRECISION); + await staking.mockSetUserIndex(thirdUser.address, 1n * PRECISION); + + await staking.mockSetAccEthPerShare(2n * PRECISION); + await simulateCssvTransfer(staking, cssvToken, cssvSigner, staker, receiver.address, transferAB); + + const expectedAfterFirstA = (amountA * (2n * PRECISION - 1n * PRECISION)) / PRECISION; + const expectedAfterFirstB = (amountB * (2n * PRECISION - 1n * PRECISION)) / PRECISION; + + expect(await staking.getUserAccrued(staker.address)).to.equal(expectedAfterFirstA); + expect(await staking.getUserAccrued(receiver.address)).to.equal(expectedAfterFirstB); + expect(await staking.getUserAccrued(thirdUser.address)).to.equal(0n); + expect(await staking.getUserIndex(staker.address)).to.equal(2n * PRECISION); + expect(await staking.getUserIndex(receiver.address)).to.equal(2n * PRECISION); + expect(await staking.getUserIndex(thirdUser.address)).to.equal(1n * PRECISION); + expect(await cssvToken.balanceOf(staker.address)).to.equal(amountA - transferAB); + expect(await cssvToken.balanceOf(receiver.address)).to.equal(amountB + transferAB); + expect(await cssvToken.balanceOf(thirdUser.address)).to.equal(amountC); + + await staking.mockSetAccEthPerShare(4n * PRECISION); + await simulateCssvTransfer(staking, cssvToken, cssvSigner, receiver, thirdUser.address, transferBC); + + const balanceAAfterFirst = amountA - transferAB; + const balanceBAfterFirst = amountB + transferAB; + const balanceBAfterSecond = balanceBAfterFirst - transferBC; + const balanceCAfterSecond = amountC + transferBC; + const expectedAfterSecondA = expectedAfterFirstA; + const expectedAfterSecondB = + expectedAfterFirstB + ((balanceBAfterFirst * (4n * PRECISION - 2n * PRECISION)) / PRECISION); + const expectedAfterSecondC = (amountC * (4n * PRECISION - 1n * PRECISION)) / PRECISION; + + expect(await staking.getUserAccrued(staker.address)).to.equal(expectedAfterSecondA); + expect(await staking.getUserAccrued(receiver.address)).to.equal(expectedAfterSecondB); + expect(await staking.getUserAccrued(thirdUser.address)).to.equal(expectedAfterSecondC); + expect(await staking.getUserIndex(staker.address)).to.equal(2n * PRECISION); + expect(await staking.getUserIndex(receiver.address)).to.equal(4n * PRECISION); + expect(await staking.getUserIndex(thirdUser.address)).to.equal(4n * PRECISION); + expect(await cssvToken.balanceOf(staker.address)).to.equal(balanceAAfterFirst); + expect(await cssvToken.balanceOf(receiver.address)).to.equal(balanceBAfterSecond); + expect(await cssvToken.balanceOf(thirdUser.address)).to.equal(balanceCAfterSecond); + + await staking.mockSetAccEthPerShare(5n * PRECISION); + await staking.connect(cssvSigner).onCSSVTransfer(thirdUser.address, staker.address, 0n); + await staking.connect(cssvSigner).onCSSVTransfer(receiver.address, staker.address, 0n); + + const expectedAccruedA = + expectedAfterSecondA + ((balanceAAfterFirst * (5n * PRECISION - 2n * PRECISION)) / PRECISION); + const expectedAccruedB = + expectedAfterSecondB + ((balanceBAfterSecond * (5n * PRECISION - 4n * PRECISION)) / PRECISION); + const expectedAccruedC = + expectedAfterSecondC + ((balanceCAfterSecond * (5n * PRECISION - 4n * PRECISION)) / PRECISION); + + expect(await staking.getUserAccrued(staker.address)).to.equal(expectedAccruedA); + expect(await staking.getUserAccrued(receiver.address)).to.equal(expectedAccruedB); + expect(await staking.getUserAccrued(thirdUser.address)).to.equal(expectedAccruedC); + + expect(await staking.getUserIndex(staker.address)).to.equal(5n * PRECISION); + expect(await staking.getUserIndex(receiver.address)).to.equal(5n * PRECISION); + expect(await staking.getUserIndex(thirdUser.address)).to.equal(5n * PRECISION); + }); + + it("Settles both users' pending rewards before transfer and applies later rewards to post-transfer balances", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const cssvSigner = await impersonate(await cssvToken.getAddress()); + await freezeSync(staking); + + const amountA = 150n; + const amountB = 250n; + const transferAmount = 100n; + const userIndexA = PRECISION / 2n; + const userIndexB = 1n * PRECISION; + const accBeforeTransfer = 3n * PRECISION; + const accAfterTransfer = 4n * PRECISION; + + await cssvToken.mint(staker.address, amountA); + await cssvToken.mint(receiver.address, amountB); + await staking.mockSetUserIndex(staker.address, userIndexA); + await staking.mockSetUserIndex(receiver.address, userIndexB); + + await staking.mockSetAccEthPerShare(accBeforeTransfer); + await simulateCssvTransfer(staking, cssvToken, cssvSigner, staker, receiver.address, transferAmount); + + await staking.mockSetAccEthPerShare(accAfterTransfer); + await staking.connect(cssvSigner).onCSSVTransfer(staker.address, receiver.address, 0n); + + const expectedInitialA = (amountA * (accBeforeTransfer - userIndexA)) / PRECISION; // 375 + const expectedInitialB = (amountB * (accBeforeTransfer - userIndexB)) / PRECISION; // 500 + const expectedAdditionalA = ((amountA - transferAmount) * (accAfterTransfer - accBeforeTransfer)) / PRECISION; // 50 + const expectedAdditionalB = ((amountB + transferAmount) * (accAfterTransfer - accBeforeTransfer)) / PRECISION; // 350 + + expect(await staking.getUserAccrued(staker.address)).to.equal(expectedInitialA + expectedAdditionalA); + expect(await staking.getUserAccrued(receiver.address)).to.equal(expectedInitialB + expectedAdditionalB); + expect(await staking.getUserIndex(staker.address)).to.equal(accAfterTransfer); + expect(await staking.getUserIndex(receiver.address)).to.equal(accAfterTransfer); + }); +}); diff --git a/test/unit/SSVStaking/reentrancy.test.ts b/test/unit/SSVStaking/reentrancy.test.ts new file mode 100644 index 000000000..6b834ba8f --- /dev/null +++ b/test/unit/SSVStaking/reentrancy.test.ts @@ -0,0 +1,50 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Errors } from "../../common/errors.ts"; +import { ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { setupTestContext } from "../../common/helpers.ts"; + +describe("SSVStaking reentrancy guard", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + it("Blocks reentrancy during ETH rewards claim", async function () { + const { staking } = await defaultStakingFixture(connection); + + const malicious = await connection.ethers.deployContract( + "MaliciousClaimEthRewards", + [await staking.getAddress()] + ); + await malicious.waitForDeployment(); + + const maliciousAddress = await malicious.getAddress(); + const stakingAddress = await staking.getAddress(); + + const accrued = connection.ethers.parseEther("0.1"); + const packedAccrued = accrued / ETH_DEDUCTED_DIGITS; + await staking.mockSetUserAccrued(maliciousAddress, accrued); + await staking.mockSetStakingEthPoolBalance(packedAccrued + 1_000_000n); + await staking.mockSetEthDaoBalance(packedAccrued + 1_000_000n); + + await networkHelpers.setBalance(stakingAddress, connection.ethers.parseEther("1")); + + await expect(malicious.attack()).to.be.revertedWithCustomError(staking, Errors.ETH_TRANSFER_FAILED); + }); + + // NOTE: withdrawUnlocked reentrancy test is not included because: + // - SSVToken is a standard ERC20 with no callbacks (no receive() or hooks) + // - ERC20.transfer() does not call back to the recipient + // - Therefore, reentrancy during withdrawUnlocked is not possible in production + // - The nonReentrant modifier on withdrawUnlocked is defensive but protects against no real attack + // + // The same applies to stake() and requestUnstake() - they only interact with standard + // ERC20 tokens (SSV and cSSV) which have no callback mechanisms. + // + // claimEthRewards() is different because it sends ETH, which triggers the receive() hook. +}); diff --git a/test/unit/SSVStaking/requestUnstake.test.ts b/test/unit/SSVStaking/requestUnstake.test.ts new file mode 100644 index 000000000..1fbd22cde --- /dev/null +++ b/test/unit/SSVStaking/requestUnstake.test.ts @@ -0,0 +1,344 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { + STAKE_AMOUNT, + DEFAULT_UNSTAKE_COOLDOWN, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVStaking function `requestUnstake()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let staker: HardhatEthersSigner; + let receiver: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [staker, receiver] } = await setupTestContext()); + }); + + const stakeFirst = async () => { + const { staking, ssvToken, cssvToken } = await defaultStakingFixture(connection); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + return { staking, ssvToken, cssvToken }; + }; + + it("Requests unstake, burns cSSV and emits UnstakeRequested event with correct args", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(stakeFirst); + + const totalSupplyBefore = await cssvToken.totalSupply(); + const cssvBalanceBefore = await cssvToken.balanceOf(staker.address); + + const unstakeAmount = STAKE_AMOUNT / 2n; + const receipt = await trackGas( + staking.requestUnstake(unstakeAmount), + [GasGroup.REQUEST_UNSTAKE] + ); + const block = await connection.ethers.provider.getBlock(receipt.blockNumber); + const expectedUnlockTime = BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + await expect(receipt) + .to.emit(staking, Events.UNSTAKE_REQUESTED) + .withArgs(staker.address, unstakeAmount, expectedUnlockTime); + const cssvBalanceAfter = await cssvToken.balanceOf(staker.address); + expect(cssvBalanceAfter).to.equal(cssvBalanceBefore - unstakeAmount); + const totalSupplyAfter = await cssvToken.totalSupply(); + expect(totalSupplyAfter).to.equal(totalSupplyBefore - unstakeAmount); + }); + + it("Creates a withdrawal request with correct unlock time", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const unstakeAmount = STAKE_AMOUNT / 2n; + const receipt = await trackGas( + staking.requestUnstake(unstakeAmount), + [GasGroup.REQUEST_UNSTAKE] + ); + + const requestCount = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCount).to.equal(1n); + + const [amount, unlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + expect(amount).to.equal(unstakeAmount); + + const receiptBlock = await connection.ethers.provider.getBlock(receipt.blockNumber); + const expectedUnlockTime = BigInt(receiptBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + expect(unlockTime).to.equal(expectedUnlockTime); + }); + + it("Is reverted with 'ZeroAmount' when requesting unstake of zero amount", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + await expect(staking.requestUnstake(0n)).to.be.revertedWithCustomError( + staking, + Errors.ZERO_AMOUNT + ); + }); + + it("Is reverted with 'MaxRequestsAmountReached' when pending requests limit is reached", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const unstakeAmount = STAKE_AMOUNT / 20000n; + for (let i = 0; i < 2000; i += 1) { + await (await staking.requestUnstake(unstakeAmount)).wait(); + } + + await expect(staking.requestUnstake(unstakeAmount)).to.be.revertedWithCustomError( + staking, + Errors.MAX_REQUESTS_AMOUNT_REACHED + ); + }); + + it("Is reverted with 'UnstakeAmountExceedsBalance' when requesting more than balance", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const excessAmount = STAKE_AMOUNT + 1n; + + await expect(staking.requestUnstake(excessAmount)).to.be.revertedWithCustomError( + staking, + Errors.UNSTAKE_AMOUNT_EXCEEDS_BALANCE + ); + }); + + it("Is reverted with 'UnstakeAmountExceedsBalance' when caller has no cSSV", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + await expect(staking.connect(receiver).requestUnstake(1n)).to.be.revertedWithCustomError( + staking, + Errors.UNSTAKE_AMOUNT_EXCEEDS_BALANCE + ); + }); + + it("Allows unstaking full balance", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(stakeFirst); + + await trackGas( + staking.requestUnstake(STAKE_AMOUNT), + [GasGroup.REQUEST_UNSTAKE] + ); + + const cssvBalance = await cssvToken.balanceOf(staker.address); + expect(cssvBalance).to.equal(0n); + + const requestCount = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCount).to.equal(1n); + + const [amount] = await staking.getWithdrawalRequest(staker.address, 0); + expect(amount).to.equal(STAKE_AMOUNT); + }); + + it("Stores withdrawal request in storage", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const unstakeAmount = STAKE_AMOUNT / 2n; + const tx = await staking.requestUnstake(unstakeAmount); + const receipt = await tx.wait(); + + const requestCount = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCount).to.equal(1n); + + const [storedAmount, storedUnlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + + expect(storedAmount).to.equal(unstakeAmount); + + const receiptBlock = await connection.ethers.provider.getBlock(receipt.blockNumber); + const expectedUnlockTime = BigInt(receiptBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + expect(storedUnlockTime).to.equal(expectedUnlockTime); + }); + + it("Allows a receiver to request unstake after receiving cSSV by transfer", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(stakeFirst); + + const receivedAmount = STAKE_AMOUNT / 2n; + await cssvToken.connect(staker).transfer(receiver.address, receivedAmount); + + expect(await cssvToken.balanceOf(receiver.address)).to.equal(receivedAmount); + + const cssvSupplyBefore = await cssvToken.totalSupply(); + const receipt = await trackGas( + staking.connect(receiver).requestUnstake(receivedAmount), + [GasGroup.REQUEST_UNSTAKE] + ); + const block = await connection.ethers.provider.getBlock(receipt.blockNumber); + const expectedUnlockTime = BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + await expect(receipt) + .to.emit(staking, Events.UNSTAKE_REQUESTED) + .withArgs(receiver.address, receivedAmount, expectedUnlockTime); + + expect(await cssvToken.balanceOf(receiver.address)).to.equal(0n); + expect(await cssvToken.balanceOf(staker.address)).to.equal( + STAKE_AMOUNT - receivedAmount, + ); + expect(await cssvToken.totalSupply()).to.equal(cssvSupplyBefore - receivedAmount); + + const requestCount = await staking.getWithdrawalRequestsCount(receiver.address); + expect(requestCount).to.equal(1n); + + const [amount, unlockTime] = await staking.getWithdrawalRequest( + receiver.address, + 0, + ); + expect(amount).to.equal(receivedAmount); + expect(unlockTime).to.equal(expectedUnlockTime); + }); + + it("Allows multiple sequential unstake requests with different unlock times", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(stakeFirst); + + const firstAmount = STAKE_AMOUNT / 4n; + const secondAmount = STAKE_AMOUNT / 4n; + const tx1 = await staking.requestUnstake(firstAmount); + const receipt1 = await tx1.wait(); + const block1 = await connection.ethers.provider.getBlock(receipt1.blockNumber); + const expectedUnlock1 = BigInt(block1!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + await networkHelpers.time.increase(100n); + const tx2 = await staking.requestUnstake(secondAmount); + const receipt2 = await tx2.wait(); + const block2 = await connection.ethers.provider.getBlock(receipt2.blockNumber); + const expectedUnlock2 = BigInt(block2!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + const requestCount = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCount).to.equal(2n); + + const [amount1, unlock1] = await staking.getWithdrawalRequest(staker.address, 0); + const [amount2, unlock2] = await staking.getWithdrawalRequest(staker.address, 1); + + expect(amount1).to.equal(firstAmount); + expect(unlock1).to.equal(expectedUnlock1); + expect(amount2).to.equal(secondAmount); + expect(unlock2).to.equal(expectedUnlock2); + const cssvBalance = await cssvToken.balanceOf(staker.address); + expect(cssvBalance).to.equal(STAKE_AMOUNT - firstAmount - secondAmount); + }); + + it("Uses block.timestamp (seconds) for unlockTime, not block.number", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const unstakeAmount = STAKE_AMOUNT / 2n; + const receipt = await trackGas( + staking.requestUnstake(unstakeAmount), + [GasGroup.REQUEST_UNSTAKE] + ); + + const block = await connection.ethers.provider.getBlock(receipt.blockNumber); + const [, unlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + const expectedFromTimestamp = BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + expect(unlockTime).to.equal(expectedFromTimestamp); + const incorrectFromBlockNumber = BigInt(block!.number) + DEFAULT_UNSTAKE_COOLDOWN; + expect(unlockTime).to.not.equal(incorrectFromBlockNumber); + }); + + it("Cooldown duration change only affects new requests, not existing ones", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const firstAmount = STAKE_AMOUNT / 4n; + const firstTx = await staking.requestUnstake(firstAmount); + const firstReceipt = await firstTx.wait(); + const firstBlock = await connection.ethers.provider.getBlock(firstReceipt!.blockNumber); + const expectedFirstUnlock = BigInt(firstBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + const [, firstUnlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + expect(firstUnlockTime).to.equal(expectedFirstUnlock); + + const newCooldown = DEFAULT_UNSTAKE_COOLDOWN * 3n; + await staking.mockSetCooldownDuration(newCooldown); + + const [, firstUnlockAfterChange] = await staking.getWithdrawalRequest(staker.address, 0); + expect(firstUnlockAfterChange).to.equal(expectedFirstUnlock); + + const secondAmount = STAKE_AMOUNT / 4n; + const secondTx = await staking.requestUnstake(secondAmount); + const secondReceipt = await secondTx.wait(); + const secondBlock = await connection.ethers.provider.getBlock(secondReceipt!.blockNumber); + const expectedSecondUnlock = BigInt(secondBlock!.timestamp) + newCooldown; + + const [, secondUnlockTime] = await staking.getWithdrawalRequest(staker.address, 1); + expect(secondUnlockTime).to.equal(expectedSecondUnlock); + }); + + it("Cooldown increase: old request keeps original unlock, new request uses increased cooldown", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const firstAmount = STAKE_AMOUNT / 4n; + const firstTx = await staking.requestUnstake(firstAmount); + const firstReceipt = await firstTx.wait(); + const firstBlock = await connection.ethers.provider.getBlock(firstReceipt!.blockNumber); + const expectedFirstUnlock = BigInt(firstBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + const increasedCooldown = DEFAULT_UNSTAKE_COOLDOWN * 5n; + await staking.mockSetCooldownDuration(increasedCooldown); + + const [, firstUnlockAfterIncrease] = await staking.getWithdrawalRequest(staker.address, 0); + expect(firstUnlockAfterIncrease).to.equal(expectedFirstUnlock); + + const secondAmount = STAKE_AMOUNT / 4n; + const secondTx = await staking.requestUnstake(secondAmount); + const secondReceipt = await secondTx.wait(); + const secondBlock = await connection.ethers.provider.getBlock(secondReceipt!.blockNumber); + const expectedSecondUnlock = BigInt(secondBlock!.timestamp) + increasedCooldown; + + const [, secondUnlockTime] = await staking.getWithdrawalRequest(staker.address, 1); + expect(secondUnlockTime).to.equal(expectedSecondUnlock); + }); + + it("Cooldown decrease: pending request not accelerated, new request uses shorter cooldown", async function () { + const { staking } = await networkHelpers.loadFixture(stakeFirst); + + const firstAmount = STAKE_AMOUNT / 4n; + const firstTx = await staking.requestUnstake(firstAmount); + const firstReceipt = await firstTx.wait(); + const firstBlock = await connection.ethers.provider.getBlock(firstReceipt!.blockNumber); + const expectedFirstUnlock = BigInt(firstBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + const shorterCooldown = DEFAULT_UNSTAKE_COOLDOWN / 4n; + await staking.mockSetCooldownDuration(shorterCooldown); + + const [, firstUnlockAfterDecrease] = await staking.getWithdrawalRequest(staker.address, 0); + expect(firstUnlockAfterDecrease).to.equal(expectedFirstUnlock); + + const secondAmount = STAKE_AMOUNT / 4n; + const secondTx = await staking.requestUnstake(secondAmount); + const secondReceipt = await secondTx.wait(); + const secondBlock = await connection.ethers.provider.getBlock(secondReceipt!.blockNumber); + const expectedSecondUnlock = BigInt(secondBlock!.timestamp) + shorterCooldown; + + const [, secondUnlockTime] = await staking.getWithdrawalRequest(staker.address, 1); + expect(secondUnlockTime).to.equal(expectedSecondUnlock); + }); + + it("Settles pending rewards before unstaking when fees have accrued", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(stakeFirst); + const newFees = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + + const userIndexBefore = await staking.getUserIndex(staker.address); + const accruedBefore = await staking.getUserAccrued(staker.address); + const expectedIndexDelta = + (newFees * ETH_DEDUCTED_DIGITS * 1_000_000_000_000_000_000n) / STAKE_AMOUNT; + const expectedAccruedDelta = + (STAKE_AMOUNT * expectedIndexDelta) / 1_000_000_000_000_000_000n; + + await trackGas( + staking.requestUnstake(STAKE_AMOUNT / 2n), + [GasGroup.REQUEST_UNSTAKE] + ); + + const userIndexAfter = await staking.getUserIndex(staker.address); + const accruedAfter = await staking.getUserAccrued(staker.address); + expect(userIndexAfter).to.equal(userIndexBefore + expectedIndexDelta); + expect(accruedAfter).to.equal(accruedBefore + expectedAccruedDelta); + }); +}); diff --git a/test/unit/SSVStaking/rescueERC20.test.ts b/test/unit/SSVStaking/rescueERC20.test.ts new file mode 100644 index 000000000..ab14011b5 --- /dev/null +++ b/test/unit/SSVStaking/rescueERC20.test.ts @@ -0,0 +1,176 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { setupTestContext } from "../../common/helpers.ts"; +import { ssvStakingHarnessFixture } from "../../setup/fixtures.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVStaking function `rescueERC20()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let owner: HardhatEthersSigner; + let recipient: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [owner, recipient] } = await setupTestContext()); + }); + + const deployWithExtraToken = async () => { + const { staking, ssvToken, cssvToken } = await defaultStakingFixture(connection); + + const randomToken = await connection.ethers.deployContract("MockToken"); + await randomToken.waitForDeployment(); + + await randomToken.mint(owner.address, connection.ethers.parseEther("1000")); + + const rescueAmount = connection.ethers.parseEther("100"); + await randomToken.transfer(await staking.getAddress(), rescueAmount); + + return { staking, ssvToken, cssvToken, randomToken, rescueAmount }; + }; + + const deployWithNonStandardToken = async () => { + const { staking } = await ssvStakingHarnessFixture(connection); + + const nonStandardToken = await connection.ethers.deployContract("NonStandardERC20Mock"); + await nonStandardToken.waitForDeployment(); + + await nonStandardToken.mint(owner.address, connection.ethers.parseEther("1000")); + + const rescueAmount = connection.ethers.parseEther("100"); + await nonStandardToken.transfer(await staking.getAddress(), rescueAmount); + + return { staking, nonStandardToken, rescueAmount }; + }; + + it("Rescues accidentally sent ERC20 tokens and emits ERC20Rescued event", async function () { + const { staking, randomToken, rescueAmount } = + await networkHelpers.loadFixture(deployWithExtraToken); + + const tokenAddress = await randomToken.getAddress(); + const tx = await trackGas( + staking.rescueERC20(tokenAddress, recipient.address, rescueAmount), + [GasGroup.RESCUE_ERC20] + ); + + await expect(tx) + .to.emit(staking, Events.ERC20_RESCUED) + .withArgs(tokenAddress, recipient.address, rescueAmount); + + const recipientBalance = await randomToken.balanceOf(recipient.address); + expect(recipientBalance).to.equal(rescueAmount); + }); + + it("Transfers the correct amount to the recipient", async function () { + const { staking, randomToken, rescueAmount } = + await networkHelpers.loadFixture(deployWithExtraToken); + + const balanceBefore = await randomToken.balanceOf(recipient.address); + + await trackGas( + staking.rescueERC20( + await randomToken.getAddress(), + recipient.address, + rescueAmount + ), + [GasGroup.RESCUE_ERC20] + ); + + const balanceAfter = await randomToken.balanceOf(recipient.address); + expect(balanceAfter - balanceBefore).to.equal(rescueAmount); + }); + + it("Rescues non-standard ERC20 tokens that do not return a value", async function () { + const { staking, nonStandardToken, rescueAmount } = + await networkHelpers.loadFixture(deployWithNonStandardToken); + + const tokenAddress = await nonStandardToken.getAddress(); + + await expect( + trackGas( + staking.rescueERC20(tokenAddress, recipient.address, rescueAmount), + [GasGroup.RESCUE_ERC20] + ) + ) + .to.emit(staking, Events.ERC20_RESCUED) + .withArgs(tokenAddress, recipient.address, rescueAmount); + + expect(await nonStandardToken.balanceOf(recipient.address)).to.equal(rescueAmount); + }); + + it("Is reverted with 'ZeroAddress' when token address is zero", async function () { + const { staking } = await networkHelpers.loadFixture(deployWithExtraToken); + + const zeroAddress = "0x0000000000000000000000000000000000000000"; + const amount = connection.ethers.parseEther("1"); + + await expect( + staking.rescueERC20(zeroAddress, recipient.address, amount) + ).to.be.revertedWithCustomError(staking, Errors.ZERO_ADDRESS); + }); + + it("Is reverted with 'ZeroAddress' when recipient address is zero", async function () { + const { staking, randomToken } = await networkHelpers.loadFixture(deployWithExtraToken); + + const zeroAddress = "0x0000000000000000000000000000000000000000"; + const amount = connection.ethers.parseEther("1"); + + await expect( + staking.rescueERC20(await randomToken.getAddress(), zeroAddress, amount) + ).to.be.revertedWithCustomError(staking, Errors.ZERO_ADDRESS); + }); + + it("Is reverted with 'InvalidToken' when trying to rescue SSV token", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(deployWithExtraToken); + + const amount = connection.ethers.parseEther("1"); + + await expect( + staking.rescueERC20(await ssvToken.getAddress(), recipient.address, amount) + ).to.be.revertedWithCustomError(staking, Errors.INVALID_TOKEN); + }); + + it("Is reverted with 'InvalidToken' when trying to rescue cSSV token", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(deployWithExtraToken); + + const amount = connection.ethers.parseEther("1"); + + await expect( + staking.rescueERC20(await cssvToken.getAddress(), recipient.address, amount) + ).to.be.revertedWithCustomError(staking, Errors.INVALID_TOKEN); + }); + + it("Is reverted with 'ZeroAmount' when amount is zero", async function () { + const { staking, randomToken } = await networkHelpers.loadFixture(deployWithExtraToken); + + await expect( + staking.rescueERC20(await randomToken.getAddress(), recipient.address, 0n) + ).to.be.revertedWithCustomError(staking, Errors.ZERO_AMOUNT); + }); + + it("Allows partial rescue of tokens", async function () { + const { staking, randomToken, rescueAmount } = + await networkHelpers.loadFixture(deployWithExtraToken); + + const partialAmount = rescueAmount / 2n; + await trackGas( + staking.rescueERC20( + await randomToken.getAddress(), + recipient.address, + partialAmount + ), + [GasGroup.RESCUE_ERC20] + ); + + const recipientBalance = await randomToken.balanceOf(recipient.address); + expect(recipientBalance).to.equal(partialAmount); + + const contractBalance = await randomToken.balanceOf(await staking.getAddress()); + expect(contractBalance).to.equal(rescueAmount - partialAmount); + }); +}); diff --git a/test/unit/SSVStaking/run-tests.sh b/test/unit/SSVStaking/run-tests.sh new file mode 100755 index 000000000..959ffaf0e --- /dev/null +++ b/test/unit/SSVStaking/run-tests.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Run all SSVStaking unit tests +npx hardhat test test/unit/SSVStaking/*.test.ts diff --git a/test/unit/SSVStaking/solvencyInvariant.test.ts b/test/unit/SSVStaking/solvencyInvariant.test.ts new file mode 100644 index 000000000..483d59d2c --- /dev/null +++ b/test/unit/SSVStaking/solvencyInvariant.test.ts @@ -0,0 +1,248 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { DEFAULT_UNSTAKE_COOLDOWN, STAKE_AMOUNT, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; + +describe("SSVStaking solvency invariant (cSSV supply <= SSV backing)", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let staker1: HardhatEthersSigner; + let staker2: HardhatEthersSigner; + let staker3: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [staker1, staker2, staker3] } = await setupTestContext()); + }); + + const deployStakingFixture = async () => defaultStakingFixture(connection); + + const expectStakingSolvent = async ( + staking: any, + ssvToken: any, + cssvToken: any + ) => { + const stakingAddress = await staking.getAddress(); + const cssvSupply = await cssvToken.totalSupply(); + const ssvBacking = await ssvToken.balanceOf(stakingAddress); + + const users = [staker1.address, staker2.address, staker3.address]; + let pendingUnstakes = 0n; + for (const user of users) { + const count = await staking.getWithdrawalRequestsCount(user); + for (let i = 0n; i < count; i++) { + const [amount] = await staking.getWithdrawalRequest(user, i); + pendingUnstakes += amount; + } + } + + expect( + ssvBacking, + "Invariant violated: staking SSV backing must equal cSSV supply plus pending unstakes" + ).to.equal(cssvSupply + pendingUnstakes); + }; + + const mintAndApprove = async ( + ssvToken: any, + staking: any, + user: HardhatEthersSigner, + amount: bigint + ) => { + await ssvToken.mint(user.address, amount); + await ssvToken.connect(user).approve(await staking.getAddress(), amount); + }; + + const impersonate = async (address: string) => { + await connection.ethers.provider.send("hardhat_impersonateAccount", [address]); + await connection.ethers.provider.send("hardhat_setBalance", [address, "0x1000000000000000000"]); + return connection.ethers.getSigner(address); + }; + + it("holds before and after stake/requestUnstake ordering for a single user", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + const partial = STAKE_AMOUNT / 3n; + await staking.requestUnstake(partial); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + const remainingCssv = await cssvToken.balanceOf(staker1.address); + expect(remainingCssv).to.equal(STAKE_AMOUNT - partial); + }); + + it("holds for multiple users with partial unstake requests", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + const stake2 = STAKE_AMOUNT * 2n; + const stake3 = STAKE_AMOUNT / 2n; + + await mintAndApprove(ssvToken, staking, staker2, stake2); + await mintAndApprove(ssvToken, staking, staker3, stake3); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + + await staking.stake(STAKE_AMOUNT); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.connect(staker2).stake(stake2); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.connect(staker3).stake(stake3); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.requestUnstake(STAKE_AMOUNT / 4n); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.connect(staker2).requestUnstake(stake2 / 3n); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.connect(staker2).requestUnstake(stake2 / 6n); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.connect(staker3).requestUnstake(stake3 / 2n); + await expectStakingSolvent(staking, ssvToken, cssvToken); + }); + + it("holds through full unstake plus withdraw cycle with mixed unlocked requests", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + await mintAndApprove(ssvToken, staking, staker2, STAKE_AMOUNT); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + + await staking.stake(STAKE_AMOUNT); + await staking.connect(staker2).stake(STAKE_AMOUNT); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + const user1Full = STAKE_AMOUNT; + const user2PartA = STAKE_AMOUNT / 4n; + const user2PartB = STAKE_AMOUNT / 4n; + + await staking.requestUnstake(user1Full); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.connect(staker2).requestUnstake(user2PartA); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN / 2n); + await staking.connect(staker2).requestUnstake(user2PartB); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await networkHelpers.time.increase((DEFAULT_UNSTAKE_COOLDOWN / 2n) + 1n); + + await staking.withdrawUnlocked(); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.connect(staker2).withdrawUnlocked(); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN / 2n); + await staking.connect(staker2).withdrawUnlocked(); + await expectStakingSolvent(staking, ssvToken, cssvToken); + }); + + it("holds through fee sync and ETH reward claims", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + await mintAndApprove(ssvToken, staking, staker2, STAKE_AMOUNT); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + + await staking.stake(STAKE_AMOUNT); + await staking.connect(staker2).stake(STAKE_AMOUNT); + await expectStakingSolvent(staking, ssvToken, cssvToken); + await staking.mockSetDaoTotalEthVUnits(0n); + await staking.mockSetEthNetworkFee(0n); + await staker1.sendTransaction({ + to: await staking.getAddress(), + value: connection.ethers.parseEther("1"), + }); + const user1Accrued = 6n * ETH_DEDUCTED_DIGITS; + const user2Accrued = 9n * ETH_DEDUCTED_DIGITS; + await staking.mockSetUserAccrued(staker1.address, user1Accrued); + await staking.mockSetUserAccrued(staker2.address, user2Accrued); + await staking.mockSetStakingEthPoolBalance(100n); + await staking.mockSetEthDaoBalance(100n); + const expectedUser1PayoutShrunk = user1Accrued / ETH_DEDUCTED_DIGITS; + const expectedUser2PayoutShrunk = user2Accrued / ETH_DEDUCTED_DIGITS; + + const poolBeforeUser1 = await staking.getStakingEthPoolBalance(); + const daoBeforeUser1 = await staking.getEthDaoBalance(); + await staking.claimEthRewards(); + expect(await staking.getUserAccrued(staker1.address)).to.equal(0n); + expect(await staking.getStakingEthPoolBalance()).to.equal(poolBeforeUser1 - expectedUser1PayoutShrunk); + expect(await staking.getEthDaoBalance()).to.equal(daoBeforeUser1 - expectedUser1PayoutShrunk); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + const poolBeforeUser2 = await staking.getStakingEthPoolBalance(); + const daoBeforeUser2 = await staking.getEthDaoBalance(); + await staking.connect(staker2).claimEthRewards(); + expect(await staking.getUserAccrued(staker2.address)).to.equal(0n); + expect(await staking.getStakingEthPoolBalance()).to.equal(poolBeforeUser2 - expectedUser2PayoutShrunk); + expect(await staking.getEthDaoBalance()).to.equal(daoBeforeUser2 - expectedUser2PayoutShrunk); + await expectStakingSolvent(staking, ssvToken, cssvToken); + }); + + it("holds during cSSV transfer settlement path", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + await mintAndApprove(ssvToken, staking, staker2, STAKE_AMOUNT); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + + await staking.stake(STAKE_AMOUNT); + await staking.connect(staker2).stake(STAKE_AMOUNT); + await expectStakingSolvent(staking, ssvToken, cssvToken); + const cssvSigner = await impersonate(await cssvToken.getAddress()); + await staking.mockSetDaoTotalEthVUnits(0n); + await staking.mockSetEthNetworkFee(0n); + await staking.mockSetAccEthPerShare(2n * 10n ** 18n); + await staking.mockSetUserIndex(staker1.address, 10n ** 18n); + await staking.mockSetUserIndex(staker2.address, 10n ** 18n); + + await staking.connect(cssvSigner).onCSSVTransfer( + staker1.address, + staker2.address, + STAKE_AMOUNT / 2n + ); + expect(await staking.getUserAccrued(staker1.address)).to.equal(STAKE_AMOUNT); + expect(await staking.getUserAccrued(staker2.address)).to.equal(STAKE_AMOUNT); + expect(await staking.getUserIndex(staker1.address)).to.equal(2n * 10n ** 18n); + expect(await staking.getUserIndex(staker2.address)).to.equal(2n * 10n ** 18n); + await expectStakingSolvent(staking, ssvToken, cssvToken); + await cssvToken.transfer(staker2.address, STAKE_AMOUNT / 2n); + await expectStakingSolvent(staking, ssvToken, cssvToken); + }); + + it("holds when all users fully unstake and withdraw", async function () { + const { staking, ssvToken, cssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + await mintAndApprove(ssvToken, staking, staker2, STAKE_AMOUNT); + await mintAndApprove(ssvToken, staking, staker3, STAKE_AMOUNT); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + + await staking.stake(STAKE_AMOUNT); + await staking.connect(staker2).stake(STAKE_AMOUNT); + await staking.connect(staker3).stake(STAKE_AMOUNT); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await staking.requestUnstake(STAKE_AMOUNT); + await staking.connect(staker2).requestUnstake(STAKE_AMOUNT); + await staking.connect(staker3).requestUnstake(STAKE_AMOUNT); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + await staking.withdrawUnlocked(); + await staking.connect(staker2).withdrawUnlocked(); + await staking.connect(staker3).withdrawUnlocked(); + await expectStakingSolvent(staking, ssvToken, cssvToken); + + expect(await cssvToken.totalSupply()).to.equal(0n); + expect(await ssvToken.balanceOf(await staking.getAddress())).to.equal(0n); + }); +}); diff --git a/test/unit/SSVStaking/stake.test.ts b/test/unit/SSVStaking/stake.test.ts new file mode 100644 index 000000000..5aaf31acb --- /dev/null +++ b/test/unit/SSVStaking/stake.test.ts @@ -0,0 +1,704 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { STAKE_AMOUNT, ETH_DEDUCTED_DIGITS, DEFAULT_UNSTAKE_COOLDOWN } from "../../common/constants.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; +import { deployMultisig, multisigExec } from "../../helpers/multisig.ts"; + +describe("SSVStaking function `stake()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let staker: HardhatEthersSigner; + let other: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [staker, other] } = await setupTestContext()); + }); + + const deployStakingFixture = async () => defaultStakingFixture(connection); + + it("Stakes SSV tokens, mints cSSV and emits Staked event", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const stakingAddress = await staking.getAddress(); + const stakerSsvBalanceBefore = await ssvToken.balanceOf(staker.address); + const stakingSsvBalanceBefore = await ssvToken.balanceOf(stakingAddress); + const cssvSupplyBefore = await cssvToken.totalSupply(); + + await ssvToken.approve(stakingAddress, STAKE_AMOUNT); + + const tx = await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + await expect(tx) + .to.emit(staking, Events.STAKED) + .withArgs(staker.address, STAKE_AMOUNT); + + const cssvBalance = await cssvToken.balanceOf(staker.address); + expect(cssvBalance).to.equal(STAKE_AMOUNT); + + const stakerSsvBalanceAfter = await ssvToken.balanceOf(staker.address); + const stakingSsvBalanceAfter = await ssvToken.balanceOf(stakingAddress); + const cssvSupplyAfter = await cssvToken.totalSupply(); + + expect(stakerSsvBalanceBefore - stakerSsvBalanceAfter).to.equal(STAKE_AMOUNT); + expect(stakingSsvBalanceAfter - stakingSsvBalanceBefore).to.equal(STAKE_AMOUNT); + expect(cssvSupplyAfter - cssvSupplyBefore).to.equal(STAKE_AMOUNT); + + const allowanceAfter = await ssvToken.allowance(staker.address, stakingAddress); + expect(allowanceAfter).to.equal(0n); + }); + + it("Updates user index after staking", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const userIndex = await staking.getUserIndex(staker.address); + const accEthPerShare = await staking.getAccEthPerShare(); + expect(userIndex).to.equal(accEthPerShare); + }); + + it("Accepts exactly the minimum stake amount", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const minAmount = 1_000_000_000n; + await ssvToken.approve(await staking.getAddress(), minAmount); + + await expect(staking.stake(minAmount)) + .to.emit(staking, Events.STAKED) + .withArgs(staker.address, minAmount); + }); + + it("Accepts a stake amount exactly 1 above the minimum", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const aboveMinimum = 1_000_000_001n; + await ssvToken.approve(await staking.getAddress(), aboveMinimum); + + await expect(staking.stake(aboveMinimum)) + .to.emit(staking, Events.STAKED) + .withArgs(staker.address, aboveMinimum); + + expect(await cssvToken.balanceOf(staker.address)).to.equal(aboveMinimum); + }); + + it("Is reverted with 'StakeTooLow' when staking zero amount", async function () { + const { staking } = + await networkHelpers.loadFixture(deployStakingFixture); + + await expect(staking.stake(0n)).to.be.revertedWithCustomError( + staking, + Errors.STAKE_TOO_LOW + ); + }); + + it("Is reverted with 'StakeTooLow' when staking below minimum", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const tooLowAmount = 999_999_999n; + await ssvToken.approve(await staking.getAddress(), tooLowAmount); + + await expect(staking.stake(tooLowAmount)).to.be.revertedWithCustomError( + staking, + Errors.STAKE_TOO_LOW + ); + }); + + it("Is reverted when allowance is insufficient", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const amount = STAKE_AMOUNT; + await ssvToken.approve(await staking.getAddress(), amount - 1n); + + await expect(staking.stake(amount)).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("Is reverted when staking without approval", async function () { + const { staking } = + await networkHelpers.loadFixture(deployStakingFixture); + + await expect(staking.stake(STAKE_AMOUNT)).to.be.revertedWith("ERC20: insufficient allowance"); + }); + + it("Is reverted when token balance is insufficient", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.connect(other).approve(await staking.getAddress(), STAKE_AMOUNT); + + await expect(staking.connect(other).stake(STAKE_AMOUNT)) + .to.be.revertedWith("ERC20: transfer amount exceeds balance"); + }); + + it("Allows multiple stakes from the same user", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const firstStake = STAKE_AMOUNT; + const secondStake = STAKE_AMOUNT * 2n; + + await ssvToken.approve(await staking.getAddress(), firstStake + secondStake); + + await trackGas( + staking.stake(firstStake), + [GasGroup.INITIAL_STAKE_SSV] + ); + await trackGas( + staking.stake(secondStake), + [GasGroup.POST_INITIAL_STAKE_SSV] + ); + + const cssvBalance = await cssvToken.balanceOf(staker.address); + expect(cssvBalance).to.equal(firstStake + secondStake); + }); + + it("Settles pending rewards for existing stake when fees accrue before staking again", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + const stake1 = await staking.stake(STAKE_AMOUNT); + const receipt1 = await stake1.wait(); + + const accruedBefore = await staking.getUserAccrued(staker.address); + expect(accruedBefore).to.equal(0n); + + await staking.mockSetDaoTotalEthVUnits(10_000n); + await staking.mockSetEthNetworkFee(1n); + + await connection.ethers.provider.send("hardhat_mine", ["0xA"]); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + const stake2 = await staking.stake(STAKE_AMOUNT); + const receipt2 = await stake2.wait(); + + const accruedAfter = await staking.getUserAccrued(staker.address); + const blocksElapsed = BigInt(receipt2.blockNumber - receipt1.blockNumber); + expect(accruedAfter).to.equal(blocksElapsed * ETH_DEDUCTED_DIGITS); + + const userIndex = await staking.getUserIndex(staker.address); + const accEthPerShare = await staking.getAccEthPerShare(); + expect(userIndex).to.equal(accEthPerShare); + }); + + it("Settles rewards correctly when staking again after a partial unstake", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const firstStake = STAKE_AMOUNT; + const partialUnstake = STAKE_AMOUNT / 2n; + const secondStake = STAKE_AMOUNT / 2n; + + await ssvToken.approve( + await staking.getAddress(), + firstStake + secondStake, + ); + + const stake1 = await staking.stake(firstStake); + const receipt1 = await stake1.wait(); + + await staking.mockSetDaoTotalEthVUnits(10_000n); + await staking.mockSetEthNetworkFee(1n); + + await connection.ethers.provider.send("hardhat_mine", ["0xA"]); + + const unstakeTx = await staking.requestUnstake(partialUnstake); + const unstakeReceipt = await unstakeTx.wait(); + + const phase1Blocks = BigInt(unstakeReceipt.blockNumber - receipt1.blockNumber); + const phase1ExpectedRewards = phase1Blocks * ETH_DEDUCTED_DIGITS; + expect(await staking.getUserAccrued(staker.address)).to.equal( + phase1ExpectedRewards, + ); + expect(await cssvToken.balanceOf(staker.address)).to.equal( + firstStake - partialUnstake, + ); + + await connection.ethers.provider.send("hardhat_mine", ["0xA"]); + + const stake2 = await staking.stake(secondStake); + const receipt2 = await stake2.wait(); + + const phase2Blocks = BigInt(receipt2.blockNumber - unstakeReceipt.blockNumber); + const phase2ExpectedRewards = phase2Blocks * ETH_DEDUCTED_DIGITS; + const expectedAccruedAfterRestake = + phase1ExpectedRewards + phase2ExpectedRewards; + + expect(await staking.getUserAccrued(staker.address)).to.equal( + expectedAccruedAfterRestake, + ); + expect(await cssvToken.balanceOf(staker.address)).to.equal(firstStake); + + const userIndex = await staking.getUserIndex(staker.address); + const accEthPerShare = await staking.getAccEthPerShare(); + expect(userIndex).to.equal(accEthPerShare); + }); + + it("Multisig contract stakes SSV tokens", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + + const ssvBefore = await ssvToken.balanceOf(multisigAddress); + const cssvBefore = await cssvToken.balanceOf(multisigAddress); + const contractSsvBefore = await ssvToken.balanceOf(stakingAddress); + + const tx = await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + await expect(tx) + .to.emit(staking, Events.STAKED) + .withArgs(multisigAddress, STAKE_AMOUNT); + + expect(await ssvToken.balanceOf(multisigAddress)).to.equal(ssvBefore - STAKE_AMOUNT); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(cssvBefore + STAKE_AMOUNT); + expect(await ssvToken.balanceOf(stakingAddress)).to.equal(contractSsvBefore + STAKE_AMOUNT); + }); + + it("Multisig contract stakes multiple times", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + const totalAmount = STAKE_AMOUNT * 3n; + await ssvToken.mint(multisigAddress, totalAmount); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, totalAmount]); + + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT); + + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT * 2n); + + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT * 3n); + + expect(await ssvToken.balanceOf(multisigAddress)).to.equal(0n); + expect(await ssvToken.balanceOf(stakingAddress)).to.equal(totalAmount); + }); + + it("Multisig earns rewards after staking", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + const totalMint = STAKE_AMOUNT * 2n; + await ssvToken.mint(multisigAddress, totalMint); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, totalMint]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const packedReward = 10_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(packedReward); + + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const accrued = await staking.getUserAccrued(multisigAddress); + expect(accrued).to.equal(packedReward * ETH_DEDUCTED_DIGITS); + }); + + it("Multisig claims ETH rewards", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const packedReward = 10_000_000_000n; + await staking.mockSetStakingEthPoolBalance(packedReward); + await staking.mockSetEthDaoBalance(packedReward); + + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("1"), + }); + + await staking.mockSetUserAccrued(multisigAddress, packedReward * ETH_DEDUCTED_DIGITS); + + const ethBefore = await connection.ethers.provider.getBalance(multisigAddress); + const tx = await multisigExec(multisig, staking, "claimEthRewards", []); + + const expectedPayout = packedReward * ETH_DEDUCTED_DIGITS; + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(multisigAddress, expectedPayout); + + const ethAfter = await connection.ethers.provider.getBalance(multisigAddress); + expect(ethAfter - ethBefore).to.equal(expectedPayout); + }); + + it("Multisig claims with dust — remainder preserved in accrued", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const accruedWithDust = 123_456_789n; + const expectedPayout = accruedWithDust - (accruedWithDust % ETH_DEDUCTED_DIGITS); + const expectedDust = accruedWithDust % ETH_DEDUCTED_DIGITS; + + await staking.mockSetStakingEthPoolBalance(100_000_000_000n); + await staking.mockSetEthDaoBalance(100_000_000_000n); + await staking.mockSetUserAccrued(multisigAddress, accruedWithDust); + + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("1"), + }); + + const tx = await multisigExec(multisig, staking, "claimEthRewards", []); + + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(multisigAddress, expectedPayout); + + const accruedAfter = await staking.getUserAccrued(multisigAddress); + expect(accruedAfter).to.equal(expectedDust); + }); + + it("Multisig transfers cSSV to EOA", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const transferAmount = STAKE_AMOUNT / 2n; + await multisigExec(multisig, cssvToken, "transfer", [other.address, transferAmount]); + + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT - transferAmount); + expect(await cssvToken.balanceOf(other.address)).to.equal(transferAmount); + }); + + it("EOA transfers cSSV to multisig", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.approve(stakingAddress, STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const transferAmount = STAKE_AMOUNT / 2n; + await cssvToken.connect(staker).transfer(multisigAddress, transferAmount); + + expect(await cssvToken.balanceOf(staker.address)).to.equal(STAKE_AMOUNT - transferAmount); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(transferAmount); + }); + + it("Multisig transfers cSSV to another multisig", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig1 = await deployMultisig(connection.ethers); + const multisig2 = await deployMultisig(connection.ethers); + const ms1Address = await multisig1.getAddress(); + const ms2Address = await multisig2.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(ms1Address, STAKE_AMOUNT); + await multisigExec(multisig1, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig1, staking, "stake", [STAKE_AMOUNT]); + + const transferAmount = STAKE_AMOUNT / 2n; + await multisigExec(multisig1, cssvToken, "transfer", [ms2Address, transferAmount]); + + expect(await cssvToken.balanceOf(ms1Address)).to.equal(STAKE_AMOUNT - transferAmount); + expect(await cssvToken.balanceOf(ms2Address)).to.equal(transferAmount); + }); + + it("Multisig requests unstake", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const unstakeAmount = STAKE_AMOUNT / 2n; + const tx = await multisigExec(multisig, staking, "requestUnstake", [unstakeAmount]); + const block = await connection.ethers.provider.getBlock(tx.blockNumber); + const expectedUnlockTime = BigInt(block!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + await expect(tx) + .to.emit(staking, Events.UNSTAKE_REQUESTED) + .withArgs(multisigAddress, unstakeAmount, expectedUnlockTime); + + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT - unstakeAmount); + + const requestCount = await staking.getWithdrawalRequestsCount(multisigAddress); + expect(requestCount).to.equal(1n); + + const [amount, unlockTime] = await staking.getWithdrawalRequest(multisigAddress, 0); + expect(amount).to.equal(unstakeAmount); + expect(unlockTime).to.equal(expectedUnlockTime); + }); + + it("Multisig creates multiple unstake requests", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const amount1 = STAKE_AMOUNT / 4n; + const amount2 = STAKE_AMOUNT / 4n; + const amount3 = STAKE_AMOUNT / 4n; + + const tx1 = await multisigExec(multisig, staking, "requestUnstake", [amount1]); + await networkHelpers.time.increase(100n); + const tx2 = await multisigExec(multisig, staking, "requestUnstake", [amount2]); + await networkHelpers.time.increase(100n); + const tx3 = await multisigExec(multisig, staking, "requestUnstake", [amount3]); + + const requestCount = await staking.getWithdrawalRequestsCount(multisigAddress); + expect(requestCount).to.equal(3n); + + const [a1] = await staking.getWithdrawalRequest(multisigAddress, 0); + const [a2] = await staking.getWithdrawalRequest(multisigAddress, 1); + const [a3] = await staking.getWithdrawalRequest(multisigAddress, 2); + expect(a1).to.equal(amount1); + expect(a2).to.equal(amount2); + expect(a3).to.equal(amount3); + + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT - amount1 - amount2 - amount3); + }); + + it("Multisig requests unstake after earning rewards", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const packedReward = 10_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(packedReward); + + const accruedBefore = await staking.getUserAccrued(multisigAddress); + expect(accruedBefore).to.equal(0n); + + await multisigExec(multisig, staking, "requestUnstake", [STAKE_AMOUNT / 2n]); + + const accruedAfter = await staking.getUserAccrued(multisigAddress); + expect(accruedAfter).to.equal(packedReward * ETH_DEDUCTED_DIGITS); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT / 2n); + }); + + it("Multisig withdraws unlocked SSV", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + await multisigExec(multisig, staking, "requestUnstake", [STAKE_AMOUNT]); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const ssvBefore = await ssvToken.balanceOf(multisigAddress); + const tx = await multisigExec(multisig, staking, "withdrawUnlocked", []); + + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(multisigAddress, STAKE_AMOUNT); + + expect(await ssvToken.balanceOf(multisigAddress)).to.equal(ssvBefore + STAKE_AMOUNT); + expect(await staking.getWithdrawalRequestsCount(multisigAddress)).to.equal(0n); + }); + + it("Multisig withdraws multiple matured requests", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + + const amount1 = STAKE_AMOUNT / 4n; + const amount2 = STAKE_AMOUNT / 4n; + const amount3 = STAKE_AMOUNT / 4n; + + await multisigExec(multisig, staking, "requestUnstake", [amount1]); + await multisigExec(multisig, staking, "requestUnstake", [amount2]); + await multisigExec(multisig, staking, "requestUnstake", [amount3]); + + expect(await staking.getWithdrawalRequestsCount(multisigAddress)).to.equal(3n); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const ssvBefore = await ssvToken.balanceOf(multisigAddress); + const totalWithdrawn = amount1 + amount2 + amount3; + const tx = await multisigExec(multisig, staking, "withdrawUnlocked", []); + + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(multisigAddress, totalWithdrawn); + + expect(await ssvToken.balanceOf(multisigAddress)).to.equal(ssvBefore + totalWithdrawn); + expect(await staking.getWithdrawalRequestsCount(multisigAddress)).to.equal(0n); + }); + + it("Multisig complete flow: stake -> earn -> claim -> unstake -> withdraw", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.mint(multisigAddress, STAKE_AMOUNT); + await multisigExec(multisig, ssvToken, "approve", [stakingAddress, STAKE_AMOUNT]); + await multisigExec(multisig, staking, "stake", [STAKE_AMOUNT]); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT); + + const packedReward = 10_000_000_000n; + const rewardWei = packedReward * ETH_DEDUCTED_DIGITS; + await staking.mockSetStakingEthPoolBalance(packedReward); + await staking.mockSetEthDaoBalance(packedReward); + await staking.mockSetUserAccrued(multisigAddress, rewardWei); + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("1"), + }); + + const ethBefore = await connection.ethers.provider.getBalance(multisigAddress); + await multisigExec(multisig, staking, "claimEthRewards", []); + const ethAfter = await connection.ethers.provider.getBalance(multisigAddress); + expect(ethAfter - ethBefore).to.equal(rewardWei); + + await multisigExec(multisig, staking, "requestUnstake", [STAKE_AMOUNT]); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(0n); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const ssvBefore = await ssvToken.balanceOf(multisigAddress); + await multisigExec(multisig, staking, "withdrawUnlocked", []); + expect(await ssvToken.balanceOf(multisigAddress)).to.equal(ssvBefore + STAKE_AMOUNT); + expect(await staking.getWithdrawalRequestsCount(multisigAddress)).to.equal(0n); + }); + + it("Mixed EOA and multisig: EOA stakes, transfers cSSV to multisig, multisig claims rewards", async function () { + const { staking, ssvToken, cssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const multisig = await deployMultisig(connection.ethers); + const multisigAddress = await multisig.getAddress(); + const stakingAddress = await staking.getAddress(); + + await ssvToken.approve(stakingAddress, STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const packedReward = 10_000_000_000n; + const rewardWei = packedReward * ETH_DEDUCTED_DIGITS; + await staking.mockSetStakingEthPoolBalance(packedReward); + await staking.mockSetEthDaoBalance(packedReward); + await staker.sendTransaction({ + to: stakingAddress, + value: connection.ethers.parseEther("1"), + }); + + await cssvToken.connect(staker).transfer(multisigAddress, STAKE_AMOUNT); + expect(await cssvToken.balanceOf(multisigAddress)).to.equal(STAKE_AMOUNT); + expect(await cssvToken.balanceOf(staker.address)).to.equal(0n); + + await staking.mockSetUserAccrued(multisigAddress, rewardWei); + + const ethBefore = await connection.ethers.provider.getBalance(multisigAddress); + const tx = await multisigExec(multisig, staking, "claimEthRewards", []); + + await expect(tx) + .to.emit(staking, Events.REWARDS_CLAIMED) + .withArgs(multisigAddress, rewardWei); + + const ethAfter = await connection.ethers.provider.getBalance(multisigAddress); + expect(ethAfter - ethBefore).to.equal(rewardWei); + }); + + it("Transfers SSV tokens to the staking contract", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + const stakingAddress = await staking.getAddress(); + const balanceBefore = await ssvToken.balanceOf(stakingAddress); + + await ssvToken.approve(stakingAddress, STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const balanceAfter = await ssvToken.balanceOf(stakingAddress); + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + }); +}); diff --git a/test/unit/SSVStaking/syncFees.test.ts b/test/unit/SSVStaking/syncFees.test.ts new file mode 100644 index 000000000..007fb1d1d --- /dev/null +++ b/test/unit/SSVStaking/syncFees.test.ts @@ -0,0 +1,352 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { Events } from "../../common/events.ts"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { STAKE_AMOUNT, ETH_DEDUCTED_DIGITS } from "../../common/constants.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVStaking function `syncFees()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let staker: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [staker] } = await setupTestContext()); + }); + + const deployStakingFixture = async () => defaultStakingFixture(connection); + + it("Updates staking pool balance and emits FeesSynced event", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const newFees = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + + const tx = await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + await expect(tx).to.emit(staking, Events.FEES_SYNCED).withArgs(newFees * ETH_DEDUCTED_DIGITS, 10_000_000_000_000n); + + const poolBalance = await staking.getStakingEthPoolBalance(); + expect(poolBalance).to.equal(newFees); + }); + + it("Updates accEthPerShare when new fees are available and total staked is non-zero", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const accBefore = await staking.getAccEthPerShare(); + + const newFees = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const accAfter = await staking.getAccEthPerShare(); + const expectedDelta = (newFees * ETH_DEDUCTED_DIGITS * 1_000_000_000_000_000_000n) / STAKE_AMOUNT; + expect(accAfter - accBefore).to.equal(expectedDelta); + }); + + it("Calculates and syncs fees based on network usage (natural accrual)", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(0n); + await staking.syncFees(); + + const accBefore = await staking.getAccEthPerShare(); + const poolBalanceBefore = await staking.getStakingEthPoolBalance(); + const vUnits = 10_000n; + const fee = 500n; + await staking.mockSetDaoTotalEthVUnits(vUnits); + await staking.mockSetEthNetworkFee(fee); + const setDaoTx = await staking.mockSetEthDaoBalance(0n); + const setDaoReceipt = await setDaoTx.wait(); + const blocksToMine = 10; + await connection.ethers.provider.send("hardhat_mine", ["0xA"]); + + const receipt = await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const blocksElapsed = BigInt(receipt.blockNumber - setDaoReceipt!.blockNumber); + const expectedEarnings = blocksElapsed * fee; + const expectedEarningsWei = expectedEarnings * ETH_DEDUCTED_DIGITS; + + const accAfter = await staking.getAccEthPerShare(); + const expectedDelta = (expectedEarningsWei * 1_000_000_000_000_000_000n) / STAKE_AMOUNT; + expect(accAfter - accBefore).to.equal(expectedDelta); + + const poolBalanceAfter = await staking.getStakingEthPoolBalance(); + expect(poolBalanceAfter).to.equal(expectedEarnings); + }); + + it("Does not emit FeesSynced or update accEthPerShare when no new fees (current == previous)", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const currentBalance = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(currentBalance); + await staking.mockSetEthDaoBalance(currentBalance); + + const accBefore = await staking.getAccEthPerShare(); + + const tx = await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + await expect(tx).to.not.emit(staking, Events.FEES_SYNCED); + + const accAfter = await staking.getAccEthPerShare(); + expect(accAfter).to.equal(accBefore); + + const poolBalance = await staking.getStakingEthPoolBalance(); + expect(poolBalance).to.equal(currentBalance); + }); + + it("Updates pool balance without emitting FeesSynced when current < previous (inconsistent state)", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const highBalance = 2_000_000_000n; + const lowBalance = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(highBalance); + await staking.mockSetEthDaoBalance(lowBalance); + + const accBefore = await staking.getAccEthPerShare(); + + const tx = await staking.syncFees(); + + await expect(tx).to.not.emit(staking, Events.FEES_SYNCED); + + const accAfter = await staking.getAccEthPerShare(); + expect(accAfter).to.equal(accBefore); + const poolBalance = await staking.getStakingEthPoolBalance(); + expect(poolBalance).to.equal(lowBalance); + }); + + it("Does not change accEthPerShare but updates pool balance when total staked is zero", async function () { + const { staking } = + await networkHelpers.loadFixture(deployStakingFixture); + + const accBefore = await staking.getAccEthPerShare(); + + const newFees = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const accAfter = await staking.getAccEthPerShare(); + expect(accAfter).to.equal(accBefore); + + const poolBalance = await staking.getStakingEthPoolBalance(); + expect(poolBalance).to.equal(newFees); + }); + + it("Syncs DAO balance correctly", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const newBalance = 5_000_000_000n; + await staking.mockSetEthDaoBalance(newBalance); + + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const ethDaoBalance = await staking.getEthDaoBalance(); + expect(ethDaoBalance).to.equal(newBalance); + }); + + it("Can be called multiple times", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(1_000_000_000n); + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const accAfterFirst = await staking.getAccEthPerShare(); + + await staking.mockSetEthDaoBalance(2_000_000_000n); + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const accAfterSecond = await staking.getAccEthPerShare(); + const secondSyncNewFees = 1_000_000_000n; + const expectedSecondDelta = (secondSyncNewFees * ETH_DEDUCTED_DIGITS * 1_000_000_000_000_000_000n) / STAKE_AMOUNT; + expect(accAfterSecond - accAfterFirst).to.equal(expectedSecondDelta); + }); + + it("Stores updated pool balance in storage", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const newFees = 5_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const storedPoolBalance = await staking.getStakingEthPoolBalance(); + expect(storedPoolBalance).to.equal(newFees); + }); + + it("Stores updated accEthPerShare in storage", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const accBefore = await staking.getAccEthPerShare(); + + const newFees = 1_000_000_000n; + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(newFees); + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const accAfter = await staking.getAccEthPerShare(); + const expectedDelta = (newFees * ETH_DEDUCTED_DIGITS * 1_000_000_000_000_000_000n) / STAKE_AMOUNT; + expect(accAfter - accBefore).to.equal(expectedDelta); + }); + + it("Produces non-zero accEthPerShare update with minimum possible fee (1 packed unit) and standard stake", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + const accBefore = await staking.getAccEthPerShare(); + + await staking.mockSetStakingEthPoolBalance(0n); + await staking.mockSetEthDaoBalance(1n); + + await trackGas( + staking.syncFees(), + [GasGroup.SYNC_FEES] + ); + + const accAfter = await staking.getAccEthPerShare(); + + const PRECISION = 1_000_000_000_000_000_000n; + const expectedDelta = (1n * ETH_DEDUCTED_DIGITS * PRECISION) / STAKE_AMOUNT; + expect(accAfter - accBefore).to.equal(expectedDelta); + }); + + it("Calling syncFees twice in the same block does not double-count fees", async function () { + const { staking, ssvToken } = + await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + + await staking.mockSetStakingEthPoolBalance(0n); + const newFees = 1_000_000_000n; + await staking.mockSetEthDaoBalance(newFees); + + const accBefore = await staking.getAccEthPerShare(); + const provider = connection.ethers.provider; + await provider.send("evm_setAutomine", [false]); + + try { + const tx1 = await staking.syncFees(); + const tx2 = await staking.syncFees(); + + await provider.send("evm_mine", []); + + const receipt1 = await tx1.wait(); + const receipt2 = await tx2.wait(); + + expect(receipt1!.blockNumber).to.equal(receipt2!.blockNumber); + await expect(tx2).to.not.emit(staking, Events.FEES_SYNCED); + } finally { + await provider.send("evm_setAutomine", [true]); + } + + const accAfter = await staking.getAccEthPerShare(); + const expectedDelta = (newFees * ETH_DEDUCTED_DIGITS * 1_000_000_000_000_000_000n) / STAKE_AMOUNT; + expect(accAfter - accBefore).to.equal(expectedDelta); + }); +}); diff --git a/test/unit/SSVStaking/withdrawUnlocked.test.ts b/test/unit/SSVStaking/withdrawUnlocked.test.ts new file mode 100644 index 000000000..bd3f7a1e8 --- /dev/null +++ b/test/unit/SSVStaking/withdrawUnlocked.test.ts @@ -0,0 +1,375 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { defaultStakingFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext } from "../../common/helpers.ts"; +import { ssvStakingHarnessFixture } from "../../setup/fixtures.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { STAKE_AMOUNT, DEFAULT_UNSTAKE_COOLDOWN } from "../../common/constants.ts"; +import { trackGas, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVStaking function `withdrawUnlocked()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let staker: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [staker] } = await setupTestContext()); + }); + + const stakeAndRequestUnstake = async () => { + const { staking, ssvToken, cssvToken } = await defaultStakingFixture(connection); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await trackGas( + staking.stake(STAKE_AMOUNT), + [GasGroup.STAKE_SSV] + ); + await trackGas( + staking.requestUnstake(STAKE_AMOUNT), + [GasGroup.REQUEST_UNSTAKE] + ); + return { staking, ssvToken, cssvToken }; + }; + + it("Withdraws unlocked tokens after cooldown and emits UnstakedWithdrawn event", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const stakerBalanceBefore = await ssvToken.balanceOf(staker.address); + const contractBalanceBefore = await ssvToken.balanceOf(await staking.getAddress()); + + const tx = await trackGas( + staking.withdrawUnlocked(), + [GasGroup.WITHDRAW_UNSTAKE] + ); + + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(staker.address, STAKE_AMOUNT); + const stakerBalanceAfter = await ssvToken.balanceOf(staker.address); + expect(stakerBalanceAfter - stakerBalanceBefore).to.equal(STAKE_AMOUNT); + const contractBalanceAfter = await ssvToken.balanceOf(await staking.getAddress()); + expect(contractBalanceBefore - contractBalanceAfter).to.equal(STAKE_AMOUNT); + }); + + it("Clears the withdrawal request after successful withdrawal", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await trackGas( + staking.withdrawUnlocked(), + [GasGroup.WITHDRAW_UNSTAKE] + ); + + const requestCount = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCount).to.equal(0n); + }); + + it("Is reverted with 'NothingToWithdraw' when there is no pending withdrawal", async function () { + const { staking } = await defaultStakingFixture(connection); + + await expect(staking.withdrawUnlocked()).to.be.revertedWithCustomError( + staking, + Errors.NOTHING_TO_WITHDRAW + ); + }); + + it("Is reverted with 'NothingToWithdraw' when cooldown has not passed", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + await expect(staking.withdrawUnlocked()).to.be.revertedWithCustomError( + staking, + Errors.NOTHING_TO_WITHDRAW + ); + }); + + it("Is reverted with 'NothingToWithdraw' when partially through cooldown", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN / 2n); + + await expect(staking.withdrawUnlocked()).to.be.revertedWithCustomError( + staking, + Errors.NOTHING_TO_WITHDRAW + ); + }); + + it("Allows withdrawal exactly at unlock time", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN); + + const balanceBefore = await ssvToken.balanceOf(staker.address); + await trackGas( + staking.withdrawUnlocked(), + [GasGroup.WITHDRAW_UNSTAKE] + ); + const balanceAfter = await ssvToken.balanceOf(staker.address); + + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + }); + + it("Clears withdrawal request from storage after withdrawal", async function () { + const { staking } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + const requestCountBefore = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCountBefore).to.equal(1n); + + const [amountBefore, unlockTimeBefore] = await staking.getWithdrawalRequest(staker.address, 0); + expect(amountBefore).to.equal(STAKE_AMOUNT); + expect(unlockTimeBefore).to.be.greaterThan(0n); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await staking.withdrawUnlocked(); + + const requestCountAfter = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCountAfter).to.equal(0n); + }); + + it("Withdraws multiple unlocked requests in a single call", async function () { + const { staking, ssvToken, cssvToken } = await defaultStakingFixture(connection); + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const amount1 = STAKE_AMOUNT / 4n; + const amount2 = STAKE_AMOUNT / 4n; + const amount3 = STAKE_AMOUNT / 4n; + + await staking.requestUnstake(amount1); + await staking.requestUnstake(amount2); + await staking.requestUnstake(amount3); + + const requestCount = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCount).to.equal(3n); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const balanceBefore = await ssvToken.balanceOf(staker.address); + const tx = await trackGas( + staking.withdrawUnlocked(), + [GasGroup.WITHDRAW_UNSTAKE] + ); + + const totalWithdrawn = amount1 + amount2 + amount3; + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(staker.address, totalWithdrawn); + + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(totalWithdrawn); + const requestCountAfter = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCountAfter).to.equal(0n); + }); + + it("Withdraws only unlocked requests, leaving locked ones pending", async function () { + const { staking, ssvToken, cssvToken } = await defaultStakingFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const amount1 = STAKE_AMOUNT / 4n; + const amount2 = STAKE_AMOUNT / 4n; + await staking.requestUnstake(amount1); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN / 2n); + await staking.requestUnstake(amount2); + await networkHelpers.time.increase((DEFAULT_UNSTAKE_COOLDOWN / 2n) + 1n); + + const balanceBefore = await ssvToken.balanceOf(staker.address); + const tx = await trackGas( + staking.withdrawUnlocked(), + [GasGroup.WITHDRAW_UNSTAKE] + ); + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(staker.address, amount1); + + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(amount1); + const requestCountAfter = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCountAfter).to.equal(1n); + const [remainingAmount] = await staking.getWithdrawalRequest(staker.address, 0); + expect(remainingAmount).to.equal(amount2); + }); + + it("Allows second withdrawal after remaining requests unlock", async function () { + const { staking, ssvToken, cssvToken } = await defaultStakingFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const amount1 = STAKE_AMOUNT / 4n; + const amount2 = STAKE_AMOUNT / 4n; + + await staking.requestUnstake(amount1); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN / 2n); + await staking.requestUnstake(amount2); + await networkHelpers.time.increase((DEFAULT_UNSTAKE_COOLDOWN / 2n) + 1n); + await staking.withdrawUnlocked(); + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN / 2n); + + const balanceBefore = await ssvToken.balanceOf(staker.address); + const tx = await staking.withdrawUnlocked(); + + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(staker.address, amount2); + + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(amount2); + const requestCountFinal = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCountFinal).to.equal(0n); + }); + + it("Withdraws full amount one year after maturity", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + const oneYear = 365n * 24n * 60n * 60n; + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + oneYear); + + const balanceBefore = await ssvToken.balanceOf(staker.address); + const tx = await trackGas( + staking.withdrawUnlocked(), + [GasGroup.WITHDRAW_UNSTAKE] + ); + + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(staker.address, STAKE_AMOUNT); + + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + + const requestCount = await staking.getWithdrawalRequestsCount(staker.address); + expect(requestCount).to.equal(0n); + }); + + it("Does not change cSSV supply on withdrawal", async function () { + const { staking, cssvToken } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const supplyBefore = await cssvToken.totalSupply(); + await trackGas( + staking.withdrawUnlocked(), + [GasGroup.WITHDRAW_UNSTAKE] + ); + const supplyAfter = await cssvToken.totalSupply(); + + expect(supplyAfter).to.equal(supplyBefore); + }); + + it("Does not allow one user to withdraw another user's tokens", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + const [, otherUser] = await connection.ethers.getSigners(); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + await expect(staking.connect(otherUser).withdrawUnlocked()).to.be.revertedWithCustomError( + staking, + Errors.NOTHING_TO_WITHDRAW + ); + const balanceBefore = await ssvToken.balanceOf(staker.address); + await staking.withdrawUnlocked(); + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + }); + + describe("Cooldown duration changes and existing pending requests", () => { + it("Does not unlock an existing request earlier when cooldown is reduced after request creation", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + const [, originalUnlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + const reducedCooldown = 1n; + await staking.mockSetCooldownDuration(reducedCooldown); + + await networkHelpers.time.increase((DEFAULT_UNSTAKE_COOLDOWN / 2n) + 1n); + + await expect(staking.withdrawUnlocked()).to.be.revertedWithCustomError( + staking, + Errors.NOTHING_TO_WITHDRAW, + ); + + const [, unlockTimeAfterUpdate] = await staking.getWithdrawalRequest(staker.address, 0); + expect(unlockTimeAfterUpdate).to.equal(originalUnlockTime); + + await networkHelpers.time.increase((DEFAULT_UNSTAKE_COOLDOWN / 2n) + 1n); + const balanceBefore = await ssvToken.balanceOf(staker.address); + await staking.withdrawUnlocked(); + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + }); + + it("Uses the updated cooldown duration for new requests created after a cooldown change", async function () { + const { staking, ssvToken } = await ssvStakingHarnessFixture(connection); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + + const firstAmount = STAKE_AMOUNT / 4n; + const secondAmount = STAKE_AMOUNT / 4n; + + const firstTx = await staking.requestUnstake(firstAmount); + const firstReceipt = await firstTx.wait(); + const firstBlock = await connection.ethers.provider.getBlock(firstReceipt!.blockNumber); + const expectedFirstUnlockTime = BigInt(firstBlock!.timestamp) + DEFAULT_UNSTAKE_COOLDOWN; + + const increasedCooldown = DEFAULT_UNSTAKE_COOLDOWN * 2n; + await staking.mockSetCooldownDuration(increasedCooldown); + + const secondTx = await staking.requestUnstake(secondAmount); + const secondReceipt = await secondTx.wait(); + const secondBlock = await connection.ethers.provider.getBlock(secondReceipt!.blockNumber); + const expectedSecondUnlockTime = BigInt(secondBlock!.timestamp) + increasedCooldown; + + const [storedFirstAmount, firstUnlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + const [storedSecondAmount, secondUnlockTime] = await staking.getWithdrawalRequest(staker.address, 1); + + expect(storedFirstAmount).to.equal(firstAmount); + expect(firstUnlockTime).to.equal(expectedFirstUnlockTime); + expect(storedSecondAmount).to.equal(secondAmount); + expect(secondUnlockTime).to.equal(expectedSecondUnlockTime); + expect(secondUnlockTime).to.be.greaterThan(firstUnlockTime); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN + 1n); + + const balanceBeforeFirstWithdrawal = await ssvToken.balanceOf(staker.address); + await staking.withdrawUnlocked(); + const balanceAfterFirstWithdrawal = await ssvToken.balanceOf(staker.address); + + expect(balanceAfterFirstWithdrawal - balanceBeforeFirstWithdrawal).to.equal(firstAmount); + expect(await staking.getWithdrawalRequestsCount(staker.address)).to.equal(1n); + + const [remainingAmount, remainingUnlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + expect(remainingAmount).to.equal(secondAmount); + expect(remainingUnlockTime).to.equal(secondUnlockTime); + + await networkHelpers.time.increase(increasedCooldown + 1n); + + const balanceBeforeSecondWithdrawal = await ssvToken.balanceOf(staker.address); + await staking.withdrawUnlocked(); + const balanceAfterSecondWithdrawal = await ssvToken.balanceOf(staker.address); + + expect(balanceAfterSecondWithdrawal - balanceBeforeSecondWithdrawal).to.equal(secondAmount); + expect(await staking.getWithdrawalRequestsCount(staker.address)).to.equal(0n); + }); + + it("Keeps original unlock time for existing request when cooldown is increased after request creation", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(stakeAndRequestUnstake); + + const [, originalUnlockTime] = await staking.getWithdrawalRequest(staker.address, 0); + const increasedCooldown = DEFAULT_UNSTAKE_COOLDOWN * 2n; + await staking.mockSetCooldownDuration(increasedCooldown); + + const [, unlockTimeAfterUpdate] = await staking.getWithdrawalRequest(staker.address, 0); + expect(unlockTimeAfterUpdate).to.equal(originalUnlockTime); + + await networkHelpers.time.increase(DEFAULT_UNSTAKE_COOLDOWN); + const balanceBefore = await ssvToken.balanceOf(staker.address); + await staking.withdrawUnlocked(); + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + }); + }); +}); diff --git a/test/unit/SSVValidator/bug4-double-deviation-liquidated.test.ts b/test/unit/SSVValidator/bug4-double-deviation-liquidated.test.ts new file mode 100644 index 000000000..c8a1d486f --- /dev/null +++ b/test/unit/SSVValidator/bug4-double-deviation-liquidated.test.ts @@ -0,0 +1,183 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, makePublicKey, parseClusterFromEvent, computeEBRoot, computeClusterId } from "../../common/helpers.ts"; +import { DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; +const OPERATOR_FEE = 10_000_000_000n; + +describe("BUG-4: Double deviation cleanup on liquidated cluster validator removal", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let clusterOwner: HardhatEthersSigner; + let liquidator: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner, liquidator] } = await setupTestContext()); + }); + + const deployClustersWithFee = async () => { + return ssvClustersHarnessFixture(connection, 4, OPERATOR_FEE); + }; + + it("should not double-subtract deviation when removing all validators from a liquidated cluster with explicit EB", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + const networkFeeRate = 100_000n; + await clusters.mockEthNetworkFee(networkFeeRate); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + const pk1 = makePublicKey(1); + const pk2 = makePublicKey(2); + const pk3 = makePublicKey(3); + const depositValue = ethers.parseEther("0.0001"); + + const reg1 = await clusters.connect(clusterOwner).registerValidator( + pk1, operatorIds, DEFAULT_SHARES, createCluster(), { value: depositValue } + ); + const cluster1 = parseClusterFromEvent(clusters, await reg1.wait(), Events.VALIDATOR_ADDED); + + const reg2 = await clusters.connect(clusterOwner).registerValidator( + pk2, operatorIds, DEFAULT_SHARES, cluster1, { value: depositValue } + ); + const cluster2 = parseClusterFromEvent(clusters, await reg2.wait(), Events.VALIDATOR_ADDED); + + const reg3 = await clusters.connect(clusterOwner).registerValidator( + pk3, operatorIds, DEFAULT_SHARES, cluster2, { value: depositValue } + ); + const clusterAfterReg = parseClusterFromEvent(clusters, await reg3.wait(), Events.VALIDATOR_ADDED); + + expect(clusterAfterReg.validatorCount).to.equal(3n); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const effectiveBalance = 160; + const root1 = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, effectiveBalance, [] + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const expectedVUnits = (BigInt(effectiveBalance) * BPS_DENOMINATOR + 31n) / 32n; + const baselineVUnits = 3n * BPS_DENOMINATOR; + const deviation = expectedVUnits - baselineVUnits; + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedVUnits); + expect(deviation).to.be.gt(0n); + const opVUnitsBefore = await clusters.getOperatorEthVUnits(operatorIds[0]); + const daoVUnitsBefore = await clusters.getDaoTotalEthVUnits(); + expect(opVUnitsBefore).to.equal(deviation); + const root2 = computeEBRoot(clusterId, 2048); + await clusters.mockSetEBRoot(2, root2); + + const ebTx2 = await clusters.updateClusterBalance( + 2, clusterOwner.address, operatorIds, clusterAfterEB, 2048, [] + ); + const clusterAfterLiq = parseClusterFromEvent(clusters, await ebTx2.wait(), Events.CLUSTER_LIQUIDATED); + + expect(clusterAfterLiq.active).to.equal(false); + expect(clusterAfterLiq.balance).to.equal(0n); + expect(clusterAfterLiq.validatorCount).to.equal(3n); + const vUnitsAt2048 = (2048n * BPS_DENOMINATOR + 31n) / 32n; + const deviationAt2048 = vUnitsAt2048 - baselineVUnits; + const opVUnitsAfterLiq = await clusters.getOperatorEthVUnits(operatorIds[0]); + const daoVUnitsAfterLiq = await clusters.getDaoTotalEthVUnits(); + const removeTx = await clusters.connect(clusterOwner).bulkRemoveValidator( + [pk1, pk2, pk3], operatorIds, clusterAfterLiq + ); + await removeTx.wait(); + const opVUnitsAfterRemove = await clusters.getOperatorEthVUnits(operatorIds[0]); + const daoVUnitsAfterRemove = await clusters.getDaoTotalEthVUnits(); + + expect(opVUnitsAfterRemove).to.equal(opVUnitsAfterLiq, + "operatorEthVUnits should not change after removing validators from a liquidated cluster"); + expect(daoVUnitsAfterRemove).to.equal(daoVUnitsAfterLiq, + "daoTotalEthVUnits should not change after removing validators from a liquidated cluster"); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + }); + + it("should not double-subtract deviation when removing validators one-by-one from a liquidated cluster", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + + await clusters.mockEthNetworkFee(100_000n); + await clusters.mockMinimumBlocksBeforeLiquidation(100n); + await clusters.mockMinimumLiquidationCollateral(0n); + + const pk1 = makePublicKey(1); + const pk2 = makePublicKey(2); + const depositValue = ethers.parseEther("0.0001"); + + const reg1 = await clusters.connect(clusterOwner).registerValidator( + pk1, operatorIds, DEFAULT_SHARES, createCluster(), { value: depositValue } + ); + const cluster1 = parseClusterFromEvent(clusters, await reg1.wait(), Events.VALIDATOR_ADDED); + + const reg2 = await clusters.connect(clusterOwner).registerValidator( + pk2, operatorIds, DEFAULT_SHARES, cluster1, { value: depositValue } + ); + const clusterAfterReg = parseClusterFromEvent(clusters, await reg2.wait(), Events.VALIDATOR_ADDED); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root1 = computeEBRoot(clusterId, 96); + await clusters.mockSetEBRoot(1, root1); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 96, [] + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + const root2 = computeEBRoot(clusterId, 2048); + await clusters.mockSetEBRoot(2, root2); + + const ebTx2 = await clusters.updateClusterBalance( + 2, clusterOwner.address, operatorIds, clusterAfterEB, 2048, [] + ); + const clusterAfterLiq = parseClusterFromEvent(clusters, await ebTx2.wait(), Events.CLUSTER_LIQUIDATED); + expect(clusterAfterLiq.active).to.equal(false); + + const opVUnitsAfterLiq = await clusters.getOperatorEthVUnits(operatorIds[0]); + const daoVUnitsAfterLiq = await clusters.getDaoTotalEthVUnits(); + const remove1 = await clusters.connect(clusterOwner).removeValidator(pk1, operatorIds, clusterAfterLiq); + const clusterAfterRemove1 = parseClusterFromEvent(clusters, await remove1.wait(), Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove1.validatorCount).to.equal(1n); + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(opVUnitsAfterLiq); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(daoVUnitsAfterLiq); + const remove2 = await clusters.connect(clusterOwner).removeValidator(pk2, operatorIds, clusterAfterRemove1); + await remove2.wait(); + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(opVUnitsAfterLiq, + "operatorEthVUnits should not change when removing last validator from liquidated cluster"); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(daoVUnitsAfterLiq, + "daoTotalEthVUnits should not change when removing last validator from liquidated cluster"); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + }); + + it("should still correctly clean up deviation when removing validators from an ACTIVE cluster", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersWithFee); + + const pk1 = makePublicKey(1); + const depositValue = ethers.parseEther("10"); + + const reg1 = await clusters.connect(clusterOwner).registerValidator( + pk1, operatorIds, DEFAULT_SHARES, createCluster(), { value: depositValue } + ); + const clusterAfterReg = parseClusterFromEvent(clusters, await reg1.wait(), Events.VALIDATOR_ADDED); + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const root = computeEBRoot(clusterId, 96); + await clusters.mockSetEBRoot(1, root); + + const ebTx = await clusters.updateClusterBalance( + 1, clusterOwner.address, operatorIds, clusterAfterReg, 96, [] + ); + const clusterAfterEB = parseClusterFromEvent(clusters, await ebTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + const expectedVUnits = (96n * BPS_DENOMINATOR + 31n) / 32n; + const deviation = expectedVUnits - BPS_DENOMINATOR; + + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(deviation); + const removeTx = await clusters.connect(clusterOwner).removeValidator(pk1, operatorIds, clusterAfterEB); + await removeTx.wait(); + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(0n, + "operatorEthVUnits should be zeroed after removing last validator from active cluster"); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + }); +}); diff --git a/test/unit/SSVValidator/bulkExitValidator.test.ts b/test/unit/SSVValidator/bulkExitValidator.test.ts new file mode 100644 index 000000000..6310dcc3d --- /dev/null +++ b/test/unit/SSVValidator/bulkExitValidator.test.ts @@ -0,0 +1,211 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { getValidatorsHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultValidatorsFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, makePublicKey, makePublicKeys, computeClusterId } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVClusters function `bulkExitValidator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let deployClustersWith7Operators!: ReturnType; + let deployClustersWith10Operators!: ReturnType; + let deployClustersWith13Operators!: ReturnType; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + + deployClustersWith7Operators = getValidatorsHarnessFixture(connection, 7); + deployClustersWith10Operators = getValidatorsHarnessFixture(connection, 10); + deployClustersWith13Operators = getValidatorsHarnessFixture(connection, 13); + }); + + const deploySSVValidatorsAndPrepareOperatorsFixture = async () => { + return defaultValidatorsFixture(connection); + }; + + it("Exits multiple validators and emits events", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const tx = await validators.bulkExitValidator(publicKeys, operatorIds); + + await expect(tx).to.emit(validators, Events.VALIDATOR_EXITED).withArgs(clusterOwner.address, operatorIds, publicKeys[0]); + await expect(tx).to.emit(validators, Events.VALIDATOR_EXITED).withArgs(clusterOwner.address, operatorIds, publicKeys[1]); + }); + + it("Does not change operatorEthVUnits or stored cluster EB snapshot when bulk exiting", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await validators.mockSetClusterVUnits(clusterId, 9n * BPS_DENOMINATOR); + + const beforeClusterVUnits = await validators.getClusterVUnits(clusterId); + const beforeOperatorVUnits = await Promise.all(operatorIds.map((id) => validators.getOperatorEthVUnits(id))); + + await validators.bulkExitValidator(publicKeys, operatorIds); + + const afterClusterVUnits = await validators.getClusterVUnits(clusterId); + const afterOperatorVUnits = await Promise.all(operatorIds.map((id) => validators.getOperatorEthVUnits(id))); + + expect(afterClusterVUnits).to.equal(beforeClusterVUnits); + expect(afterOperatorVUnits).to.deep.equal(beforeOperatorVUnits); + }); + + it("Exits 10 validators with 4 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const tx = await validators.bulkExitValidator(publicKeys, operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_EXIT_10_VALIDATOR_4]); + }); + + it("Exits 10 validators with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const tx = await validators.bulkExitValidator(publicKeys, operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_EXIT_10_VALIDATOR_7]); + }); + + it("Exits 10 validators with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const tx = await validators.bulkExitValidator(publicKeys, operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_EXIT_10_VALIDATOR_10]); + }); + + it("Exits 10 validators with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const tx = await validators.bulkExitValidator(publicKeys, operatorIds); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_EXIT_10_VALIDATOR_13]); + }); + + it("Is reverted with 'ValidatorDoesNotExist' when no public keys are provided", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await expect(validators.bulkExitValidator( + [], + operatorIds + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'ValidatorDoesNotExist' when any validator is not registered", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + await validators.registerValidator( + publicKeys[0], + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await expect(validators.bulkExitValidator( + publicKeys, + operatorIds + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectValidatorStateWithData' when operator ids do not match stored validators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const mismatchedOperatorIds = [...operatorIds]; + mismatchedOperatorIds[0] = mismatchedOperatorIds[0] + 1n; + + await expect(validators.bulkExitValidator( + publicKeys, + mismatchedOperatorIds + )).to.be.revertedWithCustomError(validators, Errors.INCORRECT_VALIDATOR_STATE_WITH_DATA).withArgs(publicKeys[0]); + }); +}); diff --git a/test/unit/SSVValidator/bulkRegisterValidator.test.ts b/test/unit/SSVValidator/bulkRegisterValidator.test.ts new file mode 100644 index 000000000..e0d47b5bb --- /dev/null +++ b/test/unit/SSVValidator/bulkRegisterValidator.test.ts @@ -0,0 +1,457 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { getValidatorsHarnessFixture } from '../../setup/fixtures.ts'; +import { defaultValidatorsFixture } from '../../helpers/fixture-presets.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import { setupTestContext, createCluster, makePublicKey, makePublicKeys, parseClusterFromEvent, computeClusterId } from '../../common/helpers.ts'; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import { Errors } from '../../common/errors.ts'; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVClusters function `bulkRegisterValidator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let deployClustersWith7Operators!: ReturnType; + let deployClustersWith10Operators!: ReturnType; + let deployClustersWith13Operators!: ReturnType; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + + deployClustersWith7Operators = getValidatorsHarnessFixture(connection, 7); + deployClustersWith10Operators = getValidatorsHarnessFixture(connection, 10); + deployClustersWith13Operators = getValidatorsHarnessFixture(connection, 13); + }); + + const deploySSVValidatorsAndPrepareOperatorsFixture = async () => { + return defaultValidatorsFixture(connection); + }; + + it("Registers multiple validators, creates new cluster with the expected data and emits correct events", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + const shares = [DEFAULT_SHARES, DEFAULT_SHARES]; + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const expectedCluster = [ + 2n, + 0n, + 0n, + true, + DEFAULT_ETH_REGISTER_VALUE, + ]; + + await expect(tx) + .to.emit(validators, Events.VALIDATOR_ADDED) + .withArgs( + clusterOwner.address, + operatorIds, + publicKeys[0], + shares[0], + expectedCluster + ); + }); + + it("Updates operatorEthVUnits even when cluster EB snapshot is not set", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + const shares = [DEFAULT_SHARES, DEFAULT_SHARES]; + + const tx = await validators.connect(clusterOwner).bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await tx.wait(); + + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(2n * BPS_DENOMINATOR); + } + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await validators.getClusterVUnits(clusterId)).to.equal(0n); + }); + + it("Increments stored EB snapshot vUnits when cluster EB snapshot is set", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const registerTx = await validators.connect(clusterOwner).registerValidator( + makePublicKey(100), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const existingCluster = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const startVUnits = 5n * BPS_DENOMINATOR; + await validators.mockSetClusterVUnits(clusterId, startVUnits); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + const shares = [DEFAULT_SHARES, DEFAULT_SHARES]; + + const tx = await validators.connect(clusterOwner).bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await tx.wait(); + + expect(await validators.getClusterVUnits(clusterId)).to.equal(startVUnits + 2n * BPS_DENOMINATOR); + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(3n * BPS_DENOMINATOR); + } + }); + + it("Registers 10 validators into a new cluster with 4 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_4]); + }); + + it("Registers 10 validators into an existing cluster with 4 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const registerTx = await validators.registerValidator( + makePublicKey(100), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const existingCluster = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const publicKeys = makePublicKeys(10, 1); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4]); + }); + + it("Registers 10 validators into a new cluster with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_7]); + }); + + it("Registers 10 validators into an existing cluster with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(100), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const existingCluster = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const publicKeys = makePublicKeys(10, 1); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_7]); + }); + + it("Registers 10 validators into a new cluster with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_10]); + }); + + it("Registers 10 validators into an existing cluster with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(100), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const existingCluster = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const publicKeys = makePublicKeys(10, 1); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_10]); + }); + + it("Registers 10 validators into a new cluster with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_13]); + }); + + it("Registers 10 validators into an existing cluster with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(100), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const existingCluster = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const publicKeys = makePublicKeys(10, 1); + const shares = Array(10).fill(DEFAULT_SHARES); + + const tx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + existingCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_13]); + }); + + it("Is reverted with 'EmptyPublicKeysList' when no public keys are provided", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await expect(validators.bulkRegisterValidator( + [], + operatorIds, + [], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.EMPTY_PUBLIC_KEYS_LIST); + }); + + it("Is reverted with 'InvalidPublicKeyLength' when any public key is empty or has invalid length", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const emptyPublicKey = "0x"; + const invalidLengthPublicKey = makePublicKey(1) + "11"; + + await expect(validators.bulkRegisterValidator( + [emptyPublicKey], + operatorIds, + [DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.INVALID_PUBLIC_KEYS_LENGTH); + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), invalidLengthPublicKey], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'PublicKeysSharesLengthMismatch' if there is a mismatch between public keys and shares", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.PUBLIC_KEYS_SHARES_LENGTH_MISMATCH); + }); + + it("Is reverted with 'ValidatorAlreadyRegistered' if trying to register already existing key", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + await expect(validators.bulkRegisterValidator( + [publicKey, publicKey], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_ALREADY_REGISTERED).withArgs(publicKey, clusterOwner.address); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the length is not allowed one for clusters", async function () { + const { validators } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + const operatorIds = [2n, 1n, 2n]; + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'UnsortedOperatorsList' if the list of operator ids is not sorted", async function () { + const { validators } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + const operatorIds = [4n, 3n, 2n, 1n]; + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the list of operator ids has duplications", async function () { + const { validators } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + const operatorIds = [1n, 1n, 2n, 4n]; + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'ClusterIsLiquidated' when trying to register to a liquidated cluster", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + await validators.mockSetClusterLiquidated(clusterOwner.address, operatorIds); + + const liquidatedCluster = createCluster({ active: false }); + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.CLUSTER_IS_LIQUIDATED); + }); + + it("Is reverted with 'OperatorDoesNotExist' when one of the operators has been removed", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await validators.mockRemoveOperator(operatorIds[2]); + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'OperatorDoesNotExist' when multiple operators have been removed", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await validators.mockRemoveOperator(operatorIds[1]); + await validators.mockRemoveOperator(operatorIds[3]); + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1), makePublicKey(2)], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATOR_DOES_NOT_EXIST); + }); +}); diff --git a/test/unit/SSVValidator/bulkRemoveValidator.test.ts b/test/unit/SSVValidator/bulkRemoveValidator.test.ts new file mode 100644 index 000000000..36e2b3efe --- /dev/null +++ b/test/unit/SSVValidator/bulkRemoveValidator.test.ts @@ -0,0 +1,434 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { getValidatorsHarnessFixture, ssvClustersHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultValidatorsFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, makePublicKey, makePublicKeys, parseClusterFromEvent, computeClusterId } from "../../common/helpers.ts"; +import { createLegacySSVCluster } from "../../helpers/cluster.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVClusters function `bulkRemoveValidator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let deployClustersWith7Operators!: ReturnType; + let deployClustersWith10Operators!: ReturnType; + let deployClustersWith13Operators!: ReturnType; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + + deployClustersWith7Operators = getValidatorsHarnessFixture(connection, 7); + deployClustersWith10Operators = getValidatorsHarnessFixture(connection, 10); + deployClustersWith13Operators = getValidatorsHarnessFixture(connection, 13); + }); + + const deploySSVValidatorsAndPrepareOperatorsFixture = async () => { + return defaultValidatorsFixture(connection); + }; + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return ssvClustersHarnessFixture(connection); + }; + + it("Removes multiple validators, updates cluster state and emits correct events", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + + const registerTx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.bulkRemoveValidator(publicKeys, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(validators, removeReceipt, Events.VALIDATOR_REMOVED); + + await expect(removeTx).to.emit(validators, Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + }); + + it("Updates operatorEthVUnits on bulk register/remove even when cluster EB snapshot is not set", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + + const registerTx = await validators.connect(clusterOwner).bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(2n * BPS_DENOMINATOR); + } + + const removeTx = await validators.connect(clusterOwner).bulkRemoveValidator(publicKeys, operatorIds, clusterAfterRegister); + await removeTx.wait(); + + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(0n); + } + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await validators.getClusterVUnits(clusterId)).to.equal(0n); + }); + + it("Decrements stored EB snapshot vUnits when set and removing a subset of validators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2), makePublicKey(3)]; + + const registerTx = await validators.connect(clusterOwner).bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await validators.mockSetClusterVUnits(clusterId, 3n * BPS_DENOMINATOR); + + const removeTx = await validators.connect(clusterOwner).bulkRemoveValidator( + [publicKeys[0], publicKeys[1]], + operatorIds, + clusterAfterRegister + ); + await removeTx.wait(); + + expect(await validators.getClusterVUnits(clusterId)).to.equal(1n * BPS_DENOMINATOR); + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(1n * BPS_DENOMINATOR); + } + }); + + it("Clears stored EB snapshot vUnits when removing the last validators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + + const registerTx = await validators.connect(clusterOwner).bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await validators.mockSetClusterVUnits(clusterId, 2n * BPS_DENOMINATOR); + + const removeTx = await validators.connect(clusterOwner).bulkRemoveValidator(publicKeys, operatorIds, clusterAfterRegister); + await removeTx.wait(); + + expect(await validators.getClusterVUnits(clusterId)).to.equal(0n); + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + }); + + it("Removes 10 validators with 4 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const registerTx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.bulkRemoveValidator(publicKeys, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + await trackGasFromReceipt(removeReceipt, [GasGroup.BULK_REMOVE_10_VALIDATOR_4]); + }); + + it("Removes 10 validators with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const registerTx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.bulkRemoveValidator(publicKeys, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + await trackGasFromReceipt(removeReceipt, [GasGroup.BULK_REMOVE_10_VALIDATOR_7]); + }); + + it("Removes 10 validators with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const registerTx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.bulkRemoveValidator(publicKeys, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + await trackGasFromReceipt(removeReceipt, [GasGroup.BULK_REMOVE_10_VALIDATOR_10]); + }); + + it("Removes 10 validators with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const registerTx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.bulkRemoveValidator(publicKeys, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + await trackGasFromReceipt(removeReceipt, [GasGroup.BULK_REMOVE_10_VALIDATOR_13]); + }); + + it("Is reverted with 'ValidatorDoesNotExist' when no public keys are provided", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await expect(validators.bulkRemoveValidator( + [], + operatorIds, + createCluster() + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'ValidatorDoesNotExist' when trying to remove non-existent validators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + const registerTx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const missingKey = makePublicKey(2); + await expect(validators.bulkRemoveValidator( + [missingKey], + operatorIds, + clusterAfterRegister + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' when provided cluster data is stale or mismatched", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2)]; + const registerTx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const mismatchedCluster = { + ...clusterAfterRegister, + balance: clusterAfterRegister.balance + 1n, + }; + + await expect(validators.bulkRemoveValidator( + publicKeys, + operatorIds, + mismatchedCluster + )).to.be.revertedWithCustomError(validators, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterDoesNotExists' when attempting to remove from a missing cluster", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await expect(validators.bulkRemoveValidator( + [makePublicKey(1)], + operatorIds, + createCluster() + )).to.be.revertedWithCustomError(validators, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Bulk removes multiple validators from active legacy SSV cluster and decrements operator/DAO counts by N", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const publicKeys = [makePublicKey(1), makePublicKey(2), makePublicKey(3)]; + const ssvCluster = createLegacySSVCluster({ validatorCount: 3n }); + + await clusters.mockRegisterSSVValidator(publicKeys[0], operatorIds, clusterOwner.address, ssvCluster); + await clusters.mockRegisterSSVValidator(publicKeys[1], operatorIds, clusterOwner.address, ssvCluster); + await clusters.mockRegisterSSVValidator(publicKeys[2], operatorIds, clusterOwner.address, ssvCluster); + + const operatorCountBefore = await clusters.getOperatorValidatorCount(operatorIds[0]); + const daoCountBefore = await clusters.getDaoValidatorCount(); + + const removeTx = await clusters.connect(clusterOwner).bulkRemoveValidator( + [publicKeys[0], publicKeys[1]], + operatorIds, + ssvCluster + ); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(1n); + expect(clusterAfterRemove.active).to.equal(true); + const removedEvents = (removeReceipt.logs ?? []).filter((log: any) => { + try { + return clusters.interface.parseLog(log)?.name === Events.VALIDATOR_REMOVED; + } catch { + return false; + } + }); + expect(removedEvents.length).to.equal(2); + const operatorCountAfter = await clusters.getOperatorValidatorCount(operatorIds[0]); + const daoCountAfter = await clusters.getDaoValidatorCount(); + expect(operatorCountAfter).to.equal(operatorCountBefore - 2n); + expect(daoCountAfter).to.equal(daoCountBefore - 2n); + }); + + it("Reverts bulk removal atomically when one validator in batch is invalid", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const key1 = makePublicKey(1); + const key2 = makePublicKey(2); + const missingKey = makePublicKey(3); + + const registerTx = await validators.bulkRegisterValidator( + [key1, key2], + operatorIds, + [DEFAULT_SHARES, DEFAULT_SHARES], + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const operatorCountBefore = await validators.getOperatorEthValidatorCount(operatorIds[0]); + const daoCountBefore = await validators.getDaoEthValidatorCount(); + + await expect( + validators.bulkRemoveValidator([key1, missingKey, key2], operatorIds, clusterAfterRegister) + ) + .to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + + expect(await validators.getOperatorEthValidatorCount(operatorIds[0])).to.equal(operatorCountBefore); + expect(await validators.getDaoEthValidatorCount()).to.equal(daoCountBefore); + expect(await validators.getValidatorData(key1, clusterOwner.address)).to.not.equal(ethers.ZeroHash); + expect(await validators.getValidatorData(key2, clusterOwner.address)).to.not.equal(ethers.ZeroHash); + }); + + it("Reverts SSV bulk removal atomically when one validator in batch is invalid", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const key1 = makePublicKey(11); + const key2 = makePublicKey(12); + const missingKey = makePublicKey(13); + const ssvCluster = createLegacySSVCluster({ validatorCount: 2n }); + + await clusters.mockRegisterSSVValidator(key1, operatorIds, clusterOwner.address, ssvCluster); + await clusters.mockRegisterSSVValidator(key2, operatorIds, clusterOwner.address, ssvCluster); + + const operatorCountBefore = await clusters.getOperatorValidatorCount(operatorIds[0]); + const daoCountBefore = await clusters.getDaoValidatorCount(); + + await expect( + clusters.connect(clusterOwner).bulkRemoveValidator([key1, missingKey, key2], operatorIds, ssvCluster) + ) + .to.be.revertedWithCustomError(clusters, Errors.VALIDATOR_DOES_NOT_EXIST); + + expect(await clusters.getOperatorValidatorCount(operatorIds[0])).to.equal(operatorCountBefore); + expect(await clusters.getDaoValidatorCount()).to.equal(daoCountBefore); + expect(await clusters.getValidatorData(key1, clusterOwner.address)).to.not.equal(ethers.ZeroHash); + expect(await clusters.getValidatorData(key2, clusterOwner.address)).to.not.equal(ethers.ZeroHash); + }); + + it("Keeps reactivate blocked for SSV clusters after bulk removing to zero validators", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const key = makePublicKey(21); + const ssvCluster = createLegacySSVCluster({ validatorCount: 1n }); + await clusters.mockRegisterSSVValidator(key, operatorIds, clusterOwner.address, ssvCluster); + + const removeTx = await clusters.connect(clusterOwner).bulkRemoveValidator([key], operatorIds, ssvCluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + + await expect( + clusters.connect(clusterOwner).reactivate(operatorIds, clusterAfterRemove, { value: DEFAULT_ETH_REGISTER_VALUE }) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_VERSION); + }); +}); diff --git a/test/unit/SSVValidator/exitValidator.test.ts b/test/unit/SSVValidator/exitValidator.test.ts new file mode 100644 index 000000000..59d4cfbc6 --- /dev/null +++ b/test/unit/SSVValidator/exitValidator.test.ts @@ -0,0 +1,145 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultValidatorsFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, makePublicKey, computeClusterId } from "../../common/helpers.ts"; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVClusters function `exitValidator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + }); + + const deploySSVValidatorsAndPrepareOperatorsFixture = async () => { + return defaultValidatorsFixture(connection); + }; + + it("Exits an existing validator and emits the correct event", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const tx = await validators.exitValidator( + publicKey, + operatorIds + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.VALIDATOR_EXIT]); + + await expect(tx).to.emit(validators, Events.VALIDATOR_EXITED).withArgs(clusterOwner.address, operatorIds, publicKey); + }); + + it("Does not change operatorEthVUnits or stored cluster EB snapshot when exiting", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await validators.mockSetClusterVUnits(clusterId, 7n * BPS_DENOMINATOR); + + const beforeClusterVUnits = await validators.getClusterVUnits(clusterId); + const beforeOperatorVUnits = await Promise.all(operatorIds.map((id) => validators.getOperatorEthVUnits(id))); + + await validators.exitValidator(publicKey, operatorIds); + + const afterClusterVUnits = await validators.getClusterVUnits(clusterId); + const afterOperatorVUnits = await Promise.all(operatorIds.map((id) => validators.getOperatorEthVUnits(id))); + + expect(afterClusterVUnits).to.equal(beforeClusterVUnits); + expect(afterOperatorVUnits).to.deep.equal(beforeOperatorVUnits); + }); + + it("Is reverted with 'IncorrectValidatorStateWithData' when validator was not registered", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const missingPk = makePublicKey(1); + + await expect(validators.exitValidator( + missingPk, + operatorIds + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Calling exitValidator twice on the same validator succeeds both times without reverting", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + await validators.exitValidator(publicKey, operatorIds); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const validatorDataBeforeSecondExit = await validators.getValidatorData(publicKey, clusterOwner.address); + const clusterVUnitsBeforeSecondExit = await validators.getClusterVUnits(clusterId); + const operatorVUnitsBeforeSecondExit = await Promise.all(operatorIds.map((id) => validators.getOperatorEthVUnits(id))); + + const tx = await validators.exitValidator(publicKey, operatorIds); + await expect(tx).to.emit(validators, Events.VALIDATOR_EXITED).withArgs(clusterOwner.address, operatorIds, publicKey); + + const validatorDataAfterSecondExit = await validators.getValidatorData(publicKey, clusterOwner.address); + const clusterVUnitsAfterSecondExit = await validators.getClusterVUnits(clusterId); + const operatorVUnitsAfterSecondExit = await Promise.all(operatorIds.map((id) => validators.getOperatorEthVUnits(id))); + + expect(validatorDataAfterSecondExit).to.equal(validatorDataBeforeSecondExit); + expect(clusterVUnitsAfterSecondExit).to.equal(clusterVUnitsBeforeSecondExit); + expect(operatorVUnitsAfterSecondExit).to.deep.equal(operatorVUnitsBeforeSecondExit); + }); + + it("Is reverted with 'IncorrectValidatorStateWithData' when operator ids do not match the validator", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const mismatchedOperatorIds = [...operatorIds]; + mismatchedOperatorIds[0] = mismatchedOperatorIds[0] + 1n; + + await expect(validators.exitValidator( + publicKey, + mismatchedOperatorIds + )).to.be.revertedWithCustomError(validators, Errors.INCORRECT_VALIDATOR_STATE_WITH_DATA).withArgs(publicKey); + }); +}); diff --git a/test/unit/SSVValidator/feeSettlement.test.ts b/test/unit/SSVValidator/feeSettlement.test.ts new file mode 100644 index 000000000..3bcf9cd43 --- /dev/null +++ b/test/unit/SSVValidator/feeSettlement.test.ts @@ -0,0 +1,218 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { defaultValidatorsFixture } from "../../helpers/fixture-presets.ts"; +import { deployHarnessModule } from "../../setup/deploy.ts"; +import { SSVModules } from "../../common/types.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, makePublicKey, makePublicKeys, makeOperatorKey, createCluster, parseClusterFromEvent } from "../../common/helpers.ts"; +import { + DEFAULT_SHARES, + ETH_DEDUCTED_DIGITS, +} from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { ethers } from "ethers"; + +const OPERATOR_FEE = 10_000_000_000n; +const DIFFERENT_FEES = [2_000_000_000n, 4_000_000_000n, 6_000_000_000n, 8_000_000_000n]; + +describe("Validator register/remove with non-zero ETH operator fees", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployValidatorsWithFee = async () => { + return defaultValidatorsFixture(connection, 4, OPERATOR_FEE); + }; + + const deployValidatorsWithDifferentFees = async () => { + const validators = await deployHarnessModule(connection, SSVModules.SSVValidators); + await validators.waitForDeployment(); + await validators.mockValidatorsPerOperatorLimit(3000); + + const [owner] = await connection.ethers.getSigners(); + const operatorIds: bigint[] = []; + + for (let i = 0; i < DIFFERENT_FEES.length; i++) { + const id = await validators.mockOperator.staticCall( + makeOperatorKey(i), owner.address, DIFFERENT_FEES[i], false + ); + await validators.mockOperator(makeOperatorKey(i), owner.address, DIFFERENT_FEES[i], false); + operatorIds.push(id); + } + + return { validators, operatorIds }; + }; + + it("registers with 4 operators at different fees and deducts sum(fees) * blocksDelta from cluster balance", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deployValidatorsWithDifferentFees); + + const depositValue = ethers.parseEther("100"); + const regTx1 = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue } + ); + const receipt1 = await regTx1.wait(); + const cluster1 = parseClusterFromEvent(validators, receipt1, Events.VALIDATOR_ADDED); + + const blockBeforeMine = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(50); + const blocksMined = (await connection.ethers.provider.getBlockNumber()) - blockBeforeMine; + + const regTx2 = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster1, + { value: 0n } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(validators, receipt2, Events.VALIDATOR_ADDED); + + const feeDeducted = cluster1.balance - cluster2.balance; + const sumPackedFees = DIFFERENT_FEES.reduce((acc, fee) => acc + fee / ETH_DEDUCTED_DIGITS, 0n); + const blocksDelta = BigInt(blocksMined + 1); + const expected = sumPackedFees * blocksDelta * 1n * ETH_DEDUCTED_DIGITS; + + expect(feeDeducted).to.equal(expected); + }); + + it("second registration after N blocks settles val1 fees; burn rate doubles when val2 is active", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deployValidatorsWithFee); + + const depositValue = ethers.parseEther("100"); + const regTx1 = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue } + ); + const receipt1 = await regTx1.wait(); + const cluster1 = parseClusterFromEvent(validators, receipt1, Events.VALIDATOR_ADDED); + + const blockBefore1 = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(100); + const blocksMined1 = (await connection.ethers.provider.getBlockNumber()) - blockBefore1; + + const regTx2 = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + cluster1, + { value: 0n } + ); + const receipt2 = await regTx2.wait(); + const cluster2 = parseClusterFromEvent(validators, receipt2, Events.VALIDATOR_ADDED); + + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const blocksDelta1 = BigInt(blocksMined1 + 1); + const expectedFee1 = 4n * packedFee * blocksDelta1 * ETH_DEDUCTED_DIGITS; + + expect(cluster1.balance - cluster2.balance).to.equal(expectedFee1); + + const blockBefore2 = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(100); + const blocksMined2 = (await connection.ethers.provider.getBlockNumber()) - blockBefore2; + + const regTx3 = await validators.registerValidator( + makePublicKey(3), + operatorIds, + DEFAULT_SHARES, + cluster2, + { value: 0n } + ); + const receipt3 = await regTx3.wait(); + const cluster3 = parseClusterFromEvent(validators, receipt3, Events.VALIDATOR_ADDED); + + const blocksDelta2 = BigInt(blocksMined2 + 1); + const expectedFee2 = 4n * packedFee * blocksDelta2 * 2n * ETH_DEDUCTED_DIGITS; + + expect(cluster2.balance - cluster3.balance).to.equal(expectedFee2); + }); + + it("removeValidator settles accumulated fees and operator snapshot balance matches expected earnings", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deployValidatorsWithFee); + + const depositValue = ethers.parseEther("100"); + const regTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: depositValue } + ); + const receipt = await regTx.wait(); + const clusterAfterReg = parseClusterFromEvent(validators, receipt, Events.VALIDATOR_ADDED); + + const blockBeforeMine = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(100); + const blocksMined = (await connection.ethers.provider.getBlockNumber()) - blockBeforeMine; + + const removeTx = await validators.removeValidator( + makePublicKey(1), + operatorIds, + clusterAfterReg + ); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(validators, removeReceipt, Events.VALIDATOR_REMOVED); + + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const blocksDelta = BigInt(blocksMined + 1); + const expectedFee = 4n * packedFee * blocksDelta * 1n * ETH_DEDUCTED_DIGITS; + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterReg.balance - clusterAfterRemove.balance).to.equal(expectedFee); + + for (const operatorId of operatorIds) { + const [, , balance] = await validators.getOperatorEthSnapshot(operatorId); + expect(balance).to.equal(packedFee * blocksDelta); + } + }); + + it("bulkRegisterValidator deducts fees proportional to bulk validator count", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deployValidatorsWithFee); + + const publicKeys = makePublicKeys(10); + const shares = Array(10).fill(DEFAULT_SHARES); + + const depositValue = ethers.parseEther("100"); + const bulkTx = await validators.bulkRegisterValidator( + publicKeys, + operatorIds, + shares, + createCluster(), + { value: depositValue } + ); + const bulkReceipt = await bulkTx.wait(); + const clusterAfterBulk = parseClusterFromEvent(validators, bulkReceipt, Events.VALIDATOR_ADDED); + + expect(clusterAfterBulk.validatorCount).to.equal(10n); + + const blockBeforeMine = await connection.ethers.provider.getBlockNumber(); + await networkHelpers.mine(50); + const blocksMined = (await connection.ethers.provider.getBlockNumber()) - blockBeforeMine; + + const settleTx = await validators.registerValidator( + makePublicKey(11), + operatorIds, + DEFAULT_SHARES, + clusterAfterBulk, + { value: 0n } + ); + const settleReceipt = await settleTx.wait(); + const clusterAfterSettle = parseClusterFromEvent(validators, settleReceipt, Events.VALIDATOR_ADDED); + + const packedFee = OPERATOR_FEE / ETH_DEDUCTED_DIGITS; + const blocksDelta = BigInt(blocksMined + 1); + const expectedFee = 4n * packedFee * blocksDelta * 10n * ETH_DEDUCTED_DIGITS; + + expect(clusterAfterBulk.balance - clusterAfterSettle.balance).to.equal(expectedFee); + }); +}); diff --git a/test/unit/SSVValidator/registerValidator.test.ts b/test/unit/SSVValidator/registerValidator.test.ts new file mode 100644 index 000000000..44440df35 --- /dev/null +++ b/test/unit/SSVValidator/registerValidator.test.ts @@ -0,0 +1,655 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import { getValidatorsHarnessFixture } from '../../setup/fixtures.ts'; +import { defaultValidatorsFixture } from '../../helpers/fixture-presets.ts'; +import type { NetworkHelpersType } from '../../common/types.ts'; +import { setupTestContext, makePublicKey, makePublicKeys, createCluster, parseClusterFromEvent, computeClusterId } from '../../common/helpers.ts'; +import { DEFAULT_ETH_REGISTER_VALUE, DEFAULT_OPERATOR_ETH_FEE, DEFAULT_SHARES, EMPTY_CLUSTER, ETH_DEDUCTED_DIGITS, BPS_DENOMINATOR } from '../../common/constants.ts'; +import { Events } from '../../common/events.ts'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/types'; +import { Errors } from '../../common/errors.ts'; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; + +describe("SSVClusters function `registerValidator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let deployClustersWith7Operators!: ReturnType; + let deployClustersWith10Operators!: ReturnType; + let deployClustersWith13Operators!: ReturnType; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + + deployClustersWith7Operators = getValidatorsHarnessFixture(connection, 7); + deployClustersWith10Operators = getValidatorsHarnessFixture(connection, 10); + deployClustersWith13Operators = getValidatorsHarnessFixture(connection, 13); + }); + + const deploySSVValidatorsAndPrepareOperatorsFixture = async () => { + return defaultValidatorsFixture(connection); + }; + + it("Registers a new validator, creates new cluster with the expected data and emits correct events", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + const tx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const expectedCluster = [ + 1n, + 0n, + 0n, + true, + DEFAULT_ETH_REGISTER_VALUE, + ]; + + await expect(tx) + .to.emit(validators, Events.VALIDATOR_ADDED) + .withArgs( + clusterOwner.address, + operatorIds, + publicKey, + DEFAULT_SHARES, + expectedCluster + ); + }); + + it("Initializes ETH defaults for legacy SSV operators and keeps them after registration", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + for (const operatorId of operatorIds) { + await validators.mockSetOperatorLegacySSV(operatorId, 1); + + const beforeSnapshot = await validators.getOperatorEthSnapshot(operatorId); + const beforeFee = await validators.getOperatorEthFee(operatorId); + const beforeValidatorCount = await validators.getOperatorEthValidatorCount(operatorId); + expect(beforeSnapshot.blockNumber).to.equal(0n); + expect(beforeSnapshot.index).to.equal(0n); + expect(beforeSnapshot.balance).to.equal(0n); + expect(beforeFee).to.equal(0n); + expect(beforeValidatorCount).to.equal(0n); + } + + const tx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + const expectedBlock = BigInt(receipt!.blockNumber); + + for (const operatorId of operatorIds) { + await expect(tx).to.emit(validators, Events.OPERATOR_FEE_EXECUTED) + .withArgs(clusterOwner.address, operatorId, expectedBlock, DEFAULT_OPERATOR_ETH_FEE); + + const afterSnapshot = await validators.getOperatorEthSnapshot(operatorId); + const afterFee = await validators.getOperatorEthFee(operatorId); + const afterValidatorCount = await validators.getOperatorEthValidatorCount(operatorId); + expect(afterSnapshot.blockNumber).to.equal(expectedBlock); + expect(afterSnapshot.index).to.equal(0n); + expect(afterSnapshot.balance).to.equal(0n); + expect(afterFee).to.equal(DEFAULT_OPERATOR_ETH_FEE / ETH_DEDUCTED_DIGITS); + expect(afterValidatorCount).to.equal(1n); + } + }); + + it("Legacy SSV operators with zero SSV fee initialize ETH snapshot but keep ethFee=0 on registration", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + for (const operatorId of operatorIds) { + await validators.mockSetOperatorLegacySSV(operatorId, 0); + const beforeSnapshot = await validators.getOperatorEthSnapshot(operatorId); + const beforeFee = await validators.getOperatorEthFee(operatorId); + expect(beforeSnapshot.blockNumber).to.equal(0n); + expect(beforeFee).to.equal(0n); + } + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + + const feeExecutedEvents = receipt?.logs + .map((log: any) => { + try { + return validators.interface.parseLog(log); + } catch { + return null; + } + }) + .filter((parsed: any) => parsed?.name === Events.OPERATOR_FEE_EXECUTED); + + expect(feeExecutedEvents).to.have.length(0); + + for (const operatorId of operatorIds) { + const afterSnapshot = await validators.getOperatorEthSnapshot(operatorId); + const afterFee = await validators.getOperatorEthFee(operatorId); + expect(afterSnapshot.blockNumber).to.equal(BigInt(receipt!.blockNumber)); + expect(afterFee).to.equal(0n); + } + }); + + it("Updates operatorEthVUnits even when cluster EB snapshot is not set", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + const tx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await tx.wait(); + + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(BPS_DENOMINATOR); + } + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await validators.getClusterVUnits(clusterId)).to.equal(0n); + }); + + it("Keeps stored EB snapshot unset when registering into an existing cluster without an explicit EB snapshot", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const registerTx1 = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt1 = await registerTx1.wait(); + const clusterAfter1 = parseClusterFromEvent(validators, receipt1, Events.VALIDATOR_ADDED); + + const registerTx2 = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfter1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await registerTx2.wait(); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await validators.getClusterVUnits(clusterId)).to.equal(0n); + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(2n * BPS_DENOMINATOR); + } + }); + + it("Increments stored EB snapshot vUnits when cluster EB snapshot is set", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const registerTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + const startVUnits = 3n * BPS_DENOMINATOR; + await validators.mockSetClusterVUnits(clusterId, startVUnits); + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterRegister, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await tx.wait(); + + expect(await validators.getClusterVUnits(clusterId)).to.equal(startVUnits + BPS_DENOMINATOR); + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(2n * BPS_DENOMINATOR); + } + }); + + it("Registers a new validator with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const tx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7]); + }); + + it("Registers a validator into an existing cluster with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterRegister, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_7]); + }); + + it("Registers a validator without additional deposit with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE * 2n } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterRegister, + { value: 0 } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_7]); + }); + + it("Registers a new validator with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const tx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10]); + }); + + it("Registers a validator into an existing cluster with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterRegister, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_10]); + }); + + it("Registers a validator without additional deposit with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE * 2n } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterRegister, + { value: 0 } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_10]); + }); + + it("Registers a new validator with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const tx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13]); + }); + + it("Registers a validator into an existing cluster with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterRegister, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_13]); + }); + + it("Registers a validator without additional deposit with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const registerTx = await validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE * 2n } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const tx = await validators.registerValidator( + makePublicKey(2), + operatorIds, + DEFAULT_SHARES, + clusterAfterRegister, + { value: 0 } + ); + const receipt = await tx.wait(); + await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_13]); + }); + + it("Is reverted with 'InvalidPublicKeyLength' when public key is empty or has invalid length", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const emptyPublicKey = '0x'; + const invalidLengthPublicKey = makePublicKey(1) + "11"; + + await expect(validators.registerValidator( + emptyPublicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.INVALID_PUBLIC_KEYS_LENGTH); + + await expect(validators.registerValidator( + invalidLengthPublicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.INVALID_PUBLIC_KEYS_LENGTH); + }); + + it("Is reverted with 'PublicKeysSharesLengthMismatch' if there is a mismatch between public keys and shares", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await expect(validators.bulkRegisterValidator( + [makePublicKey(1)], + operatorIds, + [], + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.PUBLIC_KEYS_SHARES_LENGTH_MISMATCH); + }); + + it("Is reverted with 'ValidatorAlreadyRegistered' if trying to register already existing key", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + await validators.registerValidator(publicKey, operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, { value: DEFAULT_ETH_REGISTER_VALUE }); + + await expect(validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_ALREADY_REGISTERED).withArgs(publicKey, clusterOwner.address); + }); + + it("Is reverted with 'InvalidOperatorIdsLength' if the length is not allowed one for clusters", async function () { + const { validators } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + const operatorIds = [2n, 1n, 2n]; + + await expect(validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.INVALID_OPERATOR_IDS_LENGTH); + }); + + it("Is reverted with 'UnsortedOperatorsList' if the list of operator ids is not sorted", async function () { + const { validators } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + const operatorIds = [4n, 3n, 2n, 1n]; + + await expect(validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.UNSORTED_OPERATORS_LIST); + }); + + it("Is reverted with 'OperatorsListNotUnique' if the list of operator ids has duplications", async function () { + const { validators } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + let operatorIds = [1n, 1n, 2n, 4n]; + + await expect(validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATORS_LIST_NOT_UNIQUE); + }); + + it("Is reverted with 'ClusterIsLiquidated' when trying to register to a liquidated cluster", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + await validators.mockSetClusterLiquidated(clusterOwner.address, operatorIds); + + const liquidatedCluster = { ...EMPTY_CLUSTER, active: false }; + + await expect(validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + liquidatedCluster, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.CLUSTER_IS_LIQUIDATED); + }); + + it("Is reverted with 'OperatorDoesNotExist' when one of the operators has been removed", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await validators.mockRemoveOperator(operatorIds[1]); + + await expect(validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Revert on removed operator is atomic and does not partially initialize earlier operators", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + for (const operatorId of operatorIds) { + await validators.mockSetOperatorLegacySSV(operatorId, 1); + } + + const firstOperator = operatorIds[0]; + const thirdOperator = operatorIds[2]; + const fourthOperator = operatorIds[3]; + + await validators.mockRemoveOperator(operatorIds[1]); + + const beforeFirstSnapshot = await validators.getOperatorEthSnapshot(firstOperator); + const beforeFirstFee = await validators.getOperatorEthFee(firstOperator); + const beforeFirstCount = await validators.getOperatorEthValidatorCount(firstOperator); + + await expect(validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATOR_DOES_NOT_EXIST); + + const afterFirstSnapshot = await validators.getOperatorEthSnapshot(firstOperator); + const afterFirstFee = await validators.getOperatorEthFee(firstOperator); + const afterFirstCount = await validators.getOperatorEthValidatorCount(firstOperator); + + expect(afterFirstSnapshot.blockNumber).to.equal(beforeFirstSnapshot.blockNumber); + expect(afterFirstSnapshot.index).to.equal(beforeFirstSnapshot.index); + expect(afterFirstSnapshot.balance).to.equal(beforeFirstSnapshot.balance); + expect(afterFirstFee).to.equal(beforeFirstFee); + expect(afterFirstCount).to.equal(beforeFirstCount); + + for (const operatorId of [thirdOperator, fourthOperator]) { + const snapshot = await validators.getOperatorEthSnapshot(operatorId); + const fee = await validators.getOperatorEthFee(operatorId); + const count = await validators.getOperatorEthValidatorCount(operatorId); + expect(snapshot.blockNumber).to.equal(0n); + expect(snapshot.index).to.equal(0n); + expect(snapshot.balance).to.equal(0n); + expect(fee).to.equal(0n); + expect(count).to.equal(0n); + } + }); + + it("Is reverted with 'OperatorDoesNotExist' when multiple operators have been removed", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await validators.mockRemoveOperator(operatorIds[0]); + await validators.mockRemoveOperator(operatorIds[2]); + + await expect(validators.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'ExceedValidatorLimitWithData' when registering a validator that pushes operator over the limit", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await validators.mockValidatorsPerOperatorLimit(5); + + const publicKeys = makePublicKeys(5); + const shares = new Array(5).fill(DEFAULT_SHARES); + const bulkTx = await validators.bulkRegisterValidator( + publicKeys, operatorIds, shares, createCluster(), { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const bulkReceipt = await bulkTx.wait(); + const clusterAtLimit = parseClusterFromEvent(validators, bulkReceipt, Events.VALIDATOR_ADDED); + + await expect(validators.registerValidator( + makePublicKey(6), operatorIds, DEFAULT_SHARES, clusterAtLimit, { value: DEFAULT_ETH_REGISTER_VALUE } + )).to.be.revertedWithCustomError(validators, Errors.OPERATOR_VALIDATORS_LIMIT_EXCEEDED) + .withArgs(operatorIds[0]); + }); + + it("Succeeds registering a validator after removing one to bring operator back below the limit", async function () { + const { validators, operatorIds } = await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await validators.mockValidatorsPerOperatorLimit(5); + + const publicKeys = makePublicKeys(5); + const shares = new Array(5).fill(DEFAULT_SHARES); + const bulkTx = await validators.bulkRegisterValidator( + publicKeys, operatorIds, shares, createCluster(), { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const bulkReceipt = await bulkTx.wait(); + const clusterAtLimit = parseClusterFromEvent(validators, bulkReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.removeValidator(publicKeys[0], operatorIds, clusterAtLimit); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(validators, removeReceipt, Events.VALIDATOR_REMOVED); + + const tx = await validators.registerValidator( + makePublicKey(6), operatorIds, DEFAULT_SHARES, clusterAfterRemove, { value: DEFAULT_ETH_REGISTER_VALUE } + ); + await expect(tx).to.emit(validators, Events.VALIDATOR_ADDED); + }); +}); diff --git a/test/unit/SSVValidator/removeValidator.test.ts b/test/unit/SSVValidator/removeValidator.test.ts new file mode 100644 index 000000000..c54ea0f4b --- /dev/null +++ b/test/unit/SSVValidator/removeValidator.test.ts @@ -0,0 +1,761 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; +import { ssvClustersHarnessFixture, getValidatorsHarnessFixture } from "../../setup/fixtures.ts"; +import { defaultValidatorsFixture } from "../../helpers/fixture-presets.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { setupTestContext, createCluster, makePublicKey, parseClusterFromEvent, computeClusterId } from "../../common/helpers.ts"; +import { createLegacySSVCluster } from "../../helpers/cluster.ts"; +import { DEDUCTED_DIGITS, DEFAULT_ETH_REGISTER_VALUE, DEFAULT_SHARES, BPS_DENOMINATOR } from "../../common/constants.ts"; +import { Events } from "../../common/events.ts"; +import { Errors } from "../../common/errors.ts"; +import { trackGasFromReceipt, GasGroup } from "../../helpers/gas-usage.ts"; +import { computeEBRoot } from "../../helpers/oracle.ts"; + +describe("SSVClusters function `removeValidator()`", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let clusterOwner: HardhatEthersSigner; + let deployClustersWith7Operators!: ReturnType; + let deployClustersWith10Operators!: ReturnType; + let deployClustersWith13Operators!: ReturnType; + + before(async function () { + ({ connection, networkHelpers, signers: [clusterOwner] } = await setupTestContext()); + + deployClustersWith7Operators = getValidatorsHarnessFixture(connection, 7); + deployClustersWith10Operators = getValidatorsHarnessFixture(connection, 10); + deployClustersWith13Operators = getValidatorsHarnessFixture(connection, 13); + }); + + const deploySSVValidatorsAndPrepareOperatorsFixture = async () => { + return defaultValidatorsFixture(connection); + }; + + const deploySSVClustersAndPrepareOperatorsFixture = async () => { + return ssvClustersHarnessFixture(connection); + }; + + const setValidSingleLeafRoot = async ( + clusters: any, + clusterId: string, + blockNum: number, + effectiveBalance: number + ) => { + const root = computeEBRoot(clusterId, effectiveBalance); + await clusters.mockSetEBRoot(blockNum, root); + }; + + it("Removes an existing validator, updates cluster state and emits correct events", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + const registerTx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.removeValidator(publicKey, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(validators, removeReceipt, Events.VALIDATOR_REMOVED); + + await expect(removeTx).to.emit(validators, Events.VALIDATOR_REMOVED); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + }); + + it("Updates operatorEthVUnits on register/remove even when cluster EB snapshot is not set", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + const registerTx = await validators.connect(clusterOwner).registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(BPS_DENOMINATOR); + } + + const removeTx = await validators.connect(clusterOwner).removeValidator(publicKey, operatorIds, clusterAfterRegister); + await removeTx.wait(); + + for (const operatorId of operatorIds) { + expect(await validators.getOperatorEthVUnits(operatorId)).to.equal(0n); + expect(await validators.getEffectiveOperatorVUnits(operatorId)).to.equal(0n); + } + }); + + it("Removes a validator with 7 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith7Operators); + + const publicKey = makePublicKey(1); + + const registerTx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.removeValidator(publicKey, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + await trackGasFromReceipt(removeReceipt, [GasGroup.REMOVE_VALIDATOR_7]); + }); + + it("Removes a validator with 10 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith10Operators); + + const publicKey = makePublicKey(1); + + const registerTx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.removeValidator(publicKey, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + await trackGasFromReceipt(removeReceipt, [GasGroup.REMOVE_VALIDATOR_10]); + }); + + it("Removes a validator with 13 operators", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deployClustersWith13Operators); + + const publicKey = makePublicKey(1); + + const registerTx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.removeValidator(publicKey, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + await trackGasFromReceipt(removeReceipt, [GasGroup.REMOVE_VALIDATOR_13]); + }); + + it("Is reverted with 'ValidatorDoesNotExist' when validator was not registered", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const registeredKey = makePublicKey(1); + const registerTx = await validators.registerValidator( + registeredKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const nonExistingKey = makePublicKey(2); + await expect(validators.removeValidator( + nonExistingKey, + operatorIds, + clusterAfterRegister + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Is reverted with 'IncorrectClusterState' when provided cluster data is stale or mismatched", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + const registerTx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const mismatchedCluster = { + ...clusterAfterRegister, + balance: clusterAfterRegister.balance + 1n, + }; + + await expect(validators.removeValidator( + publicKey, + operatorIds, + mismatchedCluster + )).to.be.revertedWithCustomError(validators, Errors.INCORRECT_CLUSTER_STATE); + }); + + it("Is reverted with 'ClusterDoesNotExists' when attempting to remove from a missing cluster", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + await expect(validators.removeValidator( + makePublicKey(1), + operatorIds, + createCluster() + )).to.be.revertedWithCustomError(validators, Errors.CLUSTER_DOES_NOT_EXIST); + }); + + it("Is reverted with 'ValidatorDoesNotExist' when removing a validator twice", async function () { + const { validators, operatorIds } = + await networkHelpers.loadFixture(deploySSVValidatorsAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + const registerTx = await validators.registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(validators, registerReceipt, Events.VALIDATOR_ADDED); + + const removeTx = await validators.removeValidator(publicKey, operatorIds, clusterAfterRegister); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(validators, removeReceipt, Events.VALIDATOR_REMOVED); + + await expect(validators.removeValidator( + publicKey, + operatorIds, + clusterAfterRemove + )).to.be.revertedWithCustomError(validators, Errors.VALIDATOR_DOES_NOT_EXIST); + }); + + it("Removes validator from active legacy SSV cluster and verifies operator counts and cluster hash", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + const ssvCluster = createLegacySSVCluster(); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + for (const opId of operatorIds) { + expect(await clusters.getOperatorValidatorCount(opId)).to.equal(1n); + } + expect(await clusters.getDaoValidatorCount()).to.equal(1n); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey, operatorIds, ssvCluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + + for (const opId of operatorIds) { + expect(await clusters.getOperatorValidatorCount(opId)).to.equal(0n); + } + + const storedHash = await clusters.getSSVClusterHash(clusterId); + expect(storedHash).to.not.equal(ethers.ZeroHash); + + const expectedHash = ethers.keccak256( + ethers.solidityPacked( + ["uint32", "uint64", "uint64", "uint256", "bool"], + [ + clusterAfterRemove.validatorCount, + clusterAfterRemove.networkFeeIndex, + clusterAfterRemove.index, + clusterAfterRemove.balance, + clusterAfterRemove.active, + ] + ) + ); + expect(storedHash).to.equal(expectedHash); + }); + + it("Keeps SSV cluster blocked operations after removing last SSV validator", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + const ssvCluster = createLegacySSVCluster({ balance: 10_000_000_000_000_000_000n }); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey, operatorIds, ssvCluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + + await expect( + clusters.connect(clusterOwner).withdraw(operatorIds, 1n, clusterAfterRemove) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_VERSION); + + await expect( + clusters.connect(clusterOwner).reactivate(operatorIds, clusterAfterRemove, { value: DEFAULT_ETH_REGISTER_VALUE }) + ).to.be.revertedWithCustomError(clusters, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("Removes validator from liquidated legacy SSV cluster and verifies operator counts", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + const ssvCluster = createLegacySSVCluster({ balance: 0n }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + for (const opId of operatorIds) { + expect(await clusters.getOperatorValidatorCount(opId)).to.equal(1n); + } + expect(await clusters.getDaoValidatorCount()).to.equal(1n); + + const liquidateTx = await clusters.connect(clusterOwner).liquidateSSV(clusterOwner.address, operatorIds, ssvCluster); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + for (const opId of operatorIds) { + expect(await clusters.getOperatorValidatorCount(opId)).to.equal(0n); + } + expect(await clusters.getDaoValidatorCount()).to.equal(0n); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey, operatorIds, liquidatedCluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(false); + + for (const opId of operatorIds) { + expect(await clusters.getOperatorValidatorCount(opId)).to.equal(0n); + } + expect(await clusters.getDaoValidatorCount()).to.equal(0n); + }); + + it("Handles remove -> liquidateSSV -> remove flow with expected SSV operator/DAO count deltas", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const pk1 = makePublicKey(11); + const pk2 = makePublicKey(12); + const ssvCluster = createLegacySSVCluster({ validatorCount: 2n, balance: 0n }); + + await clusters.mockRegisterSSVValidator(pk1, operatorIds, clusterOwner.address, ssvCluster); + await clusters.mockRegisterSSVValidator(pk2, operatorIds, clusterOwner.address, ssvCluster); + + const operatorCountStart = await clusters.getOperatorValidatorCount(operatorIds[0]); + const daoCountStart = await clusters.getDaoValidatorCount(); + + const remove1Tx = await clusters.connect(clusterOwner).removeValidator(pk1, operatorIds, ssvCluster); + const remove1Receipt = await remove1Tx.wait(); + const clusterAfterRemove1 = parseClusterFromEvent(clusters, remove1Receipt, Events.VALIDATOR_REMOVED); + + const operatorCountAfterRemove1 = await clusters.getOperatorValidatorCount(operatorIds[0]); + const daoCountAfterRemove1 = await clusters.getDaoValidatorCount(); + expect(operatorCountAfterRemove1).to.equal(operatorCountStart - 1n); + expect(daoCountAfterRemove1).to.equal(daoCountStart - 1n); + + const liquidateTx = await clusters.connect(clusterOwner).liquidateSSV(clusterOwner.address, operatorIds, clusterAfterRemove1); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent(clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED); + + const operatorCountAfterLiq = await clusters.getOperatorValidatorCount(operatorIds[0]); + const daoCountAfterLiq = await clusters.getDaoValidatorCount(); + expect(operatorCountAfterLiq).to.equal(operatorCountAfterRemove1 - BigInt(clusterAfterRemove1.validatorCount)); + expect(daoCountAfterLiq).to.equal(daoCountAfterRemove1 - BigInt(clusterAfterRemove1.validatorCount)); + + const remove2Tx = await clusters.connect(clusterOwner).removeValidator(pk2, operatorIds, liquidatedCluster); + const remove2Receipt = await remove2Tx.wait(); + const clusterAfterRemove2 = parseClusterFromEvent(clusters, remove2Receipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove2.validatorCount).to.equal(0n); + expect(clusterAfterRemove2.active).to.equal(false); + expect(await clusters.getOperatorValidatorCount(operatorIds[0])).to.equal(operatorCountAfterLiq); + expect(await clusters.getDaoValidatorCount()).to.equal(daoCountAfterLiq); + }); + + it("Removes from SSV, migrates to ETH, removes from ETH, then adds to ETH without storage cross-contamination", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const pk1 = makePublicKey(21); + const pk2 = makePublicKey(22); + const pk3 = makePublicKey(23); + const ssvCluster = createLegacySSVCluster({ validatorCount: 2n, balance: 0n }); + + await clusters.mockRegisterSSVValidator(pk1, operatorIds, clusterOwner.address, ssvCluster); + await clusters.mockRegisterSSVValidator(pk2, operatorIds, clusterOwner.address, ssvCluster); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + expect(await clusters.getSSVClusterHash(clusterId)).to.not.equal(ethers.ZeroHash); + + const removeSsvTx = await clusters.connect(clusterOwner).removeValidator(pk1, operatorIds, ssvCluster); + const removeSsvReceipt = await removeSsvTx.wait(); + const ssvClusterAfterRemove = parseClusterFromEvent(clusters, removeSsvReceipt, Events.VALIDATOR_REMOVED); + expect(ssvClusterAfterRemove.validatorCount).to.equal(1n); + + const migrateTx = await clusters.connect(clusterOwner).migrateClusterToETH( + operatorIds, + ssvClusterAfterRemove, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const migrateReceipt = await migrateTx.wait(); + const ethCluster = parseClusterFromEvent(clusters, migrateReceipt, Events.CLUSTER_MIGRATED_TO_ETH); + + expect(await clusters.getSSVClusterHash(clusterId)).to.equal(ethers.ZeroHash); + expect(await clusters.getClusterHash(clusterId)).to.not.equal(ethers.ZeroHash); + + const removeEthTx = await clusters.connect(clusterOwner).removeValidator(pk2, operatorIds, ethCluster); + const removeEthReceipt = await removeEthTx.wait(); + const ethClusterAfterRemove = parseClusterFromEvent(clusters, removeEthReceipt, Events.VALIDATOR_REMOVED); + expect(ethClusterAfterRemove.validatorCount).to.equal(0n); + + const addEthTx = await clusters.connect(clusterOwner).registerValidator( + pk3, + operatorIds, + DEFAULT_SHARES, + ethClusterAfterRemove, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const addEthReceipt = await addEthTx.wait(); + const ethClusterAfterAdd = parseClusterFromEvent(clusters, addEthReceipt, Events.VALIDATOR_ADDED); + expect(ethClusterAfterAdd.validatorCount).to.equal(1n); + + expect(await clusters.getSSVClusterHash(clusterId)).to.equal(ethers.ZeroHash); + expect(await clusters.getClusterHash(clusterId)).to.not.equal(ethers.ZeroHash); + }); + + it("SSV remove path leaves orphaned EB snapshot untouched (defensive behavior)", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(31); + const ssvCluster = createLegacySSVCluster({ validatorCount: 1n }); + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const clusterId = computeClusterId(clusterOwner.address, operatorIds); + await clusters.mockSetClusterVUnits(clusterId, 50_000n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(50_000n); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey, operatorIds, ssvCluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(50_000n); + }); + + it("Processes SSV and ETH removals in the same block without storage/counter collision", async function () { + const deployEightOperatorsFixture = async () => ssvClustersHarnessFixture(connection, 8); + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployEightOperatorsFixture); + + const ssvOperatorIds = operatorIds.slice(0, 4); + const ethOperatorIds = operatorIds.slice(4, 8); + + const ssvPublicKey = makePublicKey(41); + const ethPublicKey = makePublicKey(42); + const ssvCluster = createLegacySSVCluster({ validatorCount: 1n, balance: 10_000_000_000_000_000_000n }); + await clusters.mockRegisterSSVValidator(ssvPublicKey, ssvOperatorIds, clusterOwner.address, ssvCluster); + + const registerEthTx = await clusters.connect(clusterOwner).registerValidator( + ethPublicKey, + ethOperatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerEthReceipt = await registerEthTx.wait(); + const ethCluster = parseClusterFromEvent(clusters, registerEthReceipt, Events.VALIDATOR_ADDED); + + const provider = connection.ethers.provider; + await provider.send("evm_setAutomine", [false]); + let removeSsvTx: any; + let removeEthTx: any; + try { + removeSsvTx = await clusters.connect(clusterOwner).removeValidator(ssvPublicKey, ssvOperatorIds, ssvCluster); + removeEthTx = await clusters.connect(clusterOwner).removeValidator(ethPublicKey, ethOperatorIds, ethCluster); + await provider.send("evm_mine", []); + } finally { + await provider.send("evm_setAutomine", [true]); + } + + const removeSsvReceipt = await removeSsvTx.wait(); + const removeEthReceipt = await removeEthTx.wait(); + expect(removeSsvReceipt.blockNumber).to.equal(removeEthReceipt.blockNumber); + + const ssvClusterId = computeClusterId(clusterOwner.address, ssvOperatorIds); + const ethClusterId = computeClusterId(clusterOwner.address, ethOperatorIds); + expect(await clusters.getSSVClusterHash(ssvClusterId)).to.not.equal(ethers.ZeroHash); + expect(await clusters.getClusterHash(ethClusterId)).to.not.equal(ethers.ZeroHash); + + expect(await clusters.getOperatorValidatorCount(ssvOperatorIds[0])).to.equal(0n); + expect(await clusters.getOperatorEthValidatorCount(ethOperatorIds[0])).to.equal(0n); + expect(await clusters.getOperatorEthValidatorCount(ssvOperatorIds[0])).to.equal(0n); + expect(await clusters.getOperatorValidatorCount(ethOperatorIds[0])).to.equal(0n); + }); + + it("Removes validator from SSV cluster with non-zero fees and verifies balance deduction", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const opFeeUnpacked = 10_000_000_000n; + const networkFeeRaw = 500n; + for (const opId of operatorIds) { + await clusters.mockOperatorSSVFee(opId, opFeeUnpacked); + } + await clusters.mockSSVNetworkFee(networkFeeRaw); + await clusters.mockCurrentNetworkFeeIndexSSV(0n); + + const snapshots: { index: bigint; block: bigint }[] = []; + for (const opId of operatorIds) { + const [index, blockNumber] = await clusters.getOperatorSnapshot(opId); + snapshots.push({ index: BigInt(index), block: BigInt(blockNumber) }); + } + const nfiAtRegister = await clusters.getCurrentNetworkFeeIndexSSV(); + + const initialBalance = 100_000_000_000_000_000_000n; + const publicKey = makePublicKey(1); + const ssvCluster = createLegacySSVCluster({ + balance: initialBalance, + index: snapshots.reduce((acc, s) => acc + s.index, 0n), + networkFeeIndex: nfiAtRegister, + }); + + await clusters.mockRegisterSSVValidator(publicKey, operatorIds, clusterOwner.address, ssvCluster); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey, operatorIds, ssvCluster); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.balance).to.be.lt(initialBalance); + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(clusterAfterRemove.active).to.equal(true); + + const newIndex = clusterAfterRemove.index; + const newNFI = clusterAfterRemove.networkFeeIndex; + const indexDelta = newIndex - ssvCluster.index; + const nfiDelta = newNFI - ssvCluster.networkFeeIndex; + const totalUsagePacked = (indexDelta + nfiDelta) * 1n; + const totalUsage = totalUsagePacked * DEDUCTED_DIGITS; + const expectedBalance = initialBalance - totalUsage; + + expect(clusterAfterRemove.balance).to.equal(expectedBalance); + }); + + it("Keeps explicit EB snapshot consistent across updateClusterBalance and remove", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const pk1 = makePublicKey(1); + const pk2 = makePublicKey(2); + + const register1 = await clusters.connect(clusterOwner).registerValidator( + pk1, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt1 = await register1.wait(); + const clusterAfter1 = parseClusterFromEvent(clusters, receipt1, Events.VALIDATOR_ADDED); + + const register2 = await clusters.connect(clusterOwner).registerValidator( + pk2, + operatorIds, + DEFAULT_SHARES, + clusterAfter1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const receipt2 = await register2.wait(); + const clusterAfter2 = parseClusterFromEvent(clusters, receipt2, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(await clusterOwner.getAddress(), operatorIds); + const blockNum = 1; + const effectiveBalance = 160; + + await setValidSingleLeafRoot(clusters, clusterId, blockNum, effectiveBalance); + + const updateTx = await clusters.updateClusterBalance( + blockNum, + await clusterOwner.getAddress(), + operatorIds, + clusterAfter2, + effectiveBalance, + [] + ); + const updateReceipt = await updateTx.wait(); + const clusterAfterUpdate = parseClusterFromEvent(clusters, updateReceipt, "ClusterBalanceUpdated"); + const expectedUpdatedVUnits = (BigInt(effectiveBalance) * BPS_DENOMINATOR + 31n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedUpdatedVUnits); + const baselineBeforeRemove = 2n * BPS_DENOMINATOR; + const deviationAfterUpdate = expectedUpdatedVUnits - baselineBeforeRemove; + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(deviationAfterUpdate); + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(expectedUpdatedVUnits); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(pk1, operatorIds, clusterAfterUpdate); + const removeReceipt = await removeTx.wait(); + const clusterAfterRemove = parseClusterFromEvent(clusters, removeReceipt, Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(1n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedUpdatedVUnits - BPS_DENOMINATOR); + const baselineAfterRemove = 1n * BPS_DENOMINATOR; + const expectedClusterVUnitsAfterRemove = expectedUpdatedVUnits - BPS_DENOMINATOR; + expect(await clusters.getOperatorEthVUnits(operatorIds[0])).to.equal(deviationAfterUpdate); + expect(await clusters.getEffectiveOperatorVUnits(operatorIds[0])).to.equal(baselineAfterRemove + deviationAfterUpdate); + }); + + it("Clears remaining explicit EB vUnits when removing the last validator", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(1); + + const registerTx = await clusters.connect(clusterOwner).registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const registerReceipt = await registerTx.wait(); + const clusterAfterRegister = parseClusterFromEvent(clusters, registerReceipt, Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(await clusterOwner.getAddress(), operatorIds); + const blockNum = 1; + const effectiveBalance = 96; + + await setValidSingleLeafRoot(clusters, clusterId, blockNum, effectiveBalance); + + const updateTx = await clusters.updateClusterBalance( + blockNum, + await clusterOwner.getAddress(), + operatorIds, + clusterAfterRegister, + effectiveBalance, + [] + ); + const updateReceipt = await updateTx.wait(); + const clusterAfterUpdate = parseClusterFromEvent(clusters, updateReceipt, "ClusterBalanceUpdated"); + + const expectedUpdatedVUnits = (BigInt(effectiveBalance) * BPS_DENOMINATOR + 31n) / 32n; + expect(await clusters.getClusterVUnits(clusterId)).to.equal(expectedUpdatedVUnits); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey, operatorIds, clusterAfterUpdate); + await removeTx.wait(); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + for (const operatorId of operatorIds) { + expect(await clusters.getOperatorEthVUnits(operatorId)).to.equal(0n); + } + }); + + it("removing one validator keeps deviation but decrements DAO baseline exactly", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const firstPubKey = makePublicKey(101); + const secondPubKey = makePublicKey(102); + + const regTx1 = await clusters.connect(clusterOwner).registerValidator( + firstPubKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const clusterAfterReg1 = parseClusterFromEvent(clusters, await regTx1.wait(), Events.VALIDATOR_ADDED); + + const regTx2 = await clusters.connect(clusterOwner).registerValidator( + secondPubKey, + operatorIds, + DEFAULT_SHARES, + clusterAfterReg1, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const clusterAfterReg2 = parseClusterFromEvent(clusters, await regTx2.wait(), Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(await clusterOwner.getAddress(), operatorIds); + const explicitEb = 160; + await setValidSingleLeafRoot(clusters, clusterId, 1, explicitEb); + + const updateTx = await clusters.updateClusterBalance( + 1, + await clusterOwner.getAddress(), + operatorIds, + clusterAfterReg2, + explicitEb, + [] + ); + const clusterAfterUpdate = parseClusterFromEvent(clusters, await updateTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + + expect(await clusters.getClusterVUnits(clusterId)).to.equal(50000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(50000n); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(firstPubKey, operatorIds, clusterAfterUpdate); + const clusterAfterRemove = parseClusterFromEvent(clusters, await removeTx.wait(), Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(1n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(40000n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(40000n); + }); + + it("removing the last validator clears DAO deviation for explicit-EB cluster", async function () { + const { clusters, operatorIds } = + await networkHelpers.loadFixture(deploySSVClustersAndPrepareOperatorsFixture); + + const publicKey = makePublicKey(103); + const registerTx = await clusters.connect(clusterOwner).registerValidator( + publicKey, + operatorIds, + DEFAULT_SHARES, + createCluster(), + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + const clusterAfterRegister = parseClusterFromEvent(clusters, await registerTx.wait(), Events.VALIDATOR_ADDED); + + const clusterId = computeClusterId(await clusterOwner.getAddress(), operatorIds); + await setValidSingleLeafRoot(clusters, clusterId, 1, 64); + + const updateTx = await clusters.updateClusterBalance( + 1, + await clusterOwner.getAddress(), + operatorIds, + clusterAfterRegister, + 64, + [] + ); + const clusterAfterUpdate = parseClusterFromEvent(clusters, await updateTx.wait(), Events.CLUSTER_BALANCE_UPDATED); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(20000n); + + const removeTx = await clusters.connect(clusterOwner).removeValidator(publicKey, operatorIds, clusterAfterUpdate); + const clusterAfterRemove = parseClusterFromEvent(clusters, await removeTx.wait(), Events.VALIDATOR_REMOVED); + + expect(clusterAfterRemove.validatorCount).to.equal(0n); + expect(await clusters.getClusterVUnits(clusterId)).to.equal(0n); + expect(await clusters.getDaoTotalEthVUnits()).to.equal(0n); + }); +}); diff --git a/test/unit/SSVViews/views.test.ts b/test/unit/SSVViews/views.test.ts new file mode 100644 index 000000000..9b748353a --- /dev/null +++ b/test/unit/SSVViews/views.test.ts @@ -0,0 +1,293 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { ethers } from "ethers"; + +import { getTestConnection } from "../../setup/connection.ts"; +import { ssvNetworkFullFixture } from "../../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../../common/types.ts"; +import { Errors } from "../../common/errors.ts"; +import { + CLUSTER_VERSION_ETH, + CLUSTER_VERSION_SSV, + DEDUCTED_DIGITS, + DEFAULT_ETH_REGISTER_VALUE, + DEFAULT_SHARES, + ETH_DEDUCTED_DIGITS, + EMPTY_CLUSTER, +} from "../../common/constants.ts"; +import { + generateMerkleForClusterEB, + getCurrentClusterState, + makePublicKey, + parseClusterFromEvent, + registerOperators, + whitelistAddresses, +} from "../../common/helpers.ts"; +import { Events } from "../../common/events.ts"; + +describe("SSVViews dedicated coverage", () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + let operatorOwner: HardhatEthersSigner; + let clusterOwner: HardhatEthersSigner; + + before(async function () { + ({ connection, networkHelpers } = await getTestConnection()); + [operatorOwner, clusterOwner] = await connection.ethers.getSigners(); + }); + + const deployFullSSVNetworkFixture = async () => ssvNetworkFullFixture(connection); + + const registerEthCluster = async (network: any) => { + const operatorIds = await registerOperators(network, operatorOwner, 4); + await whitelistAddresses(network, operatorOwner, operatorIds, [clusterOwner.address]); + + await network.connect(clusterOwner).registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: DEFAULT_ETH_REGISTER_VALUE } + ); + + const cluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + return { operatorIds, cluster }; + }; + + const configureOracles = async (network: any) => { + const oracles = (await connection.ethers.getSigners()).slice(10, 14); + + await network.replaceOracle(1, oracles[0].address); + await network.replaceOracle(2, oracles[1].address); + await network.replaceOracle(3, oracles[2].address); + await network.replaceOracle(4, oracles[3].address); + + return oracles; + }; + + const deployViewsHarnessFixture = async () => { + const mockCSSV = await connection.ethers.deployContract("MockCSSV"); + await mockCSSV.waitForDeployment(); + + const viewsHarness = await connection.ethers.deployContract("SSVViewsHarness", [await mockCSSV.getAddress()]); + await viewsHarness.waitForDeployment(); + + return { viewsHarness }; + }; + + it("getBalance and getEffectiveBalance return expected values for active ETH cluster", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { operatorIds, cluster } = await registerEthCluster(network); + + expect(await views.getBalance(clusterOwner.address, operatorIds, cluster)).to.equal(cluster.balance); + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, cluster)).to.equal(32); + + await connection.networkHelpers.mine(12); + const balanceAfterBlocks = await views.getBalance(clusterOwner.address, operatorIds, cluster); + expect(balanceAfterBlocks).to.be.lessThan(cluster.balance); + expect(await views.getEffectiveBalance(clusterOwner.address, operatorIds, cluster)).to.equal(32); + }); + + it("liquidated clusters are reported as liquidated and balance/EB getters revert", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { operatorIds, cluster } = await registerEthCluster(network); + + await network.connect(clusterOwner).liquidate(clusterOwner.address, operatorIds, cluster); + const liquidatedCluster = await getCurrentClusterState(connection, network, clusterOwner.address, operatorIds); + + expect(await views.isLiquidated(clusterOwner.address, operatorIds, liquidatedCluster)).to.equal(true); + await expect( + views.getBalance(clusterOwner.address, operatorIds, liquidatedCluster) + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_IS_LIQUIDATED); + await expect( + views.getEffectiveBalance(clusterOwner.address, operatorIds, liquidatedCluster) + ).to.be.revertedWithCustomError(network, Errors.CLUSTER_IS_LIQUIDATED); + }); + + it("isLiquidatable respects exact minimum-collateral boundary", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { operatorIds, cluster } = await registerEthCluster(network); + + const currentBalance = await views.getBalance(clusterOwner.address, operatorIds, cluster); + const currentBurnRate = await views.getBurnRate(clusterOwner.address, operatorIds, cluster); + const rawBoundary = currentBalance > currentBurnRate ? currentBalance - currentBurnRate : 0n; + const boundaryCollateral = (rawBoundary / ETH_DEDUCTED_DIGITS) * ETH_DEDUCTED_DIGITS; + + await network.updateMinimumLiquidationCollateral(boundaryCollateral); + expect(await views.isLiquidatable(clusterOwner.address, operatorIds, cluster)).to.equal(false); + + await network.updateMinimumLiquidationCollateral(boundaryCollateral + ETH_DEDUCTED_DIGITS); + expect(await views.isLiquidatable(clusterOwner.address, operatorIds, cluster)).to.equal(true); + }); + + it("getBurnRate scales with EB vUnits (64 ETH == 2x of implicit 32 ETH)", async function () { + const { network, views, ssvToken } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { operatorIds, cluster } = await registerEthCluster(network); + + // commitRoot requires non-zero oracle weight, which exists once there is non-zero stake. + const stakeAmount = ethers.parseEther("10"); + await ssvToken.mint(clusterOwner.address, stakeAmount); + await ssvToken.connect(clusterOwner).approve(await network.getAddress(), ethers.MaxUint256); + await network.connect(clusterOwner).stake(stakeAmount); + + const baseBurnRate = await views.getBurnRate(clusterOwner.address, operatorIds, cluster); + const oracles = await configureOracles(network); + + const clusterId = ethers.keccak256( + ethers.solidityPacked(["address", "uint64[]"], [clusterOwner.address, operatorIds]) + ); + const merkleData = generateMerkleForClusterEB(connection, [{ clusterId, effectiveBalance: 64 }]); + const currentBlock = await connection.ethers.provider.getBlockNumber(); + + for (let i = 0; i < 3; i += 1) { + await network.connect(oracles[i]).commitRoot(merkleData.root, currentBlock); + } + + const tx = await network.updateClusterBalance( + currentBlock, + clusterOwner.address, + operatorIds, + cluster, + 64, + merkleData.proofs[clusterId] + ); + const receipt = await tx.wait(); + + const updatedCluster = parseClusterFromEvent(network, receipt, Events.CLUSTER_BALANCE_UPDATED); + const burnRateAfterEbUpdate = await views.getBurnRate(clusterOwner.address, operatorIds, updatedCluster); + expect(burnRateAfterEbUpdate).to.equal(baseBurnRate * 2n); + }); + + it("getOperatorEarnings exposes ETH earnings while SSV earnings stay zero in ETH-only state", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { operatorIds } = await registerEthCluster(network); + + await connection.networkHelpers.mine(100); + expect(await views.getOperatorEarnings(operatorIds[0])).to.be.greaterThan(0n); + expect(await views.getOperatorEarningsSSV(operatorIds[0])).to.equal(0n); + }); + + it("ETH-only (post-migration-equivalent) views return ETH values and zero SSV values", async function () { + const { network, views } = await networkHelpers.loadFixture(deployFullSSVNetworkFixture); + const { operatorIds, cluster } = await registerEthCluster(network); + + expect(await views.getClusterAssetType(clusterOwner.address, operatorIds)).to.equal(CLUSTER_VERSION_ETH); + expect(await views.getBalance(clusterOwner.address, operatorIds, cluster)).to.equal(cluster.balance); + expect(await views.getBurnRate(clusterOwner.address, operatorIds, cluster)).to.be.greaterThan(0n); + + await expect(views.getBalanceSSV(clusterOwner.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(views, Errors.INCORRECT_CLUSTER_VERSION); + await expect(views.getBurnRateSSV(clusterOwner.address, operatorIds, cluster)) + .to.be.revertedWithCustomError(views, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("getOperatorEarnings returns both ETH and SSV earnings when both snapshots are funded", async function () { + const { viewsHarness } = await networkHelpers.loadFixture(deployViewsHarnessFixture); + + const operatorId = 1n; + const ethEarnings = 73n * ETH_DEDUCTED_DIGITS; + const ssvEarnings = 19n * DEDUCTED_DIGITS; + + await viewsHarness.mockSetOperator(operatorId, operatorOwner.address, 0n, 0n, 1, 1); + await viewsHarness.mockSetOperatorEarnings(operatorId, ethEarnings, ssvEarnings); + + expect(await viewsHarness.getOperatorEarnings(operatorId)).to.equal(ethEarnings); + expect(await viewsHarness.getOperatorEarningsSSV(operatorId)).to.equal(ssvEarnings); + }); + + it("SSV-only clusters return positive SSV balance/burn rate while ETH getters return zero", async function () { + const { viewsHarness } = await networkHelpers.loadFixture(deployViewsHarnessFixture); + + const operatorIds = [1n, 2n, 3n, 4n]; + const ssvFeePerOperator = DEDUCTED_DIGITS; + const ssvNetworkFee = DEDUCTED_DIGITS; + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + active: true, + balance: 100n * DEDUCTED_DIGITS, + }; + + for (const operatorId of operatorIds) { + await viewsHarness.mockSetOperator(operatorId, operatorOwner.address, 0n, ssvFeePerOperator, 0, 1); + } + await viewsHarness.mockSetNetworkFeeSSV(ssvNetworkFee); + await viewsHarness.mockRegisterSSVCluster(clusterOwner.address, operatorIds, ssvCluster); + + expect(await viewsHarness.getClusterAssetType(clusterOwner.address, operatorIds)).to.equal(CLUSTER_VERSION_SSV); + const currentBlock = BigInt(await connection.ethers.provider.getBlockNumber()); + + // SPEC/FLOWS formula mirrored from SSVViews.getBalanceSSV + ClusterLib.updateBalanceSSV: + // clusterIndexRaw = sum_i(operator.snapshot.index + (block - operator.snapshot.block) * operator.feeRaw) + // currentNetworkFeeIndexRaw = protocol.networkFeeIndex + (block - protocol.networkFeeIndexBlockNumber) * protocol.networkFeeRaw + // usageRaw = (clusterIndexRaw - cluster.index) * validatorCount + (currentNetworkFeeIndexRaw - cluster.networkFeeIndex) * validatorCount + // balance = max(0, cluster.balance - usageRaw * DEDUCTED_DIGITS) + let clusterIndexRaw = 0n; + for (const operatorId of operatorIds) { + const [feeRaw, indexRaw, blockNumber] = await viewsHarness.getOperatorSSVSnapshot(operatorId); + clusterIndexRaw += BigInt(indexRaw) + (currentBlock - BigInt(blockNumber)) * BigInt(feeRaw); + } + + const [networkFeeRaw, networkFeeIndexRaw, networkFeeIndexBlock] = await viewsHarness.getNetworkFeeStateSSV(); + const currentNetworkFeeIndexRaw = + BigInt(networkFeeIndexRaw) + (currentBlock - BigInt(networkFeeIndexBlock)) * BigInt(networkFeeRaw); + + const totalUsageRaw = + (clusterIndexRaw - ssvCluster.index) * ssvCluster.validatorCount + + (currentNetworkFeeIndexRaw - ssvCluster.networkFeeIndex) * ssvCluster.validatorCount; + const expectedBalance = totalUsageRaw * DEDUCTED_DIGITS > ssvCluster.balance + ? 0n + : ssvCluster.balance - totalUsageRaw * DEDUCTED_DIGITS; + + const ssvBalance = await viewsHarness.getBalanceSSV(clusterOwner.address, operatorIds, ssvCluster); + expect(ssvBalance).to.equal(expectedBalance); + expect(await viewsHarness.getBurnRateSSV(clusterOwner.address, operatorIds, ssvCluster)).to.equal( + 5n * DEDUCTED_DIGITS + ); + + await expect(viewsHarness.getBalance(clusterOwner.address, operatorIds, ssvCluster)) + .to.be.revertedWithCustomError(viewsHarness, Errors.INCORRECT_CLUSTER_VERSION); + await expect(viewsHarness.getBurnRate(clusterOwner.address, operatorIds, ssvCluster)) + .to.be.revertedWithCustomError(viewsHarness, Errors.INCORRECT_CLUSTER_VERSION); + }); + + it("getEffectiveBalance returns implicit 32 ETH for active SSV cluster with no explicit EB snapshot", async function () { + const { viewsHarness } = await networkHelpers.loadFixture(deployViewsHarnessFixture); + + const operatorIds = [1n, 2n, 3n, 4n]; + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + active: true, + balance: 0n, + }; + + await viewsHarness.mockRegisterSSVCluster(clusterOwner.address, operatorIds, ssvCluster); + + // No clusterEB set → falls back to validatorCount * BPS_DENOMINATOR → 32 ETH per validator + expect(await viewsHarness.getEffectiveBalance(clusterOwner.address, operatorIds, ssvCluster)).to.equal(32); + }); + + it("getEffectiveBalance returns explicit EB for active SSV cluster when vUnits are set", async function () { + const { viewsHarness } = await networkHelpers.loadFixture(deployViewsHarnessFixture); + + const operatorIds = [1n, 2n, 3n, 4n]; + const ssvCluster = { + validatorCount: 1n, + networkFeeIndex: 0n, + index: 0n, + active: true, + balance: 0n, + }; + + await viewsHarness.mockRegisterSSVCluster(clusterOwner.address, operatorIds, ssvCluster); + // 64 ETH: vUnits = ceil(64 * 10_000 / 32) = 20_000 + await (viewsHarness as any).mockSetClusterEB(clusterOwner.address, operatorIds, 20_000n); + + expect(await viewsHarness.getEffectiveBalance(clusterOwner.address, operatorIds, ssvCluster)).to.equal(64); + }); +}); diff --git a/test/unit/mainnet-config-validation.test.ts b/test/unit/mainnet-config-validation.test.ts new file mode 100644 index 000000000..0d16ad76d --- /dev/null +++ b/test/unit/mainnet-config-validation.test.ts @@ -0,0 +1,709 @@ +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/types"; +import { + ssvClustersHarnessFixture, + ssvDAOHarnessFixture, + ssvOperatorsHarnessFixture, + ssvStakingHarnessFixture, +} from "../setup/fixtures.ts"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { makePublicKey, makeOperatorKey, parseClusterFromEvent, setupTestContext } from "../common/helpers.ts"; +import { + DEFAULT_SHARES, + ETH_DEDUCTED_DIGITS, + MINIMAL_LIQUIDATION_THRESHOLD, + STAKE_AMOUNT, EMPTY_CLUSTER, +} from '../common/constants.ts'; +import { Events } from "../common/events.ts"; +import { Errors } from "../common/errors.ts"; +import { ethers } from "ethers"; + +/** + * Uses exact mainnet deployment parameters (from deployments/params-candidate.json) + * to validate system behavior at the boundaries implied by those values. + * + * To propose new governance parameters: edit deployments/params-candidate.json and re-run + * the test suite. No test source changes are needed unless burn-rate assertions must be updated. + * + * Deployment Config (exact on-chain values — all fees are already packable): + * | Param | Value | Raw | + * |--------------------------------|------------------------------------|-------------------| + * | networkFeeEth | 3,550,900,000 wei/block | 3,550,900,000 | + * | minimumLiquidationCollateralEth| 940,000,000,000,000 wei (0.00094) | 940,000,000,000,000| + * | liquidationThresholdPeriod | 35,800 blocks (~5 days) | 35,800 | + * | minOperatorEthFee | 1,065,200,000 wei/block | 1,065,200,000 | + * | maxOperatorEthFee | 5,326,300,000 wei/block | 5,326,300,000 | + * | defaultOperatorEthFee | 1,778,800,000 wei/block | 1,778,800,000 | + * | quorumBps | 75% | 7,500 | + * | cooldownDuration | 604,800 seconds (7 days) | 604,800 | + * | minBlocksBetweenUpdates | 0 blocks | 0. | + * + */ + +type ParamsCandidateJson = { + networkFeeEth: string; + minimumLiquidationCollateralEth: string; + liquidationThresholdPeriod: string; + minOperatorEthFee: string; + maxOperatorEthFee: string; + defaultOperatorEthFee: string; + quorumBps: number; + cooldownDuration: number; + minBlocksBetweenUpdates: number; + defaultOracleIds: number[]; +}; + +const _raw = JSON.parse( + readFileSync(resolve(process.cwd(), "deployments/params-candidate.json"), "utf8") +) as ParamsCandidateJson; + +const CONFIG = { + networkFeeEth: BigInt(_raw.networkFeeEth), + minimumLiquidationCollateralEth: BigInt(_raw.minimumLiquidationCollateralEth), + liquidationThresholdPeriod: BigInt(_raw.liquidationThresholdPeriod), + minOperatorEthFee: BigInt(_raw.minOperatorEthFee), + maxOperatorEthFee: BigInt(_raw.maxOperatorEthFee), + defaultOperatorEthFee: BigInt(_raw.defaultOperatorEthFee), + quorumBps: BigInt(_raw.quorumBps), + cooldownDuration: BigInt(_raw.cooldownDuration), + minBlocksBetweenUpdates: BigInt(_raw.minBlocksBetweenUpdates), + defaultOracleIds: _raw.defaultOracleIds, +}; + +// Original values (raw wei, some NOT packable). Kept for the packability documentation test. +const RAW_VALUES = { + ethNetworkFee: 3_550_929_823n, + operatorMinFee: 1_065_278_947n, + operatorMaxFee: 5_326_394_735n, + defaultOperatorETHFee: 1_775_464_912n, + minimumLiquidationCollateral: 940_000_000_000_000n, +}; + +describe("Mainnet Governance Config Validation", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + describe("Config file (deployments/params-candidate.json)", () => { + const CONFIG_PATH = resolve(process.cwd(), "deployments/params-candidate.json"); + + it("exists and is readable from process.cwd()", () => { + expect(existsSync(CONFIG_PATH), `File not found: ${CONFIG_PATH}`).to.be.true; + }); + + it("contains all required fields", () => { + const required: (keyof ParamsCandidateJson)[] = [ + "networkFeeEth", + "minimumLiquidationCollateralEth", + "liquidationThresholdPeriod", + "minOperatorEthFee", + "maxOperatorEthFee", + "defaultOperatorEthFee", + "quorumBps", + "cooldownDuration", + "minBlocksBetweenUpdates", + "defaultOracleIds", + ]; + for (const field of required) { + expect(_raw[field], `Missing field: ${field}`).to.not.be.undefined; + } + }); + + it("fee fields are non-negative integer strings", () => { + const stringFields: (keyof ParamsCandidateJson)[] = [ + "networkFeeEth", + "minimumLiquidationCollateralEth", + "liquidationThresholdPeriod", + "minOperatorEthFee", + "maxOperatorEthFee", + "defaultOperatorEthFee" + ]; + for (const field of stringFields) { + const value = _raw[field]; + expect(typeof value, `${field} must be a string`).to.equal("string"); + expect(/^\d+$/.test(value as string)).to.be.true; + } + }); + + it("quorumBps is an integer in [1, 10000]", () => { + expect(Number.isInteger(_raw.quorumBps)).to.be.true; + expect(_raw.quorumBps).to.be.greaterThanOrEqual(1); + expect(_raw.quorumBps).to.be.lessThanOrEqual(10_000); + }); + + it("cooldownDuration is a positive integer", () => { + expect(Number.isInteger(_raw.cooldownDuration)).to.be.true; + expect(_raw.cooldownDuration).to.be.greaterThan(0); + }); + + it("minBlocksBetweenUpdates is a positive integer", () => { + const value = Number(_raw.minBlocksBetweenUpdates); + expect(Number.isInteger(value)).to.be.true; + }); + + it("defaultOracleIds is an array of 4 distinct valid oracle ids", () => { + expect(Array.isArray(_raw.defaultOracleIds)).to.be.true; + expect(_raw.defaultOracleIds.length).to.equal(4); + for (const id of _raw.defaultOracleIds) { + expect(Number.isInteger(id) && id > 0 && id <= 0xffffffff).to.be.true; + } + const unique = new Set(_raw.defaultOracleIds); + expect(unique.size).to.equal(4); + }); + + it("minOperatorEthFee <= defaultOperatorEthFee <= maxOperatorEthFee", () => { + const min = BigInt(_raw.minOperatorEthFee); + const def = BigInt(_raw.defaultOperatorEthFee); + const max = BigInt(_raw.maxOperatorEthFee); + expect(min <= def).to.be.true; + expect(def <= max).to.be.true; + }); + }); + + describe("Packability", () => { + let harness: any; + + const deployPackedLibFixture = async () => { + const contract = await connection.ethers.deployContract("PackedLibHarness"); + await contract.waitForDeployment(); + return { harness: contract }; + }; + + it("Confirms raw mainnet values are not packable (remainder ≠ 0 mod 100,000)", async function () { + // ethNetworkFee: 3,550,929,823 % 100,000 = 29,823 → NOT packable + expect(RAW_VALUES.ethNetworkFee % ETH_DEDUCTED_DIGITS).to.equal(29_823n); + // operatorMinFee: 1,065,278,947 % 100,000 = 78,947 → NOT packable + expect(RAW_VALUES.operatorMinFee % ETH_DEDUCTED_DIGITS).to.equal(78_947n); + // operatorMaxFee: 5,326,394,735 % 100,000 = 94,735 → NOT packable + expect(RAW_VALUES.operatorMaxFee % ETH_DEDUCTED_DIGITS).to.equal(94_735n); + // defaultOperatorETHFee: 1,775,464,912 % 100,000 = 64,912 → NOT packable + expect(RAW_VALUES.defaultOperatorETHFee % ETH_DEDUCTED_DIGITS).to.equal(64_912n); + }); + + it("Confirms all deployment config values are packable (divisible by 100,000)", async function () { + expect(CONFIG.networkFeeEth % ETH_DEDUCTED_DIGITS).to.equal(0n); + expect(CONFIG.minimumLiquidationCollateralEth % ETH_DEDUCTED_DIGITS).to.equal(0n); + expect(CONFIG.minOperatorEthFee % ETH_DEDUCTED_DIGITS).to.equal(0n); + expect(CONFIG.maxOperatorEthFee % ETH_DEDUCTED_DIGITS).to.equal(0n); + expect(CONFIG.defaultOperatorEthFee % ETH_DEDUCTED_DIGITS).to.equal(0n); + }); + + it("All packable config values survive pack/unpack round-trip", async function () { + ({ harness } = await networkHelpers.loadFixture(deployPackedLibFixture)); + + const packableValues: Record = { + networkFeeEth: CONFIG.networkFeeEth, + minimumLiquidationCollateralEth: CONFIG.minimumLiquidationCollateralEth, + minOperatorEthFee: CONFIG.minOperatorEthFee, + maxOperatorEthFee: CONFIG.maxOperatorEthFee, + packableDefaultOpFee: CONFIG.defaultOperatorEthFee, + }; + + for (const [key, value] of Object.entries(packableValues)) { + const packed = await harness.ethPack(value); + const unpacked = await harness.ethUnpack(packed); + expect(unpacked).to.equal(value, `${key}: pack/unpack round-trip failed`); + } + }); + + it("Is reverted with MaxPrecisionExceeded when packing a non-packable value", async function () { + ({ harness } = await networkHelpers.loadFixture(deployPackedLibFixture)); + + const nonPackable = [ + RAW_VALUES.ethNetworkFee, + RAW_VALUES.operatorMinFee, + RAW_VALUES.operatorMaxFee, + RAW_VALUES.defaultOperatorETHFee, + ]; + + for (const value of nonPackable) { + await expect(harness.ethPack(value)) + .to.be.revertedWithCustomError(harness, Errors.MAX_PRECISION_EXCEEDED); + } + }); + + it("Packs minimumLiquidationCollateralEth (940,000,000,000,000) without precision loss", async function () { + ({ harness } = await networkHelpers.loadFixture(deployPackedLibFixture)); + + const packed = await harness.ethPack(CONFIG.minimumLiquidationCollateralEth); + const unpacked = await harness.ethUnpack(packed); + expect(unpacked).to.equal(CONFIG.minimumLiquidationCollateralEth); + }); + }); + + + describe("Liquidation threshold math", () => { + const deployClustersFixture = async () => { + const result = await ssvClustersHarnessFixture(connection, 4, 0n); + const clusters = result.clusters; + + await clusters.mockMinimumBlocksBeforeLiquidation(CONFIG.liquidationThresholdPeriod); + await clusters.mockMinimumLiquidationCollateral( + CONFIG.minimumLiquidationCollateralEth / ETH_DEDUCTED_DIGITS + ); + + return result; + }; + + it("liquidationThresholdPeriod (35,800) is above the system minimum (21,480 blocks)", async function () { + expect(CONFIG.liquidationThresholdPeriod).to.be.greaterThanOrEqual(MINIMAL_LIQUIDATION_THRESHOLD); + }); + + it("Liquidation threshold is dominated by minimumLiquidationCollateral floor", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersFixture); + const [owner, liquidator] = await connection.ethers.getSigners(); + + // Per-operator packed fee = 1,778,800,000 / 100,000 = 17,788 + // Total operator fee (packed, per validator) = 4 × 17,788 = 71,152 + // Network fee (packed) = 3,550,900,000 / 100,000 = 35,509 + // Burn rate per validator per block (packed) = 71,152 + 35,509 = 106,661 + // + // Liquidation threshold (wei) = 35,800 × 106,661 × 100,000 = 381,846,380,000,000 + // minimumLiquidationCollateral = 940,000,000,000,000 > threshold + // → the collateral floor dominates + + const perOperatorPacked = CONFIG.defaultOperatorEthFee / ETH_DEDUCTED_DIGITS; + const totalOperatorFeePacked = perOperatorPacked * 4n; + const networkFeePacked = CONFIG.networkFeeEth / ETH_DEDUCTED_DIGITS; + const burnRatePacked = totalOperatorFeePacked + networkFeePacked; + const thresholdPacked = CONFIG.liquidationThresholdPeriod * burnRatePacked; + const thresholdWei = thresholdPacked * ETH_DEDUCTED_DIGITS; + + expect(perOperatorPacked).to.equal(17_788n); + expect(totalOperatorFeePacked).to.equal(71_152n); + expect(networkFeePacked).to.equal(35_509n); + expect(burnRatePacked).to.equal(106_661n); + expect(thresholdWei).to.equal(381_846_380_000_000n); + + expect(CONFIG.minimumLiquidationCollateralEth).to.be.greaterThan(thresholdWei); + + const largeDeposit = CONFIG.minimumLiquidationCollateralEth * 3n; + const registerTx = await clusters.registerValidator( + makePublicKey(1), + operatorIds, + DEFAULT_SHARES, + EMPTY_CLUSTER, + { value: largeDeposit } + ); + const receipt = await registerTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + + await expect( + clusters.connect(liquidator).liquidate(owner.address, operatorIds, cluster) + ).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + + const toConsume = largeDeposit - CONFIG.minimumLiquidationCollateralEth; + const netFeeIndexDelta = toConsume / ETH_DEDUCTED_DIGITS; + await clusters.mockCurrentNetworkFeeIndex(netFeeIndexDelta); + + await expect( + clusters.connect(liquidator).liquidate(owner.address, operatorIds, cluster) + ).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + + await clusters.mockCurrentNetworkFeeIndex(netFeeIndexDelta + 1n); + + const liquidateTx = await clusters.connect(liquidator).liquidate( + owner.address, operatorIds, cluster + ); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent( + clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED + ); + expect(liquidatedCluster.active).to.equal(false); + }); + }); + + describe("Operator fee boundaries", () => { + const deployOperatorsFixture = async () => { + return ssvOperatorsHarnessFixture( + connection, + CONFIG.maxOperatorEthFee, // max fee + 604_800n, // declare period (7 days) + 604_800n, // execute period (7 days) + 10_000n // max increase 100% + ); + }; + + it("defaultOperatorEthFee (1,778,800,000) is within [minOperatorEthFee, maxOperatorEthFee]", async function () { + expect(CONFIG.defaultOperatorEthFee).to.be.greaterThanOrEqual(CONFIG.minOperatorEthFee); + expect(CONFIG.defaultOperatorEthFee).to.be.lessThanOrEqual(CONFIG.maxOperatorEthFee); + }); + + it("Accepts operator fee at minOperatorEthFee (1,065,200,000)", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + await operators.mockSetMinimumOperatorEthFee(CONFIG.minOperatorEthFee); + + await expect( + operators.registerOperator(makeOperatorKey(1), Number(CONFIG.minOperatorEthFee), false) + ).to.emit(operators, Events.OPERATOR_ADDED); + }); + + it("Accepts operator fee at maxOperatorEthFee (5,326,300,000)", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + await operators.mockSetMinimumOperatorEthFee(CONFIG.minOperatorEthFee); + + await expect( + operators.registerOperator(makeOperatorKey(1), Number(CONFIG.maxOperatorEthFee), false) + ).to.emit(operators, Events.OPERATOR_ADDED); + }); + + it("Is reverted with FeeTooLow when declaring fee one packable step below minimum", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + await operators.mockSetMinimumOperatorEthFee(CONFIG.minOperatorEthFee); + + // 1,065,200,000 - 100,000 = 1,065,100,000 + const feeBelowMin = CONFIG.minOperatorEthFee - ETH_DEDUCTED_DIGITS; + + await operators.registerOperator(makeOperatorKey(1), Number(CONFIG.minOperatorEthFee), false); + + await expect( + operators.declareOperatorFee(1, Number(feeBelowMin)) + ).to.be.revertedWithCustomError(operators, Errors.FEE_TOO_LOW); + }); + + it("Is reverted with FeeTooHigh when declaring fee one packable step above maximum", async function () { + const { operators } = await networkHelpers.loadFixture(deployOperatorsFixture); + await operators.mockSetMinimumOperatorEthFee(CONFIG.minOperatorEthFee); + + // 5,326,300,000 + 100,000 = 5,326,400,000 + const feeAboveMax = CONFIG.maxOperatorEthFee + ETH_DEDUCTED_DIGITS; + + await operators.registerOperator( + makeOperatorKey(1), Number(CONFIG.maxOperatorEthFee), false + ); + + await expect( + operators.declareOperatorFee(1, Number(feeAboveMax)) + ).to.be.revertedWithCustomError(operators, Errors.FEE_TOO_HIGH); + }); + }); + + describe("Cluster burn rate", () => { + it("Computes correct burn rate for 1, 4, and 13 validators", async function () { + const perOperatorPacked = CONFIG.defaultOperatorEthFee / ETH_DEDUCTED_DIGITS; + const networkFeePacked = CONFIG.networkFeeEth / ETH_DEDUCTED_DIGITS; + const perValidatorBurnRate = (perOperatorPacked * 4n) + networkFeePacked; + + const N_BLOCKS = 1000n; + + for (const validatorCount of [1n, 4n, 13n]) { + // Total burn for N_BLOCKS (wei) = perValidatorBurnRate × validatorCount × N_BLOCKS × ETH_DEDUCTED_DIGITS + const expectedBurnWei = perValidatorBurnRate * validatorCount * N_BLOCKS * ETH_DEDUCTED_DIGITS; + + // 1 validator: 106,661 × 1 × 1,000 × 100,000 = 10,666,100,000,000 wei + // 4 validators: 106,661 × 4 × 1,000 × 100,000 = 42,664,400,000,000 wei + // 13 validators:106,661 × 13 × 1,000 × 100,000 = 138,659,300,000,000 wei + if (validatorCount === 1n) expect(expectedBurnWei).to.equal(10_666_100_000_000n); + if (validatorCount === 4n) expect(expectedBurnWei).to.equal(42_664_400_000_000n); + if (validatorCount === 13n) expect(expectedBurnWei).to.equal(138_659_300_000_000n); + } + }); + + it("Deducts networkFeeEth × N_BLOCKS from cluster balance after N blocks", async function () { + // ethNetworkFee left at 0 to avoid auto-accrual from block advancement. + const { clusters, operatorIds } = await ssvClustersHarnessFixture(connection, 4, 0n); + const networkFeePacked = CONFIG.networkFeeEth / ETH_DEDUCTED_DIGITS; + + const N_BLOCKS = 1000n; + const initialDeposit = ethers.parseEther("1"); + + const registerTx = await clusters.registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: initialDeposit } + ); + const receipt = await registerTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + expect(cluster.balance).to.equal(initialDeposit); + + const netFeeIndexDelta = networkFeePacked * N_BLOCKS; + await clusters.mockCurrentNetworkFeeIndex(netFeeIndexDelta); + + const withdrawAmount = 1n; + const withdrawTx = await clusters.withdraw(operatorIds, withdrawAmount, cluster); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfter = parseClusterFromEvent(clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN); + + const networkFeeBurn = netFeeIndexDelta * ETH_DEDUCTED_DIGITS; + // 35,509 × 1,000 × 100,000 = 3,550,900,000,000 wei + expect(networkFeeBurn).to.equal(3_550_900_000_000n); + + const expectedBalance = initialDeposit - networkFeeBurn - withdrawAmount; + expect(clusterAfter.balance).to.equal(expectedBalance); + }); + }); + + describe("Cooldown duration", () => { + const deployStakingFixture = async () => { + return ssvStakingHarnessFixture(connection, CONFIG.cooldownDuration); + }; + + it("Is reverted with NothingToWithdraw before cooldown expires (604,800 seconds)", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + await staking.requestUnstake(STAKE_AMOUNT); + + await networkHelpers.time.increase(CONFIG.cooldownDuration / 2n); + + await expect(staking.withdrawUnlocked()) + .to.be.revertedWithCustomError(staking, Errors.NOTHING_TO_WITHDRAW); + }); + + it("Can claim after 604,800 seconds (7 days) elapse", async function () { + const { staking, ssvToken } = await networkHelpers.loadFixture(deployStakingFixture); + const [staker] = await connection.ethers.getSigners(); + + await ssvToken.approve(await staking.getAddress(), STAKE_AMOUNT); + await staking.stake(STAKE_AMOUNT); + await staking.requestUnstake(STAKE_AMOUNT); + + await networkHelpers.time.increase(CONFIG.cooldownDuration + 1n); + + const balanceBefore = await ssvToken.balanceOf(staker.address); + const tx = await staking.withdrawUnlocked(); + await expect(tx) + .to.emit(staking, Events.UNSTAKE_WITHDRAWN) + .withArgs(staker.address, STAKE_AMOUNT); + + const balanceAfter = await ssvToken.balanceOf(staker.address); + expect(balanceAfter - balanceBefore).to.equal(STAKE_AMOUNT); + }); + + it("Stores cooldownDuration as 604,800 seconds (not blocks)", async function () { + const { staking } = await networkHelpers.loadFixture(deployStakingFixture); + const storedCooldown = await staking.getCooldownDuration(); + expect(storedCooldown).to.equal(CONFIG.cooldownDuration); + }); + }); + + describe("EB update frequency", () => { + const deployDAOFixture = async () => { + const { dao } = await ssvDAOHarnessFixture(connection); + return { dao }; + }; + + it("Stores minBlocksBetweenUpdates", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOFixture); + const value = Number(CONFIG.minBlocksBetweenUpdates); + + await dao.updateMinBlocksBetweenUpdates(value); + + expect(await dao.getMinBlocksBetweenUpdates()).to.equal(value); + }); + }); + + describe("Quorum", () => { + let oracle1: HardhatEthersSigner; + let oracle2: HardhatEthersSigner; + let oracle3: HardhatEthersSigner; + let oracle4: HardhatEthersSigner; + let owner: HardhatEthersSigner; + + const totalSupply = ethers.parseEther("1000"); + + before(async function () { + [owner, oracle1, oracle2, oracle3, oracle4] = await connection.ethers.getSigners(); + }); + + const deployDAOWithMainnetQuorumFixture = async () => { + const { dao, cssv } = await ssvDAOHarnessFixture(connection); + + await dao.mockSetOracle(1, oracle1.address); + await dao.mockSetOracle(2, oracle2.address); + await dao.mockSetOracle(3, oracle3.address); + await dao.mockSetOracle(4, oracle4.address); + await dao.mockupdateQuorumBps(Number(CONFIG.quorumBps)); + + await cssv.mint(owner.address, totalSupply); + + return { dao, cssv }; + }; + + it("2 votes out of 4 should NOT reach quorum (50% < 75%)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOWithMainnetQuorumFixture); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("mainnet-quorum-test")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + const tx1 = await dao.connect(oracle1).commitRoot(merkleRoot, blockNum); + await expect(tx1).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx1).to.not.emit(dao, Events.ROOT_COMMITTED); + + const tx2 = await dao.connect(oracle2).commitRoot(merkleRoot, blockNum); + await expect(tx2).to.emit(dao, Events.WEIGHTED_ROOT_PROPOSED); + await expect(tx2).to.not.emit(dao, Events.ROOT_COMMITTED); + + expect(await dao.getEBRoot(blockNum)).to.equal(ethers.ZeroHash); + }); + + it("3 votes out of 4 should reach quorum (75% >= 75%)", async function () { + const { dao } = await networkHelpers.loadFixture(deployDAOWithMainnetQuorumFixture); + + const merkleRoot = ethers.keccak256(ethers.toUtf8Bytes("mainnet-quorum-test-2")); + const blockNum = await connection.ethers.provider.getBlockNumber(); + + await dao.connect(oracle1).commitRoot(merkleRoot, blockNum); + await dao.connect(oracle2).commitRoot(merkleRoot, blockNum); + + const tx3 = await dao.connect(oracle3).commitRoot(merkleRoot, blockNum); + await expect(tx3).to.emit(dao, Events.ROOT_COMMITTED).withArgs(merkleRoot, blockNum); + + expect(await dao.getEBRoot(blockNum)).to.equal(merkleRoot); + }); + }); + + describe("Liquidation collateral", () => { + const deployClustersFixture = async () => { + const result = await ssvClustersHarnessFixture(connection, 4, 0n); + const clusters = result.clusters; + + await clusters.mockMinimumBlocksBeforeLiquidation(CONFIG.liquidationThresholdPeriod); + await clusters.mockMinimumLiquidationCollateral( + CONFIG.minimumLiquidationCollateralEth / ETH_DEDUCTED_DIGITS + ); + + return result; + }; + + it("Is reverted when liquidating a cluster with balance above minimumLiquidationCollateral", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersFixture); + const [clusterOwner, liquidator] = await connection.ethers.getSigners(); + + const depositAmount = CONFIG.minimumLiquidationCollateralEth * 2n; + + const registerTx = await clusters.registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositAmount } + ); + const receipt = await registerTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + + await expect( + clusters.connect(liquidator).liquidate(clusterOwner.address, operatorIds, cluster) + ).to.be.revertedWithCustomError(clusters, Errors.CLUSTER_NOT_LIQUIDATABLE); + }); + + it("Liquidates cluster when balance drops below minimumLiquidationCollateral", async function () { + const { clusters, operatorIds } = await networkHelpers.loadFixture(deployClustersFixture); + const [clusterOwner, liquidator] = await connection.ethers.getSigners(); + + const depositAmount = CONFIG.minimumLiquidationCollateralEth * 2n; + const registerTx = await clusters.registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: depositAmount } + ); + const receipt = await registerTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + + const balanceToConsume = depositAmount - CONFIG.minimumLiquidationCollateralEth + ETH_DEDUCTED_DIGITS; + const indexUnits = balanceToConsume / ETH_DEDUCTED_DIGITS; + await clusters.mockCurrentNetworkFeeIndex(indexUnits); + + const liquidateTx = await clusters.connect(liquidator).liquidate( + clusterOwner.address, operatorIds, cluster + ); + const liquidateReceipt = await liquidateTx.wait(); + const liquidatedCluster = parseClusterFromEvent( + clusters, liquidateReceipt, Events.CLUSTER_LIQUIDATED + ); + expect(liquidatedCluster.active).to.equal(false); + }); + }); + + + describe("Long-running clusters (1 year simulation)", () => { + it("Fee indices remain within uint64 bounds after 1 year (~2,628,000 blocks)", async function () { + const ONE_YEAR_BLOCKS = 2_628_000n; + const networkFeePacked = CONFIG.networkFeeEth / ETH_DEDUCTED_DIGITS; // 35,509 + const perOperatorPacked = CONFIG.defaultOperatorEthFee / ETH_DEDUCTED_DIGITS; // 17,788 + + const operatorIndexDelta = perOperatorPacked * ONE_YEAR_BLOCKS; + const networkFeeIndexDelta = networkFeePacked * ONE_YEAR_BLOCKS; + const maxUint64 = (1n << 64n) - 1n; + + // 17,788 × 2,628,000 = 46,746,864,000 + expect(operatorIndexDelta).to.equal(46_746_864_000n); + // 35,509 × 2,628,000 = 93,317,652,000 + expect(networkFeeIndexDelta).to.equal(93_317_652_000n); + expect(operatorIndexDelta).to.be.lessThan(maxUint64); + expect(networkFeeIndexDelta).to.be.lessThan(maxUint64); + + const totalBurnPacked = (perOperatorPacked * 4n + networkFeePacked) * ONE_YEAR_BLOCKS; + const totalBurnWei = totalBurnPacked * ETH_DEDUCTED_DIGITS; + + // (1,778,800,000 / 100,000 × 4 + 3,550,900,000 / 100,000) × 2,628,000 × 100,000 + // = (17,788 × 4 + 35,509) × 2,628,000 × 100,000 + // = 106,661 × 2,628,000 × 100,000 + // = 28,030,510,800,000,000 + expect(totalBurnWei).to.equal(28_030_510_800_000_000n); + + const { clusters, operatorIds } = await ssvClustersHarnessFixture(connection, 4, 0n); + + // Network fee burn (1 year) = 35,509 × 2,628,000 × 100,000 = 9,331,765,200,000,000 wei + const networkFeeBurnWei = networkFeeIndexDelta * ETH_DEDUCTED_DIGITS; + expect(networkFeeBurnWei).to.equal(9_331_765_200_000_000n); + + const initialDeposit = networkFeeBurnWei * 2n; + + const registerTx = await clusters.registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: initialDeposit } + ); + const receipt = await registerTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + + await clusters.mockCurrentNetworkFeeIndex(networkFeeIndexDelta); + + const withdrawAmount = 1n; + const withdrawTx = await clusters.withdraw(operatorIds, withdrawAmount, cluster); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfterYear = parseClusterFromEvent( + clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN + ); + + expect(clusterAfterYear.active).to.equal(true); + }); + + it("Balance accounting remains correct after 1 year", async function () { + const ONE_YEAR_BLOCKS = 2_628_000n; + const networkFeePacked = CONFIG.networkFeeEth / ETH_DEDUCTED_DIGITS; // 35,509 + + const { clusters, operatorIds } = await ssvClustersHarnessFixture(connection, 4, 0n); + + // = 35,509 × 2,628,000 × 100,000 = 9,331,765,200,000,000 wei ≈ 0.00933 ETH + const networkFeeBurn = networkFeePacked * ONE_YEAR_BLOCKS * ETH_DEDUCTED_DIGITS; + expect(networkFeeBurn).to.equal(9_331_765_200_000_000n); + + const initialDeposit = networkFeeBurn * 15n; + + const registerTx = await clusters.registerValidator( + makePublicKey(1), operatorIds, DEFAULT_SHARES, EMPTY_CLUSTER, + { value: initialDeposit } + ); + const receipt = await registerTx.wait(); + const cluster = parseClusterFromEvent(clusters, receipt, Events.VALIDATOR_ADDED); + expect(cluster.balance).to.equal(initialDeposit); + + const netFeeIndexDelta = networkFeePacked * ONE_YEAR_BLOCKS; + await clusters.mockCurrentNetworkFeeIndex(netFeeIndexDelta); + + const withdrawAmount = 1n; + const withdrawTx = await clusters.withdraw(operatorIds, withdrawAmount, cluster); + const withdrawReceipt = await withdrawTx.wait(); + const clusterAfter = parseClusterFromEvent( + clusters, withdrawReceipt, Events.CLUSTER_WITHDRAWN + ); + + const expectedBalance = initialDeposit - networkFeeBurn - withdrawAmount; + expect(clusterAfter.balance).to.equal(expectedBalance); + expect(clusterAfter.active).to.equal(true); + }); + }); +}); diff --git a/test/unit/packedLib.test.ts b/test/unit/packedLib.test.ts new file mode 100644 index 000000000..90919ee11 --- /dev/null +++ b/test/unit/packedLib.test.ts @@ -0,0 +1,439 @@ +import { expect } from "chai"; +import type { NetworkConnection } from "hardhat/types/network"; +import type { NetworkHelpersType } from "../common/types.ts"; +import { setupTestContext } from "../common/helpers.ts"; +import { ETH_DEDUCTED_DIGITS, DEDUCTED_DIGITS } from "../common/constants.ts"; +import { Errors } from "../common/errors.ts"; + +describe("SSVPackedLib and SSVCoreTypes", async () => { + let connection: NetworkConnection<"generic">; + let networkHelpers: NetworkHelpersType; + let harness: any; + + before(async function () { + ({ connection, networkHelpers } = await setupTestContext()); + }); + + const deployFixture = async () => { + const contract = await connection.ethers.deployContract("PackedLibHarness"); + await contract.waitForDeployment(); + return { harness: contract }; + }; + + beforeEach(async function () { + ({ harness } = await networkHelpers.loadFixture(deployFixture)); + }); + + describe("SSVCoreTypes constants", () => { + it("PACKED_ETH_ZERO is 0", async function () { + expect(await harness.getPackedEthZero()).to.equal(0n); + }); + + it("PACKED_SSV_ZERO is 0", async function () { + expect(await harness.getPackedSsvZero()).to.equal(0n); + }); + + it("VERSION_SSV is 0", async function () { + expect(await harness.getVersionSSV()).to.equal(0n); + }); + + it("VERSION_ETH is 1", async function () { + expect(await harness.getVersionETH()).to.equal(1n); + }); + + it("VERSION_UNDEFINED is type(uint8).max (255)", async function () { + expect(await harness.getVersionUndefined()).to.equal(255n); + }); + + it("DEFAULT_OPERATOR_ETH_FEE is 1778_800_000", async function () { + expect(await harness.getDefaultOperatorEthFee()).to.equal(1778_800_000n); + }); + + it("DEDUCTED_DIGITS is 10_000_000", async function () { + expect(await harness.getDeductedDigits()).to.equal(DEDUCTED_DIGITS); + }); + + it("ETH_DEDUCTED_DIGITS is 100_000", async function () { + expect(await harness.getEthDeductedDigits()).to.equal(ETH_DEDUCTED_DIGITS); + }); + }); + + describe("PackedETHLib", () => { + describe("pack / unpack", () => { + it("Packs a valid ETH value", async function () { + const value = 1_000_000n; + const packed = await harness.ethPack(value); + expect(packed).to.equal(value / ETH_DEDUCTED_DIGITS); + }); + + it("Unpacks a packed ETH value back to original", async function () { + const value = 5_000_000n; + const packed = await harness.ethPack(value); + const unpacked = await harness.ethUnpack(packed); + expect(unpacked).to.equal(value); + }); + + it("Pack then unpack is identity for aligned values", async function () { + const value = 123_456_700_000n; + const packed = await harness.ethPack(value); + const unpacked = await harness.ethUnpack(packed); + expect(unpacked).to.equal(value); + }); + + it("Packs zero", async function () { + expect(await harness.ethPack(0n)).to.equal(0n); + }); + + it("Unpacks zero", async function () { + expect(await harness.ethUnpack(0n)).to.equal(0n); + }); + + it("Packs the maximum uint64 * ETH_DEDUCTED_DIGITS value", async function () { + const maxUint64 = (1n << 64n) - 1n; + const maxValue = maxUint64 * ETH_DEDUCTED_DIGITS; + const packed = await harness.ethPack(maxValue); + expect(packed).to.equal(maxUint64); + }); + + it("Reverts with MaxValueExceeded when value exceeds uint64 range", async function () { + const maxUint64 = (1n << 64n) - 1n; + const tooLarge = (maxUint64 + 1n) * ETH_DEDUCTED_DIGITS; + await expect(harness.ethPack(tooLarge)) + .to.be.revertedWithCustomError(harness, Errors.MAX_VALUE_EXCEEDED); + }); + + it("Reverts with MaxPrecisionExceeded when value is not aligned", async function () { + await expect(harness.ethPack(1n)) + .to.be.revertedWithCustomError(harness, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Reverts with MaxPrecisionExceeded for ETH_DEDUCTED_DIGITS - 1", async function () { + await expect(harness.ethPack(ETH_DEDUCTED_DIGITS - 1n)) + .to.be.revertedWithCustomError(harness, Errors.MAX_PRECISION_EXCEEDED); + }); + }); + + describe("raw", () => { + it("Returns the raw uint64 value", async function () { + expect(await harness.ethRaw(42n)).to.equal(42n); + }); + + it("Returns 0 for zero", async function () { + expect(await harness.ethRaw(0n)).to.equal(0n); + }); + }); + + describe("comparison operators", () => { + it("eq returns true for equal values", async function () { + expect(await harness.ethEq(10n, 10n)).to.equal(true); + }); + + it("eq returns false for different values", async function () { + expect(await harness.ethEq(10n, 20n)).to.equal(false); + }); + + it("neq returns true for different values", async function () { + expect(await harness.ethNeq(10n, 20n)).to.equal(true); + }); + + it("neq returns false for equal values", async function () { + expect(await harness.ethNeq(10n, 10n)).to.equal(false); + }); + + it("gt returns true when a > b", async function () { + expect(await harness.ethGt(20n, 10n)).to.equal(true); + }); + + it("gt returns false when a == b", async function () { + expect(await harness.ethGt(10n, 10n)).to.equal(false); + }); + + it("gt returns false when a < b", async function () { + expect(await harness.ethGt(5n, 10n)).to.equal(false); + }); + + it("gte returns true when a > b", async function () { + expect(await harness.ethGte(20n, 10n)).to.equal(true); + }); + + it("gte returns true when a == b", async function () { + expect(await harness.ethGte(10n, 10n)).to.equal(true); + }); + + it("gte returns false when a < b", async function () { + expect(await harness.ethGte(5n, 10n)).to.equal(false); + }); + + it("lt returns true when a < b", async function () { + expect(await harness.ethLt(5n, 10n)).to.equal(true); + }); + + it("lt returns false when a == b", async function () { + expect(await harness.ethLt(10n, 10n)).to.equal(false); + }); + + it("lt returns false when a > b", async function () { + expect(await harness.ethLt(20n, 10n)).to.equal(false); + }); + + it("lte returns true when a < b", async function () { + expect(await harness.ethLte(5n, 10n)).to.equal(true); + }); + + it("lte returns true when a == b", async function () { + expect(await harness.ethLte(10n, 10n)).to.equal(true); + }); + + it("lte returns false when a > b", async function () { + expect(await harness.ethLte(20n, 10n)).to.equal(false); + }); + + it("Comparisons work with zero", async function () { + expect(await harness.ethEq(0n, 0n)).to.equal(true); + expect(await harness.ethGt(1n, 0n)).to.equal(true); + expect(await harness.ethLt(0n, 1n)).to.equal(true); + expect(await harness.ethGte(0n, 0n)).to.equal(true); + expect(await harness.ethLte(0n, 0n)).to.equal(true); + }); + }); + + describe("arithmetic operators", () => { + it("add returns the sum of two packed values", async function () { + expect(await harness.ethAdd(10n, 20n)).to.equal(30n); + }); + + it("add with zero", async function () { + expect(await harness.ethAdd(10n, 0n)).to.equal(10n); + expect(await harness.ethAdd(0n, 10n)).to.equal(10n); + }); + + it("sub returns the difference of two packed values", async function () { + expect(await harness.ethSub(30n, 10n)).to.equal(20n); + }); + + it("sub to zero", async function () { + expect(await harness.ethSub(10n, 10n)).to.equal(0n); + }); + + it("sub reverts on underflow", async function () { + await expect(harness.ethSub(5n, 10n)).to.be.revertedWithPanic(0x11); + }); + + it("add reverts on overflow", async function () { + const maxUint64 = (1n << 64n) - 1n; + await expect(harness.ethAdd(maxUint64, 1n)).to.be.revertedWithPanic(0x11); + }); + }); + }); + + describe("PackedSSVLib", () => { + describe("pack / unpack", () => { + it("Packs a valid SSV value", async function () { + const value = 10_000_000n; + const packed = await harness.ssvPack(value); + expect(packed).to.equal(value / DEDUCTED_DIGITS); + }); + + it("Unpacks a packed SSV value back to original", async function () { + const value = 50_000_000n; + const packed = await harness.ssvPack(value); + const unpacked = await harness.ssvUnpack(packed); + expect(unpacked).to.equal(value); + }); + + it("Pack then unpack is identity for aligned values", async function () { + const value = 1_234_560_000_000n; + const packed = await harness.ssvPack(value); + const unpacked = await harness.ssvUnpack(packed); + expect(unpacked).to.equal(value); + }); + + it("Packs zero", async function () { + expect(await harness.ssvPack(0n)).to.equal(0n); + }); + + it("Unpacks zero", async function () { + expect(await harness.ssvUnpack(0n)).to.equal(0n); + }); + + it("Packs the maximum uint64 * DEDUCTED_DIGITS value", async function () { + const maxUint64 = (1n << 64n) - 1n; + const maxValue = maxUint64 * DEDUCTED_DIGITS; + const packed = await harness.ssvPack(maxValue); + expect(packed).to.equal(maxUint64); + }); + + it("Reverts with MaxValueExceeded when value exceeds uint64 range", async function () { + const maxUint64 = (1n << 64n) - 1n; + const tooLarge = (maxUint64 + 1n) * DEDUCTED_DIGITS; + await expect(harness.ssvPack(tooLarge)) + .to.be.revertedWithCustomError(harness, Errors.MAX_VALUE_EXCEEDED); + }); + + it("Reverts with MaxPrecisionExceeded when value is not aligned", async function () { + await expect(harness.ssvPack(1n)) + .to.be.revertedWithCustomError(harness, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("Reverts with MaxPrecisionExceeded for DEDUCTED_DIGITS - 1", async function () { + await expect(harness.ssvPack(DEDUCTED_DIGITS - 1n)) + .to.be.revertedWithCustomError(harness, Errors.MAX_PRECISION_EXCEEDED); + }); + }); + + describe("raw", () => { + it("Returns the raw uint64 value", async function () { + expect(await harness.ssvRaw(42n)).to.equal(42n); + }); + + it("Returns 0 for zero", async function () { + expect(await harness.ssvRaw(0n)).to.equal(0n); + }); + }); + + describe("comparison operators", () => { + it("eq returns true for equal values", async function () { + expect(await harness.ssvEq(10n, 10n)).to.equal(true); + }); + + it("eq returns false for different values", async function () { + expect(await harness.ssvEq(10n, 20n)).to.equal(false); + }); + + it("neq returns true for different values", async function () { + expect(await harness.ssvNeq(10n, 20n)).to.equal(true); + }); + + it("neq returns false for equal values", async function () { + expect(await harness.ssvNeq(10n, 10n)).to.equal(false); + }); + + it("gt returns true when a > b", async function () { + expect(await harness.ssvGt(20n, 10n)).to.equal(true); + }); + + it("gt returns false when a == b", async function () { + expect(await harness.ssvGt(10n, 10n)).to.equal(false); + }); + + it("gt returns false when a < b", async function () { + expect(await harness.ssvGt(5n, 10n)).to.equal(false); + }); + + it("lt returns true when a < b", async function () { + expect(await harness.ssvLt(5n, 10n)).to.equal(true); + }); + + it("lt returns false when a == b", async function () { + expect(await harness.ssvLt(10n, 10n)).to.equal(false); + }); + + it("lt returns false when a > b", async function () { + expect(await harness.ssvLt(20n, 10n)).to.equal(false); + }); + + it("Comparisons work with zero", async function () { + expect(await harness.ssvEq(0n, 0n)).to.equal(true); + expect(await harness.ssvGt(1n, 0n)).to.equal(true); + expect(await harness.ssvLt(0n, 1n)).to.equal(true); + }); + }); + + describe("arithmetic operators", () => { + it("add returns the sum of two packed values", async function () { + expect(await harness.ssvAdd(10n, 20n)).to.equal(30n); + }); + + it("add with zero", async function () { + expect(await harness.ssvAdd(10n, 0n)).to.equal(10n); + expect(await harness.ssvAdd(0n, 10n)).to.equal(10n); + }); + + it("sub returns the difference of two packed values", async function () { + expect(await harness.ssvSub(30n, 10n)).to.equal(20n); + }); + + it("sub to zero", async function () { + expect(await harness.ssvSub(10n, 10n)).to.equal(0n); + }); + + it("sub reverts on underflow", async function () { + await expect(harness.ssvSub(5n, 10n)).to.be.revertedWithPanic(0x11); + }); + + it("add reverts on overflow", async function () { + const maxUint64 = (1n << 64n) - 1n; + await expect(harness.ssvAdd(maxUint64, 1n)).to.be.revertedWithPanic(0x11); + }); + }); + }); + + describe("ETH vs SSV scaling factor differences", () => { + it("Same wei value produces different packed values for ETH vs SSV", async function () { + const value = 100_000_000_000_000n; + const ethPacked = await harness.ethPack(value); + const ssvPacked = await harness.ssvPack(value); + expect(ethPacked).to.equal(1_000_000_000n); + expect(ssvPacked).to.equal(10_000_000n); + expect(ethPacked).to.be.greaterThan(ssvPacked); + }); + + it("ETH allows finer granularity than SSV", async function () { + const fineValue = ETH_DEDUCTED_DIGITS; + const ethPacked = await harness.ethPack(fineValue); + expect(ethPacked).to.equal(1n); + + await expect(harness.ssvPack(fineValue)) + .to.be.revertedWithCustomError(harness, Errors.MAX_PRECISION_EXCEEDED); + }); + + it("DEFAULT_OPERATOR_ETH_FEE is packable as ETH", async function () { + const fee = await harness.getDefaultOperatorEthFee(); + const packed = await harness.ethPack(fee); + expect(packed).to.equal(fee / ETH_DEDUCTED_DIGITS); + + const unpacked = await harness.ethUnpack(packed); + expect(unpacked).to.equal(fee); + }); + }); + + // ============ _safeUint64 ============ + + describe("_safeUint64", () => { + const MAX_UINT64 = (1n << 64n) - 1n; + + it("passes through zero", async function () { + expect(await harness.safeUint64(0n)).to.equal(0n); + }); + + it("passes through value within uint64 range", async function () { + expect(await harness.safeUint64(42n)).to.equal(42n); + }); + + it("passes through max uint64", async function () { + expect(await harness.safeUint64(MAX_UINT64)).to.equal(MAX_UINT64); + }); + + it("reverts on max uint64 + 1", async function () { + await expect(harness.safeUint64(MAX_UINT64 + 1n)) + .to.be.revertedWithCustomError(harness, "SafeCastOverflow"); + }); + + it("reverts on max uint128", async function () { + const MAX_UINT128 = (1n << 128n) - 1n; + await expect(harness.safeUint64(MAX_UINT128)) + .to.be.revertedWithCustomError(harness, "SafeCastOverflow"); + }); + + it("reverts on realistic overflow scenario (operator earnings delta)", async function () { + // Simulates: (blockDiffEthFee * effectiveVUnits) / BPS_DENOMINATOR + // where both inputs are large uint64 values + const blockDiffEthFee = MAX_UINT64; + const effectiveVUnits = MAX_UINT64; + const delta = (blockDiffEthFee * effectiveVUnits) / 10_000n; + // delta ≈ 3.39e34, far exceeds uint64 max ≈ 1.84e19 + await expect(harness.safeUint64(delta)) + .to.be.revertedWithCustomError(harness, "SafeCastOverflow"); + }); + }); +}); diff --git a/test/unit/run-tests.sh b/test/unit/run-tests.sh new file mode 100755 index 000000000..8e12d29c9 --- /dev/null +++ b/test/unit/run-tests.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Move to repo root +cd "$(dirname "${BASH_SOURCE[0]}")/../.." + +echo "Running SSVClusters tests..." +npx hardhat test test/unit/SSVClusters/*.test.ts + +echo "" +echo "Running SSVOperators tests..." +npx hardhat test test/unit/SSVOperators/*.test.ts + +echo "" +echo "Running SSVDAO tests..." +npx hardhat test test/unit/SSVDAO/*.test.ts diff --git a/test/validators/exit.ts b/test/validators/exit.ts deleted file mode 100644 index 6af881286..000000000 --- a/test/validators/exit.ts +++ /dev/null @@ -1,411 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, ssvToken: any, minDepositAmount: BigInt, firstCluster: any; - -describe('Exit Validator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvToken = metadata.ssvToken; - - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * CONFIG.minimalOperatorFee * 4n; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - // Register a validator - // cold register - await coldRegisterValidator(); - - firstCluster = ( - await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - }); - - it('Exiting a validator emits "ValidatorExited"', async () => { - await assertEvent( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(1), firstCluster.operatorIds], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorExited', - argNames: ['owner', 'operatorIds', 'publicKey'], - argValuesList: [[owners[1].account.address, firstCluster.operatorIds, DataGenerator.publicKey(1)]], - }, - ], - ); - }); - - it('Exiting a validator gas limit', async () => { - await trackGas( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(1), firstCluster.operatorIds], { - account: owners[1].account, - }), - [GasGroup.VALIDATOR_EXIT], - ); - }); - - it('Exiting one of the validators in a cluster emits "ValidatorExited"', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[1].account, - }); - - await ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - firstCluster.cluster, - ], - { - account: owners[1].account, - }, - ); - - await assertEvent( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(2), firstCluster.operatorIds], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorExited', - argNames: ['owner', 'operatorIds', 'publicKey'], - argValuesList: [[owners[1].account.address, firstCluster.operatorIds, DataGenerator.publicKey(2)]], - }, - ], - ); - }); - - it('Exiting a removed validator reverts "IncorrectValidatorStateWithData"', async () => { - await ssvNetwork.write.removeValidator( - [DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], - { - account: owners[1].account, - }, - ); - - await expect( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(1), firstCluster.operatorIds], { - account: owners[1].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', DataGenerator.publicKey(1)); - }); - - it('Exiting a non-existing validator reverts "IncorrectValidatorStateWithData"', async () => { - await expect( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(12), firstCluster.operatorIds], { - account: owners[1].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', DataGenerator.publicKey(12)); - }); - - it('Exiting a validator with empty operator list reverts "IncorrectValidatorStateWithData"', async () => { - await expect( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(1), []], { - account: owners[1].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', DataGenerator.publicKey(1)); - }); - - it('Exiting a validator with empty public key reverts "IncorrectValidatorStateWithData"', async () => { - await expect( - ssvNetwork.write.exitValidator(['0x', firstCluster.operatorIds], { - account: owners[1].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', '0x'); - }); - - it('Exiting a validator using the wrong account reverts "IncorrectValidatorStateWithData"', async () => { - await expect( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(1), firstCluster.operatorIds], { - account: owners[2].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', DataGenerator.publicKey(1)); - }); - - it('Exiting a validator with incorrect operators (unsorted list) reverts with "IncorrectValidatorStateWithData"', async () => { - await expect( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(1), [4, 3, 2, 1]], { - account: owners[1].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', DataGenerator.publicKey(1)); - }); - - it('Exiting a validator with incorrect operators (too many operators) reverts with "IncorrectValidatorState"', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * CONFIG.minimalOperatorFee * 13n; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { - account: owners[2].account, - }); - - const register = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(2, 2, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { - account: owners[2].account, - }, - ), - ); - const secondCluster = register.eventsByName.ValidatorAdded[0].args; - - await assertEvent( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(2), secondCluster.operatorIds], { - account: owners[2].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorExited', - argNames: ['owner', 'operatorIds', 'publicKey'], - argValuesList: [[owners[2].account.address, secondCluster.operatorIds, DataGenerator.publicKey(2)]], - }, - ], - ); - }); - - it('Exiting a validator with incorrect operators reverts with "IncorrectValidatorStateWithData"', async () => { - await expect( - ssvNetwork.write.exitValidator([DataGenerator.publicKey(1), [1, 2, 3, 5]], { - account: owners[1].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', DataGenerator.publicKey(1)); - }); - - it('Bulk exiting a validator emits "ValidatorExited"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await assertEvent( - ssvNetwork.write.bulkExitValidator([pks, args.operatorIds], { - account: owners[2].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorExited', - }, - ], - ); - }); - - it('Bulk exiting 10 validator (4 operators cluster) gas limit', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkExitValidator([pks, args.operatorIds], { - account: owners[2].account, - }), - [GasGroup.BULK_EXIT_10_VALIDATOR_4], - ); - }); - - it('Bulk exiting 10 validator (7 operators cluster) gas limit', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * CONFIG.minimalOperatorFee * 7n; - - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[7], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkExitValidator([pks, args.operatorIds], { - account: owners[2].account, - }), - [GasGroup.BULK_EXIT_10_VALIDATOR_7], - ); - }); - - it('Bulk exiting 10 validator (10 operators cluster) gas limit', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * CONFIG.minimalOperatorFee * 10n; - - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[10], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkExitValidator([pks, args.operatorIds], { - account: owners[2].account, - }), - [GasGroup.BULK_EXIT_10_VALIDATOR_10], - ); - }); - - it('Bulk exiting 10 validator (13 operators cluster) gas limit', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * CONFIG.minimalOperatorFee * 13n; - - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[13], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkExitValidator([pks, args.operatorIds], { - account: owners[2].account, - }), - [GasGroup.BULK_EXIT_10_VALIDATOR_13], - ); - }); - - it('Bulk exiting removed validators reverts "IncorrectValidatorStateWithData"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks.slice(0, 5), args.operatorIds, args.cluster], { - account: owners[2].account, - }), - ); - - await expect( - ssvNetwork.write.bulkExitValidator([pks.slice(0, 5), args.operatorIds], { - account: owners[2].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', pks[0]); - }); - - it('Bulk exiting non-existing validators reverts "IncorrectValidatorStateWithData"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - pks[4] = '0xabcd1234'; - - await expect( - ssvNetwork.write.bulkExitValidator([pks, args.operatorIds], { - account: owners[2].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', pks[4]); - }); - - it('Bulk exiting validators with empty operator list reverts "IncorrectValidatorStateWithData"', async () => { - const { pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await expect( - ssvNetwork.write.bulkExitValidator([pks, []], { - account: owners[2].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', pks[0]); - }); - - it('Bulk exiting validators with empty public key reverts "ValidatorDoesNotExist"', async () => { - const { args } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await expect( - ssvNetwork.write.bulkExitValidator([[], args.operatorIds], { - account: owners[2].account, - }), - ).to.be.rejectedWith('ValidatorDoesNotExist'); - }); - - it('Bulk exiting validators using the wrong account reverts "IncorrectValidatorStateWithData"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await expect( - ssvNetwork.write.bulkExitValidator([pks, args.operatorIds], { - account: owners[3].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', pks[0]); - }); - - it('Bulk exiting validators with incorrect operators (unsorted list) reverts with "IncorrectValidatorStateWithData"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await expect( - ssvNetwork.write.bulkExitValidator([pks, [4, 3, 2, 1]], { - account: owners[1].account, - }), - ).to.be.rejectedWith(ssvNetwork, 'IncorrectValidatorStateWithData', pks[0]); - }); -}); diff --git a/test/validators/register.ts b/test/validators/register.ts deleted file mode 100644 index 37a267c29..000000000 --- a/test/validators/register.ts +++ /dev/null @@ -1,1334 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - bulkRegisterValidators, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; - -let ssvNetwork: any, ssvViews: any, ssvToken: any, minDepositAmount: BigInt, cluster1: any; - -describe('Register Validator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * CONFIG.minimalOperatorFee * 13n; - - cluster1 = ( - await bulkRegisterValidators(6, 1, DEFAULT_OPERATOR_IDS[4], 1000000000000000n, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - }); - - it('Register validator with 4 operators emits "ValidatorAdded"', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - - await assertEvent( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - }, - { - contract: ssvToken, - eventName: 'Transfer', - }, - ], - ); - }); - - it('Register validator with 4 operators gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const balance = await ssvToken.read.balanceOf([ssvNetwork.address]); - - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ); - - expect(await ssvToken.read.balanceOf([ssvNetwork.address])).to.be.equal(balance + minDepositAmount); - }); - - it('Register 2 validators into the same cluster gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER], - ); - }); - - it('Register 2 validators into the same cluster and 1 validator into a new cluster gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER], - ); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[2].account }); - - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(4), - [2, 3, 4, 5], - await DataGenerator.shares(2, 4, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ); - }); - - it('Register 2 validators into the same cluster with one time deposit gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { account: owners[1].account }); - - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount * 2n, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE], - ); - - const args = eventsByName.ValidatorAdded[0].args; - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - 0, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT], - ); - }); - - it('Bulk register 10 validators with 4 operators into the same cluster', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.bulkRegisterValidator( - [ - [DataGenerator.publicKey(12)], - DEFAULT_OPERATOR_IDS[4], - [await DataGenerator.shares(1, 11, DEFAULT_OPERATOR_IDS[4])], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ), - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await bulkRegisterValidators(1, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, args.cluster, [ - GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_4, - ]); - }); - - it('Bulk register 10 validators with 4 operators new cluster', async () => { - await bulkRegisterValidators( - 1, - 10, - DEFAULT_OPERATOR_IDS[4], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_4], - ); - }); - - // 7 operators - - it('Register validator with 7 operators gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[7], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[7]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7], - ); - }); - - it('Register 2 validators with 7 operators into the same cluster gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[7], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[7]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7], - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[7], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[7]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_7], - ); - }); - - it('Register 2 validators with 7 operators into the same cluster and 1 validator into a new cluster with 7 operators gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[7], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[7]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7], - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[7], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[7]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_7], - ); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[2].account }); - - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(4), - [2, 3, 4, 5, 6, 7, 8], - await DataGenerator.shares(2, 4, DEFAULT_OPERATOR_IDS[7]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7], - ); - }); - - it('Register 2 validators with 7 operators into the same cluster with one time deposit gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { account: owners[1].account }); - - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[7], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[7]), - minDepositAmount * 2n, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_7], - ); - - const args = eventsByName.ValidatorAdded[0].args; - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[7], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[7]), - 0, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_7], - ); - }); - - it('Bulk register 10 validators with 7 operators into the same cluster', async () => { - const operatorIds = DEFAULT_OPERATOR_IDS[7]; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.bulkRegisterValidator( - [ - [DataGenerator.publicKey(12)], - operatorIds, - [await DataGenerator.shares(1, 11, operatorIds)], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ), - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await bulkRegisterValidators(1, 10, operatorIds, minDepositAmount, args.cluster, [ - GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_7, - ]); - }); - - it('Bulk register 10 validators with 7 operators new cluster', async () => { - await bulkRegisterValidators( - 1, - 10, - DEFAULT_OPERATOR_IDS[7], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_7], - ); - }); - - // 10 operators - - it('Register validator with 10 operators gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[10]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10], - ); - }); - - it('Register 2 validators with 10 operators into the same cluster gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[10]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10], - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[10]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_10], - ); - }); - - it('Register 2 validators with 10 operators into the same cluster and 1 validator into a new cluster with 10 operators gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[10]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10], - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[10]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_10], - ); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[2].account }); - - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(4), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(2, 4, DEFAULT_OPERATOR_IDS[10]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10], - ); - }); - - it('Register 2 validators with 10 operators into the same cluster with one time deposit gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { account: owners[1].account }); - - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[10]), - minDepositAmount * 2n, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_10], - ); - - const args = eventsByName.ValidatorAdded[0].args; - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[10], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[10]), - 0, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_10], - ); - }); - - it('Bulk register 10 validators with 10 operators into the same cluster', async () => { - const operatorIds = DEFAULT_OPERATOR_IDS[10]; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.bulkRegisterValidator( - [ - [DataGenerator.publicKey(12)], - operatorIds, - [await DataGenerator.shares(1, 10, operatorIds)], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ), - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await bulkRegisterValidators(1, 10, operatorIds, minDepositAmount, args.cluster, [ - GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_10, - ]); - }); - - it('Bulk register 10 validators with 10 operators new cluster', async () => { - await bulkRegisterValidators( - 1, - 10, - DEFAULT_OPERATOR_IDS[10], - minDepositAmount, - - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_10], - ); - }); - - // 13 operators - - it('Register validator with 13 operators gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13], - ); - }); - - it('Register 2 validators with 13 operators into the same cluster gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13], - ); - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_13], - ); - }); - - it('Register 2 validators with 13 operators into the same cluster and 1 validator into a new cluster with 13 operators gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13], - ); - const args = eventsByName.ValidatorAdded[0].args; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_13], - ); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[2].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(4), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(2, 4, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13], - ); - }); - - it('Register 2 validators with 13 operators into the same cluster with one time deposit gas limit', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { account: owners[1].account }); - - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[13]), - minDepositAmount * 2n, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_13], - ); - - const args = eventsByName.ValidatorAdded[0].args; - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[13], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[13]), - 0, - args.cluster, - ], - { account: owners[1].account }, - ), - [GasGroup.REGISTER_VALIDATOR_WITHOUT_DEPOSIT_13], - ); - }); - - it('Bulk register 10 validators with 13 operators into the same cluster', async () => { - const operatorIds = DEFAULT_OPERATOR_IDS[13]; - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[1].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.bulkRegisterValidator( - [ - [DataGenerator.publicKey(12)], - operatorIds, - [await DataGenerator.shares(1, 11, operatorIds)], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ), - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await bulkRegisterValidators(1, 10, operatorIds, minDepositAmount, args.cluster, [ - GasGroup.BULK_REGISTER_10_VALIDATOR_EXISTING_CLUSTER_13, - ]); - }); - - it('Bulk register 10 validators with 13 operators new cluster', async () => { - await bulkRegisterValidators( - 1, - 10, - DEFAULT_OPERATOR_IDS[13], - minDepositAmount, - - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - [GasGroup.BULK_REGISTER_10_VALIDATOR_NEW_STATE_13], - ); - }); - - it('Get cluster burn rate', async () => { - const networkFee = CONFIG.minimalOperatorFee; - await ssvNetwork.write.updateNetworkFee([networkFee]); - - let clusterData = cluster1.cluster; - expect(await ssvViews.read.getBurnRate([owners[6].account.address, DEFAULT_OPERATOR_IDS[4], clusterData])).to.equal( - CONFIG.minimalOperatorFee * 4n + networkFee, - ); - - await ssvToken.write.approve([ssvNetwork.address, 1000000000000000n], { account: owners[6].account }); - - const validator2 = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(6, 2, DEFAULT_OPERATOR_IDS[4]), - '1000000000000000', - clusterData, - ], - { account: owners[6].account }, - ), - ); - clusterData = validator2.eventsByName.ValidatorAdded[0].args.cluster; - expect(await ssvViews.read.getBurnRate([owners[6].account.address, DEFAULT_OPERATOR_IDS[4], clusterData])).to.equal( - (CONFIG.minimalOperatorFee * 4n + networkFee) * 2n, - ); - }); - - it('Get cluster burn rate when one of the operators does not exist', async () => { - const clusterData = cluster1.cluster; - await expect(ssvViews.read.getBurnRate([owners[6].account.address, [1, 2, 3, 41], clusterData])).to.be.rejectedWith( - 'ClusterDoesNotExists', - ); - }); - - it('Register validator with incorrect input data reverts "IncorrectClusterState"', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { account: owners[1].account }); - - await ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(3), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 3, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 2, - networkFeeIndex: 10, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('IncorrectClusterState'); - }); - - it('Register validator in a new cluster with incorrect input data reverts "IncorrectClusterState"', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount * 2n], { account: owners[1].account }); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(3), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 3, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 2, - networkFee: 10, - networkFeeIndex: 10, - index: 10, - balance: 10, - active: false, - }, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('IncorrectClusterState'); - }); - - it('Register validator when an operator does not exist in the cluster reverts "OperatorDoesNotExist"', async () => { - await expect( - ssvNetwork.write.registerValidator([ - DataGenerator.publicKey(2), - [1, 2, 3, 25], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]), - ).to.be.rejectedWith('OperatorDoesNotExist'); - }); - - it('Register validator with a removed operator in the cluster reverts "OperatorDoesNotExist"', async () => { - await ssvNetwork.write.removeOperator([1]); - await expect( - ssvNetwork.write.registerValidator([ - DataGenerator.publicKey(4), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(0, 4, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]), - ).to.be.rejectedWith('OperatorDoesNotExist'); - }); - - it('Register cluster with unsorted operators reverts "UnsortedOperatorsList"', async () => { - await expect( - ssvNetwork.write.registerValidator([ - DataGenerator.publicKey(1), - [3, 2, 1, 4], - await DataGenerator.shares(0, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]), - ).to.be.rejectedWith('UnsortedOperatorsList'); - }); - - it('Register cluster with duplicated operators reverts "OperatorsListNotUnique"', async () => { - await expect( - ssvNetwork.write.registerValidator([ - DataGenerator.publicKey(1), - [3, 6, 12, 12], - await DataGenerator.shares(0, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]), - ).to.be.rejectedWith('OperatorsListNotUnique'); - }); - - it('Register validator with not enough balance reverts "InsufficientBalance"', async () => { - await ssvToken.write.approve([ssvNetwork.address, CONFIG.minimalOperatorFee]); - await expect( - ssvNetwork.write.registerValidator([ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(0, 1, DEFAULT_OPERATOR_IDS[4]), - CONFIG.minimalOperatorFee, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ]), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Register validator in a liquidatable cluster with not enough balance reverts "InsufficientBalance"', async () => { - const depositAmount = BigInt(CONFIG.minimalBlocksBeforeLiquidation) * (CONFIG.minimalOperatorFee * 4n); - - await ssvToken.write.approve([ssvNetwork.address, depositAmount], { account: owners[1].account }); - - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[4]), - depositAmount, - { - validatorCount: 0, - networkFee: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ), - ); - const cluster1 = eventsByName.ValidatorAdded[0].args; - - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - - await ssvToken.write.approve([ssvNetwork.address, CONFIG.minimalOperatorFee], { account: owners[1].account }); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - CONFIG.minimalOperatorFee, - cluster1.cluster, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('InsufficientBalance'); - }); - - it('Register an existing validator with same operators setup reverts "ValidatorAlreadyExistsWithData"', async () => { - await ssvToken.write.approve([ssvNetwork.address, CONFIG.minimalOperatorFee], { account: owners[6].account }); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(6, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[6].account }, - ), - ).to.be.rejectedWith('ValidatorAlreadyExistsWithData', DataGenerator.publicKey(1)); - }); - - it('Register an existing validator with different operators setup reverts "ValidatorAlreadyExistsWithData"', async () => { - await ssvToken.write.approve([ssvNetwork.address, CONFIG.minimalOperatorFee], { account: owners[6].account }); - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [1, 2, 5, 6], - await DataGenerator.shares(6, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[6].account }, - ), - ).to.be.rejectedWith('ValidatorAlreadyExistsWithData', DataGenerator.publicKey(1)); - }); - - it('Register validator with an empty public key reverts "InvalidPublicKeyLength"', async () => { - await expect( - ssvNetwork.write.registerValidator([ - '0x', - [1, 2, 3, 4], - await DataGenerator.shares(0, 4, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ]), - ).to.be.rejectedWith('InvalidPublicKeyLength'); - }); - - it('Bulk register 10 validators with empty public keys list reverts "EmptyPublicKeysList"', async () => { - await expect( - ssvNetwork.write.bulkRegisterValidator( - [ - [], - [1, 2, 3, 4], - [], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('EmptyPublicKeysList'); - }); - - it('Bulk register 10 validators with different pks/shares lenght reverts "PublicKeysSharesLengthMismatch"', async () => { - const pks = Array.from({ length: 10 }, (_, index) => DataGenerator.publicKey(index + 1)); - const shares = await Promise.all( - Array.from({ length: 8 }, (_, index) => DataGenerator.shares(1, index, DEFAULT_OPERATOR_IDS[4])), - ); - - await expect( - ssvNetwork.write.bulkRegisterValidator( - [ - pks, - [1, 2, 3, 4], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('PublicKeysSharesLengthMismatch'); - }); - - it('Bulk register 10 validators with wrong operators length reverts "InvalidOperatorIdsLength"', async () => { - const pks = Array.from({ length: 10 }, (_, index) => DataGenerator.publicKey(index + 1)); - const shares = await Promise.all( - Array.from({ length: 10 }, (_, index) => DataGenerator.shares(1, index, DEFAULT_OPERATOR_IDS[4])), - ); - - await expect( - ssvNetwork.write.bulkRegisterValidator( - [ - pks, - [1, 2, 3, 4, 5], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('InvalidOperatorIdsLength'); - }); - - it('Bulk register 10 validators with empty operators list reverts "InvalidOperatorIdsLength"', async () => { - const pks = Array.from({ length: 10 }, (_, index) => DataGenerator.publicKey(index + 1)); - const shares = await Promise.all( - Array.from({ length: 10 }, (_, index) => DataGenerator.shares(1, index, DEFAULT_OPERATOR_IDS[4])), - ); - - await expect( - ssvNetwork.write.bulkRegisterValidator( - [ - pks, - [], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[1].account }, - ), - ).to.be.rejectedWith('InvalidOperatorIdsLength'); - }); - - it('Retrieve an existing validator', async () => { - expect(await ssvViews.read.getValidator([owners[6].account.address, DataGenerator.publicKey(1)])).to.be.equals( - true, - ); - }); - - it('Retrieve a non-existing validator', async () => { - expect(await ssvViews.read.getValidator([owners[2].account.address, DataGenerator.publicKey(1)])).to.equal(false); - }); -}); diff --git a/test/validators/remove.ts b/test/validators/remove.ts deleted file mode 100644 index 1453ef2f2..000000000 --- a/test/validators/remove.ts +++ /dev/null @@ -1,477 +0,0 @@ -// Declare imports -import { - owners, - initializeContract, - registerOperators, - coldRegisterValidator, - bulkRegisterValidators, - DataGenerator, - CONFIG, - DEFAULT_OPERATOR_IDS, -} from '../helpers/contract-helpers'; -import { assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; - -// Declare globals -let ssvNetwork: any, minDepositAmount: BigInt, firstCluster: Cluster; - -describe('Remove Validator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * CONFIG.minimalOperatorFee * 4n; - - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - // Register a validator - // cold register - await coldRegisterValidator(); - - firstCluster = ( - await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }) - ).args; - }); - - it('Remove validator emits "ValidatorRemoved"', async () => { - await assertEvent( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[1].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorRemoved', - }, - ], - ); - }); - - it('Bulk remove validator emits "ValidatorRemoved"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await assertEvent( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorRemoved', - }, - ], - ); - }); - - it('Remove validator after cluster liquidation period emits "ValidatorRemoved"', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation + 10); - - await assertEvent( - ssvNetwork.write.bulkRemoveValidator( - [[DataGenerator.publicKey(1)], firstCluster.operatorIds, firstCluster.cluster], - { - account: owners[1].account, - }, - ), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorRemoved', - }, - ], - ); - }); - - it('Remove validator gas limit (4 operators cluster)', async () => { - await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR], - ); - }); - - it('Bulk remove 10 validator gas limit (4 operators cluster)', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - [GasGroup.BULK_REMOVE_10_VALIDATOR_4], - ); - }); - - it('Remove validator gas limit (7 operators cluster)', async () => { - const { args } = await bulkRegisterValidators(1, 1, DEFAULT_OPERATOR_IDS[7], minDepositAmount * 2n, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(2), args.operatorIds, args.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR_7], - ); - }); - - it('Bulk remove 10 validator gas limit (7 operators cluster)', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * (CONFIG.minimalOperatorFee * 7n); - - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[7], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - [GasGroup.BULK_REMOVE_10_VALIDATOR_7], - ); - }); - - it('Remove validator gas limit (10 operators cluster)', async () => { - const { args } = await bulkRegisterValidators(1, 2, DEFAULT_OPERATOR_IDS[10], minDepositAmount * 3n, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(2), args.operatorIds, args.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR_10], - ); - }); - - it('Bulk remove 10 validator gas limit (10 operators cluster)', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * (CONFIG.minimalOperatorFee * 10n); - - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[10], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - [GasGroup.BULK_REMOVE_10_VALIDATOR_10], - ); - }); - - it('Remove validator gas limit (13 operators cluster)', async () => { - const { args } = await bulkRegisterValidators(1, 2, DEFAULT_OPERATOR_IDS[13], minDepositAmount * 4n, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(2), args.operatorIds, args.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR_13], - ); - }); - - it('Bulk remove 10 validator gas limit (13 operators cluster)', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * (CONFIG.minimalOperatorFee * 13n); - - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[13], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - [GasGroup.BULK_REMOVE_10_VALIDATOR_13], - ); - }); - - it('Remove validator with a removed operator in the cluster', async () => { - await trackGas(ssvNetwork.write.removeOperator([1]), [GasGroup.REMOVE_OPERATOR_WITH_WITHDRAW]); - await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR], - ); - }); - - it('Register a removed validator and remove the same validator again', async () => { - // Remove validator - const remove = await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR], - ); - const updatedCluster = remove.eventsByName.ValidatorRemoved[0].args; - - // Re-register validator - const newRegister = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - updatedCluster.operatorIds, - await DataGenerator.shares(1, 1, updatedCluster.operatorIds), - 0, - updatedCluster.cluster, - ], - { - account: owners[1].account, - }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER], - ); - const afterRegisterCluster = newRegister.eventsByName.ValidatorAdded[0].args; - - // Remove the validator again - await trackGas( - ssvNetwork.write.removeValidator( - [DataGenerator.publicKey(1), afterRegisterCluster.operatorIds, afterRegisterCluster.cluster], - { - account: owners[1].account, - }, - ), - [GasGroup.REMOVE_VALIDATOR], - ); - }); - - it('Remove validator from a liquidated cluster', async () => { - await mine(CONFIG.minimalBlocksBeforeLiquidation); - const liquidatedCluster = await trackGas( - ssvNetwork.write.liquidate([firstCluster.owner, firstCluster.operatorIds, firstCluster.cluster]), - [GasGroup.LIQUIDATE_CLUSTER_4], - ); - const updatedCluster = liquidatedCluster.eventsByName.ClusterLiquidated[0].args; - - await trackGas( - ssvNetwork.write.removeValidator( - [DataGenerator.publicKey(1), updatedCluster.operatorIds, updatedCluster.cluster], - { - account: owners[1].account, - }, - ), - [GasGroup.REMOVE_VALIDATOR], - ); - }); - - it('Remove validator with an invalid owner reverts "ClusterDoesNotExists"', async () => { - await expect( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[2].account, - }), - ).to.be.rejectedWith('ClusterDoesNotExists'); - }); - - it('Remove validator with an invalid operator setup reverts "ClusterDoesNotExists"', async () => { - await expect( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), [1, 2, 3, 5], firstCluster.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('ClusterDoesNotExists'); - }); - - it('Remove the same validator twice reverts "ValidatorDoesNotExist"', async () => { - // Remove validator - const result = await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR], - ); - - const removed = result.eventsByName.ValidatorRemoved[0].args; - - // Remove validator again - await expect( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), removed.operatorIds, removed.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('ValidatorDoesNotExist'); - }); - - it('Remove the same validator with wrong input parameters reverts "IncorrectClusterState"', async () => { - // Remove validator - await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[1].account, - }), - [GasGroup.REMOVE_VALIDATOR], - ); - - // Remove validator again - await expect( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), firstCluster.operatorIds, firstCluster.cluster], { - account: owners[1].account, - }), - ).to.be.rejectedWith('IncorrectClusterState'); - }); - - it('Bulk Remove validator that does not exist in a valid cluster reverts "IncorrectValidatorStateWithData"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - pks[2] = '0xabcd1234'; - - await expect( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', pks[2]); - }); - - it('Bulk remove validator with an invalid operator setup reverts "ClusterDoesNotExists"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await expect( - ssvNetwork.write.bulkRemoveValidator([pks, [1, 2, 3, 5], args.cluster], { - account: owners[2].account, - }), - ).to.be.rejectedWith('ClusterDoesNotExists'); - }); - - it('Bulk Remove the same validator twice reverts "IncorrectValidatorStateWithData"', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - const result = await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - ); - - const removed = result.eventsByName.ValidatorRemoved[0].args; - - // Remove validator again - await expect( - ssvNetwork.write.bulkRemoveValidator([pks, removed.operatorIds, removed.cluster], { - account: owners[2].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', pks[0]); - }); - - it('Remove validators from a liquidated cluster', async () => { - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - await mine(CONFIG.minimalBlocksBeforeLiquidation - 2); - - let result = await trackGas( - ssvNetwork.write.liquidate([args.owner, args.operatorIds, args.cluster], { - account: owners[1].account, - }), - ); - - const liquidated = result.eventsByName.ClusterLiquidated[0].args; - - result = await trackGas( - ssvNetwork.write.bulkRemoveValidator([pks.slice(0, 5), liquidated.operatorIds, liquidated.cluster], { - account: owners[2].account, - }), - ); - - const removed = result.eventsByName.ValidatorRemoved[0].args; - - expect(removed.cluster.validatorCount).to.equal(5); - expect(removed.cluster.networkFeeIndex).to.equal(0); - expect(removed.cluster.index).to.equal(0); - expect(removed.cluster.active).to.equal(false); - expect(removed.cluster.balance).to.equal(0); - }); - - it('Bulk remove 10 validator with duplicated public keys reverts "IncorrectValidatorStateWithData"', async () => { - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 10n) * (CONFIG.minimalOperatorFee * 13n); - - const { args, pks } = await bulkRegisterValidators(2, 10, DEFAULT_OPERATOR_IDS[4], minDepositAmount, { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }); - - const keys = [pks[0], pks[1], pks[2], pks[3], pks[2], pks[5], pks[2], pks[7], pks[2], pks[8]]; - - await expect( - ssvNetwork.write.bulkRemoveValidator([keys, args.operatorIds, args.cluster], { - account: owners[2].account, - }), - ).to.be.rejectedWith('IncorrectValidatorStateWithData', pks[2]); - }); - - it('Bulk remove 10 validator with empty public keys reverts "IncorrectValidatorStateWithData"', async () => { - await expect( - ssvNetwork.write.bulkRemoveValidator([[], firstCluster.operatorIds, firstCluster.cluster], { - account: owners[2].account, - }), - ).to.be.rejectedWith('ValidatorDoesNotExist'); - }); -}); diff --git a/test/validators/whitelist-register.ts b/test/validators/whitelist-register.ts deleted file mode 100644 index f8d9908c2..000000000 --- a/test/validators/whitelist-register.ts +++ /dev/null @@ -1,907 +0,0 @@ -// Declare imports -import hre from 'hardhat'; - -import { - owners, - initializeContract, - registerOperators, - bulkRegisterValidators, - DataGenerator, - getTransactionReceipt, - coldRegisterValidator, - CONFIG, - DEFAULT_OPERATOR_IDS, - MOCK_SHARES, - publicClient, -} from '../helpers/contract-helpers'; -import { assertPostTxEvent, assertEvent } from '../helpers/utils/test'; -import { trackGas, GasGroup, trackGasFromReceipt } from '../helpers/gas-usage'; - -import { mine } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers'; -import { expect } from 'chai'; - -let ssvNetwork: any, ssvViews: any, ssvToken: any, minDepositAmount: BigInt; - -describe('Register Validator Tests', () => { - beforeEach(async () => { - // Initialize contract - const metadata = await initializeContract(); - ssvNetwork = metadata.ssvNetwork; - ssvViews = metadata.ssvNetworkViews; - ssvToken = metadata.ssvToken; - }); - - describe('Generic Tests', () => { - beforeEach(async () => { - // Register operators - await registerOperators(0, 14, CONFIG.minimalOperatorFee); - - minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * CONFIG.minimalOperatorFee * 4n; - - // cold register - await coldRegisterValidator(); - }); - - it('Register whitelisted validator in 1 operator with 4 operators emits "ValidatorAdded"/gas limits/logic', async () => { - const operatorId = await registerOperators(1, 1, CONFIG.minimalOperatorFee); - - await ssvNetwork.write.setOperatorsWhitelists([[operatorId], [owners[3].account.address]], { - account: owners[1].account, - }); - await ssvNetwork.write.setOperatorsPrivateUnchecked([[operatorId]], { - account: owners[1].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - const receipt = await getTransactionReceipt( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [1, 2, 3, operatorId], - await DataGenerator.shares(3, 1, [1, 2, 3, operatorId]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - ); - - await assertPostTxEvent([ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - }, - ]); - - await trackGasFromReceipt(receipt, [GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTED_4]); - - expect(await ssvViews.read.getOperatorById([operatorId])).to.deep.equal([ - owners[1].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 1, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - true, // isPrivate - true, // active - ]); - }); - - it('Register whitelisted validator in 4 operators in 4 operators cluster gas limits/logic', async () => { - await ssvNetwork.write.setOperatorsWhitelists([DEFAULT_OPERATOR_IDS[4], [owners[3].account.address]], { - account: owners[0].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(3, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - [GasGroup.REGISTER_VALIDATOR_NEW_STATE_4_WHITELISTED_4], - ); - - // Check totalValidatorsCount is incremented for all operators - for (let i = 0; i < DEFAULT_OPERATOR_IDS[4].length; i++) { - const operatorData = await ssvViews.read.getOperatorById([DEFAULT_OPERATOR_IDS[4][i]]); - expect(operatorData[2]).to.be.equal(2); // validatorCount starts with 1 because coldRegiserValidator - } - }); - - it('Register non-whitelisted validator in 1 public operator with 4 operators emits "ValidatorAdded"/logic', async () => { - await ssvNetwork.write.setOperatorsWhitelists([[5], [owners[3].account.address]]); - - await ssvNetwork.write.setOperatorsPublicUnchecked([[5]]); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - await ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [5, 6, 7, 8], - await DataGenerator.shares(3, 1, [5, 6, 7, 8]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ); - - expect(await ssvViews.read.getOperatorById([5])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 1, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - false, // isPrivate - true, // active - ]); - }); - - it('Register whitelisted validator in 4 operator in 4 operators existing cluster gas limits', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(3, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await ssvNetwork.write.setOperatorsWhitelists([DEFAULT_OPERATOR_IDS[4], [owners[3].account.address]]); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(3, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - args.cluster, - ], - { account: owners[3].account }, - ), - [GasGroup.REGISTER_VALIDATOR_EXISTING_CLUSTER_4_WHITELISTED_4], - ); - }); - - it('Register using non-authorized account for 1 operator with 4 operators cluster reverts "CallerNotWhitelistedWithData"', async () => { - await ssvNetwork.write.setOperatorsWhitelists([[3], [owners[3].account.address]], { - account: owners[0].account, - }); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([[3]], { - account: owners[0].account, - }); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(2, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - ).to.be.rejectedWith('CallerNotWhitelistedWithData'); - }); - - it('Register using non-authorized account for 1 operator with 4 operators cluster reverts "CallerNotWhitelistedWithData"', async () => { - await ssvNetwork.write.setOperatorsPrivateUnchecked([[2]]); - - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(2, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - ).to.be.rejectedWith('CallerNotWhitelistedWithData'); - }); - - it('Register using fake whitelisting contract reverts', async () => { - const fakeWhitelistingContract = await hre.viem.deployContract( - 'FakeWhitelistingContract', - [await ssvNetwork.address], - { - client: owners[0].client, - }, - ); - - // Set the whitelisting contract for operators 1,2,3,4 - await ssvNetwork.write.setOperatorsWhitelistingContract( - [DEFAULT_OPERATOR_IDS[4], await fakeWhitelistingContract.address], - { - account: owners[0].account, - }, - ); - await ssvNetwork.write.setOperatorsPrivateUnchecked([DEFAULT_OPERATOR_IDS[4]], { - account: owners[0].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - const pk = DataGenerator.publicKey(1); - const shares = await DataGenerator.shares(3, 1, [4, 5, 6, 7]); - - // set the 2nd registerValidator input data with a new validator public key - await fakeWhitelistingContract.write.setRegisterValidatorData([ - '0xa063fa1434f4ae9bb63488cd79e2f76dea59e0e2d6cdec7236c2bb49ffb37da37cb7966be74eca5a171f659fee7bc502', - [4, 5, 6, 7], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ]); - - await expect( - ssvNetwork.write.registerValidator( - [ - pk, - [4, 5, 6, 7], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - ).to.be.rejectedWith('Call failed or was reverted'); // reverts in the fake whitelisting contract - }); - - it('Read-only reentrancy attack reverts', async () => { - // This test replicates a Read-only reentrancy attack, where a malicious whitelisting contract - // acts as an intermediary for a malicious contract that serves as an operator owner. - // It performs an attempt of withdrawing the operator earnings when the SSVNetwork contract - // still doesn't receive the funds, resulting in an inconsistent state of the contract. - // Expected result is a revert from the SSVNetwork contract because the - // ISSVWhitelistingContract.isWhitelisted function has the view modifier. - // The flow of the attack is the following: - // AttackerContract -> SSVNetwork.registerValidator() -> BadOperatorWhitelisting.fallback() - // -> BeneficiaryContract.withdrawOperatorEarnings() -> SSVNetwork.withdrawOperatorEarnings() - - const beneficiaryContract = await hre.viem.deployContract('BeneficiaryContract', [await ssvNetwork.address], { - client: owners[1].client, - }); - - const badOperatorWhitelistingContract = await hre.viem.deployContract( - 'BadOperatorWhitelistingContract', - [await beneficiaryContract.address], - { - client: owners[1].client, - }, - ); - - const attackerContract = await hre.viem.deployContract('AttackerContract', [await ssvNetwork.address], { - client: owners[1].client, - }); - - // BeneficiaryContract register the target operator - const { result: beneficiaryOperatorId } = await publicClient.simulateContract({ - address: await beneficiaryContract.address, - abi: beneficiaryContract.abi, - functionName: 'registerOperator', - account: owners[1].account, - }); - - await beneficiaryContract.write.registerOperator(); - await beneficiaryContract.write.setTargetOperatorId([beneficiaryOperatorId]); - - // Register a new operator, good owner - const { result: goodOperatorId } = await publicClient.simulateContract({ - address: await ssvNetwork.address, - abi: ssvNetwork.abi, - functionName: 'registerOperator', - args: ['0xabcd', CONFIG.minimalOperatorFee, true], - account: owners[0].account, - }); - - await ssvNetwork.write.registerOperator(['0xabcd', CONFIG.minimalOperatorFee, true]); - // Whitelist the new operator with the attacker contract - await ssvNetwork.write.setOperatorsWhitelistingContract( - [[goodOperatorId], await badOperatorWhitelistingContract.address], - { - account: owners[0].account, - }, - ); - - const goodUser = owners[1].account; - - // A good user calls registerValidator with operators: - // 1, 2, 3, beneficiaryOperatorId - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: goodUser }); - - let pk = DataGenerator.publicKey(2); - - await ssvNetwork.write.registerValidator( - [ - pk, - [1, 2, 3, beneficiaryOperatorId], - MOCK_SHARES, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: goodUser }, - ); - - // forward blocks so beneficiaryOperator generates revenue - await mine(10); - - // The attacker contract calls registerValidator with operators: - // 1, 2, beneficiaryOperatorId, goodOperatorId - const badUser = owners[3].account; - - pk = DataGenerator.publicKey(3); - - // AttackerContract starts the attact - await expect( - attackerContract.write.startAttack( - [ - pk, - [1, 2, beneficiaryOperatorId, goodOperatorId], - MOCK_SHARES, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: badUser }, - ), - ).to.be.rejected; - }); - - describe('Register using whitelisting contract', () => { - let mockWhitelistingContractAddress: any; - - beforeEach(async () => { - // Whitelist whitelistedCaller using an external contract - const mockWhitelistingContract = await hre.viem.deployContract( - 'MockWhitelistingContract', - [[owners[3].account.address]], - { - client: owners[0].client, - }, - ); - mockWhitelistingContractAddress = await mockWhitelistingContract.address; - - // Set the whitelisting contract for operators 1,2,3,4 - await ssvNetwork.write.setOperatorsWhitelistingContract( - [DEFAULT_OPERATOR_IDS[4], mockWhitelistingContractAddress], - { - account: owners[0].account, - }, - ); - await ssvNetwork.write.setOperatorsPrivateUnchecked([DEFAULT_OPERATOR_IDS[4]], { - account: owners[0].account, - }); - }); - - it('Register using whitelisting contract and SSV whitelisting module for 2 operators', async () => { - // Account A whitelists account B on SSV whitelisting module - // Account A adds a whitelisting contract - // Account A adds account C to that whitelist contract - // Register validator with account B and C both work - - // Account A = owners[0] - // Account B = owners[3] - // Account C = owners[4] - - // Account A whitelists account B on SSV whitelisting module (operator 5) - await ssvNetwork.write.setOperatorsWhitelists([[5], [owners[3].account.address]]); - - // Account A adds account C to that whitelist contract - const whitelistingContract = await hre.viem.deployContract( - 'MockWhitelistingContract', - [[owners[4].account.address]], - { - client: owners[0].client, - }, - ); - const whitelistingContractAddress = await whitelistingContract.address; - - // Account A adds a whitelisting contract (operator 6) - await ssvNetwork.write.setOperatorsWhitelistingContract([[6], whitelistingContractAddress], { - account: owners[0].account, - }); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([[5, 6]], { - account: owners[0].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - // Register validator with account B works - await assertEvent( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [2, 3, 4, 5], - MOCK_SHARES, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - argNames: ['owner', 'operatorIds'], - argValuesList: [[owners[3].account.address, [2, 3, 4, 5]]], - }, - ], - ); - - // Check the operator 5 increased validatorCount - expect(await ssvViews.read.getOperatorById([5])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 1, - ethers.ZeroAddress, - true, // isPrivate - true, // active - ]); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[4].account }); - - // Register validator with account C works - await assertEvent( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [6, 7, 8, 9], - MOCK_SHARES, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[4].account }, - ), - [ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - argNames: ['owner', 'operatorIds'], - argValuesList: [[owners[4].account.address, [6, 7, 8, 9]]], - }, - ], - ); - - // Check the operator 6 increased validatorCount - expect(await ssvViews.read.getOperatorById([6])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 1, - whitelistingContractAddress, - true, // isPrivate - true, // active - ]); - }); - - it('Register using whitelisting contract for 1 operator in 4 operators cluster gas limits/events/logic', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - const pk = DataGenerator.publicKey(1); - const shares = await DataGenerator.shares(3, 1, [4, 5, 6, 7]); - - const receipt = await getTransactionReceipt( - ssvNetwork.write.registerValidator( - [ - pk, - [4, 5, 6, 7], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ), - ); - - let registeredCluster = await trackGasFromReceipt(receipt, [ - GasGroup.REGISTER_VALIDATOR_NEW_STATE_1_WHITELISTING_CONTRACT_4, - ]); - registeredCluster = registeredCluster.eventsByName.ValidatorAdded[0].args; - - await assertPostTxEvent([ - { - contract: ssvNetwork, - eventName: 'ValidatorAdded', - argNames: ['owner', 'operatorIds', 'publicKey', 'shares', 'cluster'], - argValuesList: [[owners[3].account.address, [4, 5, 6, 7], pk, shares, registeredCluster.cluster]], - }, - ]); - - expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 2, // validatorCount -> starts with 1 validator because coldRegisterValidator - mockWhitelistingContractAddress, // whitelisting contract address - true, // isPrivate - true, // active - ]); - }); - - it('Bulk register 10 validators using whitelisting contract for 1 operator in 4 operators cluster gas limits/logic', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - const { eventsByName } = await trackGas( - ssvNetwork.write.bulkRegisterValidator( - [ - [DataGenerator.publicKey(12)], - [4, 5, 6, 7], - [await DataGenerator.shares(3, 11, [4, 5, 6, 7])], - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0, - active: true, - }, - ], - { account: owners[3].account }, - ), - ); - - const args = eventsByName.ValidatorAdded[0].args; - - await bulkRegisterValidators(3, 10, [4, 5, 6, 7], minDepositAmount, args.cluster, [ - GasGroup.BULK_REGISTER_10_VALIDATOR_1_WHITELISTING_CONTRACT_EXISTING_CLUSTER_4, - ]); - - expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 12, // validatorCount -> starts with 1 validator because coldRegisterValidator - mockWhitelistingContractAddress, // whitelisting contract address - true, // isPrivate - true, // active - ]); - }); - - it('Register using whitelisting contract for 1 public operator in 4 operators cluster', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - await ssvNetwork.write.setOperatorsPublicUnchecked([[4]]); - - await ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [4, 5, 6, 7], - await DataGenerator.shares(3, 1, [4, 5, 6, 7]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ); - - expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 2, // validatorCount -> starts with 1 validator because coldRegisterValidator - mockWhitelistingContractAddress, // whitelisting contract address - false, // isPrivate - true, // active - ]); - }); - - it('Register using whitelisting contract for 1 operator & EOA for 1 operator in 4 operators cluster', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - await ssvNetwork.write.setOperatorsWhitelists([[6], [owners[3].account.address]]); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([[6]]); - - await ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - [4, 5, 6, 7], - await DataGenerator.shares(3, 1, [4, 5, 6, 7]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ); - - expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 2, // validatorCount -> starts with 1 validator because coldRegisterValidator - mockWhitelistingContractAddress, // whitelisting contract address - true, // isPrivate - true, // active - ]); - - expect(await ssvViews.read.getOperatorById([6])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 1, // validatorCount - ethers.ZeroAddress, // whitelisting contract address - true, // isPrivate - true, // active - ]); - }); - - it('Register using whitelisting contract with an unauthorized account reverts "CallerNotWhitelistedWithData"', async () => { - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[4].account }); - - const pk = DataGenerator.publicKey(1); - const shares = await DataGenerator.shares(4, 1, [4, 5, 6, 7]); - - await expect( - ssvNetwork.write.registerValidator( - [ - pk, - [4, 5, 6, 7], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[4].account }, - ), - ).to.be.rejectedWith('CallerNotWhitelistedWithData'); - }); - - it('Register using whitelisting contract but a public operator allows registration', async () => { - // This test checks a non-whitelisted account (owners[4]) in a whitelisting contract - // can register validators in a public operator - - await ssvNetwork.write.setOperatorsPublicUnchecked([[4]], { - account: owners[0].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[4].account }); - - const pk = DataGenerator.publicKey(1); - const shares = await DataGenerator.shares(4, 1, [4, 5, 6, 7]); - - await ssvNetwork.write.registerValidator( - [ - pk, - [4, 5, 6, 7], - shares, - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[4].account }, - ); - - expect(await ssvViews.read.getOperatorById([4])).to.deep.equal([ - owners[0].account.address, // owner - CONFIG.minimalOperatorFee, // fee - 2, // validatorCount -> starts with 1 validator because coldRegisterValidator - mockWhitelistingContractAddress, // whitelisting contract address - false, // isPrivate - true, // active - ]); - }); - }); - }); - describe('Whitelist Edge Cases Tests', () => { - it('WT-1 - Register validator, 13 whitelisted operators, 1 block index each', async () => { - const minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * CONFIG.minimalOperatorFee * 13n; - - await registerOperators(2, 3100, CONFIG.minimalOperatorFee); - - const operatorIds = [2, 258, 514, 770, 1026, 1282, 1538, 1794, 2050, 2306, 2562, 2818, 3074]; - - await ssvNetwork.write.setOperatorsWhitelists([operatorIds, [owners[3].account.address]], { - account: owners[2].account, - }); - - await ssvNetwork.write.setOperatorsPrivateUnchecked([operatorIds], { - account: owners[2].account, - }); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[3].account }); - - await ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - operatorIds, - await DataGenerator.shares(3, 1, operatorIds), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[3].account }, - ); - - for (let i = 0; i < operatorIds.length; i++) { - const operatorData = await ssvViews.read.getOperatorById([operatorIds[i]]); - expect(operatorData[2]).to.be.equal(1); - } - }); - - it('WT-2 - Register 2 validators using 2 accounts and remove the first', async () => { - // 1. Account A registers a validator [1,2,3,4] (all public) - // 2. Whitelist operator without that owner from above (make 1 private account B) - // 3. Trying to register another validator fails using account A - // 4. Remove validator from step 1 and check cluster.validatorCount = 0 - - // operators' owner -> owners[1] - // Account A -> owners[2] - // Account B -> owners[3] - - // Step 1 - const minDepositAmount = (BigInt(CONFIG.minimalBlocksBeforeLiquidation) + 2n) * CONFIG.minimalOperatorFee * 4n; - - await registerOperators(1, 4, CONFIG.minimalOperatorFee); - - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[2].account }); - let clusterData = await trackGas( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(1), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 1, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - { - validatorCount: 0, - networkFeeIndex: 0, - index: 0, - balance: 0n, - active: true, - }, - ], - { account: owners[2].account }, - ), - ); - - clusterData = clusterData.eventsByName.ValidatorAdded[0].args; - - // Step 2 - await ssvNetwork.write.setOperatorsWhitelists([[2], [owners[3].account.address]], { - account: owners[1].account, - }); - await ssvNetwork.write.setOperatorsPrivateUnchecked([[2]], { - account: owners[1].account, - }); - - // Step 3 - await ssvToken.write.approve([ssvNetwork.address, minDepositAmount], { account: owners[2].account }); - await expect( - ssvNetwork.write.registerValidator( - [ - DataGenerator.publicKey(2), - DEFAULT_OPERATOR_IDS[4], - await DataGenerator.shares(1, 2, DEFAULT_OPERATOR_IDS[4]), - minDepositAmount, - clusterData.cluster, - ], - { account: owners[2].account }, - ), - ).to.be.rejectedWith('CallerNotWhitelistedWithData'); - - // Step 4 - clusterData = await trackGas( - ssvNetwork.write.removeValidator([DataGenerator.publicKey(1), DEFAULT_OPERATOR_IDS[4], clusterData.cluster], { - account: owners[2].account, - }), - ); - - clusterData = clusterData.eventsByName.ValidatorRemoved[0].args; - - expect(clusterData.cluster.validatorCount).to.be.equal(0); - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 574e785c7..7d803ee99 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,15 @@ { "compilerOptions": { - "target": "es2020", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, + "lib": ["es2023"], + "module": "node16", + "target": "es2022", "strict": true, + "esModuleInterop": true, "skipLibCheck": true, - "resolveJsonModule": true + "moduleResolution": "node16", + "resolveJsonModule": true, + "outDir": "dist", + "allowImportingTsExtensions": true, + "noEmit": true } }