diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml new file mode 100644 index 00000000..0db006ee --- /dev/null +++ b/.github/workflows/check-coverage.yml @@ -0,0 +1,64 @@ +name: Check coverage +on: + workflow_dispatch: + pull_request: + branches: + - master + +jobs: + check-coverage: + name: Check Coverage + runs-on: ubuntu-latest + defaults: + run: + working-directory: projects/vaults + permissions: + pull-requests: write + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 20.x + + - name: Install deps + run: yarn install --frozen-lockfile && cd ../.. && yarn install --frozen-lockfile + + - name: Run coverage + run: npm run coverage + env: + RPC: https://rpc.ankr.com/eth/fc046d362fd7826a53b96763a67c6338518a402f7764b10eb99eebfc0543a700 + ASSET_NAME: stETH + NETWORK: mainnet + + - name: Check coverage percentage + run: | + bash ./check-coverage.sh + + - name: Save coveage results + uses: actions/upload-artifact@v4 + with: + name: coverage_report + path: | + projects/vaults/coverage + + - name: Read logs file + run: | + { + echo "CONTENT<> "$GITHUB_ENV" + + - name: Post coverage result to PR comment + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ``` + ${{ env.CONTENT }} + ``` diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 00000000..93885f5b --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,23 @@ +name: Linter +on: + pull_request: + +jobs: + linter: + name: Linter + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 20.x + + - name: Install deps + run: yarn install --frozen-lockfile + + - name: Lint + run: npx eslint . --fix diff --git a/.github/workflows/tests-vault.yml b/.github/workflows/tests-vault.yml new file mode 100644 index 00000000..b4b5cd74 --- /dev/null +++ b/.github/workflows/tests-vault.yml @@ -0,0 +1,40 @@ +name: Vault tests +on: + pull_request: + workflow_dispatch: + +# cancel previous runs if a new one is triggered +concurrency: + group: vault-tests-${{github.event.pull_request.number}} + cancel-in-progress: true + +jobs: + test: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: '22.x' + + - name: check node version + run: | + node --version + yarn --version + + - name: Install deps + run: yarn install --frozen-lockfile && cd projects/vaults && yarn install --frozen-lockfile + + - name: Run tests + working-directory: projects/vaults + run: npm run test + env: + MAINNET_RPC: https://rpc.ankr.com/eth/fc046d362fd7826a53b96763a67c6338518a402f7764b10eb99eebfc0543a700 + RPC: https://rpc.ankr.com/eth/fc046d362fd7826a53b96763a67c6338518a402f7764b10eb99eebfc0543a700 + ASSET_NAME: stETH + NETWORK: mainnet diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..a12ee41c --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,37 @@ +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; +import pluginChaiFriendly from "eslint-plugin-chai-friendly"; + +export default defineConfig( + [ + { files: ["**/*.{js,mjs,cjs,ts}"], plugins: { js }, extends: ["js/recommended"] }, + { files: ["**/*.{js,mjs,cjs,ts}"], languageOptions: { globals: globals.browser } }, + tseslint.configs.recommended, + { + plugins: { "chai-friendly": pluginChaiFriendly }, + rules: { + "@typescript-eslint/no-unused-expressions": "off", + "chai-friendly/no-unused-expressions": "error", + + //! TODO: remove later all the items below + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "off", + "no-empty": "off", + }, + }, + ], + globalIgnores([ + "projects/vaults/typechain-types", + "projects/vaults/coverage", + "projects/vaults/.solcover.js", + + //! TODO: remove later after fix all the items below + "projects/vaults/scripts", + "projects/vaults/tasks", + "projects/airdrop", + "projects/restaking-pool", + "projects/timelocks", + ]), +); diff --git a/hh.config.ts b/hh.config.ts index 1f22b133..fdf1a6dc 100644 --- a/hh.config.ts +++ b/hh.config.ts @@ -1,4 +1,4 @@ -require("dotenv").config(); +import 'dotenv/config'; const accounts = process.env.DEPLOYER_PRIVATE_KEY ? [process.env.DEPLOYER_PRIVATE_KEY] diff --git a/package.json b/package.json index c794817b..156a12a8 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,16 @@ { "devDependencies": { + "@eslint/js": "^9.26.0", "@types/node": "^20.14.10", "dotenv": "^16.3.1", + "eslint": "^9.26.0", + "eslint-plugin-chai-friendly": "^1.0.1", "hardhat": "^2.22.10", "prettier": "^3.1.0", "prettier-plugin-solidity": "^1.2.0", "ts-node": "^10.9.2", - "typescript": "^5.5.3" + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.0" }, "name": "contracts", "version": "1.0.0", diff --git a/projects/vaults/.env.example b/projects/vaults/.env.example new file mode 100644 index 00000000..8d3b7431 --- /dev/null +++ b/projects/vaults/.env.example @@ -0,0 +1,3 @@ +NETWORK=mainnet +RPC=https://rpc.ankr.com/eth +ASSET_NAME=stETH diff --git a/projects/vaults/.openzeppelin/mainnet.json b/projects/vaults/.openzeppelin/mainnet.json index 1483cce0..a3b9f032 100644 --- a/projects/vaults/.openzeppelin/mainnet.json +++ b/projects/vaults/.openzeppelin/mainnet.json @@ -89,6 +89,11 @@ "address": "0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97", "txHash": "0xe7206ebc4df5a3408181723308c6808b53781bef01421e11506fbfe1b6ef2f20", "kind": "transparent" + }, + { + "address": "0x88fcd64FBA65f67F8A9F7A882F419D72aF905fC5", + "txHash": "0xbcd6ee5246cdb9cb01cd526bbaca019cab10b8a3db7171fcd84406accf8009dc", + "kind": "transparent" } ], "impls": { @@ -6919,6 +6924,1446 @@ }, "namespaces": {} } + }, + "c35438c2a3859075f7ba15f2bedd96a2a3b595584fd9100087384e6a747f8f9d": { + "address": "0xdae6aB0C1553C2f52B62f12887f5fe1b6322241a", + "txHash": "0x1be50775661b3790e6d299b362769ea9132c92d4bca9680f340f8922f99f5b60", + "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": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_paused", + "offset": 0, + "slot": "51", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "101", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "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": "_asset", + "offset": 0, + "slot": "251", + "type": "t_contract(IERC20)2903", + "contract": "InceptionAssetsHandler", + "src": "contracts/assets-handler/InceptionAssetsHandler.sol:22" + }, + { + "label": "__reserver", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)49_storage", + "contract": "InceptionAssetsHandler", + "src": "contracts/assets-handler/InceptionAssetsHandler.sol:24" + }, + { + "label": "epoch", + "offset": 0, + "slot": "301", + "type": "t_uint256", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:13" + }, + { + "label": "_operator", + "offset": 0, + "slot": "302", + "type": "t_address", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:16" + }, + { + "label": "mellowRestaker", + "offset": 0, + "slot": "303", + "type": "t_contract(IIMellowRestaker)6467", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:18" + }, + { + "label": "totalAmountToWithdraw", + "offset": 0, + "slot": "304", + "type": "t_uint256", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:22" + }, + { + "label": "claimerWithdrawalsQueue", + "offset": 0, + "slot": "305", + "type": "t_array(t_struct(Withdrawal)6611_storage)dyn_storage", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:24" + }, + { + "label": "redeemReservedAmount", + "offset": 0, + "slot": "306", + "type": "t_uint256", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:27" + }, + { + "label": "depositBonusAmount", + "offset": 0, + "slot": "307", + "type": "t_uint256", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:29" + }, + { + "label": "targetCapacity", + "offset": 0, + "slot": "308", + "type": "t_uint256", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:32" + }, + { + "label": "__gap", + "offset": 0, + "slot": "309", + "type": "t_array(t_uint256)42_storage", + "contract": "MellowHandler", + "src": "contracts/mellow-handler/MellowHandler.sol:36" + }, + { + "label": "inceptionToken", + "offset": 0, + "slot": "351", + "type": "t_contract(IInceptionToken)5010", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:21" + }, + { + "label": "minAmount", + "offset": 0, + "slot": "352", + "type": "t_uint256", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:24" + }, + { + "label": "_claimerWithdrawals", + "offset": 0, + "slot": "353", + "type": "t_mapping(t_address,t_struct(Withdrawal)6611_storage)", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:26" + }, + { + "label": "name", + "offset": 0, + "slot": "354", + "type": "t_string_storage", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:29" + }, + { + "label": "ratioFeed", + "offset": 0, + "slot": "355", + "type": "t_contract(IInceptionRatioFeed)4979", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:38" + }, + { + "label": "treasury", + "offset": 0, + "slot": "356", + "type": "t_address", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:39" + }, + { + "label": "protocolFee", + "offset": 20, + "slot": "356", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:40" + }, + { + "label": "maxBonusRate", + "offset": 0, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:43" + }, + { + "label": "optimalBonusRate", + "offset": 8, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:44" + }, + { + "label": "depositUtilizationKink", + "offset": 16, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:45" + }, + { + "label": "maxFlashFeeRate", + "offset": 24, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:48" + }, + { + "label": "optimalWithdrawalRate", + "offset": 0, + "slot": "358", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:49" + }, + { + "label": "withdrawUtilizationKink", + "offset": 8, + "slot": "358", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:50" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(Withdrawal)6611_storage)dyn_storage": { + "label": "struct IMellowHandler.Withdrawal[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)42_storage": { + "label": "uint256[42]", + "numberOfBytes": "1344" + }, + "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(IERC20)2903": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IIMellowRestaker)6467": { + "label": "contract IIMellowRestaker", + "numberOfBytes": "20" + }, + "t_contract(IInceptionRatioFeed)4979": { + "label": "contract IInceptionRatioFeed", + "numberOfBytes": "20" + }, + "t_contract(IInceptionToken)5010": { + "label": "contract IInceptionToken", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(Withdrawal)6611_storage)": { + "label": "mapping(address => struct IMellowHandler.Withdrawal)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Withdrawal)6611_storage": { + "label": "struct IMellowHandler.Withdrawal", + "members": [ + { + "label": "epoch", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "receiver", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "cb70277361a326b962125a4055451aaba08382f8421ac64292d44635d2f07459": { + "address": "0x24Ee753885Eb18D60794815caAF63402915BfA50", + "txHash": "0xc3277e4a30fb4c8c8f28306cdca04b9d5eb61490798caeca971b6dcd0f59fbf7", + "layout": { + "solcVersion": "0.8.28", + "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": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_paused", + "offset": 0, + "slot": "51", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "101", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "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": "_asset", + "offset": 0, + "slot": "251", + "type": "t_contract(IERC20)2130", + "contract": "InceptionAssetsHandler", + "src": "contracts/assets-handler/InceptionAssetsHandler.sol:24" + }, + { + "label": "__reserver", + "offset": 0, + "slot": "252", + "type": "t_array(t_uint256)49_storage", + "contract": "InceptionAssetsHandler", + "src": "contracts/assets-handler/InceptionAssetsHandler.sol:26" + }, + { + "label": "epoch", + "offset": 0, + "slot": "301", + "type": "t_uint256", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:20" + }, + { + "label": "_operator", + "offset": 0, + "slot": "302", + "type": "t_address", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:23" + }, + { + "label": "mellowRestaker", + "offset": 0, + "slot": "303", + "type": "t_contract(IIMellowRestaker)5620", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:25" + }, + { + "label": "totalAmountToWithdraw", + "offset": 0, + "slot": "304", + "type": "t_uint256", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:29" + }, + { + "label": "claimerWithdrawalsQueue", + "offset": 0, + "slot": "305", + "type": "t_array(t_struct(Withdrawal)4513_storage)dyn_storage", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:31" + }, + { + "label": "redeemReservedAmount", + "offset": 0, + "slot": "306", + "type": "t_uint256", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:34" + }, + { + "label": "depositBonusAmount", + "offset": 0, + "slot": "307", + "type": "t_uint256", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:36" + }, + { + "label": "targetCapacity", + "offset": 0, + "slot": "308", + "type": "t_uint256", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:39" + }, + { + "label": "symbioticRestaker", + "offset": 0, + "slot": "309", + "type": "t_contract(IISymbioticRestaker)5674", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:43" + }, + { + "label": "__gap", + "offset": 0, + "slot": "310", + "type": "t_array(t_uint256)41_storage", + "contract": "SymbioticHandler", + "src": "contracts/symbiotic-handler/SymbioticHandler.sol:45" + }, + { + "label": "inceptionToken", + "offset": 0, + "slot": "351", + "type": "t_contract(IInceptionToken)4237", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:21" + }, + { + "label": "withdrawMinAmount", + "offset": 0, + "slot": "352", + "type": "t_uint256", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:25", + "renamedFrom": "minAmount" + }, + { + "label": "_claimerWithdrawals", + "offset": 0, + "slot": "353", + "type": "t_mapping(t_address,t_struct(Withdrawal)4513_storage)", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:27" + }, + { + "label": "name", + "offset": 0, + "slot": "354", + "type": "t_string_storage", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:30" + }, + { + "label": "ratioFeed", + "offset": 0, + "slot": "355", + "type": "t_contract(IInceptionRatioFeed)4206", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:39" + }, + { + "label": "treasury", + "offset": 0, + "slot": "356", + "type": "t_address", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:40" + }, + { + "label": "protocolFee", + "offset": 20, + "slot": "356", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:41" + }, + { + "label": "maxBonusRate", + "offset": 0, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:44" + }, + { + "label": "optimalBonusRate", + "offset": 8, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:45" + }, + { + "label": "depositUtilizationKink", + "offset": 16, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:46" + }, + { + "label": "maxFlashFeeRate", + "offset": 24, + "slot": "357", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:49" + }, + { + "label": "optimalWithdrawalRate", + "offset": 0, + "slot": "358", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:50" + }, + { + "label": "withdrawUtilizationKink", + "offset": 8, + "slot": "358", + "type": "t_uint64", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:51" + }, + { + "label": "flashMinAmount", + "offset": 0, + "slot": "359", + "type": "t_uint256", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:54" + }, + { + "label": "depositMinAmount", + "offset": 0, + "slot": "360", + "type": "t_uint256", + "contract": "InceptionVault_S", + "src": "contracts/vaults/Symbiotic/InceptionVault_S.sol:55" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(Withdrawal)4513_storage)dyn_storage": { + "label": "struct ISymbioticHandler.Withdrawal[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)41_storage": { + "label": "uint256[41]", + "numberOfBytes": "1312" + }, + "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(IERC20)2130": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IIMellowRestaker)5620": { + "label": "contract IIMellowRestaker", + "numberOfBytes": "20" + }, + "t_contract(IISymbioticRestaker)5674": { + "label": "contract IISymbioticRestaker", + "numberOfBytes": "20" + }, + "t_contract(IInceptionRatioFeed)4206": { + "label": "contract IInceptionRatioFeed", + "numberOfBytes": "20" + }, + "t_contract(IInceptionToken)4237": { + "label": "contract IInceptionToken", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(Withdrawal)4513_storage)": { + "label": "mapping(address => struct ISymbioticHandler.Withdrawal)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Withdrawal)4513_storage": { + "label": "struct ISymbioticHandler.Withdrawal", + "members": [ + { + "label": "epoch", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "receiver", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "9c279e795383273178dae93ee54056900e59f6adc7c09c23449b3405b4e41253": { + "address": "0x6316De69B9E7D81A791Ece1cED621f4a1E1b8716", + "txHash": "0xf175eabae29b50fe96b2ab09c7f56b6aa0cf631e1f42c86b9d5edde2a46efcd0", + "layout": { + "solcVersion": "0.8.28", + "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": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_paused", + "offset": 0, + "slot": "51", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "101", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_owner", + "offset": 0, + "slot": "201", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_asset", + "offset": 0, + "slot": "251", + "type": "t_contract(IERC20)3071", + "contract": "ISymbioticRestaker", + "src": "contracts/restakers/ISymbioticRestaker.sol:31" + }, + { + "label": "_trusteeManager", + "offset": 0, + "slot": "252", + "type": "t_address", + "contract": "ISymbioticRestaker", + "src": "contracts/restakers/ISymbioticRestaker.sol:32" + }, + { + "label": "_vault", + "offset": 0, + "slot": "253", + "type": "t_address", + "contract": "ISymbioticRestaker", + "src": "contracts/restakers/ISymbioticRestaker.sol:33" + }, + { + "label": "_symbioticVaults", + "offset": 0, + "slot": "254", + "type": "t_struct(AddressSet)5326_storage", + "contract": "ISymbioticRestaker", + "src": "contracts/restakers/ISymbioticRestaker.sol:35" + }, + { + "label": "withdrawals", + "offset": 0, + "slot": "256", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ISymbioticRestaker", + "src": "contracts/restakers/ISymbioticRestaker.sol:38" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_bytes32)dyn_storage": { + "label": "bytes32[]", + "numberOfBytes": "32" + }, + "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)3071": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_uint256)": { + "label": "mapping(bytes32 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(AddressSet)5326_storage": { + "label": "struct EnumerableSet.AddressSet", + "members": [ + { + "label": "_inner", + "type": "t_struct(Set)5011_storage", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "64" + }, + "t_struct(Set)5011_storage": { + "label": "struct EnumerableSet.Set", + "members": [ + { + "label": "_values", + "type": "t_array(t_bytes32)dyn_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_indexes", + "type": "t_mapping(t_bytes32,t_uint256)", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "e27ca5eba034413313f93f2747077de1ec026cdb2a5ae5bf77f6d036dd821254": { + "address": "0xc4f4D60b84082a5be1E44146116b7bdEa57f8691", + "txHash": "0x0d2ca0ce7df9c4f29499a166d755bd9533b17199075312b65c073135cd621f85", + "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": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_paused", + "offset": 0, + "slot": "51", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "101", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_owner", + "offset": 0, + "slot": "201", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_asset", + "offset": 0, + "slot": "251", + "type": "t_contract(IERC20)2903", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:38" + }, + { + "label": "_trusteeManager", + "offset": 0, + "slot": "252", + "type": "t_address", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:39" + }, + { + "label": "_vault", + "offset": 0, + "slot": "253", + "type": "t_address", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:40" + }, + { + "label": "mellowDepositWrappers", + "offset": 0, + "slot": "254", + "type": "t_mapping(t_address,t_contract(IMellowDepositWrapper)6790)", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:43" + }, + { + "label": "mellowVaults", + "offset": 0, + "slot": "255", + "type": "t_array(t_contract(IMellowVault)7239)dyn_storage", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:44" + }, + { + "label": "allocations", + "offset": 0, + "slot": "256", + "type": "t_mapping(t_address,t_uint256)", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:46" + }, + { + "label": "totalAllocations", + "offset": 0, + "slot": "257", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:47" + }, + { + "label": "requestDeadline", + "offset": 0, + "slot": "258", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:49" + }, + { + "label": "depositSlippage", + "offset": 0, + "slot": "259", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:51" + }, + { + "label": "withdrawSlippage", + "offset": 0, + "slot": "260", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:52" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_contract(IMellowVault)7239)dyn_storage": { + "label": "contract IMellowVault[]", + "numberOfBytes": "32" + }, + "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(IERC20)2903": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IMellowDepositWrapper)6790": { + "label": "contract IMellowDepositWrapper", + "numberOfBytes": "20" + }, + "t_contract(IMellowVault)7239": { + "label": "contract IMellowVault", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_contract(IMellowDepositWrapper)6790)": { + "label": "mapping(address => contract IMellowDepositWrapper)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } + }, + "1dea2a8c9fc0c619f5d640e093e3d392bbe662c995d833b2064056926993a228": { + "address": "0xdd3A088D314020AF5f3C92a0681eD0B9Daa356C4", + "txHash": "0x66d74f5254f968b5e060b88de0f033289ddfe28e24d3324199c69c106d3b4066", + "layout": { + "solcVersion": "0.8.28", + "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": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40" + }, + { + "label": "_paused", + "offset": 0, + "slot": "51", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "101", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:88" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC165Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol:41" + }, + { + "label": "_owner", + "offset": 0, + "slot": "201", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_asset", + "offset": 0, + "slot": "251", + "type": "t_contract(IERC20)3071", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:42" + }, + { + "label": "_trusteeManager", + "offset": 0, + "slot": "252", + "type": "t_address", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:43" + }, + { + "label": "_vault", + "offset": 0, + "slot": "253", + "type": "t_address", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:44" + }, + { + "label": "mellowDepositWrappers", + "offset": 0, + "slot": "254", + "type": "t_mapping(t_address,t_contract(IMellowDepositWrapper)7702)", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:47" + }, + { + "label": "mellowVaults", + "offset": 0, + "slot": "255", + "type": "t_array(t_contract(IMellowVault)8266)dyn_storage", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:48" + }, + { + "label": "allocations", + "offset": 0, + "slot": "256", + "type": "t_mapping(t_address,t_uint256)", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:50" + }, + { + "label": "totalAllocations", + "offset": 0, + "slot": "257", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:51" + }, + { + "label": "requestDeadline", + "offset": 0, + "slot": "258", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:53" + }, + { + "label": "depositSlippage", + "offset": 0, + "slot": "259", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:55" + }, + { + "label": "withdrawSlippage", + "offset": 0, + "slot": "260", + "type": "t_uint256", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:56" + }, + { + "label": "ethWrapper", + "offset": 0, + "slot": "261", + "type": "t_address", + "contract": "IMellowRestaker", + "src": "contracts/restakers/IMellowRestaker.sol:58" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_contract(IMellowVault)8266)dyn_storage": { + "label": "contract IMellowVault[]", + "numberOfBytes": "32" + }, + "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(IERC20)3071": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(IMellowDepositWrapper)7702": { + "label": "contract IMellowDepositWrapper", + "numberOfBytes": "20" + }, + "t_contract(IMellowVault)8266": { + "label": "contract IMellowVault", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_contract(IMellowDepositWrapper)7702)": { + "label": "mapping(address => contract IMellowDepositWrapper)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/projects/vaults/.solcover.js b/projects/vaults/.solcover.js index 72ff6bf7..5931ae81 100644 --- a/projects/vaults/.solcover.js +++ b/projects/vaults/.solcover.js @@ -1,3 +1,12 @@ module.exports = { - skipFiles: ["tests/"], + skipFiles: [ + 'tests/', + 'restakers/', + 'vaults/EigenLayer/', + 'vaults/EigenLayer/facets/', + 'vaults/EigenLayer/facets/ERC4626Facet/', + 'vaults/Symbiotic/vault_e2/InVault_S_E2.sol', + 'lib/FullMath.sol' + ], }; + diff --git a/projects/vaults/README.md b/projects/vaults/README.md index c3379b1e..90b794e7 100644 --- a/projects/vaults/README.md +++ b/projects/vaults/README.md @@ -67,63 +67,79 @@ Additionally, the corresponding _RateProviders_ were deployed for all LRT (Incep ## Testing +Refer to `package.json` scripts to see all available test commands. + +Generally, you can run the tests with `npx hardhat test`. + +Before running tests create `.env` file in the root of the `vaults` project based on `.env.example` file. + To run tests for the Inception Protocol, please follow these instructions: -1. Set up a fork RPC: +## Coverage + +1. Generate coverage report. -- Windows: `export RPC_URL_ETHEREUM=""` -- MacOs/LinuxOs: `RPC_URL_ETHEREUM=""` +To run coverage, refer to the `package.json` scripts. -2. Set the `solidity.compilers[0].settings.runs: 0` before contracts compilation in hardhat.config.js, - otherwise may cause `Block not found` error. +It will generate a coverage report in the `coverage` folder. Open `index.html` in your browser to view the report. -3. Set any `DEPLOYER_PRIVATE_KEY` env or comment the line in hardhat.config.js. +2. Check if coverage meets the minimum threshold. -4. Compile with `npx hardhat compile`. +There is a `check-coverage.sh` script that will check if coverage meets the minimum threshold. The threshold for each coverage type could be set in the the `check-coverage.sh` file. +If any of threshold is not met, the script will exit with a non-zero code (job will fail). +After running coverage check, it will add the results as pull request comment. +Also, CI job will generate the full report as artfact, download it and open `index.html` file. -5. It's possible to run tests for specific LSTs or all supported: +> Don't decrease the coverage threshold values in the script file, write more tests instead. Increasing the value is encouraged. -- Paricular LSTs case: - `ASSETS=athc,wbeth npx hardhat test` +## CI -- Running all tests at once: - `npx hardhat test` +There is manually triggered job to run the coverage check. ## InceptionVault_S + 1. User flow: - - 1. Deposit - - Approve vault's underlying `InceptionVault_S.asset()` to the vault - - Call `deposit(uint256 amount, address receiver)` - - Receive inception token as vault shares - 2. Redeem - - Call `redeem(uint256 shares, address receiver, address owner)` - - Vault burns inception tokens of `owner` equal to `shares` - - Corresponding vault's underlying `InceptionVault_S.asset()` are received by `receiver` + + 1. Deposit + - Approve vault's underlying `InceptionVault_S.asset()` to the vault + - Call `deposit(uint256 amount, address receiver)` + - Receive inception token as vault shares + 2. Redeem + - Call `redeem(uint256 shares, address receiver, address owner)` + - Vault burns inception tokens of `owner` equal to `shares` + - Corresponding vault's underlying `InceptionVault_S.asset()` are received by `receiver` + 2. Mellow Integration: - - 1. Deposit flow - - `InceptionVault_S` via the `IMellowRestaker` deposits assets into mellow vaults proportional to assigned allocations - - `InceptionVault_S.delegateToMellowVault(address mellowVault, uint256 amount)` calls `IMellowRestaker.delegateMellow(uint256 amount, uint256 deadline, address mellowVault)` to forward assets to `IMellowRestaker` - - `IMellowRestaker` then calls `MellowWrapper.deposit(address to, address token, uint256 amount, uint256 minLpAmount, uint256 deadline)` to deposit assets to Mellow Vault - 2. Withdraw flow - - `InceptionVault_S.undelegateFrom(address mellowVault, uint256 amount)` calls `IMellowRestaker.withdrawMellow(mellowVault, amount, true)` with `closePrevious` set to `true` - - `IMellowRestaker` then calls `registerWithdrawal(address to, uint256 lpAmount, uint256[] memory minAmounts, uint256 deadline, uint256 requestDeadline, bool closePrevious)` to generate withdrawal request - 3. Emergency withdraw - - `InceptionVault_S` does support emergency withdraw using `undelegateForceFrom(address mellowVault, uint256 amount)` - - This inturn calls `IMellowRestaker.withdrawEmergencyMellow(address _mellowVault, uint256 amount)` which calls `mellowVault.function emergencyWithdraw(uint256[] memory minAmounts, uint256 deadline)` - 4. Mellow rewards - - Mellow staking rewards accumulation are reflected by `InceptionVault_S.ratio()` which takes into account the balance + rewards - 5. Flash withdraw - - `InceptionVault_S` does support flash withdrawal since withdrawal from mellow has withdrawal process delay - - `InceptionVault_S.flashWithdraw(uint256 iShares, address receiver)` allows the user to receive assets immediately on withdrawal transaction - - Flash withdrawal incurs additional flash fees, which are calculated by `InceptionLibrary` based on utilization and optimal rate - - Part of fees go to Protocol and part are added to `depositBonusAmount` for depositors + + 1. Deposit flow + - `InceptionVault_S` via the `IMellowAdapter` deposits assets into mellow vaults proportional to assigned allocations + - `InceptionVault_S.delegateToMellowVault(address mellowVault, uint256 amount)` calls `IMellowAdapter.delegateMellow(uint256 amount, uint256 deadline, address mellowVault)` to forward assets to `IMellowAdapter` + - `IMellowAdapter` then calls `MellowWrapper.deposit(address to, address token, uint256 amount, uint256 minLpAmount, uint256 deadline)` to deposit assets to Mellow Vault + 2. Withdraw flow + - `InceptionVault_S.undelegateFrom(address mellowVault, uint256 amount)` calls `IMellowAdapter.withdrawMellow(mellowVault, amount, true)` with `closePrevious` set to `true` + - `IMellowAdapter` then calls `registerWithdrawal(address to, uint256 lpAmount, uint256[] memory minAmounts, uint256 deadline, uint256 requestDeadline, bool closePrevious)` to generate withdrawal request + 3. Emergency withdraw + - `InceptionVault_S` does support emergency withdraw using `undelegateForceFrom(address mellowVault, uint256 amount)` + - This inturn calls `IMellowAdapter.withdrawEmergencyMellow(address _mellowVault, uint256 amount)` which calls `mellowVault.function emergencyWithdraw(uint256[] memory minAmounts, uint256 deadline)` + 4. Mellow rewards + - Mellow staking rewards accumulation are reflected by `InceptionVault_S.ratio()` which takes into account the balance + rewards + 5. Flash withdraw + - `InceptionVault_S` does support flash withdrawal since withdrawal from mellow has withdrawal process delay + - `InceptionVault_S.flashWithdraw(uint256 iShares, address receiver)` allows the user to receive assets immediately on withdrawal transaction + - Flash withdrawal incurs additional flash fees, which are calculated by `InceptionLibrary` based on utilization and optimal rate + - Part of fees go to Protocol and part are added to `depositBonusAmount` for depositors + 3. Mainnet params: - - - Operator = 0xd87D15b80445EC4251e33dBe0668C335624e54b7 - - withdrawUtilizationKink = 25 * 1e8 - - optimalWithdrawalRate = 5 * 1e7 - - Supported vaults = [MEV: 0x5fD13359Ba15A84B76f7F87568309040176167cd] - + - Operator = 0xd87D15b80445EC4251e33dBe0668C335624e54b7 + - withdrawUtilizationKink = 25 \* 1e8 + - optimalWithdrawalRate = 5 \* 1e7 + - Supported vaults = [MEV: 0x5fD13359Ba15A84B76f7F87568309040176167cd] + +# Troubleshooting + +- `Error: Trying to initialize a provider with block X but the current block is Y` + +Looks like the RPC provider is not in sync with the network. Please make sure you set the RPC url correctly. + +Solution: set RPC env variable (which matches the network you are using). \ No newline at end of file diff --git a/projects/vaults/check-coverage.sh b/projects/vaults/check-coverage.sh new file mode 100755 index 00000000..82b478f1 --- /dev/null +++ b/projects/vaults/check-coverage.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +{ +set -e + +# the thresholds below are supposed to be cahnged (but only ^increased) +#! DO NOT decrease the values, if coverage does not meet the threshold – write new tests +DEFAULT_THRESHOLD=90 +THRESHOLD_STATEMENTS=95 +THRESHOLD_BRANCHES=79 +THRESHOLD_FUNCTIONS=95 +THRESHOLD_LINES=96 + + +THRESHOLD_STATEMENTS=${THRESHOLD_STATEMENTS:-$DEFAULT_THRESHOLD} +THRESHOLD_BRANCHES=${THRESHOLD_BRANCHES:-$DEFAULT_THRESHOLD} +THRESHOLD_FUNCTIONS=${THRESHOLD_FUNCTIONS:-$DEFAULT_THRESHOLD} +THRESHOLD_LINES=${THRESHOLD_LINES:-$DEFAULT_THRESHOLD} + + +REPORT_FILE="./coverage/index.html" + +echo "Thresholds: Lines: $THRESHOLD_LINES%, Statements: $THRESHOLD_STATEMENTS%, Functions: $THRESHOLD_FUNCTIONS%, Branches: $THRESHOLD_BRANCHES%" +echo # add empty line + +if [ ! -f "$REPORT_FILE" ]; then + echo "❌ Coverage report not found at $REPORT_FILE" + exit 1 +fi + +extract_coverage() { + local label=$1 + awk -v target="$label" ' + BEGIN { RS="
"; FS="\n" } + $0 ~ target { + for (i=1; i<=NF; i++) { + if ($i ~ target) { + if ((i>1) && (match($(i-1), />[0-9.]+%/))) { + pct = substr($(i-1), RSTART+1, RLENGTH-2); + print pct; + exit; + } + } + } + } + ' "$REPORT_FILE" +} + +check_threshold() { + local type=$1 + local value=$2 + local threshold=$3 + local value_int=${value%.*} + + # echo "$type coverage: $value%" + + if [ "$value_int" -lt "$threshold" ]; then + echo "❌ $type coverage $value% is below threshold $threshold%" + exit 1 + else + echo "✅ $type coverage meets threshold $threshold%" + fi +} + +LINES=$(extract_coverage "Lines") +STATEMENTS=$(extract_coverage "Statements") +FUNCTIONS=$(extract_coverage "Functions") +BRANCHES=$(extract_coverage "Branches") + +check_threshold "Lines" "$LINES" "$THRESHOLD_LINES" +check_threshold "Statements" "$STATEMENTS" "$THRESHOLD_STATEMENTS" +check_threshold "Functions" "$FUNCTIONS" "$THRESHOLD_FUNCTIONS" +check_threshold "Branches" "$BRANCHES" "$THRESHOLD_BRANCHES" + +# save output to file +} 2>&1 | tee -a coverage_logs.txt \ No newline at end of file diff --git a/projects/vaults/contracts/adapter-claimers/MellowAdapterClaimer.sol b/projects/vaults/contracts/adapter-claimers/MellowAdapterClaimer.sol new file mode 100644 index 00000000..26785805 --- /dev/null +++ b/projects/vaults/contracts/adapter-claimers/MellowAdapterClaimer.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IAdapterClaimer} from "../interfaces/adapter-claimer/IAdapterClaimer.sol"; +import {IMellowSymbioticVault} from "../interfaces/symbiotic-vault/mellow-core/IMellowSymbioticVault.sol"; + +/** + * @title MellowAdapterClaimer + * @author The InceptionLRT team + * @notice Adapter claimer for Mellow Vaults + * @notice In order to claim withdrawals multiple times + * @dev This contract is used to claim rewards from Mellow Vaults + */ +contract MellowAdapterClaimer is + PausableUpgradeable, + ReentrancyGuardUpgradeable, + ERC165Upgradeable, + OwnableUpgradeable, + IAdapterClaimer +{ + address internal _adapter; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() payable { + _disableInitializers(); + } + + function initialize(address asset) external initializer { + __Pausable_init(); + __ReentrancyGuard_init(); + __Ownable_init(); + __ERC165_init(); + + _adapter = msg.sender; + require( + IERC20(asset).approve(_adapter, type(uint256).max), + ApprovalFailed() + ); + } + + /** + * @notice Claims withdrawals from a Mellow Symbiotic Vault + * @notice executable only by the adapter + * @param vault The address of the Mellow Symbiotic Vault + * @param recipient The address to receive the rewards + * @param amount The amount of withdrawals to claim + * @return The amount of withdrawals claimed + */ + function claim( + address vault, + address recipient, + uint256 amount + ) external returns (uint256) { + require(msg.sender == _adapter, OnlyAdapter()); + return + IMellowSymbioticVault(vault).claim( + address(this), + recipient, + amount + ); + } +} \ No newline at end of file diff --git a/projects/vaults/contracts/adapter-claimers/SymbioticAdapterClaimer.sol b/projects/vaults/contracts/adapter-claimers/SymbioticAdapterClaimer.sol new file mode 100644 index 00000000..a1d96ade --- /dev/null +++ b/projects/vaults/contracts/adapter-claimers/SymbioticAdapterClaimer.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IVault} from "../interfaces/symbiotic-vault/symbiotic-core/IVault.sol"; +import {IAdapterClaimer} from "../interfaces/adapter-claimer/IAdapterClaimer.sol"; + +/** + * @title SymbioticAdapterClaimer + * @author The InceptionLRT team + * @notice Adapter claimer for Symbiotic Vaults + * @notice In order to claim withdrawals multiple times + * @dev This contract is used to claim rewards from Symbiotic Vaults + */ +contract SymbioticAdapterClaimer is + PausableUpgradeable, + ReentrancyGuardUpgradeable, + ERC165Upgradeable, + OwnableUpgradeable, + IAdapterClaimer +{ + address internal _adapter; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() payable { + _disableInitializers(); + } + + function initialize(address asset) external initializer { + __Pausable_init(); + __ReentrancyGuard_init(); + __Ownable_init(); + __ERC165_init(); + + _adapter = msg.sender; + require( + IERC20(asset).approve(_adapter, type(uint256).max), + ApprovalFailed() + ); + } + + + /** + * @notice Claims rewards from a Symbiotic Vault + * @notice executable only by the adapter + * @param vault The address of the Symbiotic Vault + * @param recipient The address to receive the rewards + * @param epoch The epoch to claim withdrawals for + * @return The amount of rewards claimed + */ + function claim( + address vault, + address recipient, + uint256 epoch + ) external returns (uint256) { + require(msg.sender == _adapter, OnlyAdapter()); + return IVault(vault).claim(recipient, epoch); + } +} \ No newline at end of file diff --git a/projects/vaults/contracts/adapter-handler/AdapterHandler.sol b/projects/vaults/contracts/adapter-handler/AdapterHandler.sol new file mode 100644 index 00000000..cd3ff2b8 --- /dev/null +++ b/projects/vaults/contracts/adapter-handler/AdapterHandler.sol @@ -0,0 +1,527 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Address} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import {IAdapterHandler} from "../interfaces/adapter-handler/IAdapterHandler.sol"; +import {IInceptionBaseAdapter} from "../interfaces/adapters/IInceptionBaseAdapter.sol"; +import {IInceptionMellowAdapter} from "../interfaces/adapters/IInceptionMellowAdapter.sol"; +import {IInceptionSymbioticAdapter} from "../interfaces/adapters/IInceptionSymbioticAdapter.sol"; +import {IWithdrawalQueue} from "../interfaces/common/IWithdrawalQueue.sol"; +import {InceptionAssetsHandler, IERC20} from "../assets-handler/InceptionAssetsHandler.sol"; + +/** + * @title The AdapterHandler contract + * @author The InceptionLRT team + * @dev Serves communication with external Protocols + * @dev Specifically, this includes depositing, and handling withdrawal requests + */ +contract AdapterHandler is InceptionAssetsHandler, IAdapterHandler { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /** + * @dev Deprecated variable representing the epoch (no longer in use). + */ + uint256 private __deprecated_epoch; + + /** + * @dev The address of the inception operator responsible for managing the contract. + */ + address internal _operator; + + /** + * @dev Instance of the Mellow adapter interface for interacting with Mellow-related functionality. + */ + IInceptionMellowAdapter private __deprecated_mellowAdapter; + + /** + * @dev Deprecated variable representing the total amount pending to be redeemed by claimers. + * @notice Previously included the amount to undelegate from Mellow. + */ + uint256 private __deprecated_totalAmountToWithdraw; + + /** + * @dev Deprecated array storing the queue of withdrawal requests from claimers. + */ + __deprecated_Withdrawal[] private __deprecated_claimerWithdrawalsQueue; + + /** + * @dev Deprecated variable representing the heap reserved for claimers' withdrawals. + */ + uint256 private __deprecated_redeemReservedAmount; + + /** + * @dev The bonus amount provided for deposits to incentivize staking. + */ + uint256 public depositBonusAmount; + + /** + * @dev The target capacity of the system, measured in percentage. + * @notice Expressed as a value up to MAX_TARGET_PERCENT (100% = 100 * 1e18). + */ + uint256 public targetCapacity; + + /** + * @dev Constant representing the maximum target percentage (100%). + * @notice Used as a reference for targetCapacity calculations, scaled to 1e18. + */ + uint256 public constant MAX_TARGET_PERCENT = 100 * 1e18; + + /** + * @dev Instance of the Symbiotic adapter interface for interacting with Symbiotic-related functionality. + */ + IInceptionSymbioticAdapter private __deprecated_symbioticAdapter; + + /** + * @dev Set of adapter addresses currently registered in the system. + */ + EnumerableSet.AddressSet internal _adapters; + + /** + * @dev Instance of the withdrawal queue interface for managing withdrawal requests. + */ + IWithdrawalQueue public withdrawalQueue; + + /** + * @dev Reserved storage gap to allow for future upgrades without shifting storage layout. + * @notice Occupies 38 slots (50 total slots minus 12 used). + */ + uint256[50 - 11] private __gap; + + modifier onlyOperator() { + require(msg.sender == _operator, OnlyOperatorAllowed()); + _; + } + + /** + * @notice Initializes the AdapterHandler contract + * @param assetAddress The address of the underlying asset token + */ + function __AdapterHandler_init( + IERC20 assetAddress + ) internal onlyInitializing { + __InceptionAssetsHandler_init(assetAddress); + } + + /*////////////////////////////// + ////// Deposit functions ////// + ////////////////////////////*/ + + /** + * @notice Delegates assets to a specific adapter and vault + * @param adapter The address of the adapter to delegate to + * @param vault The address of the vault to delegate to + * @param amount The amount of assets to delegate + * @param _data Additional data required for delegation + * @dev Can only be called by the operator + */ + function delegate( + address adapter, + address vault, + uint256 amount, + bytes[] calldata _data + ) external nonReentrant whenNotPaused onlyOperator { + uint256 freeBalance = getFreeBalance(); + + require(amount <= freeBalance, InsufficientCapacity(freeBalance)); + require(adapter != address(0), NullParams()); + require(_adapters.contains(adapter), AdapterNotFound()); + + _asset.safeIncreaseAllowance(address(adapter), amount); + IInceptionBaseAdapter(adapter).delegate(vault, amount, _data); + + emit DelegatedTo(adapter, vault, amount); + } + + /** + * Undelegates assets from specified vaults and adapters for a given epoch. + * @param undelegatedEpoch The epoch in which the undelegation occurs. + * @param requests An array of UndelegateRequest structs containing undelegation details. + * Each UndelegateRequest specifies the adapter, vault, amount, and additional data for undelegation. + */ + function undelegate( + uint256 undelegatedEpoch, + UndelegateRequest[] calldata requests + ) external whenNotPaused nonReentrant onlyOperator { + if (requests.length == 0) return _undelegateAndClaim(undelegatedEpoch); + + uint256[] memory undelegatedAmounts = new uint256[](requests.length); + uint256[] memory claimedAmounts = new uint256[](requests.length); + address[] memory adapters = new address[](requests.length); + address[] memory vaults = new address[](requests.length); + + for (uint256 i = 0; i < requests.length; i++) { + // undelegate adapter + (undelegatedAmounts[i], claimedAmounts[i]) = _undelegate( + requests[i].adapter, requests[i].vault, requests[i].amount, requests[i].data, false + ); + + adapters[i] = requests[i].adapter; + vaults[i] = requests[i].vault; + + emit UndelegatedFrom( + requests[i].adapter, requests[i].vault, undelegatedAmounts[i], claimedAmounts[i], undelegatedEpoch + ); + } + + // undelegate from queue + withdrawalQueue.undelegate( + undelegatedEpoch, adapters, vaults, undelegatedAmounts, claimedAmounts + ); + } + + /** + * @notice Internal function to handle undelegation from a single adapter + * @param adapter The adapter address + * @param vault The vault address + * @param amount The amount to undelegate + * @param _data Additional data required for undelegation + * @param emergency Whether this is an emergency undelegation + * @return undelegated Amount that was undelegated + * @return claimed Amount that was claimed + */ + function _undelegate( + address adapter, + address vault, + uint256 amount, + bytes[] calldata _data, + bool emergency + ) internal returns (uint256 undelegated, uint256 claimed) { + require(_adapters.contains(adapter), AdapterNotFound()); + require(vault != address(0), InvalidAddress()); + require(amount > 0, ValueZero()); + // undelegate from adapter + return IInceptionBaseAdapter(adapter).withdraw(vault, amount, _data, emergency); + } + + /** + * @notice Processes undelegation and claims for a specific epoch + * @param undelegatedEpoch The epoch number to process + */ + function _undelegateAndClaim(uint256 undelegatedEpoch) internal { + uint256 requestedAmount = IERC4626(address(this)).convertToAssets( + withdrawalQueue.getRequestedShares(undelegatedEpoch) + ); + + if (getFlashCapacity() < requestedAmount) revert InsufficientFreeBalance(); + withdrawalQueue.forceUndelegateAndClaim(undelegatedEpoch, requestedAmount); + + emit ClaimFromVault(requestedAmount, undelegatedEpoch); + } + + /** + * @notice Initiates emergency undelegation from multiple adapters + * @param requests An array of UndelegateRequest structs containing undelegation details. + * Each UndelegateRequest specifies the adapter, vault, amount, and additional data for undelegation. + */ + function emergencyUndelegate( + UndelegateRequest[] calldata requests + ) external whenNotPaused nonReentrant onlyOperator { + require(requests.length > 0, ValueZero()); + + uint256 epoch = withdrawalQueue.EMERGENCY_EPOCH(); + for (uint256 i = 0; i < requests.length; i++) { + (uint256 undelegatedAmount, uint256 claimedAmount) = _undelegate( + requests[i].adapter, requests[i].vault, requests[i].amount, requests[i].data, true + ); + + emit UndelegatedFrom( + requests[i].adapter, requests[i].vault, undelegatedAmount, claimedAmount, epoch + ); + } + } + + /** + * @notice Claims assets from multiple adapters for a specific epoch + * @param epochNum The epoch number to claim for + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + * @param _data Array of additional data required for claiming + */ + function claim( + uint256 epochNum, + address[] calldata adapters, + address[] calldata vaults, + bytes[][] calldata _data + ) public onlyOperator whenNotPaused nonReentrant { + require(adapters.length > 0 && adapters.length == vaults.length && vaults.length == _data.length, ValueZero()); + + uint256[] memory claimedAmounts = new uint256[](adapters.length); + for (uint256 i = 0; i < adapters.length; i++) { + // claim from adapter + claimedAmounts[i] = _claim(adapters[i], _data[i], false); + emit ClaimedFrom(adapters[i], vaults[i], claimedAmounts[i], epochNum); + } + + withdrawalQueue.claim(epochNum, adapters, vaults, claimedAmounts); + } + + /** + * @notice Claims assets in emergency mode from multiple adapters + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + * @param _data Array of additional data required for claiming + */ + function emergencyClaim( + address[] calldata adapters, + address[] calldata vaults, + bytes[][] calldata _data + ) public onlyOperator whenNotPaused nonReentrant { + require(adapters.length > 0 && adapters.length == vaults.length && vaults.length == _data.length, ValueZero()); + + uint256 epoch = withdrawalQueue.EMERGENCY_EPOCH(); + for (uint256 i = 0; i < adapters.length; i++) { + // claim from adapter + uint256 claimedAmount = _claim(adapters[i], _data[i], true); + emit ClaimedFrom(adapters[i], vaults[i], claimedAmount, epoch); + } + } + + /** + * @notice Internal function to claim assets from a single adapter + * @param adapter The adapter address + * @param _data Additional data required for claiming + * @param emergency Whether this is an emergency claim + * @return Amount of assets claimed + */ + function _claim(address adapter, bytes[] calldata _data, bool emergency) internal returns (uint256) { + require(_adapters.contains(adapter), AdapterNotFound()); + return IInceptionBaseAdapter(adapter).claim(_data, emergency); + } + + /** + * @notice Claims the free balance from a specified adapter contract. + * @dev Can only be called by an operator, when the contract is not paused, and is non-reentrant. + * @param adapter The address of the adapter contract from which to claim the free balance. + */ + function claimAdapterFreeBalance(address adapter) external onlyOperator nonReentrant { + IInceptionBaseAdapter(adapter).claimFreeBalance(); + emit AdapterFreeBalanceClaimed(adapter); + } + + /** + * @notice Claim and transfer rewards from the specified adapter to the rewards treasury. + * The treasury may optionally swap the received tokens and forward them to the operator + * for further distribution to the vault as additional rewards. + * @dev Can only be called by an operator, when the contract is not paused, and is non-reentrant. + * @param adapter The address of the adapter contract from which to claim rewards. + * @param token Reward token. + * @param rewardsData Adapter related bytes of data for rewards. + */ + function claimAdapterRewards(address adapter, address token, bytes calldata rewardsData) external onlyOperator nonReentrant { + require(adapter != address(0) && token != address(0), NullParams()); + require(rewardsTreasury != address(0), RewardsTreasuryNotSet()); + + IERC20 rewardToken = IERC20(token); + uint256 rewardAmount = rewardToken.balanceOf(address(this)); + + // claim rewards from protocol + IInceptionBaseAdapter(adapter).claimRewards(token, rewardsData); + + rewardAmount = rewardToken.balanceOf(address(this)) - rewardAmount; + require(rewardAmount > 0, ValueZero()); + + rewardToken.safeTransfer(rewardsTreasury, rewardAmount); + + emit RewardsClaimed(adapter, token, rewardAmount); + } + + /** + * @notice Adds new rewards to the contract, starting a new rewards timeline. + * @dev The function allows the operator to deposit asset as rewards. + * It verifies that the previous rewards timeline is over before accepting new rewards. + */ + function addRewards(uint256 amount) external onlyOperator nonReentrant { + /// @dev verify whether the prev timeline is over + if (currentRewards > 0) { + uint256 totalDays = rewardsTimeline / 1 days; + uint256 dayNum = (block.timestamp - startTimeline) / 1 days; + if (dayNum < totalDays) revert TimelineNotOver(); + } + + _asset.safeTransferFrom(_operator, address(this), amount); + + currentRewards = amount; + startTimeline = block.timestamp; + + emit RewardsAdded(amount, startTimeline); + } + + /*////////////////////////// + ////// GET functions ////// + ////////////////////////*/ + + /** + * @notice Returns the total amount deposited across all strategies + * @return Total deposited amount including pending withdrawals and excluding bonus, redeem reserved + */ + function getTotalDeposited() public view returns (uint256) { + return + getTotalDelegated() + + totalAssets() + + getTotalInactiveBalance() - + redeemReservedAmount() - + depositBonusAmount; + } + + /** + * @notice Returns the total amount delegated across all adapters + * @return Total delegated amount + */ + function getTotalDelegated() public view returns (uint256) { + uint256 total; + for (uint256 i = 0; i < _adapters.length(); i++) { + total += IInceptionBaseAdapter(_adapters.at(i)).getTotalDeposited(); + } + return total; + } + + /** + * @notice Returns the amount delegated to a specific adapter and vault + * @param adapter The adapter address + * @param vault The vault address + * @return Amount delegated + */ + function getDelegatedTo( + address adapter, + address vault + ) external view returns (uint256) { + return IInceptionBaseAdapter(adapter).getDeposited(vault); + } + + /** + * @notice Returns the available balance for new deposits + * @return total Available balance considering target capacity + */ + function getFreeBalance() public view returns (uint256 total) { + uint256 flashCapacity = getFlashCapacity(); + uint256 targetFlash = _getTargetCapacity(); + return flashCapacity < targetFlash ? 0 : flashCapacity - targetFlash; + } + + /** + * @notice Returns pending withdrawals for a specific adapter + * @param adapter The adapter address + * @return Amount of pending withdrawals + */ + function getPendingWithdrawals( + address adapter + ) public view returns (uint256) { + return IInceptionBaseAdapter(adapter).pendingWithdrawalAmount(); + } + + /** + * @notice Returns total pending withdrawals across all adapters + * @return Total amount of pending withdrawals + */ + function getTotalPendingWithdrawals() public view returns (uint256) { + uint256 total; + for (uint256 i = 0; i < _adapters.length(); i++) { + total += getPendingWithdrawals(_adapters.at(i)); + } + return total; + } + + /** + * @notice Returns total pending emergency withdrawals across all adapters + * @return Total amount of emergency withdrawals + */ + function getTotalPendingEmergencyWithdrawals() public view returns (uint256) { + uint256 total; + for (uint256 i = 0; i < _adapters.length(); i++) { + total += IInceptionBaseAdapter(_adapters.at(i)).pendingEmergencyWithdrawalAmount(); + } + return total; + } + + /** + * @notice Returns total pending emergency withdrawals across all adapters + * @return Total amount of emergency withdrawals + */ + function getTotalInactiveBalance() public view returns (uint256) { + uint256 total; + for (uint256 i = 0; i < _adapters.length(); i++) { + total += IInceptionBaseAdapter(_adapters.at(i)).inactiveBalance(); + } + return total; + } + + /** + * @notice Returns the current flash capacity + * @return total Available capacity for flash loans + */ + function getFlashCapacity() public view returns (uint256 total) { + uint256 _assets = totalAssets(); + uint256 _sum = redeemReservedAmount() + depositBonusAmount; + return _sum > _assets ? 0 : _assets - _sum; + } + + /** + * @notice Calculates the target capacity based on total deposits + * @return Target capacity amount + */ + function _getTargetCapacity() internal view returns (uint256) { + return (targetCapacity * getTotalDeposited()) / MAX_TARGET_PERCENT; + } + + /** + * @notice Returns the total shares pending withdrawal + * @return Total shares to withdraw + */ + function totalSharesToWithdraw() public view returns (uint256) { + return withdrawalQueue.totalSharesToWithdraw(); + } + + /** + * @notice Returns the amount reserved for redemptions + * @return Reserved amount for redemptions + */ + function redeemReservedAmount() public view returns (uint256) { + return withdrawalQueue.totalAmountRedeem(); + } + + /*////////////////////////// + ////// SET functions ////// + ////////////////////////*/ + + /** + * @notice Sets the target flash capacity percentage + * @param newTargetCapacity New target capacity value (must be less than MAX_TARGET_PERCENT) + */ + function setTargetFlashCapacity( + uint256 newTargetCapacity + ) external onlyOwner { + if (newTargetCapacity == 0) revert InvalidTargetFlashCapacity(); + if (newTargetCapacity >= MAX_TARGET_PERCENT) revert MoreThanMax(); + emit TargetCapacityChanged(targetCapacity, newTargetCapacity); + targetCapacity = newTargetCapacity; + } + + /** + * @notice Adds a new adapter to the system + * @param adapter Address of the adapter to add + */ + function addAdapter(address adapter) external onlyOwner { + require(Address.isContract(adapter), NotContract()); + require(!_adapters.contains(adapter), AdapterAlreadyAdded()); + + emit AdapterAdded(adapter); + _adapters.add(adapter); + } + + /** + * @notice Removes an adapter from the system + * @param adapter Address of the adapter to remove + */ + function removeAdapter(address adapter) external onlyOwner { + require(_adapters.contains(adapter), AdapterNotFound()); + require(IInceptionBaseAdapter(adapter).getTotalBalance() == 0, AdapterNotEmpty()); + + emit AdapterRemoved(adapter); + _adapters.remove(adapter); + } +} diff --git a/projects/vaults/contracts/adapters/InceptionBaseAdapter.sol b/projects/vaults/contracts/adapters/InceptionBaseAdapter.sol new file mode 100644 index 00000000..96958797 --- /dev/null +++ b/projects/vaults/contracts/adapters/InceptionBaseAdapter.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +import {IInceptionBaseAdapter} from "../interfaces/adapters/IInceptionBaseAdapter.sol"; + +/** + * @title InceptionBaseAdapter + * @author The InceptionLRT team + */ +abstract contract InceptionBaseAdapter is + PausableUpgradeable, + ReentrancyGuardUpgradeable, + ERC165Upgradeable, + OwnableUpgradeable, + IInceptionBaseAdapter +{ + using SafeERC20 for IERC20; + + IERC20 internal _asset; + address internal _trusteeManager; + address internal _inceptionVault; + + modifier onlyTrustee() { + require( + msg.sender == _inceptionVault || msg.sender == _trusteeManager, + NotVaultOrTrusteeManager() + ); + _; + } + + /** + * @notice Internal function to initialize the base adapter + * @param asset The ERC20 token used as the underlying asset + * @param trusteeManager Address of the trustee manager + */ + function __InceptionBaseAdapter_init( + IERC20 asset, + address trusteeManager + ) internal onlyInitializing { + __Pausable_init(); + __ReentrancyGuard_init(); + __Ownable_init(); + __ERC165_init(); + + _asset = asset; + _trusteeManager = trusteeManager; + } + + /** + * @notice Claims the free balance held by this contract and transfers it to the Inception Vault. + * @dev Can only be called by a trustee. + */ + function claimFreeBalance() external onlyTrustee { + _asset.safeTransfer(_inceptionVault, claimableAmount()); + } + + /** + * @notice Returns the amount of tokens that can be claimed + * @return Amount of claimable tokens for the adapter + */ + function claimableAmount() public view virtual override returns (uint256) { + return _asset.balanceOf(address(this)); + } + + /** + * @notice Sets the inception vault address + * @dev Can only be called by owner + * @param inceptionVault New inception vault address + */ + function setInceptionVault(address inceptionVault) external onlyOwner { + require(Address.isContract(inceptionVault), NotContract()); + emit InceptionVaultSet(_inceptionVault, inceptionVault); + _inceptionVault = inceptionVault; + } + + /** + * @notice Sets the trustee manager address + * @dev Can only be called by owner + * @param _newTrusteeManager New trustee manager address + */ + function setTrusteeManager(address _newTrusteeManager) external onlyOwner { + emit TrusteeManagerSet(_trusteeManager, _newTrusteeManager); + _trusteeManager = _newTrusteeManager; + } + + /** + * @notice Pauses the contract + * @dev Can only be called by owner + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpauses the contract + * @dev Can only be called by owner + */ + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @notice Returns the contract version + * @return Version number of the contract + */ + function getVersion() external pure virtual returns (uint256) { + return 1; + } +} + diff --git a/projects/vaults/contracts/adapters/InceptionEigenAdapter.sol b/projects/vaults/contracts/adapters/InceptionEigenAdapter.sol new file mode 100644 index 00000000..9c3b3f8a --- /dev/null +++ b/projects/vaults/contracts/adapters/InceptionEigenAdapter.sol @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IInceptionEigenLayerAdapter} from "../interfaces/adapters/IInceptionEigenLayerAdapter.sol"; +import {IDelegationManager} from "../interfaces/eigenlayer-vault/eigen-core/IDelegationManager.sol"; +import {IStrategy} from "../interfaces/eigenlayer-vault/eigen-core/IStrategy.sol"; +import {IStrategyManager} from "../interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol"; +import {IRewardsCoordinator} from "../interfaces/eigenlayer-vault/eigen-core/IRewardsCoordinator.sol"; +import {InceptionBaseAdapter, IInceptionBaseAdapter} from "./InceptionBaseAdapter.sol"; + +/** + * @title The InceptionEigenAdapter Contract + * @author The InceptionLRT team + * @dev Handles delegation and withdrawal requests within the EigenLayer protocol. + * @notice Can only be executed by InceptionVault/InceptionOperator or the owner. + */ +contract InceptionEigenAdapter is InceptionBaseAdapter, IInceptionEigenLayerAdapter { + using SafeERC20 for IERC20; + + IStrategy internal _strategy; + IStrategyManager internal _strategyManager; + IDelegationManager internal _delegationManager; + IRewardsCoordinator public rewardsCoordinator; + mapping(uint256 => bool) internal _emergencyQueuedWithdrawals; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() payable { + _disableInitializers(); + } + + /** + * @notice Initializes the adapter contract with required addresses and parameters + * @param claimer Address of the contract owner + * @param rewardCoordinator Address of the rewards coordinator contract + * @param delegationManager Address of the delegation manager contract + * @param strategyManager Address of the strategy manager contract + * @param strategy Address of the strategy contract + * @param asset Address of the underlying asset token + * @param trusteeManager Address of the trustee manager + * @param inceptionVault Address of the inception vault + */ + function initialize( + address claimer, + address rewardCoordinator, + address delegationManager, + address strategyManager, + address strategy, + address asset, + address trusteeManager, + address inceptionVault + ) public initializer { + __InceptionBaseAdapter_init(IERC20(asset), trusteeManager); + + _delegationManager = IDelegationManager(delegationManager); + _strategyManager = IStrategyManager(strategyManager); + _strategy = IStrategy(strategy); + _inceptionVault = inceptionVault; + _setRewardsCoordinator(rewardCoordinator, claimer); + + // approve spending by strategyManager + _asset.safeApprove(strategyManager, type(uint256).max); + } + + /** + * @dev checks whether it's still possible to deposit into the strategy + * @param amount Amount of tokens to delegate/deposit + * @notice Be cautious when using this function, as certain strategies may not enforce TVL limits by inheritance. + */ + function _beforeDepositAssetIntoStrategy(uint256 amount) internal view { + (uint256 maxPerDeposit, uint256 maxTotalDeposits) = _strategy.getTVLLimits(); + + require(amount <= maxPerDeposit, ExceedsMaxPerDeposit(maxPerDeposit, amount)); + + uint256 currentBalance = _asset.balanceOf(address(_strategy)); + require(currentBalance + amount <= maxTotalDeposits, ExceedsMaxTotalDeposited(maxTotalDeposits, currentBalance)); + } + + /** + * @notice Delegates funds to an operator or deposits into strategy + * @dev If operator is zero address and amount > 0, deposits into strategy + * @param operator Address of the operator to delegate to + * @param amount Amount of tokens to delegate/deposit + * @param _data Additional data required for delegation [approverSalt, approverSignatureAndExpiry] + * @return Returns 0 for delegation or deposit amount for strategy deposits + */ + function delegate( + address operator, + uint256 amount, + bytes[] calldata _data + ) external override onlyTrustee whenNotPaused returns (uint256) { + // depositIntoStrategy + if (amount > 0 && operator == address(0)) { + _beforeDepositAssetIntoStrategy(amount); + + // transfer from the vault + _asset.safeTransferFrom(msg.sender, address(this), amount); + // deposit the asset to the appropriate strategy + return _strategy.sharesToUnderlying( + _strategyManager.depositIntoStrategy(_strategy, _asset, amount) + ); + } + + require(operator != address(0), NullParams()); + require(_data.length == 2, InvalidDataLength(2, _data.length)); + + // prepare delegation + bytes32 approverSalt = abi.decode(_data[0], (bytes32)); + IDelegationManager.SignatureWithExpiry + memory approverSignatureAndExpiry = abi.decode(_data[1], (IDelegationManager.SignatureWithExpiry)); + + // delegate to EL + _delegationManager.delegateTo( + operator, + approverSignatureAndExpiry, + approverSalt + ); + + return 0; + } + + /** + * @notice Undelegates the contract from the current operator. + * @dev Can only be called by the trustee when the contract is not paused. + * Emits an `Undelegated` event upon successful undelegation. + */ + function undelegate() external onlyTrustee whenNotPaused { + address undelegatedFrom = getOperatorAddress(); + bytes32[] memory withdrawalRoots = _delegationManager.undelegate(address(this)); + + emit WithdrawalsQueued(withdrawalRoots); + emit Undelegated(undelegatedFrom); + } + + /** + * @notice Redelegates the contract to a new operator. + * @dev Can only be called by the trustee when the contract is not paused. + * Emits a `RedelegatedTo` event upon successful redelegation. + * @param newOperator The address of the new operator to delegate to. + * @param newOperatorApproverSig The signature and expiry details for the new operator's approval. + * @param approverSalt A unique salt used for the approval process to prevent replay attacks. + */ + function redelegate( + address newOperator, + IDelegationManager.SignatureWithExpiry memory newOperatorApproverSig, + bytes32 approverSalt + ) external onlyTrustee whenNotPaused { + require(newOperator != address(0), ZeroAddress()); + + address undelegatedFrom = getOperatorAddress(); + bytes32[] memory withdrawalRoots = _delegationManager.redelegate( + newOperator, newOperatorApproverSig, approverSalt + ); + + emit WithdrawalsQueued(withdrawalRoots); + emit RedelegatedTo(undelegatedFrom, newOperator); + } + + /** + * @notice Initiates withdrawal process for funds + * @dev Creates a queued withdrawal request in the delegation manager + * @param amount Amount of tokens to withdraw + * @param _data Additional data (must be empty) + * @param emergency Flag for emergency withdrawal + * @return Tuple of requested amount and 0 + */ + function withdraw( + address /*operator*/, + uint256 amount, + bytes[] calldata _data, + bool emergency + ) external override onlyTrustee whenNotPaused returns (uint256, uint256) { + require(_data.length == 0, InvalidDataLength(0, _data.length)); + + uint256[] memory sharesToWithdraw = new uint256[](1); + IStrategy[] memory strategies = new IStrategy[](1); + + strategies[0] = _strategy; + sharesToWithdraw[0] = _strategy.underlyingToShares(amount); + + address staker = address(this); + uint256 nonce = _delegationManager.cumulativeWithdrawalsQueued(staker); + if (emergency) _emergencyQueuedWithdrawals[nonce] = true; + + // prepare withdrawal + IDelegationManager.QueuedWithdrawalParams[] + memory withdrawals = new IDelegationManager.QueuedWithdrawalParams[](1); + withdrawals[0] = IDelegationManager.QueuedWithdrawalParams({ + strategies: strategies, + shares: sharesToWithdraw, + withdrawer: staker + }); + + // queue from EL + bytes32[] memory withdrawalRoots = _delegationManager.queueWithdrawals(withdrawals); + + emit WithdrawalsQueued(withdrawalRoots); + emit StartWithdrawal( + staker, + _strategy, + sharesToWithdraw[0], + uint32(block.number), + _delegationManager.delegatedTo(staker), + nonce + ); + + return (amount, 0); + } + + /** + * @notice Completes the withdrawal process and claims tokens + * @dev Processes the queued withdrawal and transfers tokens to inception vault + * @param _data Array containing withdrawal data [withdrawal, tokens, receiveAsTokens] + * @param emergency Flag for emergency withdrawal + * @return Amount of tokens withdrawn + */ + function claim( + bytes[] calldata _data, bool emergency + ) external override onlyTrustee whenNotPaused returns (uint256) { + require(_data.length == 3, InvalidDataLength(3, _data.length)); + + uint256 balanceBefore = _asset.balanceOf(address(this)); + + // prepare withdrawal + IDelegationManager.Withdrawal memory withdrawal = abi.decode(_data[0], (IDelegationManager.Withdrawal)); + IERC20[] memory tokens = abi.decode(_data[1], (IERC20[][]))[0]; + bool receiveAsTokens = abi.decode(_data[2], (bool[]))[0]; + + // emergency claim available only for emergency queued withdrawals + require( + (emergency && _emergencyQueuedWithdrawals[withdrawal.nonce]) || + (!emergency && !_emergencyQueuedWithdrawals[withdrawal.nonce]), + OnlyEmergency() + ); + + // claim from EL + _delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens); + + // send tokens to the vault + uint256 withdrawnAmount; + if (receiveAsTokens) { + withdrawnAmount = _asset.balanceOf(address(this)) - balanceBefore; + // send tokens to the vault + _asset.safeTransfer(_inceptionVault, withdrawnAmount); + } + + // update emergency withdrawal state + _emergencyQueuedWithdrawals[withdrawal.nonce] = false; + + return withdrawnAmount; + } + + /** + * @notice Returns the total amount pending withdrawal + * @return Total amount of non-emergency pending withdrawals + */ + function pendingWithdrawalAmount() public view override returns (uint256) + { + return _pendingWithdrawalAmount(false); + } + + /** + * @notice Returns the total amount pending emergency withdrawal + * @return Total amount of emergency pending withdrawals + */ + function pendingEmergencyWithdrawalAmount() public view override returns (uint256) + { + return _pendingWithdrawalAmount(true); + } + + /** + * @notice Internal function to calculate pending withdrawal amount + * @dev Filters withdrawals based on emergency status + * @param emergency Flag to filter emergency withdrawals + * @return total Total amount of pending withdrawals matching emergency status + */ + function _pendingWithdrawalAmount(bool emergency) internal view returns (uint256 total) { + (IDelegationManager.Withdrawal[] memory withdrawals, + uint256[][] memory shares) = _delegationManager.getQueuedWithdrawals(address(this)); + + for (uint256 i = 0; i < withdrawals.length; i++) { + if (emergency != _emergencyQueuedWithdrawals[withdrawals[i].nonce]) { + continue; + } + + total += shares[i][0]; + } + + return _strategy.sharesToUnderlyingView(total); + } + + /** + * @notice Returns the current operator address for this adapter + * @return Address of the operator this adapter is delegated to + */ + function getOperatorAddress() public view returns (address) { + return _delegationManager.delegatedTo(address(this)); + } + + /** + * @notice Returns the amount deposited for a specific operator + * @return Amount of underlying tokens deposited + */ + function getDeposited( + address /*operatorAddress*/ + ) external view override returns (uint256) { + return _strategy.userUnderlyingView(address(this)); + } + + /** + * @notice Returns the total amount deposited in the strategy + * @return Total amount of underlying tokens deposited + */ + function getTotalDeposited() public view override returns (uint256) { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = _strategy; + + (uint256[] memory withdrawableShares,) = _delegationManager.getWithdrawableShares( + address(this), strategies + ); + + return _strategy.sharesToUnderlyingView(withdrawableShares[0]); + } + + /** + * @notice Returns the total amount tokens related to adapter + * @return total is the total amount tokens related to adapter + */ + function getTotalBalance() external view returns(uint256) { + return inactiveBalance() + getTotalDeposited(); + } + + /** + * @notice Returns the total inactive balance + * @return Sum of pending withdrawals, pending emergency withdrawals, claimable amounts + */ + function inactiveBalance() public view override returns (uint256) { + return pendingWithdrawalAmount() + pendingEmergencyWithdrawalAmount() + claimableAmount(); + } + + /** + * @notice Returns the amount of strategy shares held + * @return Amount of strategy shares + */ + function getDepositedShares() external view returns (uint256) { + return _strategy.underlyingToSharesView(_strategy.userUnderlyingView(address(this))); + } + + /** + * @notice Returns the contract version + * @return Current version number (3) + */ + function getVersion() external pure override returns (uint256) { + return 3; + } + + /******************************************************************************* + Rewards + *******************************************************************************/ + + /** + * @notice Updates the rewards coordinator address + * @dev Can only be called by the owner + * @param newRewardsCoordinator Address of the new rewards coordinator + * @param claimer Address of the owner to set as claimer + */ + function setRewardsCoordinator( + address newRewardsCoordinator, + address claimer + ) external onlyOwner { + _setRewardsCoordinator(newRewardsCoordinator, claimer); + } + + /** + * @notice Internal function to set the rewards coordinator + * @dev Updates the rewards coordinator and sets the claimer + * @param newRewardsCoordinator Address of the new rewards coordinator + * @param claimer Address of the owner to set as claimer + */ + function _setRewardsCoordinator(address newRewardsCoordinator, address claimer) internal { + IRewardsCoordinator(newRewardsCoordinator).setClaimerFor(claimer); + + emit RewardCoordinatorChanged( + address(rewardsCoordinator), + newRewardsCoordinator + ); + + rewardsCoordinator = IRewardsCoordinator(newRewardsCoordinator); + } + + /** + * @notice Claim rewards from Eigenlayer protocol. + * @dev Can only be called by trustee + * @param rewardsData Adapter related bytes of data for rewards. + */ + function claimRewards(address /* rewardToken */, bytes memory rewardsData) external onlyTrustee { + IRewardsCoordinator.RewardsMerkleClaim memory data = abi.decode(rewardsData, (IRewardsCoordinator.RewardsMerkleClaim)); + IRewardsCoordinator(rewardsCoordinator).processClaim(data, _inceptionVault); + } +} diff --git a/projects/vaults/contracts/adapters/InceptionEigenAdapterWrap.sol b/projects/vaults/contracts/adapters/InceptionEigenAdapterWrap.sol new file mode 100644 index 00000000..4fce3943 --- /dev/null +++ b/projects/vaults/contracts/adapters/InceptionEigenAdapterWrap.sol @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IWStethInterface} from "../interfaces/common/IStEth.sol"; +import {IInceptionEigenLayerAdapter} from "../interfaces/adapters/IInceptionEigenLayerAdapter.sol"; +import {IDelegationManager} from "../interfaces/eigenlayer-vault/eigen-core/IDelegationManager.sol"; +import {IStrategy} from "../interfaces/eigenlayer-vault/eigen-core/IStrategy.sol"; +import {IStrategyManager} from "../interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol"; +import {IRewardsCoordinator} from "../interfaces/eigenlayer-vault/eigen-core/IRewardsCoordinator.sol"; + +import {InceptionBaseAdapter, IInceptionBaseAdapter} from "./InceptionBaseAdapter.sol"; +import {IEmergencyClaimer} from "../interfaces/common/IEmergencyClaimer.sol"; + +/** + * @title The InceptionEigenAdapterWrap Contract + * @author The InceptionLRT team + * @dev Handles delegation and withdrawal requests within the EigenLayer protocol. + * @notice Can only be executed by InceptionVault/InceptionOperator or the owner. + */ +contract InceptionEigenAdapterWrap is InceptionBaseAdapter, IInceptionEigenLayerAdapter { + using SafeERC20 for IERC20; + + IStrategy internal _strategy; + IStrategyManager internal _strategyManager; + IDelegationManager internal _delegationManager; + IRewardsCoordinator public rewardsCoordinator; + mapping(uint256 => bool) internal _emergencyQueuedWithdrawals; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() payable { + _disableInitializers(); + } + + /** + * @notice Initializes the adapter contract with required addresses and parameters + * @param claimer Address of the contract owner + * @param rewardCoordinator Address of the rewards coordinator contract + * @param delegationManager Address of the delegation manager contract + * @param strategyManager Address of the strategy manager contract + * @param strategy Address of the strategy contract + * @param asset Address of the underlying asset token + * @param trusteeManager Address of the trustee manager + * @param inceptionVault Address of the inception vault + */ + function initialize( + address claimer, + address rewardCoordinator, + address delegationManager, + address strategyManager, + address strategy, + address asset, + address trusteeManager, + address inceptionVault + ) public initializer { + __InceptionBaseAdapter_init(IERC20(asset), trusteeManager); + + _delegationManager = IDelegationManager(delegationManager); + _strategyManager = IStrategyManager(strategyManager); + _strategy = IStrategy(strategy); + _inceptionVault = inceptionVault; + _setRewardsCoordinator(rewardCoordinator, claimer); + + // approve spending by strategyManager + _asset.safeApprove(strategyManager, type(uint256).max); + wrappedAsset().stETH().approve(strategyManager, type(uint256).max); + } + + /** + * @dev checks whether it's still possible to deposit into the strategy + * @param amount Amount of tokens to delegate/deposit + * @notice Be cautious when using this function, as certain strategies may not enforce TVL limits by inheritance. + */ + function _beforeDepositAssetIntoStrategy(uint256 amount) internal view { + (uint256 maxPerDeposit, uint256 maxTotalDeposits) = _strategy.getTVLLimits(); + + require(amount <= maxPerDeposit, ExceedsMaxPerDeposit(maxPerDeposit, amount)); + + uint256 currentBalance = _asset.balanceOf(address(_strategy)); + require(currentBalance + amount <= maxTotalDeposits, ExceedsMaxTotalDeposited(maxTotalDeposits, currentBalance)); + } + + /** + * @notice Delegates funds to an operator or deposits into strategy + * @dev If operator is zero address and amount > 0, deposits into strategy + * @param operator Address of the operator to delegate to + * @param amount Amount of tokens to delegate/deposit + * @param _data Additional data required for delegation [approverSalt, approverSignatureAndExpiry] + * @return Returns 0 for delegation or deposit amount for strategy deposits + */ + function delegate( + address operator, + uint256 amount, + bytes[] calldata _data + ) external override onlyTrustee whenNotPaused returns (uint256) { + // depositIntoStrategy + if (amount > 0 && operator == address(0)) { + _beforeDepositAssetIntoStrategy(amount); + + // transfer from the vault + _asset.safeTransferFrom(msg.sender, address(this), amount); + amount = wrappedAsset().unwrap(amount); + // deposit the asset to the appropriate strategy + return wrappedAsset().getWstETHByStETH(_strategy.sharesToUnderlying( + _strategyManager.depositIntoStrategy( + _strategy, wrappedAsset().stETH(), amount + ) + )); + } + + require(operator != address(0), NullParams()); + require(_data.length == 2, InvalidDataLength(2, _data.length)); + + // prepare delegation + bytes32 approverSalt = abi.decode(_data[0], (bytes32)); + IDelegationManager.SignatureWithExpiry + memory approverSignatureAndExpiry = abi.decode(_data[1], (IDelegationManager.SignatureWithExpiry)); + + // delegate to EL + _delegationManager.delegateTo( + operator, + approverSignatureAndExpiry, + approverSalt + ); + + return 0; + } + + /** + * @notice Undelegates the contract from the current operator. + * @dev Can only be called by the trustee when the contract is not paused. + * Emits an `Undelegated` event upon successful undelegation. + */ + function undelegate() external onlyTrustee whenNotPaused { + address undelegatedFrom = getOperatorAddress(); + bytes32[] memory withdrawalRoots = _delegationManager.undelegate(address(this)); + + emit WithdrawalsQueued(withdrawalRoots); + emit Undelegated(undelegatedFrom); + } + + /** + * @notice Redelegates the contract to a new operator. + * @dev Can only be called by the trustee when the contract is not paused. + * Emits a `RedelegatedTo` event upon successful redelegation. + * @param newOperator The address of the new operator to delegate to. + * @param newOperatorApproverSig The signature and expiry details for the new operator's approval. + * @param approverSalt A unique salt used for the approval process to prevent replay attacks. + */ + function redelegate( + address newOperator, + IDelegationManager.SignatureWithExpiry memory newOperatorApproverSig, + bytes32 approverSalt + ) external onlyTrustee whenNotPaused { + require(newOperator != address(0), ZeroAddress()); + + address undelegatedFrom = getOperatorAddress(); + bytes32[] memory withdrawalRoots = _delegationManager.redelegate( + newOperator, newOperatorApproverSig, approverSalt + ); + + emit WithdrawalsQueued(withdrawalRoots); + emit RedelegatedTo(undelegatedFrom, newOperator); + } + + /** + * @notice Initiates withdrawal process for funds + * @dev Creates a queued withdrawal request in the delegation manager + * @param amount Amount of tokens to withdraw + * @param _data Additional data (must be empty) + * @param emergency Flag for emergency withdrawal + * @return Tuple of requested amount and 0 + */ + function withdraw( + address /*operator*/, + uint256 amount, + bytes[] calldata _data, + bool emergency + ) external override onlyTrustee whenNotPaused returns (uint256, uint256) { + require(_data.length == 0, InvalidDataLength(0, _data.length)); + + uint256[] memory sharesToWithdraw = new uint256[](1); + IStrategy[] memory strategies = new IStrategy[](1); + + strategies[0] = _strategy; + sharesToWithdraw[0] = _strategy.underlyingToShares( + wrappedAsset().getStETHByWstETH(amount) + ); + + address staker = address(this); + uint256 nonce = _delegationManager.cumulativeWithdrawalsQueued(staker); + if (emergency) _emergencyQueuedWithdrawals[nonce] = true; + + // prepare withdrawal + IDelegationManager.QueuedWithdrawalParams[] + memory withdrawals = new IDelegationManager.QueuedWithdrawalParams[](1); + withdrawals[0] = IDelegationManager.QueuedWithdrawalParams({ + strategies: strategies, + shares: sharesToWithdraw, + withdrawer: staker + }); + + // queue withdrawal from EL + bytes32[] memory withdrawalRoots = _delegationManager.queueWithdrawals(withdrawals); + + emit WithdrawalsQueued(withdrawalRoots); + emit StartWithdrawal( + staker, + _strategy, + sharesToWithdraw[0], + uint32(block.number), + _delegationManager.delegatedTo(staker), + nonce + ); + + return (wrappedAsset().getWstETHByStETH( + _strategy.sharesToUnderlyingView(sharesToWithdraw[0]) + ), 0); + } + + /** + * @notice Completes the withdrawal process and claims tokens + * @dev Processes the queued withdrawal and transfers tokens to inception vault + * @param _data Array containing withdrawal data [withdrawal, tokens, receiveAsTokens] + * @param emergency Flag for emergency withdrawal + * @return Amount of tokens withdrawn + */ + function claim( + bytes[] calldata _data, bool emergency + ) external override onlyTrustee whenNotPaused returns (uint256) { + require(_data.length == 3, InvalidDataLength(3, _data.length)); + + IERC20 backedAsset = wrappedAsset().stETH(); + uint256 balanceBefore = backedAsset.balanceOf(address(this)); + + // prepare withdrawal + IDelegationManager.Withdrawal memory withdrawal = abi.decode(_data[0], (IDelegationManager.Withdrawal)); + IERC20[] memory tokens = abi.decode(_data[1], (IERC20[][]))[0]; + bool receiveAsTokens = abi.decode(_data[2], (bool[]))[0]; + + // emergency claim available only for emergency queued withdrawals + require( + (emergency && _emergencyQueuedWithdrawals[withdrawal.nonce]) || + (!emergency && !_emergencyQueuedWithdrawals[withdrawal.nonce]), + OnlyEmergency() + ); + + // claim from EL + _delegationManager.completeQueuedWithdrawal(withdrawal, tokens, receiveAsTokens); + + // send tokens to the vault + uint256 withdrawnAmount; + if (receiveAsTokens) { + withdrawnAmount = backedAsset.balanceOf(address(this)) - balanceBefore; + backedAsset.safeApprove(address(_asset), withdrawnAmount); + uint256 wrapped = wrappedAsset().wrap(withdrawnAmount); + _asset.safeTransfer(_inceptionVault, wrapped); + } + + // update emergency withdrawal state + _emergencyQueuedWithdrawals[withdrawal.nonce] = false; + + return wrappedAsset().getWstETHByStETH(withdrawnAmount); + } + + /** + * @notice Returns the total amount pending withdrawal + * @return Total amount of non-emergency pending withdrawals + */ + function pendingWithdrawalAmount() public view override returns (uint256) + { + return _pendingWithdrawalAmount(false); + } + + /** + * @notice Returns the total amount pending emergency withdrawal + * @return Total amount of emergency pending withdrawals + */ + function pendingEmergencyWithdrawalAmount() public view override returns (uint256) + { + return _pendingWithdrawalAmount(true); + } + + /** + * @notice Internal function to calculate pending withdrawal amount + * @dev Filters withdrawals based on emergency status + * @param emergency Flag to filter emergency withdrawals + * @return total Total amount of pending withdrawals matching emergency status + */ + function _pendingWithdrawalAmount(bool emergency) internal view returns (uint256 total) { + (IDelegationManager.Withdrawal[] memory withdrawals, + uint256[][] memory shares) = _delegationManager.getQueuedWithdrawals(address(this)); + + for (uint256 i = 0; i < withdrawals.length; i++) { + if (emergency != _emergencyQueuedWithdrawals[withdrawals[i].nonce]) { + continue; + } + + total += shares[i][0]; + } + + return wrappedAsset().getWstETHByStETH( + _strategy.sharesToUnderlyingView(total) + ); + } + + /** + * @notice Returns the total amount tokens related to adapter + * @return total is the total amount tokens related to adapter + */ + function getTotalBalance() external view returns(uint256) { + return inactiveBalance() + getTotalDeposited(); + } + + /** + * @notice Returns the total inactive balance + * @return Sum of pending withdrawals, pending emergency withdrawals, claimable amounts + */ + function inactiveBalance() public view override returns (uint256) { + return pendingWithdrawalAmount() + pendingEmergencyWithdrawalAmount() + claimableAmount(); + } + + /** + * @notice Returns the current operator address for this adapter + * @return Address of the operator this adapter is delegated to + */ + function getOperatorAddress() public view returns (address) { + return _delegationManager.delegatedTo(address(this)); + } + + /** + * @notice Returns the amount deposited for a specific operator + * @return Amount of underlying tokens deposited + */ + function getDeposited( + address /*operatorAddress*/ + ) external view override returns (uint256) { + return wrappedAsset().getWstETHByStETH( + _strategy.userUnderlyingView(address(this)) + ); + } + + /** + * @notice Returns the total amount deposited in the strategy + * @return Total amount of underlying tokens deposited + */ + function getTotalDeposited() public view override returns (uint256) { + IStrategy[] memory strategies = new IStrategy[](1); + strategies[0] = _strategy; + + (uint256[] memory withdrawableShares,) = _delegationManager.getWithdrawableShares( + address(this), strategies + ); + + return wrappedAsset().getWstETHByStETH( + _strategy.sharesToUnderlyingView(withdrawableShares[0]) + ); + } + + /** + * @notice Returns the amount of strategy shares held + * @return Amount of strategy shares + */ + function getDepositedShares() external view returns (uint256) { + return _strategy.underlyingToSharesView(_strategy.userUnderlyingView(address(this))); + } + + /** + * @notice Returns the wrapped asset + * @return Wrapped asset + */ + function wrappedAsset() internal view returns (IWStethInterface) { + return IWStethInterface(address(_asset)); + } + + /** + * @notice Returns the contract version + * @return Current version number (3) + */ + function getVersion() external pure override returns (uint256) { + return 3; + } + + /******************************************************************************* + Rewards + *******************************************************************************/ + + /** + * @notice Updates the rewards coordinator address + * @dev Can only be called by the owner + * @param newRewardsCoordinator Address of the new rewards coordinator + * @param claimer Address of the owner to set as claimer + */ + function setRewardsCoordinator( + address newRewardsCoordinator, + address claimer + ) external onlyOwner { + _setRewardsCoordinator(newRewardsCoordinator, claimer); + } + + /** + * @notice Internal function to set the rewards coordinator + * @dev Updates the rewards coordinator and sets the claimer + * @param newRewardsCoordinator Address of the new rewards coordinator + * @param claimer Address of the owner to set as claimer + */ + function _setRewardsCoordinator(address newRewardsCoordinator, address claimer) internal { + IRewardsCoordinator(newRewardsCoordinator).setClaimerFor(claimer); + + emit RewardCoordinatorChanged( + address(rewardsCoordinator), + newRewardsCoordinator + ); + + rewardsCoordinator = IRewardsCoordinator(newRewardsCoordinator); + } + + /** + * @notice Claim rewards from Eigenlayer protocol. + * @dev Can only be called by trustee + * @param rewardsData Adapter related bytes of data for rewards. + */ + function claimRewards(address /* rewardToken */, bytes memory rewardsData) external onlyTrustee { + IRewardsCoordinator.RewardsMerkleClaim memory data = abi.decode(rewardsData, (IRewardsCoordinator.RewardsMerkleClaim)); + IRewardsCoordinator(rewardsCoordinator).processClaim(data, _inceptionVault); + } +} diff --git a/projects/vaults/contracts/adapters/InceptionSymbioticAdapter.sol b/projects/vaults/contracts/adapters/InceptionSymbioticAdapter.sol new file mode 100644 index 00000000..52a21835 --- /dev/null +++ b/projects/vaults/contracts/adapters/InceptionSymbioticAdapter.sol @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {BeaconProxy, Address} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IInceptionSymbioticAdapter} from "../interfaces/adapters/IInceptionSymbioticAdapter.sol"; +import {IVault} from "../interfaces/symbiotic-vault/symbiotic-core/IVault.sol"; +import {IStakerRewards} from "../interfaces/symbiotic-vault/symbiotic-core/IStakerRewards.sol"; + +import {InceptionBaseAdapter, IInceptionBaseAdapter} from "./InceptionBaseAdapter.sol"; +import {SymbioticAdapterClaimer} from "../adapter-claimers/SymbioticAdapterClaimer.sol"; + +/** + * @title InceptionSymbioticAdapter + * @author The InceptionLRT team + * @dev Handles delegation and withdrawal requests within the SymbioticFi Protocol. + * @notice Can only be executed by InceptionVault/InceptionOperator or the owner. + */ +contract InceptionSymbioticAdapter is + IInceptionSymbioticAdapter, + InceptionBaseAdapter +{ + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @notice Set of supported Symbiotic vaults + EnumerableSet.AddressSet internal _symbioticVaults; + + /// @notice Mapping of vault addresses to their withdrawal epochs + mapping(address => mapping(address => uint256)) public withdrawals; + mapping(address => address) internal _claimerVaults; + + address internal _emergencyClaimer; + EnumerableSet.AddressSet internal _pendingClaimers; + address[] internal _availableClaimers; + + address internal _claimerImplementation; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() payable { + _disableInitializers(); + } + + /** + * @notice Initializes the Symbiotic adapter with vaults and parameters + * @param vaults Array of vault addresses + * @param asset Address of the underlying asset + * @param trusteeManager Address of the trustee manager + */ + function initialize( + address[] memory vaults, + IERC20 asset, + address trusteeManager + ) public initializer { + __InceptionBaseAdapter_init(asset, trusteeManager); + + for (uint256 i = 0; i < vaults.length; i++) { + require( + IVault(vaults[i]).collateral() == address(asset), + InvalidCollateral() + ); + require(_symbioticVaults.add(vaults[i]), AlreadyAdded()); + emit VaultAdded(vaults[i]); + } + } + + /** + * @notice Delegates funds to a Symbiotic vault + * @dev Can only be called by trustee when contract is not paused + * @param vaultAddress Address of the target Symbiotic vault + * @param amount Amount of tokens to delegate + */ + function delegate( + address vaultAddress, + uint256 amount, + bytes[] calldata /* _data */ + ) + external + override + onlyTrustee + whenNotPaused + returns (uint256 depositedAmount) + { + require(_symbioticVaults.contains(vaultAddress), InvalidVault()); + _asset.safeTransferFrom(msg.sender, address(this), amount); + IERC20(_asset).safeIncreaseAllowance(vaultAddress, amount); + + uint256 mintedShares; + (depositedAmount, mintedShares) = IVault(vaultAddress).deposit( + address(this), + amount + ); + + emit MintedShares(mintedShares); + return depositedAmount; + } + + /** + * @notice Initiates withdrawal from a Symbiotic vault + * @dev Can only be called by trustee when contract is not paused + * @param vaultAddress Address of the vault to withdraw from + * @param amount Amount to withdraw + * @param emergency Flag for emergency withdrawal + * @return Tuple of (amount requested, 0) + */ + function withdraw( + address vaultAddress, + uint256 amount, + bytes[] calldata /* _data */, + bool emergency + ) external onlyTrustee whenNotPaused returns (uint256, uint256) { + IVault vault = IVault(vaultAddress); + require(_symbioticVaults.contains(vaultAddress), InvalidVault()); + + address claimer = _getOrCreateClaimer(emergency); + require( + withdrawals[vaultAddress][claimer] == 0, + WithdrawalInProgress() + ); + + (uint256 burnedShares, uint256 mintedShares) = vault.withdraw( + claimer, + amount + ); + withdrawals[vaultAddress][claimer] = vault.currentEpoch() + 1; + _claimerVaults[claimer] = vaultAddress; + + emit SymbioticWithdrawn(burnedShares, mintedShares, claimer); + + return (amount, 0); + } + + /** + * @notice Claims withdrawn funds from a Symbiotic vault + * @dev Can only be called by trustee when contract is not paused + * @param _data Array containing vault address and epoch number + * @param emergency Flag for emergency claim process + * @return Amount of tokens claimed + */ + function claim( + bytes[] calldata _data, + bool emergency + ) external override onlyTrustee whenNotPaused returns (uint256) { + require(_data.length == 1, InvalidDataLength(1, _data.length)); + (address vaultAddress, address claimer) = abi.decode(_data[0], + (address, address) + ); + + require(_symbioticVaults.contains(vaultAddress), InvalidVault()); + require((emergency && claimer == _emergencyClaimer) || (!emergency && claimer != _emergencyClaimer), OnlyEmergency()); + require(withdrawals[vaultAddress][claimer] != 0, NothingToClaim()); + + uint256 epoch = withdrawals[vaultAddress][claimer]; + delete withdrawals[vaultAddress][claimer]; + if (!emergency) { + _removePendingClaimer(claimer); + } + + return + SymbioticAdapterClaimer(claimer).claim( + vaultAddress, + _inceptionVault, + epoch + ); + } + + /** + * @notice Claim rewards from Symbiotic protocol. + * @dev Can only be called by trustee + * @param rewardToken Reward token. + * @param rewardsData Adapter related bytes of data for rewards. + */ + function claimRewards( + address rewardToken, + bytes memory rewardsData + ) external onlyTrustee { + (address symbioticFarm, bytes memory farmData) = abi.decode( + rewardsData, + (address, bytes) + ); + IStakerRewards(symbioticFarm).claimRewards( + _inceptionVault, + rewardToken, + farmData + ); + } + + /** + * @notice Checks if a vault is supported by the adapter + * @param vaultAddress Address of the vault to check + * @return bool indicating if vault is supported + */ + function isVaultSupported( + address vaultAddress + ) external view returns (bool) { + return _symbioticVaults.contains(vaultAddress); + } + + /** + * @notice Returns the amount deposited in a specific vault + * @param vaultAddress Address of the vault to check + * @return Amount of active balance in the vault + */ + function getDeposited( + address vaultAddress + ) public view override returns (uint256) { + return IVault(vaultAddress).activeBalanceOf(address(this)); + } + + /** + * @notice Returns the total amount tokens related to adapter + * @return total is the total amount tokens related to adapter + */ + function getTotalBalance() external view returns(uint256) { + return inactiveBalance() + getTotalDeposited(); + } + + /** + * @notice Returns the total inactive balance + * @return Sum of pending withdrawals, pending emergency withdrawals, claimable amounts + */ + function inactiveBalance() public view override returns (uint256) { + return pendingWithdrawalAmount() + pendingEmergencyWithdrawalAmount() + claimableAmount(); + } + + /** + * @notice Returns the total amount deposited across all vaults + * @return total Sum of active balances in all vaults + */ + function getTotalDeposited() public view override returns (uint256 total) { + for (uint256 i = 0; i < _symbioticVaults.length(); i++) + total += IVault(_symbioticVaults.at(i)).activeBalanceOf( + address(this) + ); + + return total; + } + + /** + * @notice Returns the total amount pending withdrawal + * @return Amount of pending withdrawals for non-emergency claims + */ + function pendingWithdrawalAmount() public view override returns (uint256) { + return _pendingWithdrawalAmount(false); + } + + /** + * @notice Returns the total amount pending emergency withdrawal + * @return Amount of pending withdrawals for emergency claims + */ + function pendingEmergencyWithdrawalAmount() public view override returns (uint256) { + return _pendingWithdrawalAmount(true); + } + + /** + * @notice Internal function to calculate pending withdrawal amount + * @param emergency Emergency flag for claimer + * @return total Total pending withdrawal amount + */ + function _pendingWithdrawalAmount( + bool emergency + ) internal view returns (uint256 total) { + if (emergency) { + for (uint256 i = 0; i < _symbioticVaults.length(); i++) { + if ( + withdrawals[_symbioticVaults.at(i)][_emergencyClaimer] != 0 + ) { + total += IVault(_symbioticVaults.at(i)).withdrawalsOf( + withdrawals[_symbioticVaults.at(i)][_emergencyClaimer], + _emergencyClaimer + ); + } + } + + return total; + } + + for (uint256 i = 0; i < _pendingClaimers.length(); i++) { + address _claimer = _pendingClaimers.at(i); + address _vault = _claimerVaults[_claimer]; + total += IVault(_vault).withdrawalsOf( + withdrawals[_vault][_claimer], + _claimer + ); + } + + return total; + } + + /** + * @notice Internal function to calculate pending withdrawal amount for an address + * @param vault Vault address + * @param emergency Emergency flag for claimer + * @return total Total pending withdrawal amount + */ + function _pendingWithdrawalAmount( + address vault, + bool emergency + ) internal view returns (uint256 total) { + if (emergency) + return + IVault(vault).withdrawalsOf( + withdrawals[vault][_emergencyClaimer], + _emergencyClaimer + ); + + for (uint256 i = 0; i < _pendingClaimers.length(); i++) { + address _claimer = _pendingClaimers.at(i); + if (_claimerVaults[_claimer] == vault) + total += IVault(vault).withdrawalsOf( + withdrawals[vault][_claimer], + _claimer + ); + } + + return total; + } + + /** + * @notice Adds a new vault to the adapter + * @param vaultAddress Address of the new vault + */ + function addVault(address vaultAddress) external onlyOwner { + require(vaultAddress != address(0), ZeroAddress()); + require(Address.isContract(vaultAddress), NotContract()); + require(!_symbioticVaults.contains(vaultAddress), AlreadyAdded()); + require( + IVault(vaultAddress).collateral() == address(_asset), + InvalidCollateral() + ); + + _symbioticVaults.add(vaultAddress); + + emit VaultAdded(vaultAddress); + } + + /** + * @notice Removes a vault from the adapter + * @param vaultAddress Address of the vault to remove + */ + function removeVault(address vaultAddress) external onlyOwner { + require(vaultAddress != address(0), ZeroAddress()); + require(Address.isContract(vaultAddress), NotContract()); + require(_symbioticVaults.contains(vaultAddress), NotAdded()); + + if ( + getDeposited(vaultAddress) != 0 || + _pendingWithdrawalAmount(vaultAddress, false) > 0 || + _pendingWithdrawalAmount(vaultAddress, true) > 0 + ) revert VaultNotEmpty(); + + _symbioticVaults.remove(vaultAddress); + + emit VaultRemoved(vaultAddress); + } + + /** + * @notice Sets the implementation address for the claimer + * @param newImplementation The address of the new implementation + */ + function setClaimerImplementation( + address newImplementation + ) external onlyOwner { + emit EmergencyClaimerSet(_claimerImplementation, newImplementation); + _claimerImplementation = newImplementation; + } + + /** + * @notice Returns all supported vault addresses + * @return vaults Array of supported vault addresses + */ + function getAllVaults() external view returns (address[] memory vaults) { + vaults = new address[](_symbioticVaults.length()); + for (uint256 i = 0; i < _symbioticVaults.length(); i++) + vaults[i] = _symbioticVaults.at(i); + } + + /** + * @notice Retrieves or creates a claimer address based on the emergency condition + * @dev If `emergency` is true, returns the existing emergency claimer or deploys a new one if it doesn't exist. + * If `emergency` is false, reuses an available claimer from the `availableClaimers` array or deploys a new one. + * The returned claimer is added to the `pendingClaimers` set + * @param emergency Boolean indicating whether an emergency claimer is required + * @return claimer The address of the claimer to be used + */ + function _getOrCreateClaimer( + bool emergency + ) internal virtual returns (address claimer) { + if (emergency) + return + _emergencyClaimer != address(0) + ? _emergencyClaimer + : (_emergencyClaimer = _deployClaimer()); + + if (_availableClaimers.length > 0) { + claimer = _availableClaimers[_availableClaimers.length - 1]; + _availableClaimers.pop(); + } else { + claimer = _deployClaimer(); + } + + _pendingClaimers.add(claimer); + return claimer; + } + + /** + * @notice Removes a claimer from the pending list and recycles it to the available claimers + * @dev Deletes the claimer's vault mapping, removes it from `pendingClaimers`, and adds it to `availableClaimers` + * @param claimer The address of the claimer to be removed from pending status + */ + function _removePendingClaimer(address claimer) internal { + delete _claimerVaults[claimer]; + _pendingClaimers.remove(claimer); + _availableClaimers.push(claimer); + } + + /** + * @notice Deploys a new SymbioticAdapterClaimer contract instance + * @dev Creates a new claimer contract with the `_asset` address passed as a initialize parameter + * @dev ownership is transferred to the adapter owner + * @return The address of the newly deployed SymbioticAdapterClaimer contract + */ + function _deployClaimer() internal returns (address) { + if (_claimerImplementation == address(0)) + revert ClaimerImplementationNotSet(); + // deploy new beacon proxy and do init call + bytes memory data = abi.encodeWithSignature( + "initialize(address)", + address(_asset) + ); + address claimer = address(new BeaconProxy(address(this), data)); + + (bool success,) = claimer.call( + abi.encodeWithSignature("transferOwnership(address)", owner()) + ); + require(success, TransferOwnershipFailed()); + + emit ClaimerDeployed(claimer); + return claimer; + } + + /** + * @notice Beacon proxy implementation address + * @return The address of the claimer implementation + */ + function implementation() external view returns (address) { + return _claimerImplementation; + } +} diff --git a/projects/vaults/contracts/adapters/InceptionWstETHMellowAdapter.sol b/projects/vaults/contracts/adapters/InceptionWstETHMellowAdapter.sol new file mode 100644 index 00000000..e5951e0f --- /dev/null +++ b/projects/vaults/contracts/adapters/InceptionWstETHMellowAdapter.sol @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {BeaconProxy, Address} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import {IInceptionMellowAdapter} from "../interfaces/adapters/IInceptionMellowAdapter.sol"; +import {IMellowDepositWrapper} from "../interfaces/symbiotic-vault/mellow-core/IMellowDepositWrapper.sol"; +import {IMellowVault} from "../interfaces/symbiotic-vault/mellow-core/IMellowVault.sol"; +import {IEthWrapper} from "../interfaces/symbiotic-vault/mellow-core/IEthWrapper.sol"; +import {IMellowSymbioticVault} from "../interfaces/symbiotic-vault/mellow-core/IMellowSymbioticVault.sol"; +import {IStakerRewards} from "../interfaces/symbiotic-vault/symbiotic-core/IStakerRewards.sol"; + +import {InceptionBaseAdapter} from "./InceptionBaseAdapter.sol"; +import {MellowAdapterClaimer} from "../adapter-claimers/MellowAdapterClaimer.sol"; + +/** + * @title The InceptionWstETHMellowAdapter Contract + * @author The InceptionLRT team + * @dev Handles delegation and withdrawal requests within the Mellow protocol for wstETH asset token. + * @notice Can only be executed by InceptionVault/InceptionOperator or the owner and used for wstETH asset. + */ +contract InceptionWstETHMellowAdapter is + IInceptionMellowAdapter, + InceptionBaseAdapter +{ + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @dev Kept only for storage slot + mapping(address => IMellowDepositWrapper) public mellowDepositWrappers; // mellowVault => mellowDepositWrapper + IMellowVault[] public mellowVaults; + + mapping(address => uint256) public allocations; + uint256 public totalAllocations; + + /// @dev Kept only for storage slot + uint256 public requestDeadline; + /// @dev Kept only for storage slot + uint256 public depositSlippage; // BasisPoints 10,000 = 100% + /// @dev Kept only for storage slot + uint256 public withdrawSlippage; + + address public ethWrapper; + + mapping(address => address) internal _claimerVaults; + address internal _emergencyClaimer; + EnumerableSet.AddressSet internal pendingClaimers; + address[] internal availableClaimers; + + address internal _claimerImplementation; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() payable { + _disableInitializers(); + } + + /** + * @notice Initializes the Mellow adapter with vaults and parameters + * @param _mellowVaults Array of Mellow vault addresses + * @param asset Address of the underlying asset + * @param trusteeManager Address of the trustee manager + */ + function initialize( + IMellowVault[] memory _mellowVaults, + IERC20 asset, + address trusteeManager + ) public initializer { + __InceptionBaseAdapter_init(asset, trusteeManager); + + uint256 totalAllocations_; + for (uint256 i = 0; i < _mellowVaults.length; i++) { + for (uint8 j = 0; j < i; j++) + if (address(_mellowVaults[i]) == address(_mellowVaults[j])) + revert AlreadyAdded(); + mellowVaults.push(_mellowVaults[i]); + allocations[address(_mellowVaults[i])] = 1; + totalAllocations_ += 1; + } + + totalAllocations = totalAllocations_; + } + + /** + * @notice Delegates funds to a Mellow vault either directly or automatically + * @dev Can only be called by trustee when contract is not paused + * @param mellowVault Address of the target Mellow vault + * @param amount Amount of tokens to delegate + * @param _data Additional data containing referral address and auto-delegation flag + * @return depositedAmount The amount successfully deposited + */ + function delegate( + address mellowVault, + uint256 amount, + bytes[] calldata _data + ) + external + override + onlyTrustee + whenNotPaused + returns (uint256 depositedAmount) + { + (address referral, bool delegateAuto) = abi.decode( + _data[0], + (address, bool) + ); + if (!delegateAuto) return _delegate(mellowVault, amount, referral); + else return _delegateAuto(amount, referral); + } + + /** + * @notice Checks if the specified Mellow Vault address is in the list of allowed vaults + * @dev Iterates through the mellowVaults array and compares the provided address with each element + * @param mellowVault The address of the vault to check + * @return bool Returns true if the vault is found in the list, false otherwise + **/ + function _beforeDelegate(address mellowVault) internal view returns (bool) { + for (uint8 i = 0; i < mellowVaults.length; i++) { + if (mellowVault == address(mellowVaults[i])) return true; + } + + return false; + } + + /** + * @notice Internal function to delegate funds to a specific vault + * @param mellowVault Address of the Mellow vault + * @param amount Amount to delegate + * @param referral Referral address + * @return Amount successfully deposited + */ + function _delegate( + address mellowVault, + uint256 amount, + address referral + ) internal returns (uint256) { + require(_beforeDelegate(mellowVault), NotAdded()); + + _asset.safeTransferFrom(msg.sender, address(this), amount); + IERC20(_asset).safeIncreaseAllowance(address(ethWrapper), amount); + + uint256 lpAmount = IEthWrapper(ethWrapper).deposit( + address(_asset), + amount, + mellowVault, + address(this), + referral + ); + + return lpAmountToAmount(lpAmount, IMellowVault(mellowVault)); + } + + /** + * @notice Internal function to automatically delegate funds across vaults + * @param amount Total amount to delegate + * @param referral Referral address + * @return depositedAmount Total amount successfully deposited + */ + function _delegateAuto( + uint256 amount, + address referral + ) internal returns (uint256 depositedAmount) { + uint256 allocationsTotal = totalAllocations; + _asset.safeTransferFrom(msg.sender, address(this), amount); + + for (uint8 i = 0; i < mellowVaults.length; i++) { + uint256 allocation = allocations[address(mellowVaults[i])]; + if (allocation > 0) { + uint256 localBalance = (amount * allocation) / allocationsTotal; + IERC20(_asset).safeIncreaseAllowance( + address(ethWrapper), + localBalance + ); + uint256 lpAmount = IEthWrapper(ethWrapper).deposit( + address(_asset), + localBalance, + address(mellowVaults[i]), + address(this), + referral + ); + depositedAmount += lpAmountToAmount(lpAmount, mellowVaults[i]); + } + } + + uint256 left = _asset.balanceOf(address(this)); + if (left != 0) _asset.safeTransfer(_inceptionVault, left); + } + + /** + * @notice Withdraws funds from a Mellow vault + * @dev Can only be called by trustee when contract is not paused + * @param _mellowVault Address of the Mellow vault to withdraw from + * @param amount Amount to withdraw + * @param emergency Flag for emergency withdrawal + * @return Tuple of (remaining amount to withdraw, amount claimed) + */ + function withdraw( + address _mellowVault, + uint256 amount, + bytes[] calldata /*_data*/, + bool emergency + ) external override onlyTrustee whenNotPaused returns (uint256, uint256) { + address claimer = _getOrCreateClaimer(emergency); + uint256 balanceState = _asset.balanceOf(claimer); + + // claim from mellow + IERC4626(_mellowVault).withdraw(amount, claimer, address(this)); + _claimerVaults[claimer] = _mellowVault; + + uint256 claimedAmount = (_asset.balanceOf(claimer) - balanceState); + uint256 undelegatedAmount = amount - claimedAmount; + + if (claimedAmount > 0) { + claimer == address(this) + ? _asset.safeTransfer(_inceptionVault, claimedAmount) + : _asset.safeTransferFrom( + claimer, + _inceptionVault, + claimedAmount + ); + } + + if (undelegatedAmount == 0) _removePendingClaimer(claimer); + + emit MellowWithdrawn(undelegatedAmount, claimedAmount, claimer); + return (undelegatedAmount, claimedAmount); + } + + /** + * @notice Claims available rewards or withdrawn funds + * @dev Can only be called by trustee + * @param _data Array containing vault address and claim parameters + * @param emergency Flag for emergency claim process + * @return Amount of tokens claimed + */ + function claim( + bytes[] calldata _data, + bool emergency + ) external override onlyTrustee whenNotPaused returns (uint256) { + require(_data.length > 0, ValueZero()); + (address _mellowVault, address claimer) = abi.decode( + _data[0], + (address, address) + ); + + // emergency claim available only for emergency claimer + if ((emergency && _emergencyClaimer != claimer) || (!emergency && claimer == _emergencyClaimer)) revert OnlyEmergency(); + if (!emergency && _claimerVaults[claimer] != _mellowVault) revert InvalidVault(); + if (!emergency) _removePendingClaimer(claimer); + + uint256 amount = MellowAdapterClaimer(claimer).claim( + _mellowVault, + address(this), + type(uint256).max + ); + + require(amount > 0, ValueZero()); + _asset.safeTransfer(_inceptionVault, amount); + + return amount; + } + + /** + * @notice Adds a new Mellow vault to the adapter + * @param mellowVault Address of the new vault + */ + function addMellowVault(address mellowVault) external onlyOwner { + require(mellowVault != address(0), ZeroAddress()); + + for (uint8 i = 0; i < mellowVaults.length; i++) { + require(mellowVault != address(mellowVaults[i]), AlreadyAdded()); + } + + mellowVaults.push(IMellowVault(mellowVault)); + + emit VaultAdded(mellowVault); + } + + /** + * @notice Remove a Mellow vault from the adapter + * @param vault Address of the mellow vault to be removed + */ + function removeVault(address vault) external onlyOwner { + require(vault != address(0), ZeroAddress()); + require( + getDeposited(vault) == 0 && + pendingWithdrawalAmount(vault, true) == 0 && + pendingWithdrawalAmount(vault, false) == 0, + VaultNotEmpty() + ); + + uint256 index = type(uint256).max; + for (uint256 i = 0; i < mellowVaults.length; i++) { + if (address(mellowVaults[i]) == vault) { + index = i; + break; + } + } + + require(index != type(uint256).max, InvalidVault()); + + mellowVaults[index] = mellowVaults[mellowVaults.length - 1]; + mellowVaults.pop(); + + emit VaultRemoved(vault); + } + + /** + * @notice Changes allocation for a specific vault + * @param mellowVault Address of the vault + * @param newAllocation New allocation amount + */ + function changeAllocation( + address mellowVault, + uint256 newAllocation + ) external onlyOwner { + require(mellowVault != address(0), ZeroAddress()); + + bool exists; + for (uint8 i = 0; i < mellowVaults.length; i++) { + if (mellowVault == address(mellowVaults[i])) exists = true; + } + require(exists, InvalidVault()); + uint256 oldAllocation = allocations[mellowVault]; + allocations[mellowVault] = newAllocation; + + totalAllocations = totalAllocations + newAllocation - oldAllocation; + + emit AllocationChanged(mellowVault, oldAllocation, newAllocation); + } + + /** + * @notice Claim rewards from Mellow protocol. + * @dev Rewards distribution functionality is not yet available in the Mellow protocol. + */ + function claimRewards( + address /*rewardToken*/, + bytes memory /*rewardsData*/ + ) external view onlyTrustee { + // Rewards distribution functionality is not yet available in the Mellow protocol. + revert("Mellow distribution rewards not implemented yet"); + } + + /** + * @notice Returns the total amount available for withdrawal + * @return total Amount that can be claimed + */ + function claimableWithdrawalAmount() public view returns (uint256 total) { + return _claimableWithdrawalAmount(false); + } + + /** + * @notice Internal function to calculate claimable withdrawal amount for an address + * @param emergency Emergency flag for claimer + * @return total Total claimable amount + */ + function _claimableWithdrawalAmount( + bool emergency + ) internal view returns (uint256 total) { + if (emergency) { + for (uint256 i = 0; i < mellowVaults.length; i++) { + total += IMellowSymbioticVault(address(mellowVaults[i])) + .claimableAssetsOf(_emergencyClaimer); + } + return total; + } + + for (uint256 i = 0; i < pendingClaimers.length(); i++) { + total += IMellowSymbioticVault( + _claimerVaults[pendingClaimers.at(i)] + ).claimableAssetsOf(pendingClaimers.at(i)); + } + return total; + } + + /** + * @notice Returns the total amount of pending withdrawals + * @return total Amount of pending withdrawals + */ + function pendingWithdrawalAmount() public view override returns (uint256 total) { + return _pendingWithdrawalAmount(false) + _claimableWithdrawalAmount(false); + } + + /** + * @notice Returns the total inactive balance for emergency situations + * @return Sum of emergency pending withdrawals, claimable withdrawals, and claimable amount + */ + function pendingEmergencyWithdrawalAmount() public view returns (uint256) { + return _pendingWithdrawalAmount(true) + _claimableWithdrawalAmount(true); + } + + /** + * @notice Internal function to calculate pending withdrawal amount for an address + * @param emergency Emergency flag for claimer + * @return total Total pending withdrawal amount + */ + function _pendingWithdrawalAmount( + bool emergency + ) internal view returns (uint256 total) { + if (emergency) { + for (uint256 i = 0; i < mellowVaults.length; i++) { + total += IMellowSymbioticVault(address(mellowVaults[i])) + .pendingAssetsOf(_emergencyClaimer); + } + return total; + } + + for (uint256 i = 0; i < pendingClaimers.length(); i++) { + total += IMellowSymbioticVault( + _claimerVaults[pendingClaimers.at(i)] + ).pendingAssetsOf(pendingClaimers.at(i)); + } + return total; + } + + /** + * @notice Returns pending withdrawal amount for a specific vault + * @param _mellowVault Address of the vault to check + * @param emergency Emergency claimer + * @return total Amount of pending withdrawals for the vault + */ + function pendingWithdrawalAmount( + address _mellowVault, + bool emergency + ) public view returns (uint256 total) { + if (emergency) + return + IMellowSymbioticVault(_mellowVault).pendingAssetsOf( + _emergencyClaimer + ); + + for (uint256 i = 0; i < pendingClaimers.length(); i++) { + total += IMellowSymbioticVault(_mellowVault).pendingAssetsOf( + pendingClaimers.at(i) + ); + } + + return total; + } + + /** + * @notice Returns the amount deposited in a specific vault + * @param _mellowVault Address of the vault to check + * @return Amount deposited in the vault + */ + function getDeposited( + address _mellowVault + ) public view override returns (uint256) { + IMellowVault mellowVault = IMellowVault(_mellowVault); + uint256 balance = mellowVault.balanceOf(address(this)); + if (balance == 0) return 0; + + return IERC4626(address(mellowVault)).previewRedeem(balance); + } + + /** + * @notice Returns the total amount tokens related to adapter + * @return total is the total amount tokens related to adapter + */ + function getTotalBalance() external view returns(uint256) { + return inactiveBalance() + getTotalDeposited(); + } + + /** + * @notice Returns the total inactive balance + * @return Sum of pending withdrawals, pending emergency withdrawals, claimable amounts + */ + function inactiveBalance() public view override returns (uint256) { + return pendingWithdrawalAmount() + pendingEmergencyWithdrawalAmount() + claimableAmount(); + } + + /** + * @notice Returns the total amount deposited across all vaults + * @return total is the total amount deposited + */ + function getTotalDeposited() public view override returns (uint256 total) { + for (uint256 i = 0; i < mellowVaults.length; i++) { + uint256 balance = mellowVaults[i].balanceOf(address(this)); + if (balance > 0) + total += IERC4626(address(mellowVaults[i])).previewRedeem( + balance + ); + } + return total; + } + + /** + * @notice Converts token amount to LP token amount + * @param amount Amount of tokens to convert + * @param mellowVault Vault for conversion calculation + * @return lpAmount Equivalent amount in LP tokens + */ + function amountToLpAmount( + uint256 amount, + IMellowVault mellowVault + ) public view returns (uint256 lpAmount) { + return IERC4626(address(mellowVault)).convertToShares(amount); + } + + /** + * @notice Converts LP token amount to underlying token amount + * @param lpAmount Amount of LP tokens to convert + * @param mellowVault Vault for conversion calculation + * @return Equivalent amount in underlying tokens + */ + function lpAmountToAmount( + uint256 lpAmount, + IMellowVault mellowVault + ) public view returns (uint256) { + return IERC4626(address(mellowVault)).convertToAssets(lpAmount); + } + + /** + * @notice Sets the ETH wrapper contract address + * @param newEthWrapper Address of the new ETH wrapper + */ + function setEthWrapper(address newEthWrapper) external onlyOwner { + require(Address.isContract(newEthWrapper), NotContract()); + + address oldWrapper = ethWrapper; + ethWrapper = newEthWrapper; + emit EthWrapperChanged(oldWrapper, newEthWrapper); + } + + /** + * @notice Sets the implementation address for the claimer + * @param newImplementation The address of the new implementation + */ + function setClaimerImplementation( + address newImplementation + ) external onlyOwner { + emit EmergencyClaimerSet(_claimerImplementation, newImplementation); + _claimerImplementation = newImplementation; + } + + /** + * @notice Returns the contract version + * @return Current version number (3) + */ + function getVersion() external pure override returns (uint256) { + return 3; + } + + /** + * @notice Retrieves or creates a claimer address based on the emergency condition + * @dev If `emergency` is true, returns the existing emergency claimer or deploys a new one if it doesn't exist. + * If `emergency` is false, reuses an available claimer from the `availableClaimers` array or deploys a new one. + * The returned claimer is added to the `pendingClaimers` set + * @param emergency Boolean indicating whether an emergency claimer is required + * @return claimer The address of the claimer to be used + */ + function _getOrCreateClaimer( + bool emergency + ) internal virtual returns (address claimer) { + if (emergency) { + return + _emergencyClaimer != address(0) + ? _emergencyClaimer + : (_emergencyClaimer = _deployClaimer()); + } + + if (availableClaimers.length > 0) { + claimer = availableClaimers[availableClaimers.length - 1]; + availableClaimers.pop(); + } else { + claimer = _deployClaimer(); + } + + pendingClaimers.add(claimer); + return claimer; + } + + /** + * @notice Removes a claimer from the pending list and recycles it to the available claimers + * @dev Deletes the claimer's vault mapping, removes it from `pendingClaimers`, and adds it to `availableClaimers` + * @param claimer The address of the claimer to be removed from pending status + */ + function _removePendingClaimer(address claimer) internal { + delete _claimerVaults[claimer]; + pendingClaimers.remove(claimer); + availableClaimers.push(claimer); + } + + /** + * @notice Deploys a new MellowAdapterClaimer contract instance + * @dev Creates a new claimer contract with the `_asset` address passed as a initialize parameter + * @dev ownership is transferred to the adapter owner + * @return The address of the newly deployed MellowAdapterClaimer contract + */ + function _deployClaimer() internal returns (address) { + if (_claimerImplementation == address(0)) + revert ClaimerImplementationNotSet(); + // deploy new beacon proxy and do init call + bytes memory data = abi.encodeWithSignature( + "initialize(address)", + address(_asset) + ); + address claimer = address(new BeaconProxy(address(this), data)); + + (bool success,) = claimer.call( + abi.encodeWithSignature("transferOwnership(address)", owner()) + ); + require(success, TransferOwnershipFailed()); + + emit ClaimerDeployed(claimer); + return claimer; + } + + /** + * @notice Beacon proxy implementation address + * @return The address of the claimer implementation + */ + function implementation() external view returns (address) { + return _claimerImplementation; + } +} diff --git a/projects/vaults/contracts/assets-handler/InceptionAssetsHandler.sol b/projects/vaults/contracts/assets-handler/InceptionAssetsHandler.sol index 8288949e..170bce97 100644 --- a/projects/vaults/contracts/assets-handler/InceptionAssetsHandler.sol +++ b/projects/vaults/contracts/assets-handler/InceptionAssetsHandler.sol @@ -7,6 +7,7 @@ import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IInceptionVaultErrors} from "../interfaces/common/IInceptionVaultErrors.sol"; +import {IInceptionAssetsHandler} from "../interfaces/common/IInceptionAssetsHandler.sol"; /** * @title The InceptionAssetsHandler contract @@ -17,18 +18,26 @@ contract InceptionAssetsHandler is PausableUpgradeable, ReentrancyGuardUpgradeable, Ownable2StepUpgradeable, + IInceptionAssetsHandler, IInceptionVaultErrors { using SafeERC20 for IERC20; IERC20 internal _asset; - uint256[50 - 1] private __reserver; + uint256 public currentRewards; + /// @dev blockTime + uint256 public startTimeline; + /// @dev in seconds + uint256 public rewardsTimeline; + /// @dev Address of treasury which holds rewards. + address public rewardsTreasury; - function __InceptionAssetsHandler_init(IERC20 assetAddress) - internal - onlyInitializing - { + uint256[50 - 5] private __reserver; + + function __InceptionAssetsHandler_init( + IERC20 assetAddress + ) internal onlyInitializing { __Pausable_init(); __ReentrancyGuard_init(); @@ -42,35 +51,34 @@ contract InceptionAssetsHandler is /// @dev returns the balance of iVault in the asset function totalAssets() public view returns (uint256) { - return _asset.balanceOf(address(this)); + uint256 elapsedDays = (block.timestamp - startTimeline) / 1 days; + uint256 totalDays = rewardsTimeline / 1 days; + if (elapsedDays > totalDays) return _asset.balanceOf(address(this)); + uint256 reservedRewards = (currentRewards / totalDays) * (totalDays - elapsedDays); + return (_asset.balanceOf(address(this)) - reservedRewards); } - function _transferAssetFrom(address staker, uint256 amount) internal { - _asset.safeTransferFrom(staker, address(this), amount); - } + /** + * @notice Set rewards treasury address + * @param treasury Address of the treasury which holds rewards + */ + function setRewardsTreasury(address treasury) external onlyOwner { + require(treasury != address(0), NullParams()); - function _transferAssetTo(address receiver, uint256 amount) internal { - _asset.safeTransfer(receiver, amount); + emit SetRewardsTreasury(rewardsTreasury); + rewardsTreasury = treasury; } - /// @dev The functions below serve the proper withdrawal and claiming operations - /// @notice Since a particular LST loses some wei on each transfer, - /// this needs to be taken into account - function _getAssetWithdrawAmount(uint256 amount) - internal - view - virtual - returns (uint256) - { - return amount; - } + /** + * @notice Updates the duration of the rewards timeline. + * @dev The new timeline must be at least 1 day (86400 seconds) + * @param newTimelineInSeconds The new duration of the rewards timeline, measured in seconds. + */ + function setRewardsTimeline(uint256 newTimelineInSeconds) external onlyOwner { + if (newTimelineInSeconds < 1 days || newTimelineInSeconds % 1 days != 0) + revert InconsistentData(); - function _getAssetReceivedAmount(uint256 amount) - internal - view - virtual - returns (uint256) - { - return amount; + emit RewardsTimelineChanged(rewardsTimeline, newTimelineInSeconds); + rewardsTimeline = newTimelineInSeconds; } } diff --git a/projects/vaults/contracts/interfaces/adapter-claimer/IAdapterClaimer.sol b/projects/vaults/contracts/interfaces/adapter-claimer/IAdapterClaimer.sol new file mode 100644 index 00000000..93b70dbe --- /dev/null +++ b/projects/vaults/contracts/interfaces/adapter-claimer/IAdapterClaimer.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IAdapterClaimer { + error OnlyAdapter(); + + error ApprovalFailed(); +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/adapter-handler/IAdapterHandler.sol b/projects/vaults/contracts/interfaces/adapter-handler/IAdapterHandler.sol new file mode 100644 index 00000000..3ce797e5 --- /dev/null +++ b/projects/vaults/contracts/interfaces/adapter-handler/IAdapterHandler.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IAdapterHandler + * @dev Interface for handling delegation, undelegation, and adapter-related operations in a staking system. + */ +interface IAdapterHandler { + /** + * @dev Emitted when a staker delegates an amount to an operator. + * @param stakerAddress The address of the staker who initiates the delegation. + * @param operatorAddress The address of the operator receiving the delegation. + * @param amount The amount of tokens delegated. + */ + event DelegatedTo( + address indexed stakerAddress, + address indexed operatorAddress, + uint256 amount + ); + + /** + * @dev Emitted when a staker undelegates funds from an adapter and vault. + * @param adapter The address of the adapter contract involved in undelegation. + * @param vault The address of the vault from which funds are undelegated. + * @param actualAmounts The actual amount of tokens undelegated. + * @param claimedAmount The amount claimed during the undelegation process. + * @param epoch The epoch in which the undelegation occurred. + */ + event UndelegatedFrom( + address indexed adapter, + address indexed vault, + uint256 indexed actualAmounts, + uint256 claimedAmount, + uint256 epoch + ); + + /** + * @dev Emitted when a staker claims funds from an adapter and vault. + * @param adapter The address of the adapter contract involved in the claim. + * @param vault The address of the vault from which funds are claimed. + * @param claimedAmount The amount of tokens claimed. + * @param epoch The epoch in which the claim occurred. + */ + event ClaimedFrom( + address indexed adapter, + address indexed vault, + uint256 claimedAmount, + uint256 epoch + ); + + /** + * @dev Emitted when a user forcefully undelegates their claim. + * @param amount The amount of tokens undelegated. + * @param epoch The epoch in which the undelegation occurs. + */ + event ClaimFromVault(uint256 indexed amount, uint256 epoch); + + /** + * @dev Emitted when the target capacity of the system is changed. + * @param prevValue The previous target capacity value. + * @param newValue The new target capacity value. + */ + event TargetCapacityChanged(uint256 prevValue, uint256 newValue); + + /** + * @dev Emitted when a new adapter is added to the system. + * @param adapter The address of the newly added adapter. + */ + event AdapterAdded(address adapter); + + /** + * @dev Emitted when an adapter is removed from the system. + * @param adapter The address of the removed adapter. + */ + event AdapterRemoved(address adapter); + + /** + * @dev Emitted when rewards claimed from adapter. + * @param adapter The address of the removed adapter. + * @param token The address of reward token. + * @param amount Amount of reward. + */ + event RewardsClaimed(address adapter, address token, uint256 amount); + + /** + * @dev Emitted when free balance claimed from adapter + * @param adapter The address of the claimed adapter. + */ + event AdapterFreeBalanceClaimed(address adapter); + + /** + * @dev Deprecated structure representing a withdrawal request. + * @param epoch The epoch in which the withdrawal was requested. + * @param receiver The address receiving the withdrawn funds. + * @param amount The amount of tokens requested for withdrawal. + */ + struct __deprecated_Withdrawal { + uint256 epoch; + address receiver; + uint256 amount; + } + + /** + * Struct to define an undelegation request. + * @param adapter The address of the adapter contract handling the undelegation. + * @param vault The address of the vault from which assets are undelegated. + * @param amount The amount of assets to undelegate. + * @param data An array of bytes for additional parameters or instructions specific to the adapter. + */ + struct UndelegateRequest { + address adapter; + address vault; + uint256 amount; + bytes[] data; + } +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/adapters/IInceptionBaseAdapter.sol b/projects/vaults/contracts/interfaces/adapters/IInceptionBaseAdapter.sol new file mode 100644 index 00000000..1a69bed6 --- /dev/null +++ b/projects/vaults/contracts/interfaces/adapters/IInceptionBaseAdapter.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IInceptionBaseAdapter { + /************************************ + ************** Errors ************** + ************************************/ + + error ValueZero(); + + error TransferAssetFailed(address assetAddress); + + error InconsistentData(); + + error WrongClaimWithdrawalParams(); + + error NullParams(); + + error NotVaultOrTrusteeManager(); + + error LengthMismatch(); + + error InvalidVault(); + + error ZeroAddress(); + + error AlreadyAdded(); + + error NotContract(); + + error NotAdded(); + + error InvalidDataLength(uint256 expected, uint256 received); + + error OnlyEmergency(); + + error VaultNotEmpty(); + + error ClaimerImplementationNotSet(); + + error TransferOwnershipFailed(); + + /************************************ + ************** Events ************** + ************************************/ + + event InceptionVaultSet(address indexed oldVault, address indexed newVault); + + event TrusteeManagerSet( + address indexed _trusteeManager, + address indexed _newTrusteeManager + ); + + event EmergencyClaimerSet( + address indexed oldClaimer, + address indexed newClaimer + ); + + event ClaimerDeployed(address indexed claimer); + + /************************************ + ************** Functions ************** + ************************************/ + + function getDeposited(address vaultAddress) external view returns (uint256); + + function getTotalDeposited() external view returns (uint256); + + function pendingWithdrawalAmount() external view returns (uint256); + + function pendingEmergencyWithdrawalAmount() external view returns (uint256); + + function claimableAmount() external view returns (uint256); + + function inactiveBalance() external view returns (uint256); + + function getTotalBalance() external view returns(uint256); + + function delegate( + address vault, + uint256 amount, + bytes[] calldata _data + ) external returns (uint256 depositedAmount); + + function withdraw( + address vault, + uint256 amount, + bytes[] calldata _data, + bool emergency + ) external returns (uint256 undelegated, uint256 claimed); + + function claim( + bytes[] calldata _data, + bool emergency + ) external returns (uint256); + + function claimFreeBalance() external; + + function claimRewards( + address rewardToken, + bytes memory rewardData + ) external; +} diff --git a/projects/vaults/contracts/interfaces/adapters/IInceptionEigenLayerAdapter.sol b/projects/vaults/contracts/interfaces/adapters/IInceptionEigenLayerAdapter.sol new file mode 100644 index 00000000..a4a0e834 --- /dev/null +++ b/projects/vaults/contracts/interfaces/adapters/IInceptionEigenLayerAdapter.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IDelegationManager, IStrategy, IERC20} from "../eigenlayer-vault/eigen-core/IDelegationManager.sol"; +import {IMellowVault} from "../symbiotic-vault/mellow-core/IMellowVault.sol"; +import {IInceptionBaseAdapter} from "./IInceptionBaseAdapter.sol"; + +interface IInceptionEigenAdapterErrors { + /// TVL errors + error ExceedsMaxPerDeposit(uint256 max, uint256 amount); + error ExceedsMaxTotalDeposited(uint256 max, uint256 amount); +} + +interface IInceptionEigenLayerAdapter is IInceptionEigenAdapterErrors, IInceptionBaseAdapter { + event StartWithdrawal( + address indexed stakerAddress, + IStrategy strategy, + uint256 shares, + uint32 withdrawalStartBlock, + address delegatedAddress, + uint256 nonce + ); + + event Withdrawal( + bytes32 withdrawalRoot, + IStrategy[] strategies, + uint256[] shares, + uint32 withdrawalStartBlock + ); + + event RewardCoordinatorChanged( + address indexed prevValue, + address indexed newValue + ); + + event WithdrawalsQueued(bytes32[] withdrawalRoots); + + event Undelegated(address operator); + + event RedelegatedTo(address operatorFrom, address operatorTo); + + function setRewardsCoordinator(address newRewardCoordinator, address claimer) external; +} diff --git a/projects/vaults/contracts/interfaces/adapters/IInceptionMellowAdapter.sol b/projects/vaults/contracts/interfaces/adapters/IInceptionMellowAdapter.sol new file mode 100644 index 00000000..521331c8 --- /dev/null +++ b/projects/vaults/contracts/interfaces/adapters/IInceptionMellowAdapter.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IMellowVault} from "../symbiotic-vault/mellow-core/IMellowVault.sol"; +import {IInceptionBaseAdapter} from "./IInceptionBaseAdapter.sol"; + +interface IInceptionMellowAdapter is IInceptionBaseAdapter { + error InactiveWrapper(); + error NoWrapperExists(); + error BadMellowWithdrawRequest(); + error InvalidWrapperForVault(); + error InvalidAllocation(); + error TooMuchSlippage(); + error AlreadyDeactivated(); + + event AllocationChanged( + address mellowVault, + uint256 oldAllocation, + uint256 newAllocation + ); + + event VaultAdded(address indexed _mellowVault); + + event VaultRemoved(address indexed _mellowVault); + + event DeactivatedMellowVault(address indexed _mellowVault); + + event EthWrapperChanged(address indexed _old, address indexed _new); + + event MellowWithdrawn(uint256 amount, uint256 claimedAmount, address claimer); + + function claimableWithdrawalAmount() external view returns (uint256); +} diff --git a/projects/vaults/contracts/interfaces/adapters/IInceptionSymbioticAdapter.sol b/projects/vaults/contracts/interfaces/adapters/IInceptionSymbioticAdapter.sol new file mode 100644 index 00000000..feb7d694 --- /dev/null +++ b/projects/vaults/contracts/interfaces/adapters/IInceptionSymbioticAdapter.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IInceptionBaseAdapter} from "./IInceptionBaseAdapter.sol"; + +interface IInceptionSymbioticAdapter is IInceptionBaseAdapter { + error WithdrawalInProgress(); + + error NothingToClaim(); + + event VaultAdded(address indexed vault); + + event VaultRemoved(address indexed vault); + + error InvalidCollateral(); + + error InvalidEpoch(); + + error AlreadyClaimed(); + + error WrongEpoch(); + + event MintedShares(uint256 mintedShares); + + event BurnedAndMintedShares(uint256 burnedShares, uint256 mintedShares); + + event SymbioticWithdrawn(uint256 burnedShares, uint256 mintedShares, address claimer); +} diff --git a/projects/vaults/contracts/interfaces/common/IEmergencyClaimer.sol b/projects/vaults/contracts/interfaces/common/IEmergencyClaimer.sol new file mode 100644 index 00000000..2dbe2e11 --- /dev/null +++ b/projects/vaults/contracts/interfaces/common/IEmergencyClaimer.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IEmergencyClaimer { + /** + * @notice Claims funds from a Symbiotic vault for a specific epoch. + * @dev Can only be called by the symbiotic adapter. + * @param vault The address of the Symbiotic vault to claim from. + * @param recipient The address to receive the claimed funds. + * @param epoch The epoch for which the claim is being made. + * @return The amount of funds claimed. + */ + function claimSymbiotic(address vault, address recipient, uint256 epoch) external returns (uint256); + + /** + * @notice Claims funds from a Mellow Symbiotic vault. + * @dev Can only be called by the mellow adapter. + * @param vault The address of the Mellow Symbiotic vault to claim from. + * @param recipient The address to receive the claimed funds. + * @param amount The amount of funds to claim. + * @return The amount of funds claimed. + */ + function claimMellow(address vault, address recipient, uint256 amount) external returns (uint256); +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/common/IInceptionAssetsHandler.sol b/projects/vaults/contracts/interfaces/common/IInceptionAssetsHandler.sol new file mode 100644 index 00000000..f06b004a --- /dev/null +++ b/projects/vaults/contracts/interfaces/common/IInceptionAssetsHandler.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IInceptionAssetsHandler { + /** + * @dev Emitted when new rewards treasury set. + * @param treasury The address of the new treasury. + */ + event SetRewardsTreasury(address treasury); + + /** + * @dev Emitted when rewards timeline changed. + * @param rewardsTimeline new rewards timeline. + * @param newTimelineInSeconds new rewards timeline in seconds. + */ + event RewardsTimelineChanged( + uint256 rewardsTimeline, + uint256 newTimelineInSeconds + ); + + /** + * @dev Emitted when rewards added to vault. + * @param amount Amount of reward. + * @param startTimeline timestamp of added rewards. + */ + event RewardsAdded(uint256 amount, uint256 startTimeline); +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/common/IInceptionVaultErrors.sol b/projects/vaults/contracts/interfaces/common/IInceptionVaultErrors.sol index 91d4e236..80dbe03d 100644 --- a/projects/vaults/contracts/interfaces/common/IInceptionVaultErrors.sol +++ b/projects/vaults/contracts/interfaces/common/IInceptionVaultErrors.sol @@ -46,6 +46,8 @@ interface IInceptionVaultErrors { error LowerMinAmount(uint256 minAmount); + error SlippageMinOut(uint256 minOut, uint256 resultAmount); + error ZeroFlashWithdrawFee(); /// TVL errors @@ -65,6 +67,24 @@ interface IInceptionVaultErrors { error InvalidAddress(); error MoreThanMax(); - + error ValueZero(); + + error AdapterAlreadyAdded(); + + error AdapterNotFound(); + + error AdapterNotEmpty(); + + error ClaimFailed(); + + error WithdrawalFailed(); + + error InsufficientFreeBalance(); + + error MintedLess(); + + error LowerThanMinOut(uint256 minOut); + + error RewardsTreasuryNotSet(); } diff --git a/projects/vaults/contracts/interfaces/common/IStEth.sol b/projects/vaults/contracts/interfaces/common/IStEth.sol index 323b6ea8..d12a95a5 100644 --- a/projects/vaults/contracts/interfaces/common/IStEth.sol +++ b/projects/vaults/contracts/interfaces/common/IStEth.sol @@ -14,3 +14,20 @@ interface IStEth is IERC20 { uint256 _ethAmount ) external view returns (uint256); } + + +interface IWStethInterface is IERC20 { + function stETH() external returns (IERC20); + + function wrap(uint256 stethAmount) external payable returns (uint256); + + function unwrap(uint256 wstethAmount) external returns (uint256); + + function getStETHByWstETH( + uint256 wstethAmount + ) external view returns (uint256); + + function getWstETHByStETH( + uint256 stethAmount + ) external view returns (uint256); +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/common/IWithdrawalQueue.sol b/projects/vaults/contracts/interfaces/common/IWithdrawalQueue.sol new file mode 100644 index 00000000..9aff046e --- /dev/null +++ b/projects/vaults/contracts/interfaces/common/IWithdrawalQueue.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +interface IWithdrawalQueueErrors { + error ValueZero(); + error OnlyVaultOrOwnerAllowed(); + error InvalidEpoch(); + error EpochAlreadyRedeemable(); + error UndelegateEpochMismatch(); + error UndelegateNotCompleted(); + error ClaimUnknownAdapter(); + error ClaimNotCompleted(); +} + +interface IWithdrawalQueue is IWithdrawalQueueErrors { + event EpochReset(uint256 indexed epoch); + + struct WithdrawalEpoch { + bool ableRedeem; + + uint256 totalRequestedShares; + uint256 totalClaimedAmount; + uint256 totalUndelegatedAmount; + + mapping(address => uint256) userShares; + mapping(address => mapping(address => uint256)) adapterUndelegated; + + uint8 adaptersUndelegatedCounter; + uint8 adaptersClaimedCounter; + } + + /* + * @notice Requests a withdrawal for a receiver in the current epoch + * @param receiver The address requesting the withdrawal + * @param shares The number of shares to request for withdrawal + */ + function request(address receiver, uint256 shares) external; + + /* + * @notice Processes undelegation for multiple adapters and vaults in a given epoch + * @param epoch The epoch to undelegate from (must match current epoch) + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + * @param undelegatedAmounts Array of undelegated amounts + * @param claimedAmounts Array of claimed amounts + */ + function undelegate( + uint256 epoch, + address[] calldata adapters, + address[] calldata vaults, + uint256[] calldata undelegatedAmounts, + uint256[] calldata claimedAmounts + ) external; + + /* + * @notice Claims an amount for a specific adapter and vault in an epoch + * @param epoch The epoch to claim from + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + * @param claimedAmounts Array of claimed amounts + */ + function claim( + uint256 epoch, + address[] calldata adapters, + address[] calldata vaults, + uint256[] calldata claimedAmounts + ) external; + + /* + * @notice Forces undelegation and claims a specified amount for the current epoch + * @param epoch The epoch number to process, must match the current epoch + * @param claimedAmount The amount to claim, must not exceed totalAmountRedeemFree + */ + function forceUndelegateAndClaim(uint256 epoch, uint256 claimedAmount) external; + + /* + * @notice Redeems available amounts for a receiver across their epochs + * @param receiver The address to redeem for + * @return amount The total amount redeemed + */ + function redeem(address receiver) external returns (uint256 amount); + + /* + * @notice Redeems available amounts for a receiver with given epoch index + * @param receiver The address to redeem for + * @param userEpochIndex user epoch index + * @return amount The total amount redeemed + */ + function redeem(address receiver, uint256 userEpochIndex) external returns (uint256 amount); + + /*////////////////////////// + ////// GET functions ////// + ////////////////////////*/ + + /* + * @notice Returns the emergency epoch number + * @return The emergency epoch number + */ + function EMERGENCY_EPOCH() external view returns (uint256); + + /* + * @notice Returns the current epoch number + * @return The current epoch number + */ + function currentEpoch() external view returns (uint256); + + /* + * @notice Returns the total shares queued for withdrawal + * @return The total amount to withdraw + */ + function totalSharesToWithdraw() external view returns (uint256); + + /* + * @notice Returns the total amount that has been redeemed + * @return The total redeemed amount + */ + function totalAmountRedeem() external view returns (uint256); + + /* + * @notice Returns the total pending withdrawal amount for a receiver + * @param receiver The address to check + * @return amount The total pending withdrawal amount + */ + function getPendingWithdrawalOf(address receiver) external view returns (uint256 amount); + + /* + * @notice Checks if a claimer has redeemable withdrawals and their epoch indexes inside userEpoch mapping + * @param claimer The address to check + * @return able Whether there are redeemable withdrawals + * @return withdrawalIndexes Array of user epoch indexes with redeemable withdrawals + */ + function isRedeemable(address claimer) external view returns (bool able, uint256[] memory withdrawalIndexes); + + /* + * @notice Retrieves the total number of requested shares for a specific epoch + * @param epoch The epoch number for which to retrieve the requested shares + * @return The total number of shares requested in the specified epoch + */ + function getRequestedShares(uint256 epoch) external view returns (uint256); +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IDelegationManager.sol b/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IDelegationManager.sol index e6caeea0..3cae9262 100644 --- a/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IDelegationManager.sol +++ b/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IDelegationManager.sol @@ -48,27 +48,25 @@ interface IDelegationManager { uint256[] shares; } + event WithdrawalQueued(bytes32 withdrawalRoot, Withdrawal withdrawal); + function delegateTo( address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt ) external; - function undelegate(address staker) external; - - event WithdrawalQueued(bytes32 withdrawalRoot, Withdrawal withdrawal); + function undelegate(address staker) external returns (bytes32[] memory withdrawalRoots); function completeQueuedWithdrawal( Withdrawal calldata withdrawal, IERC20[] calldata tokens, - uint256 middlewareTimesIndex, bool receiveAsTokens ) external; function completeQueuedWithdrawals( Withdrawal[] calldata withdrawals, IERC20[][] calldata tokens, - uint256[] calldata middlewareTimesIndexes, bool[] calldata receiveAsTokens ) external; @@ -78,6 +76,12 @@ interface IDelegationManager { function delegatedTo(address staker) external view returns (address); + function redelegate( + address newOperator, + SignatureWithExpiry memory newOperatorApproverSig, + bytes32 approverSalt + ) external returns (bytes32[] memory withdrawalRoots); + function operatorShares( address operator, address strategy @@ -87,9 +91,29 @@ interface IDelegationManager { address staker ) external view returns (uint256); - function withdrawalDelayBlocks() external view returns (uint256); + function minWithdrawalDelayBlocks() external view returns (uint256); function isOperator(address operator) external view returns (bool); function isDelegated(address staker) external view returns (bool); + + function getQueuedWithdrawals( + address staker + ) external view returns (Withdrawal[] memory withdrawals, uint256[][] memory shares); + + function getWithdrawableShares( + address staker, + IStrategy[] memory strategies + ) external view returns (uint256[] memory withdrawableShares, uint256[] memory depositShares); + + function getQueuedWithdrawal( + bytes32 withdrawalRoot + ) external view returns (Withdrawal memory withdrawal, uint256[] memory shares); + + /// @notice Returns a list of queued withdrawal roots for the `staker`. + /// NOTE that this only returns withdrawals queued AFTER the slashing release. + function getQueuedWithdrawalRoots( + address staker + ) external view returns (bytes32[] memory); + } diff --git a/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol b/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol index 94c55112..4b500e09 100644 --- a/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol +++ b/projects/vaults/contracts/interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol @@ -91,9 +91,5 @@ interface IStrategyManager { function withdrawalDelayBlocks() external view returns (uint256); - function numWithdrawalsQueued( - address account - ) external view returns (uint256); - function delegation() external view returns (address); } diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/IInceptionVault_S.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/IInceptionVault_S.sol index 1cd7cac3..b903eec7 100644 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/IInceptionVault_S.sol +++ b/projects/vaults/contracts/interfaces/symbiotic-vault/IInceptionVault_S.sol @@ -54,7 +54,7 @@ interface IInceptionVault_S { event TreasuryChanged(address prevValue, address newValue); - event MellowRestakerChanged(address prevValue, address newValue); + event MellowAdapterChanged(address prevValue, address newValue); event ReferralCode(bytes32 indexed code); @@ -76,6 +76,10 @@ interface IInceptionVault_S { event WithdrawalFee(uint256 indexed fee); + event WithdrawalQueueChanged(address queue); + + event DepositBonusTransferred(address newVault, uint256 amount); + function inceptionToken() external view returns (IInceptionToken); function ratio() external view returns (uint256); diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/ISymbioticHandler.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/ISymbioticHandler.sol deleted file mode 100644 index 62cae8f2..00000000 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/ISymbioticHandler.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -interface IMellowHandler { - event StartMellowWithdrawal( - address indexed stakerAddress, - uint256 indexed actualAmounts - ); - - event StartEmergencyMellowWithdrawal( - address indexed stakerAddress, - uint256 indexed actualAmounts - ); - - event Delegated( - address indexed stakerAddress, - uint256 amount, - uint256 lpAmount - ); -} - -interface ISymbioticHandler is IMellowHandler { - /// @dev Epoch represents the period of the rebalancing process - /// @dev Receiver is a receiver of assets in claim() - /// @dev Amount represents the exact amount of the asset to be claimed - struct Withdrawal { - uint256 epoch; - address receiver; - uint256 amount; - } - - event DelegatedTo( - address indexed stakerAddress, - address indexed operatorAddress, - uint256 amount - ); - - event WithdrawalClaimed(uint256 totalAmount); - - event TargetCapacityChanged(uint256 prevValue, uint256 newValue); - - event SymbioticRestakerAdded(address indexed newValue); -} diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/mellow-core/IEthWrapper.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/mellow-core/IEthWrapper.sol new file mode 100644 index 00000000..8ccc9de7 --- /dev/null +++ b/projects/vaults/contracts/interfaces/symbiotic-vault/mellow-core/IEthWrapper.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +/** + * @title IEthWrapper + * @notice Interface for wrapping and converting input tokens (WETH, wstETH, stETH, ETH) into wstETH and depositing them into an ERC4626Vault. + * @dev This contract acts as an intermediary to handle deposits using various ETH derivatives and wraps them into wstETH for ERC4626 vault deposits. + */ +interface IEthWrapper { + /** + * @notice Returns the address of the WETH token. + * @return The address of WETH. + */ + function WETH() external view returns (address); + + /** + * @notice Returns the address of the wstETH token. + * @return The address of wstETH. + */ + function wstETH() external view returns (address); + + /** + * @notice Returns the address of the stETH token. + * @return The address of stETH. + */ + function stETH() external view returns (address); + + /** + * @notice Returns the address used to represent ETH (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE). + * @return The special address representing ETH. + */ + function ETH() external view returns (address); + + /** + * @notice Deposits a specified `amount` of the `depositToken` into the provided `vault`, crediting the specified `receiver` with shares. + * @param depositToken The address of the token being deposited (WETH, wstETH, stETH, or ETH). + * @param amount The amount of `depositToken` to deposit. + * @param vault The address of the ERC4626 vault where the deposit will be made. + * @param receiver The address of the account receiving shares from the deposit. + * @param referral The address of the referral, if applicable. + * @return shares The amount of vault shares received after the deposit. + * + * @dev The `depositToken` must be one of WETH, wstETH, stETH, or ETH. + * @dev If `depositToken` is ETH, the `amount` must match `msg.value`. + * @dev If `depositToken` is not ETH, `msg.value` must be zero and the specified `amount` must be transferred from the sender. + */ + function deposit( + address depositToken, + uint256 amount, + address vault, + address receiver, + address referral + ) external payable returns (uint256 shares); +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/mellow-core/IMellowSymbioticVault.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/mellow-core/IMellowSymbioticVault.sol new file mode 100644 index 00000000..dd0fd3e2 --- /dev/null +++ b/projects/vaults/contracts/interfaces/symbiotic-vault/mellow-core/IMellowSymbioticVault.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +/** + * @title IMellowSymbioticVault + * @notice Interface for the Mellow Symbiotic Vault. + */ +interface IMellowSymbioticVault { + /** + * @notice Struct to store initialization parameters for the vault. + * @param limit The maximum limit for deposits. + * @param symbioticCollateral The address of the underlying Symbiotic Collateral. + * @param symbioticVault The address of the underlying Symbiotic Vault. + * @param withdrawalQueue The address of the associated withdrawal queue. + * @param admin The address of the vault's admin. + * @param depositPause Indicates whether deposits are paused initially. + * @param withdrawalPause Indicates whether withdrawals are paused initially. + * @param depositWhitelist Indicates whether a deposit whitelist is enabled initially. + * @param name The name of the vault token. + * @param symbol The symbol of the vault token. + */ + struct InitParams { + uint256 limit; + address symbioticCollateral; + address symbioticVault; + address withdrawalQueue; + address admin; + bool depositPause; + bool withdrawalPause; + bool depositWhitelist; + string name; + string symbol; + } + + /** + * @notice Initializes the vault with the provided parameters. + * @param initParams The initialization parameters. + * + * @custom:requirements + * - The vault MUST not have been initialized before this call. + */ + function initialize(InitParams memory initParams) external; + + /** + * @notice Returns the amount of `asset` that can be claimed by a specific account. + * @param account The address of the account. + * @return claimableAssets The amount of claimable assets. + */ + function claimableAssetsOf(address account) external view returns (uint256 claimableAssets); + + /** + * @notice Returns the amount of `asset` that is in the withdrawal queue for a specific account. + * @param account The address of the account. + * @return pendingAssets The amount of pending assets that cannot be claimed yet. + */ + function pendingAssetsOf(address account) external view returns (uint256 pendingAssets); + + /** + * @notice Finalizes the withdrawal process for an account and transfers assets to the recipient. + * @param account The address of the account initiating the withdrawal. + * @param recipient The address of the recipient receiving the assets. + * @param maxAmount The maximum amount of assets to withdraw. + * @return shares The actual number of shares claimed. + * + * @custom:requirements + * - The `account` MUST be equal to `msg.sender`. + * + * @custom:effects + * - Finalizes the withdrawal process and transfers up to `maxAmount` of `asset` to the `recipient`. + */ + function claim(address account, address recipient, uint256 maxAmount) + external + returns (uint256); + + /** + * @notice Deposits available assets into the Symbiotic Vault and collateral according to their capacities. + * @return collateralWithdrawal The amount of collateral withdrawn to match the Symbiotic Vault deposit requirements. + * @return collateralDeposit The amount of assets deposited into the collateral. + * @return vaultDeposit The amount of assets deposited into the Symbiotic Vault. + * + * @dev This function first calculates the appropriate amounts to withdraw and deposit using `_calculatePushAmounts`. + * It then performs the necessary withdrawals and deposits, adjusting allowances as needed. + * Finally, it emits a `SymbioticPushed` event with the results. + * @custom:effects + * - Deposits assets into the Symbiotic Vault and collateral according to their capacities. + * - Prioritizes Symbiotic Vault deposits over collateral deposits. + * - If required withdraws collateral to match the Symbiotic Vault deposit requirements. + * - Emits the `SymbioticPushed` event. + */ + function pushIntoSymbiotic() + external + returns (uint256 collateralWithdrawal, uint256 collateralDeposit, uint256 vaultDeposit); + + /** + * @notice Pushes rewards to the Farm and Curator of the vault for a specified farm ID. + * @param farmId The ID of the farm. + * @param symbioticRewardsData The data specific to the Symbiotic Vault's `claimRewards()` method. + * + * @custom:effects + * - Transfers a portion of the Symbiotic Vault's reward token to the Curator as a fee. + * - The remaining rewards are pushed to the Farm. + * - Emits the `RewardsPushed` event. + */ + function pushRewards(uint256 farmId, bytes calldata symbioticRewardsData) external; + + /** + * @notice Returns the full balance details for a specific account. + * @param account The address of the account. + * @return accountAssets The total amount of assets belonging to the account. + * @return accountInstantAssets The amount of assets that can be withdrawn instantly. + * @return accountShares The total amount of shares belonging to the account. + * @return accountInstantShares The amount of shares that can be withdrawn instantly. + */ + function getBalances(address account) + external + view + returns ( + uint256 accountAssets, + uint256 accountInstantAssets, + uint256 accountShares, + uint256 accountInstantShares + ); + + /** + * @notice Emitted when rewards are pushed to the Farm and Curator treasury. + * @param farmId The ID of the farm. + * @param rewardAmount The amount of rewards pushed. + * @param curatorFee The fee taken by the curator. + * @param timestamp The time at which the rewards were pushed. + */ + event RewardsPushed( + uint256 indexed farmId, uint256 rewardAmount, uint256 curatorFee, uint256 timestamp + ); + + /** + * @notice Emitted when assets are pushed from the vault into the Symbiotic Vault. + * @param sender The address that initiated the push. + * @param vaultAmount The amount of assets pushed to the Symbiotic Vault. + * @param collateralDeposit The amount of collateral deposited. + * @param collateralWithdrawal The amount of collateral withdrawn. + */ + event SymbioticPushed( + address sender, uint256 collateralWithdrawal, uint256 collateralDeposit, uint256 vaultAmount + ); +} \ No newline at end of file diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IIBaseRestaker.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IIBaseRestaker.sol deleted file mode 100644 index a56c2122..00000000 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IIBaseRestaker.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -interface IIBaseRestaker { - /************************************ - ************** Errors ************** - ************************************/ - - error ValueZero(); - - error TransferAssetFailed(address assetAddress); - - error InconsistentData(); - - error WrongClaimWithdrawalParams(); - - error NullParams(); - - error NotVaultOrTrusteeManager(); - - error LengthMismatch(); - - error InvalidVault(); - - error ZeroAddress(); - - error AlreadyAdded(); - - error NotContract(); - - /************************************ - ************** Events ************** - ************************************/ - - event VaultSet(address indexed oldVault, address indexed newVault); - - event TrusteeManagerSet( - address indexed _trusteeManager, - address indexed _newTrusteeManager - ); - - function pendingWithdrawalAmount() external view returns (uint256); - - function getDeposited(address vaultAddress) external view returns (uint256); - - function getTotalDeposited() external view returns (uint256); - - function claimableAmount() external view returns (uint256); -} diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IIMellowRestaker.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IIMellowRestaker.sol deleted file mode 100644 index 37def569..00000000 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IIMellowRestaker.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IMellowVault} from "../mellow-core/IMellowVault.sol"; -import {IIBaseRestaker} from "./IIBaseRestaker.sol"; - -interface IIMellowRestaker is IIBaseRestaker { - error InactiveWrapper(); - error NoWrapperExists(); - error BadMellowWithdrawRequest(); - error InvalidWrapperForVault(); - error InvalidAllocation(); - error TooMuchSlippage(); - error AlreadyDeactivated(); - - event AllocationChanged( - address mellowVault, - uint256 oldAllocation, - uint256 newAllocation - ); - - event RequestDealineSet( - uint256 indexed oldDeadline, - uint256 indexed newDealine - ); - - event NewSlippages(uint256 _deposit, uint256 _withdraw); - - event WrappedSet(address indexed _wrapped, address indexed _newWrapped); - - event VaultAdded( - address indexed _mellowVault, - address indexed _depositWrapper - ); - - event WrapperChanged( - address indexed _mellowVault, - address indexed _oldWrapper, - address indexed _newWrapper - ); - - event DeactivatedMellowVault(address indexed _mellowVault); - - function delegateMellow( - uint256 amount, - uint256 deadline, - address mellowVault - ) external returns (uint256 lpAmount); - - function delegate(uint256 amount, uint256 deadline) - external - returns (uint256 tokenAmount, uint256 lpAmount); - - function withdrawMellow( - address mellowVault, - uint256 minLpAmount, - uint256 deadline, - bool closePrevious - ) external returns (uint256); - - // function withdrawEmergencyMellow( - // address _mellowVault, - // uint256 _deadline - // ) external returns (uint256); - - function claimMellowWithdrawalCallback() external returns (uint256); - - function pendingMellowRequest(IMellowVault mellowVault) - external - returns (IMellowVault.WithdrawalRequest memory); -} diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IISymbioticRestaker.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IISymbioticRestaker.sol deleted file mode 100644 index 0062f937..00000000 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/restakers/IISymbioticRestaker.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -import {IIBaseRestaker} from "./IIBaseRestaker.sol"; - -interface IISymbioticRestaker is IIBaseRestaker { - error WithdrawalInProgress(); - - error NothingToClaim(); - - event VaultAdded(address indexed vault); - - function delegate(address vaultAddress, uint256 amount) - external - returns (uint256 depositedAmount, uint256 mintedShares); - - function withdraw(address vaultAddress, uint256 amount) - external - returns (uint256); - - function claim(address vaultAddress, uint256 epoch) - external - returns (uint256); -} diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IStakerRewards.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IStakerRewards.sol index 9e38afb2..1ff8edea 100644 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IStakerRewards.sol +++ b/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IStakerRewards.sol @@ -9,7 +9,12 @@ interface IStakerRewards { * @param amount amount of tokens * @param data some used data */ - event DistributeRewards(address indexed network, address indexed token, uint256 amount, bytes data); + event DistributeRewards( + address indexed network, + address indexed token, + uint256 amount, + bytes data + ); /** * @notice Get a version of the staker rewards contract (different versions mean different interfaces). @@ -25,7 +30,11 @@ interface IStakerRewards { * @param data some data to use * @return amount of claimable tokens */ - function claimable(address token, address account, bytes calldata data) external view returns (uint256); + function claimable( + address token, + address account, + bytes calldata data + ) external view returns (uint256); /** * @notice Distribute rewards on behalf of a particular network using a given token. @@ -34,7 +43,12 @@ interface IStakerRewards { * @param amount amount of tokens * @param data some data to use */ - function distributeRewards(address network, address token, uint256 amount, bytes calldata data) external; + function distributeRewards( + address network, + address token, + uint256 amount, + bytes calldata data + ) external; /** * @notice Claim rewards using a given token. @@ -42,5 +56,9 @@ interface IStakerRewards { * @param token address of the token * @param data some data to use */ - function claimRewards(address recipient, address token, bytes calldata data) external; -} \ No newline at end of file + function claimRewards( + address recipient, + address token, + bytes calldata data + ) external; +} diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVault.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVault.sol index 404e59fd..e8208b2a 100644 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVault.sol +++ b/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVault.sol @@ -218,20 +218,19 @@ interface IVault is IVaultStorage { * @param account account to get the withdrawals for * @return withdrawals for the account at the epoch */ - function withdrawalsOf(uint256 epoch, address account) - external - view - returns (uint256); + function withdrawalsOf( + uint256 epoch, + address account + ) external view returns (uint256); /** * @notice Get a total amount of the collateral that can be slashed for a given account. * @param account account to get the slashable collateral for * @return total amount of the account's slashable collateral */ - function slashableBalanceOf(address account) - external - view - returns (uint256); + function slashableBalanceOf( + address account + ) external view returns (uint256); /** * @notice Deposit collateral into the vault. @@ -240,9 +239,10 @@ interface IVault is IVaultStorage { * @return depositedAmount real amount of the collateral deposited * @return mintedShares amount of the active shares minted */ - function deposit(address onBehalfOf, uint256 amount) - external - returns (uint256 depositedAmount, uint256 mintedShares); + function deposit( + address onBehalfOf, + uint256 amount + ) external returns (uint256 depositedAmount, uint256 mintedShares); /** * @notice Withdraw collateral from the vault (it will be claimable after the next epoch). @@ -251,9 +251,10 @@ interface IVault is IVaultStorage { * @return burnedShares amount of the active shares burned * @return mintedShares amount of the epoch withdrawal shares minted */ - function withdraw(address claimer, uint256 amount) - external - returns (uint256 burnedShares, uint256 mintedShares); + function withdraw( + address claimer, + uint256 amount + ) external returns (uint256 burnedShares, uint256 mintedShares); /** * @notice Redeem collateral from the vault (it will be claimable after the next epoch). @@ -262,9 +263,10 @@ interface IVault is IVaultStorage { * @return withdrawnAssets amount of the collateral withdrawn * @return mintedShares amount of the epoch withdrawal shares minted */ - function redeem(address claimer, uint256 shares) - external - returns (uint256 withdrawnAssets, uint256 mintedShares); + function redeem( + address claimer, + uint256 shares + ) external returns (uint256 withdrawnAssets, uint256 mintedShares); /** * @notice Claim collateral from the vault. @@ -272,9 +274,10 @@ interface IVault is IVaultStorage { * @param epoch epoch to claim the collateral for * @return amount amount of the collateral claimed */ - function claim(address recipient, uint256 epoch) - external - returns (uint256 amount); + function claim( + address recipient, + uint256 epoch + ) external returns (uint256 amount); /** * @notice Claim collateral from the vault for multiple epochs. @@ -282,9 +285,10 @@ interface IVault is IVaultStorage { * @param epochs epochs to claim the collateral for * @return amount amount of the collateral claimed */ - function claimBatch(address recipient, uint256[] calldata epochs) - external - returns (uint256 amount); + function claimBatch( + address recipient, + uint256[] calldata epochs + ) external returns (uint256 amount); /** * @notice Slash callback for burning collateral. @@ -293,9 +297,10 @@ interface IVault is IVaultStorage { * @return slashedAmount real amount of the collateral slashed * @dev Only the slasher can call this function. */ - function onSlash(uint256 amount, uint48 captureTimestamp) - external - returns (uint256 slashedAmount); + function onSlash( + uint256 amount, + uint48 captureTimestamp + ) external returns (uint256 slashedAmount); /** * @notice Enable/disable deposit whitelist. diff --git a/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVaultStorage.sol b/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVaultStorage.sol index 7060bd1d..451619b1 100644 --- a/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVaultStorage.sol +++ b/projects/vaults/contracts/interfaces/symbiotic-vault/symbiotic-core/IVaultStorage.sol @@ -133,10 +133,9 @@ interface IVaultStorage { * @param account address to check * @return if the account is whitelisted as a depositor */ - function isDepositorWhitelisted(address account) - external - view - returns (bool); + function isDepositorWhitelisted( + address account + ) external view returns (bool); /** * @notice Get if the deposit limit is set. @@ -156,10 +155,10 @@ interface IVaultStorage { * @param hint hint for the checkpoint index * @return total number of active shares at the timestamp */ - function activeSharesAt(uint48 timestamp, bytes memory hint) - external - view - returns (uint256); + function activeSharesAt( + uint48 timestamp, + bytes memory hint + ) external view returns (uint256); /** * @notice Get a total number of active shares in the vault. @@ -173,10 +172,10 @@ interface IVaultStorage { * @param hint hint for the checkpoint index * @return total amount of active stake at the timestamp */ - function activeStakeAt(uint48 timestamp, bytes memory hint) - external - view - returns (uint256); + function activeStakeAt( + uint48 timestamp, + bytes memory hint + ) external view returns (uint256); /** * @notice Get a total amount of active stake in the vault. @@ -224,10 +223,10 @@ interface IVaultStorage { * @param account account to get the number of withdrawal shares for * @return number of withdrawal shares for the account at the epoch */ - function withdrawalSharesOf(uint256 epoch, address account) - external - view - returns (uint256); + function withdrawalSharesOf( + uint256 epoch, + address account + ) external view returns (uint256); /** * @notice Get if the withdrawals are claimed for a particular account at a given epoch. @@ -235,8 +234,8 @@ interface IVaultStorage { * @param account account to check the withdrawals for * @return if the withdrawals are claimed for the account at the epoch */ - function isWithdrawalsClaimed(uint256 epoch, address account) - external - view - returns (bool); + function isWithdrawalsClaimed( + uint256 epoch, + address account + ) external view returns (bool); } diff --git a/projects/vaults/contracts/lib/InceptionLibrary.sol b/projects/vaults/contracts/lib/InceptionLibrary.sol index 286438af..c04234d0 100644 --- a/projects/vaults/contracts/lib/InceptionLibrary.sol +++ b/projects/vaults/contracts/lib/InceptionLibrary.sol @@ -1,6 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; +error MaxRateUnderflow(); +error TargetCapacityZero(); +error MaxRateUnderflowBySubtractor(); +error AmountGreater(); +error OptimalCapacityZero(); + /// @author The InceptionLRT team /// @title The InceptionLibrary library /// @dev It serves two primary functions: @@ -27,11 +33,15 @@ library InceptionLibrary { if (optimalCapacity < capacity + amount) replenished = optimalCapacity - capacity; + if (optimalBonusRate > maxDepositBonusRate) revert MaxRateUnderflow(); + if (targetCapacity == 0) revert TargetCapacityZero(); + uint256 bonusSlope = ((maxDepositBonusRate - optimalBonusRate) * 1e18) / ((optimalCapacity * 1e18) / targetCapacity); - uint256 bonusPercent = maxDepositBonusRate - - (bonusSlope * (capacity + replenished / 2)) / + uint256 subtractor = (bonusSlope * (capacity + replenished / 2)) / targetCapacity; + if (subtractor > maxDepositBonusRate) revert MaxRateUnderflowBySubtractor(); + uint256 bonusPercent = maxDepositBonusRate - subtractor; capacity += replenished; bonus += (replenished * bonusPercent) / MAX_PERCENT; @@ -68,11 +78,17 @@ library InceptionLibrary { } /// @dev the utilization rate is in the range [25:0] % if (amount > 0) { + if (optimaFeeRate > maxFlashWithdrawalFeeRate) revert MaxRateUnderflow(); + if (targetCapacity == 0) revert TargetCapacityZero(); + if (amount > capacity) revert AmountGreater(); + if (optimalCapacity == 0) revert OptimalCapacityZero(); + uint256 feeSlope = ((maxFlashWithdrawalFeeRate - optimaFeeRate) * 1e18) / ((optimalCapacity * 1e18) / targetCapacity); - uint256 bonusPercent = maxFlashWithdrawalFeeRate - - (feeSlope * (capacity - amount / 2)) / + uint256 subtractor = (feeSlope * (capacity - amount / 2)) / targetCapacity; + if (subtractor > maxFlashWithdrawalFeeRate) revert MaxRateUnderflowBySubtractor(); + uint256 bonusPercent = maxFlashWithdrawalFeeRate - subtractor; fee += (amount * bonusPercent) / MAX_PERCENT; if (fee == 0) ++fee; } diff --git a/projects/vaults/contracts/restakers/IBaseRestaker.sol b/projects/vaults/contracts/restakers/IBaseRestaker.sol deleted file mode 100644 index f2e7d833..00000000 --- a/projects/vaults/contracts/restakers/IBaseRestaker.sol +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {IIBaseRestaker} from "../interfaces/symbiotic-vault/restakers/IIBaseRestaker.sol"; - -abstract contract IBaseRestaker is - PausableUpgradeable, - ReentrancyGuardUpgradeable, - ERC165Upgradeable, - OwnableUpgradeable, - IIBaseRestaker -{ - using SafeERC20 for IERC20; - - IERC20 internal _asset; - address internal _trusteeManager; - address internal _inceptionVault; - - modifier onlyTrustee() { - require( - msg.sender == _inceptionVault || msg.sender == _trusteeManager, - NotVaultOrTrusteeManager() - ); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() payable { - _disableInitializers(); - } - - function __IBaseRestaker_init(IERC20 asset, address trusteeManager) - public - initializer - { - __Pausable_init(); - __ReentrancyGuard_init(); - __Ownable_init(); - __ERC165_init(); - - _asset = asset; - _trusteeManager = trusteeManager; - } - - function delegate( - uint256 amount, - address vault, - bytes calldata _data - ) external virtual returns (uint256 depositedAmount); - - function withdraw( - address vault, - uint256 amount, - bytes calldata _data - ) external virtual returns (uint256); - - function claimableAmount() external view returns (uint256) { - return _asset.balanceOf(address(this)); - } - - function claim() external virtual returns (uint256); - - function pendingWithdrawalAmount() - external - view - virtual - returns (uint256 total); - - function getDeposited(address vaultAddress) - public - view - virtual - returns (uint256); - - function getTotalDeposited() public view virtual returns (uint256); - - function setInceptionVault(address inceptionVault) external onlyOwner { - emit VaultSet(_inceptionVault, inceptionVault); - _inceptionVault = inceptionVault; - } - - function setTrusteeManager(address _newTrusteeManager) external onlyOwner { - emit TrusteeManagerSet(_trusteeManager, _newTrusteeManager); - _trusteeManager = _newTrusteeManager; - } - - function pause() external onlyOwner { - _pause(); - } - - function unpause() external onlyOwner { - _unpause(); - } - - function getVersion() external pure returns (uint256) { - return 1; - } -} diff --git a/projects/vaults/contracts/restakers/IMellowRestaker.sol b/projects/vaults/contracts/restakers/IMellowRestaker.sol deleted file mode 100644 index e94a3094..00000000 --- a/projects/vaults/contracts/restakers/IMellowRestaker.sol +++ /dev/null @@ -1,479 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {IMellowPriceOracle} from "../interfaces/symbiotic-vault/mellow-core/IMellowPriceOracle.sol"; -import {IMellowRatiosOracle} from "../interfaces/symbiotic-vault/mellow-core/IMellowRatiosOracle.sol"; - -import {IIMellowRestaker} from "../interfaces/symbiotic-vault/restakers/IIMellowRestaker.sol"; -import {IMellowDepositWrapper} from "../interfaces/symbiotic-vault/mellow-core/IMellowDepositWrapper.sol"; -import {IMellowHandler} from "../interfaces/symbiotic-vault/mellow-core/IMellowHandler.sol"; -import {IMellowVault} from "../interfaces/symbiotic-vault/mellow-core/IMellowVault.sol"; -import {IDefaultCollateral} from "../interfaces/symbiotic-vault/mellow-core/IMellowDefaultCollateral.sol"; -import {FullMath} from "../lib/FullMath.sol"; - -import {IMellowPriceOracle} from "../interfaces/symbiotic-vault/mellow-core/IMellowPriceOracle.sol"; -import {IMellowRatiosOracle} from "../interfaces/symbiotic-vault/mellow-core/IMellowRatiosOracle.sol"; - -/** - * @title The MellowRestaker Contract - * @author The InceptionLRT team - * @dev Handles delegation and withdrawal requests within the Mellow protocol. - * @notice Can only be executed by InceptionVault/InceptionOperator or the owner. - */ -contract IMellowRestaker is - PausableUpgradeable, - ReentrancyGuardUpgradeable, - ERC165Upgradeable, - OwnableUpgradeable, - IIMellowRestaker -{ - using SafeERC20 for IERC20; - - IERC20 internal _asset; - address internal _trusteeManager; - address internal _vault; - - // If mellowDepositWrapper exists, then mellowVault is active - mapping(address => IMellowDepositWrapper) public mellowDepositWrappers; // mellowVault => mellowDepositWrapper - IMellowVault[] public mellowVaults; - - mapping(address => uint256) public allocations; - uint256 public totalAllocations; - - uint256 public requestDeadline; - - uint256 public depositSlippage; // BasisPoints 10,000 = 100% - uint256 public withdrawSlippage; - - modifier onlyTrustee() { - if (msg.sender != _vault && msg.sender != _trusteeManager) - revert NotVaultOrTrusteeManager(); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() payable { - _disableInitializers(); - } - - function initialize( - IMellowDepositWrapper[] memory _mellowDepositWrapper, - IMellowVault[] memory _mellowVault, - IERC20 asset, - address trusteeManager - ) public initializer { - __Pausable_init(); - __ReentrancyGuard_init(); - __Ownable_init(); - __ERC165_init(); - - if (_mellowDepositWrapper.length != _mellowVault.length) - revert LengthMismatch(); - - for (uint256 i = 0; i < _mellowDepositWrapper.length; i++) { - if ( - address(_mellowDepositWrapper[i].vault()) != - address(_mellowVault[i]) - ) revert InvalidWrapperForVault(); - mellowDepositWrappers[ - address(_mellowVault[i]) - ] = IMellowDepositWrapper(_mellowDepositWrapper[i]); - mellowVaults.push(_mellowVault[i]); - } - _asset = asset; - _trusteeManager = trusteeManager; - - requestDeadline = 90 days; - depositSlippage = 1500; // 15% - withdrawSlippage = 10; - } - - function delegateMellow( - uint256 amount, - uint256 deadline, - address mellowVault - ) external onlyTrustee whenNotPaused returns (uint256 lpAmount) { - IMellowDepositWrapper wrapper = mellowDepositWrappers[mellowVault]; - if (address(wrapper) == address(0)) revert InactiveWrapper(); - uint256 balanceState = _asset.balanceOf(address(this)); - // transfer from the vault - _asset.safeTransferFrom(_vault, address(this), amount); - // deposit the asset to the appropriate strategy - IERC20(_asset).safeIncreaseAllowance(address(wrapper), amount); - uint256 minAmount = amountToLpAmount(amount, IMellowVault(mellowVault)); - minAmount = (minAmount * (10000 - depositSlippage)) / 10000; - lpAmount = - wrapper.deposit( - address(this), - address(_asset), - amount, - minAmount, - block.timestamp + deadline - ); - uint256 returned = _asset.balanceOf(address(this)) - balanceState; - if (returned != 0) IERC20(_asset).safeTransfer(_vault, returned); - } - - function delegate(uint256 amount, uint256 deadline) - external - onlyTrustee - whenNotPaused - returns (uint256 tokenAmount, uint256 lpAmount) - { - uint256 allocationsTotal = totalAllocations; - uint256 balanceState = _asset.balanceOf(address(this)); - _asset.safeTransferFrom(_vault, address(this), amount); - - for (uint8 i = 0; i < mellowVaults.length; i++) { - uint256 allocation = allocations[address(mellowVaults[i])]; - if (allocation > 0) { - uint256 localBalance = (amount * allocation) / allocationsTotal; - IMellowDepositWrapper wrapper = mellowDepositWrappers[ - address(mellowVaults[i]) - ]; - if (address(wrapper) == address(0)) continue; - IERC20(_asset).safeIncreaseAllowance( - address(wrapper), - localBalance - ); - uint256 minAmount = amountToLpAmount(localBalance, mellowVaults[i]); - minAmount = (minAmount * (10000 - depositSlippage)) / 10000; - lpAmount += wrapper.deposit( - address(this), - address(_asset), - localBalance, - minAmount, - block.timestamp + deadline - ); - } - } - uint256 returned = _asset.balanceOf(address(this)) - balanceState; - tokenAmount = amount - returned; - if (returned != 0) IERC20(_asset).safeTransfer(_vault, returned); - } - - function withdrawMellow( - address _mellowVault, - uint256 amount, - uint256 deadline, - bool closePrevious - ) external override onlyTrustee whenNotPaused returns (uint256) { - if (address(mellowDepositWrappers[_mellowVault]) == address(0)) - revert InvalidVault(); - IMellowVault mellowVault = IMellowVault(_mellowVault); - uint256 lpAmount = amountToLpAmount(amount, mellowVault); - uint256[] memory minAmounts = new uint256[](1); - minAmounts[0] = (amount * (10000 - withdrawSlippage)) / 10000; // slippage - - mellowVault.registerWithdrawal( - address(this), - lpAmount, - minAmounts, - block.timestamp + deadline, - block.timestamp + requestDeadline, - closePrevious - ); - - ( - bool isProcessingPossible, - , - uint256[] memory expectedAmounts - ) = mellowVault.analyzeRequest( - mellowVault.calculateStack(), - mellowVault.withdrawalRequest(address(this)) - ); - - if (!isProcessingPossible) revert BadMellowWithdrawRequest(); - return expectedAmounts[0]; - } - - // function withdrawEmergencyMellow( - // address _mellowVault, - // uint256 _deadline - // ) external override onlyTrustee whenNotPaused returns (uint256) { - // IMellowVault mellowVault = IMellowVault(_mellowVault); - // address[] memory tokens; - // uint256[] memory baseTvlAmounts; - // (tokens, baseTvlAmounts) = mellowVault.baseTvl(); - // uint256 totalSupply = IERC20(_mellowVault).totalSupply(); - - // uint256[] memory minAmounts = new uint256[](baseTvlAmounts.length); - // for (uint256 i = 0; i < baseTvlAmounts.length; i++) { - // minAmounts[i] = (baseTvlAmounts[i] * pendingMellowRequest(IMellowVault(_mellowVault)).lpAmount / totalSupply) - 1 gwei; - // } - - // if (address(mellowDepositWrappers[_mellowVault]) == address(0)) revert InvalidVault(); - - // uint256[] memory actualAmounts = mellowVault.emergencyWithdraw(minAmounts, block.timestamp + _deadline); - - // if (actualAmounts[1] > 0) { - // IDefaultCollateral(tokens[1]).withdraw(address(this), IERC20(tokens[1]).balanceOf(address(this))); - // } - - // return _asset.balanceOf(address(this)); - // } - - function claimableAmount() external view returns (uint256) { - return _asset.balanceOf(address(this)); - } - - function claimMellowWithdrawalCallback() - external - onlyTrustee - returns (uint256) - { - uint256 amount = _asset.balanceOf(address(this)); - if (amount == 0) revert ValueZero(); - - _asset.safeTransfer(_vault, amount); - - return amount; - } - - function addMellowVault(address mellowVault, address depositWrapper) - external - onlyOwner - { - if (mellowVault == address(0) || depositWrapper == address(0)) - revert ZeroAddress(); - if ( - address(IMellowDepositWrapper(depositWrapper).vault()) != - mellowVault - ) revert InvalidWrapperForVault(); - - for (uint8 i = 0; i < mellowVaults.length; i++) { - if (mellowVault == address(mellowVaults[i])) { - revert AlreadyAdded(); - } - } - - mellowDepositWrappers[mellowVault] = IMellowDepositWrapper( - depositWrapper - ); - mellowVaults.push(IMellowVault(mellowVault)); - - emit VaultAdded(mellowVault, depositWrapper); - } - - function changeMellowWrapper(address mellowVault, address newDepositWrapper) - external - onlyOwner - { - if (mellowVault == address(0) || newDepositWrapper == address(0)) - revert ZeroAddress(); - if ( - address(IMellowDepositWrapper(newDepositWrapper).vault()) != - mellowVault - ) revert InvalidWrapperForVault(); - - address oldWrapper = address(mellowDepositWrappers[mellowVault]); - if (oldWrapper == address(0)) revert NoWrapperExists(); - - mellowDepositWrappers[mellowVault] = IMellowDepositWrapper( - newDepositWrapper - ); - - emit WrapperChanged(mellowVault, oldWrapper, newDepositWrapper); - } - - function deactivateMellowVault(address mellowVault) external onlyOwner { - if (address(mellowDepositWrappers[mellowVault]) == address(0)) revert AlreadyDeactivated(); - mellowDepositWrappers[mellowVault] = IMellowDepositWrapper(address(0)); - emit DeactivatedMellowVault(mellowVault); - } - - function changeAllocation(address mellowVault, uint256 newAllocation) - external - onlyOwner - { - if (mellowVault == address(0)) revert ZeroAddress(); - - bool exists; - for (uint8 i = 0; i < mellowVaults.length; i++) { - if (mellowVault == address(mellowVaults[i])) { - exists = true; - } - } - if (!exists) revert InvalidVault(); - uint256 oldAllocation = allocations[mellowVault]; - allocations[mellowVault] = newAllocation; - - totalAllocations = totalAllocations + newAllocation - oldAllocation; - - emit AllocationChanged(mellowVault, oldAllocation, newAllocation); - } - - function pendingMellowRequest(IMellowVault mellowVault) - public - view - override - returns (IMellowVault.WithdrawalRequest memory) - { - return mellowVault.withdrawalRequest(address(this)); - } - - function pendingWithdrawalAmount() external view returns (uint256) { - uint256 total; - for (uint256 i = 0; i < mellowVaults.length; i++) { - IMellowVault.WithdrawalRequest memory request = mellowVaults[i] - .withdrawalRequest(address(this)); - total += lpAmountToAmount(request.lpAmount, mellowVaults[i]); - } - - return total; - } - - function getDeposited(address _mellowVault) public view returns (uint256) { - IMellowVault mellowVault = IMellowVault(_mellowVault); - uint256 balance = mellowVault.balanceOf(address(this)); - if (balance == 0) { - return 0; - } - return lpAmountToAmount(balance, mellowVault); - } - - function getTotalDeposited() public view returns (uint256) { - uint256 total; - for (uint256 i = 0; i < mellowVaults.length; i++) { - uint256 balance = mellowVaults[i].balanceOf(address(this)); - if (balance > 0) { - total += lpAmountToAmount(balance, mellowVaults[i]); - } - } - return total; - } - - function getVersion() external pure returns (uint256) { - return 1; - } - - function amountToLpAmount(uint256 amount, IMellowVault mellowVault) - public - view - returns (uint256 lpAmount) - { - - if (amount == 0) return 0; - - (address[] memory tokens, uint256[] memory totalAmounts) = mellowVault - .underlyingTvl(); - - uint256[] memory amounts = new uint256[](tokens.length); - amounts[0] = amount; - - uint128[] memory ratiosX96 = IMellowRatiosOracle( - mellowVault.configurator().ratiosOracle() - ).getTargetRatiosX96(address(mellowVault), false); - - uint256 ratioX96 = type(uint256).max; - for (uint256 i = 0; i < tokens.length; i++) { - if (ratiosX96[i] == 0) continue; - uint256 ratioX96_ = FullMath.mulDiv( - amounts[i], - mellowVault.Q96(), - ratiosX96[i] - ); - if (ratioX96_ < ratioX96) ratioX96 = ratioX96_; - } - if (ratioX96 == 0) revert ValueZero(); - - uint256 depositValue = 0; - uint256 totalValue = 0; - { - IMellowPriceOracle priceOracle = IMellowPriceOracle( - mellowVault.configurator().priceOracle() - ); - for (uint256 i = 0; i < tokens.length; i++) { - uint256 priceX96 = priceOracle.priceX96( - address(mellowVault), - tokens[i] - ); - totalValue += totalAmounts[i] == 0 - ? 0 - : FullMath.mulDivRoundingUp( - totalAmounts[i], - priceX96, - mellowVault.Q96() - ); - - if (ratiosX96[i] == 0) continue; - - amount = FullMath.mulDiv( - ratioX96, - ratiosX96[i], - mellowVault.Q96() - ); - depositValue += FullMath.mulDiv( - amount, - priceX96, - mellowVault.Q96() - ); - } - } - - uint256 totalSupply = mellowVault.totalSupply(); - lpAmount = FullMath.mulDiv(depositValue, totalSupply, totalValue); - } - - function lpAmountToAmount(uint256 lpAmount, IMellowVault mellowVault) - public - view - returns (uint256) - { - if (lpAmount == 0) return 0; - - IMellowVault.ProcessWithdrawalsStack memory s = mellowVault - .calculateStack(); - uint256 wstEthAmount = FullMath.mulDiv( - FullMath.mulDiv( - FullMath.mulDiv(lpAmount, s.totalValue, s.totalSupply), - mellowVault.D9() - s.feeD9, - mellowVault.D9() - ), - s.ratiosX96[0], - s.ratiosX96Value - ); - return wstEthAmount; - } - - function setVault(address vault) external onlyOwner { - emit VaultSet(_vault, vault); - _vault = vault; - } - - function setRequestDeadline(uint256 _days) external onlyOwner { - uint256 newDealine = _days * 1 days; - emit RequestDealineSet(requestDeadline, newDealine); - requestDeadline = newDealine; - } - - function setSlippages(uint256 _depositSlippage, uint256 _withdrawSlippage) - external - onlyOwner - { - if (_depositSlippage > 3000 || _withdrawSlippage > 3000) - revert TooMuchSlippage(); - depositSlippage = _depositSlippage; - withdrawSlippage = _withdrawSlippage; - emit NewSlippages(_depositSlippage, _withdrawSlippage); - } - - function setTrusteeManager(address _newTrusteeManager) external onlyOwner { - emit TrusteeManagerSet(_trusteeManager, _newTrusteeManager); - _trusteeManager = _newTrusteeManager; - } - - function pause() external onlyOwner { - _pause(); - } - - function unpause() external onlyOwner { - _unpause(); - } -} diff --git a/projects/vaults/contracts/restakers/ISymbioticRestaker.sol b/projects/vaults/contracts/restakers/ISymbioticRestaker.sol deleted file mode 100644 index 4fe3188f..00000000 --- a/projects/vaults/contracts/restakers/ISymbioticRestaker.sol +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {Address} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {IISymbioticRestaker} from "../interfaces/symbiotic-vault/restakers/IISymbioticRestaker.sol"; -import {IVault} from "../interfaces/symbiotic-vault/symbiotic-core/IVault.sol"; -import {IStakerRewards} from "../interfaces/symbiotic-vault/symbiotic-core/IStakerRewards.sol"; - -/** - * @title The ISymbioticRestaker Contract - * @author The InceptionLRT team - * @dev Handles delegation and withdrawal requests within the SymbioticFi Protocol. - * @notice Can only be executed by InceptionVault/InceptionOperator or the owner. - */ -contract ISymbioticRestaker is - PausableUpgradeable, - ReentrancyGuardUpgradeable, - ERC165Upgradeable, - OwnableUpgradeable, - IISymbioticRestaker -{ - using SafeERC20 for IERC20; - using EnumerableSet for EnumerableSet.AddressSet; - - IERC20 internal _asset; - address internal _trusteeManager; - address internal _vault; - - EnumerableSet.AddressSet internal _vaults; - - /// @dev symbioticVault => withdrawal epoch - mapping(address => uint256) public withdrawals; - - // /// @dev Symbiotic DefaultStakerRewards.sol - // IStakerRewards public stakerRewards; - - modifier onlyTrustee() { - require( - msg.sender == _vault || msg.sender == _trusteeManager, - NotVaultOrTrusteeManager() - ); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() payable { - _disableInitializers(); - } - - function initialize( - address[] memory vaults, - IERC20 asset, - address trusteeManager - ) public initializer { - __Pausable_init(); - __ReentrancyGuard_init(); - __Ownable_init(); - __ERC165_init(); - - for (uint256 i = 0; i < vaults.length; i++) { - _vaults.add(vaults[i]); - emit VaultAdded(vaults[i]); - } - - _asset = asset; - - _trusteeManager = trusteeManager; - } - - function delegate(address vaultAddress, uint256 amount) - external - onlyTrustee - whenNotPaused - returns (uint256 depositedAmount, uint256 mintedShares) - { - require(_vaults.contains(vaultAddress), InvalidVault()); - _asset.safeTransferFrom(_vault, address(this), amount); - IERC20(_asset).safeIncreaseAllowance(vaultAddress, amount); - return IVault(vaultAddress).deposit(address(this), amount); - } - - function withdraw(address vaultAddress, uint256 amount) - external - onlyTrustee - whenNotPaused - returns (uint256) - { - require(_vaults.contains(vaultAddress), InvalidVault()); - require(withdrawals[vaultAddress] == 0, WithdrawalInProgress()); - - IVault vault = IVault(vaultAddress); - (, uint256 mintedShares) = vault.withdraw(address(this), amount); - withdrawals[vaultAddress] = vault.currentEpoch() + 1; - - return mintedShares; - } - - function claim(address vaultAddress, uint256 sEpoch) - external - onlyTrustee - whenNotPaused - returns (uint256) - { - require(_vaults.contains(vaultAddress), InvalidVault()); - require(withdrawals[vaultAddress] != 0, NothingToClaim()); - - delete withdrawals[vaultAddress]; - return IVault(vaultAddress).claim(_vault, sEpoch); - } - - // /// TODO - // function pendingRewards() external view returns (uint256) { - // return stakerRewards.claimable(address(_asset), address(this), ""); - // } - - /** - * @notice Checks whether a vault is supported by the Protocol or not. - * @param vaultAddress vault address to check - */ - function isVaultSupported(address vaultAddress) - external - view - returns (bool) - { - return _vaults.contains(vaultAddress); - } - - function getDeposited(address vaultAddress) public view returns (uint256) { - return IVault(vaultAddress).activeBalanceOf(address(this)); - } - - function getTotalDeposited() public view returns (uint256 total) { - for (uint256 i = 0; i < _vaults.length(); i++) - total += IVault(_vaults.at(i)).activeBalanceOf(address(this)); - - return total; - } - - function pendingWithdrawalAmount() external view returns (uint256 total) { - for (uint256 i = 0; i < _vaults.length(); i++) - if (withdrawals[_vaults.at(i)] != 0) - total += IVault(_vaults.at(i)).withdrawalsOf( - withdrawals[_vaults.at(i)], - address(this) - ); - - return total; - } - - function claimableAmount() external pure returns (uint256) { - return 0; - } - - function addVault(address vaultAddress) external onlyOwner { - if (vaultAddress == address(0)) revert ZeroAddress(); - if (!Address.isContract(vaultAddress)) revert NotContract(); - - if (_vaults.contains(vaultAddress)) revert AlreadyAdded(); - - _vaults.add(vaultAddress); - - emit VaultAdded(vaultAddress); - } - - function setVault(address iVault) external onlyOwner { - if (iVault == address(0)) revert ZeroAddress(); - if (!Address.isContract(iVault)) revert NotContract(); - emit VaultSet(_vault, iVault); - _vault = iVault; - } - - function setTrusteeManager(address _newTrusteeManager) external onlyOwner { - if (_newTrusteeManager == address(0)) revert ZeroAddress(); - emit TrusteeManagerSet(_trusteeManager, _newTrusteeManager); - _trusteeManager = _newTrusteeManager; - } - - function pause() external onlyOwner { - _pause(); - } - - function unpause() external onlyOwner { - _unpause(); - } - - function getVersion() external pure returns (uint256) { - return 1; - } -} diff --git a/projects/vaults/contracts/restakers/InceptionEigenRestaker.sol b/projects/vaults/contracts/restakers/InceptionEigenRestaker.sol deleted file mode 100644 index 9071bd04..00000000 --- a/projects/vaults/contracts/restakers/InceptionEigenRestaker.sol +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; -import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {IInceptionEigenRestaker, IInceptionEigenRestakerErrors} from "../interfaces/eigenlayer-vault/IInceptionEigenRestaker.sol"; -import {IDelegationManager} from "../interfaces/eigenlayer-vault/eigen-core/IDelegationManager.sol"; -import {IStrategy} from "../interfaces/eigenlayer-vault/eigen-core/IStrategy.sol"; -import {IStrategyManager} from "../interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol"; -import {IRewardsCoordinator} from "../interfaces/eigenlayer-vault/eigen-core/IRewardsCoordinator.sol"; - -/** - * @title The InceptionEigenRestaker Contract - * @author The InceptionLRT team - * @dev Handles delegation and withdrawal requests within the EigenLayer protocol. - * @notice Can only be executed by InceptionVault/InceptionOperator or the owner. - */ -contract InceptionEigenRestaker is - PausableUpgradeable, - ReentrancyGuardUpgradeable, - ERC165Upgradeable, - OwnableUpgradeable, - IInceptionEigenRestaker, - IInceptionEigenRestakerErrors -{ - using SafeERC20 for IERC20; - - IERC20 internal _asset; - address internal _trusteeManager; - address internal _vault; - - IStrategy internal _strategy; - IStrategyManager internal _strategyManager; - IDelegationManager internal _delegationManager; - IRewardsCoordinator public rewardsCoordinator; - - modifier onlyTrustee() { - if (msg.sender != _vault && msg.sender != _trusteeManager) - revert OnlyTrusteeAllowed(); - - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() payable { - _disableInitializers(); - } - - function initialize( - address ownerAddress, - address rewardCoordinator, - address delegationManager, - address strategyManager, - address strategy, - address asset, - address trusteeManager - ) public initializer { - __Pausable_init(); - __ReentrancyGuard_init(); - __Ownable_init(); - // Ensure compatibility with future versions of ERC165Upgradeable - __ERC165_init(); - - _delegationManager = IDelegationManager(delegationManager); - _strategyManager = IStrategyManager(strategyManager); - _strategy = IStrategy(strategy); - _asset = IERC20(asset); - _trusteeManager = trusteeManager; - _vault = msg.sender; - _setRewardsCoordinator(rewardCoordinator, ownerAddress); - - // approve spending by strategyManager - _asset.approve(strategyManager, type(uint256).max); - } - - function depositAssetIntoStrategy(uint256 amount) external onlyTrustee { - // transfer from the vault - _asset.safeTransferFrom(_vault, address(this), amount); - // deposit the asset to the appropriate strategy - _strategyManager.depositIntoStrategy(_strategy, _asset, amount); - } - - function delegateToOperator( - address operator, - bytes32 approverSalt, - IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry - ) external onlyTrustee { - if (operator == address(0)) revert NullParams(); - - _delegationManager.delegateTo( - operator, - approverSignatureAndExpiry, - approverSalt - ); - } - - function withdrawFromEL(uint256 shares) external onlyTrustee { - uint256[] memory sharesToWithdraw = new uint256[](1); - IStrategy[] memory strategies = new IStrategy[](1); - - strategies[0] = _strategy; - sharesToWithdraw[0] = shares; - - IDelegationManager.QueuedWithdrawalParams[] - memory withdrawals = new IDelegationManager.QueuedWithdrawalParams[]( - 1 - ); - withdrawals[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategies, - shares: sharesToWithdraw, - withdrawer: address(this) - }); - - _delegationManager.queueWithdrawals(withdrawals); - } - - function claimWithdrawals( - IDelegationManager.Withdrawal[] calldata withdrawals, - IERC20[][] calldata tokens, - uint256[] calldata middlewareTimesIndexes, - bool[] calldata receiveAsTokens - ) external onlyTrustee returns (uint256) { - uint256 balanceBefore = _asset.balanceOf(address(this)); - - _delegationManager.completeQueuedWithdrawals( - withdrawals, - tokens, - middlewareTimesIndexes, - receiveAsTokens - ); - - // send tokens to the vault - uint256 withdrawnAmount = _asset.balanceOf(address(this)) - - balanceBefore; - - _asset.safeTransfer(_vault, withdrawnAmount); - - return withdrawnAmount; - } - - function getOperatorAddress() public view returns (address) { - return _delegationManager.delegatedTo(address(this)); - } - - function getVersion() external pure returns (uint256) { - return 2; - } - - function setRewardsCoordinator(address newRewardsCoordinator) - external - onlyOwner - { - _setRewardsCoordinator(newRewardsCoordinator, owner()); - } - - function _setRewardsCoordinator( - address newRewardsCoordinator, - address ownerAddress - ) internal { - IRewardsCoordinator(newRewardsCoordinator).setClaimerFor(ownerAddress); - - emit RewardCoordinatorChanged( - address(rewardsCoordinator), - newRewardsCoordinator - ); - - rewardsCoordinator = IRewardsCoordinator(newRewardsCoordinator); - } - - function pause() external onlyOwner { - _pause(); - } - - function unpause() external onlyOwner { - _unpause(); - } -} diff --git a/projects/vaults/contracts/symbiotic-handler/SymbioticHandler.sol b/projects/vaults/contracts/symbiotic-handler/SymbioticHandler.sol deleted file mode 100644 index a154e2c1..00000000 --- a/projects/vaults/contracts/symbiotic-handler/SymbioticHandler.sol +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {InceptionAssetsHandler, IERC20} from "../assets-handler/InceptionAssetsHandler.sol"; -import {ISymbioticHandler} from "../interfaces/symbiotic-vault/ISymbioticHandler.sol"; -import {IIMellowRestaker} from "../interfaces/symbiotic-vault/restakers/IIMellowRestaker.sol"; -import {IISymbioticRestaker} from "../interfaces/symbiotic-vault/restakers/IISymbioticRestaker.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Address} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; - -/** - * @title The SymbioticHandler contract - * @author The InceptionLRT team - * @dev Serves communication with external Mellow Protocol - * @dev Specifically, this includes depositing, and handling withdrawal requests - */ -contract SymbioticHandler is InceptionAssetsHandler, ISymbioticHandler { - using SafeERC20 for IERC20; - - uint256 public epoch; - - /// @dev inception operator - address internal _operator; - - IIMellowRestaker public mellowRestaker; - - /// @dev represents the pending amount to be redeemed by claimers, - /// @notice + amount to undelegate from Mellow - uint256 public totalAmountToWithdraw; - - Withdrawal[] public claimerWithdrawalsQueue; - - /// @dev heap reserved for the claimers - uint256 public redeemReservedAmount; - - uint256 public depositBonusAmount; - - /// @dev measured in percentage, MAX_TARGET_PERCENT - 100% - uint256 public targetCapacity; - - uint256 public constant MAX_TARGET_PERCENT = 100 * 1e18; - - IISymbioticRestaker public symbioticRestaker; - - /// TODO - uint256[50 - 9] private __gap; - - modifier onlyOperator() { - require(msg.sender == _operator, OnlyOperatorAllowed()); - _; - } - - function __SymbioticHandler_init( - IERC20 assetAddress, - IIMellowRestaker _mellowRestaker - ) internal onlyInitializing { - mellowRestaker = _mellowRestaker; - - __InceptionAssetsHandler_init(assetAddress); - } - - /*////////////////////////////// - ////// Deposit functions ////// - ////////////////////////////*/ - - function _beforeDeposit(uint256 amount) internal view { - uint256 freeBalance = getFreeBalance(); - if (amount > freeBalance) revert InsufficientCapacity(freeBalance); - } - - function _depositAssetIntoMellow( - uint256 amount, - address mellowVault, - uint256 deadline - ) internal { - _asset.safeIncreaseAllowance(address(mellowRestaker), amount); - mellowRestaker.delegateMellow(amount, deadline, mellowVault); - } - - function _depositAssetIntoSymbiotic(uint256 amount, address vault) - internal - { - _asset.safeIncreaseAllowance(address(symbioticRestaker), amount); - symbioticRestaker.delegate(vault, amount); - } - - /*///////////////////////////////// - ////// Withdrawal functions ////// - ///////////////////////////////*/ - - /// @dev performs creating a withdrawal request from Mellow Protocol - /// @dev requires a specific amount to withdraw - function undelegateFromMellow( - address mellowVault, - uint256 amount, - uint256 deadline - ) external whenNotPaused nonReentrant onlyOperator { - if (mellowVault == address(0)) revert InvalidAddress(); - if (amount == 0) revert ValueZero(); - amount = mellowRestaker.withdrawMellow( - mellowVault, - amount, - deadline, - true - ); - emit StartMellowWithdrawal(address(mellowRestaker), amount); - return; - } - - /// @dev performs creating a withdrawal request from Mellow Protocol - /// @dev requires a specific amount to withdraw - function undelegateFromSymbiotic(address vault, uint256 amount) - external - whenNotPaused - nonReentrant - onlyOperator - { - if (vault == address(0)) revert InvalidAddress(); - if (amount == 0) revert ValueZero(); - amount = symbioticRestaker.withdraw(vault, amount); - - /// TODO - emit StartMellowWithdrawal(address(symbioticRestaker), amount); - return; - } - - /// @dev claims completed withdrawals from Mellow Protocol, if they exist - function claimCompletedWithdrawalsMellow() - public - onlyOperator - whenNotPaused - nonReentrant - { - uint256 availableBalance = getFreeBalance(); - - uint256 withdrawnAmount = mellowRestaker - .claimMellowWithdrawalCallback(); - - emit WithdrawalClaimed(withdrawnAmount); - - _updateEpoch(availableBalance + withdrawnAmount); - } - - function claimCompletedWithdrawalsSymbiotic(address vault, uint256 sEpoch) - public - onlyOperator - whenNotPaused - nonReentrant - { - uint256 availableBalance = getFreeBalance(); - - uint256 withdrawnAmount = symbioticRestaker.claim(vault, sEpoch); - - emit WithdrawalClaimed(withdrawnAmount); - - _updateEpoch(availableBalance + withdrawnAmount); - } - - function updateEpoch() external onlyOperator whenNotPaused { - _updateEpoch(getFreeBalance()); - } - - /** - * @dev let's calculate how many withdrawals we can cover with the withdrawnAmount - * @dev #init state: - * - balance of the vault: X - * - epoch: means that the vault can handle the withdrawal queue up to the epoch index - * withdrawalQueue[... : epoch]; - * - * @dev #new state: - * - balance of the vault: X + withdrawnAmount - * - we need to recalculate a new value for epoch, new_epoch, to cover withdrawals: - * withdrawalQueue[epoch : new_epoch]; - */ - function _updateEpoch(uint256 availableBalance) internal { - uint256 withdrawalsNum = claimerWithdrawalsQueue.length; - uint256 redeemReservedBuffer; - uint256 epochBuffer; - for (uint256 i = epoch; i < withdrawalsNum; ) { - uint256 amount = claimerWithdrawalsQueue[i].amount; - unchecked { - if (amount > availableBalance) { - break; - } - redeemReservedBuffer += amount; - availableBalance -= amount; - ++epochBuffer; - ++i; - } - } - redeemReservedAmount += redeemReservedBuffer; - epoch += epochBuffer; - } - - /*////////////////////////// - ////// GET functions ////// - ////////////////////////*/ - - /// @dev returns the total deposited into asset strategy - function getTotalDeposited() public view returns (uint256) { - return - getTotalDelegated() + - totalAssets() + - symbioticRestaker.pendingWithdrawalAmount() + - getPendingWithdrawalAmountFromMellow() - - depositBonusAmount; - } - - function getTotalDelegated() public view returns (uint256) { - return - mellowRestaker.getTotalDeposited() + - symbioticRestaker.getTotalDeposited(); - } - - function getFreeBalance() public view returns (uint256 total) { - uint256 flashCapacity = getFlashCapacity(); - uint256 targetFlash = _getTargetCapacity(); - return flashCapacity < targetFlash ? 0 : flashCapacity - targetFlash; - } - - /// @dev returns the total amount of pending withdrawals from Mellow LRT - function getPendingWithdrawalAmountFromMellow() - public - view - returns (uint256) - { - uint256 pendingWithdrawal = mellowRestaker.pendingWithdrawalAmount(); - uint256 claimableAmount = mellowRestaker.claimableAmount(); - return pendingWithdrawal + claimableAmount; - } - - function getFlashCapacity() public view returns (uint256 total) { - uint256 _assets = totalAssets(); - uint256 _sum = redeemReservedAmount + depositBonusAmount; - if (_sum > _assets) return 0; - else return _assets - _sum; - } - - function _getTargetCapacity() internal view returns (uint256) { - return (targetCapacity * getTotalDeposited()) / MAX_TARGET_PERCENT; - } - - /*////////////////////////// - ////// SET functions ////// - ////////////////////////*/ - - function setTargetFlashCapacity(uint256 newTargetCapacity) - external - onlyOwner - { - if (newTargetCapacity == 0) revert InvalidTargetFlashCapacity(); - if (newTargetCapacity >= MAX_TARGET_PERCENT) revert MoreThanMax(); - emit TargetCapacityChanged(targetCapacity, newTargetCapacity); - targetCapacity = newTargetCapacity; - } - - function setSymbioticRestaker(address newSymbioticRestaker) - external - onlyOwner - { - require(newSymbioticRestaker != address(0), InvalidAddress()); - require(Address.isContract(newSymbioticRestaker), NotContract()); - - symbioticRestaker = IISymbioticRestaker(newSymbioticRestaker); - emit SymbioticRestakerAdded(newSymbioticRestaker); - } -} diff --git a/projects/vaults/contracts/tests/Ankr/AETHC.sol b/projects/vaults/contracts/tests/Ankr/AETHC.sol deleted file mode 100644 index 3387ae75..00000000 --- a/projects/vaults/contracts/tests/Ankr/AETHC.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; - -contract AETHC is ERC20Upgradeable { - function ratio() external view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/Ankr/StakingPool.sol b/projects/vaults/contracts/tests/Ankr/StakingPool.sol deleted file mode 100644 index ce9b517e..00000000 --- a/projects/vaults/contracts/tests/Ankr/StakingPool.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract AnkrStakingPool { - constructor() payable {} - - function stakeAndClaimAethC() external payable {} -} diff --git a/projects/vaults/contracts/tests/Binance/WBEth.sol b/projects/vaults/contracts/tests/Binance/WBEth.sol deleted file mode 100644 index e15ed918..00000000 --- a/projects/vaults/contracts/tests/Binance/WBEth.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract WBEth is ERC20Upgradeable { - function deposit(address referral) external payable { - // require(msg.value > 0, "zero ETH amount"); - // // msg.value and exchangeRate are all scaled by 1e18 - // uint256 wBETHAmount = msg.value.mul(_EXCHANGE_RATE_UNIT).div(exchangeRate()); - // _mint(msg.sender, wBETHAmount); - // emit DepositEth(msg.sender, msg.value, wBETHAmount, referral); - } - - // function requestWithdrawEth(uint256 wbethAmount) external { - // require(wbethAmount > 0, "zero wBETH amount"); - - // // msg.value and exchangeRate are all scaled by 1e18 - // uint256 ethAmount = wbethAmount.mul(exchangeRate()).div( - // _EXCHANGE_RATE_UNIT // 1e18 - // ); - // _burn(wbethAmount); - // IUnwrapTokenV1(_UNWRAP_ETH_ADDRESS).requestWithdraw( - // msg.sender, - // wbethAmount, - // ethAmount - // ); - // emit RequestWithdrawEth(msg.sender, wbethAmount, ethAmount); - // } - - function exchangeRate() public view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/Coinbase/CbEth.sol b/projects/vaults/contracts/tests/Coinbase/CbEth.sol deleted file mode 100644 index 5bd803c6..00000000 --- a/projects/vaults/contracts/tests/Coinbase/CbEth.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract CbEth is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/Eigen/BackingEigen.sol b/projects/vaults/contracts/tests/Eigen/BackingEigen.sol deleted file mode 100644 index 57eee37f..00000000 --- a/projects/vaults/contracts/tests/Eigen/BackingEigen.sol +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract BackingEigen is OwnableUpgradeable, ERC20Upgradeable { - /// CONSTANTS & IMMUTABLES - /// @notice the address of the wrapped Eigen token EIGEN - IERC20 public immutable EIGEN; - - /// STORAGE - /// @notice the timestamp after which transfer restrictions are disabled - uint256 public transferRestrictionsDisabledAfter; - /// @notice mapping of addresses that are allowed to transfer tokens to any address - mapping(address => bool) public allowedFrom; - /// @notice mapping of addresses that are allowed to receive tokens from any address - mapping(address => bool) public allowedTo; - - /// @notice event emitted when the allowedFrom status of an address is set - event SetAllowedFrom(address indexed from, bool isAllowedFrom); - /// @notice event emitted when the allowedTo status of an address is set - event SetAllowedTo(address indexed to, bool isAllowedTo); - /// @notice event emitted when the transfer restrictions are disabled - event TransferRestrictionsDisabled(); - /// @notice event emitted when the EIGEN token is backed - event Backed(); - - constructor(IERC20 _EIGEN) { - EIGEN = _EIGEN; - _disableInitializers(); - } - - /** - * @notice An initializer function that sets initial values for the contract's state variables. - */ - function initialize(address initialOwner) public initializer { - __Ownable_init(); - __ERC20_init("Backing Eigen", "bEIGEN"); - _transferOwnership(initialOwner); - - // set transfer restrictions to be disabled at type(uint256).max to be set down later - transferRestrictionsDisabledAfter = type(uint256).max; - - // the EIGEN contract should be allowed to transfer tokens to any address for unwrapping - // likewise, anyone should be able to transfer bEIGEN to EIGEN for wrapping - _setAllowedFrom(address(EIGEN), true); - _setAllowedTo(address(EIGEN), true); - - // Mint the entire supply of EIGEN - this is a one-time event that - // ensures bEIGEN fully backs EIGEN. - _mint(address(EIGEN), EIGEN.totalSupply()); - emit Backed(); - } - - /// EXTERNAL FUNCTIONS - - /** - * @notice This function allows the owner to set the allowedFrom status of an address - * @param from the address whose allowedFrom status is being set - * @param isAllowedFrom the new allowedFrom status - */ - function setAllowedFrom( - address from, - bool isAllowedFrom - ) external onlyOwner { - _setAllowedFrom(from, isAllowedFrom); - } - - /** - * @notice This function allows the owner to set the allowedTo status of an address - * @param to the address whose allowedTo status is being set - * @param isAllowedTo the new allowedTo status - */ - function setAllowedTo(address to, bool isAllowedTo) external onlyOwner { - _setAllowedTo(to, isAllowedTo); - } - - /** - * @notice Allows the owner to disable transfer restrictions - */ - function disableTransferRestrictions() external onlyOwner { - require( - transferRestrictionsDisabledAfter == type(uint256).max, - "BackingEigen.disableTransferRestrictions: transfer restrictions are already disabled" - ); - transferRestrictionsDisabledAfter = 0; - emit TransferRestrictionsDisabled(); - } - - /// VIEW FUNCTIONS - - /// INTERNAL FUNCTIONS - - function _setAllowedFrom(address from, bool isAllowedFrom) internal { - allowedFrom[from] = isAllowedFrom; - emit SetAllowedFrom(from, isAllowedFrom); - } - - function _setAllowedTo(address to, bool isAllowedTo) internal { - allowedTo[to] = isAllowedTo; - emit SetAllowedTo(to, isAllowedTo); - } - - /** - * @notice Overrides the beforeTokenTransfer function to enforce transfer restrictions - * @param from the address tokens are being transferred from - * @param to the address tokens are being transferred to - * @param amount the amount of tokens being transferred - */ - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal override { - // if transfer restrictions are enabled - if (block.timestamp <= transferRestrictionsDisabledAfter) { - // if both from and to are not whitelisted - require( - allowedFrom[from] || allowedTo[to] || from == address(0), - "BackingEigen._beforeTokenTransfer: from or to must be whitelisted" - ); - } - super._beforeTokenTransfer(from, to, amount); - } -} diff --git a/projects/vaults/contracts/tests/Eigen/Eigen.sol b/projects/vaults/contracts/tests/Eigen/Eigen.sol deleted file mode 100644 index 6f9a6db2..00000000 --- a/projects/vaults/contracts/tests/Eigen/Eigen.sol +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.12; - -import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract Eigen is OwnableUpgradeable, ERC20Upgradeable { - /// CONSTANTS & IMMUTABLES - /// @notice the address of the backing Eigen token bEIGEN - IERC20 public immutable bEIGEN; - - /// STORAGE - /// @notice mapping of minter addresses to the timestamp after which they are allowed to mint - mapping(address => uint256) public mintAllowedAfter; - /// @notice mapping of minter addresses to the amount of tokens they are allowed to mint - mapping(address => uint256) public mintingAllowance; - - /// @notice the timestamp after which transfer restrictions are disabled - uint256 public transferRestrictionsDisabledAfter; - /// @notice mapping of addresses that are allowed to transfer tokens to any address - mapping(address => bool) public allowedFrom; - /// @notice mapping of addresses that are allowed to receive tokens from any address - mapping(address => bool) public allowedTo; - - /// @notice event emitted when the allowedFrom status of an address is set - event SetAllowedFrom(address indexed from, bool isAllowedFrom); - /// @notice event emitted when the allowedTo status of an address is set - event SetAllowedTo(address indexed to, bool isAllowedTo); - /// @notice event emitted when a minter mints - event Mint(address indexed minter, uint256 amount); - /// @notice event emitted when the transfer restrictions disabled - event TransferRestrictionsDisabled(); - - constructor(IERC20 _bEIGEN) { - bEIGEN = _bEIGEN; - _disableInitializers(); - } - - /** - * @notice An initializer function that sets initial values for the contract's state variables. - * @param minters the addresses that are allowed to mint - * @param mintingAllowances the amount of tokens that each minter is allowed to mint - */ - function initialize( - address initialOwner, - address[] memory minters, - uint256[] memory mintingAllowances, - uint256[] memory mintAllowedAfters - ) public initializer { - __Ownable_init(); - __ERC20_init("Eigen", "EIGEN"); - _transferOwnership(initialOwner); - - require( - minters.length == mintingAllowances.length, - "Eigen.initialize: minters and mintingAllowances must be the same length" - ); - require( - minters.length == mintAllowedAfters.length, - "Eigen.initialize: minters and mintAllowedAfters must be the same length" - ); - // set minting allowances for each minter - for (uint256 i = 0; i < minters.length; i++) { - mintingAllowance[minters[i]] = mintingAllowances[i]; - mintAllowedAfter[minters[i]] = mintAllowedAfters[i]; - // allow each minter to transfer tokens - allowedFrom[minters[i]] = true; - emit SetAllowedFrom(minters[i], true); - } - - // set transfer restrictions to be disabled at type(uint256).max to be set down later - transferRestrictionsDisabledAfter = type(uint256).max; - } - - /** - * @notice This function allows the owner to set the allowedFrom status of an address - * @param from the address whose allowedFrom status is being set - * @param isAllowedFrom the new allowedFrom status - */ - function setAllowedFrom( - address from, - bool isAllowedFrom - ) external onlyOwner { - allowedFrom[from] = isAllowedFrom; - emit SetAllowedFrom(from, isAllowedFrom); - } - - /** - * @notice This function allows the owner to set the allowedTo status of an address - * @param to the address whose allowedTo status is being set - * @param isAllowedTo the new allowedTo status - */ - function setAllowedTo(address to, bool isAllowedTo) external onlyOwner { - allowedTo[to] = isAllowedTo; - emit SetAllowedTo(to, isAllowedTo); - } - - /** - * @notice Allows the owner to disable transfer restrictions - */ - function disableTransferRestrictions() external onlyOwner { - require( - transferRestrictionsDisabledAfter == type(uint256).max, - "Eigen.disableTransferRestrictions: transfer restrictions are already disabled" - ); - transferRestrictionsDisabledAfter = 0; - emit TransferRestrictionsDisabled(); - } - - /** - * @notice This function allows minter to mint tokens - */ - function mint() external { - require( - mintingAllowance[msg.sender] > 0, - "Eigen.mint: msg.sender has no minting allowance" - ); - require( - block.timestamp > mintAllowedAfter[msg.sender], - "Eigen.mint: msg.sender is not allowed to mint yet" - ); - uint256 amount = mintingAllowance[msg.sender]; - mintingAllowance[msg.sender] = 0; - _mint(msg.sender, amount); - emit Mint(msg.sender, amount); - } - - /** - * @notice This function allows bEIGEN holders to wrap their tokens into Eigen - */ - function wrap(uint256 amount) external { - require( - bEIGEN.transferFrom(msg.sender, address(this), amount), - "Eigen.wrap: bEIGEN transfer failed" - ); - _transfer(address(this), msg.sender, amount); - } - - /** - * @notice This function allows Eigen holders to unwrap their tokens into bEIGEN - */ - function unwrap(uint256 amount) external { - _transfer(msg.sender, address(this), amount); - require( - bEIGEN.transfer(msg.sender, amount), - "Eigen.unwrap: bEIGEN transfer failed" - ); - } - - /** - * @notice Allows the sender to transfer tokens to multiple addresses in a single transaction - */ - function multisend( - address[] calldata receivers, - uint256[] calldata amounts - ) public { - require( - receivers.length == amounts.length, - "Eigen.multisend: receivers and amounts must be the same length" - ); - for (uint256 i = 0; i < receivers.length; i++) { - _transfer(msg.sender, receivers[i], amounts[i]); - } - } - - /** - * @notice Overrides the beforeTokenTransfer function to enforce transfer restrictions - * @param from the address tokens are being transferred from - * @param to the address tokens are being transferred to - * @param amount the amount of tokens being transferred - */ - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal override { - // if transfer restrictions are enabled - if (block.timestamp <= transferRestrictionsDisabledAfter) { - // if both from and to are not whitelisted - require( - from == address(0) || - from == address(this) || - to == address(this) || - allowedFrom[from] || - allowedTo[to], - "Eigen._beforeTokenTransfer: from or to must be whitelisted" - ); - } - super._beforeTokenTransfer(from, to, amount); - } -} diff --git a/projects/vaults/contracts/tests/EigenLayer/StrategyManager.sol b/projects/vaults/contracts/tests/EigenLayer/StrategyManager.sol deleted file mode 100644 index 5dca40f7..00000000 --- a/projects/vaults/contracts/tests/EigenLayer/StrategyManager.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract StrategyManager { - function withdrawalDelayBlocks() external view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/EigenLayer/strategies/StrategyBaseDummy.sol b/projects/vaults/contracts/tests/EigenLayer/strategies/StrategyBaseDummy.sol deleted file mode 100644 index 7bb01832..00000000 --- a/projects/vaults/contracts/tests/EigenLayer/strategies/StrategyBaseDummy.sol +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - -contract StrategyBaseDummy { - function deposit( - IERC20 token, - uint256 amount - ) external payable returns (uint256) {} - - function withdraw( - address depositor, - IERC20 token, - uint256 amountShares - ) external {} - - function sharesToUnderlying( - uint256 amountShares - ) external returns (uint256) {} - - function underlyingToShares( - uint256 amountUnderlying - ) external returns (uint256) {} - - function userUnderlying(address user) external returns (uint256) {} - - function sharesToUnderlyingView( - uint256 amountShares - ) external view returns (uint256) {} - - function underlyingToSharesView( - uint256 amountUnderlying - ) external view returns (uint256) {} - - /** - * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in - * this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications - */ - function userUnderlyingView(address user) external view returns (uint256) {} - - /// @notice The underlying token for shares in this Strategy - function underlyingToken() external view returns (IERC20) {} - - /// @notice The total number of extant shares in this Strategy - function totalShares() external view returns (uint256) {} - - /// @notice Returns either a brief string explaining the strategy's goal & purpose, or a link to metadata that explains in more detail. - function explanation() external view returns (string memory) {} -} diff --git a/projects/vaults/contracts/tests/Frax/sfrxEth.sol b/projects/vaults/contracts/tests/Frax/sfrxEth.sol deleted file mode 100644 index fa2ac6f1..00000000 --- a/projects/vaults/contracts/tests/Frax/sfrxEth.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract sfrxEth is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/LBTC/LBTC.sol b/projects/vaults/contracts/tests/LBTC/LBTC.sol deleted file mode 100644 index 8eeb0c7d..00000000 --- a/projects/vaults/contracts/tests/LBTC/LBTC.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract LBTC is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/Lido/IWSteth.sol b/projects/vaults/contracts/tests/Lido/IWSteth.sol index 7b3bbb53..8d9e7baf 100644 --- a/projects/vaults/contracts/tests/Lido/IWSteth.sol +++ b/projects/vaults/contracts/tests/Lido/IWSteth.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; interface IWSteth is IERC20 { + function stETH() external returns(address); + function wrap(uint256 stethAmount) external payable returns (uint256); function unwrap(uint256 wstethAmount) external returns (uint256); diff --git a/projects/vaults/contracts/tests/LsETH/LsETH.sol b/projects/vaults/contracts/tests/LsETH/LsETH.sol deleted file mode 100644 index 9c481663..00000000 --- a/projects/vaults/contracts/tests/LsETH/LsETH.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract LsETH is ERC20Upgradeable { - function underlyingBalanceFromShares( - uint256 amount - ) external view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/Mantle/StakingPool.sol b/projects/vaults/contracts/tests/Mantle/StakingPool.sol deleted file mode 100644 index 66262023..00000000 --- a/projects/vaults/contracts/tests/Mantle/StakingPool.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract StakingPool { - function mETHToETH(uint256 mETHAmount) public view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/Mantle/mEth.sol b/projects/vaults/contracts/tests/Mantle/mEth.sol deleted file mode 100644 index a9b2ea1e..00000000 --- a/projects/vaults/contracts/tests/Mantle/mEth.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract mEth is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/OriginProtocol/OEth.sol b/projects/vaults/contracts/tests/OriginProtocol/OEth.sol deleted file mode 100644 index ed96571a..00000000 --- a/projects/vaults/contracts/tests/OriginProtocol/OEth.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract OEth is ERC20Upgradeable { - // function balanceOf(address _account) - // public - // view - // override - // returns (uint256) - // { - // if (_creditBalances[_account] == 0) return 0; - // return - // _creditBalances[_account].divPrecisely(_creditsPerToken(_account)); - // } - - function creditsBalanceOfHighres( - address _account - ) public view returns (uint256, uint256, bool) {} - - function creditsBalanceOf( - address _account - ) public view returns (uint256, uint256) {} -} diff --git a/projects/vaults/contracts/tests/OriginProtocol/VaultCore.sol b/projects/vaults/contracts/tests/OriginProtocol/VaultCore.sol deleted file mode 100644 index 0b3b6f7b..00000000 --- a/projects/vaults/contracts/tests/OriginProtocol/VaultCore.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract VaultCore { - function mint( - address _asset, - uint256 _amount, - uint256 _minimumOusdAmount - ) external {} -} diff --git a/projects/vaults/contracts/tests/Rocket/MockPool.sol b/projects/vaults/contracts/tests/Rocket/MockPool.sol deleted file mode 100644 index 6648db93..00000000 --- a/projects/vaults/contracts/tests/Rocket/MockPool.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -// Uncomment this line to use console.log - -contract RocketMockPool { - function deposit() external payable {} -} diff --git a/projects/vaults/contracts/tests/Rocket/rETH.sol b/projects/vaults/contracts/tests/Rocket/rETH.sol deleted file mode 100644 index 37bed036..00000000 --- a/projects/vaults/contracts/tests/Rocket/rETH.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; - -contract rETH is ERC20Upgradeable { - function initialize( - string calldata _name, - string calldata _symbol - ) public initializer { - __ERC20_init_unchained(_name, _symbol); - } - - function burn(address account, uint256 amount) external { - _burn(account, amount); - } - - function mint(address account, uint256 amount) external { - _mint(account, amount); - } - - // Calculate the amount of rETH backed by an amount of ETH - function getRethValue(uint256 _ethAmount) public view returns (uint256) {} - - function getEthValue(uint256 _rethAmount) public view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/Stader/Ethx.sol b/projects/vaults/contracts/tests/Stader/Ethx.sol deleted file mode 100644 index e076b376..00000000 --- a/projects/vaults/contracts/tests/Stader/Ethx.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract Ethx is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/Stader/StaderStakePoolsManager.sol b/projects/vaults/contracts/tests/Stader/StaderStakePoolsManager.sol deleted file mode 100644 index 3b86924f..00000000 --- a/projects/vaults/contracts/tests/Stader/StaderStakePoolsManager.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract StaderStakePoolsManager { - function convertToShares(uint256 _assets) external view returns (uint256) {} - - function convertToAssets(uint256 _shares) external view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/StakeWise/OsEth.sol b/projects/vaults/contracts/tests/StakeWise/OsEth.sol deleted file mode 100644 index bd8ed3a0..00000000 --- a/projects/vaults/contracts/tests/StakeWise/OsEth.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract OsEth is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/StakeWise/StakeWiseVault.sol b/projects/vaults/contracts/tests/StakeWise/StakeWiseVault.sol deleted file mode 100644 index a2ba0893..00000000 --- a/projects/vaults/contracts/tests/StakeWise/StakeWiseVault.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract StakeWiseVault { - function mintOsToken( - address receiver, - uint256 osTokenShares, - address referrer - ) external {} - - function deposit(address receiver, address referrer) external {} -} diff --git a/projects/vaults/contracts/tests/Swell/SwEth.sol b/projects/vaults/contracts/tests/Swell/SwEth.sol deleted file mode 100644 index 2df6e13f..00000000 --- a/projects/vaults/contracts/tests/Swell/SwEth.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract SwEth is ERC20Upgradeable { - function swETHToETHRate() external view returns (uint256) {} -} diff --git a/projects/vaults/contracts/tests/Upgrades/ProxyAdminMock.sol b/projects/vaults/contracts/tests/Upgrades/ProxyAdminMock.sol deleted file mode 100644 index cf3539a4..00000000 --- a/projects/vaults/contracts/tests/Upgrades/ProxyAdminMock.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract ProxyAdminMock { - function upgrade(address proxy, address implementation) external payable {} -} diff --git a/projects/vaults/contracts/tests/general/MockPool.sol b/projects/vaults/contracts/tests/general/MockPool.sol deleted file mode 100644 index 9e18fa39..00000000 --- a/projects/vaults/contracts/tests/general/MockPool.sol +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -contract MockPool {} diff --git a/projects/vaults/contracts/tests/sFRAX/sFRAX.sol b/projects/vaults/contracts/tests/sFRAX/sFRAX.sol deleted file mode 100644 index 996420ab..00000000 --- a/projects/vaults/contracts/tests/sFRAX/sFRAX.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract sFRAX is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/sUSDe/sUSDe.sol b/projects/vaults/contracts/tests/sUSDe/sUSDe.sol deleted file mode 100644 index dd1620ba..00000000 --- a/projects/vaults/contracts/tests/sUSDe/sUSDe.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract sUSDe is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/slisBNB/slisBNB.sol b/projects/vaults/contracts/tests/slisBNB/slisBNB.sol deleted file mode 100644 index 8ac052b8..00000000 --- a/projects/vaults/contracts/tests/slisBNB/slisBNB.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract slisBNB is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/solvBTC/solvBTC.sol b/projects/vaults/contracts/tests/solvBTC/solvBTC.sol deleted file mode 100644 index b8ffafee..00000000 --- a/projects/vaults/contracts/tests/solvBTC/solvBTC.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract solvBTC is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/tests/tBTC/tBTC.sol b/projects/vaults/contracts/tests/tBTC/tBTC.sol deleted file mode 100644 index 1ff6291c..00000000 --- a/projects/vaults/contracts/tests/tBTC/tBTC.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -contract tBTC is ERC20Upgradeable {} diff --git a/projects/vaults/contracts/vaults/EigenLayer/InceptionVaultStorage_EL.sol b/projects/vaults/contracts/vaults/EigenLayer/InceptionVaultStorage_EL.sol index a058748e..8dd2da05 100644 --- a/projects/vaults/contracts/vaults/EigenLayer/InceptionVaultStorage_EL.sol +++ b/projects/vaults/contracts/vaults/EigenLayer/InceptionVaultStorage_EL.sol @@ -14,7 +14,7 @@ import {IDelegationManager} from "../../interfaces/eigenlayer-vault/eigen-core/I import {IInceptionRatioFeed} from "../../interfaces/common/IInceptionRatioFeed.sol"; import {IInceptionVaultErrors} from "../../interfaces/common/IInceptionVaultErrors.sol"; -import {IInceptionEigenRestaker, IInceptionEigenRestakerErrors} from "../../interfaces/eigenlayer-vault/IInceptionEigenRestaker.sol"; +import {IInceptionEigenRestaker} from "../../interfaces/eigenlayer-vault/IInceptionEigenRestaker.sol"; import {IStrategyManager, IStrategy} from "../../interfaces/eigenlayer-vault/eigen-core/IStrategyManager.sol"; import {Convert} from "../../lib/Convert.sol"; diff --git a/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL.sol b/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL.sol index 2ad464a5..15cd8135 100644 --- a/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL.sol +++ b/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL.sol @@ -21,11 +21,10 @@ contract ERC4626Facet_EL is InceptionVaultStorage_EL { * @dev Issues the tokens to the specified receiver address. * @dev See {IERC4626-deposit}. */ - function deposit(uint256 amount, address receiver) - external - nonReentrant - returns (uint256) - { + function deposit( + uint256 amount, + address receiver + ) external nonReentrant returns (uint256) { return _deposit(amount, msg.sender, receiver); } @@ -36,11 +35,10 @@ contract ERC4626Facet_EL is InceptionVaultStorage_EL { * @param receiver The address of the shares receiver. * @dev See {IERC4626-mint}. */ - function mint(uint256 shares, address receiver) - external - nonReentrant - returns (uint256) - { + function mint( + uint256 shares, + address receiver + ) external nonReentrant returns (uint256) { uint256 maxShares = maxMint(msg.sender); if (shares > maxShares) revert ExceededMaxMint(receiver, shares, maxShares); @@ -186,10 +184,10 @@ contract ERC4626Facet_EL is InceptionVaultStorage_EL { * @dev Creates a withdrawal requests based on the current ratio * @param iShares is measured in Inception token(shares) */ - function flashWithdraw(uint256 iShares, address receiver) - external - nonReentrant - { + function flashWithdraw( + uint256 iShares, + address receiver + ) external nonReentrant { __beforeWithdraw(receiver, iShares); address claimer = msg.sender; (uint256 amount, uint256 fee) = _flashWithdraw( @@ -254,21 +252,15 @@ contract ERC4626Facet_EL is InceptionVaultStorage_EL { /// @dev The functions below serve the proper withdrawal and claiming operations /// @notice Since a particular LST loses some wei on each transfer, /// this needs to be taken into account - function _getAssetWithdrawAmount(uint256 amount) - internal - view - virtual - returns (uint256) - { + function _getAssetWithdrawAmount( + uint256 amount + ) internal view virtual returns (uint256) { return amount; } - function _getAssetReceivedAmount(uint256 amount) - internal - view - virtual - returns (uint256) - { + function _getAssetReceivedAmount( + uint256 amount + ) internal view virtual returns (uint256) { return amount; } } diff --git a/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E1.sol b/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E1.sol index 29ba5bb3..38fe0027 100644 --- a/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E1.sol +++ b/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E1.sol @@ -10,21 +10,15 @@ import {ERC4626Facet_EL} from "./ERC4626Facet_EL.sol"; contract ERC4626Facet_EL_E1 is ERC4626Facet_EL { constructor() payable {} - function _getAssetWithdrawAmount(uint256 amount) - internal - pure - override - returns (uint256) - { + function _getAssetWithdrawAmount( + uint256 amount + ) internal pure override returns (uint256) { return amount + 1; } - function _getAssetReceivedAmount(uint256 amount) - internal - pure - override - returns (uint256) - { + function _getAssetReceivedAmount( + uint256 amount + ) internal pure override returns (uint256) { return amount - 1; } } diff --git a/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E2.sol b/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E2.sol index 46a2bd34..b4933fe6 100644 --- a/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E2.sol +++ b/projects/vaults/contracts/vaults/EigenLayer/facets/ERC4626Facet/ERC4626Facet_EL_E2.sol @@ -10,21 +10,15 @@ import {ERC4626Facet_EL} from "./ERC4626Facet_EL.sol"; contract ERC4626Facet_EL_E2 is ERC4626Facet_EL { constructor() payable {} - function _getAssetWithdrawAmount(uint256 amount) - internal - pure - override - returns (uint256) - { + function _getAssetWithdrawAmount( + uint256 amount + ) internal pure override returns (uint256) { return amount + 2; } - function _getAssetReceivedAmount(uint256 amount) - internal - pure - override - returns (uint256) - { + function _getAssetReceivedAmount( + uint256 amount + ) internal pure override returns (uint256) { return amount - 2; } } diff --git a/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerFacet.sol b/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerFacet.sol index 45cecda0..61ea6170 100644 --- a/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerFacet.sol +++ b/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerFacet.sol @@ -83,9 +83,10 @@ contract EigenLayerFacet is InceptionVaultStorage_EL { } /// @dev deposits asset to the corresponding strategy - function _depositAssetIntoStrategy(address restaker, uint256 amount) - internal - { + function _depositAssetIntoStrategy( + address restaker, + uint256 amount + ) internal { _asset.approve(restaker, amount); IInceptionEigenRestaker(restaker).depositAssetIntoStrategy(amount); @@ -122,10 +123,10 @@ contract EigenLayerFacet is InceptionVaultStorage_EL { * @dev performs creating a withdrawal request from EigenLayer * @dev requires a specific amount to withdraw */ - function undelegateFrom(address elOperatorAddress, uint256 amount) - external - nonReentrant - { + function undelegateFrom( + address elOperatorAddress, + uint256 amount + ) external nonReentrant { address staker = _operatorRestakers[elOperatorAddress]; if (staker == address(0)) revert OperatorNotRegistered(); if (staker == _MOCK_ADDRESS) revert NullParams(); @@ -135,10 +136,10 @@ contract EigenLayerFacet is InceptionVaultStorage_EL { ); } - function _undelegate(uint256 amount, address staker) - internal - returns (uint256) - { + function _undelegate( + uint256 amount, + address staker + ) internal returns (uint256) { uint256 nonce = delegationManager.cumulativeWithdrawalsQueued(staker); uint256 totalAssetSharesInEL = strategyManager.stakerStrategyShares( staker, @@ -226,7 +227,6 @@ contract EigenLayerFacet is InceptionVaultStorage_EL { delegationManager.completeQueuedWithdrawals( withdrawals, tokens, - middlewareTimesIndexes, receiveAsTokens ); @@ -241,11 +241,9 @@ contract EigenLayerFacet is InceptionVaultStorage_EL { _updateEpoch(getFreeBalance()); } - function _restakerExists(address restakerAddress) - internal - view - returns (bool) - { + function _restakerExists( + address restakerAddress + ) internal view returns (bool) { uint256 numOfRestakers = restakers.length; for (uint256 i = 0; i < numOfRestakers; ++i) { if (restakerAddress == restakers[i]) return true; @@ -269,9 +267,10 @@ contract EigenLayerFacet is InceptionVaultStorage_EL { } } - function forceUndelegateRecovery(uint256 amount, address restaker) - external - { + function forceUndelegateRecovery( + uint256 amount, + address restaker + ) external { if (restaker == address(0)) revert NullParams(); for (uint256 i = 0; i < restakers.length; ++i) { if ( @@ -325,9 +324,9 @@ contract EigenLayerFacet is InceptionVaultStorage_EL { emit RewardsAdded(amount, startTimeline); } - function setPendingWithdrawalAmount(uint256 newPendingWithdrawalAmount) - external - { + function setPendingWithdrawalAmount( + uint256 newPendingWithdrawalAmount + ) external { _pendingWithdrawalAmount = newPendingWithdrawalAmount; } } diff --git a/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerStrategyBaseHandler.sol b/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerStrategyBaseHandler.sol index 3cb3763e..54c0710c 100644 --- a/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerStrategyBaseHandler.sol +++ b/projects/vaults/contracts/vaults/EigenLayer/facets/EigenLayerStrategyBaseHandler.sol @@ -6,7 +6,7 @@ // import {IStrategyManager, IStrategy} from "../interfaces/IStrategyManager.sol"; // import {IDelegationManager} from "../interfaces/IDelegationManager.sol"; // import {IEigenLayerHandler} from "../interfaces/IEigenLayerHandler.sol"; -// import {IInceptionRestaker} from "../interfaces/IInceptionRestaker.sol"; +// import {IInceptionAdapter} from "../interfaces/IInceptionAdapter.sol"; // /// @author The InceptionLRT team // /// @title The EigenLayerStrategyBaseHandler contract @@ -43,8 +43,8 @@ // uint256 public redeemReservedAmount; // /// @dev EigenLayer operator -> inception staker -// mapping(address => address) internal _operatorRestakers; -// address[] public restakers; +// mapping(address => address) internal _operatorAdapters; +// address[] public adapters; // uint256 public depositBonusAmount; @@ -87,23 +87,23 @@ // /// @dev deposits asset to the corresponding strategy // function _depositAssetIntoStrategy( -// address restaker, +// address adapter, // uint256 amount // ) internal { -// _asset.approve(restaker, amount); -// IInceptionRestaker(restaker).depositAssetIntoStrategy(amount); +// _asset.approve(adapter, amount); +// IInceptionAdapter(adapter).depositAssetIntoStrategy(amount); -// emit DepositedToEL(restaker, amount); +// emit DepositedToEL(adapter, amount); // } // /// @dev delegates assets held in the strategy to the EL operator. // function _delegateToOperator( -// address restaker, +// address adapter, // address elOperator, // bytes32 approverSalt, // IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry // ) internal { -// IInceptionRestaker(restaker).delegateToOperator( +// IInceptionAdapter(adapter).delegateToOperator( // elOperator, // approverSalt, // approverSignatureAndExpiry @@ -120,11 +120,11 @@ // address elOperatorAddress, // uint256 amount // ) external whenNotPaused nonReentrant onlyOperator { -// address staker = _operatorRestakers[elOperatorAddress]; +// address staker = _operatorAdapters[elOperatorAddress]; // if (staker == address(0)) revert OperatorNotRegistered(); // if (staker == _MOCK_ADDRESS) revert NullParams(); -// IInceptionRestaker(staker).withdrawFromEL(_undelegate(amount, staker)); +// IInceptionAdapter(staker).withdrawFromEL(_undelegate(amount, staker)); // } // /// @dev performs creating a withdrawal request from EigenLayer @@ -182,7 +182,7 @@ // /// @dev claims completed withdrawals from EigenLayer, if they exist // function claimCompletedWithdrawals( -// address restaker, +// address adapter, // IDelegationManager.Withdrawal[] calldata withdrawals // ) public whenNotPaused nonReentrant { // uint256 withdrawalsNum = withdrawals.length; @@ -199,7 +199,7 @@ // uint256 availableBalance = getFreeBalance(); // uint256 withdrawnAmount; -// if (restaker == address(this)) { +// if (adapter == address(this)) { // withdrawnAmount = _claimCompletedWithdrawalsForVault( // withdrawals, // tokens, @@ -207,8 +207,8 @@ // receiveAsTokens // ); // } else { -// if (!_restakerExists(restaker)) revert RestakerNotRegistered(); -// withdrawnAmount = IInceptionRestaker(restaker).claimWithdrawals( +// if (!_adapterExists(adapter)) revert AdapterNotRegistered(); +// withdrawnAmount = IInceptionAdapter(adapter).claimWithdrawals( // withdrawals, // tokens, // middlewareTimesIndexes, @@ -283,12 +283,12 @@ // } // } -// function _restakerExists( -// address restakerAddress +// function _adapterExists( +// address adapterAddress // ) internal view returns (bool) { -// uint256 numOfRestakers = restakers.length; -// for (uint256 i = 0; i < numOfRestakers; ++i) { -// if (restakerAddress == restakers[i]) return true; +// uint256 numOfAdapters = adapters.length; +// for (uint256 i = 0; i < numOfAdapters; ++i) { +// if (adapterAddress == adapters[i]) return true; // } // return false; // } @@ -307,10 +307,10 @@ // } // function getTotalDelegated() public view returns (uint256 total) { -// uint256 stakersNum = restakers.length; +// uint256 stakersNum = adapters.length; // for (uint256 i = 0; i < stakersNum; ++i) { -// if (restakers[i] == address(0)) continue; -// total += strategy.userUnderlyingView(restakers[i]); +// if (adapters[i] == address(0)) continue; +// total += strategy.userUnderlyingView(adapters[i]); // } // return total + strategy.userUnderlyingView(address(this)); // } @@ -364,15 +364,15 @@ // function forceUndelegateRecovery( // uint256 amount, -// address restaker +// address adapter // ) external onlyOperator { -// if (restaker == address(0)) revert NullParams(); -// for (uint256 i = 0; i < restakers.length; ++i) { +// if (adapter == address(0)) revert NullParams(); +// for (uint256 i = 0; i < adapters.length; ++i) { // if ( -// restakers[i] == restaker && -// !delegationManager.isDelegated(restakers[i]) +// adapters[i] == adapter && +// !delegationManager.isDelegated(adapters[i]) // ) { -// restakers[i] == _MOCK_ADDRESS; +// adapters[i] == _MOCK_ADDRESS; // break; // } // } diff --git a/projects/vaults/contracts/vaults/EigenLayer/facets/EigenSetterFacet.sol b/projects/vaults/contracts/vaults/EigenLayer/facets/EigenSetterFacet.sol index 9e3071f8..b3402a62 100644 --- a/projects/vaults/contracts/vaults/EigenLayer/facets/EigenSetterFacet.sol +++ b/projects/vaults/contracts/vaults/EigenLayer/facets/EigenSetterFacet.sol @@ -71,9 +71,9 @@ contract EigenSetterFacet is InceptionVaultStorage_EL { emit ELOperatorAdded(newELOperator); } - function setDelegationManager(IDelegationManager newDelegationManager) - external - { + function setDelegationManager( + IDelegationManager newDelegationManager + ) external { if (address(delegationManager) != address(0)) revert DelegationManagerImmutable(); diff --git a/projects/vaults/contracts/vaults/EigenLayer/facets/SwellEigenLayerFacet.sol b/projects/vaults/contracts/vaults/EigenLayer/facets/SwellEigenLayerFacet.sol deleted file mode 100644 index d14a6b7b..00000000 --- a/projects/vaults/contracts/vaults/EigenLayer/facets/SwellEigenLayerFacet.sol +++ /dev/null @@ -1,362 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; -import {ICumulativeMerkleDrop} from "../../../interfaces/common/ICumulativeMerkleDrop.sol"; - -import "../InceptionVaultStorage_EL.sol"; - -/** - * @title The SwellEigenLayerFacet contract - * @author The InceptionLRT team - * @notice This contract extends the functionality of the EigenLayerFacet - * by incorporating the Swell AirDrop feature. - */ -contract SwellEigenLayerFacet is InceptionVaultStorage_EL { - address immutable SWELL_AIDROP_CONTRACT = - address(0x342F0D375Ba986A65204750A4AECE3b39f739d75); - - address immutable INCEPTION_AIDROP_CONTRACT = - address(0x81cDDe43155DB595DBa2Cefd50d8e7714aff34f4); - - IERC20 immutable SWELL_ASSET = - IERC20(0x0a6E7Ba5042B38349e437ec6Db6214AEC7B35676); - - constructor() payable {} - - /** - * @dev checks whether it's still possible to deposit into the strategy - */ - function _beforeDepositAssetIntoStrategy(uint256 amount) internal view { - if (amount > getFreeBalance()) - revert InsufficientCapacity(totalAssets()); - - (uint256 maxPerDeposit, uint256 maxTotalDeposits) = strategy - .getTVLLimits(); - - if (amount > maxPerDeposit) - revert ExceedsMaxPerDeposit(maxPerDeposit, amount); - - uint256 currentBalance = _asset.balanceOf(address(strategy)); - if (currentBalance + amount > maxTotalDeposits) - revert ExceedsMaxTotalDeposited(maxTotalDeposits, currentBalance); - } - - function delegateToOperator( - uint256 amount, - address elOperator, - bytes32 approverSalt, - IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry - ) external { - if (elOperator == address(0)) revert NullParams(); - - _beforeDepositAssetIntoStrategy(amount); - - // try to find a restaker for the specific EL operator - address restaker = _operatorRestakers[elOperator]; - if (restaker == address(0)) revert OperatorNotRegistered(); - - bool delegate = false; - if (restaker == _MOCK_ADDRESS) { - delegate = true; - // deploy a new restaker - restaker = _deployNewStub(); - _operatorRestakers[elOperator] = restaker; - restakers.push(restaker); - } - - _depositAssetIntoStrategy(restaker, amount); - - if (delegate) - _delegateToOperator( - restaker, - elOperator, - approverSalt, - approverSignatureAndExpiry - ); - - emit DelegatedTo(restaker, elOperator, amount); - } - - /** - * @dev delegates assets held in the strategy to the EL operator. - */ - function _delegateToOperator( - address restaker, - address elOperator, - bytes32 approverSalt, - IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry - ) internal { - IInceptionEigenRestaker(restaker).delegateToOperator( - elOperator, - approverSalt, - approverSignatureAndExpiry - ); - } - - /// @dev deposits asset to the corresponding strategy - function _depositAssetIntoStrategy(address restaker, uint256 amount) - internal - { - _asset.approve(restaker, amount); - IInceptionEigenRestaker(restaker).depositAssetIntoStrategy(amount); - - emit DepositedToEL(restaker, amount); - } - - /** - * @dev performs creating a withdrawal request from EigenLayer - * @dev requires a specific amount to withdraw - */ - function undelegateVault(uint256 amount) external nonReentrant { - address staker = address(this); - - uint256[] memory sharesToWithdraw = new uint256[](1); - IStrategy[] memory strategies = new IStrategy[](1); - - sharesToWithdraw[0] = _undelegate(amount, staker); - strategies[0] = strategy; - IDelegationManager.QueuedWithdrawalParams[] - memory withdrawals = new IDelegationManager.QueuedWithdrawalParams[]( - 1 - ); - - /// @notice from Vault - withdrawals[0] = IDelegationManager.QueuedWithdrawalParams({ - strategies: strategies, - shares: sharesToWithdraw, - withdrawer: address(this) - }); - delegationManager.queueWithdrawals(withdrawals); - } - - /** - * @dev performs creating a withdrawal request from EigenLayer - * @dev requires a specific amount to withdraw - */ - function undelegateFrom(address elOperatorAddress, uint256 amount) - external - nonReentrant - { - address staker = _operatorRestakers[elOperatorAddress]; - if (staker == address(0)) revert OperatorNotRegistered(); - if (staker == _MOCK_ADDRESS) revert NullParams(); - - IInceptionEigenRestaker(staker).withdrawFromEL( - _undelegate(amount, staker) - ); - } - - function _undelegate(uint256 amount, address staker) - internal - returns (uint256) - { - uint256 nonce = delegationManager.cumulativeWithdrawalsQueued(staker); - uint256 totalAssetSharesInEL = strategyManager.stakerStrategyShares( - staker, - strategy - ); - uint256 shares = strategy.underlyingToSharesView(amount); - amount = strategy.sharesToUnderlyingView(shares); - - // we need to withdraw the remaining dust from EigenLayer - if (totalAssetSharesInEL < shares + 5) shares = totalAssetSharesInEL; - - _pendingWithdrawalAmount += amount; - emit StartWithdrawal( - staker, - strategy, - shares, - uint32(block.number), - delegationManager.delegatedTo(staker), - nonce - ); - return shares; - } - - /** - * @dev claims completed withdrawals from EigenLayer, if they exist - */ - function claimCompletedWithdrawals( - address restaker, - IDelegationManager.Withdrawal[] calldata withdrawals - ) public nonReentrant { - uint256 withdrawalsNum = withdrawals.length; - IERC20[][] memory tokens = new IERC20[][](withdrawalsNum); - uint256[] memory middlewareTimesIndexes = new uint256[](withdrawalsNum); - bool[] memory receiveAsTokens = new bool[](withdrawalsNum); - - for (uint256 i = 0; i < withdrawalsNum; ++i) { - tokens[i] = new IERC20[](1); - tokens[i][0] = _asset; - receiveAsTokens[i] = true; - } - - uint256 availableBalance = getFreeBalance(); - - uint256 withdrawnAmount; - if (restaker == address(this)) { - withdrawnAmount = _claimCompletedWithdrawalsForVault( - withdrawals, - tokens, - middlewareTimesIndexes, - receiveAsTokens - ); - } else { - if (!_restakerExists(restaker)) revert RestakerNotRegistered(); - withdrawnAmount = IInceptionEigenRestaker(restaker) - .claimWithdrawals( - withdrawals, - tokens, - middlewareTimesIndexes, - receiveAsTokens - ); - } - - emit WithdrawalClaimed(withdrawnAmount); - - _pendingWithdrawalAmount = _pendingWithdrawalAmount < withdrawnAmount - ? 0 - : _pendingWithdrawalAmount - withdrawnAmount; - - if (_pendingWithdrawalAmount < 7) { - _pendingWithdrawalAmount = 0; - } - - _updateEpoch(availableBalance + withdrawnAmount); - } - - function _claimCompletedWithdrawalsForVault( - IDelegationManager.Withdrawal[] memory withdrawals, - IERC20[][] memory tokens, - uint256[] memory middlewareTimesIndexes, - bool[] memory receiveAsTokens - ) internal returns (uint256) { - uint256 balanceBefore = _asset.balanceOf(address(this)); - - delegationManager.completeQueuedWithdrawals( - withdrawals, - tokens, - middlewareTimesIndexes, - receiveAsTokens - ); - - // send tokens to the vault - uint256 withdrawnAmount = _asset.balanceOf(address(this)) - - balanceBefore; - - return withdrawnAmount; - } - - function updateEpoch() external nonReentrant { - _updateEpoch(getFreeBalance()); - } - - function _restakerExists(address restakerAddress) - internal - view - returns (bool) - { - uint256 numOfRestakers = restakers.length; - for (uint256 i = 0; i < numOfRestakers; ++i) { - if (restakerAddress == restakers[i]) return true; - } - return false; - } - - function _updateEpoch(uint256 availableBalance) internal { - uint256 withdrawalsNum = claimerWithdrawalsQueue.length; - for (uint256 i = epoch; i < withdrawalsNum; ) { - uint256 amount = claimerWithdrawalsQueue[i].amount; - unchecked { - if (amount > availableBalance) { - break; - } - redeemReservedAmount += amount; - availableBalance -= amount; - ++epoch; - ++i; - } - } - } - - function forceUndelegateRecovery(uint256 amount, address restaker) - external - { - if (restaker == address(0)) revert NullParams(); - for (uint256 i = 0; i < restakers.length; ++i) { - if ( - restakers[i] == restaker && - !delegationManager.isDelegated(restakers[i]) - ) { - restakers[i] == _MOCK_ADDRESS; - break; - } - } - _pendingWithdrawalAmount += amount; - } - - function _deployNewStub() internal returns (address) { - if (stakerImplementation == address(0)) revert ImplementationNotSet(); - // deploy new beacon proxy and do init call - bytes memory data = abi.encodeWithSignature( - "initialize(address,address,address,address,address,address,address)", - owner(), - rewardsCoordinator, - delegationManager, - strategyManager, - strategy, - _asset, - _operator - ); - address deployedAddress = address(new BeaconProxy(address(this), data)); - - IOwnable asOwnable = IOwnable(deployedAddress); - asOwnable.transferOwnership(owner()); - - emit RestakerDeployed(deployedAddress); - return deployedAddress; - } - - /** - * @notice Adds new rewards to the contract, starting a new rewards timeline. - * @dev The function allows the operator to deposit Ether as rewards. - * It verifies that the previous rewards timeline is over before accepting new rewards. - */ - function addRewards(uint256 amount) external nonReentrant { - /// @dev verify whether the prev timeline is over - if (currentRewards > 0) { - uint256 totalDays = rewardsTimeline / 1 days; - uint256 dayNum = (block.timestamp - startTimeline) / 1 days; - if (dayNum < totalDays) revert TimelineNotOver(); - } - currentRewards = _transferAssetFrom(_operator, amount); - startTimeline = block.timestamp; - - emit RewardsAdded(amount, startTimeline); - } - - function claimSwellAidrop( - uint256 cumulativeAmount, - bytes32[] calldata merkleProof - ) external { - uint256 initBalance = SWELL_ASSET.balanceOf(INCEPTION_AIDROP_CONTRACT); - ICumulativeMerkleDrop(SWELL_AIDROP_CONTRACT).claimAndLock( - cumulativeAmount, - 0, - merkleProof - ); - - SWELL_ASSET.transfer(INCEPTION_AIDROP_CONTRACT, cumulativeAmount); - if ( - initBalance + cumulativeAmount != - SWELL_ASSET.balanceOf(INCEPTION_AIDROP_CONTRACT) - ) revert InconsistentData(); - - emit AirDropClaimed( - _msgSender(), - INCEPTION_AIDROP_CONTRACT, - cumulativeAmount - ); - } -} diff --git a/projects/vaults/contracts/vaults/InceptionBasicStrategyVault.sol b/projects/vaults/contracts/vaults/InceptionBasicStrategyVault.sol deleted file mode 100644 index 08e872ea..00000000 --- a/projects/vaults/contracts/vaults/InceptionBasicStrategyVault.sol +++ /dev/null @@ -1,584 +0,0 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.28; - -// import {BeaconProxy, Address} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; - -// import {IOwnable} from "../interfaces/IOwnable.sol"; -// import {IInceptionVault} from "../interfaces/IInceptionVault.sol"; -// import {IInceptionToken} from "../interfaces/IInceptionToken.sol"; -// import {IDelegationManager} from "../interfaces/IDelegationManager.sol"; -// import {IInceptionRatioFeed} from "../interfaces/IInceptionRatioFeed.sol"; -// import "../eigenlayer-handler/EigenLayerStrategyBaseHandler.sol"; - -// /// @author The InceptionLRT team -// /// @title The InceptionStrategyBaseVault contract -// /// @notice Aims to maximize the profit of EigenLayer for a certain asset. -// contract InceptionStrategyBaseVault is -// IInceptionVault, -// EigenLayerStrategyBaseHandler -// { -// /// @dev Inception restaking token -// IInceptionToken public inceptionToken; - -// /// @dev Reduces rounding issues -// uint256 public minAmount; - -// mapping(address => Withdrawal) private _claimerWithdrawals; - -// /// @dev the unique InceptionVault name -// string public name; - -// /// @dev Factory variables -// address private _stakerImplementation; - -// /** -// * @dev Flash withdrawal params -// */ - -// /// @dev 100% -// uint64 public constant MAX_PERCENT = 100 * 1e8; - -// IInceptionRatioFeed public ratioFeed; -// address public treasury; -// uint64 public protocolFee; - -// /// @dev deposit bonus -// uint64 public maxBonusRate; -// uint64 public optimalBonusRate; -// uint64 public depositUtilizationKink; - -// /// @dev flash withdrawal fee -// uint64 public maxFlashFeeRate; -// uint64 public optimalWithdrawalRate; -// uint64 public withdrawUtilizationKink; - -// function __InceptionVault_init( -// string memory vaultName, -// address operatorAddress, -// IStrategyManager _strategyManager, -// IInceptionToken _inceptionToken, -// IStrategy _assetStrategy, -// IERC20 asset -// ) internal { -// __Ownable_init(); -// __EigenLayerHandler_init(_strategyManager, _assetStrategy, asset); - -// name = vaultName; -// _operator = operatorAddress; -// inceptionToken = _inceptionToken; - -// minAmount = 100; - -// /// TODO -// protocolFee = 50 * 1e8; -// targetCapacity = 5 * 1e18; - -// /// @dev deposit bonus -// depositUtilizationKink = 25 * 1e8; -// maxBonusRate = 15 * 1e7; -// optimalBonusRate = 25 * 1e6; - -// /// @dev withdrawal fee -// withdrawUtilizationKink = 25 * 1e8; -// maxFlashFeeRate = 30 * 1e7; -// optimalWithdrawalRate = 5 * 1e7; - -// treasury = msg.sender; -// } - -// /*////////////////////////////// -// ////// Deposit functions ////// -// ////////////////////////////*/ - -// function __beforeDeposit(address receiver, uint256 amount) internal view { -// if (receiver == address(0)) revert NullParams(); -// if (amount < minAmount) revert LowerMinAmount(minAmount); - -// if (targetCapacity == 0) revert InceptionOnPause(); -// if (!_verifyDelegated()) revert InceptionOnPause(); -// } - -// function __afterDeposit(uint256 iShares) internal pure { -// if (iShares == 0) revert DepositInconsistentResultedState(); -// } - -// /// @dev Transfers the msg.sender's assets to the vault. -// /// @dev Mints Inception tokens in accordance with the current ratio. -// /// @dev Issues the tokens to the specified receiver address. -// function deposit( -// uint256 amount, -// address receiver -// ) external nonReentrant whenNotPaused returns (uint256) { -// return _deposit(amount, msg.sender, receiver); -// } - -// /// @notice The deposit function but with a referral code -// function depositWithReferral( -// uint256 amount, -// address receiver, -// bytes32 code -// ) external nonReentrant whenNotPaused returns (uint256) { -// emit ReferralCode(code); -// return _deposit(amount, msg.sender, receiver); -// } - -// function _deposit( -// uint256 amount, -// address sender, -// address receiver -// ) internal returns (uint256) { -// // transfers assets from the sender and returns the received amount -// // the actual received amount might slightly differ from the specified amount, -// // approximately by -2 wei -// __beforeDeposit(receiver, amount); -// uint256 depositedBefore = totalAssets(); -// uint256 depositBonus; -// uint256 availableBonusAmount = depositBonusAmount; -// if (availableBonusAmount > 0) { -// depositBonus = calculateDepositBonus(amount); -// if (depositBonus > availableBonusAmount) { -// depositBonus = availableBonusAmount; -// depositBonusAmount = 0; -// } else { -// depositBonusAmount -= depositBonus; -// } -// emit DepositBonus(depositBonus); -// } - -// // get the amount from the sender -// _transferAssetFrom(sender, amount); -// amount = totalAssets() - depositedBefore; - -// uint256 iShares = convertToShares(amount + depositBonus); -// inceptionToken.mint(receiver, iShares); -// __afterDeposit(iShares); - -// emit Deposit(sender, receiver, amount, iShares); - -// return iShares; -// } - -// /*///////////////////////////////// -// ////// Delegation functions ////// -// ///////////////////////////////*/ - -// function delegateToOperator( -// uint256 amount, -// address elOperator, -// bytes32 approverSalt, -// IDelegationManager.SignatureWithExpiry memory approverSignatureAndExpiry -// ) external nonReentrant whenNotPaused onlyOperator { -// if (elOperator == address(0)) revert NullParams(); - -// _beforeDepositAssetIntoStrategy(amount); - -// // try to find a restaker for the specific EL operator -// address restaker = _operatorRestakers[elOperator]; -// if (restaker == address(0)) revert OperatorNotRegistered(); - -// bool delegate = false; -// if (restaker == _MOCK_ADDRESS) { -// delegate = true; -// // deploy a new restaker -// restaker = _deployNewStub(); -// _operatorRestakers[elOperator] = restaker; -// restakers.push(restaker); -// } - -// _depositAssetIntoStrategy(restaker, amount); - -// if (delegate) -// _delegateToOperator( -// restaker, -// elOperator, -// approverSalt, -// approverSignatureAndExpiry -// ); - -// emit DelegatedTo(restaker, elOperator, amount); -// } - -// /*/////////////////////////////////////// -// ///////// Withdrawal functions ///////// -// /////////////////////////////////////*/ - -// function __beforeWithdraw(address receiver, uint256 iShares) internal view { -// if (iShares == 0) revert NullParams(); -// if (receiver == address(0)) revert NullParams(); - -// if (targetCapacity == 0) revert InceptionOnPause(); -// if (treasury == address(0)) revert InceptionOnPause(); -// if (!_verifyDelegated()) revert InceptionOnPause(); -// } - -// /// @dev Performs burning iToken from mgs.sender -// /// @dev Creates a withdrawal requests based on the current ratio -// /// @param iShares is measured in Inception token(shares) -// function withdraw( -// uint256 iShares, -// address receiver -// ) external whenNotPaused nonReentrant { -// __beforeWithdraw(receiver, iShares); -// address claimer = msg.sender; -// uint256 amount = convertToAssets(iShares); -// if (amount < minAmount) revert LowerMinAmount(minAmount); - -// // burn Inception token in view of the current ratio -// inceptionToken.burn(claimer, iShares); - -// // update global state and claimer's state -// totalAmountToWithdraw += amount; -// Withdrawal storage genRequest = _claimerWithdrawals[receiver]; -// genRequest.amount += _getAssetReceivedAmount(amount); -// claimerWithdrawalsQueue.push( -// Withdrawal({ -// epoch: claimerWithdrawalsQueue.length, -// receiver: receiver, -// amount: _getAssetReceivedAmount(amount) -// }) -// ); - -// emit Withdraw(claimer, receiver, claimer, amount, iShares); -// } - -// function redeem(address receiver) external whenNotPaused nonReentrant { -// (bool isAble, uint256[] memory availableWithdrawals) = isAbleToRedeem( -// receiver -// ); -// if (!isAble) revert IsNotAbleToRedeem(); - -// uint256 numOfWithdrawals = availableWithdrawals.length; -// uint256[] memory redeemedWithdrawals = new uint256[](numOfWithdrawals); - -// Withdrawal storage genRequest = _claimerWithdrawals[receiver]; -// uint256 redeemedAmount; -// for (uint256 i = 0; i < numOfWithdrawals; ++i) { -// uint256 withdrawalNum = availableWithdrawals[i]; -// Withdrawal storage request = claimerWithdrawalsQueue[withdrawalNum]; -// uint256 amount = request.amount; -// // update the genRequest and the global state -// genRequest.amount -= amount; - -// totalAmountToWithdraw -= _getAssetWithdrawAmount(amount); -// redeemReservedAmount -= amount; -// redeemedAmount += amount; -// redeemedWithdrawals[i] = withdrawalNum; - -// delete claimerWithdrawalsQueue[availableWithdrawals[i]]; -// } - -// // let's update the lowest epoch associated with the claimer -// genRequest.epoch = availableWithdrawals[numOfWithdrawals - 1]; - -// _transferAssetTo(receiver, redeemedAmount); - -// emit RedeemedRequests(redeemedWithdrawals); -// emit Redeem(msg.sender, receiver, redeemedAmount); -// } - -// /*///////////////////////////////////////////// -// ///////// Flash Withdrawal functions ///////// -// ///////////////////////////////////////////*/ - -// /// @dev Performs burning iToken from mgs.sender -// /// @dev Creates a withdrawal requests based on the current ratio -// /// @param iShares is measured in Inception token(shares) -// function flashWithdraw( -// uint256 iShares, -// address receiver -// ) external whenNotPaused nonReentrant { -// __beforeWithdraw(receiver, iShares); - -// address claimer = msg.sender; -// uint256 amount = convertToAssets(iShares); - -// if (amount < minAmount) revert LowerMinAmount(minAmount); - -// // burn Inception token in view of the current ratio -// inceptionToken.burn(claimer, iShares); - -// uint256 fee = calculateFlashWithdrawFee(amount); -// if (fee == 0) revert ZeroFlashWithdrawFee(); -// uint256 protocolWithdrawalFee = (fee * protocolFee) / MAX_PERCENT; - -// amount -= fee; -// depositBonusAmount += (fee - protocolWithdrawalFee); - -// /// @notice instant transfer fee to the treasury -// _transferAssetTo(treasury, protocolWithdrawalFee); -// /// @notice instant transfer amount to the receiver -// _transferAssetTo(receiver, amount); - -// emit FlashWithdraw(claimer, receiver, claimer, amount, iShares, fee); -// } - -// /// @notice Function to calculate deposit bonus based on the utilization rate -// function calculateDepositBonus( -// uint256 amount -// ) public view returns (uint256) { -// return -// InceptionLibrary.calculateDepositBonus( -// amount, -// getFlashCapacity(), -// (_getTargetCapacity() * depositUtilizationKink) / MAX_PERCENT, -// optimalBonusRate, -// maxBonusRate, -// _getTargetCapacity() -// ); -// } - -// /// @dev Function to calculate flash withdrawal fee based on the utilization rate -// function calculateFlashWithdrawFee( -// uint256 amount -// ) public view returns (uint256) { -// uint256 capacity = getFlashCapacity(); -// if (amount > capacity) revert InsufficientCapacity(capacity); - -// return -// InceptionLibrary.calculateWithdrawalFee( -// amount, -// capacity, -// (_getTargetCapacity() * withdrawUtilizationKink) / MAX_PERCENT, -// optimalWithdrawalRate, -// maxFlashFeeRate, -// _getTargetCapacity() -// ); -// } - -// /*////////////////////////////// -// ////// Factory functions ////// -// ////////////////////////////*/ - -// function _deployNewStub() internal returns (address) { -// if (_stakerImplementation == address(0)) revert ImplementationNotSet(); -// // deploy new beacon proxy and do init call -// bytes memory data = abi.encodeWithSignature( -// "initialize(address,address,address,address,address)", -// delegationManager, -// strategyManager, -// strategy, -// _asset, -// _operator -// ); -// address deployedAddress = address(new BeaconProxy(address(this), data)); - -// IOwnable asOwnable = IOwnable(deployedAddress); -// asOwnable.transferOwnership(owner()); - -// emit RestakerDeployed(deployedAddress); -// return deployedAddress; -// } - -// function implementation() external view returns (address) { -// return _stakerImplementation; -// } - -// function upgradeTo( -// address newImplementation -// ) external whenNotPaused onlyOwner { -// if (!Address.isContract(newImplementation)) revert NotContract(); - -// emit ImplementationUpgraded(_stakerImplementation, newImplementation); -// _stakerImplementation = newImplementation; -// } - -// function isAbleToRedeem( -// address claimer -// ) public view returns (bool able, uint256[] memory) { -// // get the general request -// uint256 index; -// Withdrawal memory genRequest = _claimerWithdrawals[claimer]; -// uint256[] memory availableWithdrawals = new uint256[]( -// epoch - genRequest.epoch -// ); -// if (genRequest.amount == 0) return (false, availableWithdrawals); - -// for (uint256 i = 0; i < epoch; ++i) { -// if (claimerWithdrawalsQueue[i].receiver == claimer) { -// able = true; -// availableWithdrawals[index] = i; -// ++index; -// } -// } -// // decrease arrays -// if (availableWithdrawals.length - index > 0) -// assembly { -// mstore(availableWithdrawals, index) -// } - -// return (able, availableWithdrawals); -// } - -// function ratio() public view returns (uint256) { -// return ratioFeed.getRatioFor(address(inceptionToken)); -// } - -// function getDelegatedTo( -// address elOperator -// ) external view returns (uint256) { -// return strategy.userUnderlyingView(_operatorRestakers[elOperator]); -// } - -// function getPendingWithdrawalOf( -// address claimer -// ) external view returns (uint256) { -// return _claimerWithdrawals[claimer].amount; -// } - -// function _verifyDelegated() internal view returns (bool) { -// for (uint256 i = 0; i < restakers.length; i++) { -// if (restakers[i] == address(0)) { -// continue; -// } -// if (!delegationManager.isDelegated(restakers[i])) return false; -// } - -// if ( -// strategy.userUnderlyingView(address(this)) > 0 && -// !delegationManager.isDelegated(address(this)) -// ) return false; - -// return true; -// } - -// function maxDeposit(address /*receiver*/) external pure returns (uint256) { -// return type(uint256).max; -// } - -// function maxRedeem( -// address account -// ) external view returns (uint256 maxShares) { -// return -// convertToAssets(IERC20(address(inceptionToken)).balanceOf(account)); -// } - -// /*////////////////////////////// -// ////// Convert functions ////// -// ////////////////////////////*/ - -// function convertToShares( -// uint256 assets -// ) public view returns (uint256 shares) { -// return Convert.multiplyAndDivideFloor(assets, ratio(), 1e18); -// } - -// function convertToAssets( -// uint256 iShares -// ) public view returns (uint256 assets) { -// return Convert.multiplyAndDivideFloor(iShares, 1e18, ratio()); -// } - -// /*////////////////////////// -// ////// SET functions ////// -// ////////////////////////*/ - -// function setDepositBonusParams( -// uint64 newMaxBonusRate, -// uint64 newOptimalBonusRate, -// uint64 newDepositUtilizationKink -// ) external onlyOwner { -// if (newMaxBonusRate > MAX_PERCENT) -// revert ParameterExceedsLimits(newMaxBonusRate); -// if (newOptimalBonusRate > MAX_PERCENT) -// revert ParameterExceedsLimits(newOptimalBonusRate); -// if (newDepositUtilizationKink > MAX_PERCENT) -// revert ParameterExceedsLimits(newDepositUtilizationKink); - -// maxBonusRate = newMaxBonusRate; -// optimalBonusRate = newOptimalBonusRate; -// depositUtilizationKink = newDepositUtilizationKink; - -// emit DepositBonusParamsChanged( -// newMaxBonusRate, -// newOptimalBonusRate, -// newDepositUtilizationKink -// ); -// } - -// function setFlashWithdrawFeeParams( -// uint64 newMaxFlashFeeRate, -// uint64 newOptimalWithdrawalRate, -// uint64 newWithdrawUtilizationKink -// ) external onlyOwner { -// if (newMaxFlashFeeRate > MAX_PERCENT) -// revert ParameterExceedsLimits(newMaxFlashFeeRate); -// if (newOptimalWithdrawalRate > MAX_PERCENT) -// revert ParameterExceedsLimits(newOptimalWithdrawalRate); -// if (newWithdrawUtilizationKink > MAX_PERCENT) -// revert ParameterExceedsLimits(newWithdrawUtilizationKink); - -// maxFlashFeeRate = newMaxFlashFeeRate; -// optimalWithdrawalRate = newOptimalWithdrawalRate; -// withdrawUtilizationKink = newWithdrawUtilizationKink; - -// emit WithdrawFeeParamsChanged( -// newMaxFlashFeeRate, -// newOptimalWithdrawalRate, -// newWithdrawUtilizationKink -// ); -// } - -// function setProtocolFee(uint64 newProtocolFee) external onlyOwner { -// if (newProtocolFee >= MAX_PERCENT) -// revert ParameterExceedsLimits(newProtocolFee); - -// emit ProtocolFeeChanged(protocolFee, newProtocolFee); -// protocolFee = newProtocolFee; -// } - -// function setTreasuryAddress(address newTreasury) external onlyOwner { -// if (newTreasury == address(0)) revert NullParams(); - -// emit TreasuryChanged(treasury, newTreasury); -// treasury = newTreasury; -// } - -// function setRatioFeed(IInceptionRatioFeed newRatioFeed) external onlyOwner { -// if (address(newRatioFeed) == address(0)) revert NullParams(); - -// emit RatioFeedChanged(address(ratioFeed), address(newRatioFeed)); -// ratioFeed = newRatioFeed; -// } - -// function setOperator(address newOperator) external onlyOwner { -// if (newOperator == address(0)) revert NullParams(); - -// emit OperatorChanged(_operator, newOperator); -// _operator = newOperator; -// } - -// function setMinAmount(uint256 newMinAmount) external onlyOwner { -// emit MinAmountChanged(minAmount, newMinAmount); -// minAmount = newMinAmount; -// } - -// function setName(string memory newVaultName) external onlyOwner { -// if (bytes(newVaultName).length == 0) revert NullParams(); - -// emit NameChanged(name, newVaultName); -// name = newVaultName; -// } - -// function addELOperator(address newELOperator) external onlyOwner { -// if (!delegationManager.isOperator(newELOperator)) -// revert NotEigenLayerOperator(); - -// if (_operatorRestakers[newELOperator] != address(0)) -// revert EigenLayerOperatorAlreadyExists(); - -// _operatorRestakers[newELOperator] = _MOCK_ADDRESS; -// emit ELOperatorAdded(newELOperator); -// } - -// /*/////////////////////////////// -// ////// Pausable functions ////// -// /////////////////////////////*/ - -// function pause() external onlyOwner { -// _pause(); -// } - -// function unpause() external onlyOwner { -// _unpause(); -// } -// } diff --git a/projects/vaults/contracts/vaults/Symbiotic/InceptionVault_S.sol b/projects/vaults/contracts/vaults/Symbiotic/InceptionVault_S.sol index 1580dab2..606ca221 100644 --- a/projects/vaults/contracts/vaults/Symbiotic/InceptionVault_S.sol +++ b/projects/vaults/contracts/vaults/Symbiotic/InceptionVault_S.sol @@ -1,37 +1,39 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.28; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; - -import {SymbioticHandler, IIMellowRestaker, IERC20} from "../../symbiotic-handler/SymbioticHandler.sol"; -import {IInceptionVault_S} from "../../interfaces/symbiotic-vault/IInceptionVault_S.sol"; -import {IInceptionToken} from "../../interfaces/common/IInceptionToken.sol"; -import {IInceptionRatioFeed} from "../../interfaces/common/IInceptionRatioFeed.sol"; -import {InceptionLibrary} from "../../lib/InceptionLibrary.sol"; +import {AdapterHandler, IERC20} from "../../adapter-handler/AdapterHandler.sol"; import {Convert} from "../../lib/Convert.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IInceptionRatioFeed} from "../../interfaces/common/IInceptionRatioFeed.sol"; +import {IInceptionToken} from "../../interfaces/common/IInceptionToken.sol"; +import {IInceptionVault_S} from "../../interfaces/symbiotic-vault/IInceptionVault_S.sol"; +import {InceptionLibrary} from "../../lib/InceptionLibrary.sol"; +import {IWithdrawalQueue} from "../../interfaces/common/IWithdrawalQueue.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -/// @author The InceptionLRT team -/// @title The InceptionVault_S contract -/// @notice Aims to maximize the profit of Mellow asset. -contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { +/** + * @title InceptionVault_S + * @author The InceptionLRT team + * @notice Aims to maximize the profit of deposited asset + * @dev Handles deposits, withdrawals, and flash withdrawals with bonus and fee mechanisms + */ +contract InceptionVault_S is AdapterHandler, IInceptionVault_S { using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; /// @dev Inception restaking token IInceptionToken public inceptionToken; /// @dev Reduces rounding issues + /// @custom:oz-renamed-from minAmount uint256 public withdrawMinAmount; - mapping(address => Withdrawal) private _claimerWithdrawals; + mapping(address => __deprecated_Withdrawal) private __deprecated_claimerWithdrawals; /// @dev the unique InceptionVault name string public name; - /** - * @dev Flash withdrawal params - */ - /// @dev 100% uint64 public constant MAX_PERCENT = 100 * 1e8; @@ -53,17 +55,28 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { uint256 public flashMinAmount; uint256 public depositMinAmount; - mapping(address => uint256) private _traversalEpoch; + mapping(address => uint256) private __deprecated_withdrawals; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() payable { + _disableInitializers(); + } - function __InceptionVault_init( + /** + * @dev Initializes the vault with basic parameters + * @param vaultName Name of the vault + * @param operatorAddress Address of the operator + * @param assetAddress Address of the asset token + * @param _inceptionToken Address of the Inception token + */ + function initialize( string memory vaultName, address operatorAddress, IERC20 assetAddress, - IInceptionToken _inceptionToken, - IIMellowRestaker _mellowRestaker - ) internal { + IInceptionToken _inceptionToken + ) public initializer { __Ownable2Step_init(); - __SymbioticHandler_init(assetAddress, _mellowRestaker); + __AdapterHandler_init(assetAddress); name = vaultName; _operator = operatorAddress; @@ -92,161 +105,178 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { ////// Deposit functions ////// ////////////////////////////*/ + /** + * @dev Internal function to validate deposit parameters + * @param receiver Address of the receiver + * @param amount Amount to deposit + */ function __beforeDeposit(address receiver, uint256 amount) internal view { if (receiver == address(0)) revert NullParams(); if (amount < depositMinAmount) revert LowerMinAmount(depositMinAmount); - - if (targetCapacity == 0) revert InceptionOnPause(); + if (targetCapacity == 0) revert NullParams(); } + /** + * @dev Internal function to validate deposit result + * @param iShares Amount of shares minted + */ function __afterDeposit(uint256 iShares) internal pure { if (iShares == 0) revert DepositInconsistentResultedState(); } - /// @dev Transfers the msg.sender's assets to the vault. - /// @dev Mints Inception tokens in accordance with the current ratio. - /// @dev Issues the tokens to the specified receiver address. - /** @dev See {IERC4626-deposit}. */ - function deposit(uint256 amount, address receiver) - external - nonReentrant - whenNotPaused - returns (uint256) - { - return _deposit(amount, msg.sender, receiver); + /** + * @dev Deposits assets into the vault and mints Inception tokens. See {IERC4626-deposit} + * @notice It is recommended to use the deposit function with the `minOut` parameter to protect against slippage. + * @param amount Amount of assets to deposit + * @param receiver Address to receive the minted tokens + * @return Amount of shares minted + */ + function deposit( + uint256 amount, + address receiver + ) external nonReentrant whenNotPaused returns (uint256) { + return _deposit(amount, msg.sender, receiver, 0); + } + + /** + * @dev Deposits assets into the vault with minimum output check. See {IERC4626-deposit} + * @param amount Amount of assets to deposit + * @param receiver Address to receive the minted tokens + * @param minOut Minimum amount of shares to receive + * @return Amount of shares minted + */ + function deposit( + uint256 amount, + address receiver, + uint256 minOut + ) external nonReentrant whenNotPaused returns (uint256) { + return _deposit(amount, msg.sender, receiver, minOut); } - /// @notice The deposit function but with a referral code + /** + * @dev Deposits assets with a referral code + * @param amount Amount of assets to deposit + * @param receiver Address to receive the minted tokens + * @param code Referral code + * @return Amount of shares minted + */ function depositWithReferral( uint256 amount, address receiver, bytes32 code ) external nonReentrant whenNotPaused returns (uint256) { emit ReferralCode(code); - return _deposit(amount, msg.sender, receiver); + return _deposit(amount, msg.sender, receiver, 0); + } + + /** + * @dev Deposits assets with a referral code and minimum output check + * @param amount Amount of assets to deposit + * @param receiver Address to receive the minted tokens + * @param code Referral code + * @param minOut Minimum amount of shares to receive + * @return Amount of shares minted + */ + function depositWithReferral( + uint256 amount, + address receiver, + bytes32 code, + uint256 minOut + ) external nonReentrant whenNotPaused returns (uint256) { + emit ReferralCode(code); + return _deposit(amount, msg.sender, receiver, minOut); } + /** + * @dev Internal function to handle deposits + * @param amount Amount of assets to deposit + * @param sender Address of the sender + * @param receiver Address to receive the minted tokens + * @param minOut Minimum amount of shares to receive + * @return Amount of shares minted + */ function _deposit( uint256 amount, address sender, - address receiver + address receiver, + uint256 minOut ) internal returns (uint256) { - // transfers assets from the sender and returns the received amount - // the actual received amount might slightly differ from the specified amount, - // approximately by -2 wei __beforeDeposit(receiver, amount); - uint256 depositedBefore = totalAssets(); + + // calculate deposit bonus uint256 depositBonus; - uint256 availableBonusAmount = depositBonusAmount; - if (availableBonusAmount > 0) { + if (depositBonusAmount > 0) { depositBonus = calculateDepositBonus(amount); - if (depositBonus > availableBonusAmount) { - depositBonus = availableBonusAmount; - depositBonusAmount = 0; - } else { - depositBonusAmount -= depositBonus; + if (depositBonus > depositBonusAmount) { + depositBonus = depositBonusAmount; } + emit DepositBonus(depositBonus); } - // get the amount from the sender - _transferAssetFrom(sender, amount); - amount = totalAssets() - depositedBefore; + + // calculate share to mint uint256 iShares = convertToShares(amount + depositBonus); + if (minOut > 0 && iShares < minOut) revert SlippageMinOut(minOut, iShares); + + // update deposit bonus state + depositBonusAmount -= depositBonus; + + // get the amount from the sender + _asset.safeTransferFrom(sender, address(this), amount); + + // mint new shares inceptionToken.mint(receiver, iShares); + __afterDeposit(iShares); emit Deposit(sender, receiver, amount, iShares); + return iShares; } - /** @dev See {IERC4626-mint}. */ - function mint(uint256 shares, address receiver) - external - nonReentrant - whenNotPaused - returns (uint256) - { - uint256 maxShares = maxMint(msg.sender); + /** + * @dev Mints shares for assets. See {IERC4626-mint}. + * @param shares Amount of shares to mint + * @param receiver Address to receive the minted shares + * @return Amount of assets deposited + */ + function mint( + uint256 shares, + address receiver + ) external nonReentrant whenNotPaused returns (uint256) { + uint256 maxShares = maxMint(receiver); if (shares > maxShares) revert ExceededMaxMint(receiver, shares, maxShares); - uint256 assetsAmount = convertToAssets(shares); - _deposit(assetsAmount, msg.sender, receiver); + uint256 assetsAmount = previewMint(shares); + if (_deposit(assetsAmount, msg.sender, receiver, 0) < shares) revert MintedLess(); return assetsAmount; } - /*///////////////////////////////// - ////// Delegation functions ////// - ///////////////////////////////*/ - - /// @dev Sends underlying to a single mellow vault - function delegateToSymbioticVault(address vault, uint256 amount) - external - nonReentrant - whenNotPaused - onlyOperator - { - if (vault == address(0) || amount == 0) revert NullParams(); - - /// TODO - // _beforeDeposit(amount); - _depositAssetIntoSymbiotic(amount, vault); - - emit DelegatedTo(address(symbioticRestaker), vault, amount); - return; - } - - /// @dev Sends underlying to a single mellow vault - function delegateToMellowVault( - address mellowVault, - uint256 amount, - uint256 deadline - ) external nonReentrant whenNotPaused onlyOperator { - if (mellowVault == address(0) || amount == 0) revert NullParams(); - - _beforeDeposit(amount); - _depositAssetIntoMellow(amount, mellowVault, deadline); - - emit DelegatedTo(address(mellowRestaker), mellowVault, amount); - return; - } - - /// @dev Sends all underlying to all mellow vaults based on allocation - function delegateAutoMellow(uint256 deadline) - external - nonReentrant - whenNotPaused - onlyOperator - { - uint256 balance = getFreeBalance(); - _asset.safeIncreaseAllowance(address(mellowRestaker), balance); - (uint256 amount, uint256 lpAmount) = mellowRestaker.delegate( - balance, - deadline - ); - - emit Delegated(address(mellowRestaker), amount, lpAmount); - } - /*/////////////////////////////////////// ///////// Withdrawal functions ///////// /////////////////////////////////////*/ + /** + * @dev Internal function to validate withdrawal parameters + * @param receiver Address of the receiver + * @param iShares Amount of shares to withdraw + */ function __beforeWithdraw(address receiver, uint256 iShares) internal view { if (iShares == 0) revert NullParams(); - if (receiver == address(0)) revert NullParams(); - - if (targetCapacity == 0) revert InceptionOnPause(); + if (receiver == address(0)) revert InvalidAddress(); + if (targetCapacity == 0) revert NullParams(); } - /// @dev Performs burning iToken from mgs.sender - /// @dev Creates a withdrawal requests based on the current ratio - /// @param iShares is measured in Inception token(shares) - function withdraw(uint256 iShares, address receiver) - external - whenNotPaused - nonReentrant - { + /** + * @dev Performs burning iToken from mgs.sender. Creates a withdrawal requests based on the current ratio + * @param iShares Amount of shares to burn + * @param receiver Address to receive the withdrawn assets + */ + function withdraw( + uint256 iShares, + address receiver + ) external whenNotPaused nonReentrant { __beforeWithdraw(receiver, iShares); address claimer = msg.sender; uint256 amount = convertToAssets(iShares); @@ -255,33 +285,27 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { // burn Inception token in view of the current ratio inceptionToken.burn(claimer, iShares); - - // update global state and claimer's state - totalAmountToWithdraw += amount; - Withdrawal storage genRequest = _claimerWithdrawals[receiver]; - genRequest.amount += _getAssetReceivedAmount(amount); - claimerWithdrawalsQueue.push( - Withdrawal({ - epoch: claimerWithdrawalsQueue.length, - receiver: receiver, - amount: _getAssetReceivedAmount(amount) - }) - ); + // add withdrawal request + withdrawalQueue.request(receiver, iShares); emit Withdraw(claimer, receiver, claimer, amount, iShares); } - /** @dev See {IERC4626-redeem}. */ + /** + * @dev Redeems shares for assets. See {IERC4626-redeem}. + * @param shares Amount of shares to redeem + * @param receiver Address to receive the assets + * @param owner Address of the share owner + * @return Amount of assets withdrawn + */ function redeem( uint256 shares, address receiver, address owner - ) external nonReentrant whenNotPaused returns (uint256 assets) { + ) external nonReentrant whenNotPaused returns (uint256) { if (owner != msg.sender) revert MsgSenderIsNotOwner(); __beforeWithdraw(receiver, shares); - assets = convertToAssets(shares); - uint256 fee; - (assets, fee) = _flashWithdraw(shares, receiver, owner); + (uint256 assets, uint256 fee) = _flashWithdraw(shares, receiver, owner, 0); emit Withdraw(owner, receiver, owner, assets, shares); emit WithdrawalFee(fee); @@ -289,68 +313,96 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { return assets; } - function redeem(address receiver) external whenNotPaused nonReentrant { - (bool isAble, uint256[] memory availableWithdrawals) = isAbleToRedeem( - receiver - ); - _traversalEpoch[receiver] = epoch - 1; - if (!isAble) revert IsNotAbleToRedeem(); - - uint256 numOfWithdrawals = availableWithdrawals.length; - uint256[] memory redeemedWithdrawals = new uint256[](numOfWithdrawals); - - Withdrawal storage genRequest = _claimerWithdrawals[receiver]; - uint256 redeemedAmount; - for (uint256 i = 0; i < numOfWithdrawals; ++i) { - uint256 withdrawalNum = availableWithdrawals[i]; - Withdrawal storage request = claimerWithdrawalsQueue[withdrawalNum]; - uint256 amount = request.amount; - // update the genRequest and the global state - genRequest.amount -= amount; - - totalAmountToWithdraw -= _getAssetWithdrawAmount(amount); - redeemReservedAmount -= amount; - redeemedAmount += amount; - redeemedWithdrawals[i] = withdrawalNum; - - delete claimerWithdrawalsQueue[availableWithdrawals[i]]; + /** + * @dev Redeems available withdrawals + * @param receiver Address to receive the assets + * @return assets Amount of assets withdrawn + */ + function redeem(address receiver) external whenNotPaused nonReentrant returns (uint256 assets) { + // redeem available withdrawals + assets = withdrawalQueue.redeem(receiver); + if (assets > 0) { + // transfer to receiver + _asset.safeTransfer(receiver, assets); + emit Redeem(msg.sender, receiver, assets); } + } - // let's update the lowest epoch associated with the claimer - genRequest.epoch = availableWithdrawals[numOfWithdrawals - 1]; - - _transferAssetTo(receiver, redeemedAmount); - - emit RedeemedRequests(redeemedWithdrawals); - emit Redeem(msg.sender, receiver, redeemedAmount); + /** + * @dev Redeems available withdrawals for a specific epoch + * @param receiver Address to receive the assets + * @param userEpochIndex Index of the epoch + * @return assets Amount of assets withdrawn + */ + function redeem(address receiver, uint256 userEpochIndex) external whenNotPaused nonReentrant returns (uint256 assets) { + // redeem available withdrawals + assets = withdrawalQueue.redeem(receiver, userEpochIndex); + if (assets > 0) { + // transfer to receiver + _asset.safeTransfer(receiver, assets); + emit Redeem(msg.sender, receiver, assets); + } } /*///////////////////////////////////////////// ///////// Flash Withdrawal functions ///////// ///////////////////////////////////////////*/ - /// @dev Performs burning iToken from mgs.sender - /// @dev Creates a withdrawal requests based on the current ratio - /// @param iShares is measured in Inception token(shares) - function flashWithdraw(uint256 iShares, address receiver) - external - whenNotPaused - nonReentrant - { + /** + * @dev Performs a flash withdrawal with minimum output check + * @param iShares Amount of shares to burn + * @param receiver Address to receive the assets + * @param minOut Minimum amount of assets to receive + */ + function flashWithdraw( + uint256 iShares, + address receiver, + uint256 minOut + ) external whenNotPaused nonReentrant { __beforeWithdraw(receiver, iShares); address claimer = msg.sender; (uint256 amount, uint256 fee) = _flashWithdraw( iShares, receiver, - claimer + claimer, + minOut ); emit FlashWithdraw(claimer, receiver, claimer, amount, iShares, fee); } + /** + * @dev Performs a flash withdrawal + * @param iShares Amount of shares to burn + * @param receiver Address to receive the assets + */ + function flashWithdraw( + uint256 iShares, + address receiver + ) external whenNotPaused nonReentrant { + __beforeWithdraw(receiver, iShares); + address claimer = msg.sender; + (uint256 amount, uint256 fee) = _flashWithdraw( + iShares, + receiver, + claimer, + 0 + ); + emit FlashWithdraw(claimer, receiver, claimer, amount, iShares, fee); + } + + /** + * @dev Internal function to handle flash withdrawals + * @param iShares Amount of shares to burn + * @param receiver Address to receive the assets + * @param owner Address of the share owner + * @param minOut Minimum amount of assets to receive + * @return Amount of assets withdrawn and fee charged + */ function _flashWithdraw( uint256 iShares, address receiver, - address owner + address owner, + uint256 minOut ) private returns (uint256, uint256) { uint256 amount = convertToAssets(iShares); @@ -360,182 +412,233 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { inceptionToken.burn(owner, iShares); uint256 fee = calculateFlashWithdrawFee(amount); - if (fee == 0) revert ZeroFlashWithdrawFee(); uint256 protocolWithdrawalFee = (fee * protocolFee) / MAX_PERCENT; amount -= fee; depositBonusAmount += (fee - protocolWithdrawalFee); /// @notice instant transfer fee to the treasury - if (protocolWithdrawalFee != 0) _transferAssetTo(treasury, protocolWithdrawalFee); + if (protocolWithdrawalFee != 0) + _asset.safeTransfer(treasury, protocolWithdrawalFee); + if (minOut != 0 && amount < minOut) revert SlippageMinOut(minOut, amount); /// @notice instant transfer amount to the receiver - _transferAssetTo(receiver, amount); + _asset.safeTransfer(receiver, amount); return (amount, fee); } - /// @notice Function to calculate deposit bonus based on the utilization rate - function calculateDepositBonus(uint256 amount) - public - view - returns (uint256) - { + /** + * @dev Calculates deposit bonus based on utilization rate + * @param amount Amount of assets to deposit + * @return Amount of bonus to be applied + */ + function calculateDepositBonus( + uint256 amount + ) public view returns (uint256) { uint256 targetCapacity = _getTargetCapacity(); return InceptionLibrary.calculateDepositBonus( - amount, - getFlashCapacity(), - (targetCapacity * depositUtilizationKink) / MAX_PERCENT, - optimalBonusRate, - maxBonusRate, - targetCapacity - ); - } - - /// @dev Function to calculate flash withdrawal fee based on the utilization rate - function calculateFlashWithdrawFee(uint256 amount) - public - view - returns (uint256) - { + amount, + getFlashCapacity(), + (targetCapacity * depositUtilizationKink) / MAX_PERCENT, + optimalBonusRate, + maxBonusRate, + targetCapacity + ); + } + + /** + * @dev Calculates flash withdrawal fee based on utilization rate + * @param amount Amount of assets to withdraw + * @return Amount of fee to be charged + */ + function calculateFlashWithdrawFee( + uint256 amount + ) public view returns (uint256) { uint256 capacity = getFlashCapacity(); if (amount > capacity) revert InsufficientCapacity(capacity); uint256 targetCapacity = _getTargetCapacity(); return InceptionLibrary.calculateWithdrawalFee( - amount, - capacity, - (targetCapacity * withdrawUtilizationKink) / MAX_PERCENT, - optimalWithdrawalRate, - maxFlashFeeRate, - targetCapacity - ); + amount, + capacity, + (targetCapacity * withdrawUtilizationKink) / MAX_PERCENT, + optimalWithdrawalRate, + maxFlashFeeRate, + targetCapacity + ); + } + + /** + * @dev Migrates deposit bonus to a new vault + * @param newVault Address of the new vault + */ + function migrateDepositBonus(address newVault) external onlyOwner { + require(getTotalDelegated() == 0, ValueZero()); + require(newVault != address(0), InvalidAddress()); + require(depositBonusAmount > 0, NullParams()); + + uint256 amount = depositBonusAmount; + depositBonusAmount = 0; + + _asset.safeTransfer(newVault, amount); + + emit DepositBonusTransferred(newVault, amount); } /*////////////////////////////// ////// Factory functions ////// ////////////////////////////*/ - function isAbleToRedeem(address claimer) - public - view - returns (bool able, uint256[] memory) - { - // get the general request - uint256 index; - Withdrawal memory genRequest = _claimerWithdrawals[claimer]; - uint256[] memory availableWithdrawals = new uint256[]( - epoch - genRequest.epoch - ); - if (genRequest.amount == 0) return (false, availableWithdrawals); - - for (uint256 i = _traversalEpoch[claimer]; i < epoch; ++i) { - if (claimerWithdrawalsQueue[i].receiver == claimer) { - able = true; - availableWithdrawals[index] = i; - ++index; - } - } - // decrease arrays - if (availableWithdrawals.length - index > 0) - assembly { - mstore(availableWithdrawals, index) - } - - return (able, availableWithdrawals); + function isAbleToRedeem( + address claimer + ) public view returns (bool, uint256[] memory) { + return withdrawalQueue.isRedeemable(claimer); } + /** + * @dev Gets the current ratio for the Inception token + * @return Current ratio value + */ function ratio() public view returns (uint256) { - return ratioFeed.getRatioFor(address(inceptionToken)); - } + uint256 totalSupply = IERC20(address(inceptionToken)).totalSupply(); + + uint256 numeral = totalSupply + totalSharesToWithdraw(); + uint256 denominator = getTotalDeposited(); + + if (denominator == 0 || numeral == 0) { + return 1e18; + } - function getDelegatedTo(address mellowVault) - external - view - returns (uint256) - { - return mellowRestaker.getDeposited(mellowVault); + return (numeral * 1e18) / denominator; } - function getPendingWithdrawalOf(address claimer) - external - view - returns (uint256) - { - return _claimerWithdrawals[claimer].amount; + /** + * @dev Gets the pending withdrawal amount for a claimer + * @param claimer Address of the claimer + * @return Amount of pending withdrawals + */ + function getPendingWithdrawalOf( + address claimer + ) external view returns (uint256) { + return withdrawalQueue.getPendingWithdrawalOf(claimer); } - /** @dev See {IERC20Metadata-decimals}. */ + /** + * @dev Gets the decimals of the Inception token. See {IERC4626-maxDeposit} + * @return Number of decimals + */ function decimals() public view returns (uint8) { return IERC20Metadata(address(inceptionToken)).decimals(); } - /** @dev See {IERC4626-maxDeposit}. */ + /** + * @dev Gets the maximum amount that can be deposited. See {IERC4626-maxDeposit} + * @param receiver Address of the receiver + * @return Maximum deposit amount + */ function maxDeposit(address receiver) public view returns (uint256) { - return !paused() ? IERC20(asset()).balanceOf(receiver) : 0; + return !paused() ? type(uint256).max : 0; } - /** @dev See {IERC4626-maxMint}. */ + /** + * @dev Gets the maximum amount of shares that can be minted. See {IERC4626-maxMint}. + * @param receiver Address of the receiver + * @return Maximum mint amount + */ function maxMint(address receiver) public view returns (uint256) { - return - !paused() ? previewDeposit(IERC20(asset()).balanceOf(receiver)) : 0; + return !paused() ? type(uint256).max : 0; } - /** @dev See {IERC4626-maxRedeem}. */ + /** + * @dev Gets the maximum amount of shares that can be redeemed. See {IERC4626-maxRedeem}. + * @param owner Address of the share owner + * @return Maximum redeem amount + */ function maxRedeem(address owner) public view returns (uint256) { if (paused()) { return 0; - } else { - uint256 ownerShares = IERC20(address(inceptionToken)).balanceOf( - owner - ); - uint256 flashShares = convertToShares(getFlashCapacity()); - return flashShares > ownerShares ? ownerShares : flashShares; } + + uint256 ownerShares = IERC20(address(inceptionToken)).balanceOf(owner); + uint256 flashShares = convertToShares(getFlashCapacity()); + return flashShares > ownerShares ? ownerShares : flashShares; } - /** @dev See {IERC4626-previewDeposit}. */ + /** + * @dev Previews the amount of shares to be received for a deposit. See {IERC4626-previewDeposit}. + * @param assets Amount of assets to deposit + * @return Amount of shares to be received + */ function previewDeposit(uint256 assets) public view returns (uint256) { + if (assets < depositMinAmount) revert LowerMinAmount(depositMinAmount); + uint256 depositBonus; if (depositBonusAmount > 0) { depositBonus = calculateDepositBonus(assets); - if (depositBonus > depositBonusAmount) - depositBonus = depositBonusAmount; + if (depositBonus > depositBonusAmount) depositBonus = depositBonusAmount; } return convertToShares(assets + depositBonus); } - /** @dev See {IERC4626-previewRedeem}. */ - function previewRedeem(uint256 shares) - public - view - returns (uint256 assets) - { - return - convertToAssets(shares) - - calculateFlashWithdrawFee(convertToAssets(shares)); + /** + * @dev Previews the amount of assets needed to mint shares. See {IERC4626-previewMint}. + * @param shares Amount of shares to mint + * @return Amount of assets needed + */ + function previewMint(uint256 shares) public view returns (uint256) { + uint256 assets = Convert.multiplyAndDivideCeil(shares, 1e18, ratio()); + if (assets < depositMinAmount) revert LowerMinAmount(depositMinAmount); + return assets; + } + + /** + * @dev Previews the amount of assets to be received for redeeming shares. {IERC4626-previewRedeem}. + * @param shares Amount of shares to redeem + * @return assets Amount of assets to be received + */ + function previewRedeem( + uint256 shares + ) public view returns (uint256 assets) { + uint256 amount = convertToAssets(shares); + uint256 capacity = getFlashCapacity(); + uint256 targetCapacity = _getTargetCapacity(); + uint256 flash = amount <= capacity ? capacity : amount; + + return amount - InceptionLibrary.calculateWithdrawalFee( + amount, + flash, + (targetCapacity * withdrawUtilizationKink) / MAX_PERCENT, + optimalWithdrawalRate, + maxFlashFeeRate, + targetCapacity + ); } /*////////////////////////////// ////// Convert functions ////// ////////////////////////////*/ - /** @dev See {IERC4626-convertToShares}. */ - function convertToShares(uint256 assets) - public - view - returns (uint256 shares) - { + /** + * @dev Converts assets to shares. See {IERC4626-convertToShares}. + * @param assets Amount of assets + * @return shares Amount of shares + */ + function convertToShares( + uint256 assets + ) public view returns (uint256 shares) { return Convert.multiplyAndDivideFloor(assets, ratio(), 1e18); } - /** @dev See {IERC4626-convertToAssets}. */ - function convertToAssets(uint256 iShares) - public - view - returns (uint256 assets) - { + /** + * @dev Converts shares to assets. See {IERC4626-convertToAssets}. + * @param iShares Amount of shares + * @return assets Amount of assets + */ + function convertToAssets( + uint256 iShares + ) public view returns (uint256 assets) { return Convert.multiplyAndDivideFloor(iShares, 1e18, ratio()); } @@ -543,6 +646,13 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { ////// SET functions ////// ////////////////////////*/ + /** + * @dev Sets deposit bonus parameters + * @param newMaxBonusRate New maximum bonus rate + * @param newOptimalBonusRate New optimal bonus rate + * @param newDepositUtilizationKink New deposit utilization kink + * @notice Be careful: settings are not validated to conform to the expected curve. + */ function setDepositBonusParams( uint64 newMaxBonusRate, uint64 newOptimalBonusRate, @@ -554,6 +664,7 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { revert ParameterExceedsLimits(newOptimalBonusRate); if (newDepositUtilizationKink > MAX_PERCENT) revert ParameterExceedsLimits(newDepositUtilizationKink); + if (newOptimalBonusRate > newMaxBonusRate) revert InconsistentData(); maxBonusRate = newMaxBonusRate; optimalBonusRate = newOptimalBonusRate; @@ -566,6 +677,13 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { ); } + /** + * @dev Sets flash withdrawal fee parameters + * @param newMaxFlashFeeRate New maximum flash fee rate + * @param newOptimalWithdrawalRate New optimal withdrawal rate + * @param newWithdrawUtilizationKink New withdrawal utilization kink + * @notice Be careful: settings are not validated to conform to the expected curve. + */ function setFlashWithdrawFeeParams( uint64 newMaxFlashFeeRate, uint64 newOptimalWithdrawalRate, @@ -577,6 +695,8 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { revert ParameterExceedsLimits(newOptimalWithdrawalRate); if (newWithdrawUtilizationKink > MAX_PERCENT) revert ParameterExceedsLimits(newWithdrawUtilizationKink); + if (newOptimalWithdrawalRate > newMaxFlashFeeRate) + revert InconsistentData(); maxFlashFeeRate = newMaxFlashFeeRate; optimalWithdrawalRate = newOptimalWithdrawalRate; @@ -589,6 +709,10 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { ); } + /** + * @dev Sets the protocol fee + * @param newProtocolFee New protocol fee value + */ function setProtocolFee(uint64 newProtocolFee) external onlyOwner { if (newProtocolFee >= MAX_PERCENT) revert ParameterExceedsLimits(newProtocolFee); @@ -597,6 +721,10 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { protocolFee = newProtocolFee; } + /** + * @dev Sets the treasury address + * @param newTreasury New treasury address + */ function setTreasuryAddress(address newTreasury) external onlyOwner { if (newTreasury == address(0)) revert NullParams(); @@ -604,6 +732,10 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { treasury = newTreasury; } + /** + * @dev Sets the ratio feed + * @param newRatioFeed New ratio feed address + */ function setRatioFeed(IInceptionRatioFeed newRatioFeed) external onlyOwner { if (address(newRatioFeed) == address(0)) revert NullParams(); @@ -611,6 +743,10 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { ratioFeed = newRatioFeed; } + /** + * @dev Sets the operator address + * @param newOperator New operator address + */ function setOperator(address newOperator) external onlyOwner { if (newOperator == address(0)) revert NullParams(); @@ -618,24 +754,40 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { _operator = newOperator; } + /** + * @dev Sets the minimum withdrawal amount + * @param newMinAmount New minimum withdrawal amount + */ function setWithdrawMinAmount(uint256 newMinAmount) external onlyOwner { if (newMinAmount == 0) revert NullParams(); emit WithdrawMinAmountChanged(withdrawMinAmount, newMinAmount); withdrawMinAmount = newMinAmount; } + /** + * @dev Sets the minimum deposit amount + * @param newMinAmount New minimum deposit amount + */ function setDepositMinAmount(uint256 newMinAmount) external onlyOwner { if (newMinAmount == 0) revert NullParams(); emit DepositMinAmountChanged(depositMinAmount, newMinAmount); depositMinAmount = newMinAmount; } + /** + * @dev Sets the minimum flash withdrawal amount + * @param newMinAmount New minimum flash withdrawal amount + */ function setFlashMinAmount(uint256 newMinAmount) external onlyOwner { if (newMinAmount == 0) revert NullParams(); emit FlashMinAmountChanged(flashMinAmount, newMinAmount); flashMinAmount = newMinAmount; } + /** + * @dev Sets the vault name + * @param newVaultName New vault name + */ function setName(string memory newVaultName) external onlyOwner { if (bytes(newVaultName).length == 0) revert NullParams(); @@ -643,14 +795,32 @@ contract InceptionVault_S is SymbioticHandler, IInceptionVault_S { name = newVaultName; } + /** + * @dev Sets the withdrawal queue + * @param newWithdrawalQueue New withdrawal queue address + * @notice Ensure the protocol was paused during deployment of the new withdrawal queue + * if the previous one contained legacy withdrawals.. + */ + function setWithdrawalQueue(IWithdrawalQueue newWithdrawalQueue) external onlyOwner { + if (address(newWithdrawalQueue) == address(0)) revert NullParams(); + withdrawalQueue = newWithdrawalQueue; + emit WithdrawalQueueChanged(address(withdrawalQueue)); + } + /*/////////////////////////////// ////// Pausable functions ////// /////////////////////////////*/ + /** + * @dev Pauses the contract + */ function pause() external onlyOwner { _pause(); } + /** + * @dev Unpauses the contract + */ function unpause() external onlyOwner { _unpause(); } diff --git a/projects/vaults/contracts/vaults/Symbiotic/vault_e2/InStrategyBaseVault_E2.sol b/projects/vaults/contracts/vaults/Symbiotic/vault_e2/InStrategyBaseVault_E2.sol deleted file mode 100644 index e105332a..00000000 --- a/projects/vaults/contracts/vaults/Symbiotic/vault_e2/InStrategyBaseVault_E2.sol +++ /dev/null @@ -1,42 +0,0 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.28; - -// import {InceptionStrategyBaseVault, IStrategyManager, IInceptionToken, IStrategy, IERC20} from "../InceptionBasicStrategyVault.sol"; - -// /// @author The InceptionLRT team -// contract InStrategyBaseVault_E2 is InceptionStrategyBaseVault { -// /// @custom:oz-upgrades-unsafe-allow constructor -// constructor() payable { -// _disableInitializers(); -// } - -// function initialize( -// string memory vaultName, -// address operatorAddress, -// IStrategyManager _strategyManager, -// IInceptionToken _inceptionToken, -// IStrategy _assetStrategy, -// IERC20 asset -// ) external initializer { -// __InceptionVault_init( -// vaultName, -// operatorAddress, -// _strategyManager, -// _inceptionToken, -// _assetStrategy, -// asset -// ); -// } - -// function _getAssetWithdrawAmount( -// uint256 amount -// ) internal pure override returns (uint256) { -// return amount + 2; -// } - -// function _getAssetReceivedAmount( -// uint256 amount -// ) internal pure override returns (uint256) { -// return amount - 2; -// } -// } diff --git a/projects/vaults/contracts/vaults/Symbiotic/vault_e2/InVault_S_E2.sol b/projects/vaults/contracts/vaults/Symbiotic/vault_e2/InVault_S_E2.sol deleted file mode 100644 index 08064312..00000000 --- a/projects/vaults/contracts/vaults/Symbiotic/vault_e2/InVault_S_E2.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.28; - -import {InceptionVault_S, IInceptionToken, IERC20} from "../InceptionVault_S.sol"; -import {IIMellowRestaker} from "../../../interfaces/symbiotic-vault/restakers/IIMellowRestaker.sol"; - -/// @author The InceptionLRT team -contract InVault_S_E2 is InceptionVault_S { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() payable { - _disableInitializers(); - } - - function initialize( - string memory vaultName, - address operatorAddress, - IERC20 assetAddress, - IInceptionToken _inceptionToken, - IIMellowRestaker _mellowRestaker - ) external initializer { - __InceptionVault_init( - vaultName, - operatorAddress, - assetAddress, - _inceptionToken, - _mellowRestaker - ); - } - - function _getAssetWithdrawAmount(uint256 amount) - internal - pure - override - returns (uint256) - { - return amount + 2; - } - - function _getAssetReceivedAmount(uint256 amount) - internal - pure - override - returns (uint256) - { - return amount - 2; - } -} diff --git a/projects/vaults/contracts/vaults/vault_e1/InStrategyBaseVault_E1.sol b/projects/vaults/contracts/vaults/vault_e1/InStrategyBaseVault_E1.sol deleted file mode 100644 index c2c7c3cc..00000000 --- a/projects/vaults/contracts/vaults/vault_e1/InStrategyBaseVault_E1.sol +++ /dev/null @@ -1,42 +0,0 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.28; - -// import {InceptionStrategyBaseVault, IStrategyManager, IInceptionToken, IStrategy, IERC20} from "../InceptionBasicStrategyVault.sol"; - -// /// @author The InceptionLRT team -// contract InStrategyBaseVault_E1 is InceptionStrategyBaseVault { -// /// @custom:oz-upgrades-unsafe-allow constructor -// constructor() payable { -// _disableInitializers(); -// } - -// function initialize( -// string memory vaultName, -// address operatorAddress, -// IStrategyManager _strategyManager, -// IInceptionToken _inceptionToken, -// IStrategy _assetStrategy, -// IERC20 asset -// ) external initializer { -// __InceptionVault_init( -// vaultName, -// operatorAddress, -// _strategyManager, -// _inceptionToken, -// _assetStrategy, -// asset -// ); -// } - -// function _getAssetWithdrawAmount( -// uint256 amount -// ) internal pure override returns (uint256) { -// return amount + 1; -// } - -// function _getAssetReceivedAmount( -// uint256 amount -// ) internal pure override returns (uint256) { -// return amount - 1; -// } -// } diff --git a/projects/vaults/contracts/withdrawal-queue/WithdrawalQueue.sol b/projects/vaults/contracts/withdrawal-queue/WithdrawalQueue.sol new file mode 100644 index 00000000..76c74714 --- /dev/null +++ b/projects/vaults/contracts/withdrawal-queue/WithdrawalQueue.sol @@ -0,0 +1,550 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {IWithdrawalQueue} from "../interfaces/common/IWithdrawalQueue.sol"; + +/** + * @title The WithdrawalQueue contract + * @author The InceptionLRT teams + * @dev Handles operations with the Inception Vault withdrawals + */ +contract WithdrawalQueue is + PausableUpgradeable, + ReentrancyGuardUpgradeable, + Ownable2StepUpgradeable, + IWithdrawalQueue +{ + using Math for uint256; + + /// @dev emergency epoch number + uint256 public constant EMERGENCY_EPOCH = 0; + /// @dev max threshold while convert shares to assets during undelegate + uint256 internal constant MAX_CONVERT_THRESHOLD = 50; + + /// @dev withdrawal queue owner + address public inceptionVault; + + /// @dev withdrawal epochs data + mapping(uint256 => WithdrawalEpoch) public withdrawals; + mapping(address => uint256[]) public userEpoch; + + /// @dev global stats across all epochs + uint256 public currentEpoch; + uint256 public totalAmountRedeem; + uint256 public totalSharesToWithdraw; + + modifier onlyVaultOrOwner() { + require(msg.sender == inceptionVault || msg.sender == owner(), OnlyVaultOrOwnerAllowed()); + _; + } + + /** + * @notice Initializes the contract with a vault address and legacy withdrawal data. + * Vault must be paused while deploying the new queue instance if it contains legacy withdrawals. + * @param _vault The address of the vault contract that will interact with this queue + * @param legacyWithdrawalAddresses Array of addresses with legacy withdrawal requests + * @param legacyWithdrawalAmounts Array of amounts corresponding to legacy withdrawal requests + * @param legacyClaimedAmount Total claimed amount for the legacy epoch + */ + function initialize( + address _vault, + address[] calldata legacyWithdrawalAddresses, + uint256[] calldata legacyWithdrawalAmounts, + uint256 legacyClaimedAmount + ) external initializer { + require(_vault != address(0), ValueZero()); + + __Pausable_init(); + __ReentrancyGuard_init(); + __Ownable2Step_init(); + + inceptionVault = _vault; + currentEpoch = EMERGENCY_EPOCH + 1; + + _initLegacyWithdrawals( + legacyWithdrawalAddresses, + legacyWithdrawalAmounts, + legacyClaimedAmount + ); + } + + /** + * @notice Initializes legacy withdrawal data for the first epoch + * @param legacyWithdrawalAddresses Array of addresses with legacy withdrawal requests + * @param legacyWithdrawalAmounts Array of amounts corresponding to legacy withdrawal requests + * @param legacyClaimedAmount Total claimed amount for the legacy epoch + */ + function _initLegacyWithdrawals( + address[] calldata legacyWithdrawalAddresses, + uint256[] calldata legacyWithdrawalAmounts, + uint256 legacyClaimedAmount + ) internal onlyInitializing { + require(legacyWithdrawalAddresses.length == legacyWithdrawalAmounts.length, ValueZero()); + if (legacyWithdrawalAddresses.length == 0) { + return; + } + + WithdrawalEpoch storage epoch = withdrawals[currentEpoch]; + epoch.totalClaimedAmount = legacyClaimedAmount; + epoch.totalRequestedShares = legacyClaimedAmount; + epoch.ableRedeem = true; + + for (uint256 i = 0; i < legacyWithdrawalAddresses.length; i++) { + epoch.userShares[legacyWithdrawalAddresses[i]] = legacyWithdrawalAmounts[i]; + _addUserEpoch(legacyWithdrawalAddresses[i], currentEpoch); + } + + // update global state + totalAmountRedeem += legacyClaimedAmount; + + currentEpoch++; + } + + /** + * @notice Requests a withdrawal for a receiver in the current epoch + * @param receiver The address requesting the withdrawal + * @param shares The number of shares to request for withdrawal + */ + function request(address receiver, uint256 shares) external whenNotPaused onlyVaultOrOwner { + require(shares > 0, ValueZero()); + require(receiver != address(0), ValueZero()); + + WithdrawalEpoch storage withdrawal = withdrawals[currentEpoch]; + withdrawal.userShares[receiver] += shares; + withdrawal.totalRequestedShares += shares; + totalSharesToWithdraw += shares; + + _addUserEpoch(receiver, currentEpoch); + } + + /** + * @notice Adds an epoch to the user's list of epochs if not already present + * @param receiver The address of the user + * @param epoch The epoch number to add + */ + function _addUserEpoch(address receiver, uint256 epoch) internal { + uint256[] storage receiverEpochs = userEpoch[receiver]; + if (receiverEpochs.length == 0 || receiverEpochs[receiverEpochs.length - 1] != epoch) { + receiverEpochs.push(epoch); + } + } + + /** + * @notice Processes undelegation for multiple adapters and vaults in a given epoch + * @param epoch The epoch to undelegate from (must match current epoch) + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + * @param undelegatedAmounts Array of undelegated amounts + * @param claimedAmounts Array of claimed amounts + */ + function undelegate( + uint256 epoch, + address[] calldata adapters, + address[] calldata vaults, + uint256[] calldata undelegatedAmounts, + uint256[] calldata claimedAmounts + ) external whenNotPaused onlyVaultOrOwner { + require(epoch >= 0 && epoch <= currentEpoch, UndelegateEpochMismatch()); + require( + adapters.length > 0 && + adapters.length == vaults.length && + adapters.length == undelegatedAmounts.length && + adapters.length == claimedAmounts.length, + ValueZero() + ); + + WithdrawalEpoch storage withdrawal = withdrawals[epoch]; + for (uint256 i = 0; i < adapters.length; i++) { + _undelegate( + withdrawal, + adapters[i], + vaults[i], + undelegatedAmounts[i], + claimedAmounts[i] + ); + } + + _afterUndelegate(epoch, withdrawal); + } + + /** + * @notice Internal function to process undelegation for a specific adapter and vault + * @param withdrawal The storage reference to the withdrawal epoch + * @param adapter The adapter address + * @param vault The vault address + * @param undelegatedAmount The amount undelegated + * @param claimedAmount The amount claimed + */ + function _undelegate( + WithdrawalEpoch storage withdrawal, + address adapter, + address vault, + uint256 undelegatedAmount, + uint256 claimedAmount + ) internal { + require( + (undelegatedAmount > 0 || claimedAmount > 0) && + adapter != address(0) && + vault != address(0), + ValueZero() + ); + + // update withdrawal data + withdrawal.adapterUndelegated[adapter][vault] = undelegatedAmount; + withdrawal.totalUndelegatedAmount += undelegatedAmount; + withdrawal.adaptersUndelegatedCounter++; + + if (claimedAmount > 0) { + withdrawal.totalClaimedAmount += claimedAmount; + } + + if (claimedAmount > 0 && undelegatedAmount == 0) { + withdrawal.adaptersClaimedCounter++; + } + } + + /** + * @notice Finalizes undelegation by advancing the epoch if completed + * @param withdrawal The storage reference to the withdrawal epoch + */ + function _afterUndelegate(uint256 epoch, WithdrawalEpoch storage withdrawal) internal { + uint256 requested = IERC4626(inceptionVault).convertToAssets(withdrawal.totalRequestedShares); + uint256 totalUndelegated = withdrawal.totalUndelegatedAmount + withdrawal.totalClaimedAmount; + + // ensure that the undelegated assets are relevant to the ratio + require( + requested >= totalUndelegated ? + requested - totalUndelegated <= MAX_CONVERT_THRESHOLD + : totalUndelegated - requested <= MAX_CONVERT_THRESHOLD, + UndelegateNotCompleted() + ); + + if (withdrawal.totalClaimedAmount > 0 && withdrawal.totalUndelegatedAmount == 0) { + _makeRedeemable(withdrawal); + } + + if (epoch == currentEpoch) { + currentEpoch++; + } + } + + /** + * @notice Claims an amount for a specific adapter and vault in an epoch + * @param epoch The epoch to claim from + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + * @param claimedAmounts Array of claimed amounts + */ + function claim( + uint256 epoch, + address[] calldata adapters, + address[] calldata vaults, + uint256[] calldata claimedAmounts + ) external whenNotPaused onlyVaultOrOwner { + require( + adapters.length > 0 && + adapters.length == vaults.length && + adapters.length == claimedAmounts.length, + ValueZero() + ); + + WithdrawalEpoch storage withdrawal = withdrawals[epoch]; + require(!withdrawal.ableRedeem, EpochAlreadyRedeemable()); + + if (epoch == EMERGENCY_EPOCH) { + // do nothing + return; + } + + for (uint256 i = 0; i < adapters.length; i++) { + _claim(withdrawal, adapters[i], vaults[i], claimedAmounts[i]); + } + + _afterClaim(epoch, withdrawal, adapters, vaults); + } + + /** + * @notice Claims an amount for a specific adapter and vault in an epoch + * @param withdrawal The storage reference to the withdrawal epoch + * @param adapter The adapter address + * @param vault The vault address + * @param claimedAmount The amount to claim + */ + function _claim( + WithdrawalEpoch storage withdrawal, + address adapter, + address vault, + uint256 claimedAmount + ) internal { + require(withdrawal.adapterUndelegated[adapter][vault] > 0, ClaimUnknownAdapter()); + + // update withdrawal state + withdrawal.totalClaimedAmount += claimedAmount; + withdrawal.adaptersClaimedCounter++; + } + + /** + * @notice Updates the redeemable status after a claim + * @param withdrawal The storage reference to the withdrawal epoch + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + */ + function _afterClaim( + uint256 epoch, + WithdrawalEpoch storage withdrawal, + address[] calldata adapters, + address[] calldata vaults + ) internal { + require(withdrawal.adaptersClaimedCounter == withdrawal.adaptersUndelegatedCounter, ClaimNotCompleted()); + + _isSlashed(withdrawal) ? + _resetEpoch(epoch, withdrawal, adapters, vaults) + : _makeRedeemable(withdrawal); + } + + /** + * @notice Checks if a withdrawal epoch is considered slashed based on the difference between claimed and current amounts. + * @dev Compares the current asset value of requested shares against the total claimed amount, considering a maximum threshold. + * @param withdrawal The storage reference to the WithdrawalEpoch struct. + * @return bool True if the withdrawal is slashed, false otherwise. + */ + function _isSlashed(WithdrawalEpoch storage withdrawal) internal view returns (bool) { + uint256 currentAmount = IERC4626(inceptionVault).convertToAssets(withdrawal.totalRequestedShares); + + if (withdrawal.totalClaimedAmount >= withdrawal.totalUndelegatedAmount) { + if (currentAmount < withdrawal.totalClaimedAmount && withdrawal.totalClaimedAmount - currentAmount > MAX_CONVERT_THRESHOLD) { + return true; + } + } else if (currentAmount > withdrawal.totalClaimedAmount && currentAmount - withdrawal.totalClaimedAmount > MAX_CONVERT_THRESHOLD) { + return true; + } else if (currentAmount < withdrawal.totalClaimedAmount && withdrawal.totalClaimedAmount - currentAmount > MAX_CONVERT_THRESHOLD) { + return true; + } + + return false; + } + + /** + * @notice Marks a withdrawal epoch as redeemable and updates global state + * @dev Ensures all adapters have completed claiming by checking if the claimed counter equals the undelegated counter. + * Sets the epoch as redeemable, updates the total redeemable amount, and reduces the total shares queued for withdrawal + * @param withdrawal The storage reference to the withdrawal epoch + */ + function _makeRedeemable(WithdrawalEpoch storage withdrawal) internal { + withdrawal.ableRedeem = true; + totalAmountRedeem += withdrawal.totalClaimedAmount; + totalSharesToWithdraw -= withdrawal.totalRequestedShares; + } + + /** + * @notice Resets the state of a withdrawal epoch to its initial values. + * @dev Clears the total claimed amount, total undelegated amount, and adapter counters for the specified withdrawal epoch. + * @param withdrawal The storage reference to the WithdrawalEpoch struct to be refreshed. + * @param adapters Array of adapter addresses + * @param vaults Array of vault addresses + */ + function _resetEpoch( + uint256 epoch, + WithdrawalEpoch storage withdrawal, + address[] calldata adapters, + address[] calldata vaults + ) internal { + withdrawal.totalClaimedAmount = 0; + withdrawal.totalUndelegatedAmount = 0; + withdrawal.adaptersClaimedCounter = 0; + withdrawal.adaptersUndelegatedCounter = 0; + + for (uint256 i = 0; i < adapters.length; i++) { + delete withdrawal.adapterUndelegated[adapters[i]][vaults[i]]; + } + + emit EpochReset(epoch); + } + + /** + * @notice Forces undelegation and claims a specified amount for the current epoch + * @param epoch The epoch number to process, must match the current epoch + * @param claimedAmount The amount to claim, must not exceed totalAmountRedeemFree + */ + function forceUndelegateAndClaim(uint256 epoch, uint256 claimedAmount) external whenNotPaused onlyVaultOrOwner { + require(epoch <= currentEpoch, UndelegateEpochMismatch()); + + WithdrawalEpoch storage withdrawal = withdrawals[epoch]; + require(withdrawal.totalRequestedShares > 0, InvalidEpoch()); + require(!withdrawal.ableRedeem, EpochAlreadyRedeemable()); + + // update epoch state + withdrawal.totalClaimedAmount = claimedAmount; + + _afterUndelegate(epoch, withdrawal); + } + + /** + * @notice Redeems available amounts for a receiver across their epochs + * @param receiver The address to redeem for + * @return amount The total amount redeemed + */ + function redeem(address receiver) external whenNotPaused onlyVaultOrOwner returns (uint256 amount) { + uint256[] storage epochs = userEpoch[receiver]; + uint256 i = 0; + + while (i < epochs.length) { + uint256 redeemAmount = _redeem(receiver, epochs, i); + if (redeemAmount == 0) { + ++i; + continue; + } + + amount += redeemAmount; + } + + if (epochs.length == 0) { + delete userEpoch[receiver]; + } + + return amount; + } + + /** + * @notice Redeems available amounts for a receiver with given user epoch index + * @param receiver The address to redeem for + * @param userEpochIndex user epoch index + * @return amount The total amount redeemed + */ + function redeem(address receiver, uint256 userEpochIndex) external whenNotPaused onlyVaultOrOwner returns (uint256 amount) { + uint256[] storage epochs = userEpoch[receiver]; + require(userEpochIndex < epochs.length, InvalidEpoch()); + + amount = _redeem(receiver, epochs, userEpochIndex); + + if (epochs.length == 0) { + delete userEpoch[receiver]; + } + + return amount; + } + + /** + * @notice Redeems the available amount for a receiver in a specific user epoch index + * @dev Processes the redemption by checking if the withdrawal is redeemable and if the receiver has shares. + * Calculates the redeemable amount, clears the receiver's shares, removes the epoch from the user's epoch list, + * and updates the global total redeemed amount + * @param receiver The address of the user redeeming the amount + * @param epochs The storage array of epoch indexes for the user + * @param userEpochIndex The index of the epoch in the user's epoch list + * @return amount The amount redeemed for the receiver + */ + function _redeem(address receiver, uint256[] storage epochs, uint256 userEpochIndex) internal returns (uint256 amount) { + WithdrawalEpoch storage withdrawal = withdrawals[epochs[userEpochIndex]]; + if (!withdrawal.ableRedeem || withdrawal.userShares[receiver] == 0) { + return 0; + } + + amount = _getRedeemAmount(withdrawal, receiver); + withdrawal.userShares[receiver] = 0; + + epochs[userEpochIndex] = epochs[epochs.length - 1]; + epochs.pop(); + + // update global state + totalAmountRedeem -= amount; + + return amount; + } + + /** + * @notice Calculates the redeemable amount for a user in an epoch + * @param withdrawal The storage reference to the withdrawal epoch + * @param receiver The address of the user + * @return The calculated redeemable amount + */ + function _getRedeemAmount(WithdrawalEpoch storage withdrawal, address receiver) internal view returns (uint256) { + return withdrawal.totalClaimedAmount.mulDiv( + withdrawal.userShares[receiver], + withdrawal.totalRequestedShares, + Math.Rounding.Down + ); + } + + /*////////////////////// + //// GET functions //// + ////////////////////*/ + + /** + * @notice Retrieves the total number of requested shares for a specific epoch + * @param epoch The epoch number for which to retrieve the requested shares + * @return The total number of shares requested in the specified epoch + */ + function getRequestedShares(uint256 epoch) external view returns (uint256) { + return withdrawals[epoch].totalRequestedShares; + } + + /** + * @notice Returns the total pending withdrawal amount for a receiver + * @param receiver The address to check + * @return amount The total pending withdrawal amount + */ + function getPendingWithdrawalOf(address receiver) external view returns (uint256 amount) { + uint256[] memory epochs = userEpoch[receiver]; + for (uint256 i = 0; i < epochs.length; i++) { + WithdrawalEpoch storage withdrawal = withdrawals[epochs[i]]; + if (withdrawal.userShares[receiver] == 0) { + continue; + } + + if (withdrawal.ableRedeem) amount += _getRedeemAmount(withdrawal, receiver); + else amount += IERC4626(inceptionVault).convertToAssets(withdrawal.userShares[receiver]); + } + + return amount; + } + + /** + * @notice Checks if a claimer has redeemable withdrawals and their epoch indexes inside userEpoch mapping + * @param claimer The address to check + * @return able Whether there are redeemable withdrawals + * @return withdrawalIndexes Array of user epoch indexes with redeemable withdrawals + */ + function isRedeemable(address claimer) external view returns (bool able, uint256[] memory withdrawalIndexes) { + uint256 index; + + uint256[] memory epochs = userEpoch[claimer]; + withdrawalIndexes = new uint256[](epochs.length); + + for (uint256 i = 0; i < epochs.length; i++) { + WithdrawalEpoch storage withdrawal = withdrawals[epochs[i]]; + if (!withdrawal.ableRedeem || withdrawal.userShares[claimer] == 0) { + continue; + } + + able = true; + withdrawalIndexes[index] = i; + ++index; + } + + return (able, withdrawalIndexes); + } + + /*/////////////////////////////// + ////// Pausable functions ////// + /////////////////////////////*/ + + /** + * @dev Pauses the contract + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @dev Unpauses the contract + */ + function unpause() external onlyOwner { + _unpause(); + } +} \ No newline at end of file diff --git a/projects/vaults/docs/baseflow.svg b/projects/vaults/docs/baseflow.svg new file mode 100644 index 00000000..4b6319b0 --- /dev/null +++ b/projects/vaults/docs/baseflow.svg @@ -0,0 +1 @@ +Base flowBase flowUserInceptionVault_SiTokenIOperatorAdapterHandlerIIBaseAdapterWithdrawalQueueUserUserInceptionVault_SInceptionVault_SiTokeniTokenIOperatorIOperatorAdapterHandlerAdapterHandlerIIBaseAdapterIIBaseAdapterWithdrawalQueueWithdrawalQueue: deposit(): to deposit assetmint():transfer sharesdelegate(): delegates assets to adapterdelegate()withdraw(shares)burn()request()update user requested sharesundelegate(): batch undelegations from adapterswithdraw()undelegate()update epochclaim(): claim asset from adapterclaim()update given epoch stateredeem()redeem(claimer)mark available withdrawals as redeemedredeemed amounttransfer assets \ No newline at end of file diff --git a/projects/vaults/docs/ratio-flow.svg b/projects/vaults/docs/ratio-flow.svg new file mode 100644 index 00000000..3d03df8c --- /dev/null +++ b/projects/vaults/docs/ratio-flow.svg @@ -0,0 +1 @@ +Base flowBase flowUserInceptionVault_SIOperatorAdapterHandlerUserUserInceptionVault_SInceptionVault_SIOperatorIOperatorAdapterHandlerAdapterHandler: deposit 10 ETH:vaultBalance = 10, supply = 10delegate 10 ETHvaultBalance = 0, totalDelegated = 10withdraw 5 ETHsupply = 5, totalSharesToWithdraw = 5undelegate 5 ETHtotalDelegated = 5, totalSharesToWithdraw = 0claim 5 ETHredeemReservedAmount = 5; vaultBalance = 5;redeem 5 ETHredeemReservedAmount = 0; vaultBalnace = 0;Emergency FlowemergencyUndelegate 2 ETHtotalDelegated = 3, emergencyPendingWithdrawals = 2emergencyClaim 2 ETHemergencyPendingWithdrawals = 0; vaultBalance = 2withdraw 1.5 ETHsupply = 3.5, totalSharesToWithdraw = 1.5force undelegateAndClaim 1.5 ETHredeemReservedAmount = 1.5, totalSharesToWithdraw = 0redeem 1.5 ETHredeemReservedAmount = 0; vaultBalance = 0.5 \ No newline at end of file diff --git a/projects/vaults/docs/src/baseflow.txt b/projects/vaults/docs/src/baseflow.txt new file mode 100644 index 00000000..bdf8ba6e --- /dev/null +++ b/projects/vaults/docs/src/baseflow.txt @@ -0,0 +1,62 @@ + @startuml + + participant User #LightBlue + participant InceptionVault_S as Vault <<.sol>> + participant iToken <<.sol>> + participant IOperator as Operator <> + participant AdapterHandler <<.sol>> + participant IIBaseAdapter <<.sol>> + participant WithdrawalQueue <<.sol>> + participant Strategy #Gold + + title Base flow + + == Deposit == + User -> Vault: //deposit(assets)// + note left: or //depositWithReferral(assets, code)//\nor //mint(shares)// + Vault --> Vault: calc bonus + note left: based on //depositBonusAmount// (provided for deposits to incentivize staking) + Vault -> iToken: //mint(shares)// + Vault --> Vault: //totalAssets//=10, //totalSupply//=10 + Vault -> User: transfer shares (//assets * ratio//) + User --> User: user balance: //shares//=10 + + Operator -> AdapterHandler: //delegate(assets)// + AdapterHandler -> IIBaseAdapter: //delegate(assets)// + AdapterHandler --> AdapterHandler: //totalAssets//=0, //totalDelegated//=10 + IIBaseAdapter -> Strategy: delegate + + + == Withdrawal == + User -> Vault: //withdraw(shares)//\n 2 shares + User --> User: user balance: //shares//=8 + Vault -> iToken: //burn()//\n2 shares + Vault --> Vault: //totalAssets//=0 + Vault -> WithdrawalQueue: //request(2)// + + WithdrawalQueue --> WithdrawalQueue: update user requested shares\n//totalSharesToWithdraw//=2 + + Operator -> AdapterHandler: undelegate()\n(batch undelegations from adapters) + AdapterHandler -> IIBaseAdapter: //withdraw(2 assets)// + IIBaseAdapter -> Strategy: undelegate + AdapterHandler -> WithdrawalQueue: //undelegate(2 assets)// + AdapterHandler --> AdapterHandler: assets //totalDelegated//=8, //totalSharesToWithdraw// = 0 + WithdrawalQueue --> WithdrawalQueue: update epoch + + Operator -> AdapterHandler: //claim(2)//\n(claim asset from adapter) + IIBaseAdapter -> Strategy: claim + AdapterHandler -> WithdrawalQueue: //claim(2)// + AdapterHandler --> AdapterHandler: //redeemReservedAmount//=2; //totalAssets//=2 + WithdrawalQueue --> WithdrawalQueue: update given epoch state + + User -> Vault: //redeem(2)// + Vault -> WithdrawalQueue: //redeem(2, claimer address)// + WithdrawalQueue --> WithdrawalQueue: mark available withdrawals as redeemed + WithdrawalQueue -> Vault: redeemed amount + Vault --> Vault: //redeemReservedAmount//=0; //totalAssets//=0; + Vault -> User: transfer assets + User --> User: user balance: //assets//=2, //shares//=8 + + + @enduml + \ No newline at end of file diff --git a/projects/vaults/docs/src/ratio.txt b/projects/vaults/docs/src/ratio.txt new file mode 100644 index 00000000..213763d7 --- /dev/null +++ b/projects/vaults/docs/src/ratio.txt @@ -0,0 +1,59 @@ + +participant User #LightBlue +participant InceptionVault_S as Vault <<.sol>> +participant IOperator <<.sol>> +participant AdapterHandler as AdHandler <<.sol>> + +@startuml + +title Base flow + +== Deposit == + +group "Deposit Process" +User -> Vault: //deposit(assets)// 10 ETH +note left: or //depositWithReferral(assets, code)//\nor //mint(shares)// +Vault -> Vault: //totalAssets// = 10, supply = 10 +note left: totalAssets is vault balance +Vault -> Vault: Calc bonus +Vault -> User: //mint()// shares + +IOperator -> AdHandler: //delegate()// 10 ETH +AdHandler -> AdHandler: totalAssets = 0, totalDelegated = 10 +group end + + +== Withdrawal == + +User -> Vault: withdraw 5 ETH +Vault -> Vault: supply = 5, totalSharesToWithdraw = 5 + +IOperator -> AdHandler: undelegate 5 ETH +AdHandler -> AdHandler: totalDelegated = 5, totalSharesToWithdraw = 0 + +IOperator -> AdHandler: claim 5 ETH +AdHandler -> AdHandler: redeemReservedAmount = 5; vaultBalance = 5; + +User -> Vault: redeem 5 ETH +Vault -> Vault: redeemReservedAmount = 0; vaultBalnace = 0; + + +== Emergency Flow == + +IOperator -> AdHandler: emergencyUndelegate 2 ETH +AdHandler -> AdHandler: totalDelegated = 3, emergencyPendingWithdrawals = 2 + +IOperator -> AdHandler: emergencyClaim 2 ETH +AdHandler -> AdHandler: emergencyPendingWithdrawals = 0; vaultBalance = 2 + +User -> Vault: withdraw 1.5 ETH +Vault -> Vault: supply = 3.5, totalSharesToWithdraw = 1.5 + +IOperator -> AdHandler: force undelegateAndClaim 1.5 ETH +AdHandler -> AdHandler: redeemReservedAmount = 1.5, totalSharesToWithdraw = 0 + + +User -> Vault: redeem 1.5 ETH +Vault -> Vault: redeemReservedAmount = 0; vaultBalance = 0.5 + +@enduml \ No newline at end of file diff --git a/projects/vaults/hardhat.config.ts b/projects/vaults/hardhat.config.ts index eda334c9..67107c36 100644 --- a/projects/vaults/hardhat.config.ts +++ b/projects/vaults/hardhat.config.ts @@ -4,11 +4,13 @@ import "@nomicfoundation/hardhat-toolbox"; import "hardhat-gas-reporter"; import "@openzeppelin/hardhat-upgrades"; import "hardhat-storage-layout"; +import 'solidity-coverage'; // Hardhat tasks import "./tasks/get-free-balances"; import "./tasks/get-inception-vaults"; import "./tasks/deposit-extra"; +import 'dotenv/config'; const config: HardhatUserConfig = { ...(CONFIG as HardhatUserConfig), @@ -17,24 +19,24 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, - runs: 100, + runs: 10, }, }, }, networks: { - // local: { - // url: "http://127.0.0.1:8545", - // // chainId: 1337, - // // gasPrice: 20000000000, - // // gas: 6721975, - // }, hardhat: { forking: { - url: `${process.env.MAINNET_RPC}`, - blockNumber: 21687985, + url: `${process.env.RPC}`, + blockNumber: 21861027, }, }, }, + mocha: { + timeout: 120_000, + // retries: 1, + // bail: true, + } }; export default config; + diff --git a/projects/vaults/package.json b/projects/vaults/package.json index 95eefc02..3b673e81 100644 --- a/projects/vaults/package.json +++ b/projects/vaults/package.json @@ -3,8 +3,14 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "mocha --timeout 15000", - "format": "prettier --write scripts/*.js tasks/*.js test/*.js" + "format": "prettier --write scripts/*.js tasks/*.js tests/*.js", + "test": "npx hardhat test", + "// coverage": "Generates coverage report to 'coverage' dir", + "coverage": "npx hardhat coverage", + "coverage:check": "./check-coverage.sh", + "// coverage:matrix": "Generate a JSON object that maps which tests hit which lines of code", + "coverage:matrix": "npx hardhat coverage --matrix", + "slither:vault": "slither ./contracts/vaults/Symbiotic/InceptionVault_S.sol --solc-remaps @openzeppelin=node_modules/@openzeppelin" }, "license": "MIT", "devDependencies": { @@ -15,7 +21,7 @@ "@nomicfoundation/hardhat-ethers": "^3.0.0", "@nomicfoundation/hardhat-ignition": "^0.15.0", "@nomicfoundation/hardhat-ignition-ethers": "^0.15.0", - "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.12", "@nomicfoundation/hardhat-toolbox": "^5.0.0", "@nomicfoundation/hardhat-verify": "^2.0.0", "@nomiclabs/hardhat-etherscan": "^3.1.8", @@ -30,14 +36,16 @@ "chai": "^4.2.0", "dotenv": "^16.3.1", "ethers": "^6.4.0", - "hardhat": "^2.14.0", + "hardhat": "^2.22.19", "hardhat-contract-sizer": "^2.10.0", + "hardhat-coverage": "^0.9.19", "hardhat-deploy": "^0.12.4", "hardhat-gas-reporter": "^1.0.8", "hardhat-storage-layout": "^0.1.7", "hardhat-tracer": "^2.6.0", + "import-sync": "^2.2.3", "prettier": "3.3.2", - "solidity-coverage": "^0.8.0", + "solidity-coverage": "^0.8.14", "ts-node": ">=8.0.0", "typechain": "^8.3.0", "typescript": ">=4.5.0" diff --git a/projects/vaults/scripts/allocation.js b/projects/vaults/scripts/allocation.js new file mode 100644 index 00000000..b51ab39b --- /dev/null +++ b/projects/vaults/scripts/allocation.js @@ -0,0 +1,97 @@ +const { ethers, network, upgrades } = require("hardhat"); +const helpers = require("@nomicfoundation/hardhat-network-helpers"); +const { toWei, impersonateWithEth, e18 } = require("../test/helpers/utils"); + +async function slashOperator() { + const allocationManagerAddr = "0x78469728304326CBc65f8f95FA756B0B73164462"; + + const ownerAddress = "0xba7cda36abeb28ad200591e6e4a963359b1f43df"; + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [ownerAddress], + }); + + + await impersonateWithEth(ownerAddress, toWei(1000)); + + const ownerSigner = await ethers.getSigner(ownerAddress); + const allocationManager = await ethers.getContractAt("AllocationManager", allocationManagerAddr); + + await allocationManager.connect(ownerSigner).slashOperator("0xba7cda36abeb28ad200591e6e4a963359b1f43df", [ + "0xd9322bb31f42c7caa12daad49699d655393f9524", + 0, + ["0x7d704507b76571a51d9cae8addabbfd0ba0e63d3"], + [1e18], + "test", + ]); + + + // address operator; + // uint32 operatorSetId; + // IStrategy[] strategies; + // uint256[] wadsToSlash; + // string description; + +} + +async function main() { + const vault = await deployVault(); + const result = await vault.getTotalDelegatedV2(100, 30, 17); + console.log(result); + + const result2 = await vault.getTotalDelegatedV2(100, 30, 14); + console.log(result2); + + // console.log("totalDelegated", ethers.formatEther(await vault.getTotalDelegated())); +} + +async function deployVault() { + let a = { + assetName: "rETH", + assetAddress: "0x7322c24752f79c05FFD1E2a6FCB97020C1C264F1", + assetPoolName: "RocketMockPool", + assetPool: "0x320f3aAB9405e38b955178BBe75c477dECBA0C27", + vaultName: "InrEthVault", + vaultFactory: "ERC4626Facet_EL_E2", + strategyManager: "0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6", + assetStrategy: "0x3A8fBdf9e77DFc25d09741f51d3E181b25d0c4E0", + iVaultOperator: "0xa4341b5Cf43afD2993e1ae47d956F44A2d6Fc08D", + delegationManager: "0xA44151489861Fe9e3055d95adC98FbD462B948e7", + rewardsCoordinator: "0xAcc1fb458a1317E886dB376Fc8141540537E68fE", + withdrawalDelayBlocks: 400, + ratioErr: 2n, + transactErr: 5n + }; + + // 1. Inception token + const iTokenFactory = await ethers.getContractFactory("InceptionToken"); + const iToken = await upgrades.deployProxy(iTokenFactory, ["TEST InceptionLRT Token", "tINt"]); + iToken.address = await iToken.getAddress(); + + // 6. Inception library + const iLibrary = await ethers.deployContract("InceptionLibrary"); + await iLibrary.waitForDeployment(); + + // 7. Inception vault + const iVaultFactory = await ethers.getContractFactory("InceptionVault_EL", { + libraries: { InceptionLibrary: await iLibrary.getAddress() }, + }); + + const iVault = await upgrades.deployProxy( + iVaultFactory, + [a.vaultName, a.iVaultOperator, a.strategyManager, iToken.address, a.assetStrategy], + { unsafeAllowLinkedLibraries: true }, + ); + iVault.address = await iVault.getAddress(); + + console.log("iVault address", iVault.address); + + return iVault; +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/projects/vaults/scripts/impl.js b/projects/vaults/scripts/impl.js index e0b766a7..6fb50610 100644 --- a/projects/vaults/scripts/impl.js +++ b/projects/vaults/scripts/impl.js @@ -1,15 +1,20 @@ const { upgrades, ethers } = require("hardhat"); +const { addresses } = require("./migration/mainnet/config-addresses"); const InVault_S = "0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97"; +const MellowRestaker = "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378"; +const LibAddress = "0x313d6c1b075077ce10b3229ee75e0af453cb7d07"; async function main() { /**************************************** ************ InceptionVault_S ************ ****************************************/ + // const library = await ethers.deployContract("InceptionLibrary"); + const iVaultFactory = await ethers.getContractFactory("InVault_S_E2", { libraries: { - InceptionLibrary: "0xF6940A8e7334Ab2a7781AF6f9E5aeD8EFB55116A", + InceptionLibrary: LibAddress, }, }); const vaultImpl = await upgrades.prepareUpgrade(InVault_S, iVaultFactory, { @@ -17,6 +22,22 @@ async function main() { unsafeAllowLinkedLibraries: true, }); console.log(`New Impl of InceptionVault(${vaultImpl}) was deployed`); + + // Symbiotic + const sRestaker = await ethers.getContractFactory("ISymbioticRestaker"); + vaults = ["0x4e0554959A631B3D3938ffC158e0a7b2124aF9c5", "0xc10A7f0AC6E3944F4860eE97a937C51572e3a1Da"]; + const s = await upgrades.deployProxy( + sRestaker, + [vaults, InVault_S, "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", addresses.Operator], + { kind: "transparent" }, + ); + await s.waitForDeployment(); + console.log(`New Impl of MellowRestaker(${await s.getAddress()}) was deployed`); + + // Mellow + const mRestaker = await ethers.getContractFactory("IMellowRestaker"); + const mImp = await upgrades.prepareUpgrade(MellowRestaker, mRestaker); + console.log(`New Impl of MellowRestaker(${mImp}) was deployed`); } main() diff --git a/projects/vaults/scripts/migration/addresses/holesky_InrEthVault.json b/projects/vaults/scripts/migration/addresses/holesky_InrEthVault.json index 8c2a1ec8..e05fe6df 100644 --- a/projects/vaults/scripts/migration/addresses/holesky_InrEthVault.json +++ b/projects/vaults/scripts/migration/addresses/holesky_InrEthVault.json @@ -5,3 +5,4 @@ "iTokenImpl": "0x9c9C5E38422FC486aAEC1159d482FfAddd023613", "RestakerImpl": "0xa6Bd9D6b9E428a17cD81B25C00Fe9725c1914100" } + diff --git a/projects/vaults/scripts/migration/addresses/holesky_InstEthVault.json b/projects/vaults/scripts/migration/addresses/holesky_InstEthVault.json index 14d14dc7..2aa9036f 100644 --- a/projects/vaults/scripts/migration/addresses/holesky_InstEthVault.json +++ b/projects/vaults/scripts/migration/addresses/holesky_InstEthVault.json @@ -5,3 +5,4 @@ "iTokenImpl": "0x9c9C5E38422FC486aAEC1159d482FfAddd023613", "RestakerImpl": "0xa6Bd9D6b9E428a17cD81B25C00Fe9725c1914100" } + diff --git a/projects/vaults/scripts/migration/deploy-vault.js b/projects/vaults/scripts/migration/deploy-vault.js index 237600b2..7018b85f 100644 --- a/projects/vaults/scripts/migration/deploy-vault.js +++ b/projects/vaults/scripts/migration/deploy-vault.js @@ -96,13 +96,13 @@ const deployVault = async (addresses, vaultName, tokenName, tokenSymbol) => { iVault = await upgrades.deployProxy( InceptionVaultFactory, [vaultName, addresses.Operator, addresses.StrategyManager, iTokenAddress, strategyAddress, assetAddress], - { unsafeAllowLinkedLibraries: true } + { unsafeAllowLinkedLibraries: true }, ); } else if (vaultFactory === "InVault_E1" || vaultFactory === "InVault_E2") { iVault = await upgrades.deployProxy( InceptionVaultFactory, [vaultName, addresses.Operator, addresses.StrategyManager, iTokenAddress, strategyAddress], - { unsafeAllowLinkedLibraries: true } + { unsafeAllowLinkedLibraries: true }, ); } else { console.error("Wrong iVaultFactory: ", vaultFactory); @@ -161,3 +161,4 @@ const deployVault = async (addresses, vaultName, tokenName, tokenSymbol) => { module.exports = { deployVault, }; + diff --git a/projects/vaults/scripts/migration/deploy-vault_S.js b/projects/vaults/scripts/migration/deploy-vault_S.js index 204d2656..b294e0e3 100644 --- a/projects/vaults/scripts/migration/deploy-vault_S.js +++ b/projects/vaults/scripts/migration/deploy-vault_S.js @@ -1,7 +1,16 @@ const { ethers, upgrades } = require("hardhat"); const fs = require("fs"); -const deployVault = async (addresses, vaultName, tokenName, tokenSymbol, mellowWrappers, mellowVaults, asset, ratioFeed) => { +const deployVault = async ( + addresses, + vaultName, + tokenName, + tokenSymbol, + mellowWrappers, + mellowVaults, + asset, + ratioFeed, +) => { const [deployer] = await ethers.getSigners(); console.log(`Deploying ${vaultName} with the account: ${deployer.address}`); @@ -17,9 +26,13 @@ const deployVault = async (addresses, vaultName, tokenName, tokenSymbol, mellowW const iTokenImplAddress = await upgrades.erc1967.getImplementationAddress(iTokenAddress); - // 2. Mellow restaker + // 2. Mellow Restaker const mellowRestakerFactory = await hre.ethers.getContractFactory("IMellowRestaker"); - const mr = await upgrades.deployProxy(mellowRestakerFactory, [mellowWrappers, mellowVaults, asset, addresses.Operator], { kind: "transparent" }); + const mr = await upgrades.deployProxy( + mellowRestakerFactory, + [mellowWrappers, mellowVaults, asset, addresses.Operator], + { kind: "transparent" }, + ); await mr.waitForDeployment(); const mrAddress = await mr.getAddress(); console.log(`MellowRestaker address: ${mrAddress}`); @@ -40,20 +53,15 @@ const deployVault = async (addresses, vaultName, tokenName, tokenSymbol, mellowW const libAddress = await lib.getAddress(); console.log("InceptionLibrary address:", libAddress); - const InceptionVaultFactory = await hre.ethers.getContractFactory(vaultFactory, - { + const InceptionVaultFactory = await hre.ethers.getContractFactory(vaultFactory, { libraries: { - InceptionLibrary: libAddress + InceptionLibrary: libAddress, }, - } -); + }); const iVault = await upgrades.deployProxy( InceptionVaultFactory, [vaultName, addresses.Operator, asset, iTokenAddress, mrAddress], - { kind: "transparent" , - unsafeAllowLinkedLibraries: true, - unsafeSkipStorageCheck: true, - } + { kind: "transparent", unsafeAllowLinkedLibraries: true, unsafeSkipStorageCheck: true }, ); await iVault.waitForDeployment(); const iVaultAddress = await iVault.getAddress(); @@ -96,3 +104,4 @@ const deployVault = async (addresses, vaultName, tokenName, tokenSymbol, mellowW module.exports = { deployVault, }; + diff --git a/projects/vaults/scripts/migration/mainnet/restaker/deploy-impl.js b/projects/vaults/scripts/migration/mainnet/restaker/deploy-impl.js index 0cfa72af..21d2d425 100644 --- a/projects/vaults/scripts/migration/mainnet/restaker/deploy-impl.js +++ b/projects/vaults/scripts/migration/mainnet/restaker/deploy-impl.js @@ -1,15 +1,16 @@ const { ethers } = require("hardhat"); async function main() { - const BeaconProxyPatternV1 = await ethers.getContractFactory("InceptionRestaker"); + const BeaconProxyPatternV1 = await ethers.getContractFactory("InceptionEigenRestaker"); const beaconImpl = await BeaconProxyPatternV1.deploy(); await beaconImpl.deployed(); - console.log(`-------- Restaker has been deployed at the address: ${beaconImpl.address}`); + console.log(`-------- InceptionEigenRestaker has been deployed at the address: ${beaconImpl.address}`); } main() .then(() => process.exit(0)) - .catch((error) => { + .catch(error => { console.error(error); process.exit(1); }); + diff --git a/projects/vaults/scripts/migration/testnet/upgrade-diamond-proxy/upgrade-restaker.js b/projects/vaults/scripts/migration/testnet/upgrade-diamond-proxy/upgrade-restaker.js index 5e9bdc4a..cc7b8e33 100644 --- a/projects/vaults/scripts/migration/testnet/upgrade-diamond-proxy/upgrade-restaker.js +++ b/projects/vaults/scripts/migration/testnet/upgrade-diamond-proxy/upgrade-restaker.js @@ -32,7 +32,7 @@ async function main() { /// 2. upgrade the Beacon's implementation for the vaults try { - for (const [vaultAddress, vaultRestakers] of restakers.entries()) { + for (const [vaultAddress, vaultRestakers] of adapters.entries()) { if (!vaultAddress || !Array.isArray(vaultRestakers)) continue; const iVault = await iVaultOldFactory.attach(vaultAddress); @@ -47,26 +47,26 @@ async function main() { /// 3. set rewardsCoordinator console.log( - `We're going to set rewardsCoordinator(${addresses.RewardsCoordinator}) for all previously deployed Restakers`, + `We're going to set rewardsCoordinator(${addresses.RewardsCoordinator}) for all previously deployed Adapters`, ); try { for (const [vaultAddress, vaultRestakers] of restakers.entries()) { if (!vaultAddress || !Array.isArray(vaultRestakers)) continue; - for (const restakerAddr of vaultRestakers) { - if (!restakerAddr) continue; + for (const adapterAddr of vaultRestakers) { + if (!adapterAddr) continue; - const restaker = BeaconProxyPatternV2.attach(restakerAddr); - tx = await restaker.setRewardsCoordinator(addresses.RewardsCoordinator); + const adapter = BeaconProxyPatternV2.attach(adapterAddr); + tx = await adapter.setRewardsCoordinator(addresses.RewardsCoordinator); await tx.wait(); console.log( - `Restaker(${await restaker.getAddress()}) for ${vaultAddress} was updated with the RewardsCoordinator:`, + `Adapter(${await adapter.getAddress()}) for ${vaultAddress} was updated with the RewardsCoordinator:`, ); } } } catch (error) { - console.error("Error processing restakers:", error); + console.error("Error processing adapters:", error); } } diff --git a/projects/vaults/scripts/migration/upgrade-diamond-proxy/upgrade-restakers.js b/projects/vaults/scripts/migration/upgrade-diamond-proxy/upgrade-restakers.js deleted file mode 100644 index 50c467aa..00000000 --- a/projects/vaults/scripts/migration/upgrade-diamond-proxy/upgrade-restakers.js +++ /dev/null @@ -1,84 +0,0 @@ -const { ethers, network } = require("hardhat"); -const { addresses } = require("../config-addresses"); - -async function main() { - /// - const [deployer] = await ethers.getSigners(); - - console.log("Deploying contracts with the account:", deployer.address); - const initBalance = await deployer.provider.getBalance(deployer.address); - console.log("Account balance:", initBalance.toString()); - - /// 1. deploy a new Restaker Implementation - - const BeaconProxyPatternV2 = await ethers.getContractFactory("InceptionEigenRestaker"); - const beaconImpl = await BeaconProxyPatternV2.deploy(); - await beaconImpl.waitForDeployment(); - const newRestakeImp = await beaconImpl.getAddress(); - console.log(`-------- Restaker has been deployed at the address: ${newRestakeImp}`); - - const iVaultOldFactory = await ethers.getContractFactory("EigenSetterFacet", { - libraries: { InceptionLibrary: INCEPTION_LIBRARY }, - }); - - /// 2. upgrade the Beacon's implementation for the vaults - - if (network.name === "mainnet") { - await createGnosisBatch(); - } else { - await upgradeOnTestnet(); - } - - if (network.name === "mainnet") { - return; - } - - /// 3. set rewardsCoordinator - console.log( - `We're going to set rewardsCoordinator(${addresses.RewardsCoordinator}) for all previously deployed Restakers`, - ); - - try { - for (const [vaultAddress, vaultRestakers] of restakers.entries()) { - if (!vaultAddress || !Array.isArray(vaultRestakers)) continue; - - for (const restakerAddr of vaultRestakers) { - if (!restakerAddr) continue; - - const restaker = BeaconProxyPatternV2.attach(restakerAddr); - tx = await restaker.setRewardsCoordinator(addresses.RewardsCoordinator); - await tx.wait(); - console.log( - `Restaker(${await restaker.getAddress()}) for ${vaultAddress} was updated with the RewardsCoordinator:`, - ); - } - } - } catch (error) { - console.error("Error processing restakers:", error); - } -} - -async function createGnosisBatch() {} - -async function upgradeOnTestnet(iVaultAddress) { - try { - for (const [vaultAddress, vaultRestakers] of restakers.entries()) { - if (!vaultAddress || !Array.isArray(vaultRestakers)) continue; - - const iVault = await ethers.getContractAt("EigenSetterFacet", vaultAddress); - let tx = await iVault.upgradeTo(newRestakeImp); - await tx.wait(); - console.log("Inception Restaker Impl has been upgraded for the vault: ", vaultAddress); - } - } catch (error) { - console.error("Error processing restakers:", error); - } -} - -main() - .then(() => process.exit(0)) - .catch(error => { - console.error(error); - process.exit(1); - }); - diff --git a/projects/vaults/test/InceptionToken.js b/projects/vaults/test/InceptionToken.ts similarity index 79% rename from projects/vaults/test/InceptionToken.js rename to projects/vaults/test/InceptionToken.ts index 16560927..75c8cf9d 100644 --- a/projects/vaults/test/InceptionToken.js +++ b/projects/vaults/test/InceptionToken.ts @@ -1,32 +1,15 @@ -///////////////////////////////////////////////// -///// Run with the default network, hardhat //// -/////////////////////////////////////////////// - -const { ethers, upgrades } = require("hardhat"); -const { expect } = require("chai"); - -const e18 = "1000000000000000000", - amount = "10000000"; - -let iToken, staker1, staker2; - -const initInceptionToken = async () => { - console.log(`... Initialization of Inception Token ...`); - - [owner, staker1, staker2] = await ethers.getSigners(); - - const iTokenFactory = await ethers.getContractFactory("InceptionToken"); - iToken = await upgrades.deployProxy(iTokenFactory, ["Test Token", "TT"]); - - await iToken.setVault(owner.address); - console.log(`... iToken initialization completed ....`); -}; +import hardhat from "hardhat"; +const { ethers, upgrades } = hardhat; +import { expect } from "chai"; +import { e18 } from "./helpers/utils"; +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; describe("Inception token", function () { - this.timeout(150000); + const amount = "10000000"; + let iToken: any, staker1: HardhatEthersSigner, staker2: HardhatEthersSigner, owner: HardhatEthersSigner; - before(async function () { - await initInceptionToken(); + before(async () => { + ({ iToken, staker1, staker2, owner } = await initInceptionToken()); }); describe("Pausable functionality", function () { @@ -92,3 +75,12 @@ describe("Inception token", function () { }); }); }); + +async function initInceptionToken() { + console.log(`Initialization of Inception Token ...`); + const [owner, staker1, staker2] = await ethers.getSigners(); + const iTokenFactory = await ethers.getContractFactory("InceptionToken"); + const iToken = await upgrades.deployProxy(iTokenFactory, ["Test Token", "TT"]); + await iToken.setVault(owner.address); + return { iToken, owner, staker1, staker2 } +}; diff --git a/projects/vaults/test/InceptionVault_E.js b/projects/vaults/test/InceptionVault_E.js deleted file mode 100644 index a2ccbf1d..00000000 --- a/projects/vaults/test/InceptionVault_E.js +++ /dev/null @@ -1,4721 +0,0 @@ -const { ethers, upgrades, network } = require("hardhat"); -const helpers = require("@nomicfoundation/hardhat-network-helpers"); -const config = require("../hardhat.config"); -const { expect } = require("chai"); -const { - addRewardsToStrategy, - withdrawDataFromTx, - impersonateWithEth, - getRandomStaker, - calculateRatio, - mineBlocks, - toWei, - randomBI, - randomBIMax, - randomAddress, - e18, - day, -} = require("./helpers/utils.js"); -const { applyProviderWrappers } = require("hardhat/internal/core/providers/construction"); -BigInt.prototype.format = function () { - return this.toLocaleString("de-DE"); -}; - -assets = [ - { - assetName: "rETH", - assetAddress: "0x7322c24752f79c05FFD1E2a6FCB97020C1C264F1", - assetPoolName: "RocketMockPool", - assetPool: "0x320f3aAB9405e38b955178BBe75c477dECBA0C27", - vaultName: "InrEthVault", - vaultFactory: "ERC4626Facet_EL_E2", - strategyManager: "0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6", - assetStrategy: "0x3A8fBdf9e77DFc25d09741f51d3E181b25d0c4E0", - iVaultOperator: "0xa4341b5Cf43afD2993e1ae47d956F44A2d6Fc08D", - delegationManager: "0xA44151489861Fe9e3055d95adC98FbD462B948e7", - rewardsCoordinator: "0xAcc1fb458a1317E886dB376Fc8141540537E68fE", - withdrawalDelayBlocks: 400, - ratioErr: 2n, - transactErr: 5n, - impersonateStaker: async (staker, iVault, asset, assetPool) => { - const donor = await impersonateWithEth("0x570EDBd50826eb9e048aA758D4d78BAFa75F14AD", toWei(1)); - await asset.connect(donor).transfer(staker.address, toWei(1000)); - const balanceAfter = await asset.balanceOf(staker.address); - await asset.connect(staker).approve(await iVault.getAddress(), balanceAfter); - return staker; - }, - }, - // { - // assetName: "stETH", - // assetAddress: "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", - // assetPoolName: "LidoMockPool", - // assetPool: "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", - // vaultName: "InstEthVault", - // vaultFactory: "ERC4626Facet_EL_E2", - // strategyManager: "0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6", - // assetStrategy: "0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3", - // iVaultOperator: "0xa4341b5Cf43afD2993e1ae47d956F44A2d6Fc08D", - // delegationManager: "0xA44151489861Fe9e3055d95adC98FbD462B948e7", - // rewardsCoordinator: "0xAcc1fb458a1317E886dB376Fc8141540537E68fE", - // withdrawalDelayBlocks: 20, - // ratioErr: 3n, - // transactErr: 5n, - // // blockNumber: 17453047, - // impersonateStaker: async (staker, iVault, asset, assetPool) => { - // const donor = await impersonateWithEth("0x66b25CFe6B9F0e61Bd80c4847225Baf4EE6Ba0A2", toWei(1)); - // await asset.connect(donor).transfer(staker.address, toWei(1000)); - // const balanceAfter = await asset.balanceOf(staker.address); - // await asset.connect(staker).approve(await iVault.getAddress(), balanceAfter); - // return staker; - // }, - // }, -]; - -//https://holesky.eigenlayer.xyz/restake -const nodeOperators = [ - "0x78FDDe7a5006cC64E109aeD99cA7B0Ad3d8687bb", - "0x1B71f18fc496194b21D0669B5ADfE299a8cFEc42", - "0x4Dbfa8bcccb1740d8044E1A093F9A078A88E45FE", - "0x5B9A8c72B29Ee17e72ba8B9626Bf43a75B15FB3d", - "0x139A091BcAad0ee1DAabe93cbBd194736B197FB6", -]; -const minWithdrawalDelayBlocks = 10; -const nodeOperatorToRestaker = new Map(); -const forcedWithdrawals = []; -let MAX_TARGET_PERCENT; - -const initVault = async a => { - const block = await ethers.provider.getBlock("latest"); - console.log(`Starting at block number: ${block.number}`); - console.log("... Initialization of Inception ...."); - - console.log("- Asset"); - const asset = await ethers.getContractAt(a.assetName, a.assetAddress); - console.log("- Asset pool"); - const assetPool = await ethers.getContractAt(a.assetPoolName, a.assetPool); - console.log("- Strategy"); - const strategy = await ethers.getContractAt("IStrategy", a.assetStrategy); - - // 1. Inception token - console.log("- iToken"); - const iTokenFactory = await ethers.getContractFactory("InceptionToken"); - const iToken = await upgrades.deployProxy(iTokenFactory, ["TEST InceptionLRT Token", "tINt"]); - iToken.address = await iToken.getAddress(); - // 2. Impersonate operator - const iVaultOperator = await impersonateWithEth(a.iVaultOperator, e18); - // 3. Staker implementation - console.log("- Restaker implementation"); - const restakerImp = await ethers.deployContract("InceptionEigenRestaker"); - restakerImp.address = await restakerImp.getAddress(); - // 4. Delegation manager - console.log("- Delegation manager"); - const delegationManager = await ethers.getContractAt("IDelegationManager", a.delegationManager); - await delegationManager.on("WithdrawalQueued", (newRoot, migratedWithdrawal) => { - console.log(`===Withdrawal queued: ${migratedWithdrawal.shares[0]}`); - }); - // 5. Ratio feed - console.log("- Ratio feed"); - const iRatioFeedFactory = await ethers.getContractFactory("InceptionRatioFeed"); - const ratioFeed = await upgrades.deployProxy(iRatioFeedFactory, []); - await ratioFeed.updateRatioBatch([iToken.address], [e18]); //Set initial ratio e18 - ratioFeed.address = await ratioFeed.getAddress(); - // 6. Inception library - console.log("- InceptionLibrary"); - const iLibrary = await ethers.deployContract("InceptionLibrary"); - await iLibrary.waitForDeployment(); - - // 7. Inception vault - console.log("- iVault"); - const iVaultFactory = await ethers.getContractFactory("InceptionVault_EL", { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const iVault = await upgrades.deployProxy( - iVaultFactory, - [a.vaultName, a.iVaultOperator, a.strategyManager, iToken.address, a.assetStrategy], - { unsafeAllowLinkedLibraries: true }, - ); - iVault.address = await iVault.getAddress(); - await iVault.on("DelegatedTo", (restaker, elOperator) => { - nodeOperatorToRestaker.set(elOperator, restaker); - }); - - /// =========================== FACETS =========================== - - const setterFacetFactory = await ethers.getContractFactory("EigenSetterFacet", { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const setterFacet = await setterFacetFactory.deploy(); - await setterFacet.waitForDeployment(); - await iVault.setSetterFacet(await setterFacet.getAddress()); - const iVaultSetters = await ethers.getContractAt("EigenSetterFacet", iVault.address); - - const eigenLayerFacetFactory = await ethers.getContractFactory("EigenLayerFacet", { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const eigenLayerFacet = await eigenLayerFacetFactory.deploy(); - await eigenLayerFacet.waitForDeployment(); - await iVault.setEigenLayerFacet(await eigenLayerFacet.getAddress()); - const iVaultEL = await ethers.getContractAt("EigenLayerFacet", iVault.address); - - const ERC4626FacetFactory = await ethers.getContractFactory(a.vaultFactory, { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const erc4626Facet = await ERC4626FacetFactory.deploy(); - await erc4626Facet.waitForDeployment(); - await iVault.setERC4626Facet(await erc4626Facet.getAddress()); - const iVault4626 = await ethers.getContractAt(a.vaultFactory, iVault.address); - - /// =========================== SET SIGNATURE <-> TARGETs =========================== - - /// =============================== SETTER =============================== - - let facetId = "0"; - let accessId = "2"; - - let funcSig = setterFacet.interface.getFunction("setDelegationManager").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setRewardsCoordinator").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("upgradeTo").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setRatioFeed").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("addELOperator").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setTreasuryAddress").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setOperator").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setMinAmount").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setName").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setTargetFlashCapacity").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setProtocolFee").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setDepositBonusParams").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setFlashWithdrawFeeParams").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = setterFacet.interface.getFunction("setRewardsTimeline").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - /// =============================== ################## =============================== - /// =============================== EigenLayer Handler =============================== - /// =============================== ################## =============================== - - facetId = "1"; - accessId = "1"; - - funcSig = eigenLayerFacet.interface.getFunction("delegateToOperator").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = eigenLayerFacet.interface.getFunction("undelegateFrom").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = eigenLayerFacet.interface.getFunction("undelegateVault").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = eigenLayerFacet.interface.getFunction("claimCompletedWithdrawals").selector; - /// Everyone is able to claim - await iVault.setSignature(funcSig, facetId, "0"); - - funcSig = eigenLayerFacet.interface.getFunction("updateEpoch").selector; - await iVault.setSignature(funcSig, facetId, "0"); - - funcSig = eigenLayerFacet.interface.getFunction("addRewards").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = eigenLayerFacet.interface.getFunction("forceUndelegateRecovery").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - /// ================================= ####### ================================= - /// ================================= ERC4626 ================================= - /// ================================= ####### ================================= - - facetId = "2"; - accessId = "0"; - - funcSig = ERC4626FacetFactory.interface.getFunction("deposit").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = ERC4626FacetFactory.interface.getFunction("mint").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = ERC4626FacetFactory.interface.getFunction("depositWithReferral").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = ERC4626FacetFactory.interface.getFunction("withdraw(uint256,address)").selector; - console.log(`funcSig: ${funcSig.toString()}`); - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = ERC4626FacetFactory.interface.getFunction("flashWithdraw").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = ERC4626FacetFactory.interface.getFunction("isAbleToRedeem").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = ERC4626FacetFactory.interface.getFunction("redeem(address)").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - funcSig = ERC4626FacetFactory.interface.getFunction("redeem(uint256,address,address)").selector; - await iVault.setSignature(funcSig, facetId, accessId); - - /// ================================= ####### ================================= - - [owner] = await ethers.getSigners(); - - await iVaultSetters.setDelegationManager(a.delegationManager); - await iVaultSetters.setRewardsCoordinator(a.rewardsCoordinator); - await iVaultSetters.upgradeTo(await restakerImp.getAddress()); - await iVaultSetters.setRatioFeed(await ratioFeed.getAddress()); - await iVaultSetters.addELOperator(nodeOperators[0]); - await iToken.setVault(await iVault.getAddress()); - - MAX_TARGET_PERCENT = await iVault.MAX_TARGET_PERCENT(); - // in % (100 * e18 == 100 %) - await iVaultSetters.setTargetFlashCapacity(1n); - console.log(`... iVault initialization completed ....`); - - iVault.withdrawFromELAndClaim = async function (nodeOperator, amount) { - let tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperator, amount); - const restaker = nodeOperatorToRestaker.get(nodeOperator); - const receipt = await tx.wait(); - if (receipt.logs.length !== 3) { - console.error("WRONG NUMBER OF EVENTS in withdrawFromEigenLayerEthAmount()", receipt.logs.length); - console.log(receipt.logs); - } - - // Loop through each log in the receipt - let WithdrawalQueuedEvent; - for (const log of receipt.logs) { - try { - const event = eigenLayerFacetFactory.interface.parseLog(log); - if (event != null) { - WithdrawalQueuedEvent = event.args; - } - } catch (error) { - console.error("Error parsing event log:", error); - } - } - - const withdrawalData = [ - WithdrawalQueuedEvent["stakerAddress"], - nodeOperator, - restaker, - WithdrawalQueuedEvent["nonce"], - WithdrawalQueuedEvent["withdrawalStartBlock"], - [WithdrawalQueuedEvent["strategy"]], - [WithdrawalQueuedEvent["shares"]], - ]; - - await mineBlocks(minWithdrawalDelayBlocks); - await iVaultEL.connect(iVaultOperator).claimCompletedWithdrawals(restaker, [withdrawalData]); - }; - - iVault.undelegateAndClaimVault = async function (nodeOperator, amount) { - const tx = await this.connect(iVaultOperator).undelegateVault(amount); - const restaker = await this.getAddress(); - const withdrawalData = await withdrawDataFromTx(tx, nodeOperator, restaker); - await mineBlocks(minWithdrawalDelayBlocks); - await this.connect(iVaultOperator).claimCompletedWithdrawals(restaker, [withdrawalData]); - }; - - return [ - iToken, - iVault, - ratioFeed, - asset, - assetPool, - strategy, - iVaultOperator, - restakerImp, - delegationManager, - iLibrary, - iVaultSetters, - iVaultEL, - iVault4626, - ]; -}; - -assets.forEach(function (a) { - describe(`Inception pool V2 ${a.assetName}`, function () { - this.timeout(150000); - let iToken, - iVault, - ratioFeed, - asset, - assetPool, - strategy, - restakerImp, - delegationManager, - iLibrary, - iVaultSetters, - iVaultEL, - iVault4626; - let iVaultOperator, deployer, staker, staker2, staker3, treasury; - let ratioErr, transactErr; - let snapshot; - - before(async function () { - if (process.env.ASSETS) { - const assets = process.env.ASSETS.toLocaleLowerCase().split(","); - if (!assets.includes(a.assetName.toLowerCase())) { - console.log(`${a.assetName} is not in the list, going to skip`); - this.skip(); - } - } - - await network.provider.send("hardhat_reset", [ - { - forking: { - jsonRpcUrl: a.url ? a.url : config.default.networks.hardhat.forking.url, - blockNumber: a.blockNumber ? a.blockNumber : config.default.networks.hardhat.forking.blockNumber, - }, - }, - ]); - - [ - iToken, - iVault, - ratioFeed, - asset, - assetPool, - strategy, - iVaultOperator, - restakerImp, - delegationManager, - iLibrary, - iVaultSetters, - iVaultEL, - iVault4626, - ] = await initVault(a); - ratioErr = a.ratioErr; - transactErr = a.transactErr; - - [deployer, staker, staker2, staker3] = await ethers.getSigners(); - staker = await a.impersonateStaker(staker, iVault, asset, assetPool); - staker2 = await a.impersonateStaker(staker2, iVault, asset, assetPool); - staker3 = await a.impersonateStaker(staker3, iVault, asset, assetPool); - treasury = await iVault.treasury(); //deployer - - snapshot = await helpers.takeSnapshot(); - }); - - after(async function () { - if (iVault) { - await iVault.removeAllListeners(); - } - if (delegationManager) { - await delegationManager.removeAllListeners(); - } - }); - - describe("Base flow", function () { - let deposited, extra; - before(async function () { - await snapshot.restore(); - }); - - it("Initial ratio is 1e18", async function () { - const ratio = await iVault.ratio(); - console.log(`Current ratio is:\t\t\t\t${ratio.format()}`); - expect(await iVault.asset()).to.be.eq(await asset.getAddress()); - expect(ratio).to.be.eq(e18); - }); - - it("Initial delegation is 0", async function () { - expect(await iVault.getTotalDelegated()).to.be.eq(0n); - }); - - it("Deposit to Vault", async function () { - deposited = toWei(20); - const expectedShares = (deposited * e18) / (await iVault.ratio()); - const tx = await iVault4626.connect(staker).deposit(deposited, staker.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.closeTo(deposited, transactErr); - expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); - console.log(`Ratio after: ${await iVault.ratio()}`); - - expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); - expect(await iVault.totalAssets()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getTotalDeposited()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet - expect(await calculateRatio(iVault, iToken)).to.be.eq(e18); - }); - - it("Balance and total supply", async function () { - expect(await iVault.balanceOf(staker.address)).to.be.eq(await iToken.balanceOf(staker.address)); - expect(await iVault.totalSupply()).to.be.eq(await iToken.totalSupply()); - }); - - it("Delegate partially", async function () { - const amount = (await iVault.totalAssets()) / 2n; - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - - const delegatedTotal = await iVault.getTotalDelegated(); - const delegatedTo = await iVault.getDelegatedTo(nodeOperators[0]); - expect(delegatedTotal).to.be.closeTo(amount, transactErr); - expect(delegatedTo).to.be.closeTo(amount, transactErr); - expect(await calculateRatio(iVault, iToken)).closeTo(e18, 1n); - }); - - it("Delegate all", async function () { - const amount = await iVault.getFreeBalance(); - - const event = await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - - const delegatedTotal = await iVault.getTotalDelegated(); - const delegatedTo = await iVault.getDelegatedTo(nodeOperators[0]); - expect(delegatedTotal).to.be.closeTo(deposited, transactErr); - expect(delegatedTo).to.be.closeTo(deposited, transactErr); - expect(await calculateRatio(iVault, iToken)).lte(e18); - }); - - it("Update asset ratio", async function () { - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const ratio = await calculateRatio(iVault, iToken); - console.log(`Calculated ratio:\t\t\t${ratio.format()}`); - await ratioFeed.updateRatioBatch([iToken.address], [ratio]); - console.log(`New ratio is:\t\t\t\t\t${(await iVault.ratio()).format()}`); - expect(await calculateRatio(iVault, iToken)).lt(e18); - }); - - it("Withdraw all", async function () { - const ratioBefore = await iVault.ratio(); - console.log(`ratioBefore: ${ratioBefore.toString()}`); - const shares = await iToken.balanceOf(staker.address); - const assetValue = await iVault.convertToAssets(shares); - console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); - console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); - - const tx = await iVault4626.connect(staker).withdraw(shares, staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["owner"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.eq(assetValue); - expect(events[0].args["iShares"]).to.be.eq(shares); - - const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - const totalPW = await iVault.totalAmountToWithdraw(); - expect(stakerPW).to.be.eq(0n); - expect(staker2PW).to.be.closeTo(assetValue, transactErr); - expect(totalPW).to.be.closeTo(assetValue, transactErr); - - console.log(`Total delegated:\t\t\t\t${(await iVault.getTotalDelegated()).format()}`); - expect(await calculateRatio(iVault, iToken)).to.be.eq(e18); - }); - - it("Withdraw from EigenLayer and claim", async function () { - const ratioBefore = await iVault.ratio(); - const totalAssetsBefore = await iVault.totalAssets(); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - let amount = await iVault.totalAmountToWithdraw(); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); - console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); - console.log(`Staker2 pending withdrawals:\t${staker2PW.format()}`); - - await iVault.withdrawFromELAndClaim(nodeOperators[0], amount); - extra = 0n; - if (!(await iVault.isAbleToRedeem(staker2.address))[0]) { - console.log( - `--- Going to change target flash capacity and transfer 1000 wei${a.assetName} to iVault to supply withdrawals ---`, - ); - await asset.connect(staker3).transfer(iVault.address, 1000n); - extra += 1000n; - await iVaultEL.connect(staker3).updateEpoch(); - } - - const totalAssetsAfter = await iVault.totalAssets(); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const redeemReserve = await iVault.redeemReservedAmount(); - - console.log(`Available withdrawals:\t${await iVault.isAbleToRedeem(staker2.address)}`); - console.log(`Total deposited after:\t${totalDepositedAfter.format()}`); - console.log(`Total delegated after:\t${totalDelegatedAfter.format()}`); - console.log(`Total assets after:\t\t${totalAssetsAfter.format()}`); - console.log(`Redeem reserve:\t\t\t${redeemReserve.format()}`); - - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount + extra, transactErr * 3n); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore + extra, transactErr); - expect(redeemReserve).to.be.eq(staker2PW); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - expect(totalDelegatedAfter).to.be.closeTo(0n, transactErr * 4n); - expect(await calculateRatio(iVault, iToken)).to.be.eq(e18); - }); - - it("Redeem withdraw", async function () { - const balanceBefore = await asset.balanceOf(staker2.address); - const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); - - const tx = await iVault4626.connect(iVaultOperator).redeem(staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Redeem"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); - - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - const balanceAfter = await asset.balanceOf(staker2.address); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); - console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); - - expect(staker2PWAfter).to.be.eq(0n); - expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); - expect(totalDepositedAfter).to.be.closeTo(extra, transactErr * 5n); - expect(totalAssetsAfter).to.be.closeTo(extra, transactErr * 3n); - }); - }); - - describe("Base flow with flash withdraw", function () { - let targetCapacity, deposited, freeBalance, depositFees; - before(async function () { - await snapshot.restore(); - targetCapacity = e18; - await iVaultSetters.setTargetFlashCapacity(targetCapacity); //1% - }); - - it("Initial ratio is 1e18", async function () { - const ratio = await iVault.ratio(); - console.log(`Current ratio is:\t\t\t\t${ratio.format()}`); - expect(ratio).to.be.eq(e18); - }); - - it("Initial delegation is 0", async function () { - expect(await iVault.getTotalDelegated()).to.be.eq(0n); - }); - - it("Deposit to Vault", async function () { - deposited = toWei(10); - freeBalance = (deposited * (MAX_TARGET_PERCENT - targetCapacity)) / MAX_TARGET_PERCENT; - const expectedShares = (deposited * e18) / (await iVault.ratio()); - const tx = await iVault4626.connect(staker).deposit(deposited, staker.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.closeTo(deposited, transactErr); - expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); - expect(receipt.logs.find(l => l.eventName === "DepositBonus")).to.be.undefined; - console.log(`Ratio after: ${await iVault.ratio()}`); - - expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); - expect(await iVault.totalAssets()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getFlashCapacity()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getFreeBalance()).to.be.closeTo(freeBalance, transactErr); - expect(await iVault.getTotalDeposited()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet - expect(await calculateRatio(iVault, iToken)).to.be.eq(e18); - }); - - it("Delegate freeBalance", async function () { - const totalDepositedBefore = await iVault.getTotalDeposited(); - const expectedFlashCapacity = (deposited * targetCapacity) / MAX_TARGET_PERCENT; - - const amount = await iVault.getFreeBalance(); - - await expect( - iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]), - ) - .to.emit(iVaultEL, "DelegatedTo") - .withArgs( - stakerAddress => { - expect(stakerAddress).to.be.properAddress; - expect(stakerAddress).to.be.not.eq(ethers.ZeroAddress); - return true; - }, - nodeOperators[0], - amount, - ); - - const delegatedTotal = await iVault.getTotalDelegated(); - const delegatedTo = await iVault.getDelegatedTo(nodeOperators[0]); - expect(totalDepositedBefore).to.be.closeTo(await iVault.getTotalDeposited(), 1n); - expect(delegatedTotal).to.be.closeTo(amount, transactErr); - expect(delegatedTo).to.be.closeTo(amount, transactErr); - expect(await iVault.getFreeBalance()).to.be.closeTo(0n, 1n); - expect(await iVault.getFlashCapacity()).to.be.closeTo(expectedFlashCapacity, 1n); - expect(await calculateRatio(iVault, iToken)).closeTo(e18, 1n); - }); - - it("Update asset ratio", async function () { - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - console.log(`New ratio is:\t\t\t\t\t${(await iVault.ratio()).format()}`); - expect(await calculateRatio(iVault, iToken)).lt(e18); - }); - - it("Flash withdraw all capacity", async function () { - const sharesBefore = await iToken.balanceOf(staker); - const assetBalanceBefore = await asset.balanceOf(staker); - const treasuryBalanceBefore = await asset.balanceOf(treasury); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const flashCapacityBefore = await iVault.getFlashCapacity(); - const freeBalanceBefore = await iVault.getFreeBalance(); - console.log(`Flash capacity before:\t${flashCapacityBefore.format()}`); - console.log(`Free balance before:\t${freeBalanceBefore.format()}`); - - const amount = await iVault.getFlashCapacity(); - const shares = await iVault.convertToShares(amount); - const receiver = staker; - const expectedFee = await iVault.calculateFlashWithdrawFee(await iVault.convertToAssets(shares)); - console.log(`Amount:\t\t\t\t\t${amount.format()}`); - console.log(`Shares:\t\t\t\t\t${shares.format()}`); - console.log(`Expected fee:\t\t\t\t${expectedFee.format()}`); - - let tx = await iVault4626.connect(staker).flashWithdraw(shares, receiver.address); - const receipt = await tx.wait(); - const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); - expect(withdrawEvent.length).to.be.eq(1); - expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); - expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); - expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); - expect(withdrawEvent[0].args["fee"]).to.be.closeTo(expectedFee, transactErr); - const collectedFees = withdrawEvent[0].args["fee"]; - depositFees = collectedFees / 2n; - - const sharesAfter = await iToken.balanceOf(staker); - const assetBalanceAfter = await asset.balanceOf(staker); - const treasuryBalanceAfter = await asset.balanceOf(treasury); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - console.log(`Shares balance diff:\t${(sharesBefore - sharesAfter).format()}`); - console.log(`Total deposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); - console.log(`Total assets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); - console.log(`Flash capacity diff:\t${(flashCapacityBefore - flashCapacityAfter).format()}`); - console.log(`Fee collected:\t\t\t${collectedFees.format()}`); - - expect(sharesBefore - sharesAfter).to.be.eq(shares); - expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); - expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); - expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); - expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); - }); - - it("Withdraw all", async function () { - const ratioBefore = await iVault.ratio(); - const shares = await iToken.balanceOf(staker.address); - const assetValue = await iVault.convertToAssets(shares); - console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); - console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); - - const tx = await iVault4626.connect(staker).withdraw(shares, staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["owner"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.eq(assetValue); - expect(events[0].args["iShares"]).to.be.eq(shares); - - const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - const totalPW = await iVault.totalAmountToWithdraw(); - expect(stakerPW).to.be.eq(0n); - expect(staker2PW).to.be.closeTo(assetValue, transactErr); - expect(totalPW).to.be.closeTo(assetValue, transactErr); - - console.log(`Total delegated:\t\t\t\t${(await iVault.getTotalDelegated()).format()}`); - console.log(`Total deposited:\t\t\t\t${(await iVault.getTotalDeposited()).format()}`); - expect(await calculateRatio(iVault, iToken)).to.be.eq(e18); - }); - - it("Withdraw from EigenLayer and claim", async function () { - const ratioBefore = await iVault.ratio(); - const totalAssetsBefore = await iVault.totalAssets(); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - const amount = await iVault.totalAmountToWithdraw(); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); - console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); - console.log(`Staker2 pending withdrawals:\t${staker2PW.format()}`); - - await iVault.withdrawFromELAndClaim(nodeOperators[0], amount); - - const totalAssetsAfter = await iVault.totalAssets(); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const redeemReserve = await iVault.redeemReservedAmount(); - - console.log(`Available withdrawals:\t${await iVault.isAbleToRedeem(staker2.address)}`); - console.log(`Total deposited after:\t${totalDepositedAfter.format()}`); - console.log(`Total delegated after:\t${totalDelegatedAfter.format()}`); - console.log(`Total assets after:\t\t${totalAssetsAfter.format()}`); - console.log(`Redeem reserve:\t\t\t${redeemReserve.format()}`); - - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr * 2n); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(redeemReserve).to.be.eq(staker2PW); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - expect(totalDelegatedAfter).to.be.closeTo(0n, transactErr * 4n); - expect(await calculateRatio(iVault, iToken)).to.be.eq(e18); - }); - - it("Redeem withdraw", async function () { - const balanceBefore = await asset.balanceOf(staker2.address); - const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); - - const tx = await iVault4626.connect(iVaultOperator).redeem(staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Redeem"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); - - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - const balanceAfter = await asset.balanceOf(staker2.address); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); - console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); - - expect(staker2PWAfter).to.be.eq(0n); - expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); - expect(totalDepositedAfter).to.be.closeTo(0n, transactErr * 5n); - expect(totalAssetsAfter).to.be.closeTo(depositFees, transactErr * 2n); - }); - }); - - describe("iVault setters", function () { - beforeEach(async function () { - await snapshot.restore(); - }); - - it("setTreasuryAddress(): only owner can", async function () { - const treasury = await iVault.treasury(); - const newTreasury = ethers.Wallet.createRandom().address; - - await expect(iVaultSetters.setTreasuryAddress(newTreasury)) - .to.emit(iVaultSetters, "TreasuryChanged") - .withArgs(treasury, newTreasury); - expect(await iVaultSetters.treasury()).to.be.eq(newTreasury); - }); - - it("setTreasuryAddress(): reverts when set to zero address", async function () { - await expect(iVaultSetters.setTreasuryAddress(ethers.ZeroAddress)).to.be.revertedWithCustomError( - iVault, - "NullParams", - ); - }); - - it("setTreasuryAddress(): reverts when caller is not an operator", async function () { - await expect(iVaultSetters.connect(staker).setTreasuryAddress(staker2.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setOperator(): only owner can", async function () { - const newOperator = staker2; - await expect(iVaultSetters.setOperator(newOperator.address)) - .to.emit(iVaultSetters, "OperatorChanged") - .withArgs(iVaultOperator.address, newOperator); - - await iVault4626.connect(staker).deposit(toWei(2), staker.address); - const amount = await iVault.getFreeBalance(); - await iVaultEL - .connect(newOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - }); - - it("setOperator(): reverts when set to zero address", async function () { - await expect(iVaultSetters.setOperator(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); - }); - - it("setOperator(): reverts when caller is not an operator", async function () { - await expect(iVaultSetters.connect(staker).setOperator(staker2.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("addELOperator(): only owner can", async function () { - const newELOperator = nodeOperators[1]; - await expect(iVaultSetters.addELOperator(newELOperator)) - .to.emit(iVaultSetters, "ELOperatorAdded") - .withArgs(newELOperator); - }); - - it("addELOperator(): reverts when caller is not an owner", async function () { - await expect(iVaultSetters.connect(staker).addELOperator(staker2.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("addELOperator(): reverts when address is not a staker-operator", async function () { - await expect(iVaultSetters.addELOperator(randomAddress())).to.be.revertedWithCustomError( - iVault, - "NotEigenLayerOperator", - ); - }); - - it("addELOperator(): reverts when address is zero address", async function () { - await expect(iVaultSetters.addELOperator(ethers.ZeroAddress)).to.be.revertedWithCustomError( - iVault, - "NotEigenLayerOperator", - ); - }); - - it("addELOperator(): reverts when address has been added already", async function () { - await expect(iVaultSetters.addELOperator(nodeOperators[0])).to.be.revertedWithCustomError( - iVault, - "EigenLayerOperatorAlreadyExists", - ); - }); - - it("setDelegationManager(): immutable", async function () { - const newManager = ethers.Wallet.createRandom().address; - await expect(iVaultSetters.setDelegationManager(newManager)).to.be.revertedWithCustomError( - iVault, - "DelegationManagerImmutable", - ); - }); - - it("setDelegationManager(): reverts when caller is not an operator", async function () { - await expect(iVaultSetters.connect(staker).setDelegationManager(staker2.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setRatioFeed(): only owner can", async function () { - const ratioFeed = await iVault.ratioFeed(); - const newRatioFeed = ethers.Wallet.createRandom().address; - await expect(iVaultSetters.setRatioFeed(newRatioFeed)) - .to.emit(iVaultSetters, "RatioFeedChanged") - .withArgs(ratioFeed, newRatioFeed); - expect(await iVault.ratioFeed()).to.be.eq(newRatioFeed); - }); - - it("setRatioFeed(): reverts when new value is zero address", async function () { - await expect(iVaultSetters.setRatioFeed(ethers.ZeroAddress)).to.be.revertedWithCustomError( - iVault, - "NullParams", - ); - }); - - it("setRatioFeed(): reverts when caller is not an owner", async function () { - const newRatioFeed = ethers.Wallet.createRandom().address; - await expect(iVaultSetters.connect(staker).setRatioFeed(newRatioFeed)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setMinAmount(): only owner can", async function () { - const prevValue = await iVault.minAmount(); - const newMinAmount = randomBI(3); - await expect(iVaultSetters.setMinAmount(newMinAmount)) - .to.emit(iVaultSetters, "MinAmountChanged") - .withArgs(prevValue, newMinAmount); - expect(await iVault.minAmount()).to.be.eq(newMinAmount); - }); - - it("setMinAmount(): another address can not", async function () { - await expect(iVaultSetters.connect(staker).setMinAmount(randomBI(3))).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setName(): only owner can", async function () { - const prevValue = await iVault.name(); - const newValue = "New name"; - await expect(iVaultSetters.setName(newValue)).to.emit(iVault, "NameChanged").withArgs(prevValue, newValue); - expect(await iVaultSetters.name()).to.be.eq(newValue); - }); - - it("setName(): reverts when name is blank", async function () { - await expect(iVaultSetters.setName("")).to.be.revertedWithCustomError(iVault, "NullParams"); - }); - - it("setName(): another address can not", async function () { - await expect(iVaultSetters.connect(staker).setName("New name")).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("updateEpoch(): reverts when paused", async function () { - await iVault.pause(); - await expect(iVaultEL.connect(iVaultOperator).updateEpoch()).to.be.revertedWith("Pausable: paused"); - }); - - it("pause(): only owner can", async function () { - expect(await iVault.paused()).is.false; - await iVault.pause(); - expect(await iVault.paused()).is.true; - }); - - it("pause(): another address can not", async function () { - await expect(iVault.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("pause(): reverts when already paused", async function () { - await iVault.pause(); - await expect(iVault.pause()).to.be.revertedWith("Pausable: paused"); - }); - - it("unpause(): only owner can", async function () { - await iVault.pause(); - expect(await iVault.paused()).is.true; - - await iVault.unpause(); - expect(await iVault.paused()).is.false; - }); - - it("unpause(): another address can not", async function () { - await iVault.pause(); - expect(await iVault.paused()).is.true; - await expect(iVault.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("upgradeTo(): only owner can", async function () { - const newRestakeImp = await ethers.deployContract("InceptionEigenRestaker"); - await expect(iVaultSetters.upgradeTo(await newRestakeImp.getAddress())) - .to.emit(iVaultSetters, "ImplementationUpgraded") - .withArgs(await restakerImp.getAddress(), await newRestakeImp.getAddress()); - }); - - it("upgradeTo(): reverts when set to zero address", async function () { - await expect(iVaultSetters.upgradeTo(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NotContract"); - }); - - it("upgradeTo(): reverts when caller is not an operator", async function () { - const newRestakeImp = await ethers.deployContract("InceptionEigenRestaker"); - await expect(iVaultSetters.connect(staker).upgradeTo(await newRestakeImp.getAddress())).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("upgradeTo(): reverts when paused", async function () { - const newRestakeImp = await ethers.deployContract("InceptionEigenRestaker"); - await iVault.pause(); - await expect(iVaultSetters.upgradeTo(await newRestakeImp.getAddress())).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - - it("setTargetFlashCapacity(): only owner can", async function () { - const prevValue = await iVault.targetCapacity(); - const newValue = randomBI(18); - await expect(iVaultSetters.connect(deployer).setTargetFlashCapacity(newValue)) - .to.emit(iVaultSetters, "TargetCapacityChanged") - .withArgs(prevValue, newValue); - expect(await iVault.targetCapacity()).to.be.eq(newValue); - }); - - it("setTargetFlashCapacity(): reverts when caller is not an owner", async function () { - const newValue = randomBI(18); - await expect(iVaultSetters.connect(staker).setTargetFlashCapacity(newValue)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setProtocolFee(): sets share of flashWithdrawFee that goes to treasury", async function () { - const prevValue = await iVault.protocolFee(); - const newValue = randomBI(10); - - await expect(iVaultSetters.setProtocolFee(newValue)) - .to.emit(iVault, "ProtocolFeeChanged") - .withArgs(prevValue, newValue); - expect(await iVaultSetters.protocolFee()).to.be.eq(newValue); - }); - - it("setProtocolFee(): reverts when > MAX_PERCENT", async function () { - const newValue = (await iVault.MAX_PERCENT()) + 1n; - await expect(iVaultSetters.setProtocolFee(newValue)) - .to.be.revertedWithCustomError(iVault, "ParameterExceedsLimits") - .withArgs(newValue); - }); - - it("setProtocolFee(): reverts when caller is not an owner", async function () { - const newValue = randomBI(10); - await expect(iVaultSetters.connect(staker).setProtocolFee(newValue)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setRewardsTimeline(): only owner can", async function () { - const prevValue = await iVaultSetters.rewardsTimeline(); - const newValue = randomBI(2) * day; - - await expect(iVaultSetters.setRewardsTimeline(newValue)) - .to.emit(iVault, "RewardsTimelineChanged") - .withArgs(prevValue, newValue); - expect(prevValue).to.be.eq(day * 7n); //default value is 7d - expect(await iVaultSetters.rewardsTimeline()).to.be.eq(newValue); - }); - - it("setRewardsTimeline(): reverts when < 1 day", async function () { - await expect(iVaultSetters.setRewardsTimeline(day - 1n)).to.be.revertedWithCustomError( - iVault, - "InconsistentData", - ); - }); - - it("setRewardsTimeline(): reverts when caller is not an owner", async function () { - const newValue = randomBI(6); - await expect(iVaultSetters.connect(staker).setRewardsTimeline(newValue)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setSignature(): reverts when called by not an owner", async function () { - const setterFacetFactory = await ethers.getContractFactory("EigenSetterFacet", { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const setterFacet = await setterFacetFactory.deploy(); - let funcSig = setterFacet.interface.getFunction("setDelegationManager").selector; - await expect(iVault.connect(staker).setSignature(funcSig, "0", "2")).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setEigenLayerFacet(): reverts when called by not an owner", async function () { - await expect( - iVault.connect(staker).setEigenLayerFacet(ethers.Wallet.createRandom().address), - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("setEigenLayerFacet(): reverts when not a contract", async function () { - await expect(iVault.setEigenLayerFacet(ethers.Wallet.createRandom().address)).to.be.revertedWithCustomError( - iVault, - "NotContract", - ); - }); - - it("setERC4626Facet(): reverts when called by not an owner", async function () { - await expect(iVault.connect(staker).setERC4626Facet(ethers.Wallet.createRandom().address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setERC4626Facet(): reverts when not a contract", async function () { - await expect(iVault.setERC4626Facet(ethers.Wallet.createRandom().address)).to.be.revertedWithCustomError( - iVault, - "NotContract", - ); - }); - - it("setSetterFacet(): reverts when called by not an owner", async function () { - await expect(iVault.connect(staker).setSetterFacet(ethers.Wallet.createRandom().address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setSetterFacet(): reverts when not a contract", async function () { - await expect(iVault.setSetterFacet(ethers.Wallet.createRandom().address)).to.be.revertedWithCustomError( - iVault, - "NotContract", - ); - }); - }); - - describe("Deposit bonus params setter and calculation", function () { - let targetCapacityPercent, MAX_PERCENT, localSnapshot; - before(async function () { - MAX_PERCENT = await iVault.MAX_PERCENT(); - }); - - const depositBonusSegment = [ - { - fromUtilization: async () => 0n, - fromPercent: async () => await iVault.maxBonusRate(), - toUtilization: async () => await iVault.depositUtilizationKink(), - toPercent: async () => await iVault.optimalBonusRate(), - }, - { - fromUtilization: async () => await iVault.depositUtilizationKink(), - fromPercent: async () => await iVault.optimalBonusRate(), - toUtilization: async () => await iVault.MAX_PERCENT(), - toPercent: async () => await iVault.optimalBonusRate(), - }, - { - fromUtilization: async () => await iVault.MAX_PERCENT(), - fromPercent: async () => 0n, - toUtilization: async () => ethers.MaxUint256, - toPercent: async () => 0n, - }, - ]; - - const args = [ - { - name: "Normal bonus rewards profile > 0", - newMaxBonusRate: BigInt(2 * 10 ** 8), //2% - newOptimalBonusRate: BigInt(0.2 * 10 ** 8), //0.2% - newDepositUtilizationKink: BigInt(25 * 10 ** 8), //25% - }, - { - name: "Optimal utilization = 0 => always optimal rate", - newMaxBonusRate: BigInt(2 * 10 ** 8), - newOptimalBonusRate: BigInt(10 ** 8), //1% - newDepositUtilizationKink: 0n, - }, - { - name: "Optimal bonus rate = 0", - newMaxBonusRate: BigInt(2 * 10 ** 8), - newOptimalBonusRate: 0n, - newDepositUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal bonus rate = max > 0 => rate is constant over utilization", - newMaxBonusRate: BigInt(2 * 10 ** 8), - newOptimalBonusRate: BigInt(2 * 10 ** 8), - newDepositUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal bonus rate = max = 0 => no bonus", - newMaxBonusRate: 0n, - newOptimalBonusRate: 0n, - newDepositUtilizationKink: BigInt(25 * 10 ** 8), - }, - //Will fail when OptimalBonusRate > MaxBonusRate - ]; - - const amounts = [ - { - name: "min amount from 0", - flashCapacity: targetCapacity => 0n, - amount: async () => (await iVault.convertToAssets(await iVault.minAmount())) + 1n, - }, - { - name: "1 wei from 0", - flashCapacity: targetCapacity => 0n, - amount: async () => 1n, - }, - { - name: "from 0 to 25% of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => (targetCapacityPercent * 25n) / 100n, - }, - { - name: "from 0 to 25% + 1wei of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => (targetCapacityPercent * 25n) / 100n, - }, - { - name: "from 25% to 100% of TARGET", - flashCapacity: targetCapacity => (targetCapacity * 25n) / 100n, - amount: async () => (targetCapacityPercent * 75n) / 100n, - }, - { - name: "from 0% to 100% of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => targetCapacityPercent, - }, - { - name: "from 0% to 200% of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => targetCapacityPercent * 2n, - }, - ]; - - args.forEach(function (arg) { - it(`setDepositBonusParams: ${arg.name}`, async function () { - await snapshot.restore(); - - await expect( - iVaultSetters.setDepositBonusParams( - arg.newMaxBonusRate, - arg.newOptimalBonusRate, - arg.newDepositUtilizationKink, - ), - ) - .to.emit(iVaultSetters, "DepositBonusParamsChanged") - .withArgs(arg.newMaxBonusRate, arg.newOptimalBonusRate, arg.newDepositUtilizationKink); - expect(await iVaultSetters.maxBonusRate()).to.be.eq(arg.newMaxBonusRate); - expect(await iVaultSetters.optimalBonusRate()).to.be.eq(arg.newOptimalBonusRate); - expect(await iVaultSetters.depositUtilizationKink()).to.be.eq(arg.newDepositUtilizationKink); - localSnapshot = await helpers.takeSnapshot(); - }); - - amounts.forEach(function (amount) { - it(`calculateDepositBonus for ${amount.name}`, async function () { - await localSnapshot.restore(); - const deposited = toWei(100); - targetCapacityPercent = e18; - const targetCapacity = (deposited * targetCapacityPercent) / MAX_TARGET_PERCENT; - await iVault4626.connect(staker).deposit(deposited, staker.address); - let flashCapacity = amount.flashCapacity(targetCapacity); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(deposited - flashCapacity - 1n, nodeOperators[0], ethers.ZeroHash, [ - ethers.ZeroHash, - 0, - ]); - await iVaultSetters.setTargetFlashCapacity(targetCapacityPercent); //1% - console.log(`Flash capacity:\t\t${await iVault.getFlashCapacity()}`); - - let _amount = await amount.amount(); - let depositBonus = 0n; - while (_amount > 0n) { - for (const feeFunc of depositBonusSegment) { - const utilization = (flashCapacity * MAX_PERCENT) / targetCapacity; - const fromUtilization = await feeFunc.fromUtilization(); - const toUtilization = await feeFunc.toUtilization(); - if (_amount > 0n && fromUtilization <= utilization && utilization < toUtilization) { - const fromPercent = await feeFunc.fromPercent(); - const toPercent = await feeFunc.toPercent(); - const upperBound = (toUtilization * targetCapacityPercent) / MAX_PERCENT; - const replenished = upperBound > flashCapacity + _amount ? _amount : upperBound - flashCapacity; - const slope = ((toPercent - fromPercent) * MAX_PERCENT) / (toUtilization - fromUtilization); - const bonusPercent = - fromPercent + (slope * (flashCapacity + replenished / 2n)) / targetCapacityPercent; - const bonus = (replenished * bonusPercent) / MAX_PERCENT; - console.log(`Replenished:\t\t\t${replenished.format()}`); - console.log(`Bonus percent:\t\t\t${bonusPercent.format()}`); - console.log(`Bonus:\t\t\t\t\t${bonus.format()}`); - flashCapacity += replenished; - _amount -= replenished; - depositBonus += bonus; - } - } - } - let contractBonus = await iVault.calculateDepositBonus(await amount.amount()); - console.log(`Expected deposit bonus:\t${depositBonus.format()}`); - console.log(`Contract deposit bonus:\t${contractBonus.format()}`); - expect(contractBonus).to.be.closeTo(depositBonus, 1n); - }); - }); - }); - - const invalidArgs = [ - { - name: "MaxBonusRate > MAX_PERCENT", - newMaxBonusRate: () => MAX_PERCENT + 1n, - newOptimalBonusRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newDepositUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "OptimalBonusRate > MAX_PERCENT", - newMaxBonusRate: () => BigInt(2 * 10 ** 8), - newOptimalBonusRate: () => MAX_PERCENT + 1n, - newDepositUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "DepositUtilizationKink > MAX_PERCENT", - newMaxBonusRate: () => BigInt(2 * 10 ** 8), - newOptimalBonusRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newDepositUtilizationKink: () => MAX_PERCENT + 1n, - customError: "ParameterExceedsLimits", - }, - ]; - invalidArgs.forEach(function (arg) { - it(`setDepositBonusParams reverts when ${arg.name}`, async function () { - await expect( - iVaultSetters.setDepositBonusParams( - arg.newMaxBonusRate(), - arg.newOptimalBonusRate(), - arg.newDepositUtilizationKink(), - ), - ).to.be.revertedWithCustomError(iVaultSetters, arg.customError); - }); - }); - - it("setDepositBonusParams reverts when caller is not an owner", async function () { - await expect( - iVaultSetters - .connect(staker) - .setDepositBonusParams(BigInt(2 * 10 ** 8), BigInt(0.2 * 10 ** 8), BigInt(25 * 10 ** 8)), - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - - describe("Withdraw fee params setter and calculation", function () { - let targetCapacityPercent, MAX_PERCENT, localSnapshot; - before(async function () { - MAX_PERCENT = await iVault.MAX_PERCENT(); - }); - - const withdrawFeeSegment = [ - { - fromUtilization: async () => 0n, - fromPercent: async () => await iVault.maxFlashFeeRate(), - toUtilization: async () => await iVault.withdrawUtilizationKink(), - toPercent: async () => await iVault.optimalWithdrawalRate(), - }, - { - fromUtilization: async () => await iVault.withdrawUtilizationKink(), - fromPercent: async () => await iVault.optimalWithdrawalRate(), - toUtilization: async () => ethers.MaxUint256, - toPercent: async () => await iVault.optimalWithdrawalRate(), - }, - ]; - - const args = [ - { - name: "Normal withdraw fee profile > 0", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), //2% - newOptimalWithdrawalRate: BigInt(0.2 * 10 ** 8), //0.2% - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal utilization = 0 => always optimal rate", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: BigInt(10 ** 8), //1% - newWithdrawUtilizationKink: 0n, - }, - { - name: "Optimal withdraw rate = 0", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: 0n, - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal withdraw rate = max > 0 => rate is constant over utilization", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: BigInt(2 * 10 ** 8), - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal withdraw rate = max = 0 => no fee", - newMaxFlashFeeRate: 0n, - newOptimalWithdrawalRate: 0n, - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - //Will fail when optimalWithdrawalRate > MaxFlashFeeRate - ]; - - const amounts = [ - { - name: "from 200% to 0% of TARGET", - flashCapacity: targetCapacity => targetCapacity * 2n, - amount: async () => await iVault.getFlashCapacity(), - }, - { - name: "from 100% to 0% of TARGET", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => await iVault.getFlashCapacity(), - }, - { - name: "1 wei from 100%", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => 1n, - }, - { - name: "min amount from 100%", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => (await iVault.convertToAssets(await iVault.minAmount())) + 1n, - }, - { - name: "from 100% to 25% of TARGET", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => (targetCapacityPercent * 75n) / 100n, - }, - { - name: "from 100% to 25% - 1wei of TARGET", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => (targetCapacityPercent * 75n) / 100n + 1n, - }, - { - name: "from 25% to 0% of TARGET", - flashCapacity: targetCapacity => (targetCapacity * 25n) / 100n, - amount: async () => await iVault.getFlashCapacity(), - }, - ]; - - args.forEach(function (arg) { - it(`setFlashWithdrawFeeParams: ${arg.name}`, async function () { - await snapshot.restore(); - await expect( - iVaultSetters.setFlashWithdrawFeeParams( - arg.newMaxFlashFeeRate, - arg.newOptimalWithdrawalRate, - arg.newWithdrawUtilizationKink, - ), - ) - .to.emit(iVaultSetters, "WithdrawFeeParamsChanged") - .withArgs(arg.newMaxFlashFeeRate, arg.newOptimalWithdrawalRate, arg.newWithdrawUtilizationKink); - - expect(await iVaultSetters.maxFlashFeeRate()).to.be.eq(arg.newMaxFlashFeeRate); - expect(await iVaultSetters.optimalWithdrawalRate()).to.be.eq(arg.newOptimalWithdrawalRate); - expect(await iVaultSetters.withdrawUtilizationKink()).to.be.eq(arg.newWithdrawUtilizationKink); - localSnapshot = await helpers.takeSnapshot(); - }); - - amounts.forEach(function (amount) { - it(`calculateFlashWithdrawFee for: ${amount.name}`, async function () { - await localSnapshot.restore(); - const deposited = toWei(100); - targetCapacityPercent = e18; - const targetCapacity = (deposited * targetCapacityPercent) / MAX_TARGET_PERCENT; - await iVault4626.connect(staker).deposit(deposited, staker.address); - let flashCapacity = amount.flashCapacity(targetCapacity); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(deposited - flashCapacity - 1n, nodeOperators[0], ethers.ZeroHash, [ - ethers.ZeroHash, - 0, - ]); - await iVaultSetters.setTargetFlashCapacity(targetCapacityPercent); //1% - console.log(`Flash capacity:\t\t\t${await iVault.getFlashCapacity()}`); - - let _amount = await amount.amount(); - let withdrawFee = 0n; - while (_amount > 1n) { - for (const feeFunc of withdrawFeeSegment) { - const utilization = (flashCapacity * MAX_PERCENT) / targetCapacity; - const fromUtilization = await feeFunc.fromUtilization(); - const toUtilization = await feeFunc.toUtilization(); - if (_amount > 0n && fromUtilization < utilization && utilization <= toUtilization) { - console.log(`Utilization:\t\t\t${utilization.format()}`); - const fromPercent = await feeFunc.fromPercent(); - const toPercent = await feeFunc.toPercent(); - const lowerBound = (fromUtilization * targetCapacityPercent) / MAX_PERCENT; - const replenished = lowerBound > flashCapacity - _amount ? flashCapacity - lowerBound : _amount; - const slope = ((toPercent - fromPercent) * MAX_PERCENT) / (toUtilization - fromUtilization); - const withdrawFeePercent = - fromPercent + (slope * (flashCapacity - replenished / 2n)) / targetCapacityPercent; - const fee = (replenished * withdrawFeePercent) / MAX_PERCENT; - console.log(`Replenished:\t\t\t${replenished.format()}`); - console.log(`Fee percent:\t\t\t${withdrawFeePercent.format()}`); - console.log(`Fee:\t\t\t\t\t${fee.format()}`); - flashCapacity -= replenished; - _amount -= replenished; - withdrawFee += fee; - } - } - } - let contractFee = await iVault.calculateFlashWithdrawFee(await amount.amount()); - console.log(`Expected withdraw fee:\t${withdrawFee.format()}`); - console.log(`Contract withdraw fee:\t${contractFee.format()}`); - expect(contractFee).to.be.closeTo(withdrawFee, 1n); - expect(contractFee).to.be.gt(0n); //flashWithdraw fee is always greater than 0 - }); - }); - }); - - const invalidArgs = [ - { - name: "MaxBonusRate > MAX_PERCENT", - newMaxFlashFeeRate: () => MAX_PERCENT + 1n, - newOptimalWithdrawalRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newWithdrawUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "OptimalBonusRate > MAX_PERCENT", - newMaxFlashFeeRate: () => BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: () => MAX_PERCENT + 1n, - newWithdrawUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "DepositUtilizationKink > MAX_PERCENT", - newMaxFlashFeeRate: () => BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newWithdrawUtilizationKink: () => MAX_PERCENT + 1n, - customError: "ParameterExceedsLimits", - }, - ]; - invalidArgs.forEach(function (arg) { - it(`setFlashWithdrawFeeParams reverts when ${arg.name}`, async function () { - await expect( - iVaultSetters.setFlashWithdrawFeeParams( - arg.newMaxFlashFeeRate(), - arg.newOptimalWithdrawalRate(), - arg.newWithdrawUtilizationKink(), - ), - ).to.be.revertedWithCustomError(iVault, arg.customError); - }); - }); - - it("calculateFlashWithdrawFee reverts when capacity is not sufficient", async function () { - await snapshot.restore(); - await iVault4626.connect(staker, staker).deposit(randomBI(19), staker.address); - const capacity = await iVault.getFlashCapacity(); - await expect(iVault.calculateFlashWithdrawFee(capacity + 1n)) - .to.be.revertedWithCustomError(iVault, "InsufficientCapacity") - .withArgs(capacity); - }); - - it("setFlashWithdrawFeeParams reverts when caller is not an owner", async function () { - await expect( - iVaultSetters - .connect(staker) - .setFlashWithdrawFeeParams(BigInt(2 * 10 ** 8), BigInt(0.2 * 10 ** 8), BigInt(25 * 10 ** 8)), - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - - describe("iToken management", function () { - beforeEach(async function () { - await snapshot.restore(); - }); - - it("Reverts: when not an owner mints", async function () { - await expect(iToken.connect(staker).mint(staker.address, toWei(1))).to.be.revertedWith( - "InceptionToken: only vault allowed", - ); - }); - - it("Reverts: when not an owner burns", async function () { - const amount = toWei(1); - await iVault4626.connect(staker).deposit(amount, staker.address); - await expect(iToken.connect(staker).burn(staker.address, toWei(1) / 2n)).to.be.revertedWith( - "InceptionToken: only vault allowed", - ); - }); - - it("setVault(): only owner can", async function () { - await expect(iToken.setVault(staker2.address)) - .to.emit(iToken, "VaultChanged") - .withArgs(await iVault.getAddress(), staker2.address); - expect(await iToken.vault()).to.be.eq(staker2.address); - }); - - it("setVault(): another address can not", async function () { - await expect(iToken.connect(staker).setVault(staker2.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("pause(): only owner can", async function () { - expect(await iToken.paused()).is.false; - await expect(iToken.pause()).to.emit(iToken, "Paused").withArgs(deployer.address); - expect(await iToken.paused()).is.true; - }); - - it("pause(): another address can not", async function () { - await expect(iToken.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("pause(): reverts when it has already been paused", async function () { - await iToken.pause(); - await expect(iToken.pause()).to.be.revertedWith("InceptionToken: paused"); - }); - - it("Reverts: deposit to iVault when iToken is paused", async function () { - await iToken.pause(); - await expect(iVault4626.connect(staker).deposit(toWei(1), staker.address)).to.be.revertedWith( - "InceptionToken: token transfer while paused", - ); - }); - - it("Reverts: deposit to iVault when iToken is paused", async function () { - await iToken.pause(); - await expect(iVault4626.connect(staker).deposit(toWei(1), staker.address)).to.be.revertedWith( - "InceptionToken: token transfer while paused", - ); - }); - - it("unpause(): only owner can", async function () { - await iToken.pause(); - expect(await iToken.paused()).is.true; - await expect(iToken.unpause()).to.emit(iToken, "Unpaused").withArgs(deployer.address); - expect(await iToken.paused()).is.false; - }); - - it("unpause(): another address can not", async function () { - await iToken.pause(); - expect(await iToken.paused()).is.true; - await expect(iToken.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("unpause(): when it is not paused", async function () { - await expect(iToken.unpause()).to.be.revertedWith("InceptionToken: not paused"); - }); - - it("User can transfer iToken", async function () { - await iVault4626.connect(staker).deposit(toWei(1), staker.address); - const amount = await iToken.balanceOf(staker.address); - await iToken.connect(staker).transfer(staker2.address, amount); - expect(await iToken.balanceOf(staker2.address)).to.be.eq(amount); - expect(await iToken.balanceOf(staker.address)).to.be.eq(0n); - }); - }); - - describe("InceptionEigenRestaker", function () { - let restaker, iVaultMock, trusteeManager; - - beforeEach(async function () { - await snapshot.restore(); - iVaultMock = staker2; - trusteeManager = staker3; - const factory = await ethers.getContractFactory("InceptionEigenRestaker", iVaultMock); - restaker = await upgrades.deployProxy(factory, [ - await owner.getAddress(), - a.rewardsCoordinator, - a.delegationManager, - a.strategyManager, - a.assetStrategy, - a.assetAddress, - trusteeManager.address, - ]); - }); - - it("depositAssetIntoStrategy: reverts when called by not a trustee", async function () { - const amount = toWei(1); - await asset.connect(iVaultMock).approve(await restaker.getAddress(), amount); - await expect(restaker.connect(staker).depositAssetIntoStrategy(amount)).to.be.revertedWithCustomError( - restaker, - "OnlyTrusteeAllowed", - ); - }); - - it("getOperatorAddress: equals 0 address before any delegation", async function () { - expect(await restaker.getOperatorAddress()).to.be.eq(ethers.ZeroAddress); - }); - - it("getOperatorAddress: equals operator after delegation", async function () { - const amount = toWei(1); - await asset.connect(iVaultMock).approve(await restaker.getAddress(), amount); - await restaker.connect(trusteeManager).depositAssetIntoStrategy(amount); - await restaker - .connect(trusteeManager) - .delegateToOperator(nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - expect(await restaker.getOperatorAddress()).to.be.eq(nodeOperators[0]); - }); - - it("delegateToOperator: reverts when called by not a trustee", async function () { - const amount = toWei(1); - await asset.connect(iVaultMock).approve(await restaker.getAddress(), amount); - await restaker.connect(trusteeManager).depositAssetIntoStrategy(amount); - - await expect( - restaker.connect(staker).delegateToOperator(nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWithCustomError(restaker, "OnlyTrusteeAllowed"); - }); - - it("delegateToOperator: reverts when delegates to 0 address", async function () { - const amount = toWei(1); - await asset.connect(iVaultMock).approve(await restaker.getAddress(), amount); - await restaker.connect(trusteeManager).depositAssetIntoStrategy(amount); - - await expect( - restaker - .connect(trusteeManager) - .delegateToOperator(ethers.ZeroAddress, ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWithCustomError(restaker, "NullParams"); - }); - - it("delegateToOperator: reverts when delegates unknown operator", async function () { - const amount = toWei(1); - await asset.connect(iVaultMock).approve(await restaker.getAddress(), amount); - await restaker.connect(trusteeManager).depositAssetIntoStrategy(amount); - - const unknownOperator = ethers.Wallet.createRandom().address; - await expect( - restaker.connect(trusteeManager).delegateToOperator(unknownOperator, ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWith("DelegationManager._delegate: operator is not registered in EigenLayer"); - }); - - it("withdrawFromEL: reverts when called by not a trustee", async function () { - const amount = toWei(1); - await asset.connect(iVaultMock).approve(await restaker.getAddress(), amount); - await restaker.connect(trusteeManager).depositAssetIntoStrategy(amount); - await restaker - .connect(trusteeManager) - .delegateToOperator(nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - - await expect(restaker.connect(staker).withdrawFromEL(amount / 2n)).to.be.revertedWithCustomError( - restaker, - "OnlyTrusteeAllowed", - ); - }); - - it("getVersion: equals 2", async function () { - expect(await restaker.getVersion()).to.be.eq(2); - }); - - it("pause(): only owner can", async function () { - expect(await restaker.paused()).is.false; - await restaker.connect(iVaultMock).pause(); - expect(await restaker.paused()).is.true; - }); - - it("pause(): another address can not", async function () { - await expect(restaker.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("unpause(): only owner can", async function () { - await restaker.connect(iVaultMock).pause(); - expect(await restaker.paused()).is.true; - - await restaker.connect(iVaultMock).unpause(); - expect(await restaker.paused()).is.false; - }); - - it("unpause(): another address can not", async function () { - await restaker.connect(iVaultMock).pause(); - expect(await restaker.paused()).is.true; - await expect(restaker.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - - describe("Deposit: user can restake asset", function () { - let ratio, TARGET; - - before(async function () { - await snapshot.restore(); - //Deposit to change ratio - try { - // await asset.connect(staker3).approve(await iVault.getAddress(), e18); - await iVault4626.connect(staker3).deposit(e18, staker3.address); - const amount = await iVault.totalAssets(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(nodeOperators[0], amount, ethers.ZeroHash, [ethers.ZeroHash, 0]); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const ratio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [ratio]); - } catch (e) { - console.warn("Deposit to strategy failed"); - } - ratio = await iVault.ratio(); - console.log(`Initial ratio: ${ratio.format()}`); - }); - - const args = [ - { - amount: async () => 4798072939323319141n, - receiver: () => staker.address, - }, - { - amount: async () => 999999999999999999n, - receiver: () => staker2.address, - }, - { - amount: async () => 888888888888888888n, - receiver: () => staker.address, - }, - { - amount: async () => 777777777777777777n, - receiver: () => staker.address, - }, - { - amount: async () => 666666666666666666n, - receiver: () => staker.address, - }, - { - amount: async () => 555555555555555555n, - receiver: () => staker.address, - }, - { - amount: async () => 444444444444444444n, - receiver: () => staker.address, - }, - { - amount: async () => 333333333333333333n, - receiver: () => staker.address, - }, - { - amount: async () => 222222222222222222n, - receiver: () => staker.address, - }, - { - amount: async () => 111111111111111111n, - receiver: () => staker.address, - }, - { - amount: async () => (await iVault.convertToAssets(await iVault.minAmount())) + 1n, - receiver: () => staker.address, - }, - ]; - - args.forEach(function (arg) { - it(`Deposit amount ${arg.amount}`, async function () { - const receiver = arg.receiver(); - const balanceBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - - const amount = await arg.amount(); - const convertedShares = await iVault.convertToShares(amount); - const expectedShares = (amount * (await iVault.ratio())) / e18; - - const tx = await iVault4626.connect(staker).deposit(amount, receiver); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(receiver); - expect(events[0].args["amount"]).to.be.closeTo(amount, transactErr); - expect(events[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); - - const balanceAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after: ${ratioAfter}`); - - expect(balanceAfter - balanceBefore).to.be.closeTo(expectedShares, transactErr); - expect(balanceAfter - balanceBefore).to.be.closeTo(convertedShares, transactErr); - - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit - expect(ratioAfter).to.be.closeTo(ratio, ratioErr); //Ratio stays the same - }); - - it(`Mint amount ${arg.amount()}`, async function () { - const receiver = arg.receiver(); - const balanceBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - - const shares = await arg.amount(); - const convertedAmount = await iVault.convertToAssets(shares); - - const tx = await iVault4626.connect(staker).mint(shares, receiver); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(receiver); - expect(events[0].args["amount"]).to.be.closeTo(convertedAmount, transactErr); - expect(events[0].args["iShares"]).to.be.closeTo(shares, transactErr); - - const balanceAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after: ${ratioAfter}`); - - expect(balanceAfter - balanceBefore).to.be.closeTo(shares, transactErr); - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(convertedAmount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(convertedAmount, transactErr); //Everything stays on iVault after deposit - expect(ratioAfter).to.be.closeTo(ratio, ratioErr); //Ratio stays the same - }); - - it("Delegate free balance", async function () { - const delegatedBefore = await iVault.getDelegatedTo(nodeOperators[0]); - const totalDepositedBefore = await iVault.getTotalDeposited(); - console.log(`Delegated before: ${delegatedBefore}`); - console.log(`Total deposited before: ${totalDepositedBefore}`); - - const amount = await iVault.getFreeBalance(); - const tx = await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "DelegatedTo"); - expect(events.length).to.be.eq(1); - expect(events[0].args["stakerAddress"]).to.be.not.eq(ethers.ZeroAddress); - expect(events[0].args["stakerAddress"]).to.be.properAddress; - expect(events[0].args["operatorAddress"]).to.be.eq(nodeOperators[0]); - - const delegatedAfter = await iVault.getDelegatedTo(nodeOperators[0]); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after: ${ratioAfter}`); - - expect(delegatedAfter - delegatedBefore).to.be.closeTo(amount, transactErr); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(totalAssetsAfter).to.be.lte(transactErr); - }); - }); - - const depositInvalidArgs = [ - { - name: "amount is 0", - amount: async () => 0n, - receiver: () => staker.address, - isCustom: true, - error: "LowerMinAmount", - }, - { - name: "amount < min", - amount: async () => (await iVault.minAmount()) - 1n, - receiver: () => staker.address, - isCustom: true, - error: "LowerMinAmount", - }, - { - name: "to zero address", - amount: async () => randomBI(18), - isCustom: true, - receiver: () => ethers.ZeroAddress, - error: "NullParams", - }, - ]; - - depositInvalidArgs.forEach(function (arg) { - it(`Reverts when: deposit ${arg.name}`, async function () { - const amount = await arg.amount(); - const receiver = arg.receiver(); - if (arg.isCustom) { - await expect(iVault4626.connect(staker).deposit(amount, receiver)).to.be.revertedWithCustomError( - iVault, - arg.error, - ); - } else { - await expect(iVault4626.connect(staker).deposit(amount, receiver)).to.be.revertedWith(arg.error); - } - }); - }); - - it("Reverts: deposit when iVault is paused", async function () { - await iVault.pause(); - const amount = randomBI(19); - await expect(iVault4626.connect(staker).deposit(amount, staker.address)).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - - it("Reverts: mint when iVault is paused", async function () { - await iVault.pause(); - const shares = randomBI(19); - await expect(iVault4626.connect(staker).mint(shares, staker.address)).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - - it("Reverts: depositWithReferral when iVault is paused", async function () { - await iVault.pause(); - const depositAmount = randomBI(19); - const code = ethers.encodeBytes32String(randomAddress().slice(0, 8)); - await expect(iVault4626.connect(staker).depositWithReferral(depositAmount, staker, code)).to.be.revertedWith( - "Pausable: paused", - ); - await iVault.unpause(); - }); - - it("Reverts: deposit when targetCapacity is not set", async function () { - await iVaultSetters.setTargetFlashCapacity(0n); - const depositAmount = randomBI(19); - await expect(iVault4626.connect(staker).deposit(depositAmount, staker.address)).to.be.revertedWithCustomError( - iVault, - "InceptionOnPause", - ); - }); - - const convertSharesArgs = [ - { - name: "amount = 0", - amount: async () => 0n, - }, - { - name: "amount = 1", - amount: async () => 0n, - }, - { - name: "amount < min", - amount: async () => (await iVault.minAmount()) - 1n, - }, - ]; - - convertSharesArgs.forEach(function (arg) { - it(`Convert to shares: ${arg.name}`, async function () { - const amount = await arg.amount(); - const ratio = await iVault.ratio(); - expect(await iVault.convertToShares(amount)).to.be.eq((amount * ratio) / e18); - }); - }); - - it("Max mint and deposit", async function () { - const stakerBalance = await asset.balanceOf(staker); - const calculatedBonus = await iVault.calculateDepositBonus(stakerBalance); - const realBonus = await iVault.depositBonusAmount(); - const bonus = realBonus > calculatedBonus ? calculatedBonus : realBonus; - expect(await iVault.maxMint(staker)).to.be.eq(await iVault.convertToShares(stakerBalance + bonus)); - expect(await iVault.maxDeposit(staker)).to.be.eq(stakerBalance); - }); - - it("Max mint and deposit when iVault is paused equal 0", async function () { - await iVault.pause(); - const maxMint = await iVault.maxMint(staker); - const maxDeposit = await iVault.maxDeposit(staker); - await iVault.unpause(); - expect(maxMint).to.be.eq(0n); - expect(maxDeposit).to.be.eq(0n); - }); - - it("Max mint and deposit reverts when > available amount", async function () { - const maxMint = await iVault.maxMint(staker); - await expect(iVault4626.connect(staker).mint(maxMint + 1n, staker.address)).to.be.revertedWithCustomError( - iVault4626, - "ExceededMaxMint", - ); - }); - }); - - describe("Deposit with bonus for replenish", function () { - const states = [ - { - name: "deposit bonus = 0", - withBonus: false, - }, - { - name: "deposit bonus > 0", - withBonus: true, - }, - ]; - - const amounts = [ - { - name: "for the first time", - predepositAmount: targetCapacity => 0n, - amount: targetCapacity => randomBIMax(targetCapacity / 4n) + targetCapacity / 4n, - receiver: () => staker.address, - }, - { - name: "more", - predepositAmount: targetCapacity => targetCapacity / 3n, - amount: targetCapacity => randomBIMax(targetCapacity / 3n), - receiver: () => staker.address, - }, - { - name: "up to target cap", - predepositAmount: targetCapacity => targetCapacity / 10n, - amount: targetCapacity => (targetCapacity * 9n) / 10n, - receiver: () => staker.address, - }, - { - name: "all rewards", - predepositAmount: targetCapacity => 0n, - amount: targetCapacity => targetCapacity, - receiver: () => staker.address, - }, - { - name: "up to target cap and above", - predepositAmount: targetCapacity => targetCapacity / 10n, - amount: targetCapacity => targetCapacity, - receiver: () => staker.address, - }, - { - name: "above target cap", - predepositAmount: targetCapacity => targetCapacity, - amount: targetCapacity => randomBI(19), - receiver: () => staker.address, - }, - ]; - - states.forEach(function (state) { - let localSnapshot; - const targetCapacityPercent = e18; - const targetCapacity = e18; - it(`---Prepare state: ${state.name}`, async function () { - await snapshot.restore(); - const deposited = (targetCapacity * MAX_TARGET_PERCENT) / targetCapacityPercent; - if (state.withBonus) { - await iVaultSetters.setTargetFlashCapacity(targetCapacityPercent); - await iVault4626.connect(staker3).deposit(toWei(1.5), staker3.address); - const balanceOf = await iToken.balanceOf(staker3.address); - await iVault4626.connect(staker3).flashWithdraw(balanceOf, staker3.address); - await iVaultSetters.setTargetFlashCapacity(1n); - } - - await iVault4626.connect(staker3).deposit(deposited, staker3.address); - console.log(`Total assets:\t\t${(await iVault.totalAssets()).format()}`); - console.log(`Deposit bonus:\t\t${(await iVault.depositBonusAmount()).format()}`); - localSnapshot = await helpers.takeSnapshot(); - }); - - it("Max mint and deposit", async function () { - const stakerBalance = await asset.balanceOf(staker); - const calculatedBonus = await iVault.calculateDepositBonus(stakerBalance); - const realBonus = await iVault.depositBonusAmount(); - const bonus = realBonus > calculatedBonus ? calculatedBonus : realBonus; - expect(await iVault.maxMint(staker)).to.be.eq(await iVault.convertToShares(stakerBalance + bonus)); - expect(await iVault.maxDeposit(staker)).to.be.eq(stakerBalance); - }); - - amounts.forEach(function (arg) { - it(`Deposit ${arg.name}`, async function () { - if (localSnapshot) { - await localSnapshot.restore(); - } else { - expect(false).to.be.true("Can not restore local snapshot"); - } - - const flashCapacityBefore = arg.predepositAmount(targetCapacity); - const freeBalance = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalance - flashCapacityBefore, nodeOperators[0], ethers.ZeroHash, [ - ethers.ZeroHash, - 0, - ]); - await iVaultSetters.setTargetFlashCapacity(targetCapacityPercent); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - - const ratioBefore = await iVault.ratio(); - let availableBonus = await iVault.depositBonusAmount(); - const receiver = arg.receiver(); - const stakerSharesBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - console.log(`Target capacity:\t\t${targetCapacity.format()}`); - console.log(`Flash capacity before:\t${flashCapacityBefore.format()}`); - - const amount = await arg.amount(targetCapacity); - console.log(`Amount:\t\t\t\t\t${amount.format()}`); - const calculatedBonus = await iVault.calculateDepositBonus(amount); - console.log(`Calculated bonus:\t\t${calculatedBonus.format()}`); - console.log(`Available bonus:\t\t${availableBonus.format()}`); - const expectedBonus = calculatedBonus <= availableBonus ? calculatedBonus : availableBonus; - availableBonus -= expectedBonus; - console.log(`Expected bonus:\t\t\t${expectedBonus.format()}`); - const convertedShares = await iVault.convertToShares(amount + expectedBonus); - const expectedShares = ((amount + expectedBonus) * (await iVault.ratio())) / e18; - const previewShares = await iVault.previewDeposit(amount); - - const tx = await iVault4626.connect(staker).deposit(amount, receiver); - const receipt = await tx.wait(); - const depositEvent = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(depositEvent.length).to.be.eq(1); - expect(depositEvent[0].args["sender"]).to.be.eq(staker.address); - expect(depositEvent[0].args["receiver"]).to.be.eq(receiver); - expect(depositEvent[0].args["amount"]).to.be.closeTo(amount, transactErr); - expect(depositEvent[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); - //DepositBonus event - expect(receipt.logs.find(l => l.eventName === "DepositBonus")?.args.amount || 0n).to.be.closeTo( - expectedBonus, - transactErr, - ); - - const stakerSharesAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after:\t\t\t${ratioAfter.format()}`); - console.log(`Bonus after:\t\t\t${availableBonus.format()}`); - - expect(stakerSharesAfter - stakerSharesBefore).to.be.closeTo(expectedShares, transactErr); - expect(stakerSharesAfter - stakerSharesBefore).to.be.closeTo(convertedShares, transactErr); - - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount + expectedBonus, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit - expect(flashCapacityAfter).to.be.closeTo(flashCapacityBefore + amount + expectedBonus, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBefore, ratioErr); //Ratio stays the same - expect(previewShares).to.be.eq(stakerSharesAfter - stakerSharesBefore); //Ratio stays the same - }); - }); - }); - }); - - describe("Deposit and delegateToOperator", function () { - let ratio, firstDeposit; - - beforeEach(async function () { - await snapshot.restore(); - await asset.connect(staker3).approve(await iVault.getAddress(), e18); - await iVault4626.connect(staker3).deposit(e18, staker3.address); - firstDeposit = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(firstDeposit, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - await addRewardsToStrategy(a.assetStrategy, toWei(0.001), staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - console.log(`Initial ratio: ${ratio.format()}`); - }); - - const args2 = [ - { - name: "random amounts ~ e18", - depositAmount: async () => toWei(1), - }, - { - name: "amounts which are close to min", - depositAmount: async () => (await iVault.minAmount()) + 1n, - }, - ]; - - args2.forEach(function (arg) { - it(`Deposit and delegate ${arg.name} many times`, async function () { - await iVaultSetters.setTargetFlashCapacity(1n); - let totalDelegated = 0n; - const count = 10; - for (let i = 0; i < count; i++) { - const deposited = await arg.depositAmount(); - await iVault4626.connect(staker).deposit(deposited, staker.address); - const delegated = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(delegated, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - - totalDelegated += deposited; - } - console.log(`Final ratio:\t${(await iVault.ratio()).format()}`); - console.log(`Total delegated:\t${totalDelegated.format()}`); - - const balanceExpected = (totalDelegated * ratio) / e18; - const totalSupplyExpected = balanceExpected + firstDeposit; - const err = BigInt(count) * transactErr * 2n; - - const balanceAfter = await iToken.balanceOf(staker.address); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedToAfter = await iVault.getDelegatedTo(nodeOperators[0]); - const totalSupplyAfter = await iToken.totalSupply(); - const totalAssetsAfter = await iVault.totalAssets(); - console.log(`Staker balance after: ${balanceAfter.format()}`); - console.log(`Total deposited after: ${totalDepositedAfter.format()}`); - console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); - console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); - console.log(`Total assets after: ${totalAssetsAfter.format()}`); - - expect(balanceAfter - balanceExpected).to.be.closeTo(0, err); - expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0, err); - expect(totalAssetsAfter).to.be.lte(transactErr); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, BigInt(count) * ratioErr); - }); - }); - - const args3 = [ - { - name: "by the same staker", - staker: async () => staker, - }, - { - name: "by different stakers", - staker: async () => await getRandomStaker(iVault, asset, staker3, toWei(1)), - }, - ]; - - args3.forEach(function (arg) { - it(`Deposit many times and delegate once ${arg.name}`, async function () { - await iVaultSetters.setTargetFlashCapacity(1n); - let totalDeposited = 0n; - const count = 10; - for (let i = 0; i < count; i++) { - const staker = await arg.staker(); - const deposited = await randomBI(18); - await iVault4626.connect(staker).deposit(deposited, staker.address); - totalDeposited += deposited; - } - const totalDelegated = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(totalDelegated, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - console.log(`Final ratio:\t${await iVault.ratio()}`); - console.log(`Total deposited:\t${totalDeposited.format()}`); - console.log(`Total delegated:\t${totalDelegated.format()}`); - - const balanceExpected = (totalDelegated * ratio) / e18; - const totalSupplyExpected = balanceExpected + firstDeposit; - const err = BigInt(count) * transactErr * 2n; - - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedToAfter = await iVault.getDelegatedTo(nodeOperators[0]); - const totalSupplyAfter = await iToken.totalSupply(); - const totalAssetsAfter = await iVault.totalAssets(); - console.log(`Total deposited after: ${totalDepositedAfter.format()}`); - console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); - console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); - console.log(`Total assets after: ${totalAssetsAfter.format()}`); - - expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0n, err); - expect(totalAssetsAfter).to.be.lte(transactErr); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, BigInt(count) * ratioErr); - }); - }); - - const args4 = [ - { - name: "to the different operators", - count: 20, - stakerOperator: i => nodeOperators[i % nodeOperators.length], - }, - { - name: "to the same operator", - count: 10, - stakerOperator: i => nodeOperators[0], - }, - ]; - - args4.forEach(function (arg) { - it(`Delegate many times ${arg.name}`, async function () { - await iVaultSetters.setTargetFlashCapacity(1n); - //Deposit by 2 stakers - const totalDelegated = toWei(60); - await iVault4626.connect(staker).deposit(totalDelegated / 2n, staker.address); - await iVault4626.connect(staker2).deposit(totalDelegated / 2n, staker2.address); - const deployedStakers = [nodeOperators[0]]; - //Delegate - for (let i = 0; i < arg.count; i++) { - const taBefore = await iVault.totalAssets(); - const stakerOperator = arg.stakerOperator(i); - console.log(`#${i} Operator: ${stakerOperator}`); - - const isFirstDelegation = !deployedStakers.includes(stakerOperator); - if (isFirstDelegation) { - await iVaultSetters.addELOperator(stakerOperator); //Add operator - deployedStakers.push(stakerOperator); //Remember the address - } - const fb = await iVault.getFreeBalance(); - const amount = fb / BigInt(arg.count - i); - const tx = await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, stakerOperator, ethers.ZeroHash, [ethers.ZeroHash, 0]); - const receipt = await tx.wait(); - let events = receipt.logs?.filter(e => { - return e.eventName === "DelegatedTo"; - }); - expect(events.length).to.be.eq(1); - expect(events[0].args["stakerAddress"]).to.be.not.eq(ethers.ZeroAddress); - expect(events[0].args["stakerAddress"]).to.be.properAddress; - expect(events[0].args["operatorAddress"]).to.be.eq(stakerOperator); - - //Check that RestakerDeployed event was emitted on the first delegation - if (isFirstDelegation) { - let events = receipt.logs?.filter(e => { - return e.eventName === "RestakerDeployed"; - }); - expect(events.length).to.be.eq(1); - expect(events[0].args["restaker"]).to.be.not.eq(ethers.ZeroAddress); - expect(events[0].args["restaker"]).to.be.properAddress; - } else { - expect(receipt.logs.map(e => e.event)).to.not.include("RestakerDeployed"); - } - const taAfter = await iVault.totalAssets(); - expect(taBefore - taAfter).to.be.closeTo(amount, transactErr); - } - console.log(`Final ratio:\t${await iVault.ratio()}`); - - const balanceExpected = (totalDelegated * ratio) / e18; - const totalSupplyExpected = balanceExpected + firstDeposit; - const err = BigInt(arg.count) * transactErr * 2n; - - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedToAfter = await iVault.getDelegatedTo(nodeOperators[0]); - const totalSupplyAfter = await iToken.totalSupply(); - const totalAssetsAfter = await iVault.totalAssets(); - console.log(`Total deposited after: ${totalDepositedAfter.format()}`); - console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); - console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); - console.log(`Total assets after: ${totalAssetsAfter.format()}`); - - expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0, err); - expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0, err); - expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0, err); - expect(totalAssetsAfter).to.be.lte(transactErr); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, BigInt(arg.count) * ratioErr); - }); - }); - - //Delegate invalid params - const invalidArgs = [ - { - name: "amount is 0", - deposited: toWei(1), - amount: async () => 0n, - stakerOperator: async () => nodeOperators[0], - operator: () => iVaultOperator, - }, - { - name: "amount is 1", - deposited: toWei(1), - amount: async () => 1n, - stakerOperator: async () => nodeOperators[0], - operator: () => iVaultOperator, - error: "StrategyBase.deposit: newShares cannot be zero", - }, - { - name: "amount is greater than free balance", - deposited: toWei(10), - targetCapacityPercent: e18, - amount: async () => (await iVault.getFreeBalance()) + 1n, - stakerOperator: async () => nodeOperators[0], - operator: () => iVaultOperator, - isCustom: true, - error: "InsufficientCapacity", - }, - { - name: "operator is not added to iVault", - deposited: toWei(1), - amount: async () => await iVault.getFreeBalance(), - stakerOperator: async () => nodeOperators[1], - operator: () => iVaultOperator, - isCustom: true, - error: "OperatorNotRegistered", - }, - { - name: "operator is zero address", - deposited: toWei(1), - amount: async () => await iVault.totalAssets(), - stakerOperator: async () => ethers.ZeroAddress, - operator: () => iVaultOperator, - isCustom: true, - error: "NullParams", - }, - { - name: "caller is not an operator", - deposited: toWei(1), - amount: async () => await iVault.totalAssets(), - stakerOperator: async () => ethers.ZeroAddress, - operator: () => staker, - isCustom: true, - error: "OnlyOperatorAllowed", - }, - ]; - - invalidArgs.forEach(function (arg) { - it(`Reverts when: delegate ${arg.name}`, async function () { - if (arg.targetCapacityPercent) { - await iVaultSetters.setTargetFlashCapacity(arg.targetCapacityPercent); - } - await asset.connect(staker3).approve(await iVault.getAddress(), arg.deposited); - await iVault4626.connect(staker3).deposit(arg.deposited, staker3.address); - - const operator = arg.operator(); - const delegateAmount = await arg.amount(); - const stakerOperator = await arg.stakerOperator(); - - if (arg.isCustom) { - await expect( - iVaultEL - .connect(operator) - .delegateToOperator(delegateAmount, stakerOperator, ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWithCustomError(iVault, arg.error); - } else if (arg.error) { - await expect( - iVaultEL - .connect(operator) - .delegateToOperator(delegateAmount, stakerOperator, ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWith(arg.error); - } else { - await expect( - iVaultEL - .connect(operator) - .delegateToOperator(delegateAmount, stakerOperator, ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.reverted; - } - }); - }); - - it("Deposit with Referral code", async function () { - const receiver = staker; - const balanceBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const amount = await toWei(1); - const convertedShares = await iVault.convertToShares(amount); - const expectedShares = (amount * (await iVault.ratio())) / e18; - const code = ethers.encodeBytes32String(randomAddress().slice(0, 8)); - const tx = await iVault4626.connect(staker2).depositWithReferral(amount, receiver, code); - const receipt = await tx.wait(); - let events = receipt.logs?.filter(e => { - return e.eventName === "Deposit"; - }); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker2.address); - expect(events[0].args["receiver"]).to.be.eq(receiver); - expect(events[0].args["amount"]).to.be.closeTo(amount, transactErr); - expect(events[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); - //Code event - events = receipt.logs?.filter(e => { - return e.eventName === "ReferralCode"; - }); - expect(events.length).to.be.eq(1); - expect(events[0].args["code"]).to.be.eq(code); - - const balanceAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - - expect(balanceAfter - balanceBefore).to.be.closeTo(expectedShares, transactErr); - expect(balanceAfter - balanceBefore).to.be.closeTo(convertedShares, transactErr); - - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, ratioErr); //Ratio stays the same - }); - - it("Reverts: delegate when iVault is paused", async function () { - const amount = randomBI(18); - await iVault4626.connect(staker).deposit(amount, staker.address); - await iVault.pause(); - await expect( - iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - - it("Reverts: when there is no restaker implementation", async function () { - const iVaultFactory = await ethers.getContractFactory("InceptionVault_EL", { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const iVault = await upgrades.deployProxy( - iVaultFactory, - [a.vaultName, a.iVaultOperator, a.strategyManager, iToken.address, a.assetStrategy], - { unsafeAllowLinkedLibraries: true }, - ); - iVault.address = await iVault.getAddress(); - - const setterFacetFactory = await ethers.getContractFactory("EigenSetterFacet", { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const setterFacet = await setterFacetFactory.deploy(); - await setterFacet.waitForDeployment(); - await iVault.setSetterFacet(await setterFacet.getAddress()); - const iVaultSetters = await ethers.getContractAt("EigenSetterFacet", iVault.address); - - const eigenLayerFacetFactory = await ethers.getContractFactory("EigenLayerFacet", { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const eigenLayerFacet = await eigenLayerFacetFactory.deploy(); - await eigenLayerFacet.waitForDeployment(); - await iVault.setEigenLayerFacet(await eigenLayerFacet.getAddress()); - const iVaultEL = await ethers.getContractAt("EigenLayerFacet", iVault.address); - - const ERC4626FacetFactory = await ethers.getContractFactory(a.vaultFactory, { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const erc4626Facet = await ERC4626FacetFactory.deploy(); - await erc4626Facet.waitForDeployment(); - await iVault.setERC4626Facet(await erc4626Facet.getAddress()); - const iVault4626 = await ethers.getContractAt(a.vaultFactory, iVault.address); - - let funcSig = eigenLayerFacet.interface.getFunction("delegateToOperator").selector; - await iVault.setSignature(funcSig, "1", "1"); - - funcSig = ERC4626FacetFactory.interface.getFunction("deposit").selector; - await iVault.setSignature(funcSig, "2", "0"); - - funcSig = setterFacet.interface.getFunction("setDelegationManager").selector; - await iVault.setSignature(funcSig, "0", "2"); - - funcSig = setterFacet.interface.getFunction("setRatioFeed").selector; - await iVault.setSignature(funcSig, "0", "2"); - - funcSig = setterFacet.interface.getFunction("addELOperator").selector; - await iVault.setSignature(funcSig, "0", "2"); - - funcSig = setterFacet.interface.getFunction("setTargetFlashCapacity").selector; - await iVault.setSignature(funcSig, "0", "2"); - - await iVaultSetters.setDelegationManager(a.delegationManager); - await iVaultSetters.setRatioFeed(ratioFeed.address); - await iVaultSetters.addELOperator(nodeOperators[0]); - await iToken.setVault(await iVault.getAddress()); - await iVaultSetters.setTargetFlashCapacity(1n); - - const amount = toWei(1); - await asset.connect(staker).approve(await iVault.getAddress(), amount); - await iVault4626.connect(staker).deposit(amount, staker.address); - const freeBalance = await iVault.getFreeBalance(); - await expect( - iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalance, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWithCustomError(iVaultEL, "ImplementationNotSet"); - }); - }); - - describe("Withdraw: user can unstake", function () { - let ratio, totalDeposited, TARGET; - - before(async function () { - await snapshot.restore(); - await iVault4626.connect(staker).deposit(toWei(20), staker.address); - const freeBalanace = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalanace, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - totalDeposited = await iVault.getTotalDeposited(); - TARGET = 1000_000n; - await iVaultSetters.setTargetFlashCapacity(TARGET); - ratio = await iVault.ratio(); - console.log(`Initial ratio: ${ratio}`); - }); - - const testData = [ - { - name: "random e18", - amount: async shares => 724399519262012598n, - receiver: () => staker.address, - }, - { - name: "999999999999999999", - amount: async shares => 999999999999999999n, - receiver: () => staker2.address, - }, - { - name: "888888888888888888", - amount: async shares => 888888888888888888n, - receiver: () => staker2.address, - }, - { - name: "777777777777777777", - amount: async shares => 777777777777777777n, - receiver: () => staker2.address, - }, - { - name: "666666666666666666", - amount: async shares => 666666666666666666n, - receiver: () => staker2.address, - }, - { - name: "555555555555555555", - amount: async shares => 555555555555555555n, - receiver: () => staker2.address, - }, - { - name: "444444444444444444", - amount: async shares => 444444444444444444n, - receiver: () => staker2.address, - }, - { - name: "333333333333333333", - amount: async shares => 333333333333333333n, - receiver: () => staker2.address, - }, - { - name: "222222222222222222", - amount: async shares => 222222222222222222n, - receiver: () => staker2.address, - }, - { - name: "111111111111111111", - amount: async shares => 111111111111111111n, - receiver: () => staker2.address, - }, - { - name: "min amount", - amount: async shares => (await iVault.convertToAssets(await iVault.minAmount())) + 1n, - receiver: () => staker2.address, - }, - { - name: "all", - amount: async shares => shares, - receiver: () => staker2.address, - }, - ]; - - testData.forEach(function (test) { - it(`Withdraw(amount, receiver) ${test.name}`, async function () { - const ratioBefore = await iVault.ratio(); - const balanceBefore = await iToken.balanceOf(staker.address); - const amount = await test.amount(balanceBefore); - const assetValue = await iVault.convertToAssets(amount); - const stakerPWBefore = await iVault.getPendingWithdrawalOf(test.receiver()); - const totalPWBefore = await iVault.totalAmountToWithdraw(); - - const tx = await iVault4626.connect(staker).withdraw(amount, test.receiver()); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(test.receiver()); - expect(events[0].args["owner"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.closeTo(assetValue, transactErr); - expect(events[0].args["iShares"]).to.be.eq(amount); - - expect(balanceBefore - (await iToken.balanceOf(staker.address))).to.be.eq(amount); - expect((await iVault.getPendingWithdrawalOf(test.receiver())) - stakerPWBefore).to.be.closeTo( - assetValue, - transactErr, - ); - expect((await iVault.totalAmountToWithdraw()) - totalPWBefore).to.be.closeTo(assetValue, transactErr); - expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); - if ((await iToken.totalSupply()) == 0n) { - expect(await calculateRatio(iVault, iToken)).to.be.equal(e18); - } else { - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratioBefore, ratioErr); - } - }); - }); - }); - - describe("Withdraw: negative cases", function () { - before(async function () { - await snapshot.restore(); - await iVault4626.connect(staker).deposit(toWei(10), staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalance, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - await addRewardsToStrategy(a.assetStrategy, toWei(0.001), staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - }); - - const invalidData = [ - { - name: "> balance", - amount: async () => (await iToken.balanceOf(staker.address)) + 1n, - receiver: () => staker.address, - isCustom: false, - error: "ERC20: burn amount exceeds balance", - }, - { - name: "< min amount", - amount: async () => (await iVault.minAmount()) - 1n, - receiver: () => staker.address, - isCustom: true, - error: "LowerMinAmount", - }, - { - name: "0", - amount: async () => 0n, - receiver: () => staker.address, - isCustom: true, - error: "NullParams", - }, - { - name: "to zero address", - amount: async () => randomBI(18), - receiver: () => ethers.ZeroAddress, - isCustom: true, - error: "NullParams", - }, - ]; - - invalidData.forEach(function (test) { - it(`Reverts: withdraws ${test.name}`, async function () { - const amount = await test.amount(); - const receiver = test.receiver(); - if (test.isCustom) { - await expect(iVault4626.connect(staker).withdraw(amount, receiver)).to.be.revertedWithCustomError( - iVault, - test.error, - ); - } else { - await expect(iVault4626.connect(staker).withdraw(amount, receiver)).to.be.revertedWith(test.error); - } - }); - }); - - it("Withdraw small amount many times", async function () { - const ratioBefore = await iVault.ratio(); - console.log(`Ratio before:\t${ratioBefore.format()}`); - - const count = 100; - const amount = await iVault.minAmount(); - for (let i = 0; i < count; i++) { - await iVault4626.connect(staker).withdraw(amount, staker.address); - } - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after:\t${ratioAfter.format()}`); - - expect(ratioBefore - ratioAfter).to.be.closeTo(0, count); - - await iVault4626.connect(staker).withdraw(e18, staker.address); - console.log(`Ratio after withdraw 1eth:\t${await iVault.ratio()}`); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratioAfter, ratioErr); - }); - - it("Reverts: withdraw when iVault is paused", async function () { - await iVault.pause(); - await expect(iVault4626.connect(staker).withdraw(toWei(1), staker.address)).to.be.revertedWith( - "Pausable: paused", - ); - await iVault.unpause(); - }); - - it("Reverts: withdraw when target capacity is not set", async function () { - await iVaultSetters.setTargetFlashCapacity(0n); - await expect(iVault4626.connect(staker).withdraw(toWei(1), staker.address)).to.be.revertedWithCustomError( - iVault, - "InceptionOnPause", - ); - await iVaultSetters.setTargetFlashCapacity(1n); - }); - }); - - describe("Flash withdraw with fee", function () { - const targetCapacityPercent = e18; - const targetCapacity = e18; - let deposited = 0n; - beforeEach(async function () { - await snapshot.restore(); - await iVaultSetters.setTargetFlashCapacity(1n); - deposited = (targetCapacity * MAX_TARGET_PERCENT) / targetCapacityPercent; - await iVault4626.connect(staker3).deposit(deposited, staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalance, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - await iVaultSetters.setTargetFlashCapacity(targetCapacityPercent); - }); - - const args = [ - { - name: "part of the free balance when pool capacity > TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, - amount: async () => (await iVault.getFreeBalance()) / 2n, - receiver: () => staker, - }, - { - name: "all of the free balance when pool capacity > TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, - amount: async () => await iVault.getFreeBalance(), - receiver: () => staker, - }, - { - name: "all when pool capacity > TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, - amount: async () => await iVault.getFlashCapacity(), - receiver: () => staker, - }, - { - name: "partially when pool capacity = TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent, - amount: async () => (await iVault.getFlashCapacity()) / 2n, - receiver: () => staker, - }, - { - name: "all when pool capacity = TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent, - amount: async () => await iVault.getFlashCapacity(), - receiver: () => staker, - }, - { - name: "partially when pool capacity < TARGET", - poolCapacity: targetCapacityPercent => (targetCapacityPercent * 3n) / 4n, - amount: async () => (await iVault.getFlashCapacity()) / 2n, - receiver: () => staker, - }, - { - name: "all when pool capacity < TARGET", - poolCapacity: targetCapacityPercent => (targetCapacityPercent * 3n) / 4n, - amount: async () => await iVault.getFlashCapacity(), - receiver: () => staker, - }, - ]; - - args.forEach(function (arg) { - it(`flashWithdraw: ${arg.name}`, async function () { - //Undelegate from EL - const undelegatePercent = arg.poolCapacity(targetCapacityPercent); - const undelegateAmount = (deposited * undelegatePercent) / MAX_TARGET_PERCENT; - await iVault.withdrawFromELAndClaim(nodeOperators[0], undelegateAmount); - - //flashWithdraw - const ratioBefore = await iVault.ratio(); - console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); - - const sharesBefore = await iToken.balanceOf(staker); - const assetBalanceBefore = await asset.balanceOf(staker); - const treasuryBalanceBefore = await asset.balanceOf(treasury); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const flashCapacityBefore = await iVault.getFlashCapacity(); - const freeBalanceBefore = await iVault.getFreeBalance(); - console.log(`flashCapacityBefore:\t${flashCapacityBefore.format()}`); - console.log(`freeBalanceBefore:\t\t${freeBalanceBefore.format()}`); - - const amount = await arg.amount(); - const shares = await iVault.convertToShares(amount + 1n); //+1 to compensate rounding after converting from shares to amount - const receiver = await arg.receiver(); - const expectedFee = await iVault.calculateFlashWithdrawFee(amount); - console.log(`Expected fee:\t\t\t${expectedFee.format()}`); - - let tx = await iVault4626.connect(staker).flashWithdraw(shares, receiver.address); - const receipt = await tx.wait(); - const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); - expect(withdrawEvent.length).to.be.eq(1); - expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); - expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); - expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); - const fee = withdrawEvent[0].args["fee"]; - expect(fee).to.be.closeTo(expectedFee, transactErr); - - const sharesAfter = await iToken.balanceOf(staker); - const assetBalanceAfter = await asset.balanceOf(staker); - const treasuryBalanceAfter = await asset.balanceOf(treasury); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - console.log(`Balance diff:\t\t\t${(sharesBefore - sharesAfter).format()}`); - console.log(`TotalDeposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); - console.log(`TotalAssets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); - console.log(`FlashCapacity diff:\t\t${(flashCapacityBefore - flashCapacityAfter).format()}`); - console.log(`Fee:\t\t\t\t\t${fee.format()}`); - - expect(sharesBefore - sharesAfter).to.be.eq(shares); - expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); - expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); - expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); - expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); - }); - - it(`redeem(shares,receiver,owner): ${arg.name}`, async function () { - //Undelegate from EL - const undelegatePercent = arg.poolCapacity(targetCapacityPercent); - const undelegateAmount = (deposited * undelegatePercent) / MAX_TARGET_PERCENT; - await iVault.withdrawFromELAndClaim(nodeOperators[0], undelegateAmount); - - //flashWithdraw - const ratioBefore = await iVault.ratio(); - console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); - - const sharesBefore = await iToken.balanceOf(staker); - const assetBalanceBefore = await asset.balanceOf(staker); - const treasuryBalanceBefore = await asset.balanceOf(treasury); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const flashCapacityBefore = await iVault.getFlashCapacity(); - const freeBalanceBefore = await iVault.getFreeBalance(); - console.log(`flashCapacityBefore:\t${flashCapacityBefore.format()}`); - console.log(`freeBalanceBefore:\t\t${freeBalanceBefore.format()}`); - - const amount = await arg.amount(); - const shares = await iVault.convertToShares(amount); //+1 to compensate rounding after converting from shares to amount - const previewAmount = await iVault.previewRedeem(shares); - const receiver = await arg.receiver(); - const expectedFee = await iVault.calculateFlashWithdrawFee(amount); - console.log(`Expected fee:\t\t\t${expectedFee.format()}`); - - let tx = await iVault4626 - .connect(staker) - ["redeem(uint256,address,address)"](shares, receiver.address, staker.address); - const receipt = await tx.wait(); - const withdrawEvent = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(withdrawEvent.length).to.be.eq(1); - expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); - expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); - expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); - const feeEvent = receipt.logs?.filter(e => e.eventName === "WithdrawalFee"); - const fee = feeEvent[0].args["fee"]; - expect(fee).to.be.closeTo(expectedFee, transactErr); - - const sharesAfter = await iToken.balanceOf(staker); - const assetBalanceAfter = await asset.balanceOf(staker); - const treasuryBalanceAfter = await asset.balanceOf(treasury); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - console.log(`Balance diff:\t\t\t${(sharesBefore - sharesAfter).format()}`); - console.log(`TotalDeposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); - console.log(`TotalAssets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); - console.log(`FlashCapacity diff:\t\t${(flashCapacityBefore - flashCapacityAfter).format()}`); - console.log(`Fee:\t\t\t\t\t${fee.format()}`); - - expect(sharesBefore - sharesAfter).to.be.eq(shares); - expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); - expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); - expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); - expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); - expect(previewAmount).to.be.eq(assetBalanceAfter - assetBalanceBefore); - }); - }); - - it("Reverts when capacity is not sufficient", async function () { - const shares = await iToken.balanceOf(staker.address); - const capacity = await iVault.getFlashCapacity(); - await expect(iVault4626.connect(staker).flashWithdraw(shares, staker.address)) - .to.be.revertedWithCustomError(iVault, "InsufficientCapacity") - .withArgs(capacity); - }); - - it("Reverts when amount < min", async function () { - const minAmount = await iVault.minAmount(); - const shares = (await iVault.convertToShares(minAmount)) - 1n; - await expect(iVault4626.connect(staker).flashWithdraw(shares, staker.address)) - .to.be.revertedWithCustomError(iVault, "LowerMinAmount") - .withArgs(minAmount); - }); - - it("Reverts when iVault is paused", async function () { - await iVault4626.connect(staker).deposit(e18, staker.address); - await iVault.pause(); - const amount = await iVault.getFlashCapacity(); - await expect(iVault4626.connect(staker).flashWithdraw(amount, staker.address)).to.be.revertedWith( - "Pausable: paused", - ); - await expect( - iVault4626.connect(staker)["redeem(uint256,address,address)"](amount, staker.address, staker.address), - ).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - - it("Reverts redeem when owner != message sender", async function () { - await iVault4626.connect(staker).deposit(e18, staker.address); - const amount = await iVault.getFlashCapacity(); - await expect( - iVault4626.connect(staker)["redeem(uint256,address,address)"](amount, staker.address, staker2.address), - ).to.be.revertedWithCustomError(iVault4626, "MsgSenderIsNotOwner"); - }); - }); - - describe("Max redeem", function () { - beforeEach(async function () { - await snapshot.restore(); - await iVaultSetters.setTargetFlashCapacity(1n); - await iVault4626.connect(staker3).deposit(randomBI(18), staker3.address); - const freeBalance = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalance / 2n, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - }); - - const args = [ - { - name: "User amount = 0", - sharesOwner: () => ethers.Wallet.createRandom(), - maxRedeem: async () => 0n, - }, - { - name: "User amount < flash capacity", - sharesOwner: () => staker, - deposited: randomBI(18), - maxRedeem: async () => await iToken.balanceOf(staker), - }, - { - name: "User amount = flash capacity", - sharesOwner: () => staker, - deposited: randomBI(18), - delegated: async deposited => (await iVault.totalAssets()) - deposited, - maxRedeem: async () => await iToken.balanceOf(staker), - }, - { - name: "User amount > flash capacity > 0", - sharesOwner: () => staker, - deposited: randomBI(18), - delegated: async deposited => (await iVault.totalAssets()) - randomBI(17), - maxRedeem: async () => await iVault.convertToShares(await iVault.getFlashCapacity()), - }, - { - name: "User amount > flash capacity = 0", - sharesOwner: () => staker3, - delegated: async deposited => await iVault.totalAssets(), - maxRedeem: async () => 0n, - }, - ]; - - async function prepareState(arg) { - const sharesOwner = arg.sharesOwner(); - console.log(sharesOwner.address); - if (arg.deposited) { - await iVault4626.connect(sharesOwner).deposit(arg.deposited, sharesOwner.address); - } - - if (arg.delegated) { - const delegated = await arg.delegated(arg.deposited); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(delegated, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - } - return sharesOwner; - } - - args.forEach(function (arg) { - it(`maxReedem: ${arg.name}`, async function () { - const sharesOwner = await prepareState(arg); - - const maxRedeem = await iVault.maxRedeem(sharesOwner); - const expectedMaxRedeem = await arg.maxRedeem(); - - console.log(`User shares:\t\t${(await iToken.balanceOf(sharesOwner)).format()}`); - console.log(`flashCapacity:\t\t${(await iVault.convertToShares(await iVault.getFlashCapacity())).format()}`); - console.log(`total assets:\t\t${await iVault.totalAssets()}`); - console.log(`maxRedeem:\t\t\t${maxRedeem.format()}`); - console.log(`expected Redeem:\t${expectedMaxRedeem.format()}`); - - if (maxRedeem > 0n) { - await iVault4626.connect(sharesOwner).redeem(maxRedeem, sharesOwner.address, sharesOwner.address); - } - expect(maxRedeem).to.be.eq(expectedMaxRedeem); - }); - }); - - it("Reverts when iVault is paused", async function () { - await iVault4626.connect(staker).deposit(e18, staker.address); - await iVault.pause(); - expect(await iVault4626.maxRedeem(staker)).to.be.eq(0n); - }); - }); - - describe("UndelegateFrom: request withdrawal assets staked by restaker", function () { - let ratio, - ratioDiff, - depositedAmount, - assets1, - assets2, - withdrawalData1, - withdrawalData2, - withdrawalAssets, - shares1, - shares2; - before(async function () { - await snapshot.restore(); - await iVaultSetters.setTargetFlashCapacity(1n); - await new Promise(r => setTimeout(r, 2000)); - //Deposit and delegate to default stakerOperator - depositedAmount = randomBI(19); - await iVault4626.connect(staker).deposit(depositedAmount, staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalance, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - }); - - it("Operator can undelegateFrom stakerOperator", async function () { - shares1 = 460176234800292249n; - assets1 = await iVault.convertToAssets(shares1); - console.log(`Staker is going to withdraw:\t${shares1.format()}/${assets1.format()}`); - await iVault4626.connect(staker).withdraw(shares1, staker.address); - console.log(`Staker's pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - const pendingWithdrawalsELBefore = await iVault.getPendingWithdrawalAmountFromEL(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const ratioBefore = await iVault.ratio(); - - const tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], assets1); - const receipt = await tx.wait(); - const startWithdrawal = receipt.logs?.filter(e => e.eventName === "StartWithdrawal"); - expect(startWithdrawal.length).to.be.eq(1); - const WithdrawalQueuedEvent = startWithdrawal[0].args; - withdrawalData1 = [ - WithdrawalQueuedEvent["stakerAddress"], - nodeOperators[0], - nodeOperatorToRestaker.get(nodeOperators[0]), - WithdrawalQueuedEvent["nonce"], - WithdrawalQueuedEvent["withdrawalStartBlock"], - [WithdrawalQueuedEvent["strategy"]], - [WithdrawalQueuedEvent["shares"]], - ]; - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const ratioAfter = await iVault.ratio(); - - expect(totalDelegatedBefore - totalDelegatedAfter).to.be.closeTo(assets1, transactErr); - expect(await iVault.totalAssets()).to.be.lte(transactErr); - const pendingWithdrawalsELAfter = await iVault.getPendingWithdrawalAmountFromEL(); - console.log(`EL's pending withdrawals:\t\t${pendingWithdrawalsELAfter.format()}`); - expect(pendingWithdrawalsELAfter - pendingWithdrawalsELBefore).to.be.closeTo(shares1, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBefore, 1n); - }); - - it("Operator can do more undelegateFrom stakerOperator", async function () { - shares2 = 460176234800292249n; - assets2 = await iVault.convertToAssets(shares2); - console.log(`Staker is going to withdraw:\t${shares2.format()}/${assets2.format()}`); - await iVault4626.connect(staker).withdraw(shares2, staker2.address); - console.log(`Staker's pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - - //Change asset ratio - const ratioBefore = await iVault.ratio(); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - ratioDiff = ratioBefore - ratio; - - const totalPWBefore = await iVault.getPendingWithdrawalAmountFromEL(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const ratioBeforeUndelegate = await iVault.ratio(); - const tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], assets2); - const receipt = await tx.wait(); - const startWithdrawal = receipt.logs?.filter(e => e.eventName === "StartWithdrawal"); - expect(startWithdrawal.length).to.be.eq(1); - const WithdrawalQueuedEvent = startWithdrawal[0].args; - withdrawalData2 = [ - WithdrawalQueuedEvent["stakerAddress"], - nodeOperators[0], - nodeOperatorToRestaker.get(nodeOperators[0]), - WithdrawalQueuedEvent["nonce"], - WithdrawalQueuedEvent["withdrawalStartBlock"], - [WithdrawalQueuedEvent["strategy"]], - [WithdrawalQueuedEvent["shares"]], - ]; - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const ratioAfter = await iVault.ratio(); - - expect(totalDelegatedBefore - totalDelegatedAfter).to.be.closeTo(assets2, transactErr); - expect(await iVault.totalAssets()).to.be.lte(transactErr); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, ratioErr); - const totalPWAfter = await iVault.getPendingWithdrawalAmountFromEL(); - expect(totalPWAfter - totalPWBefore).to.be.closeTo(shares2, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBeforeUndelegate, ratioErr); - }); - - it("Claim the 2nd withdrawal from EL", async function () { - await mineBlocks(minWithdrawalDelayBlocks); - console.log(`Restaker: ${withdrawalData2[2]}`); - console.log(`Withdrawal data: ${withdrawalData2}`); - await iVaultEL.connect(staker).claimCompletedWithdrawals(withdrawalData2[2], [withdrawalData2]); - const totalPWAfter = await iVault.getPendingWithdrawalAmountFromEL(); - - console.log(`Ratio:\t\t\t\t\t\t${(await iVault.ratio()).format()}`); - console.log(`iVault assets:\t\t\t\t${(await iVault.totalAssets()).format()}`); - console.log(`Pending withdrawal staker:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - console.log(`Pending withdrawal staker2:\t${(await iVault.getPendingWithdrawalOf(staker2.address)).format()}`); - console.log(`Total pending withdrawal:\t${totalPWAfter.format()}`); - - expect((await iVault.totalAssets()) - assets2).to.be.lte(transactErr); - expect(totalPWAfter - shares1).to.be.closeTo(0, transactErr); - }); - - it("Claim missed withdrawal from EL", async function () { - await iVaultEL.claimCompletedWithdrawals(withdrawalData1[2], [withdrawalData1]); - const totalPendingWithdrawalAfter = await iVault.getPendingWithdrawalAmountFromEL(); - const totalAssetsAfter = await iVault.totalAssets(); - const stakerPWAfter = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - - console.log(`Ratio:\t\t\t\t\t\t${(await iVault.ratio()).toString()}`); - console.log(`Total assets:\t\t\t\t${totalAssetsAfter.format()}`); - console.log(`Pending withdrawal staker:\t${stakerPWAfter.format()}`); - console.log(`Pending withdrawal staker2:\t${staker2PWAfter.format()}`); - console.log(`Total pending withdrawal:\t${totalPendingWithdrawalAfter.format()}`); - - expect(stakerPWAfter - assets1).to.be.closeTo(0, transactErr); - expect(staker2PWAfter - assets2).to.be.closeTo(0, transactErr); - expect(totalAssetsAfter - assets1 - assets2).to.be.gt(0); - expect(totalPendingWithdrawalAfter).to.be.eq(0n); - }); - - it("Reverts: when delegating pending withdrawals back to EL", async function () { - const totalAssets = await iVault.totalAssets(); - await expect( - iVaultEL - .connect(iVaultOperator) - .delegateToOperator(totalAssets, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]), - ).to.be.revertedWithCustomError(iVault, "InsufficientCapacity"); - }); - - it("Operator can delegate part of leftover", async function () { - const totalAssets = await iVault.totalAssets(); - const stakerPWAfter = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - const part = (totalAssets - stakerPWAfter - staker2PWAfter) / 2n; - const totalDelegatedBefore = await iVault.getTotalDelegated(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(part, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - - expect(totalDelegatedAfter - totalDelegatedBefore).to.be.closeTo(part, transactErr); - }); - - it("Operator can delegate all leftover", async function () { - const totalAssets = await iVault.totalAssets(); - const stakerPWAfter = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - const leftover = totalAssets - stakerPWAfter - staker2PWAfter; - const totalDelegatedBefore = await iVault.getTotalDelegated(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(leftover, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - - const totalDelegatedAfter = await iVault.getTotalDelegated(); - expect(totalDelegatedAfter - totalDelegatedBefore).to.be.closeTo(leftover, transactErr); - }); - - it("Staker is able to redeem", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - }); - - it("Staker2 is able to redeem", async function () { - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - }); - - it("Staker redeems withdrawals", async function () { - const stakerBalanceBefore = await asset.balanceOf(staker.address); - const stakerPWBefore = await iVault.getPendingWithdrawalOf(staker.address); - - await iVault4626.redeem(staker.address); - const stakerBalanceAfter = await asset.balanceOf(staker.address); - const stakerPWAfter = await iVault.getPendingWithdrawalOf(staker.address); - - console.log(`Staker balance after: ${stakerBalanceAfter.format()}`); - console.log(`Staker pending withdrawals after: ${stakerPWAfter.format()}`); - - expect(stakerPWBefore - stakerPWAfter).to.be.closeTo(assets1, transactErr * 2n); - expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(assets1, transactErr * 2n); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - }); - - describe.skip("UndelegateVault: request withdrawal assets staked by iVault", function () { - let ratio, - ratioDiff, - depositedAmount, - assets1, - assets2, - withdrawalData1, - withdrawalData2, - withdrawalAssets, - shares1, - shares2; - before(async function () { - await snapshot.restore(); - await new Promise(r => setTimeout(r, 2000)); - //Deposit and delegate to default stakerOperator - depositedAmount = randomBI(19); - await iVault4626.connect(staker).deposit(depositedAmount, staker.address); - await iVault.connect(iVaultOperator).depositAssetIntoStrategyFromVault(await iVault.getFreeBalance()); - await iVault - .connect(iVaultOperator) - .delegateToOperatorFromVault(nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - }); - - it("Operator can undelegate for iVault", async function () { - shares1 = 460176234800292249n; - assets1 = await iVault.convertToAssets(shares1); - console.log(`Staker is going to withdraw:\t${shares1.format()}/${assets1.format()}`); - await iVault4626.connect(staker).withdraw(shares1, staker.address); - console.log(`Staker's pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - const pendingWithdrawalsELBefore = await iVault.getPendingWithdrawalAmountFromEL(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const ratioBefore = await iVault.ratio(); - - const tx = await iVault.connect(iVaultOperator).undelegateVault(assets1); - const receipt = await tx.wait(); - const startWithdrawal = receipt.logs?.filter(e => e.eventName === "StartWithdrawal"); - expect(startWithdrawal.length).to.be.eq(1); - const WithdrawalQueuedEvent = startWithdrawal[0].args; - withdrawalData1 = [ - WithdrawalQueuedEvent["stakerAddress"], - nodeOperators[0], - nodeOperatorToRestaker.get(nodeOperators[0]), - WithdrawalQueuedEvent["nonce"], - WithdrawalQueuedEvent["withdrawalStartBlock"], - [WithdrawalQueuedEvent["strategy"]], - [WithdrawalQueuedEvent["shares"]], - ]; - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const ratioAfter = await iVault.ratio(); - - expect(totalDelegatedBefore - totalDelegatedAfter).to.be.closeTo(assets1, transactErr); - expect(await iVault.totalAssets()).to.be.closeTo(0, transactErr); - const pendingWithdrawalsELAfter = await iVault.getPendingWithdrawalAmountFromEL(); - console.log(`EL's pending withdrawals:\t\t${pendingWithdrawalsELAfter.format()}`); - expect(pendingWithdrawalsELAfter - pendingWithdrawalsELBefore).to.be.closeTo(shares1, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBefore, 5n); - }); - - it("Operator can do more undelegateFrom stakerOperator", async function () { - shares2 = 460176234800292249n; - assets2 = await iVault.convertToAssets(shares2); - console.log(`Staker is going to withdraw:\t${shares2.format()}/${assets2.format()}`); - await iVault4626.connect(staker).withdraw(shares2, staker2.address); - console.log(`Staker's pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - - //Change asset ratio - const ratioBefore = await iVault.ratio(); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - ratioDiff = ratioBefore - ratio; - - const totalPWBefore = await iVault.getPendingWithdrawalAmountFromEL(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const ratioBeforeUndelegate = await iVault.ratio(); - const tx = await iVault.connect(iVaultOperator).undelegateVault(assets2); - const receipt = await tx.wait(); - const startWithdrawal = receipt.logs?.filter(e => e.eventName === "StartWithdrawal"); - expect(startWithdrawal.length).to.be.eq(1); - const WithdrawalQueuedEvent = startWithdrawal[0].args; - withdrawalData2 = [ - WithdrawalQueuedEvent["stakerAddress"], - nodeOperators[0], - nodeOperatorToRestaker.get(nodeOperators[0]), - WithdrawalQueuedEvent["nonce"], - WithdrawalQueuedEvent["withdrawalStartBlock"], - [WithdrawalQueuedEvent["strategy"]], - [WithdrawalQueuedEvent["shares"]], - ]; - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const ratioAfter = await iVault.ratio(); - - expect(totalDelegatedBefore - totalDelegatedAfter).to.be.closeTo(assets2, transactErr); - expect(await iVault.totalAssets()).to.be.closeTo(0, transactErr); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, ratioErr); - const totalPWAfter = await iVault.getPendingWithdrawalAmountFromEL(); - expect(totalPWAfter - totalPWBefore).to.be.closeTo(shares2, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBeforeUndelegate, ratioErr); - }); - - it("Claim the 2nd withdrawal from EL", async function () { - await mineBlocks(minWithdrawalDelayBlocks); - console.log(`Restaker: ${withdrawalData2[2]}`); - console.log(`Withdrawal data: ${withdrawalData2}`); - await iVaultEL.connect(staker).claimCompletedWithdrawals(withdrawalData2[2], [withdrawalData2]); - const totalPWAfter = await iVault.getPendingWithdrawalAmountFromEL(); - - console.log(`iVault assets:\t\t\t\t${(await iVault.totalAssets()).format()}`); - console.log(`Pending withdrawal staker:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - console.log(`Pending withdrawal staker2:\t${(await iVault.getPendingWithdrawalOf(staker2.address)).format()}`); - console.log(`Total pending withdrawal:\t${totalPWAfter.format()}`); - console.log(`Ratio:\t\t\t\t\t\t${(await iVault.ratio()).format()}`); - - expect((await iVault.totalAssets()) - assets2).to.be.closeTo(0, transactErr); - expect(totalPWAfter - shares1).to.be.closeTo(0, transactErr); - }); - - it("Claim missed withdrawal from EL", async function () { - await iVaultEL.claimCompletedWithdrawals(withdrawalData1[2], [withdrawalData1]); - const totalPendingWithdrawalAfter = await iVault.getPendingWithdrawalAmountFromEL(); - const totalAssetsAfter = await iVault.totalAssets(); - const stakerPWAfter = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - - console.log(`Total assets:\t\t\t\t${totalAssetsAfter.format()}`); - console.log(`Pending withdrawal staker:\t${stakerPWAfter.format()}`); - console.log(`Pending withdrawal staker2:\t${staker2PWAfter.format()}`); - console.log(`Total pending withdrawal:\t${totalPendingWithdrawalAfter.format()}`); - console.log(`Ratio:\t\t\t\t\t\t${(await iVault.ratio()).format()}`); - - expect(stakerPWAfter - assets1).to.be.closeTo(0, transactErr); - expect(staker2PWAfter - assets2).to.be.closeTo(0, transactErr); - expect(totalAssetsAfter - assets1 - assets2).to.be.gt(0); - expect(totalPendingWithdrawalAfter).to.be.eq(0n); - }); - }); - - describe("UndelegateFrom different operators", function () { - const withdrawalData = []; - let totalAssetsBefore; - - before(async function () { - await snapshot.restore(); - await new Promise(r => setTimeout(r, 2000)); - for (const operatorAddress of nodeOperators.slice(1)) { - await iVaultSetters.addELOperator(operatorAddress); //Add default operator - } - }); - - it("Deposit and delegate to different operators", async function () { - //Deposit - const staker1Amount = randomBI(19); - await iVault4626.connect(staker).deposit(staker1Amount, staker.address); - const staker2Amount = randomBI(19); - await iVault4626.connect(staker2).deposit(staker2Amount, staker2.address); - totalAssetsBefore = await iVault.totalAssets(); - - //Delegate to each operator - let i = 0; - for (const operatorAddress of nodeOperators) { - const ta = await iVault.totalAssets(); - const amount = ta / BigInt(nodeOperators.length - i); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, operatorAddress, ethers.ZeroHash, [ethers.ZeroHash, 0]); - expect(await iVault.getDelegatedTo(operatorAddress)).to.be.closeTo(amount, transactErr); - } - }); - - it("Stakers withdraw", async function () { - const staker1Amount = await iToken.balanceOf(staker.address); - await iVault4626.connect(staker).withdraw(staker1Amount, staker.address); - const staker2Amount = await iToken.balanceOf(staker2.address); - await iVault4626.connect(staker2).withdraw(staker2Amount, staker2.address); - }); - - it("undelegateFrom operator more than delegated", async function () { - const amount = await iVault.getDelegatedTo(nodeOperators[0]); - expect(amount).gt(0n); - - const pendingWithdrawalsELBefore = await iVault.getPendingWithdrawalAmountFromEL(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const ratioBefore = await calculateRatio(iVault, iToken); - - const tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], amount + e18); - const receipt = await tx.wait(); - const startWithdrawal = receipt.logs?.filter(e => e.eventName === "StartWithdrawal"); - expect(startWithdrawal.length).to.be.eq(1); - const WithdrawalQueuedEvent = startWithdrawal[0].args; - const data = [ - WithdrawalQueuedEvent["stakerAddress"], - nodeOperators[0], - nodeOperatorToRestaker.get(nodeOperators[0]), - WithdrawalQueuedEvent["nonce"], - WithdrawalQueuedEvent["withdrawalStartBlock"], - [WithdrawalQueuedEvent["strategy"]], - [WithdrawalQueuedEvent["shares"]], - ]; - withdrawalData.push(data); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const ratioAfter = await calculateRatio(iVault, iToken); - - expect(totalDelegatedBefore - totalDelegatedAfter).to.be.closeTo(amount, transactErr); - const pendingWithdrawalsELAfter = await iVault.getPendingWithdrawalAmountFromEL(); - console.log(`EL's pending withdrawals:\t\t${pendingWithdrawalsELAfter.format()}`); - expect(pendingWithdrawalsELAfter - pendingWithdrawalsELBefore).to.be.closeTo(amount, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBefore, 1n); - }); - - it("undelegateFrom each operator and claim", async function () { - for (const operatorAddress of nodeOperators) { - const amount = await iVault.getDelegatedTo(operatorAddress); - if (amount > 0n) { - let tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(operatorAddress, amount); - const data = await withdrawDataFromTx(tx, operatorAddress, nodeOperatorToRestaker.get(operatorAddress)); - withdrawalData.push(data); - } - } - }); - - it("claim from EL", async function () { - await mineBlocks(minWithdrawalDelayBlocks); - let i = 0; - for (const data of withdrawalData) { - console.log(`Withdraw #${++i}`); - await iVaultEL.claimCompletedWithdrawals(data[2], [data]); - } - const totalAssetsAfter = await iVault.totalAssets(); - - console.log(`Total assets before: ${totalAssetsBefore.format()}`); - console.log(`Total assets after: ${totalAssetsAfter.format()}`); - console.log(`Total pending wwls: ${(await iVault.totalAmountToWithdraw()).format()}`); - - expect(totalAssetsAfter).to.be.closeTo(totalAssetsBefore, transactErr * BigInt(nodeOperators.length)); - - await asset.connect(staker3).approve(await iVault.getAddress(), 1000); - await iVault4626.connect(staker3).deposit(1000, staker3.address); - await iVaultEL.connect(iVaultOperator).updateEpoch(); - - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - }); - }); - - describe("Force undelegate by node operator", function () { - let ratio, - ratioDiff, - depositedAmount, - assets1, - assets2, - withdrawalData1, - withdrawalData2, - withdrawalAssets, - shares1, - shares2; - let nodeOperator, restaker, delegatedNodeOperator1; - before(async function () { - await snapshot.restore(); - await new Promise(r => setTimeout(r, 2000)); - forcedWithdrawals.length = 0; - await iVaultSetters.addELOperator(nodeOperators[1]); - //Deposit and delegate to default stakerOperator - depositedAmount = toWei(20); - await iVault4626.connect(staker).deposit(depositedAmount, staker.address); - const totalAssets = await iVault.totalAssets(); - delegatedNodeOperator1 = totalAssets / 2n; - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(delegatedNodeOperator1, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(totalAssets / 4n, nodeOperators[1], ethers.ZeroHash, [ethers.ZeroHash, 0]); - }); - - it("Node operator makes force undelegate", async function () { - nodeOperator = await impersonateWithEth(nodeOperators[0], 0n); - restaker = nodeOperatorToRestaker.get(nodeOperator.address); - - console.log(`Total delegated ${await iVault.getTotalDelegated()}`); - console.log(`Ratio before ${await iVault.ratio()}`); - console.log(`Shares before ${await delegationManager.operatorShares(nodeOperators[0], a.assetStrategy)}`); - - //Force undelegate - const tx = await delegationManager.connect(nodeOperator).undelegate(restaker); - const receipt = await tx.wait(); - console.log(`Ratio after ${await iVault.ratio()}`); - console.log(`Total delegated ${await iVault.getTotalDelegated()}`); - console.log(`Shares after ${await delegationManager.operatorShares(nodeOperators[0], a.assetStrategy)}`); - - const withdrawalQueued = receipt.logs?.filter(e => e.eventName === "WithdrawalQueued"); - expect(withdrawalQueued.length).to.be.eq(1); - const WithdrawalQueuedEvent = withdrawalQueued[0].args.toObject(); - withdrawalData1 = [ - WithdrawalQueuedEvent.withdrawal.staker, - nodeOperator.address, - nodeOperatorToRestaker.get(nodeOperators[0]), - WithdrawalQueuedEvent.withdrawal.nonce, - WithdrawalQueuedEvent.withdrawal.startBlock, - [...WithdrawalQueuedEvent.withdrawal.strategies], - [...WithdrawalQueuedEvent.withdrawal.shares], - ]; - }); - - it("Deposits paused", async function () { - await expect(iVault4626.connect(staker).deposit(randomBI(18), staker.address)).to.be.revertedWithCustomError( - iVault, - "InceptionOnPause", - ); - }); - - it("Withdrawals paused", async function () { - const shares = await iToken.balanceOf(staker.address); - await expect(iVault4626.connect(staker).withdraw(shares, staker.address)).to.be.revertedWithCustomError( - iVault, - "InceptionOnPause", - ); - }); - - it("forceUndelegateRecovery: only iVault operator can", async function () { - await expect( - iVaultEL.connect(staker).forceUndelegateRecovery(delegatedNodeOperator1, restaker), - ).to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); - }); - - it("Fix ratio with forceUndelegateRecovery", async function () { - const pendingWwlsBefore = await iVault.getPendingWithdrawalAmountFromEL(); - await iVaultEL.connect(iVaultOperator).forceUndelegateRecovery(delegatedNodeOperator1, restaker); - const pendingWwlsAfter = await iVault.getPendingWithdrawalAmountFromEL(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after ${ratioAfter}`); - expect(pendingWwlsAfter - pendingWwlsBefore).to.be.eq(delegatedNodeOperator1); - }); - - it("Add rewards to strategy", async function () { - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - }); - - it("Claim force undelegate", async function () { - await mineBlocks(minWithdrawalDelayBlocks); - const restaker = nodeOperatorToRestaker.get(nodeOperator.address); - const iVaultBalanceBefore = await asset.balanceOf(iVault.address); - - console.log(`iVault balance before: ${iVaultBalanceBefore.format()}`); - await iVaultEL.connect(staker).claimCompletedWithdrawals(restaker, [withdrawalData1]); - - const iVaultBalanceAfter = await asset.balanceOf(iVault.address); - const pendingWwlsAfter = await iVault.getPendingWithdrawalAmountFromEL(); - console.log(`iVault balance after: ${iVaultBalanceAfter.format()}`); - console.log(`Pending wwls after: ${pendingWwlsAfter.format()}`); - console.log(`Ratio after: ${await iVault.ratio()}`); - }); - }); - - describe("UndelegateFrom: negative cases", function () { - beforeEach(async function () { - await snapshot.restore(); - await iVault4626.connect(staker).deposit(randomBI(19), staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(freeBalance, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - console.log(`Delegated amount: \t${freeBalance.format()}`); - }); - - const invalidArgs = [ - { - name: "0 amount", - amount: async () => 0n, - nodeOperator: async () => nodeOperators[0], - operator: () => iVaultOperator, - error: "StrategyManager._removeShares: shareAmount should not be zero!", - }, - { - name: "from unknown operator", - amount: async () => await iVault.getDelegatedTo(nodeOperators[0]), - nodeOperator: async () => randomAddress(), - operator: () => iVaultOperator, - customError: "OperatorNotRegistered", - }, - { - name: "from _MOCK_ADDRESS", - amount: async () => await iVault.getDelegatedTo(nodeOperators[0]), - nodeOperator: async () => { - await iVaultSetters.addELOperator(nodeOperators[1]); - return nodeOperators[1]; - }, - operator: () => iVaultOperator, - customError: "NullParams", - }, - { - name: "from zero address", - amount: async () => await iVault.getDelegatedTo(nodeOperators[0]), - nodeOperator: async () => ethers.ZeroAddress, - operator: () => iVaultOperator, - customError: "OperatorNotRegistered", - }, - { - name: "not an operator", - amount: async () => await iVault.getDelegatedTo(nodeOperators[0]), - nodeOperator: async () => nodeOperators[0], - operator: () => staker, - customError: "OnlyOperatorAllowed", - }, - ]; - - invalidArgs.forEach(function (arg) { - it(`Reverts: when undelegates ${arg.name}`, async function () { - const amount = await arg.amount(); - const nodeOperator = await arg.nodeOperator(); - console.log(`Undelegate amount: \t${amount.format()}`); - if (arg.customError) { - await expect( - iVaultEL.connect(arg.operator()).undelegateFrom(nodeOperator, amount), - ).to.be.revertedWithCustomError(iVault, arg.customError); - } else { - await expect(iVaultEL.connect(arg.operator()).undelegateFrom(nodeOperator, amount)).to.be.revertedWith( - arg.error, - ); - } - }); - }); - - it("Reverts: undelegate when iVault is paused", async function () { - const amount = randomBI(18); - await iVault.pause(); - await expect(iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], amount)).to.be.revertedWith( - "Pausable: paused", - ); - await iVault.unpause(); - }); - }); - - describe.skip("UndelegateVault: negative cases", function () { - beforeEach(async function () { - await snapshot.restore(); - await iVault4626.connect(staker).deposit(randomBI(19), staker.address); - const amount = await iVault.totalAssets(); - await iVault.connect(iVaultOperator).depositAssetIntoStrategyFromVault(amount); - await iVault - .connect(iVaultOperator) - .delegateToOperatorFromVault(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - console.log(`Delegated amount: \t${amount.format()}`); - }); - - const invalidArgs = [ - { - name: "0 amount", - amount: async () => 0n, - operator: () => iVaultOperator, - error: "StrategyManager._removeShares: shareAmount should not be zero!", - }, - { - name: "not an operator", - amount: async () => await iVault.getDelegatedTo(nodeOperators[0]), - operator: () => staker, - isCustom: true, - error: "OnlyOperatorAllowed", - }, - ]; - - invalidArgs.forEach(function (arg) { - it(`Reverts: when undelegates ${arg.name}`, async function () { - const amount = await arg.amount(); - console.log(`Undelegate amount: \t${amount.format()}`); - if (arg.isCustom) { - await expect(iVault.connect(arg.operator()).undelegateVault(amount)).to.be.revertedWithCustomError( - iVault, - arg.error, - ); - } else { - await expect(iVault.connect(arg.operator()).undelegateVault(amount)).to.be.revertedWith(arg.error); - } - }); - }); - - it("Reverts: undelegate when iVault is paused", async function () { - const amount = randomBI(18); - await iVault.pause(); - await expect(iVault.connect(iVaultOperator).undelegateVault(amount)).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - }); - - describe("UndelegateFrom and redeem in a loop", function () { - let ratio, - stakers, - withdrawals = new Map(); - - before(async function () { - await snapshot.restore(); - await new Promise(r => setTimeout(r, 2000)); - stakers = [staker, staker2]; - //Deposit and delegate - for (const s of stakers) { - await iVault4626.connect(s).deposit(randomBI(19), s.address); - } - const amount = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - ratio = await iVault.ratio(); - console.log(`Ratio ${ratio.toString()}`); - }); - - const count = 10; - for (let i = 0; i < count; i++) { - it(`${i}. Iteration`, async function () { - withdrawals.set(staker.address, 0n); - withdrawals.set(staker2.address, 0n); - - //Withdraw staker1 only - let shares = randomBI(16); - await iVault4626.connect(staker).withdraw(shares, staker.address); - let w = withdrawals.get(staker.address); - withdrawals.set(staker.address, w + shares); - - //Withdraw EL#1 - const totalPW1 = await iVault.totalAmountToWithdraw(); - console.log(`Total pending withdrawals#1: ${totalPW1}`); - let tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], totalPW1); - const w1data = await withdrawDataFromTx(tx, nodeOperators[0], nodeOperatorToRestaker.get(nodeOperators[0])); - const totalPWEL1 = await iVault.getPendingWithdrawalAmountFromEL(); - expect(totalPWEL1).to.be.closeTo(totalPW1, transactErr); - - //Withdraw staker and staker2 - for (const s of stakers) { - const shares = randomBI(16); - await iVault4626.connect(s).withdraw(shares, s.address); - const w = withdrawals.get(s.address); - withdrawals.set(s.address, w + shares); - } - - //Withdraw EL#2 - const totalPW2 = await iVault.totalAmountToWithdraw(); - tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], totalPW2 - totalPWEL1); - const w2data = await withdrawDataFromTx(tx, nodeOperators[0], nodeOperatorToRestaker.get(nodeOperators[0])); - const totalPWEL2 = await iVault.getPendingWithdrawalAmountFromEL(); - expect(totalPWEL2 - totalPWEL1).to.be.closeTo(totalPW2 - totalPW1, transactErr); - - await mineBlocks(minWithdrawalDelayBlocks); - //ClaimEL w1 - await iVaultEL.connect(staker).claimCompletedWithdrawals(w1data[2], [w1data]); - expect(await iVault.totalAssets()).to.be.closeTo(totalPW1, transactErr * 4n * BigInt(i + 1)); - //ClaimEL w2 - await iVaultEL.connect(staker).claimCompletedWithdrawals(w2data[2], [w2data]); - expect(await iVault.totalAssets()).to.be.closeTo(totalPW2, transactErr * 4n * BigInt(i + 1)); - expect(await iVault.getPendingWithdrawalAmountFromEL()).to.be.eq(0); //Everything was claims from EL; - console.log(`Total pwwls: ${totalPWEL2.format()}`); - console.log(`Total assets: ${(await iVault.totalAssets()).format()}`); - - const staker1BalanceBefore = await asset.balanceOf(staker.address); - const staker2BalanceBefore = await asset.balanceOf(staker2.address); - const staker1PW = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - - //Staker1 redeems - console.log(`### Staker1 redeems`); - const staker1RedeemBefore = await iVault.isAbleToRedeem(staker.address); - console.log(`Staker redeem epoch ${staker1RedeemBefore}`); - expect(staker1RedeemBefore[0]).to.be.true; - await iVault4626.redeem(staker.address); - const staker1BalanceAfter = await asset.balanceOf(staker.address); - expect(await iVault.getPendingWithdrawalOf(staker.address)).to.be.eq(0); - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; - expect(staker1BalanceAfter - staker1BalanceBefore).to.be.closeTo(staker1PW, transactErr * 2n); - - //Staker2 redeems - console.log(`### Staker2 redeems`); - const staker2RedeemBefore = await iVault.isAbleToRedeem(staker2.address); - console.log(`Staker redeem epoch ${staker2RedeemBefore}`); - expect(staker2RedeemBefore[0]).to.be.true; - await iVault4626.connect(staker2).redeem(staker2.address); - const staker2BalanceAfter = await asset.balanceOf(staker2.address); - expect(await iVault.getPendingWithdrawalOf(staker2.address)).to.be.eq(0); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.false; - expect(staker2BalanceAfter - staker2BalanceBefore).to.be.closeTo(staker2PW, transactErr * 2n); - console.log(`Total assets: ${await iVault.totalAssets()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - } - - it("Stakers withdraw all and redeem", async function () { - //Stakers withdraw all - console.log(`Total pending withdrawals: ${(await iVault.totalAmountToWithdraw()).format()}`); - for (const s of stakers) { - const shares = await iToken.balanceOf(s.address); - await iVault4626.connect(s).withdraw(shares, s.address); - const w = withdrawals.get(s.address); - withdrawals.set(s.address, w + shares); - } - //Withdraw and claim from EL - const amount = await iVault.totalAmountToWithdraw(); - await iVault.withdrawFromELAndClaim(nodeOperators[0], amount); - - //Stakers redeem - let stakerCounter = 1; - for (const s of stakers) { - console.log(`iToken balance staker${stakerCounter} before:\t\t${await iToken.balanceOf(s.address)}`); - console.log(`iVault assets before:\t\t\t\t${(await iVault.totalAssets()).format()}`); - console.log( - `Pending withdrawal staker${stakerCounter} before:\t${(await iVault.getPendingWithdrawalOf(s.address)).format()}`, - ); - console.log(`### Staker${stakerCounter} redeems`); - await iVault4626.connect(s).redeem(s.address); - console.log( - `Pending withdrawal staker${stakerCounter} after:\t${(await iVault.getPendingWithdrawalOf(s.address)).format()}`, - ); - console.log(`Ratio: ${await iVault.ratio()}`); - stakerCounter++; - } - expect(await iVault.totalAssets()).to.be.lt(100); - }); - }); - - describe("ClaimCompletedWithdrawals: claims withdraw from EL", function () { - let ratio, delegatedAmount, withdrawalAmount, withdrawalData, withdrawalCount, withdrawalAssets; - - before(async function () { - await snapshot.restore(); - await new Promise(r => setTimeout(r, 2000)); - ratio = await iVault.ratio(); - - //Deposit and withdraw - await iVault4626.connect(staker).deposit(toWei(10), staker.address); - delegatedAmount = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(delegatedAmount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - - //Withdraw 10 times - withdrawalCount = 12; - for (let i = 0; i < withdrawalCount; i++) { - await iVault4626.connect(staker).withdraw(randomBI(18), staker.address); - } - withdrawalAmount = - (await iVault.getPendingWithdrawalOf(staker.address)) + BigInt(withdrawalCount) * transactErr * 2n; - console.log(`Pending withdrawals: ${withdrawalAmount}`); - - const tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], withdrawalAmount); - withdrawalData = await withdrawDataFromTx(tx, nodeOperators[0], nodeOperatorToRestaker.get(nodeOperators[0])); - }); - - beforeEach(async function () { - if (await iVault.paused()) { - await iVault.unpause(); - } - }); - - it("Reverts: when iVault is paused", async function () { - await iVault.pause(); - await expect(iVaultEL.claimCompletedWithdrawals(withdrawalData[2], [withdrawalData])).to.be.revertedWith( - "Pausable: paused", - ); - }); - - it("Reverts: when claim without delay", async function () { - await expect( - iVaultEL.connect(staker).claimCompletedWithdrawals(withdrawalData[2], [withdrawalData]), - ).to.be.revertedWith( - "DelegationManager._completeQueuedWithdrawal: minWithdrawalDelayBlocks period has not yet passed", - ); - }); - - it("Successful claim from EL", async function () { - await mineBlocks(minWithdrawalDelayBlocks); - console.log(`iVault assets before: ${await iVault.totalAssets()}`); - const epochBefore = await iVault.epoch(); - console.log(`Epoch before: ${epochBefore}`); - - await iVaultEL.connect(staker).claimCompletedWithdrawals(withdrawalData[2], [withdrawalData]); - console.log(`iVault assets after: ${await iVault.totalAssets()}`); - console.log(`Epoch after: ${await iVault.epoch()}`); - - expect(await iVault.totalAssets()).to.be.closeTo(withdrawalAmount, transactErr); - expect(await iVault.epoch()).to.be.eq(epochBefore + BigInt(withdrawalCount)); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, ratioErr); - }); - - it("getTotalDeposited() = iVault + EL", async function () { - const amount = await iVault.getTotalDeposited(); - console.log(`getTotalDeposited: ${amount}`); - expect(amount).to.be.closeTo(delegatedAmount, transactErr); - }); - - it("Reverts: when claim the 2nd time", async function () { - await expect( - iVaultEL.connect(staker).claimCompletedWithdrawals(withdrawalData[2], [withdrawalData]), - ).to.be.revertedWith("DelegationManager._completeQueuedWithdrawal: action is not in queue"); - }); - }); - - describe("ClaimCompletedWithdrawals: claim multiple undelegates", function () { - let ratio, - delegatedAmount, - withdrawalAmount = 0n, - withdrawalCount; - const wDatas = []; - - before(async function () { - await snapshot.restore(); - ratio = await iVault.ratio(); - - //Deposit and withdraw - await iVault4626.connect(staker).deposit(toWei(10), staker.address); - delegatedAmount = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(delegatedAmount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - - //Withdraw and undelegate 10 times - withdrawalCount = 10; - for (let i = 0; i < withdrawalCount; i++) { - const amount = randomBI(18); - withdrawalAmount += amount; - await iVault4626.connect(staker).withdraw(amount, staker.address); - const tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], amount); - const wData = await withdrawDataFromTx(tx, nodeOperators[0], nodeOperatorToRestaker.get(nodeOperators[0])); - wDatas.push(wData); - } - await mineBlocks(minWithdrawalDelayBlocks); - }); - - it("Reverts: node operator does not match", async function () { - await expect(iVaultEL.connect(staker).claimCompletedWithdrawals(ethers.Wallet.createRandom().address, wDatas)) - .to.be.reverted; - }); - - it("Successful claim from EL", async function () { - const epochBefore = await iVault.epoch(); - console.log(`Epoch before: ${epochBefore}`); - - await iVaultEL.connect(staker).claimCompletedWithdrawals(nodeOperatorToRestaker.get(nodeOperators[0]), wDatas); - console.log(`iVault assets after: ${await iVault.totalAssets()}`); - console.log(`Epoch after: ${await iVault.epoch()}`); - - expect(await iVault.getPendingWithdrawalOf(staker.address)).to.be.closeTo( - withdrawalAmount, - transactErr * BigInt(withdrawalCount), - ); - expect(await iVault.totalAssets()).to.be.closeTo(withdrawalAmount, transactErr * BigInt(withdrawalCount)); - expect(await iVault.epoch()).to.be.eq(epochBefore + BigInt(withdrawalCount)); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, ratioErr); - }); - }); - - describe("Redeem: retrieves assets after they were taken from EL", function () { - let ratio, stakerAmount, staker2Amount, stakerUnstakeAmount, staker2UnstakeAmount, firstDeposit; - before(async function () { - await snapshot.restore(); - await asset.connect(staker3).approve(await iVault.getAddress(), e18); - await iVault4626.connect(staker3).deposit(e18, staker3.address); - firstDeposit = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(firstDeposit, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - await iVaultSetters.setTargetFlashCapacity(1n); - ratio = await iVault.ratio(); - }); - - it("Stakers deposit", async function () { - stakerAmount = 9399680561290658040n; - await iVault4626.connect(staker).deposit(stakerAmount, staker.address); - staker2Amount = 1348950494309030813n; - await iVault4626.connect(staker2).deposit(staker2Amount, staker2.address); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(await iVault.getFreeBalance(), nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - console.log(`Staker amount: ${stakerAmount}`); - console.log(`Staker2 amount: ${staker2Amount}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker has nothing to claim yet", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; - }); - - it("Staker withdraws half", async function () { - const shares = await iToken.balanceOf(staker.address); - stakerUnstakeAmount = shares / 2n; - await iVault4626.connect(staker).withdraw(stakerUnstakeAmount, staker.address); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker is not able to claim yet", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; - }); - - it("Reverts: when redeems the same epoch", async function () { - await expect(iVault4626.connect(iVaultOperator).redeem(staker.address)).to.be.revertedWithCustomError( - iVault, - "IsNotAbleToRedeem", - ); - }); - - it("updateEpoch without available does not affect pending withdrawals", async function () { - const wwlBefore = await iVault.claimerWithdrawalsQueue(0); - const epochBefore = await iVault.epoch(); - await iVaultEL.connect(staker).updateEpoch(); - - const wwlAfter = await iVault.claimerWithdrawalsQueue(0); - const epochAfter = await iVault.epoch(); - expect(wwlBefore).to.be.deep.eq(wwlAfter); - expect(epochAfter).to.be.eq(epochBefore); - }); - - it("Withdraw and claim from EL 1", async function () { - const amount = await iVault.totalAmountToWithdraw(); - await iVault.withdrawFromELAndClaim(nodeOperators[0], amount); - console.log(`Total assets:\t\t${(await iVault.totalAssets()).format()}`); - console.log(`Pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker is now able to claim", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - }); - - it("Reverts: redeem when iVault is paused", async function () { - await iVault.pause(); - await expect(iVault4626.connect(iVaultOperator).redeem(staker.address)).to.be.revertedWith("Pausable: paused"); - }); - - it("Unpause after previous test", async function () { - await iVault.unpause(); - }); - - it("Staker2 withdraws < staker pending withdrawal", async function () { - const stakerPendingWithdrawal = await iVault.getPendingWithdrawalOf(staker.address); - staker2UnstakeAmount = stakerPendingWithdrawal / 10n; - await iVault4626.connect(staker2).withdraw(staker2UnstakeAmount, staker2.address); - }); - - it("Staker2 is not able to claim yet", async function () { - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.false; - }); - - it("Staker is still able to claim", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - }); - - it("Reverts: when staker2 redeems out of turn", async function () { - await expect(iVault4626.connect(iVaultOperator).redeem(staker2.address)).to.be.revertedWithCustomError( - iVault, - "IsNotAbleToRedeem", - ); - }); - - it("New withdrawal is going to the end of the queue", async function () { - const shares = (await iToken.balanceOf(staker.address)) / 2n; - await iVault4626.connect(staker).withdraw(shares, staker.address); - stakerUnstakeAmount = stakerUnstakeAmount + shares; - console.log(`Pending withdrawals: ${await iVault.getPendingWithdrawalOf(staker.address)}`); - console.log(`Unstake amount: ${stakerUnstakeAmount.toString()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker is still able to claim", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - }); - - it("Withdraw and claim from EL to cover only staker2 withdrawal", async function () { - const amount = await iVault.getPendingWithdrawalOf(staker2.address); - await iVault.withdrawFromELAndClaim(nodeOperators[0], amount + transactErr * 2n); - console.log(`Total assets:\t\t${(await iVault.totalAssets()).format()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker is able to claim only the 1st wwl", async function () { - const ableRedeem = await iVault.isAbleToRedeem(staker.address); - expect(ableRedeem[0]).to.be.true; - expect([...ableRedeem[1]]).to.have.members([0n]); - }); - - it("Staker2 is able to claim", async function () { - const ableRedeem = await iVault.isAbleToRedeem(staker2.address); - expect(ableRedeem[0]).to.be.true; - expect([...ableRedeem[1]]).to.have.members([1n]); - }); - - it("Deposit and update epoch to cover pending wwls", async function () { - const totalPWBefore = await iVault.totalAmountToWithdraw(); - const redeemReserveBefore = await iVault.redeemReservedAmount(); - console.log(`Total pending wwls:\t\t${totalPWBefore.format()}`); - console.log(`Redeem reserve before:\t${redeemReserveBefore.format()}`); - - const amount = totalPWBefore - redeemReserveBefore + 100n; - await asset.connect(staker3).approve(await iVault.getAddress(), amount); - await iVault4626.connect(staker3).deposit(amount, staker3.address); - await iVaultEL.connect(iVaultOperator).updateEpoch(); - - const redeemReserveAfter = await iVault.redeemReservedAmount(); - console.log(`Redeem reserve after:\t${redeemReserveAfter.format()}`); - expect(redeemReserveAfter).to.be.closeTo(totalPWBefore, transactErr * 4n); - - const ableRedeem = await iVault.isAbleToRedeem(staker.address); - console.log(`Staker redeem: ${ableRedeem}`); - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - expect([...ableRedeem[1]]).to.have.members([0n, 2n]); - }); - - it("Staker redeems withdrawals", async function () { - console.log(`Ratio: ${await iVault.ratio()}`); - const stakerBalanceBefore = await asset.balanceOf(staker.address); - const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker.address); - const stakerUnstakeAmountAssetValue = await iVault.convertToAssets(stakerUnstakeAmount); - await iVault4626.connect(staker).redeem(staker.address); - const stakerBalanceAfter = await asset.balanceOf(staker.address); - const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker.address); - - console.log(`Staker balance after: ${stakerBalanceAfter}`); - console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); - console.log(`stakerUnstakeAmountAssetValue: ${stakerUnstakeAmountAssetValue}`); - console.log(`stakerPendingWithdrawalsBefore[0]: ${stakerPendingWithdrawalsBefore}`); - - expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( - stakerUnstakeAmountAssetValue, - transactErr * 3n, - ); - expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(stakerUnstakeAmountAssetValue, transactErr * 3n); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker2 redeems withdrawals", async function () { - console.log(`Ratio: ${await iVault.ratio()}`); - const stakerBalanceBefore = await asset.balanceOf(staker2.address); - const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker2.address); - await iVault4626.connect(staker2).redeem(staker2.address); - const stakerBalanceAfter = await asset.balanceOf(staker2.address); - const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker2.address); - - console.log(`Staker balance after: ${stakerBalanceAfter}`); - console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); - const stakerUnstakeAmountAssetValue = await iVault.convertToAssets(staker2UnstakeAmount); - expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( - stakerUnstakeAmountAssetValue, - transactErr * 2n, - ); - expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(stakerUnstakeAmountAssetValue, transactErr * 2n); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Ratio is ok after all", async function () { - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(ratio, ratioErr); - }); - }); - - describe("Redeem: to the different addresses", function () { - let ratio, recipients, pendingShares; - - before(async function () { - await snapshot.restore(); - await iVault4626.connect(staker).deposit("9292557565124725653", staker.address); - const amount = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - }); - - const count = 3; - for (let j = 0; j < count; j++) { - it(`${j} Withdraw to 5 random addresses`, async function () { - recipients = []; - pendingShares = 0n; - for (let i = 0; i < 5; i++) { - const recipient = randomAddress(); - const shares = randomBI(17); - pendingShares = pendingShares + shares; - await iVault4626.connect(staker).withdraw(shares, recipient); - recipients.push(recipient); - } - }); - - it(`${j} Withdraw from EL and update ratio`, async function () { - const amount = await iVault.totalAmountToWithdraw(); - let tx = await iVaultEL.connect(iVaultOperator).undelegateFrom(nodeOperators[0], amount); - const data = await withdrawDataFromTx(tx, nodeOperators[0], nodeOperatorToRestaker.get(nodeOperators[0])); - - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - console.log(`New ratio is: ${ratio}`); - - await mineBlocks(minWithdrawalDelayBlocks); - await iVaultEL.connect(staker).claimCompletedWithdrawals(data[2], [data]); - console.log(`Total assets: ${await iVault.totalAssets()}`); - console.log(`Total withdrawed shares to assets ${await iVault.convertToAssets(pendingShares)}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it(`${j} Recipients claim`, async function () { - for (const r of recipients) { - const rBalanceBefore = await asset.balanceOf(r); - const rPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(r); - await iVault4626.connect(deployer).redeem(r); - const rBalanceAfter = await asset.balanceOf(r); - const rPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(r); - - expect(rBalanceAfter - rPendingWithdrawalsBefore).to.be.closeTo(0, transactErr); - expect(rBalanceBefore - rPendingWithdrawalsAfter).to.be.closeTo(0, transactErr); - } - expect(await calculateRatio(iVault, iToken)).to.be.lte(ratio); - console.log(`Total assets: ${await iVault.totalAssets()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it(`${j} Deposit extra from iVault`, async function () { - const totalDepositedBefore = await iVault.getTotalDeposited(); - - const amount = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - const totalDepositedAfter = await iVault.getTotalDeposited(); - - console.log(`Total assets: ${await iVault.totalAssets()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(await iVault.totalAssets()).to.be.lte(100); - expect(await calculateRatio(iVault, iToken)).to.be.lte(ratio); - }); - } - - it("Update asset ratio and withdraw the rest", async function () { - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - ratio = await iVault.ratio(); - console.log(`New ratio is: ${ratio}`); - - //Withdraw all and take from EL - const shares = await iToken.balanceOf(staker.address); - await iVault4626.connect(staker).withdraw(shares, staker.address); - const amount = await iVault.getTotalDelegated(); - await iVault.withdrawFromELAndClaim(nodeOperators[0], amount); - await iVault4626.connect(iVaultOperator).redeem(staker.address); - - console.log(`iVault total assets: ${await iVault.totalAssets()}`); - console.log(`Total deposited: ${await iVault.getTotalDeposited()}`); - }); - }); - - describe("addRewards: gradually adds amount to the iVault", function () { - const totalDays = 7n; - let totalRewardsAmount; - before(async function () { - await snapshot.restore(); - await asset.connect(staker3).transfer(iVaultOperator.address, 10n * e18); - const operatorBalance = await asset.balanceOf(iVaultOperator.address); - await asset.connect(iVaultOperator).approve(iVault.address, operatorBalance); - await iVaultSetters.setRewardsTimeline(totalDays * 86400n); - }); - - it("addRewards when there are no other rewards have been added", async function () { - const operatorBalanceBefore = await asset.balanceOf(iVaultOperator.address); - const iVaultBalanceBefore = await asset.balanceOf(iVault.address); - const totalAssetsBefore = await iVault.totalAssets(); - - const latestBlock = await ethers.provider.getBlock("latest"); - const nextBlockTimestamp = BigInt(latestBlock.timestamp) + randomBI(2); - await helpers.time.setNextBlockTimestamp(nextBlockTimestamp); - - totalRewardsAmount = randomBI(17); - console.log("Amount:", totalRewardsAmount.format()); - - // const event = await iVaultEL.connect(iVaultOperator).addRewards(totalRewardsAmount); - // expect(event.args["amount"]).to.be.closeTo(totalRewardsAmount, transactErr); - - await expect(iVaultEL.connect(iVaultOperator).addRewards(totalRewardsAmount)) - .to.emit(iVaultEL, "RewardsAdded") - .withArgs(amount => { - expect(amount).to.be.closeTo(totalRewardsAmount, transactErr); - return true; - }, nextBlockTimestamp); - - const operatorBalanceAfter = await asset.balanceOf(iVaultOperator.address); - const iVaultBalanceAfter = await asset.balanceOf(iVault.address); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log("Operator balance diff:", (operatorBalanceBefore - operatorBalanceAfter).format()); - console.log("iVault balance diff:", (iVaultBalanceAfter - iVaultBalanceBefore).format()); - console.log("Total assets diff:", (totalAssetsAfter - totalAssetsBefore).format()); - - expect(operatorBalanceBefore - operatorBalanceAfter).to.be.closeTo(totalRewardsAmount, transactErr); - expect(iVaultBalanceAfter - iVaultBalanceBefore).to.be.closeTo(totalRewardsAmount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(0n, totalDays); - }); - - it("Can not add more rewards until the end of timeline", async function () { - const amount = randomBI(17); - await expect(iVaultEL.connect(iVaultOperator).addRewards(amount)).to.revertedWithCustomError( - iVault, - "TimelineNotOver", - ); - }); - - it("Stake some amount", async function () { - await iVault4626.connect(staker).deposit(10n * e18, staker.address); - }); - - it("Check total assets every day", async function () { - let startTimeline = await iVault.startTimeline(); - let latestBlock; - let daysPassed; - do { - const totalAssetsBefore = await iVault.totalAssets(); - await helpers.time.increase(day); - latestBlock = await ethers.provider.getBlock("latest"); - const currentTime = BigInt(latestBlock.timestamp); - daysPassed = (currentTime - startTimeline) / day; - - const totalAssetsAfter = await iVault.totalAssets(); - console.log("Total assets increased by:", (totalAssetsAfter - totalAssetsBefore).format()); - console.log("Ratio:", await calculateRatio(iVault, iToken)); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(totalRewardsAmount / totalDays, totalDays); - } while (daysPassed < totalDays); - - console.log("Total assets after:\t\t", (await iVault.totalAssets()).format()); - console.log("iVault balance after:\t", (await asset.balanceOf(iVault.address)).format()); - }); - - it("Total assets does not change on a next day after timeline passed", async function () { - const totalAssetsBefore = await iVault.totalAssets(); - await helpers.time.increase(day); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log("Total assets increased by:", (totalAssetsAfter - totalAssetsBefore).format()); - expect(totalAssetsAfter).to.be.eq(totalAssetsBefore); - }); - - it("New rewards can be added", async function () { - const operatorBalanceBefore = await asset.balanceOf(iVaultOperator.address); - const iVaultBalanceBefore = await asset.balanceOf(iVault.address); - const totalAssetsBefore = await iVault.totalAssets(); - - const latestBlock = await ethers.provider.getBlock("latest"); - const nextBlockTimestamp = BigInt(latestBlock.timestamp) + randomBI(2); - await helpers.time.setNextBlockTimestamp(nextBlockTimestamp); - - totalRewardsAmount = randomBI(17); - await expect(iVaultEL.connect(iVaultOperator).addRewards(totalRewardsAmount)) - .to.emit(iVaultEL, "RewardsAdded") - .withArgs(amount => { - expect(amount).to.be.closeTo(totalRewardsAmount, transactErr); - return true; - }, nextBlockTimestamp); - - const operatorBalanceAfter = await asset.balanceOf(iVaultOperator.address); - const iVaultBalanceAfter = await asset.balanceOf(iVault.address); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log("Operator balance diff:", (operatorBalanceBefore - operatorBalanceAfter).format()); - console.log("iVault balance diff:", (iVaultBalanceAfter - iVaultBalanceBefore).format()); - console.log("Total assets diff:", (totalAssetsAfter - totalAssetsBefore).format()); - expect(operatorBalanceBefore - operatorBalanceAfter).to.be.closeTo(totalRewardsAmount, transactErr); - expect(iVaultBalanceAfter - iVaultBalanceBefore).to.be.closeTo(totalRewardsAmount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(0n, totalDays); - }); - }); - - describe("Redeem all after asset ratio changed", function () { - let staker1UnstakeAmount, staker2UnstakeAmount, withdrawRatio; - let TARGET; - before(async function () { - await snapshot.restore(); - TARGET = 1000_000n; - await iVaultSetters.setTargetFlashCapacity(TARGET); - }); - - it("Stakers deposit and delegate", async function () { - const staker1Amount = 9399680561290658040n; - await iVault4626.connect(staker).deposit(staker1Amount, staker.address); - const staker2Amount = 1348950494309030813n; - await iVault4626.connect(staker2).deposit(staker2Amount, staker2.address); - console.log(`Staker desposited:\t${staker1Amount.format()}`); - console.log(`Staker2 deposited:\t${staker2Amount.format()}`); - const amount = await iVault.getFreeBalance(); - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - console.log(`Ratio after delegation:\t${await iVault.ratio()}`); - }); - - it("Change ratio - transfer to strategy", async function () { - console.log(`Ratio before:\t\t${(await iVault.ratio()).format()}`); - await addRewardsToStrategy(a.assetStrategy, e18, staker3); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - withdrawRatio = await iVault.ratio(); - console.log(`Ratio after update:\t${withdrawRatio.format()}`); - }); - - it("Staker1 withdraws", async function () { - staker1UnstakeAmount = await iToken.balanceOf(staker.address); - expect(staker1UnstakeAmount).to.be.gt(0); - const expectedPending = await iVault.convertToAssets(staker1UnstakeAmount); - await iVault4626.connect(staker).withdraw(staker1UnstakeAmount, staker.address); - const pendingWithdrawal = await iVault.getPendingWithdrawalOf(staker.address); - console.log(`Pending withdrawal:\t${pendingWithdrawal.format()}`); - - expect(pendingWithdrawal).to.be.closeTo(expectedPending, transactErr); - expect(pendingWithdrawal).to.be.closeTo((staker1UnstakeAmount * e18) / withdrawRatio, transactErr * 3n); - console.log(`Ratio after:\t\t\t${await iVault.ratio()}`); - }); - - it("Staker2 withdraws", async function () { - staker2UnstakeAmount = await iToken.balanceOf(staker2.address); - expect(staker2UnstakeAmount).to.be.gt(0); - const expectedPending = await iVault.convertToAssets(staker2UnstakeAmount); - await iVault4626.connect(staker2).withdraw(staker2UnstakeAmount, staker2.address); - const pendingWithdrawal = await iVault.getPendingWithdrawalOf(staker2.address); - console.log(`Pending withdrawal:\t${pendingWithdrawal.format()}`); - - expect(pendingWithdrawal).to.be.closeTo(expectedPending, transactErr); - expect(pendingWithdrawal).to.be.closeTo((staker2UnstakeAmount * e18) / withdrawRatio, transactErr * 3n); - console.log(`Ratio after: ${await iVault.ratio()}`); - }); - - it("Withdraw and claim from EL", async function () { - console.log(`Total assets before: ${(await iVault.totalAssets()).format()}`); - const amount = await iVault.totalAmountToWithdraw(); - await iVault.withdrawFromELAndClaim(nodeOperators[0], amount); - console.log(`Total assets after: ${(await iVault.totalAssets()).format()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Stakers are able to redeem", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - - console.log( - `--- Going to change target flash capacity and transfer 1000 wei${a.assetName} to iVault to supply withdrawals ---`, - ); - await iVaultSetters.setTargetFlashCapacity(1n); - await asset.connect(staker3).transfer(iVault.address, 1000n); - await iVaultEL.connect(staker3).updateEpoch(); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - }); - - it("Staker redeems withdrawals", async function () { - console.log(`Ratio: ${await iVault.ratio()}`); - const stakerBalanceBefore = await asset.balanceOf(staker.address); - const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker.address); - await iVault4626.connect(staker).redeem(staker.address); - const stakerBalanceAfter = await asset.balanceOf(staker.address); - const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker.address); - - console.log(`Staker balance after: ${stakerBalanceAfter.format()}`); - console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); - - expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( - stakerBalanceAfter - stakerBalanceBefore, - transactErr, - ); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker2 redeems withdrawals", async function () { - console.log(`Ratio: ${await iVault.ratio()}`); - const stakerBalanceBefore = await asset.balanceOf(staker2.address); - const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker2.address); - await iVault4626.connect(staker2).redeem(staker2.address); - const stakerBalanceAfter = await asset.balanceOf(staker2.address); - const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker2.address); - - console.log(`Staker balance after: ${stakerBalanceAfter.format()}`); - console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); - - expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( - stakerBalanceAfter - stakerBalanceBefore, - transactErr, - ); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - }); - - //Deprecated flow. Ratio calculation moved to the backend - describe.skip("iToken ratio depends on the ratio of strategies", function () { - before(async function () { - await snapshot.restore(); - await iVault4626.connect(staker).deposit(e18, staker.address); - const amount = await iVault.totalAssets(); - }); - - it("Ratio is not affected by strategy rewards until the first deposit to EL", async function () { - const ratioBefore = await iVault.ratio(); - await addRewardsToStrategy(a.assetStrategy, toWei(1), staker2); - const ratioAfter = await iVault.ratio(); - - console.log(`Ratio before:\t${ratioBefore.format()}`); - console.log(`Ratio after:\t${ratioAfter.format()}`); - console.log(`Diff:\t\t\t${(ratioBefore - ratioAfter).format()}`); - - expect(ratioAfter).to.be.eq(ratioBefore); - }); - - it("Ratio declines along with the ratio of rebase-like asset", async function () { - const ratioBefore = await iVault.ratio(); - await asset.connect(staker2).transfer(await iVault.getAddress(), toWei(1)); - const ratioAfter = await iVault.ratio(); - - console.log(`Ratio before:\t${ratioBefore.format()}`); - console.log(`Ratio after:\t${ratioAfter.format()}`); - console.log(`Diff:\t\t\t${(ratioBefore - ratioAfter).format()}`); - - expect(ratioAfter).to.be.lt(ratioBefore); - }); - - const testData = [ - { amount: "1000000000000000000" }, - { amount: "1000000000000000000" }, - { amount: "1000000000000000000" }, - ]; - - testData.forEach(function (test) { - it(`Ratio declines when the strategy rewards are growing: ${test.amount}`, async function () { - const amount = await iVault.totalAssets(); - if (amount > 10n) { - await iVaultEL - .connect(iVaultOperator) - .delegateToOperator(amount, nodeOperators[0], ethers.ZeroHash, [ethers.ZeroHash, 0]); - } - const ratioBefore = await iVault.ratio(); - await addRewardsToStrategy(a.assetStrategy, test.amount, staker2); - const ratioAfter = await iVault.ratio(); - - console.log(`Ratio before:\t${ratioBefore.format()}`); - console.log(`Ratio after:\t${ratioAfter.format()}`); - console.log(`Diff:\t\t\t${(ratioBefore - ratioAfter).format()}`); - - expect(ratioAfter).to.be.lt(ratioBefore); - }); - }); - }); - }); -}); - diff --git a/projects/vaults/test/InceptionVault_S.js b/projects/vaults/test/InceptionVault_S.js deleted file mode 100644 index 655ed020..00000000 --- a/projects/vaults/test/InceptionVault_S.js +++ /dev/null @@ -1,4344 +0,0 @@ -const helpers = require("@nomicfoundation/hardhat-network-helpers"); -const { ethers, upgrades, network } = require("hardhat"); -const { expect } = require("chai"); -const { - impersonateWithEth, - setBlockTimestamp, - getRandomStaker, - calculateRatio, - toWei, - randomBI, - mineBlocks, - randomBIMax, - randomAddress, - e18, - day, -} = require("./helpers/utils.js"); -const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); -const { ZeroAddress } = require("ethers"); -BigInt.prototype.format = function () { - return this.toLocaleString("de-DE"); -}; - -const assets = [ - { - assetName: "stETH", - assetAddress: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - vaultName: "InstEthVault", - vaultFactory: "InVault_S_E2", - iVaultOperator: "0xd87D15b80445EC4251e33dBe0668C335624e54b7", - ratioErr: 3n, - transactErr: 5n, - blockNumber: 21687985, - impersonateStaker: async function (staker, iVault) { - const donor = await impersonateWithEth("0x43594da5d6A03b2137a04DF5685805C676dEf7cB", toWei(1)); - const stEth = await ethers.getContractAt("stETH", "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); - const stEthAmount = toWei(1000); - await stEth.connect(donor).approve(this.assetAddress, stEthAmount); - - const wstEth = await ethers.getContractAt("IWSteth", this.assetAddress); - const balanceBefore = await wstEth.balanceOf(donor.address); - await wstEth.connect(donor).wrap(stEthAmount); - const balanceAfter = await wstEth.balanceOf(donor.address); - - const wstAmount = balanceAfter - balanceBefore; - await wstEth.connect(donor).transfer(staker.address, wstAmount); - await wstEth.connect(staker).approve(await iVault.getAddress(), wstAmount); - return staker; - }, - addRewardsMellowVault: async function (amount, mellowVault) { - const donor = await impersonateWithEth("0x43594da5d6A03b2137a04DF5685805C676dEf7cB", toWei(1)); - const stEth = await ethers.getContractAt("stETH", "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"); - await stEth.connect(donor).approve(this.assetAddress, amount); - - const wstEth = await ethers.getContractAt("IWSteth", this.assetAddress); - const balanceBefore = await wstEth.balanceOf(donor); - await wstEth.connect(donor).wrap(amount); - const balanceAfter = await wstEth.balanceOf(donor); - const wstAmount = balanceAfter - balanceBefore; - await wstEth.connect(donor).transfer(mellowVault, wstAmount); - }, - }, -]; -let MAX_TARGET_PERCENT; - -//https://docs.mellow.finance/mellow-lrt-lst-primitive/contract-deployments -const mellowVaults = [ - { - name: "P2P", - vaultAddress: "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", - wrapperAddress: "0x41A1FBEa7Ace3C3a6B66a73e96E5ED07CDB2A34d", - bondStrategyAddress: "0xA0ea6d4fe369104eD4cc18951B95C3a43573C0F6", - curatorAddress: "0x4a3c7F2470Aa00ebE6aE7cB1fAF95964b9de1eF4", - configuratorAddress: "0x84b240E99d4C473b5E3dF1256300E2871412dDfe", - }, - { - name: "Mev Capital", - vaultAddress: "0x5fD13359Ba15A84B76f7F87568309040176167cd", - wrapperAddress: "0xdC1741f9bD33DD791942CC9435A90B0983DE8665", - bondStrategyAddress: "0xc3A149b5Ca3f4A5F17F5d865c14AA9DBb570F10A", - curatorAddress: "0xA1E38210B06A05882a7e7Bfe167Cd67F07FA234A", - configuratorAddress: "0x2dEc4fDC225C1f71161Ea481E23D66fEaAAE2391", - }, - { - name: "Re7", - vaultAddress: "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", - wrapperAddress: "0x70cD3464A41B6692413a1Ba563b9D53955D5DE0d", - bondStrategyAddress: "0xcE3A8820265AD186E8C1CeAED16ae97176D020bA", - curatorAddress: "0xE86399fE6d7007FdEcb08A2ee1434Ee677a04433", - configuratorAddress: "0x214d66d110060dA2848038CA0F7573486363cAe4", - }, -]; - -const symbioticVaults = [ - { - name: "Gauntlet Restaked wstETH", - vaultAddress: "0xc10A7f0AC6E3944F4860eE97a937C51572e3a1Da", - collateral: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - burner: "0xDB0737bd7eBEA50135e4c8af56900b029b858371", - delegator: "0x1f16782a9b75FfFAD87e7936791C672bdDBCb8Ec", - slasher: "0x541c86eb2C5e7F3E0C04eF82aeb68EA6A86409ef", - }, - { - name: "Ryabina wstETH", - vaultAddress: "0x93b96D7cDe40DC340CA55001F46B3B8E41bC89B4", - collateral: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", - burner: "0x80918bcD2d1e343ed46E201CD09238149dB5A5bF", - delegator: "0x742DD9676086579994E9a3DD536C9CCc0Cc6e78D", - slasher: "0xCCA42120Dc4fc945F2fBd227d7D9EA5963bba490", - }, -]; - -const initVault = async a => { - const block = await ethers.provider.getBlock("latest"); - console.log(`Starting at block number: ${block.number}`); - console.log("... Initialization of Inception ...."); - - console.log("- Asset"); - const asset = await ethers.getContractAt(a.assetName, a.assetAddress); - asset.address = await asset.getAddress(); - - /// =============================== Mellow Vaults =============================== - for (const mVaultInfo of mellowVaults) { - console.log(`- MellowVault ${mVaultInfo.name} and curator`); - mVaultInfo.vault = await ethers.getContractAt("IMellowVault", mVaultInfo.vaultAddress); - - const mellowVaultOperatorMock = await ethers.deployContract("OperatorMock", [mVaultInfo.bondStrategyAddress]); - mellowVaultOperatorMock.address = await mellowVaultOperatorMock.getAddress(); - await network.provider.send("hardhat_setCode", [ - mVaultInfo.curatorAddress, - await mellowVaultOperatorMock.getDeployedCode(), - ]); - //Copy storage values - for (let i = 0; i < 5; i++) { - const slot = "0x" + i.toString(16); - const value = await network.provider.send("eth_getStorageAt", [mellowVaultOperatorMock.address, slot, "latest"]); - await network.provider.send("hardhat_setStorageAt", [mVaultInfo.curatorAddress, slot, value]); - } - mVaultInfo.curator = await ethers.getContractAt("OperatorMock", mVaultInfo.curatorAddress); - } - - /// =============================== Symbiotic Vaults =============================== - - for (const sVaultInfo of symbioticVaults) { - console.log(`- Symbiotic ${sVaultInfo.name}`); - sVaultInfo.vault = await ethers.getContractAt("IVault", sVaultInfo.vaultAddress); - - // const mellowVaultOperatorMock = await ethers.deployContract("OperatorMock", [mVaultInfo.bondStrategyAddress]); - // mellowVaultOperatorMock.address = await mellowVaultOperatorMock.getAddress(); - // await network.provider.send("hardhat_setCode", [mVaultInfo.curatorAddress, await mellowVaultOperatorMock.getDeployedCode()]); - // //Copy storage values - // for (let i = 0; i < 5; i++) { - // const slot = "0x" + i.toString(16); - // const value = await network.provider.send("eth_getStorageAt", [mellowVaultOperatorMock.address, slot, "latest"]); - // await network.provider.send("hardhat_setStorageAt", [mVaultInfo.curatorAddress, slot, value]); - // } - // mVaultInfo.curator = await ethers.getContractAt("OperatorMock", mVaultInfo.curatorAddress); - } - - /// =============================== Inception Vault =============================== - console.log("- iToken"); - const iTokenFactory = await ethers.getContractFactory("InceptionToken"); - const iToken = await upgrades.deployProxy(iTokenFactory, ["TEST InceptionLRT Token", "tINt"]); - iToken.address = await iToken.getAddress(); - - console.log("- iVault operator"); - const iVaultOperator = await impersonateWithEth(a.iVaultOperator, e18); - - console.log("- Mellow Restaker"); - const mellowRestakerFactory = await ethers.getContractFactory("IMellowRestaker"); - let mellowRestaker = await upgrades.deployProxy(mellowRestakerFactory, [ - [mellowVaults[0].wrapperAddress], - [mellowVaults[0].vaultAddress], - a.assetAddress, - a.iVaultOperator, - ]); - mellowRestaker.address = await mellowRestaker.getAddress(); - - console.log("- Symbiotic Restaker"); - const symbioticRestakerFactory = await ethers.getContractFactory("ISymbioticRestaker"); - let symbioticRestaker = await upgrades.deployProxy(symbioticRestakerFactory, [ - [symbioticVaults[0].vaultAddress], - a.assetAddress, - a.iVaultOperator, - ]); - symbioticRestaker.address = await symbioticRestaker.getAddress(); - - console.log("- Ratio feed"); - const iRatioFeedFactory = await ethers.getContractFactory("InceptionRatioFeed"); - const ratioFeed = await upgrades.deployProxy(iRatioFeedFactory, []); - await ratioFeed.updateRatioBatch([iToken.address], [e18]); //Set initial ratio e18 - ratioFeed.address = await ratioFeed.getAddress(); - - console.log("- InceptionLibrary"); - const iLibrary = await ethers.deployContract("InceptionLibrary"); - await iLibrary.waitForDeployment(); - - console.log("- iVault"); - const iVaultFactory = await ethers.getContractFactory(a.vaultFactory, { - libraries: { InceptionLibrary: await iLibrary.getAddress() }, - }); - const iVault = await upgrades.deployProxy( - iVaultFactory, - [a.vaultName, a.iVaultOperator, a.assetAddress, iToken.address, mellowRestaker.address], - { - unsafeAllowLinkedLibraries: true, - }, - ); - iVault.address = await iVault.getAddress(); - - await iVault.setRatioFeed(ratioFeed.address); - await iVault.setSymbioticRestaker(symbioticRestaker.address); - await mellowRestaker.setVault(iVault.address); - await symbioticRestaker.setVault(iVault.address); - await iToken.setVault(iVault.address); - MAX_TARGET_PERCENT = await iVault.MAX_TARGET_PERCENT(); - console.log("... iVault initialization completed ...."); - - iVault.withdrawFromMellowAndClaim = async function (mellowVaultAddress, amount) { - await this.connect(iVaultOperator).undelegateFromMellow(mellowVaultAddress, amount, 1296000); - await mellowVaults[0].curator.processWithdrawals([mellowRestaker.address]); - await this.connect(iVaultOperator).claimCompletedWithdrawalsMellow(); - }; - - return [iToken, iVault, ratioFeed, asset, iVaultOperator, mellowRestaker, symbioticRestaker, iLibrary]; -}; - -assets.forEach(function (a) { - describe(`Inception Symbiotic Vault ${a.assetName}`, function () { - this.timeout(150000); - let iToken, iVault, ratioFeed, asset, mellowRestaker, symbioticRestaker, iLibrary; - let iVaultOperator, deployer, staker, staker2, staker3, treasury; - let ratioErr, transactErr; - let snapshot; - - before(async function () { - if (process.env.ASSETS) { - const assets = process.env.ASSETS.toLocaleLowerCase().split(","); - if (!assets.includes(a.assetName.toLowerCase())) { - console.log(`${a.assetName} is not in the list, going to skip`); - this.skip(); - } - } - - await network.provider.send("hardhat_reset", [ - { - forking: { - jsonRpcUrl: a.url ? a.url : network.config.forking.url, - blockNumber: a.blockNumber ? a.blockNumber : network.config.forking.blockNumber, - }, - }, - ]); - - [iToken, iVault, ratioFeed, asset, iVaultOperator, mellowRestaker, symbioticRestaker, iLibrary] = - await initVault(a); - ratioErr = a.ratioErr; - transactErr = a.transactErr; - - [deployer, staker, staker2, staker3] = await ethers.getSigners(); - - staker = await a.impersonateStaker(staker, iVault); - staker2 = await a.impersonateStaker(staker2, iVault); - staker3 = await a.impersonateStaker(staker3, iVault); - treasury = await iVault.treasury(); //deployer - - snapshot = await helpers.takeSnapshot(); - }); - - after(async function () { - if (iVault) { - await iVault.removeAllListeners(); - } - }); - - describe("Symbiotic Native | Base flow no flash", function () { - let totalDeposited = 0n; - let delegatedSymbiotic = 0n; - let rewardsSymbiotic = 0n; - - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - }); - - it("Initial stats", async function () { - expect(await iVault.ratio()).to.be.eq(e18); - expect(await iVault.totalAssets()).to.be.eq(0n); - expect(await iVault.getTotalDeposited()).to.be.eq(0n); - expect(await iVault.getTotalDelegated()).to.be.eq(0n); - expect(await iVault.getFlashCapacity()).to.be.eq(0n); - expect(await iVault.getFreeBalance()).to.be.eq(0n); - }); - - it("User can deposit to iVault", async function () { - totalDeposited += toWei(20); - const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit - const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); - expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); - - expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); - expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); - expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); - expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(e18, 1n); - }); - - it("Delegate to symbioticVault#1", async function () { - const amount = (await iVault.totalAssets()) / 3n; - expect(amount).to.be.gt(0n); - const totalAssetsBefore = await iVault.totalAssets(); - - const sVault = await ethers.getContractAt("IVault", symbioticVaults[0].vaultAddress); - const code = await ethers.provider.getCode(symbioticVaults[0].vaultAddress); - console.log("Deployed Code len:", code.length); - // await sVault.connect(staker).deposit(staker.address, amount); - console.log("totalStake: ", await sVault.totalStake()); - - await iVault.connect(iVaultOperator).delegateToSymbioticVault(symbioticVaults[0].vaultAddress, amount); - delegatedSymbiotic += amount; - - console.log("totalStake new: ", await sVault.totalStake()); - - const symbioticBalance = await symbioticVaults[0].vault.activeBalanceOf(symbioticRestaker.address); - const symbioticBalance2 = await symbioticVaults[1].vault.activeBalanceOf(symbioticRestaker.address); - const totalAssetsAfter = await iVault.totalAssets(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const delegatedTo = await symbioticRestaker.getDeposited(symbioticVaults[0].vaultAddress); - const delegatedTo2 = await symbioticRestaker.getDeposited(symbioticVaults[1].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - console.log("Mellow LP token balance: ", symbioticBalance.format()); - console.log("Mellow LP token balance2: ", symbioticBalance2.format()); - console.log("Amount delegated: ", delegatedSymbiotic.format()); - - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); - expect(totalDelegatedAfter).to.be.closeTo(delegatedSymbiotic, transactErr); - expect(delegatedTo).to.be.closeTo(amount, transactErr); - expect(delegatedTo2).to.be.closeTo(0n, transactErr); - expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr); - expect(symbioticBalance).to.be.gte(amount / 2n); - expect(symbioticBalance2).to.be.eq(0n); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(e18, ratioErr); - }); - - it("Add new symbioticVault", async function () { - await expect(symbioticRestaker.addVault(symbioticVaults[1].vaultAddress)) - .to.emit(symbioticRestaker, "VaultAdded") - .withArgs(symbioticVaults[1].vaultAddress); - }); - - it("Delegate all to symbioticVault#2", async function () { - const amount = await iVault.getFreeBalance(); - expect(amount).to.be.gt(0n); - const totalAssetsBefore = await iVault.totalAssets(); - - await iVault.connect(iVaultOperator).delegateToSymbioticVault(symbioticVaults[1].vaultAddress, amount); - delegatedSymbiotic += amount; - - const symbioticBalance = await symbioticVaults[0].vault.activeBalanceOf(symbioticRestaker.address); - const symbioticBalance2 = await symbioticVaults[1].vault.activeBalanceOf(symbioticRestaker.address); - const totalAssetsAfter = await iVault.totalAssets(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const delegatedTo2 = await symbioticRestaker.getDeposited(symbioticVaults[1].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - console.log("Symbiotic LP token balance: ", symbioticBalance.format()); - console.log("Symbiotic LP token balance2: ", symbioticBalance2.format()); - console.log("Amount delegated: ", delegatedSymbiotic.format()); - - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); - expect(totalDelegatedAfter).to.be.closeTo(delegatedSymbiotic, transactErr * 2n); - expect(delegatedTo2).to.be.closeTo(amount, transactErr); - expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr * 2n); - expect(symbioticBalance2).to.be.gte(amount / 2n); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(e18, ratioErr); - }); - - it("Update ratio", async function () { - const ratio = await calculateRatio(iVault, iToken); - console.log(`Calculated ratio:\t\t\t${ratio.format()}`); - await ratioFeed.updateRatioBatch([iToken.address], [ratio]); - console.log(`iVault ratio:\t\t\t\t${(await iVault.ratio()).format()}`); - expect(await iVault.ratio()).eq(ratio); - }); - - it("Add rewards to Symbiotic protocol and estimate ratio, it remains the same", async function () { - const ratioBefore = await calculateRatio(iVault, iToken); - const totalDelegatedToBefore = await symbioticRestaker.getDeposited(symbioticVaults[0].vaultAddress); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); - console.log(`Delegated to before:\t${totalDelegatedToBefore.format()}`); - - console.log(`vault bal before: ${await asset.balanceOf(symbioticVaults[0].vaultAddress)}`); - await asset.connect(staker3).transfer(symbioticVaults[0].vaultAddress, e18); - console.log(`vault bal after: ${await asset.balanceOf(symbioticVaults[0].vaultAddress)}`); - - const ratioAfter = await calculateRatio(iVault, iToken); - const totalDelegatedToAfter = await symbioticRestaker.getDeposited(symbioticVaults[0].vaultAddress); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - expect(ratioAfter).to.be.eq(ratioBefore); - expect(totalDelegatedToAfter - totalDelegatedToBefore).to.be.eq(0n); - expect(totalDelegatedAfter - totalDelegatedBefore).to.be.eq(totalDelegatedToAfter - totalDelegatedToBefore); - }); - - it("User can withdraw all", async function () { - const shares = await iToken.balanceOf(staker.address); - const assetValue = await iVault.convertToAssets(shares); - console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); - console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); - const tx = await iVault.connect(staker).withdraw(shares, staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["owner"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.eq(assetValue); - expect(events[0].args["iShares"]).to.be.eq(shares); - - const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - const totalPW = await iVault.totalAmountToWithdraw(); - expect(stakerPW).to.be.eq(0n); - expect(staker2PW).to.be.closeTo(assetValue, transactErr); - expect(totalPW).to.be.closeTo(assetValue, transactErr); - }); - - it("Update ratio after all shares burn", async function () { - const calculatedRatio = await calculateRatio(iVault, iToken); - console.log(`Calculated ratio:\t\t\t${calculatedRatio.format()}`); - expect(calculatedRatio).to.be.eq(e18); //Because all shares have been burnt at this point - - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - console.log(`iVault ratio after:\t\t\t${(await iVault.ratio()).format()}`); - expect(await iVault.ratio()).eq(calculatedRatio); - }); - - it("Undelegate from Symbiotic", async function () { - const totalAssetsBefore = await iVault.totalAssets(); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); - console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); - - const amount = await symbioticRestaker.getDeposited(symbioticVaults[0].vaultAddress); - const amount2 = await symbioticRestaker.getDeposited(symbioticVaults[1].vaultAddress); - await iVault.connect(iVaultOperator).undelegateFromSymbiotic(symbioticVaults[0].vaultAddress, amount); - await iVault.connect(iVaultOperator).undelegateFromSymbiotic(symbioticVaults[1].vaultAddress, amount2); - - symbioticVaultEpoch1 = symbioticVaults[0].vault.currentEpoch() + 1n; - symbioticVaultEpoch2 = symbioticVaults[1].vault.currentEpoch() + 1n; - - const totalAssetsAfter = await iVault.totalAssets(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedTo = await symbioticRestaker.getDeposited(symbioticVaults[0].vaultAddress); - const totalDelegatedTo2 = await symbioticRestaker.getDeposited(symbioticVaults[1].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const pendingWithdrawalsSymbioticAfter = await symbioticRestaker.pendingWithdrawalAmount(); - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending from Symbiotic:\t\t${pendingWithdrawalsSymbioticAfter.format()}`); - - expect(totalAssetsAfter).to.be.eq(totalAssetsBefore); //Nothing has come to the iVault yet - expect(totalDelegatedAfter).to.be.closeTo(0n, transactErr); - expect(totalDelegatedTo).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow - expect(totalDelegatedTo2).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr * 2n); //Total deposited amount did not change - expect(pendingWithdrawalsSymbioticAfter).to.be.closeTo(amount + amount2, transactErr * 2n); - }); - - it("Process request to transfers pending funds to symbioticRestaker", async function () { - console.log(`current epoch of 1: ${await symbioticVaults[0].vault.currentEpoch()}`); - console.log(`current epoch of 2: ${await symbioticVaults[1].vault.currentEpoch()}`); - - const epochDuration1 = await symbioticVaults[0].vault.epochDuration(); - const epochDuration2 = await symbioticVaults[1].vault.epochDuration(); - - const nextEpochStart1 = await symbioticVaults[0].vault.nextEpochStart(); - const nextEpochStart2 = await symbioticVaults[1].vault.nextEpochStart(); - - const maxNextEpochStart = nextEpochStart1 > nextEpochStart2 ? nextEpochStart1 : nextEpochStart2; - const maxEpochDuration = epochDuration1 > epochDuration2 ? epochDuration1 : epochDuration2; - - console.log(`maxNextEpochStart: ${maxNextEpochStart}`); - - await setBlockTimestamp(Number(maxNextEpochStart + maxEpochDuration + 1n)); - - console.log(`current epoch of 1: ${await symbioticVaults[0].vault.currentEpoch()}`); - - // const totalDepositedBefore = await iVault.getTotalDeposited(); - // const pendingWithdrawalsMellowBefore = await symbioticRestaker.pendingWithdrawalAmount(); - // const restakerBalanceBefore = await asset.balanceOf(symbioticRestaker.address); - // console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - // console.log(`Pending from Mellow before:\t\t${pendingWithdrawalsMellowBefore.format()}`); - // await mellowVaults[0].curator.processWithdrawals([mellowRestaker.address]); - // await mellowVaults[1].curator.processWithdrawals([mellowRestaker.address]); - // const totalDepositedAfter = await iVault.getTotalDeposited(); - // const pendingWithdrawalsMellowAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - // const restakerBalanceAfter = await asset.balanceOf(mellowRestaker.address); - // console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); - // console.log(`Pending from Mellow:\t\t\t${pendingWithdrawalsMellowAfter.format()}`); - // console.log(`Restaker balance diff:\t\t\t${(restakerBalanceAfter - restakerBalanceBefore).format()}`); - // expect(restakerBalanceAfter - restakerBalanceBefore).to.be.eq(pendingWithdrawalsMellowBefore); - // expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - // expect(pendingWithdrawalsMellowAfter).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); - }); - - it("Claim Symbiotic withdrawal transfer funds from Symbiotic to the vault", async function () { - const pendingWithdrawalsSymbiotic = await symbioticRestaker.pendingWithdrawalAmount(); - const totalAssetsBefore = await iVault.totalAssets(); - const restakerBalanceBefore = await asset.balanceOf(symbioticRestaker.address); - - await iVault - .connect(iVaultOperator) - .claimCompletedWithdrawalsSymbiotic( - symbioticVaults[0].vaultAddress, - (await symbioticVaults[0].vault.currentEpoch()) - 1n, - ); - - await iVault - .connect(iVaultOperator) - .claimCompletedWithdrawalsSymbiotic( - symbioticVaults[1].vaultAddress, - (await symbioticVaults[1].vault.currentEpoch()) - 1n, - ); - - const totalAssetsAfter = await iVault.totalAssets(); - const restakerBalanceAfter = await asset.balanceOf(mellowRestaker.address); - - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(pendingWithdrawalsSymbiotic, transactErr); - expect(restakerBalanceBefore).to.be.closeTo(restakerBalanceAfter, transactErr); - }); - - it("Staker is able to redeem", async function () { - const queuedPendingWithdrawal = (await iVault.claimerWithdrawalsQueue(0)).amount; - const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); - const redeemReserve = await iVault.redeemReservedAmount(); - const freeBalance = await iVault.getFreeBalance(); - - console.log("Queued withdrawal", queuedPendingWithdrawal.format()); - console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); - console.log("Redeem reserve", redeemReserve.format()); - console.log("Free balance", freeBalance.format()); - - //Compensate transactions loses - const diff = queuedPendingWithdrawal - freeBalance - redeemReserve; - if (diff > 0n) { - expect(diff).to.be.lte(transactErr * 2n); - await asset.connect(staker3).transfer(iVault.address, diff + 1n); - await iVault.connect(staker3).updateEpoch(); - } - - console.log("Redeem reserve after", await iVault.redeemReservedAmount()); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - }); - - it("Redeem withdraw", async function () { - const balanceBefore = await asset.balanceOf(staker2.address); - const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); - - const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Redeem"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); - - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - const balanceAfter = await asset.balanceOf(staker2.address); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); - console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); - - expect(staker2PWAfter).to.be.eq(0n); - expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); - expect(totalDepositedAfter).to.be.closeTo(0n, transactErr); - expect(totalAssetsAfter).to.be.closeTo(0n, transactErr); - }); - }); - - describe("Base flow no flash", function () { - let totalDeposited = 0n; - let delegatedMellow = 0n; - let rewardsMellow = 0n; - - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - }); - - it("Initial stats", async function () { - expect(await iVault.ratio()).to.be.eq(e18); - expect(await iVault.totalAssets()).to.be.eq(0n); - expect(await iVault.getTotalDeposited()).to.be.eq(0n); - expect(await iVault.getTotalDelegated()).to.be.eq(0n); - expect(await iVault.getFlashCapacity()).to.be.eq(0n); - expect(await iVault.getFreeBalance()).to.be.eq(0n); - }); - - it("User can deposit to iVault", async function () { - totalDeposited += toWei(20); - const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit - const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); - expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); - - expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); - expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); - expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); - expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(e18, 1n); - }); - - it("Delegate to mellowVault#1", async function () { - const amount = (await iVault.getFreeBalance()) / 3n; - expect(amount).to.be.gt(0n); - const totalAssetsBefore = await iVault.totalAssets(); - - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000); - delegatedMellow += amount; - - const mellowBalance = await mellowVaults[0].vault.balanceOf(mellowRestaker.address); - const mellowBalance2 = await mellowVaults[1].vault.balanceOf(mellowRestaker.address); - const totalAssetsAfter = await iVault.totalAssets(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const delegatedTo = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const delegatedTo2 = await iVault.getDelegatedTo(mellowVaults[1].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - console.log("Mellow LP token balance: ", mellowBalance.format()); - console.log("Mellow LP token balance2: ", mellowBalance2.format()); - console.log("Amount delegated: ", delegatedMellow.format()); - - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); - expect(totalDelegatedAfter).to.be.closeTo(delegatedMellow, transactErr); - expect(delegatedTo).to.be.closeTo(amount, transactErr); - expect(delegatedTo2).to.be.closeTo(0n, transactErr); - expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr); - expect(mellowBalance).to.be.gte(amount / 2n); - expect(mellowBalance2).to.be.eq(0n); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(e18, ratioErr); - }); - - it("Add new mellowVault", async function () { - await expect(mellowRestaker.addMellowVault(mellowVaults[1].vaultAddress, mellowVaults[1].wrapperAddress)) - .to.emit(mellowRestaker, "VaultAdded") - .withArgs(mellowVaults[1].vaultAddress, mellowVaults[1].wrapperAddress); - }); - - it("Delegate all to mellowVault#2", async function () { - const amount = await iVault.getFreeBalance(); - expect(amount).to.be.gt(0n); - const totalAssetsBefore = await iVault.totalAssets(); - - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[1].vaultAddress, amount, 1296000); - delegatedMellow += amount; - - const mellowBalance = await mellowVaults[0].vault.balanceOf(mellowRestaker.address); - const mellowBalance2 = await mellowVaults[1].vault.balanceOf(mellowRestaker.address); - const totalAssetsAfter = await iVault.totalAssets(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const delegatedTo2 = await iVault.getDelegatedTo(mellowVaults[1].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - console.log("Mellow LP token balance: ", mellowBalance.format()); - console.log("Mellow LP token balance2: ", mellowBalance2.format()); - console.log("Amount delegated: ", delegatedMellow.format()); - - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); - expect(totalDelegatedAfter).to.be.closeTo(delegatedMellow, transactErr * 2n); - expect(delegatedTo2).to.be.closeTo(amount, transactErr); - expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr * 2n); - expect(mellowBalance2).to.be.gte(amount / 2n); - expect(await calculateRatio(iVault, iToken)).to.be.closeTo(e18, ratioErr); - }); - - it("Update ratio", async function () { - const ratio = await calculateRatio(iVault, iToken); - console.log(`Calculated ratio:\t\t\t${ratio.format()}`); - await ratioFeed.updateRatioBatch([iToken.address], [ratio]); - console.log(`iVault ratio:\t\t\t\t${(await iVault.ratio()).format()}`); - expect(await iVault.ratio()).eq(ratio); - }); - - it("Add rewards to Mellow protocol and estimate ratio", async function () { - const ratioBefore = await calculateRatio(iVault, iToken); - const totalDelegatedToBefore = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); - console.log(`Delegated to before:\t${totalDelegatedToBefore.format()}`); - - await asset.connect(staker3).transfer(mellowVaults[0].vaultAddress, e18); - - const ratioAfter = await calculateRatio(iVault, iToken); - const totalDelegatedToAfter = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - rewardsMellow += totalDelegatedToAfter - totalDelegatedToBefore; - - console.log(`Ratio after:\t\t\t${ratioAfter.format()}`); - console.log(`Delegated to after:\t\t${totalDelegatedToAfter.format()}`); - console.log(`mellow rewards:\t\t\t${rewardsMellow.format()}`); - await ratioFeed.updateRatioBatch([iToken.address], [ratioAfter]); - expect(totalDelegatedAfter - totalDelegatedBefore).to.be.eq(totalDelegatedToAfter - totalDelegatedToBefore); - }); - - it("Estimate the amount that user can withdraw", async function () { - const shares = await iToken.balanceOf(staker.address); - const assetValue = await iVault.convertToAssets(shares); - expect(assetValue).closeTo(totalDeposited + rewardsMellow, transactErr * 10n); - }); - - it("User can withdraw all", async function () { - const shares = await iToken.balanceOf(staker.address); - const assetValue = await iVault.convertToAssets(shares); - console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); - console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); - const tx = await iVault.connect(staker).withdraw(shares, staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["owner"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.eq(assetValue); - expect(events[0].args["iShares"]).to.be.eq(shares); - - const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - const totalPW = await iVault.totalAmountToWithdraw(); - expect(stakerPW).to.be.eq(0n); - expect(staker2PW).to.be.closeTo(assetValue, transactErr); - expect(totalPW).to.be.closeTo(assetValue, transactErr); - }); - - it("Update ratio after all shares burn", async function () { - const calculatedRatio = await calculateRatio(iVault, iToken); - console.log(`Calculated ratio:\t\t\t${calculatedRatio.format()}`); - expect(calculatedRatio).to.be.eq(e18); //Because all shares have been burnt at this point - - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - console.log(`iVault ratio after:\t\t\t${(await iVault.ratio()).format()}`); - expect(await iVault.ratio()).eq(calculatedRatio); - }); - - it("Undelegate from Mellow", async function () { - const totalAssetsBefore = await iVault.totalAssets(); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); - console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); - - const amount = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const amount2 = await iVault.getDelegatedTo(mellowVaults[1].vaultAddress); - await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[0].vaultAddress, amount, 1296000); - await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[1].vaultAddress, amount2, 1296000); - - const totalAssetsAfter = await iVault.totalAssets(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedTo = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalDelegatedTo2 = await iVault.getDelegatedTo(mellowVaults[1].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const pendingWithdrawalsMellowAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending from Mellow:\t\t${pendingWithdrawalsMellowAfter.format()}`); - - expect(totalAssetsAfter).to.be.eq(totalAssetsBefore); //Nothing has come to the iVault yet - expect(totalDelegatedAfter).to.be.closeTo(0n, transactErr); - expect(totalDelegatedTo).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow - expect(totalDelegatedTo2).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr * 2n); //Total deposited amount did not change - expect(pendingWithdrawalsMellowAfter).to.be.closeTo(amount + amount2, transactErr * 2n); - }); - - it("Process request to transfers pending funds to mellowRestaker", async function () { - const totalDepositedBefore = await iVault.getTotalDeposited(); - const pendingWithdrawalsMellowBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const restakerBalanceBefore = await asset.balanceOf(mellowRestaker.address); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Pending from Mellow before:\t\t${pendingWithdrawalsMellowBefore.format()}`); - - await mellowVaults[0].curator.processWithdrawals([mellowRestaker.address]); - await mellowVaults[1].curator.processWithdrawals([mellowRestaker.address]); - - const totalDepositedAfter = await iVault.getTotalDeposited(); - const pendingWithdrawalsMellowAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - const restakerBalanceAfter = await asset.balanceOf(mellowRestaker.address); - console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); - console.log(`Pending from Mellow:\t\t\t${pendingWithdrawalsMellowAfter.format()}`); - console.log(`Restaker balance diff:\t\t\t${(restakerBalanceAfter - restakerBalanceBefore).format()}`); - - expect(restakerBalanceAfter - restakerBalanceBefore).to.be.eq(pendingWithdrawalsMellowBefore); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(pendingWithdrawalsMellowAfter).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); - }); - - it("Claim Mellow withdrawal transfer funds from restaker to vault", async function () { - const pendingWithdrawalsMellowBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalAssetsBefore = await iVault.totalAssets(); - const restakerBalanceBefore = await asset.balanceOf(mellowRestaker.address); - - await iVault.connect(iVaultOperator).claimCompletedWithdrawalsMellow(); - - const totalAssetsAfter = await iVault.totalAssets(); - const restakerBalanceAfter = await asset.balanceOf(mellowRestaker.address); - - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); - expect(restakerBalanceBefore - restakerBalanceAfter).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); - }); - - it("Staker is able to redeem", async function () { - const queuedPendingWithdrawal = (await iVault.claimerWithdrawalsQueue(0)).amount; - const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); - const redeemReserve = await iVault.redeemReservedAmount(); - const freeBalance = await iVault.getFreeBalance(); - - console.log("Queued withdrawal", queuedPendingWithdrawal.format()); - console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); - console.log("Redeem reserve", redeemReserve.format()); - console.log("Free balance", freeBalance.format()); - - //Compensate transactions loses - const diff = queuedPendingWithdrawal - freeBalance - redeemReserve; - if (diff > 0n) { - expect(diff).to.be.lte(transactErr * 2n); - await asset.connect(staker3).transfer(iVault.address, diff + 1n); - await iVault.connect(staker3).updateEpoch(); - } - - console.log("Redeem reserve after", await iVault.redeemReservedAmount()); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - }); - - it("Redeem withdraw", async function () { - const balanceBefore = await asset.balanceOf(staker2.address); - const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); - - const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Redeem"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); - - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - const balanceAfter = await asset.balanceOf(staker2.address); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); - console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); - - expect(staker2PWAfter).to.be.eq(0n); - expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); - expect(totalDepositedAfter).to.be.closeTo(0n, transactErr); - expect(totalAssetsAfter).to.be.closeTo(0n, transactErr); - }); - }); - - describe("Base flow with flash withdraw", function () { - let targetCapacity, deposited, freeBalance, depositFees; - before(async function () { - await snapshot.restore(); - targetCapacity = e18; - await iVault.setTargetFlashCapacity(targetCapacity); //1% - }); - - it("Initial ratio is 1e18", async function () { - const ratio = await iVault.ratio(); - console.log(`Current ratio is:\t\t\t\t${ratio.format()}`); - expect(ratio).to.be.eq(e18); - }); - - it("Initial delegation is 0", async function () { - expect(await iVault.getTotalDelegated()).to.be.eq(0n); - }); - - it("Deposit to Vault", async function () { - deposited = toWei(10); - freeBalance = (deposited * (MAX_TARGET_PERCENT - targetCapacity)) / MAX_TARGET_PERCENT; - const expectedShares = (deposited * e18) / (await iVault.ratio()); - const tx = await iVault.connect(staker).deposit(deposited, staker.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.closeTo(deposited, transactErr); - expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); - expect(receipt.logs.find(l => l.eventName === "DepositBonus")).to.be.undefined; - console.log(`Ratio after: ${await iVault.ratio()}`); - - expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); - expect(await iVault.totalAssets()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getFlashCapacity()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getFreeBalance()).to.be.closeTo(freeBalance, transactErr); - expect(await iVault.getTotalDeposited()).to.be.closeTo(deposited, transactErr); - expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet - expect(await iVault.ratio()).to.be.eq(e18); - }); - - it("Delegate freeBalance", async function () { - const totalDepositedBefore = await iVault.getTotalDeposited(); - const expectedFlashCapacity = (deposited * targetCapacity) / MAX_TARGET_PERCENT; - - const amount = await iVault.getFreeBalance(); - - await expect( - iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000), - ) - .to.emit(iVault, "DelegatedTo") - .withArgs(mellowRestaker.address, mellowVaults[0].vaultAddress, amount); - - const delegatedTotal = await iVault.getTotalDelegated(); - const delegatedTo = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - expect(totalDepositedBefore).to.be.closeTo(await iVault.getTotalDeposited(), transactErr); - expect(delegatedTotal).to.be.closeTo(amount, transactErr); - expect(delegatedTo).to.be.closeTo(amount, transactErr); - expect(await iVault.getFreeBalance()).to.be.closeTo(0n, transactErr); - expect(await iVault.getFlashCapacity()).to.be.closeTo(expectedFlashCapacity, transactErr); - expect(await iVault.ratio()).closeTo(e18, ratioErr); - }); - - it("Update asset ratio", async function () { - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - console.log(`New ratio is:\t\t\t\t\t${(await iVault.ratio()).format()}`); - expect(await iVault.ratio()).lt(e18); - }); - - it("Flash withdraw all capacity", async function () { - const sharesBefore = await iToken.balanceOf(staker); - const assetBalanceBefore = await asset.balanceOf(staker); - const treasuryBalanceBefore = await asset.balanceOf(treasury); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const flashCapacityBefore = await iVault.getFlashCapacity(); - const freeBalanceBefore = await iVault.getFreeBalance(); - console.log(`Flash capacity before:\t${flashCapacityBefore.format()}`); - console.log(`Free balance before:\t${freeBalanceBefore.format()}`); - - const amount = await iVault.getFlashCapacity(); - const shares = await iVault.convertToShares(amount); - const receiver = staker; - const expectedFee = await iVault.calculateFlashWithdrawFee(await iVault.convertToAssets(shares)); - console.log(`Amount:\t\t\t\t\t${amount.format()}`); - console.log(`Shares:\t\t\t\t\t${shares.format()}`); - console.log(`Expected fee:\t\t\t${expectedFee.format()}`); - - let tx = await iVault.connect(staker).flashWithdraw(shares, receiver.address); - const receipt = await tx.wait(); - const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); - expect(withdrawEvent.length).to.be.eq(1); - expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); - expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); - expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); - expect(withdrawEvent[0].args["fee"]).to.be.closeTo(expectedFee, transactErr); - const collectedFees = withdrawEvent[0].args["fee"]; - depositFees = collectedFees / 2n; - - const sharesAfter = await iToken.balanceOf(staker); - const assetBalanceAfter = await asset.balanceOf(staker); - const treasuryBalanceAfter = await asset.balanceOf(treasury); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - const depositBonus = await iVault.depositBonusAmount(); - console.log(`Shares balance diff:\t${(sharesBefore - sharesAfter).format()}`); - console.log(`Total deposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); - console.log(`Total assets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); - console.log(`Flash capacity diff:\t${(flashCapacityBefore - flashCapacityAfter).format()}`); - console.log(`Deposit bonus:\t\t\t${depositBonus.format()}`); - console.log(`Fee collected:\t\t\t${collectedFees.format()}`); - - expect(sharesBefore - sharesAfter).to.be.eq(shares); - expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); - expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); - expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); - expect(flashCapacityAfter).to.be.closeTo(0n, transactErr); - }); - - it("Withdraw all", async function () { - const ratioBefore = await iVault.ratio(); - const shares = await iToken.balanceOf(staker.address); - const assetValue = await iVault.convertToAssets(shares); - console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); - console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); - - const tx = await iVault.connect(staker).withdraw(shares, staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["owner"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.eq(assetValue); - expect(events[0].args["iShares"]).to.be.eq(shares); - - const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); - const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); - const totalPW = await iVault.totalAmountToWithdraw(); - expect(stakerPW).to.be.eq(0n); - expect(staker2PW).to.be.closeTo(assetValue, transactErr); - expect(totalPW).to.be.closeTo(assetValue, transactErr); - - console.log(`Total delegated:\t\t\t\t${(await iVault.getTotalDelegated()).format()}`); - console.log(`Total deposited:\t\t\t\t${(await iVault.getTotalDeposited()).format()}`); - expect(await iVault.ratio()).to.be.eq(ratioBefore); - }); - - it("Undelegate from Mellow", async function () { - const totalAssetsBefore = await iVault.totalAssets(); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalDelegatedBefore = await iVault.getTotalDelegated(); - console.log(`Total deposited before:\t\t${totalDepositedBefore.format()}`); - console.log(`Total delegated before:\t\t${totalDelegatedBefore.format()}`); - console.log(`Total assets before:\t\t${totalAssetsBefore.format()}`); - console.log("======================================================"); - - const amount = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[0].vaultAddress, amount, 1296000); - - const totalAssetsAfter = await iVault.totalAssets(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedTo = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const pendingWithdrawalsMellowAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending from Mellow:\t\t${pendingWithdrawalsMellowAfter.format()}`); - - expect(totalAssetsAfter).to.be.eq(totalAssetsBefore); //Nothing has come to the iVault yet - expect(totalDelegatedAfter).to.be.closeTo(0n, transactErr); - expect(totalDelegatedTo).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr * 2n); //Total deposited amount did not change - expect(pendingWithdrawalsMellowAfter).to.be.closeTo(amount, transactErr * 2n); - }); - - it("Process request to transfers pending funds to mellowRestaker", async function () { - const totalDepositedBefore = await iVault.getTotalDeposited(); - const pendingWithdrawalsMellowBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const restakerBalanceBefore = await asset.balanceOf(mellowRestaker.address); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Pending from Mellow before:\t\t${pendingWithdrawalsMellowBefore.format()}`); - - await mellowVaults[0].curator.processWithdrawals([mellowRestaker.address]); - await mellowVaults[1].curator.processWithdrawals([mellowRestaker.address]); - - const totalDepositedAfter = await iVault.getTotalDeposited(); - const pendingWithdrawalsMellowAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - const restakerBalanceAfter = await asset.balanceOf(mellowRestaker.address); - console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); - console.log(`Pending from Mellow:\t\t\t${pendingWithdrawalsMellowAfter.format()}`); - console.log(`Restaker balance diff:\t\t\t${(restakerBalanceAfter - restakerBalanceBefore).format()}`); - - expect(restakerBalanceAfter - restakerBalanceBefore).to.be.eq(pendingWithdrawalsMellowBefore); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(pendingWithdrawalsMellowAfter).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); - }); - - it("Claim Mellow withdrawal transfer funds from restaker to vault", async function () { - const pendingWithdrawalsMellowBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalAssetsBefore = await iVault.totalAssets(); - const restakerBalanceBefore = await asset.balanceOf(mellowRestaker.address); - - await iVault.connect(iVaultOperator).claimCompletedWithdrawalsMellow(); - - const totalAssetsAfter = await iVault.totalAssets(); - const restakerBalanceAfter = await asset.balanceOf(mellowRestaker.address); - - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); - expect(restakerBalanceBefore - restakerBalanceAfter).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); - }); - - it("Staker is able to redeem", async function () { - const queuedPendingWithdrawal = (await iVault.claimerWithdrawalsQueue(0)).amount; - const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); - const redeemReserve = await iVault.redeemReservedAmount(); - const freeBalance = await iVault.getFreeBalance(); - - console.log("Queued withdrawal", queuedPendingWithdrawal.format()); - console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); - console.log("Redeem reserve", redeemReserve.format()); - console.log("Free balance", freeBalance.format()); - - const diff = queuedPendingWithdrawal - freeBalance - redeemReserve; - if (diff > 0n) { - expect(diff).to.be.lte(transactErr * 2n); - await asset.connect(staker3).transfer(iVault.address, diff + 1n); - await iVault.connect(staker3).updateEpoch(); - } - - console.log("Redeem reserve after", await iVault.redeemReservedAmount()); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - }); - - it("Redeem withdraw", async function () { - const balanceBefore = await asset.balanceOf(staker2.address); - const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); - - const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Redeem"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); - expect(events[0].args["receiver"]).to.be.eq(staker2.address); - expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); - - const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); - const balanceAfter = await asset.balanceOf(staker2.address); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - - console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); - console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); - console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); - console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); - - expect(staker2PWAfter).to.be.eq(0n); - expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); - expect(totalDepositedAfter).to.be.closeTo(0n, transactErr * 3n); - expect(totalAssetsAfter).to.be.closeTo(depositFees, transactErr * 3n); - }); - }); - - describe("iVault getters and setters", function () { - beforeEach(async function () { - await snapshot.restore(); - }); - - it("Assset", async function () { - expect(await iVault.asset()).to.be.eq(asset.address); - }); - - it("Default epoch", async function () { - expect(await iVault.epoch()).to.be.eq(0n); - }); - - it("setTreasuryAddress(): only owner can", async function () { - const treasury = await iVault.treasury(); - const newTreasury = ethers.Wallet.createRandom().address; - - await expect(iVault.setTreasuryAddress(newTreasury)) - .to.emit(iVault, "TreasuryChanged") - .withArgs(treasury, newTreasury); - expect(await iVault.treasury()).to.be.eq(newTreasury); - }); - - it("setTreasuryAddress(): reverts when set to zero address", async function () { - await expect(iVault.setTreasuryAddress(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); - }); - - it("setTreasuryAddress(): reverts when caller is not an operator", async function () { - await expect(iVault.connect(staker).setTreasuryAddress(staker2.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setOperator(): only owner can", async function () { - const newOperator = staker2; - await expect(iVault.setOperator(newOperator.address)) - .to.emit(iVault, "OperatorChanged") - .withArgs(iVaultOperator.address, newOperator); - - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(toWei(2), staker.address); - const amount = await iVault.getFreeBalance(); - await iVault.connect(newOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000); - }); - - it("setOperator(): reverts when set to zero address", async function () { - await expect(iVault.setOperator(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); - }); - - it("setOperator(): reverts when caller is not an operator", async function () { - await expect(iVault.connect(staker).setOperator(staker2.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setRatioFeed(): only owner can", async function () { - const ratioFeed = await iVault.ratioFeed(); - const newRatioFeed = ethers.Wallet.createRandom().address; - await expect(iVault.setRatioFeed(newRatioFeed)) - .to.emit(iVault, "RatioFeedChanged") - .withArgs(ratioFeed, newRatioFeed); - expect(await iVault.ratioFeed()).to.be.eq(newRatioFeed); - }); - - it("setRatioFeed(): reverts when new value is zero address", async function () { - await expect(iVault.setRatioFeed(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); - }); - - it("setRatioFeed(): reverts when caller is not an owner", async function () { - const newRatioFeed = ethers.Wallet.createRandom().address; - await expect(iVault.connect(staker).setRatioFeed(newRatioFeed)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setWithdrawMinAmount(): only owner can", async function () { - const prevValue = await iVault.withdrawMinAmount(); - const newMinAmount = randomBI(3); - await expect(iVault.setWithdrawMinAmount(newMinAmount)) - .to.emit(iVault, "WithdrawMinAmountChanged") - .withArgs(prevValue, newMinAmount); - expect(await iVault.withdrawMinAmount()).to.be.eq(newMinAmount); - }); - - it("setWithdrawMinAmount(): another address can not", async function () { - await expect(iVault.connect(staker).setWithdrawMinAmount(randomBI(3))).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setName(): only owner can", async function () { - const prevValue = await iVault.name(); - const newValue = "New name"; - await expect(iVault.setName(newValue)).to.emit(iVault, "NameChanged").withArgs(prevValue, newValue); - expect(await iVault.name()).to.be.eq(newValue); - }); - - it("setName(): reverts when name is blank", async function () { - await expect(iVault.setName("")).to.be.revertedWithCustomError(iVault, "NullParams"); - }); - - it("setName(): another address can not", async function () { - await expect(iVault.connect(staker).setName("New name")).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("updateEpoch(): reverts when iVault is paused", async function () { - await iVault.pause(); - await expect(iVault.connect(iVaultOperator).updateEpoch()).to.be.revertedWith("Pausable: paused"); - }); - - it("pause(): only owner can", async function () { - expect(await iVault.paused()).is.false; - await iVault.pause(); - expect(await iVault.paused()).is.true; - }); - - it("pause(): another address can not", async function () { - await expect(iVault.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("pause(): reverts when already paused", async function () { - await iVault.pause(); - await expect(iVault.pause()).to.be.revertedWith("Pausable: paused"); - }); - - it("unpause(): only owner can", async function () { - await iVault.pause(); - expect(await iVault.paused()).is.true; - - await iVault.unpause(); - expect(await iVault.paused()).is.false; - }); - - it("unpause(): another address can not", async function () { - await iVault.pause(); - expect(await iVault.paused()).is.true; - await expect(iVault.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("setTargetFlashCapacity(): only owner can", async function () { - const prevValue = await iVault.targetCapacity(); - const newValue = randomBI(18); - await expect(iVault.connect(deployer).setTargetFlashCapacity(newValue)) - .to.emit(iVault, "TargetCapacityChanged") - .withArgs(prevValue, newValue); - expect(await iVault.targetCapacity()).to.be.eq(newValue); - }); - - it("setTargetFlashCapacity(): reverts when caller is not an owner", async function () { - const newValue = randomBI(18); - await expect(iVault.connect(staker).setTargetFlashCapacity(newValue)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setTargetFlashCapacity(): reverts when set to 0", async function () { - await expect(iVault.connect(deployer).setTargetFlashCapacity(0n)).to.revertedWithCustomError( - iVault, - "InvalidTargetFlashCapacity", - ); - }); - - it("setProtocolFee(): sets share of flashWithdrawFee that goes to treasury", async function () { - const prevValue = await iVault.protocolFee(); - const newValue = randomBI(10); - - await expect(iVault.setProtocolFee(newValue)) - .to.emit(iVault, "ProtocolFeeChanged") - .withArgs(prevValue, newValue); - expect(await iVault.protocolFee()).to.be.eq(newValue); - }); - - it("setProtocolFee(): reverts when > MAX_PERCENT", async function () { - const newValue = (await iVault.MAX_PERCENT()) + 1n; - await expect(iVault.setProtocolFee(newValue)) - .to.be.revertedWithCustomError(iVault, "ParameterExceedsLimits") - .withArgs(newValue); - }); - - it("setProtocolFee(): reverts when caller is not an owner", async function () { - const newValue = randomBI(10); - await expect(iVault.connect(staker).setProtocolFee(newValue)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - }); - - describe("Mellow restaker getters and setters", function () { - beforeEach(async function () { - await snapshot.restore(); - }); - - it("delegateMellow reverts when called by not a trustee", async function () { - await asset.connect(staker).approve(mellowRestaker.address, e18); - - let time = await helpers.time.latest(); - await expect( - mellowRestaker.connect(staker).delegateMellow(randomBI(9), time + 1, mellowVaults[0].vaultAddress), - ).to.revertedWithCustomError(mellowRestaker, "NotVaultOrTrusteeManager"); - }); - - it("delegateMellow reverts when called by not a trustee", async function () { - await asset.connect(staker).approve(mellowRestaker.address, e18); - - let time = await helpers.time.latest(); - await expect( - mellowRestaker.connect(staker).delegateMellow(randomBI(9), time + 1, mellowVaults[0].vaultAddress), - ).to.revertedWithCustomError(mellowRestaker, "NotVaultOrTrusteeManager"); - }); - - it("delegate reverts when called by not a trustee", async function () { - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(e18, staker.address); - await mellowRestaker.changeAllocation(mellowVaults[0].vaultAddress, 1n); - - let time = await helpers.time.latest(); - await expect( - mellowRestaker.connect(staker).delegate(await iVault.getFreeBalance(), time + 1000), - ).to.revertedWithCustomError(mellowRestaker, "NotVaultOrTrusteeManager"); - }); - - it("withdrawMellow reverts when called by not a trustee", async function () { - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(randomBI(19), staker.address); - const delegated = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, delegated, 1296000); - - await expect( - mellowRestaker.connect(staker).withdrawMellow(mellowVaults[0].vaultAddress, delegated, 1296000, true), - ).to.revertedWithCustomError(mellowRestaker, "NotVaultOrTrusteeManager"); - }); - - it("claimMellowWithdrawalCallback reverts when called by not a trustee", async function () { - await asset.connect(staker).transfer(mellowRestaker.address, e18); - - await expect(mellowRestaker.connect(staker).claimMellowWithdrawalCallback()).to.revertedWithCustomError( - mellowRestaker, - "NotVaultOrTrusteeManager", - ); - }); - - it("getVersion", async function () { - expect(await mellowRestaker.getVersion()).to.be.eq(1n); - }); - - it("setVault(): only owner can", async function () { - const prevValue = iVault.address; - const newValue = staker.address; - - await expect(mellowRestaker.setVault(newValue)) - .to.emit(mellowRestaker, "VaultSet") - .withArgs(prevValue, newValue); - - await asset.connect(staker).approve(mellowRestaker.address, e18); - let time = await helpers.time.latest(); - await mellowRestaker.connect(staker).delegateMellow(randomBI(9), time + 1, mellowVaults[0].vaultAddress); - }); - - it("setVault(): reverts when caller is not an owner", async function () { - await expect(mellowRestaker.connect(staker).setVault(staker.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setRequestDeadline(): only owner can", async function () { - const prevValue = await mellowRestaker.requestDeadline(); - const newValue = randomBI(2); - - await expect(mellowRestaker.setRequestDeadline(newValue)) - .to.emit(mellowRestaker, "RequestDealineSet") - .withArgs(prevValue, newValue * day); - - expect(await mellowRestaker.requestDeadline()).to.be.eq(newValue * day); - }); - - it("setRequestDeadline(): reverts when caller is not an owner", async function () { - const newValue = randomBI(2); - await expect(mellowRestaker.connect(staker).setRequestDeadline(newValue)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setSlippages(): only owner can", async function () { - const depositSlippage = randomBI(3); - const withdrawSlippage = randomBI(3); - - await expect(mellowRestaker.setSlippages(depositSlippage, withdrawSlippage)) - .to.emit(mellowRestaker, "NewSlippages") - .withArgs(depositSlippage, withdrawSlippage); - - expect(await mellowRestaker.depositSlippage()).to.be.eq(depositSlippage); - expect(await mellowRestaker.withdrawSlippage()).to.be.eq(withdrawSlippage); - }); - - it("setSlippages(): reverts when depositSlippage > 30%", async function () { - const depositSlippage = 3001; - const withdrawSlippage = randomBI(3); - await expect(mellowRestaker.setSlippages(depositSlippage, withdrawSlippage)).to.be.revertedWithCustomError( - mellowRestaker, - "TooMuchSlippage", - ); - }); - - it("setSlippages(): reverts when withdrawSlippage > 30%", async function () { - const depositSlippage = randomBI(3); - const withdrawSlippage = 3001; - await expect(mellowRestaker.setSlippages(depositSlippage, withdrawSlippage)).to.be.revertedWithCustomError( - mellowRestaker, - "TooMuchSlippage", - ); - }); - - it("setSlippages(): reverts when caller is not an owner", async function () { - const depositSlippage = randomBI(3); - const withdrawSlippage = randomBI(3); - await expect(mellowRestaker.connect(staker).setSlippages(depositSlippage, withdrawSlippage)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("setTrusteeManager(): only owner can", async function () { - const prevValue = iVaultOperator.address; - const newValue = staker.address; - - await expect(mellowRestaker.setTrusteeManager(newValue)) - .to.emit(mellowRestaker, "TrusteeManagerSet") - .withArgs(prevValue, newValue); - - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(randomBI(19), staker.address); - const delegated = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, delegated, 1296000); - - await mellowRestaker.connect(staker).withdrawMellow(mellowVaults[0].vaultAddress, delegated, 1296000, true); - }); - - it("setTrusteeManager(): reverts when caller is not an owner", async function () { - await expect(mellowRestaker.connect(staker).setTrusteeManager(staker.address)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("pause(): reverts when caller is not an owner", async function () { - await expect(mellowRestaker.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - - it("unpause(): reverts when caller is not an owner", async function () { - await mellowRestaker.pause(); - await expect(mellowRestaker.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - - describe("Deposit bonus params setter and calculation", function () { - let targetCapacityPercent, MAX_PERCENT, localSnapshot; - before(async function () { - await iVault.setTargetFlashCapacity(1n); - MAX_PERCENT = await iVault.MAX_PERCENT(); - }); - - const depositBonusSegment = [ - { - fromUtilization: async () => 0n, - fromPercent: async () => await iVault.maxBonusRate(), - toUtilization: async () => await iVault.depositUtilizationKink(), - toPercent: async () => await iVault.optimalBonusRate(), - }, - { - fromUtilization: async () => await iVault.depositUtilizationKink(), - fromPercent: async () => await iVault.optimalBonusRate(), - toUtilization: async () => await iVault.MAX_PERCENT(), - toPercent: async () => await iVault.optimalBonusRate(), - }, - { - fromUtilization: async () => await iVault.MAX_PERCENT(), - fromPercent: async () => 0n, - toUtilization: async () => ethers.MaxUint256, - toPercent: async () => 0n, - }, - ]; - - const args = [ - { - name: "Normal bonus rewards profile > 0", - newMaxBonusRate: BigInt(2 * 10 ** 8), //2% - newOptimalBonusRate: BigInt(0.2 * 10 ** 8), //0.2% - newDepositUtilizationKink: BigInt(25 * 10 ** 8), //25% - }, - { - name: "Optimal utilization = 0 => always optimal rate", - newMaxBonusRate: BigInt(2 * 10 ** 8), - newOptimalBonusRate: BigInt(10 ** 8), //1% - newDepositUtilizationKink: 0n, - }, - { - name: "Optimal bonus rate = 0", - newMaxBonusRate: BigInt(2 * 10 ** 8), - newOptimalBonusRate: 0n, - newDepositUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal bonus rate = max > 0 => rate is constant over utilization", - newMaxBonusRate: BigInt(2 * 10 ** 8), - newOptimalBonusRate: BigInt(2 * 10 ** 8), - newDepositUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal bonus rate = max = 0 => no bonus", - newMaxBonusRate: 0n, - newOptimalBonusRate: 0n, - newDepositUtilizationKink: BigInt(25 * 10 ** 8), - }, - //Will fail when OptimalBonusRate > MaxBonusRate - ]; - - const amounts = [ - { - name: "min amount from 0", - flashCapacity: targetCapacity => 0n, - amount: async () => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, - }, - { - name: "1 wei from 0", - flashCapacity: targetCapacity => 0n, - amount: async () => 1n, - }, - { - name: "from 0 to 25% of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => (targetCapacityPercent * 25n) / 100n, - }, - { - name: "from 0 to 25% + 1wei of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => (targetCapacityPercent * 25n) / 100n, - }, - { - name: "from 25% to 100% of TARGET", - flashCapacity: targetCapacity => (targetCapacity * 25n) / 100n, - amount: async () => (targetCapacityPercent * 75n) / 100n, - }, - { - name: "from 0% to 100% of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => targetCapacityPercent, - }, - { - name: "from 0% to 200% of TARGET", - flashCapacity: targetCapacity => 0n, - amount: async () => targetCapacityPercent * 2n, - }, - ]; - - args.forEach(function (arg) { - it(`setDepositBonusParams: ${arg.name}`, async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await expect( - iVault.setDepositBonusParams(arg.newMaxBonusRate, arg.newOptimalBonusRate, arg.newDepositUtilizationKink), - ) - .to.emit(iVault, "DepositBonusParamsChanged") - .withArgs(arg.newMaxBonusRate, arg.newOptimalBonusRate, arg.newDepositUtilizationKink); - expect(await iVault.maxBonusRate()).to.be.eq(arg.newMaxBonusRate); - expect(await iVault.optimalBonusRate()).to.be.eq(arg.newOptimalBonusRate); - expect(await iVault.depositUtilizationKink()).to.be.eq(arg.newDepositUtilizationKink); - localSnapshot = await helpers.takeSnapshot(); - }); - - amounts.forEach(function (amount) { - it(`calculateDepositBonus for ${amount.name}`, async function () { - await localSnapshot.restore(); - const deposited = toWei(100); - targetCapacityPercent = e18; - const targetCapacity = (deposited * targetCapacityPercent) / MAX_TARGET_PERCENT; - await iVault.connect(staker).deposit(deposited, staker.address); - let flashCapacity = amount.flashCapacity(targetCapacity); - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, deposited - flashCapacity - 1n, 1296000); - await iVault.setTargetFlashCapacity(targetCapacityPercent); //1% - console.log(`Flash capacity:\t\t${await iVault.getFlashCapacity()}`); - - let _amount = await amount.amount(); - let depositBonus = 0n; - while (_amount > 0n) { - for (const feeFunc of depositBonusSegment) { - const utilization = (flashCapacity * MAX_PERCENT) / targetCapacity; - const fromUtilization = await feeFunc.fromUtilization(); - const toUtilization = await feeFunc.toUtilization(); - if (_amount > 0n && fromUtilization <= utilization && utilization < toUtilization) { - const fromPercent = await feeFunc.fromPercent(); - const toPercent = await feeFunc.toPercent(); - const upperBound = (toUtilization * targetCapacityPercent) / MAX_PERCENT; - const replenished = upperBound > flashCapacity + _amount ? _amount : upperBound - flashCapacity; - const slope = ((toPercent - fromPercent) * MAX_PERCENT) / (toUtilization - fromUtilization); - const bonusPercent = - fromPercent + (slope * (flashCapacity + replenished / 2n)) / targetCapacityPercent; - const bonus = (replenished * bonusPercent) / MAX_PERCENT; - console.log(`Replenished:\t\t\t${replenished.format()}`); - console.log(`Bonus percent:\t\t\t${bonusPercent.format()}`); - console.log(`Bonus:\t\t\t\t\t${bonus.format()}`); - flashCapacity += replenished; - _amount -= replenished; - depositBonus += bonus; - } - } - } - let contractBonus = await iVault.calculateDepositBonus(await amount.amount()); - console.log(`Expected deposit bonus:\t${depositBonus.format()}`); - console.log(`Contract deposit bonus:\t${contractBonus.format()}`); - expect(contractBonus).to.be.closeTo(depositBonus, 1n); - }); - }); - }); - - const invalidArgs = [ - { - name: "MaxBonusRate > MAX_PERCENT", - newMaxBonusRate: () => MAX_PERCENT + 1n, - newOptimalBonusRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newDepositUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "OptimalBonusRate > MAX_PERCENT", - newMaxBonusRate: () => BigInt(2 * 10 ** 8), - newOptimalBonusRate: () => MAX_PERCENT + 1n, - newDepositUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "DepositUtilizationKink > MAX_PERCENT", - newMaxBonusRate: () => BigInt(2 * 10 ** 8), - newOptimalBonusRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newDepositUtilizationKink: () => MAX_PERCENT + 1n, - customError: "ParameterExceedsLimits", - }, - ]; - invalidArgs.forEach(function (arg) { - it(`setDepositBonusParams reverts when ${arg.name}`, async function () { - await expect( - iVault.setDepositBonusParams( - arg.newMaxBonusRate(), - arg.newOptimalBonusRate(), - arg.newDepositUtilizationKink(), - ), - ).to.be.revertedWithCustomError(iVault, arg.customError); - }); - }); - - it("setDepositBonusParams reverts when caller is not an owner", async function () { - await expect( - iVault - .connect(staker) - .setDepositBonusParams(BigInt(2 * 10 ** 8), BigInt(0.2 * 10 ** 8), BigInt(25 * 10 ** 8)), - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - - describe("Withdraw fee params setter and calculation", function () { - let targetCapacityPercent, MAX_PERCENT, localSnapshot; - before(async function () { - MAX_PERCENT = await iVault.MAX_PERCENT(); - }); - - const withdrawFeeSegment = [ - { - fromUtilization: async () => 0n, - fromPercent: async () => await iVault.maxFlashFeeRate(), - toUtilization: async () => await iVault.withdrawUtilizationKink(), - toPercent: async () => await iVault.optimalWithdrawalRate(), - }, - { - fromUtilization: async () => await iVault.withdrawUtilizationKink(), - fromPercent: async () => await iVault.optimalWithdrawalRate(), - toUtilization: async () => ethers.MaxUint256, - toPercent: async () => await iVault.optimalWithdrawalRate(), - }, - ]; - - const args = [ - { - name: "Normal withdraw fee profile > 0", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), //2% - newOptimalWithdrawalRate: BigInt(0.2 * 10 ** 8), //0.2% - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal utilization = 0 => always optimal rate", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: BigInt(10 ** 8), //1% - newWithdrawUtilizationKink: 0n, - }, - { - name: "Optimal withdraw rate = 0", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: 0n, - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal withdraw rate = max > 0 => rate is constant over utilization", - newMaxFlashFeeRate: BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: BigInt(2 * 10 ** 8), - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - { - name: "Optimal withdraw rate = max = 0 => no fee", - newMaxFlashFeeRate: 0n, - newOptimalWithdrawalRate: 0n, - newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), - }, - //Will fail when optimalWithdrawalRate > MaxFlashFeeRate - ]; - - const amounts = [ - { - name: "from 200% to 0% of TARGET", - flashCapacity: targetCapacity => targetCapacity * 2n, - amount: async () => await iVault.getFlashCapacity(), - }, - { - name: "from 100% to 0% of TARGET", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => await iVault.getFlashCapacity(), - }, - { - name: "1 wei from 100%", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => 1n, - }, - { - name: "min amount from 100%", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, - }, - { - name: "from 100% to 25% of TARGET", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => (targetCapacityPercent * 75n) / 100n, - }, - { - name: "from 100% to 25% - 1wei of TARGET", - flashCapacity: targetCapacity => targetCapacity, - amount: async () => (targetCapacityPercent * 75n) / 100n + 1n, - }, - { - name: "from 25% to 0% of TARGET", - flashCapacity: targetCapacity => (targetCapacity * 25n) / 100n, - amount: async () => await iVault.getFlashCapacity(), - }, - ]; - - args.forEach(function (arg) { - it(`setFlashWithdrawFeeParams: ${arg.name}`, async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await expect( - iVault.setFlashWithdrawFeeParams( - arg.newMaxFlashFeeRate, - arg.newOptimalWithdrawalRate, - arg.newWithdrawUtilizationKink, - ), - ) - .to.emit(iVault, "WithdrawFeeParamsChanged") - .withArgs(arg.newMaxFlashFeeRate, arg.newOptimalWithdrawalRate, arg.newWithdrawUtilizationKink); - - expect(await iVault.maxFlashFeeRate()).to.be.eq(arg.newMaxFlashFeeRate); - expect(await iVault.optimalWithdrawalRate()).to.be.eq(arg.newOptimalWithdrawalRate); - expect(await iVault.withdrawUtilizationKink()).to.be.eq(arg.newWithdrawUtilizationKink); - localSnapshot = await helpers.takeSnapshot(); - }); - - amounts.forEach(function (amount) { - it(`calculateFlashWithdrawFee for: ${amount.name}`, async function () { - await localSnapshot.restore(); - const deposited = toWei(100); - targetCapacityPercent = e18; - const targetCapacity = (deposited * targetCapacityPercent) / MAX_TARGET_PERCENT; - await iVault.connect(staker).deposit(deposited, staker.address); - let flashCapacity = amount.flashCapacity(targetCapacity); - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, deposited - flashCapacity - 1n, 1296000); - await iVault.setTargetFlashCapacity(targetCapacityPercent); //1% - console.log(`Flash capacity:\t\t\t${await iVault.getFlashCapacity()}`); - - let _amount = await amount.amount(); - let withdrawFee = 0n; - while (_amount > 1n) { - for (const feeFunc of withdrawFeeSegment) { - const utilization = (flashCapacity * MAX_PERCENT) / targetCapacity; - const fromUtilization = await feeFunc.fromUtilization(); - const toUtilization = await feeFunc.toUtilization(); - if (_amount > 0n && fromUtilization < utilization && utilization <= toUtilization) { - console.log(`Utilization:\t\t\t${utilization.format()}`); - const fromPercent = await feeFunc.fromPercent(); - const toPercent = await feeFunc.toPercent(); - const lowerBound = (fromUtilization * targetCapacityPercent) / MAX_PERCENT; - const replenished = lowerBound > flashCapacity - _amount ? flashCapacity - lowerBound : _amount; - const slope = ((toPercent - fromPercent) * MAX_PERCENT) / (toUtilization - fromUtilization); - const withdrawFeePercent = - fromPercent + (slope * (flashCapacity - replenished / 2n)) / targetCapacityPercent; - const fee = (replenished * withdrawFeePercent) / MAX_PERCENT; - console.log(`Replenished:\t\t\t${replenished.format()}`); - console.log(`Fee percent:\t\t\t${withdrawFeePercent.format()}`); - console.log(`Fee:\t\t\t\t\t${fee.format()}`); - flashCapacity -= replenished; - _amount -= replenished; - withdrawFee += fee; - } - } - } - let contractFee = await iVault.calculateFlashWithdrawFee(await amount.amount()); - console.log(`Expected withdraw fee:\t${withdrawFee.format()}`); - console.log(`Contract withdraw fee:\t${contractFee.format()}`); - expect(contractFee).to.be.closeTo(withdrawFee, 1n); - expect(contractFee).to.be.gt(0n); //flashWithdraw fee is always greater than 0 - }); - }); - }); - - const invalidArgs = [ - { - name: "MaxBonusRate > MAX_PERCENT", - newMaxFlashFeeRate: () => MAX_PERCENT + 1n, - newOptimalWithdrawalRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newWithdrawUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "OptimalBonusRate > MAX_PERCENT", - newMaxFlashFeeRate: () => BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: () => MAX_PERCENT + 1n, - newWithdrawUtilizationKink: () => BigInt(25 * 10 ** 8), - customError: "ParameterExceedsLimits", - }, - { - name: "DepositUtilizationKink > MAX_PERCENT", - newMaxFlashFeeRate: () => BigInt(2 * 10 ** 8), - newOptimalWithdrawalRate: () => BigInt(0.2 * 10 ** 8), //0.2% - newWithdrawUtilizationKink: () => MAX_PERCENT + 1n, - customError: "ParameterExceedsLimits", - }, - ]; - invalidArgs.forEach(function (arg) { - it(`setFlashWithdrawFeeParams reverts when ${arg.name}`, async function () { - await expect( - iVault.setFlashWithdrawFeeParams( - arg.newMaxFlashFeeRate(), - arg.newOptimalWithdrawalRate(), - arg.newWithdrawUtilizationKink(), - ), - ).to.be.revertedWithCustomError(iVault, arg.customError); - }); - }); - - it("calculateFlashWithdrawFee reverts when capacity is not sufficient", async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker, staker).deposit(randomBI(19), staker.address); - const capacity = await iVault.getFlashCapacity(); - await expect(iVault.calculateFlashWithdrawFee(capacity + 1n)) - .to.be.revertedWithCustomError(iVault, "InsufficientCapacity") - .withArgs(capacity); - }); - - it("setFlashWithdrawFeeParams reverts when caller is not an owner", async function () { - await expect( - iVault - .connect(staker) - .setFlashWithdrawFeeParams(BigInt(2 * 10 ** 8), BigInt(0.2 * 10 ** 8), BigInt(25 * 10 ** 8)), - ).to.be.revertedWith("Ownable: caller is not the owner"); - }); - }); - - describe("Deposit: user can restake asset", function () { - let ratio; - - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker3).deposit(e18, staker3.address); - const amount = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000); - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - ratio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [ratio]); - console.log(`Initial ratio: ${ratio.format()}`); - }); - - afterEach(async function () { - if (await iVault.paused()) { - await iVault.unpause(); - } - }); - - it("maxDeposit: returns max amount that can be delegated to strategy", async function () { - expect(await iVault.maxDeposit(staker.address)).to.be.gt(0n); - }); - - const args = [ - { - amount: async () => 4798072939323319141n, - receiver: () => staker.address, - }, - { - amount: async () => 999999999999999999n, - receiver: () => ethers.Wallet.createRandom().address, - }, - { - amount: async () => 888888888888888888n, - receiver: () => staker.address, - }, - { - amount: async () => 777777777777777777n, - receiver: () => staker.address, - }, - { - amount: async () => 666666666666666666n, - receiver: () => staker.address, - }, - { - amount: async () => 555555555555555555n, - receiver: () => staker.address, - }, - { - amount: async () => 444444444444444444n, - receiver: () => staker.address, - }, - { - amount: async () => 333333333333333333n, - receiver: () => staker.address, - }, - { - amount: async () => 222222222222222222n, - receiver: () => staker.address, - }, - { - amount: async () => 111111111111111111n, - receiver: () => staker.address, - }, - { - amount: async () => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, - receiver: () => staker.address, - }, - ]; - - args.forEach(function (arg) { - it(`Deposit amount ${arg.amount}`, async function () { - const receiver = arg.receiver(); - const balanceBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - - const amount = await arg.amount(); - const convertedShares = await iVault.convertToShares(amount); - const expectedShares = (amount * (await iVault.ratio())) / e18; - - const tx = await iVault.connect(staker).deposit(amount, receiver); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(receiver); - expect(events[0].args["amount"]).to.be.closeTo(amount, transactErr); - expect(events[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); - - const balanceAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after: ${ratioAfter}`); - - expect(balanceAfter - balanceBefore).to.be.closeTo(expectedShares, transactErr); - expect(balanceAfter - balanceBefore).to.be.closeTo(convertedShares, transactErr); - - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit - expect(ratioAfter).to.be.closeTo(ratio, ratioErr); //Ratio stays the same - }); - - it(`Mint amount ${arg.amount}`, async function () { - const receiver = arg.receiver(); - const balanceBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - - const shares = await arg.amount(); - const convertedAmount = await iVault.convertToAssets(shares); - - const tx = await iVault.connect(staker).mint(shares, receiver); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(receiver); - expect(events[0].args["amount"]).to.be.closeTo(convertedAmount, transactErr); - expect(events[0].args["iShares"]).to.be.closeTo(shares, transactErr); - - const balanceAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after: ${ratioAfter}`); - - expect(balanceAfter - balanceBefore).to.be.closeTo(shares, transactErr); - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(convertedAmount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(convertedAmount, transactErr); //Everything stays on iVault after deposit - expect(ratioAfter).to.be.closeTo(ratio, ratioErr); //Ratio stays the same - }); - - it("Delegate free balance", async function () { - const delegatedBefore = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalDepositedBefore = await iVault.getTotalDeposited(); - console.log(`Delegated before: ${delegatedBefore}`); - console.log(`Total deposited before: ${totalDepositedBefore}`); - - const amount = await iVault.getFreeBalance(); - await expect( - iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000), - ) - .to.emit(iVault, "DelegatedTo") - .withArgs(mellowRestaker.address, mellowVaults[0].vaultAddress, amount); - - const delegatedAfter = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after: ${ratioAfter}`); - - expect(delegatedAfter - delegatedBefore).to.be.closeTo(amount, transactErr); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(totalAssetsAfter).to.be.lte(transactErr); - }); - }); - - it("Deposit with Referral code", async function () { - const receiver = staker; - const balanceBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const amount = await toWei(1); - const convertedShares = await iVault.convertToShares(amount); - const expectedShares = (amount * (await iVault.ratio())) / e18; - const code = ethers.encodeBytes32String(randomAddress().slice(0, 8)); - const tx = await iVault.connect(staker2).depositWithReferral(amount, receiver, code); - const receipt = await tx.wait(); - let events = receipt.logs?.filter(e => { - return e.eventName === "Deposit"; - }); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker2.address); - expect(events[0].args["receiver"]).to.be.eq(receiver); - expect(events[0].args["amount"]).to.be.closeTo(amount, transactErr); - expect(events[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); - //Code event - events = receipt.logs?.filter(e => { - return e.eventName === "ReferralCode"; - }); - expect(events.length).to.be.eq(1); - expect(events[0].args["code"]).to.be.eq(code); - - const balanceAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - - expect(balanceAfter - balanceBefore).to.be.closeTo(expectedShares, transactErr); - expect(balanceAfter - balanceBefore).to.be.closeTo(convertedShares, transactErr); - - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit - expect(await iVault.ratio()).to.be.closeTo(ratio, ratioErr); //Ratio stays the same - }); - - const depositInvalidArgs = [ - { - name: "amount is 0", - amount: async () => 0n, - receiver: () => staker.address, - isCustom: true, - error: "LowerMinAmount", - }, - { - name: "amount < min", - amount: async () => (await iVault.withdrawMinAmount()) - 1n, - receiver: () => staker.address, - isCustom: true, - error: "LowerMinAmount", - }, - { - name: "to zero address", - amount: async () => randomBI(18), - isCustom: true, - receiver: () => ethers.ZeroAddress, - error: "NullParams", - }, - ]; - - depositInvalidArgs.forEach(function (arg) { - it(`Reverts when: deposit ${arg.name}`, async function () { - const amount = await arg.amount(); - const receiver = arg.receiver(); - if (arg.isCustom) { - await expect(iVault.connect(staker).deposit(amount, receiver)).to.be.revertedWithCustomError( - iVault, - arg.error, - ); - } else { - await expect(iVault.connect(staker).deposit(amount, receiver)).to.be.revertedWith(arg.error); - } - }); - }); - - it("Reverts: deposit when iVault is paused", async function () { - await iVault.pause(); - const depositAmount = randomBI(19); - await expect(iVault.connect(staker).deposit(depositAmount, staker.address)).to.be.revertedWith( - "Pausable: paused", - ); - }); - - it("Reverts: mint when iVault is paused", async function () { - await iVault.pause(); - const shares = randomBI(19); - await expect(iVault.connect(staker).mint(shares, staker.address)).to.be.revertedWith("Pausable: paused"); - }); - - it("Reverts: depositWithReferral when iVault is paused", async function () { - await iVault.pause(); - const depositAmount = randomBI(19); - const code = ethers.encodeBytes32String(randomAddress().slice(0, 8)); - await expect(iVault.connect(staker2).depositWithReferral(depositAmount, staker, code)).to.be.revertedWith( - "Pausable: paused", - ); - }); - - it("Reverts: deposit when targetCapacity is not set", async function () { - await snapshot.restore(); - const depositAmount = randomBI(19); - await expect(iVault.connect(staker).deposit(depositAmount, staker.address)).to.be.revertedWithCustomError( - iVault, - "InceptionOnPause", - ); - }); - - const convertSharesArgs = [ - { - name: "amount = 0", - amount: async () => 0n, - }, - { - name: "amount = 1", - amount: async () => 0n, - }, - { - name: "amount < min", - amount: async () => (await iVault.withdrawMinAmount()) - 1n, - }, - ]; - - convertSharesArgs.forEach(function (arg) { - it(`Convert to shares: ${arg.name}`, async function () { - const amount = await arg.amount(); - const ratio = await iVault.ratio(); - expect(await iVault.convertToShares(amount)).to.be.eq((amount * ratio) / e18); - }); - }); - - it("Max mint and deposit", async function () { - const stakerBalance = await asset.balanceOf(staker); - const calculatedBonus = await iVault.calculateDepositBonus(stakerBalance); - const realBonus = await iVault.depositBonusAmount(); - const bonus = realBonus > calculatedBonus ? calculatedBonus : realBonus; - expect(await iVault.maxMint(staker)).to.be.eq(await iVault.convertToShares(stakerBalance + bonus)); - expect(await iVault.maxDeposit(staker)).to.be.eq(stakerBalance); - }); - - it("Max mint and deposit when iVault is paused equal 0", async function () { - await iVault.pause(); - const maxMint = await iVault.maxMint(staker); - const maxDeposit = await iVault.maxDeposit(staker); - expect(maxMint).to.be.eq(0n); - expect(maxDeposit).to.be.eq(0n); - }); - - it("Max mint and deposit reverts when > available amount", async function () { - const maxMint = await iVault.maxMint(staker); - await expect(iVault.connect(staker).mint(maxMint + 1n, staker.address)).to.be.revertedWithCustomError( - iVault, - "ExceededMaxMint", - ); - }); - }); - - describe("Deposit with bonus for replenish", function () { - const states = [ - { - name: "deposit bonus = 0", - withBonus: false, - }, - { - name: "deposit bonus > 0", - withBonus: true, - }, - ]; - - const amounts = [ - { - name: "for the first time", - predepositAmount: targetCapacity => 0n, - amount: targetCapacity => randomBIMax(targetCapacity / 4n) + targetCapacity / 4n, - receiver: () => staker.address, - }, - { - name: "more", - predepositAmount: targetCapacity => targetCapacity / 3n, - amount: targetCapacity => randomBIMax(targetCapacity / 3n), - receiver: () => staker.address, - }, - { - name: "up to target cap", - predepositAmount: targetCapacity => targetCapacity / 10n, - amount: targetCapacity => (targetCapacity * 9n) / 10n, - receiver: () => staker.address, - }, - { - name: "all rewards", - predepositAmount: targetCapacity => 0n, - amount: targetCapacity => targetCapacity, - receiver: () => staker.address, - }, - { - name: "up to target cap and above", - predepositAmount: targetCapacity => targetCapacity / 10n, - amount: targetCapacity => targetCapacity, - receiver: () => staker.address, - }, - { - name: "above target cap", - predepositAmount: targetCapacity => targetCapacity, - amount: targetCapacity => randomBI(19), - receiver: () => staker.address, - }, - ]; - - states.forEach(function (state) { - let localSnapshot; - const targetCapacityPercent = e18; - const targetCapacity = e18; - it(`---Prepare state: ${state.name}`, async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - const deposited = (targetCapacity * MAX_TARGET_PERCENT) / targetCapacityPercent; - if (state.withBonus) { - await iVault.setTargetFlashCapacity(targetCapacityPercent); - await iVault.connect(staker3).deposit(toWei(1.5), staker3.address); - const balanceOf = await iToken.balanceOf(staker3.address); - await iVault.connect(staker3).flashWithdraw(balanceOf, staker3.address); - await iVault.setTargetFlashCapacity(1n); - } - - await iVault.connect(staker3).deposit(deposited, staker3.address); - console.log(`Total assets:\t\t${(await iVault.totalAssets()).format()}`); - console.log(`Deposit bonus:\t\t${(await iVault.depositBonusAmount()).format()}`); - localSnapshot = await helpers.takeSnapshot(); - }); - - it("Max mint and deposit", async function () { - const stakerBalance = await asset.balanceOf(staker); - const calculatedBonus = await iVault.calculateDepositBonus(stakerBalance); - const realBonus = await iVault.depositBonusAmount(); - const bonus = realBonus > calculatedBonus ? calculatedBonus : realBonus; - expect(await iVault.maxMint(staker)).to.be.eq(await iVault.convertToShares(stakerBalance + bonus)); - expect(await iVault.maxDeposit(staker)).to.be.eq(stakerBalance); - }); - - amounts.forEach(function (arg) { - it(`Deposit ${arg.name}`, async function () { - if (localSnapshot) { - await localSnapshot.restore(); - } else { - expect(false).to.be.true("Can not restore local snapshot"); - } - - const flashCapacityBefore = arg.predepositAmount(targetCapacity); - const freeBalance = await iVault.getFreeBalance(); - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, freeBalance - flashCapacityBefore, 1296000); - await iVault.setTargetFlashCapacity(targetCapacityPercent); - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - - const ratioBefore = await iVault.ratio(); - let availableBonus = await iVault.depositBonusAmount(); - const receiver = arg.receiver(); - const stakerSharesBefore = await iToken.balanceOf(receiver); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - console.log(`Target capacity:\t\t${targetCapacity.format()}`); - console.log(`Flash capacity before:\t${flashCapacityBefore.format()}`); - - const amount = await arg.amount(targetCapacity); - console.log(`Amount:\t\t\t\t\t${amount.format()}`); - const calculatedBonus = await iVault.calculateDepositBonus(amount); - console.log(`Calculated bonus:\t\t${calculatedBonus.format()}`); - console.log(`Available bonus:\t\t${availableBonus.format()}`); - const expectedBonus = calculatedBonus <= availableBonus ? calculatedBonus : availableBonus; - availableBonus -= expectedBonus; - console.log(`Expected bonus:\t\t\t${expectedBonus.format()}`); - const convertedShares = await iVault.convertToShares(amount + expectedBonus); - const expectedShares = ((amount + expectedBonus) * (await iVault.ratio())) / e18; - const previewShares = await iVault.previewDeposit(amount); - - const tx = await iVault.connect(staker).deposit(amount, receiver); - const receipt = await tx.wait(); - const depositEvent = receipt.logs?.filter(e => e.eventName === "Deposit"); - expect(depositEvent.length).to.be.eq(1); - expect(depositEvent[0].args["sender"]).to.be.eq(staker.address); - expect(depositEvent[0].args["receiver"]).to.be.eq(receiver); - expect(depositEvent[0].args["amount"]).to.be.closeTo(amount, transactErr); - expect(depositEvent[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); - //DepositBonus event - expect(receipt.logs.find(l => l.eventName === "DepositBonus")?.args.amount || 0n).to.be.closeTo( - expectedBonus, - transactErr, - ); - - const stakerSharesAfter = await iToken.balanceOf(receiver); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after:\t\t\t${ratioAfter.format()}`); - console.log(`Bonus after:\t\t\t${availableBonus.format()}`); - - expect(stakerSharesAfter - stakerSharesBefore).to.be.closeTo(expectedShares, transactErr); - expect(stakerSharesAfter - stakerSharesBefore).to.be.closeTo(convertedShares, transactErr); - - expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount + expectedBonus, transactErr); - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit - expect(flashCapacityAfter).to.be.closeTo(flashCapacityBefore + amount + expectedBonus, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBefore, ratioErr); //Ratio stays the same - expect(previewShares).to.be.eq(stakerSharesAfter - stakerSharesBefore); //Ratio stays the same - }); - }); - }); - }); - - describe("Delegate to mellow vault", function () { - let ratio, firstDeposit; - - beforeEach(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker3).deposit(e18, staker3.address); - firstDeposit = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, firstDeposit, 1296000); - await a.addRewardsMellowVault(toWei(0.001), mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - console.log(`Initial ratio: ${ratio.format()}`); - }); - - const args = [ - { - name: "random amounts ~ e18", - depositAmount: async () => toWei(1), - }, - { - name: "amounts which are close to min", - depositAmount: async () => (await iVault.withdrawMinAmount()) + 1n, - }, - ]; - - args.forEach(function (arg) { - it(`Deposit and delegate ${arg.name} many times`, async function () { - await iVault.setTargetFlashCapacity(1n); - let totalDelegated = 0n; - const count = 10; - for (let i = 0; i < count; i++) { - const deposited = await arg.depositAmount(); - await iVault.connect(staker).deposit(deposited, staker.address); - const delegated = await iVault.getFreeBalance(); - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, delegated, 1296000); - - totalDelegated += deposited; - } - console.log(`Final ratio:\t${(await iVault.ratio()).format()}`); - console.log(`Total delegated:\t${totalDelegated.format()}`); - - const balanceExpected = (totalDelegated * ratio) / e18; - const totalSupplyExpected = balanceExpected + firstDeposit; - const err = BigInt(count) * transactErr * 2n; - - const balanceAfter = await iToken.balanceOf(staker.address); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedToAfter = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalSupplyAfter = await iToken.totalSupply(); - const totalAssetsAfter = await iVault.totalAssets(); - console.log(`Staker balance after: ${balanceAfter.format()}`); - console.log(`Total deposited after: ${totalDepositedAfter.format()}`); - console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); - console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); - console.log(`Total assets after: ${totalAssetsAfter.format()}`); - - expect(balanceAfter - balanceExpected).to.be.closeTo(0, err); - expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0, err); - expect(totalAssetsAfter).to.be.lte(transactErr); - expect(await iVault.ratio()).to.be.closeTo(ratio, BigInt(count) * ratioErr); - }); - }); - - const args2 = [ - { - name: "by the same staker", - staker: async () => staker, - }, - { - name: "by different stakers", - staker: async () => await getRandomStaker(iVault, asset, staker3, toWei(1)), - }, - ]; - - args2.forEach(function (arg) { - it(`Deposit many times and delegate once ${arg.name}`, async function () { - await iVault.setTargetFlashCapacity(1n); - let totalDeposited = 0n; - const count = 10; - for (let i = 0; i < count; i++) { - const staker = await arg.staker(); - const deposited = await randomBI(18); - await iVault.connect(staker).deposit(deposited, staker.address); - totalDeposited += deposited; - } - const totalDelegated = await iVault.getFreeBalance(); - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, totalDelegated, 1296000); - - console.log(`Final ratio:\t${await iVault.ratio()}`); - console.log(`Total deposited:\t${totalDeposited.format()}`); - console.log(`Total delegated:\t${totalDelegated.format()}`); - - const balanceExpected = (totalDelegated * ratio) / e18; - const totalSupplyExpected = balanceExpected + firstDeposit; - const err = BigInt(count) * transactErr * 2n; - - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedToAfter = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalSupplyAfter = await iToken.totalSupply(); - const totalAssetsAfter = await iVault.totalAssets(); - console.log(`Total deposited after: ${totalDepositedAfter.format()}`); - console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); - console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); - console.log(`Total assets after: ${totalAssetsAfter.format()}`); - - expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); - expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0n, err); - expect(totalAssetsAfter).to.be.lte(transactErr); - expect(await iVault.ratio()).to.be.closeTo(ratio, BigInt(count) * ratioErr); - }); - }); - - const args3 = [ - { - name: "to the different operators", - count: 20, - mellowVault: i => mellowVaults[i % mellowVaults.length].vaultAddress, - }, - { - name: "to the same operator", - count: 10, - mellowVault: i => mellowVaults[0].vaultAddress, - }, - ]; - - args3.forEach(function (arg) { - it(`Delegate many times ${arg.name}`, async function () { - for (let i = 1; i < mellowVaults.length; i++) { - await mellowRestaker.addMellowVault(mellowVaults[i].vaultAddress, mellowVaults[i].wrapperAddress); - } - - await iVault.setTargetFlashCapacity(1n); - //Deposit by 2 stakers - const totalDelegated = toWei(60); - await iVault.connect(staker).deposit(totalDelegated / 2n, staker.address); - await iVault.connect(staker2).deposit(totalDelegated / 2n, staker2.address); - //Delegate - for (let i = 0; i < arg.count; i++) { - const taBefore = await iVault.totalAssets(); - const mVault = arg.mellowVault(i); - console.log(`#${i} mellow vault: ${mVault}`); - const fb = await iVault.getFreeBalance(); - const amount = fb / BigInt(arg.count - i); - await expect(iVault.connect(iVaultOperator).delegateToMellowVault(mVault, amount, 1296000)) - .to.emit(iVault, "DelegatedTo") - .withArgs(mellowRestaker.address, mVault, amount); - - const taAfter = await iVault.totalAssets(); - expect(taBefore - taAfter).to.be.closeTo(amount, transactErr); - } - console.log(`Final ratio:\t${await iVault.ratio()}`); - - const balanceExpected = (totalDelegated * ratio) / e18; - const totalSupplyExpected = balanceExpected + firstDeposit; - const err = BigInt(arg.count) * transactErr * 2n; - - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const totalDelegatedToAfter = await iVault.getDelegatedTo(mellowVaults[0].vaultAddress); - const totalSupplyAfter = await iToken.totalSupply(); - const totalAssetsAfter = await iVault.totalAssets(); - console.log(`Total deposited after: ${totalDepositedAfter.format()}`); - console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); - console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); - console.log(`Total assets after: ${totalAssetsAfter.format()}`); - - expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0, err); - expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0, err); - expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0, err); - expect(totalAssetsAfter).to.be.lte(transactErr); - expect(await iVault.ratio()).to.be.closeTo(ratio, BigInt(arg.count) * ratioErr); - }); - }); - - //Delegate invalid params - const invalidArgs = [ - { - name: "amount is 0", - deposited: toWei(1), - amount: async () => 0n, - mVault: async () => mellowVaults[0].vaultAddress, - operator: () => iVaultOperator, - }, - { - name: "amount is greater than free balance", - deposited: toWei(10), - targetCapacityPercent: e18, - amount: async () => (await iVault.getFreeBalance()) + 1n, - mVault: async () => mellowVaults[0].vaultAddress, - operator: () => iVaultOperator, - customError: "InsufficientCapacity", - source: () => iVault, - }, - { - name: "unknown mellow vault", - deposited: toWei(1), - amount: async () => await iVault.getFreeBalance(), - mVault: async () => mellowVaults[1].vaultAddress, - operator: () => iVaultOperator, - customError: "InactiveWrapper", - source: () => mellowRestaker, - }, - { - name: "mellow vault is zero address", - deposited: toWei(1), - amount: async () => await iVault.getFreeBalance(), - mVault: async () => ethers.ZeroAddress, - operator: () => iVaultOperator, - customError: "NullParams", - source: () => iVault, - }, - { - name: "caller is not an operator", - deposited: toWei(1), - amount: async () => await iVault.getFreeBalance(), - mVault: async () => mellowVaults[0].vaultAddress, - operator: () => staker, - customError: "OnlyOperatorAllowed", - source: () => iVault, - }, - ]; - - invalidArgs.forEach(function (arg) { - it(`delegateToMellowVault reverts when ${arg.name}`, async function () { - if (arg.targetCapacityPercent) { - await iVault.setTargetFlashCapacity(arg.targetCapacityPercent); - } - await asset.connect(staker3).approve(await iVault.getAddress(), arg.deposited); - await iVault.connect(staker3).deposit(arg.deposited, staker3.address); - - const operator = arg.operator(); - const delegateAmount = await arg.amount(); - const mVault = await arg.mVault(); - - if (arg.customError) { - await expect( - iVault.connect(operator).delegateToMellowVault(mVault, delegateAmount, 1296000), - ).to.be.revertedWithCustomError(arg.source(), arg.customError); - } else { - await expect(iVault.connect(operator).delegateToMellowVault(mVault, delegateAmount, 1296000)).to.be - .reverted; - } - }); - }); - - it("delegateToMellowVault reverts when iVault is paused", async function () { - const amount = randomBI(18); - await iVault.connect(staker).deposit(amount, staker.address); - await iVault.pause(); - await expect( - iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000), - ).to.be.revertedWith("Pausable: paused"); - }); - - it("delegateToMellowVault reverts when mellowRestaker is paused", async function () { - if (await iVault.paused()) { - await iVault.unpause(); - } - const amount = randomBI(18); - await iVault.connect(staker).deposit(amount, staker.address); - await mellowRestaker.pause(); - - await expect( - iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000), - ).to.be.revertedWith("Pausable: paused"); - await mellowRestaker.unpause(); - }); - }); - - // describe("Delegate auto according allocation", function () { - // describe("Set allocation", function () { - // before(async function () { - // await snapshot.restore(); - // await mellowRestaker.addMellowVault(mellowVaults[1].vaultAddress, mellowVaults[1].wrapperAddress); - // }); - - // const args = [ - // { - // name: "Set allocation for the 1st vault", - // vault: () => mellowVaults[0].vaultAddress, - // shares: randomBI(2), - // }, - // { - // name: "Set allocation for another vault", - // vault: () => mellowVaults[1].vaultAddress, - // shares: randomBI(2), - // }, - // { - // name: "Change allocation", - // vault: () => mellowVaults[1].vaultAddress, - // shares: randomBI(2), - // }, - // { - // name: "Set allocation for address that is not in the list", - // vault: () => ethers.Wallet.createRandom().address, - // shares: randomBI(2), - // }, - // { - // name: "Change allocation to 0", - // vault: () => mellowVaults[1].vaultAddress, - // shares: 0n, - // }, - // ]; - - // args.forEach(function (arg) { - // it(`${arg.name}`, async function () { - // const vaultAddress = arg.vault(); - // const totalAllocationBefore = await mellowRestaker.totalAllocations(); - // const sharesBefore = await mellowRestaker.allocations(vaultAddress); - // console.log(`sharesBefore: ${sharesBefore.toString()}`); - - // await expect(mellowRestaker.changeAllocation(vaultAddress, arg.shares)) - // .to.be.emit(mellowRestaker, "AllocationChanged") - // .withArgs(vaultAddress, sharesBefore, arg.shares); - - // const totalAllocationAfter = await mellowRestaker.totalAllocations(); - // const sharesAfter = await mellowRestaker.allocations(vaultAddress); - // console.log("Total allocation after:", totalAllocationAfter.format()); - // console.log("Restaker allocation after:", sharesAfter.format()); - - // expect(sharesAfter).to.be.eq(arg.shares); - // expect(totalAllocationAfter - totalAllocationBefore).to.be.eq(sharesAfter - sharesBefore); - // }); - // }); - - // it("changeAllocation reverts when vault is 0 address", async function () { - // const shares = randomBI(2); - // const vaultAddress = ethers.ZeroAddress; - // await expect(mellowRestaker.changeAllocation(vaultAddress, shares)).to.be.revertedWithCustomError( - // mellowRestaker, - // "ZeroAddress", - // ); - // }); - - // it("changeAllocation reverts when called by not an owner", async function () { - // const shares = randomBI(2); - // const vaultAddress = mellowVaults[1].vaultAddress; - // await expect(mellowRestaker.connect(staker).changeAllocation(vaultAddress, shares)).to.be.revertedWith( - // "Ownable: caller is not the owner", - // ); - // }); - // }); - - // describe("Delegate auto", function () { - // let totalDeposited; - - // beforeEach(async function () { - // await snapshot.restore(); - // await iVault.setTargetFlashCapacity(1n); - // totalDeposited = randomBI(19); - // await iVault.connect(staker).deposit(totalDeposited, staker.address); - // }); - - // //mellowVaults[0] added at deploy - // const args = [ - // { - // name: "1 vault, no allocation", - // addVaults: [], - // allocations: [], - // }, - // { - // name: "1 vault; allocation 100%", - // addVaults: [], - // allocations: [ - // { - // vault: mellowVaults[0].vaultAddress, - // amount: 1n, - // }, - // ], - // }, - // { - // name: "1 vault; allocation 100% and 0% to unregistered", - // addVaults: [], - // allocations: [ - // { - // vault: mellowVaults[0].vaultAddress, - // amount: 1n, - // }, - // { - // vault: mellowVaults[1].vaultAddress, - // amount: 0n, - // }, - // ], - // }, - // { - // name: "1 vault; allocation 50% and 50% to unregistered", - // addVaults: [], - // allocations: [ - // { - // vault: mellowVaults[0].vaultAddress, - // amount: 1n, - // }, - // { - // vault: mellowVaults[1].vaultAddress, - // amount: 1n, - // }, - // ], - // }, - // { - // name: "2 vaults; allocations: 100%, 0%", - // addVaults: [mellowVaults[1]], - // allocations: [ - // { - // vault: mellowVaults[0].vaultAddress, - // amount: 1n, - // }, - // { - // vault: mellowVaults[1].vaultAddress, - // amount: 0n, - // }, - // ], - // }, - // { - // name: "2 vaults; allocations: 50%, 50%", - // addVaults: [mellowVaults[1]], - // allocations: [ - // { - // vault: mellowVaults[0].vaultAddress, - // amount: 1n, - // }, - // { - // vault: mellowVaults[1].vaultAddress, - // amount: 1n, - // }, - // ], - // }, - // { - // name: "3 vaults; allocations: 33%, 33%, 33%", - // addVaults: [mellowVaults[1], mellowVaults[2]], - // allocations: [ - // { - // vault: mellowVaults[0].vaultAddress, - // amount: 1n, - // }, - // { - // vault: mellowVaults[1].vaultAddress, - // amount: 1n, - // }, - // { - // vault: mellowVaults[2].vaultAddress, - // amount: 1n, - // }, - // ], - // }, - // ]; - - // args.forEach(function (arg) { - // it(`Delegate auto when ${arg.name}`, async function () { - // //Add restakers - // const addedVaults = [mellowVaults[0].vaultAddress]; - // for (const vault of arg.addVaults) { - // await mellowRestaker.addMellowVault(vault.vaultAddress, vault.wrapperAddress); - // addedVaults.push(vault.vaultAddress); - // } - // //Set allocations - // let totalAllocations = 0n; - // for (const allocation of arg.allocations) { - // await mellowRestaker.changeAllocation(allocation.vault, allocation.amount); - // totalAllocations += allocation.amount; - // } - // //Calculate expected delegated amounts - // const freeBalance = await iVault.getFreeBalance(); - // expect(freeBalance).to.be.closeTo(totalDeposited, 1n); - // let expectedDelegated = 0n; - // const expectedDelegations = new Map(); - // for (const allocation of arg.allocations) { - // let amount = 0n; - // if (addedVaults.includes(allocation.vault)) { - // amount += (freeBalance * allocation.amount) / totalAllocations; - // } - // expectedDelegations.set(allocation.vault, amount); - // expectedDelegated += amount; - // } - - // await iVault.connect(iVaultOperator).delegateAuto(1296000); - - // const totalDepositedAfter = await iVault.getTotalDeposited(); - // const totalDelegatedAfter = await iVault.getTotalDelegated(); - // const totalAssetsAfter = await iVault.totalAssets(); - // console.log(`Total deposited after: ${totalDepositedAfter.format()}`); - // console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); - // console.log(`Total assets after: ${totalAssetsAfter.format()}`); - - // expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr * BigInt(addedVaults.length)); - // expect(totalDelegatedAfter).to.be.closeTo(expectedDelegated, transactErr * BigInt(addedVaults.length)); - // expect(totalAssetsAfter).to.be.closeTo(totalDeposited - expectedDelegated, transactErr); - - // for (const allocation of arg.allocations) { - // expect(expectedDelegations.get(allocation.vault)).to.be.closeTo( - // await iVault.getDelegatedTo(allocation.vault), - // transactErr, - // ); - // } - // }); - // }); - - // it("delegateAuto reverts when called by not an owner", async function () { - // await mellowRestaker.changeAllocation(mellowVaults[0].vaultAddress, 1n); - // await expect(iVault.connect(staker).delegateAuto(1296000)).to.revertedWithCustomError( - // iVault, - // "OnlyOperatorAllowed", - // ); - // }); - - // it("delegateAuto reverts when iVault is paused", async function () { - // await mellowRestaker.changeAllocation(mellowVaults[0].vaultAddress, 1n); - // await iVault.pause(); - // await expect(iVault.connect(iVaultOperator).delegateAuto(1296000)).to.be.revertedWith("Pausable: paused"); - // }); - - // it("delegateAuto reverts when mellowRestaker is paused", async function () { - // if (await iVault.paused()) { - // await iVault.unpause(); - // } - // await mellowRestaker.changeAllocation(mellowVaults[0].vaultAddress, 1n); - // await mellowRestaker.pause(); - // await expect(iVault.connect(iVaultOperator).delegateAuto(1296000)).to.be.revertedWith("Pausable: paused"); - // }); - // }); - // }); - - describe("Withdraw: user can unstake", function () { - let ratio, totalDeposited, TARGET; - - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(toWei(10), staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, freeBalance, 1296000); - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - totalDeposited = await iVault.getTotalDeposited(); - TARGET = 1000_000n; - await iVault.setTargetFlashCapacity(TARGET); - ratio = await iVault.ratio(); - console.log(`Initial ratio: ${ratio}`); - }); - - const testData = [ - { - name: "random e18", - amount: async shares => 724399519262012598n, - receiver: () => staker.address, - }, - { - name: "999999999999999999", - amount: async shares => 999999999999999999n, - receiver: () => staker2.address, - }, - { - name: "888888888888888888", - amount: async shares => 888888888888888888n, - receiver: () => staker2.address, - }, - { - name: "777777777777777777", - amount: async shares => 777777777777777777n, - receiver: () => staker2.address, - }, - { - name: "666666666666666666", - amount: async shares => 666666666666666666n, - receiver: () => staker2.address, - }, - { - name: "555555555555555555", - amount: async shares => 555555555555555555n, - receiver: () => staker2.address, - }, - { - name: "444444444444444444", - amount: async shares => 444444444444444444n, - receiver: () => staker2.address, - }, - { - name: "333333333333333333", - amount: async shares => 333333333333333333n, - receiver: () => staker2.address, - }, - { - name: "222222222222222222", - amount: async shares => 222222222222222222n, - receiver: () => staker2.address, - }, - { - name: "111111111111111111", - amount: async shares => 111111111111111111n, - receiver: () => staker2.address, - }, - { - name: "min amount", - amount: async shares => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, - receiver: () => staker2.address, - }, - { - name: "all", - amount: async shares => shares, - receiver: () => staker2.address, - }, - ]; - - testData.forEach(function (test) { - it(`Withdraw ${test.name}`, async function () { - const ratioBefore = await iVault.ratio(); - const balanceBefore = await iToken.balanceOf(staker.address); - const amount = await test.amount(balanceBefore); - const assetValue = await iVault.convertToAssets(amount); - const stakerPWBefore = await iVault.getPendingWithdrawalOf(test.receiver()); - const totalPWBefore = await iVault.totalAmountToWithdraw(); - - const tx = await iVault.connect(staker).withdraw(amount, test.receiver()); - const receipt = await tx.wait(); - const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(events.length).to.be.eq(1); - expect(events[0].args["sender"]).to.be.eq(staker.address); - expect(events[0].args["receiver"]).to.be.eq(test.receiver()); - expect(events[0].args["owner"]).to.be.eq(staker.address); - expect(events[0].args["amount"]).to.be.closeTo(assetValue, transactErr); - expect(events[0].args["iShares"]).to.be.eq(amount); - - expect(balanceBefore - (await iToken.balanceOf(staker.address))).to.be.eq(amount); - expect((await iVault.getPendingWithdrawalOf(test.receiver())) - stakerPWBefore).to.be.closeTo( - assetValue, - transactErr, - ); - expect((await iVault.totalAmountToWithdraw()) - totalPWBefore).to.be.closeTo(assetValue, transactErr); - expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); - expect(await iVault.ratio()).to.be.closeTo(ratioBefore, ratioErr); - }); - }); - }); - - describe("Withdraw: negative cases", function () { - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(toWei(10), staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, freeBalance, 1296000); - await a.addRewardsMellowVault(toWei(0.001), mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - }); - - const invalidData = [ - { - name: "> balance", - amount: async () => (await iToken.balanceOf(staker.address)) + 1n, - receiver: () => staker.address, - error: "ERC20: burn amount exceeds balance", - }, - { - name: "< min amount", - amount: async () => (await iVault.convertToShares(await iVault.withdrawMinAmount())) - 1n, - receiver: () => staker.address, - customError: "LowerMinAmount", - }, - { - name: "0", - amount: async () => 0n, - receiver: () => staker.address, - customError: "NullParams", - }, - { - name: "to zero address", - amount: async () => randomBI(18), - receiver: () => ethers.ZeroAddress, - customError: "NullParams", - }, - ]; - - invalidData.forEach(function (test) { - it(`Reverts: withdraws ${test.name}`, async function () { - const amount = await test.amount(); - const receiver = test.receiver(); - if (test.customError) { - await expect(iVault.connect(staker).withdraw(amount, receiver)).to.be.revertedWithCustomError( - iVault, - test.customError, - ); - } else if (test.error) { - await expect(iVault.connect(staker).withdraw(amount, receiver)).to.be.revertedWith(test.error); - } - }); - }); - - it("Withdraw small amount many times", async function () { - const ratioBefore = await iVault.ratio(); - console.log(`Ratio before:\t${ratioBefore.format()}`); - - const count = 100; - const amount = await iVault.withdrawMinAmount(); - for (let i = 0; i < count; i++) { - await iVault.connect(staker).withdraw(amount, staker.address); - } - const ratioAfter = await iVault.ratio(); - console.log(`Ratio after:\t${ratioAfter.format()}`); - - expect(ratioBefore - ratioAfter).to.be.closeTo(0, count); - - await iVault.connect(staker).withdraw(e18, staker.address); - console.log(`Ratio after withdraw 1eth:\t${await iVault.ratio()}`); - expect(await iVault.ratio()).to.be.closeTo(ratioAfter, ratioErr); - }); - - it("Reverts: withdraw when iVault is paused", async function () { - await iVault.pause(); - await expect(iVault.connect(staker).withdraw(toWei(1), staker.address)).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - - it("Reverts: withdraw when targetCapacity is not set", async function () { - await snapshot.restore(); - await expect(iVault.connect(staker).withdraw(toWei(1), staker.address)).to.be.revertedWithCustomError( - iVault, - "InceptionOnPause", - ); - }); - }); - - describe("Flash withdraw with fee", function () { - const targetCapacityPercent = e18; - const targetCapacity = e18; - let deposited = 0n; - beforeEach(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - deposited = (targetCapacity * MAX_TARGET_PERCENT) / targetCapacityPercent; - await iVault.connect(staker3).deposit(deposited, staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, freeBalance, 1296000); - - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - await iVault.setTargetFlashCapacity(targetCapacityPercent); - }); - - const args = [ - { - name: "part of the free balance when pool capacity > TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, - amount: async () => (await iVault.getFreeBalance()) / 2n, - receiver: () => staker, - }, - { - name: "all of the free balance when pool capacity > TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, - amount: async () => await iVault.getFreeBalance(), - receiver: () => staker, - }, - { - name: "all when pool capacity > TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, - amount: async () => await iVault.getFlashCapacity(), - receiver: () => staker, - }, - { - name: "partially when pool capacity = TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent, - amount: async () => (await iVault.getFlashCapacity()) / 2n, - receiver: () => staker, - }, - { - name: "all when pool capacity = TARGET", - poolCapacity: targetCapacityPercent => targetCapacityPercent, - amount: async () => await iVault.getFlashCapacity(), - receiver: () => staker, - }, - { - name: "partially when pool capacity < TARGET", - poolCapacity: targetCapacityPercent => (targetCapacityPercent * 3n) / 4n, - amount: async () => (await iVault.getFlashCapacity()) / 2n, - receiver: () => staker, - }, - { - name: "all when pool capacity < TARGET", - poolCapacity: targetCapacityPercent => (targetCapacityPercent * 3n) / 4n, - amount: async () => await iVault.getFlashCapacity(), - receiver: () => staker, - }, - ]; - - args.forEach(function (arg) { - it(`flashWithdraw: ${arg.name}`, async function () { - //Undelegate from Mellow - const undelegatePercent = arg.poolCapacity(targetCapacityPercent); - const undelegateAmount = (deposited * undelegatePercent) / MAX_TARGET_PERCENT; - await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, undelegateAmount); - - //flashWithdraw - const ratioBefore = await iVault.ratio(); - console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); - - const sharesBefore = await iToken.balanceOf(staker); - const assetBalanceBefore = await asset.balanceOf(staker); - const treasuryBalanceBefore = await asset.balanceOf(treasury); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const flashCapacityBefore = await iVault.getFlashCapacity(); - const freeBalanceBefore = await iVault.getFreeBalance(); - console.log(`flashCapacityBefore:\t${flashCapacityBefore.format()}`); - console.log(`freeBalanceBefore:\t\t${freeBalanceBefore.format()}`); - - const amount = await arg.amount(); - const shares = await iVault.convertToShares(amount); - const receiver = await arg.receiver(); - const expectedFee = await iVault.calculateFlashWithdrawFee(amount); - console.log(`Expected fee:\t\t\t${expectedFee.format()}`); - - let tx = await iVault.connect(staker).flashWithdraw(shares, receiver.address); - const receipt = await tx.wait(); - const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); - expect(withdrawEvent.length).to.be.eq(1); - expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); - expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); - expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); - const fee = withdrawEvent[0].args["fee"]; - expect(fee).to.be.closeTo(expectedFee, transactErr); - - const sharesAfter = await iToken.balanceOf(staker); - const assetBalanceAfter = await asset.balanceOf(staker); - const treasuryBalanceAfter = await asset.balanceOf(treasury); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - console.log(`Balance diff:\t\t\t${(sharesBefore - sharesAfter).format()}`); - console.log(`TotalDeposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); - console.log(`TotalAssets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); - console.log(`FlashCapacity diff:\t\t${(flashCapacityBefore - flashCapacityAfter).format()}`); - console.log(`Fee:\t\t\t\t\t${fee.format()}`); - - expect(sharesBefore - sharesAfter).to.be.eq(shares); - expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); - expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); - expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); - expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); - }); - - it(`redeem(shares,receiver,owner): ${arg.name}`, async function () { - //Undelegate from Mellow - const undelegatePercent = arg.poolCapacity(targetCapacityPercent); - const undelegateAmount = (deposited * undelegatePercent) / MAX_TARGET_PERCENT; - await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, undelegateAmount); - - //flashWithdraw - const ratioBefore = await iVault.ratio(); - console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); - - const sharesBefore = await iToken.balanceOf(staker); - const assetBalanceBefore = await asset.balanceOf(staker); - const treasuryBalanceBefore = await asset.balanceOf(treasury); - const totalDepositedBefore = await iVault.getTotalDeposited(); - const totalAssetsBefore = await iVault.totalAssets(); - const flashCapacityBefore = await iVault.getFlashCapacity(); - const freeBalanceBefore = await iVault.getFreeBalance(); - console.log(`flashCapacityBefore:\t${flashCapacityBefore.format()}`); - console.log(`freeBalanceBefore:\t\t${freeBalanceBefore.format()}`); - - const amount = await arg.amount(); - const shares = await iVault.convertToShares(amount); //+1 to compensate rounding after converting from shares to amount - const previewAmount = await iVault.previewRedeem(shares); - const receiver = await arg.receiver(); - const expectedFee = await iVault.calculateFlashWithdrawFee(amount); - console.log(`Expected fee:\t\t\t${expectedFee.format()}`); - - let tx = await iVault - .connect(staker) - ["redeem(uint256,address,address)"](shares, receiver.address, staker.address); - const receipt = await tx.wait(); - const withdrawEvent = receipt.logs?.filter(e => e.eventName === "Withdraw"); - expect(withdrawEvent.length).to.be.eq(1); - expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); - expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); - expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); - expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); - const feeEvent = receipt.logs?.filter(e => e.eventName === "WithdrawalFee"); - const fee = feeEvent[0].args["fee"]; - expect(fee).to.be.closeTo(expectedFee, transactErr); - - const sharesAfter = await iToken.balanceOf(staker); - const assetBalanceAfter = await asset.balanceOf(staker); - const treasuryBalanceAfter = await asset.balanceOf(treasury); - const totalDepositedAfter = await iVault.getTotalDeposited(); - const totalAssetsAfter = await iVault.totalAssets(); - const flashCapacityAfter = await iVault.getFlashCapacity(); - console.log(`Balance diff:\t\t\t${(sharesBefore - sharesAfter).format()}`); - console.log(`TotalDeposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); - console.log(`TotalAssets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); - console.log(`FlashCapacity diff:\t\t${(flashCapacityBefore - flashCapacityAfter).format()}`); - console.log(`Fee:\t\t\t\t\t${fee.format()}`); - - expect(sharesBefore - sharesAfter).to.be.eq(shares); - expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); - expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); - expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); - expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); - expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); - expect(previewAmount).to.be.eq(assetBalanceAfter - assetBalanceBefore); - }); - }); - - it("Reverts when capacity is not sufficient", async function () { - const shares = await iToken.balanceOf(staker.address); - const capacity = await iVault.getFlashCapacity(); - await expect(iVault.connect(staker).flashWithdraw(shares, staker.address)) - .to.be.revertedWithCustomError(iVault, "InsufficientCapacity") - .withArgs(capacity); - }); - - it("Reverts when amount < min", async function () { - const withdrawMinAmount = await iVault.withdrawMinAmount(); - const shares = (await iVault.convertToShares(withdrawMinAmount)) - 1n; - await expect(iVault.connect(staker).flashWithdraw(shares, staker.address)) - .to.be.revertedWithCustomError(iVault, "LowerMinAmount") - .withArgs(withdrawMinAmount); - }); - - it("Reverts redeem when owner != message sender", async function () { - await iVault.connect(staker).deposit(e18, staker.address); - const amount = await iVault.getFlashCapacity(); - await expect( - iVault.connect(staker)["redeem(uint256,address,address)"](amount, staker.address, staker2.address), - ).to.be.revertedWithCustomError(iVault, "MsgSenderIsNotOwner"); - }); - - it("Reverts when iVault is paused", async function () { - await iVault.connect(staker).deposit(e18, staker.address); - await iVault.pause(); - const amount = await iVault.getFlashCapacity(); - await expect(iVault.connect(staker).flashWithdraw(amount, staker.address)).to.be.revertedWith( - "Pausable: paused", - ); - await expect( - iVault.connect(staker)["redeem(uint256,address,address)"](amount, staker.address, staker.address), - ).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - }); - - describe("Max redeem", function () { - beforeEach(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker3).deposit(randomBI(18), staker3.address); - const freeBalance = await iVault.getFreeBalance(); - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, freeBalance / 2n, 1296000); - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - }); - - const args = [ - { - name: "User amount = 0", - sharesOwner: () => ethers.Wallet.createRandom(), - maxRedeem: async () => 0n, - }, - { - name: "User amount < flash capacity", - sharesOwner: () => staker, - deposited: randomBI(18), - maxRedeem: async () => await iToken.balanceOf(staker), - }, - { - name: "User amount = flash capacity", - sharesOwner: () => staker, - deposited: randomBI(18), - delegated: async deposited => (await iVault.totalAssets()) - deposited, - maxRedeem: async () => await iToken.balanceOf(staker), - }, - { - name: "User amount > flash capacity > 0", - sharesOwner: () => staker, - deposited: randomBI(18), - delegated: async deposited => (await iVault.totalAssets()) - randomBI(17), - maxRedeem: async () => await iVault.convertToShares(await iVault.getFlashCapacity()), - }, - { - name: "User amount > flash capacity = 0", - sharesOwner: () => staker3, - delegated: async deposited => await iVault.totalAssets(), - maxRedeem: async () => 0n, - }, - ]; - - async function prepareState(arg) { - const sharesOwner = arg.sharesOwner(); - console.log(sharesOwner.address); - if (arg.deposited) { - await iVault.connect(sharesOwner).deposit(arg.deposited, sharesOwner.address); - } - - if (arg.delegated) { - const delegated = await arg.delegated(arg.deposited); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, delegated, 1296000); - } - return sharesOwner; - } - - args.forEach(function (arg) { - it(`maxReedem: ${arg.name}`, async function () { - const sharesOwner = await prepareState(arg); - - const maxRedeem = await iVault.maxRedeem(sharesOwner); - const expectedMaxRedeem = await arg.maxRedeem(); - - console.log(`User shares:\t\t${(await iToken.balanceOf(sharesOwner)).format()}`); - console.log(`flashCapacity:\t\t${(await iVault.convertToShares(await iVault.getFlashCapacity())).format()}`); - console.log(`total assets:\t\t${await iVault.totalAssets()}`); - console.log(`maxRedeem:\t\t\t${maxRedeem.format()}`); - console.log(`expected Redeem:\t${expectedMaxRedeem.format()}`); - - if (maxRedeem > 0n) { - await iVault.connect(sharesOwner).redeem(maxRedeem, sharesOwner.address, sharesOwner.address); - } - expect(maxRedeem).to.be.eq(expectedMaxRedeem); - }); - }); - - it("Reverts when iVault is paused", async function () { - await iVault.connect(staker).deposit(e18, staker.address); - await iVault.pause(); - expect(await iVault.maxRedeem(staker)).to.be.eq(0n); - }); - }); - - describe("Mellow vaults management", function () { - beforeEach(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(e18, staker.address); - }); - - it("addMellowVault reverts when already added", async function () { - const mellowVault = mellowVaults[0].vaultAddress; - const wrapper = mellowVaults[0].wrapperAddress; - await expect(mellowRestaker.addMellowVault(mellowVault, wrapper)).to.revertedWithCustomError( - mellowRestaker, - "AlreadyAdded", - ); - }); - - it("addMellowVault vault is 0 address", async function () { - const mellowVault = ethers.ZeroAddress; - const wrapper = mellowVaults[1].wrapperAddress; - await expect(mellowRestaker.addMellowVault(mellowVault, wrapper)).to.revertedWithCustomError( - mellowRestaker, - "ZeroAddress", - ); - }); - - it("addMellowVault wrapper is 0 address", async function () { - const mellowVault = mellowVaults[1].vaultAddress; - const wrapper = ethers.ZeroAddress; - await expect(mellowRestaker.addMellowVault(mellowVault, wrapper)).to.revertedWithCustomError( - mellowRestaker, - "ZeroAddress", - ); - }); - - it("addMellowVault reverts when called by not an owner", async function () { - const mellowVault = mellowVaults[1].vaultAddress; - const wrapper = mellowVaults[1].wrapperAddress; - await expect(mellowRestaker.connect(staker).addMellowVault(mellowVault, wrapper)).to.revertedWith( - "Ownable: caller is not the owner", - ); - }); - - it("changeMellowWrapper", async function () { - const mellowVault = mellowVaults[1].vaultAddress; - const prevValue = mellowVaults[1].wrapperAddress; - await expect(mellowRestaker.addMellowVault(mellowVault, prevValue)) - .to.emit(mellowRestaker, "VaultAdded") - .withArgs(mellowVault, prevValue); - expect(await mellowRestaker.mellowDepositWrappers(mellowVault)).to.be.eq(prevValue); - - const newValue = mellowVaults[1].wrapperAddress; - await expect(mellowRestaker.changeMellowWrapper(mellowVault, newValue)) - .to.emit(mellowRestaker, "WrapperChanged") - .withArgs(mellowVault, prevValue, newValue); - expect(await mellowRestaker.mellowDepositWrappers(mellowVault)).to.be.eq(newValue); - - const freeBalance = await iVault.getFreeBalance(); - await expect(iVault.connect(iVaultOperator).delegateToMellowVault(mellowVault, freeBalance, 1296000)) - .emit(iVault, "DelegatedTo") - .withArgs(mellowRestaker.address, mellowVault, freeBalance); - }); - - it("changeMellowWrapper reverts when vault is 0 address", async function () { - const vaultAddress = ethers.ZeroAddress; - const newValue = ethers.Wallet.createRandom().address; - await expect(mellowRestaker.changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWithCustomError( - mellowRestaker, - "ZeroAddress", - ); - }); - - it("changeMellowWrapper reverts when wrapper is 0 address", async function () { - const vaultAddress = mellowVaults[0].vaultAddress; - const newValue = ethers.ZeroAddress; - await expect(mellowRestaker.changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWithCustomError( - mellowRestaker, - "ZeroAddress", - ); - }); - - it("changeMellowWrapper reverts when vault is unknown", async function () { - const vaultAddress = mellowVaults[2].vaultAddress; - const newValue = mellowVaults[2].wrapperAddress; - await expect(mellowRestaker.changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWithCustomError( - mellowRestaker, - "NoWrapperExists", - ); - }); - - it("changeMellowWrapper reverts when called by not an owner", async function () { - const vaultAddress = mellowVaults[0].vaultAddress; - const newValue = ethers.Wallet.createRandom().address; - await expect(mellowRestaker.connect(staker).changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWith( - "Ownable: caller is not the owner", - ); - }); - }); - - describe("undelegateFromMellow: request withdrawal from mellow vault", function () { - let ratio, ratioDiff, totalDeposited, assets1, assets2, rewards, vault1Delegated, vault2Delegated; - - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - totalDeposited = 10n * e18; - await iVault.connect(staker).deposit(totalDeposited, staker.address); - }); - - it("Delegate to mellowVault#1", async function () { - vault1Delegated = (await iVault.getFreeBalance()) / 2n; - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, vault1Delegated, 1296000); - - expect(await mellowRestaker.getDeposited(mellowVaults[0].vaultAddress)).to.be.closeTo( - vault1Delegated, - transactErr, - ); - }); - - it("Add mellowVault#2 and delegate the rest", async function () { - await mellowRestaker.addMellowVault(mellowVaults[1].vaultAddress, mellowVaults[1].wrapperAddress); - vault2Delegated = await iVault.getFreeBalance(); - - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[1].vaultAddress, vault2Delegated, 1296000); - - expect(await mellowRestaker.getDeposited(mellowVaults[1].vaultAddress)).to.be.closeTo( - vault2Delegated, - transactErr, - ); - expect(await mellowRestaker.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr * 2n); - expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); - }); - - it("Staker withdraws shares1", async function () { - assets1 = e18; - const shares = await iVault.convertToShares(assets1); - console.log(`Staker is going to withdraw:\t${assets1.format()}`); - await iVault.connect(staker).withdraw(shares, staker.address); - console.log(`Staker's pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - }); - - it("undelegateFromMellow from mellowVault#1 by operator", async function () { - const totalDelegatedBefore = await iVault.getTotalDelegated(); - const pendingWithdrawalsBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const ratioBefore = await calculateRatio(iVault, iToken); - - await expect( - iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[0].vaultAddress, assets1, 1296000), - ) - .to.emit(iVault, "StartMellowWithdrawal") - .withArgs(mellowRestaker.address, amount => { - expect(amount).to.be.closeTo(assets1, transactErr); - return true; - }); - - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const pendingWithdrawalsAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - const vault1DelegatedAfter = await mellowRestaker.getDeposited(mellowVaults[0].vaultAddress); - const withdrawRequest = await mellowRestaker.pendingMellowRequest(mellowVaults[0].vaultAddress); - const ratioAfter = await calculateRatio(iVault, iToken); - - expect(totalDelegatedBefore - totalDelegatedAfter).to.be.closeTo(assets1, transactErr); - expect(pendingWithdrawalsAfter - pendingWithdrawalsBefore).to.be.closeTo(assets1, transactErr); - expect(vault1DelegatedAfter).to.be.closeTo(vault1Delegated - assets1, transactErr); - expect(withdrawRequest.to).to.be.eq(mellowRestaker.address); - expect(withdrawRequest.timestamp).to.be.eq((await ethers.provider.getBlock("latest")).timestamp); - expect(ratioAfter).to.be.closeTo(ratioBefore, 1n); - }); - - it("Adding rewards to mellowVault#1 increases pending withdrawal respectively", async function () { - const pendingMellowWithdrawalsBefore = await mellowRestaker.pendingWithdrawalAmount(); - const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const vault1DelegatedBefore = await mellowRestaker.getDeposited(mellowVaults[0].vaultAddress); - const ratioBefore = await iVault.ratio(); - - //Add rewards - await a.addRewardsMellowVault(10n * e18, mellowVaults[0].vaultAddress); - const vault1DelegatedAfter = await mellowRestaker.getDeposited(mellowVaults[0].vaultAddress); - const pendingMellowWithdrawalsAfter = await mellowRestaker.pendingWithdrawalAmount(); - rewards = - vault1DelegatedAfter + pendingMellowWithdrawalsAfter - vault1DelegatedBefore - pendingMellowWithdrawalsBefore; - vault1Delegated += rewards; - totalDeposited += rewards; - //Update ratio - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - ratioDiff = ratioBefore - ratio; - - const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - expect((pendingMellowWithdrawalsBefore * vault1DelegatedAfter) / vault1DelegatedBefore).to.be.closeTo( - pendingMellowWithdrawalsAfter, - transactErr, - ); - expect((totalPendingMellowWithdrawalsBefore * vault1DelegatedAfter) / vault1DelegatedBefore).to.be.closeTo( - totalPendingMellowWithdrawalsAfter, - transactErr, - ); - expect(totalDeposited).to.be.closeTo(await iVault.getTotalDeposited(), transactErr); - }); - - it("Staker withdraws shares2 to Staker2", async function () { - assets2 = e18; - const shares = await iVault.convertToShares(assets2); - console.log(`Staker is going to withdraw:\t${assets2.format()}`); - await iVault.connect(staker).withdraw(shares, staker2.address); - console.log( - `Staker2's pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker2.address)).format()}`, - ); - }); - - it("undelegateFromMellow replaces pending withdraw from mellowVault#1", async function () { - const ratioBeforeUndelegate = await iVault.ratio(); - - const amount = assets2; - await expect(iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[0].vaultAddress, amount, 1296000)) - .to.emit(iVault, "StartMellowWithdrawal") - .withArgs(mellowRestaker.address, a => { - expect(a).to.be.closeTo(amount, transactErr); - return true; - }); - - const pendingMellowWithdrawalsAfter = await mellowRestaker.pendingWithdrawalAmount(); - const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - const ratioAfter = await calculateRatio(iVault, iToken); - - expect(pendingMellowWithdrawalsAfter).to.be.closeTo(amount, transactErr); - expect(totalPendingMellowWithdrawalsAfter).to.be.closeTo(amount, transactErr); - expect(totalDeposited - totalDelegatedAfter).to.be.closeTo(amount, transactErr); - expect(ratioAfter).to.be.closeTo(ratioBeforeUndelegate, ratioErr); - }); - - it("undelegateFromMellow all from mellowVault#2", async function () { - const pendingMellowWithdrawalsBefore = await mellowRestaker.pendingWithdrawalAmount(); - const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - - //Amount can slightly exceed delegatedTo, but final number will be corrected - //undelegateFromMellow fails when deviation is too big - await expect( - iVault - .connect(iVaultOperator) - .undelegateFromMellow(mellowVaults[1].vaultAddress, vault2Delegated + 1000_000_000n, 1296000), - ) - .to.emit(iVault, "StartMellowWithdrawal") - .withArgs(mellowRestaker.address, a => { - expect(a).to.be.closeTo(vault2Delegated, transactErr); - return true; - }); - - const pendingMellowWithdrawalsAfter = await mellowRestaker.pendingWithdrawalAmount(); - const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalDelegatedAfter = await iVault.getTotalDelegated(); - - expect(pendingMellowWithdrawalsAfter - pendingMellowWithdrawalsBefore).to.be.closeTo( - vault2Delegated, - transactErr, - ); - expect(totalPendingMellowWithdrawalsAfter - totalPendingMellowWithdrawalsBefore).to.be.closeTo( - vault2Delegated, - transactErr, - ); - expect(totalDeposited - totalDelegatedAfter).to.be.closeTo(vault2Delegated + assets2, transactErr); - expect(await iVault.ratio()).to.be.eq(await calculateRatio(iVault, iToken)); - }); - - it("Can not claim when restaker balance is 0", async function () { - await expect(iVault.connect(iVaultOperator).claimCompletedWithdrawalsMellow()).to.be.revertedWithCustomError( - mellowRestaker, - "ValueZero", - ); - }); - - it("Process pending withdrawal from mellowVault#1 to mellowRestaker", async function () { - const restakerBalanceBefore = await mellowRestaker.claimableAmount(); - const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalDepositedBefore = await iVault.getTotalDeposited(); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Pending from Mellow before:\t\t${totalPendingMellowWithdrawalsBefore.format()}`); - - await mellowVaults[0].curator.processWithdrawals([mellowRestaker.address]); - - const restakerBalanceAfter = await mellowRestaker.claimableAmount(); - const pendingMellowWithdrawalsAfter = await mellowRestaker.pendingWithdrawalAmount(); - const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalDepositedAfter = await iVault.getTotalDeposited(); - console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); - console.log(`Pending from Mellow:\t\t\t${totalPendingMellowWithdrawalsAfter.format()}`); - console.log(`Restaker balance diff:\t\t\t${(restakerBalanceAfter - restakerBalanceBefore).format()}`); - - expect(restakerBalanceAfter - restakerBalanceBefore).to.be.closeTo(assets2, transactErr); - expect(pendingMellowWithdrawalsAfter).to.be.closeTo(vault2Delegated, transactErr); - expect(totalPendingMellowWithdrawalsAfter).to.be.closeTo(totalPendingMellowWithdrawalsBefore, transactErr); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(await iVault.ratio()).to.be.eq(await calculateRatio(iVault, iToken)); - }); - - it("Process pending withdrawal from mellowVault#2 to mellowRestaker", async function () { - const restakerBalanceBefore = await mellowRestaker.claimableAmount(); - const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalDepositedBefore = await iVault.getTotalDeposited(); - console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); - console.log(`Pending from Mellow before:\t\t${totalPendingMellowWithdrawalsBefore.format()}`); - - await mellowVaults[1].curator.processWithdrawals([mellowRestaker.address]); - - const restakerBalanceAfter = await mellowRestaker.claimableAmount(); - const pendingMellowWithdrawalsAfter = await mellowRestaker.pendingWithdrawalAmount(); - const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawalAmountFromMellow(); - const totalDepositedAfter = await iVault.getTotalDeposited(); - console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); - console.log(`Pending from Mellow:\t\t\t${totalPendingMellowWithdrawalsAfter.format()}`); - console.log(`Restaker balance diff:\t\t\t${(restakerBalanceAfter - restakerBalanceBefore).format()}`); - - expect(restakerBalanceAfter - restakerBalanceBefore).to.be.closeTo(vault2Delegated, transactErr); - expect(pendingMellowWithdrawalsAfter).to.be.eq(0n); - expect(totalPendingMellowWithdrawalsAfter).to.be.eq(totalPendingMellowWithdrawalsBefore); - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(await iVault.ratio()).to.be.eq(await calculateRatio(iVault, iToken)); - }); - - it("Can not claim funds from mellowRestaker when iVault is paused", async function () { - await iVault.pause(); - await expect(iVault.connect(iVaultOperator).claimCompletedWithdrawalsMellow()).to.be.revertedWith( - "Pausable: paused", - ); - }); - - it("Claim funds from mellowRestaker to iVault", async function () { - if (await iVault.paused()) { - await iVault.unpause(); - } - const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawalAmountFromMellow(); - const usersTotalWithdrawals = await iVault.totalAmountToWithdraw(); - const totalAssetsBefore = await iVault.totalAssets(); - const freeBalanceBefore = await iVault.getFreeBalance(); - - await iVault.connect(iVaultOperator).claimCompletedWithdrawalsMellow(); - console.log("getTotalDelegated", await iVault.getTotalDelegated()); - console.log("totalAssets", await iVault.totalAssets()); - console.log("getPendingWithdrawalAmountFromMellow", await iVault.getPendingWithdrawalAmountFromMellow()); - console.log("redeemReservedAmount", await iVault.redeemReservedAmount()); - console.log("depositBonusAmount", await iVault.depositBonusAmount()); - - const totalAssetsAfter = await iVault.totalAssets(); - const restakerBalanceAfter = await mellowRestaker.claimableAmount(); - const freeBalanceAfter = await iVault.getFreeBalance(); - - expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(totalPendingMellowWithdrawalsBefore, transactErr); - expect(restakerBalanceAfter).to.be.eq(0n, transactErr); - //Withdraw leftover goes to freeBalance - expect(freeBalanceAfter - freeBalanceBefore).to.be.closeTo( - totalPendingMellowWithdrawalsBefore - usersTotalWithdrawals, - transactErr, - ); - - console.log("vault ratio:", await iVault.ratio()); - console.log("calculated ratio:", await calculateRatio(iVault, iToken)); - - expect(await iVault.ratio()).to.be.eq(await calculateRatio(iVault, iToken)); - }); - - it("Staker is able to redeem", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - }); - - it("Staker2 is able to redeem", async function () { - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; - }); - - it("Staker redeems withdrawals", async function () { - const stakerBalanceBefore = await asset.balanceOf(staker.address); - const stakerPWBefore = await iVault.getPendingWithdrawalOf(staker.address); - - await iVault.redeem(staker.address); - const stakerBalanceAfter = await asset.balanceOf(staker.address); - const stakerPWAfter = await iVault.getPendingWithdrawalOf(staker.address); - - console.log(`Staker balance after: ${stakerBalanceAfter.format()}`); - console.log(`Staker pending withdrawals after: ${stakerPWAfter.format()}`); - - expect(stakerPWBefore - stakerPWAfter).to.be.closeTo(assets1, transactErr * 2n); - expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(assets1, transactErr * 2n); - expect(await iVault.ratio()).to.be.closeTo(await calculateRatio(iVault, iToken), 1n); - }); - }); - - describe("undelegateFromMellow: negative cases", function () { - beforeEach(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit(randomBI(19), staker.address); - const freeBalance = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, freeBalance, 1296000); - console.log(`Delegated amount: \t${freeBalance.format()}`); - }); - - const invalidArgs = [ - { - name: "amount is 0", - amount: async () => 0n, - mellowVault: async () => mellowVaults[0].vaultAddress, - operator: () => iVaultOperator, - customError: "ValueZero", - source: () => mellowRestaker, - }, - { - name: "amount > delegatedTo", - amount: async () => (await iVault.getDelegatedTo(mellowVaults[0].vaultAddress)) + e18, - mellowVault: async () => mellowVaults[0].vaultAddress, - operator: () => iVaultOperator, - customError: "BadMellowWithdrawRequest", - source: () => mellowRestaker, - }, - { - name: "mellowVault is unregistered", - amount: async () => await iVault.getDelegatedTo(mellowVaults[0].vaultAddress), - mellowVault: async () => mellowVaults[1].vaultAddress, - operator: () => iVaultOperator, - customError: "InvalidVault", - source: () => mellowRestaker, - }, - { - name: "mellowVault is 0 address", - amount: async () => await iVault.getDelegatedTo(mellowVaults[0].vaultAddress), - mellowVault: async () => ethers.ZeroAddress, - operator: () => iVaultOperator, - customError: "InvalidAddress", - source: () => iVault, - }, - { - name: "called by not an operator", - amount: async () => await iVault.getDelegatedTo(mellowVaults[0].vaultAddress), - mellowVault: async () => mellowVaults[0].vaultAddress, - operator: () => staker, - customError: "OnlyOperatorAllowed", - source: () => iVault, - }, - ]; - - invalidArgs.forEach(function (arg) { - it(`Reverts: when ${arg.name}`, async function () { - const amount = await arg.amount(); - const mellowVault = await arg.mellowVault(); - console.log(`Undelegate amount: \t${amount.format()}`); - if (arg.customError) { - await expect( - iVault.connect(arg.operator()).undelegateFromMellow(mellowVault, amount, 1296000), - ).to.be.revertedWithCustomError(arg.source(), arg.customError); - } else { - await expect( - iVault.connect(arg.operator()).undelegateFromMellow(mellowVault, amount, 1296000), - ).to.be.revertedWith(arg.error); - } - }); - }); - - it("Reverts: undelegate when iVault is paused", async function () { - const amount = randomBI(17); - await iVault.pause(); - await expect( - iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[0].vaultAddress, amount, 1296000), - ).to.be.revertedWith("Pausable: paused"); - await iVault.unpause(); - }); - - it("Reverts: undelegate when mellowRestaker is paused", async function () { - if (await iVault.paused()) { - await iVault.unpause(); - } - - const amount = randomBI(17); - await mellowRestaker.pause(); - await expect( - iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[0].vaultAddress, amount, 1296000), - ).to.be.revertedWith("Pausable: paused"); - }); - }); - - /** - * Forces execution of pending withdrawal, - * if configurator.emergencyWithdrawalDelay() has passed since its creation - * but not later than fulfill deadline. - */ - // describe("undelegateForceFrom", function () { - // let delegated; - // let emergencyWithdrawalDelay; - // let mVault, configurator; - - // before(async function () { - // await snapshot.restore(); - // await iVault.setTargetFlashCapacity(1n); - // await iVault.connect(staker).deposit(10n * e18, staker.address); - // delegated = await iVault.getFreeBalance(); - // await mellowRestaker.addMellowVault(mellowVaults[2].vaultAddress, mellowVaults[2].wrapperAddress); - // await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[2].vaultAddress, delegated, 1296000); - // console.log(`Delegated amount: \t${delegated.format()}`); - - // mVault = await ethers.getContractAt("IMellowVault", mellowVaults[2].vaultAddress); - // configurator = await ethers.getContractAt("IMellowVaultConfigurator", mellowVaults[2].configuratorAddress); - // emergencyWithdrawalDelay = (await configurator.emergencyWithdrawalDelay()) / day; - // }); - - // it("undelegateForceFrom reverts when there is no pending withdraw request", async function () { - // await expect( - // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), - // ).to.be.revertedWithCustomError(mVault, "InvalidState"); - // }); - - // it("set request deadline > emergencyWithdrawalDelay", async function () { - // const newDeadline = emergencyWithdrawalDelay + 10n; //~ 100d - // await mellowRestaker.setRequestDeadline(newDeadline); - // console.log("New request deadline in days:", (await mellowRestaker.requestDeadline()) / day); - // expect(await mellowRestaker.requestDeadline()).to.be.eq(newDeadline * day); - // }); - - // it("undelegateForceFrom reverts when it is less than emergencyWithdrawalDelay has passed since submission", async function () { - // await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[2].vaultAddress, delegated / 2n, 1296000); - // await helpers.time.increase((emergencyWithdrawalDelay - 1n) * day); - - // await expect( - // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), - // ).to.be.revertedWithCustomError(mVault, "InvalidState"); - // }); - - // it("undelegateForceFrom cancels expired request", async function () { - // await helpers.time.increase(12n * day); //Wait until request expired - - // const tx = await iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000); - - // await expect(tx).to.emit(mVault, "WithdrawalRequestCanceled").withArgs(mellowRestaker.address, anyValue); - // await expect(await mellowRestaker.getDeposited(mellowVaults[2].vaultAddress)).to.be.closeTo( - // delegated, - // transactErr, - // ); - // await expect(await mellowRestaker.pendingWithdrawalAmount()).to.be.eq(0n); - // }); - - // it("undelegateForceFrom reverts if it can not provide min amount", async function () { - // await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[2].vaultAddress, e18, 1296000); - // await helpers.time.increase(emergencyWithdrawalDelay * day + 1n); - - // await expect( - // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), - // ).to.be.revertedWithCustomError(mVault, "InsufficientAmount"); - // }); - - // it("undelegateForceFrom reverts when called by not an operator", async function () { - // await expect( - // iVault.connect(staker).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), - // ).to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); - // }); - - // it("withdrawEmergencyMellow reverts when called by not a trustee", async function () { - // await expect( - // mellowRestaker.connect(staker).withdrawEmergencyMellow(mellowVaults[0].vaultAddress, 1296000), - // ).to.revertedWithCustomError(mellowRestaker, "NotVaultOrTrusteeManager"); - // }); - - // it("undelegateForceFrom reverts when iVault is paused", async function () { - // await iVault.pause(); - // await expect( - // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), - // ).to.be.revertedWith("Pausable: paused"); - // }); - - // it("undelegateForceFrom reverts when mellowRestaker is paused", async function () { - // if (await iVault.paused()) { - // await iVault.unpause(); - // } - - // await mellowRestaker.pause(); - // await expect( - // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), - // ).to.be.revertedWith("Pausable: paused"); - // }); - - // it("undelegateForceFrom withdraws all from mellow vault when there is suitable request", async function () { - // if (await mellowRestaker.paused()) { - // await mellowRestaker.unpause(); - // } - - // const newSlippage = 3_000; //30% - // await mellowRestaker.setSlippages(newSlippage, newSlippage); - - // //!!!_Test fails because slippage is too high - // await iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000); - - // expect(await asset.balanceOf(mellowRestaker.address)).to.be.gte(0n); - // expect(await mellowRestaker.pendingWithdrawalAmount()).to.be.eq(0n); - // }); - // }); - - describe("Redeem: retrieves assets after they were received from Mellow", function () { - let ratio, stakerAmount, staker2Amount, stakerUnstakeAmount1, stakerUnstakeAmount2, staker2UnstakeAmount; - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker3).deposit(e18, staker3.address); - await iVault - .connect(iVaultOperator) - .delegateToMellowVault(mellowVaults[0].vaultAddress, await iVault.getFreeBalance(), 1296000); - await ratioFeed.updateRatioBatch([iToken.address], [await calculateRatio(iVault, iToken)]); - ratio = await iVault.ratio(); - }); - - it("Deposit and Delegate partially", async function () { - stakerAmount = 9_399_680_561_290_658_040n; - await iVault.connect(staker).deposit(stakerAmount, staker.address); - staker2Amount = 1_348_950_494_309_030_813n; - await iVault.connect(staker2).deposit(staker2Amount, staker2.address); - - const delegated = (await iVault.getFreeBalance()) - e18; - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, delegated, 1296000); - - await ratioFeed.updateRatioBatch([iToken.address], [await calculateRatio(iVault, iToken)]); - console.log(`Staker amount: ${stakerAmount}`); - console.log(`Staker2 amount: ${staker2Amount}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker has nothing to claim yet", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; - }); - - it("Staker withdraws half of their shares", async function () { - const shares = await iToken.balanceOf(staker.address); - stakerUnstakeAmount1 = shares / 2n; - await iVault.connect(staker).withdraw(stakerUnstakeAmount1, staker.address); - await ratioFeed.updateRatioBatch([iToken.address], [await calculateRatio(iVault, iToken)]); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it("Staker is not able to redeem yet", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; - }); - - it("updateEpoch can not unlock withdrawals without enough freeBalance", async function () { - const redeemReserveBefore = await iVault.redeemReservedAmount(); - const freeBalanceBefore = await iVault.getFreeBalance(); - const epochBefore = await iVault.epoch(); - await iVault.connect(iVaultOperator).updateEpoch(); - - const redeemReserveAfter = await iVault.redeemReservedAmount(); - const freeBalanceAfter = await iVault.getFreeBalance(); - const epochAfter = await iVault.epoch(); - - expect(redeemReserveAfter).to.be.eq(redeemReserveBefore); - expect(freeBalanceAfter).to.be.eq(freeBalanceBefore); - expect(epochAfter).to.be.eq(epochBefore); - }); - - it("Withdraw from mellowVault amount = pending withdrawals", async function () { - const redeemReserveBefore = await iVault.redeemReservedAmount(); - const freeBalanceBefore = await iVault.getFreeBalance(); - const amount = await iVault.totalAmountToWithdraw(); - - await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, amount); - const redeemReserveAfter = await iVault.redeemReservedAmount(); - const freeBalanceAfter = await iVault.getFreeBalance(); - await ratioFeed.updateRatioBatch([iToken.address], [await calculateRatio(iVault, iToken)]); - console.log(`Total assets:\t\t${(await iVault.totalAssets()).format()}`); - console.log(`Pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - - expect(redeemReserveAfter - redeemReserveBefore).to.be.closeTo(amount, transactErr); - expect(freeBalanceAfter).to.be.eq(freeBalanceBefore); - }); - - it("Staker is now able to redeem", async function () { - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; - }); - - it("Redeem reverts when iVault is paused", async function () { - await iVault.pause(); - await expect(iVault.connect(iVaultOperator).redeem(staker.address)).to.be.revertedWith("Pausable: paused"); - }); - - it("Unpause after previous test", async function () { - await iVault.unpause(); - }); - - it("Staker2 withdraws < freeBalance", async function () { - staker2UnstakeAmount = (await iVault.getFreeBalance()) - 1000_000_000n; - await iVault.connect(staker2).withdraw(staker2UnstakeAmount, staker2.address); - }); - - it("Staker2 can not claim the same epoch even if freeBalance is enough", async function () { - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.false; - }); - - it("Staker is still able to claim", async function () { - const ableRedeem = await iVault.isAbleToRedeem(staker.address); - expect(ableRedeem[0]).to.be.true; - expect([...ableRedeem[1]]).to.have.members([0n]); - }); - - it("Redeem reverts when pending withdrawal is not covered", async function () { - await expect(iVault.connect(iVaultOperator).redeem(staker2.address)).to.be.revertedWithCustomError( - iVault, - "IsNotAbleToRedeem", - ); - }); - - it("Stakers new withdrawal goes to the end of queue", async function () { - stakerUnstakeAmount2 = (await iToken.balanceOf(staker.address)) / 2n; - await iVault.connect(staker).withdraw(stakerUnstakeAmount2, staker.address); - - const newQueuedWithdrawal = await iVault.claimerWithdrawalsQueue(2); - console.log(`Pending withdrawals: ${await iVault.getPendingWithdrawalOf(staker.address)}`); - console.log(`Unstake amount: ${stakerUnstakeAmount2.toString()}`); - console.log(`Ratio: ${await calculateRatio(iVault, iToken)}`); - - expect(newQueuedWithdrawal.epoch).to.be.eq(2n); //queue length - 1 - expect(newQueuedWithdrawal.receiver).to.be.eq(staker.address); - expect(newQueuedWithdrawal.amount).to.be.closeTo( - await iVault.convertToAssets(stakerUnstakeAmount2), - transactErr, - ); - }); - - it("Staker is still able to redeem the 1st withdrawal", async function () { - const ableRedeem = await iVault.isAbleToRedeem(staker.address); - expect(ableRedeem[0]).to.be.true; - expect([...ableRedeem[1]]).to.have.members([0n]); - }); - - it("updateEpoch unlocks pending withdrawals in order they were submitted", async function () { - const staker2Pending = await iVault.getPendingWithdrawalOf(staker2.address); - const redeemReserveBefore = await iVault.redeemReservedAmount(); - const freeBalanceBefore = await iVault.getFreeBalance(); - const epochBefore = await iVault.epoch(); - await iVault.connect(iVaultOperator).updateEpoch(); - - const redeemReserveAfter = await iVault.redeemReservedAmount(); - const freeBalanceAfter = await iVault.getFreeBalance(); - const epochAfter = await iVault.epoch(); - - expect(redeemReserveAfter - redeemReserveBefore).to.be.closeTo(staker2Pending, transactErr); - expect(freeBalanceBefore - freeBalanceAfter).to.be.closeTo(staker2Pending, transactErr); - expect(epochAfter).to.be.eq(epochBefore + 1n); - }); - - it("Staker2 is able to claim", async function () { - const ableRedeem = await iVault.isAbleToRedeem(staker2.address); - expect(ableRedeem[0]).to.be.true; - expect([...ableRedeem[1]]).to.have.members([1n]); - }); - - it("Staker is able to claim only the 1st wwl", async function () { - const ableRedeem = await iVault.isAbleToRedeem(staker.address); - expect(ableRedeem[0]).to.be.true; - expect([...ableRedeem[1]]).to.have.members([0n]); - }); - - it("Staker redeems withdrawals", async function () { - const stakerBalanceBefore = await asset.balanceOf(staker.address); - const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker.address); - const stakerRedeemedAmount = await iVault.convertToAssets(stakerUnstakeAmount1); - const stakerPendingAmount = await iVault.convertToAssets(stakerUnstakeAmount2); - - await iVault.connect(staker).redeem(staker.address); - const stakerBalanceAfter = await asset.balanceOf(staker.address); - const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker.address); - - console.log(`Staker balance after: ${stakerBalanceAfter}`); - console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); - console.log(`stakerUnstakeAmountAssetValue: ${stakerRedeemedAmount}`); - console.log(`stakerPendingWithdrawalsBefore[0]: ${stakerPendingWithdrawalsBefore}`); - - expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( - stakerRedeemedAmount, - transactErr, - ); - expect(stakerPendingWithdrawalsAfter).to.be.closeTo(stakerPendingAmount, transactErr); - expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(stakerRedeemedAmount, transactErr); - expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; - expect(await iVault.ratio()).to.be.closeTo(await calculateRatio(iVault, iToken), ratioErr); - }); - - it("Staker2 redeems withdrawals", async function () { - const stakerBalanceBefore = await asset.balanceOf(staker2.address); - const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker2.address); - - await iVault.connect(staker2).redeem(staker2.address); - const stakerBalanceAfter = await asset.balanceOf(staker2.address); - const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker2.address); - - console.log(`Staker balance after: ${stakerBalanceAfter}`); - console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); - const stakerUnstakeAmountAssetValue = await iVault.convertToAssets(staker2UnstakeAmount); - expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( - stakerUnstakeAmountAssetValue, - transactErr * 2n, - ); - expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(stakerUnstakeAmountAssetValue, transactErr * 2n); - expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.false; - expect(await iVault.ratio()).to.be.closeTo(await calculateRatio(iVault, iToken), ratioErr); - }); - }); - - describe("Redeem: to the different addresses", function () { - let ratio, recipients, pendingShares; - - before(async function () { - await snapshot.restore(); - await iVault.setTargetFlashCapacity(1n); - await iVault.connect(staker).deposit("9292557565124725653", staker.address); - const amount = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000); - }); - - const count = 3; - for (let j = 0; j < count; j++) { - it(`${j} Withdraw to 5 random addresses`, async function () { - recipients = []; - pendingShares = 0n; - for (let i = 0; i < 5; i++) { - const recipient = randomAddress(); - const shares = randomBI(17); - pendingShares = pendingShares + shares; - await iVault.connect(staker).withdraw(shares, recipient); - recipients.push(recipient); - } - }); - - it(`${j} Withdraw from EL and update ratio`, async function () { - const amount = await iVault.totalAmountToWithdraw(); - await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[0].vaultAddress, amount, 1296000); - - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - console.log(`New ratio is: ${ratio}`); - - await mellowVaults[0].curator.processWithdrawals([mellowRestaker.address]); - await iVault.connect(iVaultOperator).claimCompletedWithdrawalsMellow(); - console.log(`Total assets: ${await iVault.totalAssets()}`); - console.log(`Total withdrawn shares to assets ${await iVault.convertToAssets(pendingShares)}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it(`${j} Recipients claim`, async function () { - for (const r of recipients) { - const rBalanceBefore = await asset.balanceOf(r); - const rPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(r); - await iVault.connect(deployer).redeem(r); - const rBalanceAfter = await asset.balanceOf(r); - const rPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(r); - - expect(rBalanceAfter - rPendingWithdrawalsBefore).to.be.closeTo(0, transactErr); - expect(rBalanceBefore - rPendingWithdrawalsAfter).to.be.closeTo(0, transactErr); - } - expect(await iVault.ratio()).to.be.lte(ratio); - console.log(`Total assets: ${await iVault.totalAssets()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - }); - - it(`${j} Deposit extra from iVault`, async function () { - const totalDepositedBefore = await iVault.getTotalDeposited(); - - const amount = await iVault.getFreeBalance(); - await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[0].vaultAddress, amount, 1296000); - const totalDepositedAfter = await iVault.getTotalDeposited(); - - console.log(`Total assets: ${await iVault.totalAssets()}`); - console.log(`Ratio: ${await iVault.ratio()}`); - - expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); - expect(await iVault.totalAssets()).to.be.lte(100); - expect(await iVault.ratio()).to.be.lte(ratio); - }); - } - - it("Update asset ratio and withdraw the rest", async function () { - await a.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); - const calculatedRatio = await calculateRatio(iVault, iToken); - await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); - ratio = await iVault.ratio(); - console.log(`New ratio is: ${ratio}`); - - //Withdraw all and take from EL - const shares = await iToken.balanceOf(staker.address); - await iVault.connect(staker).withdraw(shares, staker.address); - const amount = await iVault.getTotalDelegated(); - await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, amount); - await iVault.connect(iVaultOperator).redeem(staker.address); - - console.log(`iVault total assets: ${await iVault.totalAssets()}`); - console.log(`Total deposited: ${await iVault.getTotalDeposited()}`); - }); - }); - }); -}); - diff --git a/projects/vaults/test/InceptionVault_S_EL.ts b/projects/vaults/test/InceptionVault_S_EL.ts new file mode 100644 index 00000000..c9e5a494 --- /dev/null +++ b/projects/vaults/test/InceptionVault_S_EL.ts @@ -0,0 +1,1026 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import hardhat from "hardhat"; +import { wstETH } from "./data/assets/stETH"; +import { vaults } from "./data/vaults"; +import { + addRewardsToStrategy, + calculateRatio, + e18, + mineBlocks, + toWei, +} from "./helpers/utils"; +import { adapters, emptyBytes } from "./src/constants"; +import { abi, initVault } from "./src/init-vault"; + +const { ethers, upgrades, network } = hardhat; +const assetData = wstETH; +const eigenLayerVaults = vaults.eigenLayer; + +describe(`Inception Symbiotic Vault ${assetData.assetName}`, function() { + const coder = abi; + const encodedSignatureWithExpiry = coder.encode( + ["tuple(uint256 expiry, bytes signature)"], [{ expiry: 0, signature: ethers.ZeroHash }], + ); + const delegateData = [ethers.ZeroHash, encodedSignatureWithExpiry]; + + let iToken, iVault, ratioFeed, asset, eigenLayerAdapter, withdrawalQueue; + let iVaultOperator, deployer, staker, staker2, staker3, treasury; + let ratioErr, transactErr; + let snapshot; + + before(async function() { + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: assetData.url ? assetData.url : network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, ratioFeed, asset, iVaultOperator, eigenLayerAdapter, withdrawalQueue } = + await initVault(assetData, { + adapters: [adapters.EigenLayer], + eigenAdapterContractName: "InceptionEigenAdapter", + })); + + ratioErr = assetData.ratioErr; + transactErr = assetData.transactErr; + + [deployer, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + treasury = await iVault.treasury(); //deployer + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function() { + if (iVault) { + await iVault.removeAllListeners(); + } + }); + + describe("InceptionEigenAdapter", function() { + let adapter, iVaultMock, trusteeManager; + + beforeEach(async function() { + await snapshot.restore(); + iVaultMock = staker2; + trusteeManager = staker3; + + console.log(`iVaultMock balance of asset after: ${await asset.balanceOf(iVaultMock.address)}`); + console.log(`trusteeManager balance of asset after: ${await asset.balanceOf(trusteeManager.address)}`); + + const InceptionEigenAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapter", iVaultMock); + adapter = await upgrades.deployProxy(InceptionEigenAdapterFactory, [ + await deployer.getAddress(), + assetData.rewardsCoordinator, + assetData.delegationManager, + assetData.strategyManager, + assetData.assetStrategy, + assetData.assetAddress, + trusteeManager.address, + iVault.address, + ]); + }); + + it("getOperatorAddress: equals 0 address before any delegation", async function() { + expect(await adapter.getOperatorAddress()).to.be.eq(ethers.ZeroAddress); + }); + + it("getOperatorAddress: reverts when _data length is < 2", async function() { + const amount = toWei(0); + console.log(`asset address: ${await asset.balanceOf(trusteeManager.address)}`); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await expect(adapter.connect(trusteeManager).delegate(eigenLayerVaults[0], amount, [])).to.be.revertedWithCustomError(adapter, "InvalidDataLength"); + }); + + it("getOperatorAddress: equals operator after delegation", async function() { + console.log(`asset address: ${await asset.balanceOf(trusteeManager.address)}`); + await adapter.connect(trusteeManager).delegate(eigenLayerVaults[0], 0n, delegateData); + expect(await adapter.getOperatorAddress()).to.be.eq(eigenLayerVaults[0]); + }); + + it("delegateToOperator: reverts when called by not a trustee", async function() { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, []); + + await expect( + adapter.connect(staker).delegate(eigenLayerVaults[0], 0n, delegateData), + ).to.be.revertedWithCustomError(adapter, "NotVaultOrTrusteeManager"); + }); + + it("delegateToOperator: reverts when delegates to 0 address", async function() { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, []); + + await expect( + adapter.connect(trusteeManager).delegate(ethers.ZeroAddress, 0n, delegateData), + ).to.be.revertedWithCustomError(adapter, "NullParams"); + }); + + it("delegateToOperator: reverts when delegates unknown operator", async function() { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, delegateData); + + const unknownOperator = ethers.Wallet.createRandom().address; + await expect(adapter.connect(trusteeManager) + .delegate(unknownOperator, 0n, delegateData)) + .to.be.revertedWithCustomError(iVault, "OperatorNotRegistered"); + }); + + it("withdrawFromEL: reverts when called by not a trustee", async function() { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, delegateData); + await adapter.connect(trusteeManager).delegate(eigenLayerVaults[0], 0n, delegateData); + + await expect(adapter.connect(staker).withdraw(ZeroAddress, amount / 2n, [], false)).to.be.revertedWithCustomError( + adapter, + "NotVaultOrTrusteeManager", + ); + }); + + it("getVersion: equals 3", async function() { + expect(await adapter.getVersion()).to.be.eq(3); + }); + + it("pause(): only owner can", async function() { + expect(await adapter.paused()).is.false; + await adapter.connect(iVaultMock).pause(); + expect(await adapter.paused()).is.true; + }); + + it("pause(): another address can not", async function() { + await expect(adapter.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("unpause(): only owner can", async function() { + await adapter.connect(iVaultMock).pause(); + expect(await adapter.paused()).is.true; + + await adapter.connect(iVaultMock).unpause(); + expect(await adapter.paused()).is.false; + }); + + it("unpause(): another address can not", async function() { + await adapter.connect(iVaultMock).pause(); + expect(await adapter.paused()).is.true; + await expect(adapter.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("EigenLayer | Base flow no flash", function() { + let delegatedEL = 0n; + let tx; + let undelegateEpoch; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("Initial stats", async function() { + expect(await iVault.ratio()).to.be.eq(e18); + expect(await iVault.totalAssets()).to.be.eq(0n); + expect(await iVault.getTotalDeposited()).to.be.eq(0n); + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + expect(await iVault.getFlashCapacity()).to.be.eq(0n); + expect(await iVault.getFreeBalance()).to.be.eq(0n); + }); + + it("User can deposit to iVault", async function() { + const totalDeposited = toWei(20); + const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit + const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Delegate to EigenLayer#1", async function() { + const amount = (await iVault.getFreeBalance()) / 3n; + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, amount, []); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, eigenLayerVaults[0], 0n, delegateData); + + delegatedEL += amount; + }); + + it("Delegate all to eigenOperator#1", async function() { + const amount = await iVault.getFreeBalance(); + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, amount, []); + delegatedEL += amount; + }); + + it("Update ratio", async function() { + const ratio = await iVault.ratio(); + console.log(`Calculated ratio:\t\t\t${ratio.format()}`); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + console.log(`iVault ratio:\t\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).eq(ratio); + }); + + it("Update asset ratio", async function() { + console.log("totalDelegatedBefore", await iVault.getTotalDelegated()); + await addRewardsToStrategy(assetData.assetStrategy, e18, staker3); + console.log("totalDelegatedAfter", await iVault.getTotalDelegated()); + const ratio = await iVault.ratio(); + console.log(`Calculated ratio:\t\t\t${ratio.format()}`); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + console.log(`New ratio is:\t\t\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).lt(e18); + }); + + it("User can withdraw all", async function() { + const shares = await iToken.balanceOf(staker.address); + const assetValue = await iVault.convertToAssets(shares); + console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); + console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); + const tx = await iVault.connect(staker).withdraw(shares, staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(assetValue); + expect(events[0].args["iShares"]).to.be.eq(shares); + + const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); + const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); + + const withdrawalEpoch = await withdrawalQueue.withdrawals(await withdrawalQueue.currentEpoch()); + const totalPW = withdrawalEpoch[1]; + + expect(stakerPW).to.be.eq(0n); + expect(staker2PW).to.be.closeTo(assetValue, transactErr); + expect(totalPW).to.be.closeTo(shares, transactErr); + }); + + // it("Update ratio after all shares burn", async function () { + // const calculatedRatio = await iVault.ratio(); + // console.log(`Calculated ratio:\t\t\t${calculatedRatio.format()}`); + // expect(calculatedRatio).to.be.eq(999999045189759685n); //Because all shares have been burnt at this point + // + // await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + // console.log(`iVault ratio after:\t\t\t${(await iVault.ratio()).format()}`); + // expect(await iVault.ratio()).eq(calculatedRatio); + // }); + + it("Undelegate from EigenLayer", async function() { + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + undelegateEpoch = await withdrawalQueue.currentEpoch(); + + console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); + + tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), + [[eigenLayerAdapter.address, eigenLayerVaults[0], totalDelegatedBefore, []]], + ); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + + console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); + console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); + }); + + it("Claim from EigenLayer", async function() { + const receipt = await tx.wait(); + + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapter"); + let withdrawalQueuedEvent; + receipt.logs.forEach(log => { + try { + const parsedLog = eigenLayerAdapterFactory.interface.parseLog(log); + if (parsedLog) { + console.log("🔹 Event Detected:"); + console.log(parsedLog); + withdrawalQueuedEvent = parsedLog.args; + return; + } + } catch (error) { + } + }); + + const wData = { + staker1: withdrawalQueuedEvent["stakerAddress"], + staker2: eigenLayerVaults[0], + staker3: eigenLayerAdapter.address, + nonce1: withdrawalQueuedEvent["nonce"], + nonce2: withdrawalQueuedEvent["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent["strategy"]], + shares: [withdrawalQueuedEvent["shares"]], + }; + + console.log(wData); + + // Encode the data + const _data = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + + await iVault.connect(iVaultOperator).claim( + undelegateEpoch, [eigenLayerAdapter.address], [eigenLayerVaults[0]], [_data], + ); + + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + + console.log(`Total deposited after claim:\t\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated after claim:\t\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets after claim:\t\t\t${totalAssetsBefore.format()}`); + }); + + it("Staker is able to redeem", async function() { + const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); + const redeemReserve = await iVault.redeemReservedAmount(); + const freeBalance = await iVault.getFreeBalance(); + + console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); + console.log("Redeem reserve", redeemReserve.format()); + console.log("Free balance", freeBalance.format()); + console.log("Redeem reserve after", await iVault.redeemReservedAmount()); + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; + }); + + it("Redeem withdraw", async function() { + const balanceBefore = await asset.balanceOf(staker2.address); + const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); + + console.log(`staker2PWBefore: ${staker2PWBefore.toString()}`); + console.log(`staker2PWBefore: ${(await iVault.redeemReservedAmount()).toString()}`); + console.log(`staker2PWBefore: ${(await asset.balanceOf(iVault.address)).toString()}`); + console.log(`staker2PWBefore: ${(await eigenLayerAdapter.getDepositedShares()).toString()}`); + + const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); + + const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); + const balanceAfter = await asset.balanceOf(staker2.address); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); + console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); + + expect(staker2PWAfter).to.be.eq(0n); + expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); + expect(totalDepositedAfter).to.be.closeTo(0n, transactErr * 3n); + expect(totalAssetsAfter).to.be.closeTo(0n, transactErr * 3n); + }); + }); + + describe("Emergency undelegate", function() { + let undelegateTx; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("Initial stats", async function() { + expect(await iVault.ratio()).to.be.eq(e18); + expect(await iVault.totalAssets()).to.be.eq(0n); + expect(await iVault.getTotalDeposited()).to.be.eq(0n); + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + expect(await iVault.getFlashCapacity()).to.be.eq(0n); + expect(await iVault.getFreeBalance()).to.be.eq(0n); + }); + + it("User can deposit to iVault", async function() { + let totalDeposited = toWei(20); + const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit + const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Delegate to EigenLayer#1", async function() { + const amount = await iVault.getFreeBalance(); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, amount, []); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, eigenLayerVaults[0], 0n, delegateData); + expect(await iVault.getTotalDelegated()).to.be.closeTo(toWei(20), transactErr); + }); + + it("Emergency undelegate", async function() { + undelegateTx = await iVault.connect(iVaultOperator) + .emergencyUndelegate([[eigenLayerAdapter.address, eigenLayerVaults[0], toWei(5), []]]); + + expect(await iVault.getTotalPendingWithdrawals()).to.be.eq(0); + expect(await iVault.getTotalDelegated()).to.be.closeTo(toWei(15), transactErr); + expect(await iVault.getTotalPendingEmergencyWithdrawals()).to.be.closeTo(toWei(5), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + + it("User withdraw", async function() { + const tx = await iVault.connect(staker).withdraw(toWei(2), staker); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(toWei(2)); + expect(events[0].args["iShares"]).to.be.eq(toWei(2)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + + it("Emergency claim", async function() { + const receipt = await undelegateTx.wait(); + + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapter"); + let withdrawalQueuedEvent; + receipt.logs.forEach(log => { + try { + const parsedLog = eigenLayerAdapterFactory.interface.parseLog(log); + if (parsedLog) { + console.log("🔹 Event Detected:"); + withdrawalQueuedEvent = parsedLog.args; + return; + } + } catch (error) { + } + }); + + const wData = { + staker1: withdrawalQueuedEvent["stakerAddress"], + staker2: eigenLayerVaults[0], + staker3: eigenLayerAdapter.address, + nonce1: withdrawalQueuedEvent["nonce"], + nonce2: withdrawalQueuedEvent["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent["strategy"]], + shares: [withdrawalQueuedEvent["shares"]], + }; + + console.log(wData); + + // Encode the data + const _data = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + + await iVault.connect(iVaultOperator).emergencyClaim( + [eigenLayerAdapter.address], [eigenLayerVaults[0]], [_data], + ); + + expect(await asset.balanceOf(iVault.address)).to.be.closeTo(toWei(5), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + + it("Force undelegate & claim", async function() { + await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), []); + + expect(await asset.balanceOf(iVault.address)).to.be.closeTo(toWei(5), transactErr); + expect(await withdrawalQueue.totalAmountRedeem()).to.be.closeTo(toWei(2), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + }); + + it("Redeem", async function() { + const tx = await iVault.connect(staker).redeem(staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(toWei(2), transactErr); + expect(await asset.balanceOf(iVault.address)).to.be.closeTo(toWei(3), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + }); + + describe("Two adapters", function() { + let eigenLayerAdapter2, undelegateEpoch, tx; + let totalDeposited = 0n; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("Add second adapter", async function() { + let [deployer] = await ethers.getSigners(); + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapter"); + + eigenLayerAdapter2 = await upgrades.deployProxy(eigenLayerAdapterFactory, [ + await deployer.getAddress(), + assetData.rewardsCoordinator, + assetData.delegationManager, + assetData.strategyManager, + assetData.assetStrategy, + assetData.assetAddress, + assetData.iVaultOperator, + iVault.address, + ]); + + eigenLayerAdapter2.address = await eigenLayerAdapter2.getAddress(); + await iVault.addAdapter(eigenLayerAdapter2.address); + }); + + it("Initial stats", async function() { + expect(await iVault.ratio()).to.be.eq(e18); + expect(await iVault.totalAssets()).to.be.eq(0n); + expect(await iVault.getTotalDeposited()).to.be.eq(0n); + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + expect(await iVault.getFlashCapacity()).to.be.eq(0n); + expect(await iVault.getFreeBalance()).to.be.eq(0n); + }); + + it("User can deposit to iVault", async function() { + totalDeposited += toWei(20); + const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit + const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Delegate to EigenLayer", async function() { + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, eigenLayerVaults[0], 0n, delegateData); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, toWei(10), []); + + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter2.address, eigenLayerVaults[1], 0n, delegateData); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter2.address, ZeroAddress, toWei(10), []); + + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("User can withdraw all", async function() { + const shares = await iToken.balanceOf(staker.address); + const assetValue = await iVault.convertToAssets(shares); + console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); + console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); + const tx = await iVault.connect(staker).withdraw(shares, staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(assetValue); + expect(events[0].args["iShares"]).to.be.eq(shares); + + const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); + const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); + + const withdrawalEpoch = await withdrawalQueue.withdrawals(await withdrawalQueue.currentEpoch()); + const totalPW = withdrawalEpoch[1]; + + expect(stakerPW).to.be.eq(0n); + expect(staker2PW).to.be.closeTo(assetValue, transactErr); + expect(totalPW).to.be.closeTo(shares, transactErr); + }); + + it("Undelegate from EigenLayer", async function() { + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + + const delegatedTo1 = await iVault.getDelegatedTo(eigenLayerAdapter.address, eigenLayerVaults[0]); + const delegatedTo2 = await iVault.getDelegatedTo(eigenLayerAdapter2.address, eigenLayerVaults[1]); + + undelegateEpoch = await withdrawalQueue.currentEpoch(); + + console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); + console.log(`Delegated to 1:\t\t\t${delegatedTo1.format()}`); + console.log(`Delegated to 2:\t\t\t${delegatedTo2.format()}`); + + tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), + [ + [eigenLayerAdapter.address, eigenLayerVaults[0], delegatedTo1, []], + [eigenLayerAdapter2.address, eigenLayerVaults[1], delegatedTo2, []], + ]); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + + console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); + console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); + + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Claim from EigenLayer", async function() { + const receipt = await tx.wait(); + + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapter"); + let withdrawalQueuedEvent = []; + receipt.logs.forEach(log => { + try { + const parsedLog = eigenLayerAdapterFactory.interface.parseLog(log); + if (parsedLog && parsedLog.name == "StartWithdrawal") { + withdrawalQueuedEvent.push(parsedLog.args); + } + } catch (error) { + } + }); + + const wData = { + staker1: withdrawalQueuedEvent[0]["stakerAddress"], + staker2: eigenLayerVaults[0], + staker3: eigenLayerAdapter.address, + nonce1: withdrawalQueuedEvent[0]["nonce"], + nonce2: withdrawalQueuedEvent[0]["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent[0]["strategy"]], + shares: [withdrawalQueuedEvent[0]["shares"]], + }; + + console.log(wData); + + // Encode the data + const _data = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + const wData2 = { + staker1: withdrawalQueuedEvent[1]["stakerAddress"], + staker2: eigenLayerVaults[1], + staker3: eigenLayerAdapter2.address, + nonce1: withdrawalQueuedEvent[1]["nonce"], + nonce2: withdrawalQueuedEvent[1]["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent[1]["strategy"]], + shares: [withdrawalQueuedEvent[1]["shares"]], + }; + + // Encode the data + const _data2 = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData2]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + + await iVault.connect(iVaultOperator).claim( + undelegateEpoch, + [eigenLayerAdapter.address, eigenLayerAdapter2.address], + [eigenLayerVaults[0], eigenLayerVaults[1]], + [_data, _data2], + ); + + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + + console.log(`Total deposited after claim:\t\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated after claim:\t\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets after claim:\t\t\t${totalAssetsBefore.format()}`); + + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Staker is able to redeem", async function() { + const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); + const redeemReserve = await iVault.redeemReservedAmount(); + const freeBalance = await iVault.getFreeBalance(); + + console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); + console.log("Redeem reserve", redeemReserve.format()); + console.log("Free balance", freeBalance.format()); + console.log("Redeem reserve after", await iVault.redeemReservedAmount()); + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; + }); + + it("Redeem withdraw", async function() { + const balanceBefore = await asset.balanceOf(staker2.address); + const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); + + console.log(`staker2PWBefore: ${staker2PWBefore.toString()}`); + console.log(`staker2PWBefore: ${(await iVault.redeemReservedAmount()).toString()}`); + console.log(`staker2PWBefore: ${(await asset.balanceOf(iVault.address)).toString()}`); + console.log(`staker2PWBefore: ${(await eigenLayerAdapter.getDepositedShares()).toString()}`); + + const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); + + const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); + const balanceAfter = await asset.balanceOf(staker2.address); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); + console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); + + expect(staker2PWAfter).to.be.eq(0n); + expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); + expect(totalDepositedAfter).to.be.closeTo(0n, transactErr * 3n); + expect(totalAssetsAfter).to.be.closeTo(0n, transactErr * 3n); + + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + }); + + describe("Emergency undelegate cannot finish normal undelegation flow", function() { + it("deposit & delegate & undelegate", async function() { + const elVault = eigenLayerVaults[0]; + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit & delegate 10 + await iVault.connect(staker).deposit(toWei(10), staker.address); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, elVault, 0n, delegateData); + await iVault.connect(iVaultOperator).delegate(await eigenLayerAdapter.getAddress(), ZeroAddress, toWei(9), []); + + // withdraw 3 + await iVault.connect(staker).withdraw(toWei(3), staker.address); + + // emergency undelegate 5 + await iVault.connect(iVaultOperator).emergencyUndelegate([[await eigenLayerAdapter.getAddress(), elVault, toWei(5), []]]); + // normal undelegate 3 + let tx = await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), [[await eigenLayerAdapter.getAddress(), elVault, toWei(3), []]]); + + // get emergency claimer + const receipt = await tx.wait(); + + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapter"); + let withdrawalQueuedEvent; + receipt.logs.forEach(log => { + try { + const parsedLog = eigenLayerAdapterFactory.interface.parseLog(log); + if (parsedLog) { + console.log("🔹 Event Detected:"); + withdrawalQueuedEvent = parsedLog.args; + return; + } + } catch (error) { + } + }); + + const wData = { + staker1: withdrawalQueuedEvent["stakerAddress"], + staker2: elVault, + staker3: eigenLayerAdapter.address, + nonce1: withdrawalQueuedEvent["nonce"], + nonce2: withdrawalQueuedEvent["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent["strategy"]], + shares: [withdrawalQueuedEvent["shares"]], + }; + + // Encode the data + const _data = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + + // claim + await expect(iVault.connect(iVaultOperator).emergencyClaim([eigenLayerAdapter.address], [elVault], [_data])) + .to.be.revertedWithCustomError(eigenLayerAdapter, "OnlyEmergency"); + }); + }); + + describe("Eigenlayer: redelegate & undelegate", function() { + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + + // delegate + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, toWei(5), []); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, eigenLayerVaults[0], 0n, delegateData); + }); + + it("should be able to re-delegate", async function() { + // redelegate to a new operator + let tx = await eigenLayerAdapter.connect(iVaultOperator).redelegate(vaults.eigenLayer[1], { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash); + let receipt = await tx.wait(); + + // check that required events are emitted + expect(tx).to.be.emit(eigenLayerAdapter, "RedelegatedTo"); + expect(tx).to.be.emit(eigenLayerAdapter, "WithdrawalsQueued"); + + // check that operator has been updated in EL + const delegationManager = await ethers.getContractAt("IDelegationManager", assetData.delegationManager); + expect(await delegationManager.delegatedTo(eigenLayerAdapter.address)).to.be.eq(vaults.eigenLayer[1]); + + // check that queued withdrawal root exists + const withdrawalRoots = receipt.logs.filter(e => e.eventName === "WithdrawalsQueued")[0].args["withdrawalRoots"][0]; + const queuedWithdrawalRoots = await delegationManager.getQueuedWithdrawalRoots(eigenLayerAdapter.address); + expect(queuedWithdrawalRoots.length).to.be.eq(1); + expect(queuedWithdrawalRoots[0]).to.be.eq(withdrawalRoots); + + // check that total deposited zero + expect(await eigenLayerAdapter.getTotalDeposited()).to.be.eq(0n); + + // claim withdrawals + const queuedWithdrawal = (await delegationManager.getQueuedWithdrawals(eigenLayerAdapter.address))[0][0]; + const claimData = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [queuedWithdrawal]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[false]]), + ]; + + await mineBlocks(50); + await eigenLayerAdapter.connect(iVaultOperator).claim(claimData, false); + // ------------------------------ + + // Check that total deposited equal to previous + expect(await eigenLayerAdapter.getTotalDeposited()).to.be.closeTo(toWei(5), transactErr); + }); + + it("should be able to undelegate", async function() { + const vaultBalanceBefore = await iVault.totalAssets(); + + // undelegate operator + let tx = await eigenLayerAdapter.connect(iVaultOperator).undelegate(); + let receipt = await tx.wait(); + + // check that required events are emitted + expect(tx).to.be.emit(eigenLayerAdapter, "RedelegatedTo"); + expect(tx).to.be.emit(eigenLayerAdapter, "WithdrawalsQueued"); + + // check that operator has been updated in EL + const delegationManager = await ethers.getContractAt("IDelegationManager", assetData.delegationManager); + expect(await delegationManager.delegatedTo(eigenLayerAdapter.address)).to.be.eq(ZeroAddress); + + // check that queued withdrawal root exists + const withdrawalRoots = receipt.logs.filter(e => e.eventName === "WithdrawalsQueued")[0].args["withdrawalRoots"][0]; + const queuedWithdrawalRoots = await delegationManager.getQueuedWithdrawalRoots(eigenLayerAdapter.address); + expect(queuedWithdrawalRoots.length).to.be.eq(1); + expect(queuedWithdrawalRoots[0]).to.be.eq(withdrawalRoots); + + // claim withdrawals + const queuedWithdrawal = (await delegationManager.getQueuedWithdrawals(eigenLayerAdapter.address))[0][0]; + const claimData = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [queuedWithdrawal]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + await eigenLayerAdapter.connect(iVaultOperator).claim(claimData, false); + // ------------------------------ + + // check that total deposited equal to zero + // check that vault balance increased + const vaultBalanceAfter = await iVault.totalAssets(); + expect(await eigenLayerAdapter.getTotalDeposited()).to.be.eq(0); + expect(vaultBalanceAfter - vaultBalanceBefore).to.be.closeTo(toWei(5), transactErr); + }); + }); + + describe("Eigenlayer: rewards", function() { + it("Can be called only by trustee", async function() { + await expect(eigenLayerAdapter.connect(staker).claimRewards(assetData.assetAddress, "0x")) + .to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + }); + + it("Can set rewards coordinator", async function() { + await expect(eigenLayerAdapter.setRewardsCoordinator(assetData.rewardsCoordinator, ethers.Wallet.createRandom().address)) + .to.be.emit(eigenLayerAdapter, "RewardCoordinatorChanged"); + }); + }); + + describe("Eigenlayer: input args", function() { + beforeEach(async function() { + await snapshot.restore(); + }); + + it("Delegate: input args", async function() { + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).delegate(eigenLayerVaults[0], 0n, [])) + .to.be.revertedWith("Pausable: paused"); + }); + + it("Undelegate: input args", async function() { + await expect(eigenLayerAdapter.connect(staker).undelegate()) + .to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).undelegate()) + .to.be.revertedWith("Pausable: paused"); + }); + + it("Redelegate: input args", async function() { + await expect(eigenLayerAdapter.connect(staker).redelegate( + vaults.eigenLayer[1], { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash, + )).to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + + await expect(eigenLayerAdapter.connect(iVaultOperator).redelegate( + ZeroAddress, { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash, + )).to.be.revertedWithCustomError(eigenLayerAdapter, "ZeroAddress"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).redelegate( + ZeroAddress, { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash, + )).to.be.revertedWith("Pausable: paused"); + }); + + it("Withdraw: input args", async function() { + await expect(eigenLayerAdapter.connect(iVaultOperator).withdraw(ZeroAddress, 0n, ["0x"], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "InvalidDataLength"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).withdraw(ZeroAddress, 0n, [], false)) + .to.be.revertedWith("Pausable: paused"); + }); + + it("Claim: input args", async function() { + await expect(eigenLayerAdapter.connect(staker).claim([], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + + await expect(eigenLayerAdapter.connect(iVaultOperator).claim(["0x", "0x"], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "InvalidDataLength"); + + await expect(eigenLayerAdapter.connect(iVaultOperator).claim(["0x", "0x", "0x", "0x"], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "InvalidDataLength"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).claim([], false)) + .to.be.revertedWith("Pausable: paused"); + }); + }); +}); + diff --git a/projects/vaults/test/InceptionVault_S_EL_wst.ts b/projects/vaults/test/InceptionVault_S_EL_wst.ts new file mode 100644 index 00000000..059ee629 --- /dev/null +++ b/projects/vaults/test/InceptionVault_S_EL_wst.ts @@ -0,0 +1,772 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { ethers, network, upgrades } from "hardhat"; +import { wstETHWrapped } from "./data/assets/stETH"; +import { vaults } from './data/vaults'; +import { + addRewardsToStrategy, + calculateRatio, + e18, + mineBlocks, + toWei, +} from "./helpers/utils"; +import { adapters } from "./src/constants"; +import { abi, initVault } from "./src/init-vault"; + +const assetData = wstETHWrapped; +const eigenLayerVaults = vaults.eigenLayer; + +describe(`Inception Symbiotic Vault ${assetData.assetName}`, function () { + const coder = abi; + const encodedSignatureWithExpiry = coder.encode( + ["tuple(uint256 expiry, bytes signature)"], + [{ expiry: 0, signature: ethers.ZeroHash }], + ); + const delegateData = [ethers.ZeroHash, encodedSignatureWithExpiry]; + + let iToken, iVault, ratioFeed, asset, eigenLayerAdapter, withdrawalQueue; + let iVaultOperator, deployer, staker, staker2, staker3, treasury; + let ratioErr, transactErr; + let snapshot; + + before(async function () { + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: assetData.url ? assetData.url : network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, ratioFeed, asset, iVaultOperator, eigenLayerAdapter, withdrawalQueue } = + await initVault(assetData, { adapters: [adapters.EigenLayer], eigenAdapterContractName: 'InceptionEigenAdapterWrap' })); + ratioErr = assetData.ratioErr; + transactErr = assetData.transactErr; + + [deployer, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + treasury = await iVault.treasury(); //deployer + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function () { + await iVault?.removeAllListeners(); + }); + + describe("InceptionEigenAdapter", function () { + let adapter, iVaultMock, trusteeManager; + + beforeEach(async function () { + await snapshot.restore(); + iVaultMock = staker2; + trusteeManager = staker3; + + console.log(`iVaultMock balance of asset after: ${await asset.balanceOf(iVaultMock.address)}`); + console.log(`trusteeManager balance of asset after: ${await asset.balanceOf(trusteeManager.address)}`); + + const InceptionEigenAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapterWrap", iVaultMock); + adapter = await upgrades.deployProxy(InceptionEigenAdapterFactory, [ + await deployer.getAddress(), + assetData.rewardsCoordinator, + assetData.delegationManager, + assetData.strategyManager, + assetData.assetStrategy, + assetData.assetAddress, + trusteeManager.address, + iVault.address, + ]); + }); + + it("getOperatorAddress: equals 0 address before any delegation", async function () { + expect(await adapter.getOperatorAddress()).to.be.eq(ethers.ZeroAddress); + }); + + it("getOperatorAddress: reverts when _data length is < 2", async function () { + const amount = toWei(0); + console.log(`asset address: ${await asset.balanceOf(trusteeManager.address)}`); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await expect(adapter.connect(trusteeManager).delegate(eigenLayerVaults[0], amount, [])).to.be.revertedWithCustomError(adapter, "InvalidDataLength"); + }); + + it("getOperatorAddress: equals operator after delegation", async function () { + console.log(`asset address: ${await asset.balanceOf(trusteeManager.address)}`); + await adapter.connect(trusteeManager).delegate(eigenLayerVaults[0], 0n, delegateData); + expect(await adapter.getOperatorAddress()).to.be.eq(eigenLayerVaults[0]); + }); + + it("delegateToOperator: reverts when called by not a trustee", async function () { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, []); + + await expect( + adapter.connect(staker).delegate(eigenLayerVaults[0], 0n, delegateData), + ).to.be.revertedWithCustomError(adapter, "NotVaultOrTrusteeManager"); + }); + + it("delegateToOperator: reverts when delegates to 0 address", async function () { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, []); + + await expect( + adapter.connect(trusteeManager).delegate(ethers.ZeroAddress, 0n, delegateData), + ).to.be.revertedWithCustomError(adapter, "NullParams"); + }); + + it("delegateToOperator: reverts when delegates unknown operator", async function () { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, delegateData); + + const unknownOperator = ethers.Wallet.createRandom().address; + await expect(adapter.connect(trusteeManager) + .delegate(unknownOperator, 0n, delegateData)) + .to.be.revertedWithCustomError(iVault, "OperatorNotRegistered"); + }); + + it("withdrawFromEL: reverts when called by not a trustee", async function () { + const amount = toWei(1); + await asset.connect(trusteeManager).approve(await adapter.getAddress(), amount); + await adapter.connect(trusteeManager).delegate(ZeroAddress, amount, delegateData); + await adapter.connect(trusteeManager).delegate(eigenLayerVaults[0], 0n, delegateData); + + await expect(adapter.connect(staker).withdraw(ZeroAddress, amount / 2n, [], false)).to.be.revertedWithCustomError( + adapter, "NotVaultOrTrusteeManager", + ); + }); + + it("getVersion: equals 3", async function () { + expect(await adapter.getVersion()).to.be.eq(3); + }); + + it("pause(): only owner can", async function () { + expect(await adapter.paused()).is.false; + await adapter.connect(iVaultMock).pause(); + expect(await adapter.paused()).is.true; + }); + + it("pause(): another address can not", async function () { + await expect(adapter.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("unpause(): only owner can", async function () { + await adapter.connect(iVaultMock).pause(); + expect(await adapter.paused()).is.true; + + await adapter.connect(iVaultMock).unpause(); + expect(await adapter.paused()).is.false; + }); + + it("unpause(): another address can not", async function () { + await adapter.connect(iVaultMock).pause(); + expect(await adapter.paused()).is.true; + await expect(adapter.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("EigenLayer | Base flow no flash", function () { + let delegatedEL = 0n; + let tx; + let undelegateEpoch; + + before(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("Initial stats", async function () { + expect(await iVault.ratio()).to.be.eq(e18); + expect(await iVault.totalAssets()).to.be.eq(0n); + expect(await iVault.getTotalDeposited()).to.be.eq(0n); + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + expect(await iVault.getFlashCapacity()).to.be.eq(0n); + expect(await iVault.getFreeBalance()).to.be.eq(0n); + }); + + it("User can deposit to iVault", async function () { + const totalDeposited = toWei(20); + const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit + const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Delegate to EigenLayer#1", async function () { + const amount = (await iVault.getFreeBalance()) / 3n; + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, amount, []); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, eigenLayerVaults[0], 0n, delegateData); + + delegatedEL += amount; + }); + + it("Delegate all to eigenOperator#1", async function () { + const amount = await iVault.getFreeBalance(); + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, amount, []); + delegatedEL += amount; + }); + + it("Update ratio", async function () { + const ratio = await iVault.ratio(); + console.log(`Calculated ratio:\t\t\t${ratio.format()}`); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + console.log(`iVault ratio:\t\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).eq(ratio); + }); + + it("Update asset ratio", async function () { + console.log("totalDelegatedBefore", await iVault.getTotalDelegated()); + await addRewardsToStrategy(assetData.assetStrategy, e18, staker3); + console.log("totalDelegatedAfter", await iVault.getTotalDelegated()); + const ratio = await iVault.ratio(); + console.log(`Calculated ratio:\t\t\t${ratio.format()}`); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + console.log(`New ratio is:\t\t\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).lt(e18); + }); + + it("User can withdraw all", async function () { + const shares = await iToken.balanceOf(staker.address); + const assetValue = await iVault.convertToAssets(shares); + console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); + console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); + const tx = await iVault.connect(staker).withdraw(shares, staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(assetValue); + expect(events[0].args["iShares"]).to.be.eq(shares); + + const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); + const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); + + const withdrawalEpoch = await withdrawalQueue.withdrawals(await withdrawalQueue.currentEpoch()); + const totalPW = withdrawalEpoch[1]; + + expect(stakerPW).to.be.eq(0n); + expect(staker2PW).to.be.closeTo(assetValue, transactErr); + expect(totalPW).to.be.closeTo(shares, transactErr); + }); + + // it("Update ratio after all shares burn", async function () { + // const calculatedRatio = await iVault.ratio(); + // console.log(`Calculated ratio:\t\t\t${calculatedRatio.format()}`); + // expect(calculatedRatio).to.be.eq(999999045189759685n); //Because all shares have been burnt at this point + // + // await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + // console.log(`iVault ratio after:\t\t\t${(await iVault.ratio()).format()}`); + // expect(await iVault.ratio()).eq(calculatedRatio); + // }); + + it("Undelegate from EigenLayer", async function () { + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + undelegateEpoch = await withdrawalQueue.currentEpoch(); + + console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); + + tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [ + [eigenLayerAdapter.address, eigenLayerVaults[0], totalDelegatedBefore, []], + ]); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + + console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); + console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); + }); + + it("Claim from EigenLayer", async function () { + const receipt = await tx.wait(); + + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapterWrap"); + let withdrawalQueuedEvent; + receipt.logs.forEach(log => { + try { + const parsedLog = eigenLayerAdapterFactory.interface.parseLog(log); + if (parsedLog) { + console.log("🔹 Event Detected:"); + withdrawalQueuedEvent = parsedLog.args; + return; + } + } catch (error) { + } + }); + + const wData = { + staker1: withdrawalQueuedEvent["stakerAddress"], + staker2: eigenLayerVaults[0], + staker3: eigenLayerAdapter.address, + nonce1: withdrawalQueuedEvent["nonce"], + nonce2: withdrawalQueuedEvent["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent["strategy"]], + shares: [withdrawalQueuedEvent["shares"]], + }; + + // Encode the data + const _data = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData]), + coder.encode(["address[][]"], [[[assetData.backedAssetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + + await iVault.connect(iVaultOperator).claim( + undelegateEpoch, [eigenLayerAdapter.address], [eigenLayerVaults[0]], [_data], + ); + + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + + console.log(`Total deposited after claim:\t\t\t${totalDepositedBefore.format()} + Total delegated after claim:\t\t\t${totalDelegatedBefore.format()} + Total assets after claim:\t\t\t${totalAssetsBefore.format()}`); + }); + + it("Staker is able to redeem", async function () { + const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); + const redeemReserve = await iVault.redeemReservedAmount(); + const freeBalance = await iVault.getFreeBalance(); + + console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); + console.log("Redeem reserve", redeemReserve.format()); + console.log("Free balance", freeBalance.format()); + console.log("Redeem reserve after", await iVault.redeemReservedAmount()); + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; + }); + + it("Redeem withdraw", async function () { + const balanceBefore = await asset.balanceOf(staker2.address); + const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); + + console.log(`staker2PWBefore: ${staker2PWBefore.toString()}`); + console.log(`staker2PWBefore: ${(await iVault.redeemReservedAmount()).toString()}`); + console.log(`staker2PWBefore: ${(await asset.balanceOf(iVault.address)).toString()}`); + console.log(`staker2PWBefore: ${(await eigenLayerAdapter.getDepositedShares()).toString()}`); + + const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); + + const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); + const balanceAfter = await asset.balanceOf(staker2.address); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); + console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); + + expect(staker2PWAfter).to.be.eq(0n); + expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); + expect(totalDepositedAfter).to.be.closeTo(0n, transactErr * 3n); + expect(totalAssetsAfter).to.be.closeTo(0n, transactErr * 3n); + }); + }); + + describe("Emergency undelegate", function () { + let undelegateTx; + + before(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("Initial stats", async function () { + expect(await iVault.ratio()).to.be.eq(e18); + expect(await iVault.totalAssets()).to.be.eq(0n); + expect(await iVault.getTotalDeposited()).to.be.eq(0n); + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + expect(await iVault.getFlashCapacity()).to.be.eq(0n); + expect(await iVault.getFreeBalance()).to.be.eq(0n); + }); + + it("User can deposit to iVault", async function () { + let totalDeposited = toWei(20); + const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit + const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Delegate to EigenLayer#1", async function () { + const amount = await iVault.getFreeBalance(); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, amount, []); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, eigenLayerVaults[0], 0n, delegateData); + expect(await iVault.getTotalDelegated()).to.be.closeTo(toWei(20), transactErr); + }); + + it("Emergency undelegate", async function () { + undelegateTx = await iVault.connect(iVaultOperator) + .emergencyUndelegate([[eigenLayerAdapter.address, eigenLayerVaults[0], toWei(5), []]]); + + expect(await iVault.getTotalPendingWithdrawals()).to.be.eq(0); + expect(await iVault.getTotalDelegated()).to.be.closeTo(toWei(15), transactErr); + expect(await iVault.getTotalPendingEmergencyWithdrawals()).to.be.closeTo(toWei(5), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + + it("User withdraw", async function () { + const tx = await iVault.connect(staker).withdraw(toWei(2), staker); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(toWei(2)); + expect(events[0].args["iShares"]).to.be.eq(toWei(2)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + + it("Emergency claim", async function () { + const receipt = await undelegateTx.wait(); + + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapterWrap"); + let withdrawalQueuedEvent; + receipt.logs.forEach(log => { + try { + const parsedLog = eigenLayerAdapterFactory.interface.parseLog(log); + if (parsedLog) { + console.log("🔹 Event Detected:"); + withdrawalQueuedEvent = parsedLog.args; + return; + } + } catch (error) { + } + }); + + const wData = { + staker1: withdrawalQueuedEvent["stakerAddress"], + staker2: eigenLayerVaults[0], + staker3: eigenLayerAdapter.address, + nonce1: withdrawalQueuedEvent["nonce"], + nonce2: withdrawalQueuedEvent["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent["strategy"]], + shares: [withdrawalQueuedEvent["shares"]], + }; + + console.log(wData); + + // Encode the data + const _data = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData]), + coder.encode(["address[][]"], [[[assetData.backedAssetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + + await iVault.connect(iVaultOperator).emergencyClaim( + [eigenLayerAdapter.address], [eigenLayerVaults[0]], [_data], + ); + + expect(await asset.balanceOf(iVault.address)).to.be.closeTo(toWei(5), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + + it("Force undelegate & claim", async function () { + await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), []) + + expect(await asset.balanceOf(iVault.address)).to.be.closeTo(toWei(5), transactErr); + expect(await withdrawalQueue.totalAmountRedeem()).to.be.closeTo(toWei(2), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + }); + + it("Redeem", async function () { + const tx = await iVault.connect(staker).redeem(staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(toWei(2), transactErr); + expect(await asset.balanceOf(iVault.address)).to.be.closeTo(toWei(3), transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + }); + }); + + describe("Emergency undelegate cannot finish normal undelegation flow", function() { + it("deposit & delegate & undelegate", async function() { + const elVault = eigenLayerVaults[0]; + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit & delegate 10 + await iVault.connect(staker).deposit(toWei(10), staker.address); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, elVault, 0n, delegateData); + await iVault.connect(iVaultOperator).delegate(await eigenLayerAdapter.getAddress(), ZeroAddress, toWei(9), []); + + // withdraw 3 + await iVault.connect(staker).withdraw(toWei(3), staker.address); + + // emergency undelegate 5 + await iVault.connect(iVaultOperator).emergencyUndelegate([[await eigenLayerAdapter.getAddress(), elVault, toWei(5), []]]); + // normal undelegate 3 + let tx = await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), [[await eigenLayerAdapter.getAddress(), elVault, toWei(3), []]]); + + // get emergency claimer + const receipt = await tx.wait(); + + const eigenLayerAdapterFactory = await ethers.getContractFactory("InceptionEigenAdapter"); + let withdrawalQueuedEvent; + receipt.logs.forEach(log => { + try { + const parsedLog = eigenLayerAdapterFactory.interface.parseLog(log); + if (parsedLog) { + console.log("🔹 Event Detected:"); + withdrawalQueuedEvent = parsedLog.args; + return; + } + } catch (error) { + } + }); + + const wData = { + staker1: withdrawalQueuedEvent["stakerAddress"], + staker2: elVault, + staker3: eigenLayerAdapter.address, + nonce1: withdrawalQueuedEvent["nonce"], + nonce2: withdrawalQueuedEvent["withdrawalStartBlock"], + tokens: [withdrawalQueuedEvent["strategy"]], + shares: [withdrawalQueuedEvent["shares"]], + }; + + // Encode the data + const _data = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [wData]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + + // claim + await expect(iVault.connect(iVaultOperator).emergencyClaim([eigenLayerAdapter.address], [elVault], [_data])) + .to.be.revertedWithCustomError(eigenLayerAdapter, "OnlyEmergency"); + }); + }); + + describe("Eigenlayer: redelegate & undelegate", function() { + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + + // delegate + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, ZeroAddress, toWei(5), []); + await iVault.connect(iVaultOperator).delegate(eigenLayerAdapter.address, eigenLayerVaults[0], 0n, delegateData); + }); + + it("should be able to re-delegate", async function() { + // redelegate to a new operator + let tx = await eigenLayerAdapter.connect(iVaultOperator).redelegate(vaults.eigenLayer[1], { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash); + let receipt = await tx.wait(); + + // check that required events are emitted + expect(tx).to.be.emit(eigenLayerAdapter, "RedelegatedTo"); + expect(tx).to.be.emit(eigenLayerAdapter, "WithdrawalsQueued"); + + // check that operator has been updated in EL + const delegationManager = await ethers.getContractAt("IDelegationManager", assetData.delegationManager); + expect(await delegationManager.delegatedTo(eigenLayerAdapter.address)).to.be.eq(vaults.eigenLayer[1]); + + // check that queued withdrawal root exists + const withdrawalRoots = receipt.logs.filter(e => e.eventName === "WithdrawalsQueued")[0].args["withdrawalRoots"][0]; + const queuedWithdrawalRoots = await delegationManager.getQueuedWithdrawalRoots(eigenLayerAdapter.address); + expect(queuedWithdrawalRoots.length).to.be.eq(1); + expect(queuedWithdrawalRoots[0]).to.be.eq(withdrawalRoots); + + // check that total deposited zero + expect(await eigenLayerAdapter.getTotalDeposited()).to.be.eq(0n); + + // claim withdrawals + const queuedWithdrawal = (await delegationManager.getQueuedWithdrawals(eigenLayerAdapter.address))[0][0]; + const claimData = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [queuedWithdrawal]), + coder.encode(["address[][]"], [[[assetData.assetAddress]]]), + coder.encode(["bool[]"], [[false]]), + ]; + + await mineBlocks(50); + await eigenLayerAdapter.connect(iVaultOperator).claim(claimData, false); + // ------------------------------ + + // Check that total deposited equal to previous + expect(await eigenLayerAdapter.getTotalDeposited()).to.be.closeTo(toWei(5), transactErr); + }); + + it("should be able to undelegate", async function() { + const vaultBalanceBefore = await iVault.totalAssets(); + + // undelegate operator + let tx = await eigenLayerAdapter.connect(iVaultOperator).undelegate(); + let receipt = await tx.wait(); + + // check that required events are emitted + expect(tx).to.be.emit(eigenLayerAdapter, "RedelegatedTo"); + expect(tx).to.be.emit(eigenLayerAdapter, "WithdrawalsQueued"); + + // check that operator has been updated in EL + const delegationManager = await ethers.getContractAt("IDelegationManager", assetData.delegationManager); + expect(await delegationManager.delegatedTo(eigenLayerAdapter.address)).to.be.eq(ZeroAddress); + + // check that queued withdrawal root exists + const withdrawalRoots = receipt.logs.filter(e => e.eventName === "WithdrawalsQueued")[0].args["withdrawalRoots"][0]; + const queuedWithdrawalRoots = await delegationManager.getQueuedWithdrawalRoots(eigenLayerAdapter.address); + expect(queuedWithdrawalRoots.length).to.be.eq(1); + expect(queuedWithdrawalRoots[0]).to.be.eq(withdrawalRoots); + + // claim withdrawals + const queuedWithdrawal = (await delegationManager.getQueuedWithdrawals(eigenLayerAdapter.address))[0][0]; + const claimData = [ + coder.encode(["tuple(address staker1,address staker2,address staker3,uint256 nonce1,uint256 nonce2,address[] tokens,uint256[] shares)"], [queuedWithdrawal]), + coder.encode(["address[][]"], [[[assetData.backedAssetAddress]]]), + coder.encode(["bool[]"], [[true]]), + ]; + + await mineBlocks(50); + await eigenLayerAdapter.connect(iVaultOperator).claim(claimData, false); + // ------------------------------ + + // check that total deposited equal to zero + // check that vault balance increased + const vaultBalanceAfter = await iVault.totalAssets(); + expect(await eigenLayerAdapter.getTotalDeposited()).to.be.eq(0); + expect(vaultBalanceAfter - vaultBalanceBefore).to.be.closeTo(toWei(5), transactErr); + }); + }); + + describe("Eigenlayer: rewards", function() { + it("Can be called only by trustee", async function() { + await expect(eigenLayerAdapter.connect(staker).claimRewards(assetData.assetAddress, "0x")) + .to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + }); + + it("Can set rewards coordinator", async function() { + await expect(eigenLayerAdapter.setRewardsCoordinator(assetData.rewardsCoordinator, ethers.Wallet.createRandom().address)) + .to.be.emit(eigenLayerAdapter, "RewardCoordinatorChanged"); + }); + }); + + describe("Eigenlayer: input args", function() { + beforeEach(async function() { + await snapshot.restore(); + }); + + it("Delegate: input args", async function() { + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).delegate(eigenLayerVaults[0], 0n, [])) + .to.be.revertedWith("Pausable: paused"); + }); + + it("Undelegate: input args", async function() { + await expect(eigenLayerAdapter.connect(staker).undelegate()) + .to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).undelegate()) + .to.be.revertedWith("Pausable: paused"); + }); + + it("Redelegate: input args", async function() { + await expect(eigenLayerAdapter.connect(staker).redelegate( + vaults.eigenLayer[1], { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash, + )).to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + + await expect(eigenLayerAdapter.connect(iVaultOperator).redelegate( + ZeroAddress, { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash, + )).to.be.revertedWithCustomError(eigenLayerAdapter, "ZeroAddress"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).redelegate( + ZeroAddress, { + signature: "0x", + expiry: 0, + }, ethers.ZeroHash, + )).to.be.revertedWith("Pausable: paused"); + }); + + it("Withdraw: input args", async function() { + await expect(eigenLayerAdapter.connect(iVaultOperator).withdraw(ZeroAddress, 0n, ["0x"], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "InvalidDataLength"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).withdraw(ZeroAddress, 0n, [], false)) + .to.be.revertedWith("Pausable: paused"); + }); + + it("Claim: input args", async function() { + await expect(eigenLayerAdapter.connect(staker).claim([], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "NotVaultOrTrusteeManager"); + + await expect(eigenLayerAdapter.connect(iVaultOperator).claim(["0x", "0x"], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "InvalidDataLength"); + + await expect(eigenLayerAdapter.connect(iVaultOperator).claim(["0x", "0x", "0x", "0x"], false)) + .to.be.revertedWithCustomError(eigenLayerAdapter, "InvalidDataLength"); + + await eigenLayerAdapter.pause(); + await expect(eigenLayerAdapter.connect(iVaultOperator).claim([], false)) + .to.be.revertedWith("Pausable: paused"); + }); + }); +}); diff --git a/projects/vaults/test/InceptionVault_S_slashing.ts b/projects/vaults/test/InceptionVault_S_slashing.ts new file mode 100644 index 00000000..f9684180 --- /dev/null +++ b/projects/vaults/test/InceptionVault_S_slashing.ts @@ -0,0 +1,1997 @@ +// Just slashing tests for all adapters + +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { calculateRatio, setBlockTimestamp, skipEpoch, toWei } from "./helpers/utils"; +import { adapters, emptyBytes } from './src/constants'; +import { abi, initVault } from "./src/init-vault-new"; +import { testrunConfig } from './testrun.config'; +import { withdrawals } from "../typechain-types/contracts"; +import { ZeroAddress } from "ethers"; + +const assetDataNew = testrunConfig.assetData; + +const mellowVaults = assetDataNew.adapters.mellow; +const symbioticVaults = assetDataNew.adapters.symbiotic; +const { ethers, network, upgrades } = hardhat; + +async function symbioticClaimParams(symbioticVault, claimer) { + return abi.encode( + ["address", "address"], + [symbioticVault.vaultAddress, claimer], + ); +} + +async function mellowClaimParams(mellowVault, claimer) { + return abi.encode(["address", "address"], [mellowVault.vaultAddress, claimer]); +} + +let iToken, iVault, ratioFeed, asset, mellowAdapter, symbioticAdapter, iLibrary, withdrawalQueue; +let iVaultOperator, deployer, staker, staker2, staker3, treasury; +let ratioErr, transactErr; +let snapshot; +let params; + +describe("Symbiotic Vault Slashing", function() { + + before(async function () { + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: network.config.forking.url, + blockNumber: assetDataNew.blockNumber ? assetDataNew.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, ratioFeed, asset, iVaultOperator, mellowAdapter, symbioticAdapter, iLibrary, withdrawalQueue } = + await initVault(assetDataNew, { adapters: [adapters.Mellow, adapters.Symbiotic] })); + ratioErr = assetDataNew.ratioErr; + transactErr = assetDataNew.transactErr; + + [deployer, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetDataNew.impersonateStaker(staker, iVault); + staker2 = await assetDataNew.impersonateStaker(staker2, iVault); + staker3 = await assetDataNew.impersonateStaker(staker3, iVault); + treasury = await iVault.treasury(); //deployer + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function() { + if (iVault) { + await iVault.removeAllListeners(); + } + }); + + describe(`Symbiotic ${assetDataNew.asset.name}`, function () { + beforeEach(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + // flow: deposit -> delegate -> withdraw -> undelegate -> claim -> redeem + it("one withdrawal without slash", async function() { + const depositAmount = toWei(10); + // deposit + let tx = await iVault.connect(staker).deposit(depositAmount, staker.address); + await tx.wait(); + // assert vault balance (token/asset) + expect(await asset.balanceOf(iVault.address)).to.be.eq(depositAmount); + expect(await iToken.totalSupply()).to.be.eq(depositAmount); + expect(await iVault.totalAssets()).to.be.eq(depositAmount); + // assert user balance (shares) + expect(await iToken.balanceOf(staker.address)).to.be.eq(depositAmount); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, depositAmount, emptyBytes); + await tx.wait(); + // assert delegated amount + expect(await iVault.getTotalDelegated()).to.be.eq(depositAmount); + // assert vault balance (token/asset) + expect(await iToken.totalSupply()).to.be.eq(depositAmount); + expect(await asset.balanceOf(iVault.address)).to.be.eq(0); + expect(await iVault.totalAssets()).to.be.eq(0); + + // one withdraw + let shares = await iToken.balanceOf(staker.address); + tx = await iVault.connect(staker).withdraw(shares, staker.address); + await tx.wait(); + + expect(await asset.balanceOf(iVault.address)).to.be.eq(0); + expect(await iVault.totalAssets()).to.be.eq(0); + expect(await iVault.getTotalDelegated()).to.be.eq(depositAmount); + // shares burned + expect(await iToken.totalSupply()).to.be.eq(0); + expect(await iToken.balanceOf(staker.address)).to.be.eq(0); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + + expect(await withdrawalQueue.currentEpoch()).to.be.eq(1, "Current epoch should be 1"); + expect(await withdrawalQueue.totalSharesToWithdraw()).to.be.eq(depositAmount); + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(epochShares).to.be.eq(shares); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + const adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + // assert balances + expect(await iVault.getTotalDelegated()).to.be.eq(0); + expect(await iToken.totalSupply()).to.be.eq(0); + expect(events[0].args["epoch"]).to.be.eq(1); + expect(await asset.balanceOf(iVault.address)).to.be.eq(0); + expect(await withdrawalQueue.currentEpoch()).to.be.eq(2, "Current epoch should be 2"); + + expect(events[0].args["adapter"]).to.be.eq(symbioticAdapter.address); + expect(events[0].args["actualAmounts"]).to.be.eq(depositAmount); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + const params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await withdrawalQueue.totalSharesToWithdraw()).to.be.eq(0); + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(depositAmount); + expect(await asset.balanceOf(iVault.address)).to.be.eq(depositAmount); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(0); + + expect(events[0].args["amount"]).to.be.closeTo(depositAmount, transactErr); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + }); + + // flow: + // deposit -> delegate -> withdraw -> undelegate -> claim -> + // withdraw -> slash -> undelegate -> claim -> redeem -> redeem + it("2 withdraw & slash between undelegate", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(5), staker.address); + await tx.wait(); + + tx = await iVault.connect(staker2).deposit(toWei(5), staker2.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // undelegate + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(2),[]]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // second withdraw + tx = await iVault.connect(staker2).withdraw(toWei(2), staker2.address); + await tx.wait(); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // undelegate + let amount = await iVault.convertToAssets( + await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()), + ); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress,amount, []]]); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(toWei(2), transactErr); + expect(await iVault.ratio()).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker2).redeem(staker2.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(toWei(1.8), transactErr); + expect(await iVault.ratio()).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> deposit #2 -> delegate -> withdraw #1 -> undelegate -> claim -> + // withdraw #2 -> undelegate -> slash -> claim -> redeem -> redeem + it("2 withdraw & slash after undelegate", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(5), staker.address); + await tx.wait(); + + tx = await iVault.connect(staker2).deposit(toWei(5), staker2.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + // ---------------- + + // second withdraw + tx = await iVault.connect(staker2).withdraw(toWei(2), staker2.address); + await tx.wait(); + // ---------------- + + // undelegate + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + claimer = adapterEvents[0].args["claimer"]; + // ---------------- + + console.log("pending withdrawals", await iVault.getTotalPendingWithdrawals()); + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + console.log("pending withdrawals", await iVault.getTotalPendingWithdrawals()); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(toWei(2), transactErr); + expect(await iVault.ratio()).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + + // // redeem + // tx = await iVault.connect(staker2).redeem(staker2.address); + // receipt = await tx.wait(); + // events = receipt.logs?.filter(e => e.eventName === "Redeem"); + // expect(events.length).to.be.gte(1); + // expect(events[0].args["amount"]).to.be.closeTo(toWei(1.8), transactErr); + // expect(await iVault.ratio()).to.be.closeTo(1111111111111111111n, ratioErr); + // // ---------------- + }); + + // flow: + // deposit #1 -> deposit #2 -> delegate #1 -> withdraw #1 -> slash -> withdraw #2 -> + // deposit #3 -> delegate #2 -> undelegate -> claim -> redeem -> redeem + it("slash between withdraw", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(5), staker.address); + await tx.wait(); + + tx = await iVault.connect(staker2).deposit(toWei(5), staker2.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker2).withdraw(toWei(2), staker2.address); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // deposit + tx = await iVault.connect(staker3).deposit(toWei(2), staker3.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(2), emptyBytes); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // undelegate + let epochShares = await iVault.convertToAssets( + await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()), + ); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(1797344482370384621n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker2).redeem(staker2.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(1797344482370384621n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> deposit #2 -> delegate #1 -> withdraw #1 -> slash -> withdraw #2 -> + // slash -> deposit #3 -> delegate #2 -> undelegate -> claim -> redeem -> redeem + it("withdraw->slash->withdraw->slash", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(5), staker.address); + await tx.wait(); + + tx = await iVault.connect(staker2).deposit(toWei(5), staker2.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker2).withdraw(toWei(2), staker2.address); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // apply slash + totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1238424970834390498n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // deposit + tx = await iVault.connect(staker3).deposit(toWei(2), staker3.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(2), emptyBytes); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1238424970834390498n, ratioErr); + // ---------------- + + // undelegate + let epochShares = await iVault.convertToAssets( + await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()), + ); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(1238424970834390498n, ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1238424970834390498n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(1614954516503730780n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1238424970834390498n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker2).redeem(staker2.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(1614954516503730780n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1238424970834390498n, ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> delegate #1 -> withdraw #1 -> slash -> withdraw #2 -> + // slash -> deposit #2 -> delegate #2 -> undelegate -> claim -> redeem -> redeem + it("withdraw all->slash->redeem all", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // undelegate + let amount = await iVault.getTotalDelegated(); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress,amount, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(8986722411851923107n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> delegate #1 -> withdraw #1 -> undelegate -> slash -> claim -> redeem + it("slash after undelegate", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(4493361205925961555n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> delegate #1 -> withdraw #1 -> undelegate -> claim -> deposit #2 -> slash + it("slash after deposit", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2.5), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // deposit + tx = await iVault.connect(staker2).deposit(toWei(5), staker2.address); + await tx.wait(); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1034482758620689656n, ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> delegate #1 -> withdraw #1 -> undelegate -> claim -> slash + it("slash after claim", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2.5), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1111111111111111111n, ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> delegate #1 -> withdraw #1 -> withdraw #1 -> undelegate -> slash -> claim -> redeem + it("2 withdraw from one user in epoch", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(6290705688296346177n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + }); + + // flow: + // deposit #1 -> delegate #1 -> withdraw #1 -> undelegate -> slash -> claim -> withdraw -> undelegate -> claim -> redeem + it("2 withdraw from one user in different epochs", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // apply slash + const totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let amount = await iVault.convertToAssets( + await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()), + ); + + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress,amount, []]]); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(6290705688296346177n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + }); + + it("redeem unavailable claim", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let undelegateEvents = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + // ---------------- + + // failed redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events.length).to.be.equals(0); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(undelegateEvents[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + // ---------------- + + // success redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(toWei(5), transactErr); + // ---------------- + + // failed redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events.length).to.be.equals(0); + // ---------------- + + }); + + it("undelegate from symbiotic and mellow", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), emptyBytes); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(5), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(4), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + tx = await iVault.connect(iVaultOperator) + .undelegate( + await withdrawalQueue.currentEpoch(), + [ + [mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(2), []], + [symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(2), []], + ], + ); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer2 = adapterEvents[0].args["claimer"]; + + adapterEvents = receipt.logs?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer1 = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + console.log("before", await symbioticVaults[0].vault.totalStake()); + console.log("before totalDelegated", await iVault.getTotalDelegated()); + console.log("pending withdrawals", await iVault.getTotalPendingWithdrawals()); + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1053370378591850307n, ratioErr); + // ---------------- + + console.log("after", await symbioticVaults[0].vault.totalStake()); + console.log("after totalDelegated", await iVault.getTotalDelegated()); + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + tx = await iVault.connect(iVaultOperator) + .claim( + events[0].args["epoch"], + [mellowAdapter.address, symbioticAdapter.address], + [mellowVaults[0].vaultAddress, symbioticVaults[0].vaultAddress], + [[await mellowClaimParams(mellowVaults[0], claimer1)], [await symbioticClaimParams(symbioticVaults[0], claimer2)]], + ); + await tx.wait(); + + expect(await iVault.ratio()).to.be.closeTo(1053370378591850307n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(3797334803877071085n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1053370378591850307n, ratioErr); + // ---------------- + }); + + it("partially undelegate from mellow", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // add rewards + console.log("total delegated before", await iVault.getTotalDelegated()); + + await assetDataNew.addRewardsMellowVault(toWei(5), mellowVaults[0].vaultAddress); + + console.log("total delegated after", await iVault.getTotalDelegated()); + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [await iVault.ratio()]); + // ---------------- + + // undelegate + let undelegateAmount = await iVault.convertToAssets(toWei(5)); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[mellowAdapter.address, mellowVaults[0].vaultAddress, undelegateAmount, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let adapterEvents = receipt.logs?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(events[0].args["actualAmounts"]).to.be.eq(813088477205661249n); + expect(await iVault.ratio()).to.be.closeTo(999822420543056026n, ratioErr); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + params = await mellowClaimParams(mellowVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [mellowAdapter.address], [mellowVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await withdrawalQueue.totalAmountRedeem()).to.be.closeTo(undelegateAmount, transactErr); + expect(await withdrawalQueue.totalSharesToWithdraw()).to.be.eq(0n); + expect(await iVault.ratio()).to.be.closeTo(999822420543056026n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(undelegateAmount, transactErr); + expect(await iVault.ratio()).to.be.closeTo(999822420543056026n, ratioErr); + // ---------------- + }); + + it("emergency undelegate", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // check totalDelegated + const totalDeposited = await iVault.getTotalDeposited(); + const totalDelegated = await iVault.getTotalDelegated(); + const totalAssets = await iVault.totalAssets(); + const totalPendingWithdrawals = await iVault.getTotalPendingWithdrawals(); + const totalPendingEmergencyWithdrawals = await iVault.getTotalPendingEmergencyWithdrawals(); + const redeemable = await iVault.redeemReservedAmount(); + + expect(totalDeposited).to.be.eq( + totalDelegated + totalAssets + totalPendingWithdrawals + totalPendingEmergencyWithdrawals - redeemable, + ); + // ---------------- + + // emergency undelegate + tx = await iVault.connect(iVaultOperator) + .emergencyUndelegate([[symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), []]]); + + let receipt = await tx.wait(); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + await skipEpoch(symbioticVaults[0]); + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + // ---------------- + + // emergency claim + tx = await iVault.connect(iVaultOperator) + .emergencyClaim( + [symbioticAdapter.address], + [symbioticVaults[0].vaultAddress], + [[await symbioticClaimParams(symbioticVaults[0], claimer)]], + ); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // undelegate and claim + tx = await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), []); + + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(1797344482370384621n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1112752741401218766n, ratioErr); + // ---------------- + }); + + it("multiple deposits and delegates", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // deposit + tx = await iVault.connect(staker2).deposit(toWei(5), staker2.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), emptyBytes); + await tx.wait(); + // ---------------- + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(5), staker.address); + await tx.wait(); + // ---------------- + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + + // ---------------- + // apply slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake * 10n / 100n); + let ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1112746792749504069n, ratioErr); + + // ---------------- + + // update ratio + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + // claim + await skipEpoch(symbioticVaults[0]); + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + expect(await iVault.ratio()).to.be.closeTo(1112746792749504069n, ratioErr); + + // ---------------- + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(4493385227060883306n, transactErr); + expect(await iVault.ratio()).to.be.closeTo(1112746792749504069n, ratioErr); + // ---------------- + // redeem + tx = await iVault.connect(staker2).redeem(staker2.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(await iVault.ratio()).to.be.closeTo(1112746792749504069n, ratioErr); + // ---------------- + }); + + it(`base flow: deposit -> delegate -> SLASH > withdraw -> undelegate -> claim -> redeem + with check ratio after each step`, async function() { + const depositAmount = toWei(10); + // deposit + let tx = await iVault.connect(staker).deposit(depositAmount, staker.address); + await tx.wait(); + // assert vault balance (token/asset) + expect(await asset.balanceOf(iVault.address)).to.be.eq(depositAmount); + expect(await iToken.totalSupply()).to.be.eq(depositAmount); + expect(await iVault.totalAssets()).to.be.eq(depositAmount); + // assert user balance (shares) + expect(await iToken.balanceOf(staker.address)).to.be.eq(depositAmount); + + let ratio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + let contractRatio = await iVault.ratio(); + expect(contractRatio).to.eq(toWei(1), ratioErr); + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, depositAmount, emptyBytes); + await tx.wait(); + // assert delegated amount + expect(await iVault.getTotalDelegated()).to.be.eq(depositAmount); + // assert vault balance (token/asset) + expect(await iToken.totalSupply()).to.be.eq(depositAmount); + expect(await asset.balanceOf(iVault.address)).to.be.eq(0); + expect(await iVault.totalAssets()).to.be.eq(0); + + ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1000000000000000000n, ratioErr); + + // slash + // let totalDelegated = await iVault.getTotalDelegated(); + let totalStake = await symbioticVaults[0].vault.totalStake(); + + // slash half of the stake + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake / 2n); + // const totalDelegated2 = await iVault.getTotalDelegated(); + // console.log("totalDelegated", totalDelegated); + // console.log("totalDelegated2", totalDelegated2); + + // console.log("diff", totalDelegated - totalDelegated * e18 / 2n); + ratio = await iVault.ratio(); + const totalSupply = await iToken.totalSupply(); + expect(ratio).to.be.closeTo(totalSupply * BigInt(10 ** 18) / await iVault.getTotalDelegated(), ratioErr); + return; + + // one withdraw + let shares = await iToken.balanceOf(staker.address); + tx = await iVault.connect(staker).withdraw(shares, staker.address); + await tx.wait(); + + ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1000000000000000000n, ratioErr); + + + expect(await asset.balanceOf(iVault.address)).to.be.eq(0); + expect(await iVault.totalAssets()).to.be.eq(0); + expect(await iVault.getTotalDelegated()).to.be.eq(depositAmount); + // shares burned + expect(await iToken.totalSupply()).to.be.eq(0); + expect(await iToken.balanceOf(staker.address)).to.be.eq(0); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + + expect(await withdrawalQueue.currentEpoch()).to.be.eq(1, "Current epoch should be 1"); + expect(await withdrawalQueue.totalSharesToWithdraw()).to.be.eq(depositAmount); + + ratio = await iVault.ratio(); + expect(ratio).to.be.closeTo(1000000000000000000n, ratioErr); + + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(epochShares).to.be.eq(shares); + tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + const adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + // assert balances + expect(await iVault.getTotalDelegated()).to.be.eq(0); + expect(await iToken.totalSupply()).to.be.eq(0); + expect(events[0].args["epoch"]).to.be.eq(1); + expect(await asset.balanceOf(iVault.address)).to.be.eq(0); + expect(await withdrawalQueue.totalSharesToWithdraw()).to.be.eq(0); + expect(await withdrawalQueue.currentEpoch()).to.be.eq(2, "Current epoch should be 2"); + + + expect(events[0].args["adapter"]).to.be.eq(symbioticAdapter.address); + expect(events[0].args["actualAmounts"]).to.be.eq(depositAmount); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // claim + await skipEpoch(symbioticVaults[0]); + const params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(depositAmount); + expect(await asset.balanceOf(iVault.address)).to.be.eq(depositAmount); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(0); + + expect(events[0].args["amount"]).to.be.closeTo(depositAmount, transactErr); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + }); + }); + + describe("Withdrawal queue: negative cases", async function() { + let customVault, withdrawalQueue; + + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + [customVault] = await ethers.getSigners(); + const withdrawalQueueFactory = await ethers.getContractFactory("WithdrawalQueue"); + withdrawalQueue = await upgrades.deployProxy(withdrawalQueueFactory, [customVault.address, [], [], 0]); + withdrawalQueue.address = await withdrawalQueue.getAddress(); + }); + + it("only vault", async function() { + await expect(withdrawalQueue.connect(staker).request(iVault.address, toWei(1))) + .to.be.revertedWithCustomError(withdrawalQueue, "OnlyVaultOrOwnerAllowed"); + + await expect(withdrawalQueue.connect(staker) + .undelegate(await withdrawalQueue.currentEpoch(), [iVault.address], [iVault.address], [1n], [0n])) + .to.be.revertedWithCustomError(withdrawalQueue, "OnlyVaultOrOwnerAllowed"); + + await expect(withdrawalQueue.connect(staker) + .claim(await withdrawalQueue.currentEpoch(), [iVault.address], [iVault.address], [1n])) + .to.be.revertedWithCustomError(withdrawalQueue, "OnlyVaultOrOwnerAllowed"); + + await expect(withdrawalQueue.connect(staker) + .forceUndelegateAndClaim(0n, 0n)) + .to.be.revertedWithCustomError(withdrawalQueue, "OnlyVaultOrOwnerAllowed"); + + await expect(withdrawalQueue.connect(staker).redeem(iVault.address)) + .to.be.revertedWithCustomError(withdrawalQueue, "OnlyVaultOrOwnerAllowed"); + + await expect(withdrawalQueue.connect(staker)["redeem(address,uint256)"](iVault.address, 0n)) + .to.be.revertedWithCustomError(withdrawalQueue, "OnlyVaultOrOwnerAllowed"); + }); + + it("zero value", async function() { + await expect(withdrawalQueue.connect(customVault).request(iVault.address, 0)) + .to.be.revertedWithCustomError(withdrawalQueue, "ValueZero"); + + await expect(withdrawalQueue.connect(customVault).request(ZeroAddress, 10n)) + .to.be.revertedWithCustomError(withdrawalQueue, "ValueZero"); + + await expect(withdrawalQueue.connect(customVault) + .undelegate(await withdrawalQueue.currentEpoch(), [iVault.address], [iVault.address], [0], [0n])) + .to.be.revertedWithCustomError(withdrawalQueue, "ValueZero"); + }); + + it("undelegate failed", async function() { + await withdrawalQueue.connect(customVault).request(iVault.address, toWei(5)); + + await expect(withdrawalQueue.connect(customVault) + .undelegate(2, [iVault.address], [iVault.address], [0n], [0n])) + .to.be.revertedWithCustomError(withdrawalQueue, "UndelegateEpochMismatch()"); + }); + + it("claim failed", async function() { + await expect( + withdrawalQueue.connect(customVault).claim(1, [mellowAdapter.address], [mellowVaults[0].vaultAddress], [1n]), + ).to.be.revertedWithCustomError(withdrawalQueue, "ClaimUnknownAdapter"); + }); + + it("initialize", async function() { + const withdrawalQueueFactory = await ethers.getContractFactory("WithdrawalQueue"); + await expect(upgrades.deployProxy(withdrawalQueueFactory, ["0x0000000000000000000000000000000000000000", [], [], 0])) + .to.be.revertedWithCustomError(withdrawalQueue, "ValueZero"); + + await expect(upgrades.deployProxy(withdrawalQueueFactory, [iVault.address, [staker.address], [], 0])) + .to.be.revertedWithCustomError(withdrawalQueue, "ValueZero"); + + await expect(withdrawalQueue.initialize(iVault.address, [], [], 0)) + .to.be.revertedWith("Initializable: contract is already initialized"); + }); + + it("Reverts: forceUndelegateAndClaim when epoch less higher than current", async function() { + await expect(withdrawalQueue.connect(customVault).forceUndelegateAndClaim(10n, 0n)) + .to.be.revertedWithCustomError(withdrawalQueue, "UndelegateEpochMismatch"); + }); + }); + + describe("Withdrawal queue: legacy", async function() { + it("Redeem", async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + const withdrawalQueueFactory = await ethers.getContractFactory("WithdrawalQueue"); + const legacyWithdrawalQueue = await upgrades.deployProxy(withdrawalQueueFactory, + [ + iVault.address, + [staker.address, staker2.address, staker3.address], + [toWei(1), toWei(2.5), toWei(1.5)], + toWei(5), + ], + ); + + legacyWithdrawalQueue.address = await legacyWithdrawalQueue.getAddress(); + await iVault.setWithdrawalQueue(legacyWithdrawalQueue); + + expect(await legacyWithdrawalQueue.currentEpoch()).to.be.eq(2); + expect(await legacyWithdrawalQueue.totalSharesToWithdraw()).to.be.eq(0); + expect(await legacyWithdrawalQueue.totalAmountRedeem()).to.be.eq(toWei(5)); + expect(await legacyWithdrawalQueue.getPendingWithdrawalOf(staker.address)).to.be.eq(toWei(1)); + expect(await legacyWithdrawalQueue.getPendingWithdrawalOf(staker2.address)).to.be.eq(toWei(2.5)); + expect(await legacyWithdrawalQueue.getPendingWithdrawalOf(staker3.address)).to.be.eq(toWei(1.5)); + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(toWei(1), transactErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker2).redeem(staker2.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(toWei(2.5), transactErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker3).redeem(staker3.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events[0].args["amount"]).to.be.closeTo(toWei(1.5), transactErr); + // ---------------- + }); + }); + + describe("pending emergency", async function() { + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("symbiotic", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // emergency undelegate + tx = await iVault.connect(iVaultOperator) + .emergencyUndelegate([[symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + const adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.getTotalPendingEmergencyWithdrawals()).to.be.eq(toWei(5)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + await skipEpoch(symbioticVaults[0]); + + // emergency claim + let params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .emergencyClaim([symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await asset.balanceOf(iVault.address)).to.be.eq(toWei(5)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // undelegate and claim + tx = await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), []); + await tx.wait(); + + expect(await asset.balanceOf(iVault.address)).to.be.eq(toWei(5)); + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(toWei(2)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(toWei(2), transactErr); + expect(await asset.balanceOf(iVault.address)).to.be.eq(toWei(3)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + }); + + it("mellow", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + // ---------------- + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + // ---------------- + + // emergency undelegate + tx = await iVault.connect(iVaultOperator) + .emergencyUndelegate([[mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(5), []]]); + await tx.wait(); + + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + const adapterEvents = receipt.logs?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.getTotalPendingEmergencyWithdrawals()).to.be.eq(toWei(5)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + expect(await mellowAdapter["pendingWithdrawalAmount(address,bool)"](mellowVaults[0].vaultAddress, true)).to.be.equal(toWei(5)); + + // withdraw + tx = await iVault.connect(staker).withdraw(toWei(2), staker.address); + await tx.wait(); + + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + await skipEpoch(symbioticVaults[0]); + + // emergency claim + let params = await mellowClaimParams(mellowVaults[0], claimer); + tx = await iVault.connect(iVaultOperator).emergencyClaim( + [mellowAdapter.address], [mellowVaults[0].vaultAddress], [[params]], + ); + await tx.wait(); + + expect(await asset.balanceOf(iVault.address)).to.be.eq(toWei(5)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // undelegate and claim + tx = await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), []); + await tx.wait(); + + expect(await asset.balanceOf(iVault.address)).to.be.eq(toWei(5)); + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(toWei(2)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(events[0].args["amount"]).to.be.closeTo(toWei(2), transactErr); + expect(await asset.balanceOf(iVault.address)).to.be.eq(toWei(3)); + expect(await iVault.ratio()).to.be.closeTo(toWei(1), ratioErr); + // ---------------- + }); + }); + + describe("ratio change after adding rewards", async function() { + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("mellow", async function() { + // deposit + let tx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await tx.wait(); + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(10), emptyBytes); + await tx.wait(); + expect(await iVault.ratio()).to.be.closeTo(1000000000000000000n, ratioErr); + + + // add rewards + const totalStake = await symbioticVaults[0].vault.totalStake(); + console.log("total delegated before", await iVault.getTotalDelegated()); + + // await assetData.addRewardsMellowVault(totalStake, mellowVaults[0].vaultAddress); + await assetDataNew.addRewardsMellowVault(toWei(10000), mellowVaults[0].vaultAddress); + let ratio = await iVault.ratio(); + console.log("total delegated after", await iVault.getTotalDelegated()); + + expect(await iVault.ratio()).to.be.closeTo(737886489752208013n, ratioErr); + }); + + // TODO + // it("symbiotic", async function () { + // }); + }); + + describe("two adapters", function() { + // beforeEach(async function() { + // await snapshot.restore(); + // await iVault.setTargetFlashCapacity(1n); + // }); + + it("one claimed adapter slashed", async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit + let tx = await iVault.connect(staker).deposit(toWei(100), staker.address); + await tx.wait(); + + // deposit + tx = await iVault.connect(staker2).deposit(toWei(10), staker2.address); + await tx.wait(); + + // delegate #1 + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(100), emptyBytes); + await tx.wait(); + // assert delegated amount + expect(await iVault.getTotalDelegated()).to.be.eq(toWei(100)); + + // delegate #2 + tx = await iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(9.5), emptyBytes); + await tx.wait(); + // assert delegated amount + expect(await iVault.getTotalDelegated()).to.be.closeTo(toWei(109.5), transactErr); + + // one withdraw + let shares = await iToken.balanceOf(staker.address); + tx = await iVault.connect(staker).withdraw(shares, staker.address); + await tx.wait(); + + // shares burned + expect(await iToken.totalSupply()).to.be.eq(await iToken.balanceOf(staker2.address)); + expect(await iToken.balanceOf(staker.address)).to.be.eq(0); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(epochShares).to.be.eq(shares); + tx = await iVault.connect(iVaultOperator) + .undelegate(1, [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + // assert balances + expect(await iVault.getTotalDelegated()).to.be.closeTo(toWei(9.5), transactErr); + expect(events[0].args["epoch"]).to.be.eq(1); + expect(events[0].args["adapter"]).to.be.eq(symbioticAdapter.address); + expect(events[0].args["actualAmounts"]).to.be.eq(toWei(100)); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + // slash half of the stake + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake / 2n); + expect(await iVault.ratio()).to.be.eq(1852573758880544819n); + + const pendingWithdrawal = await iVault.getPendingWithdrawals(symbioticAdapter.address); + + // claim + await skipEpoch(symbioticVaults[0]); + const params = await symbioticClaimParams(symbioticVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(0); + // expect(await asset.balanceOf(iVault.address)).to.be.eq(pendingWithdrawal); + // ---------------- + + // update ratio + let ratio = await iVault.ratio(); + expect(ratio).to.be.eq(1852573758880544819n); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + // ---------------- + + // undelegate #2 + tx = await iVault.connect(iVaultOperator) + .emergencyUndelegate([[mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(5), []]]); + receipt = await tx.wait(); + adapterEvents = receipt.logs?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + claimer = adapterEvents[0].args["claimer"]; + + expect(await iVault.ratio()).to.be.eq(1852573758880544819n); + + // claim #2 + await skipEpoch(symbioticVaults[0]); + tx = await iVault.connect(iVaultOperator).emergencyClaim( + [mellowAdapter.address], [mellowVaults[0].vaultAddress], [[await mellowClaimParams(mellowVaults[0], claimer)]], + ) + // ---------------- + + expect(await iVault.ratio()).to.be.closeTo(1852573758880544819n, 10n); + + + // force undelegate and claim + const redeemReservedBefore = await iVault.redeemReservedAmount(); + await iVault.connect(iVaultOperator).undelegate(1, []); + const redeemReservedAfter = await iVault.redeemReservedAmount(); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(0); + + expect(events[0].args["amount"]).to.be.closeTo(redeemReservedAfter - redeemReservedBefore, transactErr); + // expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + }); + + it("one non-claimed adapter slashed", async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit + let tx = await iVault.connect(staker).deposit(toWei(100), staker.address); + await tx.wait(); + + // deposit + tx = await iVault.connect(staker2).deposit(toWei(10), staker2.address); + await tx.wait(); + + // delegate #1 + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(100), emptyBytes); + await tx.wait(); + // assert delegated amount + expect(await iVault.getTotalDelegated()).to.be.eq(toWei(100)); + + // delegate #2 + tx = await iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(9.5), emptyBytes); + await tx.wait(); + // assert delegated amount + expect(await iVault.getTotalDelegated()).to.be.closeTo(toWei(109.5), transactErr); + + // one withdraw + tx = await iVault.connect(staker).withdraw(toWei(5), staker.address); + await tx.wait(); + + // undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(epochShares).to.be.eq(toWei(5)); + tx = await iVault.connect(iVaultOperator) + .undelegate(1, [[mellowAdapter.address, mellowVaults[0].vaultAddress, epochShares, []]]); + let receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + let adapterEvents = receipt.logs?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + // assert balances + expect(events[0].args["epoch"]).to.be.eq(1); + expect(events[0].args["adapter"]).to.be.eq(mellowAdapter.address); + expect(events[0].args["actualAmounts"]).to.be.eq(toWei(5)); + expect(await iVault.ratio()).to.be.eq(toWei(1)); + // ---------------- + + // slash + let totalStake = await symbioticVaults[0].vault.totalStake(); + // slash half of the stake + await assetDataNew.applySymbioticSlash(symbioticVaults[0].vault, totalStake / 2n); + expect(await iVault.ratio()).to.be.eq(1852573758880544819n); + + const pendingWithdrawal = await iVault.getPendingWithdrawals(symbioticAdapter.address); + + // claim + await skipEpoch(symbioticVaults[0]); + const params = await mellowClaimParams(mellowVaults[0], claimer); + tx = await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [mellowAdapter.address], [mellowVaults[0].vaultAddress], [[params]]); + await tx.wait(); + + let withdrawalEpoch = await withdrawalQueue.withdrawals(events[0].args["epoch"]); + expect(withdrawalEpoch[0]).to.be.eq(false); + expect(withdrawalEpoch[1]).to.be.eq(epochShares); + expect(withdrawalEpoch[2]).to.be.eq(0n); + expect(withdrawalEpoch[3]).to.be.eq(0n); + expect(withdrawalEpoch[4]).to.be.eq(0n); + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(0); + expect(await withdrawalQueue.getPendingWithdrawalOf(staker)).to.be.greaterThan(0); + // ---------------- + + // force undelegate and claim + const redeemReservedBefore = await iVault.redeemReservedAmount(); + await iVault.connect(iVaultOperator).undelegate(1, []); + const redeemReservedAfter = await iVault.redeemReservedAmount(); + // ---------------- + + // redeem + tx = await iVault.connect(staker).redeem(staker.address); + receipt = await tx.wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(0); + + expect(events[0].args["amount"]).to.be.closeTo(redeemReservedAfter - redeemReservedBefore, transactErr); + expect(await iVault.ratio()).to.be.eq(1852573758880544819n); + // ---------------- + }) + }); +}); diff --git a/projects/vaults/test/MellowV2.ts b/projects/vaults/test/MellowV2.ts new file mode 100644 index 00000000..a0ff2ae1 --- /dev/null +++ b/projects/vaults/test/MellowV2.ts @@ -0,0 +1,936 @@ +import hardhat from "hardhat"; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; + +const { ethers, network, upgrades } = hardhat; + +describe("Mellow v2", function () { + let deployer, owner, operator; + + beforeEach(async function () { + // IMPERSONATION + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: ["0x650bD9Dee50E3eE15cbb49749ff6ABcf55A8FB1e"], + }); + await network.provider.send("hardhat_setBalance", [ + "0x650bD9Dee50E3eE15cbb49749ff6ABcf55A8FB1e", + "0x10000000000000000000", + ]); + deployer = await ethers.getSigner("0x650bD9Dee50E3eE15cbb49749ff6ABcf55A8FB1e"); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: ["0x8e6C8799B542E507bfDDCA1a424867e885D96e79"], + }); + await network.provider.send("hardhat_setBalance", [ + "0x8e6C8799B542E507bfDDCA1a424867e885D96e79", + "0x10000000000000000000", + ]); + owner = await ethers.getSigner("0x8e6C8799B542E507bfDDCA1a424867e885D96e79"); + + await hre.network.provider.request({ + method: "hardhat_impersonateAccount", + params: ["0xd87D15b80445EC4251e33dBe0668C335624e54b7"], + }); + await network.provider.send("hardhat_setBalance", [ + "0xd87D15b80445EC4251e33dBe0668C335624e54b7", + "0x10000000000000000000", + ]); + operator = await ethers.getSigner("0xd87D15b80445EC4251e33dBe0668C335624e54b7"); + }); + + describe("test #1", function () { + before(async function () { + // FORKING + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: "https://eth.drpc.org", + blockNumber: 21717995, + }, + }, + ], + }); + }); + + it("", async function () { + await ethers.getContractAt("InceptionToken", "0x8E0789d39db454DBE9f4a77aCEF6dc7c69f6D552"); + let oldAbi = [ + "function totalAmountToWithdraw() external view returns(uint256)", + "function getTotalDeposited() external view returns(uint256)", + "function getTotalDelegated() external view returns(uint256)", + "function getDelegatedTo(address) external view returns(uint256)", + "function getFreeBalance() external view returns(uint256)", + "function getFlashCapacity() external view returns(uint256)", + "function getPendingWithdrawalAmountFromMellow() external view returns(uint256)", + ]; + let vault = await ethers.getContractAt(oldAbi, "0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97"); + + console.log("1==== 21717995 - Final block where all integrated vaults are mellowv1"); + console.log("Our contracts are not upgraded"); + // console.log("Ratio : " + (await inceptionToken.totalSupply() * 1000000000000000000n) / (await vault.getTotalDeposited() - await vault.totalAmountToWithdraw())); + console.log("Total Deposited: " + (await vault.getTotalDeposited())); + console.log("Total Delegated: " + (await vault.getTotalDelegated())); + console.log("Delegated (MEV): " + (await vault.getDelegatedTo("0x5fD13359Ba15A84B76f7F87568309040176167cd"))); + console.log("FreeBalance : " + (await vault.getFreeBalance())); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + console.log("PendingWithdraw: " + (await vault.getPendingWithdrawalAmountFromMellow())); + + let adapter = await ethers.getContractAt( + "InceptionWstETHMellowAdapter", + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + ); + console.log("CONVERSIONS"); + console.log( + "Vault 1: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + console.log( + "Vault 1: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + + console.log( + "Vault 2: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + console.log( + "Vault 2: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + + console.log( + "Vault 3: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + console.log( + "Vault 3: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + + console.log( + "Vault 4: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + console.log( + "Vault 4: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + + console.log( + "Vault 5: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + console.log( + "Vault 5: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + + console.log( + "Vault 6: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + console.log( + "Vault 6: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + + console.log("Depositing 20 wstETH to all vaults"); + let oldVault = await ethers.getContractAt( + [ + "function delegateToMellowVault(address,uint256) external", + "function undelegateFrom(address,uint256) external", + ], + await vault.getAddress(), + ); + + await oldVault + .connect(operator) + .delegateToMellowVault("0x5fD13359Ba15A84B76f7F87568309040176167cd", "20000000000000000000"); + await oldVault + .connect(operator) + .delegateToMellowVault("0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", "20000000000000000000"); + await oldVault + .connect(operator) + .delegateToMellowVault("0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", "20000000000000000000"); + await oldVault + .connect(operator) + .delegateToMellowVault("0x49cd586dd9BA227Be9654C735A659a1dB08232a9", "20000000000000000000"); + await oldVault + .connect(operator) + .delegateToMellowVault("0xd6E09a5e6D719d1c881579C9C8670a210437931b", "20000000000000000000"); + await oldVault + .connect(operator) + .delegateToMellowVault("0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD", "20000000000000000000"); + + console.log("AFTER DEPOSITS"); + // console.log("Ratio : " + (await inceptionToken.totalSupply() * 1000000000000000000n) / (await vault.getTotalDeposited() - await vault.totalAmountToWithdraw())); + console.log("Total Deposited: " + (await vault.getTotalDeposited())); + console.log("Total Delegated: " + (await vault.getTotalDelegated())); + console.log("Delegated (MEV): " + (await vault.getDelegatedTo("0x5fD13359Ba15A84B76f7F87568309040176167cd"))); + console.log("FreeBalance : " + (await vault.getFreeBalance())); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + console.log("PendingWithdraw: " + (await vault.getPendingWithdrawalAmountFromMellow())); + + console.log( + "Vault 1: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + console.log( + "Vault 1: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + + console.log( + "Vault 2: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + console.log( + "Vault 2: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + + console.log( + "Vault 3: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + console.log( + "Vault 3: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + + console.log( + "Vault 4: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + console.log( + "Vault 4: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + + console.log( + "Vault 5: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + console.log( + "Vault 5: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + + console.log( + "Vault 6: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + console.log( + "Vault 6: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + + console.log("Withdrawing 20 wstETH from all vaults"); + await oldVault + .connect(operator) + .undelegateFrom("0x5fD13359Ba15A84B76f7F87568309040176167cd", "20000000000000000000"); + await oldVault + .connect(operator) + .undelegateFrom("0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", "20000000000000000000"); + await oldVault + .connect(operator) + .undelegateFrom("0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", "20000000000000000000"); + await oldVault + .connect(operator) + .undelegateFrom("0x49cd586dd9BA227Be9654C735A659a1dB08232a9", "20000000000000000000"); + await oldVault + .connect(operator) + .undelegateFrom("0xd6E09a5e6D719d1c881579C9C8670a210437931b", "20000000000000000000"); + await oldVault + .connect(operator) + .undelegateFrom("0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD", "20000000000000000000"); + + console.log("AFTER WITHDRAWS"); + // console.log("Ratio : " + (await inceptionToken.totalSupply() * 1000000000000000000n) / (await vault.getTotalDeposited() - await vault.totalAmountToWithdraw())); + console.log("Total Deposited: " + (await vault.getTotalDeposited())); + console.log("Total Delegated: " + (await vault.getTotalDelegated())); + console.log("Delegated (MEV): " + (await vault.getDelegatedTo("0x5fD13359Ba15A84B76f7F87568309040176167cd"))); + console.log("FreeBalance : " + (await vault.getFreeBalance())); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + console.log("PendingWithdraw: " + (await vault.getPendingWithdrawalAmountFromMellow())); + + console.log( + "Vault 1: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + console.log( + "Vault 1: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + + console.log( + "Vault 2: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + console.log( + "Vault 2: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + + console.log( + "Vault 3: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + console.log( + "Vault 3: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + + console.log( + "Vault 4: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + console.log( + "Vault 4: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + + console.log( + "Vault 5: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + console.log( + "Vault 5: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + + console.log( + "Vault 6: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + console.log( + "Vault 6: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + }); + }); + describe("test #2", function () { + before(async function () { + // FORKING + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: "https://eth.drpc.org", + blockNumber: 21717996, + }, + }, + ], + }); + }); + + it("", async function () { + this.timeout(150000000); + + // Factory + const VaultFactory = await hre.ethers.getContractFactory("InceptionVault_S", { + libraries: { + InceptionLibrary: "0xF6940A8e7334Ab2a7781AF6f9E5aeD8EFB55116A", + }, + }); + const MellowRestakerFactory = await hre.ethers.getContractFactory("InceptionWstETHMellowAdapter"); + + // Imps + let vaultImp = await VaultFactory.deploy(); + await vaultImp.waitForDeployment(); + let restakerImp = await MellowRestakerFactory.deploy(); + await restakerImp.waitForDeployment(); + + // Upgrades + let proxyAdminVault = await ethers.getContractAt( + ["function upgradeAndCall(address,address,bytes) external payable"], + "0xC40F099e73aDB9b78a6c1AB22c520D635fFb4D53", + ); + let proxyAdminRestaker = await ethers.getContractAt( + ["function upgradeAndCall(address,address,bytes) external payable"], + "0xAb31156bcDD9C280Bb7b0d8062EFeD26e5c725AF", + ); + + await proxyAdminVault + .connect(deployer) + .upgradeAndCall("0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97", await vaultImp.getAddress(), "0x"); + await proxyAdminRestaker + .connect(deployer) + .upgradeAndCall("0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", await restakerImp.getAddress(), "0x"); + + let inceptionToken = await ethers.getContractAt("InceptionToken", "0x8E0789d39db454DBE9f4a77aCEF6dc7c69f6D552"); + let vault = await ethers.getContractAt("InceptionVault_S", "0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97"); + + const withdrawalQueueFactory = await ethers.getContractFactory("WithdrawalQueue"); + let withdrawalQueue = await upgrades.deployProxy(withdrawalQueueFactory, [await vault.getAddress(), [], [], 0]); + await vault.connect(owner).setWithdrawalQueue(await withdrawalQueue.getAddress()); + + console.log("2==== 21717996 - First block where MEV is now using mellowv2"); + console.log("Our contracts are upgraded"); + // // console.log("Ratio : " + (await inceptionToken.totalSupply() * 1000000000000000000n) / (await vault.getTotalDeposited() - await vault.totalAmountToWithdraw())); + // console.log("Total Deposited: " + await vault.getTotalDeposited()); + // console.log("Total Delegated: " + await vault.getTotalDelegated()); + console.log( + "Delegated (MEV): " + + (await vault.getDelegatedTo( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x5fD13359Ba15A84B76f7F87568309040176167cd", + )), + ); + // console.log("FreeBalance : " + await vault.getFreeBalance()); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + // console.log("PendingWithdraw: " + await vault.getPendingWithdrawalAmountFromMellow()); + }); + }); + describe("test #3", function () { + before(async function () { + // FORKING + await network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: "https://eth.drpc.org", + blockNumber: 21737235, + }, + }, + ], + }); + }); + + it("", async function () { + this.timeout(150000000); + + // Factory + const VaultFactory = await hre.ethers.getContractFactory("InceptionVault_S", { + libraries: { + InceptionLibrary: "0xF6940A8e7334Ab2a7781AF6f9E5aeD8EFB55116A", + }, + }); + const MellowRestakerFactory = await hre.ethers.getContractFactory("InceptionWstETHMellowAdapter"); + + // Imps + let vaultImp = await VaultFactory.deploy(); + await vaultImp.waitForDeployment(); + let restakerImp = await MellowRestakerFactory.deploy(); + await restakerImp.waitForDeployment(); + + // Upgrades + let proxyAdminVault = await ethers.getContractAt( + ["function upgradeAndCall(address,address,bytes) external payable"], + "0xC40F099e73aDB9b78a6c1AB22c520D635fFb4D53", + ); + let proxyAdminRestaker = await ethers.getContractAt( + ["function upgradeAndCall(address,address,bytes) external payable"], + "0xAb31156bcDD9C280Bb7b0d8062EFeD26e5c725AF", + ); + + await proxyAdminVault + .connect(deployer) + .upgradeAndCall("0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97", await vaultImp.getAddress(), "0x"); + await proxyAdminRestaker + .connect(deployer) + .upgradeAndCall("0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", await restakerImp.getAddress(), "0x"); + + let inceptionToken = await ethers.getContractAt("InceptionToken", "0x8E0789d39db454DBE9f4a77aCEF6dc7c69f6D552"); + let vault = await ethers.getContractAt("InceptionVault_S", "0xf9D9F828989A624423C48b95BC04E9Ae0ef5Ec97"); + + console.log("3==== All mellowvaults are using mellowv2"); + console.log("Setting ethWrapper"); + let adapter = await ethers.getContractAt( + "InceptionWstETHMellowAdapter", + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + ); + await adapter.connect(owner).setEthWrapper("0x7A69820e9e7410098f766262C326E211BFa5d1B1"); + + const MellowAdapterClaimerFactory = await hre.ethers.getContractFactory("MellowAdapterClaimer"); + const claimerImplementation = await MellowAdapterClaimerFactory.deploy(); + await claimerImplementation.waitForDeployment(); + + await adapter.connect(owner).setClaimerImplementation(await claimerImplementation.getAddress()); + + const withdrawalQueueFactory = await ethers.getContractFactory("WithdrawalQueue"); + let withdrawalQueue = await upgrades.deployProxy(withdrawalQueueFactory, [await vault.getAddress(), [], [], 0]); + await vault.connect(owner).setWithdrawalQueue(await withdrawalQueue.getAddress()); + + console.log("Our contracts are upgraded"); + console.log("Total Deposited: " + (await vault.getTotalDeposited())); + console.log("Total Delegated: " + (await vault.getTotalDelegated())); + console.log( + "Delegated (MEV): " + + (await vault.getDelegatedTo( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x5fD13359Ba15A84B76f7F87568309040176167cd", + )), + ); + console.log("FreeBalance : " + (await vault.getFreeBalance())); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + console.log( + "PendingWithdraw: " + (await vault.getPendingWithdrawals("0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378")), + ); + + console.log("CONVERSIONS"); + console.log( + "Vault 1: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + console.log( + "Vault 1: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + + console.log( + "Vault 2: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + console.log( + "Vault 2: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + + console.log( + "Vault 3: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + console.log( + "Vault 3: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + + console.log( + "Vault 4: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + console.log( + "Vault 4: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + + console.log( + "Vault 5: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + console.log( + "Vault 5: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + + console.log( + "Vault 6: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + console.log( + "Vault 6: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + + console.log("Depositing 20 wstETH to all vaults"); + + console.log("operator addr", await operator.getAddress()); + await vault.connect(owner).addAdapter("0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378"); + await vault + .connect(operator) + .delegate( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x5fD13359Ba15A84B76f7F87568309040176167cd", + "20000000000000000000", + [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ], + ); + await vault + .connect(operator) + .delegate( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", + "20000000000000000000", + [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ], + ); + await vault + .connect(operator) + .delegate( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", + "20000000000000000000", + [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ], + ); + await vault + .connect(operator) + .delegate( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x49cd586dd9BA227Be9654C735A659a1dB08232a9", + "20000000000000000000", + [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ], + ); + await vault + .connect(operator) + .delegate( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0xd6E09a5e6D719d1c881579C9C8670a210437931b", + "20000000000000000000", + [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ], + ); + await vault + .connect(operator) + .delegate( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD", + "20000000000000000000", + [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ], + ); + + console.log("AFTER DEPOSITS"); + // console.log("Ratio : " + (await inceptionToken.totalSupply() * 1000000000000000000n) / (await vault.getTotalDeposited() - await vault.totalAmountToWithdraw())); + console.log("Total Deposited: " + (await vault.getTotalDeposited())); + console.log("Total Delegated: " + (await vault.getTotalDelegated())); + console.log( + "Delegated (MEV): " + + (await vault.getDelegatedTo( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x5fD13359Ba15A84B76f7F87568309040176167cd", + )), + ); + console.log("FreeBalance : " + (await vault.getFreeBalance())); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + console.log( + "PendingWithdraw: " + (await vault.getPendingWithdrawals("0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378")), + ); + + console.log( + "Vault 1: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + console.log( + "Vault 1: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + + console.log( + "Vault 2: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + console.log( + "Vault 2: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + + console.log( + "Vault 3: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + console.log( + "Vault 3: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + + console.log( + "Vault 4: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + console.log( + "Vault 4: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + + console.log( + "Vault 5: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + console.log( + "Vault 5: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + + console.log( + "Vault 6: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + console.log( + "Vault 6: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + + console.log("Withdrawing 20 wstETH from all vaults"); + console.log( + "MellowV2 gives portion on withdrawal, portion is in pending state which will become in claimable state after some epoch", + ); + + let tx = await vault + .connect(operator) + .emergencyUndelegate([ + ["0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0x5fD13359Ba15A84B76f7F87568309040176167cd", "10000000000000000000", ["0x"]], + ]); + let receipt = await tx.wait(); + let events1 = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let tx2 = await vault + .connect(operator) + .emergencyUndelegate([ + ["0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", "15000000000000000000", ["0x"]], + ]); + let receipt2 = await tx2.wait(); + let events2 = receipt2.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let tx3 = await vault + .connect(operator) + .emergencyUndelegate([ + ["0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", "10000000000000000000", ["0x"]], + ]); + let receipt3 = await tx3.wait(); + let events3 = receipt3.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let tx4 = await vault + .connect(operator) + .emergencyUndelegate([ + ["0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0x49cd586dd9BA227Be9654C735A659a1dB08232a9", "15000000000000000000", ["0x"]], + ]); + let receipt4 = await tx4.wait(); + let events4 = receipt4.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let tx5 = await vault + .connect(operator) + .emergencyUndelegate([ + ["0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0xd6E09a5e6D719d1c881579C9C8670a210437931b", "10000000000000000000", ["0x"]], + ]); + let receipt5 = await tx5.wait(); + let events5 = receipt5.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let tx6 = await vault + .connect(operator) + .emergencyUndelegate([ + ["0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD", "15000000000000000000", ["0x"]] + ]); + let receipt6 = await tx6.wait(); + let events6 = receipt6.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + let adapterAddress = await adapter.getAddress(); + const adapterEvents = receipt6.logs + ?.filter(log => log.address === adapterAddress) + .map(log => adapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + console.log("AFTER WITHDRAWS"); + // console.log("Ratio : " + (await inceptionToken.totalSupply() * 1000000000000000000n) / (await vault.getTotalDeposited() - await vault.totalAmountToWithdraw())); + console.log("Total Deposited: " + (await vault.getTotalDeposited())); + console.log("Total Delegated: " + (await vault.getTotalDelegated())); + console.log( + "Delegated (MEV): " + + (await vault.getDelegatedTo( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x5fD13359Ba15A84B76f7F87568309040176167cd", + )), + ); + console.log("FreeBalance : " + (await vault.getFreeBalance())); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + console.log( + "PendingWithdraw: " + (await vault.getPendingWithdrawals("0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378")), + ); + + console.log( + "Vault 1: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + console.log( + "Vault 1: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + + console.log( + "Vault 2: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + console.log( + "Vault 2: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + + console.log( + "Vault 3: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + console.log( + "Vault 3: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + + console.log( + "Vault 4: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + console.log( + "Vault 4: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + + console.log( + "Vault 5: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + console.log( + "Vault 5: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + + console.log( + "Vault 6: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + console.log( + "Vault 6: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + + console.log("PendingWithdrawalAmountInMellow : " + (await adapter.pendingWithdrawalAmount())); + console.log("ClaimableWithdrawalAmountInMellow: " + (await adapter.claimableWithdrawalAmount())); + console.log("PortionsGivenBackOnWithdrawTX : " + (await adapter.claimableAmount())); + + console.log("Increasing epoch"); + await helpers.time.increase(1209900); + + console.log("ClaimMellowWithdrawCallback"); + const abi = ethers.AbiCoder.defaultAbiCoder(); + + if (events1[0].args["actualAmounts"] > 0) { + await vault + .connect(operator) + .claim( + 0, + [ + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + ], + [ + "0x5fD13359Ba15A84B76f7F87568309040176167cd", + "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", + "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", + "0x49cd586dd9BA227Be9654C735A659a1dB08232a9", + "0xd6E09a5e6D719d1c881579C9C8670a210437931b", + "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD", + ], + [ + [abi.encode(["address", "address"], ["0x5fD13359Ba15A84B76f7F87568309040176167cd", claimer])], + [abi.encode(["address", "address"], ["0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", claimer])], + [abi.encode(["address", "address"], ["0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", claimer])], + [abi.encode(["address", "address"], ["0x49cd586dd9BA227Be9654C735A659a1dB08232a9", claimer])], + [abi.encode(["address", "address"], ["0xd6E09a5e6D719d1c881579C9C8670a210437931b", claimer])], + [abi.encode(["address", "address"], ["0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD", claimer])], + ], + ); + } + + // if (events2[0].args["actualAmounts"] > 0) { + // await vault.connect(operator).claim(0, "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", [abi.encode(["address"], ["0x7a4EffD87C2f3C55CA251080b1343b605f327E3a"])]); + // } + // + // if (events3[0].args["actualAmounts"] > 0) { + // await vault.connect(operator).claim(0, "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", [abi.encode(["address"], ["0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a"])]); + // } + // + // if (events4[0].args["actualAmounts"] > 0) { + // await vault.connect(operator).claim(0, "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0x49cd586dd9BA227Be9654C735A659a1dB08232a9", [abi.encode(["address"], ["0x49cd586dd9BA227Be9654C735A659a1dB08232a9"])]); + // } + // + // if (events5[0].args["actualAmounts"] > 0) { + // await vault.connect(operator).claim(0, "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0xd6E09a5e6D719d1c881579C9C8670a210437931b", [abi.encode(["address"], ["0xd6E09a5e6D719d1c881579C9C8670a210437931b"])]); + // } + // + // if (events6[0].args["actualAmounts"] > 0) { + // await vault.connect(operator).claim(0, "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD", [abi.encode(["address"], ["0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD"])]); + // } + + console.log("AFTER ClaimMellowWithdrawCallback"); + // console.log("Ratio : " + (await inceptionToken.totalSupply() * 1000000000000000000n) / (await vault.getTotalDeposited() - await vault.totalAmountToWithdraw())); + console.log("Total Deposited: " + (await vault.getTotalDeposited())); + console.log("Total Delegated: " + (await vault.getTotalDelegated())); + console.log( + "Delegated (MEV): " + + (await vault.getDelegatedTo( + "0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378", + "0x5fD13359Ba15A84B76f7F87568309040176167cd", + )), + ); + console.log("FreeBalance : " + (await vault.getFreeBalance())); + console.log("FlashCapacity : " + (await vault.getFlashCapacity())); + console.log( + "PendingWithdraw: " + (await vault.getPendingWithdrawals("0x09740e3B2CCF6e82F4fb3A57519c8b65dA728378")), + ); + + console.log( + "Vault 1: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + console.log( + "Vault 1: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x5fD13359Ba15A84B76f7F87568309040176167cd")), + ); + + console.log( + "Vault 2: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + console.log( + "Vault 2: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a")), + ); + + console.log( + "Vault 3: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + console.log( + "Vault 3: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a")), + ); + + console.log( + "Vault 4: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + console.log( + "Vault 4: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0x49cd586dd9BA227Be9654C735A659a1dB08232a9")), + ); + + console.log( + "Vault 5: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + console.log( + "Vault 5: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xd6E09a5e6D719d1c881579C9C8670a210437931b")), + ); + + console.log( + "Vault 6: " + + (await adapter.amountToLpAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + console.log( + "Vault 6: " + + (await adapter.lpAmountToAmount(1000000000000000000n, "0xcC36e5272c422BEE9A8144cD2493Ac472082eBaD")), + ); + + console.log("PendingWithdrawalAmountInMellow : " + (await adapter.pendingWithdrawalAmount())); + console.log("ClaimableWithdrawalAmountInMellow: " + (await adapter.claimableWithdrawalAmount())); + console.log("PortionsGivenBackOnWithdrawTX : " + (await adapter.claimableAmount())); + }); + }); +}); + diff --git a/projects/vaults/test/data/assets/inception-vault-s.ts b/projects/vaults/test/data/assets/inception-vault-s.ts new file mode 100644 index 00000000..58dd69fe --- /dev/null +++ b/projects/vaults/test/data/assets/inception-vault-s.ts @@ -0,0 +1,43 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import hardhat from "hardhat"; +import { impersonateWithEth, toWei } from '../../helpers/utils'; +const { ethers } = hardhat; + +const donorAddress = '0x5313b39bf226ced2332C81eB97BB28c6fD50d1a3'; +const stETHAddress = '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84'; // Lido stETH + +export const stETH = { + assetName: "stETH", + assetAddress: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", // wstETH, collateral + vaultName: "InstEthVault", + vaultFactory: "InceptionVault_S", + iVaultOperator: "0xd87D15b80445EC4251e33dBe0668C335624e54b7", + ratioErr: 3n, + transactErr: 5n, + blockNumber: 22516224, //21687985, + impersonateStaker: async function (staker, iVault) { + const donor = await impersonateWithEth(donorAddress, toWei(1)); + const wstEth = await ethers.getContractAt("IWSteth", this.assetAddress); + await wstEth.connect(donor).transfer(staker.address, toWei(1000)); + await wstEth.connect(staker).approve(await iVault.getAddress(), toWei(1000)); + return staker; + }, + addRewardsMellowVault: async function (amount, mellowVault) { + const donor = await impersonateWithEth(donorAddress, toWei(1)); + const wstEth = await ethers.getContractAt("IWSteth", this.assetAddress); + await wstEth.connect(donor).transfer(mellowVault, amount); + }, + applySymbioticSlash: async function (symbioticVault, slashAmount) { + const slasherAddressStorageIndex = 3; + + const [deployer] = await ethers.getSigners(); + + await helpers.setStorageAt( + await symbioticVault.getAddress(), + slasherAddressStorageIndex, + ethers.AbiCoder.defaultAbiCoder().encode(["address"], [await deployer.getAddress()]), + ); + + await symbioticVault.connect(deployer).onSlash(slashAmount, await symbioticVault.currentEpochStart()); + }, +}; diff --git a/projects/vaults/test/data/assets/new/index.ts b/projects/vaults/test/data/assets/new/index.ts new file mode 100644 index 00000000..6f6d19b7 --- /dev/null +++ b/projects/vaults/test/data/assets/new/index.ts @@ -0,0 +1,79 @@ +import 'dotenv/config'; +import importSync from 'import-sync'; +import { stETH } from './stETH'; +import { impersonateWithEth, toWei } from '../../../helpers/utils'; +import hardhat from "hardhat"; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; + +const { ethers } = hardhat; + +const assetName = process.env.ASSET_NAME; +if (!assetName) throw new Error("ASSET_NAME variable is required. Please set it in your .env file"); + +type AssetData = typeof stETH & { + impersonateStaker: (staker: any, iVault: any) => Promise; + addRewardsMellowVault: (amount: any, mellowVault: any) => Promise; + applySymbioticSlash: (symbioticVault: any, slashAmount: any) => Promise; +}; + +const filePath = `./${assetName}.ts`; +let assetData: AssetData; +try { + const importedModule = importSync(filePath); + const importedModuleKeys = Object.keys(importedModule); + if (importedModuleKeys.length === 0) { + throw new Error(`No exports found in ${filePath}`); + } + assetData = importedModule[importedModuleKeys[0]] as AssetData; +} catch (error) { + // const filesInDir = fs.readdirSync(`${process.cwd()}/test/data/assets/new/${assetName}`); + // const availableAssetNames = filesInDir.map(file => file.replace('.ts', '')).filter(name => name !== 'index'); + // throw new Error(`Asset data file not found. Available asset names: ${availableAssetNames.join(', ')}`); + throw new Error(`File with data for ${assetName} not found.`); +} + +assetData.impersonateStaker = async function (staker, iVault) { + const donor = await impersonateWithEth(assetData.asset.donor, toWei(1)); + const stEth = await ethers.getContractAt("stETH", assetData.asset.nonWrappedAssetAddress); + const stEthAmount = toWei(1000); + await stEth.connect(donor).approve(this.asset.address, stEthAmount); + + const wstEth = await ethers.getContractAt("IWSteth", this.asset.address); + const balanceBefore = await wstEth.balanceOf(donor.address); + await wstEth.connect(donor).wrap(stEthAmount); + const balanceAfter = await wstEth.balanceOf(donor.address); + + const wstAmount = balanceAfter - balanceBefore; + await wstEth.connect(donor).transfer(staker.address, wstAmount); + await wstEth.connect(staker).approve(await iVault.getAddress(), wstAmount); + return staker; +}; + +assetData.addRewardsMellowVault = async function (amount, mellowVault) { + const donor = await impersonateWithEth(this.asset.donor, toWei(1)); + const stEth = await ethers.getContractAt("stETH", assetData.asset.nonWrappedAssetAddress); + await stEth.connect(donor).approve(this.asset.address, amount); + + const wstEth = await ethers.getContractAt("IWSteth", this.asset.address); + const balanceBefore = await wstEth.balanceOf(donor); + await wstEth.connect(donor).wrap(amount); + const balanceAfter = await wstEth.balanceOf(donor); + const wstAmount = balanceAfter - balanceBefore; + await wstEth.connect(donor).transfer(mellowVault, wstAmount); +}; + +assetData.applySymbioticSlash = async function (symbioticVault, slashAmount) { + const slasherAddressStorageIndex = 3; + + const [deployer] = await ethers.getSigners(); + + await helpers.setStorageAt( + await symbioticVault.getAddress(), + slasherAddressStorageIndex, + ethers.AbiCoder.defaultAbiCoder().encode(["address"], [await deployer.getAddress()]), + ); + + await symbioticVault.connect(deployer).onSlash(slashAmount, await symbioticVault.currentEpochStart()); +}; + +export { assetData }; diff --git a/projects/vaults/test/data/assets/new/stETH.ts b/projects/vaults/test/data/assets/new/stETH.ts new file mode 100644 index 00000000..8249f094 --- /dev/null +++ b/projects/vaults/test/data/assets/new/stETH.ts @@ -0,0 +1,67 @@ +const stETHAddress = '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84'; // Lido stETH + +export const stETH = { + blockNumber: 21850700, + ratioErr: 3n, + transactErr: 5n, + vault: { + name: "InstEthVault", + contractName: "InceptionVault_S", // vaultFactory + operator: '0xd87D15b80445EC4251e33dBe0668C335624e54b7', // iVaultOperator + }, + + asset: { + name: "stETH", + nonWrappedAssetAddress: stETHAddress, + address: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", // wstETH, collateral + strategy: "0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3", + donor: "0x43594da5d6A03b2137a04DF5685805C676dEf7cB", + }, + + adapters: { + mellow: [ + { + name: "P2P", + vaultAddress: "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", + wrapperAddress: "0x41A1FBEa7Ace3C3a6B66a73e96E5ED07CDB2A34d", + bondStrategyAddress: "0xA0ea6d4fe369104eD4cc18951B95C3a43573C0F6", + curatorAddress: "0x4a3c7F2470Aa00ebE6aE7cB1fAF95964b9de1eF4", + configuratorAddress: "0x84b240E99d4C473b5E3dF1256300E2871412dDfe", + }, + { + name: "Mev Capital", + vaultAddress: "0x5fD13359Ba15A84B76f7F87568309040176167cd", + wrapperAddress: "0xdC1741f9bD33DD791942CC9435A90B0983DE8665", + bondStrategyAddress: "0xc3A149b5Ca3f4A5F17F5d865c14AA9DBb570F10A", + curatorAddress: "0xA1E38210B06A05882a7e7Bfe167Cd67F07FA234A", + configuratorAddress: "0x2dEc4fDC225C1f71161Ea481E23D66fEaAAE2391", + }, + { + name: "Re7", + vaultAddress: "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", + wrapperAddress: "0x70cD3464A41B6692413a1Ba563b9D53955D5DE0d", + bondStrategyAddress: "0xcE3A8820265AD186E8C1CeAED16ae97176D020bA", + curatorAddress: "0xE86399fE6d7007FdEcb08A2ee1434Ee677a04433", + configuratorAddress: "0x214d66d110060dA2848038CA0F7573486363cAe4", + }, + ], + symbiotic: [ + { + name: "Gauntlet Restaked wstETH", + vaultAddress: "0xc10A7f0AC6E3944F4860eE97a937C51572e3a1Da", + collateral: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + burner: "0xDB0737bd7eBEA50135e4c8af56900b029b858371", + delegator: "0x1f16782a9b75FfFAD87e7936791C672bdDBCb8Ec", + slasher: "0x541c86eb2C5e7F3E0C04eF82aeb68EA6A86409ef", + }, + { + name: "Ryabina wstETH", + vaultAddress: "0x93b96D7cDe40DC340CA55001F46B3B8E41bC89B4", + collateral: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + burner: "0x80918bcD2d1e343ed46E201CD09238149dB5A5bF", + delegator: "0x742DD9676086579994E9a3DD536C9CCc0Cc6e78D", + slasher: "0xCCA42120Dc4fc945F2fBd227d7D9EA5963bba490", + }, + ] + }, +}; diff --git a/projects/vaults/test/data/assets/stETH.ts b/projects/vaults/test/data/assets/stETH.ts new file mode 100644 index 00000000..a43441d7 --- /dev/null +++ b/projects/vaults/test/data/assets/stETH.ts @@ -0,0 +1,56 @@ +import { ethers } from "hardhat"; +import { impersonateWithEth, toWei } from "../../helpers/utils"; + +export const wstETH = { + vaultName: "InstEthVault", + vaultFactory: "InceptionVault_S", + assetName: "stETH", + assetAddress: "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", + assetPoolName: "LidoMockPool", + assetPool: "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", + assetStrategy: "0x7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3", + strategyManager: "0xdfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6", + iVaultOperator: "0xa4341b5Cf43afD2993e1ae47d956F44A2d6Fc08D", + delegationManager: "0xA44151489861Fe9e3055d95adC98FbD462B948e7", + rewardsCoordinator: "0xAcc1fb458a1317E886dB376Fc8141540537E68fE", + withdrawalDelayBlocks: 400, + ratioErr: 2n, + transactErr: 5n, + blockNumber: 3338549, + url: "https://holesky.drpc.org", + impersonateStaker: async function (staker, iVault) { + const stETHDonorAddress = "0x66b25CFe6B9F0e61Bd80c4847225Baf4EE6Ba0A2"; + const donor = await impersonateWithEth(stETHDonorAddress, toWei(1)); + const stEth = await ethers.getContractAt("stETH", this.assetAddress); + const stEthAmount = toWei(1000); + await stEth.connect(donor).transfer(staker.address, stEthAmount); + await stEth.connect(staker).approve(iVault, stEthAmount); + return staker; + }, + }; + +export const wstETHWrapped = { + ...wstETH, + assetAddress: "0x8d09a4502cc8cf1547ad300e066060d043f6982d", + backedAssetAddress: "0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034", + impersonateStaker: async function(staker, iVault) { + const wstETHDonorAddress = "0x0000000000a2d441d85315e5163dEEC094bf6FE1"; + const donor1 = await impersonateWithEth(wstETHDonorAddress, toWei(10)); + + const wstAmount = toWei(100); + const wstEth = await ethers.getContractAt("IWSteth", this.assetAddress); + await wstEth.connect(donor1).transfer(staker.address, wstAmount); + await wstEth.connect(staker).approve(await iVault.getAddress(), wstAmount); + + const stETHDonorAddress = "0x66b25CFe6B9F0e61Bd80c4847225Baf4EE6Ba0A2"; + const donor2 = await impersonateWithEth(stETHDonorAddress, toWei(1)); + const stEth = await ethers.getContractAt("stETH", this.backedAssetAddress); + const stEthAmount = toWei(1000); + await stEth.connect(donor2).transfer(staker.address, stEthAmount); + await stEth.connect(staker).approve(iVault, stEthAmount); + + return staker; + }, +}; + +export type AssetData = typeof wstETH | typeof wstETHWrapped; diff --git a/projects/vaults/test/data/vaults.ts b/projects/vaults/test/data/vaults.ts new file mode 100644 index 00000000..4c6b27d6 --- /dev/null +++ b/projects/vaults/test/data/vaults.ts @@ -0,0 +1,55 @@ +export const vaults = { + eigenLayer: [ + "0x78FDDe7a5006cC64E109aeD99cA7B0Ad3d8687bb", + "0x1B71f18fc496194b21D0669B5ADfE299a8cFEc42", + "0x4Dbfa8bcccb1740d8044E1A093F9A078A88E45FE", + "0x5B9A8c72B29Ee17e72ba8B9626Bf43a75B15FB3d", + "0x139A091BcAad0ee1DAabe93cbBd194736B197FB6", + ], + + mellow: [ + { + name: "P2P", + vaultAddress: "0x7a4EffD87C2f3C55CA251080b1343b605f327E3a", + wrapperAddress: "0x41A1FBEa7Ace3C3a6B66a73e96E5ED07CDB2A34d", + bondStrategyAddress: "0xA0ea6d4fe369104eD4cc18951B95C3a43573C0F6", + curatorAddress: "0x4a3c7F2470Aa00ebE6aE7cB1fAF95964b9de1eF4", + configuratorAddress: "0x84b240E99d4C473b5E3dF1256300E2871412dDfe", + }, + { + name: "Mev Capital", + vaultAddress: "0x5fD13359Ba15A84B76f7F87568309040176167cd", + wrapperAddress: "0xdC1741f9bD33DD791942CC9435A90B0983DE8665", + bondStrategyAddress: "0xc3A149b5Ca3f4A5F17F5d865c14AA9DBb570F10A", + curatorAddress: "0xA1E38210B06A05882a7e7Bfe167Cd67F07FA234A", + configuratorAddress: "0x2dEc4fDC225C1f71161Ea481E23D66fEaAAE2391", + }, + { + name: "Re7", + vaultAddress: "0x84631c0d0081FDe56DeB72F6DE77abBbF6A9f93a", + wrapperAddress: "0x70cD3464A41B6692413a1Ba563b9D53955D5DE0d", + bondStrategyAddress: "0xcE3A8820265AD186E8C1CeAED16ae97176D020bA", + curatorAddress: "0xE86399fE6d7007FdEcb08A2ee1434Ee677a04433", + configuratorAddress: "0x214d66d110060dA2848038CA0F7573486363cAe4", + }, + ], + + symbiotic: [ + { + name: "Gauntlet Restaked wstETH", + vaultAddress: "0xc10A7f0AC6E3944F4860eE97a937C51572e3a1Da", + collateral: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + burner: "0xDB0737bd7eBEA50135e4c8af56900b029b858371", + delegator: "0x1f16782a9b75FfFAD87e7936791C672bdDBCb8Ec", + slasher: "0x541c86eb2C5e7F3E0C04eF82aeb68EA6A86409ef", + }, + { + name: "Ryabina wstETH", + vaultAddress: "0x93b96D7cDe40DC340CA55001F46B3B8E41bC89B4", + collateral: "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0", + burner: "0x80918bcD2d1e343ed46E201CD09238149dB5A5bF", + delegator: "0x742DD9676086579994E9a3DD536C9CCc0Cc6e78D", + slasher: "0xCCA42120Dc4fc945F2fBd227d7D9EA5963bba490", + }, + ] +} diff --git a/projects/vaults/test/helpers/utils.js b/projects/vaults/test/helpers/utils.ts similarity index 64% rename from projects/vaults/test/helpers/utils.js rename to projects/vaults/test/helpers/utils.ts index 78ec4640..8799560e 100644 --- a/projects/vaults/test/helpers/utils.js +++ b/projects/vaults/test/helpers/utils.ts @@ -1,14 +1,23 @@ -const helpers = require("@nomicfoundation/hardhat-network-helpers"); -const { ethers, network } = require("hardhat"); -BigInt.prototype.format = function () { +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { ethers, network } from "hardhat"; +import { abi } from "../src/init-vault-new"; + +BigInt.prototype.format = function() { return this.toLocaleString("de-DE"); }; -const addRewardsToStrategyEigen = async (strategyAddress, amount, staker) => { - //const strategy = await ethers.getContractAt("IStrategy", strategyAddress); - const asset = await ethers.getContractAt("Eigen", "0x3B78576F7D6837500bA3De27A60c7f594934027E"); +const addRewardsToStrategyWrap = async (strategyAddress, assetAddres, amount, staker) => { + const strategy = await ethers.getContractAt("IStrategy", strategyAddress); + + console.log("await strategy.underlyingToken(): ", await strategy.underlyingToken()); + console.log("assetAddres: ", assetAddres); + + const asset = await ethers.getContractAt("Eigen", assetAddres); + console.log("assetAddres: ", await asset.balanceOf(await staker.getAddress())); + await asset.connect(staker).unwrap(amount); - const bEigen = await ethers.getContractAt("BackingEigen", "0x275cCf9Be51f4a6C94aBa6114cdf2a4c45B9cb27"); + + const bEigen = await ethers.getContractAt("Eigen", await strategy.underlyingToken()); await bEigen.connect(staker).transfer(strategyAddress, amount); }; @@ -27,42 +36,43 @@ const addRewardsToStrategy = async (strategyAddress, amount, staker) => { }; const calculateRatio = async (vault, token) => { + return vault.ratio(); + const totalDelegated = await vault.getTotalDelegated(); + const totalAssets = await vault.totalAssets(); + const depositBonusAmount = await vault.depositBonusAmount(); + const emergencyPendingWithdrawals = await vault.getTotalPendingEmergencyWithdrawals(); + const pendingWithdrawals = await vault.getTotalPendingWithdrawals(); + const totalSharesToWithdraw = await vault.totalSharesToWithdraw(); + const redeemReservedAmount = await vault.redeemReservedAmount(); const totalSupply = await token.totalSupply(); - const totalDeposited = await vault.getTotalDeposited(); - const totalAmountToWithdraw = await vault.totalAmountToWithdraw(); - let denominator; - if (totalDeposited < totalAmountToWithdraw) { - denominator = 0n; - } else { - denominator = totalDeposited - totalAmountToWithdraw; - } + // shares + const numeral = totalSupply + totalSharesToWithdraw; + // tokens/assets + const denominator = totalDelegated + totalAssets + emergencyPendingWithdrawals - depositBonusAmount + pendingWithdrawals - redeemReservedAmount; - if (denominator === 0n || totalSupply === 0n) { - console.log("iToken supply is 0, so the ration is going to be 1e18"); + if (denominator === 0n || numeral === 0n || (totalSupply === 0n && totalDelegated <= 0n)) { + console.log("iToken supply is 0, so the ratio is going to be 1e18"); return e18; } - const ratio = (totalSupply * e18) / denominator; - if ((totalSupply * e18) % denominator !== 0n) { - return ratio + 1n; - } - // console.log(`Current ratio is:\t\t\t\t${ratio.format()}`); + const ratio = (numeral * e18) / denominator; return ratio; }; -const withdrawDataFromTx = async (tx, operatorAddress, restaker) => { +const withdrawDataFromTx = async (tx, operatorAddress, adapter) => { const receipt = await tx.wait(); if (receipt.logs.length !== 3) { console.error("WRONG NUMBER OF EVENTS in withdrawFromEigenLayerEthAmount()", receipt.logs.length); console.log(receipt.logs); } + console.log(receipt.logs[receipt.logs.length - 2]); const WithdrawalQueuedEvent = receipt.logs?.find((e) => e.eventName === "StartWithdrawal").args; return [ WithdrawalQueuedEvent["stakerAddress"], operatorAddress, - restaker, + adapter, WithdrawalQueuedEvent["nonce"], WithdrawalQueuedEvent["withdrawalStartBlock"], [WithdrawalQueuedEvent["strategy"]], @@ -137,17 +147,31 @@ const randomBIMax = (max) => { async function sleep(msec) { return new Promise((resolve) => setTimeout(resolve, msec)); } + const randomAddress = () => ethers.Wallet.createRandom().address; const format = (bi) => bi.toLocaleString("de-DE"); const e18 = 1000_000_000_000_000_000n; -const e9 = 1000_000_000n; -const zeroWithdrawalData = [ethers.ZeroAddress, ethers.ZeroAddress, ethers.ZeroAddress, 0, 1, [ethers.ZeroAddress], [0]]; const day = 86400n; -module.exports = { + +async function skipEpoch(symbioticVault) { + let epochDuration = await symbioticVault.vault.epochDuration(); + let nextEpochStart = await symbioticVault.vault.nextEpochStart(); + await setBlockTimestamp(Number(nextEpochStart + epochDuration + 1n)); +} + +async function symbioticClaimParams(symbioticVault, claimer) { + return abi.encode( + ["address", "address"], + [symbioticVault.vaultAddress, claimer], + ); +} + +export { addRewardsToStrategy, + addRewardsToStrategyWrap, withdrawDataFromTx, impersonateWithEth, setBlockTimestamp, @@ -159,9 +183,12 @@ module.exports = { toBN, randomBI, randomBIMax, + skipEpoch, sleep, randomAddress, + symbioticClaimParams, format, e18, day, }; + diff --git a/projects/vaults/test/src/constants.ts b/projects/vaults/test/src/constants.ts new file mode 100644 index 00000000..c671dd01 --- /dev/null +++ b/projects/vaults/test/src/constants.ts @@ -0,0 +1,21 @@ +export enum Network { + mainnet = 'mainnet', + testnet = 'testnet' +}; + +export const Assets = { + stETH: 'stETH', + wstETH: 'wstETH', +} + +export const emptyBytes = [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", +]; + +export const adapters = { + EigenLayer: 'EigenLayer', + Mellow: 'Mellow', + Symbiotic: 'Symbiotic', +}; + +export type Adapter = typeof adapters[keyof typeof adapters]; diff --git a/projects/vaults/test/src/init-vault-new.ts b/projects/vaults/test/src/init-vault-new.ts new file mode 100644 index 00000000..5cd625b8 --- /dev/null +++ b/projects/vaults/test/src/init-vault-new.ts @@ -0,0 +1,212 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import hardhat from "hardhat"; +import { stETH } from "../data/assets/new/stETH"; +import { e18, impersonateWithEth } from "../helpers/utils"; +import { Adapter, adapters, emptyBytes } from "./constants"; +const { ethers, upgrades, network } = hardhat; + +export async function initVault( + assetData: typeof stETH, + options?: { adapters?: Adapter[]; eigenAdapterContractName?: string }, +) { + if (options?.adapters?.includes(adapters.EigenLayer) && !options.eigenAdapterContractName) { + throw new Error("EigenLayer adapter requires eigenAdapterContractName"); + } + + const block = await ethers.provider.getBlock("latest"); + if (!block) throw new Error("Failed to get latest block"); + + console.log(`Starting at block number: ${block.number}`); + console.log("Initialization of Inception ...."); + + const asset = await ethers.getContractAt("IERC20", assetData.asset.address); + asset.address = await asset.getAddress(); + + if (options?.adapters?.includes(adapters.Mellow)) { + // for (const mVaultInfo of mellowVaults) { + for (const mVaultInfo of assetData.adapters.mellow) { + console.log(`- MellowVault ${mVaultInfo.name} and curator`); + mVaultInfo.vault = await ethers.getContractAt("IMellowVault", mVaultInfo.vaultAddress); + + const mellowVaultOperatorMock = await ethers.deployContract("OperatorMock", [mVaultInfo.bondStrategyAddress]); + mellowVaultOperatorMock.address = await mellowVaultOperatorMock.getAddress(); + await network.provider.send("hardhat_setCode", [ + mVaultInfo.curatorAddress, + await mellowVaultOperatorMock.getDeployedCode(), + ]); + + //Copy storage values + for (let i = 0; i < 5; i++) { + const slot = "0x" + i.toString(16); + const value = await network.provider.send("eth_getStorageAt", [ + mellowVaultOperatorMock.address, + slot, + "latest", + ]); + await network.provider.send("hardhat_setStorageAt", [mVaultInfo.curatorAddress, slot, value]); + } + + mVaultInfo.curator = await ethers.getContractAt("OperatorMock", mVaultInfo.curatorAddress); + } + } + + if (options?.adapters?.includes(adapters.Symbiotic)) { + // for (const sVaultInfo of symbioticVaults) { + for (const sVaultInfo of assetData.adapters.symbiotic) { + console.log(`- Symbiotic ${sVaultInfo.name}`); + sVaultInfo.vault = await ethers.getContractAt("IVault", sVaultInfo.vaultAddress); + } + } + + const iTokenFactory = await ethers.getContractFactory("InceptionToken"); + const iToken = await upgrades.deployProxy(iTokenFactory, ["TEST InceptionLRT Token", "tINt"]); + iToken.address = await iToken.getAddress(); + + const iVaultOperator = await impersonateWithEth(assetData.vault.operator, e18); + + let mellowAdapter: any, symbioticAdapter: any, eigenLayerAdapter: any; + + if (options?.adapters?.includes(adapters.Mellow)) { + const mellowAdapterFactory = await ethers.getContractFactory("InceptionWstETHMellowAdapter"); + mellowAdapter = await upgrades.deployProxy(mellowAdapterFactory, [ + // [mellowVaults[0].vaultAddress], assetData.asset.address, assetData.vault.operator, + [assetData.adapters.mellow[0].vaultAddress], + assetData.asset.address, + assetData.vault.operator, + ]); + + // deploy a claimer implementation + const MellowAdapterClaimerFactory = await ethers.getContractFactory("MellowAdapterClaimer"); + const claimerImplementation = await MellowAdapterClaimerFactory.deploy(); + await claimerImplementation.waitForDeployment(); + + await mellowAdapter.setClaimerImplementation(await claimerImplementation.getAddress()); + + mellowAdapter.address = await mellowAdapter.getAddress(); + } + + if (options?.adapters?.includes(adapters.Symbiotic)) { + const symbioticAdapterFactory = await ethers.getContractFactory("InceptionSymbioticAdapter"); + symbioticAdapter = await upgrades.deployProxy(symbioticAdapterFactory, [ + // [symbioticVaults[0].vaultAddress], assetData.asset.address, assetData.vault.operator, + [assetData.adapters.symbiotic[0].vaultAddress], + assetData.asset.address, + assetData.vault.operator, + ]); + + // deploy a claimer implementation + const SymbioticAdapterClaimerFactory = await ethers.getContractFactory("SymbioticAdapterClaimer"); + const claimerImplementation = await SymbioticAdapterClaimerFactory.deploy(); + await claimerImplementation.waitForDeployment(); + + await symbioticAdapter.setClaimerImplementation(await claimerImplementation.getAddress()); + + symbioticAdapter.address = await symbioticAdapter.getAddress(); + } + + // ratio + const iRatioFeedFactory = await ethers.getContractFactory("InceptionRatioFeed"); + const ratioFeed = await upgrades.deployProxy(iRatioFeedFactory, []); + await ratioFeed.updateRatioBatch([iToken.address], [e18]); //Set initial ratio e18 + ratioFeed.address = await ratioFeed.getAddress(); + + const iLibrary = await ethers.deployContract("InceptionLibrary"); + await iLibrary.waitForDeployment(); + + const iVaultFactory = await ethers.getContractFactory(assetData.vault.contractName, { + libraries: { InceptionLibrary: await iLibrary.getAddress() }, + }); + const iVault = await upgrades.deployProxy( + iVaultFactory, + [assetData.vault.name, assetData.vault.operator, assetData.asset.address, iToken.address], + { + unsafeAllowLinkedLibraries: true, + }, + ); + iVault.address = await iVault.getAddress(); + + // if (options?.adapters?.includes(adapters.EigenLayer) && options.eigenAdapterContractName) { + // let [deployer] = await ethers.getSigners(); + // const eigenLayerAdapterFactory = await ethers.getContractFactory(options.eigenAdapterContractName); + // eigenLayerAdapter = await upgrades.deployProxy(eigenLayerAdapterFactory, [ + // await deployer.getAddress(), + // assetData.rewardsCoordinator, + // assetData.delegationManager, + // assetData.strategyManager, + // assetData.assetStrategy, + // assetData.assetAddress, + // assetData.iVaultOperator, + // iVault.address, + // ]); + // eigenLayerAdapter.address = await eigenLayerAdapter.getAddress(); + // } + + const withdrawalQueueFactory = await ethers.getContractFactory("WithdrawalQueue"); + const withdrawalQueue = await upgrades.deployProxy(withdrawalQueueFactory, [iVault.address, [], [], 0]); + withdrawalQueue.address = await withdrawalQueue.getAddress(); + + await iVault.setRatioFeed(ratioFeed.address); + + if (options?.adapters?.includes(adapters.Mellow)) { + await iVault.addAdapter(mellowAdapter.address); + await mellowAdapter.setInceptionVault(iVault.address); + await mellowAdapter.setEthWrapper("0x7A69820e9e7410098f766262C326E211BFa5d1B1"); + + // await emergencyClaimer.approveSpender(assetData.assetAddress, mellowAdapter.address); + MAX_TARGET_PERCENT = await iVault.MAX_TARGET_PERCENT(); + console.log("... iVault initialization completed ...."); + + iVault.withdrawFromMellowAndClaim = async function (mellowVaultAddress, amount) { + const tx = await this.connect(iVaultOperator).emergencyUndelegate([ + [await mellowAdapter.getAddress(), mellowVaultAddress, amount, []] + ]); + + const receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + // NEW + const adapterEvents = receipt.logs + ?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + await helpers.time.increase(1209900); + const params = abi.encode(["address", "address"], [mellowVaultAddress, claimer]); + if (events[0].args["actualAmounts"] > 0) { + await this.connect(iVaultOperator).emergencyClaim( + [await mellowAdapter.getAddress()], + [mellowVaultAddress], + [[params]], + ); + } + }; + } + if (options?.adapters?.includes(adapters.Symbiotic)) { + await iVault.addAdapter(symbioticAdapter.address); + await symbioticAdapter.setInceptionVault(iVault.address); + } + if (options?.adapters?.includes(adapters.EigenLayer)) { + await iVault.addAdapter(eigenLayerAdapter.address); + await eigenLayerAdapter.setInceptionVault(iVault.address); + } + + await iVault.setWithdrawalQueue(withdrawalQueue.address); + await iToken.setVault(iVault.address); + + return { + iToken, + iVault, + ratioFeed, + asset, + iVaultOperator, + iLibrary, + withdrawalQueue, + mellowAdapter, + symbioticAdapter, + eigenLayerAdapter, + }; +} + +export const abi = ethers.AbiCoder.defaultAbiCoder(); +export let MAX_TARGET_PERCENT: BigInt; + diff --git a/projects/vaults/test/src/init-vault.ts b/projects/vaults/test/src/init-vault.ts new file mode 100644 index 00000000..f69adfba --- /dev/null +++ b/projects/vaults/test/src/init-vault.ts @@ -0,0 +1,212 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import hardhat from "hardhat"; +import { AssetData } from "../data/assets/stETH"; +import { vaults } from "../data/vaults"; +import { e18, impersonateWithEth } from "../helpers/utils"; +import { Adapter, adapters, emptyBytes } from "./constants"; +const { ethers, upgrades, network } = hardhat; + +let symbioticVaults = vaults.symbiotic; +let mellowVaults = vaults.mellow; + +export async function initVault( + assetData: AssetData, + options?: { adapters?: Adapter[]; eigenAdapterContractName?: string }, +) { + if (options?.adapters?.includes(adapters.EigenLayer) && !options.eigenAdapterContractName) { + throw new Error("EigenLayer adapter requires eigenAdapterContractName"); + } + + const block = await ethers.provider.getBlock("latest"); + if (!block) throw new Error("Failed to get latest block"); + + console.log(`Starting at block number: ${block.number}`); + console.log("Initialization of Inception ...."); + + const asset = await ethers.getContractAt("IERC20", assetData.assetAddress); + asset.address = await asset.getAddress(); + + if (options?.adapters?.includes(adapters.Mellow)) { + for (const mVaultInfo of mellowVaults) { + console.log(`- MellowVault ${mVaultInfo.name} and curator`); + mVaultInfo.vault = await ethers.getContractAt("IMellowVault", mVaultInfo.vaultAddress); + + const mellowVaultOperatorMock = await ethers.deployContract("OperatorMock", [mVaultInfo.bondStrategyAddress]); + mellowVaultOperatorMock.address = await mellowVaultOperatorMock.getAddress(); + await network.provider.send("hardhat_setCode", [ + mVaultInfo.curatorAddress, + await mellowVaultOperatorMock.getDeployedCode(), + ]); + + //Copy storage values + for (let i = 0; i < 5; i++) { + const slot = "0x" + i.toString(16); + const value = await network.provider.send("eth_getStorageAt", [ + mellowVaultOperatorMock.address, + slot, + "latest", + ]); + await network.provider.send("hardhat_setStorageAt", [mVaultInfo.curatorAddress, slot, value]); + } + + mVaultInfo.curator = await ethers.getContractAt("OperatorMock", mVaultInfo.curatorAddress); + } + } + + if (options?.adapters?.includes(adapters.Symbiotic)) { + for (const sVaultInfo of symbioticVaults) { + console.log(`- Symbiotic ${sVaultInfo.name}`); + sVaultInfo.vault = await ethers.getContractAt("IVault", sVaultInfo.vaultAddress); + } + } + + const iTokenFactory = await ethers.getContractFactory("InceptionToken"); + const iToken = await upgrades.deployProxy(iTokenFactory, ["TEST InceptionLRT Token", "tINt"]); + iToken.address = await iToken.getAddress(); + + const iVaultOperator = await impersonateWithEth(assetData.iVaultOperator, e18); + + let mellowAdapter: any, symbioticAdapter: any, eigenLayerAdapter: any; + + if (options?.adapters?.includes(adapters.Mellow)) { + const mellowAdapterFactory = await ethers.getContractFactory("InceptionWstETHMellowAdapter"); + mellowAdapter = await upgrades.deployProxy(mellowAdapterFactory, [ + [mellowVaults[0].vaultAddress], + assetData.assetAddress, + assetData.iVaultOperator, + ]); + + // deploy a claimer implementation + const MellowAdapterClaimerFactory = await ethers.getContractFactory("MellowAdapterClaimer"); + const claimerImplementation = await MellowAdapterClaimerFactory.deploy(); + await claimerImplementation.waitForDeployment(); + + await mellowAdapter.setClaimerImplementation(await claimerImplementation.getAddress()); + + mellowAdapter.address = await mellowAdapter.getAddress(); + } + + if (options?.adapters?.includes(adapters.Symbiotic)) { + const symbioticAdapterFactory = await ethers.getContractFactory("InceptionSymbioticAdapter"); + symbioticAdapter = await upgrades.deployProxy(symbioticAdapterFactory, [ + [symbioticVaults[0].vaultAddress], + assetData.assetAddress, + assetData.iVaultOperator, + ]); + + // deploy a claimer implementation + const SymbioticAdapterClaimerFactory = await ethers.getContractFactory("SymbioticAdapterClaimer"); + const claimerImplementation = await SymbioticAdapterClaimerFactory.deploy(); + await claimerImplementation.waitForDeployment(); + + await symbioticAdapter.setClaimerImplementation(await claimerImplementation.getAddress()); + + symbioticAdapter.address = await symbioticAdapter.getAddress(); + } + + // ratio + const iRatioFeedFactory = await ethers.getContractFactory("InceptionRatioFeed"); + const ratioFeed = await upgrades.deployProxy(iRatioFeedFactory, []); + await ratioFeed.updateRatioBatch([iToken.address], [e18]); //Set initial ratio e18 + ratioFeed.address = await ratioFeed.getAddress(); + + const iLibrary = await ethers.deployContract("InceptionLibrary"); + await iLibrary.waitForDeployment(); + + const iVaultFactory = await ethers.getContractFactory(assetData.vaultFactory, { + libraries: { InceptionLibrary: await iLibrary.getAddress() }, + }); + const iVault = await upgrades.deployProxy( + iVaultFactory, + [assetData.vaultName, assetData.iVaultOperator, assetData.assetAddress, iToken.address], + { + unsafeAllowLinkedLibraries: true, + }, + ); + iVault.address = await iVault.getAddress(); + + if (options?.adapters?.includes(adapters.EigenLayer) && options.eigenAdapterContractName) { + let [deployer] = await ethers.getSigners(); + const eigenLayerAdapterFactory = await ethers.getContractFactory(options.eigenAdapterContractName); + eigenLayerAdapter = await upgrades.deployProxy(eigenLayerAdapterFactory, [ + await deployer.getAddress(), + assetData.rewardsCoordinator, + assetData.delegationManager, + assetData.strategyManager, + assetData.assetStrategy, + assetData.assetAddress, + assetData.iVaultOperator, + iVault.address, + ]); + eigenLayerAdapter.address = await eigenLayerAdapter.getAddress(); + } + + const withdrawalQueueFactory = await ethers.getContractFactory("WithdrawalQueue"); + const withdrawalQueue = await upgrades.deployProxy(withdrawalQueueFactory, [iVault.address, [], [], 0]); + withdrawalQueue.address = await withdrawalQueue.getAddress(); + + await iVault.setRatioFeed(ratioFeed.address); + + if (options?.adapters?.includes(adapters.Mellow)) { + await iVault.addAdapter(mellowAdapter.address); + await mellowAdapter.setInceptionVault(iVault.address); + await mellowAdapter.setEthWrapper("0x7A69820e9e7410098f766262C326E211BFa5d1B1"); + + // await emergencyClaimer.approveSpender(assetData.assetAddress, mellowAdapter.address); + MAX_TARGET_PERCENT = await iVault.MAX_TARGET_PERCENT(); + console.log("... iVault initialization completed ...."); + + iVault.withdrawFromMellowAndClaim = async function (mellowVaultAddress, amount) { + const tx = await this.connect(iVaultOperator).emergencyUndelegate([ + [await mellowAdapter.getAddress(), mellowVaultAddress, amount, []] + ]); + + const receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + // NEW + const adapterEvents = receipt.logs + ?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + await helpers.time.increase(1209900); + const params = abi.encode(["address", "address"], [mellowVaultAddress, claimer]); + if (events[0].args["actualAmounts"] > 0) { + await this.connect(iVaultOperator).emergencyClaim( + [await mellowAdapter.getAddress()], + [mellowVaultAddress], + [[params]], + ); + } + }; + } + if (options?.adapters?.includes(adapters.Symbiotic)) { + await iVault.addAdapter(symbioticAdapter.address); + await symbioticAdapter.setInceptionVault(iVault.address); + } + if (options?.adapters?.includes(adapters.EigenLayer)) { + await iVault.addAdapter(eigenLayerAdapter.address); + await eigenLayerAdapter.setInceptionVault(iVault.address); + } + + await iVault.setWithdrawalQueue(withdrawalQueue.address); + await iToken.setVault(iVault.address); + + return { + iToken, + iVault, + ratioFeed, + asset, + iVaultOperator, + iLibrary, + withdrawalQueue, + mellowAdapter, + symbioticAdapter, + eigenLayerAdapter, + }; +} + +export const abi = ethers.AbiCoder.defaultAbiCoder(); +export let MAX_TARGET_PERCENT: BigInt; + diff --git a/projects/vaults/test/testrun.config.ts b/projects/vaults/test/testrun.config.ts new file mode 100644 index 00000000..8d65b533 --- /dev/null +++ b/projects/vaults/test/testrun.config.ts @@ -0,0 +1,17 @@ +import 'dotenv/config'; +import { assetData } from './data/assets/new'; + +const rpcURL = process.env.RPC; +if (!rpcURL) throw new Error("RPC variable is required. Please set it in your .env file"); + +const testrunConfig: { + network: string; + RPC: string; + assetData: typeof assetData; +} = { + network: process.env.NETWORK || 'mainnet', + RPC: rpcURL, + assetData: assetData, +} + +export {testrunConfig}; diff --git a/projects/vaults/test/tests-e2e/InceptionVault_S.test.ts b/projects/vaults/test/tests-e2e/InceptionVault_S.test.ts new file mode 100644 index 00000000..f4c5b230 --- /dev/null +++ b/projects/vaults/test/tests-e2e/InceptionVault_S.test.ts @@ -0,0 +1,1047 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { + calculateRatio, + e18, + setBlockTimestamp, + toWei, +} from "../helpers/utils"; +import { adapters, emptyBytes } from "../src/constants"; +import { abi, initVault, MAX_TARGET_PERCENT } from "../src/init-vault-new"; +import { testrunConfig } from '../testrun.config'; + +const { ethers, network } = hardhat; +const assetData = testrunConfig.assetData; +const symbioticVaults = assetData.adapters.symbiotic; +const mellowVaults = assetData.adapters.mellow; + +describe(`Inception Symbiotic Vault ${assetData.asset.name} e2e tests`, function () { + this.timeout(150000); + let iToken, iVault, ratioFeed, asset, mellowAdapter, symbioticAdapter, withdrawalQueue; + let iVaultOperator, deployer, staker, staker2, staker3, treasury; + let ratioErr, transactErr; + let snapshot; + let params; + + before(async function() { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.asset.name.toLowerCase())) { + console.log(`${assetData.asset.name} is not in the list, going to skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, ratioFeed, asset, iVaultOperator, mellowAdapter, symbioticAdapter, withdrawalQueue } + = await initVault(assetData, { adapters: [adapters.Mellow, adapters.Symbiotic] })); + + ratioErr = assetData.ratioErr; + transactErr = assetData.transactErr; + + [deployer, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + treasury = await iVault.treasury(); //deployer + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function() { + if (iVault) { + await iVault.removeAllListeners(); + } + }); + + describe("Symbiotic Native | Base flow no flash", function() { + let totalDeposited = 0n; + let delegatedSymbiotic = 0n; + let rewardsSymbiotic = 0n; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("Initial stats", async function() { + expect(await iVault.ratio()).to.be.eq(e18); + expect(await iVault.totalAssets()).to.be.eq(0n); + expect(await iVault.getTotalDeposited()).to.be.eq(0n); + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + expect(await iVault.getFlashCapacity()).to.be.eq(0n); + expect(await iVault.getFreeBalance()).to.be.eq(0n); + expect((await symbioticAdapter.getAllVaults())[0]).to.be.eq(symbioticVaults[0].vaultAddress); + expect(await symbioticAdapter.isVaultSupported(symbioticVaults[0].vaultAddress)).to.be.eq(true); + }); + + it("User can deposit to iVault", async function() { + totalDeposited += toWei(20); + const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit + const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Delegate to symbioticVault#1", async function() { + const amount = (await iVault.totalAssets()) / 3n; + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + const sVault = await ethers.getContractAt("IVault", symbioticVaults[0].vaultAddress); + const code = await ethers.provider.getCode(symbioticVaults[0].vaultAddress); + console.log("Deployed Code len:", code.length); + // await sVault.connect(staker).deposit(staker.address, amount); + console.log("totalStake: ", await sVault.totalStake()); + + await iVault + .connect(iVaultOperator) + .delegate(await symbioticAdapter.getAddress(), symbioticVaults[0].vaultAddress, amount, emptyBytes); + delegatedSymbiotic += amount; + + console.log("totalStake new: ", await sVault.totalStake()); + + const symbioticBalance = await symbioticVaults[0].vault.activeBalanceOf(symbioticAdapter.address); + const symbioticBalance2 = await symbioticVaults[1].vault.activeBalanceOf(symbioticAdapter.address); + const totalAssetsAfter = await iVault.totalAssets(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const delegatedTo = await symbioticAdapter.getDeposited(symbioticVaults[0].vaultAddress); + // const delegatedTo2 = await symbioticAdapter.getDeposited(symbioticVaults[1].vaultAddress); + const totalDepositedAfter = await iVault.getTotalDeposited(); + console.log("Mellow LP token balance: ", symbioticBalance.format()); + console.log("Mellow LP token balance2: ", symbioticBalance2.format()); + console.log("Amount delegated: ", delegatedSymbiotic.format()); + + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); + expect(totalDelegatedAfter).to.be.closeTo(delegatedSymbiotic, transactErr); + expect(delegatedTo).to.be.closeTo(amount, transactErr); + // expect(delegatedTo2).to.be.closeTo(0n, transactErr); + expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr); + expect(symbioticBalance).to.be.gte(amount / 2n); + expect(symbioticBalance2).to.be.eq(0n); + expect(await iVault.ratio()).to.be.closeTo(e18, ratioErr); + }); + + it("Add new symbioticVault", async function() { + await expect(symbioticAdapter.addVault(ethers.ZeroAddress)).to.be.revertedWithCustomError( + symbioticAdapter, + "ZeroAddress", + ); + await expect(symbioticAdapter.addVault(await iVaultOperator.getAddress())).to.be.revertedWithCustomError( + symbioticAdapter, + "NotContract", + ); + await expect(symbioticAdapter.addVault(symbioticVaults[1].vaultAddress)) + .to.emit(symbioticAdapter, "VaultAdded") + .withArgs(symbioticVaults[1].vaultAddress); + await expect(symbioticAdapter.addVault(symbioticVaults[1].vaultAddress)).to.be.revertedWithCustomError( + symbioticAdapter, + "AlreadyAdded", + ); + }); + + it("Delegate all to symbioticVault#2", async function() { + const amount = await iVault.getFreeBalance(); + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + await expect( + iVault + .connect(iVaultOperator) + .delegate(await symbioticAdapter.getAddress(), await iVaultOperator.getAddress(), amount, emptyBytes), + ).to.be.revertedWithCustomError(symbioticAdapter, "InvalidVault"); + + await iVault + .connect(iVaultOperator) + .delegate(await symbioticAdapter.getAddress(), symbioticVaults[1].vaultAddress, amount, emptyBytes); + delegatedSymbiotic += amount; + + const symbioticBalance = await symbioticVaults[0].vault.activeBalanceOf(symbioticAdapter.address); + const symbioticBalance2 = await symbioticVaults[1].vault.activeBalanceOf(symbioticAdapter.address); + const totalAssetsAfter = await iVault.totalAssets(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const delegatedTo2 = await symbioticAdapter.getDeposited(symbioticVaults[1].vaultAddress); + const totalDepositedAfter = await iVault.getTotalDeposited(); + console.log("Symbiotic LP token balance: ", symbioticBalance.format()); + console.log("Symbiotic LP token balance2: ", symbioticBalance2.format()); + console.log("Amount delegated: ", delegatedSymbiotic.format()); + + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); + expect(totalDelegatedAfter).to.be.closeTo(delegatedSymbiotic, transactErr * 2n); + expect(delegatedTo2).to.be.closeTo(amount, transactErr); + expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr * 2n); + expect(symbioticBalance2).to.be.gte(amount / 2n); + expect(await iVault.ratio()).to.be.closeTo(e18, ratioErr); + }); + + it("Update ratio", async function() { + const ratio = await iVault.ratio(); + console.log(`Calculated ratio:\t\t\t${ratio.format()}`); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + console.log(`iVault ratio:\t\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).eq(ratio); + }); + + it("Add rewards to Symbiotic protocol and estimate ratio, it remains the same", async function() { + const ratioBefore = await iVault.ratio(); + const totalDelegatedToBefore = await symbioticAdapter.getDeposited(symbioticVaults[0].vaultAddress); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); + console.log(`Delegated to before:\t${totalDelegatedToBefore.format()}`); + + console.log(`vault bal before: ${await asset.balanceOf(symbioticVaults[0].vaultAddress)}`); + await asset.connect(staker3).transfer(symbioticVaults[0].vaultAddress, e18); + console.log(`vault bal after: ${await asset.balanceOf(symbioticVaults[0].vaultAddress)}`); + + const ratioAfter = await iVault.ratio(); + const totalDelegatedToAfter = await symbioticAdapter.getDeposited(symbioticVaults[0].vaultAddress); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + expect(ratioAfter).to.be.eq(ratioBefore); + expect(totalDelegatedToAfter - totalDelegatedToBefore).to.be.eq(0n); + expect(totalDelegatedAfter - totalDelegatedBefore).to.be.eq(totalDelegatedToAfter - totalDelegatedToBefore); + }); + + it("User can withdraw all", async function() { + const shares = await iToken.balanceOf(staker.address); + const assetValue = await iVault.convertToAssets(shares); + console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); + console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); + const tx = await iVault.connect(staker).withdraw(shares, staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(assetValue); + expect(events[0].args["iShares"]).to.be.eq(shares); + + const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); + const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); + const epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(stakerPW).to.be.eq(0n); + expect(staker2PW).to.be.closeTo(assetValue, transactErr); + expect(epochShares).to.be.closeTo(shares, transactErr); + }); + + it("Update ratio after all shares burn", async function() { + const calculatedRatio = await iVault.ratio(); + console.log(`Calculated ratio:\t\t\t${calculatedRatio.format()}`); + expect(calculatedRatio).to.be.eq(e18); //Because all shares have been burnt at this point + + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + console.log(`iVault ratio after:\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).eq(calculatedRatio); + }); + + let symbioticVaultEpoch1; + let symbioticVaultEpoch2; + let undelegateClaimer1; + let undelegateClaimer2; + + it("Undelegate from Symbiotic", async function() { + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); + + const amount = await symbioticAdapter.getDeposited(symbioticVaults[0].vaultAddress); + const amount2 = await symbioticAdapter.getDeposited(symbioticVaults[1].vaultAddress); + const tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [ + [await symbioticAdapter.getAddress(), symbioticVaults[0].vaultAddress, amount, emptyBytes], + [await symbioticAdapter.getAddress(), symbioticVaults[1].vaultAddress, amount2, emptyBytes], + ]); + + const receipt = await tx.wait(); + const events = receipt.logs + ?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + + expect(events.length).to.be.eq(4); + undelegateClaimer1 = events[0].args["claimer"]; + undelegateClaimer2 = events[2].args["claimer"]; + + symbioticVaultEpoch1 = (await symbioticVaults[0].vault.currentEpoch()) + 1n; + symbioticVaultEpoch2 = (await symbioticVaults[1].vault.currentEpoch()) + 1n; + + const totalAssetsAfter = await iVault.totalAssets(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const totalDelegatedTo = await symbioticAdapter.getDeposited(symbioticVaults[0].vaultAddress); + const totalDelegatedTo2 = await symbioticAdapter.getDeposited(symbioticVaults[1].vaultAddress); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const pendingWithdrawalsSymbioticAfter = await symbioticAdapter.pendingWithdrawalAmount(); + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending from Symbiotic:\t\t${pendingWithdrawalsSymbioticAfter.format()}`); + + expect(totalAssetsAfter).to.be.eq(totalAssetsBefore); //Nothing has come to the iVault yet + expect(totalDelegatedAfter).to.be.closeTo(0n, transactErr); + expect(totalDelegatedTo).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow + expect(totalDelegatedTo2).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow + expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr * 2n); //Total deposited amount did not change + expect(pendingWithdrawalsSymbioticAfter).to.be.closeTo(amount + amount2, transactErr * 2n); + }); + + it("Process request to transfers pending funds to symbioticAdapter", async function() { + console.log(`current epoch of 1: ${await symbioticVaults[0].vault.currentEpoch()}`); + console.log(`current epoch of 2: ${await symbioticVaults[1].vault.currentEpoch()}`); + + const epochDuration1 = await symbioticVaults[0].vault.epochDuration(); + const epochDuration2 = await symbioticVaults[1].vault.epochDuration(); + + const nextEpochStart1 = await symbioticVaults[0].vault.nextEpochStart(); + const nextEpochStart2 = await symbioticVaults[1].vault.nextEpochStart(); + + const maxNextEpochStart = nextEpochStart1 > nextEpochStart2 ? nextEpochStart1 : nextEpochStart2; + const maxEpochDuration = epochDuration1 > epochDuration2 ? epochDuration1 : epochDuration2; + + console.log(`maxNextEpochStart: ${maxNextEpochStart}`); + + await setBlockTimestamp(Number(maxNextEpochStart + maxEpochDuration + 1n)); + + console.log(`current epoch of 1: ${await symbioticVaults[0].vault.currentEpoch()}`); + }); + + it("Cannot remove symbioticVault", async function() { + await expect(symbioticAdapter.removeVault(ethers.ZeroAddress)) + .to.be.revertedWithCustomError(symbioticAdapter, "ZeroAddress"); + await expect(symbioticAdapter.removeVault(await iVaultOperator.getAddress())) + .to.be.revertedWithCustomError(symbioticAdapter, "NotContract"); + await expect(symbioticAdapter.removeVault(symbioticVaults[1].vaultAddress)) + .to.be.revertedWithCustomError(symbioticAdapter, "VaultNotEmpty"); + }); + + it("Claim Symbiotic withdrawal transfer funds from Symbiotic to the vault", async function() { + const pendingWithdrawalsSymbiotic = await symbioticAdapter.pendingWithdrawalAmount(); + const totalAssetsBefore = await iVault.totalAssets(); + const adapterBalanceBefore = await asset.balanceOf(symbioticAdapter.address); + + // Vault 1 + params = abi.encode( + ["address", "address"], + [await iVaultOperator.getAddress(), undelegateClaimer1], + ); + + await expect(iVault.connect(iVaultOperator).claim( + 1, [await symbioticAdapter.getAddress()], [await iVaultOperator.getAddress()], [[params]]), + ).to.be.revertedWithCustomError(symbioticAdapter, "InvalidVault"); + + params = abi.encode( + ["address", "address"], + [symbioticVaults[0].vaultAddress, undelegateClaimer1], + ); + + // Vault 2 + let params2 = abi.encode( + ["address", "address"], + [symbioticVaults[1].vaultAddress, undelegateClaimer2], + ); + + await iVault.connect(iVaultOperator).claim(1, + [await symbioticAdapter.getAddress(), await symbioticAdapter.getAddress()], + [symbioticVaults[0].vaultAddress, symbioticVaults[1].vaultAddress], + [[params], [params2]], + ); + + await expect(iVault.connect(iVaultOperator).claim( + 1, [await symbioticAdapter.getAddress()], [symbioticVaults[0].vaultAddress], [[params]]), + ).to.be.revertedWithCustomError(symbioticAdapter, "NothingToClaim"); + + const totalAssetsAfter = await iVault.totalAssets(); + const adapterBalanceAfter = await asset.balanceOf(mellowAdapter.address); + + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(pendingWithdrawalsSymbiotic, transactErr); + expect(adapterBalanceBefore).to.be.closeTo(adapterBalanceAfter, transactErr); + }); + + it("Remove symbioticVault", async function() { + await expect(symbioticAdapter.removeVault(symbioticVaults[1].vaultAddress)) + .to.emit(symbioticAdapter, "VaultRemoved") + .withArgs(symbioticVaults[1].vaultAddress); + + await expect(symbioticAdapter.removeVault(symbioticVaults[1].vaultAddress)) + .to.be.revertedWithCustomError(symbioticAdapter, "NotAdded"); + }); + + it("Staker is able to redeem", async function() { + const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); + const redeemReserve = await iVault.redeemReservedAmount(); + const freeBalance = await iVault.getFreeBalance(); + + console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); + console.log("Redeem reserve", redeemReserve.format()); + console.log("Free balance", freeBalance.format()); + console.log("Redeem reserve after", await iVault.redeemReservedAmount()); + + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; + }); + + it("Redeem withdraw", async function() { + const balanceBefore = await asset.balanceOf(staker2.address); + const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); + + const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); + + const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); + const balanceAfter = await asset.balanceOf(staker2.address); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); + console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); + + expect(staker2PWAfter).to.be.eq(0n); + expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); + expect(totalDepositedAfter).to.be.closeTo(0n, transactErr); + expect(totalAssetsAfter).to.be.closeTo(0n, transactErr); + }); + }); + + describe("Base flow no flash", function() { + let totalDeposited = 0n; + let delegatedMellow = 0n; + let rewardsMellow = 0n; + let undelegatedEpoch; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("Initial stats", async function() { + expect(await iVault.ratio()).to.be.eq(e18); + expect(await iVault.totalAssets()).to.be.eq(0n); + expect(await iVault.getTotalDeposited()).to.be.eq(0n); + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + expect(await iVault.getFlashCapacity()).to.be.eq(0n); + expect(await iVault.getFreeBalance()).to.be.eq(0n); + }); + + it("User can deposit to iVault", async function() { + totalDeposited += toWei(20); + const expectedShares = totalDeposited; //Because ratio is 1e18 at the first deposit + const tx = await iVault.connect(staker).deposit(totalDeposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(totalDeposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.closeTo(e18, 1n); + }); + + it("Delegate to mellowVault#1", async function() { + const amount = (await iVault.getFreeBalance()) / 3n; + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes); + delegatedMellow += amount; + + const mellowBalance = await mellowVaults[0].vault.balanceOf(mellowAdapter.address); + const mellowBalance2 = await mellowVaults[1].vault.balanceOf(mellowAdapter.address); + const totalAssetsAfter = await iVault.totalAssets(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const delegatedTo = await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress); + const delegatedTo2 = await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress); + const totalDepositedAfter = await iVault.getTotalDeposited(); + console.log("Mellow LP token balance: ", mellowBalance.format()); + console.log("Mellow LP token balance2: ", mellowBalance2.format()); + console.log("Amount delegated: ", delegatedMellow.format()); + + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); + expect(totalDelegatedAfter).to.be.closeTo(delegatedMellow, transactErr); + expect(delegatedTo).to.be.closeTo(amount, transactErr); + expect(delegatedTo2).to.be.closeTo(0n, transactErr); + expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr); + expect(mellowBalance).to.be.gte(amount / 2n); + expect(mellowBalance2).to.be.eq(0n); + expect(await iVault.ratio()).to.be.closeTo(e18, ratioErr); + }); + + it("Add new mellowVault", async function() { + await expect(mellowAdapter.addMellowVault(mellowVaults[1].vaultAddress)) + .to.emit(mellowAdapter, "VaultAdded") + .withArgs(mellowVaults[1].vaultAddress); + }); + + it("Delegate all to mellowVault#2", async function() { + const amount = await iVault.getFreeBalance(); + expect(amount).to.be.gt(0n); + const totalAssetsBefore = await iVault.totalAssets(); + + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress, amount, emptyBytes); + delegatedMellow += amount; + + const mellowBalance = await mellowVaults[0].vault.balanceOf(mellowAdapter.address); + const mellowBalance2 = await mellowVaults[1].vault.balanceOf(mellowAdapter.address); + const totalAssetsAfter = await iVault.totalAssets(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const delegatedTo2 = await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress); + const totalDepositedAfter = await iVault.getTotalDeposited(); + console.log("Mellow LP token balance: ", mellowBalance.format()); + console.log("Mellow LP token balance2: ", mellowBalance2.format()); + console.log("Amount delegated: ", delegatedMellow.format()); + + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount, transactErr); + expect(totalDelegatedAfter).to.be.closeTo(delegatedMellow, transactErr * 2n); + expect(delegatedTo2).to.be.closeTo(amount, transactErr); + expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr * 2n); + expect(mellowBalance2).to.be.gte(amount / 2n); + expect(await iVault.ratio()).to.be.closeTo(e18, ratioErr); + }); + + it("Update ratio", async function() { + const ratio = await iVault.ratio(); + console.log(`Calculated ratio:\t\t\t${ratio.format()}`); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + console.log(`iVault ratio:\t\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).eq(ratio); + }); + + it("Add rewards to Mellow protocol and estimate ratio", async function() { + const ratioBefore = await iVault.ratio(); + const totalDelegatedToBefore = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); + console.log(`Delegated to before:\t${totalDelegatedToBefore.format()}`); + + await asset.connect(staker3).transfer(mellowVaults[0].vaultAddress, e18); + + const ratioAfter = await iVault.ratio(); + const totalDelegatedToAfter = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + rewardsMellow += totalDelegatedToAfter - totalDelegatedToBefore; + + console.log(`Ratio after:\t\t\t${ratioAfter.format()}`); + console.log(`Delegated to after:\t\t${totalDelegatedToAfter.format()}`); + console.log(`mellow rewards:\t\t\t${rewardsMellow.format()}`); + await ratioFeed.updateRatioBatch([iToken.address], [ratioAfter]); + expect(totalDelegatedAfter - totalDelegatedBefore).to.be.eq(totalDelegatedToAfter - totalDelegatedToBefore); + }); + + it("Estimate the amount that user can withdraw", async function() { + const shares = await iToken.balanceOf(staker.address); + const assetValue = await iVault.convertToAssets(shares); + expect(assetValue).closeTo(totalDeposited + rewardsMellow, transactErr * 10n); + }); + + it("User can withdraw all", async function() { + const shares = await iToken.balanceOf(staker.address); + const assetValue = await iVault.convertToAssets(shares); + console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); + console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); + const tx = await iVault.connect(staker).withdraw(shares, staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(assetValue); + expect(events[0].args["iShares"]).to.be.eq(shares); + + const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); + const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); + const epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(stakerPW).to.be.eq(0n); + expect(staker2PW).to.be.closeTo(assetValue, transactErr); + expect(epochShares).to.be.closeTo(shares, transactErr); + }); + + // it("Update ratio after all shares burn", async function () { + // const calculatedRatio = await iVault.ratio(); + // console.log(`Calculated ratio:\t\t\t${calculatedRatio.format()}`); + // expect(calculatedRatio).to.be.eq(e18); //Because all shares have been burnt at this point + // + // await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + // console.log(`iVault ratio after:\t\t\t${(await iVault.ratio()).format()}`); + // expect(await iVault.ratio()).eq(calculatedRatio); + // }); + + let undelegateClaimer1; + let undelegateClaimer2; + + it("Undelegate from Mellow", async function() { + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + + undelegatedEpoch = await withdrawalQueue.currentEpoch(); + const totalSupply = await withdrawalQueue.getRequestedShares(undelegatedEpoch); + + console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated before:\t\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets before:\t\t\t${totalAssetsBefore.format()}`); + + console.log( + "Mellow1 delegated", + await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress), + ); + console.log( + "Mellow2 delegated", + await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress), + ); + + const assets1 = await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress); + const assets2 = await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress); + + const tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [ + [await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, assets1, emptyBytes], + [await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress, assets2, emptyBytes], + ]); + + const receipt = await tx.wait(); + const events = receipt.logs + ?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + + expect(events.length).to.be.eq(4); + undelegateClaimer1 = events[0].args["claimer"]; + undelegateClaimer2 = events[2].args["claimer"]; + + console.log( + "Mellow1 delegated", + await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress), + ); + console.log( + "Mellow2 delegated", + await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress), + ); + + const totalAssetsAfter = await iVault.totalAssets(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const totalDelegatedTo = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalDelegatedTo2 = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[1].vaultAddress, + ); + const totalDepositedAfter = await iVault.getTotalDeposited(); + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + // console.log(`Pending from Mellow:\t\t${pendingWithdrawalsMellowAfter.format()}`); + + expect(totalDelegatedAfter).to.be.closeTo(0n, transactErr); + expect(totalDelegatedTo).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow + expect(totalDelegatedTo2).to.be.closeTo(0n, transactErr); //Everything was requested for withdrawal from Mellow + expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr * 2n); //Total deposited amount did not change + // expect(pendingWithdrawalsMellowAfter).to.be.closeTo(amount + amount2, transactErr * 2n); + }); + + it("Claim Mellow withdrawal transfer funds from adapter to vault", async function() { + await helpers.time.increase(1209900); + + const pendingWithdrawalsMellowBefore = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + const totalAssetsBefore = await iVault.totalAssets(); + const withdrawalEpochBefore = await withdrawalQueue.withdrawals(undelegatedEpoch); + + const params1 = abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, undelegateClaimer1]); + const params2 = abi.encode(["address", "address"], [mellowVaults[1].vaultAddress, undelegateClaimer2]); + + await iVault + .connect(iVaultOperator) + .claim( + undelegatedEpoch, + [await mellowAdapter.getAddress(), await mellowAdapter.getAddress()], + [mellowVaults[0].vaultAddress, mellowVaults[1].vaultAddress], + [[params1], [params2]], + ); + + const withdrawalEpochAfter = await withdrawalQueue.withdrawals(1); + const totalAssetsAfter = await iVault.totalAssets(); + + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); + expect(withdrawalEpochAfter[2] - withdrawalEpochBefore[2]).to.be.closeTo( + pendingWithdrawalsMellowBefore, + transactErr, + ); + }); + + it("getTotalDeposited includes redeemable amount", async function() { + const totalDeposited = await iVault.getTotalDeposited(); + const totalDelegated = await iVault.getTotalDelegated(); + const totalAssets = await iVault.totalAssets(); + const totalPendingWithdrawals = await iVault.getTotalPendingWithdrawals(); + const totalPendingEmergencyWithdrawals = await iVault.getTotalPendingEmergencyWithdrawals(); + const redeemable = await iVault.redeemReservedAmount(); + + expect(totalDeposited).to.be.eq( + totalDelegated + totalAssets + totalPendingWithdrawals + totalPendingEmergencyWithdrawals - redeemable, + ); + }); + + it("Staker is able to redeem", async function() { + const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); + const redeemReserve = await iVault.redeemReservedAmount(); + const freeBalance = await iVault.getFreeBalance(); + + console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); + console.log("Redeem reserve", redeemReserve.format()); + console.log("Free balance", freeBalance.format()); + + console.log("Redeem reserve after", await iVault.redeemReservedAmount()); + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; + }); + + it("Redeem withdraw", async function() { + const balanceBefore = await asset.balanceOf(staker2.address); + const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); + + const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); + + const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); + const balanceAfter = await asset.balanceOf(staker2.address); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); + console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); + + expect(staker2PWAfter).to.be.eq(0n); + expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr + 13n); + expect(totalDepositedAfter).to.be.closeTo(0n, transactErr + 13n); + expect(totalAssetsAfter).to.be.closeTo(0n, transactErr + 13n); + }); + }); + + describe("Base flow with flash withdraw", function() { + let targetCapacity, deposited, freeBalance, depositFees; + before(async function() { + await snapshot.restore(); + targetCapacity = e18; + await iVault.setTargetFlashCapacity(targetCapacity); //1% + }); + + it("Initial ratio is 1e18", async function() { + const ratio = await iVault.ratio(); + console.log(`Current ratio is:\t\t\t\t${ratio.format()}`); + expect(ratio).to.be.eq(e18); + }); + + it("Initial delegation is 0", async function() { + expect(await iVault.getTotalDelegated()).to.be.eq(0n); + }); + + it("Deposit to Vault", async function() { + // made by user + deposited = toWei(10); + freeBalance = (deposited * (MAX_TARGET_PERCENT - targetCapacity)) / MAX_TARGET_PERCENT; + const expectedShares = (deposited * e18) / (await iVault.ratio()); + const tx = await iVault.connect(staker).deposit(deposited, staker.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(deposited, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(expectedShares, transactErr); + expect(receipt.logs.find(l => l.eventName === "DepositBonus")).to.be.undefined; + console.log(`Ratio after: ${await iVault.ratio()}`); + + expect(await iToken.balanceOf(staker.address)).to.be.closeTo(expectedShares, transactErr); + expect(await iVault.totalAssets()).to.be.closeTo(deposited, transactErr); + expect(await iVault.getFlashCapacity()).to.be.closeTo(deposited, transactErr); + expect(await iVault.getFreeBalance()).to.be.closeTo(freeBalance, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(deposited, transactErr); + expect(await iVault.getTotalDelegated()).to.be.eq(0); //Nothing has been delegated yet + expect(await iVault.ratio()).to.be.eq(e18); + }); + + it("Delegate freeBalance", async function() { + // made by operator + const totalDepositedBefore = await iVault.getTotalDeposited(); + const expectedFlashCapacity = (deposited * targetCapacity) / MAX_TARGET_PERCENT; + + const amount = await iVault.getFreeBalance(); + + await expect( + iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes), + ) + .to.emit(iVault, "DelegatedTo") + .withArgs(mellowAdapter.address, mellowVaults[0].vaultAddress, amount); + + const delegatedTotal = await iVault.getTotalDelegated(); + const delegatedTo = await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress); + expect(totalDepositedBefore).to.be.closeTo(await iVault.getTotalDeposited(), transactErr); + expect(delegatedTotal).to.be.closeTo(amount, transactErr); + expect(delegatedTo).to.be.closeTo(amount, transactErr); + expect(await iVault.getFreeBalance()).to.be.closeTo(0n, transactErr); + expect(await iVault.getFlashCapacity()).to.be.closeTo(expectedFlashCapacity, transactErr); + expect(await iVault.ratio()).closeTo(e18, ratioErr); + }); + + it("Update asset ratio", async function() { + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + console.log(`New ratio is:\t\t\t\t\t${(await iVault.ratio()).format()}`); + expect(await iVault.ratio()).lt(e18); + }); + + it("Flash withdraw all capacity", async function() { + // made by user (flash capacity tests ends on this step) + const sharesBefore = await iToken.balanceOf(staker); + const assetBalanceBefore = await asset.balanceOf(staker); + const treasuryBalanceBefore = await asset.balanceOf(treasury); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + const flashCapacityBefore = await iVault.getFlashCapacity(); + const freeBalanceBefore = await iVault.getFreeBalance(); + console.log(`Flash capacity before:\t${flashCapacityBefore.format()}`); + console.log(`Free balance before:\t${freeBalanceBefore.format()}`); + + const amount = await iVault.getFlashCapacity(); + const shares = await iVault.convertToShares(amount); + const receiver = staker; + const expectedFee = await iVault.calculateFlashWithdrawFee(await iVault.convertToAssets(shares)); + console.log(`Amount:\t\t\t\t\t${amount.format()}`); + console.log(`Shares:\t\t\t\t\t${shares.format()}`); + console.log(`Expected fee:\t\t\t${expectedFee.format()}`); + + let tx = await iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](shares, receiver.address, 0n); + const receipt = await tx.wait(); + const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); + expect(withdrawEvent.length).to.be.eq(1); + expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); + expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); + expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); + expect(withdrawEvent[0].args["fee"]).to.be.closeTo(expectedFee, transactErr); + const collectedFees = withdrawEvent[0].args["fee"]; + depositFees = collectedFees / 2n; + + const sharesAfter = await iToken.balanceOf(staker); + const assetBalanceAfter = await asset.balanceOf(staker); + const treasuryBalanceAfter = await asset.balanceOf(treasury); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const flashCapacityAfter = await iVault.getFlashCapacity(); + const depositBonus = await iVault.depositBonusAmount(); + console.log(`Shares balance diff:\t${(sharesBefore - sharesAfter).format()}`); + console.log(`Total deposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); + console.log(`Total assets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); + console.log(`Flash capacity diff:\t${(flashCapacityBefore - flashCapacityAfter).format()}`); + console.log(`Deposit bonus:\t\t\t${depositBonus.format()}`); + console.log(`Fee collected:\t\t\t${collectedFees.format()}`); + + expect(sharesBefore - sharesAfter).to.be.eq(shares); + expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); + expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); + expect(flashCapacityAfter).to.be.closeTo(0n, transactErr); + }); + + // made by user (withdrawal of funds if something left after flash withdraw) + it("Withdraw all", async function() { + const ratioBefore = await iVault.ratio(); + const shares = await iToken.balanceOf(staker.address); + const assetValue = await iVault.convertToAssets(shares); + console.log(`Shares:\t\t\t\t\t\t\t${shares.format()}`); + console.log(`Asset value:\t\t\t\t\t${assetValue.format()}`); + + const tx = await iVault.connect(staker).withdraw(shares, staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.eq(assetValue); + expect(events[0].args["iShares"]).to.be.eq(shares); + + const stakerPW = await iVault.getPendingWithdrawalOf(staker.address); + const staker2PW = await iVault.getPendingWithdrawalOf(staker2.address); + const epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(stakerPW).to.be.eq(0n); + expect(staker2PW).to.be.closeTo(assetValue, transactErr); + expect(epochShares).to.be.closeTo(shares, transactErr); + + console.log(`Total delegated:\t\t\t\t${(await iVault.getTotalDelegated()).format()}`); + console.log(`Total deposited:\t\t\t\t${(await iVault.getTotalDeposited()).format()}`); + expect(await iVault.ratio()).to.be.eq(ratioBefore); + }); + + let undelegateClaimer; + + it("Undelegate from Mellow", async function() { + // made by operator + const totalAssetsBefore = await iVault.totalAssets(); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalDelegatedBefore = await iVault.getTotalDelegated(); + console.log(`Total deposited before:\t\t${totalDepositedBefore.format()}`); + console.log(`Total delegated before:\t\t${totalDelegatedBefore.format()}`); + console.log(`Total assets before:\t\t${totalAssetsBefore.format()}`); + console.log("======================================================"); + + const amount = await iVault.getTotalDelegated(); + + const tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes]]); + + const receipt = await tx.wait(); + const events = receipt.logs + ?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + + expect(events.length).to.be.eq(2); + undelegateClaimer = events[0].args["claimer"]; + + const totalAssetsAfter = await iVault.totalAssets(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const totalDelegatedTo = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const pendingWithdrawalsMellowAfter = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total delegated after:\t\t${totalDelegatedAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending from Mellow:\t\t${pendingWithdrawalsMellowAfter.format()}`); + + // expect(totalAssetsAfter).to.be.eq(totalAssetsBefore); //Nothing has come to the iVault yet + expect(totalDelegatedAfter).to.be.closeTo(0, transactErr); + expect(totalDelegatedTo).to.be.closeTo(0, transactErr); //Everything was requested for withdrawal from Mellow + expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr * 2n); //Total deposited amount did not change + }); + + // made by operator + it("Claim Mellow withdrawal transfer funds from adapter to vault", async function() { + await helpers.time.increase(1209900); + + const pendingWithdrawalsMellowBefore = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + const totalAssetsBefore = await iVault.totalAssets(); + // const adapterBalanceBefore = await asset.balanceOf(mellowAdapter.address); + const withdrawalEpochBefore = await withdrawalQueue.withdrawals(1); + + const params = abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, undelegateClaimer]); + await iVault + .connect(iVaultOperator) + .claim(1, [await mellowAdapter.getAddress()], [mellowVaults[0].vaultAddress], [[params]]); + + const withdrawalEpochAfter = await withdrawalQueue.withdrawals(1); + const totalAssetsAfter = await iVault.totalAssets(); + // const adapterBalanceAfter = await asset.balanceOf(mellowAdapter.address); + + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); + expect(withdrawalEpochAfter[2] - withdrawalEpochBefore[2]).to.be.closeTo( + pendingWithdrawalsMellowBefore, + transactErr, + ); + // expect(adapterBalanceBefore - adapterBalanceAfter).to.be.closeTo(pendingWithdrawalsMellowBefore, transactErr); + }); + + // made by user + it("Staker is able to redeem", async function() { + const pendingWithdrawalByStaker = await iVault.getPendingWithdrawalOf(staker2.address); + const redeemReserve = await iVault.redeemReservedAmount(); + const freeBalance = await iVault.getFreeBalance(); + + console.log("Pending withdrawal by staker", pendingWithdrawalByStaker.format()); + console.log("Redeem reserve", redeemReserve.format()); + console.log("Free balance", freeBalance.format()); + + console.log("Redeem reserve after", await iVault.redeemReservedAmount()); + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; + }); + + // made by operator + it("Redeem withdraw", async function() { + const balanceBefore = await asset.balanceOf(staker2.address); + const staker2PWBefore = await iVault.getPendingWithdrawalOf(staker2.address); + + const tx = await iVault.connect(iVaultOperator).redeem(staker2.address); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Redeem"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(iVaultOperator.address); + expect(events[0].args["receiver"]).to.be.eq(staker2.address); + expect(events[0].args["amount"]).to.be.eq(staker2PWBefore); + + const staker2PWAfter = await iVault.getPendingWithdrawalOf(staker2.address); + const balanceAfter = await asset.balanceOf(staker2.address); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + console.log(`Total assets after:\t\t\t${totalAssetsAfter.format()}`); + console.log(`Total deposited after:\t\t${totalDepositedAfter.format()}`); + console.log(`Pending withdrawals after:\t${staker2PWAfter.format()}`); + console.log(`Ratio after:\t\t\t\t${(await iVault.ratio()).format()}`); + + expect(staker2PWAfter).to.be.eq(0n); + expect(balanceAfter - balanceBefore).to.be.closeTo(staker2PWBefore, transactErr); + expect(totalDepositedAfter).to.be.closeTo(0n, transactErr * 3n); + expect(totalAssetsAfter).to.be.closeTo(depositFees, transactErr * 3n); + }); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S.test.ts new file mode 100644 index 00000000..af8bef45 --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S.test.ts @@ -0,0 +1,484 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { e18, skipEpoch, symbioticClaimParams, toWei } from "../helpers/utils"; +import { initVault } from "../src/init-vault-new"; +const { ethers, network } = hardhat; +import { testrunConfig } from '../testrun.config'; +import { adapters, emptyBytes } from "../src/constants"; + +const assetData = testrunConfig.assetData; +const symbioticVaults = assetData.adapters.symbiotic; + +describe(`Inception Symbiotic Vault ${assetData.asset.name}`, function () { + let iVault; + let asset; + let staker: HardhatEthersSigner, staker2: HardhatEthersSigner; + let transactErr: bigint; + let snapshot: helpers.SnapshotRestorer; + let ratioFeed; + let iToken; + let iVaultOperator; + let symbioticAdapter; + let withdrawalQueue; + + before(async function () { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.asset.name.toLowerCase())) { + console.log(`Asset "${assetData.asset.name}" is not in test data, skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [{ + forking: { + jsonRpcUrl: network.config.forking.url, + blockNumber: assetData.blockNumber || network.config.forking.blockNumber, + }, + }]); + + ({ iToken, iVault, iVaultOperator, asset, ratioFeed, symbioticAdapter, withdrawalQueue } = + await initVault(assetData, { adapters: [adapters.Symbiotic] })); + transactErr = assetData.transactErr; + + [, staker, staker2] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + + snapshot = await helpers.takeSnapshot(); + }); + + describe("Flash withdrawal: setFlashMinAmount method", () => { + const flashMinAmount = toWei(1); + const depositedAmount = toWei(2); + + beforeEach(async () => { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(e18); //1% + + // deposit to vault + const tx = await iVault.connect(staker).deposit(depositedAmount, staker.address); + await tx.wait(); + + // set flash min amount + await iVault.setFlashMinAmount(flashMinAmount); + }); + + it("Flash min amount could be set", async () => { + expect(await iVault.flashMinAmount()).to.be.eq(flashMinAmount); + }); + + it("Event FlashMinAmountChanged is emitted", async () => { + // act + const newFlashMinAmount = 2n; + const tx = await iVault.setFlashMinAmount(newFlashMinAmount); + const receipt = await tx.wait(); + + // assert + const event = receipt.logs?.find(e => e.eventName === "FlashMinAmountChanged"); + expect(event).to.exist; + expect(event.args).to.have.lengthOf(2); + expect(event?.args[0]).to.be.eq(flashMinAmount); + expect(event?.args[1]).to.be.eq(newFlashMinAmount); + }); + + it("Error when set flash min amount to 0", async () => { + await expect(iVault.setFlashMinAmount(0)).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("Flash min amount could be set only by owner", async () => { + await expect(iVault.connect(staker2).setFlashMinAmount(flashMinAmount)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("Successfully withdraw MORE than min flash amount", async () => { + // arrange + const assetBalanceBefore = await asset.balanceOf(staker); + const withdrawalAmount = flashMinAmount + 1n; + + // act + const tx = await iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](withdrawalAmount, staker.address, 0n); + const receipt = await tx.wait(); + const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); + + // assert + const collectedFees = withdrawEvent[0].args["fee"]; + const assetBalanceAfter = await asset.balanceOf(staker); + expect(assetBalanceAfter).to.be.closeTo(assetBalanceBefore + withdrawalAmount - collectedFees, transactErr); + }); + + it("Successfully withdraw the amount EQUAL to min flash amount", async () => { + // arrange + const assetBalanceBefore = await asset.balanceOf(staker); + const withdrawalAmount = flashMinAmount; + + // act + const tx = await iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](withdrawalAmount, staker.address, 0n); + const receipt = await tx.wait(); + const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); + + // assert + const collectedFees = withdrawEvent[0].args["fee"]; + const assetBalanceAfter = await asset.balanceOf(staker); + expect(assetBalanceAfter).to.be.closeTo(assetBalanceBefore + withdrawalAmount - collectedFees, transactErr); + }); + + it("Error when withdraw the amount LESS to min flash amount", async () => { + // arrange + const assetBalanceBefore = await asset.balanceOf(staker); + const withdrawalAmount = flashMinAmount - 1n; + // act + const withdrawalTx = iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](withdrawalAmount, staker.address, 0n); + await expect(withdrawalTx).to.be.revertedWithCustomError(iVault, "LowerMinAmount"); + + // assert + const assetBalanceAfter = await asset.balanceOf(staker); + expect(assetBalanceAfter).to.be.closeTo(assetBalanceBefore, transactErr); + }); + }); + + describe('setDepositMinAmount method', () => { + const depositMinAmount = toWei(1); + + before(async () => { + await iVault.setTargetFlashCapacity(e18); //1% + // const helpers = await import('@nomicfoundation/hardhat-network-helpers'); + snapshot = await helpers.takeSnapshot(); + }); + + beforeEach(async () => { + await snapshot.restore(); + await iVault.setDepositMinAmount(depositMinAmount); + }); + + it('Deposit min amount could be set', async () => { + expect(await iVault.depositMinAmount()).to.be.eq(depositMinAmount); + }); + + it('Event DepositMinAmountChanged is emitted', async () => { + // act + const newDepositMinAmount = 2n; + const tx = await iVault.setDepositMinAmount(newDepositMinAmount); + const receipt = await tx.wait(); + + // assert + const event = receipt.logs?.find(e => e.eventName === 'DepositMinAmountChanged'); + expect(event).to.exist; + expect(event.args).to.have.lengthOf(2); + expect(event?.args[0]).to.be.eq(depositMinAmount); + expect(event?.args[1]).to.be.eq(newDepositMinAmount); + }); + + it('Error when set deposit min amount to 0', async () => { + await expect(iVault.setDepositMinAmount(0)).to.be.revertedWithCustomError(iVault, 'NullParams'); + }); + + it('Deposit min amount could be set only by owner', async () => { + await expect(iVault.connect(staker2).setDepositMinAmount(depositMinAmount)).to.be.revertedWith('Ownable: caller is not the owner'); + }); + + it('Successfully deposit MORE than min deposit amount', async () => { + // arrange + const assetBalanceBefore = await asset.balanceOf(iVault.address); + const depositAmount = depositMinAmount + 1n; + + // act + const tx = await iVault.connect(staker).deposit(depositAmount, staker.address); + await tx.wait(); + + // assert + const assetBalanceAfter = await asset.balanceOf(iVault.address); + expect(assetBalanceAfter).to.be.eq(assetBalanceBefore + depositAmount); + }); + + it('Successfully deposit the amount EQUAL to min deposit amount', async () => { + // arrange + const assetBalanceBefore = await asset.balanceOf(iVault.address); + const depositAmount = depositMinAmount; + + // act + const tx = await iVault.connect(staker).deposit(depositAmount, staker.address); + await tx.wait(); + + // assert + const assetBalanceAfter = await asset.balanceOf(iVault.address); + expect(assetBalanceAfter).to.be.eq(assetBalanceBefore + depositAmount); + }); + + it('Error when deposit the amount LESS to min deposit amount', async () => { + // arrange + const assetBalanceBefore = await asset.balanceOf(iVault.address); + const depositAmount = depositMinAmount - 1n; + + // act + const depositTx = iVault.connect(staker).deposit(depositAmount, staker.address); + await expect(depositTx).to.be.revertedWithCustomError(iVault, 'LowerMinAmount'); + + // assert + const assetBalanceAfter = await asset.balanceOf(iVault.address); + expect(assetBalanceAfter).to.be.eq(assetBalanceBefore); + }); + }); + + describe('decimals method', () => { + it('should return token decimals', async () => { + const tokenAddress = await iVault.inceptionToken(); + + const iVaultDecimals = await iVault.decimals(); + const tokenDecimals = await (await ethers.getContractAt('IERC20Metadata', tokenAddress)).decimals(); + + expect(iVaultDecimals).to.be.eq(tokenDecimals); + expect(iVaultDecimals).to.be.eq(18n); + }); + }); + + describe('migrateDepositBonus method', () => { + beforeEach(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(e18); + }); + + it('should migrate deposit bonus to a new vault', async () => { + // Arrange + // set ratio to 1:1 + await ratioFeed.updateRatioBatch([iToken.address], [toWei(1)]); + + // deposit + let depositTx = await iVault.connect(staker).deposit(toWei(10), staker.address); + await depositTx.wait(); + + // flash withdraw (to generate deposit bonus) + let flashWithdrawTx = + await iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](toWei(5), staker.address, 0n); + await flashWithdrawTx.wait(); + + // Assert: check deposit bonus + const depositBonusAmount = await iVault.depositBonusAmount(); + expect(depositBonusAmount).to.be.gt(0); + + // Act + const { iVault: iVaultNew } = await initVault(assetData); + await (await iVault.migrateDepositBonus(await iVaultNew.getAddress())).wait(); + + // Assert: bonus migrated + const oldDepositBonus = await iVault.depositBonusAmount(); + expect(oldDepositBonus, 'Old vault deposit bonus should equal 0').to.be.eq(0); + + const newVaultBalance = await asset.balanceOf(iVaultNew.address); + expect(newVaultBalance, 'New vault balance should equal to transferred deposit bonus').to.be.eq(depositBonusAmount); + }); + + it('should revert if the new vault address is zero', async () => { + await expect(iVault.migrateDepositBonus(ethers.ZeroAddress)).to.be.revertedWithCustomError( + iVault, + 'InvalidAddress' + ); + }); + + it('should revert if there is no deposit bonus to migrate', async () => { + expect(await iVault.depositBonusAmount(), 'Deposit bonus should be 0').to.be.eq(0); + + await expect(iVault.migrateDepositBonus(staker.address)).to.be.revertedWithCustomError(iVault, 'NullParams'); + }); + + it('should revert if there are delegated funds', async () => { + // Arrange + // deposit + delegate + const depositAmount = toWei(10); + await (await iVault.connect(staker).deposit(depositAmount, staker.address)).wait(); + await (await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), emptyBytes)).wait(); + + // flash withdraw (to generate deposit bonus) + let flashWithdrawTx = + await iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](toWei(1), staker.address, 0n); + await flashWithdrawTx.wait(); + + const depositBonusAmount = await iVault.depositBonusAmount(); + expect(depositBonusAmount, 'Deposit bonus should exist').to.be.gt(0); + + const { iVault: iVaultNew } = await initVault(assetData); + + // Act/Assert + await expect(iVault.migrateDepositBonus(iVaultNew.address)).to.be.revertedWithCustomError(iVault, 'ValueZero'); + }); + + it('should only allow the owner to migrate the deposit bonus', async () => { + await expect(iVault.connect(staker).migrateDepositBonus(staker.address)).to.be.revertedWith( + 'Ownable: caller is not the owner' + ); + }); + + it('should emit an event', async () => { + // Arrange + // deposit + let depositTx = await iVault.connect(staker).deposit(toWei(50), staker.address); + await depositTx.wait(); + + // flash withdraw (to generate deposit bonus) + let flashWithdrawTx = + await iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](toWei(25), staker.address, 0n); + await flashWithdrawTx.wait(); + const depositBonusAmount = await iVault.depositBonusAmount(); + + // Act + const { iVault: iVaultNew } = await initVault(assetData); + const migrateTx = await (await iVault.migrateDepositBonus(await iVaultNew.getAddress())).wait(); + + // Assert: event emitted + await expect(migrateTx) + .to.emit(iVault, 'DepositBonusTransferred') + .withArgs(iVaultNew.address, depositBonusAmount); + }); + }); + + describe('Undelegation', function () { + beforeEach(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("request two withdrawals in the same epoch", async function () { + const depositAmount = toWei(10); + + await (await iVault.connect(staker).deposit(depositAmount, staker.address)).wait(); + + await (await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, depositAmount, emptyBytes)).wait(); + + // first withdraw + await iVault.connect(staker).withdraw(toWei(1), staker.address); + + const previousSymbioticVault = await symbioticVaults[0].vault.currentEpoch(); + + // first undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + let receipt = await (await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]])) + .wait(); + let adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address).map(log => symbioticAdapter.interface.parseLog(log)); + let claimer1 = adapterEvents[0].args["claimer"]; + + // second withdraw + await iVault.connect(staker).withdraw(toWei(1), staker.address); + + // second undelegate + epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + receipt = await (await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]])) + .wait(); + adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address).map(log => symbioticAdapter.interface.parseLog(log)); + let claimer2 = adapterEvents[0].args["claimer"]; + + await skipEpoch(symbioticVaults[0]); + expect(await symbioticVaults[0].vault.currentEpoch()).to.be.greaterThan(previousSymbioticVault); + + let balanceBefore = await iVault.totalAssets(); + + // claim + let params = await symbioticClaimParams(symbioticVaults[0], claimer1); + await iVault.connect(iVaultOperator).claim( + 1, [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]], + ); + + expect((await iVault.totalAssets()) - balanceBefore).to.be.eq(toWei(1)); + balanceBefore = await iVault.totalAssets(); + + // claim + params = await symbioticClaimParams(symbioticVaults[0], claimer2); + await iVault.connect(iVaultOperator).claim( + 2, [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]], + ); + + expect((await iVault.totalAssets()) - balanceBefore).to.be.eq(toWei(1)); + }); + + it('epoch should be changed if undelegate current epoch', async () => { + // Arrange: deposit > delegate > withdraw + const depositAmount = toWei(10); + + await (await iVault.connect(staker).deposit(depositAmount, staker.address)).wait(); + + await (await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, depositAmount, emptyBytes)).wait(); + + const tx = await iVault.connect(staker).withdraw(toWei(1), staker.address); + await tx.wait(); + + const currentEpoch = await withdrawalQueue.currentEpoch(); + + // Act: undelegate + let epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + await (await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]])) + .wait(); + + // Assert: epoch increased + const newEpoch = await withdrawalQueue.currentEpoch(); + expect(newEpoch).to.eq(currentEpoch + 1n); + }); + + it('should revert if requested amount is greater than available capacity', async () => { + const depositAmount = toWei(10); + // deposit + let tx = await iVault.connect(staker).deposit(depositAmount, staker.address); + await tx.wait(); + + // delegate + tx = await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, depositAmount, emptyBytes); + await tx.wait(); + + // one withdraw + let shares = await iToken.balanceOf(staker.address); + tx = await iVault.connect(staker).withdraw(shares, staker.address); + await tx.wait(); + + // Act/Assert: undelegate and check revered with error + await expect(iVault.connect(iVaultOperator).undelegate( + await withdrawalQueue.currentEpoch(), + [] + )).to.be.revertedWithCustomError(iVault, "InsufficientFreeBalance"); + }); + }); + + describe('claimAdapterFreeBalance', () => { + beforeEach(async () => { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("can be called only by operator", async() => { + await expect(iVault.connect(staker).claimAdapterFreeBalance(symbioticAdapter.address)) + .to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); + + await expect(symbioticAdapter.claimFreeBalance()) + .to.be.revertedWithCustomError(symbioticAdapter, "NotVaultOrTrusteeManager"); + }); + + it('should claim free balance for the adapter', async () => { + // Arrange + const amount = toWei(5); + await asset.connect(staker).transfer(symbioticAdapter.address, amount); + + const vaultBalanceBefore = await iVault.totalAssets(); + const adapterBalanceBefore = await asset.balanceOf(symbioticAdapter.address); + + console.log(`Vault balance before: ${vaultBalanceBefore}`); + console.log(`Adapter balance before: ${adapterBalanceBefore}`); + + // Act + await iVault.connect(iVaultOperator).claimAdapterFreeBalance(symbioticAdapter.address); + + // Assert: assets transferred to vault + const adapterBalance = await asset.balanceOf(symbioticAdapter.address); + expect(adapterBalance).to.be.eq(0n); + const vaultBalanceAfter = await iVault.totalAssets(); + expect(vaultBalanceAfter).to.be.eq(vaultBalanceBefore + amount); + }); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/adapter.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S/adapter.test.ts new file mode 100644 index 00000000..ab42b620 --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/adapter.test.ts @@ -0,0 +1,333 @@ +// Tests for InceptionVault_S contract; +// The S in name does not mean only Symbiotic; this file contains tests for Symbiotic and Mellow adapters + +import { expect } from "chai"; +import hardhat from "hardhat"; +import { stETH } from "../../data/assets/inception-vault-s"; +import { vaults } from "../../data/vaults"; +import { adapters, emptyBytes } from "../../src/constants"; +import { initVault, abi } from "../../src/init-vault"; +import { calculateRatio, toWei } from "../../helpers/utils"; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { ZeroAddress } from "ethers"; + +const { ethers, network } = hardhat; + +const symbioticVaults = vaults.symbiotic; +const mellowVaults = vaults.mellow; +const assetData = stETH; +describe(`Inception Symbiotic Vault ${assetData.assetName}`, function() { + let iToken, iVault, mellowAdapter, symbioticAdapter, withdrawalQueue; + let iVaultOperator, staker, staker2, staker3; + let snapshot; + + before(async function() { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.assetName.toLowerCase())) { + console.log(`${assetData.assetName} is not in the list, going to skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, iVaultOperator, mellowAdapter, symbioticAdapter, withdrawalQueue } + = await initVault(assetData, { adapters: [adapters.Mellow, adapters.Symbiotic] })); + + [, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function() { + await iVault?.removeAllListeners(); + }); + + describe("AdapterHandler negative cases", function() { + it("null adapter delegation", async function() { + await expect( + iVault + .connect(iVaultOperator) + .delegate("0x0000000000000000000000000000000000000000", symbioticVaults[0].vaultAddress, 0, emptyBytes), + ).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("adapter not exists", async function() { + await expect( + iVault.connect(iVaultOperator).delegate(staker.address, symbioticVaults[0].vaultAddress, 0, emptyBytes), + ).to.be.revertedWithCustomError(iVault, "AdapterNotFound"); + }); + + it("undelegate input args", async function() { + await expect( + iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), + [ + ["0x0000000000000000000000000000000000000000", mellowVaults[0].vaultAddress, 1n, emptyBytes], + ], + ), + ).to.be.revertedWithCustomError(iVault, "AdapterNotFound"); + + await expect( + iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, 0n, emptyBytes]]), + ).to.be.revertedWithCustomError(iVault, "ValueZero"); + }); + + it("undelegateVault input args", async function() { + await expect( + iVault + .connect(iVaultOperator) + .emergencyUndelegate([[staker.address, mellowVaults[0].vaultAddress, 1n, []]]), + ).to.be.revertedWithCustomError(iVault, "AdapterNotFound"); + + await expect( + iVault.connect(iVaultOperator) + .emergencyUndelegate([[mellowAdapter.address, "0x0000000000000000000000000000000000000000", 1n, []]]), + ).to.be.revertedWithCustomError(iVault, "InvalidAddress"); + + await expect( + iVault + .connect(iVaultOperator) + .emergencyUndelegate([[mellowAdapter.address, mellowVaults[0].vaultAddress, 0n, []]]), + ).to.be.revertedWithCustomError(iVault, "ValueZero"); + + await expect( + iVault + .connect(staker) + .emergencyUndelegate([[mellowAdapter.address, mellowVaults[0].vaultAddress, 0n, []]]), + ).to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); + }); + + it("claim input args", async function() { + await expect( + iVault.connect(staker).claim(0, [mellowAdapter.address], [mellowVaults[0].vaultAddress], [emptyBytes]), + ).to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); + + await expect( + iVault.connect(iVaultOperator).claim(0, [staker.address], [mellowVaults[0].vaultAddress], [emptyBytes]), + ).to.be.revertedWithCustomError(iVault, "AdapterNotFound"); + + await expect(iVault.connect(iVaultOperator).claim(0, [], [], [])) + .to.be.revertedWithCustomError(iVault, "ValueZero"); + }); + + it("addAdapter input args", async function() { + await expect(iVault.addAdapter(ethers.Wallet.createRandom().address)) + .to.be.revertedWithCustomError(iVault, "NotContract"); + + await expect(iVault.addAdapter(mellowAdapter.address)).to.be.revertedWithCustomError( + iVault, + "AdapterAlreadyAdded", + ); + + await expect(iVault.connect(iVaultOperator).addAdapter(mellowAdapter.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("removeAdapter input args", async function() { + await expect(iVault.removeAdapter(iToken.address)) + .to.be.revertedWithCustomError(iVault, "AdapterNotFound"); + + await expect(iVault.connect(staker) + .removeAdapter(mellowAdapter.address), + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await iVault.removeAdapter(mellowAdapter.address); + }); + + it("emergencyClaim input args", async function() { + await expect(iVault.connect(staker).emergencyClaim([], [], [])) + .to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); + + await iVault.pause(); + await expect(iVault.connect(iVaultOperator).emergencyClaim([], [], [])) + .to.be.revertedWith("Pausable: paused"); + await iVault.unpause(); + + await expect(iVault.connect(iVaultOperator).emergencyClaim([], [], [])) + .to.be.revertedWithCustomError(iVault, "ValueZero"); + }); + + it("unavailable to set empty inception vault", async function() { + await expect(mellowAdapter.setInceptionVault(ZeroAddress)) + .to.be.revertedWithCustomError(mellowAdapter, "NotContract"); + + await expect(symbioticAdapter.setInceptionVault(ZeroAddress)) + .to.be.revertedWithCustomError(symbioticAdapter, "NotContract"); + }); + }); + + describe("SymbioticAdapter input args", function() { + it("withdraw input args", async function() { + await expect( + iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await symbioticAdapter.getAddress(), staker.address, 1n, emptyBytes]]), + ).to.be.revertedWithCustomError(symbioticAdapter, "InvalidVault"); + + await expect(symbioticAdapter.connect(staker).withdraw(ZeroAddress, 0n, [], false)) + .to.be.revertedWithCustomError(symbioticAdapter, "NotVaultOrTrusteeManager"); + + await expect(symbioticAdapter.setClaimerImplementation(ZeroAddress)); + await expect(symbioticAdapter.connect(iVaultOperator).withdraw(symbioticVaults[0].vaultAddress, 1n, [], false)) + .to.be.revertedWithCustomError(symbioticAdapter, "ClaimerImplementationNotSet"); + await snapshot.restore(); + }); + + it("claim input args", async function() { + await expect(symbioticAdapter.connect(staker).claim(["0x", "0x"], false)) + .to.be.revertedWithCustomError(symbioticAdapter, "NotVaultOrTrusteeManager"); + + await expect(symbioticAdapter.connect(iVaultOperator).claim(["0x", "0x"], false)) + .to.be.revertedWithCustomError(symbioticAdapter, "InvalidDataLength"); + + await expect(symbioticAdapter.connect(iVaultOperator).claim( + [abi.encode(["address", "address"], [symbioticVaults[0].vaultAddress, ethers.Wallet.createRandom().address])], true), + ).to.be.revertedWithCustomError(symbioticAdapter, "OnlyEmergency"); + }); + + it("add & remove vaults input args", async function() { + await expect(symbioticAdapter.connect(iVaultOperator).addVault(staker.address)) + .to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + symbioticAdapter.connect(iVaultOperator).removeVault(symbioticVaults[0].vaultAddress), + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("unavailable to while paused", async function() { + await symbioticAdapter.pause(); + + await expect(symbioticAdapter.connect(iVaultOperator).delegate(ethers.ZeroAddress, 0n, [])) + .to.be.revertedWith("Pausable: paused"); + + await expect(symbioticAdapter.connect(iVaultOperator).withdraw(ethers.ZeroAddress, 0n, [], false)) + .to.be.revertedWith("Pausable: paused"); + + await expect(symbioticAdapter.connect(iVaultOperator).claim([], false)) + .to.be.revertedWith("Pausable: paused"); + + await symbioticAdapter.unpause(); + }); + + it("set claimer implementation", async function() { + await expect(symbioticAdapter.connect(staker).setClaimerImplementation(ZeroAddress)) + .to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("MellowAdapter input args", function() { + it("claim input args", async function() { + await expect(mellowAdapter.connect(iVaultOperator).claim([], false)) + .to.be.revertedWithCustomError(mellowAdapter, "ValueZero"); + + await expect(mellowAdapter.connect(iVaultOperator).claim( + [abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, ethers.Wallet.createRandom().address])], true), + ).to.be.revertedWithCustomError(mellowAdapter, "OnlyEmergency"); + + await expect(mellowAdapter.connect(iVaultOperator).claim( + [abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, ethers.Wallet.createRandom().address])], false), + ).to.be.revertedWithCustomError(mellowAdapter, "InvalidVault"); + }); + + it("setEthWrapper input args", async function() { + await expect(mellowAdapter.connect(iVaultOperator).setEthWrapper(staker.address)) + .to.be.revertedWith("Ownable: caller is not the owner"); + + await expect(mellowAdapter.setEthWrapper(ethers.Wallet.createRandom().address)) + .to.be.revertedWithCustomError(mellowAdapter, "NotContract"); + }); + + it("unable to run while paused", async function() { + await mellowAdapter.pause(); + + await expect(mellowAdapter.connect(iVaultOperator).delegate(ethers.ZeroAddress, 0n, [])) + .to.be.revertedWith("Pausable: paused"); + + await expect(mellowAdapter.connect(iVaultOperator).withdraw(ethers.ZeroAddress, 0n, [], false)) + .to.be.revertedWith("Pausable: paused"); + + await expect(mellowAdapter.connect(iVaultOperator).claim([], false)) + .to.be.revertedWith("Pausable: paused"); + + await mellowAdapter.unpause(); + }); + + it("change allocation input args", async function() { + await expect(mellowAdapter.connect(staker).changeAllocation(ZeroAddress, 0n)) + .to.be.revertedWith("Ownable: caller is not the owner"); + + await expect(mellowAdapter.changeAllocation(ZeroAddress, 0n)) + .to.be.revertedWithCustomError(mellowAdapter, "ZeroAddress"); + + await expect(mellowAdapter.changeAllocation(ethers.Wallet.createRandom().address, 0n)) + .to.be.revertedWithCustomError(mellowAdapter, "InvalidVault"); + }); + + it("mellow input args", async function() { + await expect(mellowAdapter.setClaimerImplementation(ZeroAddress)); + await expect(mellowAdapter.connect(iVaultOperator).withdraw(mellowVaults[0].vaultAddress, 1n, [], false)) + .to.be.revertedWithCustomError(mellowAdapter, "ClaimerImplementationNotSet"); + await snapshot.restore(); + }); + }); + + describe("Adapter claimers", function() { + let mellowClaimerAddr, symbioticClaimerAddr; + + before(async function() { + await snapshot.restore(); + + iVault.setTargetFlashCapacity(1n); + // deposit + await iVault.connect(staker).deposit(toWei(100), staker.address); + // delegate mellow + await iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, mellowVaults[0].vaultAddress, toWei(5), emptyBytes); + // delegate symbiotic + await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, toWei(5), emptyBytes); + // withdraw + await iVault.connect(staker).withdraw(toWei(2), staker.address); + // undelegate + const tx = await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [ + [await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, toWei(1), emptyBytes], + [await symbioticAdapter.getAddress(), symbioticVaults[0].vaultAddress, toWei(1), emptyBytes], + ]); + const receipt = await tx.wait(); + let adapterEvents = receipt.logs?.filter(log => log.address === mellowAdapter.address).map(log => mellowAdapter.interface.parseLog(log)); + mellowClaimerAddr = adapterEvents[0].args["claimer"]; + adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address).map(log => symbioticAdapter.interface.parseLog(log)); + symbioticClaimerAddr = adapterEvents[0].args["claimer"]; + }); + + it("mellow claimer: only adapter claim", async function() { + const mellowClaimer = await ethers.getContractAt("MellowAdapterClaimer", mellowClaimerAddr); + await expect(mellowClaimer.claim(ethers.ZeroAddress, ethers.ZeroAddress, 0n)) + .to.be.revertedWithCustomError(mellowClaimer, "OnlyAdapter"); + }); + + it("symbiotic claimer: only adapter claim", async function() { + const symbioticClaimer = await ethers.getContractAt("SymbioticAdapterClaimer", symbioticClaimerAddr); + await expect(symbioticClaimer.claim(ethers.ZeroAddress, ethers.ZeroAddress, 0n)) + .to.be.revertedWithCustomError(symbioticClaimer, "OnlyAdapter"); + }); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/commented-tests.ts b/projects/vaults/test/tests-unit/InceptionVault_S/commented-tests.ts new file mode 100644 index 00000000..8da218ff --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/commented-tests.ts @@ -0,0 +1,368 @@ +// describe("Delegate auto according allocation", function () { + // describe("Set allocation", function () { + // before(async function () { + // await snapshot.restore(); + // await mellowAdapter.addMellowVault(mellowVaults[1].vaultAddress, mellowVaults[1].wrapperAddress); + // }); + + // const args = [ + // { + // name: "Set allocation for the 1st vault", + // vault: () => mellowVaults[0].vaultAddress, + // shares: randomBI(2), + // }, + // { + // name: "Set allocation for another vault", + // vault: () => mellowVaults[1].vaultAddress, + // shares: randomBI(2), + // }, + // { + // name: "Change allocation", + // vault: () => mellowVaults[1].vaultAddress, + // shares: randomBI(2), + // }, + // { + // name: "Set allocation for address that is not in the list", + // vault: () => ethers.Wallet.createRandom().address, + // shares: randomBI(2), + // }, + // { + // name: "Change allocation to 0", + // vault: () => mellowVaults[1].vaultAddress, + // shares: 0n, + // }, + // ]; + + // args.forEach(function (arg) { + // it(`${arg.name}`, async function () { + // const vaultAddress = arg.vault(); + // const totalAllocationBefore = await mellowAdapter.totalAllocations(); + // const sharesBefore = await mellowAdapter.allocations(vaultAddress); + // console.log(`sharesBefore: ${sharesBefore.toString()}`); + + // await expect(mellowAdapter.changeAllocation(vaultAddress, arg.shares)) + // .to.be.emit(mellowAdapter, "AllocationChanged") + // .withArgs(vaultAddress, sharesBefore, arg.shares); + + // const totalAllocationAfter = await mellowAdapter.totalAllocations(); + // const sharesAfter = await mellowAdapter.allocations(vaultAddress); + // console.log("Total allocation after:", totalAllocationAfter.format()); + // console.log("Adapter allocation after:", sharesAfter.format()); + + // expect(sharesAfter).to.be.eq(arg.shares); + // expect(totalAllocationAfter - totalAllocationBefore).to.be.eq(sharesAfter - sharesBefore); + // }); + // }); + + // it("changeAllocation reverts when vault is 0 address", async function () { + // const shares = randomBI(2); + // const vaultAddress = ethers.ZeroAddress; + // await expect(mellowAdapter.changeAllocation(vaultAddress, shares)).to.be.revertedWithCustomError( + // mellowAdapter, + // "ZeroAddress", + // ); + // }); + + // it("changeAllocation reverts when called by not an owner", async function () { + // const shares = randomBI(2); + // const vaultAddress = mellowVaults[1].vaultAddress; + // await expect(mellowAdapter.connect(staker).changeAllocation(vaultAddress, shares)).to.be.revertedWith( + // "Ownable: caller is not the owner", + // ); + // }); + // }); + + // describe("Delegate auto", function () { + // let totalDeposited; + + // beforeEach(async function () { + // await snapshot.restore(); + // await iVault.setTargetFlashCapacity(1n); + // totalDeposited = randomBI(19); + // await iVault.connect(staker).deposit(totalDeposited, staker.address); + // }); + + // //mellowVaults[0] added at deploy + // const args = [ + // { + // name: "1 vault, no allocation", + // addVaults: [], + // allocations: [], + // }, + // { + // name: "1 vault; allocation 100%", + // addVaults: [], + // allocations: [ + // { + // vault: mellowVaults[0].vaultAddress, + // amount: 1n, + // }, + // ], + // }, + // { + // name: "1 vault; allocation 100% and 0% to unregistered", + // addVaults: [], + // allocations: [ + // { + // vault: mellowVaults[0].vaultAddress, + // amount: 1n, + // }, + // { + // vault: mellowVaults[1].vaultAddress, + // amount: 0n, + // }, + // ], + // }, + // { + // name: "1 vault; allocation 50% and 50% to unregistered", + // addVaults: [], + // allocations: [ + // { + // vault: mellowVaults[0].vaultAddress, + // amount: 1n, + // }, + // { + // vault: mellowVaults[1].vaultAddress, + // amount: 1n, + // }, + // ], + // }, + // { + // name: "2 vaults; allocations: 100%, 0%", + // addVaults: [mellowVaults[1]], + // allocations: [ + // { + // vault: mellowVaults[0].vaultAddress, + // amount: 1n, + // }, + // { + // vault: mellowVaults[1].vaultAddress, + // amount: 0n, + // }, + // ], + // }, + // { + // name: "2 vaults; allocations: 50%, 50%", + // addVaults: [mellowVaults[1]], + // allocations: [ + // { + // vault: mellowVaults[0].vaultAddress, + // amount: 1n, + // }, + // { + // vault: mellowVaults[1].vaultAddress, + // amount: 1n, + // }, + // ], + // }, + // { + // name: "3 vaults; allocations: 33%, 33%, 33%", + // addVaults: [mellowVaults[1], mellowVaults[2]], + // allocations: [ + // { + // vault: mellowVaults[0].vaultAddress, + // amount: 1n, + // }, + // { + // vault: mellowVaults[1].vaultAddress, + // amount: 1n, + // }, + // { + // vault: mellowVaults[2].vaultAddress, + // amount: 1n, + // }, + // ], + // }, + // ]; + + // args.forEach(function (arg) { + // it(`Delegate auto when ${arg.name}`, async function () { + // //Add adapters + // const addedVaults = [mellowVaults[0].vaultAddress]; + // for (const vault of arg.addVaults) { + // await mellowAdapter.addMellowVault(vault.vaultAddress, vault.wrapperAddress); + // addedVaults.push(vault.vaultAddress); + // } + // //Set allocations + // let totalAllocations = 0n; + // for (const allocation of arg.allocations) { + // await mellowAdapter.changeAllocation(allocation.vault, allocation.amount); + // totalAllocations += allocation.amount; + // } + // //Calculate expected delegated amounts + // const freeBalance = await iVault.getFreeBalance(); + // expect(freeBalance).to.be.closeTo(totalDeposited, 1n); + // let expectedDelegated = 0n; + // const expectedDelegations = new Map(); + // for (const allocation of arg.allocations) { + // let amount = 0n; + // if (addedVaults.includes(allocation.vault)) { + // amount += (freeBalance * allocation.amount) / totalAllocations; + // } + // expectedDelegations.set(allocation.vault, amount); + // expectedDelegated += amount; + // } + + // await iVault.connect(iVaultOperator).delegateAuto(1296000); + + // const totalDepositedAfter = await iVault.getTotalDeposited(); + // const totalDelegatedAfter = await iVault.getTotalDelegated(); + // const totalAssetsAfter = await iVault.totalAssets(); + // console.log(`Total deposited after: ${totalDepositedAfter.format()}`); + // console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); + // console.log(`Total assets after: ${totalAssetsAfter.format()}`); + + // expect(totalDepositedAfter).to.be.closeTo(totalDeposited, transactErr * BigInt(addedVaults.length)); + // expect(totalDelegatedAfter).to.be.closeTo(expectedDelegated, transactErr * BigInt(addedVaults.length)); + // expect(totalAssetsAfter).to.be.closeTo(totalDeposited - expectedDelegated, transactErr); + + // for (const allocation of arg.allocations) { + // expect(expectedDelegations.get(allocation.vault)).to.be.closeTo( + // await iVault.getDelegatedTo(allocation.vault), + // transactErr, + // ); + // } + // }); + // }); + + // it("delegateAuto reverts when called by not an owner", async function () { + // await mellowAdapter.changeAllocation(mellowVaults[0].vaultAddress, 1n); + // await expect(iVault.connect(staker).delegateAuto(1296000)).to.revertedWithCustomError( + // iVault, + // "OnlyOperatorAllowed", + // ); + // }); + + // it("delegateAuto reverts when iVault is paused", async function () { + // await mellowAdapter.changeAllocation(mellowVaults[0].vaultAddress, 1n); + // await iVault.pause(); + // await expect(iVault.connect(iVaultOperator).delegateAuto(1296000)).to.be.revertedWith("Pausable: paused"); + // }); + + // it("delegateAuto reverts when mellowAdapter is paused", async function () { + // if (await iVault.paused()) { + // await iVault.unpause(); + // } + // await mellowAdapter.changeAllocation(mellowVaults[0].vaultAddress, 1n); + // await mellowAdapter.pause(); + // await expect(iVault.connect(iVaultOperator).delegateAuto(1296000)).to.be.revertedWith("Pausable: paused"); + // }); + // }); + // }); + + + /** + * Forces execution of pending withdrawal, + * if configurator.emergencyWithdrawalDelay() has passed since its creation + * but not later than fulfill deadline. + */ + // describe("undelegateForceFrom", function () { + // let delegated; + // let emergencyWithdrawalDelay; + // let mVault, configurator; + + // before(async function () { + // await snapshot.restore(); + // await iVault.setTargetFlashCapacity(1n); + // await iVault.connect(staker).deposit(10n * e18, staker.address); + // delegated = await iVault.getFreeBalance(); + // await mellowAdapter.addMellowVault(mellowVaults[2].vaultAddress, mellowVaults[2].wrapperAddress); + // await iVault.connect(iVaultOperator).delegateToMellowVault(mellowVaults[2].vaultAddress, delegated, 1296000); + // console.log(`Delegated amount: \t${delegated.format()}`); + + // mVault = await ethers.getContractAt("IMellowVault", mellowVaults[2].vaultAddress); + // configurator = await ethers.getContractAt("IMellowVaultConfigurator", mellowVaults[2].configuratorAddress); + // emergencyWithdrawalDelay = (await configurator.emergencyWithdrawalDelay()) / day; + // }); + + // it("undelegateForceFrom reverts when there is no pending withdraw request", async function () { + // await expect( + // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), + // ).to.be.revertedWithCustomError(mVault, "InvalidState"); + // }); + + // it("set request deadline > emergencyWithdrawalDelay", async function () { + // const newDeadline = emergencyWithdrawalDelay + 10n; //~ 100d + // await mellowAdapter.setRequestDeadline(newDeadline); + // console.log("New request deadline in days:", (await mellowAdapter.requestDeadline()) / day); + // expect(await mellowAdapter.requestDeadline()).to.be.eq(newDeadline * day); + // }); + + // it("undelegateForceFrom reverts when it is less than emergencyWithdrawalDelay has passed since submission", async function () { + // await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[2].vaultAddress, delegated / 2n, 1296000); + // await helpers.time.increase((emergencyWithdrawalDelay - 1n) * day); + + // await expect( + // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), + // ).to.be.revertedWithCustomError(mVault, "InvalidState"); + // }); + + // it("undelegateForceFrom cancels expired request", async function () { + // await helpers.time.increase(12n * day); //Wait until request expired + + // const tx = await iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000); + + // await expect(tx).to.emit(mVault, "WithdrawalRequestCanceled").withArgs(mellowAdapter.address, anyValue); + // await expect(await mellowAdapter.getDeposited(mellowVaults[2].vaultAddress)).to.be.closeTo( + // delegated, + // transactErr, + // ); + // await expect(await mellowAdapter.pendingWithdrawalAmount()).to.be.eq(0n); + // }); + + // it("undelegateForceFrom reverts if it can not provide min amount", async function () { + // await iVault.connect(iVaultOperator).undelegateFromMellow(mellowVaults[2].vaultAddress, e18, 1296000); + // await helpers.time.increase(emergencyWithdrawalDelay * day + 1n); + + // await expect( + // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), + // ).to.be.revertedWithCustomError(mVault, "InsufficientAmount"); + // }); + + // it("undelegateForceFrom reverts when called by not an operator", async function () { + // await expect( + // iVault.connect(staker).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), + // ).to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); + // }); + + // it("withdrawEmergencyMellow reverts when called by not a trustee", async function () { + // await expect( + // mellowAdapter.connect(staker).withdrawEmergencyMellow(mellowVaults[0].vaultAddress, 1296000), + // ).to.revertedWithCustomError(mellowAdapter, "NotVaultOrTrusteeManager"); + // }); + + // it("undelegateForceFrom reverts when iVault is paused", async function () { + // await iVault.pause(); + // await expect( + // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), + // ).to.be.revertedWith("Pausable: paused"); + // }); + + // it("undelegateForceFrom reverts when mellowAdapter is paused", async function () { + // if (await iVault.paused()) { + // await iVault.unpause(); + // } + + // await mellowAdapter.pause(); + // await expect( + // iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000), + // ).to.be.revertedWith("Pausable: paused"); + // }); + + // it("undelegateForceFrom withdraws all from mellow vault when there is suitable request", async function () { + // if (await mellowAdapter.paused()) { + // await mellowAdapter.unpause(); + // } + + // const newSlippage = 3_000; //30% + // await mellowAdapter.setSlippages(newSlippage, newSlippage); + + // //!!!_Test fails because slippage is too high + // await iVault.connect(iVaultOperator).undelegateForceFrom(mellowVaults[2].vaultAddress, 1296000); + + // expect(await asset.balanceOf(mellowAdapter.address)).to.be.gte(0n); + // expect(await mellowAdapter.pendingWithdrawalAmount()).to.be.eq(0n); + // }); + // }); + + \ No newline at end of file diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/delegate.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S/delegate.test.ts new file mode 100644 index 00000000..a435afc1 --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/delegate.test.ts @@ -0,0 +1,357 @@ +// Tests for InceptionVault_S contract; +// The S in name does not mean only Symbiotic; this file contains tests for Symbiotic and Mellow adapters + +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { stETH } from "../../data/assets/inception-vault-s"; +import { vaults } from "../../data/vaults"; +import { calculateRatio, e18, getRandomStaker, randomBI, toWei } from "../../helpers/utils"; +import { adapters, emptyBytes } from "../../src/constants"; +import { initVault } from "../../src/init-vault"; + +const mellowVaults = vaults.mellow; +const { ethers, network } = hardhat; +const assetData = stETH; + +describe(`Inception Symbiotic Vault ${assetData.assetName}`, function () { + let iToken, iVault, ratioFeed, asset, mellowAdapter, withdrawalQueue; + let iVaultOperator, staker, staker2, staker3; + let ratioErr, transactErr; + let snapshot; + + before(async function () { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.assetName.toLowerCase())) { + console.log(`${assetData.assetName} is not in the list, going to skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: assetData.url ? assetData.url : network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, ratioFeed, asset, iVaultOperator, mellowAdapter, withdrawalQueue } + = await initVault(assetData, { adapters: [adapters.Mellow] })); + + ratioErr = assetData.ratioErr; + transactErr = assetData.transactErr; + + [, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function () { + await iVault?.removeAllListeners(); + }); + + describe("Delegate to mellow vault", function () { + let ratio, firstDeposit; + + beforeEach(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker3).deposit(e18, staker3.address); + firstDeposit = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, firstDeposit, emptyBytes); + await assetData.addRewardsMellowVault(toWei(0.001), mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + ratio = await iVault.ratio(); + console.log(`Initial ratio: ${ratio.format()}`); + }); + + const args = [ + { + name: "random amounts ~ e18", + depositAmount: async () => toWei(1), + }, + { + name: "amounts which are close to min", + depositAmount: async () => (await iVault.withdrawMinAmount()) + 1n, + }, + ]; + + args.forEach(function (arg) { + it(`Deposit and delegate ${arg.name} many times`, async function () { + await iVault.setTargetFlashCapacity(1n); + let totalDelegated = 0n; + const count = 10; + for (let i = 0; i < count; i++) { + const deposited = await arg.depositAmount(); + await iVault.connect(staker).deposit(deposited, staker.address); + const delegated = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, delegated, emptyBytes); + + totalDelegated += deposited; + } + console.log(`Final ratio:\t${(await iVault.ratio()).format()}`); + console.log(`Total delegated:\t${totalDelegated.format()}`); + + const balanceExpected = (totalDelegated * ratio) / e18; + const totalSupplyExpected = balanceExpected + firstDeposit; + const err = BigInt(count) * transactErr * 2n; + + const balanceAfter = await iToken.balanceOf(staker.address); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const totalDelegatedToAfter = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalSupplyAfter = await iToken.totalSupply(); + const totalAssetsAfter = await iVault.totalAssets(); + console.log(`Staker balance after: ${balanceAfter.format()}`); + console.log(`Total deposited after: ${totalDepositedAfter.format()}`); + console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); + console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); + console.log(`Total assets after: ${totalAssetsAfter.format()}`); + + expect(balanceAfter - balanceExpected).to.be.closeTo(0, err); + expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); + expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); + expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0, err); + expect(totalAssetsAfter).to.be.lte(transactErr); + expect(await iVault.ratio()).to.be.closeTo(ratio, BigInt(count) * ratioErr); + }); + }); + + const args2 = [ + { + name: "by the same staker", + staker: async () => staker, + }, + { + name: "by different stakers", + staker: async () => await getRandomStaker(iVault, asset, staker3, toWei(1)), + }, + ]; + + args2.forEach(function (arg) { + it(`Deposit many times and delegate once ${arg.name}`, async function () { + await iVault.setTargetFlashCapacity(1n); + let totalDeposited = 0n; + const count = 10; + for (let i = 0; i < count; i++) { + const staker = await arg.staker(); + const deposited = await randomBI(18); + await iVault.connect(staker).deposit(deposited, staker.address); + totalDeposited += deposited; + } + const totalDelegated = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, totalDelegated, emptyBytes); + + console.log(`Final ratio:\t${await iVault.ratio()}`); + console.log(`Total deposited:\t${totalDeposited.format()}`); + console.log(`Total delegated:\t${totalDelegated.format()}`); + + const balanceExpected = (totalDelegated * ratio) / e18; + const totalSupplyExpected = balanceExpected + firstDeposit; + const err = BigInt(count) * transactErr * 2n; + + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const totalDelegatedToAfter = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalSupplyAfter = await iToken.totalSupply(); + const totalAssetsAfter = await iVault.totalAssets(); + console.log(`Total deposited after: ${totalDepositedAfter.format()}`); + console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); + console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); + console.log(`Total assets after: ${totalAssetsAfter.format()}`); + + expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); + expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0n, err); + expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0n, err); + expect(totalAssetsAfter).to.be.lte(transactErr); + expect(await iVault.ratio()).to.be.closeTo(ratio, BigInt(count) * ratioErr); + }); + }); + + const args3 = [ + { + name: "to the different operators", + count: 20, + mellowVault: i => mellowVaults[i % mellowVaults.length].vaultAddress, + }, + { + name: "to the same operator", + count: 10, + mellowVault: i => mellowVaults[0].vaultAddress, + }, + ]; + + args3.forEach(function (arg) { + it(`Delegate many times ${arg.name}`, async function () { + for (let i = 1; i < mellowVaults.length; i++) { + await mellowAdapter.addMellowVault(mellowVaults[i].vaultAddress); + } + + await iVault.setTargetFlashCapacity(1n); + //Deposit by 2 stakers + const totalDelegated = toWei(60); + await iVault.connect(staker).deposit(totalDelegated / 2n, staker.address); + await iVault.connect(staker2).deposit(totalDelegated / 2n, staker2.address); + //Delegate + for (let i = 0; i < arg.count; i++) { + const taBefore = await iVault.totalAssets(); + const mVault = arg.mellowVault(i); + console.log(`#${i} mellow vault: ${mVault}`); + const fb = await iVault.getFreeBalance(); + const amount = fb / BigInt(arg.count - i); + await expect( + iVault.connect(iVaultOperator).delegate(await mellowAdapter.getAddress(), mVault, amount, emptyBytes), + ) + .to.emit(iVault, "DelegatedTo") + .withArgs(mellowAdapter.address, mVault, amount); + + const taAfter = await iVault.totalAssets(); + expect(taBefore - taAfter).to.be.closeTo(amount, transactErr); + } + console.log(`Final ratio:\t${await iVault.ratio()}`); + + const balanceExpected = (totalDelegated * ratio) / e18; + const totalSupplyExpected = balanceExpected + firstDeposit; + const err = BigInt(arg.count) * transactErr * 2n; + + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const totalDelegatedToAfter = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalSupplyAfter = await iToken.totalSupply(); + const totalAssetsAfter = await iVault.totalAssets(); + console.log(`Total deposited after: ${totalDepositedAfter.format()}`); + console.log(`Total delegated after: ${totalDelegatedAfter.format()}`); + console.log(`Total delegatedTo after: ${totalDelegatedToAfter.format()}`); + console.log(`Total assets after: ${totalAssetsAfter.format()}`); + + expect(totalDepositedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0, err); + expect(totalDelegatedAfter - ((firstDeposit * e18) / ratio + totalDelegated)).to.be.closeTo(0, err); + expect(totalSupplyAfter - totalSupplyExpected).to.be.closeTo(0, err); + expect(totalAssetsAfter).to.be.lte(transactErr); + expect(await iVault.ratio()).to.be.closeTo(ratio, BigInt(arg.count) * ratioErr); + }); + }); + + //Delegate invalid params + const invalidArgs = [ + { + name: "amount is 0", + deposited: toWei(1), + amount: async () => 0n, + mVault: async () => mellowVaults[0].vaultAddress, + operator: () => iVaultOperator, + }, + { + name: "amount is greater than free balance", + deposited: toWei(10), + targetCapacityPercent: e18, + amount: async () => (await iVault.getFreeBalance()) + 1n, + mVault: async () => mellowVaults[0].vaultAddress, + operator: () => iVaultOperator, + customError: "InsufficientCapacity", + source: () => iVault, + }, + // { + // name: "unknown mellow vault", + // deposited: toWei(1), + // amount: async () => await iVault.getFreeBalance(), + // mVault: async () => mellowVaults[1].vaultAddress, + // operator: () => iVaultOperator, + // customError: "InactiveWrapper", + // source: () => mellowAdapter, + // }, + // { + // name: "mellow vault is zero address", + // deposited: toWei(1), + // amount: async () => await iVault.getFreeBalance(), + // mVault: async () => ethers.ZeroAddress, + // operator: () => iVaultOperator, + // customError: "NullParams", + // source: () => iVault, + // }, + { + name: "caller is not an operator", + deposited: toWei(1), + amount: async () => await iVault.getFreeBalance(), + mVault: async () => mellowVaults[0].vaultAddress, + operator: () => staker, + customError: "OnlyOperatorAllowed", + source: () => iVault, + }, + ]; + + invalidArgs.forEach(function (arg) { + it(`delegateToMellowVault reverts when ${arg.name}`, async function () { + if (arg.targetCapacityPercent) { + await iVault.setTargetFlashCapacity(arg.targetCapacityPercent); + } + await asset.connect(staker3).approve(await iVault.getAddress(), arg.deposited); + await iVault.connect(staker3).deposit(arg.deposited, staker3.address); + + const operator = arg.operator(); + const delegateAmount = await arg.amount(); + const mVault = await arg.mVault(); + + if (arg.customError) { + await expect( + iVault.connect(operator).delegate(await mellowAdapter.getAddress(), mVault, delegateAmount, emptyBytes), + ).to.be.revertedWithCustomError(arg.source(), arg.customError); + } else { + await expect( + iVault.connect(operator).delegate(await mellowAdapter.getAddress(), mVault, delegateAmount, emptyBytes), + ).to.be.reverted; + } + }); + }); + + it("delegateToMellowVault reverts when iVault is paused", async function () { + const amount = randomBI(18); + await iVault.connect(staker).deposit(amount, staker.address); + await iVault.pause(); + await expect( + iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes), + ).to.be.revertedWith("Pausable: paused"); + }); + + it("delegateToMellowVault reverts when mellowAdapter is paused", async function () { + if (await iVault.paused()) { + await iVault.unpause(); + } + const amount = randomBI(18); + await iVault.connect(staker).deposit(amount, staker.address); + await mellowAdapter.pause(); + + await expect( + iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes), + ).to.be.revertedWith("Pausable: paused"); + await mellowAdapter.unpause(); + }); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/deposit-withdraw.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S/deposit-withdraw.test.ts new file mode 100644 index 00000000..571695b5 --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/deposit-withdraw.test.ts @@ -0,0 +1,1627 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { stETH } from "../../data/assets/inception-vault-s"; +import { vaults } from "../../data/vaults"; +import { + calculateRatio, + e18, + randomAddress, + randomBI, + randomBIMax, + toWei, +} from "../../helpers/utils"; +import { adapters, emptyBytes } from "../../src/constants"; +import { initVault, MAX_TARGET_PERCENT } from "../../src/init-vault"; + +const { ethers, network } = hardhat; +const assetData = stETH; +const mellowVaults = vaults.mellow; + +describe(`Inception Symbiotic Vault ${assetData.assetName}`, function() { + let iToken, iVault, ratioFeed, asset, mellowAdapter, withdrawalQueue; + let iVaultOperator, staker, staker2, staker3, treasury; + let ratioErr, transactErr; + let snapshot; + + before(async function() { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.assetName.toLowerCase())) { + console.log(`${assetData.assetName} is not in the list, going to skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: assetData.url ? assetData.url : network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, ratioFeed, asset, iVaultOperator, mellowAdapter, withdrawalQueue } + = await initVault(assetData, { adapters: [adapters.Mellow, adapters.Symbiotic] })); + + ratioErr = assetData.ratioErr; + transactErr = assetData.transactErr; + + [, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + treasury = await iVault.treasury(); //deployer + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function() { + await iVault?.removeAllListeners(); + }); + + describe("Deposit bonus params setter and calculation", function() { + let targetCapacityPercent, MAX_PERCENT, localSnapshot; + before(async function() { + await iVault.setTargetFlashCapacity(1n); + MAX_PERCENT = await iVault.MAX_PERCENT(); + }); + + const depositBonusSegment = [ + { + fromUtilization: async () => 0n, + fromPercent: async () => await iVault.maxBonusRate(), + toUtilization: async () => await iVault.depositUtilizationKink(), + toPercent: async () => await iVault.optimalBonusRate(), + }, + { + fromUtilization: async () => await iVault.depositUtilizationKink(), + fromPercent: async () => await iVault.optimalBonusRate(), + toUtilization: async () => await iVault.MAX_PERCENT(), + toPercent: async () => await iVault.optimalBonusRate(), + }, + { + fromUtilization: async () => await iVault.MAX_PERCENT(), + fromPercent: async () => 0n, + toUtilization: async () => ethers.MaxUint256, + toPercent: async () => 0n, + }, + ]; + + const args = [ + { + name: "Normal bonus rewards profile > 0", + newMaxBonusRate: BigInt(2 * 10 ** 8), //2% + newOptimalBonusRate: BigInt(0.2 * 10 ** 8), //0.2% + newDepositUtilizationKink: BigInt(25 * 10 ** 8), //25% + }, + { + name: "Optimal utilization = 0 => always optimal rate", + newMaxBonusRate: BigInt(2 * 10 ** 8), + newOptimalBonusRate: BigInt(10 ** 8), //1% + newDepositUtilizationKink: 0n, + }, + { + name: "Optimal bonus rate = 0", + newMaxBonusRate: BigInt(2 * 10 ** 8), + newOptimalBonusRate: 0n, + newDepositUtilizationKink: BigInt(25 * 10 ** 8), + }, + { + name: "Optimal bonus rate = max > 0 => rate is constant over utilization", + newMaxBonusRate: BigInt(2 * 10 ** 8), + newOptimalBonusRate: BigInt(2 * 10 ** 8), + newDepositUtilizationKink: BigInt(25 * 10 ** 8), + }, + { + name: "Optimal bonus rate = max = 0 => no bonus", + newMaxBonusRate: 0n, + newOptimalBonusRate: 0n, + newDepositUtilizationKink: BigInt(25 * 10 ** 8), + }, + //Will fail when OptimalBonusRate > MaxBonusRate + ]; + + const amounts = [ + { + name: "min amount from 0", + flashCapacity: targetCapacity => 0n, + amount: async () => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, + }, + { + name: "1 wei from 0", + flashCapacity: targetCapacity => 0n, + amount: async () => 1n, + }, + { + name: "from 0 to 25% of TARGET", + flashCapacity: targetCapacity => 0n, + amount: async () => (targetCapacityPercent * 25n) / 100n, + }, + { + name: "from 0 to 25% + 1wei of TARGET", + flashCapacity: targetCapacity => 0n, + amount: async () => (targetCapacityPercent * 25n) / 100n, + }, + { + name: "from 25% to 100% of TARGET", + flashCapacity: targetCapacity => (targetCapacity * 25n) / 100n, + amount: async () => (targetCapacityPercent * 75n) / 100n, + }, + { + name: "from 0% to 100% of TARGET", + flashCapacity: targetCapacity => 0n, + amount: async () => targetCapacityPercent, + }, + { + name: "from 0% to 200% of TARGET", + flashCapacity: targetCapacity => 0n, + amount: async () => targetCapacityPercent * 2n, + }, + ]; + + args.forEach(function(arg) { + it(`setDepositBonusParams: ${arg.name}`, async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await expect( + iVault.setDepositBonusParams(arg.newMaxBonusRate, arg.newOptimalBonusRate, arg.newDepositUtilizationKink), + ) + .to.emit(iVault, "DepositBonusParamsChanged") + .withArgs(arg.newMaxBonusRate, arg.newOptimalBonusRate, arg.newDepositUtilizationKink); + expect(await iVault.maxBonusRate()).to.be.eq(arg.newMaxBonusRate); + expect(await iVault.optimalBonusRate()).to.be.eq(arg.newOptimalBonusRate); + expect(await iVault.depositUtilizationKink()).to.be.eq(arg.newDepositUtilizationKink); + localSnapshot = await helpers.takeSnapshot(); + }); + + amounts.forEach(function(amount) { + it(`calculateDepositBonus for ${amount.name}`, async function() { + await localSnapshot.restore(); + const deposited = toWei(100); + targetCapacityPercent = e18; + const targetCapacity = (deposited * targetCapacityPercent) / MAX_TARGET_PERCENT; + await iVault.connect(staker).deposit(deposited, staker.address); + let flashCapacity = amount.flashCapacity(targetCapacity); + await iVault + .connect(iVaultOperator) + .delegate( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + deposited - flashCapacity - 1n, + emptyBytes, + ); + await iVault.setTargetFlashCapacity(targetCapacityPercent); //1% + console.log(`Flash capacity:\t\t${await iVault.getFlashCapacity()}`); + + let _amount = await amount.amount(); + let depositBonus = 0n; + while (_amount > 0n) { + for (const feeFunc of depositBonusSegment) { + const utilization = (flashCapacity * MAX_PERCENT) / targetCapacity; + const fromUtilization = await feeFunc.fromUtilization(); + const toUtilization = await feeFunc.toUtilization(); + if (_amount > 0n && fromUtilization <= utilization && utilization < toUtilization) { + const fromPercent = await feeFunc.fromPercent(); + const toPercent = await feeFunc.toPercent(); + const upperBound = (toUtilization * targetCapacityPercent) / MAX_PERCENT; + const replenished = upperBound > flashCapacity + _amount ? _amount : upperBound - flashCapacity; + const slope = ((toPercent - fromPercent) * MAX_PERCENT) / (toUtilization - fromUtilization); + const bonusPercent = fromPercent + (slope * (flashCapacity + replenished / 2n)) / targetCapacityPercent; + const bonus = (replenished * bonusPercent) / MAX_PERCENT; + console.log(`Replenished:\t\t\t${replenished.format()}`); + console.log(`Bonus percent:\t\t\t${bonusPercent.format()}`); + console.log(`Bonus:\t\t\t\t\t${bonus.format()}`); + flashCapacity += replenished; + _amount -= replenished; + depositBonus += bonus; + } + } + } + const contractBonus = await iVault.calculateDepositBonus(await amount.amount()); + console.log(`Expected deposit bonus:\t${depositBonus.format()}`); + console.log(`Contract deposit bonus:\t${contractBonus.format()}`); + expect(contractBonus).to.be.closeTo(depositBonus, 1n); + }); + }); + }); + + const invalidArgs = [ + { + name: "MaxBonusRate > MAX_PERCENT", + newMaxBonusRate: () => MAX_PERCENT + 1n, + newOptimalBonusRate: () => BigInt(0.2 * 10 ** 8), //0.2% + newDepositUtilizationKink: () => BigInt(25 * 10 ** 8), + customError: "ParameterExceedsLimits", + }, + { + name: "OptimalBonusRate > MAX_PERCENT", + newMaxBonusRate: () => BigInt(2 * 10 ** 8), + newOptimalBonusRate: () => MAX_PERCENT + 1n, + newDepositUtilizationKink: () => BigInt(25 * 10 ** 8), + customError: "ParameterExceedsLimits", + }, + { + name: "DepositUtilizationKink > MAX_PERCENT", + newMaxBonusRate: () => BigInt(2 * 10 ** 8), + newOptimalBonusRate: () => BigInt(0.2 * 10 ** 8), //0.2% + newDepositUtilizationKink: () => MAX_PERCENT + 1n, + customError: "ParameterExceedsLimits", + }, + { + name: "newOptimalBonusRate > newMaxBonusRate", + newMaxBonusRate: () => BigInt(0.2 * 10 ** 8), + newOptimalBonusRate: () => BigInt(2 * 10 ** 8), + newDepositUtilizationKink: () => BigInt(25 * 10 ** 8), + customError: "InconsistentData", + }, + ]; + invalidArgs.forEach(function(arg) { + it(`setDepositBonusParams reverts when ${arg.name}`, async function() { + await expect( + iVault.setDepositBonusParams( + arg.newMaxBonusRate(), + arg.newOptimalBonusRate(), + arg.newDepositUtilizationKink(), + ), + ).to.be.revertedWithCustomError(iVault, arg.customError); + }); + }); + + it("setDepositBonusParams reverts when caller is not an owner", async function() { + await expect( + iVault.connect(staker).setDepositBonusParams(BigInt(2 * 10 ** 8), BigInt(0.2 * 10 ** 8), BigInt(25 * 10 ** 8)), + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("Withdraw fee params setter and calculation", function() { + let targetCapacityPercent, MAX_PERCENT, localSnapshot; + before(async function() { + MAX_PERCENT = await iVault.MAX_PERCENT(); + }); + + const withdrawFeeSegment = [ + { + fromUtilization: async () => 0n, + fromPercent: async () => await iVault.maxFlashFeeRate(), + toUtilization: async () => await iVault.withdrawUtilizationKink(), + toPercent: async () => await iVault.optimalWithdrawalRate(), + }, + { + fromUtilization: async () => await iVault.withdrawUtilizationKink(), + fromPercent: async () => await iVault.optimalWithdrawalRate(), + toUtilization: async () => ethers.MaxUint256, + toPercent: async () => await iVault.optimalWithdrawalRate(), + }, + ]; + + const args = [ + { + name: "Normal withdraw fee profile > 0", + newMaxFlashFeeRate: BigInt(2 * 10 ** 8), //2% + newOptimalWithdrawalRate: BigInt(0.2 * 10 ** 8), //0.2% + newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), + }, + { + name: "Optimal utilization = 0 => always optimal rate", + newMaxFlashFeeRate: BigInt(2 * 10 ** 8), + newOptimalWithdrawalRate: BigInt(10 ** 8), //1% + newWithdrawUtilizationKink: 0n, + }, + { + name: "Optimal withdraw rate = 0", + newMaxFlashFeeRate: BigInt(2 * 10 ** 8), + newOptimalWithdrawalRate: 0n, + newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), + }, + { + name: "Optimal withdraw rate = max > 0 => rate is constant over utilization", + newMaxFlashFeeRate: BigInt(2 * 10 ** 8), + newOptimalWithdrawalRate: BigInt(2 * 10 ** 8), + newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), + }, + { + name: "Optimal withdraw rate = max = 0 => no fee", + newMaxFlashFeeRate: 0n, + newOptimalWithdrawalRate: 0n, + newWithdrawUtilizationKink: BigInt(25 * 10 ** 8), + }, + //Will fail when optimalWithdrawalRate > MaxFlashFeeRate + ]; + + const amounts = [ + { + name: "from 200% to 0% of TARGET", + flashCapacity: targetCapacity => targetCapacity * 2n, + amount: async () => await iVault.getFlashCapacity(), + }, + { + name: "from 100% to 0% of TARGET", + flashCapacity: targetCapacity => targetCapacity, + amount: async () => await iVault.getFlashCapacity(), + }, + { + name: "1 wei from 100%", + flashCapacity: targetCapacity => targetCapacity, + amount: async () => 1n, + }, + { + name: "min amount from 100%", + flashCapacity: targetCapacity => targetCapacity, + amount: async () => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, + }, + { + name: "from 100% to 25% of TARGET", + flashCapacity: targetCapacity => targetCapacity, + amount: async () => (targetCapacityPercent * 75n) / 100n, + }, + { + name: "from 100% to 25% - 1wei of TARGET", + flashCapacity: targetCapacity => targetCapacity, + amount: async () => (targetCapacityPercent * 75n) / 100n + 1n, + }, + { + name: "from 25% to 0% of TARGET", + flashCapacity: targetCapacity => (targetCapacity * 25n) / 100n, + amount: async () => await iVault.getFlashCapacity(), + }, + ]; + + args.forEach(function(arg) { + it(`setFlashWithdrawFeeParams: ${arg.name}`, async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await expect( + iVault.setFlashWithdrawFeeParams( + arg.newMaxFlashFeeRate, + arg.newOptimalWithdrawalRate, + arg.newWithdrawUtilizationKink, + ), + ) + .to.emit(iVault, "WithdrawFeeParamsChanged") + .withArgs(arg.newMaxFlashFeeRate, arg.newOptimalWithdrawalRate, arg.newWithdrawUtilizationKink); + + expect(await iVault.maxFlashFeeRate()).to.be.eq(arg.newMaxFlashFeeRate); + expect(await iVault.optimalWithdrawalRate()).to.be.eq(arg.newOptimalWithdrawalRate); + expect(await iVault.withdrawUtilizationKink()).to.be.eq(arg.newWithdrawUtilizationKink); + localSnapshot = await helpers.takeSnapshot(); + }); + + amounts.forEach(function(amount) { + it(`calculateFlashWithdrawFee for: ${amount.name}`, async function() { + await localSnapshot.restore(); + const deposited = toWei(100); + targetCapacityPercent = e18; + const targetCapacity = (deposited * targetCapacityPercent) / MAX_TARGET_PERCENT; + await iVault.connect(staker).deposit(deposited, staker.address); + let flashCapacity = amount.flashCapacity(targetCapacity); + await iVault + .connect(iVaultOperator) + .delegate( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + deposited - flashCapacity - 1n, + emptyBytes, + ); + await iVault.setTargetFlashCapacity(targetCapacityPercent); //1% + console.log(`Flash capacity:\t\t\t${await iVault.getFlashCapacity()}`); + + let _amount = await amount.amount(); + let withdrawFee = 0n; + while (_amount > 1n) { + for (const feeFunc of withdrawFeeSegment) { + const utilization = (flashCapacity * MAX_PERCENT) / targetCapacity; + const fromUtilization = await feeFunc.fromUtilization(); + const toUtilization = await feeFunc.toUtilization(); + if (_amount > 0n && fromUtilization < utilization && utilization <= toUtilization) { + console.log(`Utilization:\t\t\t${utilization.format()}`); + const fromPercent = await feeFunc.fromPercent(); + const toPercent = await feeFunc.toPercent(); + const lowerBound = (fromUtilization * targetCapacityPercent) / MAX_PERCENT; + const replenished = lowerBound > flashCapacity - _amount ? flashCapacity - lowerBound : _amount; + const slope = ((toPercent - fromPercent) * MAX_PERCENT) / (toUtilization - fromUtilization); + const withdrawFeePercent = + fromPercent + (slope * (flashCapacity - replenished / 2n)) / targetCapacityPercent; + const fee = (replenished * withdrawFeePercent) / MAX_PERCENT; + console.log(`Replenished:\t\t\t${replenished.format()}`); + console.log(`Fee percent:\t\t\t${withdrawFeePercent.format()}`); + console.log(`Fee:\t\t\t\t\t${fee.format()}`); + flashCapacity -= replenished; + _amount -= replenished; + withdrawFee += fee; + } + } + } + const contractFee = await iVault.calculateFlashWithdrawFee(await amount.amount()); + console.log(`Expected withdraw fee:\t${withdrawFee.format()}`); + console.log(`Contract withdraw fee:\t${contractFee.format()}`); + expect(contractFee).to.be.closeTo(withdrawFee, 1n); + expect(contractFee).to.be.gt(0n); //flashWithdraw fee is always greater than 0 + }); + }); + }); + + const invalidArgs = [ + { + name: "MaxBonusRate > MAX_PERCENT", + newMaxFlashFeeRate: () => MAX_PERCENT + 1n, + newOptimalWithdrawalRate: () => BigInt(0.2 * 10 ** 8), //0.2% + newWithdrawUtilizationKink: () => BigInt(25 * 10 ** 8), + customError: "ParameterExceedsLimits", + }, + { + name: "OptimalBonusRate > MAX_PERCENT", + newMaxFlashFeeRate: () => BigInt(2 * 10 ** 8), + newOptimalWithdrawalRate: () => MAX_PERCENT + 1n, + newWithdrawUtilizationKink: () => BigInt(25 * 10 ** 8), + customError: "ParameterExceedsLimits", + }, + { + name: "DepositUtilizationKink > MAX_PERCENT", + newMaxFlashFeeRate: () => BigInt(2 * 10 ** 8), + newOptimalWithdrawalRate: () => BigInt(0.2 * 10 ** 8), //0.2% + newWithdrawUtilizationKink: () => MAX_PERCENT + 1n, + customError: "ParameterExceedsLimits", + }, + { + name: "newOptimalWithdrawalRate > newMaxFlashFeeRate", + newMaxFlashFeeRate: () => BigInt(2 * 10 ** 8), + newOptimalWithdrawalRate: () => BigInt(3 * 10 ** 8), + newWithdrawUtilizationKink: () => BigInt(25 * 10 ** 8), + customError: "InconsistentData", + }, + ]; + invalidArgs.forEach(function(arg) { + it(`setFlashWithdrawFeeParams reverts when ${arg.name}`, async function() { + await expect( + iVault.setFlashWithdrawFeeParams( + arg.newMaxFlashFeeRate(), + arg.newOptimalWithdrawalRate(), + arg.newWithdrawUtilizationKink(), + ), + ).to.be.revertedWithCustomError(iVault, arg.customError); + }); + }); + + it("calculateFlashWithdrawFee reverts when capacity is not sufficient", async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker, staker).deposit(randomBI(19), staker.address); + const capacity = await iVault.getFlashCapacity(); + await expect(iVault.calculateFlashWithdrawFee(capacity + 1n)) + .to.be.revertedWithCustomError(iVault, "InsufficientCapacity") + .withArgs(capacity); + }); + + it("setFlashWithdrawFeeParams reverts when caller is not an owner", async function() { + await expect( + iVault + .connect(staker) + .setFlashWithdrawFeeParams(BigInt(2 * 10 ** 8), BigInt(0.2 * 10 ** 8), BigInt(25 * 10 ** 8)), + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); + + describe("Deposit: user can restake asset", function() { + let ratio; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker3).deposit(e18, staker3.address); + const amount = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes); + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + ratio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [ratio]); + console.log(`Initial ratio: ${ratio.format()}`); + }); + + afterEach(async function() { + if (await iVault.paused()) { + await iVault.unpause(); + } + }); + + it("maxDeposit: returns max amount that can be delegated to strategy", async function() { + expect(await iVault.maxDeposit(staker.address)).to.equal(2n ** 256n - 1n); + }); + + const args = [ + { + amount: async () => 4798072939323319141n, + receiver: () => staker.address, + }, + { + amount: async () => 999999999999999999n, + receiver: () => ethers.Wallet.createRandom().address, + }, + { + amount: async () => 888888888888888888n, + receiver: () => staker.address, + }, + { + amount: async () => 777777777777777777n, + receiver: () => staker.address, + }, + { + amount: async () => 666666666666666666n, + receiver: () => staker.address, + }, + { + amount: async () => 555555555555555555n, + receiver: () => staker.address, + }, + { + amount: async () => 444444444444444444n, + receiver: () => staker.address, + }, + { + amount: async () => 333333333333333333n, + receiver: () => staker.address, + }, + { + amount: async () => 222222222222222222n, + receiver: () => staker.address, + }, + { + amount: async () => 111111111111111111n, + receiver: () => staker.address, + }, + { + amount: async () => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, + receiver: () => staker.address, + }, + ]; + + args.forEach(function(arg) { + it(`Deposit amount ${arg.amount}`, async function() { + const receiver = arg.receiver(); + const balanceBefore = await iToken.balanceOf(receiver); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + + const amount = await arg.amount(); + const convertedShares = await iVault.convertToShares(amount); + const expectedShares = (amount * (await iVault.ratio())) / e18; + + const tx = await iVault.connect(staker).deposit(amount, receiver); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(receiver); + expect(events[0].args["amount"]).to.be.closeTo(amount, transactErr); + expect(events[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); + + const balanceAfter = await iToken.balanceOf(receiver); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const ratioAfter = await iVault.ratio(); + console.log(`Ratio after: ${ratioAfter}`); + + expect(balanceAfter - balanceBefore).to.be.closeTo(expectedShares, transactErr); + expect(balanceAfter - balanceBefore).to.be.closeTo(convertedShares, transactErr); + + expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount, transactErr); + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit + expect(ratioAfter).to.be.closeTo(ratio, ratioErr); //Ratio stays the same + }); + + it(`Mint amount ${arg.amount}`, async function() { + const receiver = arg.receiver(); + const balanceBefore = await iToken.balanceOf(receiver); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + + const shares = await arg.amount(); + const convertedAmount = await iVault.convertToAssets(shares); + + const tx = await iVault.connect(staker).mint(shares, receiver); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(receiver); + expect(events[0].args["amount"]).to.be.closeTo(convertedAmount, transactErr); + expect(events[0].args["iShares"]).to.be.closeTo(shares, transactErr); + + const balanceAfter = await iToken.balanceOf(receiver); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const ratioAfter = await iVault.ratio(); + console.log(`Ratio after: ${ratioAfter}`); + + expect(balanceAfter - balanceBefore).to.be.closeTo(shares, transactErr); + expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(convertedAmount, transactErr); + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(convertedAmount, transactErr); //Everything stays on iVault after deposit + expect(ratioAfter).to.be.closeTo(ratio, ratioErr); //Ratio stays the same + }); + + it("Delegate free balance", async function() { + const delegatedBefore = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalDepositedBefore = await iVault.getTotalDeposited(); + console.log(`Delegated before: ${delegatedBefore}`); + console.log(`Total deposited before: ${totalDepositedBefore}`); + + const amount = await iVault.getFreeBalance(); + await expect( + iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes), + ) + .to.emit(iVault, "DelegatedTo") + .withArgs(mellowAdapter.address, mellowVaults[0].vaultAddress, amount); + + const delegatedAfter = await iVault.getDelegatedTo( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + ); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const ratioAfter = await iVault.ratio(); + console.log(`Ratio after: ${ratioAfter}`); + + expect(delegatedAfter - delegatedBefore).to.be.closeTo(amount, transactErr); + expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); + expect(totalAssetsAfter).to.be.lte(transactErr); + }); + }); + + it("Deposit with Referral code", async function() { + const receiver = staker; + const balanceBefore = await iToken.balanceOf(receiver); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + const amount = await toWei(1); + const convertedShares = await iVault.convertToShares(amount); + const expectedShares = (amount * (await iVault.ratio())) / e18; + const code = ethers.encodeBytes32String(randomAddress().slice(0, 8)); + const tx = await iVault.connect(staker2).depositWithReferral(amount, receiver, code); + const receipt = await tx.wait(); + let events = receipt.logs?.filter(e => { + return e.eventName === "Deposit"; + }); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker2.address); + expect(events[0].args["receiver"]).to.be.eq(receiver); + expect(events[0].args["amount"]).to.be.closeTo(amount, transactErr); + expect(events[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); + //Code event + events = receipt.logs?.filter(e => { + return e.eventName === "ReferralCode"; + }); + expect(events.length).to.be.eq(1); + expect(events[0].args["code"]).to.be.eq(code); + + const balanceAfter = await iToken.balanceOf(receiver); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + expect(balanceAfter - balanceBefore).to.be.closeTo(expectedShares, transactErr); + expect(balanceAfter - balanceBefore).to.be.closeTo(convertedShares, transactErr); + + expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount, transactErr); + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit + expect(await iVault.ratio()).to.be.closeTo(ratio, ratioErr); //Ratio stays the same + }); + + it("Deposit with Referral code and min out", async function() { + const receiver = staker; + const balanceBefore = await iToken.balanceOf(receiver); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + const amount = await toWei(1); + const convertedShares = await iVault.convertToShares(amount); + const expectedShares = (amount * (await iVault.ratio())) / e18; + const code = ethers.encodeBytes32String(randomAddress().slice(0, 8)); + + let tx = await iVault.connect(staker2)["depositWithReferral(uint256,address,bytes32,uint256)"](amount, receiver, code, toWei(0.5)); + const receipt = await tx.wait(); + + const balanceAfter = await iToken.balanceOf(receiver); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + + expect(tx).to.be.emit("Deposit"); + expect(tx).to.be.emit("ReferralCode"); + + let events = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker2.address); + expect(events[0].args["receiver"]).to.be.eq(receiver); + expect(events[0].args["amount"]).to.be.closeTo(amount, transactErr); + expect(events[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); + + events = receipt.logs?.filter(e => e.eventName === "ReferralCode"); + expect(events.length).to.be.eq(1); + expect(events[0].args["code"]).to.be.eq(code); + + expect(balanceAfter - balanceBefore).to.be.closeTo(expectedShares, transactErr); + expect(balanceAfter - balanceBefore).to.be.closeTo(convertedShares, transactErr); + expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount, transactErr); + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); + expect(await iVault.ratio()).to.be.closeTo(ratio, ratioErr); + + await expect(iVault.connect(staker2)["depositWithReferral(uint256,address,bytes32,uint256)"](amount, receiver, code, toWei(1.5))) + .to.be.revertedWithCustomError(iVault, "SlippageMinOut"); + }); + + const depositInvalidArgs = [ + { + name: "amount is 0", + amount: async () => 0n, + receiver: () => staker.address, + isCustom: true, + error: "LowerMinAmount", + }, + { + name: "amount < min", + amount: async () => (await iVault.withdrawMinAmount()) - 1n, + receiver: () => staker.address, + isCustom: true, + error: "LowerMinAmount", + }, + { + name: "to zero address", + amount: async () => randomBI(18), + isCustom: true, + receiver: () => ethers.ZeroAddress, + error: "NullParams", + }, + ]; + + depositInvalidArgs.forEach(function(arg) { + it(`Reverts when: deposit ${arg.name}`, async function() { + const amount = await arg.amount(); + const receiver = arg.receiver(); + if (arg.isCustom) { + await expect(iVault.connect(staker).deposit(amount, receiver)).to.be.revertedWithCustomError( + iVault, + arg.error, + ); + } else { + await expect(iVault.connect(staker).deposit(amount, receiver)).to.be.revertedWith(arg.error); + } + }); + }); + + it("Reverts: deposit when iVault is paused", async function() { + await iVault.pause(); + const depositAmount = randomBI(19); + await expect(iVault.connect(staker).deposit(depositAmount, staker.address)).to.be.revertedWith( + "Pausable: paused", + ); + }); + + it("Reverts: mint when iVault is paused", async function() { + await iVault.pause(); + const shares = randomBI(19); + await expect(iVault.connect(staker).mint(shares, staker.address)).to.be.revertedWith("Pausable: paused"); + }); + + it("Reverts: depositWithReferral when iVault is paused", async function() { + await iVault.pause(); + const depositAmount = randomBI(19); + const code = ethers.encodeBytes32String(randomAddress().slice(0, 8)); + await expect(iVault.connect(staker2).depositWithReferral(depositAmount, staker, code)).to.be.revertedWith( + "Pausable: paused", + ); + }); + + it("Reverts: deposit when targetCapacity is not set", async function() { + await snapshot.restore(); + const depositAmount = randomBI(19); + await expect(iVault.connect(staker).deposit(depositAmount, staker.address)).to.be.revertedWithCustomError( + iVault, + "NullParams", + ); + }); + + const convertSharesArgs = [ + { + name: "amount = 0", + amount: async () => 0n, + }, + { + name: "amount = 1", + amount: async () => 0n, + }, + { + name: "amount < min", + amount: async () => (await iVault.withdrawMinAmount()) - 1n, + }, + ]; + + convertSharesArgs.forEach(function(arg) { + it(`Convert to shares: ${arg.name}`, async function() { + const amount = await arg.amount(); + const ratio = await iVault.ratio(); + expect(await iVault.convertToShares(amount)).to.be.eq((amount * ratio) / e18); + }); + }); + + // it.skip("Max mint and deposit", async function () { + // const stakerBalance = await asset.balanceOf(staker); + // const calculatedBonus = await iVault.calculateDepositBonus(stakerBalance); + // const realBonus = await iVault.depositBonusAmount(); + // const bonus = realBonus > calculatedBonus ? calculatedBonus : realBonus; + // expect(await iVault.maxDeposit(staker)).to.be.eq(stakerBalance); + // }); + + it("Max mint and deposit when iVault is paused equal 0", async function() { + await iVault.pause(); + const maxMint = await iVault.maxMint(staker); + const maxDeposit = await iVault.maxDeposit(staker); + expect(maxMint).to.be.eq(0n); + expect(maxDeposit).to.be.eq(0n); + }); + + // it("Max mint and deposit reverts when > available amount", async function() { + // const maxMint = await iVault.maxMint(staker); + // await expect(iVault.connect(staker).mint(maxMint + 1n, staker.address)).to.be.revertedWithCustomError( + // iVault, + // "ExceededMaxMint", + // ); + // }); + }); + + describe("Deposit with bonus for replenish", function() { + const states = [ + // { + // name: "deposit bonus = 0", + // withBonus: false, + // }, + { + name: "deposit bonus > 0", + withBonus: true, + }, + ]; + + const amounts = [ + { + name: "for the first time", + predepositAmount: targetCapacity => 0n, + amount: targetCapacity => randomBIMax(targetCapacity / 4n) + targetCapacity / 4n, + receiver: () => staker.address, + }, + { + name: "more", + predepositAmount: targetCapacity => targetCapacity / 3n, + amount: targetCapacity => randomBIMax(targetCapacity / 3n), + receiver: () => staker.address, + }, + { + name: "up to target cap", + predepositAmount: targetCapacity => targetCapacity / 10n, + amount: targetCapacity => (targetCapacity * 9n) / 10n, + receiver: () => staker.address, + }, + { + name: "all rewards", + predepositAmount: targetCapacity => 0n, + amount: targetCapacity => targetCapacity, + receiver: () => staker.address, + }, + { + name: "up to target cap and above", + predepositAmount: targetCapacity => targetCapacity / 10n, + amount: targetCapacity => targetCapacity, + receiver: () => staker.address, + }, + { + name: "above target cap", + predepositAmount: targetCapacity => targetCapacity, + amount: targetCapacity => randomBI(19), + receiver: () => staker.address, + }, + ]; + + states.forEach(function(state) { + let localSnapshot; + const targetCapacityPercent = e18; + const targetCapacity = e18; + it(`---Prepare state: ${state.name}`, async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.setDepositMinAmount(1n); + const deposited = (targetCapacity * MAX_TARGET_PERCENT) / targetCapacityPercent; + if (state.withBonus) { + await iVault.setTargetFlashCapacity(targetCapacityPercent); + await iVault.connect(staker3).deposit(toWei(1.5), staker3.address); + const balanceOf = await iToken.balanceOf(staker3.address); + await iVault.connect(staker3)["flashWithdraw(uint256,address,uint256)"](balanceOf, staker3.address, 0n); + await iVault.setTargetFlashCapacity(1n); + } + + await iVault.connect(staker3).deposit(deposited, staker3.address); + console.log(`Total assets:\t\t${(await iVault.totalAssets()).format()}`); + console.log(`Deposit bonus:\t\t${(await iVault.depositBonusAmount()).format()}`); + localSnapshot = await helpers.takeSnapshot(); + }); + + // it.skip("Max mint and deposit", async function () { + // const stakerBalance = await asset.balanceOf(staker); + // const calculatedBonus = await iVault.calculateDepositBonus(stakerBalance); + // const realBonus = await iVault.depositBonusAmount(); + // const bonus = realBonus > calculatedBonus ? calculatedBonus : realBonus; + // // expect(await iVault.maxMint(staker)).to.be.eq(await iVault.convertToShares(stakerBalance + bonus)); + // expect(await iVault.maxDeposit(staker)).to.be.eq(stakerBalance); + // }); + + amounts.forEach(function(arg) { + it(`Deposit ${arg.name}`, async function() { + if (localSnapshot) { + await localSnapshot.restore(); + } else { + expect(false).to.be.true("Can not restore local snapshot"); + } + + const flashCapacityBefore = arg.predepositAmount(targetCapacity); + const freeBalance = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + freeBalance - flashCapacityBefore, + emptyBytes, + ); + await iVault.setTargetFlashCapacity(targetCapacityPercent); + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + const ratioBefore = await iVault.ratio(); + + let availableBonus = await iVault.depositBonusAmount(); + const receiver = arg.receiver(); + const stakerSharesBefore = await iToken.balanceOf(receiver); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + console.log(`Target capacity:\t\t${targetCapacity.format()}`); + console.log(`Flash capacity before:\t${flashCapacityBefore.format()}`); + + const amount = await arg.amount(targetCapacity); + console.log(`Amount:\t\t\t\t\t${amount.format()}`); + const calculatedBonus = await iVault.calculateDepositBonus(amount); + console.log(`Calculated bonus:\t\t${calculatedBonus.format()}`); + console.log(`Available bonus:\t\t${availableBonus.format()}`); + const expectedBonus = calculatedBonus <= availableBonus ? calculatedBonus : availableBonus; + availableBonus -= expectedBonus; + console.log(`Expected bonus:\t\t\t${expectedBonus.format()}`); + const convertedShares = await iVault.convertToShares(amount + expectedBonus); + const expectedShares = ((amount + expectedBonus) * (await iVault.ratio())) / e18; + const previewShares = await iVault.previewDeposit(amount); + + const tx = await iVault.connect(staker).deposit(amount, receiver); + const receipt = await tx.wait(); + const depositEvent = receipt.logs?.filter(e => e.eventName === "Deposit"); + expect(depositEvent.length).to.be.eq(1); + expect(depositEvent[0].args["sender"]).to.be.eq(staker.address); + expect(depositEvent[0].args["receiver"]).to.be.eq(receiver); + expect(depositEvent[0].args["amount"]).to.be.closeTo(amount, transactErr); + expect(depositEvent[0].args["iShares"] - expectedShares).to.be.closeTo(0, transactErr); + //DepositBonus event + expect(receipt.logs.find(l => l.eventName === "DepositBonus")?.args.amount || 0n).to.be.closeTo( + expectedBonus, + transactErr, + ); + + const stakerSharesAfter = await iToken.balanceOf(receiver); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const flashCapacityAfter = await iVault.getFlashCapacity(); + const ratioAfter = await iVault.ratio(); + console.log(`Ratio after:\t\t\t${ratioAfter.format()}`); + console.log(`Bonus after:\t\t\t${availableBonus.format()}`); + + expect(stakerSharesAfter - stakerSharesBefore).to.be.closeTo(expectedShares, transactErr); + expect(stakerSharesAfter - stakerSharesBefore).to.be.closeTo(convertedShares, transactErr); + + expect(totalDepositedAfter - totalDepositedBefore).to.be.closeTo(amount + expectedBonus, transactErr); + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(amount, transactErr); //Everything stays on iVault after deposit + expect(flashCapacityAfter).to.be.closeTo(flashCapacityBefore + amount + expectedBonus, transactErr); + expect(ratioAfter).to.be.closeTo(ratioBefore, ratioErr); //Ratio stays the same + expect(previewShares).to.be.eq(stakerSharesAfter - stakerSharesBefore); //Ratio stays the same + }); + }); + }); + }); + + describe("Withdraw: user can unstake", function() { + let ratio, totalDeposited, TARGET; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(toWei(10), staker.address); + const freeBalance = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, freeBalance, emptyBytes); + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + totalDeposited = await iVault.getTotalDeposited(); + TARGET = 1000_000n; + await iVault.setTargetFlashCapacity(TARGET); + ratio = await iVault.ratio(); + console.log(`Initial ratio: ${ratio}`); + }); + + const testData = [ + { + name: "random e18", + amount: async shares => 724399519262012598n, + receiver: () => staker.address, + }, + { + name: "999999999999999999", + amount: async shares => 999999999999999999n, + receiver: () => staker2.address, + }, + { + name: "888888888888888888", + amount: async shares => 888888888888888888n, + receiver: () => staker2.address, + }, + { + name: "777777777777777777", + amount: async shares => 777777777777777777n, + receiver: () => staker2.address, + }, + { + name: "666666666666666666", + amount: async shares => 666666666666666666n, + receiver: () => staker2.address, + }, + { + name: "555555555555555555", + amount: async shares => 555555555555555555n, + receiver: () => staker2.address, + }, + { + name: "444444444444444444", + amount: async shares => 444444444444444444n, + receiver: () => staker2.address, + }, + { + name: "333333333333333333", + amount: async shares => 333333333333333333n, + receiver: () => staker2.address, + }, + { + name: "222222222222222222", + amount: async shares => 222222222222222222n, + receiver: () => staker2.address, + }, + { + name: "111111111111111111", + amount: async shares => 111111111111111111n, + receiver: () => staker2.address, + }, + { + name: "min amount", + amount: async shares => (await iVault.convertToAssets(await iVault.withdrawMinAmount())) + 1n, + receiver: () => staker2.address, + }, + { + name: "all", + amount: async shares => shares, + receiver: () => staker2.address, + }, + ]; + + testData.forEach(function(test) { + it(`Withdraw ${test.name}`, async function() { + const ratioBefore = await iVault.ratio(); + const balanceBefore = await iToken.balanceOf(staker.address); + const amount = await test.amount(balanceBefore); + const assetValue = await iVault.convertToAssets(amount); + const stakerPWBefore = await iVault.getPendingWithdrawalOf(test.receiver()); + const withdrawalEpochBefore = await withdrawalQueue.withdrawals(await withdrawalQueue.currentEpoch()); + const totalEpochSharesBefore = withdrawalEpochBefore[1]; + + const tx = await iVault.connect(staker).withdraw(amount, test.receiver()); + const receipt = await tx.wait(); + const events = receipt.logs?.filter(e => e.eventName === "Withdraw"); + const epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + expect(events.length).to.be.eq(1); + expect(events[0].args["sender"]).to.be.eq(staker.address); + expect(events[0].args["receiver"]).to.be.eq(test.receiver()); + expect(events[0].args["owner"]).to.be.eq(staker.address); + expect(events[0].args["amount"]).to.be.closeTo(assetValue, transactErr); + expect(events[0].args["iShares"]).to.be.eq(amount); + + expect(balanceBefore - (await iToken.balanceOf(staker.address))).to.be.eq(amount); + expect((await iVault.getPendingWithdrawalOf(test.receiver())) - stakerPWBefore).to.be.closeTo( + assetValue, + transactErr, + ); + expect(epochShares - totalEpochSharesBefore).to.be.closeTo(amount, transactErr); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + expect(await iVault.ratio()).to.be.closeTo(ratioBefore, ratioErr); + }); + }); + }); + + describe("Withdraw: negative cases", function() { + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(toWei(10), staker.address); + const freeBalance = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, freeBalance, emptyBytes); + await assetData.addRewardsMellowVault(toWei(0.001), mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + }); + + const invalidData = [ + { + name: "> balance", + amount: async () => (await iToken.balanceOf(staker.address)) + 1n, + receiver: () => staker.address, + error: "ERC20: burn amount exceeds balance", + }, + { + name: "< min amount", + amount: async () => (await iVault.convertToShares(await iVault.withdrawMinAmount())) - 1n, + receiver: () => staker.address, + customError: "LowerMinAmount", + }, + { + name: "0", + amount: async () => 0n, + receiver: () => staker.address, + customError: "NullParams", + }, + { + name: "to zero address", + amount: async () => randomBI(18), + receiver: () => ethers.ZeroAddress, + customError: "InvalidAddress", + }, + ]; + + invalidData.forEach(function(test) { + it(`Reverts: withdraws ${test.name}`, async function() { + const amount = await test.amount(); + const receiver = test.receiver(); + if (test.customError) { + await expect(iVault.connect(staker).withdraw(amount, receiver)).to.be.revertedWithCustomError( + iVault, + test.customError, + ); + } else if (test.error) { + await expect(iVault.connect(staker).withdraw(amount, receiver)).to.be.revertedWith(test.error); + } + }); + }); + + it("Withdraw small amount many times", async function() { + const ratioBefore = await iVault.ratio(); + console.log(`Ratio before:\t${ratioBefore.format()}`); + + const count = 100; + const amount = await iVault.withdrawMinAmount(); + for (let i = 0; i < count; i++) { + await iVault.connect(staker).withdraw(amount, staker.address); + } + const ratioAfter = await iVault.ratio(); + console.log(`Ratio after:\t${ratioAfter.format()}`); + + expect(ratioBefore - ratioAfter).to.be.closeTo(0, count); + + await iVault.connect(staker).withdraw(e18, staker.address); + console.log(`Ratio after withdraw 1eth:\t${await iVault.ratio()}`); + expect(await iVault.ratio()).to.be.closeTo(ratioAfter, ratioErr); + }); + + it("Reverts: withdraw when iVault is paused", async function() { + await iVault.pause(); + await expect(iVault.connect(staker).withdraw(toWei(1), staker.address)).to.be.revertedWith("Pausable: paused"); + await iVault.unpause(); + }); + + it("Reverts: withdraw when targetCapacity is not set", async function() { + await snapshot.restore(); + await expect(iVault.connect(staker).withdraw(toWei(1), staker.address)).to.be.revertedWithCustomError( + iVault, + "NullParams", + ); + }); + }); + + describe("Flash withdraw with fee", function() { + const targetCapacityPercent = e18; + const targetCapacity = e18; + let deposited = 0n; + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + deposited = (targetCapacity * MAX_TARGET_PERCENT) / targetCapacityPercent; + await iVault.connect(staker3).deposit(deposited, staker.address); + const freeBalance = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, freeBalance, emptyBytes); + + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + await iVault.setTargetFlashCapacity(targetCapacityPercent); + }); + + const args = [ + { + name: "part of the free balance when pool capacity > TARGET", + poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, + amount: async () => (await iVault.getFreeBalance()) / 2n, + receiver: () => staker, + }, + { + name: "all of the free balance when pool capacity > TARGET", + poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, + amount: async () => await iVault.getFreeBalance(), + receiver: () => staker, + }, + { + name: "all when pool capacity > TARGET", + poolCapacity: targetCapacityPercent => targetCapacityPercent + e18, + amount: async () => await iVault.getFlashCapacity(), + receiver: () => staker, + }, + { + name: "partially when pool capacity = TARGET", + poolCapacity: targetCapacityPercent => targetCapacityPercent, + amount: async () => (await iVault.getFlashCapacity()) / 2n, + receiver: () => staker, + }, + { + name: "all when pool capacity = TARGET", + poolCapacity: targetCapacityPercent => targetCapacityPercent, + amount: async () => await iVault.getFlashCapacity(), + receiver: () => staker, + }, + { + name: "partially when pool capacity < TARGET", + poolCapacity: targetCapacityPercent => (targetCapacityPercent * 3n) / 4n, + amount: async () => (await iVault.getFlashCapacity()) / 2n, + receiver: () => staker, + }, + { + name: "all when pool capacity < TARGET", + poolCapacity: targetCapacityPercent => (targetCapacityPercent * 3n) / 4n, + amount: async () => await iVault.getFlashCapacity(), + receiver: () => staker, + }, + ]; + + args.forEach(function(arg) { + it(`flashWithdraw: ${arg.name}`, async function() { + //Undelegate from Mellow + const undelegatePercent = arg.poolCapacity(targetCapacityPercent); + const undelegateAmount = (deposited * undelegatePercent) / MAX_TARGET_PERCENT; + await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, undelegateAmount); + //flashWithdraw + const ratioBefore = await iVault.ratio(); + console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); + + const sharesBefore = await iToken.balanceOf(staker); + const assetBalanceBefore = await asset.balanceOf(staker); + const treasuryBalanceBefore = await asset.balanceOf(treasury); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + const flashCapacityBefore = await iVault.getFlashCapacity(); + const freeBalanceBefore = await iVault.getFreeBalance(); + console.log(`flashCapacityBefore:\t${flashCapacityBefore.format()}`); + console.log(`freeBalanceBefore:\t\t${freeBalanceBefore.format()}`); + + const amount = await arg.amount(); + const shares = await iVault.convertToShares(amount); + const receiver = await arg.receiver(); + const expectedFee = await iVault.calculateFlashWithdrawFee(amount); + console.log(`Expected fee:\t\t\t${expectedFee.format()}`); + + const tx = await iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](shares, receiver.address, 0n); + const receipt = await tx.wait(); + const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); + expect(withdrawEvent.length).to.be.eq(1); + expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); + expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); + expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); + const fee = withdrawEvent[0].args["fee"]; + expect(fee).to.be.closeTo(expectedFee, transactErr); + + const sharesAfter = await iToken.balanceOf(staker); + const assetBalanceAfter = await asset.balanceOf(staker); + const treasuryBalanceAfter = await asset.balanceOf(treasury); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const flashCapacityAfter = await iVault.getFlashCapacity(); + console.log(`Balance diff:\t\t\t${(sharesBefore - sharesAfter).format()}`); + console.log(`TotalDeposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); + console.log(`TotalAssets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); + console.log(`FlashCapacity diff:\t\t${(flashCapacityBefore - flashCapacityAfter).format()}`); + console.log(`Fee:\t\t\t\t\t${fee.format()}`); + + expect(sharesBefore - sharesAfter).to.be.eq(shares); + expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); + expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); + expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); + }); + + it(`flashWithdraw without slippage: ${arg.name}`, async function() { + //Undelegate from Mellow + const undelegatePercent = arg.poolCapacity(targetCapacityPercent); + const undelegateAmount = (deposited * undelegatePercent) / MAX_TARGET_PERCENT; + await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, undelegateAmount); + //flashWithdraw + const ratioBefore = await iVault.ratio(); + console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); + + const sharesBefore = await iToken.balanceOf(staker); + const assetBalanceBefore = await asset.balanceOf(staker); + const treasuryBalanceBefore = await asset.balanceOf(treasury); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + const flashCapacityBefore = await iVault.getFlashCapacity(); + const freeBalanceBefore = await iVault.getFreeBalance(); + console.log(`flashCapacityBefore:\t${flashCapacityBefore.format()}`); + console.log(`freeBalanceBefore:\t\t${freeBalanceBefore.format()}`); + + const amount = await arg.amount(); + const shares = await iVault.convertToShares(amount); + const receiver = await arg.receiver(); + const expectedFee = await iVault.calculateFlashWithdrawFee(amount); + console.log(`Expected fee:\t\t\t${expectedFee.format()}`); + + const tx = await iVault.connect(staker)["flashWithdraw(uint256,address)"](shares, receiver.address); + const receipt = await tx.wait(); + const withdrawEvent = receipt.logs?.filter(e => e.eventName === "FlashWithdraw"); + expect(withdrawEvent.length).to.be.eq(1); + expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); + expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); + expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); + const fee = withdrawEvent[0].args["fee"]; + expect(fee).to.be.closeTo(expectedFee, transactErr); + + const sharesAfter = await iToken.balanceOf(staker); + const assetBalanceAfter = await asset.balanceOf(staker); + const treasuryBalanceAfter = await asset.balanceOf(treasury); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const flashCapacityAfter = await iVault.getFlashCapacity(); + console.log(`Balance diff:\t\t\t${(sharesBefore - sharesAfter).format()}`); + console.log(`TotalDeposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); + console.log(`TotalAssets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); + console.log(`FlashCapacity diff:\t\t${(flashCapacityBefore - flashCapacityAfter).format()}`); + console.log(`Fee:\t\t\t\t\t${fee.format()}`); + + expect(sharesBefore - sharesAfter).to.be.eq(shares); + expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); + expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); + expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); + }); + + it(`redeem(shares,receiver,owner): ${arg.name}`, async function() { + //Undelegate from Mellow + const undelegatePercent = arg.poolCapacity(targetCapacityPercent); + const undelegateAmount = (deposited * undelegatePercent) / MAX_TARGET_PERCENT; + await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, undelegateAmount); + + //flashWithdraw + const ratioBefore = await iVault.ratio(); + console.log(`Ratio before:\t\t\t${ratioBefore.format()}`); + + const sharesBefore = await iToken.balanceOf(staker); + const assetBalanceBefore = await asset.balanceOf(staker); + const treasuryBalanceBefore = await asset.balanceOf(treasury); + const totalDepositedBefore = await iVault.getTotalDeposited(); + const totalAssetsBefore = await iVault.totalAssets(); + const flashCapacityBefore = await iVault.getFlashCapacity(); + const freeBalanceBefore = await iVault.getFreeBalance(); + console.log(`flashCapacityBefore:\t${flashCapacityBefore.format()}`); + console.log(`freeBalanceBefore:\t\t${freeBalanceBefore.format()}`); + + const amount = await arg.amount(); + const shares = await iVault.convertToShares(amount); //+1 to compensate rounding after converting from shares to amount + const previewAmount = await iVault.previewRedeem(shares); + const receiver = await arg.receiver(); + const expectedFee = await iVault.calculateFlashWithdrawFee(amount); + console.log(`Expected fee:\t\t\t${expectedFee.format()}`); + + const tx = await iVault.connect(staker)["redeem(uint256,address,address)"](shares, receiver.address, staker.address); + const receipt = await tx.wait(); + const withdrawEvent = receipt.logs?.filter(e => e.eventName === "Withdraw"); + expect(withdrawEvent.length).to.be.eq(1); + expect(withdrawEvent[0].args["sender"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["receiver"]).to.be.eq(receiver.address); + expect(withdrawEvent[0].args["owner"]).to.be.eq(staker.address); + expect(withdrawEvent[0].args["amount"]).to.be.closeTo(amount - expectedFee, transactErr); + expect(withdrawEvent[0].args["iShares"]).to.be.closeTo(shares, transactErr); + const feeEvent = receipt.logs?.filter(e => e.eventName === "WithdrawalFee"); + const fee = feeEvent[0].args["fee"]; + expect(fee).to.be.closeTo(expectedFee, transactErr); + + const sharesAfter = await iToken.balanceOf(staker); + const assetBalanceAfter = await asset.balanceOf(staker); + const treasuryBalanceAfter = await asset.balanceOf(treasury); + const totalDepositedAfter = await iVault.getTotalDeposited(); + const totalAssetsAfter = await iVault.totalAssets(); + const flashCapacityAfter = await iVault.getFlashCapacity(); + console.log(`Balance diff:\t\t\t${(sharesBefore - sharesAfter).format()}`); + console.log(`TotalDeposited diff:\t${(totalDepositedBefore - totalDepositedAfter).format()}`); + console.log(`TotalAssets diff:\t\t${(totalAssetsBefore - totalAssetsAfter).format()}`); + console.log(`FlashCapacity diff:\t\t${(flashCapacityBefore - flashCapacityAfter).format()}`); + console.log(`Fee:\t\t\t\t\t${fee.format()}`); + + expect(sharesBefore - sharesAfter).to.be.eq(shares); + expect(assetBalanceAfter - assetBalanceBefore).to.be.closeTo(amount - expectedFee, 2n); + expect(treasuryBalanceAfter - treasuryBalanceBefore).to.be.closeTo(expectedFee / 2n, 2n); + expect(totalDepositedBefore - totalDepositedAfter).to.be.closeTo(amount, transactErr); + expect(totalAssetsBefore - totalAssetsAfter).to.be.closeTo(amount - expectedFee / 2n, transactErr); + expect(flashCapacityBefore - flashCapacityAfter).to.be.closeTo(amount, transactErr); + expect(previewAmount).to.be.eq(assetBalanceAfter - assetBalanceBefore); + }); + }); + + it("Reverts when capacity is not sufficient", async function() { + const shares = await iToken.balanceOf(staker.address); + const capacity = await iVault.getFlashCapacity(); + await expect(iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](shares, staker.address, 0n)) + .to.be.revertedWithCustomError(iVault, "InsufficientCapacity") + .withArgs(capacity); + }); + + it("Reverts when amount < min", async function() { + const withdrawMinAmount = await iVault.withdrawMinAmount(); + const shares = (await iVault.convertToShares(withdrawMinAmount)) - 1n; + await expect(iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](shares, staker.address, 0n)) + .to.be.revertedWithCustomError(iVault, "LowerMinAmount") + .withArgs(withdrawMinAmount); + }); + + it("Reverts redeem when owner != message sender", async function() { + await iVault.connect(staker).deposit(e18, staker.address); + const amount = await iVault.getFlashCapacity(); + await expect( + iVault.connect(staker)["redeem(uint256,address,address)"](amount, staker.address, staker2.address), + ).to.be.revertedWithCustomError(iVault, "MsgSenderIsNotOwner"); + }); + + it("Reverts when iVault is paused", async function() { + await iVault.connect(staker).deposit(e18, staker.address); + await iVault.pause(); + const amount = await iVault.getFlashCapacity(); + await expect(iVault.connect(staker)["flashWithdraw(uint256,address,uint256)"](amount, staker.address, 0n)).to.be.revertedWith( + "Pausable: paused", + ); + await expect( + iVault.connect(staker)["redeem(uint256,address,address)"](amount, staker.address, staker.address), + ).to.be.revertedWith("Pausable: paused"); + await iVault.unpause(); + }); + }); + + describe("Max redeem", function() { + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker3).deposit(randomBI(18), staker3.address); + const freeBalance = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, freeBalance / 2n, emptyBytes); + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + }); + + const args = [ + { + name: "User amount = 0", + sharesOwner: () => ethers.Wallet.createRandom(), + maxRedeem: async () => 0n, + }, + { + name: "User amount < flash capacity", + sharesOwner: () => staker, + deposited: randomBI(18), + maxRedeem: async () => await iToken.balanceOf(staker), + }, + { + name: "User amount = flash capacity", + sharesOwner: () => staker, + deposited: randomBI(18), + delegated: async deposited => (await iVault.totalAssets()) - deposited, + maxRedeem: async () => await iToken.balanceOf(staker), + }, + { + name: "User amount > flash capacity > 0", + sharesOwner: () => staker, + deposited: randomBI(18), + delegated: async deposited => (await iVault.totalAssets()) - randomBI(17), + maxRedeem: async () => await iVault.convertToShares(await iVault.getFlashCapacity()), + }, + { + name: "User amount > flash capacity = 0", + sharesOwner: () => staker3, + delegated: async deposited => await iVault.totalAssets(), + maxRedeem: async () => 0n, + }, + ]; + + async function prepareState(arg) { + const sharesOwner = arg.sharesOwner(); + console.log(sharesOwner.address); + if (arg.deposited) { + await iVault.connect(sharesOwner).deposit(arg.deposited, sharesOwner.address); + } + + if (arg.delegated) { + const delegated = await arg.delegated(arg.deposited); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, delegated, emptyBytes); + } + return sharesOwner; + } + + args.forEach(function(arg) { + it(`maxReedem: ${arg.name}`, async function() { + const sharesOwner = await prepareState(arg); + + const maxRedeem = await iVault.maxRedeem(sharesOwner); + const expectedMaxRedeem = await arg.maxRedeem(); + + console.log(`User shares:\t\t${(await iToken.balanceOf(sharesOwner)).format()}`); + console.log(`flashCapacity:\t\t${(await iVault.convertToShares(await iVault.getFlashCapacity())).format()}`); + console.log(`total assets:\t\t${await iVault.totalAssets()}`); + console.log(`maxRedeem:\t\t\t${maxRedeem.format()}`); + console.log(`expected Redeem:\t${expectedMaxRedeem.format()}`); + + if (maxRedeem > 0n) { + await iVault.connect(sharesOwner)["redeem( uint256 shares, address receiver, address owner )"](maxRedeem, sharesOwner.address, sharesOwner.address); + } + expect(maxRedeem).to.be.closeTo(expectedMaxRedeem, ratioErr); + }); + }); + + it("Reverts when iVault is paused", async function() { + await iVault.connect(staker).deposit(e18, staker.address); + await iVault.pause(); + expect(await iVault.maxRedeem(staker)).to.be.eq(0n); + }); + }); + + describe("Deposit slippage", function() { + it("Deposited less shares than min out", async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await expect( + iVault.connect(staker)["deposit(uint256,address,uint256)"](toWei(1), staker.address, toWei(100)), + ).to.be.revertedWithCustomError(iVault, "SlippageMinOut"); + }); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/getters-setters.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S/getters-setters.test.ts new file mode 100644 index 00000000..377168fd --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/getters-setters.test.ts @@ -0,0 +1,400 @@ +// Tests for InceptionVault_S contract; +// The S in name does not mean only Symbiotic; this file contains tests for Symbiotic and Mellow adapters + +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { stETH } from "../../data/assets/inception-vault-s"; +import { vaults } from "../../data/vaults"; +import { + e18, randomAddress, + randomBI, + toWei, +} from "../../helpers/utils"; +import { adapters, emptyBytes } from "../../src/constants"; +import { initVault, MAX_TARGET_PERCENT } from "../../src/init-vault"; +import { ZeroAddress } from "ethers"; + +const { ethers, network } = hardhat; +const mellowVaults = vaults.mellow; + +const assetData = stETH; +describe(`Inception Symbiotic Vault ${assetData.assetName}`, function() { + let iVault, asset, mellowAdapter, symbioticAdapter, withdrawalQueue; + let iVaultOperator, deployer, staker, staker2, staker3, treasury; + let snapshot; + + before(async function() { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.assetName.toLowerCase())) { + console.log(`${assetData.assetName} is not in the list, going to skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [{ + forking: { + jsonRpcUrl: network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }]); + + ({ iVault, asset, iVaultOperator, mellowAdapter, symbioticAdapter, withdrawalQueue } + = await initVault(assetData, { adapters: [adapters.Mellow, adapters.Symbiotic] })); + + [deployer, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + treasury = await iVault.treasury(); //deployer + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function() { + await iVault?.removeAllListeners(); + }); + + describe("iVault getters and setters", function() { + beforeEach(async function() { + await snapshot.restore(); + }); + + it("Assset", async function() { + expect(await iVault.asset()).to.be.eq(asset.address); + }); + + it("Default epoch", async function() { + expect(await withdrawalQueue.currentEpoch()).to.be.eq(1n); + }); + + it("setTreasuryAddress(): only owner can", async function() { + const treasury = await iVault.treasury(); + const newTreasury = ethers.Wallet.createRandom().address; + + await expect(iVault.setTreasuryAddress(newTreasury)) + .to.emit(iVault, "TreasuryChanged") + .withArgs(treasury, newTreasury); + expect(await iVault.treasury()).to.be.eq(newTreasury); + }); + + it("setTreasuryAddress(): reverts when set to zero address", async function() { + await expect(iVault.setTreasuryAddress(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("setTreasuryAddress(): reverts when caller is not an operator", async function() { + await expect(iVault.connect(staker).setTreasuryAddress(staker2.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("setOperator(): only owner can", async function() { + const newOperator = staker2; + await expect(iVault.setOperator(newOperator.address)) + .to.emit(iVault, "OperatorChanged") + .withArgs(iVaultOperator.address, newOperator); + + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(toWei(2), staker.address); + const amount = await iVault.getFreeBalance(); + await iVault + .connect(newOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes); + }); + + it("setOperator(): reverts when set to zero address", async function() { + await expect(iVault.setOperator(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("setOperator(): reverts when caller is not an operator", async function() { + await expect(iVault.connect(staker).setOperator(staker2.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("setRatioFeed(): only owner can", async function() { + const ratioFeed = await iVault.ratioFeed(); + const newRatioFeed = ethers.Wallet.createRandom().address; + await expect(iVault.setRatioFeed(newRatioFeed)) + .to.emit(iVault, "RatioFeedChanged") + .withArgs(ratioFeed, newRatioFeed); + expect(await iVault.ratioFeed()).to.be.eq(newRatioFeed); + }); + + it("setRatioFeed(): reverts when new value is zero address", async function() { + await expect(iVault.setRatioFeed(ethers.ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("setRatioFeed(): reverts when caller is not an owner", async function() { + const newRatioFeed = ethers.Wallet.createRandom().address; + await expect(iVault.connect(staker).setRatioFeed(newRatioFeed)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("setWithdrawMinAmount(): only owner can", async function() { + const prevValue = await iVault.withdrawMinAmount(); + const newMinAmount = randomBI(3); + await expect(iVault.setWithdrawMinAmount(newMinAmount)) + .to.emit(iVault, "WithdrawMinAmountChanged") + .withArgs(prevValue, newMinAmount); + expect(await iVault.withdrawMinAmount()).to.be.eq(newMinAmount); + }); + + it("setWithdrawMinAmount(): another address can not", async function() { + await expect(iVault.connect(staker).setWithdrawMinAmount(randomBI(3))).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("setWithdrawMinAmount(): error if try to set 0", async function() { + await expect(iVault.setWithdrawMinAmount(0)).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("setWithdrawalQueue(): only owner can", async function() { + await expect(iVault.connect(staker).setWithdrawalQueue(ZeroAddress)).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("setWithdrawalQueue(): reverts when zero address", async function() { + await expect(iVault.setWithdrawalQueue(ZeroAddress)).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("setName(): only owner can", async function() { + const prevValue = await iVault.name(); + const newValue = "New name"; + await expect(iVault.setName(newValue)).to.emit(iVault, "NameChanged").withArgs(prevValue, newValue); + expect(await iVault.name()).to.be.eq(newValue); + }); + + it("setName(): reverts when name is blank", async function() { + await expect(iVault.setName("")).to.be.revertedWithCustomError(iVault, "NullParams"); + }); + + it("setName(): another address can not", async function() { + await expect(iVault.connect(staker).setName("New name")).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("pause(): only owner can", async function() { + expect(await iVault.paused()).is.false; + await iVault.pause(); + expect(await iVault.paused()).is.true; + }); + + it("pause(): another address can not", async function() { + await expect(iVault.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("pause(): reverts when already paused", async function() { + await iVault.pause(); + await expect(iVault.pause()).to.be.revertedWith("Pausable: paused"); + }); + + it("unpause(): only owner can", async function() { + await iVault.pause(); + expect(await iVault.paused()).is.true; + + await iVault.unpause(); + expect(await iVault.paused()).is.false; + }); + + it("unpause(): another address can not", async function() { + await iVault.pause(); + expect(await iVault.paused()).is.true; + await expect(iVault.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("setTargetFlashCapacity(): only owner can", async function() { + const prevValue = await iVault.targetCapacity(); + const newValue = randomBI(18); + await expect(iVault.connect(deployer).setTargetFlashCapacity(newValue)) + .to.emit(iVault, "TargetCapacityChanged") + .withArgs(prevValue, newValue); + expect(await iVault.targetCapacity()).to.be.eq(newValue); + }); + + it("setTargetFlashCapacity(): reverts when caller is not an owner", async function() { + const newValue = randomBI(18); + await expect(iVault.connect(staker).setTargetFlashCapacity(newValue)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("setTargetFlashCapacity(): reverts when set to 0", async function() { + await expect(iVault.connect(deployer).setTargetFlashCapacity(0n)).to.revertedWithCustomError( + iVault, + "InvalidTargetFlashCapacity", + ); + }); + + it("setTargetFlashCapacity(): reverts when set to 0", async function() { + await expect(iVault.connect(deployer).setTargetFlashCapacity(MAX_TARGET_PERCENT + 1n)).to.revertedWithCustomError( + iVault, + "MoreThanMax", + ); + }); + + it("setProtocolFee(): sets share of flashWithdrawFee that goes to treasury", async function() { + const prevValue = await iVault.protocolFee(); + const newValue = randomBI(10); + + await expect(iVault.setProtocolFee(newValue)).to.emit(iVault, "ProtocolFeeChanged").withArgs(prevValue, newValue); + expect(await iVault.protocolFee()).to.be.eq(newValue); + }); + + it("setProtocolFee(): reverts when > MAX_PERCENT", async function() { + const newValue = (await iVault.MAX_PERCENT()) + 1n; + await expect(iVault.setProtocolFee(newValue)) + .to.be.revertedWithCustomError(iVault, "ParameterExceedsLimits") + .withArgs(newValue); + }); + + it("setProtocolFee(): reverts when caller is not an owner", async function() { + const newValue = randomBI(10); + await expect(iVault.connect(staker).setProtocolFee(newValue)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("redeem not available while paused", async function() { + await iVault.pause(); + await expect(iVault.connect(staker)["redeem(address,uint256)"](ZeroAddress, 0n)).to.be.revertedWith("Pausable: paused"); + }); + + it("deposit not available while paused", async function() { + await iVault.pause(); + await expect(iVault.connect(staker)["deposit(uint256,address)"](0n, ZeroAddress)) + .to.be.revertedWith("Pausable: paused"); + await expect(iVault.connect(staker)["deposit(uint256,address,uint256)"](0n, ZeroAddress, 0n)) + .to.be.revertedWith("Pausable: paused"); + await expect(iVault.connect(staker)["depositWithReferral(uint256,address,bytes32,uint256)"](0n, ZeroAddress, ethers.encodeBytes32String(randomAddress().slice(0, 8)), 0n)) + .to.be.revertedWith("Pausable: paused"); + }); + + it("Reverts: previewDeposit when asset less than depositMinAmount", async function() { + await iVault.setDepositMinAmount(100n); + await expect(iVault.connect(staker).previewDeposit(10n)) + .to.be.revertedWithCustomError(iVault, "LowerMinAmount"); + }); + + it("Reverts: previewMint when asset less than depositMinAmount", async function() { + await iVault.setDepositMinAmount(100n); + await expect(iVault.connect(staker).previewMint(10n)) + .to.be.revertedWithCustomError(iVault, "LowerMinAmount"); + }); + }); + + describe("Mellow adapter getters and setters", function() { + beforeEach(async function() { + await snapshot.restore(); + }); + + it("delegateMellow reverts when called by not a trustee", async function() { + await asset.connect(staker).approve(mellowAdapter.address, e18); + + let time = await helpers.time.latest(); + await expect( + mellowAdapter.connect(staker).delegate(mellowVaults[0].vaultAddress, randomBI(9), emptyBytes), + ).to.revertedWithCustomError(mellowAdapter, "NotVaultOrTrusteeManager"); + }); + + it("delegateMellow reverts when called by not a trustee", async function() { + await asset.connect(staker).approve(mellowAdapter.address, e18); + + let time = await helpers.time.latest(); + await expect( + mellowAdapter.connect(staker).delegate(mellowVaults[0].vaultAddress, randomBI(9), emptyBytes), + ).to.revertedWithCustomError(mellowAdapter, "NotVaultOrTrusteeManager"); + }); + + it("delegate reverts when called by not a trustee", async function() { + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(e18, staker.address); + await mellowAdapter.changeAllocation(mellowVaults[0].vaultAddress, 1n); + + let time = await helpers.time.latest(); + await expect( + mellowAdapter + .connect(staker) + .delegate(mellowVaults[0].vaultAddress, randomBI(9), [ + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + ]), + ).to.revertedWithCustomError(mellowAdapter, "NotVaultOrTrusteeManager"); + }); + + it("withdrawMellow reverts when called by not a trustee", async function() { + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(randomBI(19), staker.address); + const delegated = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, delegated, emptyBytes); + + await expect( + mellowAdapter.connect(staker).withdraw(mellowVaults[0].vaultAddress, delegated, emptyBytes, false), + ).to.revertedWithCustomError(mellowAdapter, "NotVaultOrTrusteeManager"); + }); + + it("claimMellowWithdrawalCallback reverts when called by not a trustee", async function() { + await asset.connect(staker).transfer(mellowAdapter.address, e18); + + await expect(mellowAdapter.connect(staker).claim(emptyBytes, false)).to.revertedWithCustomError( + mellowAdapter, + "NotVaultOrTrusteeManager", + ); + }); + + it("getVersion", async function() { + expect(await mellowAdapter.getVersion()).to.be.eq(3n); + }); + + it("setVault(): only owner can", async function() { + const prevValue = iVault.address; + const newValue = await symbioticAdapter.getAddress(); + + await expect(mellowAdapter.setInceptionVault(newValue)) + .to.emit(mellowAdapter, "InceptionVaultSet") + .withArgs(prevValue, newValue); + }); + + it("setVault(): reverts when caller is not an owner", async function() { + await expect(mellowAdapter.connect(staker).setInceptionVault(staker.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("setTrusteeManager(): only owner can", async function() { + const prevValue = iVaultOperator.address; + const newValue = staker.address; + + await expect(mellowAdapter.setTrusteeManager(newValue)) + .to.emit(mellowAdapter, "TrusteeManagerSet") + .withArgs(prevValue, newValue); + + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(randomBI(19), staker.address); + const delegated = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, delegated, emptyBytes); + + await mellowAdapter.connect(staker).withdraw(mellowVaults[0].vaultAddress, delegated - 1n, emptyBytes, false); + }); + + it("setTrusteeManager(): reverts when caller is not an owner", async function() { + await expect(mellowAdapter.connect(staker).setTrusteeManager(staker.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("pause(): reverts when caller is not an owner", async function() { + await expect(mellowAdapter.connect(staker).pause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("unpause(): reverts when caller is not an owner", async function() { + await mellowAdapter.pause(); + await expect(mellowAdapter.connect(staker).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); + }); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/mellow.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S/mellow.test.ts new file mode 100644 index 00000000..e8ff40c8 --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/mellow.test.ts @@ -0,0 +1,1022 @@ +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { stETH } from "../../data/assets/inception-vault-s"; +import { vaults } from "../../data/vaults"; +import { + calculateRatio, + e18, + randomAddress, + randomBI, + toWei, +} from "../../helpers/utils"; +import { adapters, emptyBytes } from "../../src/constants"; +import { abi, initVault } from "../../src/init-vault"; +import { ZeroAddress } from "ethers"; + +const { ethers, network } = hardhat; +const mellowVaults = vaults.mellow; + +const assetData = stETH; +describe(`Inception Symbiotic Vault ${assetData.assetName}`, function() { + let iToken, iVault, ratioFeed, asset, mellowAdapter, withdrawalQueue; + let iVaultOperator, deployer, staker, staker2, staker3; + let ratioErr, transactErr; + let snapshot; + let params; + + before(async function() { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.assetName.toLowerCase())) { + console.log(`${assetData.assetName} is not in the list, going to skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: assetData.url ? assetData.url : network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, ratioFeed, asset, iVaultOperator, mellowAdapter, withdrawalQueue } + = await initVault(assetData, { adapters: [adapters.Mellow, adapters.Symbiotic] })); + + ratioErr = assetData.ratioErr; + transactErr = assetData.transactErr; + + [deployer, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + + snapshot = await helpers.takeSnapshot(); + }); + + after(async function() { + await iVault?.removeAllListeners(); + }); + + describe("Mellow vaults management", function() { + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(e18, staker.address); + }); + + it("addMellowVault reverts when already added", async function() { + const mellowVault = mellowVaults[0].vaultAddress; + // const wrapper = mellowVaults[0].wrapperAddress; + await expect(mellowAdapter.addMellowVault(mellowVault)).to.revertedWithCustomError(mellowAdapter, "AlreadyAdded"); + }); + + it("addMellowVault vault is 0 address", async function() { + const mellowVault = ethers.ZeroAddress; + // const wrapper = mellowVaults[1].wrapperAddress; + await expect(mellowAdapter.addMellowVault(mellowVault)).to.revertedWithCustomError(mellowAdapter, "ZeroAddress"); + }); + + it("remove vault: reverts when not an owner", async function() { + await expect(mellowAdapter.connect(staker).removeVault(ZeroAddress)) + .to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("remove vault: reverts when unknown vault", async function() { + await expect(mellowAdapter.removeVault(mellowVaults[2].vaultAddress)) + .to.be.revertedWithCustomError(mellowAdapter, "InvalidVault"); + }); + + it("remove vault: reverts when vault is zero address", async function() { + await expect(mellowAdapter.removeVault(ZeroAddress)).to.be.revertedWithCustomError(mellowAdapter, "ZeroAddress"); + }); + + it("remove vault: reverts when vault is not empty", async function() { + const vault = mellowVaults[0].vaultAddress; + // delegate vault to be non empty + await iVault.connect(staker).deposit(toWei(10), staker.address); + await iVault.connect(iVaultOperator).delegate(mellowAdapter.address, vault, toWei(2), emptyBytes); + // try to remove vault + await expect(mellowAdapter.removeVault(vault)).to.be.revertedWithCustomError(mellowAdapter, "VaultNotEmpty"); + }); + + it("remove vault: success", async function() { + const vault = mellowVaults[0].vaultAddress; + await expect(mellowAdapter.removeVault(vault)).to.emit(mellowAdapter, "VaultRemoved"); + }); + + // it("addMellowVault wrapper is 0 address", async function () { + // const mellowVault = mellowVaults[1].vaultAddress; + // const wrapper = ethers.ZeroAddress; + // await expect(mellowAdapter.addMellowVault(mellowVault)).to.revertedWithCustomError( + // mellowAdapter, + // "ZeroAddress", + // ); + // }); + + it("addMellowVault reverts when called by not an owner", async function() { + const mellowVault = mellowVaults[1].vaultAddress; + const wrapper = mellowVaults[1].wrapperAddress; + await expect(mellowAdapter.connect(staker).addMellowVault(mellowVault)).to.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("change allocation for specific vault", async function() { + await expect(mellowAdapter.changeAllocation(mellowVaults[0].vaultAddress, 1n)) + .to.be.emit(mellowAdapter, "AllocationChanged"); + }); + + it("set claimer implementation only owner", async function() { + await expect(mellowAdapter.connect(staker).setClaimerImplementation(ZeroAddress)).to.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + // it("changeMellowWrapper", async function () { + // const mellowVault = mellowVaults[1].vaultAddress; + // const prevValue = mellowVaults[1].wrapperAddress; + // await expect(mellowAdapter.addMellowVault(mellowVault)) + // .to.emit(mellowAdapter, "VaultAdded") + // .withArgs(mellowVault, prevValue); + // expect(await mellowAdapter.mellowDepositWrappers(mellowVault)).to.be.eq(prevValue); + + // const newValue = mellowVaults[1].wrapperAddress; + // await expect(mellowAdapter.changeMellowWrapper(mellowVault, newValue)) + // .to.emit(mellowAdapter, "WrapperChanged") + // .withArgs(mellowVault, prevValue, newValue); + // expect(await mellowAdapter.mellowDepositWrappers(mellowVault)).to.be.eq(newValue); + + // const freeBalance = await iVault.getFreeBalance(); + // await expect(iVault.connect(iVaultOperator).delegate(await mellowAdapter.getAddress(), mellowVault, freeBalance, emptyBytes)) + // .emit(iVault, "DelegatedTo") + // .withArgs(mellowAdapter.address, mellowVault, freeBalance); + // }); + + // it("changeMellowWrapper reverts when vault is 0 address", async function () { + // const vaultAddress = ethers.ZeroAddress; + // const newValue = ethers.Wallet.createRandom().address; + // await expect(mellowAdapter.changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWithCustomError( + // mellowAdapter, + // "ZeroAddress", + // ); + // }); + + // it("changeMellowWrapper reverts when wrapper is 0 address", async function () { + // const vaultAddress = mellowVaults[0].vaultAddress; + // const newValue = ethers.ZeroAddress; + // await expect(mellowAdapter.changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWithCustomError( + // mellowAdapter, + // "ZeroAddress", + // ); + // }); + + // it("changeMellowWrapper reverts when vault is unknown", async function () { + // const vaultAddress = mellowVaults[2].vaultAddress; + // const newValue = mellowVaults[2].wrapperAddress; + // await expect(mellowAdapter.changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWithCustomError( + // mellowAdapter, + // "NoWrapperExists", + // ); + // }); + + // it("changeMellowWrapper reverts when called by not an owner", async function () { + // const vaultAddress = mellowVaults[0].vaultAddress; + // const newValue = ethers.Wallet.createRandom().address; + // await expect(mellowAdapter.connect(staker).changeMellowWrapper(vaultAddress, newValue)).to.be.revertedWith( + // "Ownable: caller is not the owner", + // ); + // }); + }); + + describe("undelegateFromMellow: request withdrawal from mellow vault", function() { + let totalDeposited, assets1, assets2, vault1Delegated, vault2Delegated; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + totalDeposited = 10n * e18; + await iVault.connect(staker).deposit(totalDeposited, staker.address); + }); + + + it("Unable to delegate to unknown vault", async function() { + await expect(iVault.connect(iVaultOperator) + .delegate(mellowAdapter.address, staker.address, 1n, emptyBytes) + ).to.be.revertedWithCustomError(mellowAdapter, "NotAdded"); + }); + + it("Delegate to mellowVault#1", async function() { + vault1Delegated = (await iVault.getFreeBalance()) / 2n; + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, vault1Delegated, emptyBytes); + + expect(await mellowAdapter.getDeposited(mellowVaults[0].vaultAddress)).to.be.closeTo( + vault1Delegated, + transactErr, + ); + }); + + it("Add mellowVault#2 and delegate the rest", async function() { + await mellowAdapter.addMellowVault(mellowVaults[1].vaultAddress); + vault2Delegated = await iVault.getFreeBalance(); + + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress, vault2Delegated, emptyBytes); + + expect(await mellowAdapter.getDeposited(mellowVaults[1].vaultAddress)).to.be.closeTo( + vault2Delegated, + transactErr, + ); + expect(await mellowAdapter.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr * 2n); + expect(await iVault.getTotalDeposited()).to.be.closeTo(totalDeposited, transactErr); + }); + + it("Staker withdraws shares1", async function() { + assets1 = e18; + const shares = await iVault.convertToShares(assets1); + console.log(`Staker is going to withdraw:\t${assets1.format()}`); + await iVault.connect(staker).withdraw(shares, staker.address); + console.log(`Staker's pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); + }); + + let undelegateClaimer1; + + it("undelegateFromMellow from mellowVault#1 by operator", async function() { + const totalDelegatedBefore = await iVault.getTotalDelegated(); + const pendingWithdrawalsBefore = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + const ratioBefore = await iVault.ratio(); + + let tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, assets1, emptyBytes]]); + const receipt = await tx.wait(); + + const events = receipt.logs?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + undelegateClaimer1 = events[0].args["claimer"]; + + expect(await mellowAdapter["pendingWithdrawalAmount(address,bool)"](mellowVaults[0].vaultAddress, false)).to.be.equal( + assets1, + ); + + const totalDelegatedAfter = await iVault.getTotalDelegated(); + const pendingWithdrawalsAfter = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + const vault1DelegatedAfter = await mellowAdapter.getDeposited(mellowVaults[0].vaultAddress); + // const withdrawRequest = await mellowAdapter.pendingMellowRequest(mellowVaults[0].vaultAddress); + const ratioAfter = await iVault.ratio(); + + expect(totalDelegatedBefore - totalDelegatedAfter).to.be.closeTo(assets1, transactErr); + expect(pendingWithdrawalsAfter - pendingWithdrawalsBefore).to.be.closeTo(assets1, transactErr); + expect(vault1DelegatedAfter).to.be.closeTo(vault1Delegated - assets1, transactErr); + // expect(withdrawRequest.to).to.be.eq(mellowAdapter.address); + // expect(withdrawRequest.timestamp).to.be.eq((await ethers.provider.getBlock("latest")).timestamp); + expect(ratioAfter).to.be.closeTo(ratioBefore, 1n); + }); + + // it("Adding rewards to mellowVault#1 increases pending withdrawal respectively", async function () { + // const pendingMellowWithdrawalsBefore = await mellowAdapter.pendingWithdrawalAmount(); + // const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + // const vault1DelegatedBefore = await mellowAdapter.getDeposited(mellowVaults[0].vaultAddress); + // const ratioBefore = await iVault.ratio(); + + // //Add rewards + // await assetData.addRewardsMellowVault(10n * e18, mellowVaults[0].vaultAddress); + // const vault1DelegatedAfter = await mellowAdapter.getDeposited(mellowVaults[0].vaultAddress); + // const pendingMellowWithdrawalsAfter = await mellowAdapter.pendingWithdrawalAmount(); + // rewards = + // vault1DelegatedAfter + pendingMellowWithdrawalsAfter - vault1DelegatedBefore - pendingMellowWithdrawalsBefore; + // vault1Delegated += rewards; + // totalDeposited += rewards; + // //Update ratio + // const calculatedRatio = await iVault.ratio(); + // await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + // ratio = await iVault.ratio(); + // ratioDiff = ratioBefore - ratio; + + // const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + // expect((pendingMellowWithdrawalsBefore * vault1DelegatedAfter) / vault1DelegatedBefore).to.be.closeTo( + // pendingMellowWithdrawalsAfter, + // transactErr, + // ); + // expect((totalPendingMellowWithdrawalsBefore * vault1DelegatedAfter) / vault1DelegatedBefore).to.be.closeTo( + // totalPendingMellowWithdrawalsAfter, + // transactErr, + // ); + // expect(totalDeposited).to.be.closeTo(await iVault.getTotalDeposited(), transactErr); + // }); + + it("Staker withdraws shares2 to Staker2", async function() { + assets2 = e18; + const shares = await iVault.convertToShares(assets2); + console.log(`Staker is going to withdraw:\t${assets2.format()}`); + await iVault.connect(staker).withdraw(shares, staker2.address); + console.log( + `Staker2's pending withdrawals:\t${(await iVault.getPendingWithdrawals(await mellowAdapter.getAddress())).format()}`, + ); + }); + + // it("undelegateFromMellow replaces pending withdraw from mellowVault#1", async function () { + // const ratioBeforeUndelegate = await iVault.ratio(); + + // const amount = assets2; + // await expect(iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes)) + // .to.emit(iVault, "UndelegatedFrom") + // .withArgs(mellowAdapter.address, a => { + // expect(a).to.be.closeTo(amount, transactErr); + // return true; + // }); + + // const pendingMellowWithdrawalsAfter = await mellowAdapter.pendingWithdrawalAmount(); + // const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + // const totalDelegatedAfter = await iVault.getTotalDelegated(); + // const ratioAfter = await iVault.ratio(); + + // expect(pendingMellowWithdrawalsAfter).to.be.closeTo(amount, transactErr); + // expect(totalPendingMellowWithdrawalsAfter).to.be.closeTo(amount, transactErr); + // expect(totalDeposited - totalDelegatedAfter).to.be.closeTo(amount, transactErr); + // expect(ratioAfter).to.be.closeTo(ratioBeforeUndelegate, ratioErr); + // }); + + let undelegateClaimer2; + + it("undelegateFromMellow all from mellowVault#2", async function() { + const pendingMellowWithdrawalsBefore = await mellowAdapter.pendingWithdrawalAmount(); + const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawals( + await mellowAdapter.getAddress(), + ); + + //Amount can slightly exceed delegatedTo, but final number will be corrected + //undelegateFromMellow fails when deviation is too big + const epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + const undelegatedAmount = await iVault.convertToAssets(epochShares); + + const tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), + [[await mellowAdapter.getAddress(), mellowVaults[1].vaultAddress, undelegatedAmount, emptyBytes]] + ); + + const receipt = await tx.wait(); + const events = receipt.logs?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + receipt.logs?.filter(log => console.log(log.address)); + undelegateClaimer2 = events[0].args["claimer"]; + + // todo: recheck + // .to.emit(iVault, "UndelegatedFrom") + // .withArgs(mellowAdapter.address, mellowVaults[1].vaultAddress, a => { + // expect(a).to.be.closeTo(0, transactErr); + // return true; + // }); + + expect(await mellowAdapter["pendingWithdrawalAmount(address,bool)"](mellowVaults[1].vaultAddress, false)).to.be.equal( + undelegatedAmount, + ); + + const pendingMellowWithdrawalsAfter = await mellowAdapter.pendingWithdrawalAmount(); + const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + const totalDelegatedAfter = await iVault.getTotalDelegated(); + + // expect(pendingMellowWithdrawalsAfter - pendingMellowWithdrawalsBefore).to.be.closeTo( + // vault2Delegated, + // transactErr, + // ); + expect(totalPendingMellowWithdrawalsAfter - totalPendingMellowWithdrawalsBefore).to.be.closeTo( + undelegatedAmount, + transactErr, + ); + expect(totalDeposited - totalDelegatedAfter).to.be.closeTo(undelegatedAmount + assets2, transactErr); + expect(await iVault.ratio()).to.be.closeTo(await iVault.ratio(), transactErr); + }); + + it("Can not claim when adapter balance is 0", async function() { + vault2Delegated = vault2Delegated - (await mellowAdapter.claimableAmount()); + params = abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, undelegateClaimer1]); + await expect( + iVault + .connect(iVaultOperator) + .claim(1, [await mellowAdapter.getAddress()], [mellowVaults[0].vaultAddress], [[params]]), + ).to.be.revertedWithCustomError(mellowAdapter, "ValueZero"); + }); + + it("Process pending withdrawal from mellowVault#1 and mellowVault#2 to mellowAdapter", async function() { + await helpers.time.increase(1209900); + + // todo: recheck + // const adapterBalanceBefore = await mellowAdapter.claimableAmount(); + // const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawals( + // await mellowAdapter.getAddress(), + // ); + // const totalDepositedBefore = await iVault.getTotalDeposited(); + // console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); + // console.log(`Pending from Mellow before:\t\t${totalPendingMellowWithdrawalsBefore.format()}`); + // + // // await mellowVaults[0].curator.processWithdrawals([mellowAdapter.address]); + // await helpers.time.increase(1209900); + // + // const adapterBalanceAfter = await mellowAdapter.claimableAmount(); + // const pendingMellowWithdrawalsAfter = await mellowAdapter.pendingWithdrawalAmount(); + // const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + // const totalDepositedAfter = await iVault.getTotalDeposited(); + // console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); + // console.log(`Pending from Mellow:\t\t\t${totalPendingMellowWithdrawalsAfter.format()}`); + // console.log(`Adapter balance diff:\t\t\t${(adapterBalanceAfter - adapterBalanceBefore).format()}`); + // + // expect(adapterBalanceAfter - adapterBalanceBefore).to.be.closeTo(vault2Delegated + assets1, transactErr); + // expect(pendingMellowWithdrawalsAfter).to.be.closeTo(0, transactErr); + // expect(totalPendingMellowWithdrawalsAfter).to.be.closeTo(vault2Delegated + assets1, transactErr); + // expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); + // expect(await iVault.ratio()).to.be.closeTo(await iVault.ratio(), transactErr); + }); + + // it("Process pending withdrawal from mellowVault#2 to mellowAdapter", async function () { + // const adapterBalanceBefore = await mellowAdapter.claimableAmount(); + // const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + // const totalDepositedBefore = await iVault.getTotalDeposited(); + // console.log(`Total deposited before:\t\t\t${totalDepositedBefore.format()}`); + // console.log(`Pending from Mellow before:\t\t${totalPendingMellowWithdrawalsBefore.format()}`); + + // // await mellowVaults[1].curator.processWithdrawals([mellowRestaker.address]); + // await helpers.time.increase(1209900); + // await mellowAdapter.claimPending(); + + // const adapterBalanceAfter = await mellowAdapter.claimableAmount(); + // const pendingMellowWithdrawalsAfter = await mellowAdapter.pendingWithdrawalAmount(); + // const totalPendingMellowWithdrawalsAfter = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + // const totalDepositedAfter = await iVault.getTotalDeposited(); + // console.log(`Total deposited after:\t\t\t${totalDepositedAfter.format()}`); + // console.log(`Pending from Mellow:\t\t\t${totalPendingMellowWithdrawalsAfter.format()}`); + // console.log(`Adapter balance diff:\t\t\t${(adapterBalanceAfter - adapterBalanceBefore).format()}`); + + // expect(adapterBalanceAfter - adapterBalanceBefore).to.be.closeTo(vault2Delegated, transactErr); + // expect(pendingMellowWithdrawalsAfter).to.be.eq(0n); + // expect(totalPendingMellowWithdrawalsAfter).to.be.eq(totalPendingMellowWithdrawalsBefore); + // expect(totalDepositedAfter).to.be.closeTo(totalDepositedBefore, transactErr); + // expect(await iVault.ratio()).to.be.closeTo(await iVault.ratio(), transactErr); + // }); + + it("Can not claim funds from mellowAdapter when iVault is paused", async function() { + await iVault.pause(); + await expect( + iVault + .connect(iVaultOperator) + .claim( + await withdrawalQueue.currentEpoch(), + [await mellowAdapter.getAddress()], + [mellowVaults[0].vaultAddress], + [emptyBytes], + ), + ).to.be.revertedWith("Pausable: paused"); + }); + + it("Claim funds from mellowAdapter to iVault", async function() { + if (await iVault.paused()) { + await iVault.unpause(); + } + const totalPendingMellowWithdrawalsBefore = await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()); + + // const usersTotalWithdrawals = await iVault.totalSharesToWithdraw(); + const totalAssetsBefore = await iVault.totalAssets(); + const freeBalanceBefore = await iVault.getFreeBalance(); + + params = abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, undelegateClaimer1]); + await iVault + .connect(iVaultOperator) + .claim(1, [await mellowAdapter.getAddress()], [mellowVaults[0].vaultAddress], [[params]]); + params = abi.encode(["address", "address"], [mellowVaults[1].vaultAddress, undelegateClaimer2]); + await iVault + .connect(iVaultOperator) + .claim(2, [await mellowAdapter.getAddress()], [mellowVaults[1].vaultAddress], [[params]]); + console.log("getTotalDelegated", await iVault.getTotalDelegated()); + console.log("totalAssets", await iVault.totalAssets()); + console.log( + "getPendingWithdrawalAmountFromMellow", + await await iVault.getPendingWithdrawals(await mellowAdapter.getAddress()), + ); + console.log("redeemReservedAmount", await iVault.redeemReservedAmount()); + console.log("depositBonusAmount", await iVault.depositBonusAmount()); + + const totalAssetsAfter = await iVault.totalAssets(); + const adapterBalanceAfter = await mellowAdapter.claimableAmount(); + const freeBalanceAfter = await iVault.getFreeBalance(); + + expect(totalAssetsAfter - totalAssetsBefore).to.be.closeTo(totalPendingMellowWithdrawalsBefore, transactErr); + expect(adapterBalanceAfter).to.be.eq(0n, transactErr); + //Withdraw leftover goes to freeBalance + // expect(freeBalanceAfter - freeBalanceBefore).to.be.closeTo( + // totalPendingMellowWithdrawalsBefore - usersTotalWithdrawals, + // transactErr, + // ); + + console.log("vault ratio:", await iVault.ratio()); + console.log("calculated ratio:", await iVault.ratio()); + + expect(await iVault.ratio()).to.be.closeTo(await iVault.ratio(), transactErr); + }); + + it("Staker is able to redeem", async function() { + expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; + }); + + it("Staker2 is able to redeem", async function() { + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.true; + }); + + it("Staker redeems withdrawals", async function() { + const stakerBalanceBefore = await asset.balanceOf(staker.address); + const stakerPWBefore = await iVault.getPendingWithdrawalOf(staker.address); + + await iVault.redeem(staker.address); + const stakerBalanceAfter = await asset.balanceOf(staker.address); + const stakerPWAfter = await iVault.getPendingWithdrawalOf(staker.address); + + console.log(`Staker balance after: ${stakerBalanceAfter.format()}`); + console.log(`Staker pending withdrawals after: ${stakerPWAfter.format()}`); + + expect(stakerPWBefore - stakerPWAfter).to.be.closeTo(assets1, transactErr * 2n); + expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(assets1, transactErr * 2n); + expect(await iVault.ratio()).to.be.closeTo(await iVault.ratio(), 1n); + }); + }); + + describe("undelegateFromMellow: negative cases", function() { + beforeEach(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(randomBI(19), staker.address); + const freeBalance = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, freeBalance, emptyBytes); + console.log(`Delegated amount: \t${freeBalance.format()}`); + }); + + const invalidArgs = [ + // { + // name: "amount is 0", + // amount: async () => 0n, + // mellowVault: async () => mellowVaults[0].vaultAddress, + // operator: () => iVaultOperator, + // customError: "ValueZero", + // source: () => mellowAdapter, + // }, + // { + // name: "amount > delegatedTo", + // amount: async () => (await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress)) + e18, + // mellowVault: async () => mellowVaults[0].vaultAddress, + // operator: () => iVaultOperator, + // customError: "BadMellowWithdrawRequest", + // source: () => mellowAdapter, + // }, + // { + // name: "mellowVault is unregistered", + // amount: async () => await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress), + // mellowVault: async () => mellowVaults[1].vaultAddress, + // operator: () => iVaultOperator, + // customError: "InvalidVault", + // source: () => mellowAdapter, + // }, + { + name: "mellowVault is 0 address", + amount: async () => await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress), + mellowVault: async () => ethers.ZeroAddress, + operator: () => iVaultOperator, + customError: "InvalidAddress", + source: () => iVault, + }, + { + name: "called by not an operator", + amount: async () => await iVault.getDelegatedTo(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress), + mellowVault: async () => mellowVaults[0].vaultAddress, + operator: () => staker, + customError: "OnlyOperatorAllowed", + source: () => iVault, + }, + ]; + + invalidArgs.forEach(function(arg) { + it(`Reverts: when ${arg.name}`, async function() { + const amount = await arg.amount(); + const mellowVault = await arg.mellowVault(); + console.log(`Undelegate amount: \t${amount.format()}`); + if (arg.customError) { + await expect( + iVault + .connect(arg.operator()) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVault, amount, emptyBytes]]), + ).to.be.revertedWithCustomError(arg.source(), arg.customError); + } else { + await expect( + iVault + .connect(arg.operator()) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVault, amount, emptyBytes]]), + ).to.be.revertedWith(arg.error); + } + }); + }); + + it("Reverts: undelegate when iVault is paused", async function() { + const amount = randomBI(17); + await iVault.pause(); + await expect( + iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes]]), + ).to.be.revertedWith("Pausable: paused"); + await iVault.unpause(); + }); + + it("Reverts: undelegate when mellowAdapter is paused", async function() { + if (await iVault.paused()) { + await iVault.unpause(); + } + + const amount = randomBI(17); + await mellowAdapter.pause(); + await expect( + iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes]]), + ).to.be.revertedWith("Pausable: paused"); + }); + }); + + describe("Redeem: retrieves assets after they were received from Mellow", function() { + let ratio, stakerAmount, staker2Amount, stakerUnstakeAmount1, staker2UnstakeAmount; + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker3).deposit(e18, staker3.address); + await iVault + .connect(iVaultOperator) + .delegate( + await mellowAdapter.getAddress(), + mellowVaults[0].vaultAddress, + await iVault.getFreeBalance(), + emptyBytes, + ); + await ratioFeed.updateRatioBatch([iToken.address], [await iVault.ratio()]); + ratio = await iVault.ratio(); + }); + + it("Deposit and Delegate partially", async function() { + stakerAmount = 9_399_680_561_290_658_040n; + await iVault.connect(staker).deposit(stakerAmount, staker.address); + staker2Amount = 1_348_950_494_309_030_813n; + await iVault.connect(staker2).deposit(staker2Amount, staker2.address); + + const delegated = (await iVault.getFreeBalance()) - e18; + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, delegated, emptyBytes); + + await ratioFeed.updateRatioBatch([iToken.address], [await iVault.ratio()]); + console.log(`Staker amount: ${stakerAmount}`); + console.log(`Staker2 amount: ${staker2Amount}`); + console.log(`Ratio: ${await iVault.ratio()}`); + }); + + it("Staker has nothing to claim yet", async function() { + expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; + }); + + it("Staker withdraws half of their shares", async function() { + const shares = await iToken.balanceOf(staker.address); + stakerUnstakeAmount1 = shares / 2n; + await iVault.connect(staker).withdraw(stakerUnstakeAmount1, staker.address); + await ratioFeed.updateRatioBatch([iToken.address], [await iVault.ratio()]); + console.log(`Ratio: ${await iVault.ratio()}`); + }); + + it("Staker is not able to redeem yet", async function() { + expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; + }); + + // todo: recheck + // it("updateEpoch can not unlock withdrawals without enough freeBalance", async function () { + // const redeemReserveBefore = await iVault.redeemReservedAmount(); + // const freeBalanceBefore = await iVault.getFreeBalance(); + // const epochBefore = await iVault.epoch(); + // await iVault.connect(iVaultOperator).updateEpoch(); + // + // const redeemReserveAfter = await iVault.redeemReservedAmount(); + // const freeBalanceAfter = await iVault.getFreeBalance(); + // const epochAfter = await iVault.epoch(); + // + // expect(redeemReserveAfter).to.be.eq(redeemReserveBefore); + // expect(freeBalanceAfter).to.be.eq(freeBalanceBefore); + // expect(epochAfter).to.be.eq(epochBefore); + // }); + + it("Withdraw from mellowVault amount = pending withdrawals", async function() { + const redeemReserveBefore = await iVault.redeemReservedAmount(); + const freeBalanceBefore = await iVault.getFreeBalance(); + + const epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + const amount = await iVault.convertToAssets(epochShares); + + const tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, epochShares, emptyBytes]]); + + const receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + const adapterEvents = receipt.logs + ?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + await helpers.time.increase(1209900); + + if (events[0].args["actualAmounts"] > 0) { + params = abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, claimer]); + await iVault + .connect(iVaultOperator) + .claim( + events[0].args["epoch"], + [await mellowAdapter.getAddress()], + [mellowVaults[0].vaultAddress], + [[params]], + ); + } + + const redeemReserveAfter = await iVault.redeemReservedAmount(); + const freeBalanceAfter = await iVault.getFreeBalance(); + await ratioFeed.updateRatioBatch([iToken.address], [await iVault.ratio()]); + console.log(`Total assets:\t\t${(await iVault.totalAssets()).format()}`); + console.log(`Pending withdrawals:\t${(await iVault.getPendingWithdrawalOf(staker.address)).format()}`); + console.log(`Ratio: ${await iVault.ratio()}`); + + expect(redeemReserveAfter - redeemReserveBefore).to.be.closeTo(amount, transactErr); + // expect(freeBalanceAfter).to.be.closeTo(freeBalanceBefore, transactErr); // todo: recheck + }); + + it("Staker is now able to redeem", async function() { + expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.true; + }); + + it("Redeem reverts when iVault is paused", async function() { + await iVault.pause(); + await expect(iVault.connect(iVaultOperator).redeem(staker.address)).to.be.revertedWith("Pausable: paused"); + }); + + it("Unpause after previous test", async function() { + await iVault.unpause(); + }); + + it("Staker2 withdraws < freeBalance", async function() { + staker2UnstakeAmount = (await iVault.getFreeBalance()) - 1000_000_000n; + await iVault.connect(staker2).withdraw(staker2UnstakeAmount, staker2.address); + }); + + it("Staker2 can not claim the same epoch even if freeBalance is enough", async function() { + expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.false; + }); + + it("Staker is still able to claim", async function() { + const ableRedeem = await iVault.isAbleToRedeem(staker.address); + expect(ableRedeem[0]).to.be.true; + expect([...ableRedeem[1]]).to.have.members([0n]); + }); + + // it("Stakers new withdrawal goes to the end of queue", async function () { + // stakerUnstakeAmount2 = (await iToken.balanceOf(staker.address)) / 2n; + // await iVault.connect(staker).withdraw(stakerUnstakeAmount2, staker.address); + // + // console.log(`Pending withdrawals: ${await iVault.getPendingWithdrawalOf(staker.address)}`); + // console.log(`Unstake amount: ${stakerUnstakeAmount2.toString()}`); + // console.log(`Ratio: ${await iVault.ratio()}`); + // + // expect(newQueuedWithdrawal.epoch).to.be.eq(2n); //queue length - 1 + // expect(newQueuedWithdrawal.receiver).to.be.eq(staker.address); + // expect(newQueuedWithdrawal.amount).to.be.closeTo( + // await iVault.convertToAssets(stakerUnstakeAmount2), + // transactErr, + // ); + // }); + + it("Staker is still able to redeem the 1st withdrawal", async function() { + const ableRedeem = await iVault.isAbleToRedeem(staker.address); + expect(ableRedeem[0]).to.be.true; + expect([...ableRedeem[1]]).to.have.members([0n]); + }); + + // i"updateEpoch unlocks pending withdrawals in order they were submitted", async function () { + // // const staker2Pending = await iVault.getPendingWithdrawalOf(staker2.address); + // // const redeemReserveBefore = await iVault.redeemReservedAmount(); + // // const freeBalanceBefore = await iVault.getFreeBalance(); + // // const epochBefore = await iVault.epoch(); + // // await iVault.connect(iVaultOperator).updateEpoch(); + // // + // // const redeemReserveAfter = await iVault.redeemReservedAmount(); + // // const freeBalanceAfter = await iVault.getFreeBalance(); + // // const epochAfter = await iVault.epoch(); + // // + // // expect(redeemReserveAfter - redeemReserveBefore).to.be.closeTo(staker2Pending, transactErr); + // // expect(freeBalanceBefore - freeBalanceAfter).to.be.closeTo(staker2Pending, transactErr); + // // expect(epochAfter).to.be.eq(epochBefore + 1n); + // // });t( + + // it("Staker2 is able to claim", async function () { + // const ableRedeem = await iVault.isAbleToRedeem(staker2.address); + // expect(ableRedeem[0]).to.be.true; + // expect([...ableRedeem[1]]).to.have.members([1n]); + // }); + + it("Staker is able to claim only the 1st wwl", async function() { + const ableRedeem = await iVault.isAbleToRedeem(staker.address); + expect(ableRedeem[0]).to.be.true; + expect([...ableRedeem[1]]).to.have.members([0n]); + }); + + it("Staker redeems withdrawals", async function() { + const stakerBalanceBefore = await asset.balanceOf(staker.address); + const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker.address); + const stakerRedeemedAmount = await iVault.convertToAssets(stakerUnstakeAmount1); + // const stakerPendingAmount = await iVault.convertToAssets(stakerUnstakeAmount2); + + await iVault.connect(staker).redeem(staker.address); + const stakerBalanceAfter = await asset.balanceOf(staker.address); + const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker.address); + + console.log(`Staker balance after: ${stakerBalanceAfter}`); + console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); + console.log(`stakerUnstakeAmountAssetValue: ${stakerRedeemedAmount}`); + console.log(`stakerPendingWithdrawalsBefore[0]: ${stakerPendingWithdrawalsBefore}`); + + expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( + stakerRedeemedAmount, + transactErr, + ); + // expect(stakerPendingWithdrawalsAfter).to.be.closeTo(stakerPendingAmount, transactErr); + expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(stakerRedeemedAmount, transactErr); + expect((await iVault.isAbleToRedeem(staker.address))[0]).to.be.false; + expect(await iVault.ratio()).to.be.closeTo(await iVault.ratio(), ratioErr); + }); + + // todo: recheck + // it("Staker2 redeems withdrawals", async function () { + // const stakerBalanceBefore = await asset.balanceOf(staker2.address); + // const stakerPendingWithdrawalsBefore = await iVault.getPendingWithdrawalOf(staker2.address); + // + // await iVault.connect(staker2).redeem(staker2.address); + // const stakerBalanceAfter = await asset.balanceOf(staker2.address); + // const stakerPendingWithdrawalsAfter = await iVault.getPendingWithdrawalOf(staker2.address); + // + // console.log(`Staker balance after: ${stakerBalanceAfter}`); + // console.log(`Staker pending withdrawals after: ${stakerPendingWithdrawalsAfter}`); + // const stakerUnstakeAmountAssetValue = await iVault.convertToAssets(staker2UnstakeAmount); + // expect(stakerPendingWithdrawalsBefore - stakerPendingWithdrawalsAfter).to.be.closeTo( + // stakerUnstakeAmountAssetValue, + // transactErr * 2n, + // ); + // expect(stakerBalanceAfter - stakerBalanceBefore).to.be.closeTo(stakerUnstakeAmountAssetValue, transactErr * 2n); + // expect((await iVault.isAbleToRedeem(staker2.address))[0]).to.be.false; + // expect(await iVault.ratio()).to.be.closeTo(await iVault.ratio(), ratioErr); + // }); + }); + + describe("Redeem: to the different addresses", function() { + let ratio, recipients, pendingShares, undelegatedEpoch; + + before(async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit("9292557565124725653", staker.address); + const amount = await iVault.getFreeBalance(); + await iVault + .connect(iVaultOperator) + .delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes); + }); + + const count = 3; + for (let j = 0; j < count; j++) { + it(`${j} Withdraw to 5 random addresses`, async function() { + recipients = []; + pendingShares = 0n; + for (let i = 0; i < 5; i++) { + const recipient = randomAddress(); + const shares = randomBI(17); + pendingShares = pendingShares + shares; + await iVault.connect(staker).withdraw(shares, recipient); + recipients.push(recipient); + } + }); + + it(`${j} Withdraw from EL and update ratio`, async function() { + undelegatedEpoch = await withdrawalQueue.currentEpoch(); + let amount = await iVault.convertToAssets(await withdrawalQueue.getRequestedShares(undelegatedEpoch)); + + const tx = await iVault + .connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, amount, emptyBytes]]); + const receipt = await tx.wait(); + let events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + + const adapterEvents = receipt.logs + ?.filter(log => log.address === mellowAdapter.address) + .map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + ratio = await iVault.ratio(); + console.log(`New ratio is: ${ratio}`); + + // await mellowVaults[0].curator.processWithdrawals([mellowRestaker.address]); + await helpers.time.increase(1209900); + + if (events[0].args["actualAmounts"] > 0) { + params = abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, claimer]); + await iVault + .connect(iVaultOperator) + .claim(undelegatedEpoch, [await mellowAdapter.getAddress()], [mellowVaults[0].vaultAddress], [[params]]); + } + + console.log(`Total assets: ${await iVault.totalAssets()}`); + console.log(`Total withdrawn shares to assets ${await iVault.convertToAssets(pendingShares)}`); + console.log(`Ratio: ${await iVault.ratio()}`); + }); + + it(`${j} Recipients claim`, async function() { + for (const r of recipients) { + const rBalanceBefore = await asset.balanceOf(r); + const rPendingWithdrawalsBefore = await withdrawalQueue.getPendingWithdrawalOf(r); + await iVault.connect(deployer).redeem(r); + const rBalanceAfter = await asset.balanceOf(r); + const rPendingWithdrawalsAfter = await withdrawalQueue.getPendingWithdrawalOf(r); + + console.log("rBalanceAfter", rBalanceAfter); + console.log("rPendingWithdrawalsBefore", rPendingWithdrawalsBefore); + expect(rBalanceAfter - rPendingWithdrawalsBefore).to.be.closeTo(0, transactErr); + expect(rBalanceBefore - rPendingWithdrawalsAfter).to.be.closeTo(0, transactErr); + } + + expect(await iVault.ratio()).to.be.lte(ratio); + console.log(`Total assets: ${await iVault.totalAssets()}`); + console.log(`Ratio: ${await iVault.ratio()}`); + }); + } + + it("Update asset ratio and withdraw the rest", async function() { + await assetData.addRewardsMellowVault(e18, mellowVaults[0].vaultAddress); + const calculatedRatio = await iVault.ratio(); + await ratioFeed.updateRatioBatch([iToken.address], [calculatedRatio]); + ratio = await iVault.ratio(); + console.log(`New ratio is: ${ratio}`); + + //Withdraw all and take from EL + const shares = await iToken.balanceOf(staker.address); + await iVault.connect(staker).withdraw(shares, staker.address); + const amount = await iVault.getTotalDelegated(); + console.log("totalDElegated", amount); + console.log("shares", shares); + await iVault.withdrawFromMellowAndClaim(mellowVaults[0].vaultAddress, amount); + // await iVault.undelegate(await withdrawalQueue.currentEpoch(), await withdrawalQueue.currentEpoch(), []) + await iVault.connect(iVaultOperator).redeem(staker.address); + + console.log(`iVault total assets: ${await iVault.totalAssets()}`); + console.log(`Total deposited: ${await iVault.getTotalDeposited()}`); + }); + }); + + describe("Emergency undelegate cannot finish normal undelegation flow", function() { + it("deposit & delegate & undelegate", async function() { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + + // deposit & delegate 10 + await iVault.connect(staker).deposit(toWei(10), staker.address); + await iVault.connect(iVaultOperator).delegate(await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, toWei(10), emptyBytes); + + // withdraw 3 + await iVault.connect(staker).withdraw(toWei(3), staker.address); + + // emergency undelegate 5 + await iVault.connect(iVaultOperator).emergencyUndelegate([[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, toWei(5),[]]]); + // normal undelegate 3 + let tx = await iVault.connect(iVaultOperator).undelegate(await withdrawalQueue.currentEpoch(), [[await mellowAdapter.getAddress(), mellowVaults[0].vaultAddress, toWei(3), emptyBytes]]); + + // get emergency claimer + let receipt = await tx.wait(); + let adapterEvents = receipt.logs?.filter(log => log.address === mellowAdapter.address).map(log => mellowAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + await helpers.time.increase(1209900); + + // claim + const params = abi.encode(["address", "address"], [mellowVaults[0].vaultAddress, claimer]); + await expect( + iVault.connect(iVaultOperator).emergencyClaim([mellowAdapter.address], [mellowVaults[0].vaultAddress], [[params]]), + ).to.be.revertedWithCustomError(mellowAdapter, "OnlyEmergency"); + }); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/redeem.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S/redeem.test.ts new file mode 100644 index 00000000..ccc9b72a --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/redeem.test.ts @@ -0,0 +1,109 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { skipEpoch, symbioticClaimParams, toWei } from "../../helpers/utils"; +import { initVault } from "../../src/init-vault-new"; +const { ethers, network } = hardhat; +import { testrunConfig } from '../../testrun.config'; +import { adapters, emptyBytes } from "../../src/constants"; + +const assetData = testrunConfig.assetData; +const symbioticVaults = assetData.adapters.symbiotic; + +describe('redeem with specified epoch', async function () { + let iVault; + let asset; + let staker: HardhatEthersSigner, staker2: HardhatEthersSigner; + let transactErr: bigint; + let snapshot: helpers.SnapshotRestorer; + let iToken; + let iVaultOperator; + let symbioticAdapter; + let withdrawalQueue; + let receipt; + let events; + const depositAmount = toWei(10); + + before(async function () { + await network.provider.send("hardhat_reset", [{ + forking: { + jsonRpcUrl: network.config.forking.url, + blockNumber: assetData.blockNumber || network.config.forking.blockNumber, + }, + }]); + + ({ iToken, iVault, iVaultOperator, asset, symbioticAdapter, withdrawalQueue } = + await initVault(assetData, { adapters: [adapters.Symbiotic] })); + transactErr = assetData.transactErr; + + [, staker, staker2] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + + snapshot = await helpers.takeSnapshot(); + }); + + beforeEach(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(2n); + + // Arrange: deposit > delegate > withdraw > undelegate > claim + // deposit + await (await iVault.connect(staker).deposit(depositAmount, staker.address)) + .wait(); + + // delegate + await (await iVault.connect(iVaultOperator) + .delegate(symbioticAdapter.address, symbioticVaults[0].vaultAddress, depositAmount, emptyBytes)) + .wait(); + + // withdraw + const shares = await iToken.balanceOf(staker.address); + await (await iVault.connect(staker).withdraw(shares, staker.address)) + .wait(); + + // undelegate + const epochShares = await withdrawalQueue.getRequestedShares(await withdrawalQueue.currentEpoch()); + receipt = await (await iVault.connect(iVaultOperator) + .undelegate(await withdrawalQueue.currentEpoch(), [[symbioticAdapter.address, symbioticVaults[0].vaultAddress, epochShares, []]])) + .wait(); + events = receipt.logs?.filter(e => e.eventName === "UndelegatedFrom"); + const adapterEvents = receipt.logs?.filter(log => log.address === symbioticAdapter.address) + .map(log => symbioticAdapter.interface.parseLog(log)); + let claimer = adapterEvents[0].args["claimer"]; + + // claim + await skipEpoch(symbioticVaults[0]); + const params = await symbioticClaimParams(symbioticVaults[0], claimer); + await (await iVault.connect(iVaultOperator) + .claim(events[0].args["epoch"], [symbioticAdapter.address], [symbioticVaults[0].vaultAddress], [[params]])) + .wait(); + }); + + it("successful redeem with specified valid epoch", async function () { + // Act: redeem + const userBalanceBeforeRedeem = await asset.balanceOf(staker.address); + + // redeem with specifying epoch + receipt = await (await iVault.connect(staker)["redeem(address,uint256)"](staker.address, 0)) + .wait(); + events = receipt.logs?.filter(e => e.eventName === "Redeem"); + + // Assert: user balance increased by deposit/redeem amount + expect(await withdrawalQueue.totalAmountRedeem()).to.be.eq(0); + expect(events[0].args["amount"]).to.be.closeTo(depositAmount, transactErr); + + const userBalanceAfterRedeem = await asset.balanceOf(staker.address); + expect(userBalanceAfterRedeem).to.be.closeTo(userBalanceBeforeRedeem + depositAmount, transactErr); + }); + + it('revert if invalid epoch specified', async function () { + // Act/Assert: redeem with specifying epoch + await expect(iVault.connect(staker)["redeem(address,uint256)"](staker.address, 2)).to.be.revertedWithCustomError( + withdrawalQueue, + "InvalidEpoch" + ); + }); +}); diff --git a/projects/vaults/test/tests-unit/InceptionVault_S/rewards.test.ts b/projects/vaults/test/tests-unit/InceptionVault_S/rewards.test.ts new file mode 100644 index 00000000..a709c6d3 --- /dev/null +++ b/projects/vaults/test/tests-unit/InceptionVault_S/rewards.test.ts @@ -0,0 +1,219 @@ +// Tests for InceptionVault_S contract; +// The S in name does not mean only Symbiotic; this file contains tests for Symbiotic and Mellow adapters + +import * as helpers from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import hardhat from "hardhat"; +import { stETH } from "../../data/assets/inception-vault-s"; +import { vaults } from "../../data/vaults"; +import { toWei } from "../../helpers/utils"; +import { adapters, emptyBytes } from "../../src/constants"; +import { abi, initVault } from "../../src/init-vault"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import exp from "node:constants"; +import { ZeroAddress, ZeroHash } from "ethers"; +import { eigenLayer } from "../../../typechain-types/contracts/vaults"; + +const { ethers, network } = hardhat; +const assetData = stETH; + +describe("Farm rewards", function() { + let iToken, iVault, asset, mellowAdapter, symbioticAdapter, withdrawalQueue; + let iVaultOperator, staker, staker2, staker3; + let ratioErr, transactErr; + let snapshot; + + before(async function() { + if (process.env.ASSETS) { + const assets = process.env.ASSETS.toLocaleLowerCase().split(","); + if (!assets.includes(assetData.assetName.toLowerCase())) { + console.log(`${assetData.assetName} is not in the list, going to skip`); + this.skip(); + } + } + + await network.provider.send("hardhat_reset", [ + { + forking: { + jsonRpcUrl: assetData.url ? assetData.url : network.config.forking.url, + blockNumber: assetData.blockNumber ? assetData.blockNumber : network.config.forking.blockNumber, + }, + }, + ]); + + ({ iToken, iVault, asset, iVaultOperator, mellowAdapter, symbioticAdapter, withdrawalQueue } + = await initVault(assetData, { adapters: [adapters.Mellow, adapters.Symbiotic] })); + + ratioErr = assetData.ratioErr; + transactErr = assetData.transactErr; + + [, staker, staker2, staker3] = await ethers.getSigners(); + + staker = await assetData.impersonateStaker(staker, iVault); + staker2 = await assetData.impersonateStaker(staker2, iVault); + staker3 = await assetData.impersonateStaker(staker3, iVault); + + snapshot = await helpers.takeSnapshot(); + }); + + describe("Symbiotic farm rewards", function() { + let stakerRewardsContract, networkAddr, rewardsTreasury; + + it("Set rewards treasury address", async function() { + rewardsTreasury = ethers.Wallet.createRandom().address; + await iVault.setRewardsTreasury(rewardsTreasury); + }); + + it("Deposit and delegate to symbiotic vault", async function() { + await iVault.setTargetFlashCapacity(1n); + await iVault.connect(staker).deposit(toWei(100), staker.address); + await iVault.connect(iVaultOperator).delegate(symbioticAdapter.address, vaults.symbiotic[0].vaultAddress, toWei(90), emptyBytes); + }); + + it("Add rewards to symbiotic", async function() { + const [deployer] = await ethers.getSigners(); + networkAddr = await deployer.getAddress(); + + // register network + const networkRegistryAddr = "0xC773b1011461e7314CF05f97d95aa8e92C1Fd8aA"; + const networkRegistryABI = ["function registerNetwork() external"]; + const networkRegistry = new ethers.Contract(networkRegistryAddr, networkRegistryABI, deployer); + await networkRegistry.registerNetwork(); + + // set network middleware + const networkMiddlewareServiceAddr = "0xD7dC9B366c027743D90761F71858BCa83C6899Ad"; + const networkMiddlewareServiceABI = ["function setMiddleware( address middleware ) external"]; + const networkMiddlewareService = new ethers.Contract(networkMiddlewareServiceAddr, networkMiddlewareServiceABI, deployer); + await networkMiddlewareService.setMiddleware(deployer); + + // define and make factory + const stakerRewardsFactoryAddr = "0xFEB871581C2ab2e1EEe6f7dDC7e6246cFa087A23"; + const stakerRewardsFactory = new ethers.Contract(stakerRewardsFactoryAddr, [ + "function create((address vault, uint256 adminFee, address defaultAdminRoleHolder, address adminFeeClaimRoleHolder, address adminFeeSetRoleHolder)) external returns (address)", + "event AddEntity(address indexed entity)", + ], deployer); + + // create new staker rewards contract by factory + const tx = await stakerRewardsFactory.create({ + vault: vaults.symbiotic[0].vaultAddress, + adminFee: 0n, + defaultAdminRoleHolder: deployer.address, + adminFeeClaimRoleHolder: deployer.address, + adminFeeSetRoleHolder: deployer.address, + }); + const receipt = await tx.wait(); + + // define and make staker rewards contract based on created contract address + const stakerRewardsContractAddr = receipt.logs.find((l) => l.eventName === "AddEntity").args.entity; + stakerRewardsContract = new ethers.Contract(stakerRewardsContractAddr, [ + "function distributeRewards(address network, address token, uint256 amount, bytes calldata data) external", + "function claimable( address token, address account, bytes calldata data ) external view override returns (uint256 amount)", + ], deployer); + + // distribute rewards + const rewardTimestamp = await time.latest(); + const rewardsAmount = toWei(100); + await asset.connect(staker3).transfer(deployer, rewardsAmount); + await asset.connect(deployer).approve(stakerRewardsContractAddr, rewardsAmount); + + await stakerRewardsContract.distributeRewards(networkAddr, assetData.assetAddress, rewardsAmount, abi.encode( + ["uint48", "uint256", "bytes", "bytes"], + [rewardTimestamp, 0n, "0x", "0x"], + ), + ); + }); + + it("Claim symbiotic rewards", async function() { + const claimData = abi.encode(["address", "uint256", "bytes"], [networkAddr, 1n, "0x"]); + const farmData = abi.encode(["address", "bytes"], [await stakerRewardsContract.getAddress(), claimData]); + + const claimable = await stakerRewardsContract.claimable(assetData.assetAddress, symbioticAdapter.address, claimData); + expect(claimable).to.greaterThan(0); + + await iVault.connect(iVaultOperator).claimAdapterRewards(symbioticAdapter.address, assetData.assetAddress, farmData); + expect(await asset.balanceOf(rewardsTreasury)).to.eq(claimable); + }); + }); + + describe("Mellow farm rewards", function() { + it("available to run only by trustee", async function() { + await expect(mellowAdapter.connect(staker).claimRewards(ZeroAddress, "0x")) + .to.be.revertedWithCustomError(mellowAdapter, "NotVaultOrTrusteeManager"); + }); + + it("claim rewards reverts", async function() { + await expect(mellowAdapter.connect(iVaultOperator).claimRewards(ZeroAddress, "0x")) + .to.be.revertedWith("Mellow distribution rewards not implemented yet"); + }); + }); + + describe("Add rewards to vault", function () { + before(async function () { + await snapshot.restore(); + await iVault.setTargetFlashCapacity(1n); + }); + + it("set rewards timeline", async function () { + const timeline = 86400; + + await iVault.setRewardsTimeline(timeline); + expect(await iVault.rewardsTimeline()).to.be.eq(timeline); + }); + + it("set rewards timeline: invalid data", async function() { + await expect(iVault.setRewardsTimeline(3600n)) + .to.be.revertedWithCustomError(iVault, "InconsistentData"); + + await expect(iVault.setRewardsTimeline(90000n)) + .to.be.revertedWithCustomError(iVault, "InconsistentData"); + }); + + it("set rewards timeline: only owner", async function() { + await expect(iVault.connect(staker).setRewardsTimeline(1n)) + .to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("set rewards treasury: only owner", async function() { + await expect(iVault.connect(staker).setRewardsTreasury(ZeroAddress)) + .to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("add rewards for the first time", async function() { + const rewardsAmount = toWei(10); + + const totalAssetsBefore = await iVault.totalAssets(); + await asset.connect(staker).transfer(iVaultOperator, rewardsAmount); + await asset.connect(iVaultOperator).approve(iVault.address, rewardsAmount); + await iVault.connect(iVaultOperator).addRewards(rewardsAmount); + const totalAssetsAfter = await iVault.totalAssets(); + + expect(await iVault.currentRewards()).to.eq(rewardsAmount); + expect(totalAssetsBefore - totalAssetsAfter).to.be.eq(0); + }); + + it("add rewards not available while timeline not over", async function() { + await expect(iVault.connect(iVaultOperator).addRewards(toWei(1))) + .to.be.revertedWithCustomError(iVault, "TimelineNotOver"); + }); + + it("add rewards: only operator", async function() { + await expect(iVault.connect(staker).addRewards(toWei(1))) + .to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); + }); + }); + + describe("Claim rewards", function() { + it("Can be called only by operator", async function() { + await expect(iVault.connect(staker).claimAdapterRewards(symbioticAdapter.address, assetData.assetAddress, "0x")) + .to.be.revertedWithCustomError(iVault, "OnlyOperatorAllowed"); + }); + + it("Can be called only by trustee", async function() { + await expect(symbioticAdapter.connect(staker).claimRewards(assetData.assetAddress, "0x")) + .to.be.revertedWithCustomError(symbioticAdapter, "NotVaultOrTrusteeManager"); + + await expect(mellowAdapter.connect(staker).claimRewards(assetData.assetAddress, "0x")) + .to.be.revertedWithCustomError(mellowAdapter, "NotVaultOrTrusteeManager"); + }); + }); +}); diff --git a/projects/vaults/tsconfig.json b/projects/vaults/tsconfig.json index 574e785c..6a0942dd 100644 --- a/projects/vaults/tsconfig.json +++ b/projects/vaults/tsconfig.json @@ -6,6 +6,11 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "strictNullChecks": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, } } diff --git a/projects/vaults/yarn.lock b/projects/vaults/yarn.lock index f39d6cdb..4b413bf6 100644 --- a/projects/vaults/yarn.lock +++ b/projects/vaults/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== +"@adraffy/ens-normalize@^1.8.8": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" + integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== + "@aragon/os@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@aragon/os/-/os-4.4.0.tgz#af3e80b11f71209f86dba8b2b3f37a4b025aff7b" @@ -165,6 +170,24 @@ crc-32 "^1.2.0" ethereumjs-util "^7.1.5" +"@ethereumjs/common@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-3.2.0.tgz#b71df25845caf5456449163012074a55f048e0a0" + integrity sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA== + dependencies: + "@ethereumjs/util" "^8.1.0" + crc-32 "^1.2.0" + +"@ethereumjs/rlp@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" + integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== + +"@ethereumjs/rlp@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842" + integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA== + "@ethereumjs/tx@^3.3.0": version "3.5.2" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.2.tgz#197b9b6299582ad84f9527ca961466fce2296c1c" @@ -173,6 +196,25 @@ "@ethereumjs/common" "^2.6.4" ethereumjs-util "^7.1.5" +"@ethereumjs/tx@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-4.2.0.tgz#5988ae15daf5a3b3c815493bc6b495e76009e853" + integrity sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw== + dependencies: + "@ethereumjs/common" "^3.2.0" + "@ethereumjs/rlp" "^4.0.1" + "@ethereumjs/util" "^8.1.0" + ethereum-cryptography "^2.0.0" + +"@ethereumjs/util@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4" + integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA== + dependencies: + "@ethereumjs/rlp" "^4.0.1" + ethereum-cryptography "^2.0.0" + micro-ftch "^0.3.1" + "@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.0-beta.146", "@ethersproject/abi@^5.0.9", "@ethersproject/abi@^5.1.2", "@ethersproject/abi@^5.4.7", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" @@ -526,6 +568,11 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@httptoolkit/esm@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@httptoolkit/esm/-/esm-3.3.1.tgz#4869136f1170caeccea7287ee23968f829be250c" + integrity sha512-XvWsT5qskZQoiHgg0kEoIonB+Zj/0T/W0rosjzyPuY++iBwO5c9fMfgvPBCffwY3cTrTD4KYpTPUEtLD0I1lmQ== + "@jridgewell/resolve-uri@^3.0.3": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -570,6 +617,13 @@ dependencies: "@noble/hashes" "1.3.2" +"@noble/curves@1.4.2", "@noble/curves@~1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" + integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== + dependencies: + "@noble/hashes" "1.4.0" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -580,6 +634,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@1.4.0", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -606,53 +665,53 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@nomicfoundation/edr-darwin-arm64@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.6.5.tgz#37a31565d7ef42bed9028ac44aed82144de30bd1" - integrity sha512-A9zCCbbNxBpLgjS1kEJSpqxIvGGAX4cYbpDYCU2f3jVqOwaZ/NU761y1SvuCRVpOwhoCXqByN9b7HPpHi0L4hw== +"@nomicfoundation/edr-darwin-arm64@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.8.0.tgz#70a23214a2dd2941fcb55e47bb4653514d2dae06" + integrity sha512-sKTmOu/P5YYhxT0ThN2Pe3hmCE/5Ag6K/eYoiavjLWbR7HEb5ZwPu2rC3DpuUk1H+UKJqt7o4/xIgJxqw9wu6A== -"@nomicfoundation/edr-darwin-x64@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.6.5.tgz#3252f6e86397af460b7a480bfe1b889464d75b89" - integrity sha512-x3zBY/v3R0modR5CzlL6qMfFMdgwd6oHrWpTkuuXnPFOX8SU31qq87/230f4szM+ukGK8Hi+mNq7Ro2VF4Fj+w== +"@nomicfoundation/edr-darwin-x64@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.8.0.tgz#89c11ae510b3ac5c0e5268cd3a6b04194552112f" + integrity sha512-8ymEtWw1xf1Id1cc42XIeE+9wyo3Dpn9OD/X8GiaMz9R70Ebmj2g+FrbETu8o6UM+aL28sBZQCiCzjlft2yWAg== -"@nomicfoundation/edr-linux-arm64-gnu@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.6.5.tgz#e7dc2934920b6cfabeb5ee7a5e26c8fb0d4964ac" - integrity sha512-HGpB8f1h8ogqPHTyUpyPRKZxUk2lu061g97dOQ/W4CxevI0s/qiw5DB3U3smLvSnBHKOzYS1jkxlMeGN01ky7A== +"@nomicfoundation/edr-linux-arm64-gnu@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.8.0.tgz#02c1b4f426576af4e464320e340855139a00fe9b" + integrity sha512-h/wWzS2EyQuycz+x/SjMRbyA+QMCCVmotRsgM1WycPARvVZWIVfwRRsKoXKdCftsb3S8NTprqBdJlOmsFyETFA== -"@nomicfoundation/edr-linux-arm64-musl@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.6.5.tgz#00459cd53e9fb7bd5b7e32128b508a6e89079d89" - integrity sha512-ESvJM5Y9XC03fZg9KaQg3Hl+mbx7dsSkTIAndoJS7X2SyakpL9KZpOSYrDk135o8s9P9lYJdPOyiq+Sh+XoCbQ== +"@nomicfoundation/edr-linux-arm64-musl@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.8.0.tgz#9b432dca973068f16a33abb70260e904494638dd" + integrity sha512-gnWxDgdkka0O9GpPX/gZT3REeKYV28Guyg13+Vj/bbLpmK1HmGh6Kx+fMhWv+Ht/wEmGDBGMCW1wdyT/CftJaQ== -"@nomicfoundation/edr-linux-x64-gnu@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.6.5.tgz#5c9e4e2655caba48e0196977cba395bbde6fe97d" - integrity sha512-HCM1usyAR1Ew6RYf5AkMYGvHBy64cPA5NMbaeY72r0mpKaH3txiMyydcHibByOGdQ8iFLWpyUdpl1egotw+Tgg== +"@nomicfoundation/edr-linux-x64-gnu@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.8.0.tgz#72954e5fd875df17c43d4ef3fcc381e3312e1347" + integrity sha512-DTMiAkgAx+nyxcxKyxFZk1HPakXXUCgrmei7r5G7kngiggiGp/AUuBBWFHi8xvl2y04GYhro5Wp+KprnLVoAPA== -"@nomicfoundation/edr-linux-x64-musl@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.6.5.tgz#9c220751b66452dc43a365f380e1e236a0a8c5a9" - integrity sha512-nB2uFRyczhAvWUH7NjCsIO6rHnQrof3xcCe6Mpmnzfl2PYcGyxN7iO4ZMmRcQS7R1Y670VH6+8ZBiRn8k43m7A== +"@nomicfoundation/edr-linux-x64-musl@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.8.0.tgz#0d59390c512106010d6f4d94b7fffd99fb7fd8ae" + integrity sha512-iTITWe0Zj8cNqS0xTblmxPbHVWwEtMiDC+Yxwr64d7QBn/1W0ilFQ16J8gB6RVVFU3GpfNyoeg3tUoMpSnrm6Q== -"@nomicfoundation/edr-win32-x64-msvc@0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.6.5.tgz#90d3ac2a6a8a687522bda5ff2e92dd97e68126ea" - integrity sha512-B9QD/4DSSCFtWicO8A3BrsnitO1FPv7axB62wq5Q+qeJ50yJlTmyeGY3cw62gWItdvy2mh3fRM6L1LpnHiB77A== +"@nomicfoundation/edr-win32-x64-msvc@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.8.0.tgz#d14225c513372fda54684de1229cc793ffe48c12" + integrity sha512-mNRDyd/C3j7RMcwapifzv2K57sfA5xOw8g2U84ZDvgSrXVXLC99ZPxn9kmolb+dz8VMm9FONTZz9ESS6v8DTnA== -"@nomicfoundation/edr@^0.6.5": - version "0.6.5" - resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.6.5.tgz#b3b1ebcdd0148cfe67cca128e7ebe8092e200359" - integrity sha512-tAqMslLP+/2b2sZP4qe9AuGxG3OkQ5gGgHE4isUuq6dUVjwCRPFhAOhpdFl+OjY5P3yEv3hmq9HjUGRa2VNjng== +"@nomicfoundation/edr@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.8.0.tgz#63441bb24c1804b6d27b075d0d29f3a02d94fc4f" + integrity sha512-dwWRrghSVBQDpt0wP+6RXD8BMz2i/9TI34TcmZqeEAZuCLei3U9KZRgGTKVAM1rMRvrpf5ROfPqrWNetKVUTag== dependencies: - "@nomicfoundation/edr-darwin-arm64" "0.6.5" - "@nomicfoundation/edr-darwin-x64" "0.6.5" - "@nomicfoundation/edr-linux-arm64-gnu" "0.6.5" - "@nomicfoundation/edr-linux-arm64-musl" "0.6.5" - "@nomicfoundation/edr-linux-x64-gnu" "0.6.5" - "@nomicfoundation/edr-linux-x64-musl" "0.6.5" - "@nomicfoundation/edr-win32-x64-msvc" "0.6.5" + "@nomicfoundation/edr-darwin-arm64" "0.8.0" + "@nomicfoundation/edr-darwin-x64" "0.8.0" + "@nomicfoundation/edr-linux-arm64-gnu" "0.8.0" + "@nomicfoundation/edr-linux-arm64-musl" "0.8.0" + "@nomicfoundation/edr-linux-x64-gnu" "0.8.0" + "@nomicfoundation/edr-linux-x64-musl" "0.8.0" + "@nomicfoundation/edr-win32-x64-msvc" "0.8.0" "@nomicfoundation/ethereumjs-common@4.0.4": version "4.0.4" @@ -720,10 +779,10 @@ json5 "^2.2.3" prompts "^2.4.2" -"@nomicfoundation/hardhat-network-helpers@^1.0.0": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.11.tgz#64096829661b960b88679bd5c4fbcb50654672d1" - integrity sha512-uGPL7QSKvxrHRU69dx8jzoBvuztlLCtyFsbgfXIwIjnO3dqZRz2GNMHJoO3C3dIiUNM6jdNF4AUnoQKDscdYrA== +"@nomicfoundation/hardhat-network-helpers@^1.0.12": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.12.tgz#2c0abec0c50b75f9d0d71776e49e3b5ef746d289" + integrity sha512-xTNQNI/9xkHvjmCJnJOTyqDSl8uq1rKb2WOVmixQxFtRd7Oa3ecO8zM0cyC2YmOK+jHB9WPZ+F/ijkHg1CoORA== dependencies: ethereumjs-util "^7.1.4" @@ -1010,6 +1069,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/base@~1.1.6": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + "@scure/bip32@1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" @@ -1019,6 +1083,15 @@ "@noble/secp256k1" "~1.7.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" + integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== + dependencies: + "@noble/curves" "~1.4.0" + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -1027,6 +1100,14 @@ "@noble/hashes" "~1.2.0" "@scure/base" "~1.1.0" +"@scure/bip39@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.3.0.tgz#0f258c16823ddd00739461ac31398b4e7d6a18c3" + integrity sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ== + dependencies: + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -1109,6 +1190,13 @@ dependencies: antlr4ts "^0.5.0-alpha.4" +"@solidity-parser/parser@^0.16.0": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.2.tgz#42cb1e3d88b3e8029b0c9befff00b634cd92d2fa" + integrity sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg== + dependencies: + antlr4ts "^0.5.0-alpha.4" + "@solidity-parser/parser@^0.19.0": version "0.19.0" resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.19.0.tgz#37a8983b2725af9b14ff8c4a475fa0e98d773c3f" @@ -1367,6 +1455,13 @@ dependencies: "@types/node" "*" +"@types/ws@8.5.3": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== + dependencies: + "@types/node" "*" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1377,6 +1472,11 @@ abbrev@1.0.x: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" integrity sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q== +abitype@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.7.1.tgz#16db20abe67de80f6183cf75f3de1ff86453b745" + integrity sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ== + abstract-leveldown@~2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz#1c5e8c6a5ef965ae8c35dfb3a8770c476b82c4b8" @@ -1403,6 +1503,11 @@ acorn@^8.11.0, acorn@^8.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== +address@^1.0.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" + integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== + adm-zip@^0.4.16: version "0.4.16" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" @@ -1698,6 +1803,13 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + await-semaphore@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3" @@ -1942,6 +2054,14 @@ call-bind-apply-helpers@^1.0.0: es-errors "^1.3.0" function-bind "^1.1.2" +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -1950,7 +2070,7 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -call-bind@^1.0.7: +call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== @@ -1960,6 +2080,14 @@ call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.2" +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + camelcase@^5.0.0: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -2280,7 +2408,7 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -crc-32@^1.2.0: +crc-32@^1.2.0, crc-32@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== @@ -2321,6 +2449,13 @@ cross-fetch@^2.1.0, cross-fetch@^2.1.1: node-fetch "^2.6.7" whatwg-fetch "^2.0.4" +cross-fetch@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.1.0.tgz#8f69355007ee182e47fa692ecbaa37a52e43c3d2" + integrity sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw== + dependencies: + node-fetch "^2.7.0" + "crypt@>= 0.0.1": version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" @@ -2443,6 +2578,14 @@ depd@2.0.0: resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +detect-port@^1.3.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.6.1.tgz#45e4073997c5f292b957cb678fb0bb8ed4250a67" + integrity sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q== + dependencies: + address "^1.0.1" + debug "4" + diff@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -2487,6 +2630,15 @@ dotenv@^16.3.1: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -2659,11 +2811,23 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -2856,6 +3020,16 @@ ethereum-cryptography@^1.0.3: "@scure/bip32" "1.1.5" "@scure/bip39" "1.1.1" +ethereum-cryptography@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz#58f2810f8e020aecb97de8c8c76147600b0b8ccf" + integrity sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg== + dependencies: + "@noble/curves" "1.4.2" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.3.0" + ethereum-protocol@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ethereum-protocol/-/ethereum-protocol-1.0.1.tgz#b7d68142f4105e0ae7b5e178cf42f8d4dc4b93cf" @@ -3069,6 +3243,11 @@ ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: is-hex-prefixed "1.0.0" strip-hex-prefix "1.0.0" +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3236,6 +3415,13 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -3406,11 +3592,35 @@ get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-port@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -3563,6 +3773,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -3607,6 +3822,35 @@ hardhat-contract-sizer@^2.10.0: cli-table3 "^0.6.0" strip-ansi "^6.0.0" +hardhat-coverage@^0.9.19: + version "0.9.19" + resolved "https://registry.yarnpkg.com/hardhat-coverage/-/hardhat-coverage-0.9.19.tgz#455fe5ad46da0a9d19a82fd07902d3ee1300cf03" + integrity sha512-lLn6gBo0fcbixzBrByMWlrxdBCntTm40RbJAimaECJP4uZ57t4CqA2pT2W0koc0fFecYtJASs/YX/3M8IaOd3Q== + dependencies: + "@ethereumjs/common" "^3.2.0" + "@ethereumjs/tx" "^4.2.0" + "@ethersproject/abi" "^5.0.9" + "@solidity-parser/parser" "^0.16.0" + chalk "^2.4.2" + death "^1.1.0" + detect-port "^1.3.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.15" + mocha "7.1.2" + 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 "^4.0.3" + web3-utils "^1.3.6" + hardhat-deploy@^0.12.4: version "0.12.4" resolved "https://registry.yarnpkg.com/hardhat-deploy/-/hardhat-deploy-0.12.4.tgz#5ebef37f1004f52a74987213b0465ad7c9433fb2" @@ -3662,14 +3906,14 @@ hardhat-tracer@^2.6.0: debug "^4.3.4" ethers "^5.6.1" -hardhat@^2.14.0: - version "2.22.17" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.22.17.tgz#96036bbe6bad8eb6a6b65c54dc5fbc1324541612" - integrity sha512-tDlI475ccz4d/dajnADUTRc1OJ3H8fpP9sWhXhBPpYsQOg8JHq5xrDimo53UhWPl7KJmAeDCm1bFG74xvpGRpg== +hardhat@^2.22.19: + version "2.22.19" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.22.19.tgz#92eb6f59e75b0dded841fecf16260a5e3f6eb4eb" + integrity sha512-jptJR5o6MCgNbhd7eKa3mrteR+Ggq1exmE5RUL5ydQEVKcZm0sss5laa86yZ0ixIavIvF4zzS7TdGDuyopj0sQ== dependencies: "@ethersproject/abi" "^5.1.2" "@metamask/eth-sig-util" "^4.0.0" - "@nomicfoundation/edr" "^0.6.5" + "@nomicfoundation/edr" "^0.8.0" "@nomicfoundation/ethereumjs-common" "4.0.4" "@nomicfoundation/ethereumjs-tx" "5.0.4" "@nomicfoundation/ethereumjs-util" "9.0.4" @@ -3756,6 +4000,11 @@ has-symbols@^1.0.0, has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -3763,6 +4012,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -3795,7 +4051,7 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" -hasown@^2.0.0: +hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== @@ -3915,6 +4171,13 @@ immutable@^4.0.0-rc.12: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== +import-sync@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/import-sync/-/import-sync-2.2.3.tgz#09602878c91703e8b4ab2da293d917a739b1d7af" + integrity sha512-ZnF84+eGjetsXwYEuFZADO4eCYr1ngBo6UK476Oq4q6dkiDM1TN+6D5iQ5/e3erCyjo7O6xT3xHE6xdtCgDYhw== + dependencies: + "@httptoolkit/esm" "^3.3.1" + imul@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/imul/-/imul-1.0.1.tgz#9d5867161e8b3de96c2c38d5dc7cb102f35e2ac9" @@ -3964,6 +4227,14 @@ io-ts@1.10.4: dependencies: fp-ts "^1.0.0" +is-arguments@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" @@ -4044,6 +4315,16 @@ is-function@^1.0.1: resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== +is-generator-function@^1.0.7: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== + dependencies: + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -4086,6 +4367,16 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" @@ -4130,6 +4421,13 @@ is-typed-array@^1.1.12: dependencies: which-typed-array "^1.1.11" +is-typed-array@^1.1.3: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -4175,6 +4473,11 @@ isomorphic-unfetch@^3.0.0: node-fetch "^2.6.1" unfetch "^4.2.0" +isomorphic-ws@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -4505,6 +4808,11 @@ match-all@^1.2.6: resolved "https://registry.yarnpkg.com/match-all/-/match-all-1.2.6.tgz#66d276ad6b49655551e63d3a6ee53e8be0566f8d" integrity sha512-0EESkXiTkWzrQQntBu2uzKvLu6vVkUGz40nGPbSZuegcfE5UuSzNjLaIu76zJWuaT/2I3Z/8M06OlUOZLGwLlQ== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -4550,6 +4858,11 @@ merkle-patricia-tree@^2.1.2, merkle-patricia-tree@^2.3.2: rlp "^2.0.0" semaphore ">=1.0.1" +micro-ftch@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" + integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== + micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -4653,6 +4966,36 @@ mnemonist@^0.38.0: dependencies: obliterator "^2.0.0" +mocha@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.2.tgz#8e40d198acf91a52ace122cd7599c9ab857b29e6" + integrity sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA== + dependencies: + ansi-colors "3.2.3" + browser-stdout "1.3.1" + chokidar "3.3.0" + debug "3.2.6" + diff "3.5.0" + escape-string-regexp "1.0.5" + find-up "3.0.0" + glob "7.1.3" + growl "1.10.5" + he "1.2.0" + js-yaml "3.13.1" + log-symbols "3.0.0" + minimatch "3.0.4" + mkdirp "0.5.5" + ms "2.1.1" + node-environment-flags "1.0.6" + object.assign "4.1.0" + strip-json-comments "2.0.1" + supports-color "6.0.0" + which "1.3.1" + wide-align "1.1.3" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.0" + mocha@^10.0.0: version "10.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" @@ -4801,7 +5144,7 @@ node-environment-flags@1.0.6: object.getownpropertydescriptors "^2.0.3" semver "^5.7.0" -node-fetch@^2.6.1: +node-fetch@^2.6.1, node-fetch@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -5098,6 +5441,11 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + precond@0.2: version "0.2.3" resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" @@ -5509,6 +5857,15 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -5733,7 +6090,7 @@ solidity-ast@^0.4.51: dependencies: array.prototype.findlast "^1.2.2" -solidity-coverage@^0.8.0: +solidity-coverage@^0.8.14: version "0.8.14" resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.8.14.tgz#db9bfcc10e3bc369fc074b35b267d665bcc6ae2e" integrity sha512-ItAAObe5GaEOp20kXC2BZRnph+9P7Rtoqg2mQc2SXGEHgSDF2wWd1Wxz3ntzQWXkbCtIIGdJT918HG00cObwbA== @@ -6387,6 +6744,17 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + 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" + uuid@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.1.tgz#c2a30dedb3e535d72ccf82e343941a50ba8533ac" @@ -6421,6 +6789,188 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +web3-core@^4.4.0, web3-core@^4.5.0, web3-core@^4.6.0, web3-core@^4.7.1: + version "4.7.1" + resolved "https://registry.yarnpkg.com/web3-core/-/web3-core-4.7.1.tgz#bc56cd7959fe44ee77139d591211f69851140009" + integrity sha512-9KSeASCb/y6BG7rwhgtYC4CvYY66JfkmGNEYb7q1xgjt9BWfkf09MJPaRyoyT5trdOxYDHkT9tDlypvQWaU8UQ== + dependencies: + web3-errors "^1.3.1" + web3-eth-accounts "^4.3.1" + web3-eth-iban "^4.0.7" + web3-providers-http "^4.2.0" + web3-providers-ws "^4.0.8" + web3-types "^1.10.0" + web3-utils "^4.3.3" + web3-validator "^2.0.6" + optionalDependencies: + web3-providers-ipc "^4.0.7" + +web3-errors@^1.1.3, web3-errors@^1.2.0, web3-errors@^1.3.0, web3-errors@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/web3-errors/-/web3-errors-1.3.1.tgz#163bc4d869f98614760b683d733c3ed1fb415d98" + integrity sha512-w3NMJujH+ZSW4ltIZZKtdbkbyQEvBzyp3JRn59Ckli0Nz4VMsVq8aF1bLWM7A2kuQ+yVEm3ySeNU+7mSRwx7RQ== + dependencies: + web3-types "^1.10.0" + +web3-eth-abi@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/web3-eth-abi/-/web3-eth-abi-4.4.1.tgz#1dca9d80341b3cd7a1ae07dc98080c2073d62a29" + integrity sha512-60ecEkF6kQ9zAfbTY04Nc9q4eEYM0++BySpGi8wZ2PD1tw/c0SDvsKhV6IKURxLJhsDlb08dATc3iD6IbtWJmg== + dependencies: + abitype "0.7.1" + web3-errors "^1.3.1" + web3-types "^1.10.0" + web3-utils "^4.3.3" + web3-validator "^2.0.6" + +web3-eth-accounts@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/web3-eth-accounts/-/web3-eth-accounts-4.3.1.tgz#6712ea915940d03d596015a87f9167171e8306a6" + integrity sha512-rTXf+H9OKze6lxi7WMMOF1/2cZvJb2AOnbNQxPhBDssKOllAMzLhg1FbZ4Mf3lWecWfN6luWgRhaeSqO1l+IBQ== + dependencies: + "@ethereumjs/rlp" "^4.0.1" + crc-32 "^1.2.2" + ethereum-cryptography "^2.0.0" + web3-errors "^1.3.1" + web3-types "^1.10.0" + web3-utils "^4.3.3" + web3-validator "^2.0.6" + +web3-eth-contract@^4.5.0, web3-eth-contract@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/web3-eth-contract/-/web3-eth-contract-4.7.2.tgz#a1851e566ceb4b0da3792ff4d8f7cb6fd91d3401" + integrity sha512-3ETqs2pMNPEAc7BVY/C3voOhTUeJdkf2aM3X1v+edbngJLHAxbvxKpOqrcO0cjXzC4uc2Q8Zpf8n8zT5r0eLnA== + dependencies: + "@ethereumjs/rlp" "^5.0.2" + web3-core "^4.7.1" + web3-errors "^1.3.1" + web3-eth "^4.11.1" + web3-eth-abi "^4.4.1" + web3-types "^1.10.0" + web3-utils "^4.3.3" + web3-validator "^2.0.6" + +web3-eth-ens@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/web3-eth-ens/-/web3-eth-ens-4.4.0.tgz#bc0d11d755cb15ed4b82e38747c5104622d9a4b9" + integrity sha512-DeyVIS060hNV9g8dnTx92syqvgbvPricE3MerCxe/DquNZT3tD8aVgFfq65GATtpCgDDJffO2bVeHp3XBemnSQ== + dependencies: + "@adraffy/ens-normalize" "^1.8.8" + web3-core "^4.5.0" + web3-errors "^1.2.0" + web3-eth "^4.8.0" + web3-eth-contract "^4.5.0" + web3-net "^4.1.0" + web3-types "^1.7.0" + web3-utils "^4.3.0" + web3-validator "^2.0.6" + +web3-eth-iban@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/web3-eth-iban/-/web3-eth-iban-4.0.7.tgz#ee504f845d7b6315f0be78fcf070ccd5d38e4aaf" + integrity sha512-8weKLa9KuKRzibC87vNLdkinpUE30gn0IGY027F8doeJdcPUfsa4IlBgNC4k4HLBembBB2CTU0Kr/HAOqMeYVQ== + dependencies: + web3-errors "^1.1.3" + web3-types "^1.3.0" + web3-utils "^4.0.7" + web3-validator "^2.0.3" + +web3-eth-personal@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/web3-eth-personal/-/web3-eth-personal-4.1.0.tgz#f5b506a4570bf1241d1db2de12cb413ea0bb4486" + integrity sha512-RFN83uMuvA5cu1zIwwJh9A/bAj0OBxmGN3tgx19OD/9ygeUZbifOL06jgFzN0t+1ekHqm3DXYQM8UfHpXi7yDQ== + dependencies: + web3-core "^4.6.0" + web3-eth "^4.9.0" + web3-rpc-methods "^1.3.0" + web3-types "^1.8.0" + web3-utils "^4.3.1" + web3-validator "^2.0.6" + +web3-eth@^4.11.1, web3-eth@^4.8.0, web3-eth@^4.9.0: + version "4.11.1" + resolved "https://registry.yarnpkg.com/web3-eth/-/web3-eth-4.11.1.tgz#f558ab1482d4196de0f0a7da5985de42d06664ea" + integrity sha512-q9zOkzHnbLv44mwgLjLXuyqszHuUgZWsQayD2i/rus2uk0G7hMn11bE2Q3hOVnJS4ws4VCtUznlMxwKQ+38V2w== + dependencies: + setimmediate "^1.0.5" + web3-core "^4.7.1" + web3-errors "^1.3.1" + web3-eth-abi "^4.4.1" + web3-eth-accounts "^4.3.1" + web3-net "^4.1.0" + web3-providers-ws "^4.0.8" + web3-rpc-methods "^1.3.0" + web3-types "^1.10.0" + web3-utils "^4.3.3" + web3-validator "^2.0.6" + +web3-net@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/web3-net/-/web3-net-4.1.0.tgz#db7bde675e58b153339e4f149f29ec0410d6bab2" + integrity sha512-WWmfvHVIXWEoBDWdgKNYKN8rAy6SgluZ0abyRyXOL3ESr7ym7pKWbfP4fjApIHlYTh8tNqkrdPfM4Dyi6CA0SA== + dependencies: + web3-core "^4.4.0" + web3-rpc-methods "^1.3.0" + web3-types "^1.6.0" + web3-utils "^4.3.0" + +web3-providers-http@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/web3-providers-http/-/web3-providers-http-4.2.0.tgz#0f4bf424681a068d49994aa7fabc69bed45ac50b" + integrity sha512-IPMnDtHB7dVwaB7/mMxAZzyq7d5ezfO1+Vw0bNfAeIi7gaDlJiggp85SdyAfOgov8AMUA/dyiY72kQ0KmjXKvQ== + dependencies: + cross-fetch "^4.0.0" + web3-errors "^1.3.0" + web3-types "^1.7.0" + web3-utils "^4.3.1" + +web3-providers-ipc@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/web3-providers-ipc/-/web3-providers-ipc-4.0.7.tgz#9ec4c8565053af005a5170ba80cddeb40ff3e3d3" + integrity sha512-YbNqY4zUvIaK2MHr1lQFE53/8t/ejHtJchrWn9zVbFMGXlTsOAbNoIoZWROrg1v+hCBvT2c9z8xt7e/+uz5p1g== + dependencies: + web3-errors "^1.1.3" + web3-types "^1.3.0" + web3-utils "^4.0.7" + +web3-providers-ws@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/web3-providers-ws/-/web3-providers-ws-4.0.8.tgz#6de7b262f7ec6df1a2dff466ba91d7ebdac2c45e" + integrity sha512-goJdgata7v4pyzHRsg9fSegUG4gVnHZSHODhNnn6J93ykHkBI1nz4fjlGpcQLUMi4jAMz6SHl9Ibzs2jj9xqPw== + dependencies: + "@types/ws" "8.5.3" + isomorphic-ws "^5.0.0" + web3-errors "^1.2.0" + web3-types "^1.7.0" + web3-utils "^4.3.1" + ws "^8.17.1" + +web3-rpc-methods@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/web3-rpc-methods/-/web3-rpc-methods-1.3.0.tgz#d5ee299a69389d63822d354ddee2c6a121a6f670" + integrity sha512-/CHmzGN+IYgdBOme7PdqzF+FNeMleefzqs0LVOduncSaqsppeOEoskLXb2anSpzmQAP3xZJPaTrkQPWSJMORig== + dependencies: + web3-core "^4.4.0" + web3-types "^1.6.0" + web3-validator "^2.0.6" + +web3-rpc-providers@^1.0.0-rc.4: + version "1.0.0-rc.4" + resolved "https://registry.yarnpkg.com/web3-rpc-providers/-/web3-rpc-providers-1.0.0-rc.4.tgz#93cec88175eb2f7972e12be30af4c2f296b1923f" + integrity sha512-PXosCqHW0EADrYzgmueNHP3Y5jcSmSwH+Dkqvn7EYD0T2jcsdDAIHqk6szBiwIdhumM7gv9Raprsu/s/f7h1fw== + dependencies: + web3-errors "^1.3.1" + web3-providers-http "^4.2.0" + web3-providers-ws "^4.0.8" + web3-types "^1.10.0" + web3-utils "^4.3.3" + web3-validator "^2.0.6" + +web3-types@^1.10.0, web3-types@^1.3.0, web3-types@^1.6.0, web3-types@^1.7.0, web3-types@^1.8.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/web3-types/-/web3-types-1.10.0.tgz#41b0b4d2dd75e919d5b6f37bf139e29f445db04e" + integrity sha512-0IXoaAFtFc8Yin7cCdQfB9ZmjafrbP6BO0f0KT/khMhXKUpoJ6yShrVhiNpyRBo8QQjuOagsWzwSK2H49I7sbw== + web3-utils@^1.3.6: version "1.10.0" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.0.tgz#ca4c1b431a765c14ac7f773e92e0fd9377ccf578" @@ -6434,6 +6984,51 @@ web3-utils@^1.3.6: randombytes "^2.1.0" utf8 "3.0.0" +web3-utils@^4.0.7, web3-utils@^4.3.0, web3-utils@^4.3.1, web3-utils@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-4.3.3.tgz#e380a1c03a050d3704f94bd08c1c9f50a1487205" + integrity sha512-kZUeCwaQm+RNc2Bf1V3BYbF29lQQKz28L0y+FA4G0lS8IxtJVGi5SeDTUkpwqqkdHHC7JcapPDnyyzJ1lfWlOw== + dependencies: + ethereum-cryptography "^2.0.0" + eventemitter3 "^5.0.1" + web3-errors "^1.3.1" + web3-types "^1.10.0" + web3-validator "^2.0.6" + +web3-validator@^2.0.3, web3-validator@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/web3-validator/-/web3-validator-2.0.6.tgz#a0cdaa39e1d1708ece5fae155b034e29d6a19248" + integrity sha512-qn9id0/l1bWmvH4XfnG/JtGKKwut2Vokl6YXP5Kfg424npysmtRLe9DgiNBM9Op7QL/aSiaA0TVXibuIuWcizg== + dependencies: + ethereum-cryptography "^2.0.0" + util "^0.12.5" + web3-errors "^1.2.0" + web3-types "^1.6.0" + zod "^3.21.4" + +web3@^4.0.3: + version "4.16.0" + resolved "https://registry.yarnpkg.com/web3/-/web3-4.16.0.tgz#1da10d8405bf27a76de6cbbce3de9fa93f7c0449" + integrity sha512-SgoMSBo6EsJ5GFCGar2E/pR2lcR/xmUSuQ61iK6yDqzxmm42aPPxSqZfJz2z/UCR6pk03u77pU8TGV6lgMDdIQ== + dependencies: + web3-core "^4.7.1" + web3-errors "^1.3.1" + web3-eth "^4.11.1" + web3-eth-abi "^4.4.1" + web3-eth-accounts "^4.3.1" + web3-eth-contract "^4.7.2" + web3-eth-ens "^4.4.0" + web3-eth-iban "^4.0.7" + web3-eth-personal "^4.1.0" + web3-net "^4.1.0" + web3-providers-http "^4.2.0" + web3-providers-ws "^4.0.8" + web3-rpc-methods "^1.3.0" + web3-rpc-providers "^1.0.0-rc.4" + web3-types "^1.10.0" + web3-utils "^4.3.3" + web3-validator "^2.0.6" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -6479,6 +7074,19 @@ which-typed-array@^1.1.11: gopd "^1.0.1" has-tostringtag "^1.0.0" +which-typed-array@^1.1.16, which-typed-array@^1.1.2: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" @@ -6585,6 +7193,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.17.1: + version "8.18.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" + integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== + xhr@^2.2.0: version "2.6.0" resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" @@ -6714,3 +7327,8 @@ zksync-ethers@^5.0.0: integrity sha512-OAjTGAHF9wbdkRGkj7XZuF/a1Sk/FVbwH4pmLjAKlR7mJ7sQtQhBhrPU2dCc67xLaNvEESPfwil19ES5wooYFg== dependencies: ethers "~5.7.0" + +zod@^3.21.4: + version "3.24.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.4.tgz#e2e2cca5faaa012d76e527d0d36622e0a90c315f" + integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg== diff --git a/ratio.uml b/ratio.uml new file mode 100644 index 00000000..79064e87 --- /dev/null +++ b/ratio.uml @@ -0,0 +1,58 @@ + +participant User #LightBlue +participant InceptionVault_S as Vault #LightYellow +participant IOperator +participant AdapterHandler as AdHandler + +@startuml + +title Base flow + +== Deposit == + +group "Deposit Process" +User -> Vault: //deposit(assets)// 10 ETH +note left: or //depositWithReferral(assets, code)//\nor //mint(shares)// +Vault -> Vault: //vaultBalance// = 10, supply = 10 +Vault -> Vault: Calc bonus +Vault -> User: //mint()// shares + +IOperator -> AdHandler: //delegate()// 10 ETH +AdHandler -> AdHandler: vaultBalance = 0, totalDelegated = 10 +group end + + +== Withdrawal == + +User -> Vault: withdraw 5 ETH +Vault -> Vault: supply = 5, totalSharesToWithdraw = 5 + +IOperator -> AdHandler: undelegate 5 ETH +AdHandler -> AdHandler: totalDelegated = 5, totalSharesToWithdraw = 0 + +IOperator -> AdHandler: claim 5 ETH +AdHandler -> AdHandler: redeemReservedAmount = 5; vaultBalance = 5; + +User -> Vault: redeem 5 ETH +Vault -> Vault: redeemReservedAmount = 0; vaultBalnace = 0; + + +== Emergency Flow == + +IOperator -> AdHandler: emergencyUndelegate 2 ETH +AdHandler -> AdHandler: totalDelegated = 3, emergencyPendingWithdrawals = 2 + +IOperator -> AdHandler: emergencyClaim 2 ETH +AdHandler -> AdHandler: emergencyPendingWithdrawals = 0; vaultBalance = 2 + +User -> Vault: withdraw 1.5 ETH +Vault -> Vault: supply = 3.5, totalSharesToWithdraw = 1.5 + +IOperator -> AdHandler: force undelegateAndClaim 1.5 ETH +AdHandler -> AdHandler: redeemReservedAmount = 1.5, totalSharesToWithdraw = 0 + + +User -> Vault: redeem 1.5 ETH +Vault -> Vault: redeemReservedAmount = 0; vaultBalance = 0.5 + +@enduml \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ae11c80a..6230df43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,6 +9,72 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.20.0.tgz#7a1232e82376712d3340012a2f561a2764d1988f" + integrity sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ== + dependencies: + "@eslint/object-schema" "^2.1.6" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/config-helpers@^0.2.1": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.2.tgz#3779f76b894de3a8ec4763b79660e6d54d5b1010" + integrity sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg== + +"@eslint/core@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.13.0.tgz#bf02f209846d3bf996f9e8009db62df2739b458c" + integrity sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964" + integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.26.0", "@eslint/js@^9.26.0": + version "9.26.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.26.0.tgz#1e13126b67a3db15111d2dcc61f69a2acff70bd5" + integrity sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ== + +"@eslint/object-schema@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" + integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== + +"@eslint/plugin-kit@^0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8" + integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA== + dependencies: + "@eslint/core" "^0.13.0" + levn "^0.4.1" + "@ethersproject/abi@^5.1.2": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" @@ -191,6 +257,34 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" + integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== + +"@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + "@jridgewell/resolve-uri@^3.0.3": version "3.1.2" resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" @@ -220,6 +314,22 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" +"@modelcontextprotocol/sdk@^1.8.0": + version "1.11.2" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz#d81784c140d1a9cc937f61af9f071d8b78befe30" + integrity sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ== + dependencies: + content-type "^1.0.5" + cors "^2.8.5" + cross-spawn "^7.0.3" + eventsource "^3.0.2" + express "^5.0.1" + express-rate-limit "^7.5.0" + pkce-challenge "^5.0.0" + raw-body "^3.0.0" + zod "^3.23.8" + zod-to-json-schema "^3.24.1" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -230,6 +340,27 @@ resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@nomicfoundation/edr-darwin-arm64@0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.5.2.tgz#72f7a826c9f0f2c91308edca562de3b9484ac079" @@ -485,6 +616,16 @@ dependencies: "@types/node" "*" +"@types/estree@^1.0.6": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/lru-cache@^5.1.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.1.tgz#c48c2e27b65d2a153b19bfc1a317e30872e01eef" @@ -511,6 +652,100 @@ dependencies: "@types/node" "*" +"@typescript-eslint/eslint-plugin@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz#86630dd3084f9d6c4239bbcd6a7ee1a7ee844f7f" + integrity sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.32.0" + "@typescript-eslint/type-utils" "8.32.0" + "@typescript-eslint/utils" "8.32.0" + "@typescript-eslint/visitor-keys" "8.32.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/parser@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.32.0.tgz#fe840ecb2726a82fa9f5562837ec40503ae71caf" + integrity sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A== + dependencies: + "@typescript-eslint/scope-manager" "8.32.0" + "@typescript-eslint/types" "8.32.0" + "@typescript-eslint/typescript-estree" "8.32.0" + "@typescript-eslint/visitor-keys" "8.32.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz#6be89f652780f0d3d19d58dc0ee107b1a9e3282c" + integrity sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ== + dependencies: + "@typescript-eslint/types" "8.32.0" + "@typescript-eslint/visitor-keys" "8.32.0" + +"@typescript-eslint/type-utils@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz#5e0882393e801963f749bea38888e716045fe895" + integrity sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg== + dependencies: + "@typescript-eslint/typescript-estree" "8.32.0" + "@typescript-eslint/utils" "8.32.0" + debug "^4.3.4" + ts-api-utils "^2.1.0" + +"@typescript-eslint/types@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.32.0.tgz#a4a66b8876b8391970cf069b49572e43f1fc957a" + integrity sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA== + +"@typescript-eslint/typescript-estree@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz#11d45f47bfabb141206a3da6c7b91a9d869ff32d" + integrity sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ== + dependencies: + "@typescript-eslint/types" "8.32.0" + "@typescript-eslint/visitor-keys" "8.32.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/utils@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.32.0.tgz#24570f68cf845d198b73a7f94ca88d8c2505ba47" + integrity sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw== + dependencies: + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.32.0" + "@typescript-eslint/types" "8.32.0" + "@typescript-eslint/typescript-estree" "8.32.0" + +"@typescript-eslint/visitor-keys@8.32.0": + version "8.32.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz#0cca2cac046bc71cc40ce8214bac2850d6ecf4a6" + integrity sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w== + dependencies: + "@typescript-eslint/types" "8.32.0" + eslint-visitor-keys "^4.2.0" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + acorn-walk@^8.1.1: version "8.3.3" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz" @@ -523,6 +758,11 @@ acorn@^8.11.0, acorn@^8.4.1: resolved "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +acorn@^8.14.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + adm-zip@^0.4.16: version "0.4.16" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" @@ -543,6 +783,16 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + ansi-align@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" @@ -631,6 +881,21 @@ bn.js@^5.2.0, bn.js@^5.2.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== +body-parser@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa" + integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.0" + http-errors "^2.0.0" + iconv-lite "^0.6.3" + on-finished "^2.4.1" + qs "^6.14.0" + raw-body "^3.0.0" + type-is "^2.0.0" + boxen@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" @@ -660,7 +925,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -715,11 +980,32 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== -bytes@3.1.2: +bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + camelcase@^6.0.0, camelcase@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" @@ -734,7 +1020,7 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -828,11 +1114,41 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +content-disposition@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2" + integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + cookie@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -861,6 +1177,15 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-spawn@^7.0.3, cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + debug@4, debug@^4.1.1, debug@^4.3.5: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" @@ -868,12 +1193,24 @@ debug@4, debug@^4.1.1, debug@^4.3.5: dependencies: ms "2.1.2" +debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decamelize@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -depd@2.0.0: +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -893,6 +1230,20 @@ dotenv@^16.3.1: resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + elliptic@6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" @@ -924,6 +1275,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enquirer@^2.3.0: version "2.4.1" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" @@ -937,11 +1293,33 @@ env-paths@^2.2.0: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -952,6 +1330,110 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +eslint-plugin-chai-friendly@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.0.1.tgz#c3290b5294c1145934cf9c07eaa4cec87921d18c" + integrity sha512-dxD/uz1YKJ8U4yah1i+V/p/u+kHRy3YxTPe2nJGqb5lCR+ucan/KIexfZ5+q4X+tkllyMe86EBbAkdlwxNy3oQ== + +eslint-scope@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.3.0.tgz#10cd3a918ffdd722f5f3f7b5b83db9b23c87340d" + integrity sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@^9.26.0: + version "9.26.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.26.0.tgz#978fe029adc2aceed28ab437bca876e83461c3b4" + integrity sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.20.0" + "@eslint/config-helpers" "^0.2.1" + "@eslint/core" "^0.13.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.26.0" + "@eslint/plugin-kit" "^0.2.8" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@modelcontextprotocol/sdk" "^1.8.0" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.3.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + zod "^3.24.2" + +espree@^10.0.1, espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== + dependencies: + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" + +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + ethereum-cryptography@0.1.3, ethereum-cryptography@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-0.1.3.tgz#8d6143cfc3d74bf79bbd8edecdf29e4ae20dd191" @@ -1012,6 +1494,18 @@ ethjs-util@0.1.6, ethjs-util@^0.1.6: is-hex-prefixed "1.0.0" strip-hex-prefix "1.0.0" +eventsource-parser@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.1.tgz#5e358dba9a55ba64ca90da883c4ca35bd82467bd" + integrity sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA== + +eventsource@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + evp_bytestokey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" @@ -1020,6 +1514,84 @@ evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" +express-rate-limit@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.0.tgz#6a67990a724b4fbbc69119419feef50c51e8b28f" + integrity sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg== + +express@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9" + integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.0" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + 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.8" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" @@ -1027,6 +1599,18 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +finalhandler@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f" + integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -1042,16 +1626,34 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + flat@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.3.tgz#67c8fad95454a7c7abebf74bb78ee74a44023358" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + follow-redirects@^1.12.1: version "1.15.6" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + fp-ts@1.19.3: version "1.19.3" resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.3.tgz#261a60d1088fbff01f91256f91d21d0caaaaa96f" @@ -1062,6 +1664,11 @@ fp-ts@^1.0.0: resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-1.19.5.tgz#3da865e585dfa1fdfd51785417357ac50afc520a" integrity sha512-wDNqTimnzs8QqpldiId9OavWK2NptormjXnRJTQecNjzwfyp6P/8s/zG8e4h3ja3oqkKaY72UlTjQYt/1yXf9A== +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -1081,18 +1688,54 @@ fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -glob-parent@~5.1.2: +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob@7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -1116,11 +1759,26 @@ glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + hardhat@^2.22.10: version "2.22.10" resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.22.10.tgz#826ab56e47af98406e6dd105ba6d2dbb148013d9" @@ -1180,6 +1838,11 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + hash-base@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" @@ -1197,6 +1860,13 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -1211,7 +1881,7 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -1237,11 +1907,36 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ignore@^5.2.0, ignore@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + immutable@^4.0.0-rc.12: version "4.3.6" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -1267,6 +1962,11 @@ io-ts@1.10.4: dependencies: fp-ts "^1.0.0" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1284,7 +1984,7 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -1306,11 +2006,21 @@ is-plain-obj@^2.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + js-sha3@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" @@ -1323,6 +2033,21 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -1339,6 +2064,21 @@ keccak@^3.0.0, keccak@^3.0.2: node-gyp-build "^4.2.0" readable-stream "^3.6.0" +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -1354,6 +2094,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash@^4.17.11: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -1377,6 +2122,11 @@ make-error@^1.1.1: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -1386,11 +2136,46 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== + dependencies: + mime-db "^1.54.0" + minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -1401,7 +2186,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.4: +minimatch@^3.0.4, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1415,6 +2200,13 @@ minimatch@^5.0.1, minimatch@^5.1.6: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + mnemonist@^0.38.0: version "0.38.5" resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" @@ -1458,6 +2250,16 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + node-addon-api@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" @@ -1473,18 +2275,47 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + obliterator@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== -once@^1.3.0: +on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -1530,6 +2361,18 @@ p-try@^1.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parseurl@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -1545,11 +2388,21 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + pbkdf2@^3.0.17: version "3.1.2" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" @@ -1561,11 +2414,21 @@ pbkdf2@^3.0.17: safe-buffer "^5.0.1" sha.js "^2.4.8" -picomatch@^2.0.4, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pkce-challenge@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz#c3a405cb49e272094a38e890a2b51da0228c4d97" + integrity sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prettier-plugin-solidity@^1.2.0: version "1.3.1" resolved "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz" @@ -1580,6 +2443,31 @@ prettier@^3.1.0: resolved "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz" integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== +proxy-addr@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" + integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== + dependencies: + side-channel "^1.1.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -1587,6 +2475,11 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + raw-body@^2.4.1: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" @@ -1597,6 +2490,16 @@ raw-body@^2.4.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -1618,6 +2521,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve@1.17.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" @@ -1625,6 +2533,11 @@ resolve@1.17.0: dependencies: path-parse "^1.0.6" +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -1640,12 +2553,30 @@ rlp@^2.2.3: dependencies: bn.js "^5.2.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -1679,6 +2610,28 @@ semver@^7.5.4: resolved "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +semver@^7.6.0: + version "7.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" + integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== + +send@^1.1.0, send@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212" + integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== + dependencies: + debug "^4.3.5" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.0" + mime-types "^3.0.1" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.1" + serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -1686,6 +2639,16 @@ serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.0" +serve-static@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9" + integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" @@ -1704,6 +2667,58 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + solc@0.8.26: version "0.8.26" resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.26.tgz#afc78078953f6ab3e727c338a2fefcd80dd5b01a" @@ -1742,7 +2757,7 @@ stacktrace-parser@^0.1.10: dependencies: type-fest "^0.7.1" -statuses@2.0.1: +statuses@2.0.1, statuses@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== @@ -1822,6 +2837,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== + ts-node@^10.9.2: version "10.9.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" @@ -1861,6 +2881,13 @@ tweetnacl@^1.0.3: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -1876,10 +2903,28 @@ type-fest@^0.7.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== -typescript@^5.5.3: - version "5.5.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz" - integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== +type-is@^2.0.0, type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + +typescript-eslint@^8.32.0: + version "8.32.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.32.0.tgz#032cf9176d987caff291990ea6313bf4c0b63b4e" + integrity sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A== + dependencies: + "@typescript-eslint/eslint-plugin" "8.32.0" + "@typescript-eslint/parser" "8.32.0" + "@typescript-eslint/utils" "8.32.0" + +typescript@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== undici-types@~5.26.4: version "5.26.5" @@ -1903,6 +2948,13 @@ unpipe@1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -1918,6 +2970,18 @@ v8-compile-cache-lib@^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== +vary@^1, vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" @@ -1925,6 +2989,11 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + workerpool@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" @@ -1991,3 +3060,13 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod-to-json-schema@^3.24.1: + version "3.24.5" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3" + integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== + +zod@^3.23.8, zod@^3.24.2: + version "3.24.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.4.tgz#e2e2cca5faaa012d76e527d0d36622e0a90c315f" + integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==