From 98ac8ac18b77ae4c2af7dd01356533873f34a759 Mon Sep 17 00:00:00 2001 From: swayam karle Date: Mon, 21 Oct 2024 04:20:58 +0530 Subject: [PATCH 1/7] Init Contract for Hookathon with basic test cases --- .../foundry/contracts/hooks/Hookathon/.czrc | 3 + .../contracts/hooks/Hookathon/.editorconfig | 16 + .../contracts/hooks/Hookathon/.env.example | 13 + .../hooks/Hookathon/.github/workflows/ci.yml | 51 + .../contracts/hooks/Hookathon/.gitignore | 31 + .../contracts/hooks/Hookathon/.gitpod.yml | 10 + .../contracts/hooks/Hookathon/.prettierignore | 22 + .../contracts/hooks/Hookathon/.prettierrc.yml | 23 + .../contracts/hooks/Hookathon/.solcover.js | 11 + .../contracts/hooks/Hookathon/.solhint.json | 24 + .../contracts/hooks/Hookathon/.solhintignore | 3 + .../hooks/Hookathon/CustomProvider.ts | 62 + .../contracts/hooks/Hookathon/LICENSE.md | 16 + .../contracts/hooks/Hookathon/Makefile | 50 + .../contracts/hooks/Hookathon/README.md | 232 + .../Hookathon/contracts/EncryptedERC20.sol | 167 + .../contracts/interfaces/BaseHooks.sol | 122 + .../contracts/interfaces/EnumerableMap.sol | 449 ++ .../contracts/interfaces/FixedPoint.sol | 155 + .../interfaces/IAllowanceTransfer.sol | 165 + .../contracts/interfaces/IAuthentication.sol | 16 + .../contracts/interfaces/IAuthorizer.sol | 15 + .../contracts/interfaces/IBasePoolFactory.sol | 50 + .../contracts/interfaces/IEIP712.sol | 6 + .../Hookathon/contracts/interfaces/IHooks.sol | 247 + .../interfaces/IProtocolFeeController.sol | 315 + .../contracts/interfaces/IRateProvider.sol | 12 + .../contracts/interfaces/IRouterCommon.sol | 92 + .../Hookathon/contracts/interfaces/IVault.sol | 15 + .../contracts/interfaces/IVaultAdmin.sol | 380 ++ .../contracts/interfaces/IVaultErrors.sol | 430 ++ .../contracts/interfaces/IVaultEvents.sol | 215 + .../contracts/interfaces/IVaultExtension.sol | 469 ++ .../contracts/interfaces/IVaultMain.sol | 173 + .../contracts/interfaces/LogExpMath.sol | 555 ++ .../contracts/interfaces/VaultGuard.sol | 26 + .../contracts/interfaces/VaultTypes.sol | 418 ++ .../Hookathon/contracts/secretLottery.sol | 210 + .../contracts/hooks/Hookathon/default.toml | 56 + .../hooks/Hookathon/deploy/deploy.ts | 0 .../hooks/Hookathon/deploy/instance.ts | 0 .../docker-compose-full.yml.template | 98 + .../hooks/Hookathon/eslint.config.mjs | 17 + .../hooks/Hookathon/hardhat.config.ts | 186 + .../contracts/hooks/Hookathon/launch-fhevm.sh | 23 + .../contracts/hooks/Hookathon/package.json | 107 + .../contracts/hooks/Hookathon/pnpm-lock.yaml | 6063 +++++++++++++++++ .../contracts/hooks/Hookathon/remappings.txt | 7 + .../hooks/Hookathon/scripts/copy_fhe_keys.sh | 103 + .../scripts/fund_test_addresses_docker.sh | 60 + .../Hookathon/scripts/get_kms_core_version.sh | 23 + .../Hookathon/scripts/get_repository_info.sh | 25 + .../Hookathon/scripts/precomputeAddresses.sh | 38 + .../hooks/Hookathon/scripts/prepare_test.sh | 9 + .../scripts/prepare_volumes_from_kms_core.sh | 99 + .../scripts/rewrite-docker-compose.sh | 23 + .../contracts/hooks/Hookathon/setup.sh | 151 + .../hooks/Hookathon/tasks/accounts.ts | 9 + .../hooks/Hookathon/tasks/checkNodeVersion.js | 2 + .../hooks/Hookathon/tasks/deployERC20.ts | 10 + .../Hookathon/tasks/getEthereumAddress.ts | 34 + .../hooks/Hookathon/tasks/taskDeploy.ts | 65 + .../Hookathon/tasks/taskGatewayRelayer.ts | 149 + .../Hookathon/tasks/taskOracleRelayer.ts | 145 + .../hooks/Hookathon/tasks/taskTFHE.ts | 97 + .../hooks/Hookathon/test/asyncDecrypt.ts | 160 + .../hooks/Hookathon/test/coprocessorUtils.ts | 893 +++ .../encryptedERC20/EncryptedERC20.fixture.ts | 15 + .../test/encryptedERC20/EncryptedERC20.ts | 114 + .../hooks/Hookathon/test/execution.ts | 289 + .../hooks/Hookathon/test/fhevmjsMocked.ts | 302 + .../hooks/Hookathon/test/instance.ts | 196 + .../contracts/hooks/Hookathon/test/signers.ts | 29 + .../contracts/hooks/Hookathon/test/types.ts | 19 + .../contracts/hooks/Hookathon/test/utils.ts | 97 + .../contracts/hooks/Hookathon/tsconfig.json | 22 + 76 files changed, 15004 insertions(+) create mode 100644 packages/foundry/contracts/hooks/Hookathon/.czrc create mode 100644 packages/foundry/contracts/hooks/Hookathon/.editorconfig create mode 100644 packages/foundry/contracts/hooks/Hookathon/.env.example create mode 100644 packages/foundry/contracts/hooks/Hookathon/.github/workflows/ci.yml create mode 100644 packages/foundry/contracts/hooks/Hookathon/.gitignore create mode 100644 packages/foundry/contracts/hooks/Hookathon/.gitpod.yml create mode 100644 packages/foundry/contracts/hooks/Hookathon/.prettierignore create mode 100644 packages/foundry/contracts/hooks/Hookathon/.prettierrc.yml create mode 100644 packages/foundry/contracts/hooks/Hookathon/.solcover.js create mode 100644 packages/foundry/contracts/hooks/Hookathon/.solhint.json create mode 100644 packages/foundry/contracts/hooks/Hookathon/.solhintignore create mode 100644 packages/foundry/contracts/hooks/Hookathon/CustomProvider.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/LICENSE.md create mode 100644 packages/foundry/contracts/hooks/Hookathon/Makefile create mode 100644 packages/foundry/contracts/hooks/Hookathon/README.md create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/EncryptedERC20.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/BaseHooks.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/EnumerableMap.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/FixedPoint.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAllowanceTransfer.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthentication.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthorizer.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IBasePoolFactory.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IEIP712.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IHooks.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IProtocolFeeController.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRateProvider.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRouterCommon.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVault.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultAdmin.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultErrors.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultEvents.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultExtension.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultMain.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/LogExpMath.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultGuard.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultTypes.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/contracts/secretLottery.sol create mode 100644 packages/foundry/contracts/hooks/Hookathon/default.toml create mode 100644 packages/foundry/contracts/hooks/Hookathon/deploy/deploy.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/deploy/instance.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/docker-compose/docker-compose-full.yml.template create mode 100644 packages/foundry/contracts/hooks/Hookathon/eslint.config.mjs create mode 100644 packages/foundry/contracts/hooks/Hookathon/hardhat.config.ts create mode 100755 packages/foundry/contracts/hooks/Hookathon/launch-fhevm.sh create mode 100644 packages/foundry/contracts/hooks/Hookathon/package.json create mode 100644 packages/foundry/contracts/hooks/Hookathon/pnpm-lock.yaml create mode 100644 packages/foundry/contracts/hooks/Hookathon/remappings.txt create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/copy_fhe_keys.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/fund_test_addresses_docker.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/get_kms_core_version.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/get_repository_info.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/precomputeAddresses.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/prepare_test.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/prepare_volumes_from_kms_core.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/scripts/rewrite-docker-compose.sh create mode 100755 packages/foundry/contracts/hooks/Hookathon/setup.sh create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/accounts.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/checkNodeVersion.js create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/deployERC20.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/getEthereumAddress.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/taskDeploy.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/taskGatewayRelayer.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/taskOracleRelayer.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/tasks/taskTFHE.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/asyncDecrypt.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/coprocessorUtils.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.fixture.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/execution.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/fhevmjsMocked.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/instance.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/signers.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/types.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/test/utils.ts create mode 100644 packages/foundry/contracts/hooks/Hookathon/tsconfig.json diff --git a/packages/foundry/contracts/hooks/Hookathon/.czrc b/packages/foundry/contracts/hooks/Hookathon/.czrc new file mode 100644 index 00000000..d1bcc209 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.czrc @@ -0,0 +1,3 @@ +{ + "path": "cz-conventional-changelog" +} diff --git a/packages/foundry/contracts/hooks/Hookathon/.editorconfig b/packages/foundry/contracts/hooks/Hookathon/.editorconfig new file mode 100644 index 00000000..6fd96c1e --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.sol] +indent_size = 4 diff --git a/packages/foundry/contracts/hooks/Hookathon/.env.example b/packages/foundry/contracts/hooks/Hookathon/.env.example new file mode 100644 index 00000000..b2a29631 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.env.example @@ -0,0 +1,13 @@ +export INFURA_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" +export MNEMONIC="adapt mosquito move limb mobile illegal tree voyage juice mosquito burger raise father hope layer" +export PRIVATE_KEY_GATEWAY_DEPLOYER="717fd99986df414889fd8b51069d4f90a50af72e542c58ee065f5883779099c6" +export PRIVATE_KEY_GATEWAY_OWNER="717fd99986df414889fd8b51069d4f90a50af72e542c58ee065f5883779099c6" +export PRIVATE_KEY_GATEWAY_RELAYER="7ec931411ad75a7c201469a385d6f18a325d4923f9f213bd882bbea87e160b67" + +# Block explorer API keys +export ARBISCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" +export BSCSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" +export ETHERSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" +export OPTIMISM_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" +export POLYGONSCAN_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" +export SNOWTRACE_API_KEY="zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" diff --git a/packages/foundry/contracts/hooks/Hookathon/.github/workflows/ci.yml b/packages/foundry/contracts/hooks/Hookathon/.github/workflows/ci.yml new file mode 100644 index 00000000..0fabbfc2 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: "CI" + +env: + DOTENV_CONFIG_PATH: "./.env.example" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + ci: + runs-on: "ubuntu-latest" + steps: + - name: "Check out the repo" + uses: "actions/checkout@v3" + + - name: "Install Pnpm" + uses: "pnpm/action-setup@v2" + with: + version: "8" + + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + cache: "pnpm" + node-version: "lts/*" + + - name: "Install the dependencies" + run: "pnpm install" + + - name: "Lint the code" + run: "pnpm lint" + + - name: "Add lint summary" + run: | + echo "## Lint results" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY + + - name: "Compile the contracts and generate the TypeChain bindings" + run: "pnpm typechain" + + - name: "Test the contracts and generate the coverage report" + run: "pnpm coverage:mock" + + - name: "Add test summary" + run: | + echo "## Test results" >> $GITHUB_STEP_SUMMARY + echo "✅ Passed" >> $GITHUB_STEP_SUMMARY diff --git a/packages/foundry/contracts/hooks/Hookathon/.gitignore b/packages/foundry/contracts/hooks/Hookathon/.gitignore new file mode 100644 index 00000000..f1fbdfd5 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.gitignore @@ -0,0 +1,31 @@ +# directories +.coverage_artifacts +.coverage_cache +.coverage_contracts +artifacts +build +cache +coverage +dist +node_modules +types +deployments +kms-fhe-keys/ +network-fhe-keys/ +fhevmTemp/ +abi/ + +# files +*.env +*.log +.env.docker +.DS_Store +.pnp.* +coverage.json +package-lock.json +yarn.lock + +res/ +running_node/ + +docker-compose/docker-compose-full.yml diff --git a/packages/foundry/contracts/hooks/Hookathon/.gitpod.yml b/packages/foundry/contracts/hooks/Hookathon/.gitpod.yml new file mode 100644 index 00000000..dd2444f5 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.gitpod.yml @@ -0,0 +1,10 @@ +image: "gitpod/workspace-node:latest" + +tasks: + - init: "pnpm install" + +vscode: + extensions: + - "esbenp.prettier-vscode" + - "NomicFoundation.hardhat-solidity" + - "ritwickdey.LiveServer" diff --git a/packages/foundry/contracts/hooks/Hookathon/.prettierignore b/packages/foundry/contracts/hooks/Hookathon/.prettierignore new file mode 100644 index 00000000..d1401c91 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.prettierignore @@ -0,0 +1,22 @@ +# directories +.coverage_artifacts +.coverage_cache +.coverage_contracts +artifacts +build +cache +coverage +dist +node_modules +types + +# files +*.env +*.log +.DS_Store +.pnp.* +coverage.json +package-lock.json +pnpm-lock.yaml +yarn.lock +README.md \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/.prettierrc.yml b/packages/foundry/contracts/hooks/Hookathon/.prettierrc.yml new file mode 100644 index 00000000..56221e36 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.prettierrc.yml @@ -0,0 +1,23 @@ +bracketSpacing: true +plugins: + - "@trivago/prettier-plugin-sort-imports" + - "prettier-plugin-solidity" +printWidth: 120 +proseWrap: "always" +singleQuote: false +tabWidth: 2 +trailingComma: "all" + +overrides: + - files: "*.sol" + options: + compiler: "0.8.24" + parser: "solidity-parse" + tabWidth: 4 + - files: "*.ts" + options: + importOrder: ["", "^[./]"] + importOrderParserPlugins: ["typescript"] + importOrderSeparation: true + importOrderSortSpecifiers: true + parser: "typescript" diff --git a/packages/foundry/contracts/hooks/Hookathon/.solcover.js b/packages/foundry/contracts/hooks/Hookathon/.solcover.js new file mode 100644 index 00000000..938b911a --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.solcover.js @@ -0,0 +1,11 @@ +module.exports = { + istanbulReporter: ["html", "lcov"], + providerOptions: { + mnemonic: process.env.MNEMONIC, + }, + skipFiles: ["test", "fhevmTemp"], + mocha: { + fgrep: "[skip-on-coverage]", + invert: true, + }, +}; diff --git a/packages/foundry/contracts/hooks/Hookathon/.solhint.json b/packages/foundry/contracts/hooks/Hookathon/.solhint.json new file mode 100644 index 00000000..826fe4e2 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.solhint.json @@ -0,0 +1,24 @@ +{ + "extends": "solhint:recommended", + "plugins": ["prettier"], + "rules": { + "const-name-snakecase": "off", + "no-global-import": "off", + "reason-string": "off", + "state-visibility": "off", + "custom-errors": "off", + "code-complexity": ["error", 8], + "compiler-version": ["error", ">=0.8.4"], + "func-visibility": ["error", { "ignoreConstructors": true }], + "max-line-length": ["error", 120], + "named-parameters-mapping": "off", + "no-console": "off", + "not-rely-on-time": "off", + "prettier/prettier": [ + "error", + { + "endOfLine": "auto" + } + ] + } +} diff --git a/packages/foundry/contracts/hooks/Hookathon/.solhintignore b/packages/foundry/contracts/hooks/Hookathon/.solhintignore new file mode 100644 index 00000000..16dc0c0d --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/.solhintignore @@ -0,0 +1,3 @@ +# directories +**/artifacts +**/node_modules diff --git a/packages/foundry/contracts/hooks/Hookathon/CustomProvider.ts b/packages/foundry/contracts/hooks/Hookathon/CustomProvider.ts new file mode 100644 index 00000000..b60308e8 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/CustomProvider.ts @@ -0,0 +1,62 @@ +import { ethers } from "ethers"; +import { ProviderWrapper } from "hardhat/plugins"; +import type { EIP1193Provider, RequestArguments } from "hardhat/types"; + +interface Test { + request: EIP1193Provider["request"]; +} + +class CustomProvider extends ProviderWrapper implements Test { + public lastBlockSnapshot: number; + public lastCounterRand: number; + public lastBlockSnapshotForDecrypt: number; + + constructor(protected readonly _wrappedProvider: EIP1193Provider) { + super(_wrappedProvider); + this.lastBlockSnapshot = 0; // Initialize the variable + this.lastCounterRand = 0; + this.lastBlockSnapshotForDecrypt = 0; + } + + async request(args: RequestArguments): ReturnType { + if (args.method === "eth_estimateGas") { + const estimatedGasLimit = BigInt((await this._wrappedProvider.request(args)) as bigint); + const increasedGasLimit = ethers.toBeHex((estimatedGasLimit * 120n) / 100n); // override estimated gasLimit by 120%, to avoid some edge case with ethermint gas estimation + return increasedGasLimit; + } + if (args.method === "evm_revert") { + const result = await this._wrappedProvider.request(args); + const blockNumberHex = (await this._wrappedProvider.request({ method: "eth_blockNumber" })) as string; + this.lastBlockSnapshot = parseInt(blockNumberHex); + this.lastBlockSnapshotForDecrypt = parseInt(blockNumberHex); + + const callData = { + to: "0x000000000000000000000000000000000000005d", + data: "0x1f20d85c", + }; + this.lastCounterRand = (await this._wrappedProvider.request({ + method: "eth_call", + params: [callData, "latest"], + })) as number; + return result; + } + if (args.method === "get_lastBlockSnapshot") { + return [this.lastBlockSnapshot, this.lastCounterRand]; + } + if (args.method === "get_lastBlockSnapshotForDecrypt") { + return this.lastBlockSnapshotForDecrypt; + } + if (args.method === "set_lastBlockSnapshot") { + this.lastBlockSnapshot = Array.isArray(args.params!) && args.params[0]; + return this.lastBlockSnapshot; + } + if (args.method === "set_lastBlockSnapshotForDecrypt") { + this.lastBlockSnapshotForDecrypt = Array.isArray(args.params!) && args.params[0]; + return this.lastBlockSnapshotForDecrypt; + } + const result = this._wrappedProvider.request(args); + return result; + } +} + +export default CustomProvider; diff --git a/packages/foundry/contracts/hooks/Hookathon/LICENSE.md b/packages/foundry/contracts/hooks/Hookathon/LICENSE.md new file mode 100644 index 00000000..88a2b87b --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/LICENSE.md @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2023 Paul Razvan Berg + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/foundry/contracts/hooks/Hookathon/Makefile b/packages/foundry/contracts/hooks/Hookathon/Makefile new file mode 100644 index 00000000..26c17274 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/Makefile @@ -0,0 +1,50 @@ +#!/usr/bin/make -f + +include .env + +KEY_GEN = false +BINDIR ?= $(GOPATH)/bin +ETHERMINT_BINARY = ethermintd +ETHERMINT_DIR = ethermint + + +# This version must the same as in docker-compose-full.yml +# TODO add check +KMS_DEV_VERSION ?= v0.7.1 + +export GO111MODULE = on + +# Default target executed when no arguments are given to make. +default_target: all + +.PHONY: default_target + +# process build tags + +############################################################################### +### Single validator ### +############################################################################### + + + +generate-fhe-keys: + @bash ./scripts/copy_fhe_keys.sh $(KMS_DEV_VERSION) $(PWD)/network-fhe-keys $(PWD)/kms-fhe-keys + +run-full: + $(MAKE) generate-fhe-keys + @docker compose --env-file .env.docker -f docker-compose/docker-compose-full.yml up --detach + @echo 'sleep a little to let the docker start up' + sleep 5 + +stop-full: + @docker compose --env-file .env.docker -f docker-compose/docker-compose-full.yml down + + +clean: + $(MAKE) stop-full + rm -rf network-fhe-keys + rm -rf kms-fhe-keys + + +print-info: + @echo 'KMS_DEV_VERSION: $(KMS_DEV_VERSION) for KEY_GEN---extracted from Makefile' diff --git a/packages/foundry/contracts/hooks/Hookathon/README.md b/packages/foundry/contracts/hooks/Hookathon/README.md new file mode 100644 index 00000000..3fcf20e5 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/README.md @@ -0,0 +1,232 @@ +# Hardhat Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Hardhat][hardhat-badge]][hardhat] [![License: MIT][license-badge]][license] + +[gitpod]: https://gitpod.io/#https://github.com/zama-ai/fhevm-hardhat-template +[gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod +[gha]: https://github.com/zama-ai/fhevm-hardhat-template/actions +[gha-badge]: https://github.com/zama-ai/fhevm-hardhat-template/actions/workflows/ci.yml/badge.svg +[hardhat]: https://hardhat.org/ +[hardhat-badge]: https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg +[license]: https://opensource.org/licenses/MIT +[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg + +A Hardhat-based template for developing Solidity smart contracts, with sensible defaults. + +- [Hardhat](https://github.com/nomiclabs/hardhat): compile, run and test smart contracts +- [TypeChain](https://github.com/ethereum-ts/TypeChain): generate TypeScript bindings for smart contracts +- [Ethers](https://github.com/ethers-io/ethers.js/): renowned Ethereum library and wallet implementation +- [Solhint](https://github.com/protofire/solhint): code linter +- [Solcover](https://github.com/sc-forks/solidity-coverage): code coverage +- [Prettier Plugin Solidity](https://github.com/prettier-solidity/prettier-plugin-solidity): code formatter + +## Getting Started + +Click the [`Use this template`](https://github.com/zama-ai/fhevm-hardhat-template/generate) button at the top of the +page to create a new repository with this repo as the initial state. + +## Features + +This template builds upon the frameworks and libraries mentioned above, so for details about their specific features, +please consult their respective documentations. + +For example, for Hardhat, you can refer to the [Hardhat Tutorial](https://hardhat.org/tutorial) and the +[Hardhat Docs](https://hardhat.org/docs). You might be in particular interested in reading the +[Testing Contracts](https://hardhat.org/tutorial/testing-contracts) section. + +### Sensible Defaults + +This template comes with sensible default configurations in the following files: + +```text +├── .editorconfig +├── .eslintignore +├── .eslintrc.yml +├── .gitignore +├── .prettierignore +├── .prettierrc.yml +├── .solcover.js +├── .solhint.json +└── hardhat.config.ts +``` + +### VSCode Integration + +This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic +Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity). + +### GitHub Actions + +This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull +request made to the `main` branch. + +Note though that to make this work, you must use your `INFURA_API_KEY` and your `MNEMONIC` as GitHub secrets. + +You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). + +## Usage + +### Pre Requisites + +Install [docker](https://docs.docker.com/engine/install/) + +Install [pnpm](https://pnpm.io/installation) + +Before being able to run any command, you need to create a `.env` file and set a BIP-39 compatible mnemonic as an +environment variable. You can follow the example in `.env.example` and start with the following command: + +```sh +cp .env.example .env +``` + +If you don't already have a mnemonic, you can use this [website](https://iancoleman.io/bip39/) to generate one. + +Then, proceed with installing dependencies - please **_make sure to use Node v20_** or more recent or this will fail: + +```sh +pnpm install +``` + +### Start fhEVM + +During installation (see previous section) we recommend you for easier setup to not change the default `.env` : simply +copy the original `.env.example` file to a new `.env` file in the root of the repo. + +Then, start a local fhEVM docker compose that inlcudes everything needed to deploy FHE encrypted smart contracts using: + +```sh +# In one terminal, keep it opened +# The node logs are printed +pnpm fhevm:start +``` + +Previous command will take 2 to 3 minutes to do the whole initial setup - wait until the blockchain logs appear to make +sure setup is complete (we are working on making initial deployment faster). + +You can then run the tests simply in a new terminal via : + +``` +pnpm test +``` + +Once your done with your tests, to stop the node: + +```sh +pnpm fhevm:stop +``` + +### Compile + +Compile the smart contracts with Hardhat: + +```sh +pnpm compile +``` + +### TypeChain + +Compile the smart contracts and generate TypeChain bindings: + +```sh +pnpm typechain +``` + +### List accounts + +From the mnemonic in .env file, list all the derived Ethereum adresses: + +```sh +pnpm task:accounts +``` + +### Get some native coins + +In order to interact with the blockchain, one need some coins. This command will give coins to the first 5 addresses +derived from the mnemonic in .env file. + +```sh +pnpm fhevm:faucet +``` + +
+
+ To get the first derived address from mnemonic +
+ +```sh +pnpm task:getEthereumAddress +``` + +
+
+ +### Test + +Run the tests with Hardhat: + +```sh +pnpm test +``` + +### Lint Solidity + +Lint the Solidity code: + +```sh +pnpm lint:sol +``` + +### Lint TypeScript + +Lint the TypeScript code: + +```sh +pnpm lint:ts +``` + +### Report Gas + +See the gas usage per unit test and average gas per method call: + +```sh +REPORT_GAS=true pnpm test +``` + +### Clean + +Delete the smart contract artifacts, the coverage reports and the Hardhat cache: + +```sh +pnpm clean +``` + +### Mocked mode + +The mocked mode allows faster testing and the ability to analyze coverage of the tests. In this mocked version, +encrypted types are not really encrypted, and the tests are run on the original version of the EVM, on a local hardhat +network instance. To run the tests in mocked mode, you can use directly the following command: + +```bash +pnpm test:mock +``` + +To analyze the coverage of the tests (in mocked mode necessarily, as this cannot be done on the real fhEVM node), you +can use this command : + +```bash +pnpm coverage:mock +``` + +Then open the file `coverage/index.html`. You can see there which line or branch for each contract which has been +covered or missed by your test suite. This allows increased security by pointing out missing branches not covered yet by +the current tests. + +> [!Note] +> Due to intrinsic limitations of the original EVM, the mocked version differ in few corner cases from the real fhEVM, the main difference is the difference in gas prices for the FHE operations. This means that before deploying to production, developers still need to run the tests with the original fhEVM node, as a final check in non-mocked mode, with `pnpm test`. + +### Syntax Highlighting + +If you use VSCode, you can get Solidity syntax highlighting with the +[hardhat-solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) extension. + +## License + +This project is licensed under MIT. \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/EncryptedERC20.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/EncryptedERC20.sol new file mode 100644 index 00000000..61c89223 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/EncryptedERC20.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.24; + +import "fhevm/lib/TFHE.sol"; +import "@openzeppelin/contracts/access/Ownable2Step.sol"; +import "fhevm/gateway/GatewayCaller.sol"; + +contract EncryptedERC20 is Ownable2Step, GatewayCaller { + event Transfer(address indexed from, address indexed to); + event Approval(address indexed owner, address indexed spender); + event Mint(address indexed to, uint64 amount); + + uint64 private _totalSupply; + string private _name; + string private _symbol; + uint8 public constant decimals = 6; + + // A mapping from address to an encrypted balance. + mapping(address => euint64) internal balances; + + // A mapping of the form mapping(owner => mapping(spender => allowance)). + mapping(address => mapping(address => euint64)) internal allowances; + + constructor(string memory name_, string memory symbol_) Ownable(msg.sender) { + _name = name_; + _symbol = symbol_; + } + + // Returns the name of the token. + function name() public view virtual returns (string memory) { + return _name; + } + + // Returns the symbol of the token, usually a shorter version of the name. + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + // Returns the total supply of the token + function totalSupply() public view virtual returns (uint64) { + return _totalSupply; + } + + // Sets the balance of the owner to the given encrypted balance. + function mint(uint64 mintedAmount) public virtual onlyOwner { + balances[owner()] = TFHE.add(balances[owner()], mintedAmount); // overflow impossible because of next line + TFHE.allow(balances[owner()], address(this)); + TFHE.allow(balances[owner()], owner()); + _totalSupply = _totalSupply + mintedAmount; + emit Mint(owner(), mintedAmount); + } + + // Transfers an encrypted amount from the message sender address to the `to` address. + function transfer(address to, einput encryptedAmount, bytes calldata inputProof) public virtual returns (bool) { + transfer(to, TFHE.asEuint64(encryptedAmount, inputProof)); + return true; + } + + // Transfers an amount from the message sender address to the `to` address. + function transfer(address to, euint64 amount) public virtual returns (bool) { + require(TFHE.isSenderAllowed(amount)); + // makes sure the owner has enough tokens + ebool canTransfer = TFHE.le(amount, balances[msg.sender]); + _transfer(msg.sender, to, amount, canTransfer); + return true; + } + + // Returns the balance handle of the caller. + function balanceOf(address wallet) public view virtual returns (euint64) { + return balances[wallet]; + } + + // Sets the `encryptedAmount` as the allowance of `spender` over the caller's tokens. + function approve(address spender, einput encryptedAmount, bytes calldata inputProof) public virtual returns (bool) { + approve(spender, TFHE.asEuint64(encryptedAmount, inputProof)); + return true; + } + + // Sets the `amount` as the allowance of `spender` over the caller's tokens. + function approve(address spender, euint64 amount) public virtual returns (bool) { + require(TFHE.isSenderAllowed(amount)); + address owner = msg.sender; + _approve(owner, spender, amount); + emit Approval(owner, spender); + return true; + } + + // Returns the remaining number of tokens that `spender` is allowed to spend + // on behalf of the caller. + function allowance(address owner, address spender) public view virtual returns (euint64) { + return _allowance(owner, spender); + } + + // Transfers `encryptedAmount` tokens using the caller's allowance. + function transferFrom( + address from, + address to, + einput encryptedAmount, + bytes calldata inputProof + ) public virtual returns (bool) { + transferFrom(from, to, TFHE.asEuint64(encryptedAmount, inputProof)); + return true; + } + + // Transfers `amount` tokens using the caller's allowance. + function transferFrom(address from, address to, euint64 amount) public virtual returns (bool) { + require(TFHE.isSenderAllowed(amount)); + address spender = msg.sender; + ebool isTransferable = _updateAllowance(from, spender, amount); + _transfer(from, to, amount, isTransferable); + return true; + } + + function _approve(address owner, address spender, euint64 amount) internal virtual { + allowances[owner][spender] = amount; + TFHE.allow(amount, address(this)); + TFHE.allow(amount, owner); + TFHE.allow(amount, spender); + } + + function _allowance(address owner, address spender) internal view virtual returns (euint64) { + return allowances[owner][spender]; + } + + function _updateAllowance(address owner, address spender, euint64 amount) internal virtual returns (ebool) { + euint64 currentAllowance = _allowance(owner, spender); + // makes sure the allowance suffices + ebool allowedTransfer = TFHE.le(amount, currentAllowance); + // makes sure the owner has enough tokens + ebool canTransfer = TFHE.le(amount, balances[owner]); + ebool isTransferable = TFHE.and(canTransfer, allowedTransfer); + _approve(owner, spender, TFHE.select(isTransferable, TFHE.sub(currentAllowance, amount), currentAllowance)); + return isTransferable; + } + + // Transfers an encrypted amount. + function _transfer(address from, address to, euint64 amount, ebool isTransferable) internal virtual { + // Add to the balance of `to` and subract from the balance of `from`. + euint64 transferValue = TFHE.select(isTransferable, amount, TFHE.asEuint64(0)); + euint64 newBalanceTo = TFHE.add(balances[to], transferValue); + balances[to] = newBalanceTo; + TFHE.allow(newBalanceTo, address(this)); + TFHE.allow(newBalanceTo, to); + euint64 newBalanceFrom = TFHE.sub(balances[from], transferValue); + balances[from] = newBalanceFrom; + TFHE.allow(newBalanceFrom, address(this)); + TFHE.allow(newBalanceFrom, from); + emit Transfer(from, to); + } + + ebool xBool; + bool public yBool; + + function decryptARandomNumber() public { + xBool = TFHE.asEbool(true); + TFHE.allow(xBool, address(this)); + uint256[] memory cts = new uint256[](1); + cts[0] = Gateway.toUint256(xBool); + Gateway.requestDecryption(cts, this.myCustomCallback.selector, 0, block.timestamp + 100, false); + } + + function myCustomCallback(uint256 /*requestID*/, bool decryptedInput) public onlyGateway returns (bool) { + yBool = decryptedInput; + return yBool; + } +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/BaseHooks.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/BaseHooks.sol new file mode 100644 index 00000000..5c918f01 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/BaseHooks.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IHooks } from "./IHooks.sol"; +import { + AddLiquidityKind, + HookFlags, + LiquidityManagement, + RemoveLiquidityKind, + TokenConfig, + PoolSwapParams, + AfterSwapParams +} from "./VaultTypes.sol"; + +/** + * @notice Base for pool hooks contracts. + * @dev Hook contracts that only implement a subset of callbacks can inherit from here instead of IHooks, + * and only override what they need. `VaultGuard` allows use of the `onlyVault` modifier, which isn't used + * in this abstract contract, but should be used in real derived hook contracts. + */ +abstract contract BaseHooks is IHooks { + /// @inheritdoc IHooks + function onRegister( + address, + address, + TokenConfig[] memory, + LiquidityManagement calldata + ) public virtual returns (bool) { + // By default, deny all factories. This method must be overwritten by the hook contract. + return false; + } + + /// @inheritdoc IHooks + function getHookFlags() public view virtual returns (HookFlags memory); + + /// @inheritdoc IHooks + function onBeforeInitialize(uint256[] memory, bytes memory) public virtual returns (bool) { + return false; + } + + /// @inheritdoc IHooks + function onAfterInitialize(uint256[] memory, uint256, bytes memory) public virtual returns (bool) { + return false; + } + + /// @inheritdoc IHooks + function onBeforeAddLiquidity( + address, + address, + AddLiquidityKind, + uint256[] memory, + uint256, + uint256[] memory, + bytes memory + ) public virtual returns (bool) { + return false; + } + + /// @inheritdoc IHooks + function onAfterAddLiquidity( + address, + address, + AddLiquidityKind, + uint256[] memory, + uint256[] memory amountsInRaw, + uint256, + uint256[] memory, + bytes memory + ) public virtual returns (bool, uint256[] memory) { + return (false, amountsInRaw); + } + + /// @inheritdoc IHooks + function onBeforeRemoveLiquidity( + address, + address, + RemoveLiquidityKind, + uint256, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual returns (bool) { + return false; + } + + /// @inheritdoc IHooks + function onAfterRemoveLiquidity( + address, + address, + RemoveLiquidityKind, + uint256, + uint256[] memory, + uint256[] memory amountsOutRaw, + uint256[] memory, + bytes memory + ) public virtual returns (bool, uint256[] memory) { + return (false, amountsOutRaw); + } + + /// @inheritdoc IHooks + function onBeforeSwap(PoolSwapParams calldata, address) public virtual returns (bool) { + // return false to trigger an error if shouldCallBeforeSwap is true but this function is not overridden. + return false; + } + + /// @inheritdoc IHooks + function onAfterSwap(AfterSwapParams calldata) public virtual returns (bool, uint256) { + // return false to trigger an error if shouldCallAfterSwap is true but this function is not overridden. + // The second argument is not used. + return (false, 0); + } + + /// @inheritdoc IHooks + function onComputeDynamicSwapFeePercentage( + PoolSwapParams calldata, + address, + uint256 + ) public view virtual returns (bool, uint256) { + return (false, 0); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/EnumerableMap.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/EnumerableMap.sol new file mode 100644 index 00000000..d733e8de --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/EnumerableMap.sol @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @notice Library for managing an enumerable variant of Solidity's + * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] type. + * + * @dev Based on the EnumerableMap library from OpenZeppelin Contracts, altered to include the following: + * a map from IERC20 to Uint256. + * + * Entries are stored in mappings instead of arrays, reducing implicit storage reads for out-of-bounds checks + * unchecked_at and unchecked_valueAt, which allow for more gas efficient data reads in some scenarios + * indexOf, unchecked_indexOf and unchecked_setAt, which allow for more gas efficient data writes in some scenarios. + * + * Additionally, the base private functions that work on bytes32 were removed and replaced with a native implementation + * for IERC20 keys, to reduce bytecode size and runtime costs. + * + * Maps have the following properties: + * + * - Entries are added, removed, and checked for existence in constant time (O(1)). + * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * + * ``` + * contract Example { + * // Add the library methods + * using EnumerableMap for EnumerableMap.UintToAddressMap; + * + * // Declare a set state variable + * EnumerableMap.UintToAddressMap private myMap; + * } + * ``` + */ +library EnumerableMap { + // The original OpenZeppelin implementation uses a generic Map type with bytes32 keys: this was replaced with + // IERC20ToUint256Map, resulting in more dense bytecode. + + // solhint-disable func-name-mixedcase + + // The original OpenZeppelin implementation uses a generic Map type with bytes32 keys: this was replaced with + // IERC20ToBytes32Map and IERC20ToUint256Map, resulting in more dense bytecode (as long as each contract only uses + // one of these - there'll otherwise be duplicated code). + + // IERC20ToBytes32Map + + // solhint-disable func-name-mixedcase + + struct IERC20ToBytes32MapEntry { + IERC20 key; + bytes32 value; + } + + struct IERC20ToBytes32Map { + // Number of entries in the map + uint256 _length; + // Storage of map keys and values + mapping(uint256 indexValue => IERC20ToBytes32MapEntry mapEntry) entries; + // Position of the entry defined by a key in the `entries` array, plus 1 + // because index 0 means a key is not in the map. + mapping(IERC20 tokenKey => uint256 indexValue) indexes; + } + + /// @notice An index is beyond the current bounds of the set. + error IndexOutOfBounds(); + + /// @notice This error is thrown when attempting to retrieve an entry that is not present in the map. + error KeyNotFound(); + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(IERC20ToBytes32Map storage map, IERC20 key, bytes32 value) internal returns (bool) { + // We read and store the key's index to prevent multiple reads from the same storage slot + uint256 keyIndex = map.indexes[key]; + unchecked { + // Equivalent to !contains(map, key) + if (keyIndex == 0) { + uint256 previousLength = map._length; + map.entries[previousLength] = IERC20ToBytes32MapEntry({ key: key, value: value }); + map._length = previousLength + 1; + + // The entry is stored at previousLength, but we add 1 to all indexes + // and use 0 as a sentinel value + map.indexes[key] = previousLength + 1; + return true; + } else { + map.entries[keyIndex - 1].value = value; + return false; + } + } + } + + /** + * @dev Updates the value for an entry, given its key's index. The key index can be retrieved via + * {unchecked_indexOf}, and it should be noted that key indices may change when calling {set} or {remove}. O(1). + * + * This function performs one less storage read than {set}, but it should only be used when `index` is known to be + * within bounds. + */ + function unchecked_setAt(IERC20ToBytes32Map storage map, uint256 index, bytes32 value) internal { + map.entries[index].value = value; + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(IERC20ToBytes32Map storage map, IERC20 key) internal returns (bool) { + // We read and store the key's index to prevent multiple reads from the same storage slot + uint256 keyIndex = map.indexes[key]; + + // Equivalent to contains(map, key) + if (keyIndex != 0) { + // To delete a key-value pair from the entries pseudo-array in O(1), we swap the entry to delete with the + // one at the highest index, and then remove this last entry (sometimes called as 'swap and pop'). + // This modifies the order of the pseudo-array, as noted in {at}. + + uint256 toDeleteIndex; + uint256 lastIndex; + + unchecked { + toDeleteIndex = keyIndex - 1; + lastIndex = map._length - 1; + } + + // The swap is only necessary if we're not removing the last element + if (toDeleteIndex != lastIndex) { + IERC20ToBytes32MapEntry storage lastEntry = map.entries[lastIndex]; + + // Move the last entry to the index where the entry to delete is + map.entries[toDeleteIndex] = lastEntry; + // Update the index for the moved entry + map.indexes[lastEntry.key] = keyIndex; // All indexes are 1-based + } + + // Delete the slot where the moved entry was stored + delete map.entries[lastIndex]; + map._length = lastIndex; + + // Delete the index for the deleted slot + delete map.indexes[key]; + + return true; + } else { + return false; + } + } + + /// @dev Returns true if the key is in the map. O(1). + function contains(IERC20ToBytes32Map storage map, IERC20 key) internal view returns (bool) { + return map.indexes[key] != 0; + } + + /// @dev Returns the number of key-value pairs in the map. O(1). + function length(IERC20ToBytes32Map storage map) internal view returns (uint256) { + return map._length; + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(IERC20ToBytes32Map storage map, uint256 index) internal view returns (IERC20, bytes32) { + if (index >= map._length) { + revert IndexOutOfBounds(); + } + + return unchecked_at(map, index); + } + + /** + * @dev Same as {at}, except this doesn't revert if `index` it outside of the map (i.e. if it is equal or larger + * than {length}). O(1). + * + * This function performs one less storage read than {at}, but should only be used when `index` is known to be + * within bounds. + */ + function unchecked_at(IERC20ToBytes32Map storage map, uint256 index) internal view returns (IERC20, bytes32) { + IERC20ToBytes32MapEntry storage entry = map.entries[index]; + return (entry.key, entry.value); + } + + /** + * @dev Same as {unchecked_At}, except it only returns the key and not the value (performing one less storage + * read). O(1). + */ + function unchecked_keyAt(IERC20ToBytes32Map storage map, uint256 index) internal view returns (IERC20) { + return map.entries[index].key; + } + + /** + * @dev Same as {unchecked_At}, except it only returns the value and not the key (performing one less storage + * read). O(1). + */ + function unchecked_valueAt(IERC20ToBytes32Map storage map, uint256 index) internal view returns (bytes32) { + return map.entries[index].value; + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. Reverts with `errorCode` otherwise. + */ + function get(IERC20ToBytes32Map storage map, IERC20 key) internal view returns (bytes32) { + uint256 index = map.indexes[key]; + if (index == 0) { + revert KeyNotFound(); + } + + unchecked { + return unchecked_valueAt(map, index - 1); + } + } + + /** + * @dev Returns the index for `key`. + * + * Requirements: + * + * - `key` must be in the map. + */ + function indexOf(IERC20ToBytes32Map storage map, IERC20 key) internal view returns (uint256) { + uint256 uncheckedIndex = unchecked_indexOf(map, key); + if (uncheckedIndex == 0) { + revert KeyNotFound(); + } + + unchecked { + return uncheckedIndex - 1; + } + } + + /** + * @dev Returns the index for `key` **plus one**. Does not revert if the key is not in the map, and returns 0 + * instead. + */ + function unchecked_indexOf(IERC20ToBytes32Map storage map, IERC20 key) internal view returns (uint256) { + return map.indexes[key]; + } + + // IERC20ToUint256Map + + struct IERC20ToUint256MapEntry { + IERC20 key; + uint256 value; + } + + struct IERC20ToUint256Map { + // Number of entries in the map + uint256 size; + // Storage of map keys and values + mapping(uint256 indexValue => IERC20ToUint256MapEntry mapEntry) entries; + // Position of the entry defined by a key in the `entries` array, plus 1 + // because index 0 means a key is not in the map. + mapping(IERC20 tokenKey => uint256 indexValue) indexes; + } + + /** + * @dev Adds a key-value pair to a map, or updates the value for an existing + * key. O(1). + * + * Returns true if the key was added to the map, that is if it was not + * already present. + */ + function set(IERC20ToUint256Map storage map, IERC20 key, uint256 value) internal returns (bool) { + // We read and store the key's index to prevent multiple reads from the same storage slot + uint256 keyIndex = map.indexes[key]; + + unchecked { + // Equivalent to !contains(map, key) + if (keyIndex == 0) { + uint256 previousLength = map.size; + map.entries[previousLength] = IERC20ToUint256MapEntry(key, value); + map.size = previousLength + 1; + + // The entry is stored at previousLength, but we add 1 to all indexes + // and use 0 as a sentinel value + map.indexes[key] = previousLength + 1; + return true; + } else { + map.entries[keyIndex - 1].value = value; + return false; + } + } + } + + /** + * @dev Updates the value for an entry, given its key's index. The key index can be retrieved via + * {unchecked_indexOf}, and it should be noted that key indices may change when calling {set} or {remove}. O(1). + * + * This function performs one less storage read than {set}, but it should only be used when `index` is known to be + * within bounds. + */ + function unchecked_setAt(IERC20ToUint256Map storage map, uint256 index, uint256 value) internal { + map.entries[index].value = value; + } + + /** + * @dev Removes a key-value pair from a map. O(1). + * + * Returns true if the key was removed from the map, that is if it was present. + */ + function remove(IERC20ToUint256Map storage map, IERC20 key) internal returns (bool) { + // We read and store the key's index to prevent multiple reads from the same storage slot + uint256 keyIndex = map.indexes[key]; + + // Equivalent to contains(map, key) + if (keyIndex != 0) { + // To delete a key-value pair from the entries pseudo-array in O(1), we swap the entry to delete with the + // one at the highest index, and then remove this last entry (sometimes called as 'swap and pop'). + // This modifies the order of the pseudo-array, as noted in {at}. + uint256 toDeleteIndex; + uint256 lastIndex; + + unchecked { + toDeleteIndex = keyIndex - 1; + lastIndex = map.size - 1; + } + + // The swap is only necessary if we're not removing the last element + if (toDeleteIndex != lastIndex) { + IERC20ToUint256MapEntry storage lastEntry = map.entries[lastIndex]; + + // Move the last entry to the index where the entry to delete is + map.entries[toDeleteIndex] = lastEntry; + // Update the index for the moved entry + map.indexes[lastEntry.key] = keyIndex; // = toDeleteIndex + 1; all indices are 1-based + } + + // Delete the slot where the moved entry was stored + delete map.entries[lastIndex]; + map.size = lastIndex; + + // Delete the index for the deleted slot + delete map.indexes[key]; + + return true; + } else { + return false; + } + } + + /// @dev Returns true if the key is in the map. O(1). + function contains(IERC20ToUint256Map storage map, IERC20 key) internal view returns (bool) { + return map.indexes[key] != 0; + } + + /// @dev Returns the number of key-value pairs in the map. O(1). + function length(IERC20ToUint256Map storage map) internal view returns (uint256) { + return map.size; + } + + /** + * @dev Returns the key-value pair stored at position `index` in the map. O(1). + * + * Note that there are no guarantees on the ordering of entries inside the + * array, and it may change when more entries are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {size}. + */ + function at(IERC20ToUint256Map storage map, uint256 index) internal view returns (IERC20, uint256) { + if (index >= map.size) { + revert IndexOutOfBounds(); + } + + return unchecked_at(map, index); + } + + /** + * @dev Same as {at}, except this doesn't revert if `index` it outside of the map (i.e. if it is equal or larger + * than {size}). O(1). + * + * This function performs one less storage read than {at}, but should only be used when `index` is known to be + * within bounds. + */ + function unchecked_at(IERC20ToUint256Map storage map, uint256 index) internal view returns (IERC20, uint256) { + IERC20ToUint256MapEntry storage entry = map.entries[index]; + return (entry.key, entry.value); + } + + /** + * @dev Same as {unchecked_At}, except it only returns the value and not the key (performing one less storage + * read). O(1). + */ + function unchecked_valueAt(IERC20ToUint256Map storage map, uint256 index) internal view returns (uint256) { + return map.entries[index].value; + } + + /** + * @dev Returns the value associated with `key`. O(1). + * + * Requirements: + * + * - `key` must be in the map. + */ + function get(IERC20ToUint256Map storage map, IERC20 key) internal view returns (uint256) { + uint256 index = map.indexes[key]; + if (index == 0) { + revert KeyNotFound(); + } + + unchecked { + return unchecked_valueAt(map, index - 1); + } + } + + /** + * @dev Returns the index for `key`. + * + * Requirements: + * + * - `key` must be in the map. + */ + function indexOf(IERC20ToUint256Map storage map, IERC20 key) internal view returns (uint256) { + uint256 uncheckedIndex = unchecked_indexOf(map, key); + if (uncheckedIndex == 0) { + revert KeyNotFound(); + } + + unchecked { + return uncheckedIndex - 1; + } + } + + /** + * @dev Returns the index for `key` **plus one**. Does not revert if the key is not in the map, and returns 0 + * instead. + */ + function unchecked_indexOf(IERC20ToUint256Map storage map, IERC20 key) internal view returns (uint256) { + return map.indexes[key]; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/FixedPoint.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/FixedPoint.sol new file mode 100644 index 00000000..10cc3c58 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/FixedPoint.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { LogExpMath } from "./LogExpMath.sol"; + +/// @notice Support 18-decimal fixed point arithmetic. All Vault calculations use this for high and uniform precision. +library FixedPoint { + /// @notice Attempted division by zero. + error ZeroDivision(); + + // solhint-disable no-inline-assembly + // solhint-disable private-vars-leading-underscore + + uint256 internal constant ONE = 1e18; // 18 decimal places + uint256 internal constant TWO = 2 * ONE; + uint256 internal constant FOUR = 4 * ONE; + uint256 internal constant MAX_POW_RELATIVE_ERROR = 10000; // 10^(-14) + + function mulDown(uint256 a, uint256 b) internal pure returns (uint256) { + // Multiplication overflow protection is provided by Solidity 0.8.x + uint256 product = a * b; + + return product / ONE; + } + + function mulUp(uint256 a, uint256 b) internal pure returns (uint256 result) { + // Multiplication overflow protection is provided by Solidity 0.8.x + uint256 product = a * b; + + // Equivalent to: + // result = product == 0 ? 0 : ((product - 1) / FixedPoint.ONE) + 1; + assembly ("memory-safe") { + result := mul(iszero(iszero(product)), add(div(sub(product, 1), ONE), 1)) + } + } + + function divDown(uint256 a, uint256 b) internal pure returns (uint256) { + // Solidity 0.8 reverts with a Panic code (0x11) if the multiplication overflows. + uint256 aInflated = a * ONE; + + // Solidity 0.8 reverts with a "Division by Zero" Panic code (0x12) if b is zero + return aInflated / b; + } + + function divUp(uint256 a, uint256 b) internal pure returns (uint256 result) { + return mulDivUp(a, ONE, b); + } + + /// @dev Return (a * b) / c, rounding up. + function mulDivUp(uint256 a, uint256 b, uint256 c) internal pure returns (uint256 result) { + // This check is required because Yul's `div` doesn't revert on c==0 + if (c == 0) { + revert ZeroDivision(); + } + + // Multiple overflow protection is done by Solidity 0.8x + uint256 product = a * b; + + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, if x == 0 then the result is zero + // + // Equivalent to: + // result = a == 0 ? 0 : (a * b - 1) / c + 1; + assembly ("memory-safe") { + result := mul(iszero(iszero(product)), add(div(sub(product, 1), c), 1)) + } + } + + /** + * @dev Version of divUp when the input is raw (i.e., already "inflated"). For instance, + * invariant * invariant (36 decimals) vs. invariant.mulDown(invariant) (18 decimal FP). + * This can occur in calculations with many successive multiplications and divisions, and + * we want to minimize the number of operations by avoiding unnecessary scaling by ONE. + */ + function divUpRaw(uint256 a, uint256 b) internal pure returns (uint256 result) { + // This check is required because Yul's `div` doesn't revert on b==0 + if (b == 0) { + revert ZeroDivision(); + } + + // Equivalent to: + // result = a == 0 ? 0 : 1 + (a - 1) / b; + assembly ("memory-safe") { + result := mul(iszero(iszero(a)), add(1, div(sub(a, 1), b))) + } + } + + /** + * @dev Returns x^y, assuming both are fixed point numbers, rounding down. The result is guaranteed to not be above + * the true value (that is, the error function expected - actual is always positive). + */ + function powDown(uint256 x, uint256 y) internal pure returns (uint256) { + // Optimize for when y equals 1.0, 2.0 or 4.0, as those are very simple to implement and occur often in 50/50 + // and 80/20 Weighted Pools + if (y == ONE) { + return x; + } else if (y == TWO) { + return mulDown(x, x); + } else if (y == FOUR) { + uint256 square = mulDown(x, x); + return mulDown(square, square); + } else { + uint256 raw = LogExpMath.pow(x, y); + uint256 maxError = mulUp(raw, MAX_POW_RELATIVE_ERROR) + 1; + + if (raw < maxError) { + return 0; + } else { + unchecked { + return raw - maxError; + } + } + } + } + + /** + * @dev Returns x^y, assuming both are fixed point numbers, rounding up. The result is guaranteed to not be below + * the true value (that is, the error function expected - actual is always negative). + */ + function powUp(uint256 x, uint256 y) internal pure returns (uint256) { + // Optimize for when y equals 1.0, 2.0 or 4.0, as those are very simple to implement and occur often in 50/50 + // and 80/20 Weighted Pools + if (y == ONE) { + return x; + } else if (y == TWO) { + return mulUp(x, x); + } else if (y == FOUR) { + uint256 square = mulUp(x, x); + return mulUp(square, square); + } else { + uint256 raw = LogExpMath.pow(x, y); + uint256 maxError = mulUp(raw, MAX_POW_RELATIVE_ERROR) + 1; + + return raw + maxError; + } + } + + /** + * @dev Returns the complement of a value (1 - x), capped to 0 if x is larger than 1. + * + * Useful when computing the complement for values with some level of relative error, as it strips this error and + * prevents intermediate negative values. + */ + function complement(uint256 x) internal pure returns (uint256 result) { + // Equivalent to: + // result = (x < ONE) ? (ONE - x) : 0; + assembly ("memory-safe") { + result := mul(lt(x, ONE), sub(ONE, x)) + } + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAllowanceTransfer.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAllowanceTransfer.sol new file mode 100644 index 00000000..bc5f826c --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAllowanceTransfer.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IEIP712} from "./IEIP712.sol"; + +/// @title AllowanceTransfer +/// @notice Handles ERC20 token permissions through signature based allowance setting and ERC20 token transfers by checking allowed amounts +/// @dev Requires user's token approval on the Permit2 contract +interface IAllowanceTransfer is IEIP712 { + /// @notice Thrown when an allowance on a token has expired. + /// @param deadline The timestamp at which the allowed amount is no longer valid + error AllowanceExpired(uint256 deadline); + + /// @notice Thrown when an allowance on a token has been depleted. + /// @param amount The maximum amount allowed + error InsufficientAllowance(uint256 amount); + + /// @notice Thrown when too many nonces are invalidated. + error ExcessiveInvalidation(); + + /// @notice Emits an event when the owner successfully invalidates an ordered nonce. + event NonceInvalidation( + address indexed owner, address indexed token, address indexed spender, uint48 newNonce, uint48 oldNonce + ); + + /// @notice Emits an event when the owner successfully sets permissions on a token for the spender. + event Approval( + address indexed owner, address indexed token, address indexed spender, uint160 amount, uint48 expiration + ); + + /// @notice Emits an event when the owner successfully sets permissions using a permit signature on a token for the spender. + event Permit( + address indexed owner, + address indexed token, + address indexed spender, + uint160 amount, + uint48 expiration, + uint48 nonce + ); + + /// @notice Emits an event when the owner sets the allowance back to 0 with the lockdown function. + event Lockdown(address indexed owner, address token, address spender); + + /// @notice The permit data for a token + struct PermitDetails { + // ERC20 token address + address token; + // the maximum amount allowed to spend + uint160 amount; + // timestamp at which a spender's token allowances become invalid + uint48 expiration; + // an incrementing value indexed per owner,token,and spender for each signature + uint48 nonce; + } + + /// @notice The permit message signed for a single token allowance + struct PermitSingle { + // the permit data for a single token alownce + PermitDetails details; + // address permissioned on the allowed tokens + address spender; + // deadline on the permit signature + uint256 sigDeadline; + } + + /// @notice The permit message signed for multiple token allowances + struct PermitBatch { + // the permit data for multiple token allowances + PermitDetails[] details; + // address permissioned on the allowed tokens + address spender; + // deadline on the permit signature + uint256 sigDeadline; + } + + /// @notice The saved permissions + /// @dev This info is saved per owner, per token, per spender and all signed over in the permit message + /// @dev Setting amount to type(uint160).max sets an unlimited approval + struct PackedAllowance { + // amount allowed + uint160 amount; + // permission expiry + uint48 expiration; + // an incrementing value indexed per owner,token,and spender for each signature + uint48 nonce; + } + + /// @notice A token spender pair. + struct TokenSpenderPair { + // the token the spender is approved + address token; + // the spender address + address spender; + } + + /// @notice Details for a token transfer. + struct AllowanceTransferDetails { + // the owner of the token + address from; + // the recipient of the token + address to; + // the amount of the token + uint160 amount; + // the token to be transferred + address token; + } + + /// @notice A mapping from owner address to token address to spender address to PackedAllowance struct, which contains details and conditions of the approval. + /// @notice The mapping is indexed in the above order see: allowance[ownerAddress][tokenAddress][spenderAddress] + /// @dev The packed slot holds the allowed amount, expiration at which the allowed amount is no longer valid, and current nonce thats updated on any signature based approvals. + function allowance(address user, address token, address spender) + external + view + returns (uint160 amount, uint48 expiration, uint48 nonce); + + /// @notice Approves the spender to use up to amount of the specified token up until the expiration + /// @param token The token to approve + /// @param spender The spender address to approve + /// @param amount The approved amount of the token + /// @param expiration The timestamp at which the approval is no longer valid + /// @dev The packed allowance also holds a nonce, which will stay unchanged in approve + /// @dev Setting amount to type(uint160).max sets an unlimited approval + function approve(address token, address spender, uint160 amount, uint48 expiration) external; + + /// @notice Permit a spender to a given amount of the owners token via the owner's EIP-712 signature + /// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce + /// @param owner The owner of the tokens being approved + /// @param permitSingle Data signed over by the owner specifying the terms of approval + /// @param signature The owner's signature over the permit data + function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external; + + /// @notice Permit a spender to the signed amounts of the owners tokens via the owner's EIP-712 signature + /// @dev May fail if the owner's nonce was invalidated in-flight by invalidateNonce + /// @param owner The owner of the tokens being approved + /// @param permitBatch Data signed over by the owner specifying the terms of approval + /// @param signature The owner's signature over the permit data + function permit(address owner, PermitBatch memory permitBatch, bytes calldata signature) external; + + /// @notice Transfer approved tokens from one address to another + /// @param from The address to transfer from + /// @param to The address of the recipient + /// @param amount The amount of the token to transfer + /// @param token The token address to transfer + /// @dev Requires the from address to have approved at least the desired amount + /// of tokens to msg.sender. + function transferFrom(address from, address to, uint160 amount, address token) external; + + /// @notice Transfer approved tokens in a batch + /// @param transferDetails Array of owners, recipients, amounts, and tokens for the transfers + /// @dev Requires the from addresses to have approved at least the desired amount + /// of tokens to msg.sender. + function transferFrom(AllowanceTransferDetails[] calldata transferDetails) external; + + /// @notice Enables performing a "lockdown" of the sender's Permit2 identity + /// by batch revoking approvals + /// @param approvals Array of approvals to revoke. + function lockdown(TokenSpenderPair[] calldata approvals) external; + + /// @notice Invalidate nonces for a given (token, spender) pair + /// @param token The token to invalidate nonces for + /// @param spender The spender to invalidate nonces for + /// @param newNonce The new nonce to set. Invalidates all nonces less than it. + /// @dev Can't invalidate more than 2**16 nonces per transaction. + function invalidateNonces(address token, address spender, uint48 newNonce) external; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthentication.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthentication.sol new file mode 100644 index 00000000..3178f914 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthentication.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/// @notice Simple interface for permissioned calling of external functions. +interface IAuthentication { + /// @notice The sender does not have permission to call a function. + error SenderNotAllowed(); + + /** + * @notice Returns the action identifier associated with the external function described by `selector`. + * @param selector The 4-byte selector of the permissioned function + * @return actionId The computed actionId + */ + function getActionId(bytes4 selector) external view returns (bytes32); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthorizer.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthorizer.sol new file mode 100644 index 00000000..07936156 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthorizer.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/// @notice Interface to the Vault's permission system. +interface IAuthorizer { + /** + * @notice Returns true if `account` can perform the action described by `actionId` in the contract `where`. + * @param actionId Identifier for the action to be performed + * @param account Account trying to perform the action + * @param where Target contract for the action + * @return success True if the action is permitted + */ + function canPerform(bytes32 actionId, address account, address where) external view returns (bool); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IBasePoolFactory.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IBasePoolFactory.sol new file mode 100644 index 00000000..0bad999c --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IBasePoolFactory.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IAuthentication } from "./IAuthentication.sol"; + +/** + * @notice Base interface for a Balancer Pool Factory. + * @dev All pool factories should be derived from `BasePoolFactory` to enable common behavior for all pool types + * (e.g., address prediction, tracking deployed pools, and governance-facilitated migration). + */ +interface IBasePoolFactory is IAuthentication { + /** + * @notice A pool was deployed. + * @param pool The address of the new pool + */ + event PoolCreated(address indexed pool); + + /// @notice The factory was disabled by governance. + event FactoryDisabled(); + + /// @notice Attempted pool creation after the factory was disabled. + error Disabled(); + + /** + * @notice Check whether a pool was deployed by this factory. + * @param pool The pool to check + * @return success True if `pool` was created by this factory + */ + function isPoolFromFactory(address pool) external view returns (bool); + + /** + * @notice Return the address where a new pool will be deployed, based on the factory address and salt. + * @param salt The salt used to deploy the pool + * @return deploymentAddress The predicted address of the pool, given the salt + */ + function getDeploymentAddress(bytes32 salt) external view returns (address); + + /** + * @notice Check whether this factory has been disabled by governance. + * @return success True if this factory was disabled + */ + function isDisabled() external view returns (bool); + + /** + * @notice Disable the factory, preventing the creation of more pools. + * @dev Existing pools are unaffected. Once a factory is disabled, it cannot be re-enabled. + */ + function disable() external; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IEIP712.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IEIP712.sol new file mode 100644 index 00000000..355c443f --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IEIP712.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IEIP712 { + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IHooks.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IHooks.sol new file mode 100644 index 00000000..71e87a87 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IHooks.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +// Explicitly import VaultTypes structs because we expect this interface to be heavily used by external developers. +// Internally, when this list gets too long, we usually just do a simple import to keep things tidy. +import { + TokenConfig, + LiquidityManagement, + PoolSwapParams, + AfterSwapParams, + HookFlags, + AddLiquidityKind, + RemoveLiquidityKind, + SwapKind +} from "./VaultTypes.sol"; + +/** + * @notice Interface for pool hooks. + * @dev Hooks are functions invoked by the Vault at specific points in the flow of each operation. This guarantees that + * they are called in the correct order, and with the correct arguments. To maintain this security, these functions + * should only be called by the Vault. The recommended way to do this is to derive the hook contract from `BaseHooks`, + * then use the `onlyVault` modifier from `VaultGuard`. (See the examples in /pool-hooks.) + */ +interface IHooks { + /*************************************************************************** + Register + ***************************************************************************/ + + /** + * @notice Hook executed when a pool is registered with a non-zero hooks contract. + * @dev Returns true if registration was successful, and false to revert the pool registration. + * Make sure this function is properly implemented (e.g. check the factory, and check that the + * given pool is from the factory). The Vault address will be msg.sender. + * + * @param factory Address of the pool factory (contract deploying the pool) + * @param pool Address of the pool + * @param tokenConfig An array of descriptors for the tokens the pool will manage + * @param liquidityManagement Liquidity management flags indicating which functions are enabled + * @return success True if the hook allowed the registration, false otherwise + */ + function onRegister( + address factory, + address pool, + TokenConfig[] memory tokenConfig, + LiquidityManagement calldata liquidityManagement + ) external returns (bool); + + /** + * @notice Return the set of hooks implemented by the contract. + * @dev The Vault will only call hooks the pool says it supports, and of course only if a hooks contract is defined + * (i.e., the `poolHooksContract` in `PoolRegistrationParams` is non-zero). + * `onRegister` is the only "mandatory" hook. + * + * @return hookFlags Flags indicating which hooks the contract supports + */ + function getHookFlags() external view returns (HookFlags memory hookFlags); + + /*************************************************************************** + Initialize + ***************************************************************************/ + + /** + * @notice Hook executed before pool initialization. + * @dev Called if the `shouldCallBeforeInitialize` flag is set in the configuration. Hook contracts should use + * the `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param exactAmountsIn Exact amounts of input tokens + * @param userData Optional, arbitrary data sent with the encoded request + * @return success True if the pool wishes to proceed with initialization + */ + function onBeforeInitialize(uint256[] memory exactAmountsIn, bytes memory userData) external returns (bool); + + /** + * @notice Hook to be executed after pool initialization. + * @dev Called if the `shouldCallAfterInitialize` flag is set in the configuration. Hook contracts should use + * the `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param exactAmountsIn Exact amounts of input tokens + * @param bptAmountOut Amount of pool tokens minted during initialization + * @param userData Optional, arbitrary data sent with the encoded request + * @return success True if the pool accepts the initialization results + */ + function onAfterInitialize( + uint256[] memory exactAmountsIn, + uint256 bptAmountOut, + bytes memory userData + ) external returns (bool); + + /*************************************************************************** + Add Liquidity + ***************************************************************************/ + + /** + * @notice Hook to be executed before adding liquidity. + * @dev Called if the `shouldCallBeforeAddLiquidity` flag is set in the configuration. Hook contracts should use + * the `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param router The address (usually a router contract) that initiated an add liquidity operation on the Vault + * @param pool Pool address, used to fetch pool information from the Vault (pool config, tokens, etc.) + * @param kind The type of add liquidity operation (e.g., proportional, custom) + * @param maxAmountsInScaled18 Maximum amounts of input tokens + * @param minBptAmountOut Minimum amount of output pool tokens + * @param balancesScaled18 Current pool balances, sorted in token registration order + * @param userData Optional, arbitrary data sent with the encoded request + * @return success True if the pool wishes to proceed with settlement + */ + function onBeforeAddLiquidity( + address router, + address pool, + AddLiquidityKind kind, + uint256[] memory maxAmountsInScaled18, + uint256 minBptAmountOut, + uint256[] memory balancesScaled18, + bytes memory userData + ) external returns (bool success); + + /** + * @notice Hook to be executed after adding liquidity. + * @dev Called if the `shouldCallAfterAddLiquidity` flag is set in the configuration. The Vault will ignore + * `hookAdjustedAmountsInRaw` unless `enableHookAdjustedAmounts` is true. Hook contracts should use the + * `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param router The address (usually a router contract) that initiated an add liquidity operation on the Vault + * @param pool Pool address, used to fetch pool information from the Vault (pool config, tokens, etc.) + * @param kind The type of add liquidity operation (e.g., proportional, custom) + * @param amountsInScaled18 Actual amounts of tokens added, sorted in token registration order + * @param amountsInRaw Actual amounts of tokens added, sorted in token registration order + * @param bptAmountOut Amount of pool tokens minted + * @param balancesScaled18 Current pool balances, sorted in token registration order + * @param userData Additional (optional) data provided by the user + * @return success True if the pool wishes to proceed with settlement + * @return hookAdjustedAmountsInRaw New amountsInRaw, potentially modified by the hook + */ + function onAfterAddLiquidity( + address router, + address pool, + AddLiquidityKind kind, + uint256[] memory amountsInScaled18, + uint256[] memory amountsInRaw, + uint256 bptAmountOut, + uint256[] memory balancesScaled18, + bytes memory userData + ) external returns (bool success, uint256[] memory hookAdjustedAmountsInRaw); + + /*************************************************************************** + Remove Liquidity + ***************************************************************************/ + + /** + * @notice Hook to be executed before removing liquidity. + * @dev Called if the `shouldCallBeforeRemoveLiquidity` flag is set in the configuration. Hook contracts should use + * the `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param router The address (usually a router contract) that initiated a remove liquidity operation on the Vault + * @param pool Pool address, used to fetch pool information from the Vault (pool config, tokens, etc.) + * @param kind The type of remove liquidity operation (e.g., proportional, custom) + * @param maxBptAmountIn Maximum amount of input pool tokens + * @param minAmountsOutScaled18 Minimum output amounts, sorted in token registration order + * @param balancesScaled18 Current pool balances, sorted in token registration order + * @param userData Optional, arbitrary data sent with the encoded request + * @return success True if the pool wishes to proceed with settlement + */ + function onBeforeRemoveLiquidity( + address router, + address pool, + RemoveLiquidityKind kind, + uint256 maxBptAmountIn, + uint256[] memory minAmountsOutScaled18, + uint256[] memory balancesScaled18, + bytes memory userData + ) external returns (bool success); + + /** + * @notice Hook to be executed after removing liquidity. + * @dev Called if the `shouldCallAfterRemoveLiquidity` flag is set in the configuration. The Vault will ignore + * `hookAdjustedAmountsOutRaw` unless `enableHookAdjustedAmounts` is true. Hook contracts should use the + * `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param router The address (usually a router contract) that initiated a remove liquidity operation on the Vault + * @param pool Pool address, used to fetch pool information from the Vault (pool config, tokens, etc.) + * @param kind The type of remove liquidity operation (e.g., proportional, custom) + * @param bptAmountIn Amount of pool tokens to burn + * @param amountsOutScaled18 Scaled amount of tokens to receive, sorted in token registration order + * @param amountsOutRaw Actual amount of tokens to receive, sorted in token registration order + * @param balancesScaled18 Current pool balances, sorted in token registration order + * @param userData Additional (optional) data provided by the user + * @return success True if the pool wishes to proceed with settlement + * @return hookAdjustedAmountsOutRaw New amountsOutRaw, potentially modified by the hook + */ + function onAfterRemoveLiquidity( + address router, + address pool, + RemoveLiquidityKind kind, + uint256 bptAmountIn, + uint256[] memory amountsOutScaled18, + uint256[] memory amountsOutRaw, + uint256[] memory balancesScaled18, + bytes memory userData + ) external returns (bool success, uint256[] memory hookAdjustedAmountsOutRaw); + + /*************************************************************************** + Swap + ***************************************************************************/ + + /** + * @notice Called before a swap to give the Pool an opportunity to perform actions. + * @dev Called if the `shouldCallBeforeSwap` flag is set in the configuration. Hook contracts should use the + * `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param params Swap parameters (see PoolSwapParams for struct definition) + * @param pool Pool address, used to get pool information from the Vault (poolData, token config, etc.) + * @return success True if the pool wishes to proceed with settlement + */ + function onBeforeSwap(PoolSwapParams calldata params, address pool) external returns (bool success); + + /** + * @notice Called after a swap to perform further actions once the balances have been updated by the swap. + * @dev Called if the `shouldCallAfterSwap` flag is set in the configuration. The Vault will ignore + * `hookAdjustedAmountCalculatedRaw` unless `enableHookAdjustedAmounts` is true. Hook contracts should + * use the `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param params Swap parameters (see above for struct definition) + * @return success True if the pool wishes to proceed with settlement + * @return hookAdjustedAmountCalculatedRaw New amount calculated, potentially modified by the hook + */ + function onAfterSwap( + AfterSwapParams calldata params + ) external returns (bool success, uint256 hookAdjustedAmountCalculatedRaw); + + /** + * @notice Called after `onBeforeSwap` and before the main swap operation, if the pool has dynamic fees. + * @dev Called if the `shouldCallComputeDynamicSwapFee` flag is set in the configuration. Hook contracts should use + * the `onlyVault` modifier to guarantee this is only called by the Vault. + * + * @param params Swap parameters (see PoolSwapParams for struct definition) + * @param pool Pool address, used to get pool information from the Vault (poolData, token config, etc.) + * @param staticSwapFeePercentage 18-decimal FP value of the static swap fee percentage, for reference + * @return success True if the pool wishes to proceed with settlement + * @return dynamicSwapFeePercentage Value of the swap fee percentage, as an 18-decimal FP value + */ + function onComputeDynamicSwapFeePercentage( + PoolSwapParams calldata params, + address pool, + uint256 staticSwapFeePercentage + ) external view returns (bool success, uint256 dynamicSwapFeePercentage); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IProtocolFeeController.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IProtocolFeeController.sol new file mode 100644 index 00000000..e00bc6f0 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IProtocolFeeController.sol @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "./IVault.sol"; + +/// @notice Contract that handles protocol and pool creator fees for the Vault. +interface IProtocolFeeController { + /** + * @notice Emitted when the protocol swap fee percentage is updated. + * @param swapFeePercentage The updated protocol swap fee percentage + */ + event GlobalProtocolSwapFeePercentageChanged(uint256 swapFeePercentage); + + /** + * @notice Emitted when the protocol yield fee percentage is updated. + * @param yieldFeePercentage The updated protocol yield fee percentage + */ + event GlobalProtocolYieldFeePercentageChanged(uint256 yieldFeePercentage); + + /** + * @notice Emitted when the protocol swap fee percentage is updated for a specific pool. + * @param pool The pool whose protocol swap fee will be changed + * @param swapFeePercentage The updated protocol swap fee percentage + */ + event ProtocolSwapFeePercentageChanged(address indexed pool, uint256 swapFeePercentage); + + /** + * @notice Emitted when the protocol yield fee percentage is updated for a specific pool. + * @param pool The pool whose protocol yield fee will be changed + * @param yieldFeePercentage The updated protocol yield fee percentage + */ + event ProtocolYieldFeePercentageChanged(address indexed pool, uint256 yieldFeePercentage); + + /** + * @notice Emitted when the pool creator swap fee percentage of a pool is updated. + * @param poolCreatorSwapFeePercentage The new pool creator swap fee percentage for the pool + */ + event PoolCreatorSwapFeePercentageChanged(address indexed pool, uint256 poolCreatorSwapFeePercentage); + + /** + * @notice Emitted when the pool creator yield fee percentage of a pool is updated. + * @param poolCreatorYieldFeePercentage The new pool creator yield fee percentage for the pool + */ + event PoolCreatorYieldFeePercentageChanged(address indexed pool, uint256 poolCreatorYieldFeePercentage); + + /** + * @notice Logs the collection of protocol swap fees in a specific token and amount. + * @dev Note that since charging protocol fees (i.e., distributing tokens between pool and fee balances) occurs + * in the Vault, but fee collection happens in the ProtocolFeeController, the swap fees reported here may encompass + * multiple operations. + * + * @param pool The pool on which the swap fee was charged + * @param token The token in which the swap fee was charged + * @param amount The amount of the token collected in fees + */ + event ProtocolSwapFeeCollected(address indexed pool, IERC20 indexed token, uint256 amount); + + /** + * @notice Logs the collection of protocol yield fees in a specific token and amount. + * @dev Note that since charging protocol fees (i.e., distributing tokens between pool and fee balances) occurs + * in the Vault, but fee collection happens in the ProtocolFeeController, the yield fees reported here may encompass + * multiple operations. + * + * @param pool The pool on which the yield fee was charged + * @param token The token in which the yield fee was charged + * @param amount The amount of the token collected in fees + */ + event ProtocolYieldFeeCollected(address indexed pool, IERC20 indexed token, uint256 amount); + + /** + * @notice Logs the withdrawal of protocol fees in a specific token and amount. + * @param pool The pool from which protocol fees are being withdrawn + * @param token The token being withdrawn + * @param recipient The recipient of the funds + * @param amount The amount of the fee token that was withdrawn + */ + event ProtocolFeesWithdrawn(address indexed pool, IERC20 indexed token, address indexed recipient, uint256 amount); + + /** + * @notice Logs the withdrawal of pool creator fees in a specific token and amount. + * @param pool The pool from which pool creator fees are being withdrawn + * @param token The token being withdrawn + * @param recipient The recipient of the funds (the pool creator if permissionless, or another account) + * @param amount The amount of the fee token that was withdrawn + */ + event PoolCreatorFeesWithdrawn( + address indexed pool, + IERC20 indexed token, + address indexed recipient, + uint256 amount + ); + + /** + * @notice Error raised when the protocol swap fee percentage exceeds the maximum allowed value. + * @dev Note that this is checked for both the global and pool-specific protocol swap fee percentages. + */ + error ProtocolSwapFeePercentageTooHigh(); + + /** + * @notice Error raised when the protocol yield fee percentage exceeds the maximum allowed value. + * @dev Note that this is checked for both the global and pool-specific protocol swap fee percentages. + */ + error ProtocolYieldFeePercentageTooHigh(); + + /** + * @notice Error raised if there is no pool creator on a withdrawal attempt from the given pool. + * @param pool The pool with no creator + */ + error PoolCreatorNotRegistered(address pool); + + /** + * @notice Error raised if the wrong account attempts to withdraw pool creator fees. + * @param caller The account attempting to withdraw pool creator fees + * @param pool The pool the caller tried to withdraw from + */ + error CallerIsNotPoolCreator(address caller, address pool); + + /// @notice Error raised when the pool creator swap or yield fee percentage exceeds the maximum allowed value. + error PoolCreatorFeePercentageTooHigh(); + + /** + * @notice Get the address of the main Vault contract. + * @return vault The Vault address + */ + function vault() external view returns (IVault); + + /** + * @notice Collects aggregate fees from the Vault for a given pool. + * @param pool The pool with aggregate fees + */ + function collectAggregateFees(address pool) external; + + /** + * @notice Getter for the current global protocol swap fee. + * @return protocolSwapFeePercentage The global protocol swap fee percentage + */ + function getGlobalProtocolSwapFeePercentage() external view returns (uint256); + + /** + * @notice Getter for the current global protocol yield fee. + * @return protocolYieldFeePercentage The global protocol yield fee percentage + */ + function getGlobalProtocolYieldFeePercentage() external view returns (uint256); + + /** + * @notice Getter for the current protocol swap fee for a given pool. + * @param pool The address of the pool + * @return protocolSwapFeePercentage The global protocol swap fee percentage + * @return isOverride True if the protocol fee has been overridden + */ + function getPoolProtocolSwapFeeInfo(address pool) external view returns (uint256, bool); + + /** + * @notice Getter for the current protocol yield fee for a given pool. + * @param pool The address of the pool + * @return protocolYieldFeePercentage The global protocol yield fee percentage + * @return isOverride True if the protocol fee has been overridden + */ + function getPoolProtocolYieldFeeInfo(address pool) external view returns (uint256, bool); + + /** + * @notice Returns the amount of each pool token allocated to the protocol for withdrawal. + * @dev Includes both swap and yield fees. + * @param pool The address of the pool on which fees were collected + * @return feeAmounts The total amounts of each token available for withdrawal, sorted in token registration order + */ + function getProtocolFeeAmounts(address pool) external view returns (uint256[] memory feeAmounts); + + /** + * @notice Returns the amount of each pool token allocated to the pool creator for withdrawal. + * @dev Includes both swap and yield fees. + * @param pool The address of the pool on which fees were collected + * @return feeAmounts The total amounts of each token available for withdrawal, sorted in token registration order + */ + function getPoolCreatorFeeAmounts(address pool) external view returns (uint256[] memory feeAmounts); + + /** + * @notice Returns a calculated aggregate percentage from protocol and pool creator fee percentages. + * @dev Not tied to any particular pool; this just performs the low-level "additive fee" calculation. + * Note that pool creator fees are calculated based on creatorAndLpFees, and not in totalFees. + * Since aggregate fees are stored in the Vault with 24-bit precision, this will revert if greater + * precision would be required. + * + * See example below: + * + * tokenOutAmount = 10000; poolSwapFeePct = 10%; protocolFeePct = 40%; creatorFeePct = 60% + * totalFees = tokenOutAmount * poolSwapFeePct = 10000 * 10% = 1000 + * protocolFees = totalFees * protocolFeePct = 1000 * 40% = 400 + * creatorAndLpFees = totalFees - protocolFees = 1000 - 400 = 600 + * creatorFees = creatorAndLpFees * creatorFeePct = 600 * 60% = 360 + * lpFees (will stay in the pool) = creatorAndLpFees - creatorFees = 600 - 360 = 240 + * + * @param protocolFeePercentage The protocol portion of the aggregate fee percentage + * @param poolCreatorFeePercentage The pool creator portion of the aggregate fee percentage + * @return aggregateFeePercentage The computed aggregate percentage + */ + function computeAggregateFeePercentage( + uint256 protocolFeePercentage, + uint256 poolCreatorFeePercentage + ) external pure returns (uint256 aggregateFeePercentage); + + /** + * @notice Override the protocol swap fee percentage for a specific pool. + * @dev This is a permissionless call, and will set the pool's fee to the current global fee, if it is different + * from the current value, and the fee is not controlled by governance (i.e., has never been overridden). + * + * @param pool The pool for which we are setting the protocol swap fee + */ + function updateProtocolSwapFeePercentage(address pool) external; + + /** + * @notice Override the protocol yield fee percentage for a specific pool. + * @dev This is a permissionless call, and will set the pool's fee to the current global fee, if it is different + * from the current value, and the fee is not controlled by governance (i.e., has never been overridden). + * + * @param pool The pool for which we are setting the protocol yield fee + */ + function updateProtocolYieldFeePercentage(address pool) external; + + /*************************************************************************** + Permissioned Functions + ***************************************************************************/ + + /** + * @notice Add pool-specific entries to the protocol swap and yield percentages. + * @dev This must be called from the Vault during pool registration. It will initialize the pool to the global + * protocol fee percentage values (or 0, if the `protocolFeeExempt` flags is set), and return the initial aggregate + * fee percentages, based on an initial pool creator fee of 0. + * + * @param pool The address of the pool being registered + * @param poolCreator The address of the pool creator (or 0 if there won't be a pool creator fee) + * @param protocolFeeExempt If true, the pool is initially exempt from protocol fees + * @return aggregateSwapFeePercentage The initial aggregate swap fee percentage + * @return aggregateYieldFeePercentage The initial aggregate yield fee percentage + */ + function registerPool( + address pool, + address poolCreator, + bool protocolFeeExempt + ) external returns (uint256 aggregateSwapFeePercentage, uint256 aggregateYieldFeePercentage); + + /** + * @notice Set the global protocol swap fee percentage, used by standard pools. + * @param newProtocolSwapFeePercentage The new protocol swap fee percentage + */ + function setGlobalProtocolSwapFeePercentage(uint256 newProtocolSwapFeePercentage) external; + + /** + * @notice Set the global protocol yield fee percentage, used by standard pools. + * @param newProtocolYieldFeePercentage The new protocol yield fee percentage + */ + function setGlobalProtocolYieldFeePercentage(uint256 newProtocolYieldFeePercentage) external; + + /** + * @notice Override the protocol swap fee percentage for a specific pool. + * @param pool The address of the pool for which we are setting the protocol swap fee + * @param newProtocolSwapFeePercentage The new protocol swap fee percentage for the pool + */ + function setProtocolSwapFeePercentage(address pool, uint256 newProtocolSwapFeePercentage) external; + + /** + * @notice Override the protocol yield fee percentage for a specific pool. + * @param pool The address of the pool for which we are setting the protocol yield fee + * @param newProtocolYieldFeePercentage The new protocol yield fee percentage for the pool + */ + function setProtocolYieldFeePercentage(address pool, uint256 newProtocolYieldFeePercentage) external; + + /** + * @notice Assigns a new pool creator swap fee percentage to the specified pool. + * @dev Fees are divided between the protocol, pool creator, and LPs. The pool creator percentage is applied to + * the "net" amount after protocol fees, and divides the remainder between the pool creator and LPs. If the + * pool creator fee is 100%, none of the fee amount remains in the pool for LPs. + * + * @param pool The address of the pool for which the pool creator fee will be changed + * @param poolCreatorSwapFeePercentage The new pool creator swap fee percentage to apply to the pool + */ + function setPoolCreatorSwapFeePercentage(address pool, uint256 poolCreatorSwapFeePercentage) external; + + /** + * @notice Assigns a new pool creator yield fee percentage to the specified pool. + * @dev Fees are divided between the protocol, pool creator, and LPs. The pool creator percentage is applied to + * the "net" amount after protocol fees, and divides the remainder between the pool creator and LPs. If the + * pool creator fee is 100%, none of the fee amount remains in the pool for LPs. + * + * @param pool The address of the pool for which the pool creator fee will be changed + * @param poolCreatorYieldFeePercentage The new pool creator yield fee percentage to apply to the pool + */ + function setPoolCreatorYieldFeePercentage(address pool, uint256 poolCreatorYieldFeePercentage) external; + + /** + * @notice Withdraw collected protocol fees for a given pool. This is a permissioned function. + * @dev Sends swap and yield protocol fees to the recipient. + * @param pool The pool on which fees were collected + * @param recipient Address to send the tokens + */ + function withdrawProtocolFees(address pool, address recipient) external; + + /** + * @notice Withdraw collected pool creator fees for a given pool. This is a permissioned function. + * @dev Sends swap and yield pool creator fees to the recipient. + * @param pool The pool on which fees were collected + * @param recipient Address to send the tokens + */ + function withdrawPoolCreatorFees(address pool, address recipient) external; + + /** + * @notice Withdraw collected pool creator fees for a given pool. + * @dev Sends swap and yield pool creator fees to the registered poolCreator. + * @param pool The pool on which fees were collected + */ + function withdrawPoolCreatorFees(address pool) external; +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRateProvider.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRateProvider.sol new file mode 100644 index 00000000..d2e49dc6 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRateProvider.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/// @notice General interface for token exchange rates. +interface IRateProvider { + /** + * @dev Returns an 18 decimal fixed point number that is the exchange rate of the token to some other underlying + * token. The meaning of this rate depends on the context. + */ + function getRate() external view returns (uint256); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRouterCommon.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRouterCommon.sol new file mode 100644 index 00000000..4bd84fe1 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRouterCommon.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IAllowanceTransfer } from "./IAllowanceTransfer.sol"; +import { AddLiquidityKind, RemoveLiquidityKind } from "./VaultTypes.sol"; + +/// @notice Interface for functions shared between the `Router` and `BatchRouter`. +interface IRouterCommon { + /** + * @notice Data for the add liquidity hook. + * @param sender Account originating the add liquidity operation + * @param pool Address of the liquidity pool + * @param maxAmountsIn Maximum amounts of tokens to be added, sorted in token registration order + * @param minBptAmountOut Minimum amount of pool tokens to be received + * @param kind Type of join (e.g., single or multi-token) + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to add liquidity + */ + struct AddLiquidityHookParams { + address sender; + address pool; + uint256[] maxAmountsIn; + uint256 minBptAmountOut; + AddLiquidityKind kind; + bool wethIsEth; + bytes userData; + } + + /** + * @notice Data for the remove liquidity hook. + * @param sender Account originating the remove liquidity operation + * @param pool Address of the liquidity pool + * @param minAmountsOut Minimum amounts of tokens to be received, sorted in token registration order + * @param maxBptAmountIn Maximum amount of pool tokens provided + * @param kind Type of exit (e.g., single or multi-token) + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to remove liquidity + */ + struct RemoveLiquidityHookParams { + address sender; + address pool; + uint256[] minAmountsOut; + uint256 maxBptAmountIn; + RemoveLiquidityKind kind; + bool wethIsEth; + bytes userData; + } + + /** + * @notice Get the first sender which initialized the call to Router. + * @return address The sender address + */ + function getSender() external view returns (address); + + /******************************************************************************* + Utils + *******************************************************************************/ + + struct PermitApproval { + address token; + address owner; + address spender; + uint256 amount; + uint256 nonce; + uint256 deadline; + } + + /** + * @notice Permits multiple allowances and executes a batch of function calls on this contract. + * @param permitBatch An array of `PermitApproval` structs, each representing an ERC20 permit request + * @param permitSignatures An array of bytes, corresponding to the permit request signature in `permitBatch` + * @param permit2Batch A batch of permit2 approvals + * @param permit2Signature A permit2 signature for the batch approval + * @param multicallData An array of bytes arrays, each representing an encoded function call on this contract + * @return results Array of bytes arrays, each representing the return data from each function call executed + */ + function permitBatchAndCall( + PermitApproval[] calldata permitBatch, + bytes[] calldata permitSignatures, + IAllowanceTransfer.PermitBatch calldata permit2Batch, + bytes calldata permit2Signature, + bytes[] calldata multicallData + ) external payable returns (bytes[] memory results); + + /** + * @notice Executes a batch of function calls on this contract. + * @param data Encoded function calls to be executed in the batch. + * @return results Array of bytes arrays, each representing the return data from each function call executed. + */ + function multicall(bytes[] calldata data) external payable returns (bytes[] memory results); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVault.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVault.sol new file mode 100644 index 00000000..b1b1b08c --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVault.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IAuthentication } from "./IAuthentication.sol"; +import { IVaultAdmin } from "./IVaultAdmin.sol"; +import { IVaultExtension } from "./IVaultExtension.sol"; +import { IVaultMain } from "./IVaultMain.sol"; +import { IVaultErrors } from "./IVaultErrors.sol"; +import { IVaultEvents } from "./IVaultEvents.sol"; + +interface IVault is IVaultMain, IVaultExtension, IVaultAdmin, IVaultErrors, IVaultEvents, IAuthentication { + /// @dev Returns the main Vault address. + function vault() external view override(IVaultAdmin, IVaultExtension) returns (IVault); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultAdmin.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultAdmin.sol new file mode 100644 index 00000000..367617c9 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultAdmin.sol @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { IProtocolFeeController } from "./IProtocolFeeController.sol"; +import { IAuthorizer } from "./IAuthorizer.sol"; +import { IVault } from "./IVault.sol"; + +/** + * @notice Interface for functions defined on the `VaultAdmin` contract. + * @dev `VaultAdmin` is the Proxy extension of `VaultExtension`, and handles the least critical operations, + * as two delegate calls add gas to each call. Most of the permissioned calls are here. + */ +interface IVaultAdmin { + /******************************************************************************* + Constants and immutables + *******************************************************************************/ + + /** + * @notice Returns the main Vault address. + * @dev The main Vault contains the entrypoint and main liquidity operation implementations. + * @return vault The address of the main Vault + */ + function vault() external view returns (IVault); + + /** + * @notice Returns the Vault's pause window end time. + * @dev This value is immutable, and represents the timestamp after which the Vault can no longer be paused + * by governance. + * + * @return pauseWindowEndTime The timestamp when the Vault's pause window ends + */ + function getPauseWindowEndTime() external view returns (uint32); + + /** + * @notice Returns the Vault's buffer period duration. + * @dev This value is immutable. It represents the period during which, if paused, the Vault will remain paused. + * This ensures there is time available to address whatever issue caused the Vault to be paused. + * + * @return bufferPeriodDuration The length of the buffer period in seconds + */ + function getBufferPeriodDuration() external view returns (uint32); + + /** + * @notice Returns the Vault's buffer period end time. + * @dev This value is immutable. If already paused, the Vault can be unpaused until this timestamp. + * @return bufferPeriodEndTime The timestamp after which the Vault remains permanently unpaused + */ + function getBufferPeriodEndTime() external view returns (uint32); + + /** + * @notice Get the minimum number of tokens in a pool. + * @dev We expect the vast majority of pools to be 2-token. + * @return minTokens The minimum token count of a pool + */ + function getMinimumPoolTokens() external pure returns (uint256); + + /** + * @notice Get the maximum number of tokens in a pool. + * @return maxTokens The maximum token count of a pool + */ + function getMaximumPoolTokens() external pure returns (uint256); + + /** + * @notice Get the minimum total supply of pool tokens (BPT) for an initialized pool. + * @dev This prevents pools from being completely drained. When the pool is initialized, this minimum amount of BPT + * is minted to the zero address. This is an 18-decimal floating point number; BPT are always 18 decimals. + * + * @return minimumTotalSupply The minimum total supply a pool can have after initialization + */ + function getPoolMinimumTotalSupply() external pure returns (uint256); + + /** + * @notice Get the minimum total supply of an ERC4626 wrapped token buffer in the Vault. + * @dev This prevents buffers from being completely drained. When the buffer is initialized, this minimum number + * of shares is added to the shares resulting from the initial deposit. Buffer total supply accounting is internal + * to the Vault, as buffers are not tokenized. + * + * @return minimumTotalSupply The minimum total supply a buffer can have after initialization + */ + function getBufferMinimumTotalSupply() external pure returns (uint256); + + /** + * @notice Get the minimum trade amount in a pool operation. + * @dev This limit is applied to the 18-decimal "upscaled" amount in any operation (swap, add/remove liquidity). + * @return minimumTradeAmount The minimum trade amount as an 18-decimal floating point number + */ + function getMinimumTradeAmount() external view returns (uint256); + + /** + * @notice Get the minimum wrap amount in a buffer operation. + * @dev This limit is applied to the wrap operation amount, in native underlying token decimals. + * @return minimumWrapAmount The minimum wrap amount in native underlying token decimals + */ + function getMinimumWrapAmount() external view returns (uint256); + + /******************************************************************************* + Vault Pausing + *******************************************************************************/ + + /** + * @notice Indicates whether the Vault is paused. + * @dev If the Vault is paused, all non-Recovery Mode state-changing operations will revert. + * @return paused True if the Vault is paused + */ + function isVaultPaused() external view returns (bool); + + /** + * @notice Returns the paused status, and end times of the Vault's pause window and buffer period. + * @return paused True if the Vault is paused + * @return vaultPauseWindowEndTime The timestamp of the end of the Vault's pause window + * @return vaultBufferPeriodEndTime The timestamp of the end of the Vault's buffer period + */ + function getVaultPausedState() external view returns (bool, uint32, uint32); + + /** + * @notice Pause the Vault: an emergency action which disables all operational state-changing functions. + * @dev This is a permissioned function that will only work during the Pause Window set during deployment. + */ + function pauseVault() external; + + /** + * @notice Reverse a `pause` operation, and restore the Vault to normal functionality. + * @dev This is a permissioned function that will only work on a paused Vault within the Buffer Period set during + * deployment. Note that the Vault will automatically unpause after the Buffer Period expires. + */ + function unpauseVault() external; + + /******************************************************************************* + Pool Pausing + *******************************************************************************/ + + /** + * @notice Pause the Pool: an emergency action which disables all pool functions. + * @dev This is a permissioned function that will only work during the Pause Window set during pool factory + * deployment. + * + * @param pool The pool being paused + */ + function pausePool(address pool) external; + + /** + * @notice Reverse a `pause` operation, and restore the Pool to normal functionality. + * @dev This is a permissioned function that will only work on a paused Pool within the Buffer Period set during + * deployment. Note that the Pool will automatically unpause after the Buffer Period expires. + * + * @param pool The pool being unpaused + */ + function unpausePool(address pool) external; + + /******************************************************************************* + Fees + *******************************************************************************/ + + /** + * @notice Assigns a new static swap fee percentage to the specified pool. + * @dev This is a permissioned function, disabled if the pool is paused. The swap fee percentage must be within + * the bounds specified by the pool's implementation of `ISwapFeePercentageBounds`. + * Emits the SwapFeePercentageChanged event. + * + * @param pool The address of the pool for which the static swap fee will be changed + * @param swapFeePercentage The new swap fee percentage to apply to the pool + */ + function setStaticSwapFeePercentage(address pool, uint256 swapFeePercentage) external; + + /** + * @notice Collects accumulated aggregate swap and yield fees for the specified pool. + * @dev Fees are sent to the ProtocolFeeController address. + * @param pool The pool on which all aggregate fees should be collected + * @return swapFeeAmounts An array with the total swap fees collected, sorted in token registration order + * @return yieldFeeAmounts An array with the total yield fees collected, sorted in token registration order + */ + function collectAggregateFees( + address pool + ) external returns (uint256[] memory swapFeeAmounts, uint256[] memory yieldFeeAmounts); + + /** + * @notice Update an aggregate swap fee percentage. + * @dev Can only be called by the current protocol fee controller. Called when governance overrides a protocol fee + * for a specific pool, or to permissionlessly update a pool to a changed global protocol fee value (if the pool's + * fee has not previously been set by governance). Ensures the aggregate percentage <= FixedPoint.ONE, and also + * that the final value does not lose precision when stored in 24 bits (see `FEE_BITLENGTH` in VaultTypes.sol). + * Emits an `AggregateSwapFeePercentageChanged` event. + * + * @param pool The pool whose fee will be updated + * @param newAggregateSwapFeePercentage The new aggregate swap fee percentage + */ + function updateAggregateSwapFeePercentage(address pool, uint256 newAggregateSwapFeePercentage) external; + + /** + * @notice Update an aggregate yield fee percentage. + * @dev Can only be called by the current protocol fee controller. Called when governance overrides a protocol fee + * for a specific pool, or to permissionlessly update a pool to a changed global protocol fee value (if the pool's + * fee has not previously been set by governance). Ensures the aggregate percentage <= FixedPoint.ONE, and also + * that the final value does not lose precision when stored in 24 bits (see `FEE_BITLENGTH` in VaultTypes.sol). + * Emits an `AggregateYieldFeePercentageChanged` event. + * + * @param pool The pool whose fee will be updated + * @param newAggregateYieldFeePercentage The new aggregate yield fee percentage + */ + function updateAggregateYieldFeePercentage(address pool, uint256 newAggregateYieldFeePercentage) external; + + /** + * @notice Sets a new Protocol Fee Controller for the Vault. + * @dev This is a permissioned call. Emits a `ProtocolFeeControllerChanged` event. + * @param newProtocolFeeController The address of the new Protocol Fee Controller + */ + function setProtocolFeeController(IProtocolFeeController newProtocolFeeController) external; + + /******************************************************************************* + Recovery Mode + *******************************************************************************/ + + /** + * @notice Enable recovery mode for a pool. + * @dev This is a permissioned function. It enables a safe proportional withdrawal, with no external calls. + * Since there are no external calls, live balances cannot be updated while in Recovery Mode. + * + * @param pool The address of the pool + */ + function enableRecoveryMode(address pool) external; + + /** + * @notice Disable recovery mode for a pool. + * @dev This is a permissioned function. It re-syncs live balances (which could not be updated during + * Recovery Mode), forfeiting any yield fees that accrued while enabled. It makes external calls, and could + * potentially fail if there is an issue with any associated Rate Providers. + * + * @param pool The address of the pool + */ + function disableRecoveryMode(address pool) external; + + /******************************************************************************* + Queries + *******************************************************************************/ + + /// @notice Disables queries functionality on the Vault. Can only be called by governance. + function disableQuery() external; + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /** + * @notice Indicates whether the Vault buffers are paused. + * @dev When buffers are paused, all buffer operations (i.e., calls on the Router with `isBuffer` true) + * will revert. Pausing buffers is reversible. + * + * @return buffersPaused True if the Vault buffers are paused + */ + function areBuffersPaused() external view returns (bool); + + /** + * @notice Pauses native vault buffers globally. + * @dev When buffers are paused, it's not possible to add liquidity or wrap/unwrap tokens using the Vault's + * `erc4626BufferWrapOrUnwrap` primitive. However, it's still possible to remove liquidity. Currently it's not + * possible to pause vault buffers individually. + * + * This is a permissioned call, and is reversible (see `unpauseVaultBuffers`). + */ + function pauseVaultBuffers() external; + + /** + * @notice Unpauses native vault buffers globally. + * @dev When buffers are paused, it's not possible to add liquidity or wrap/unwrap tokens using the Vault's + * `erc4626BufferWrapOrUnwrap` primitive. However, it's still possible to remove liquidity. + * + * This is a permissioned call. + */ + function unpauseVaultBuffers() external; + + /** + * @notice Initializes buffer for the given wrapped token. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param amountUnderlyingRaw Amount of underlying tokens that will be deposited into the buffer + * @param amountWrappedRaw Amount of wrapped tokens that will be deposited into the buffer + * @param sharesOwner Address that will own the deposited liquidity. Only this address will be able to remove + * liquidity from the buffer + * @return issuedShares the amount of tokens sharesOwner has in the buffer, expressed in underlying token amounts. + * (it is the BPT of an internal ERC4626 buffer). It is expressed in underlying token native decimals. + */ + function initializeBuffer( + IERC4626 wrappedToken, + uint256 amountUnderlyingRaw, + uint256 amountWrappedRaw, + address sharesOwner + ) external returns (uint256 issuedShares); + + /** + * @notice Adds liquidity to an internal ERC4626 buffer in the Vault, proportionally. + * @dev The buffer needs to be initialized beforehand. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param exactSharesToIssue The value in underlying tokens that `sharesOwner` wants to add to the buffer, + * in underlying token decimals + * @param sharesOwner Address that will own the deposited liquidity. Only this address will be able to remove + * liquidity from the buffer + * @return amountUnderlyingRaw Amount of underlying tokens deposited into the buffer + * @return amountWrappedRaw Amount of wrapped tokens deposited into the buffer + */ + function addLiquidityToBuffer( + IERC4626 wrappedToken, + uint256 exactSharesToIssue, + address sharesOwner + ) external returns (uint256 amountUnderlyingRaw, uint256 amountWrappedRaw); + + /** + * @notice Removes liquidity from an internal ERC4626 buffer in the Vault. + * @dev Only proportional exits are supported, and the sender has to be the owner of the shares. + * This function unlocks the Vault just for this operation; it does not work with a Router as an entrypoint. + * + * Pre-conditions: + * - The buffer needs to be initialized. + * - sharesOwner is the original msg.sender, it needs to be checked in the Router. That's why + * this call is authenticated; only routers approved by the DAO can remove the liquidity of a buffer. + * - The buffer needs to have some liquidity and have its asset registered in `_bufferAssets` storage. + * + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param sharesToRemove Amount of shares to remove from the buffer. Cannot be greater than sharesOwner's + * total shares. It is expressed in underlying token native decimals. + * @return removedUnderlyingBalanceRaw Amount of underlying tokens returned to the user + * @return removedWrappedBalanceRaw Amount of wrapped tokens returned to the user + */ + function removeLiquidityFromBuffer( + IERC4626 wrappedToken, + uint256 sharesToRemove + ) external returns (uint256 removedUnderlyingBalanceRaw, uint256 removedWrappedBalanceRaw); + + /** + * @notice Returns the asset registered for a given wrapped token. + * @dev The asset can never change after buffer initialization. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @return underlyingToken Address of the underlying token registered for the wrapper; `address(0)` if the buffer + * has not been initialized. + */ + function getBufferAsset(IERC4626 wrappedToken) external view returns (address underlyingToken); + + /** + * @notice Returns the shares (internal buffer BPT) of a liquidity owner: a user that deposited assets + * in the buffer. + * + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param liquidityOwner Address of the user that owns liquidity in the wrapped token's buffer + * @return ownerShares Amount of shares allocated to the liquidity owner, in native underlying token decimals + */ + function getBufferOwnerShares( + IERC4626 wrappedToken, + address liquidityOwner + ) external view returns (uint256 ownerShares); + + /** + * @notice Returns the supply shares (internal buffer BPT) of the ERC4626 buffer. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @return bufferShares Amount of supply shares of the buffer, in native underlying token decimals + */ + function getBufferTotalShares(IERC4626 wrappedToken) external view returns (uint256 bufferShares); + + /** + * @notice Returns the amount of underlying and wrapped tokens deposited in the internal buffer of the Vault. + * @dev All values are in native token decimals of the wrapped or underlying tokens. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @return underlyingBalanceRaw Amount of underlying tokens deposited into the buffer, in native token decimals + * @return wrappedBalanceRaw Amount of wrapped tokens deposited into the buffer, in native token decimals + */ + function getBufferBalance( + IERC4626 wrappedToken + ) external view returns (uint256 underlyingBalanceRaw, uint256 wrappedBalanceRaw); + + /******************************************************************************* + Authentication + *******************************************************************************/ + + /** + * @notice Sets a new Authorizer for the Vault. + * @dev This is a permissioned call. Emits an `AuthorizerChanged` event. + * @param newAuthorizer The address of the new authorizer + */ + function setAuthorizer(IAuthorizer newAuthorizer) external; +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultErrors.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultErrors.sol new file mode 100644 index 00000000..d0f2edbf --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultErrors.sol @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +/// @notice Errors are declared inside an interface (namespace) to improve DX with Typechain. +interface IVaultErrors { + /******************************************************************************* + Registration and Initialization + *******************************************************************************/ + + /** + * @notice A pool has already been registered. `registerPool` may only be called once. + * @param pool The already registered pool + */ + error PoolAlreadyRegistered(address pool); + + /** + * @notice A pool has already been initialized. `initialize` may only be called once. + * @param pool The already initialized pool + */ + error PoolAlreadyInitialized(address pool); + + /** + * @notice A pool has not been registered. + * @param pool The unregistered pool + */ + error PoolNotRegistered(address pool); + + /** + * @notice A referenced pool has not been initialized. + * @param pool The uninitialized pool + */ + error PoolNotInitialized(address pool); + + /** + * @notice A hook contract rejected a pool on registration. + * @param poolHooksContract Address of the hook contract that rejected the pool registration + * @param pool Address of the rejected pool + * @param poolFactory Address of the pool factory + */ + error HookRegistrationFailed(address poolHooksContract, address pool, address poolFactory); + + /** + * @notice A token was already registered (i.e., it is a duplicate in the pool). + * @param token The duplicate token + */ + error TokenAlreadyRegistered(IERC20 token); + + /// @notice The token count is below the minimum allowed. + error MinTokens(); + + /// @notice The token count is above the maximum allowed. + error MaxTokens(); + + /// @notice Invalid tokens (e.g., zero) cannot be registered. + error InvalidToken(); + + /// @notice The token type given in a TokenConfig during pool registration is invalid. + error InvalidTokenType(); + + /// @notice The data in a TokenConfig struct is inconsistent or unsupported. + error InvalidTokenConfiguration(); + + /// @notice Tokens with more than 18 decimals are not supported. + error InvalidTokenDecimals(); + + /** + * @notice The token list passed into an operation does not match the pool tokens in the pool. + * @param pool Address of the pool + * @param expectedToken The correct token at a given index in the pool + * @param actualToken The actual token found at that index + */ + error TokensMismatch(address pool, address expectedToken, address actualToken); + + /******************************************************************************* + Transient Accounting + *******************************************************************************/ + + /// @notice A transient accounting operation completed with outstanding token deltas. + error BalanceNotSettled(); + + /// @notice A user called a Vault function (swap, add/remove liquidity) outside the lock context. + error VaultIsNotUnlocked(); + + /// @notice The pool has returned false to the beforeSwap hook, indicating the transaction should revert. + error DynamicSwapFeeHookFailed(); + + /// @notice The pool has returned false to the beforeSwap hook, indicating the transaction should revert. + error BeforeSwapHookFailed(); + + /// @notice The pool has returned false to the afterSwap hook, indicating the transaction should revert. + error AfterSwapHookFailed(); + + /// @notice The pool has returned false to the beforeInitialize hook, indicating the transaction should revert. + error BeforeInitializeHookFailed(); + + /// @notice The pool has returned false to the afterInitialize hook, indicating the transaction should revert. + error AfterInitializeHookFailed(); + + /// @notice The pool has returned false to the beforeAddLiquidity hook, indicating the transaction should revert. + error BeforeAddLiquidityHookFailed(); + + /// @notice The pool has returned false to the afterAddLiquidity hook, indicating the transaction should revert. + error AfterAddLiquidityHookFailed(); + + /// @notice The pool has returned false to the beforeRemoveLiquidity hook, indicating the transaction should revert. + error BeforeRemoveLiquidityHookFailed(); + + /// @notice The pool has returned false to the afterRemoveLiquidity hook, indicating the transaction should revert. + error AfterRemoveLiquidityHookFailed(); + + /// @notice An unauthorized Router tried to call a permissioned function (i.e., using the Vault's token allowance). + error RouterNotTrusted(); + + /******************************************************************************* + Swaps + *******************************************************************************/ + + /// @notice The user tried to swap zero tokens. + error AmountGivenZero(); + + /// @notice The user attempted to swap a token for itself. + error CannotSwapSameToken(); + + /** + * @notice The user attempted to operate with a token that is not in the pool. + * @param token The unregistered token + */ + error TokenNotRegistered(IERC20 token); + + /** + * @notice An amount in or out has exceeded the limit specified in the swap request. + * @param amount The total amount in or out + * @param limit The amount of the limit that has been exceeded + */ + error SwapLimit(uint256 amount, uint256 limit); + + /** + * @notice A hook adjusted amount in or out has exceeded the limit specified in the swap request. + * @param amount The total amount in or out + * @param limit The amount of the limit that has been exceeded + */ + error HookAdjustedSwapLimit(uint256 amount, uint256 limit); + + /// @notice The amount given or calculated for an operation is below the minimum limit. + error TradeAmountTooSmall(); + + /******************************************************************************* + Add Liquidity + *******************************************************************************/ + + /// @notice Add liquidity kind not supported. + error InvalidAddLiquidityKind(); + + /** + * @notice A required amountIn exceeds the maximum limit specified for the operation. + * @param tokenIn The incoming token + * @param amountIn The total token amount in + * @param maxAmountIn The amount of the limit that has been exceeded + */ + error AmountInAboveMax(IERC20 tokenIn, uint256 amountIn, uint256 maxAmountIn); + + /** + * @notice A hook adjusted amountIn exceeds the maximum limit specified for the operation. + * @param tokenIn The incoming token + * @param amountIn The total token amount in + * @param maxAmountIn The amount of the limit that has been exceeded + */ + error HookAdjustedAmountInAboveMax(IERC20 tokenIn, uint256 amountIn, uint256 maxAmountIn); + + /** + * @notice The BPT amount received from adding liquidity is below the minimum specified for the operation. + * @param amountOut The total BPT amount out + * @param minAmountOut The amount of the limit that has been exceeded + */ + error BptAmountOutBelowMin(uint256 amountOut, uint256 minAmountOut); + + /// @notice Pool does not support adding liquidity with a customized input. + error DoesNotSupportAddLiquidityCustom(); + + /// @notice Pool does not support adding liquidity through donation. + error DoesNotSupportDonation(); + + /******************************************************************************* + Remove Liquidity + *******************************************************************************/ + + /// @notice Remove liquidity kind not supported. + error InvalidRemoveLiquidityKind(); + + /** + * @notice The actual amount out is below the minimum limit specified for the operation. + * @param tokenOut The outgoing token + * @param amountOut The total BPT amount out + * @param minAmountOut The amount of the limit that has been exceeded + */ + error AmountOutBelowMin(IERC20 tokenOut, uint256 amountOut, uint256 minAmountOut); + + /** + * @notice The hook adjusted amount out is below the minimum limit specified for the operation. + * @param tokenOut The outgoing token + * @param amountOut The total BPT amount out + * @param minAmountOut The amount of the limit that has been exceeded + */ + error HookAdjustedAmountOutBelowMin(IERC20 tokenOut, uint256 amountOut, uint256 minAmountOut); + + /** + * @notice The required BPT amount in exceeds the maximum limit specified for the operation. + * @param amountIn The total BPT amount in + * @param maxAmountIn The amount of the limit that has been exceeded + */ + error BptAmountInAboveMax(uint256 amountIn, uint256 maxAmountIn); + + /// @notice Pool does not support removing liquidity with a customized input. + error DoesNotSupportRemoveLiquidityCustom(); + + /******************************************************************************* + Fees + *******************************************************************************/ + + /** + * @notice Error raised when there is an overflow in the fee calculation. + * @dev This occurs when the sum of the parts (aggregate swap or yield fee) is greater than the whole + * (total swap or yield fee). Also validated when the protocol fee controller updates aggregate fee + * percentages in the Vault. + */ + error ProtocolFeesExceedTotalCollected(); + + /** + * @notice Error raised when the swap fee percentage is less than the minimum allowed value. + * @dev The Vault itself does not impose a universal minimum. Rather, it validates against the + * range specified by the `ISwapFeePercentageBounds` interface. and reverts with this error + * if it is below the minimum value returned by the pool. + * + * Pools with dynamic fees do not check these limits. + */ + error SwapFeePercentageTooLow(); + + /** + * @notice Error raised when the swap fee percentage is greater than the maximum allowed value. + * @dev The Vault itself does not impose a universal minimum. Rather, it validates against the + * range specified by the `ISwapFeePercentageBounds` interface. and reverts with this error + * if it is above the maximum value returned by the pool. + * + * Pools with dynamic fees do not check these limits. + */ + error SwapFeePercentageTooHigh(); + + /** + * @notice Primary fee percentages result in an aggregate fee that cannot be stored with the required precision. + * @dev Primary fee percentages are 18-decimal values, stored here in 64 bits, and calculated with full 256-bit + * precision. However, the resulting aggregate fees are stored in the Vault with 24-bit precision, which + * corresponds to 0.00001% resolution (i.e., a fee can be 1%, 1.00001%, 1.00002%, but not 1.000005%). + * Disallow setting fees such that there would be precision loss in the Vault, leading to a discrepancy between + * the aggregate fee calculated here and that stored in the Vault. + */ + error FeePrecisionTooHigh(); + + /** + * @notice A given percentage is above the maximum (usually FixedPoint.ONE, or 1e18 wei). + * @dev Providing the value might be helpful for debugging. + */ + error PercentageAboveMax(); + + /******************************************************************************* + Queries + *******************************************************************************/ + + /// @notice A user tried to execute a query operation when they were disabled. + error QueriesDisabled(); + + /******************************************************************************* + Recovery Mode + *******************************************************************************/ + + /** + * @notice Cannot enable recovery mode when already enabled. + * @param pool The pool + */ + error PoolInRecoveryMode(address pool); + + /** + * @notice Cannot disable recovery mode when not enabled. + * @param pool The pool + */ + error PoolNotInRecoveryMode(address pool); + + /******************************************************************************* + Authentication + *******************************************************************************/ + + /** + * @notice Error indicating the sender is not the Vault (e.g., someone is trying to call a permissioned function). + * @param sender The account attempting to call a permissioned function + */ + error SenderIsNotVault(address sender); + + /******************************************************************************* + Pausing + *******************************************************************************/ + + /// @notice The caller specified a pause window period longer than the maximum. + error VaultPauseWindowDurationTooLarge(); + + /// @notice The caller specified a buffer period longer than the maximum. + error PauseBufferPeriodDurationTooLarge(); + + /// @notice A user tried to perform an operation while the Vault was paused. + error VaultPaused(); + + /// @notice Governance tried to unpause the Vault when it was not paused. + error VaultNotPaused(); + + /// @notice Governance tried to pause the Vault after the pause period expired. + error VaultPauseWindowExpired(); + + /** + * @notice A user tried to perform an operation involving a paused Pool. + * @param pool The paused pool + */ + error PoolPaused(address pool); + + /** + * @notice Governance tried to unpause the Pool when it was not paused. + * @param pool The unpaused pool + */ + error PoolNotPaused(address pool); + + /** + * @notice Governance tried to pause a Pool after the pause period expired. + * @param pool The pool + */ + error PoolPauseWindowExpired(address pool); + + /******************************************************************************* + ERC4626 token buffers + *******************************************************************************/ + + /** + * @notice The buffer for the given wrapped token was already initialized. + * @param wrappedToken The wrapped token corresponding to the buffer + */ + error BufferAlreadyInitialized(IERC4626 wrappedToken); + + /** + * @notice The buffer for the given wrapped token was not initialized. + * @param wrappedToken The wrapped token corresponding to the buffer + */ + error BufferNotInitialized(IERC4626 wrappedToken); + + /// @notice The user is trying to remove more than their allocated shares from the buffer. + error NotEnoughBufferShares(); + + /** + * @notice The wrapped token asset does not match the underlying token. + * @dev This should never happen, but a malicious wrapper contract might not return the correct address. + * Legitimate wrapper contracts should make the asset a constant or immutable value. + * + * @param wrappedToken The wrapped token corresponding to the buffer + * @param underlyingToken The underlying token returned by `asset` + */ + error WrongUnderlyingToken(IERC4626 wrappedToken, address underlyingToken); + + /** + * @notice A wrapped token reported the zero address as its underlying token asset. + * @dev This should never happen, but a malicious wrapper contract might do this (e.g., in an attempt to + * re-initialize the buffer). + * + * @param wrappedToken The wrapped token corresponding to the buffer + */ + error InvalidUnderlyingToken(IERC4626 wrappedToken); + + /** + * @notice The amount given to wrap/unwrap was too small, which can introduce rounding issues. + * @param wrappedToken The wrapped token corresponding to the buffer + */ + error WrapAmountTooSmall(IERC4626 wrappedToken); + + /// @notice Buffer operation attempted while vault buffers are paused. + error VaultBuffersArePaused(); + + /// @notice Buffer shares were minted to the zero address. + error BufferSharesInvalidReceiver(); + + /// @notice Buffer shares were burned from the zero address. + error BufferSharesInvalidOwner(); + + /** + * @notice The total supply of a buffer can't be lower than the absolute minimum. + * @param totalSupply The total supply value that was below the minimum + */ + error BufferTotalSupplyTooLow(uint256 totalSupply); + + /// @dev A wrap/unwrap operation consumed more or returned less underlying tokens than it should. + error NotEnoughUnderlying(IERC4626 wrappedToken, uint256 expectedUnderlyingAmount, uint256 actualUnderlyingAmount); + + /// @dev A wrap/unwrap operation consumed more or returned less wrapped tokens than it should. + error NotEnoughWrapped(IERC4626 wrappedToken, uint256 expectedWrappedAmount, uint256 actualWrappedAmount); + + /******************************************************************************* + Miscellaneous + *******************************************************************************/ + + /// @notice Pool does not support adding / removing liquidity with an unbalanced input. + error DoesNotSupportUnbalancedLiquidity(); + + /// @notice The contract should not receive ETH. + error CannotReceiveEth(); + + /** + * @notice The `VaultExtension` contract was called by an account directly. + * @dev It can only be called by the Vault via delegatecall. + */ + error NotVaultDelegateCall(); + + /// @notice The `VaultExtension` contract was configured with an incorrect Vault address. + error WrongVaultExtensionDeployment(); + + /// @notice The `ProtocolFeeController` contract was configured with an incorrect Vault address. + error WrongProtocolFeeControllerDeployment(); + + /// @notice The `VaultAdmin` contract was configured with an incorrect Vault address. + error WrongVaultAdminDeployment(); + + /// @notice Quote reverted with a reserved error code. + error QuoteResultSpoofed(); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultEvents.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultEvents.sol new file mode 100644 index 00000000..3de66ab2 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultEvents.sol @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { IAuthorizer } from "./IAuthorizer.sol"; +import { IHooks } from "./IHooks.sol"; +import { IProtocolFeeController } from "./IProtocolFeeController.sol"; +import "./VaultTypes.sol"; + +/// @dev Events are declared inside an interface (namespace) to improve DX with Typechain. +interface IVaultEvents { + /** + * @notice A Pool was registered by calling `registerPool`. + * @param pool The pool being registered + * @param factory The factory creating the pool + * @param tokenConfig An array of descriptors for the tokens the pool will manage + * @param swapFeePercentage The static swap fee of the pool + * @param pauseWindowEndTime The pool's pause window end time + * @param roleAccounts Addresses the Vault will allow to change certain pool settings + * @param hooksConfig Flags indicating which hooks the pool supports and address of hooks contract + * @param liquidityManagement Supported liquidity management hook flags + */ + event PoolRegistered( + address indexed pool, + address indexed factory, + TokenConfig[] tokenConfig, + uint256 swapFeePercentage, + uint32 pauseWindowEndTime, + PoolRoleAccounts roleAccounts, + HooksConfig hooksConfig, + LiquidityManagement liquidityManagement + ); + + /** + * @notice A Pool was initialized by calling `initialize`. + * @param pool The pool being initialized + */ + event PoolInitialized(address indexed pool); + + /** + * @notice A swap has occurred. + * @param pool The pool with the tokens being swapped + * @param tokenIn The token entering the Vault (balance increases) + * @param tokenOut The token leaving the Vault (balance decreases) + * @param amountIn Number of tokenIn tokens + * @param amountOut Number of tokenOut tokens + * @param swapFeePercentage Swap fee percentage applied (can differ if dynamic) + * @param swapFeeAmount Swap fee amount paid + */ + event Swap( + address indexed pool, + IERC20 indexed tokenIn, + IERC20 indexed tokenOut, + uint256 amountIn, + uint256 amountOut, + uint256 swapFeePercentage, + uint256 swapFeeAmount + ); + + /** + * @notice A wrap operation has occurred. + * @param underlyingToken The underlying token address + * @param wrappedToken The wrapped token address + * @param depositedUnderlying Number of underlying tokens deposited + * @param mintedShares Number of shares (wrapped tokens) minted + */ + event Wrap( + IERC20 indexed underlyingToken, + IERC4626 indexed wrappedToken, + uint256 depositedUnderlying, + uint256 mintedShares + ); + + /** + * @notice An unwrap operation has occurred. + * @param wrappedToken The wrapped token address + * @param underlyingToken The underlying token address + * @param burnedShares Number of shares (wrapped tokens) burned + * @param withdrawnUnderlying Number of underlying tokens withdrawn + */ + event Unwrap( + IERC4626 indexed wrappedToken, + IERC20 indexed underlyingToken, + uint256 burnedShares, + uint256 withdrawnUnderlying + ); + + /** + * @notice Pool balances have changed (e.g., after initialization, add/remove liquidity). + * @param pool The pool being registered + * @param liquidityProvider The user performing the operation + * @param totalSupply The total supply of the pool after the operation + * @param deltas The amount each token changed, sorted in the pool tokens' order + */ + event PoolBalanceChanged( + address indexed pool, + address indexed liquidityProvider, + uint256 totalSupply, + int256[] deltas, + uint256[] swapFeeAmountsRaw + ); + + /** + * @notice The Vault's pause status has changed. + * @param paused True if the Vault was paused + */ + event VaultPausedStateChanged(bool paused); + + /// @notice `disableQuery` has been called on the Vault, permanently disabling query functionality. + event VaultQueriesDisabled(); + + /** + * @notice A Pool's pause status has changed. + * @param pool The pool that was just paused or unpaused + * @param paused True if the pool was paused + */ + event PoolPausedStateChanged(address indexed pool, bool paused); + + /** + * @notice Emitted when the swap fee percentage of a pool is updated. + * @param swapFeePercentage The new swap fee percentage for the pool + */ + event SwapFeePercentageChanged(address indexed pool, uint256 swapFeePercentage); + + /** + * @notice Recovery mode has been enabled or disabled for a pool. + * @param pool The pool + * @param recoveryMode True if recovery mode was enabled + */ + event PoolRecoveryModeStateChanged(address indexed pool, bool recoveryMode); + + /** + * @notice A protocol or pool creator fee has changed, causing an update to the aggregate swap fee. + * @dev The `ProtocolFeeController` will emit an event with the underlying change. + * @param pool The pool whose aggregate swap fee percentage changed + * @param aggregateSwapFeePercentage The new aggregate swap fee percentage + */ + event AggregateSwapFeePercentageChanged(address indexed pool, uint256 aggregateSwapFeePercentage); + + /** + * @notice A protocol or pool creator fee has changed, causing an update to the aggregate yield fee. + * @dev The `ProtocolFeeController` will emit an event with the underlying change. + * @param pool The pool whose aggregate yield fee percentage changed + * @param aggregateYieldFeePercentage The new aggregate yield fee percentage + */ + event AggregateYieldFeePercentageChanged(address indexed pool, uint256 aggregateYieldFeePercentage); + + /** + * @notice A new authorizer is set by `setAuthorizer`. + * @param newAuthorizer The address of the new authorizer + */ + event AuthorizerChanged(IAuthorizer indexed newAuthorizer); + + /** + * @notice A new protocol fee controller is set by `setProtocolFeeController`. + * @param newProtocolFeeController The address of the new protocol fee controller + */ + event ProtocolFeeControllerChanged(IProtocolFeeController indexed newProtocolFeeController); + + /** + * @notice Liquidity was added to an ERC4626 buffer corresponding to the given wrapped token. + * @dev The underlying token can be derived from the wrapped token, so it's not included here. + * + * @param wrappedToken The wrapped token that identifies the buffer + * @param amountUnderlying The amount of the underlying token that was deposited + * @param amountWrapped The amount of the wrapped token that was deposited + */ + event LiquidityAddedToBuffer(IERC4626 indexed wrappedToken, uint256 amountUnderlying, uint256 amountWrapped); + + /** + * @notice Buffer shares were minted for an ERC4626 buffer corresponding to a given wrapped token. + * @dev The shares are not tokenized like pool BPT, but accounted for in the Vault. `getBufferOwnerShares` + * retrieves the current total shares for a given buffer and address, and `getBufferTotalShares` returns the + * "totalSupply" of a buffer. + * + * @param wrappedToken The wrapped token that identifies the buffer + * @param to The owner of the minted shares + * @param issuedShares The amount of "internal BPT" shares created + */ + event BufferSharesMinted(IERC4626 indexed wrappedToken, address indexed to, uint256 issuedShares); + + /** + * @notice Buffer shares were burned for an ERC4626 buffer corresponding to a given wrapped token. + * @dev The shares are not tokenized like pool BPT, but accounted for in the Vault. `getBufferOwnerShares` + * retrieves the current total shares for a given buffer and address, and `getBufferTotalShares` returns the + * "totalSupply" of a buffer. + * + * @param wrappedToken The wrapped token that identifies the buffer + * @param from The owner of the burned shares + * @param burnedShares The amount of "internal BPT" shares burned + */ + event BufferSharesBurned(IERC4626 indexed wrappedToken, address indexed from, uint256 burnedShares); + + /** + * @notice Liquidity was removed from an ERC4626 buffer. + * @dev The underlying token can be derived from the wrapped token, so it's not included here. + * + * @param wrappedToken The wrapped token that identifies the buffer + * @param amountUnderlying The amount of the underlying token that was withdrawn + * @param amountWrapped The amount of the wrapped token that was withdrawn + */ + event LiquidityRemovedFromBuffer(IERC4626 indexed wrappedToken, uint256 amountUnderlying, uint256 amountWrapped); + + /** + * @notice The Vault buffers pause status has changed. + * @dev If buffers all paused, all buffer operations (i.e., all calls through the Router with `isBuffer` + * set to true) will revert. + * + * @param paused True if the Vault buffers were paused + */ + event VaultBuffersPausedStateChanged(bool paused); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultExtension.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultExtension.sol new file mode 100644 index 00000000..42f0d4c5 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultExtension.sol @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { IVault } from "./IVault.sol"; +import { IHooks } from "./IHooks.sol"; +import { IProtocolFeeController } from "./IProtocolFeeController.sol"; +import "./VaultTypes.sol"; + +/** + * @notice Interface for functions defined on the `VaultExtension` contract. + * @dev `VaultExtension` handles less critical or frequently used functions, since delegate calls through + * the Vault are more expensive than direct calls. The main Vault contains the core code for swaps and + * liquidity operations. + */ +interface IVaultExtension { + /******************************************************************************* + Constants and immutables + *******************************************************************************/ + + /** + * @notice Returns the main Vault address. + * @dev The main Vault contains the entrypoint and main liquidity operation implementations. + * @return vault The address of the main Vault + */ + function vault() external view returns (IVault); + + /** + * @notice Returns the VaultAdmin contract address. + * @dev The VaultAdmin contract mostly implements permissioned functions. + * @return vaultAdmin The address of the Vault admin + */ + function getVaultAdmin() external view returns (address); + + /******************************************************************************* + Transient Accounting + *******************************************************************************/ + + /** + * @notice Returns whether the Vault is unlocked (i.e., executing an operation). + * @dev The Vault must be unlocked to perform state-changing liquidity operations. + * @return unlocked True if the Vault is unlocked, false otherwise + */ + function isUnlocked() external view returns (bool); + + /** + * @notice Returns the count of non-zero deltas. + * @return nonzeroDeltaCount The current value of `_nonzeroDeltaCount` + */ + function getNonzeroDeltaCount() external view returns (uint256); + + /** + * @notice Retrieves the token delta for a specific token. + * @dev This function allows reading the value from the `_tokenDeltas` mapping. + * @param token The token for which the delta is being fetched + * @return tokenDelta The delta of the specified token + */ + function getTokenDelta(IERC20 token) external view returns (int256); + + /** + * @notice Retrieves the reserve (i.e., total Vault balance) of a given token. + * @param token The token for which to retrieve the reserve + * @return reserveAmount The amount of reserves for the given token + */ + function getReservesOf(IERC20 token) external view returns (uint256); + + /** + * @notice This flag is used to detect and tax "round trip" transactions (adding and removing liquidity in the + * same pool). + * @dev Taxing remove liquidity proportional whenever liquidity was added in the same transaction adds an extra + * layer of security, discouraging operations that try to undo others for profit. Remove liquidity proportional + * is the only standard way to exit a position without fees, and this flag is used to enable fees in that case. + * It also discourages indirect swaps via unbalanced add and remove proportional, as they are expected to be worse + * than a simple swap for every pool type. + * @param pool Address of the pool to check + * @return liquidityAdded True if liquidity has been added to this pool in the current transaction + */ + function getAddLiquidityCalledFlag(address pool) external view returns (bool); + + /******************************************************************************* + Pool Registration + *******************************************************************************/ + + /** + * @notice Registers a pool, associating it with its factory and the tokens it manages. + * @dev A pool can opt-out of pausing by providing a zero value for the pause window, or allow pausing indefinitely + * by providing a large value. (Pool pause windows are not limited by the Vault maximums.) The vault defines an + * additional buffer period during which a paused pool will stay paused. After the buffer period passes, a paused + * pool will automatically unpause. + * + * A pool can opt out of Balancer governance pausing by providing a custom `pauseManager`. This might be a + * multi-sig contract or an arbitrary smart contract with its own access controls, that forwards calls to + * the Vault. + * + * If the zero address is provided for the `pauseManager`, permissions for pausing the pool will default to the + * authorizer. + * + * @param pool The address of the pool being registered + * @param tokenConfig An array of descriptors for the tokens the pool will manage + * @param swapFeePercentage The initial static swap fee percentage of the pool + * @param pauseWindowEndTime The timestamp after which it is no longer possible to pause the pool + * @param protocolFeeExempt If true, the pool's initial aggregate fees will be set to 0 + * @param roleAccounts Addresses the Vault will allow to change certain pool settings + * @param poolHooksContract Contract that implements the hooks for the pool + * @param liquidityManagement Liquidity management flags with implemented methods + */ + function registerPool( + address pool, + TokenConfig[] memory tokenConfig, + uint256 swapFeePercentage, + uint32 pauseWindowEndTime, + bool protocolFeeExempt, + PoolRoleAccounts calldata roleAccounts, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external; + + /** + * @notice Checks whether a pool is registered. + * @param pool Address of the pool to check + * @return registered True if the pool is registered, false otherwise + */ + function isPoolRegistered(address pool) external view returns (bool); + + /** + * @notice Initializes a registered pool by adding liquidity; mints BPT tokens for the first time in exchange. + * @param pool Address of the pool to initialize + * @param to Address that will receive the output BPT + * @param tokens Tokens used to seed the pool (must match the registered tokens) + * @param exactAmountsIn Exact amounts of input tokens + * @param minBptAmountOut Minimum amount of output pool tokens + * @param userData Additional (optional) data required for adding initial liquidity + * @return bptAmountOut Output pool token amount + */ + function initialize( + address pool, + address to, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bytes memory userData + ) external returns (uint256 bptAmountOut); + + /******************************************************************************* + Pool Information + *******************************************************************************/ + + /** + * @notice Checks whether a pool is initialized. + * @dev An initialized pool can be considered registered as well. + * @param pool Address of the pool to check + * @return initialized True if the pool is initialized, false otherwise + */ + function isPoolInitialized(address pool) external view returns (bool); + + /** + * @notice Gets the tokens registered to a pool. + * @param pool Address of the pool + * @return tokens List of tokens in the pool + */ + function getPoolTokens(address pool) external view returns (IERC20[] memory); + + /** + * @notice Gets pool token rates. + * @dev This function performs external calls if tokens are yield-bearing. All returned arrays are in token + * registration order. + * + * @param pool Address of the pool + * @return decimalScalingFactors Conversion factor used to adjust for token decimals for uniform precision in + * calculations. FP(1) for 18-decimal tokens + * @return tokenRates 18-decimal FP values for rate tokens (e.g., yield-bearing), or FP(1) for standard tokens + */ + function getPoolTokenRates( + address pool + ) external view returns (uint256[] memory decimalScalingFactors, uint256[] memory tokenRates); + + /** + * @notice Returns comprehensive pool data for the given pool. + * @dev This contains the pool configuration (flags), tokens and token types, rates, scaling factors, and balances. + * @param pool The address of the pool + * @return poolData The `PoolData` result + */ + function getPoolData(address pool) external view returns (PoolData memory); + + /** + * @notice Gets the raw data for a pool: tokens, raw balances, scaling factors. + * @param pool Address of the pool + * @return tokens The pool tokens, sorted in registration order + * @return tokenInfo Token info structs (type, rate provider, yield flag), sorted in pool registration order + * @return balancesRaw Current native decimal balances of the pool tokens, sorted in pool registration order + * @return lastBalancesLiveScaled18 Last saved live balances, sorted in token registration order + */ + function getPoolTokenInfo( + address pool + ) + external + view + returns ( + IERC20[] memory tokens, + TokenInfo[] memory tokenInfo, + uint256[] memory balancesRaw, + uint256[] memory lastBalancesLiveScaled18 + ); + + /** + * @notice Gets current live balances of a given pool (fixed-point, 18 decimals), corresponding to its tokens in + * registration order. + * + * @param pool Address of the pool + * @return balancesLiveScaled18 Token balances after paying yield fees, applying decimal scaling and rates + */ + function getCurrentLiveBalances(address pool) external view returns (uint256[] memory balancesLiveScaled18); + + /** + * @notice Gets the configuration parameters of a pool. + * @dev The `PoolConfig` contains liquidity management and other state flags, fee percentages, the pause window. + * @param pool Address of the pool + * @return poolConfig The pool configuration as a `PoolConfig` struct + */ + function getPoolConfig(address pool) external view returns (PoolConfig memory); + + /** + * @notice Gets the hooks configuration parameters of a pool. + * @dev The `HooksConfig` contains flags indicating which pool hooks are implemented. + * @param pool Address of the pool + * @return hooksConfig The hooks configuration as a `HooksConfig` struct + */ + function getHooksConfig(address pool) external view returns (HooksConfig memory); + + /** + * @notice The current rate of a pool token (BPT) = invariant / totalSupply. + * @param pool Address of the pool + * @return rate BPT rate + */ + function getBptRate(address pool) external view returns (uint256 rate); + + /******************************************************************************* + Balancer Pool Tokens + *******************************************************************************/ + + /** + * @notice Gets the total supply of a given ERC20 token. + * @param token The token address + * @return totalSupply Total supply of the token + */ + function totalSupply(address token) external view returns (uint256); + + /** + * @notice Gets the balance of an account for a given ERC20 token. + * @param token Address of the token + * @param account Address of the account + * @return balance Balance of the account for the token + */ + function balanceOf(address token, address account) external view returns (uint256); + + /** + * @notice Gets the allowance of a spender for a given ERC20 token and owner. + * @param token Address of the token + * @param owner Address of the owner + * @param spender Address of the spender + * @return allowance Amount of tokens the spender is allowed to spend + */ + function allowance(address token, address owner, address spender) external view returns (uint256); + + /** + * @notice Approves a spender to spend pool tokens on behalf of sender. + * @dev Notice that the pool token address is not included in the params. This function is exclusively called by + * the pool contract, so msg.sender is used as the token address. + * + * @param owner Address of the owner + * @param spender Address of the spender + * @param amount Amount of tokens to approve + * @return success True if successful, false otherwise + */ + function approve(address owner, address spender, uint256 amount) external returns (bool); + + /** + * @notice Transfers pool token from owner to a recipient. + * @dev Notice that the pool token address is not included in the params. This function is exclusively called by + * the pool contract, so msg.sender is used as the token address. + * + * @param owner Address of the owner + * @param to Address of the recipient + * @param amount Amount of tokens to transfer + * @return success True if successful, false otherwise + */ + function transfer(address owner, address to, uint256 amount) external returns (bool); + + /** + * @notice Transfers pool token from a sender to a recipient using an allowance. + * @dev Notice that the pool token address is not included in the params. This function is exclusively called by + * the pool contract, so msg.sender is used as the token address. + * + * @param spender Address allowed to perform the transfer + * @param from Address of the sender + * @param to Address of the recipient + * @param amount Amount of tokens to transfer + * @return success True if successful, false otherwise + */ + function transferFrom(address spender, address from, address to, uint256 amount) external returns (bool); + + /******************************************************************************* + Pool Pausing + *******************************************************************************/ + + /** + * @notice Indicates whether a pool is paused. + * @dev If a pool is paused, all non-Recovery Mode state-changing operations will revert. + * @param pool The pool to be checked + * @return paused True if the pool is paused + */ + function isPoolPaused(address pool) external view returns (bool); + + /** + * @notice Returns the paused status, and end times of the Pool's pause window and buffer period. + * @dev Note that even when set to a paused state, the pool will automatically unpause at the end of + * the buffer period. + * + * @param pool The pool whose data is requested + * @return paused True if the Pool is paused + * @return poolPauseWindowEndTime The timestamp of the end of the Pool's pause window + * @return poolBufferPeriodEndTime The timestamp after which the Pool unpauses itself (if paused) + * @return pauseManager The pause manager, or the zero address + */ + function getPoolPausedState(address pool) external view returns (bool, uint32, uint32, address); + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /** + * @notice Checks if the wrapped token has an initialized buffer in the Vault. + * @dev An initialized buffer should have an asset registered in the Vault. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @return isBufferInitialized True if the ERC4626 buffer is initialized + */ + function isERC4626BufferInitialized(IERC4626 wrappedToken) external view returns (bool isBufferInitialized); + + /******************************************************************************* + Fees + *******************************************************************************/ + + /** + * @notice Returns the accumulated swap fees (including aggregate fees) in `token` collected by the pool. + * @param pool The address of the pool for which aggregate fees have been collected + * @param token The address of the token in which fees have been accumulated + * @return swapFeeAmount The total amount of fees accumulated in the specified token + */ + function getAggregateSwapFeeAmount(address pool, IERC20 token) external view returns (uint256); + + /** + * @notice Returns the accumulated yield fees (including aggregate fees) in `token` collected by the pool. + * @param pool The address of the pool for which aggregate fees have been collected + * @param token The address of the token in which fees have been accumulated + * @return yieldFeeAmount The total amount of fees accumulated in the specified token + */ + function getAggregateYieldFeeAmount(address pool, IERC20 token) external view returns (uint256); + + /** + * @notice Fetches the static swap fee percentage for a given pool. + * @param pool The address of the pool whose static swap fee percentage is being queried + * @return swapFeePercentage The current static swap fee percentage for the specified pool + */ + function getStaticSwapFeePercentage(address pool) external view returns (uint256); + + /** + * @notice Fetches the role accounts for a given pool (pause manager, swap manager, pool creator) + * @param pool The address of the pool whose roles are being queried + * @return roleAccounts A struct containing the role accounts for the pool (or 0 if unassigned) + */ + function getPoolRoleAccounts(address pool) external view returns (PoolRoleAccounts memory); + + /** + * @notice Query the current dynamic swap fee percentage of a pool, given a set of swap parameters. + * @dev Reverts if the hook doesn't return the success flag set to `true`. + * @param pool The pool + * @param swapParams The swap parameters used to compute the fee + * @return dynamicSwapFeePercentage The dynamic swap fee percentage + */ + function computeDynamicSwapFeePercentage( + address pool, + PoolSwapParams memory swapParams + ) external view returns (uint256); + + /** + * @notice Returns the Protocol Fee Controller address. + * @return protocolFeeController Address of the ProtocolFeeController + */ + function getProtocolFeeController() external view returns (IProtocolFeeController); + + /******************************************************************************* + Recovery Mode + *******************************************************************************/ + + /** + * @notice Checks whether a pool is in Recovery Mode. + * @dev Recovery Mode enables a safe proportional withdrawal path, with no external calls. + * @param pool Address of the pool to check + * @return recoveryMode True if the pool is in Recovery Mode, false otherwise + */ + function isPoolInRecoveryMode(address pool) external view returns (bool); + + /** + * @notice Remove liquidity from a pool specifying exact pool tokens in, with proportional token amounts out. + * The request is implemented by the Vault without any interaction with the pool, ensuring that + * it works the same for all pools, and cannot be disabled by a new pool type. + * + * @param pool Address of the pool + * @param from Address of user to burn pool tokens from + * @param exactBptAmountIn Input pool token amount + * @return amountsOut Actual calculated amounts of output tokens, sorted in token registration order + */ + function removeLiquidityRecovery( + address pool, + address from, + uint256 exactBptAmountIn + ) external returns (uint256[] memory amountsOut); + + /******************************************************************************* + Queries + *******************************************************************************/ + + /** + * @notice Performs a callback on msg.sender with arguments provided in `data`. + * @dev Used to query a set of operations on the Vault. Only off-chain eth_call are allowed, + * anything else will revert. + * + * Allows querying any operation on the Vault that has the `onlyWhenUnlocked` modifier. + * + * Allows the external calling of a function via the Vault contract to + * access Vault's functions guarded by `onlyWhenUnlocked`. + * `transient` modifier ensuring balances changes within the Vault are settled. + * + * @param data Contains function signature and args to be passed to the msg.sender + * @return result Resulting data from the call + */ + function quote(bytes calldata data) external returns (bytes memory result); + + /** + * @notice Performs a callback on msg.sender with arguments provided in `data`. + * @dev Used to query a set of operations on the Vault. Only off-chain eth_call are allowed, + * anything else will revert. + * + * Allows querying any operation on the Vault that has the `onlyWhenUnlocked` modifier. + * + * Allows the external calling of a function via the Vault contract to + * access Vault's functions guarded by `onlyWhenUnlocked`. + * `transient` modifier ensuring balances changes within the Vault are settled. + * + * This call always reverts, returning the result in the revert reason. + * + * @param data Contains function signature and args to be passed to the msg.sender + */ + function quoteAndRevert(bytes calldata data) external; + + /** + * @notice Checks if the queries enabled on the Vault. + * @dev This is a one-way switch. Once queries are disabled, they can never be re-enabled. + * The query functions rely on a specific EVM feature to detect static calls. Query operations are exempt from + * settlement constraints, so it's critical that no state changes can occur. We retain the ability to disable + * queries in the unlikely event that EVM changes violate its assumptions (perhaps on an L2). + * + * @return queryDisabled If true, then queries are disabled + */ + function isQueryDisabled() external view returns (bool); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultMain.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultMain.sol new file mode 100644 index 00000000..3a38b643 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultMain.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IAuthorizer } from "./IAuthorizer.sol"; +import "./VaultTypes.sol"; + +/** + * @notice Interface for functions defined on the main Vault contract. + * @dev These are generally "critical path" functions (swap, add/remove liquidity) that are in the main contract + * for technical or performance reasons. + */ +interface IVaultMain { + /******************************************************************************* + Transient Accounting + *******************************************************************************/ + + /** + * @notice Creates a context for a sequence of operations (i.e., "unlocks" the Vault). + * @dev Performs a callback on msg.sender with arguments provided in `data`. The Callback is `transient`, + * meaning all balances for the caller have to be settled at the end. + * + * @param data Contains function signature and args to be passed to the msg.sender + * @return result Resulting data from the call + */ + function unlock(bytes calldata data) external returns (bytes memory result); + + /** + * @notice Settles deltas for a token; must be successful for the current lock to be released. + * @dev Protects the caller against leftover dust in the Vault for the token being settled. The caller + * should know in advance how many tokens were paid to the Vault, so it can provide it as a hint to discard any + * excess in the Vault balance. + * + * If the given hint is equal to or higher than the difference in reserves, the difference in reserves is given as + * credit to the caller. If it's higher, the caller sent fewer tokens than expected, so settlement would fail. + * + * If the given hint is lower than the difference in reserves, the hint is given as credit to the caller. + * In this case, the excess would be absorbed by the Vault (and reflected correctly in the reserves), but would + * not affect settlement. + * + * The credit supplied by the Vault can be calculated as `min(reserveDifference, amountHint)`, where the reserve + * difference equals current balance of the token minus existing reserves of the token when the function is called. + * + * @param token Address of the token + * @param amountHint Amount paid as reported by the caller + * @return credit Credit received in return of the payment + */ + function settle(IERC20 token, uint256 amountHint) external returns (uint256 credit); + + /** + * @notice Sends tokens to a recipient. + * @dev There is no inverse operation for this function. Transfer funds to the Vault and call `settle` to cancel + * debts. + * + * @param token Address of the token + * @param to Recipient address + * @param amount Amount of tokens to send + */ + function sendTo(IERC20 token, address to, uint256 amount) external; + + /*************************************************************************** + Swaps + ***************************************************************************/ + + /** + * @notice Swaps tokens based on provided parameters. + * @dev All parameters are given in raw token decimal encoding. + * @param vaultSwapParams Parameters for the swap (see above for struct definition) + * @return amountCalculatedRaw Calculated swap amount + * @return amountInRaw Amount of input tokens for the swap + * @return amountOutRaw Amount of output tokens from the swap + */ + function swap( + VaultSwapParams memory vaultSwapParams + ) external returns (uint256 amountCalculatedRaw, uint256 amountInRaw, uint256 amountOutRaw); + + /*************************************************************************** + Add Liquidity + ***************************************************************************/ + + /** + * @notice Adds liquidity to a pool. + * @dev Caution should be exercised when adding liquidity because the Vault has the capability + * to transfer tokens from any user, given that it holds all allowances. + * + * @param params Parameters for the add liquidity (see above for struct definition) + * @return amountsIn Actual amounts of input tokens + * @return bptAmountOut Output pool token amount + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function addLiquidity( + AddLiquidityParams memory params + ) external returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData); + + /*************************************************************************** + Remove Liquidity + ***************************************************************************/ + + /** + * @notice Removes liquidity from a pool. + * @dev Trusted routers can burn pool tokens belonging to any user and require no prior approval from the user. + * Untrusted routers require prior approval from the user. This is the only function allowed to call + * _queryModeBalanceIncrease (and only in a query context). + * + * @param params Parameters for the remove liquidity (see above for struct definition) + * @return bptAmountIn Actual amount of BPT burned + * @return amountsOut Actual amounts of output tokens + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function removeLiquidity( + RemoveLiquidityParams memory params + ) external returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData); + + /******************************************************************************* + Pool Information + *******************************************************************************/ + + /** + * @notice Gets the index of a token in a given pool. + * @dev Reverts if the pool is not registered, or if the token does not belong to the pool. + * @param pool Address of the pool + * @param token Address of the token + * @return tokenCount Number of tokens in the pool + * @return index Index corresponding to the given token in the pool's token list + */ + function getPoolTokenCountAndIndexOfToken(address pool, IERC20 token) external view returns (uint256, uint256); + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /** + * @notice Wraps/unwraps tokens based on the parameters provided. + * @dev All parameters are given in raw token decimal encoding. It requires the buffer to be initialized, + * and uses the internal wrapped token buffer when it has enough liquidity to avoid external calls. + * + * @param params Parameters for the wrap/unwrap operation (see struct definition) + * @return amountCalculatedRaw Calculated swap amount + * @return amountInRaw Amount of input tokens for the swap + * @return amountOutRaw Amount of output tokens from the swap + */ + function erc4626BufferWrapOrUnwrap( + BufferWrapOrUnwrapParams memory params + ) external returns (uint256 amountCalculatedRaw, uint256 amountInRaw, uint256 amountOutRaw); + + /******************************************************************************* + Authentication + *******************************************************************************/ + + /** + * @notice Returns the Authorizer address. + * @dev The authorizer holds the permissions granted by governance. It is set on Vault deployment, + * and can be changed through a permissioned call. + * + * @return authorizer Address of the authorizer contract + */ + function getAuthorizer() external view returns (IAuthorizer); + + /******************************************************************************* + Miscellaneous + *******************************************************************************/ + + /** + * @notice Returns the VaultExtension contract address. + * @dev Function is in the main Vault contract. The VaultExtension handles less critical or frequently used + * functions, since delegate calls through the Vault are more expensive than direct calls. + * + * @return vaultExtension Address of the VaultExtension + */ + function getVaultExtension() external view returns (address); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/LogExpMath.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/LogExpMath.sol new file mode 100644 index 00000000..f0d535b6 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/LogExpMath.sol @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +// solhint-disable + +/** + * @dev Exponentiation and logarithm functions for 18 decimal fixed point numbers (both base and exponent/argument). + * + * Exponentiation and logarithm with arbitrary bases (x^y and log_x(y)) are implemented by conversion to natural + * exponentiation and logarithm (where the base is Euler's number). + * + * All math operations are unchecked in order to save gas. + * + * @author Fernando Martinelli - @fernandomartinelli + * @author Sergio Yuhjtman - @sergioyuhjtman + * @author Daniel Fernandez - @dmf7z + */ +library LogExpMath { + /// @notice This error is thrown when a base is not within an acceptable range. + error BaseOutOfBounds(); + + /// @notice This error is thrown when a exponent is not within an acceptable range. + error ExponentOutOfBounds(); + + /// @notice This error is thrown when the exponent * ln(base) is not within an acceptable range. + error ProductOutOfBounds(); + + /// @notice This error is thrown when an exponent used in the exp function is not within an acceptable range. + error InvalidExponent(); + + /// @notice This error is thrown when a variable or result is not within the acceptable bounds defined in the function. + error OutOfBounds(); + + // All fixed point multiplications and divisions are inlined. This means we need to divide by ONE when multiplying + // two numbers, and multiply by ONE when dividing them. + + // All arguments and return values are 18 decimal fixed point numbers. + int256 constant ONE_18 = 1e18; + + // Internally, intermediate values are computed with higher precision as 20 decimal fixed point numbers, and in the + // case of ln36, 36 decimals. + int256 constant ONE_20 = 1e20; + int256 constant ONE_36 = 1e36; + + // The domain of natural exponentiation is bound by the word size and number of decimals used. + // + // Because internally the result will be stored using 20 decimals, the largest possible result is + // (2^255 - 1) / 10^20, which makes the largest exponent ln((2^255 - 1) / 10^20) = 130.700829182905140221. + // The smallest possible result is 10^(-18), which makes largest negative argument + // ln(10^(-18)) = -41.446531673892822312. + // We use 130.0 and -41.0 to have some safety margin. + int256 constant MAX_NATURAL_EXPONENT = 130e18; + int256 constant MIN_NATURAL_EXPONENT = -41e18; + + // Bounds for ln_36's argument. Both ln(0.9) and ln(1.1) can be represented with 36 decimal places in a fixed point + // 256 bit integer. + int256 constant LN_36_LOWER_BOUND = ONE_18 - 1e17; + int256 constant LN_36_UPPER_BOUND = ONE_18 + 1e17; + + uint256 constant MILD_EXPONENT_BOUND = 2 ** 254 / uint256(ONE_20); + + // 18 decimal constants + int256 constant x0 = 128000000000000000000; // 2ˆ7 + int256 constant a0 = 38877084059945950922200000000000000000000000000000000000; // eˆ(x0) (no decimals) + int256 constant x1 = 64000000000000000000; // 2ˆ6 + int256 constant a1 = 6235149080811616882910000000; // eˆ(x1) (no decimals) + + // 20 decimal constants + int256 constant x2 = 3200000000000000000000; // 2ˆ5 + int256 constant a2 = 7896296018268069516100000000000000; // eˆ(x2) + int256 constant x3 = 1600000000000000000000; // 2ˆ4 + int256 constant a3 = 888611052050787263676000000; // eˆ(x3) + int256 constant x4 = 800000000000000000000; // 2ˆ3 + int256 constant a4 = 298095798704172827474000; // eˆ(x4) + int256 constant x5 = 400000000000000000000; // 2ˆ2 + int256 constant a5 = 5459815003314423907810; // eˆ(x5) + int256 constant x6 = 200000000000000000000; // 2ˆ1 + int256 constant a6 = 738905609893065022723; // eˆ(x6) + int256 constant x7 = 100000000000000000000; // 2ˆ0 + int256 constant a7 = 271828182845904523536; // eˆ(x7) + int256 constant x8 = 50000000000000000000; // 2ˆ-1 + int256 constant a8 = 164872127070012814685; // eˆ(x8) + int256 constant x9 = 25000000000000000000; // 2ˆ-2 + int256 constant a9 = 128402541668774148407; // eˆ(x9) + int256 constant x10 = 12500000000000000000; // 2ˆ-3 + int256 constant a10 = 113314845306682631683; // eˆ(x10) + int256 constant x11 = 6250000000000000000; // 2ˆ-4 + int256 constant a11 = 106449445891785942956; // eˆ(x11) + + /** + * @dev Exponentiation (x^y) with unsigned 18 decimal fixed point base and exponent. + * + * Reverts if ln(x) * y is smaller than `MIN_NATURAL_EXPONENT`, or larger than `MAX_NATURAL_EXPONENT`. + */ + function pow(uint256 x, uint256 y) internal pure returns (uint256) { + if (y == 0) { + // We solve the 0^0 indetermination by making it equal one. + return uint256(ONE_18); + } + + if (x == 0) { + return 0; + } + + // Instead of computing x^y directly, we instead rely on the properties of logarithms and exponentiation to + // arrive at that result. In particular, exp(ln(x)) = x, and ln(x^y) = y * ln(x). This means + // x^y = exp(y * ln(x)). + + // The ln function takes a signed value, so we need to make sure x fits in the signed 256 bit range. + if (x >> 255 != 0) { + revert BaseOutOfBounds(); + } + int256 x_int256 = int256(x); + + // We will compute y * ln(x) in a single step. Depending on the value of x, we can either use ln or ln_36. In + // both cases, we leave the division by ONE_18 (due to fixed point multiplication) to the end. + + // This prevents y * ln(x) from overflowing, and at the same time guarantees y fits in the signed 256 bit range. + if (y >= MILD_EXPONENT_BOUND) { + revert ExponentOutOfBounds(); + } + int256 y_int256 = int256(y); + + int256 logx_times_y; + unchecked { + if (LN_36_LOWER_BOUND < x_int256 && x_int256 < LN_36_UPPER_BOUND) { + int256 ln_36_x = _ln_36(x_int256); + + // ln_36_x has 36 decimal places, so multiplying by y_int256 isn't as straightforward, since we can't just + // bring y_int256 to 36 decimal places, as it might overflow. Instead, we perform two 18 decimal + // multiplications and add the results: one with the first 18 decimals of ln_36_x, and one with the + // (downscaled) last 18 decimals. + logx_times_y = ((ln_36_x / ONE_18) * y_int256 + ((ln_36_x % ONE_18) * y_int256) / ONE_18); + } else { + logx_times_y = _ln(x_int256) * y_int256; + } + logx_times_y /= ONE_18; + } + + // Finally, we compute exp(y * ln(x)) to arrive at x^y + if (!(MIN_NATURAL_EXPONENT <= logx_times_y && logx_times_y <= MAX_NATURAL_EXPONENT)) { + revert ProductOutOfBounds(); + } + + return uint256(exp(logx_times_y)); + } + + /** + * @dev Natural exponentiation (e^x) with signed 18 decimal fixed point exponent. + * + * Reverts if `x` is smaller than MIN_NATURAL_EXPONENT, or larger than `MAX_NATURAL_EXPONENT`. + */ + function exp(int256 x) internal pure returns (int256) { + if (!(x >= MIN_NATURAL_EXPONENT && x <= MAX_NATURAL_EXPONENT)) { + revert InvalidExponent(); + } + + // We avoid using recursion here because zkSync doesn't support it. + bool negativeExponent = false; + + if (x < 0) { + // We only handle positive exponents: e^(-x) is computed as 1 / e^x. We can safely make x positive since it + // fits in the signed 256 bit range (as it is larger than MIN_NATURAL_EXPONENT). In the negative + // exponent case, compute e^x, then return 1 / result. + unchecked { + x = -x; + } + negativeExponent = true; + } + + // First, we use the fact that e^(x+y) = e^x * e^y to decompose x into a sum of powers of two, which we call x_n, + // where x_n == 2^(7 - n), and e^x_n = a_n has been precomputed. We choose the first x_n, x0, to equal 2^7 + // because all larger powers are larger than MAX_NATURAL_EXPONENT, and therefore not present in the + // decomposition. + // At the end of this process we will have the product of all e^x_n = a_n that apply, and the remainder of this + // decomposition, which will be lower than the smallest x_n. + // exp(x) = k_0 * a_0 * k_1 * a_1 * ... + k_n * a_n * exp(remainder), where each k_n equals either 0 or 1. + // We mutate x by subtracting x_n, making it the remainder of the decomposition. + + // The first two a_n (e^(2^7) and e^(2^6)) are too large if stored as 18 decimal numbers, and could cause + // intermediate overflows. Instead we store them as plain integers, with 0 decimals. + // Additionally, x0 + x1 is larger than MAX_NATURAL_EXPONENT, which means they will not both be present in the + // decomposition. + + // For each x_n, we test if that term is present in the decomposition (if x is larger than it), and if so deduct + // it and compute the accumulated product. + + int256 firstAN; + unchecked { + if (x >= x0) { + x -= x0; + firstAN = a0; + } else if (x >= x1) { + x -= x1; + firstAN = a1; + } else { + firstAN = 1; // One with no decimal places + } + + // We now transform x into a 20 decimal fixed point number, to have enhanced precision when computing the + // smaller terms. + x *= 100; + } + + // `product` is the accumulated product of all a_n (except a0 and a1), which starts at 20 decimal fixed point + // one. Recall that fixed point multiplication requires dividing by ONE_20. + int256 product = ONE_20; + + unchecked { + if (x >= x2) { + x -= x2; + product = (product * a2) / ONE_20; + } + if (x >= x3) { + x -= x3; + product = (product * a3) / ONE_20; + } + if (x >= x4) { + x -= x4; + product = (product * a4) / ONE_20; + } + if (x >= x5) { + x -= x5; + product = (product * a5) / ONE_20; + } + if (x >= x6) { + x -= x6; + product = (product * a6) / ONE_20; + } + if (x >= x7) { + x -= x7; + product = (product * a7) / ONE_20; + } + if (x >= x8) { + x -= x8; + product = (product * a8) / ONE_20; + } + if (x >= x9) { + x -= x9; + product = (product * a9) / ONE_20; + } + } + + // x10 and x11 are unnecessary here since we have high enough precision already. + + // Now we need to compute e^x, where x is small (in particular, it is smaller than x9). We use the Taylor series + // expansion for e^x: 1 + x + (x^2 / 2!) + (x^3 / 3!) + ... + (x^n / n!). + + int256 seriesSum = ONE_20; // The initial one in the sum, with 20 decimal places. + int256 term; // Each term in the sum, where the nth term is (x^n / n!). + + // The first term is simply x. + term = x; + unchecked { + seriesSum += term; + + // Each term (x^n / n!) equals the previous one times x, divided by n. Since x is a fixed point number, + // multiplying by it requires dividing by ONE_20, but dividing by the non-fixed point n values does not. + + term = ((term * x) / ONE_20) / 2; + seriesSum += term; + + term = ((term * x) / ONE_20) / 3; + seriesSum += term; + + term = ((term * x) / ONE_20) / 4; + seriesSum += term; + + term = ((term * x) / ONE_20) / 5; + seriesSum += term; + + term = ((term * x) / ONE_20) / 6; + seriesSum += term; + + term = ((term * x) / ONE_20) / 7; + seriesSum += term; + + term = ((term * x) / ONE_20) / 8; + seriesSum += term; + + term = ((term * x) / ONE_20) / 9; + seriesSum += term; + + term = ((term * x) / ONE_20) / 10; + seriesSum += term; + + term = ((term * x) / ONE_20) / 11; + seriesSum += term; + + term = ((term * x) / ONE_20) / 12; + seriesSum += term; + + // 12 Taylor terms are sufficient for 18 decimal precision. + + // We now have the first a_n (with no decimals), and the product of all other a_n present, and the Taylor + // approximation of the exponentiation of the remainder (both with 20 decimals). All that remains is to multiply + // all three (one 20 decimal fixed point multiplication, dividing by ONE_20, and one integer multiplication), + // and then drop two digits to return an 18 decimal value. + + int256 result = (((product * seriesSum) / ONE_20) * firstAN) / 100; + + // We avoid using recursion here because zkSync doesn't support it. + return negativeExponent ? (ONE_18 * ONE_18) / result : result; + } + } + + /// @dev Logarithm (log(arg, base), with signed 18 decimal fixed point base and argument. + function log(int256 arg, int256 base) internal pure returns (int256) { + // This performs a simple base change: log(arg, base) = ln(arg) / ln(base). + + // Both logBase and logArg are computed as 36 decimal fixed point numbers, either by using ln_36, or by + // upscaling. + + int256 logBase; + unchecked { + if (LN_36_LOWER_BOUND < base && base < LN_36_UPPER_BOUND) { + logBase = _ln_36(base); + } else { + logBase = _ln(base) * ONE_18; + } + } + + int256 logArg; + unchecked { + if (LN_36_LOWER_BOUND < arg && arg < LN_36_UPPER_BOUND) { + logArg = _ln_36(arg); + } else { + logArg = _ln(arg) * ONE_18; + } + + // When dividing, we multiply by ONE_18 to arrive at a result with 18 decimal places + return (logArg * ONE_18) / logBase; + } + } + + /// @dev Natural logarithm (ln(a)) with signed 18 decimal fixed point argument. + function ln(int256 a) internal pure returns (int256) { + // The real natural logarithm is not defined for negative numbers or zero. + if (a <= 0) { + revert OutOfBounds(); + } + if (LN_36_LOWER_BOUND < a && a < LN_36_UPPER_BOUND) { + unchecked { + return _ln_36(a) / ONE_18; + } + } else { + return _ln(a); + } + } + + /// @dev Internal natural logarithm (ln(a)) with signed 18 decimal fixed point argument. + function _ln(int256 a) private pure returns (int256) { + // We avoid using recursion here because zkSync doesn't support it. + bool negativeExponent = false; + + if (a < ONE_18) { + // Since ln(a^k) = k * ln(a), we can compute ln(a) as ln(a) = ln((1/a)^(-1)) = - ln((1/a)). If a is less + // than one, 1/a will be greater than one, so in this case we compute ln(1/a) and negate the final result. + unchecked { + a = (ONE_18 * ONE_18) / a; + } + negativeExponent = true; + } + + // First, we use the fact that ln^(a * b) = ln(a) + ln(b) to decompose ln(a) into a sum of powers of two, which + // we call x_n, where x_n == 2^(7 - n), which are the natural logarithm of precomputed quantities a_n (that is, + // ln(a_n) = x_n). We choose the first x_n, x0, to equal 2^7 because the exponential of all larger powers cannot + // be represented as 18 fixed point decimal numbers in 256 bits, and are therefore larger than a. + // At the end of this process we will have the sum of all x_n = ln(a_n) that apply, and the remainder of this + // decomposition, which will be lower than the smallest a_n. + // ln(a) = k_0 * x_0 + k_1 * x_1 + ... + k_n * x_n + ln(remainder), where each k_n equals either 0 or 1. + // We mutate a by subtracting a_n, making it the remainder of the decomposition. + + // For reasons related to how `exp` works, the first two a_n (e^(2^7) and e^(2^6)) are not stored as fixed point + // numbers with 18 decimals, but instead as plain integers with 0 decimals, so we need to multiply them by + // ONE_18 to convert them to fixed point. + // For each a_n, we test if that term is present in the decomposition (if a is larger than it), and if so divide + // by it and compute the accumulated sum. + + int256 sum = 0; + unchecked { + if (a >= a0 * ONE_18) { + a /= a0; // Integer, not fixed point division + sum += x0; + } + + if (a >= a1 * ONE_18) { + a /= a1; // Integer, not fixed point division + sum += x1; + } + + // All other a_n and x_n are stored as 20 digit fixed point numbers, so we convert the sum and a to this format. + sum *= 100; + a *= 100; + + // Because further a_n are 20 digit fixed point numbers, we multiply by ONE_20 when dividing by them. + + if (a >= a2) { + a = (a * ONE_20) / a2; + sum += x2; + } + + if (a >= a3) { + a = (a * ONE_20) / a3; + sum += x3; + } + + if (a >= a4) { + a = (a * ONE_20) / a4; + sum += x4; + } + + if (a >= a5) { + a = (a * ONE_20) / a5; + sum += x5; + } + + if (a >= a6) { + a = (a * ONE_20) / a6; + sum += x6; + } + + if (a >= a7) { + a = (a * ONE_20) / a7; + sum += x7; + } + + if (a >= a8) { + a = (a * ONE_20) / a8; + sum += x8; + } + + if (a >= a9) { + a = (a * ONE_20) / a9; + sum += x9; + } + + if (a >= a10) { + a = (a * ONE_20) / a10; + sum += x10; + } + + if (a >= a11) { + a = (a * ONE_20) / a11; + sum += x11; + } + } + + // a is now a small number (smaller than a_11, which roughly equals 1.06). This means we can use a Taylor series + // that converges rapidly for values of `a` close to one - the same one used in ln_36. + // Let z = (a - 1) / (a + 1). + // ln(a) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 20 digit fixed point division requires multiplying by ONE_20, and multiplication requires + // division by ONE_20. + unchecked { + int256 z = ((a - ONE_20) * ONE_20) / (a + ONE_20); + int256 z_squared = (z * z) / ONE_20; + + // num is the numerator of the series: the z^(2 * n + 1) term + int256 num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + int256 seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / ONE_20; + seriesSum += num / 3; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 5; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 7; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 9; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 11; + + // 6 Taylor terms are sufficient for 36 decimal precision. + + // Finally, we multiply by 2 (non fixed point) to compute ln(remainder) + seriesSum *= 2; + + // We now have the sum of all x_n present, and the Taylor approximation of the logarithm of the remainder (both + // with 20 decimals). All that remains is to sum these two, and then drop two digits to return a 18 decimal + // value. + + int256 result = (sum + seriesSum) / 100; + + // We avoid using recursion here because zkSync doesn't support it. + return negativeExponent ? -result : result; + } + } + + /** + * @dev Internal high precision (36 decimal places) natural logarithm (ln(x)) with signed 18 decimal fixed point argument, + * for x close to one. + * + * Should only be used if x is between LN_36_LOWER_BOUND and LN_36_UPPER_BOUND. + */ + function _ln_36(int256 x) private pure returns (int256) { + // Since ln(1) = 0, a value of x close to one will yield a very small result, which makes using 36 digits + // worthwhile. + + // First, we transform x to a 36 digit fixed point value. + unchecked { + x *= ONE_18; + + // We will use the following Taylor expansion, which converges very rapidly. Let z = (x - 1) / (x + 1). + // ln(x) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 36 digit fixed point division requires multiplying by ONE_36, and multiplication requires + // division by ONE_36. + int256 z = ((x - ONE_36) * ONE_36) / (x + ONE_36); + int256 z_squared = (z * z) / ONE_36; + + // num is the numerator of the series: the z^(2 * n + 1) term + int256 num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + int256 seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / ONE_36; + seriesSum += num / 3; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 5; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 7; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 9; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 11; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 13; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 15; + + // 8 Taylor terms are sufficient for 36 decimal precision. + + // All that remains is multiplying by 2 (non fixed point). + return seriesSum * 2; + } + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultGuard.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultGuard.sol new file mode 100644 index 00000000..d3bc2929 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultGuard.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IVault } from "./IVault.sol"; +import { IVaultErrors } from "./IVaultErrors.sol"; + +/// @notice Contract that shares the modifier `onlyVault`. +contract VaultGuard { + IVault internal immutable _vault; + + constructor(IVault vault) { + _vault = vault; + } + + modifier onlyVault() { + _ensureOnlyVault(); + _; + } + + function _ensureOnlyVault() private view { + if (msg.sender != address(_vault)) { + revert IVaultErrors.SenderIsNotVault(msg.sender); + } + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultTypes.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultTypes.sol new file mode 100644 index 00000000..5b568f79 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultTypes.sol @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { IRateProvider } from "./IRateProvider.sol"; + +/** + * @notice Represents a pool's liquidity management configuration. + * @param disableUnbalancedLiquidity If set, liquidity can only be added or removed proportionally + * @param enableAddLiquidityCustom If set, the pool has implemented `onAddLiquidityCustom` + * @param enableRemoveLiquidityCustom If set, the pool has implemented `onRemoveLiquidityCustom` + * @param enableDonation If set, the pool will not revert if liquidity is added with AddLiquidityKind.DONATION + */ +struct LiquidityManagement { + bool disableUnbalancedLiquidity; + bool enableAddLiquidityCustom; + bool enableRemoveLiquidityCustom; + bool enableDonation; +} + +// @notice Custom type to store the entire configuration of the pool. +type PoolConfigBits is bytes32; + +/// @notice Represents a pool's configuration (hooks configuration are separated in another struct). +struct PoolConfig { + LiquidityManagement liquidityManagement; + uint256 staticSwapFeePercentage; + uint256 aggregateSwapFeePercentage; + uint256 aggregateYieldFeePercentage; + uint40 tokenDecimalDiffs; + uint32 pauseWindowEndTime; + bool isPoolRegistered; + bool isPoolInitialized; + bool isPoolPaused; + bool isPoolInRecoveryMode; +} + +/** + * @notice The flag portion of the `HooksConfig`. + * @dev `enableHookAdjustedAmounts` must be true for all contracts that modify the `amountCalculated` + * in after hooks. Otherwise, the Vault will ignore any "hookAdjusted" amounts. Setting any "shouldCall" + * flags to true will cause the Vault to call the corresponding hook during operations. + */ +struct HookFlags { + bool enableHookAdjustedAmounts; + bool shouldCallBeforeInitialize; + bool shouldCallAfterInitialize; + bool shouldCallComputeDynamicSwapFee; + bool shouldCallBeforeSwap; + bool shouldCallAfterSwap; + bool shouldCallBeforeAddLiquidity; + bool shouldCallAfterAddLiquidity; + bool shouldCallBeforeRemoveLiquidity; + bool shouldCallAfterRemoveLiquidity; +} + +/// @notice Represents a hook contract configuration for a pool (HookFlags + hooksContract address). +struct HooksConfig { + bool enableHookAdjustedAmounts; + bool shouldCallBeforeInitialize; + bool shouldCallAfterInitialize; + bool shouldCallComputeDynamicSwapFee; + bool shouldCallBeforeSwap; + bool shouldCallAfterSwap; + bool shouldCallBeforeAddLiquidity; + bool shouldCallAfterAddLiquidity; + bool shouldCallBeforeRemoveLiquidity; + bool shouldCallAfterRemoveLiquidity; + address hooksContract; +} + +/// @notice Represents temporary state used in a swap operation. +struct SwapState { + uint256 indexIn; + uint256 indexOut; + uint256 amountGivenScaled18; + uint256 swapFeePercentage; +} + +/** + * @notice Represents the Vault's configuration. + * @param isQueryDisabled If set to true, disables query functionality of the Vault. Can be modified only by + * governance. + * @param isVaultPaused If set to true, Swaps and Add/Remove Liquidity operations are halted + * @param areBuffersPaused If set to true, the Vault wrap/unwrap primitives associated with buffers will be disabled + */ +struct VaultState { + bool isQueryDisabled; + bool isVaultPaused; + bool areBuffersPaused; +} + +/** + * @notice Represents the accounts holding certain roles for a given pool. This is passed in on pool registration. + * @param pauseManager Account empowered to pause/unpause the pool (or 0 to delegate to governance) + * @param swapFeeManager Account empowered to set static swap fees for a pool (or 0 to delegate to governance) + * @param poolCreator Account empowered to set the pool creator fee percentage + */ +struct PoolRoleAccounts { + address pauseManager; + address swapFeeManager; + address poolCreator; +} + +/******************************************************************************* + Tokens +*******************************************************************************/ + +// Note that the following tokens are unsupported by the Vault. This list is not meant to be exhaustive, but covers +// many common types of tokens that will not work with the Vault architecture. (See https://github.com/d-xo/weird-erc20 +// for examples of token features that are problematic for many protocols.) +// +// * Rebasing tokens (e.g., aDAI). The Vault keeps track of token balances in its internal accounting; any token whose +// balance changes asynchronously (i.e., outside a swap or liquidity operation), would get out-of-sync with this +// internal accounting. This category would also include "airdrop" tokens, whose balances can change unexpectedly. +// +// * Double entrypoint (e.g., old Synthetix tokens, now fixed). These could likewise bypass internal accounting by +// registering the token under one address, then accessing it through another. This is especially troublesome +// in v3, with the introduction of ERC4626 buffers. +// +// * Fee on transfer (e.g., PAXG). The Vault issues credits and debits according to given and calculated token amounts, +// and settlement assumes that the send/receive transfer functions transfer exactly the given number of tokens. +// If this is not the case, transactions will not settle. Unlike with the other types, which are fundamentally +// incompatible, it would be possible to design a Router to handle this - but we didn't try it. In any case, it's +// not supported in the current Routers. +// +// * Tokens with more than 18 decimals (e.g., YAM-V2). The Vault handles token scaling: i.e., handling I/O for +// amounts in native token decimals, but doing calculations with full 18-decimal precision. This requires reading +// and storing the decimals for each token. Since virtually all tokens are 18 or fewer decimals, and we have limited +// storage space, 18 was a reasonable maximum. Unlike the other types, this is enforceable by the Vault. Attempting +// to register such tokens will revert with `InvalidTokenDecimals`. Of course, we must also be able to read the token +// decimals, so the Vault only supports tokens that implement `IERC20Metadata.decimals`, and return a value less than +// or equal to 18. +// +// These types of tokens are supported but discouraged, as they don't tend to play well with AMMs generally. +// +// * Very low-decimal tokens (e.g., GUSD). The Vault has been extensively tested with 6-decimal tokens (e.g., USDC), +// but going much below that may lead to unanticipated effects due to precision loss, especially with smaller trade +// values. +// +// * Revert on zero value approval/transfer. The Vault has been tested against these, but peripheral contracts, such +// as hooks, might not have been designed with this in mind. +// +// * Other types from "weird-erc20," such as upgradeable, pausable, or tokens with blocklists. We have seen cases +// where a token upgrade fails, "bricking" the token - and many operations on pools containing that token. Any +// sort of "permissioned" token that can make transfers fail can cause operations on pools containing them to +// revert. Even Recovery Mode cannot help then, as it does a proportional withdrawal of all tokens. If one of +// them is bricked, the whole operation will revert. Since v3 does not have "internal balances" like v2, there +// is no recourse. +// +// Of course, many tokens in common use have some of these "features" (especially centralized stable coins), so +// we have to support them anyway. Working with common centralized tokens is a risk common to all of DeFi. + +/** + * @notice Token types supported by the Vault. + * @dev In general, pools may contain any combination of these tokens. + * + * STANDARD tokens (e.g., BAL, WETH) have no rate provider. + * WITH_RATE tokens (e.g., wstETH) require a rate provider. These may be tokens like wstETH, which need to be wrapped + * because the underlying stETH token is rebasing, and such tokens are unsupported by the Vault. They may also be + * tokens like sEUR, which track an underlying asset, but are not yield-bearing. Finally, this encompasses + * yield-bearing ERC4626 tokens, which can be used to facilitate swaps without requiring wrapping or unwrapping + * in most cases. The `paysYieldFees` flag can be used to indicate whether a token is yield-bearing (e.g., waDAI), + * not yield-bearing (e.g., sEUR), or yield-bearing but exempt from fees (e.g., in certain nested pools, where + * yield fees are charged elsewhere). + * + * NB: STANDARD must always be the first enum element, so that newly initialized data structures default to Standard. + */ +enum TokenType { + STANDARD, + WITH_RATE +} + +/** + * @notice Encapsulate the data required for the Vault to support a token of the given type. + * @dev For STANDARD tokens, the rate provider address must be 0, and paysYieldFees must be false. All WITH_RATE tokens + * need a rate provider, and may or may not be yield-bearing. + * + * At registration time, it is useful to include the token address along with the token parameters in the structure + * passed to `registerPool`, as the alternative would be parallel arrays, which would be error prone and require + * validation checks. `TokenConfig` is only used for registration, and is never put into storage (see `TokenInfo`). + * + * @param token The token address + * @param tokenType The token type (see the enum for supported types) + * @param rateProvider The rate provider for a token (see further documentation above) + * @param paysYieldFees Flag indicating whether yield fees should be charged on this token + */ +struct TokenConfig { + IERC20 token; + TokenType tokenType; + IRateProvider rateProvider; + bool paysYieldFees; +} + +/** + * @notice This data structure is stored in `_poolTokenInfo`, a nested mapping from pool -> (token -> TokenInfo). + * @dev Since the token is already the key of the nested mapping, it would be redundant (and an extra SLOAD) to store + * it again in the struct. When we construct PoolData, the tokens are separated into their own array. + * + * @param tokenType The token type (see the enum for supported types) + * @param rateProvider The rate provider for a token (see further documentation above) + * @param paysYieldFees Flag indicating whether yield fees should be charged on this token + */ +struct TokenInfo { + TokenType tokenType; + IRateProvider rateProvider; + bool paysYieldFees; +} + +/** + * @notice Data structure used to represent the current pool state in memory + * @param poolConfigBits Custom type to store the entire configuration of the pool. + * @param tokens Pool tokens, sorted in pool registration order + * @param tokenInfo Configuration data for each token, sorted in pool registration order + * @param balancesRaw Token balances in native decimals + * @param balancesLiveScaled18 Token balances after paying yield fees, applying decimal scaling and rates + * @param tokenRates 18-decimal FP values for rate tokens (e.g., yield-bearing), or FP(1) for standard tokens + * @param decimalScalingFactors Conversion factor used to adjust for token decimals for uniform precision in + * calculations. FP(1) for 18-decimal tokens + */ +struct PoolData { + PoolConfigBits poolConfigBits; + IERC20[] tokens; + TokenInfo[] tokenInfo; + uint256[] balancesRaw; + uint256[] balancesLiveScaled18; + uint256[] tokenRates; + uint256[] decimalScalingFactors; +} + +enum Rounding { + ROUND_UP, + ROUND_DOWN +} + +/******************************************************************************* + Swaps +*******************************************************************************/ + +enum SwapKind { + EXACT_IN, + EXACT_OUT +} + +// There are two "SwapParams" structs defined below. `VaultSwapParams` corresponds to the external swap API defined +// in the Router contracts, which uses explicit token addresses, the amount given and limit on the calculated amount +// expressed in native token decimals, and optional user data passed in from the caller. +// +// `PoolSwapParams` passes some of this information through (kind, userData), but "translates" the parameters to fit +// the internal swap API used by `IBasePool`. It scales amounts to full 18-decimal precision, adds the token balances, +// converts the raw token addresses to indices, and adds the address of the Router originating the request. It does +// not need the limit, since this is checked at the Router level. + +/** + * @notice Data passed into primary Vault `swap` operations. + * @param kind Type of swap (Exact In or Exact Out) + * @param pool The pool with the tokens being swapped + * @param tokenIn The token entering the Vault (balance increases) + * @param tokenOut The token leaving the Vault (balance decreases) + * @param amountGivenRaw Amount specified for tokenIn or tokenOut (depending on the type of swap) + * @param limitRaw Minimum or maximum value of the calculated amount (depending on the type of swap) + * @param userData Additional (optional) user data + */ +struct VaultSwapParams { + SwapKind kind; + address pool; + IERC20 tokenIn; + IERC20 tokenOut; + uint256 amountGivenRaw; + uint256 limitRaw; + bytes userData; +} + +/** + * @notice Data for a swap operation, used by contracts implementing `IBasePool`. + * @param kind Type of swap (exact in or exact out) + * @param amountGivenScaled18 Amount given based on kind of the swap (e.g., tokenIn for EXACT_IN) + * @param balancesScaled18 Current pool balances + * @param indexIn Index of tokenIn + * @param indexOut Index of tokenOut + * @param router The address (usually a router contract) that initiated a swap operation on the Vault + * @param userData Additional (optional) data required for the swap + */ +struct PoolSwapParams { + SwapKind kind; + uint256 amountGivenScaled18; + uint256[] balancesScaled18; + uint256 indexIn; + uint256 indexOut; + address router; + bytes userData; +} + +/** + * @notice Data for the hook after a swap operation. + * @param kind Type of swap (exact in or exact out) + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param amountInScaled18 Amount of tokenIn (entering the Vault) + * @param amountOutScaled18 Amount of tokenOut (leaving the Vault) + * @param tokenInBalanceScaled18 Updated (after swap) balance of tokenIn + * @param tokenOutBalanceScaled18 Updated (after swap) balance of tokenOut + * @param amountCalculatedScaled18 Token amount calculated by the swap + * @param amountCalculatedRaw Token amount calculated by the swap + * @param user Account originating the swap operation + * @param router The address (usually a router contract) that initiated a swap operation on the Vault + * @param pool Pool address + * @param userData Additional (optional) data required for the swap + */ +struct AfterSwapParams { + SwapKind kind; + IERC20 tokenIn; + IERC20 tokenOut; + uint256 amountInScaled18; + uint256 amountOutScaled18; + uint256 tokenInBalanceScaled18; + uint256 tokenOutBalanceScaled18; + uint256 amountCalculatedScaled18; + uint256 amountCalculatedRaw; + address router; + address pool; + bytes userData; +} + +/******************************************************************************* + Add liquidity +*******************************************************************************/ + +enum AddLiquidityKind { + PROPORTIONAL, + UNBALANCED, + SINGLE_TOKEN_EXACT_OUT, + DONATION, + CUSTOM +} + +/** + * @notice Data for an add liquidity operation. + * @param pool Address of the pool + * @param to Address of user to mint to + * @param maxAmountsIn Maximum amounts of input tokens + * @param minBptAmountOut Minimum amount of output pool tokens + * @param kind Add liquidity kind + * @param userData Optional user data + */ +struct AddLiquidityParams { + address pool; + address to; + uint256[] maxAmountsIn; + uint256 minBptAmountOut; + AddLiquidityKind kind; + bytes userData; +} + +/******************************************************************************* + Remove liquidity +*******************************************************************************/ + +enum RemoveLiquidityKind { + PROPORTIONAL, + SINGLE_TOKEN_EXACT_IN, + SINGLE_TOKEN_EXACT_OUT, + CUSTOM +} + +/** + * @notice Data for an remove liquidity operation. + * @param pool Address of the pool + * @param from Address of user to burn from + * @param maxBptAmountIn Maximum amount of input pool tokens + * @param minAmountsOut Minimum amounts of output tokens + * @param kind Remove liquidity kind + * @param userData Optional user data + */ +struct RemoveLiquidityParams { + address pool; + address from; + uint256 maxBptAmountIn; + uint256[] minAmountsOut; + RemoveLiquidityKind kind; + bytes userData; +} + +/******************************************************************************* + Remove liquidity +*******************************************************************************/ + +enum WrappingDirection { + WRAP, + UNWRAP +} + +/** + * @notice Data for a wrap/unwrap operation. + * @param kind Type of swap (Exact In or Exact Out) + * @param direction Direction of the wrapping operation (Wrap or Unwrap) + * @param wrappedToken Wrapped token, compatible with interface ERC4626 + * @param amountGivenRaw Amount specified for tokenIn or tokenOut (depends on the type of swap and wrapping direction) + * @param limitRaw Minimum or maximum amount specified for the other token (depends on the type of swap and wrapping + * direction) + */ +struct BufferWrapOrUnwrapParams { + SwapKind kind; + WrappingDirection direction; + IERC4626 wrappedToken; + uint256 amountGivenRaw; + uint256 limitRaw; +} + +// Protocol Fees are 24-bit values. We transform them by multiplying by 1e11, so that they can be set to any value +// between 0% and 100% (step 0.00001%). Protocol and pool creator fees are set in the `ProtocolFeeController`, and +// ensure both constituent and aggregate fees do not exceed this precision. +uint256 constant FEE_BITLENGTH = 24; +uint256 constant MAX_FEE_PERCENTAGE = 1e18; // 100% +uint256 constant FEE_SCALING_FACTOR = 1e11; \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/secretLottery.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/secretLottery.sol new file mode 100644 index 00000000..e2c3c072 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/secretLottery.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { IHooks } from "./interfaces/IHooks.sol"; +import { IRouterCommon } from "./interfaces/IRouterCommon.sol"; +import { IVault } from "./interfaces/IVault.sol"; +import { AfterSwapParams, LiquidityManagement, SwapKind, TokenConfig, HookFlags } from "./interfaces/VaultTypes.sol"; + +import { EnumerableMap } from "./interfaces/EnumerableMap.sol"; +import { FixedPoint } from "./interfaces/FixedPoint.sol"; +import { VaultGuard } from "./interfaces/VaultGuard.sol"; +import { BaseHooks } from "./interfaces/BaseHooks.sol"; + +import "fhevm/lib/TFHE.sol"; +import "fhevm/gateway/GatewayCaller.sol"; + +/** + * @notice Hook that randomly rewards accumulated fees to a user performing a swap. + * @dev In this example, every time a swap is executed in a pool registered with this hook, a "random" number is drawn. + * If the drawn number is not equal to the LUCKY_NUMBER, the user will pay fees to the hook contract. But, if the + * drawn number is equal to LUCKY_NUMBER, the user won't pay hook fees and will receive all fees accrued by the hook. + */ +contract LotteryHookExample is BaseHooks, VaultGuard, Ownable, GatewayCaller { + using FixedPoint for uint256; + using EnumerableMap for EnumerableMap.IERC20ToUint256Map; + using SafeERC20 for IERC20; + + // Trusted router is needed since we rely on `getSender` to know which user should receive the prize. + address private immutable _trustedRouter; + address private immutable _allowedFactory; + + mapping(address => mapping(IERC20 => euint64)) private userAddressToThereCreditValue; + uint256 constant DIVISION_FACTOR = 10 ** 12; + uint256 private _counter = 0; + + /** + * @notice A new `LotteryHookExample` contract has been registered successfully for a given factory and pool. + * @dev If the registration fails the call will revert, so there will be no event. + * @param hooksContract This contract + * @param pool The pool on which the hook was registered + */ + event SecretSwapHookRegistered(address indexed hooksContract, address indexed pool); + + struct CallBackStruct { + address userAddress; + IERC20 token1; + IERC20 token2; + } + mapping(uint256 id => CallBackStruct callBackStruct) public requestIdToCallBackStruct; + + /** + * @notice Fee collected and added to the lottery pot. + * @dev The current user did not win the lottery. + * @param hooksContract This contract + * @param token The token in which the fee was collected + * @param amount The amount of the fee collected + */ + event TokenDeposited(address indexed hooksContract, IERC20 indexed token, uint256 amount); + + constructor(IVault vault, address router) VaultGuard(vault) Ownable(msg.sender) { + _trustedRouter = router; + } + + /// @inheritdoc IHooks + function onRegister( + address, + address pool, + TokenConfig[] memory, + LiquidityManagement calldata + ) public override onlyVault returns (bool) { + emit SecretSwapHookRegistered(address(this), pool); + + return true; + } + + /// @inheritdoc IHooks + function getHookFlags() public pure override returns (HookFlags memory) { + HookFlags memory hookFlags; + hookFlags.enableHookAdjustedAmounts = true; + hookFlags.shouldCallAfterSwap = true; + return hookFlags; + } + + /// @inheritdoc IHooks + function onAfterSwap( + AfterSwapParams calldata params + ) public override onlyVault returns (bool success, uint256 hookAdjustedAmountCalculatedRaw) { + uint256 operation = abi.decode(params.userData, (uint256)); + + // Do Deposit + if (params.router == _trustedRouter && operation == 1) { + hookAdjustedAmountCalculatedRaw = params.amountCalculatedRaw; + uint256 amount = params.amountCalculatedRaw.mulDown(1); + if (params.kind == SwapKind.EXACT_IN) { + uint256 feeToPay = _depositToken(params.router, params.tokenOut, amount); + if (feeToPay > 0) { + hookAdjustedAmountCalculatedRaw -= feeToPay; + } + } else { + uint256 feeToPay = _depositToken(params.router, params.tokenIn, amount); + if (feeToPay > 0) { + hookAdjustedAmountCalculatedRaw += feeToPay; + } + } + return (true, hookAdjustedAmountCalculatedRaw); + } + // Do Withdraw + else if (params.router == _trustedRouter && operation == 2) { + _withdrawToken(params.router, params.tokenIn, params.tokenOut); + } else { + return (true, hookAdjustedAmountCalculatedRaw); + } + } + + // If drawnNumber == LUCKY_NUMBER, user wins the pot and pays no fees. Otherwise, the hook fee adds to the pot. + function _depositToken(address router, IERC20 token, uint256 amount) private returns (uint256) { + address user = IRouterCommon(router).getSender(); + if (amount > 0) { + _vault.sendTo(token, address(this), amount); + userAddressToThereCreditValue[user][token] = TFHE.add( + userAddressToThereCreditValue[user][token], + TFHE.asEuint64(amount / DIVISION_FACTOR) + ); + TFHE.allow(userAddressToThereCreditValue[user][token], address(this)); + TFHE.allow(userAddressToThereCreditValue[user][token], owner()); + emit TokenDeposited(address(this), token, amount); + } + return amount; + } + + function _withdrawToken(address router, IERC20 token1, IERC20 token2) private returns (uint256) { + address user = IRouterCommon(router).getSender(); + + euint64 token1Amount = TFHE.asEuint64(0); + if (TFHE.isInitialized(userAddressToThereCreditValue[user][token1])) { + token1Amount = userAddressToThereCreditValue[user][token1]; + } + + euint64 token2Amount = TFHE.asEuint64(0); + if (TFHE.isInitialized(userAddressToThereCreditValue[user][token2])) { + token2Amount = userAddressToThereCreditValue[user][token2]; + } + + TFHE.allow(token1Amount, address(this)); + TFHE.allow(token2Amount, address(this)); + + uint256[] memory cts = new uint256[](2); + cts[0] = Gateway.toUint256(token1Amount); + cts[1] = Gateway.toUint256(token2Amount); + + uint256 requestId = Gateway.requestDecryption( + cts, + this.callBackResolver.selector, + 0, + block.timestamp + 100, + false + ); + + requestIdToCallBackStruct[requestId] = CallBackStruct(user, token1, token2); + + return 0; + } + + function callBackResolver( + uint256 requestID, + uint64 _token1Amount, + uint64 _token2Amount + ) external onlyGateway returns (bool) { + CallBackStruct memory _callBackStruct = requestIdToCallBackStruct[requestID]; + if (_token1Amount > 0) { + _callBackStruct.token1.safeTransfer(_callBackStruct.userAddress, _token1Amount * DIVISION_FACTOR); + } + if (_token2Amount > 0) { + _callBackStruct.token2.safeTransfer(_callBackStruct.userAddress, _token2Amount * DIVISION_FACTOR); + } + + userAddressToThereCreditValue[_callBackStruct.userAddress][_callBackStruct.token1] = TFHE.asEuint64(0); + userAddressToThereCreditValue[_callBackStruct.userAddress][_callBackStruct.token2] = TFHE.asEuint64(0); + + return true; + } + + function transferCredits(address to, IERC20 token, einput encryptedAmount, bytes calldata inputProof) public { + euint64 amount = TFHE.asEuint64(encryptedAmount, inputProof); + ebool canTransfer = TFHE.le(amount, userAddressToThereCreditValue[msg.sender][token]); + _transfer(msg.sender, to, amount, canTransfer, token); + } + + // Transfers an encrypted amount. + function _transfer(address from, address to, euint64 amount, ebool isTransferable, IERC20 token) internal virtual { + // Add to the balance of `to` and subract from the balance of `from`. + euint64 transferValue = TFHE.select(isTransferable, amount, TFHE.asEuint64(0)); + euint64 newBalanceTo = TFHE.add(userAddressToThereCreditValue[to][token], transferValue); + userAddressToThereCreditValue[to][token] = newBalanceTo; + TFHE.allow(newBalanceTo, address(this)); + TFHE.allow(newBalanceTo, to); + euint64 newBalanceFrom = TFHE.sub(userAddressToThereCreditValue[from][token], transferValue); + userAddressToThereCreditValue[from][token] = newBalanceFrom; + TFHE.allow(newBalanceFrom, address(this)); + TFHE.allow(newBalanceFrom, from); + emit Transfer(from, to); + } + + event Transfer(address, address); +} diff --git a/packages/foundry/contracts/hooks/Hookathon/default.toml b/packages/foundry/contracts/hooks/Hookathon/default.toml new file mode 100644 index 00000000..803cce81 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/default.toml @@ -0,0 +1,56 @@ +tick_interval_secs = 1 +storage_path = "./temp/events.toml" + +[oracle] +addresses = ["http://fhevm-validator:26657"] + +[blockchain] +addresses = ["http://kms-validator:9090"] +contract = "wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d" + +[blockchain.signkey] +# It should be either mnemonic or bip32 +mnemonic = "bachelor similar spirit copper rely carbon web hobby conduct wrap conduct wire shine parrot erosion divert crucial balance lock reason price ignore educate open" +bip32 = "xprv9s21ZrQH143K3FQVQn1Z6" + +[blockchain.fee] +denom = "ucosm" +amount = 3000000 + +[core] +addresses = ["http://localhost:50051"] + +[core.timeout_config] +channel_timeout = 60 + +[core.timeout_config.crs] +initial_wait_time = 60 +retry_interval = 60 +max_poll_count = 120 + +[core.timeout_config.keygen] +initial_wait_time = 18000 +retry_interval = 15000 +max_poll_count = 1150 + +[core.timeout_config.preproc] +initial_wait_time = 18000 +retry_interval = 15000 +max_poll_count = 1150 + +[core.timeout_config.decryption] +initial_wait_time = 1 +retry_interval = 1 +max_poll_count = 24 + +[core.timeout_config.reencryption] +initial_wait_time = 1 +retry_interval = 1 +max_poll_count = 24 + +[tracing] +service_name = "kms-asc-connector" +endpoint = "http://localhost:4317" + +[store] +url = "http://store:8088" \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/deploy/deploy.ts b/packages/foundry/contracts/hooks/Hookathon/deploy/deploy.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/foundry/contracts/hooks/Hookathon/deploy/instance.ts b/packages/foundry/contracts/hooks/Hookathon/deploy/instance.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/foundry/contracts/hooks/Hookathon/docker-compose/docker-compose-full.yml.template b/packages/foundry/contracts/hooks/Hookathon/docker-compose/docker-compose-full.yml.template new file mode 100644 index 00000000..45ba51bd --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/docker-compose/docker-compose-full.yml.template @@ -0,0 +1,98 @@ +name: zama-dev + +services: + gateway-store: + image: ghcr.io/zama-ai/kms-blockchain-gateway-dev:v0.7.1 + command: + - "kv_store" + ports: + - "8088:8088" + + kms-validator: + image: ghcr.io/zama-ai/kms-blockchain-asc-dev:v0.7.1 + ports: + - "36656:26656" + - "36657:26657" + - "1317:1317" + - "9090:9090" + entrypoint: ["/app/bootstrap.sh"] + healthcheck: + test: "wget -Sq --spider http://localhost:26657/status" + interval: 1s + timeout: 1s + retries: 5 + start_period: 10s + + connector: + image: ghcr.io/zama-ai/kms-blockchain-connector-dev:v0.7.1 + command: + - "kms-blockchain-connector" + environment: + - ASC_CONN__BLOCKCHAIN__ADDRESSES=http://kms-validator:9090 + - ASC_CONN__CORE__ADDRESSES=http://kms-core:50051 + - ASC_CONN__STORE__URL=http://gateway-store:8088 + - ASC_CONN__CORE__TIMEOUT_CONFIG__DECRYPTION__INITIAL_WAIT_TIME=1 + - ASC_CONN__CORE__TIMEOUT_CONFIG__DECRYPTION__RETRY_INTERVAL=1 + - ASC_CONN__CORE__TIMEOUT_CONFIG__REENCRYPTION__INITIAL_WAIT_TIME=1 + - ASC_CONN__CORE__TIMEOUT_CONFIG__REENCRYPTION__RETRY_INTERVAL=1 + depends_on: + kms-validator: + condition: service_healthy + kms-core: + condition: service_healthy + + kms-core: + image: ghcr.io/zama-ai/kms-service-dev:v0.7.1 + ports: + - "50051:50051" + healthcheck: + test: "grpc-health-probe --addr=localhost:50051" + interval: 1s + timeout: 1s + retries: 5 + start_period: 10s + + gateway: + image: ghcr.io/zama-ai/kms-blockchain-gateway-dev:v0.7.1 + ports: + - "7077:7077" + command: + - "gateway" + volumes: + - ../default.toml:/app/gateway/config/default.toml:Z + environment: + - GATEWAY__ETHEREUM__LISTENER_TYPE=FHEVM_V1_1 + - GATEWAY__ETHEREUM__WSS_URL=ws://fhevm-validator:8546 + - GATEWAY__ETHEREUM__HTTP_URL=http://fhevm-validator:8545 + - GATEWAY__ETHEREUM__FHE_LIB_ADDRESS=000000000000000000000000000000000000005d + - GATEWAY__ETHEREUM__ORACLE_PREDEPLOY_ADDRESS=${GATEWAY_CONTRACT_PREDEPLOY_ADDRESS} + - GATEWAY__KMS__ADDRESS=http://kms-validator:9090 + - GATEWAY__KMS__KEY_ID=408d8cbaa51dece7f782fe04ba0b1c1d017b1088 + - GATEWAY__STORAGE__URL=http://gateway-store:8088 + - ASC_CONN__BLOCKCHAIN__ADDRESSES=http://kms-validator:9090 + - GATEWAY__ETHEREUM__RELAYER_KEY=${PRIVATE_KEY_GATEWAY_RELAYER} + - RUST_BACKTRACE=1 + depends_on: + fhevm-validator: + condition: service_healthy + kms-validator: + condition: service_healthy + + fhevm-validator: + environment: + - TFHE_EXECUTOR_CONTRACT_ADDRESS=${TFHE_EXECUTOR_CONTRACT_ADDRESS} + image: ghcr.io/zama-ai/ethermint-dev-node:v0.5.0-2 + ports: + - "26656-26657:26656-26657" + - "8545-8546:8545-8546" + volumes: + - $PWD/network-fhe-keys:/network-fhe-keys:Z + - ../setup.sh:/config/setup.sh:Z + security_opt: + - no-new-privileges:true + healthcheck: + test: 'curl -s -H "Connection: Upgrade" -H "Upgrade: websocket" http://localhost:8546' + interval: 5s + timeout: 3s + retries: 5 + start_period: 20s diff --git a/packages/foundry/contracts/hooks/Hookathon/eslint.config.mjs b/packages/foundry/contracts/hooks/Hookathon/eslint.config.mjs new file mode 100644 index 00000000..03e31735 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/eslint.config.mjs @@ -0,0 +1,17 @@ +import eslint from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default [ + { + languageOptions: { + globals: globals.node, + }, + linterOptions: { + reportUnusedDisableDirectives: "off", + }, + ignores: ["abi/", "artifacts/", "cache/", "res/", "types/*"], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, +]; diff --git a/packages/foundry/contracts/hooks/Hookathon/hardhat.config.ts b/packages/foundry/contracts/hooks/Hookathon/hardhat.config.ts new file mode 100644 index 00000000..326f3524 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/hardhat.config.ts @@ -0,0 +1,186 @@ +import "@nomicfoundation/hardhat-toolbox"; +import dotenv from "dotenv"; +import * as fs from "fs-extra"; +import "hardhat-deploy"; +import "hardhat-ignore-warnings"; +import type { HardhatUserConfig } from "hardhat/config"; +import { extendProvider } from "hardhat/config"; +import { task } from "hardhat/config"; +import type { NetworkUserConfig } from "hardhat/types"; +import { resolve } from "path"; +import * as path from "path"; + +import CustomProvider from "./CustomProvider"; +// Adjust the import path as needed +import "./tasks/accounts"; +import "./tasks/getEthereumAddress"; +import "./tasks/taskDeploy"; +import "./tasks/taskGatewayRelayer"; +import "./tasks/taskTFHE"; + +extendProvider(async (provider) => { + const newProvider = new CustomProvider(provider); + return newProvider; +}); + +task("compile:specific", "Compiles only the specified contract") + .addParam("contract", "The contract's path") + .setAction(async ({ contract }, hre) => { + // Adjust the configuration to include only the specified contract + hre.config.paths.sources = contract; + + await hre.run("compile"); + }); + +const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || "./.env"; +dotenv.config({ path: resolve(__dirname, dotenvConfigPath) }); + +// Ensure that we have all the environment variables we need. +const mnemonic: string | undefined = process.env.MNEMONIC; +if (!mnemonic) { + throw new Error("Please set your MNEMONIC in a .env file"); +} + +const chainIds = { + Inco: 9000, + local: 9000, + localNetwork1: 9000, + multipleValidatorTestnet: 8009, +}; + +function getChainConfig(chain: keyof typeof chainIds): NetworkUserConfig { + let jsonRpcUrl: string; + switch (chain) { + case "local": + jsonRpcUrl = "http://localhost:8545"; + break; + case "localNetwork1": + jsonRpcUrl = "http://127.0.0.1:9650/ext/bc/fhevm/rpc"; + break; + case "multipleValidatorTestnet": + jsonRpcUrl = "https://rpc.fhe-ethermint.zama.ai"; + break; + case "Inco": + jsonRpcUrl = "https://validator.rivest.inco.org"; + break; + } + return { + accounts: { + count: 10, + mnemonic, + path: "m/44'/60'/0'/0", + }, + chainId: chainIds[chain], + url: jsonRpcUrl, + }; +} + +task("coverage").setAction(async (taskArgs, hre, runSuper) => { + hre.config.networks.hardhat.allowUnlimitedContractSize = true; + hre.config.networks.hardhat.blockGasLimit = 1099511627775; + await runSuper(taskArgs); +}); + +task("test", async (taskArgs, hre, runSuper) => { + // Run modified test task + if (hre.network.name === "hardhat") { + // in fhevm mode all this block is done when launching the node via `pnmp fhevm:start` + await hre.run("compile:specific", { contract: "contracts" }); + const sourceDir = path.resolve(__dirname, "node_modules/fhevm/"); + const destinationDir = path.resolve(__dirname, "fhevmTemp/"); + fs.copySync(sourceDir, destinationDir, { dereference: true }); + await hre.run("compile:specific", { contract: "fhevmTemp/lib" }); + await hre.run("compile:specific", { contract: "fhevmTemp/gateway" }); + const abiDir = path.resolve(__dirname, "abi"); + fs.ensureDirSync(abiDir); + const sourceFile = path.resolve(__dirname, "artifacts/fhevmTemp/lib/TFHEExecutor.sol/TFHEExecutor.json"); + const destinationFile = path.resolve(abiDir, "TFHEExecutor.json"); + fs.copyFileSync(sourceFile, destinationFile); + + const targetAddress = "0x000000000000000000000000000000000000005d"; + const MockedPrecompile = await hre.artifacts.readArtifact("MockedPrecompile"); + const bytecode = MockedPrecompile.deployedBytecode; + await hre.network.provider.send("hardhat_setCode", [targetAddress, bytecode]); + console.log(`Code of Mocked Pre-compile set at address: ${targetAddress}`); + + const privKeyDeployer = process.env.PRIVATE_KEY_GATEWAY_DEPLOYER; + await hre.run("task:computePredeployAddress", { privateKey: privKeyDeployer }); + await hre.run("task:computeACLAddress"); + await hre.run("task:computeTFHEExecutorAddress"); + await hre.run("task:computeKMSVerifierAddress"); + await hre.run("task:deployACL"); + await hre.run("task:deployTFHEExecutor"); + await hre.run("task:deployKMSVerifier"); + await hre.run("task:launchFhevm", { skipGetCoin: false }); + } + await runSuper(); +}); + +const config: HardhatUserConfig = { + defaultNetwork: "local", + namedAccounts: { + deployer: 0, + }, + mocha: { + timeout: 500000, + }, + gasReporter: { + currency: "USD", + enabled: process.env.REPORT_GAS ? true : false, + excludeContracts: [], + src: "./contracts", + }, + networks: { + hardhat: { + accounts: { + count: 10, + mnemonic, + path: "m/44'/60'/0'/0", + }, + }, + Inco: getChainConfig("Inco"), + localDev: getChainConfig("local"), + local: getChainConfig("local"), + localNetwork1: getChainConfig("localNetwork1"), + multipleValidatorTestnet: getChainConfig("multipleValidatorTestnet"), + }, + paths: { + artifacts: "./artifacts", + cache: "./cache", + sources: "./contracts", + tests: "./test", + }, + solidity: { + version: "0.8.24", + settings: { + metadata: { + // Not including the metadata hash + // https://github.com/paulrberg/hardhat-template/issues/31 + bytecodeHash: "none", + }, + viaIR: true, + // Disable the optimizer when debugging + // https://hardhat.org/hardhat-network/#solidity-optimizer-support + optimizer: { + enabled: true, + runs: 200, + details: { + yul: true + } + }, + evmVersion: "cancun", + + }, + }, + warnings: { + "*": { + "transient-storage": false, + }, + }, + typechain: { + outDir: "types", + target: "ethers-v6", + }, +}; + +export default config; \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/launch-fhevm.sh b/packages/foundry/contracts/hooks/Hookathon/launch-fhevm.sh new file mode 100755 index 00000000..97a9e8f7 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/launch-fhevm.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Assumes the following: +# 1. Predeploys addresses have been precomputed via the precomputeAddresses.sh script. +# 2. A local and **fresh** fhEVM node is already running. +# 3. All test addresses are funded (e.g. via the fund_test_addresses.sh script). + +npx hardhat compile:specific --contract contracts + +mkdir -p fhevmTemp +cp -L -r node_modules/fhevm fhevmTemp/ +npx hardhat compile:specific --contract fhevmTemp/fhevm/lib +npx hardhat compile:specific --contract fhevmTemp/fhevm/gateway +mkdir -p abi +cp artifacts/fhevmTemp/fhevm/lib/TFHEExecutor.sol/TFHEExecutor.json abi/TFHEExecutor.json + +npx hardhat task:deployACL +npx hardhat task:deployTFHEExecutor +npx hardhat task:deployKMSVerifier + +npx hardhat task:launchFhevm --skip-get-coin true + +rm -rf fhevmTemp \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/package.json b/packages/foundry/contracts/hooks/Hookathon/package.json new file mode 100644 index 00000000..c60edec7 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/package.json @@ -0,0 +1,107 @@ +{ + "name": "@zama-ai/fhevm-hardhat-template", + "description": "fhEVM hardhat template", + "version": "1.0.0", + "engines": { + "node": ">=20.0.0" + }, + "author": { + "name": "zama-ai", + "url": "https://github.com/zama-ai" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "@nomicfoundation/hardhat-foundry": "^1.1.2", + "@nomicfoundation/hardhat-network-helpers": "^1.0.6", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.0.0", + "@openzeppelin/contracts": "^5.0.2", + "@trivago/prettier-plugin-sort-imports": "^4.0.0", + "@typechain/ethers-v6": "^0.4.0", + "@typechain/hardhat": "^8.0.0", + "@types/chai": "^4.3.4", + "@types/eslint__js": "^8.42.3", + "@types/fs-extra": "^9.0.13", + "@types/mocha": "^10.0.0", + "@types/node": "^18.11.9", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", + "chai": "^4.3.7", + "cross-env": "^7.0.3", + "dotenv": "^16.0.3", + "eslint": "^9.9.0", + "eslint-config-prettier": "^8.5.0", + "ethers": "^6.8.0", + "fhevm": "^0.5.8", + "fhevmjs": "^0.5.2", + "fs-extra": "^10.1.0", + "globals": "^15.9.0", + "hardhat": "^2.22.8", + "hardhat-deploy": "^0.12.4", + "hardhat-gas-reporter": "^1.0.9", + "hardhat-ignore-warnings": "^0.2.11", + "hardhat-preprocessor": "^0.1.5", + "lodash": "^4.17.21", + "mocha": "^10.1.0", + "prettier": "^2.8.4", + "prettier-plugin-solidity": "^1.1.2", + "rimraf": "^4.1.2", + "solhint": "^3.4.0", + "solhint-plugin-prettier": "^0.0.5", + "solidity-coverage": "0.8.12", + "ts-generator": "^0.1.1", + "ts-node": "^10.9.1", + "typechain": "^8.2.0", + "typescript": "^5.5.4", + "typescript-eslint": "^8.0.1" + }, + "files": [ + "contracts" + ], + "keywords": [ + "blockchain", + "ethers", + "ethereum", + "hardhat", + "smart-contracts", + "solidity", + "template", + "typescript", + "typechain" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "preinstall": "node tasks/checkNodeVersion.js", + "clean": "rimraf ./artifacts ./cache ./coverage ./types ./coverage.json && pnpm typechain", + "compile": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat compile", + "deploy:contracts": "hardhat deploy", + "lint": "pnpm lint:sol && pnpm lint:ts && pnpm prettier:check", + "lint:sol": "solhint --max-warnings 25 \"contracts/**/*.sol\"", + "lint:ts": "eslint .", + "postinstall": "DOTENV_CONFIG_PATH=./.env.example pnpm typechain", + "prettier:check": "prettier --check \"**/*.{js,json,md,sol,ts,yml}\"", + "prettier:write": "prettier --write \"**/*.{js,json,md,sol,ts,yml}\"", + "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain", + "test": "hardhat test", + "test:mock": "hardhat test --network hardhat", + "coverage:mock": "hardhat coverage", + "task:getEthereumAddress": "hardhat task:getEthereumAddress", + "task:deployERC20": "hardhat task:deployERC20", + "task:accounts": "hardhat task:accounts", + "fhevm:start": "./scripts/precomputeAddresses.sh && ./scripts/rewrite-docker-compose.sh && make run-full && ./scripts/fund_test_addresses_docker.sh && ./launch-fhevm.sh", + "fhevm:stop": "make clean", + "fhevm:restart": "fhevm:stop && fhevm:start", + "fhevm:faucet": "npm run fhevm:faucet:alice && sleep 5 && npm run fhevm:faucet:bob && sleep 5 && npm run fhevm:faucet:carol && sleep 5 && npm run fhevm:faucet:dave && sleep 5 && npm run fhevm:faucet:eve", + "fhevm:faucet:alice": "docker exec -i zama-dev-fhevm-validator-1 faucet $(npx hardhat task:getEthereumAddressAlice)", + "fhevm:faucet:bob": "docker exec -i zama-dev-fhevm-validator-1 faucet $(npx hardhat task:getEthereumAddressBob)", + "fhevm:faucet:carol": "docker exec -i zama-dev-fhevm-validator-1 faucet $(npx hardhat task:getEthereumAddressCarol)", + "fhevm:faucet:dave": "docker exec -i zama-dev-fhevm-validator-1 faucet $(npx hardhat task:getEthereumAddressDave)", + "fhevm:faucet:eve": "docker exec -i zama-dev-fhevm-validator-1 faucet $(npx hardhat task:getEthereumAddressEve)" + }, + "dependencies": { + } +} diff --git a/packages/foundry/contracts/hooks/Hookathon/pnpm-lock.yaml b/packages/foundry/contracts/hooks/Hookathon/pnpm-lock.yaml new file mode 100644 index 00000000..81af5e87 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/pnpm-lock.yaml @@ -0,0 +1,6063 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@uniswap/permit2-sdk': + specifier: ^1.3.0 + version: 1.3.0 + devDependencies: + '@eslint/js': + specifier: ^9.9.0 + version: 9.9.0 + '@nomicfoundation/hardhat-chai-matchers': + specifier: ^2.0.0 + version: 2.0.7(@nomicfoundation/hardhat-ethers@3.0.6(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)))(chai@4.5.0)(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@nomicfoundation/hardhat-ethers': + specifier: ^3.0.0 + version: 3.0.6(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@nomicfoundation/hardhat-foundry': + specifier: ^1.1.2 + version: 1.1.2(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@nomicfoundation/hardhat-network-helpers': + specifier: ^1.0.6 + version: 1.0.11(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@nomicfoundation/hardhat-toolbox': + specifier: ^3.0.0 + version: 3.0.0(geae4jvdfg2eknoy6ku3h4qpgy) + '@nomicfoundation/hardhat-verify': + specifier: ^1.0.0 + version: 1.1.1(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@openzeppelin/contracts': + specifier: ^5.0.2 + version: 5.0.2 + '@trivago/prettier-plugin-sort-imports': + specifier: ^4.0.0 + version: 4.3.0(prettier@2.8.8) + '@typechain/ethers-v6': + specifier: ^0.4.0 + version: 0.4.3(ethers@6.13.2)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4) + '@typechain/hardhat': + specifier: ^8.0.0 + version: 8.0.3(@typechain/ethers-v6@0.4.3(ethers@6.13.2)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4))(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))(typechain@8.3.2(typescript@5.5.4)) + '@types/chai': + specifier: ^4.3.4 + version: 4.3.17 + '@types/eslint__js': + specifier: ^8.42.3 + version: 8.42.3 + '@types/fs-extra': + specifier: ^9.0.13 + version: 9.0.13 + '@types/mocha': + specifier: ^10.0.0 + version: 10.0.7 + '@types/node': + specifier: ^18.11.9 + version: 18.19.44 + '@typescript-eslint/eslint-plugin': + specifier: ^8.0.1 + version: 8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/parser': + specifier: ^8.0.1 + version: 8.0.1(eslint@9.9.0)(typescript@5.5.4) + chai: + specifier: ^4.3.7 + version: 4.5.0 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + dotenv: + specifier: ^16.0.3 + version: 16.4.5 + eslint: + specifier: ^9.9.0 + version: 9.9.0 + eslint-config-prettier: + specifier: ^8.5.0 + version: 8.10.0(eslint@9.9.0) + ethers: + specifier: ^6.8.0 + version: 6.13.2 + fhevm: + specifier: ^0.5.8 + version: 0.5.9 + fhevmjs: + specifier: ^0.5.2 + version: 0.5.2(encoding@0.1.13) + fs-extra: + specifier: ^10.1.0 + version: 10.1.0 + globals: + specifier: ^15.9.0 + version: 15.9.0 + hardhat: + specifier: ^2.22.8 + version: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + hardhat-deploy: + specifier: ^0.12.4 + version: 0.12.4 + hardhat-gas-reporter: + specifier: ^1.0.9 + version: 1.0.10(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + hardhat-ignore-warnings: + specifier: ^0.2.11 + version: 0.2.11 + hardhat-preprocessor: + specifier: ^0.1.5 + version: 0.1.5(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + lodash: + specifier: ^4.17.21 + version: 4.17.21 + mocha: + specifier: ^10.1.0 + version: 10.7.3 + prettier: + specifier: ^2.8.4 + version: 2.8.8 + prettier-plugin-solidity: + specifier: ^1.1.2 + version: 1.3.1(prettier@2.8.8) + rimraf: + specifier: ^4.1.2 + version: 4.4.1 + solhint: + specifier: ^3.4.0 + version: 3.6.2(typescript@5.5.4) + solhint-plugin-prettier: + specifier: ^0.0.5 + version: 0.0.5(prettier-plugin-solidity@1.3.1(prettier@2.8.8))(prettier@2.8.8) + solidity-coverage: + specifier: 0.8.12 + version: 0.8.12(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + ts-generator: + specifier: ^0.1.1 + version: 0.1.1 + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@types/node@18.19.44)(typescript@5.5.4) + typechain: + specifier: ^8.2.0 + version: 8.3.2(typescript@5.5.4) + typescript: + specifier: ^5.5.4 + version: 5.5.4 + typescript-eslint: + specifier: ^8.0.1 + version: 8.0.1(eslint@9.9.0)(typescript@5.5.4) + +packages: + + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.17.7': + resolution: {integrity: sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.25.0': + resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-function-name@7.24.7': + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-hoist-variables@7.24.7': + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.24.7': + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.8': + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.25.3': + resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.25.0': + resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.23.2': + resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.17.0': + resolution: {integrity: sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.25.2': + resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.17.1': + resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.9.0': + resolution: {integrity: sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ethereumjs/rlp@4.0.1': + resolution: {integrity: sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==} + engines: {node: '>=14'} + hasBin: true + + '@ethereumjs/util@8.1.0': + resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} + engines: {node: '>=14'} + + '@ethersproject/abi@5.7.0': + resolution: {integrity: sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==} + + '@ethersproject/abstract-provider@5.7.0': + resolution: {integrity: sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==} + + '@ethersproject/abstract-signer@5.7.0': + resolution: {integrity: sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==} + + '@ethersproject/address@5.7.0': + resolution: {integrity: sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==} + + '@ethersproject/base64@5.7.0': + resolution: {integrity: sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==} + + '@ethersproject/basex@5.7.0': + resolution: {integrity: sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==} + + '@ethersproject/bignumber@5.7.0': + resolution: {integrity: sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==} + + '@ethersproject/bytes@5.7.0': + resolution: {integrity: sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==} + + '@ethersproject/constants@5.7.0': + resolution: {integrity: sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==} + + '@ethersproject/contracts@5.7.0': + resolution: {integrity: sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==} + + '@ethersproject/hash@5.7.0': + resolution: {integrity: sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==} + + '@ethersproject/hdnode@5.7.0': + resolution: {integrity: sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==} + + '@ethersproject/json-wallets@5.7.0': + resolution: {integrity: sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==} + + '@ethersproject/keccak256@5.7.0': + resolution: {integrity: sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==} + + '@ethersproject/logger@5.7.0': + resolution: {integrity: sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==} + + '@ethersproject/networks@5.7.1': + resolution: {integrity: sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==} + + '@ethersproject/pbkdf2@5.7.0': + resolution: {integrity: sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==} + + '@ethersproject/properties@5.7.0': + resolution: {integrity: sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==} + + '@ethersproject/providers@5.7.2': + resolution: {integrity: sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==} + + '@ethersproject/random@5.7.0': + resolution: {integrity: sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==} + + '@ethersproject/rlp@5.7.0': + resolution: {integrity: sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==} + + '@ethersproject/sha2@5.7.0': + resolution: {integrity: sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==} + + '@ethersproject/signing-key@5.7.0': + resolution: {integrity: sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==} + + '@ethersproject/solidity@5.7.0': + resolution: {integrity: sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==} + + '@ethersproject/strings@5.7.0': + resolution: {integrity: sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==} + + '@ethersproject/transactions@5.7.0': + resolution: {integrity: sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==} + + '@ethersproject/units@5.7.0': + resolution: {integrity: sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==} + + '@ethersproject/wallet@5.7.0': + resolution: {integrity: sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==} + + '@ethersproject/web@5.7.1': + resolution: {integrity: sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==} + + '@ethersproject/wordlists@5.7.0': + resolution: {integrity: sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.0': + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@metamask/eth-sig-util@4.0.1': + resolution: {integrity: sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ==} + engines: {node: '>=12.0.0'} + + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + + '@noble/curves@1.4.2': + resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} + + '@noble/hashes@1.2.0': + resolution: {integrity: sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==} + + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/secp256k1@1.7.1': + resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nomicfoundation/edr-darwin-arm64@0.5.2': + resolution: {integrity: sha512-Gm4wOPKhbDjGTIRyFA2QUAPfCXA1AHxYOKt3yLSGJkQkdy9a5WW+qtqKeEKHc/+4wpJSLtsGQfpzyIzggFfo/A==} + engines: {node: '>= 18'} + + '@nomicfoundation/edr-darwin-x64@0.5.2': + resolution: {integrity: sha512-ClyABq2dFCsrYEED3/UIO0c7p4H1/4vvlswFlqUyBpOkJccr75qIYvahOSJRM62WgUFRhbSS0OJXFRwc/PwmVg==} + engines: {node: '>= 18'} + + '@nomicfoundation/edr-linux-arm64-gnu@0.5.2': + resolution: {integrity: sha512-HWMTVk1iOabfvU2RvrKLDgtFjJZTC42CpHiw2h6rfpsgRqMahvIlx2jdjWYzFNy1jZKPTN1AStQ/91MRrg5KnA==} + engines: {node: '>= 18'} + + '@nomicfoundation/edr-linux-arm64-musl@0.5.2': + resolution: {integrity: sha512-CwsQ10xFx/QAD5y3/g5alm9+jFVuhc7uYMhrZAu9UVF+KtVjeCvafj0PaVsZ8qyijjqVuVsJ8hD1x5ob7SMcGg==} + engines: {node: '>= 18'} + + '@nomicfoundation/edr-linux-x64-gnu@0.5.2': + resolution: {integrity: sha512-CWVCEdhWJ3fmUpzWHCRnC0/VLBDbqtqTGTR6yyY1Ep3S3BOrHEAvt7h5gx85r2vLcztisu2vlDq51auie4IU1A==} + engines: {node: '>= 18'} + + '@nomicfoundation/edr-linux-x64-musl@0.5.2': + resolution: {integrity: sha512-+aJDfwhkddy2pP5u1ISg3IZVAm0dO836tRlDTFWtvvSMQ5hRGqPcWwlsbobhDQsIxhPJyT7phL0orCg5W3WMeA==} + engines: {node: '>= 18'} + + '@nomicfoundation/edr-win32-x64-msvc@0.5.2': + resolution: {integrity: sha512-CcvvuA3sAv7liFNPsIR/68YlH6rrybKzYttLlMr80d4GKJjwJ5OKb3YgE6FdZZnOfP19HEHhsLcE0DPLtY3r0w==} + engines: {node: '>= 18'} + + '@nomicfoundation/edr@0.5.2': + resolution: {integrity: sha512-hW/iLvUQZNTVjFyX/I40rtKvvDOqUEyIi96T28YaLfmPL+3LW2lxmYLUXEJ6MI14HzqxDqrLyhf6IbjAa2r3Dw==} + engines: {node: '>= 18'} + + '@nomicfoundation/ethereumjs-common@4.0.4': + resolution: {integrity: sha512-9Rgb658lcWsjiicr5GzNCjI1llow/7r0k50dLL95OJ+6iZJcVbi15r3Y0xh2cIO+zgX0WIHcbzIu6FeQf9KPrg==} + + '@nomicfoundation/ethereumjs-rlp@5.0.4': + resolution: {integrity: sha512-8H1S3s8F6QueOc/X92SdrA4RDenpiAEqMg5vJH99kcQaCy/a3Q6fgseo75mgWlbanGJXSlAPtnCeG9jvfTYXlw==} + engines: {node: '>=18'} + hasBin: true + + '@nomicfoundation/ethereumjs-tx@5.0.4': + resolution: {integrity: sha512-Xjv8wAKJGMrP1f0n2PeyfFCCojHd7iS3s/Ab7qzF1S64kxZ8Z22LCMynArYsVqiFx6rzYy548HNVEyI+AYN/kw==} + engines: {node: '>=18'} + peerDependencies: + c-kzg: ^2.1.2 + peerDependenciesMeta: + c-kzg: + optional: true + + '@nomicfoundation/ethereumjs-util@9.0.4': + resolution: {integrity: sha512-sLOzjnSrlx9Bb9EFNtHzK/FJFsfg2re6bsGqinFinH1gCqVfz9YYlXiMWwDM4C/L4ywuHFCYwfKTVr/QHQcU0Q==} + engines: {node: '>=18'} + peerDependencies: + c-kzg: ^2.1.2 + peerDependenciesMeta: + c-kzg: + optional: true + + '@nomicfoundation/hardhat-chai-matchers@2.0.7': + resolution: {integrity: sha512-RQfsiTwdf0SP+DtuNYvm4921X6VirCQq0Xyh+mnuGlTwEFSPZ/o27oQC+l+3Y/l48DDU7+ZcYBR+Fp+Rp94LfQ==} + peerDependencies: + '@nomicfoundation/hardhat-ethers': ^3.0.0 + chai: ^4.2.0 + ethers: ^6.1.0 + hardhat: ^2.9.4 + + '@nomicfoundation/hardhat-ethers@3.0.6': + resolution: {integrity: sha512-/xzkFQAaHQhmIAYOQmvHBPwL+NkwLzT9gRZBsgWUYeV+E6pzXsBQsHfRYbAZ3XEYare+T7S+5Tg/1KDJgepSkA==} + peerDependencies: + ethers: ^6.1.0 + hardhat: ^2.0.0 + + '@nomicfoundation/hardhat-foundry@1.1.2': + resolution: {integrity: sha512-f5Vhj3m2qvKGpr6NAINYwNgILDsai8dVCsFb1rAVLkJxOmD2pAtfCmOH5SBVr9yUI5B1z9rbTwPBJVrqnb+PXQ==} + peerDependencies: + hardhat: ^2.17.2 + + '@nomicfoundation/hardhat-network-helpers@1.0.11': + resolution: {integrity: sha512-uGPL7QSKvxrHRU69dx8jzoBvuztlLCtyFsbgfXIwIjnO3dqZRz2GNMHJoO3C3dIiUNM6jdNF4AUnoQKDscdYrA==} + peerDependencies: + hardhat: ^2.9.5 + + '@nomicfoundation/hardhat-toolbox@3.0.0': + resolution: {integrity: sha512-MsteDXd0UagMksqm9KvcFG6gNKYNa3GGNCy73iQ6bEasEgg2v8Qjl6XA5hjs8o5UD5A3153B6W2BIVJ8SxYUtA==} + peerDependencies: + '@nomicfoundation/hardhat-chai-matchers': ^2.0.0 + '@nomicfoundation/hardhat-ethers': ^3.0.0 + '@nomicfoundation/hardhat-network-helpers': ^1.0.0 + '@nomicfoundation/hardhat-verify': ^1.0.0 + '@typechain/ethers-v6': ^0.4.0 + '@typechain/hardhat': ^8.0.0 + '@types/chai': ^4.2.0 + '@types/mocha': '>=9.1.0' + '@types/node': '>=12.0.0' + chai: ^4.2.0 + ethers: ^6.4.0 + hardhat: ^2.11.0 + hardhat-gas-reporter: ^1.0.8 + solidity-coverage: ^0.8.1 + ts-node: '>=8.0.0' + typechain: ^8.2.0 + typescript: '>=4.5.0' + + '@nomicfoundation/hardhat-verify@1.1.1': + resolution: {integrity: sha512-9QsTYD7pcZaQFEA3tBb/D/oCStYDiEVDN7Dxeo/4SCyHRSm86APypxxdOMEPlGmXsAvd+p1j/dTODcpxb8aztA==} + peerDependencies: + hardhat: ^2.0.4 + + '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': + resolution: {integrity: sha512-JaqcWPDZENCvm++lFFGjrDd8mxtf+CtLd2MiXvMNTBD33dContTZ9TWETwNFwg7JTJT5Q9HEecH7FA+HTSsIUw==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2': + resolution: {integrity: sha512-fZNmVztrSXC03e9RONBT+CiksSeYcxI1wlzqyr0L7hsQlK1fzV+f04g2JtQ1c/Fe74ZwdV6aQBdd6Uwl1052sw==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2': + resolution: {integrity: sha512-3d54oc+9ZVBuB6nbp8wHylk4xh0N0Gc+bk+/uJae+rUgbOBwQSfuGIbAZt1wBXs5REkSmynEGcqx6DutoK0tPA==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2': + resolution: {integrity: sha512-iDJfR2qf55vgsg7BtJa7iPiFAsYf2d0Tv/0B+vhtnI16+wfQeTbP7teookbGvAo0eJo7aLLm0xfS/GTkvHIucA==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2': + resolution: {integrity: sha512-9dlHMAt5/2cpWyuJ9fQNOUXFB/vgSFORg1jpjX1Mh9hJ/MfZXlDdHQ+DpFCs32Zk5pxRBb07yGvSHk9/fezL+g==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2': + resolution: {integrity: sha512-GzzVeeJob3lfrSlDKQw2bRJ8rBf6mEYaWY+gW0JnTDHINA0s2gPR4km5RLIj1xeZZOYz4zRw+AEeYgLRqB2NXg==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2': + resolution: {integrity: sha512-Fdjli4DCcFHb4Zgsz0uEJXZ2K7VEO+w5KVv7HmT7WO10iODdU9csC2az4jrhEsRtiR9Gfd74FlG0NYlw1BMdyA==} + engines: {node: '>= 12'} + + '@nomicfoundation/solidity-analyzer@0.1.2': + resolution: {integrity: sha512-q4n32/FNKIhQ3zQGGw5CvPF6GTvDCpYwIf7bEY/dZTZbgfDsHyjJwURxUJf3VQuuJj+fDIFl4+KkBVbw4Ef6jA==} + engines: {node: '>= 12'} + + '@openzeppelin/contracts@5.0.2': + resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} + + '@scure/base@1.1.7': + resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} + + '@scure/bip32@1.1.5': + resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} + + '@scure/bip32@1.4.0': + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + + '@scure/bip39@1.1.1': + resolution: {integrity: sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==} + + '@scure/bip39@1.3.0': + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + + '@sentry/core@5.30.0': + resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} + engines: {node: '>=6'} + + '@sentry/hub@5.30.0': + resolution: {integrity: sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==} + engines: {node: '>=6'} + + '@sentry/minimal@5.30.0': + resolution: {integrity: sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==} + engines: {node: '>=6'} + + '@sentry/node@5.30.0': + resolution: {integrity: sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==} + engines: {node: '>=6'} + + '@sentry/tracing@5.30.0': + resolution: {integrity: sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==} + engines: {node: '>=6'} + + '@sentry/types@5.30.0': + resolution: {integrity: sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==} + engines: {node: '>=6'} + + '@sentry/utils@5.30.0': + resolution: {integrity: sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==} + engines: {node: '>=6'} + + '@solidity-parser/parser@0.14.5': + resolution: {integrity: sha512-6dKnHZn7fg/iQATVEzqyUOyEidbn05q7YA2mQ9hC0MMXhhV3/JrsxmFSYZAcr7j1yUP700LLhTruvJ3MiQmjJg==} + + '@solidity-parser/parser@0.16.2': + resolution: {integrity: sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg==} + + '@solidity-parser/parser@0.17.0': + resolution: {integrity: sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw==} + + '@solidity-parser/parser@0.18.0': + resolution: {integrity: sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA==} + + '@trivago/prettier-plugin-sort-imports@4.3.0': + resolution: {integrity: sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@typechain/ethers-v6@0.4.3': + resolution: {integrity: sha512-TrxBsyb4ryhaY9keP6RzhFCviWYApcLCIRMPyWaKp2cZZrfaM3QBoxXTnw/eO4+DAY3l+8O0brNW0WgeQeOiDA==} + peerDependencies: + ethers: 6.x + typechain: ^8.3.1 + typescript: '>=4.7.0' + + '@typechain/hardhat@8.0.3': + resolution: {integrity: sha512-MytSmJJn+gs7Mqrpt/gWkTCOpOQ6ZDfRrRT2gtZL0rfGe4QrU4x9ZdW15fFbVM/XTa+5EsKiOMYXhRABibNeng==} + peerDependencies: + '@typechain/ethers-v6': ^0.4.3 + ethers: ^6.1.0 + hardhat: ^2.9.9 + typechain: ^8.3.1 + + '@types/bn.js@4.11.6': + resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==} + + '@types/bn.js@5.1.5': + resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==} + + '@types/chai-as-promised@7.1.8': + resolution: {integrity: sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==} + + '@types/chai@4.3.17': + resolution: {integrity: sha512-zmZ21EWzR71B4Sscphjief5djsLre50M6lI622OSySTmn9DB3j+C3kWroHfBQWXbOBwbgg/M8CG/hUxDLIloow==} + + '@types/concat-stream@1.6.1': + resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} + + '@types/eslint@9.6.0': + resolution: {integrity: sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==} + + '@types/eslint__js@8.42.3': + resolution: {integrity: sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==} + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/form-data@0.0.33': + resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/keccak@3.0.4': + resolution: {integrity: sha512-hdnkmbie7tE0yXnQQvlIOqCyjEsoXDVEZ3ACqO+F305XgUOW4Z9ElWdogCXXRAW/khnZ7GxM0t/BGB5bORKt/g==} + + '@types/lru-cache@5.1.1': + resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/mkdirp@0.5.2': + resolution: {integrity: sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg==} + + '@types/mocha@10.0.7': + resolution: {integrity: sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==} + + '@types/node@10.17.60': + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + + '@types/node@18.15.13': + resolution: {integrity: sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==} + + '@types/node@18.19.44': + resolution: {integrity: sha512-ZsbGerYg72WMXUIE9fYxtvfzLEuq6q8mKERdWFnqTmOvudMxnz+CBNRoOwJ2kNpFOncrKjT1hZwxjlFgQ9qvQA==} + + '@types/node@8.10.66': + resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} + + '@types/pbkdf2@3.1.2': + resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==} + + '@types/prettier@2.7.3': + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + + '@types/qs@6.9.15': + resolution: {integrity: sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==} + + '@types/resolve@0.0.8': + resolution: {integrity: sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==} + + '@types/secp256k1@4.0.6': + resolution: {integrity: sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ==} + + '@typescript-eslint/eslint-plugin@8.0.1': + resolution: {integrity: sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.0.1': + resolution: {integrity: sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.0.1': + resolution: {integrity: sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.0.1': + resolution: {integrity: sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.0.1': + resolution: {integrity: sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.0.1': + resolution: {integrity: sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.0.1': + resolution: {integrity: sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.0.1': + resolution: {integrity: sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@uniswap/permit2-sdk@1.3.0': + resolution: {integrity: sha512-LstYQWP47dwpQrgqBJ+ysFstne9LgI5FGiKHc2ewjj91MTY8Mq1reocu6U/VDncdR5ef30TUOcZ7gPExRY8r6Q==} + + abbrev@1.0.9: + resolution: {integrity: sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.3: + resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} + engines: {node: '>=0.4.0'} + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + adm-zip@0.4.16: + resolution: {integrity: sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==} + engines: {node: '>=0.3.0'} + + aes-js@3.0.0: + resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} + + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + antlr4@4.13.2: + resolution: {integrity: sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==} + engines: {node: '>=16'} + + antlr4ts@0.5.0-alpha.4: + resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + + array-back@4.0.2: + resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==} + engines: {node: '>=8'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array-uniq@1.0.3: + resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} + engines: {node: '>=0.10.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + ast-parents@0.0.1: + resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async@1.5.2: + resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axios@0.21.4: + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + + axios@1.7.3: + resolution: {integrity: sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-x@3.0.10: + resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bech32@1.1.4: + resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} + + bigint-buffer@1.1.5: + resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} + engines: {node: '>= 10.0.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + blakejs@1.2.1: + resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} + + bn.js@4.11.6: + resolution: {integrity: sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==} + + bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + + bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + + bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + + bs58check@2.1.2: + resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + cbor@8.1.0: + resolution: {integrity: sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==} + engines: {node: '>=12.19'} + + chai-as-promised@7.1.2: + resolution: {integrity: sha512-aBDHZxRzYnUYuIAIPBH2s511DjlKPzXNlXSGFC8CwmroWQLfrW0LtE1nK3MAwwNhJPa9raEjNCmRoFpG0Hurdw==} + peerDependencies: + chai: '>= 2.1.2 < 6' + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + cipher-base@1.0.4: + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + cli-table3@0.5.1: + resolution: {integrity: sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==} + engines: {node: '>=6'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + + command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + + command-line-usage@6.1.3: + resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==} + engines: {node: '>=8.0.0'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + death@1.1.0: + resolution: {integrity: sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==} + + debug@4.3.6: + resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + difflib@0.2.4: + resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + + elliptic@6.5.6: + resolution: {integrity: sha512-mpzdtpeCLuS3BmE3pO3Cpp5bbjlOPY2Q0PgoF+Od1XZrHLYI28Xe3ossCmYCQt11FQKEYd9+PF8jymTvtWJSHQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@1.8.1: + resolution: {integrity: sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==} + engines: {node: '>=0.12.0'} + hasBin: true + + eslint-config-prettier@8.10.0: + resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.9.0: + resolution: {integrity: sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@2.7.3: + resolution: {integrity: sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==} + engines: {node: '>=0.10.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@1.9.3: + resolution: {integrity: sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==} + engines: {node: '>=0.10.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eth-gas-reporter@0.2.27: + resolution: {integrity: sha512-femhvoAM7wL0GcI8ozTdxfuBtBFJ9qsyIAsmKVjlWAHUbdnnXHt+lKzz/kmldM5lA9jLuNHGwuIxorNpLbR1Zw==} + peerDependencies: + '@codechecks/client': ^0.1.0 + peerDependenciesMeta: + '@codechecks/client': + optional: true + + ethereum-bloom-filters@1.2.0: + resolution: {integrity: sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==} + + ethereum-cryptography@0.1.3: + resolution: {integrity: sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==} + + ethereum-cryptography@1.2.0: + resolution: {integrity: sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==} + + ethereum-cryptography@2.2.1: + resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + + ethereumjs-abi@0.6.8: + resolution: {integrity: sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==} + + ethereumjs-util@6.2.1: + resolution: {integrity: sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==} + + ethereumjs-util@7.1.5: + resolution: {integrity: sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==} + engines: {node: '>=10.0.0'} + + ethers@5.7.2: + resolution: {integrity: sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==} + + ethers@6.13.2: + resolution: {integrity: sha512-9VkriTTed+/27BGuY1s0hf441kqwHJ1wtN2edksEtiRvXx+soxRX3iSXTfFqq2+YwrOqbDoTHjIhQnjJRlzKmg==} + engines: {node: '>=14.0.0'} + + ethjs-unit@0.1.6: + resolution: {integrity: sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==} + engines: {node: '>=6.5.0', npm: '>=3'} + + ethjs-util@0.1.6: + resolution: {integrity: sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==} + engines: {node: '>=6.5.0', npm: '>=3'} + + evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.0.1: + resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fhevm@0.5.9: + resolution: {integrity: sha512-9cSfmAa4AJUuROd4kESvtNYJpnea1PUvjU7yaNnrJsaHqPkVGQvpnIDEnO4KTsJ36Etup1ksPs8PvyGNYwkatQ==} + engines: {node: '>=20.0.0'} + + fhevmjs@0.5.2: + resolution: {integrity: sha512-p/p4VTuUUohhGLZ8UgWUnO0rbuw5VummB4fAovUyILeyJ0oBVpGKKwkoaBXNKv40HDo+mvpkDBi6+HdSsMHvNg==} + engines: {node: '>=20'} + hasBin: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fmix@0.1.0: + resolution: {integrity: sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==} + + follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + form-data@2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + fp-ts@1.19.3: + resolution: {integrity: sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-readdir-recursive@1.1.0: + resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-port@3.2.0: + resolution: {integrity: sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==} + engines: {node: '>=4'} + + ghost-testrpc@0.0.2: + resolution: {integrity: sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ==} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@5.0.15: + resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.9.0: + resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} + engines: {node: '>=18'} + + globby@10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + hardhat-deploy@0.12.4: + resolution: {integrity: sha512-bYO8DIyeGxZWlhnMoCBon9HNZb6ji0jQn7ngP1t5UmGhC8rQYhji7B73qETMOFhzt5ECZPr+U52duj3nubsqdQ==} + + hardhat-gas-reporter@1.0.10: + resolution: {integrity: sha512-02N4+So/fZrzJ88ci54GqwVA3Zrf0C9duuTyGt0CFRIh/CdNwbnTgkXkRfojOMLBQ+6t+lBIkgbsOtqMvNwikA==} + peerDependencies: + hardhat: ^2.0.2 + + hardhat-ignore-warnings@0.2.11: + resolution: {integrity: sha512-+nHnRbP6COFZaXE7HAY7TZNE3au5vHe5dkcnyq0XaP07ikT2fJ3NhFY0vn7Deh4Qbz0Z/9Xpnj2ki6Ktgk61pg==} + + hardhat-preprocessor@0.1.5: + resolution: {integrity: sha512-j8m44mmPxpxAAd0G8fPHRHOas/INZdzptSur0TNJvMEGcFdLDhbHHxBcqZVQ/bmiW42q4gC60AP4CXn9EF018g==} + peerDependencies: + hardhat: ^2.0.5 + + hardhat@2.22.8: + resolution: {integrity: sha512-hPh2feBGRswkXkoXUFW6NbxgiYtEzp/3uvVFjYROy6fA9LH8BobUyxStlyhSKj4+v1Y23ZoUBOVWL84IcLACrA==} + hasBin: true + peerDependencies: + ts-node: '*' + typescript: '*' + peerDependenciesMeta: + ts-node: + optional: true + typescript: + optional: true + + has-flag@1.0.0: + resolution: {integrity: sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==} + engines: {node: '>=0.10.0'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hash-base@3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + + hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + + http-basic@8.1.3: + resolution: {integrity: sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==} + engines: {node: '>=6.0.0'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-response-object@3.0.2: + resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + imul@1.0.1: + resolution: {integrity: sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA==} + engines: {node: '>=0.10.0'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + io-ts@1.10.4: + resolution: {integrity: sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==} + + is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.15.0: + resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hex-prefixed@1.0.0: + resolution: {integrity: sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==} + engines: {node: '>=6.5.0', npm: '>=3'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonschema@1.4.1: + resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==} + + keccak@3.0.4: + resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} + engines: {node: '>=10.0.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru_map@0.3.3: + resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + markdown-table@1.1.3: + resolution: {integrity: sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==} + + match-all@1.2.6: + resolution: {integrity: sha512-0EESkXiTkWzrQQntBu2uzKvLu6vVkUGz40nGPbSZuegcfE5UuSzNjLaIu76zJWuaT/2I3Z/8M06OlUOZLGwLlQ==} + + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micro-ftch@0.3.1: + resolution: {integrity: sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mnemonist@0.38.5: + resolution: {integrity: sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==} + + mocha@10.7.3: + resolution: {integrity: sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==} + engines: {node: '>= 14.0.0'} + hasBin: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + murmur-128@0.2.1: + resolution: {integrity: sha512-WseEgiRkI6aMFBbj8Cg9yBj/y+OdipwVC7zUo3W2W1JAJITwouUOtpqsmGSg67EQmwwSyod7hsVsWY5LsrfQVg==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-addon-api@2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.1: + resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} + hasBin: true + + node-interval-tree@2.1.2: + resolution: {integrity: sha512-bJ9zMDuNGzVQg1xv0bCPzyEDxHgbrx7/xGj6CDokvizZZmastPsOh0JJLuY8wA5q2SfX1TLNMk7XNV8WxbGxzA==} + engines: {node: '>= 14.0.0'} + + node-tfhe@0.6.4: + resolution: {integrity: sha512-V/SJjc5GPKIB/KcNGEFSDDQelIKvFIFiCgefeDARgxOnN69fyB+omV4e4j3tMDEz6aN7xRd/NqqQ0im17emXeA==} + + nofilter@3.1.0: + resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} + engines: {node: '>=12.19'} + + nopt@3.0.6: + resolution: {integrity: sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + number-to-bn@1.7.0: + resolution: {integrity: sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==} + engines: {node: '>=6.5.0', npm: '>=3'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + + obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ordinal@1.0.3: + resolution: {integrity: sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-cache-control@1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + pbkdf2@3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier-plugin-solidity@1.3.1: + resolution: {integrity: sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA==} + engines: {node: '>=16'} + peerDependencies: + prettier: '>=2.3.0' + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + recursive-readdir@2.2.3: + resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} + engines: {node: '>=6.0.0'} + + reduce-flatten@2.0.0: + resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} + engines: {node: '>=6'} + + req-cwd@2.0.0: + resolution: {integrity: sha512-ueoIoLo1OfB6b05COxAA9UpeoscNpYyM+BqYlA7H6LVF4hKGPXQQSSaD2YmvDVJMkk4UDpAHIeU1zG53IqjvlQ==} + engines: {node: '>=4'} + + req-from@2.0.0: + resolution: {integrity: sha512-LzTfEVDVQHBRfjOUMgNBA+V6DWsSnoeKzf42J7l0xa/B4jyPOuuF5MlNSmomLNGemWTnV2TIdjSSLnEn95fOQA==} + engines: {node: '>=4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.1.7: + resolution: {integrity: sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==} + + resolve@1.17.0: + resolution: {integrity: sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@4.4.1: + resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} + engines: {node: '>=14'} + hasBin: true + + ripemd160@2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + + rlp@2.2.7: + resolution: {integrity: sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sc-istanbul@0.4.6: + resolution: {integrity: sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g==} + hasBin: true + + scrypt-js@3.0.1: + resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} + + secp256k1@4.0.3: + resolution: {integrity: sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==} + engines: {node: '>=10.0.0'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + + sha1@1.1.1: + resolution: {integrity: sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==} + + sha3@2.1.4: + resolution: {integrity: sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + solc@0.8.26: + resolution: {integrity: sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g==} + engines: {node: '>=10.0.0'} + hasBin: true + + solhint-plugin-prettier@0.0.5: + resolution: {integrity: sha512-7jmWcnVshIrO2FFinIvDQmhQpfpS2rRRn3RejiYgnjIE68xO2bvrYvjqVNfrio4xH9ghOqn83tKuTzLjEbmGIA==} + peerDependencies: + prettier: ^1.15.0 || ^2.0.0 + prettier-plugin-solidity: ^1.0.0-alpha.14 + + solhint@3.6.2: + resolution: {integrity: sha512-85EeLbmkcPwD+3JR7aEMKsVC9YrRSxd4qkXuMzrlf7+z2Eqdfm1wHWq1ffTuo5aDhoZxp2I9yF3QkxZOxOL7aQ==} + hasBin: true + + solidity-comments-darwin-arm64@0.0.2: + resolution: {integrity: sha512-HidWkVLSh7v+Vu0CA7oI21GWP/ZY7ro8g8OmIxE8oTqyMwgMbE8F1yc58Sj682Hj199HCZsjmtn1BE4PCbLiGA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + solidity-comments-darwin-x64@0.0.2: + resolution: {integrity: sha512-Zjs0Ruz6faBTPT6fBecUt6qh4CdloT8Bwoc0+qxRoTn9UhYscmbPQkUgQEbS0FQPysYqVzzxJB4h1Ofbf4wwtA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + solidity-comments-extractor@0.0.8: + resolution: {integrity: sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g==} + + solidity-comments-freebsd-x64@0.0.2: + resolution: {integrity: sha512-8Qe4mpjuAxFSwZJVk7B8gAoLCdbtS412bQzBwk63L8dmlHogvE39iT70aAk3RHUddAppT5RMBunlPUCFYJ3ZTw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + solidity-comments-linux-arm64-gnu@0.0.2: + resolution: {integrity: sha512-spkb0MZZnmrP+Wtq4UxP+nyPAVRe82idOjqndolcNR0S9Xvu4ebwq+LvF4HiUgjTDmeiqYiFZQ8T9KGdLSIoIg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + solidity-comments-linux-arm64-musl@0.0.2: + resolution: {integrity: sha512-guCDbHArcjE+JDXYkxx5RZzY1YF6OnAKCo+sTC5fstyW/KGKaQJNPyBNWuwYsQiaEHpvhW1ha537IvlGek8GqA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + solidity-comments-linux-x64-gnu@0.0.2: + resolution: {integrity: sha512-zIqLehBK/g7tvrFmQljrfZXfkEeLt2v6wbe+uFu6kH/qAHZa7ybt8Vc0wYcmjo2U0PeBm15d79ee3AkwbIjFdQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + solidity-comments-linux-x64-musl@0.0.2: + resolution: {integrity: sha512-R9FeDloVlFGTaVkOlELDVC7+1Tjx5WBPI5L8r0AGOPHK3+jOcRh6sKYpI+VskSPDc3vOO46INkpDgUXrKydlIw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + solidity-comments-win32-arm64-msvc@0.0.2: + resolution: {integrity: sha512-QnWJoCQcJj+rnutULOihN9bixOtYWDdF5Rfz9fpHejL1BtNjdLW1om55XNVHGAHPqBxV4aeQQ6OirKnp9zKsug==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + solidity-comments-win32-ia32-msvc@0.0.2: + resolution: {integrity: sha512-vUg4nADtm/NcOtlIymG23NWJUSuMsvX15nU7ynhGBsdKtt8xhdP3C/zA6vjDk8Jg+FXGQL6IHVQ++g/7rSQi0w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + solidity-comments-win32-x64-msvc@0.0.2: + resolution: {integrity: sha512-36j+KUF4V/y0t3qatHm/LF5sCUCBx2UndxE1kq5bOzh/s+nQgatuyB+Pd5BfuPQHdWu2KaExYe20FlAa6NL7+Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + solidity-comments@0.0.2: + resolution: {integrity: sha512-G+aK6qtyUfkn1guS8uzqUeua1dURwPlcOjoTYW/TwmXAcE7z/1+oGCfZUdMSe4ZMKklNbVZNiG5ibnF8gkkFfw==} + engines: {node: '>= 12'} + + solidity-coverage@0.8.12: + resolution: {integrity: sha512-8cOB1PtjnjFRqOgwFiD8DaUsYJtVJ6+YdXQtSZDrLGf8cdhhh8xzTtGzVTGeBf15kTv0v7lYPJlV/az7zLEPJw==} + hasBin: true + peerDependencies: + hardhat: ^2.11.0 + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.2.0: + resolution: {integrity: sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==} + engines: {node: '>=0.8.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stacktrace-parser@0.1.10: + resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} + engines: {node: '>=6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + string-format@2.0.0: + resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==} + + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-hex-prefix@1.0.0: + resolution: {integrity: sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==} + engines: {node: '>=6.5.0', npm: '>=3'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@3.2.3: + resolution: {integrity: sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==} + engines: {node: '>=0.8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + sync-request@6.1.0: + resolution: {integrity: sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==} + engines: {node: '>=8.0.0'} + + sync-rpc@1.3.6: + resolution: {integrity: sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==} + + table-layout@1.0.2: + resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} + engines: {node: '>=8.0.0'} + + table@6.8.2: + resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==} + engines: {node: '>=10.0.0'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + tfhe@0.6.4: + resolution: {integrity: sha512-80fP2iJJQXrlhyC81u6/aUWMMMDoOXmlY5uqKka2CEd222xM96+z9+FTZc0MjlrR3U+NHja1WOte+A3nlvZkKw==} + + then-request@6.0.2: + resolution: {integrity: sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==} + engines: {node: '>=6.0.0'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-api-utils@1.3.0: + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-command-line-args@2.5.1: + resolution: {integrity: sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==} + hasBin: true + + ts-essentials@1.0.4: + resolution: {integrity: sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ==} + + ts-essentials@7.0.3: + resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==} + peerDependencies: + typescript: '>=3.7.0' + + ts-generator@0.1.1: + resolution: {integrity: sha512-N+ahhZxTLYu1HNTQetwWcx3so8hcYbkKBHTr4b4/YgObFTIKkOSSsaa+nal12w8mfrJAyzJfETXawbNjSfP2gQ==} + hasBin: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.4.0: + resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} + + tsort@0.0.1: + resolution: {integrity: sha512-Tyrf5mxF8Ofs1tNoxA13lFeZ2Zrbd6cKbuH3V+MQ5sb6DtBj5FjrXVsRWT8YvNAQTqNoz66dz1WsbigI22aEnw==} + + tweetnacl-util@0.15.1: + resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} + + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + typechain@8.3.2: + resolution: {integrity: sha512-x/sQYr5w9K7yv3es7jo4KTX05CLxOf7TRWwoHlrjRh8H82G64g+k7VuWPJlgMo6qrjfCulOdfBjiaDtmhFYD/Q==} + hasBin: true + peerDependencies: + typescript: '>=4.3.0' + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.0.1: + resolution: {integrity: sha512-V3Y+MdfhawxEjE16dWpb7/IOgeXnLwAEEkS7v8oDqNcR1oYlqWhGH/iHqHdKVdpWme1VPZ0SoywXAkCqawj2eQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + + typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + + typical@5.2.0: + resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==} + engines: {node: '>=8'} + + uglify-js@3.19.2: + resolution: {integrity: sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + web3-errors@1.2.1: + resolution: {integrity: sha512-dIsi8SFC9TCAWpPmacXeVMk/F8tDNa1Bvg8/Cc2cvJo8LRSWd099szEyb+/SiMYcLlEbwftiT9Rpukz7ql4hBg==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-types@1.7.0: + resolution: {integrity: sha512-nhXxDJ7a5FesRw9UG5SZdP/C/3Q2EzHGnB39hkAV+YGXDMgwxBXFWebQLfEzZzuArfHnvC0sQqkIHNwSKcVjdA==} + engines: {node: '>=14', npm: '>=6.12.0'} + + web3-utils@1.10.4: + resolution: {integrity: sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==} + engines: {node: '>=8.0.0'} + + web3-validator@2.0.6: + resolution: {integrity: sha512-qn9id0/l1bWmvH4XfnG/JtGKKwut2Vokl6YXP5Kfg424npysmtRLe9DgiNBM9Op7QL/aSiaA0TVXibuIuWcizg==} + engines: {node: '>=14', npm: '>=6.12.0'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wordwrapjs@4.0.1: + resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} + engines: {node: '>=8.0.0'} + + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.4.6: + resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zksync-ethers@5.9.2: + resolution: {integrity: sha512-Y2Mx6ovvxO6UdC2dePLguVzvNToOY8iLWeq5ne+jgGSJxAi/f4He/NF6FNsf6x1aWX0o8dy4Df8RcOQXAkj5qw==} + engines: {node: '>=16.0.0'} + peerDependencies: + ethers: ~5.7.0 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + +snapshots: + + '@adraffy/ens-normalize@1.10.1': {} + + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + + '@babel/generator@7.17.7': + dependencies: + '@babel/types': 7.17.0 + jsesc: 2.5.2 + source-map: 0.5.7 + + '@babel/generator@7.25.0': + dependencies: + '@babel/types': 7.25.2 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + '@babel/helper-environment-visitor@7.24.7': + dependencies: + '@babel/types': 7.25.2 + + '@babel/helper-function-name@7.24.7': + dependencies: + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 + + '@babel/helper-hoist-variables@7.24.7': + dependencies: + '@babel/types': 7.25.2 + + '@babel/helper-split-export-declaration@7.24.7': + dependencies: + '@babel/types': 7.25.2 + + '@babel/helper-string-parser@7.24.8': {} + + '@babel/helper-validator-identifier@7.24.7': {} + + '@babel/highlight@7.24.7': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + + '@babel/parser@7.25.3': + dependencies: + '@babel/types': 7.25.2 + + '@babel/template@7.25.0': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 + + '@babel/traverse@7.23.2': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.25.0 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.25.3 + '@babel/types': 7.25.2 + debug: 4.3.6(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.17.0': + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@babel/types@7.25.2': + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@eslint-community/eslint-utils@4.4.0(eslint@9.9.0)': + dependencies: + eslint: 9.9.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} + + '@eslint/config-array@0.17.1': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.6(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.6(supports-color@8.1.1) + espree: 10.1.0 + globals: 14.0.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.9.0': {} + + '@eslint/object-schema@2.1.4': {} + + '@ethereumjs/rlp@4.0.1': {} + + '@ethereumjs/util@8.1.0': + dependencies: + '@ethereumjs/rlp': 4.0.1 + ethereum-cryptography: 2.2.1 + micro-ftch: 0.3.1 + + '@ethersproject/abi@5.7.0': + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@ethersproject/abstract-provider@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/properties': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/web': 5.7.1 + + '@ethersproject/abstract-signer@5.7.0': + dependencies: + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + + '@ethersproject/address@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/rlp': 5.7.0 + + '@ethersproject/base64@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + + '@ethersproject/basex@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/properties': 5.7.0 + + '@ethersproject/bignumber@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + bn.js: 5.2.1 + + '@ethersproject/bytes@5.7.0': + dependencies: + '@ethersproject/logger': 5.7.0 + + '@ethersproject/constants@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + + '@ethersproject/contracts@5.7.0': + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/transactions': 5.7.0 + + '@ethersproject/hash@5.7.0': + dependencies: + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@ethersproject/hdnode@5.7.0': + dependencies: + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wordlists': 5.7.0 + + '@ethersproject/json-wallets@5.7.0': + dependencies: + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + aes-js: 3.0.0 + scrypt-js: 3.0.1 + + '@ethersproject/keccak256@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + js-sha3: 0.8.0 + + '@ethersproject/logger@5.7.0': {} + + '@ethersproject/networks@5.7.1': + dependencies: + '@ethersproject/logger': 5.7.0 + + '@ethersproject/pbkdf2@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/sha2': 5.7.0 + + '@ethersproject/properties@5.7.0': + dependencies: + '@ethersproject/logger': 5.7.0 + + '@ethersproject/providers@5.7.2': + dependencies: + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/web': 5.7.1 + bech32: 1.1.4 + ws: 7.4.6 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@ethersproject/random@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + + '@ethersproject/rlp@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + + '@ethersproject/sha2@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + hash.js: 1.1.7 + + '@ethersproject/signing-key@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + bn.js: 5.2.1 + elliptic: 6.5.4 + hash.js: 1.1.7 + + '@ethersproject/solidity@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@ethersproject/strings@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + + '@ethersproject/transactions@5.7.0': + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + + '@ethersproject/units@5.7.0': + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + + '@ethersproject/wallet@5.7.0': + dependencies: + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/json-wallets': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wordlists': 5.7.0 + + '@ethersproject/web@5.7.1': + dependencies: + '@ethersproject/base64': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@ethersproject/wordlists@5.7.0': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + + '@fastify/busboy@2.1.1': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.0': {} + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@metamask/eth-sig-util@4.0.1': + dependencies: + ethereumjs-abi: 0.6.8 + ethereumjs-util: 6.2.1 + ethjs-util: 0.1.6 + tweetnacl: 1.0.3 + tweetnacl-util: 0.15.1 + + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + + '@noble/curves@1.4.2': + dependencies: + '@noble/hashes': 1.4.0 + + '@noble/hashes@1.2.0': {} + + '@noble/hashes@1.3.2': {} + + '@noble/hashes@1.4.0': {} + + '@noble/secp256k1@1.7.1': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@nomicfoundation/edr-darwin-arm64@0.5.2': {} + + '@nomicfoundation/edr-darwin-x64@0.5.2': {} + + '@nomicfoundation/edr-linux-arm64-gnu@0.5.2': {} + + '@nomicfoundation/edr-linux-arm64-musl@0.5.2': {} + + '@nomicfoundation/edr-linux-x64-gnu@0.5.2': {} + + '@nomicfoundation/edr-linux-x64-musl@0.5.2': {} + + '@nomicfoundation/edr-win32-x64-msvc@0.5.2': {} + + '@nomicfoundation/edr@0.5.2': + dependencies: + '@nomicfoundation/edr-darwin-arm64': 0.5.2 + '@nomicfoundation/edr-darwin-x64': 0.5.2 + '@nomicfoundation/edr-linux-arm64-gnu': 0.5.2 + '@nomicfoundation/edr-linux-arm64-musl': 0.5.2 + '@nomicfoundation/edr-linux-x64-gnu': 0.5.2 + '@nomicfoundation/edr-linux-x64-musl': 0.5.2 + '@nomicfoundation/edr-win32-x64-msvc': 0.5.2 + + '@nomicfoundation/ethereumjs-common@4.0.4': + dependencies: + '@nomicfoundation/ethereumjs-util': 9.0.4 + transitivePeerDependencies: + - c-kzg + + '@nomicfoundation/ethereumjs-rlp@5.0.4': {} + + '@nomicfoundation/ethereumjs-tx@5.0.4': + dependencies: + '@nomicfoundation/ethereumjs-common': 4.0.4 + '@nomicfoundation/ethereumjs-rlp': 5.0.4 + '@nomicfoundation/ethereumjs-util': 9.0.4 + ethereum-cryptography: 0.1.3 + + '@nomicfoundation/ethereumjs-util@9.0.4': + dependencies: + '@nomicfoundation/ethereumjs-rlp': 5.0.4 + ethereum-cryptography: 0.1.3 + + '@nomicfoundation/hardhat-chai-matchers@2.0.7(@nomicfoundation/hardhat-ethers@3.0.6(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)))(chai@4.5.0)(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))': + dependencies: + '@nomicfoundation/hardhat-ethers': 3.0.6(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@types/chai-as-promised': 7.1.8 + chai: 4.5.0 + chai-as-promised: 7.1.2(chai@4.5.0) + deep-eql: 4.1.4 + ethers: 6.13.2 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + ordinal: 1.0.3 + + '@nomicfoundation/hardhat-ethers@3.0.6(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))': + dependencies: + debug: 4.3.6(supports-color@8.1.1) + ethers: 6.13.2 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + lodash.isequal: 4.5.0 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/hardhat-foundry@1.1.2(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))': + dependencies: + chalk: 2.4.2 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + + '@nomicfoundation/hardhat-network-helpers@1.0.11(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))': + dependencies: + ethereumjs-util: 7.1.5 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + + '@nomicfoundation/hardhat-toolbox@3.0.0(geae4jvdfg2eknoy6ku3h4qpgy)': + dependencies: + '@nomicfoundation/hardhat-chai-matchers': 2.0.7(@nomicfoundation/hardhat-ethers@3.0.6(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)))(chai@4.5.0)(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@nomicfoundation/hardhat-ethers': 3.0.6(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@nomicfoundation/hardhat-network-helpers': 1.0.11(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@nomicfoundation/hardhat-verify': 1.1.1(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + '@typechain/ethers-v6': 0.4.3(ethers@6.13.2)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4) + '@typechain/hardhat': 8.0.3(@typechain/ethers-v6@0.4.3(ethers@6.13.2)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4))(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))(typechain@8.3.2(typescript@5.5.4)) + '@types/chai': 4.3.17 + '@types/mocha': 10.0.7 + '@types/node': 18.19.44 + chai: 4.5.0 + ethers: 6.13.2 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + hardhat-gas-reporter: 1.0.10(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + solidity-coverage: 0.8.12(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)) + ts-node: 10.9.2(@types/node@18.19.44)(typescript@5.5.4) + typechain: 8.3.2(typescript@5.5.4) + typescript: 5.5.4 + + '@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))': + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/address': 5.7.0 + cbor: 8.1.0 + chalk: 2.4.2 + debug: 4.3.6(supports-color@8.1.1) + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + lodash.clonedeep: 4.5.0 + semver: 6.3.1 + table: 6.8.2 + undici: 5.28.4 + transitivePeerDependencies: + - supports-color + + '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-darwin-x64@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-arm64-musl@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-x64-gnu@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-linux-x64-musl@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer-win32-x64-msvc@0.1.2': + optional: true + + '@nomicfoundation/solidity-analyzer@0.1.2': + optionalDependencies: + '@nomicfoundation/solidity-analyzer-darwin-arm64': 0.1.2 + '@nomicfoundation/solidity-analyzer-darwin-x64': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-arm64-musl': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-x64-gnu': 0.1.2 + '@nomicfoundation/solidity-analyzer-linux-x64-musl': 0.1.2 + '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.1.2 + + '@openzeppelin/contracts@5.0.2': {} + + '@scure/base@1.1.7': {} + + '@scure/bip32@1.1.5': + dependencies: + '@noble/hashes': 1.2.0 + '@noble/secp256k1': 1.7.1 + '@scure/base': 1.1.7 + + '@scure/bip32@1.4.0': + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + + '@scure/bip39@1.1.1': + dependencies: + '@noble/hashes': 1.2.0 + '@scure/base': 1.1.7 + + '@scure/bip39@1.3.0': + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + + '@sentry/core@5.30.0': + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/minimal': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + + '@sentry/hub@5.30.0': + dependencies: + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + + '@sentry/minimal@5.30.0': + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/types': 5.30.0 + tslib: 1.14.1 + + '@sentry/node@5.30.0': + dependencies: + '@sentry/core': 5.30.0 + '@sentry/hub': 5.30.0 + '@sentry/tracing': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + cookie: 0.4.2 + https-proxy-agent: 5.0.1 + lru_map: 0.3.3 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + + '@sentry/tracing@5.30.0': + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/minimal': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + + '@sentry/types@5.30.0': {} + + '@sentry/utils@5.30.0': + dependencies: + '@sentry/types': 5.30.0 + tslib: 1.14.1 + + '@solidity-parser/parser@0.14.5': + dependencies: + antlr4ts: 0.5.0-alpha.4 + + '@solidity-parser/parser@0.16.2': + dependencies: + antlr4ts: 0.5.0-alpha.4 + + '@solidity-parser/parser@0.17.0': {} + + '@solidity-parser/parser@0.18.0': {} + + '@trivago/prettier-plugin-sort-imports@4.3.0(prettier@2.8.8)': + dependencies: + '@babel/generator': 7.17.7 + '@babel/parser': 7.25.3 + '@babel/traverse': 7.23.2 + '@babel/types': 7.17.0 + javascript-natural-sort: 0.7.1 + lodash: 4.17.21 + prettier: 2.8.8 + transitivePeerDependencies: + - supports-color + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@typechain/ethers-v6@0.4.3(ethers@6.13.2)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4)': + dependencies: + ethers: 6.13.2 + lodash: 4.17.21 + ts-essentials: 7.0.3(typescript@5.5.4) + typechain: 8.3.2(typescript@5.5.4) + typescript: 5.5.4 + + '@typechain/hardhat@8.0.3(@typechain/ethers-v6@0.4.3(ethers@6.13.2)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4))(ethers@6.13.2)(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4))(typechain@8.3.2(typescript@5.5.4))': + dependencies: + '@typechain/ethers-v6': 0.4.3(ethers@6.13.2)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4) + ethers: 6.13.2 + fs-extra: 9.1.0 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + typechain: 8.3.2(typescript@5.5.4) + + '@types/bn.js@4.11.6': + dependencies: + '@types/node': 18.19.44 + + '@types/bn.js@5.1.5': + dependencies: + '@types/node': 18.19.44 + + '@types/chai-as-promised@7.1.8': + dependencies: + '@types/chai': 4.3.17 + + '@types/chai@4.3.17': {} + + '@types/concat-stream@1.6.1': + dependencies: + '@types/node': 18.19.44 + + '@types/eslint@9.6.0': + dependencies: + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 + + '@types/eslint__js@8.42.3': + dependencies: + '@types/eslint': 9.6.0 + + '@types/estree@1.0.5': {} + + '@types/form-data@0.0.33': + dependencies: + '@types/node': 18.19.44 + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 18.19.44 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 18.19.44 + + '@types/json-schema@7.0.15': {} + + '@types/keccak@3.0.4': + dependencies: + '@types/node': 18.19.44 + + '@types/lru-cache@5.1.1': {} + + '@types/minimatch@5.1.2': {} + + '@types/mkdirp@0.5.2': + dependencies: + '@types/node': 18.19.44 + + '@types/mocha@10.0.7': {} + + '@types/node@10.17.60': {} + + '@types/node@18.15.13': {} + + '@types/node@18.19.44': + dependencies: + undici-types: 5.26.5 + + '@types/node@8.10.66': {} + + '@types/pbkdf2@3.1.2': + dependencies: + '@types/node': 18.19.44 + + '@types/prettier@2.7.3': {} + + '@types/qs@6.9.15': {} + + '@types/resolve@0.0.8': + dependencies: + '@types/node': 18.19.44 + + '@types/secp256k1@4.0.6': + dependencies: + '@types/node': 18.19.44 + + '@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.0.1 + '@typescript-eslint/type-utils': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.0.1 + eslint: 9.9.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.0.1 + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/typescript-estree': 8.0.1(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.0.1 + debug: 4.3.6(supports-color@8.1.1) + eslint: 9.9.0 + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.0.1': + dependencies: + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/visitor-keys': 8.0.1 + + '@typescript-eslint/type-utils@8.0.1(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@typescript-eslint/typescript-estree': 8.0.1(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + debug: 4.3.6(supports-color@8.1.1) + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.0.1': {} + + '@typescript-eslint/typescript-estree@8.0.1(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/visitor-keys': 8.0.1 + debug: 4.3.6(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.0.1(eslint@9.9.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) + '@typescript-eslint/scope-manager': 8.0.1 + '@typescript-eslint/types': 8.0.1 + '@typescript-eslint/typescript-estree': 8.0.1(typescript@5.5.4) + eslint: 9.9.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.0.1': + dependencies: + '@typescript-eslint/types': 8.0.1 + eslint-visitor-keys: 3.4.3 + + '@uniswap/permit2-sdk@1.3.0': + dependencies: + ethers: 5.7.2 + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + abbrev@1.0.9: {} + + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn-walk@8.3.3: + dependencies: + acorn: 8.12.1 + + acorn@8.12.1: {} + + adm-zip@0.4.16: {} + + aes-js@3.0.0: {} + + aes-js@4.0.0-beta.5: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.1 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + amdefine@1.0.1: + optional: true + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@3.0.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + antlr4@4.13.2: {} + + antlr4ts@0.5.0-alpha.4: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-back@3.1.0: {} + + array-back@4.0.2: {} + + array-union@2.1.0: {} + + array-uniq@1.0.3: {} + + asap@2.0.6: {} + + assertion-error@1.1.0: {} + + ast-parents@0.0.1: {} + + astral-regex@2.0.0: {} + + async@1.5.2: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + axios@0.21.4(debug@4.3.6): + dependencies: + follow-redirects: 1.15.6(debug@4.3.6) + transitivePeerDependencies: + - debug + + axios@1.7.3: + dependencies: + follow-redirects: 1.15.6(debug@4.3.6) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + base-x@3.0.10: + dependencies: + safe-buffer: 5.2.1 + + base64-js@1.5.1: {} + + bech32@1.1.4: {} + + bigint-buffer@1.1.5: + dependencies: + bindings: 1.5.0 + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + blakejs@1.2.1: {} + + bn.js@4.11.6: {} + + bn.js@4.12.0: {} + + bn.js@5.2.1: {} + + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + brorand@1.1.0: {} + + browser-stdout@1.3.1: {} + + browserify-aes@1.2.0: + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.4 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + bs58@4.0.1: + dependencies: + base-x: 3.0.10 + + bs58check@2.1.2: + dependencies: + bs58: 4.0.1 + create-hash: 1.2.0 + safe-buffer: 5.2.1 + + buffer-from@1.1.2: {} + + buffer-xor@1.0.3: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bytes@3.1.2: {} + + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + callsites@3.1.0: {} + + camelcase@6.3.0: {} + + caseless@0.12.0: {} + + cbor@8.1.0: + dependencies: + nofilter: 3.1.0 + + chai-as-promised@7.1.2(chai@4.5.0): + dependencies: + chai: 4.5.0 + check-error: 1.0.3 + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + charenc@0.0.2: {} + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + ci-info@2.0.0: {} + + cipher-base@1.0.4: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + + clean-stack@2.2.0: {} + + cli-boxes@2.2.1: {} + + cli-table3@0.5.1: + dependencies: + object-assign: 4.1.1 + string-width: 2.1.1 + optionalDependencies: + colors: 1.4.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colors@1.4.0: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + command-exists@1.2.9: {} + + command-line-args@5.2.1: + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + + command-line-usage@6.1.3: + dependencies: + array-back: 4.0.2 + chalk: 2.4.2 + table-layout: 1.0.2 + typical: 5.2.0 + + commander@10.0.1: {} + + commander@11.1.0: {} + + commander@8.3.0: {} + + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + cookie@0.4.2: {} + + core-util-is@1.0.3: {} + + cosmiconfig@8.3.6(typescript@5.5.4): + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.5.4 + + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.4 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.2 + sha.js: 2.4.11 + + create-hmac@1.1.7: + dependencies: + cipher-base: 1.0.4 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + + create-require@1.1.1: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypt@0.0.2: {} + + death@1.1.0: {} + + debug@4.3.6(supports-color@8.1.1): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + diff@4.0.2: {} + + diff@5.2.0: {} + + difflib@0.2.4: + dependencies: + heap: 0.2.7 + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dotenv@16.4.5: {} + + elliptic@6.5.4: + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + elliptic@6.5.6: + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + emoji-regex@8.0.0: {} + + encode-utf8@1.0.3: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + env-paths@2.2.1: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + escalade@3.1.2: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escodegen@1.8.1: + dependencies: + esprima: 2.7.3 + estraverse: 1.9.3 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.2.0 + + eslint-config-prettier@8.10.0(eslint@9.9.0): + dependencies: + eslint: 9.9.0 + + eslint-scope@8.0.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.0.0: {} + + eslint@9.9.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/config-array': 0.17.1 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.9.0 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.6(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 8.0.2 + eslint-visitor-keys: 4.0.0 + espree: 10.1.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@10.1.0: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.0.0 + + esprima@2.7.3: {} + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@1.9.3: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eth-gas-reporter@0.2.27: + dependencies: + '@solidity-parser/parser': 0.14.5 + axios: 1.7.3 + cli-table3: 0.5.1 + colors: 1.4.0 + ethereum-cryptography: 1.2.0 + ethers: 5.7.2 + fs-readdir-recursive: 1.1.0 + lodash: 4.17.21 + markdown-table: 1.1.3 + mocha: 10.7.3 + req-cwd: 2.0.0 + sha1: 1.1.1 + sync-request: 6.1.0 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + ethereum-bloom-filters@1.2.0: + dependencies: + '@noble/hashes': 1.4.0 + + ethereum-cryptography@0.1.3: + dependencies: + '@types/pbkdf2': 3.1.2 + '@types/secp256k1': 4.0.6 + blakejs: 1.2.1 + browserify-aes: 1.2.0 + bs58check: 2.1.2 + create-hash: 1.2.0 + create-hmac: 1.1.7 + hash.js: 1.1.7 + keccak: 3.0.4 + pbkdf2: 3.1.2 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + scrypt-js: 3.0.1 + secp256k1: 4.0.3 + setimmediate: 1.0.5 + + ethereum-cryptography@1.2.0: + dependencies: + '@noble/hashes': 1.2.0 + '@noble/secp256k1': 1.7.1 + '@scure/bip32': 1.1.5 + '@scure/bip39': 1.1.1 + + ethereum-cryptography@2.2.1: + dependencies: + '@noble/curves': 1.4.2 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + + ethereumjs-abi@0.6.8: + dependencies: + bn.js: 4.12.0 + ethereumjs-util: 6.2.1 + + ethereumjs-util@6.2.1: + dependencies: + '@types/bn.js': 4.11.6 + bn.js: 4.12.0 + create-hash: 1.2.0 + elliptic: 6.5.6 + ethereum-cryptography: 0.1.3 + ethjs-util: 0.1.6 + rlp: 2.2.7 + + ethereumjs-util@7.1.5: + dependencies: + '@types/bn.js': 5.1.5 + bn.js: 5.2.1 + create-hash: 1.2.0 + ethereum-cryptography: 0.1.3 + rlp: 2.2.7 + + ethers@5.7.2: + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/contracts': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/json-wallets': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/providers': 5.7.2 + '@ethersproject/random': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/solidity': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/units': 5.7.0 + '@ethersproject/wallet': 5.7.0 + '@ethersproject/web': 5.7.1 + '@ethersproject/wordlists': 5.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ethers@6.13.2: + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 18.15.13 + aes-js: 4.0.0-beta.5 + tslib: 2.4.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ethjs-unit@0.1.6: + dependencies: + bn.js: 4.11.6 + number-to-bn: 1.7.0 + + ethjs-util@0.1.6: + dependencies: + is-hex-prefixed: 1.0.0 + strip-hex-prefix: 1.0.0 + + evp_bytestokey@1.0.3: + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.0.1: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fhevm@0.5.9: + dependencies: + '@openzeppelin/contracts': 5.0.2 + + fhevmjs@0.5.2(encoding@0.1.13): + dependencies: + '@types/keccak': 3.0.4 + bigint-buffer: 1.1.5 + commander: 11.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + node-tfhe: 0.6.4 + sha3: 2.1.4 + tfhe: 0.6.4 + url: 0.11.4 + web3-validator: 2.0.6 + transitivePeerDependencies: + - encoding + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-replace@3.0.0: + dependencies: + array-back: 3.1.0 + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flat@5.0.2: {} + + flatted@3.3.1: {} + + fmix@0.1.0: + dependencies: + imul: 1.0.1 + + follow-redirects@1.15.6(debug@4.3.6): + optionalDependencies: + debug: 4.3.6(supports-color@8.1.1) + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + form-data@2.5.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fp-ts@1.19.3: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-readdir-recursive@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-port@3.2.0: {} + + ghost-testrpc@0.0.2: + dependencies: + chalk: 2.4.2 + node-emoji: 1.11.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@5.0.15: + dependencies: + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.1.7: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.2.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.4 + minipass: 4.2.8 + path-scurry: 1.11.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.9.0: {} + + globby@10.0.2: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + glob: 7.2.3 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.2 + + hardhat-deploy@0.12.4: + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/contracts': 5.7.0 + '@ethersproject/providers': 5.7.2 + '@ethersproject/solidity': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wallet': 5.7.0 + '@types/qs': 6.9.15 + axios: 0.21.4(debug@4.3.6) + chalk: 4.1.2 + chokidar: 3.6.0 + debug: 4.3.6(supports-color@8.1.1) + enquirer: 2.4.1 + ethers: 5.7.2 + form-data: 4.0.0 + fs-extra: 10.1.0 + match-all: 1.2.6 + murmur-128: 0.2.1 + qs: 6.13.0 + zksync-ethers: 5.9.2(ethers@5.7.2) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + hardhat-gas-reporter@1.0.10(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)): + dependencies: + array-uniq: 1.0.3 + eth-gas-reporter: 0.2.27 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + sha1: 1.1.1 + transitivePeerDependencies: + - '@codechecks/client' + - bufferutil + - debug + - utf-8-validate + + hardhat-ignore-warnings@0.2.11: + dependencies: + minimatch: 5.1.6 + node-interval-tree: 2.1.2 + solidity-comments: 0.0.2 + + hardhat-preprocessor@0.1.5(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)): + dependencies: + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + murmur-128: 0.2.1 + + hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4): + dependencies: + '@ethersproject/abi': 5.7.0 + '@metamask/eth-sig-util': 4.0.1 + '@nomicfoundation/edr': 0.5.2 + '@nomicfoundation/ethereumjs-common': 4.0.4 + '@nomicfoundation/ethereumjs-tx': 5.0.4 + '@nomicfoundation/ethereumjs-util': 9.0.4 + '@nomicfoundation/solidity-analyzer': 0.1.2 + '@sentry/node': 5.30.0 + '@types/bn.js': 5.1.5 + '@types/lru-cache': 5.1.1 + adm-zip: 0.4.16 + aggregate-error: 3.1.0 + ansi-escapes: 4.3.2 + boxen: 5.1.2 + chalk: 2.4.2 + chokidar: 3.6.0 + ci-info: 2.0.0 + debug: 4.3.6(supports-color@8.1.1) + enquirer: 2.4.1 + env-paths: 2.2.1 + ethereum-cryptography: 1.2.0 + ethereumjs-abi: 0.6.8 + find-up: 2.1.0 + fp-ts: 1.19.3 + fs-extra: 7.0.1 + glob: 7.2.0 + immutable: 4.3.7 + io-ts: 1.10.4 + keccak: 3.0.4 + lodash: 4.17.21 + mnemonist: 0.38.5 + mocha: 10.7.3 + p-map: 4.0.0 + raw-body: 2.5.2 + resolve: 1.17.0 + semver: 6.3.1 + solc: 0.8.26(debug@4.3.6) + source-map-support: 0.5.21 + stacktrace-parser: 0.1.10 + tsort: 0.0.1 + undici: 5.28.4 + uuid: 8.3.2 + ws: 7.5.10 + optionalDependencies: + ts-node: 10.9.2(@types/node@18.19.44)(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - bufferutil + - c-kzg + - supports-color + - utf-8-validate + + has-flag@1.0.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + hash-base@3.1.0: + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + heap@0.2.7: {} + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + http-basic@8.1.3: + dependencies: + caseless: 0.12.0 + concat-stream: 1.6.2 + http-response-object: 3.0.2 + parse-cache-control: 1.0.1 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-response-object@3.0.2: + dependencies: + '@types/node': 10.17.60 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.6(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: {} + + ignore@5.3.1: {} + + immutable@4.3.7: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imul@1.0.1: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + interpret@1.4.0: {} + + io-ts@1.10.4: + dependencies: + fp-ts: 1.19.3 + + is-arguments@1.1.1: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.15.0: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hex-prefixed@1.0.0: {} + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@2.1.0: {} + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-unicode-supported@0.1.0: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + javascript-natural-sort@0.7.1: {} + + js-sha3@0.8.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@2.5.2: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonschema@1.4.1: {} + + keccak@3.0.4: + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.8.1 + readable-stream: 3.6.2 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + levn@0.3.0: + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + + lodash.isequal@4.5.0: {} + + lodash.merge@4.6.2: {} + + lodash.truncate@4.4.2: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + lru-cache@10.4.3: {} + + lru_map@0.3.3: {} + + make-error@1.3.6: {} + + markdown-table@1.1.3: {} + + match-all@1.2.6: {} + + md5.js@1.3.5: + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + memorystream@0.3.1: {} + + merge2@1.4.1: {} + + micro-ftch@0.3.1: {} + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@8.0.4: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass@4.2.8: {} + + minipass@7.1.2: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mnemonist@0.38.5: + dependencies: + obliterator: 2.0.4 + + mocha@10.7.3: + dependencies: + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.6.0 + debug: 4.3.6(supports-color@8.1.1) + diff: 5.2.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.1.6 + ms: 2.1.3 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + yargs-unparser: 2.0.0 + + ms@2.1.2: {} + + ms@2.1.3: {} + + murmur-128@0.2.1: + dependencies: + encode-utf8: 1.0.3 + fmix: 0.1.0 + imul: 1.0.1 + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-addon-api@2.0.2: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.17.21 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-gyp-build@4.8.1: {} + + node-interval-tree@2.1.2: + dependencies: + shallowequal: 1.1.0 + + node-tfhe@0.6.4: {} + + nofilter@3.1.0: {} + + nopt@3.0.6: + dependencies: + abbrev: 1.0.9 + + normalize-path@3.0.0: {} + + number-to-bn@1.7.0: + dependencies: + bn.js: 4.11.6 + strip-hex-prefix: 1.0.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.2: {} + + obliterator@2.0.4: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.8.3: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ordinal@1.0.3: {} + + os-tmpdir@1.0.2: {} + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-try@1.0.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-cache-control@1.0.1: {} + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.24.7 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + pathval@1.1.1: {} + + pbkdf2@3.1.2: + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + + picocolors@1.0.1: {} + + picomatch@2.3.1: {} + + pify@4.0.1: {} + + pluralize@8.0.0: {} + + possible-typed-array-names@1.0.0: {} + + prelude-ls@1.1.2: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier-plugin-solidity@1.3.1(prettier@2.8.8): + dependencies: + '@solidity-parser/parser': 0.17.0 + prettier: 2.8.8 + semver: 7.6.3 + solidity-comments-extractor: 0.0.8 + + prettier@2.8.8: {} + + process-nextick-args@2.0.1: {} + + promise@8.3.0: + dependencies: + asap: 2.0.6 + + proxy-from-env@1.1.0: {} + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + + queue-microtask@1.2.3: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + rechoir@0.6.2: + dependencies: + resolve: 1.22.8 + + recursive-readdir@2.2.3: + dependencies: + minimatch: 3.1.2 + + reduce-flatten@2.0.0: {} + + req-cwd@2.0.0: + dependencies: + req-from: 2.0.0 + + req-from@2.0.0: + dependencies: + resolve-from: 3.0.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@3.0.0: {} + + resolve-from@4.0.0: {} + + resolve@1.1.7: {} + + resolve@1.17.0: + dependencies: + path-parse: 1.0.7 + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.0.4: {} + + rimraf@4.4.1: + dependencies: + glob: 9.3.5 + + ripemd160@2.0.2: + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + + rlp@2.2.7: + dependencies: + bn.js: 5.2.1 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sc-istanbul@0.4.6: + dependencies: + abbrev: 1.0.9 + async: 1.5.2 + escodegen: 1.8.1 + esprima: 2.7.3 + glob: 5.0.15 + handlebars: 4.7.8 + js-yaml: 3.14.1 + mkdirp: 0.5.6 + nopt: 3.0.6 + once: 1.4.0 + resolve: 1.1.7 + supports-color: 3.2.3 + which: 1.3.1 + wordwrap: 1.0.0 + + scrypt-js@3.0.1: {} + + secp256k1@4.0.3: + dependencies: + elliptic: 6.5.6 + node-addon-api: 2.0.2 + node-gyp-build: 4.8.1 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.6.3: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + sha.js@2.4.11: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + + sha1@1.1.1: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + + sha3@2.1.4: + dependencies: + buffer: 6.0.3 + + shallowequal@1.1.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + + slash@3.0.0: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + solc@0.8.26(debug@4.3.6): + dependencies: + command-exists: 1.2.9 + commander: 8.3.0 + follow-redirects: 1.15.6(debug@4.3.6) + js-sha3: 0.8.0 + memorystream: 0.3.1 + semver: 5.7.2 + tmp: 0.0.33 + transitivePeerDependencies: + - debug + + solhint-plugin-prettier@0.0.5(prettier-plugin-solidity@1.3.1(prettier@2.8.8))(prettier@2.8.8): + dependencies: + prettier: 2.8.8 + prettier-linter-helpers: 1.0.0 + prettier-plugin-solidity: 1.3.1(prettier@2.8.8) + + solhint@3.6.2(typescript@5.5.4): + dependencies: + '@solidity-parser/parser': 0.16.2 + ajv: 6.12.6 + antlr4: 4.13.2 + ast-parents: 0.0.1 + chalk: 4.1.2 + commander: 10.0.1 + cosmiconfig: 8.3.6(typescript@5.5.4) + fast-diff: 1.3.0 + glob: 8.1.0 + ignore: 5.3.1 + js-yaml: 4.1.0 + lodash: 4.17.21 + pluralize: 8.0.0 + semver: 7.6.3 + strip-ansi: 6.0.1 + table: 6.8.2 + text-table: 0.2.0 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - typescript + + solidity-comments-darwin-arm64@0.0.2: + optional: true + + solidity-comments-darwin-x64@0.0.2: + optional: true + + solidity-comments-extractor@0.0.8: {} + + solidity-comments-freebsd-x64@0.0.2: + optional: true + + solidity-comments-linux-arm64-gnu@0.0.2: + optional: true + + solidity-comments-linux-arm64-musl@0.0.2: + optional: true + + solidity-comments-linux-x64-gnu@0.0.2: + optional: true + + solidity-comments-linux-x64-musl@0.0.2: + optional: true + + solidity-comments-win32-arm64-msvc@0.0.2: + optional: true + + solidity-comments-win32-ia32-msvc@0.0.2: + optional: true + + solidity-comments-win32-x64-msvc@0.0.2: + optional: true + + solidity-comments@0.0.2: + optionalDependencies: + solidity-comments-darwin-arm64: 0.0.2 + solidity-comments-darwin-x64: 0.0.2 + solidity-comments-freebsd-x64: 0.0.2 + solidity-comments-linux-arm64-gnu: 0.0.2 + solidity-comments-linux-arm64-musl: 0.0.2 + solidity-comments-linux-x64-gnu: 0.0.2 + solidity-comments-linux-x64-musl: 0.0.2 + solidity-comments-win32-arm64-msvc: 0.0.2 + solidity-comments-win32-ia32-msvc: 0.0.2 + solidity-comments-win32-x64-msvc: 0.0.2 + + solidity-coverage@0.8.12(hardhat@2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4)): + dependencies: + '@ethersproject/abi': 5.7.0 + '@solidity-parser/parser': 0.18.0 + chalk: 2.4.2 + death: 1.1.0 + difflib: 0.2.4 + fs-extra: 8.1.0 + ghost-testrpc: 0.0.2 + global-modules: 2.0.0 + globby: 10.0.2 + hardhat: 2.22.8(ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4))(typescript@5.5.4) + jsonschema: 1.4.1 + lodash: 4.17.21 + mocha: 10.7.3 + node-emoji: 1.11.0 + pify: 4.0.1 + recursive-readdir: 2.2.3 + sc-istanbul: 0.4.6 + semver: 7.6.3 + shelljs: 0.8.5 + web3-utils: 1.10.4 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.2.0: + dependencies: + amdefine: 1.0.1 + optional: true + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + + stacktrace-parser@0.1.10: + dependencies: + type-fest: 0.7.1 + + statuses@2.0.1: {} + + string-format@2.0.0: {} + + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-hex-prefix@1.0.0: + dependencies: + is-hex-prefixed: 1.0.0 + + strip-json-comments@3.1.1: {} + + supports-color@3.2.3: + dependencies: + has-flag: 1.0.0 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + sync-request@6.1.0: + dependencies: + http-response-object: 3.0.2 + sync-rpc: 1.3.6 + then-request: 6.0.2 + + sync-rpc@1.3.6: + dependencies: + get-port: 3.2.0 + + table-layout@1.0.2: + dependencies: + array-back: 4.0.2 + deep-extend: 0.6.0 + typical: 5.2.0 + wordwrapjs: 4.0.1 + + table@6.8.2: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + text-table@0.2.0: {} + + tfhe@0.6.4: {} + + then-request@6.0.2: + dependencies: + '@types/concat-stream': 1.6.1 + '@types/form-data': 0.0.33 + '@types/node': 8.10.66 + '@types/qs': 6.9.15 + caseless: 0.12.0 + concat-stream: 1.6.2 + form-data: 2.5.1 + http-basic: 8.1.3 + http-response-object: 3.0.2 + promise: 8.3.0 + qs: 6.13.0 + + tiny-invariant@1.3.3: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-fast-properties@2.0.0: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + ts-api-utils@1.3.0(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + ts-command-line-args@2.5.1: + dependencies: + chalk: 4.1.2 + command-line-args: 5.2.1 + command-line-usage: 6.1.3 + string-format: 2.0.0 + + ts-essentials@1.0.4: {} + + ts-essentials@7.0.3(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + + ts-generator@0.1.1: + dependencies: + '@types/mkdirp': 0.5.2 + '@types/prettier': 2.7.3 + '@types/resolve': 0.0.8 + chalk: 2.4.2 + glob: 7.2.3 + mkdirp: 0.5.6 + prettier: 2.8.8 + resolve: 1.22.8 + ts-essentials: 1.0.4 + + ts-node@10.9.2(@types/node@18.19.44)(typescript@5.5.4): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.44 + acorn: 8.12.1 + acorn-walk: 8.3.3 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@1.14.1: {} + + tslib@2.4.0: {} + + tsort@0.0.1: {} + + tweetnacl-util@0.15.1: {} + + tweetnacl@1.0.3: {} + + type-check@0.3.2: + dependencies: + prelude-ls: 1.1.2 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.1.0: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.7.1: {} + + typechain@8.3.2(typescript@5.5.4): + dependencies: + '@types/prettier': 2.7.3 + debug: 4.3.6(supports-color@8.1.1) + fs-extra: 7.0.1 + glob: 7.1.7 + js-sha3: 0.8.0 + lodash: 4.17.21 + mkdirp: 1.0.4 + prettier: 2.8.8 + ts-command-line-args: 2.5.1 + ts-essentials: 7.0.3(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + + typedarray@0.0.6: {} + + typescript-eslint@8.0.1(eslint@9.9.0)(typescript@5.5.4): + dependencies: + '@typescript-eslint/eslint-plugin': 8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/parser': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.0.1(eslint@9.9.0)(typescript@5.5.4) + optionalDependencies: + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + + typescript@5.5.4: {} + + typical@4.0.0: {} + + typical@5.2.0: {} + + uglify-js@3.19.2: + optional: true + + undici-types@5.26.5: {} + + undici@5.28.4: + dependencies: + '@fastify/busboy': 2.1.1 + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.13.0 + + utf8@3.0.0: {} + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + + uuid@8.3.2: {} + + v8-compile-cache-lib@3.0.1: {} + + web3-errors@1.2.1: + dependencies: + web3-types: 1.7.0 + + web3-types@1.7.0: {} + + web3-utils@1.10.4: + dependencies: + '@ethereumjs/util': 8.1.0 + bn.js: 5.2.1 + ethereum-bloom-filters: 1.2.0 + ethereum-cryptography: 2.2.1 + ethjs-unit: 0.1.6 + number-to-bn: 1.7.0 + randombytes: 2.1.0 + utf8: 3.0.0 + + web3-validator@2.0.6: + dependencies: + ethereum-cryptography: 2.2.1 + util: 0.12.5 + web3-errors: 1.2.1 + web3-types: 1.7.0 + zod: 3.23.8 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wordwrapjs@4.0.1: + dependencies: + reduce-flatten: 2.0.0 + typical: 5.2.0 + + workerpool@6.5.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@7.4.6: {} + + ws@7.5.10: {} + + ws@8.17.1: {} + + y18n@5.0.8: {} + + yargs-parser@20.2.9: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + zksync-ethers@5.9.2(ethers@5.7.2): + dependencies: + ethers: 5.7.2 + + zod@3.23.8: {} diff --git a/packages/foundry/contracts/hooks/Hookathon/remappings.txt b/packages/foundry/contracts/hooks/Hookathon/remappings.txt new file mode 100644 index 00000000..1e3bc738 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/remappings.txt @@ -0,0 +1,7 @@ +lib/TFHE=mocks/TFHE +@balancer-labs/v3-solidity-utils/=lib/balancer-v3-monorepo/pkg/solidity-utils/ +@balancer-labs/v3-pool-utils/=lib/balancer-v3-monorepo/pkg/pool-utils/ +@balancer-labs/v3-interfaces/=lib/balancer-v3-monorepo/pkg/interfaces/ +@balancer-labs/v3-pool-weighted/=lib/balancer-v3-monorepo/pkg/pool-weighted/ +@balancer-labs/v3-vault/=lib/balancer-v3-monorepo/pkg/vault/ +permit2/=lib/permit2/ \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/copy_fhe_keys.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/copy_fhe_keys.sh new file mode 100755 index 00000000..f3f14e42 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/copy_fhe_keys.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +# This bash script creates global fhe keys +# and copy them to the right folder in volumes directory. +# It accepts +# - the version of kms-dev as the first parameter +# - the LOCAL_BUILD_PUBLIC_KEY_PATH as the second optional parameter. +# - the LOCAL_BUILD_PRIVATE_KEY_PATH as the third optional parameter. + +# mkdir -p temp; docker run --rm -v $PWD/temp:/keys ghcr.io/zama-ai/kms-service:c744ada ./bin/kms-gen-keys centralized --write-privkey --pub-path /keys --priv-path /keys + +set -Eeuo pipefail + +if [ "$#" -lt 1 ]; then + echo "Usage: $(basename "$0") [LOCAL_BUILD_PUBLIC_KEY_PATH] [LOCAL_BUILD_PRIVATE_KEY_PATH]" + echo "Example: $(basename "$0") c744ada $(PWD)/running_node/node1/.ethermintd/zama/keys/network-fhe-keys /PATH_TO_KMS_KEYS" + exit +fi + +KMS_DEV_VERSION=$1 +DOCKER_IMAGE=ghcr.io/zama-ai/kms-service-dev:"$KMS_DEV_VERSION" +echo "$DOCKER_IMAGE" +CURRENT_FOLDER=$PWD + +DOCKER_COMPOSE_KMS_VERSION= +DOCKER_COMPOSE_KMS_VERSION=$(./scripts/get_kms_core_version.sh ./docker-compose/docker-compose-full.yml kms-service-dev) + +if [ "$DOCKER_COMPOSE_KMS_VERSION" != "$KMS_DEV_VERSION" ]; then + echo "Versions do not match!" + echo "DOCKER_COMPOSE_KMS_VERSION in docker-compose-full.yml: $DOCKER_COMPOSE_KMS_VERSION" + echo "KMS_DEV_VERSION given to key copy/gen script: $KMS_DEV_VERSION" + exit 1 +fi + + +KEYS_FULL_PATH=$CURRENT_FOLDER/res/keys +mkdir -p $KEYS_FULL_PATH + +if [ "$#" -ge 3 ]; then + LOCAL_BUILD_PUBLIC_KEY_PATH=$2 + LOCAL_BUILD_PRIVATE_KEY_PATH=$3 + NETWORK_KEYS_PUBLIC_PATH="${LOCAL_BUILD_PUBLIC_KEY_PATH}" + NETWORK_KEYS_PRIVATE_PATH="${LOCAL_BUILD_PRIVATE_KEY_PATH}" +else + NETWORK_KEYS_PUBLIC_PATH="./volumes/network-public-fhe-keys" + NETWORK_KEYS_PRIVATE_PATH="./volumes/network-private-fhe-keys" +fi + +mkdir -p "$KEYS_FULL_PATH" +docker pull $DOCKER_IMAGE + +docker create --name temp-container $DOCKER_IMAGE +docker cp temp-container:/app/kms/core/service/keys res +docker rm temp-container + +echo "$KEYS_FULL_PATH" + + + +echo "###########################################################" +echo "Keys creation is done, they are stored in $KEYS_FULL_PATH" +echo "###########################################################" + +echo "$NETWORK_KEYS_PUBLIC_PATH" +echo "$NETWORK_KEYS_PRIVATE_PATH" + +MANDATORY_KEYS_LIST=('PUB/ServerKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088' 'PRIV/FhePrivateKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088' 'PUB/PublicKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088') + +for key in "${MANDATORY_KEYS_LIST[@]}"; do + if [ ! -f "$KEYS_FULL_PATH/$key" ]; then + echo "#####ATTENTION######" + echo "$key does not exist in $KEYS_FULL_PATH!" + echo "####################" + exit + fi +done + + +echo "###########################################################" +echo "All the required keys exist in $KEYS_FULL_PATH" +echo "###########################################################" + +mkdir -p $NETWORK_KEYS_PUBLIC_PATH +mkdir -p $NETWORK_KEYS_PRIVATE_PATH + +key="PUB/ServerKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088" +echo "Copying $key to $NETWORK_KEYS_PUBLIC_PATH, please wait ..." +cp $KEYS_FULL_PATH/$key $NETWORK_KEYS_PUBLIC_PATH/sks + +key="PUB/PublicKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088" +echo "Copying $key to $NETWORK_KEYS_PUBLIC_PATH, please wait ..." +cp $KEYS_FULL_PATH/$key $NETWORK_KEYS_PUBLIC_PATH/pks + + +key="PRIV/FhePrivateKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088" +echo "Copying $key to $NETWORK_KEYS_PRIVATE_PATH, please wait ..." +cp $KEYS_FULL_PATH/$key $NETWORK_KEYS_PRIVATE_PATH/cks +# TODO remove it after, for now npx hardhat test expects cks +# in $HOME/network-fhe-keys/cks +mkdir -p $HOME/network-fhe-keys +cp $KEYS_FULL_PATH/$key $HOME/network-fhe-keys/cks + + diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/fund_test_addresses_docker.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/fund_test_addresses_docker.sh new file mode 100755 index 00000000..d99a87a1 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/fund_test_addresses_docker.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# Read MNEMONIC from .env file, remove 'export' and quotes +MNEMONIC=$(grep MNEMONIC .env | cut -d '"' -f 2) +PRIVATE_KEY_GATEWAY_DEPLOYER=$(grep PRIVATE_KEY_GATEWAY_DEPLOYER .env | cut -d '"' -f 2) +PRIVATE_KEY_GATEWAY_OWNER=$(grep PRIVATE_KEY_GATEWAY_OWNER .env | cut -d '"' -f 2) +PRIVATE_KEY_GATEWAY_RELAYER=$(grep PRIVATE_KEY_GATEWAY_RELAYER .env | cut -d '"' -f 2) + +# Verify that global envs are set +if [ -z "$MNEMONIC" ]; then + echo "Error: MNEMONIC is not set." + exit 1 +fi +if [ -z "$PRIVATE_KEY_GATEWAY_DEPLOYER" ]; then + echo "Error: PRIVATE_KEY_GATEWAY_DEPLOYER is not set." + exit 1 +fi +if [ -z "$PRIVATE_KEY_GATEWAY_OWNER" ]; then + echo "Error: PRIVATE_KEY_GATEWAY_OWNER is not set." + exit 1 +fi +if [ -z "$PRIVATE_KEY_GATEWAY_RELAYER" ]; then + echo "Error: PRIVATE_KEY_GATEWAY_RELAYER is not set." + exit 1 +fi + +# Compute addresses using ethers.js v6 - signers[0] to signers[4] are Alice, Bob, Carol, David and Eve. signers[9] is the fhevm deployer. +addresses=$(node -e " +const { ethers } = require('ethers'); +const derivationPath = \"m/44'/60'/0'/0\"; +const mnemonicInstance = ethers.Mnemonic.fromPhrase('$MNEMONIC'); +const hdNode = ethers.HDNodeWallet.fromMnemonic(mnemonicInstance, derivationPath); +const indices = [0, 1, 2, 3, 4, 9]; +for (const i of indices) { + const childNode = hdNode.derivePath(\`\${i}\`); + console.log(childNode.address); +} +const deployerAddress = new ethers.Wallet('$PRIVATE_KEY_GATEWAY_DEPLOYER').address; +console.log(deployerAddress); +const ownerAddress = new ethers.Wallet('$PRIVATE_KEY_GATEWAY_OWNER').address; +console.log(ownerAddress); +const relayerAddress = new ethers.Wallet('$PRIVATE_KEY_GATEWAY_RELAYER').address; +console.log(relayerAddress); +" 2>/dev/null) + +# Check if addresses were generated successfully +if [ -z "$addresses" ]; then + echo "Error: Failed to generate addresses." + exit 1 +fi + +# Convert the addresses string into an array +IFS=$'\n' read -rd '' -a addressArray <<<"$addresses" + +# Loop through each address, strip '0x', and run the Docker command +for addr in "${addressArray[@]}"; do + addr_no0x=${addr#0x} + docker exec -i zama-dev-fhevm-validator-1 faucet "$addr_no0x" + sleep 8 +done diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/get_kms_core_version.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/get_kms_core_version.sh new file mode 100755 index 00000000..89234263 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/get_kms_core_version.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Check if a file name is provided as an argument +if [ $# -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign the first argument to a variable +file="$1" +docker_image="$2" + +# Check if the file exists +if [ ! -f "$file" ]; then + echo "File does not exist: $file" + exit 1 +fi + + +# Extracting the version using grep and awk +version=$(grep 'ghcr.io/zama-ai/'$docker_image "$file" | awk -F':' '{print $3}' | tr -d '[:space:]') + +echo $version diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/get_repository_info.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/get_repository_info.sh new file mode 100755 index 00000000..2c33d0f0 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/get_repository_info.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +REPO_NAME=$1 +REPO_PATH=$2 + +if [ ! -d "$REPO_PATH" ]; then + echo "######################################################" + echo "WARNING: $REPO_NAME does not exist or is not a directory" + echo "Given path: $REPO_PATH" + echo "######################################################" + exit +fi + +if [ ! -d "$REPO_PATH/.git" ]; then + echo "Error: $REPO_PATH is not a Git repository" + exit 1 +fi + +cd $REPO_PATH + +BRANCH=$(git rev-parse --abbrev-ref HEAD) +TAG=$(git describe --tags --exact-match 2>/dev/null) +COMMIT=$(git rev-parse HEAD | cut -c 1-8) + +echo "$REPO_NAME --- branch: $BRANCH | tag: $TAG | commit: $COMMIT | path: $REPO_PATH" diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/precomputeAddresses.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/precomputeAddresses.sh new file mode 100755 index 00000000..0575ae5d --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/precomputeAddresses.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +npx hardhat clean +PRIVATE_KEY_GATEWAY_DEPLOYER=$(grep PRIVATE_KEY_GATEWAY_DEPLOYER .env | cut -d '"' -f 2) +npx hardhat task:computeACLAddress --network hardhat +npx hardhat task:computeTFHEExecutorAddress --network hardhat +npx hardhat task:computeKMSVerifierAddress --network hardhat +npx hardhat task:computePredeployAddress --private-key "$PRIVATE_KEY_GATEWAY_DEPLOYER" --network hardhat + +# Paths to input files +ENV_EXEC="node_modules/fhevm/lib/.env.exec" +ENV_GATEWAY="node_modules/fhevm/gateway/.env.gateway" +ENV_FILE=".env" + +# Path to output file +ENV_DOCKER=".env.docker" + +# Check if input files exist +for file in "$ENV_EXEC" "$ENV_GATEWAY" "$ENV_FILE"; do + if [ ! -f "$file" ]; then + echo "Error: $file does not exist." + exit 1 + fi +done + +# Extract values +TFHE_EXECUTOR_CONTRACT_ADDRESS=$(grep "^TFHE_EXECUTOR_CONTRACT_ADDRESS=" "$ENV_EXEC" | cut -d'=' -f2) +GATEWAY_CONTRACT_PREDEPLOY_ADDRESS=$(grep "^GATEWAY_CONTRACT_PREDEPLOY_ADDRESS=" "$ENV_GATEWAY" | cut -d'=' -f2) +GATEWAY_CONTRACT_PREDEPLOY_ADDRESS_NO0X=${GATEWAY_CONTRACT_PREDEPLOY_ADDRESS#0x} +PRIVATE_KEY_GATEWAY_RELAYER=$(grep PRIVATE_KEY_GATEWAY_RELAYER .env | cut -d '"' -f 2) + +# Write to .env.docker +{ + echo "TFHE_EXECUTOR_CONTRACT_ADDRESS=$TFHE_EXECUTOR_CONTRACT_ADDRESS" + echo "GATEWAY_CONTRACT_PREDEPLOY_ADDRESS=$GATEWAY_CONTRACT_PREDEPLOY_ADDRESS_NO0X" + echo "PRIVATE_KEY_GATEWAY_RELAYER=$PRIVATE_KEY_GATEWAY_RELAYER" +} > "$ENV_DOCKER" +echo "Successfully created $ENV_DOCKER with the aggregated environment variables." \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_test.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_test.sh new file mode 100755 index 00000000..12c853e7 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +make init-ethermint-node +# Run fhEVM + full KMS components +make run-full +# Deploy ACL, Gateway ..., please wait until the end before testing!!! +make prepare-e2e-test +# This test will fail (first event catch is buggy - we are on it) +make run-async-test diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_volumes_from_kms_core.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_volumes_from_kms_core.sh new file mode 100755 index 00000000..b54f5f4b --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_volumes_from_kms_core.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# This bash script creates global fhe keys +# and copy them to the right folder in volumes directory. +# It accepts +# - the version of kms-dev as the first parameter +# - the LOCAL_BUILD_PUBLIC_KEY_PATH as the second optional parameter. +# - the LOCAL_BUILD_PRIVATE_KEY_PATH as the third optional parameter. + +# mkdir -p temp; docker run --rm -v $PWD/temp:/keys ghcr.io/zama-ai/kms-service:c744ada ./bin/kms-gen-keys centralized --write-privkey --pub-path /keys --priv-path /keys + +set -Eeuo pipefail + +if [ "$#" -lt 1 ]; then + echo "Usage: $(basename "$0") [LOCAL_BUILD_PUBLIC_KEY_PATH] [LOCAL_BUILD_PRIVATE_KEY_PATH]" + echo "Example: $(basename "$0") c744ada $(PWD)/running_node/node1/.ethermintd/zama/keys/network-fhe-keys /PATH_TO_KMS_KEYS" + exit +fi + +KMS_DEV_VERSION=$1 +BINARY_NAME="./bin/kms-gen-keys" +DOCKER_IMAGE=ghcr.io/zama-ai/kms-service-dev:"$KMS_DEV_VERSION" +CURRENT_FOLDER=$PWD + +DOCKER_COMPOSE_KMS_VERSION=$(./scripts/get_kms_core_version.sh ./docker-compose/docker-compose-full.yml kms-service-dev) + +if [ "$DOCKER_COMPOSE_KMS_VERSION" != "$KMS_DEV_VERSION" ]; then + echo "Versions do not match!" + echo "DOCKER_COMPOSE_KMS_VERSION in docker-compose-full.yml: $DOCKER_COMPOSE_KMS_VERSION" + echo "KMS_DEV_VERSION given to key copy/gen script: $KMS_DEV_VERSION" + exit 1 +fi + + +KEYS_FULL_PATH=$CURRENT_FOLDER/res/keys +mkdir -p $KEYS_FULL_PATH + +if [ "$#" -ge 3 ]; then + LOCAL_BUILD_PUBLIC_KEY_PATH=$2 + LOCAL_BUILD_PRIVATE_KEY_PATH=$3 + NETWORK_KEYS_PUBLIC_PATH="${LOCAL_BUILD_PUBLIC_KEY_PATH}" + NETWORK_KEYS_PRIVATE_PATH="${LOCAL_BUILD_PRIVATE_KEY_PATH}" +else + NETWORK_KEYS_PUBLIC_PATH="./volumes/network-public-fhe-keys" + NETWORK_KEYS_PRIVATE_PATH="./volumes/network-private-fhe-keys" +fi + +mkdir -p "$KEYS_FULL_PATH" + +docker run -v "$PWD/res/keys:/keys" "$DOCKER_IMAGE" "$BINARY_NAME" centralized --write-privkey --pub-path /keys --priv-path /keys + +echo "$KEYS_FULL_PATH" + +echo "###########################################################" +echo "Keys creation is done, they are stored in $KEYS_FULL_PATH" +echo "###########################################################" + + + +echo "$NETWORK_KEYS_PUBLIC_PATH" +echo "$NETWORK_KEYS_PRIVATE_PATH" + +MANDATORY_KEYS_LIST=('PUB/ServerKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088' 'PRIV/FhePrivateKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088' 'PUB/PublicKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088') + +for key in "${MANDATORY_KEYS_LIST[@]}"; do + if [ ! -f "$KEYS_FULL_PATH/$key" ]; then + echo "#####ATTENTION######" + echo "$key does not exist in $KEYS_FULL_PATH!" + echo "####################" + exit + fi +done + + +echo "###########################################################" +echo "All the required keys exist in $KEYS_FULL_PATH" +echo "###########################################################" + +mkdir -p $NETWORK_KEYS_PUBLIC_PATH +mkdir -p $NETWORK_KEYS_PRIVATE_PATH + +key="PUB/ServerKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088" +echo "Copying $key to $NETWORK_KEYS_PUBLIC_PATH, please wait ..." +cp $KEYS_FULL_PATH/$key $NETWORK_KEYS_PUBLIC_PATH/sks + +key="PUB/PublicKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088" +echo "Copying $key to $NETWORK_KEYS_PUBLIC_PATH, please wait ..." +cp $KEYS_FULL_PATH/$key $NETWORK_KEYS_PUBLIC_PATH/pks + + +key="PRIV/FhePrivateKey/408d8cbaa51dece7f782fe04ba0b1c1d017b1088" +echo "Copying $key to $NETWORK_KEYS_PRIVATE_PATH, please wait ..." +cp $KEYS_FULL_PATH/$key $NETWORK_KEYS_PRIVATE_PATH/cks +# TODO remove it after, for now npx hardhat test expects cks +# in $HOME/network-fhe-keys/cks +mkdir -p $HOME/network-fhe-keys +cp $KEYS_FULL_PATH/$key $HOME/network-fhe-keys/cks + + diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/rewrite-docker-compose.sh b/packages/foundry/contracts/hooks/Hookathon/scripts/rewrite-docker-compose.sh new file mode 100755 index 00000000..8c92da62 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/scripts/rewrite-docker-compose.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +INPUT_FILE="docker-compose/docker-compose-full.yml.template" +OUTPUT_FILE="docker-compose/docker-compose-full.yml" +ENV_FILE=".env.docker" + +TEMP_FILE=$(mktemp) +cp "$INPUT_FILE" "$TEMP_FILE" + +while IFS= read -r line +do + [[ $line =~ ^[[:space:]]*$ || $line =~ ^# ]] && continue + key=$(echo "$line" | cut -d= -f1) + value=$(echo "$line" | cut -d= -f2-) + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + value=$(printf '%s\n' "$value" | sed -e 's/[\/&]/\\&/g') + sed -i.bak "s|\${$key}|$value|g" "$TEMP_FILE" +done < "$ENV_FILE" + +mv "$TEMP_FILE" "$OUTPUT_FILE" + +echo "Processed $INPUT_FILE and wrote result to $OUTPUT_FILE" \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/setup.sh b/packages/foundry/contracts/hooks/Hookathon/setup.sh new file mode 100755 index 00000000..d877a03c --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/setup.sh @@ -0,0 +1,151 @@ +#!/bin/bash + +CHAINID="ethermint_9000-1" +MONIKER="localtestnet" +KEYRING="test" +KEYALGO="eth_secp256k1" +HOME_ETHERMINTD="$HOME/.ethermintd" +ETHERMINTD="ethermintd" + +mkdir -p $HOME_ETHERMINTD/config + +# validate dependencies are installed +command -v jq > /dev/null 2>&1 || { echo >&2 "jq not installed. More info: https://stedolan.github.io/jq/download/"; exit 1; } + +# used to exit on first error (any non-zero exit code) +set -e + +$ETHERMINTD config keyring-backend $KEYRING +$ETHERMINTD config chain-id $CHAINID +KEY1="orchestrator" + +# if $KEY exists it should be deleted +$ETHERMINTD keys add $KEY1 --keyring-backend $KEYRING --algo $KEYALGO +# orchestrator address 0x7cb61d4117ae31a12e393a1cfa3bac666481d02e | evmos10jmp6sgh4cc6zt3e8gw05wavvejgr5pwjnpcky +# VAL_MNEMONIC="gesture inject test cycle original hollow east ridge hen combine junk child bacon zero hope comfort vacuum milk pitch cage oppose unhappy lunar seat" +# # Import keys from mnemonics +# echo "$VAL_MNEMONIC" | $ETHERMINTD keys add $KEY1 --recover + +# Set moniker and chain-id for Ethermint (Moniker can be anything, chain-id must be an integer) +$ETHERMINTD init $MONIKER --chain-id $CHAINID + +# Change parameter token denominations to aphoton +cat $HOME_ETHERMINTD/config/genesis.json | jq '.app_state["staking"]["params"]["bond_denom"]="aphoton"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json +cat $HOME_ETHERMINTD/config/genesis.json | jq '.app_state["crisis"]["constant_fee"]["denom"]="aphoton"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json +cat $HOME_ETHERMINTD/config/genesis.json | jq '.app_state["gov"]["deposit_params"]["min_deposit"][0]["denom"]="aphoton"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json +cat $HOME_ETHERMINTD/config/genesis.json | jq '.app_state["mint"]["params"]["mint_denom"]="aphoton"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json + +# Set EVM RPC HTTP server address bind to 0.0.0.0 (needed to reach docker from host) +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' 's/127.0.0.1:8545/0.0.0.0:8545/g' $HOME_ETHERMINTD/config/app.toml + else + sed -i 's/127.0.0.1:8545/0.0.0.0:8545/g' $HOME_ETHERMINTD/config/app.toml +fi + +# Set EVM websocket server address bind to 0.0.0.0 (needed to reach docker from host) + +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' 's/127.0.0.1:8546/0.0.0.0:8546/g' $HOME_ETHERMINTD/config/app.toml + else + sed -i 's/127.0.0.1:8546/0.0.0.0:8546/g' $HOME_ETHERMINTD/config/app.toml +fi + +# Set gas limit of 10000000 and txn limit of 4 MB in genesis +cat $HOME_ETHERMINTD/config/genesis.json | jq '.consensus_params["block"]["max_gas"]="10000000"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json +cat $HOME_ETHERMINTD/config/genesis.json | jq '.consensus_params["block"]["max_bytes"]="4194304"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json + +# Set claims start time +node_address=$($ETHERMINTD keys list | grep "address: " | cut -c12-) +current_date=$(date -u +"%Y-%m-%dT%TZ") +cat $HOME_ETHERMINTD/config/genesis.json | jq -r --arg current_date "$current_date" '.app_state["claims"]["params"]["airdrop_start_time"]=$current_date' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json + +# Set claims records for validator account +amount_to_claim=10000 +cat $HOME_ETHERMINTD/config/genesis.json | jq -r --arg node_address "$node_address" --arg amount_to_claim "$amount_to_claim" '.app_state["claims"]["claims_records"]=[{"initial_claimable_amount":$amount_to_claim, "actions_completed":[false, false, false, false],"address":$node_address}]' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json + +# Set claims decay +cat $HOME_ETHERMINTD/config/genesis.json | jq '.app_state["claims"]["params"]["duration_of_decay"]="1000000s"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json +cat $HOME_ETHERMINTD/config/genesis.json | jq '.app_state["claims"]["params"]["duration_until_decay"]="100000s"' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json + +# Claim module account: +# 0xA61808Fe40fEb8B3433778BBC2ecECCAA47c8c47 || ethm15cvq3ljql6utxseh0zau9m8ve2j8erz8u5tz0g +cat $HOME_ETHERMINTD/config/genesis.json | jq -r --arg amount_to_claim "$amount_to_claim" '.app_state["bank"]["balances"] += [{"address":"ethm15cvq3ljql6utxseh0zau9m8ve2j8erz8u5tz0g","coins":[{"denom":"aphoton", "amount":$amount_to_claim}]}]' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json + + +# Disable production of empty blocks. +# Increase transaction and HTTP server body sizes. +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' 's/create_empty_blocks = true/create_empty_blocks = false/g' $HOME_ETHERMINTD/config/config.toml + else + sed -i 's/create_empty_blocks = true/create_empty_blocks = false/g' $HOME_ETHERMINTD/config/config.toml +fi + + +# Allocate genesis accounts (cosmos formatted addresses) +$ETHERMINTD add-genesis-account $KEY1 100000000000000000000000000aphoton --keyring-backend $KEYRING + + +# Update total supply with claim values +validators_supply=$(cat $HOME_ETHERMINTD/config/genesis.json | jq -r '.app_state["bank"]["supply"][0]["amount"]') +# Bc is required to add this big numbers +# total_supply=$(bc <<< "$amount_to_claim+$validators_supply") +total_supply=100000000000000000000010000 +cat $HOME_ETHERMINTD/config/genesis.json | jq -r --arg total_supply "$total_supply" '.app_state["bank"]["supply"][0]["amount"]=$total_supply' > $HOME_ETHERMINTD/config/tmp_genesis.json && mv $HOME_ETHERMINTD/config/tmp_genesis.json $HOME_ETHERMINTD/config/genesis.json + + + +# Sign genesis transaction +$ETHERMINTD gentx $KEY1 1000000000000000000000aphoton --keyring-backend $KEYRING --chain-id $CHAINID + +# Collect genesis tx +$ETHERMINTD collect-gentxs + +# Run this to ensure everything worked and that the genesis file is setup correctly +$ETHERMINTD validate-genesis + +# disable produce empty block and enable prometheus metrics +if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' 's/create_empty_blocks = true/create_empty_blocks = false/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/prometheus = false/prometheus = true/' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/prometheus-retention-time = 0/prometheus-retention-time = 1000000000000/g' $HOME_ETHERMINTD/config/app.toml + sed -i '' 's/enabled = false/enabled = true/g' $HOME_ETHERMINTD/config/app.toml +else + sed -i 's/create_empty_blocks = true/create_empty_blocks = false/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/prometheus = false/prometheus = true/' $HOME_ETHERMINTD/config/config.toml + sed -i 's/prometheus-retention-time = "0"/prometheus-retention-time = "1000000000000"/g' $HOME_ETHERMINTD/config/app.toml + sed -i 's/enabled = false/enabled = true/g' $HOME_ETHERMINTD/config/app.toml +fi + +if [[ $1 == "pending" ]]; then + echo "pending mode is on, please wait for the first block committed." + if [[ $OSTYPE == "darwin"* ]]; then + sed -i '' 's/create_empty_blocks_interval = "0s"/create_empty_blocks_interval = "30s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_propose = "3s"/timeout_propose = "30s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_propose_delta = "500ms"/timeout_propose_delta = "5s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_prevote = "1s"/timeout_prevote = "10s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_prevote_delta = "500ms"/timeout_prevote_delta = "5s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_precommit = "1s"/timeout_precommit = "10s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_precommit_delta = "500ms"/timeout_precommit_delta = "5s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_commit = "5s"/timeout_commit = "150s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i '' 's/timeout_broadcast_tx_commit = "10s"/timeout_broadcast_tx_commit = "150s"/g' $HOME_ETHERMINTD/config/config.toml + else + sed -i 's/create_empty_blocks_interval = "0s"/create_empty_blocks_interval = "30s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_propose = "3s"/timeout_propose = "30s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_propose_delta = "500ms"/timeout_propose_delta = "5s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_prevote = "1s"/timeout_prevote = "10s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_prevote_delta = "500ms"/timeout_prevote_delta = "5s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_precommit = "1s"/timeout_precommit = "10s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_precommit_delta = "500ms"/timeout_precommit_delta = "5s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_commit = "5s"/timeout_commit = "150s"/g' $HOME_ETHERMINTD/config/config.toml + sed -i 's/timeout_broadcast_tx_commit = "10s"/timeout_broadcast_tx_commit = "150s"/g' $HOME_ETHERMINTD/config/config.toml + fi +fi + +# Create Zama-specific directories and files. +mkdir -p $HOME_ETHERMINTD/zama/keys/network-fhe-keys +mkdir -p $HOME_ETHERMINTD/zama/config + +touch $HOME/privkey +$ETHERMINTD keys unsafe-export-eth-key $KEY1 --keyring-backend test > $HOME/privkey +touch $HOME/node_id +$ETHERMINTD tendermint show-node-id > $HOME/node_id diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/accounts.ts b/packages/foundry/contracts/hooks/Hookathon/tasks/accounts.ts new file mode 100644 index 00000000..866b83a8 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/accounts.ts @@ -0,0 +1,9 @@ +import { task } from "hardhat/config"; + +task("task:accounts", "Prints the list of accounts", async (_taskArgs, hre) => { + const accounts = await hre.ethers.getSigners(); + + for (const account of accounts) { + console.log(account.address); + } +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/checkNodeVersion.js b/packages/foundry/contracts/hooks/Hookathon/tasks/checkNodeVersion.js new file mode 100644 index 00000000..30fe5823 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/checkNodeVersion.js @@ -0,0 +1,2 @@ +if (parseFloat(process.versions.node) < 20.0) + throw new Error("Unsupported Node.js version. Please use Node.js >= 20.0.0."); diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/deployERC20.ts b/packages/foundry/contracts/hooks/Hookathon/tasks/deployERC20.ts new file mode 100644 index 00000000..9eeafc4a --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/deployERC20.ts @@ -0,0 +1,10 @@ +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +task("task:deployERC20").setAction(async function (taskArguments: TaskArguments, { ethers }) { + const signers = await ethers.getSigners(); + const erc20Factory = await ethers.getContractFactory("MyERC20"); + const encryptedERC20 = await erc20Factory.connect(signers[0]).deploy(); + await encryptedERC20.waitForDeployment(); + console.log("MyERC20 deployed to: ", await encryptedERC20.getAddress()); +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/getEthereumAddress.ts b/packages/foundry/contracts/hooks/Hookathon/tasks/getEthereumAddress.ts new file mode 100644 index 00000000..c3001a60 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/getEthereumAddress.ts @@ -0,0 +1,34 @@ +import dotenv from "dotenv"; +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +dotenv.config(); + +const getEthereumAddress = + (index: number = 0) => + async (_taskArgs: unknown, hre: HardhatRuntimeEnvironment) => { + const { ethers } = hre; + const words = process.env.MNEMONIC!; + const mnemonic = ethers.Mnemonic.fromPhrase(words); + if (!mnemonic) { + throw new Error("No MNEMONIC in .env file"); + } + const wallet = ethers.HDNodeWallet.fromMnemonic(mnemonic, `m/44'/60'/0'/0`); + console.log(wallet.deriveChild(index).address); + }; + +task( + "task:getEthereumAddress", + "Gets the first address derived from a mnemonic phrase defined in .env", + getEthereumAddress(0), +); + +const accounts = ["Alice", "Bob", "Carol", "Dave", "Eve"]; + +accounts.forEach((name, index) => { + task( + `task:getEthereumAddress${name}`, + "Gets the first address derived from a mnemonic phrase defined in .env", + getEthereumAddress(index), + ); +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskDeploy.ts b/packages/foundry/contracts/hooks/Hookathon/tasks/taskDeploy.ts new file mode 100644 index 00000000..86888ce8 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/taskDeploy.ts @@ -0,0 +1,65 @@ +import dotenv from "dotenv"; +import fs from "fs"; +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; + +task("task:deployGateway") + .addParam("privateKey", "The deployer private key") + .addParam("ownerAddress", "The owner address") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = new ethers.Wallet(taskArguments.privateKey).connect(ethers.provider); + const envConfig2 = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.kmsverifier")); + const gatewayFactory = await ethers.getContractFactory("fhevm/gateway/GatewayContract.sol:GatewayContract"); + const Gateway = await gatewayFactory + .connect(deployer) + .deploy(taskArguments.ownerAddress, envConfig2.KMS_VERIFIER_CONTRACT_ADDRESS); + await Gateway.waitForDeployment(); + const GatewayContractAddress = await Gateway.getAddress(); + const envConfig = dotenv.parse(fs.readFileSync("node_modules/fhevm/gateway/.env.gateway")); + if (GatewayContractAddress !== envConfig.GATEWAY_CONTRACT_PREDEPLOY_ADDRESS) { + throw new Error( + `The nonce of the deployer account is not null. Please use another deployer private key or relaunch a clean instance of the fhEVM`, + ); + } + console.log("GatewayContract was deployed at address: ", GatewayContractAddress); + }); + +task("task:deployACL").setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = (await ethers.getSigners())[9]; + const factory = await ethers.getContractFactory("fhevm/lib/ACL.sol:ACL"); + const envConfigExec = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.exec")); + const acl = await factory.connect(deployer).deploy(envConfigExec.TFHE_EXECUTOR_CONTRACT_ADDRESS); + await acl.waitForDeployment(); + const address = await acl.getAddress(); + const envConfigAcl = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.acl")); + if (address !== envConfigAcl.ACL_CONTRACT_ADDRESS) { + throw new Error(`The nonce of the deployer account is not corret. Please relaunch a clean instance of the fhEVM`); + } + console.log("ACL was deployed at address:", address); +}); + +task("task:deployTFHEExecutor").setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = (await ethers.getSigners())[9]; + const factory = await ethers.getContractFactory("TFHEExecutor"); + const exec = await factory.connect(deployer).deploy(); + await exec.waitForDeployment(); + const address = await exec.getAddress(); + const envConfig = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.exec")); + if (address !== envConfig.TFHE_EXECUTOR_CONTRACT_ADDRESS) { + throw new Error(`The nonce of the deployer account is not corret. Please relaunch a clean instance of the fhEVM`); + } + console.log("TFHEExecutor was deployed at address:", address); +}); + +task("task:deployKMSVerifier").setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = (await ethers.getSigners())[9]; + const factory = await ethers.getContractFactory("fhevm/lib/KMSVerifier.sol:KMSVerifier"); + const exec = await factory.connect(deployer).deploy(); + await exec.waitForDeployment(); + const address = await exec.getAddress(); + const envConfig = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.kmsverifier")); + if (address !== envConfig.KMS_VERIFIER_CONTRACT_ADDRESS) { + throw new Error(`The nonce of the deployer account is not corret. Please relaunch a clean instance of the fhEVM`); + } + console.log("KMSVerifier was deployed at address:", address); +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskGatewayRelayer.ts b/packages/foundry/contracts/hooks/Hookathon/tasks/taskGatewayRelayer.ts new file mode 100644 index 00000000..fb22ce94 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/taskGatewayRelayer.ts @@ -0,0 +1,149 @@ +import { exec as oldExec } from "child_process"; +import dotenv from "dotenv"; +import fs from "fs"; +import { task, types } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import path from "path"; +import { promisify } from "util"; + +const exec = promisify(oldExec); + +const getCoin = async (address: string) => { + const containerName = process.env["TEST_CONTAINER_NAME"] || "zama-kms-validator-1"; + const response = await exec(`docker exec -i ${containerName} faucet ${address} | grep height`); + const res = JSON.parse(response.stdout); + if (res.raw_log.match("account sequence mismatch")) await getCoin(address); +}; + +task("task:computePredeployAddress") + .addParam("privateKey", "The deployer private key") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployerAddress = new ethers.Wallet(taskArguments.privateKey).address; + const gatewayContractAddressPrecomputed = ethers.getCreateAddress({ + from: deployerAddress, + nonce: 0, // deployer is supposed to have nonce 0 when deploying GatewayContract + }); + const envFilePath = path.join(__dirname, "../node_modules/fhevm/gateway/.env.gateway"); + const content = `GATEWAY_CONTRACT_PREDEPLOY_ADDRESS=${gatewayContractAddressPrecomputed}\n`; + try { + fs.writeFileSync(envFilePath, content, { flag: "w" }); + console.log("gatewayContractAddress written to gateway/.env.gateway successfully!"); + } catch (err) { + console.error("Failed to write to node_modules/fhevm/gateway/.env.gateway:", err); + } + + const solidityTemplate = `// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.24; + +address constant GATEWAY_CONTRACT_PREDEPLOY_ADDRESS = ${gatewayContractAddressPrecomputed}; + `; + + try { + fs.writeFileSync("./node_modules/fhevm/gateway/lib/PredeployAddress.sol", solidityTemplate, { + encoding: "utf8", + flag: "w", + }); + console.log("node_modules/fhevm/gateway/lib/PredeployAddress.sol file has been generated successfully."); + } catch (error) { + console.error("Failed to write node_modules/fhevm/gateway/lib/PredeployAddress.sol", error); + } + }); + +task("task:addRelayer") + .addParam("privateKey", "The owner private key") + .addParam("gatewayAddress", "The GatewayContract address") + .addParam("relayerAddress", "The relayer address") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const codeAtAddress = await ethers.provider.getCode(taskArguments.gatewayAddress); + if (codeAtAddress === "0x") { + throw Error(`${taskArguments.gatewayAddress} is not a smart contract`); + } + const owner = new ethers.Wallet(taskArguments.privateKey).connect(ethers.provider); + const gateway = await ethers.getContractAt( + "fhevm/gateway/GatewayContract.sol:GatewayContract", + taskArguments.gatewayAddress, + owner, + ); + const tx = await gateway.addRelayer(taskArguments.relayerAddress); + const rcpt = await tx.wait(); + if (rcpt!.status === 1) { + console.log(`Account ${taskArguments.relayerAddress} was succesfully added as an gateway relayer`); + } else { + console.log("Adding relayer failed"); + } + }); + +task("task:removeRelayer") + .addParam("privateKey", "The owner private key") + .addParam("gatewayAddress", "The GatewayContract address") + .addParam("relayerAddress", "The relayer address") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const codeAtAddress = await ethers.provider.getCode(taskArguments.gatewayAddress); + if (codeAtAddress === "0x") { + throw Error(`${taskArguments.gatewayAddress} is not a smart contract`); + } + const owner = new ethers.Wallet(taskArguments.privateKey).connect(ethers.provider); + const gateway = await ethers.getContractAt( + "fhevm/gateway/GatewayContract.sol:GatewayContract", + taskArguments.gatewayAddress, + owner, + ); + const tx = await gateway.removeRelayer(taskArguments.relayerAddress); + const rcpt = await tx.wait(); + if (rcpt!.status === 1) { + console.log(`Account ${taskArguments.relayerAddress} was succesfully removed from authorized relayers`); + } else { + console.log("Removing relayer failed"); + } + }); + +task("task:launchFhevm") + .addOptionalParam("skipGetCoin", "Skip calling getCoin()", false, types.boolean) + .setAction(async function (taskArgs, hre) { + const privKeyDeployer = process.env.PRIVATE_KEY_GATEWAY_DEPLOYER; + const privKeyOwner = process.env.PRIVATE_KEY_GATEWAY_OWNER; + const privKeyRelayer = process.env.PRIVATE_KEY_GATEWAY_RELAYER; + const deployerAddress = new hre.ethers.Wallet(privKeyDeployer!).address; + const ownerAddress = new hre.ethers.Wallet(privKeyOwner!).address; + const relayerAddress = new hre.ethers.Wallet(privKeyRelayer!).address; + if (!taskArgs.skipGetCoin) { + if (hre.network.name === "hardhat") { + const bal = "0x1000000000000000000000000000000000000000"; + const p1 = hre.network.provider.send("hardhat_setBalance", [deployerAddress, bal]); + const p2 = hre.network.provider.send("hardhat_setBalance", [ownerAddress, bal]); + const p3 = hre.network.provider.send("hardhat_setBalance", [relayerAddress, bal]); + await Promise.all([p1, p2, p3]); + } else { + const p1 = getCoin(deployerAddress); + const p2 = getCoin(ownerAddress); + const p3 = getCoin(relayerAddress); + await Promise.all([p1, p2, p3]); + await new Promise((res) => setTimeout(res, 5000)); // wait 5 seconds + } + } + console.log(`privateKey ${privKeyDeployer}`); + console.log(`ownerAddress ${ownerAddress}`); + await hre.run("task:deployGateway", { privateKey: privKeyDeployer, ownerAddress: ownerAddress }); + + const parsedEnv = dotenv.parse(fs.readFileSync("node_modules/fhevm/gateway/.env.gateway")); + const gatewayContractAddress = parsedEnv.GATEWAY_CONTRACT_PREDEPLOY_ADDRESS; + + await hre.run("task:addRelayer", { + privateKey: privKeyOwner, + gatewayAddress: gatewayContractAddress, + relayerAddress: relayerAddress, + }); + }); + +task("task:getBalances").setAction(async function (taskArgs, hre) { + const privKeyDeployer = process.env.PRIVATE_KEY_GATEWAY_DEPLOYER; + const privKeyOwner = process.env.PRIVATE_KEY_GATEWAY_OWNER; + const privKeyRelayer = process.env.PRIVATE_KEY_GATEWAY_RELAYER; + const deployerAddress = new hre.ethers.Wallet(privKeyDeployer!).address; + const ownerAddress = new hre.ethers.Wallet(privKeyOwner!).address; + const relayerAddress = new hre.ethers.Wallet(privKeyRelayer!).address; + console.log(await hre.ethers.provider.getBalance(deployerAddress)); + console.log(await hre.ethers.provider.getBalance(ownerAddress)); + console.log(await hre.ethers.provider.getBalance(relayerAddress)); +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskOracleRelayer.ts b/packages/foundry/contracts/hooks/Hookathon/tasks/taskOracleRelayer.ts new file mode 100644 index 00000000..f20472c8 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/taskOracleRelayer.ts @@ -0,0 +1,145 @@ +import { exec as oldExec } from "child_process"; +import dotenv from "dotenv"; +import fs from "fs"; +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import path from "path"; +import { promisify } from "util"; + +const exec = promisify(oldExec); + +task("task:deployOracle") + .addParam("privateKey", "The deployer private key") + .addParam("ownerAddress", "The owner address") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = new ethers.Wallet(taskArguments.privateKey).connect(ethers.provider); + const oracleFactory = await ethers.getContractFactory("OraclePredeploy"); + const oracle = await oracleFactory.connect(deployer).deploy(taskArguments.ownerAddress); + await oracle.waitForDeployment(); + const oraclePredeployAddress = await oracle.getAddress(); + const envConfig = dotenv.parse(fs.readFileSync("node_modules/fhevm/oracle/.env.oracle")); + if (oraclePredeployAddress !== envConfig.ORACLE_CONTRACT_PREDEPLOY_ADDRESS) { + throw new Error( + `The nonce of the deployer account is not null. Please use another deployer private key or relaunch a clean instance of the fhEVM`, + ); + } + console.log("OraclePredeploy was deployed at address: ", oraclePredeployAddress); + }); + +const getCoin = async (address: string) => { + const containerName = process.env["TEST_CONTAINER_NAME"] || "fhevm"; + const response = await exec(`docker exec -i ${containerName} faucet ${address} | grep height`); + const res = JSON.parse(response.stdout); + if (res.raw_log.match("account sequence mismatch")) await getCoin(address); +}; + +task("task:computePredeployAddress") + .addParam("privateKey", "The deployer private key") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployerAddress = new ethers.Wallet(taskArguments.privateKey).address; + const oraclePredeployAddressPrecomputed = ethers.getCreateAddress({ + from: deployerAddress, + nonce: 0, // deployer is supposed to have nonce 0 when deploying OraclePredeploy + }); + const envFilePath = path.join(__dirname, "../node_modules/fhevm/oracle/.env.oracle"); + const content = `ORACLE_CONTRACT_PREDEPLOY_ADDRESS=${oraclePredeployAddressPrecomputed}\n`; + try { + fs.writeFileSync(envFilePath, content, { flag: "w" }); + console.log("oraclePredeployAddress written to node_modules/fhevm/oracle/.env.oracle successfully!"); + } catch (err) { + console.error("Failed to write to node_modules/fhevm/oracle/.env.oracle:", err); + } + + const solidityTemplate = `// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.20; + +address constant ORACLE_CONTRACT_PREDEPLOY_ADDRESS = ${oraclePredeployAddressPrecomputed}; + `; + + try { + fs.writeFileSync("./node_modules/fhevm/oracle/lib/PredeployAddress.sol", solidityTemplate, { + encoding: "utf8", + flag: "w", + }); + console.log("node_modules/fhevm/oracle/lib/PredeployAddress.sol file has been generated successfully."); + } catch (error) { + console.error("Failed to write node_modules/fhevm/oracle/lib/PredeployAddress.sol", error); + } + }); + +task("task:addRelayer") + .addParam("privateKey", "The owner private key") + .addParam("oracleAddress", "The OraclePredeploy address") + .addParam("relayerAddress", "The relayer address") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const codeAtAddress = await ethers.provider.getCode(taskArguments.oracleAddress); + if (codeAtAddress === "0x") { + throw Error(`${taskArguments.oracleAddress} is not a smart contract`); + } + const owner = new ethers.Wallet(taskArguments.privateKey).connect(ethers.provider); + const oracle = await ethers.getContractAt("OraclePredeploy", taskArguments.oracleAddress, owner); + const tx = await oracle.addRelayer(taskArguments.relayerAddress); + const rcpt = await tx.wait(); + if (rcpt!.status === 1) { + console.log(`Account ${taskArguments.relayerAddress} was succesfully added as an oracle relayer`); + } else { + console.log("Adding relayer failed"); + } + }); + +task("task:removeRelayer") + .addParam("privateKey", "The owner private key") + .addParam("oracleAddress", "The OraclePredeploy address") + .addParam("relayerAddress", "The relayer address") + .setAction(async function (taskArguments: TaskArguments, { ethers }) { + const codeAtAddress = await ethers.provider.getCode(taskArguments.oracleAddress); + if (codeAtAddress === "0x") { + throw Error(`${taskArguments.oracleAddress} is not a smart contract`); + } + const owner = new ethers.Wallet(taskArguments.privateKey).connect(ethers.provider); + const oracle = await ethers.getContractAt("OraclePredeploy", taskArguments.oracleAddress, owner); + const tx = await oracle.removeRelayer(taskArguments.relayerAddress); + const rcpt = await tx.wait(); + if (rcpt!.status === 1) { + console.log(`Account ${taskArguments.relayerAddress} was succesfully removed from authorized relayers`); + } else { + console.log("Removing relayer failed"); + } + }); + +task("task:launchFhevm").setAction(async function (taskArgs, hre) { + const privKeyDeployer = process.env.PRIVATE_KEY_ORACLE_DEPLOYER; + const privKeyOwner = process.env.PRIVATE_KEY_ORACLE_OWNER; + const privKeyRelayer = process.env.PRIVATE_KEY_ORACLE_RELAYER; + const deployerAddress = new hre.ethers.Wallet(privKeyDeployer!).address; + const ownerAddress = new hre.ethers.Wallet(privKeyOwner!).address; + const relayerAddress = new hre.ethers.Wallet(privKeyRelayer!).address; + const p1 = getCoin(deployerAddress); + const p2 = getCoin(ownerAddress); + const p3 = getCoin(relayerAddress); + await Promise.all([p1, p2, p3]); + await new Promise((res) => setTimeout(res, 5000)); // wait 5 seconds + await hre.run("task:deployOracle", { privateKey: privKeyDeployer, ownerAddress: ownerAddress }); + + const parsedEnv = dotenv.parse(fs.readFileSync("node_modules/fhevm/oracle/.env.oracle")); + const oraclePredeployAddress = parsedEnv.ORACLE_CONTRACT_PREDEPLOY_ADDRESS; + + await hre.run("task:addRelayer", { + privateKey: privKeyOwner, + oracleAddress: oraclePredeployAddress, + relayerAddress: relayerAddress, + }); +}); + +task("task:getBalances").setAction(async function (taskArgs, hre) { + const privKeyDeployer = process.env.PRIVATE_KEY_ORACLE_DEPLOYER; + const privKeyOwner = process.env.PRIVATE_KEY_ORACLE_OWNER; + const privKeyRelayer = process.env.PRIVATE_KEY_ORACLE_RELAYER; + const deployerAddress = new hre.ethers.Wallet(privKeyDeployer!).address; + const ownerAddress = new hre.ethers.Wallet(privKeyOwner!).address; + const relayerAddress = new hre.ethers.Wallet(privKeyRelayer!).address; + console.log(await hre.ethers.provider.getBalance(deployerAddress)); + console.log(await hre.ethers.provider.getBalance(ownerAddress)); + console.log(await hre.ethers.provider.getBalance(relayerAddress)); +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskTFHE.ts b/packages/foundry/contracts/hooks/Hookathon/tasks/taskTFHE.ts new file mode 100644 index 00000000..15ee0ef2 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tasks/taskTFHE.ts @@ -0,0 +1,97 @@ +import fs from "fs"; +import { task } from "hardhat/config"; +import type { TaskArguments } from "hardhat/types"; +import path from "path"; + +task("task:computeACLAddress").setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = (await ethers.getSigners())[9].address; + const aclAddress = ethers.getCreateAddress({ + from: deployer, + nonce: 0, // using nonce of 0 for the ACL contract + }); + const envFilePath = path.join(__dirname, "../node_modules/fhevm/lib/.env.acl"); + const content = `ACL_CONTRACT_ADDRESS=${aclAddress}\n`; + try { + fs.writeFileSync(envFilePath, content, { flag: "w" }); + console.log(`ACL address ${aclAddress} written successfully!`); + } catch (err) { + console.error("Failed to write ACL address:", err); + } + + const solidityTemplate = `// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.24; + +address constant aclAdd = ${aclAddress};\n`; + + try { + fs.writeFileSync("node_modules/fhevm/lib/ACLAddress.sol", solidityTemplate, { encoding: "utf8", flag: "w" }); + console.log("node_modules/fhevm/lib/ACLAddress.sol file generated successfully!"); + } catch (error) { + console.error("Failed to write node_modules/fhevm/lib/ACLAddress.sol", error); + } +}); + +task("task:computeTFHEExecutorAddress").setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = (await ethers.getSigners())[9].address; + const execAddress = ethers.getCreateAddress({ + from: deployer, + nonce: 1, // using nonce of 1 for the TFHEExecutor contract + }); + const envFilePath = path.join(__dirname, "../node_modules/fhevm/lib/.env.exec"); + const content = `TFHE_EXECUTOR_CONTRACT_ADDRESS=${execAddress}\n`; + try { + fs.writeFileSync(envFilePath, content, { flag: "w" }); + console.log(`TFHE Executor address ${execAddress} written successfully!`); + } catch (err) { + console.error("Failed to write TFHE Executor address:", err); + } + + const solidityTemplateCoprocessor = `// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.24; + +address constant fhevmCoprocessorAdd = ${execAddress};\n`; + + try { + fs.writeFileSync("node_modules/fhevm/lib/FHEVMCoprocessorAddress.sol", solidityTemplateCoprocessor, { + encoding: "utf8", + flag: "w", + }); + console.log("node_modules/fhevm/lib/FHEVMCoprocessorAddress.sol file generated successfully!"); + } catch (error) { + console.error("Failed to write node_modules/fhevm/lib/FHEVMCoprocessorAddress.sol", error); + } +}); + +task("task:computeKMSVerifierAddress").setAction(async function (taskArguments: TaskArguments, { ethers }) { + const deployer = (await ethers.getSigners())[9].address; + const kmsVerifierAddress = ethers.getCreateAddress({ + from: deployer, + nonce: 2, // using nonce of 2 for the Kms Verifier contract + }); + const envFilePath = path.join(__dirname, "../node_modules/fhevm/lib/.env.kmsverifier"); + const content = `KMS_VERIFIER_CONTRACT_ADDRESS=${kmsVerifierAddress}\n`; + try { + fs.writeFileSync(envFilePath, content, { flag: "w" }); + console.log(`KMS Verifier address ${kmsVerifierAddress} written successfully!`); + } catch (err) { + console.error("Failed to write KMS Verifier address:", err); + } + + const solidityTemplate = `// SPDX-License-Identifier: BSD-3-Clause-Clear + +pragma solidity ^0.8.24; + +address constant KMS_VERIFIER_CONTRACT_ADDRESS = ${kmsVerifierAddress};\n`; + + try { + fs.writeFileSync("node_modules/fhevm/lib/KMSVerifierAddress.sol", solidityTemplate, { + encoding: "utf8", + flag: "w", + }); + console.log("node_modules/fhevm/lib/KMSVerifierAddress.sol file generated successfully!"); + } catch (error) { + console.error("Failed to write node_modules/fhevm/lib/KMSVerifierAddress.sol", error); + } +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/test/asyncDecrypt.ts b/packages/foundry/contracts/hooks/Hookathon/test/asyncDecrypt.ts new file mode 100644 index 00000000..7dff151a --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/asyncDecrypt.ts @@ -0,0 +1,160 @@ +import dotenv from "dotenv"; +import fs from "fs"; +import { ethers, network } from "hardhat"; + +import { GatewayContract } from "../types"; +import { awaitCoprocessor, getClearText } from "./coprocessorUtils"; +import { waitNBlocks } from "./utils"; + +const networkName = network.name; + +const parsedEnvACL = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.acl")); +const aclAdd = parsedEnvACL.ACL_CONTRACT_ADDRESS.replace(/^0x/, "").replace(/^0+/, "").toLowerCase(); + +const CiphertextType = { + 0: "bool", + 1: "uint8", // corresponding to euint4 + 2: "uint8", // corresponding to euint8 + 3: "uint16", + 4: "uint32", + 5: "uint64", + 6: "uint128", + 7: "address", + 11: "bytes", +}; + +const currentTime = (): string => { + const now = new Date(); + return now.toLocaleTimeString("en-US", { hour12: true, hour: "numeric", minute: "numeric", second: "numeric" }); +}; + +const parsedEnv = dotenv.parse(fs.readFileSync("node_modules/fhevm/gateway/.env.gateway")); +const privKeyRelayer = process.env.PRIVATE_KEY_GATEWAY_RELAYER; +const relayer = new ethers.Wallet(privKeyRelayer!, ethers.provider); + +const argEvents = + "(uint256 indexed requestID, uint256[] cts, address contractCaller, bytes4 callbackSelector, uint256 msgValue, uint256 maxTimestamp, bool passSignaturesToCaller)"; +const ifaceEventDecryption = new ethers.Interface(["event EventDecryption" + argEvents]); + +const argEvents2 = "(uint256 indexed requestID, bool success, bytes result)"; +const ifaceResultCallback = new ethers.Interface(["event ResultCallback" + argEvents2]); + +let gateway: GatewayContract; +let firstBlockListening: number; +let lastBlockSnapshotForDecrypt: number; + +export const asyncDecrypt = async (): Promise => { + firstBlockListening = await ethers.provider.getBlockNumber(); + if (networkName === "hardhat" && hre.__SOLIDITY_COVERAGE_RUNNING !== true) { + // evm_snapshot is not supported in coverage mode + await ethers.provider.send("set_lastBlockSnapshotForDecrypt", [firstBlockListening]); + } + // this function will emit logs for every request and fulfilment of a decryption + gateway = await ethers.getContractAt( + "fhevm/gateway/GatewayContract.sol:GatewayContract", + parsedEnv.GATEWAY_CONTRACT_PREDEPLOY_ADDRESS, + ); + await gateway.on( + "EventDecryption", + async (requestID, cts, contractCaller, callbackSelector, msgValue, maxTimestamp, eventData) => { + const blockNumber = eventData.log.blockNumber; + console.log(`${await currentTime()} - Requested decrypt on block ${blockNumber} (requestID ${requestID})`); + }, + ); + await gateway.on("ResultCallback", async (requestID, success, result, eventData) => { + const blockNumber = eventData.log.blockNumber; + console.log(`${await currentTime()} - Fulfilled decrypt on block ${blockNumber} (requestID ${requestID})`); + }); +}; + +export const awaitAllDecryptionResults = async (): Promise => { + gateway = await ethers.getContractAt( + "fhevm/gateway/GatewayContract.sol:GatewayContract", + parsedEnv.GATEWAY_CONTRACT_PREDEPLOY_ADDRESS, + ); + const provider = ethers.provider; + if (networkName === "hardhat" && hre.__SOLIDITY_COVERAGE_RUNNING !== true) { + // evm_snapshot is not supported in coverage mode + lastBlockSnapshotForDecrypt = await provider.send("get_lastBlockSnapshotForDecrypt"); + if (lastBlockSnapshotForDecrypt < firstBlockListening) { + firstBlockListening = lastBlockSnapshotForDecrypt + 1; + } + } + await fulfillAllPastRequestsIds(networkName === "hardhat"); + firstBlockListening = (await ethers.provider.getBlockNumber()) + 1; + if (networkName === "hardhat" && hre.__SOLIDITY_COVERAGE_RUNNING !== true) { + // evm_snapshot is not supported in coverage mode + await provider.send("set_lastBlockSnapshotForDecrypt", [firstBlockListening]); + } +}; + +const getAlreadyFulfilledDecryptions = async (): Promise<[bigint]> => { + let results = []; + const eventDecryptionResult = await gateway.filters.ResultCallback().getTopicFilter(); + const filterDecryptionResult = { + address: process.env.GATEWAY_CONTRACT_PREDEPLOY_ADDRESS, + fromBlock: firstBlockListening, + toBlock: "latest", + topics: eventDecryptionResult, + }; + const pastResults = await ethers.provider.getLogs(filterDecryptionResult); + results = results.concat(pastResults.map((result) => ifaceResultCallback.parseLog(result).args[0])); + + return results; +}; + +const allTrue = (arr: boolean[], fn = Boolean) => arr.every(fn); + +const fulfillAllPastRequestsIds = async (mocked: boolean) => { + const eventDecryption = await gateway.filters.EventDecryption().getTopicFilter(); + const results = await getAlreadyFulfilledDecryptions(); + const filterDecryption = { + address: process.env.GATEWAY_CONTRACT_PREDEPLOY_ADDRESS, + fromBlock: firstBlockListening, + toBlock: "latest", + topics: eventDecryption, + }; + const pastRequests = await ethers.provider.getLogs(filterDecryption); + for (const request of pastRequests) { + const event = ifaceEventDecryption.parseLog(request); + const requestID = event.args[0]; + const handles = event.args[1]; + const typesList = handles.map((handle) => parseInt(handle.toString(16).slice(-4, -2), 16)); + const msgValue = event.args[4]; + if (!results.includes(requestID)) { + // if request is not already fulfilled + if (mocked) { + // in mocked mode, we trigger the decryption fulfillment manually + await awaitCoprocessor(); + + // first check tat all handles are allowed for decryption + const aclFactory = await ethers.getContractFactory("fhevm/lib/ACL.sol:ACL"); + const acl = aclFactory.attach(`0x${aclAdd}`); + const isAllowedForDec = await Promise.all(handles.map(async (handle) => acl.allowedForDecryption(handle))); + if (!allTrue(isAllowedForDec)) { + throw new Error("Some handle is not authorized for decryption"); + } + + const types = typesList.map((num) => CiphertextType[num]); + const values = await Promise.all(handles.map(async (handle) => BigInt(await getClearText(handle)))); + const valuesFormatted = values.map((value, index) => + types[index] === "address" ? "0x" + value.toString(16).padStart(40, "0") : value, + ); + const valuesFormatted2 = valuesFormatted.map((value, index) => + types[index] === "bytes" ? "0x" + value.toString(16).padStart(512, "0") : value, + ); + + const abiCoder = new ethers.AbiCoder(); + const encodedData = abiCoder.encode(["uint256", ...types], [31, ...valuesFormatted2]); // 31 is just a dummy uint256 requestID to get correct abi encoding for the remaining arguments (i.e everything except the requestID) + const calldata = "0x" + encodedData.slice(66); // we just pop the dummy requestID to get the correct value to pass for `decryptedCts` + + const tx = await gateway.connect(relayer).fulfillRequest(requestID, calldata, [], { value: msgValue }); + await tx.wait(); + } else { + // in fhEVM mode we must wait until the gateway service relayer submits the decryption fulfillment tx + await waitNBlocks(1); + await fulfillAllPastRequestsIds(mocked); + } + } + } +}; diff --git a/packages/foundry/contracts/hooks/Hookathon/test/coprocessorUtils.ts b/packages/foundry/contracts/hooks/Hookathon/test/coprocessorUtils.ts new file mode 100644 index 00000000..12bab6b2 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/coprocessorUtils.ts @@ -0,0 +1,893 @@ +import dotenv from "dotenv"; +import { log2 } from "extra-bigint"; +import * as fs from "fs"; +import { ethers } from "hardhat"; +import hre from "hardhat"; +import { Database } from "sqlite3"; + +const parsedEnvCoprocessor = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.exec")); +const coprocAdd = parsedEnvCoprocessor.TFHE_EXECUTOR_CONTRACT_ADDRESS.replace(/^0x/, "") + .replace(/^0+/, "") + .toLowerCase(); + +let firstBlockListening = 0; +let lastBlockSnapshot = 0; +let lastCounterRand = 0; +let counterRand = 0; + +const contractABI = JSON.parse(fs.readFileSync("abi/TFHEExecutor.json").toString()).abi; + +const iface = new ethers.Interface(contractABI); + +const functions = iface.fragments.filter((fragment) => fragment.type === "function"); + +const selectors = functions.reduce((acc, func) => { + const signature = `${func.name}(${func.inputs.map((input) => input.type).join(",")})`; + acc[func.selector] = signature; + return acc; +}, {}); + +//const db = new Database('./sql.db'); // on-disk db for debugging +const db = new Database(":memory:"); + +function insertSQL(handle: string, clearText: bigint, replace: boolean = false) { + if (replace) { + // this is useful if using snapshots while sampling different random numbers on each revert + db.run("INSERT OR REPLACE INTO ciphertexts (handle, clearText) VALUES (?, ?)", [handle, clearText.toString()]); + } else { + db.run("INSERT OR IGNORE INTO ciphertexts (handle, clearText) VALUES (?, ?)", [handle, clearText.toString()]); + } +} + +// Decrypt any handle, bypassing ACL +// WARNING : only for testing or internal use +export const getClearText = async (handle: bigint): Promise => { + const handleStr = "0x" + handle.toString(16).padStart(64, "0"); + + return new Promise((resolve, reject) => { + let attempts = 0; + const maxRetries = 10; + + function executeQuery() { + db.get("SELECT clearText FROM ciphertexts WHERE handle = ?", [handleStr], (err, row) => { + if (err) { + reject(new Error(`Error querying database: ${err.message}`)); + } else if (row) { + resolve(row.clearText); + } else if (attempts < maxRetries) { + attempts++; + executeQuery(); + } else { + reject(new Error("No record found after maximum retries")); + } + }); + } + + executeQuery(); + }); +}; + +db.serialize(() => db.run("CREATE TABLE IF NOT EXISTS ciphertexts (handle BINARY PRIMARY KEY,clearText TEXT)")); + +enum Operators { + fheAdd = 0, + fheSub, + fheMul, + fheDiv, + fheRem, + fheBitAnd, + fheBitOr, + fheBitXor, + fheShl, + fheShr, + fheRotl, + fheRotr, + fheEq, + fheNe, + fheGe, + fheGt, + fheLe, + fheLt, + fheMin, + fheMax, + fheNeg, + fheNot, + verifyCiphertext, + cast, + trivialEncrypt, + fheIfThenElse, + fheRand, + fheRandBounded, +} + +interface EvmState { + stack: string[]; + memory: string[]; +} + +function extractCalldata(memory: string[], offset: number, size: number): string { + const startIndex = Math.floor(offset / 32); + const endIndex = Math.ceil((offset + size) / 32); + const memorySegments = memory.slice(startIndex, endIndex); + let calldata = ""; + for (let i = 0; i < memorySegments.length; i++) { + calldata += memorySegments[i]; + } + const calldataStart = (offset % 32) * 2; + const calldataEnd = calldataStart + size * 2; + return calldata.slice(calldataStart, calldataEnd); +} + +const TypesBytesSize = { + 0: 1, //ebool + 1: 1, //euint4 + 2: 1, //euint8 + 3: 2, //euint16 + 4: 4, //euint32 + 5: 8, //euint64 + 6: 16, //euint128 + 7: 20, //eaddress + 8: 32, //euint256 + 9: 64, //ebytes64 + 10: 128, //ebytes128 + 11: 256, //ebytes256 +}; + +const NumBits = { + 0: 1n, //ebool + 1: 4n, //euint4 + 2: 8n, //euint8 + 3: 16n, //euint16 + 4: 32n, //euint32 + 5: 64n, //euint64 + 6: 128n, //euint128 + 7: 160n, //eaddress + 8: 256n, //euint256 + 9: 512n, //ebytes64 + 10: 1024n, //ebytes128 + 11: 2048n, //ebytes256 +}; + +const HANDLE_VERSION = 0; + +export function numberToEvenHexString(num: number) { + if (typeof num !== "number" || num < 0) { + throw new Error("Input should be a non-negative number."); + } + let hexString = num.toString(16); + if (hexString.length % 2 !== 0) { + hexString = "0" + hexString; + } + return hexString; +} + +function appendType(handle: string, type: number): string { + return handle.slice(0, -4) + numberToEvenHexString(type) + numberToEvenHexString(HANDLE_VERSION); +} + +function getRandomBigInt(numBits: number): bigint { + if (numBits <= 0) { + throw new Error("Number of bits must be greater than 0"); + } + const numBytes = Math.ceil(numBits / 8); + const randomBytes = new Uint8Array(numBytes); + crypto.getRandomValues(randomBytes); + let randomBigInt = BigInt(0); + for (let i = 0; i < numBytes; i++) { + randomBigInt = (randomBigInt << BigInt(8)) | BigInt(randomBytes[i]); + } + const mask = (BigInt(1) << BigInt(numBits)) - BigInt(1); + randomBigInt = randomBigInt & mask; + return randomBigInt; +} + +async function insertHandle(obj2: EvmState, validIdxes: [number]) { + const obj = obj2.value; + if (isCoprocAdd(obj!.stack.at(-2))) { + const argsOffset = Number(`0x${obj!.stack.at(-4)}`); + const argsSize = Number(`0x${obj!.stack.at(-5)}`); + const calldata = extractCalldata(obj.memory, argsOffset, argsSize); + const currentSelector = "0x" + calldata.slice(0, 8); + const decodedData = iface.decodeFunctionData(currentSelector, "0x" + calldata); + + let handle; + let clearText; + let clearLHS; + let clearRHS; + let lhsType; + let resultType; + let shift; + + switch (selectors[currentSelector]) { + case "trivialEncrypt(uint256,bytes1)": + resultType = Number(decodedData[1]); + clearText = decodedData[0]; + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "bytes1"], + [Operators.trivialEncrypt, decodedData[0], decodedData[1]], + ), + ); + handle = appendType(handle, resultType); + insertSQL(handle, clearText); + break; + + case "fheAdd(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheAdd, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) + decodedData[1]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) + BigInt(clearRHS); + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheSub(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheSub, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) - decodedData[1]; + if (clearText < 0n) clearText = clearText + 2n ** NumBits[resultType]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) - BigInt(clearRHS); + if (clearText < 0n) clearText = clearText + 2n ** NumBits[resultType]; + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheMul(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheMul, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) * decodedData[1]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) * BigInt(clearRHS); + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheDiv(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheDiv, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) / decodedData[1]; + } else { + throw new Error("Non-scalar div not implemented yet"); + } + insertSQL(handle, clearText); + break; + + case "fheRem(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheRem, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) % decodedData[1]; + } else { + throw new Error("Non-scalar rem not implemented yet"); + } + insertSQL(handle, clearText); + break; + + case "fheBitAnd(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheBitAnd, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) & decodedData[1]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) & BigInt(clearRHS); + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheBitOr(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheBitOr, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) | decodedData[1]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) | BigInt(clearRHS); + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheBitXor(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheBitXor, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) ^ decodedData[1]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) ^ BigInt(clearRHS); + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheShl(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheShl, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) << decodedData[1] % NumBits[resultType]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) << BigInt(clearRHS) % NumBits[resultType]; + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheShr(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheShr, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) >> decodedData[1] % NumBits[resultType]; + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) >> BigInt(clearRHS) % NumBits[resultType]; + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheRotl(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheRotl, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + + if (decodedData[2] === "0x01") { + shift = decodedData[1] % NumBits[resultType]; + clearText = (BigInt(clearLHS) << shift) | (BigInt(clearLHS) >> (NumBits[resultType] - shift)); + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + shift = BigInt(clearRHS) % NumBits[resultType]; + clearText = (BigInt(clearLHS) << shift) | (BigInt(clearLHS) >> (NumBits[resultType] - shift)); + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheRotr(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheRotr, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + + if (decodedData[2] === "0x01") { + shift = decodedData[1] % NumBits[resultType]; + clearText = (BigInt(clearLHS) >> shift) | (BigInt(clearLHS) << (NumBits[resultType] - shift)); + clearText = clearText % 2n ** NumBits[resultType]; + } else { + clearRHS = await getClearText(decodedData[1]); + shift = BigInt(clearRHS) % NumBits[resultType]; + clearText = (BigInt(clearLHS) >> shift) | (BigInt(clearLHS) << (NumBits[resultType] - shift)); + clearText = clearText % 2n ** NumBits[resultType]; + } + insertSQL(handle, clearText); + break; + + case "fheEq(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheEq, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, 0); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) === decodedData[1] ? 1n : 0n; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) === BigInt(clearRHS) ? 1n : 0n; + } + insertSQL(handle, clearText); + break; + + case "fheNe(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheNe, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, 0); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) !== decodedData[1] ? 1n : 0n; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) !== BigInt(clearRHS) ? 1n : 0n; + } + insertSQL(handle, clearText); + break; + + case "fheGe(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheGe, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, 0); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) >= decodedData[1] ? 1n : 0n; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) >= BigInt(clearRHS) ? 1n : 0n; + } + insertSQL(handle, clearText); + break; + + case "fheGt(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheGt, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, 0); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) > decodedData[1] ? 1n : 0n; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) > BigInt(clearRHS) ? 1n : 0n; + } + insertSQL(handle, clearText); + break; + + case "fheLe(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheLe, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, 0); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) <= decodedData[1] ? 1n : 0n; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) <= BigInt(clearRHS) ? 1n : 0n; + } + insertSQL(handle, clearText); + break; + + case "fheLt(uint256,uint256,bytes1)": + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheLt, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, 0); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) < decodedData[1] ? 1n : 0n; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) < BigInt(clearRHS) ? 1n : 0n; + } + insertSQL(handle, clearText); + break; + + case "fheMax(uint256,uint256,bytes1)": + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheMax, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) > decodedData[1] ? clearLHS : decodedData[1]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) > BigInt(clearRHS) ? clearLHS : clearRHS; + } + insertSQL(handle, clearText); + break; + + case "fheMin(uint256,uint256,bytes1)": + lhsType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + resultType = lhsType; + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "bytes1"], + [Operators.fheMin, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, resultType); + clearLHS = await getClearText(decodedData[0]); + if (decodedData[2] === "0x01") { + clearText = BigInt(clearLHS) < decodedData[1] ? clearLHS : decodedData[1]; + } else { + clearRHS = await getClearText(decodedData[1]); + clearText = BigInt(clearLHS) < BigInt(clearRHS) ? clearLHS : clearRHS; + } + insertSQL(handle, clearText); + break; + + case "cast(uint256,bytes1)": + resultType = parseInt(decodedData[1]); + handle = ethers.keccak256( + ethers.solidityPacked(["uint8", "uint256", "bytes1"], [Operators.cast, decodedData[0], decodedData[1]]), + ); + clearText = BigInt(await getClearText(decodedData[0])) % 2n ** NumBits[resultType]; + handle = appendType(handle, resultType); + insertSQL(handle, clearText); + break; + + case "fheNot(uint256)": + resultType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + handle = ethers.keccak256(ethers.solidityPacked(["uint8", "uint256"], [Operators.fheNot, decodedData[0]])); + handle = appendType(handle, resultType); + clearText = BigInt(await getClearText(decodedData[0])); + clearText = bitwiseNotUintBits(clearText, Number(NumBits[resultType])); + insertSQL(handle, clearText); + break; + + case "fheNeg(uint256)": + resultType = parseInt(decodedData[0].toString(16).slice(-4, -2), 16); + handle = ethers.keccak256(ethers.solidityPacked(["uint8", "uint256"], [Operators.fheNeg, decodedData[0]])); + handle = appendType(handle, resultType); + clearText = BigInt(await getClearText(decodedData[0])); + clearText = bitwiseNotUintBits(clearText, Number(NumBits[resultType])); + clearText = (clearText + 1n) % 2n ** NumBits[resultType]; + insertSQL(handle, clearText); + break; + + case "verifyCiphertext(bytes32,address,bytes,bytes1)": + { + handle = decodedData[0]; + const type = parseInt(handle.slice(-4, -2), 16); + if (type !== 11) { + //not an ebytes256 + const typeSize = TypesBytesSize[type]; + const idx = parseInt(handle.slice(-6, -4), 16); + const inputProof = decodedData[2].replace(/^0x/, ""); + clearText = BigInt("0x" + inputProof.slice(2 + 2 * 53 * idx, 2 + 2 * typeSize + 2 * 53 * idx)); + insertSQL(handle, clearText); + } else { + const inputProof = decodedData[2].replace(/^0x/, ""); + clearText = BigInt("0x" + inputProof.slice(2, 2 + 2 * 256)); + insertSQL(handle, clearText); + } + } + break; + + case "fheIfThenElse(uint256,uint256,uint256)": + { + resultType = parseInt(decodedData[1].toString(16).slice(-4, -2), 16); + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "uint256", "uint256"], + [Operators.fheIfThenElse, decodedData[0], decodedData[1], decodedData[2]], + ), + ); + handle = appendType(handle, resultType); + const clearControl = BigInt(await getClearText(decodedData[0])); + const clearIfTrue = BigInt(await getClearText(decodedData[1])); + const clearIfFalse = BigInt(await getClearText(decodedData[2])); + if (clearControl === 1n) { + clearText = clearIfTrue; + } else { + clearText = clearIfFalse; + } + insertSQL(handle, clearText); + } + break; + + case "fheRand(bytes1)": + if (validIdxes.includes(obj2.index)) { + resultType = parseInt(decodedData[0], 16); + handle = ethers.keccak256( + ethers.solidityPacked(["uint8", "bytes1", "uint256"], [Operators.fheRand, decodedData[0], counterRand]), + ); + handle = appendType(handle, resultType); + clearText = getRandomBigInt(Number(NumBits[resultType])); + insertSQL(handle, clearText, true); + counterRand++; + } + break; + + case "fheRandBounded(uint256,bytes1)": + if (validIdxes.includes(obj2.index)) { + resultType = parseInt(decodedData[1], 16); + handle = ethers.keccak256( + ethers.solidityPacked( + ["uint8", "uint256", "bytes1", "uint256"], + [Operators.fheRandBounded, decodedData[0], decodedData[1], counterRand], + ), + ); + handle = appendType(handle, resultType); + clearText = getRandomBigInt(Number(log2(BigInt(decodedData[0])))); + insertSQL(handle, clearText, true); + counterRand++; + } + break; + } + } +} + +function bitwiseNotUintBits(value: bigint, numBits: number) { + if (typeof value !== "bigint") { + throw new TypeError("The input value must be a BigInt."); + } + if (typeof numBits !== "number" || numBits <= 0) { + throw new TypeError("The numBits parameter must be a positive integer."); + } + + // Create the mask with numBits bits set to 1 + const BIT_MASK = (BigInt(1) << BigInt(numBits)) - BigInt(1); + + return ~value & BIT_MASK; +} + +function isCoprocAdd(longString: string): boolean { + const strippedLongString = longString.replace(/^0+/, ""); + const normalizedLongString = strippedLongString.toLowerCase(); + return normalizedLongString === coprocAdd; +} + +async function processLogs(trace, validSubcallsIndexes) { + for (const obj of trace.structLogs + .map((value, index) => ({ value, index })) + .filter((obj) => obj.value.op === "CALL")) { + await insertHandle(obj, validSubcallsIndexes); + } +} + +export const awaitCoprocessor = async (): Promise => { + const pastTxHashes = await getAllPastTransactionHashes(); + for (const txHash of pastTxHashes) { + const trace = await ethers.provider.send("debug_traceTransaction", [txHash[0]]); + + if (!trace.failed) { + const callTree = await buildCallTree(trace, txHash[1]); + const validSubcallsIndexes = getValidSubcallsIds(callTree)[1]; + await processLogs(trace, validSubcallsIndexes); + } + } +}; + +async function getAllPastTransactionHashes() { + const provider = ethers.provider; + const latestBlockNumber = await provider.getBlockNumber(); + const txHashes = []; + + if (hre.__SOLIDITY_COVERAGE_RUNNING !== true) { + // evm_snapshot is not supported in coverage mode + [lastBlockSnapshot, lastCounterRand] = await provider.send("get_lastBlockSnapshot"); + if (lastBlockSnapshot < firstBlockListening) { + firstBlockListening = lastBlockSnapshot + 1; + counterRand = Number(lastCounterRand); + } + } + + // Iterate through all blocks and collect transaction hashes + for (let i = firstBlockListening; i <= latestBlockNumber; i++) { + const block = await provider.getBlock(i, true); + block!.transactions.forEach((tx, index) => { + const rcpt = block?.prefetchedTransactions[index]; + txHashes.push([tx, { to: rcpt.to, status: rcpt.status }]); + }); + } + firstBlockListening = latestBlockNumber + 1; + if (hre.__SOLIDITY_COVERAGE_RUNNING !== true) { + // evm_snapshot is not supported in coverage mode + await provider.send("set_lastBlockSnapshot", [firstBlockListening]); + } + return txHashes; +} + +async function buildCallTree(trace, receipt) { + const structLogs = trace.structLogs; + + const callStack = []; + const callTree = { + id: 0, + type: receipt.to ? "TOPCALL" : "TOPCREATE", + revert: receipt.status === 1 ? false : true, + to: receipt.to ? receipt.to : null, + calls: [], + indexTrace: 0, + }; + let currentNode = callTree; + const lenStructLogs = structLogs.length; + let index = 1; + for (const [i, log] of structLogs.entries()) { + if (i < lenStructLogs - 1) { + if (structLogs[i].depth - structLogs[i + 1].depth === 1) { + if (!["RETURN", "SELFDESTRUCT", "STOP", "REVERT", "INVALID"].includes(structLogs[i].op)) { + currentNode.outofgasOrOther = true; + currentNode = callStack.pop(); + } + } + } + + switch (log.op) { + case "CALL": + case "DELEGATECALL": + case "CALLCODE": + case "STATICCALL": + case "CREATE": + case "CREATE2": + if (i < lenStructLogs - 1) { + if (structLogs[i + 1].depth - structLogs[i].depth === 1) { + const newNode = { + id: index, + type: log.op, + to: log.stack[log.stack.length - 2], + calls: [], + revert: true, + outofgasOrOther: false, + indexTrace: i, + }; + currentNode.calls.push(newNode); + callStack.push(currentNode); + currentNode = newNode; + index += 1; + } + } + break; + case "RETURN": // some edge case probably not handled well : if memory expansion cost on RETURN exceeds the remaining gas in current subcall, but it's OK for a mocked mode + case "SELFDESTRUCT": // some edge case probably not handled well : if there is not enough gas remaining on SELFDESTRUCT, but it's OK for a mocked mode + case "STOP": + currentNode.revert = false; + currentNode = callStack.pop(); + break; + case "REVERT": + case "INVALID": + currentNode = callStack.pop(); + break; + } + + switch (log.op) { + case "CREATE": + case "CREATE2": + currentNode.to = null; + break; + } + } + return callTree; +} + +function getValidSubcallsIds(tree) { + const result = []; + const resultIndexes = []; + + function traverse(node, ancestorReverted) { + if (ancestorReverted || node.revert) { + ancestorReverted = true; + } else { + result.push(node.id); + resultIndexes.push(node.indexTrace); + } + for (const child of node.calls) { + traverse(child, ancestorReverted); + } + } + + traverse(tree, false); + + return [result, resultIndexes]; +} diff --git a/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.fixture.ts b/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.fixture.ts new file mode 100644 index 00000000..a8708ea9 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.fixture.ts @@ -0,0 +1,15 @@ +import { ethers } from "hardhat"; + +import type { EncryptedERC20 } from "../../types"; +import { getSigners } from "../signers"; + +export async function deployEncryptedERC20Fixture(): Promise { + const signers = await getSigners(); + + const contractFactory = await ethers.getContractFactory("EncryptedERC20"); + const contract = await contractFactory.connect(signers.alice).deploy("Naraggara", "NARA"); // City of Zama's battle + await contract.waitForDeployment(); + console.log("Encrypted ERC20 Contract address is: ", await contract.getAddress()); + + return contract; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.ts b/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.ts new file mode 100644 index 00000000..ca69ad8e --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.ts @@ -0,0 +1,114 @@ +import { expect } from "chai"; + +import { createInstances } from "../instance"; +import { getSigners, initSigners } from "../signers"; +import { deployEncryptedERC20Fixture } from "./EncryptedERC20.fixture"; +import { asyncDecrypt, awaitAllDecryptionResults } from "../asyncDecrypt"; + +describe("EncryptedERC20", function () { + before(async function () { + await initSigners(); + this.signers = await getSigners(); + }); + + beforeEach(async function () { + const contract = await deployEncryptedERC20Fixture(); + this.contractAddress = await contract.getAddress(); + this.erc20 = contract; + this.instances = await createInstances(this.signers); + }); + + it("should mint the contract", async function () { + const transaction = await this.erc20.mint(1000); + await transaction.wait(); + + // Reencrypt Alice's balance + const balanceHandleAlice = await this.erc20.balanceOf(this.signers.alice); + const { publicKey: publicKeyAlice, privateKey: privateKeyAlice } = this.instances.alice.generateKeypair(); + const eip712 = this.instances.alice.createEIP712(publicKeyAlice, this.contractAddress); + const signatureAlice = await this.signers.alice.signTypedData( + eip712.domain, + { Reencrypt: eip712.types.Reencrypt }, + eip712.message, + ); + const balanceAlice = await this.instances.alice.reencrypt( + balanceHandleAlice, + privateKeyAlice, + publicKeyAlice, + signatureAlice.replace("0x", ""), + this.contractAddress, + this.signers.alice.address, + ); + expect(balanceAlice).to.equal(1000); + + const totalSupply = await this.erc20.totalSupply(); + expect(totalSupply).to.equal(1000); + }); + + it("should transfer tokens between two users", async function () { + const transaction = await this.erc20.mint(10000); + const t1 = await transaction.wait(); + expect(t1?.status).to.eq(1); + + const input = this.instances.alice.createEncryptedInput(this.contractAddress, this.signers.alice.address); + input.add64(1337); + const encryptedTransferAmount = input.encrypt(); + const tx = await this.erc20["transfer(address,bytes32,bytes)"]( + this.signers.bob.address, + encryptedTransferAmount.handles[0], + encryptedTransferAmount.inputProof, + ); + const t2 = await tx.wait(); + expect(t2?.status).to.eq(1); + + // Reencrypt Alice's balance + const balanceHandleAlice = await this.erc20.balanceOf(this.signers.alice); + const { publicKey: publicKeyAlice, privateKey: privateKeyAlice } = this.instances.alice.generateKeypair(); + const eip712 = this.instances.alice.createEIP712(publicKeyAlice, this.contractAddress); + const signatureAlice = await this.signers.alice.signTypedData( + eip712.domain, + { Reencrypt: eip712.types.Reencrypt }, + eip712.message, + ); + const balanceAlice = await this.instances.alice.reencrypt( + balanceHandleAlice, + privateKeyAlice, + publicKeyAlice, + signatureAlice.replace("0x", ""), + this.contractAddress, + this.signers.alice.address, + ); + + expect(balanceAlice).to.equal(10000 - 1337); + + // Reencrypt Bob's balance + const balanceHandleBob = await this.erc20.balanceOf(this.signers.bob); + + const { publicKey: publicKeyBob, privateKey: privateKeyBob } = this.instances.bob.generateKeypair(); + const eip712Bob = this.instances.bob.createEIP712(publicKeyBob, this.contractAddress); + const signatureBob = await this.signers.bob.signTypedData( + eip712Bob.domain, + { Reencrypt: eip712Bob.types.Reencrypt }, + eip712Bob.message, + ); + const balanceBob = await this.instances.bob.reencrypt( + balanceHandleBob, + privateKeyBob, + publicKeyBob, + signatureBob.replace("0x", ""), + this.contractAddress, + this.signers.bob.address, + ); + + expect(balanceBob).to.equal(1337); + }); + + it("should be able to decrypt the number using Gateway", async function () { + const decrypteTransaction = await this.erc20.decryptARandomNumber(); + await decrypteTransaction.wait(); + await asyncDecrypt(); + await awaitAllDecryptionResults(); + console.log("The decrypted Boolean value is " + (await this.erc20.yBool()) + "\n"); + }); + +}); diff --git a/packages/foundry/contracts/hooks/Hookathon/test/execution.ts b/packages/foundry/contracts/hooks/Hookathon/test/execution.ts new file mode 100644 index 00000000..3d023b2d --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/execution.ts @@ -0,0 +1,289 @@ +import { BaseContract, BigNumberish, Signer, ethers } from "ethers"; + +import { Safe } from "../types"; + +const AddressZero = "0x0000000000000000000000000000000000000000"; + +export const EIP_DOMAIN = { + EIP712Domain: [ + { type: "uint256", name: "chainId" }, + { type: "address", name: "verifyingContract" }, + ], +}; + +export const EIP712_SAFE_TX_TYPE = { + // "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)" + SafeTx: [ + { type: "address", name: "to" }, + { type: "uint256", name: "value" }, + { type: "bytes", name: "data" }, + { type: "uint8", name: "operation" }, + { type: "uint256", name: "safeTxGas" }, + { type: "uint256", name: "baseGas" }, + { type: "uint256", name: "gasPrice" }, + { type: "address", name: "gasToken" }, + { type: "address", name: "refundReceiver" }, + { type: "uint256", name: "nonce" }, + ], +}; + +export const EIP712_SAFE_MESSAGE_TYPE = { + // "SafeMessage(bytes message)" + SafeMessage: [{ type: "bytes", name: "message" }], +}; + +export interface MetaTransaction { + to: string; + value: BigNumberish; + data: string; + operation: number; +} + +export interface SafeTransaction extends MetaTransaction { + safeTxGas: BigNumberish; + baseGas: BigNumberish; + gasPrice: BigNumberish; + gasToken: string; + refundReceiver: string; + nonce: BigNumberish; +} + +export interface SafeSignature { + signer: string; + data: string; + // a flag to indicate if the signature is a contract signature and the data has to be appended to the dynamic part of signature bytes + dynamic?: true; +} + +export const calculateSafeDomainSeparator = (safeAddress: string, chainId: BigNumberish): string => { + return ethers.TypedDataEncoder.hashDomain({ verifyingContract: safeAddress, chainId }); +}; + +export const preimageSafeTransactionHash = ( + safeAddress: string, + safeTx: SafeTransaction, + chainId: BigNumberish, +): string => { + return ethers.TypedDataEncoder.encode({ verifyingContract: safeAddress, chainId }, EIP712_SAFE_TX_TYPE, safeTx); +}; + +export const calculateSafeTransactionHash = ( + safeAddress: string, + safeTx: SafeTransaction, + chainId: BigNumberish, +): string => { + return ethers.TypedDataEncoder.hash({ verifyingContract: safeAddress, chainId }, EIP712_SAFE_TX_TYPE, safeTx); +}; + +export const preimageSafeMessageHash = (safeAddress: string, message: string, chainId: BigNumberish): string => { + return ethers.TypedDataEncoder.encode({ verifyingContract: safeAddress, chainId }, EIP712_SAFE_MESSAGE_TYPE, { + message, + }); +}; + +export const calculateSafeMessageHash = (safeAddress: string, message: string, chainId: BigNumberish): string => { + return ethers.TypedDataEncoder.hash({ verifyingContract: safeAddress, chainId }, EIP712_SAFE_MESSAGE_TYPE, { + message, + }); +}; + +export const safeApproveHash = async ( + signer: Signer, + safe: Safe, + safeTx: SafeTransaction, + skipOnChainApproval?: boolean, +): Promise => { + if (!skipOnChainApproval) { + if (!signer.provider) throw Error("Provider required for on-chain approval"); + const chainId = (await signer.provider.getNetwork()).chainId; + const safeAddress = await safe.getAddress(); + const typedDataHash = calculateSafeTransactionHash(safeAddress, safeTx, chainId); + const signerSafe = safe.connect(signer); + await signerSafe.approveHash(typedDataHash); + } + const signerAddress = await signer.getAddress(); + return { + signer: signerAddress, + data: + "0x000000000000000000000000" + + signerAddress.slice(2) + + "0000000000000000000000000000000000000000000000000000000000000000" + + "01", + }; +}; + +export const safeSignTypedData = async ( + signer: Signer, + safeAddress: string, + safeTx: SafeTransaction, + chainId?: BigNumberish, +): Promise => { + if (!chainId && !signer.provider) throw Error("Provider required to retrieve chainId"); + const cid = chainId || (await signer.provider!.getNetwork()).chainId; + const signerAddress = await signer.getAddress(); + return { + signer: signerAddress, + data: await signer.signTypedData({ verifyingContract: safeAddress, chainId: cid }, EIP712_SAFE_TX_TYPE, safeTx), + }; +}; + +export const signHash = async (signer: Signer, hash: string): Promise => { + const typedDataHash = ethers.getBytes(hash); + const signerAddress = await signer.getAddress(); + return { + signer: signerAddress, + data: (await signer.signMessage(typedDataHash)).replace(/1b$/, "1f").replace(/1c$/, "20"), + }; +}; + +export const safeSignMessage = async ( + signer: Signer, + safeAddress: string, + safeTx: SafeTransaction, + chainId?: BigNumberish, +): Promise => { + const cid = chainId || (await signer.provider!.getNetwork()).chainId; + return signHash(signer, calculateSafeTransactionHash(safeAddress, safeTx, cid)); +}; + +export const buildContractSignature = (signerAddress: string, signature: string): SafeSignature => { + return { + signer: signerAddress, + data: signature, + dynamic: true, + }; +}; + +export const buildSignatureBytes = (signatures: SafeSignature[]): string => { + const SIGNATURE_LENGTH_BYTES = 65; + signatures.sort((left, right) => left.signer.toLowerCase().localeCompare(right.signer.toLowerCase())); + + let signatureBytes = "0x"; + let dynamicBytes = ""; + for (const sig of signatures) { + if (sig.dynamic) { + /* + A contract signature has a static part of 65 bytes and the dynamic part that needs to be appended + at the end of signature bytes. + The signature format is + Signature type == 0 + Constant part: 65 bytes + {32-bytes signature verifier}{32-bytes dynamic data position}{1-byte signature type} + Dynamic part (solidity bytes): 32 bytes + signature data length + {32-bytes signature length}{bytes signature data} + */ + const dynamicPartPosition = (signatures.length * SIGNATURE_LENGTH_BYTES + dynamicBytes.length / 2) + .toString(16) + .padStart(64, "0"); + const dynamicPartLength = (sig.data.slice(2).length / 2).toString(16).padStart(64, "0"); + const staticSignature = `${sig.signer.slice(2).padStart(64, "0")}${dynamicPartPosition}00`; + const dynamicPartWithLength = `${dynamicPartLength}${sig.data.slice(2)}`; + + signatureBytes += staticSignature; + dynamicBytes += dynamicPartWithLength; + } else { + signatureBytes += sig.data.slice(2); + } + } + + return signatureBytes + dynamicBytes; +}; + +export const logGas = async (message: string, tx: Promise, skip?: boolean): Promise => { + return tx.then(async (result) => { + const receipt = await result.wait(); + if (!skip) console.log(" Used", receipt.gasUsed, `gas for >${message}<`); + return result; + }); +}; + +export const executeTx = async ( + safe: Safe, + safeTx: SafeTransaction, + signatures: SafeSignature[], + overrides?: any, +): Promise => { + const signatureBytes = buildSignatureBytes(signatures); + return safe.execTransaction( + safeTx.to, + safeTx.value, + safeTx.data, + safeTx.operation, + safeTx.safeTxGas, + safeTx.baseGas, + safeTx.gasPrice, + safeTx.gasToken, + safeTx.refundReceiver, + signatureBytes, + overrides || {}, + ); +}; + +export const buildContractCall = async ( + contract: BaseContract, + method: string, + params: any[], + nonce: BigNumberish, + delegateCall?: boolean, + overrides?: Partial, +): Promise => { + const data = contract.interface.encodeFunctionData(method, params); + const contractAddress = await contract.getAddress(); + + return buildSafeTransaction( + Object.assign( + { + to: contractAddress, + data, + operation: delegateCall ? 1 : 0, + nonce, + }, + overrides, + ), + ); +}; + +export const executeTxWithSigners = async (safe: Safe, tx: SafeTransaction, signers: Signer[], overrides?: any) => { + const safeAddress = await safe.getAddress(); + const sigs = await Promise.all(signers.map((signer) => safeSignTypedData(signer, safeAddress, tx))); + return executeTx(safe, tx, sigs, overrides); +}; + +export const executeContractCallWithSigners = async ( + safe: Safe, + contract: BaseContract, + method: string, + params: any[], + signers: Signer[], + delegateCall?: boolean, + overrides?: Partial, +) => { + const tx = await buildContractCall(contract, method, params, await safe.nonce(), delegateCall, overrides); + return executeTxWithSigners(safe, tx, signers); +}; + +export const buildSafeTransaction = (template: { + to: string; + value?: BigNumberish; + data?: string; + operation?: number; + safeTxGas?: BigNumberish; + baseGas?: BigNumberish; + gasPrice?: BigNumberish; + gasToken?: string; + refundReceiver?: string; + nonce: BigNumberish; +}): SafeTransaction => { + return { + to: template.to, + value: template.value || 0, + data: template.data || "0x", + operation: template.operation || 0, + safeTxGas: template.safeTxGas || 0, + baseGas: template.baseGas || 0, + gasPrice: template.gasPrice || 0, + gasToken: template.gasToken || AddressZero, + refundReceiver: template.refundReceiver || AddressZero, + nonce: template.nonce, + }; +}; diff --git a/packages/foundry/contracts/hooks/Hookathon/test/fhevmjsMocked.ts b/packages/foundry/contracts/hooks/Hookathon/test/fhevmjsMocked.ts new file mode 100644 index 00000000..42ee6d27 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/fhevmjsMocked.ts @@ -0,0 +1,302 @@ +import { toBigIntLE } from "bigint-buffer"; +import { toBufferBE } from "bigint-buffer"; +import crypto from "crypto"; +import dotenv from "dotenv"; +import { ethers } from "ethers"; +import * as fs from "fs"; +import hre from "hardhat"; +import { Keccak } from "sha3"; +import { isAddress } from "web3-validator"; + +import { awaitCoprocessor, getClearText } from "./coprocessorUtils"; + +const parsedEnvACL = dotenv.parse(fs.readFileSync("node_modules/fhevm/lib/.env.acl")); +const aclAdd = parsedEnvACL.ACL_CONTRACT_ADDRESS.replace(/^0x/, "").replace(/^0+/, "").toLowerCase(); + +enum Types { + ebool = 0, + euint4, + euint8, + euint16, + euint32, + euint64, + euint128, + eaddress, + euint256, + ebytes64, + ebytes128, + ebytes256, +} + +function bytesToBigInt(byteArray: Uint8Array): bigint { + if (!byteArray || byteArray?.length === 0) { + return BigInt(0); + } + const buffer = Buffer.from(byteArray); + const result = toBigIntLE(buffer); + return result; +} + +function createUintToUint8ArrayFunction(numBits: number) { + const numBytes = Math.ceil(numBits / 8); + return function (uint: number | bigint | boolean) { + const buffer = toBufferBE(BigInt(uint), numBytes); + + // concatenate 32 random bytes at the end of buffer to simulate encryption noise + const randomBytes = crypto.randomBytes(32); + const combinedBuffer = Buffer.concat([buffer, randomBytes]); + + let byteBuffer; + let totalBuffer; + const padBuffer = numBytes <= 20 ? Buffer.alloc(20 - numBytes) : Buffer.alloc(0); // to fit it in an E160List + + switch (numBits) { + case 1: + byteBuffer = Buffer.from([Types.ebool]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer, padBuffer]); + break; + case 4: + byteBuffer = Buffer.from([Types.euint4]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer, padBuffer]); + break; + case 8: + byteBuffer = Buffer.from([Types.euint8]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer, padBuffer]); + break; + case 16: + byteBuffer = Buffer.from([Types.euint16]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer, padBuffer]); + break; + case 32: + byteBuffer = Buffer.from([Types.euint32]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer, padBuffer]); + break; + case 64: + byteBuffer = Buffer.from([Types.euint64]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer, padBuffer]); + break; + case 160: + byteBuffer = Buffer.from([Types.eaddress]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer]); + break; + case 2048: + byteBuffer = Buffer.from([Types.ebytes256]); + totalBuffer = Buffer.concat([byteBuffer, combinedBuffer]); + break; + default: + throw Error("Non-supported numBits"); + } + + return totalBuffer; + }; +} + +export const reencryptRequestMocked = async ( + handle: bigint, + privateKey: string, + publicKey: string, + signature: string, + contractAddress: string, + userAddress: string, +) => { + // Signature checking: + const domain = { + name: "Authorization token", + version: "1", + chainId: hre.network.config.chainId, + verifyingContract: contractAddress, + }; + const types = { + Reencrypt: [{ name: "publicKey", type: "bytes" }], + }; + const value = { + publicKey: `0x${publicKey}`, + }; + const signerAddress = ethers.verifyTypedData(domain, types, value, `0x${signature}`); + const normalizedSignerAddress = ethers.getAddress(signerAddress); + const normalizedUserAddress = ethers.getAddress(userAddress); + if (normalizedSignerAddress !== normalizedUserAddress) { + throw new Error("Invalid EIP-712 signature!"); + } + + // ACL checking + const aclFactory = await hre.ethers.getContractFactory("fhevm/lib/ACL.sol:ACL"); + const acl = aclFactory.attach(`0x${aclAdd}`); + const userAllowed = await acl.persistAllowed(handle, userAddress); + const contractAllowed = await acl.persistAllowed(handle, contractAddress); + const isAllowed = userAllowed && contractAllowed; + if (!isAllowed) { + throw new Error("User is not authorized to reencrypt this handle!"); + } + await awaitCoprocessor(); + return BigInt(await getClearText(handle)); +}; + +export const createEncryptedInputMocked = (contractAddress: string, callerAddress: string) => { + if (!isAddress(contractAddress)) { + throw new Error("Contract address is not a valid address."); + } + + if (!isAddress(callerAddress)) { + throw new Error("User address is not a valid address."); + } + + const values: bigint[] = []; + const bits: (keyof typeof ENCRYPTION_TYPES)[] = []; + return { + addBool(value: boolean | number | bigint) { + if (value == null) throw new Error("Missing value"); + if (typeof value !== "boolean" && typeof value !== "number" && typeof value !== "bigint") + throw new Error("The value must be a boolean, a number or a bigint."); + if ((typeof value !== "bigint" || typeof value !== "number") && Number(value) > 1) + throw new Error("The value must be 1 or 0."); + values.push(BigInt(value)); + bits.push(1); + return this; + }, + add4(value: number | bigint) { + checkEncryptedValue(value, 4); + values.push(BigInt(value)); + bits.push(4); + return this; + }, + add8(value: number | bigint) { + checkEncryptedValue(value, 8); + values.push(BigInt(value)); + bits.push(8); + return this; + }, + add16(value: number | bigint) { + checkEncryptedValue(value, 16); + values.push(BigInt(value)); + bits.push(16); + return this; + }, + add32(value: number | bigint) { + checkEncryptedValue(value, 32); + values.push(BigInt(value)); + bits.push(32); + return this; + }, + add64(value: number | bigint) { + checkEncryptedValue(value, 64); + values.push(BigInt(value)); + bits.push(64); + return this; + }, + add128(value: number | bigint) { + checkEncryptedValue(value, 128); + values.push(BigInt(value)); + bits.push(128); + return this; + }, + addAddress(value: string) { + if (!isAddress(value)) { + throw new Error("The value must be a valid address."); + } + values.push(BigInt(value)); + bits.push(160); + return this; + }, + addBytes256(value: Uint8Array) { + const bigIntValue = bytesToBigInt(value); + checkEncryptedValue(bigIntValue, 2048); + values.push(bigIntValue); + bits.push(2048); + return this; + }, + getValues() { + return values; + }, + getBits() { + return bits; + }, + resetValues() { + values.length = 0; + bits.length = 0; + return this; + }, + encrypt() { + const listType = getListType(bits); + + let encrypted = Buffer.alloc(0); + + switch (listType) { + case 160: { + bits.map((v, i) => { + encrypted = Buffer.concat([encrypted, createUintToUint8ArrayFunction(v)(values[i])]); + }); + break; + } + case 2048: { + encrypted = createUintToUint8ArrayFunction(2048)(values[0]); + break; + } + } + + const inputProof = new Uint8Array(encrypted); + const hash = new Keccak(256).update(Buffer.from(inputProof)).digest(); + + const handles = bits.map((v, i) => { + const dataWithIndex = new Uint8Array(hash.length + 1); + dataWithIndex.set(hash, 0); + dataWithIndex.set([i], hash.length); + const finalHash = new Keccak(256).update(Buffer.from(dataWithIndex)).digest(); + const dataInput = new Uint8Array(32); + dataInput.set(finalHash, 0); + dataInput.set([i, ENCRYPTION_TYPES[v], 0], 29); + return dataInput; + }); + return { + handles, + inputProof, + }; + }, + }; +}; + +const checkEncryptedValue = (value: number | bigint, bits: number) => { + if (value == null) throw new Error("Missing value"); + let limit; + if (bits >= 8) { + limit = BigInt(`0x${new Array(bits / 8).fill(null).reduce((v) => `${v}ff`, "")}`); + } else { + limit = BigInt(2 ** bits - 1); + } + if (typeof value !== "number" && typeof value !== "bigint") throw new Error("Value must be a number or a bigint."); + if (value > limit) { + throw new Error(`The value exceeds the limit for ${bits}bits integer (${limit.toString()}).`); + } +}; + +export const ENCRYPTION_TYPES = { + 1: 0, + 4: 1, + 8: 2, + 16: 3, + 32: 4, + 64: 5, + 128: 6, + 160: 7, + 256: 8, + 512: 9, + 1024: 10, + 2048: 11, +}; + +const getListType = (bits: (keyof typeof ENCRYPTION_TYPES)[]) => { + // We limit to 12 items because for now we are using FheUint160List + if (bits.length > 12) { + throw new Error("You can't pack more than 12 values."); + } + + if (bits.reduce((total, v) => total + v, 0) > 2048) { + throw new Error("Too many bits in provided values. Maximum is 2048."); + } + + if (bits.some((v) => v === 2048)) { + return 2048; + } else { + return 160; + } +}; diff --git a/packages/foundry/contracts/hooks/Hookathon/test/instance.ts b/packages/foundry/contracts/hooks/Hookathon/test/instance.ts new file mode 100644 index 00000000..9a412c4f --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/instance.ts @@ -0,0 +1,196 @@ +import { clientKeyDecryptor, createInstance as createFhevmInstance, getCiphertextCallParams } from "fhevmjs"; +import { readFileSync } from "fs"; +import { ethers, ethers as hethers, network } from "hardhat"; +import { homedir } from "os"; +import path from "path"; + +import { awaitCoprocessor, getClearText } from "./coprocessorUtils"; +import { createEncryptedInputMocked, reencryptRequestMocked } from "./fhevmjsMocked"; +import type { Signers } from "./signers"; +import { FhevmInstances } from "./types"; + +const FHE_CLIENT_KEY_PATH = process.env.FHE_CLIENT_KEY_PATH; + +let clientKey: Uint8Array | undefined; + +const createInstanceMocked = async () => { + const instance = await createFhevmInstance({ + chainId: network.config.chainId, + }); + instance.reencrypt = reencryptRequestMocked; + instance.createEncryptedInput = createEncryptedInputMocked; + instance.getPublicKey = () => "0xFFAA44433"; + return instance; +}; + +export const createInstances = async (accounts: Signers): Promise => { + // Create instance + const instances: FhevmInstances = {} as FhevmInstances; + if (network.name === "hardhat") { + await Promise.all( + Object.keys(accounts).map(async (k) => { + instances[k as keyof FhevmInstances] = await createInstanceMocked(); + }), + ); + } else { + await Promise.all( + Object.keys(accounts).map(async (k) => { + instances[k as keyof FhevmInstances] = await createInstance(); + }), + ); + } + return instances; +}; + +export const createInstance = async () => { + const instance = await createFhevmInstance({ + networkUrl: network.config.url, + gatewayUrl: "https://gateway.rivest.inco.org" //"https://gateway.rivest.inco.org" //"https://gateway.rivest.inco.org", // "http://localhost:7077" + }); + return instance; +}; + +const getCiphertext = async (handle: bigint, ethers: typeof hethers): Promise => { + return ethers.provider.call(getCiphertextCallParams(handle)); +}; + +const getDecryptor = () => { + if (clientKey == null) { + if (FHE_CLIENT_KEY_PATH) { + clientKey = readFileSync(FHE_CLIENT_KEY_PATH); + } else { + const home = homedir(); + const clientKeyPath = path.join(home, "network-fhe-keys/cks"); + clientKey = readFileSync(clientKeyPath); + } + } + return clientKeyDecryptor(clientKey); +}; + +/** + * @debug + * This function is intended for debugging purposes only. + * It cannot be used in production code, since it requires the FHE private key for decryption. + * In production, decryption is only possible via an asyncronous on-chain call to the Gateway. + * + * @param {bigint} a handle to decrypt + * @returns {bool} + */ +export const decryptBool = async (handle: bigint): Promise => { + if (network.name === "hardhat") { + await awaitCoprocessor(); + return (await getClearText(handle)) === "1"; + } else { + return getDecryptor().decryptBool(await getCiphertext(handle, ethers)); + } +}; + +/** + * @debug + * This function is intended for debugging purposes only. + * It cannot be used in production code, since it requires the FHE private key for decryption. + * In production, decryption is only possible via an asyncronous on-chain call to the Gateway. + * + * @param {bigint} a handle to decrypt + * @returns {bigint} + */ +export const decrypt4 = async (handle: bigint): Promise => { + if (network.name === "hardhat") { + await awaitCoprocessor(); + return BigInt(await getClearText(handle)); + } else { + return getDecryptor().decrypt4(await getCiphertext(handle, ethers)); + } +}; + +/** + * @debug + * This function is intended for debugging purposes only. + * It cannot be used in production code, since it requires the FHE private key for decryption. + * In production, decryption is only possible via an asyncronous on-chain call to the Gateway. + * + * @param {bigint} a handle to decrypt + * @returns {bigint} + */ +export const decrypt8 = async (handle: bigint): Promise => { + if (network.name === "hardhat") { + await awaitCoprocessor(); + return BigInt(await getClearText(handle)); + } else { + return getDecryptor().decrypt8(await getCiphertext(handle, ethers)); + } +}; + +/** + * @debug + * This function is intended for debugging purposes only. + * It cannot be used in production code, since it requires the FHE private key for decryption. + * In production, decryption is only possible via an asyncronous on-chain call to the Gateway. + * + * @param {bigint} a handle to decrypt + * @returns {bigint} + */ +export const decrypt16 = async (handle: bigint): Promise => { + if (network.name === "hardhat") { + await awaitCoprocessor(); + return BigInt(await getClearText(handle)); + } else { + return getDecryptor().decrypt16(await getCiphertext(handle, ethers)); + } +}; + +/** + * @debug + * This function is intended for debugging purposes only. + * It cannot be used in production code, since it requires the FHE private key for decryption. + * In production, decryption is only possible via an asyncronous on-chain call to the Gateway. + * + * @param {bigint} a handle to decrypt + * @returns {bigint} + */ +export const decrypt32 = async (handle: bigint): Promise => { + if (network.name === "hardhat") { + await awaitCoprocessor(); + return BigInt(await getClearText(handle)); + } else { + return getDecryptor().decrypt32(await getCiphertext(handle, ethers)); + } +}; + +/** + * @debug + * This function is intended for debugging purposes only. + * It cannot be used in production code, since it requires the FHE private key for decryption. + * In production, decryption is only possible via an asyncronous on-chain call to the Gateway. + * + * @param {bigint} a handle to decrypt + * @returns {bigint} + */ +export const decrypt64 = async (handle: bigint): Promise => { + if (network.name === "hardhat") { + await awaitCoprocessor(); + return BigInt(await getClearText(handle)); + } else { + return getDecryptor().decrypt64(await getCiphertext(handle, ethers)); + } +}; + +/** + * @debug + * This function is intended for debugging purposes only. + * It cannot be used in production code, since it requires the FHE private key for decryption. + * In production, decryption is only possible via an asyncronous on-chain call to the Gateway. + * + * @param {bigint} a handle to decrypt + * @returns {string} + */ +export const decryptAddress = async (handle: bigint): Promise => { + if (network.name === "hardhat") { + await awaitCoprocessor(); + const bigintAdd = BigInt(await getClearText(handle)); + const handleStr = "0x" + bigintAdd.toString(16).padStart(40, "0"); + return handleStr; + } else { + return getDecryptor().decryptAddress(await getCiphertext(handle, ethers)); + } +}; diff --git a/packages/foundry/contracts/hooks/Hookathon/test/signers.ts b/packages/foundry/contracts/hooks/Hookathon/test/signers.ts new file mode 100644 index 00000000..47155923 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/signers.ts @@ -0,0 +1,29 @@ +import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { ethers } from "hardhat"; + +export interface Signers { + alice: HardhatEthersSigner; + bob: HardhatEthersSigner; + carol: HardhatEthersSigner; + dave: HardhatEthersSigner; + eve: HardhatEthersSigner; +} + +let signers: Signers; + +export const initSigners = async (): Promise => { + if (!signers) { + const eSigners = await ethers.getSigners(); + signers = { + alice: eSigners[0], + bob: eSigners[1], + carol: eSigners[2], + dave: eSigners[3], + eve: eSigners[4], + }; + } +}; + +export const getSigners = async (): Promise => { + return signers; +}; diff --git a/packages/foundry/contracts/hooks/Hookathon/test/types.ts b/packages/foundry/contracts/hooks/Hookathon/test/types.ts new file mode 100644 index 00000000..6afcffea --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/types.ts @@ -0,0 +1,19 @@ +import type { FhevmInstance } from "fhevmjs"; + +import type { Signers } from "./signers"; + +declare module "mocha" { + export interface Context { + signers: Signers; + contractAddress: string; + instances: FhevmInstances; + } +} + +export interface FhevmInstances { + alice: FhevmInstance; + bob: FhevmInstance; + carol: FhevmInstance; + dave: FhevmInstance; + eve: FhevmInstance; +} diff --git a/packages/foundry/contracts/hooks/Hookathon/test/utils.ts b/packages/foundry/contracts/hooks/Hookathon/test/utils.ts new file mode 100644 index 00000000..5af310ba --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/test/utils.ts @@ -0,0 +1,97 @@ +import { toBufferLE } from "bigint-buffer"; +import { ContractMethodArgs, Typed } from "ethers"; +import { ethers, network } from "hardhat"; + +import { TypedContractMethod } from "../types/common"; + +export const waitForBlock = (blockNumber: bigint | number) => { + if (network.name === "hardhat") { + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + try { + const currentBlock = await ethers.provider.getBlockNumber(); + if (BigInt(currentBlock) >= blockNumber) { + clearInterval(intervalId); + resolve(currentBlock); + } + } catch (error) { + clearInterval(intervalId); + reject(error); + } + }, 50); // Check every 50 milliseconds + }); + } else { + return new Promise((resolve, reject) => { + const waitBlock = async (currentBlock: number) => { + if (blockNumber <= BigInt(currentBlock)) { + await ethers.provider.off("block", waitBlock); + resolve(blockNumber); + } + }; + ethers.provider.on("block", waitBlock).catch((err) => { + reject(err); + }); + }); + } +}; + +export const waitNBlocks = async (Nblocks: number) => { + const currentBlock = await ethers.provider.getBlockNumber(); + if (network.name === "hardhat") { + await produceDummyTransactions(Nblocks); + } + await waitForBlock(currentBlock + Nblocks); +}; + +export const waitForBalance = async (address: string): Promise => { + return new Promise((resolve, reject) => { + const checkBalance = async () => { + const balance = await ethers.provider.getBalance(address); + if (balance > 0) { + await ethers.provider.off("block", checkBalance); + resolve(); + } + }; + ethers.provider.on("block", checkBalance).catch((err) => { + reject(err); + }); + }); +}; + +export const createTransaction = async ( + method: TypedContractMethod, + ...params: A +) => { + const gasLimit = await method.estimateGas(...params); + const updatedParams: ContractMethodArgs = [ + ...params, + { gasLimit: Math.min(Math.round(+gasLimit.toString() * 1.2), 10000000) }, + ]; + return method(...updatedParams); +}; + +export const produceDummyTransactions = async (blockCount: number) => { + let counter = blockCount; + while (counter >= 0) { + counter--; + const [signer] = await ethers.getSigners(); + const nullAddress = "0x0000000000000000000000000000000000000000"; + const tx = { + to: nullAddress, + value: 0n, + }; + const receipt = await signer.sendTransaction(tx); + await receipt.wait(); + } +}; + +export const mineNBlocks = async (n: number) => { + for (let index = 0; index < n; index++) { + await ethers.provider.send("evm_mine"); + } +}; + +export const bigIntToBytes = (value: bigint) => { + const byteArrayLength = Math.ceil(value.toString(2).length / 8); + return new Uint8Array(toBufferLE(value, byteArrayLength)); +}; diff --git a/packages/foundry/contracts/hooks/Hookathon/tsconfig.json b/packages/foundry/contracts/hooks/Hookathon/tsconfig.json new file mode 100644 index 00000000..734e21a2 --- /dev/null +++ b/packages/foundry/contracts/hooks/Hookathon/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2020"], + "module": "commonjs", + "moduleResolution": "node", + "noImplicitAny": true, + "removeComments": true, + "resolveJsonModule": true, + "sourceMap": true, + "strict": true, + "target": "es2020" + }, + "exclude": ["node_modules"], + "files": ["./hardhat.config.ts"], + "include": ["src/**/*", "tasks/**/*", "test/**/*", "deploy/**/*", "types/"] +} From ccab0dadc00c2fa0bc641271e299584f19644448 Mon Sep 17 00:00:00 2001 From: swayam karle Date: Mon, 21 Oct 2024 04:39:46 +0530 Subject: [PATCH 2/7] Standard Readme Setup --- .../contracts/hooks/Hookathon/README.md | 243 +++++------------- .../{secretLottery.sol => secretSwap.sol} | 130 ++++++---- 2 files changed, 149 insertions(+), 224 deletions(-) rename packages/foundry/contracts/hooks/Hookathon/contracts/{secretLottery.sol => secretSwap.sol} (59%) diff --git a/packages/foundry/contracts/hooks/Hookathon/README.md b/packages/foundry/contracts/hooks/Hookathon/README.md index 3fcf20e5..f6e0fadc 100644 --- a/packages/foundry/contracts/hooks/Hookathon/README.md +++ b/packages/foundry/contracts/hooks/Hookathon/README.md @@ -1,232 +1,121 @@ -# Hardhat Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Hardhat][hardhat-badge]][hardhat] [![License: MIT][license-badge]][license] - -[gitpod]: https://gitpod.io/#https://github.com/zama-ai/fhevm-hardhat-template -[gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod -[gha]: https://github.com/zama-ai/fhevm-hardhat-template/actions -[gha-badge]: https://github.com/zama-ai/fhevm-hardhat-template/actions/workflows/ci.yml/badge.svg -[hardhat]: https://hardhat.org/ -[hardhat-badge]: https://img.shields.io/badge/Built%20with-Hardhat-FFDB1C.svg -[license]: https://opensource.org/licenses/MIT -[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg - -A Hardhat-based template for developing Solidity smart contracts, with sensible defaults. - -- [Hardhat](https://github.com/nomiclabs/hardhat): compile, run and test smart contracts -- [TypeChain](https://github.com/ethereum-ts/TypeChain): generate TypeScript bindings for smart contracts -- [Ethers](https://github.com/ethers-io/ethers.js/): renowned Ethereum library and wallet implementation -- [Solhint](https://github.com/protofire/solhint): code linter -- [Solcover](https://github.com/sc-forks/solidity-coverage): code coverage -- [Prettier Plugin Solidity](https://github.com/prettier-solidity/prettier-plugin-solidity): code formatter - -## Getting Started - -Click the [`Use this template`](https://github.com/zama-ai/fhevm-hardhat-template/generate) button at the top of the -page to create a new repository with this repo as the initial state. - -## Features - -This template builds upon the frameworks and libraries mentioned above, so for details about their specific features, -please consult their respective documentations. - -For example, for Hardhat, you can refer to the [Hardhat Tutorial](https://hardhat.org/tutorial) and the -[Hardhat Docs](https://hardhat.org/docs). You might be in particular interested in reading the -[Testing Contracts](https://hardhat.org/tutorial/testing-contracts) section. - -### Sensible Defaults - -This template comes with sensible default configurations in the following files: - -```text -├── .editorconfig -├── .eslintignore -├── .eslintrc.yml -├── .gitignore -├── .prettierignore -├── .prettierrc.yml -├── .solcover.js -├── .solhint.json -└── hardhat.config.ts -``` - -### VSCode Integration - -This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic -Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity). - -### GitHub Actions - -This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull -request made to the `main` branch. - -Note though that to make this work, you must use your `INFURA_API_KEY` and your `MNEMONIC` as GitHub secrets. - -You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). +## SecretSwap: Confidential Swaps -## Usage +SecretSwap allows users to perform confidential token swaps using Balancer V3 hooks and Fully Homomorphic Encryption (FHE) provided by Zama's Co-Processor Model. Users can choose to either: +- **Deposit tokens** into the contract (operation `1`) +- **Withdraw tokens** (operation `2`) +- **Perform a standard swap** without any operation specified. -### Pre Requisites +### How It Works -Install [docker](https://docs.docker.com/engine/install/) +1. **Deposit:** + - When a user performs a deposit (operation `1`), their tokens are securely transferred to the contract, and they receive **encrypted credits** representing their balance. + - The contract uses FHE to keep the user's balance confidential, and credits are stored securely. + +2. **Withdraw:** + - When a user withdraws (operation `2`), their **encrypted credits** are decrypted using Zama's Co-Processor Model, and the corresponding token amounts are transferred back to the user. + - Users can withdraw their full balance in both `tokenA` and `tokenB` based on their current credit value. -Install [pnpm](https://pnpm.io/installation) +3. **Standard Swap:** + - If no operation is specified (or an invalid operation is provided), the tokens proceed with a standard Balancer V3 swap. -Before being able to run any command, you need to create a `.env` file and set a BIP-39 compatible mnemonic as an -environment variable. You can follow the example in `.env.example` and start with the following command: +### Key Features +- **Confidentiality:** All operations are handled in a privacy-preserving manner using FHE, ensuring that no sensitive information is leaked on-chain. +- **Flexible Swaps:** Users can seamlessly perform standard token swaps alongside confidential deposits and withdrawals. +- **Encrypted Credits:** Token balances are encrypted and managed using FHE, enabling secure, private transactions. +- **Decryption via Callback:** Upon withdrawal, encrypted credit balances are decrypted using the FHE gateway, and tokens are securely transferred to the user. -```sh -cp .env.example .env -``` +### Operations +- **Operation 1 (Deposit):** Deposit tokens into the SecretSwap contract. Users receive encrypted credits. +- **Operation 2 (Withdraw):** Withdraw tokens based on the encrypted credits. +- **No Operation:** Perform a normal swap using Balancer V3 without any encrypted actions. -If you don't already have a mnemonic, you can use this [website](https://iancoleman.io/bip39/) to generate one. +--- -Then, proceed with installing dependencies - please **_make sure to use Node v20_** or more recent or this will fail: +### Setup and Run Instructions -```sh -pnpm install -``` +### Prerequisites -### Start fhEVM +Before you begin, ensure you have the following installed: -During installation (see previous section) we recommend you for easier setup to not change the default `.env` : simply -copy the original `.env.example` file to a new `.env` file in the root of the repo. +- [Docker](https://docs.docker.com/engine/install/) +- [pnpm](https://pnpm.io/installation) +- Node.js (version 20 or higher) -Then, start a local fhEVM docker compose that inlcudes everything needed to deploy FHE encrypted smart contracts using: - -```sh -# In one terminal, keep it opened -# The node logs are printed -pnpm fhevm:start -``` +### 1. Clone the Repository -Previous command will take 2 to 3 minutes to do the whole initial setup - wait until the blockchain logs appear to make -sure setup is complete (we are working on making initial deployment faster). - -You can then run the tests simply in a new terminal via : - -``` -pnpm test -``` - -Once your done with your tests, to stop the node: - -```sh -pnpm fhevm:stop -``` - -### Compile - -Compile the smart contracts with Hardhat: - -```sh -pnpm compile +```bash +git clone https://github.com/zama-ai/fhevm-hardhat-template +cd fhevm-hardhat-template ``` -### TypeChain - -Compile the smart contracts and generate TypeChain bindings: +### 2. Install Dependencies -```sh -pnpm typechain +```bash +pnpm install ``` -### List accounts +### 3. Set Up `.env` File -From the mnemonic in .env file, list all the derived Ethereum adresses: - -```sh -pnpm task:accounts +```bash +cp .env.example .env ``` -### Get some native coins +Generate a mnemonic using [this tool](https://iancoleman.io/bip39/) and paste it into the `.env` file. -In order to interact with the blockchain, one need some coins. This command will give coins to the first 5 addresses -derived from the mnemonic in .env file. +### 4. Start fheVM -```sh -pnpm fhevm:faucet -``` - -
-
- To get the first derived address from mnemonic -
+To start the local FHEVM environment using Docker: -```sh -pnpm task:getEthereumAddress +```bash +pnpm fhevm:start ``` -
-
+Wait until the blockchain logs are printed, indicating that the setup is complete. -### Test +### 5. Run Tests -Run the tests with Hardhat: +In another terminal, run the tests to verify the contracts: -```sh +```bash pnpm test ``` -### Lint Solidity - -Lint the Solidity code: +### 6. Stop fheVM -```sh -pnpm lint:sol -``` +When you are finished, stop the local node: -### Lint TypeScript - -Lint the TypeScript code: - -```sh -pnpm lint:ts -``` - -### Report Gas - -See the gas usage per unit test and average gas per method call: - -```sh -REPORT_GAS=true pnpm test +```bash +pnpm fhevm:stop ``` -### Clean +### Contract Compilation -Delete the smart contract artifacts, the coverage reports and the Hardhat cache: +To compile the contracts: -```sh -pnpm clean +```bash +pnpm compile ``` -### Mocked mode +### Further Testing -The mocked mode allows faster testing and the ability to analyze coverage of the tests. In this mocked version, -encrypted types are not really encrypted, and the tests are run on the original version of the EVM, on a local hardhat -network instance. To run the tests in mocked mode, you can use directly the following command: +To run tests in mocked mode (faster), use: ```bash pnpm test:mock ``` -To analyze the coverage of the tests (in mocked mode necessarily, as this cannot be done on the real fhEVM node), you -can use this command : +To analyze test coverage: ```bash pnpm coverage:mock ``` -Then open the file `coverage/index.html`. You can see there which line or branch for each contract which has been -covered or missed by your test suite. This allows increased security by pointing out missing branches not covered yet by -the current tests. +--- -> [!Note] -> Due to intrinsic limitations of the original EVM, the mocked version differ in few corner cases from the real fhEVM, the main difference is the difference in gas prices for the FHE operations. This means that before deploying to production, developers still need to run the tests with the original fhEVM node, as a final check in non-mocked mode, with `pnpm test`. - -### Syntax Highlighting +## License -If you use VSCode, you can get Solidity syntax highlighting with the -[hardhat-solidity](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity) extension. +This project is licensed under the MIT License. -## License +--- -This project is licensed under MIT. \ No newline at end of file +This README provides a clear explanation of how SecretSwap works, instructions for setup, and testing guidance for running the contracts on the `fhevm` using Balancer V3 hooks. \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/secretLottery.sol b/packages/foundry/contracts/hooks/Hookathon/contracts/secretSwap.sol similarity index 59% rename from packages/foundry/contracts/hooks/Hookathon/contracts/secretLottery.sol rename to packages/foundry/contracts/hooks/Hookathon/contracts/secretSwap.sol index e2c3c072..4ad40772 100644 --- a/packages/foundry/contracts/hooks/Hookathon/contracts/secretLottery.sol +++ b/packages/foundry/contracts/hooks/Hookathon/contracts/secretSwap.sol @@ -20,47 +20,44 @@ import "fhevm/lib/TFHE.sol"; import "fhevm/gateway/GatewayCaller.sol"; /** - * @notice Hook that randomly rewards accumulated fees to a user performing a swap. - * @dev In this example, every time a swap is executed in a pool registered with this hook, a "random" number is drawn. - * If the drawn number is not equal to the LUCKY_NUMBER, the user will pay fees to the hook contract. But, if the - * drawn number is equal to LUCKY_NUMBER, the user won't pay hook fees and will receive all fees accrued by the hook. + * @title SecretSwapHook + * @notice This contract implements a secret swap mechanism using Balancer V3 hooks and FHE (Fully Homomorphic Encryption). + * Users can deposit tokens, withdraw tokens, or perform a standard swap using this hook. + * The operations (deposit or withdraw) are encrypted using Zama's Co-Processor model for confidential transactions. */ -contract LotteryHookExample is BaseHooks, VaultGuard, Ownable, GatewayCaller { +contract SecretSwapHook is BaseHooks, VaultGuard, Ownable, GatewayCaller { using FixedPoint for uint256; using EnumerableMap for EnumerableMap.IERC20ToUint256Map; using SafeERC20 for IERC20; - // Trusted router is needed since we rely on `getSender` to know which user should receive the prize. + // Router and factory addresses for swap validation address private immutable _trustedRouter; address private immutable _allowedFactory; - mapping(address => mapping(IERC20 => euint64)) private userAddressToThereCreditValue; + // Tracks user credit balances (encrypted using TFHE) per token + mapping(address => mapping(IERC20 => euint64)) private userAddressToCreditValue; + + // Division factor for scaling token amounts uint256 constant DIVISION_FACTOR = 10 ** 12; - uint256 private _counter = 0; /** - * @notice A new `LotteryHookExample` contract has been registered successfully for a given factory and pool. - * @dev If the registration fails the call will revert, so there will be no event. - * @param hooksContract This contract - * @param pool The pool on which the hook was registered + * @notice Event emitted when the SecretSwapHook is registered for a pool. */ event SecretSwapHookRegistered(address indexed hooksContract, address indexed pool); + /** + * @notice Event emitted when tokens are deposited into the contract. + */ + event TokenDeposited(address indexed hooksContract, IERC20 indexed token, uint256 amount); + struct CallBackStruct { address userAddress; IERC20 token1; IERC20 token2; } - mapping(uint256 id => CallBackStruct callBackStruct) public requestIdToCallBackStruct; - /** - * @notice Fee collected and added to the lottery pot. - * @dev The current user did not win the lottery. - * @param hooksContract This contract - * @param token The token in which the fee was collected - * @param amount The amount of the fee collected - */ - event TokenDeposited(address indexed hooksContract, IERC20 indexed token, uint256 amount); + // Map request ID to callback struct for handling decryption callbacks + mapping(uint256 id => CallBackStruct) public requestIdToCallBackStruct; constructor(IVault vault, address router) VaultGuard(vault) Ownable(msg.sender) { _trustedRouter = router; @@ -74,7 +71,6 @@ contract LotteryHookExample is BaseHooks, VaultGuard, Ownable, GatewayCaller { LiquidityManagement calldata ) public override onlyVault returns (bool) { emit SecretSwapHookRegistered(address(this), pool); - return true; } @@ -86,16 +82,21 @@ contract LotteryHookExample is BaseHooks, VaultGuard, Ownable, GatewayCaller { return hookFlags; } - /// @inheritdoc IHooks + /** + * @notice Called after a swap operation, this function handles deposits, withdrawals, or standard swaps + * based on the user's selected operation (encoded in `params.userData`). + * @param params Swap parameters, including tokens, amounts, and user data. + */ function onAfterSwap( AfterSwapParams calldata params ) public override onlyVault returns (bool success, uint256 hookAdjustedAmountCalculatedRaw) { uint256 operation = abi.decode(params.userData, (uint256)); - // Do Deposit + // Handle deposit operation (1) if (params.router == _trustedRouter && operation == 1) { hookAdjustedAmountCalculatedRaw = params.amountCalculatedRaw; uint256 amount = params.amountCalculatedRaw.mulDown(1); + if (params.kind == SwapKind.EXACT_IN) { uint256 feeToPay = _depositToken(params.router, params.tokenOut, amount); if (feeToPay > 0) { @@ -109,42 +110,52 @@ contract LotteryHookExample is BaseHooks, VaultGuard, Ownable, GatewayCaller { } return (true, hookAdjustedAmountCalculatedRaw); } - // Do Withdraw + // Handle withdrawal operation (2) else if (params.router == _trustedRouter && operation == 2) { _withdrawToken(params.router, params.tokenIn, params.tokenOut); } else { + // Default case: standard swap return (true, hookAdjustedAmountCalculatedRaw); } } - // If drawnNumber == LUCKY_NUMBER, user wins the pot and pays no fees. Otherwise, the hook fee adds to the pot. + /** + * @notice Handles deposit of tokens into the contract. User receives encrypted credits. + * @param router Address of the router that initiated the swap. + * @param token Token to be deposited. + * @param amount Amount of the token to be deposited. + */ function _depositToken(address router, IERC20 token, uint256 amount) private returns (uint256) { address user = IRouterCommon(router).getSender(); if (amount > 0) { _vault.sendTo(token, address(this), amount); - userAddressToThereCreditValue[user][token] = TFHE.add( - userAddressToThereCreditValue[user][token], + userAddressToCreditValue[user][token] = TFHE.add( + userAddressToCreditValue[user][token], TFHE.asEuint64(amount / DIVISION_FACTOR) ); - TFHE.allow(userAddressToThereCreditValue[user][token], address(this)); - TFHE.allow(userAddressToThereCreditValue[user][token], owner()); + TFHE.allow(userAddressToCreditValue[user][token], address(this)); + TFHE.allow(userAddressToCreditValue[user][token], owner()); emit TokenDeposited(address(this), token, amount); } return amount; } + /** + * @notice Handles token withdrawal by decrypting user's credit balance and transferring tokens. + * @param router Address of the router that initiated the swap. + * @param token1 First token for withdrawal. + * @param token2 Second token for withdrawal. + */ function _withdrawToken(address router, IERC20 token1, IERC20 token2) private returns (uint256) { address user = IRouterCommon(router).getSender(); - euint64 token1Amount = TFHE.asEuint64(0); - if (TFHE.isInitialized(userAddressToThereCreditValue[user][token1])) { - token1Amount = userAddressToThereCreditValue[user][token1]; - } + euint64 token1Amount = TFHE.isInitialized(userAddressToCreditValue[user][token1]) + ? userAddressToCreditValue[user][token1] + : TFHE.asEuint64(0); - euint64 token2Amount = TFHE.asEuint64(0); - if (TFHE.isInitialized(userAddressToThereCreditValue[user][token2])) { - token2Amount = userAddressToThereCreditValue[user][token2]; - } + euint64 token2Amount = TFHE.isInitialized(userAddressToCreditValue[user][token2]) + ? userAddressToCreditValue[user][token2] + : TFHE.asEuint64(0); TFHE.allow(token1Amount, address(this)); TFHE.allow(token2Amount, address(this)); @@ -166,12 +177,19 @@ contract LotteryHookExample is BaseHooks, VaultGuard, Ownable, GatewayCaller { return 0; } + /** + * @notice Callback function after decryption to transfer tokens to user. + * @param requestID ID of the decryption request. + * @param _token1Amount Decrypted amount of token1. + * @param _token2Amount Decrypted amount of token2. + */ function callBackResolver( uint256 requestID, uint64 _token1Amount, uint64 _token2Amount ) external onlyGateway returns (bool) { CallBackStruct memory _callBackStruct = requestIdToCallBackStruct[requestID]; + if (_token1Amount > 0) { _callBackStruct.token1.safeTransfer(_callBackStruct.userAddress, _token1Amount * DIVISION_FACTOR); } @@ -179,32 +197,50 @@ contract LotteryHookExample is BaseHooks, VaultGuard, Ownable, GatewayCaller { _callBackStruct.token2.safeTransfer(_callBackStruct.userAddress, _token2Amount * DIVISION_FACTOR); } - userAddressToThereCreditValue[_callBackStruct.userAddress][_callBackStruct.token1] = TFHE.asEuint64(0); - userAddressToThereCreditValue[_callBackStruct.userAddress][_callBackStruct.token2] = TFHE.asEuint64(0); + userAddressToCreditValue[_callBackStruct.userAddress][_callBackStruct.token1] = TFHE.asEuint64(0); + userAddressToCreditValue[_callBackStruct.userAddress][_callBackStruct.token2] = TFHE.asEuint64(0); return true; } + /** + * @notice Transfer encrypted credits between users. + * @param to Recipient address. + * @param token Token being transferred. + * @param encryptedAmount Encrypted amount of the token. + * @param inputProof Proof for the transfer. + */ function transferCredits(address to, IERC20 token, einput encryptedAmount, bytes calldata inputProof) public { euint64 amount = TFHE.asEuint64(encryptedAmount, inputProof); - ebool canTransfer = TFHE.le(amount, userAddressToThereCreditValue[msg.sender][token]); + ebool canTransfer = TFHE.le(amount, userAddressToCreditValue[msg.sender][token]); _transfer(msg.sender, to, amount, canTransfer, token); } - // Transfers an encrypted amount. + /** + * @notice Internal function for transferring encrypted credits between users. + * @param from Sender address. + * @param to Recipient address. + * @param amount Encrypted amount. + * @param isTransferable Boolean indicating if the transfer is allowed. + * @param token Token being transferred. + */ function _transfer(address from, address to, euint64 amount, ebool isTransferable, IERC20 token) internal virtual { - // Add to the balance of `to` and subract from the balance of `from`. euint64 transferValue = TFHE.select(isTransferable, amount, TFHE.asEuint64(0)); - euint64 newBalanceTo = TFHE.add(userAddressToThereCreditValue[to][token], transferValue); - userAddressToThereCreditValue[to][token] = newBalanceTo; + euint64 newBalanceTo = TFHE.add(userAddressToCreditValue[to][token], transferValue); + userAddressToCreditValue[to][token] = newBalanceTo; + TFHE.allow(newBalanceTo, address(this)); TFHE.allow(newBalanceTo, to); - euint64 newBalanceFrom = TFHE.sub(userAddressToThereCreditValue[from][token], transferValue); - userAddressToThereCreditValue[from][token] = newBalanceFrom; + + euint64 newBalanceFrom = TFHE.sub(userAddressToCreditValue[from][token], transferValue); + userAddressToCreditValue[from][token] = newBalanceFrom; + TFHE.allow(newBalanceFrom, address(this)); TFHE.allow(newBalanceFrom, from); + emit Transfer(from, to); } + // Event emitted when credits are transferred event Transfer(address, address); } From f89785044f1a990c7f535ddd527be73e1272481f Mon Sep 17 00:00:00 2001 From: swayam karle Date: Mon, 21 Oct 2024 04:48:42 +0530 Subject: [PATCH 3/7] Changed name from Hookathon To SecretSwap --- packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.czrc | 0 .../contracts/hooks/{Hookathon => SecretSwap}/.editorconfig | 0 .../contracts/hooks/{Hookathon => SecretSwap}/.env.example | 0 .../hooks/{Hookathon => SecretSwap}/.github/workflows/ci.yml | 0 .../foundry/contracts/hooks/{Hookathon => SecretSwap}/.gitignore | 0 .../foundry/contracts/hooks/{Hookathon => SecretSwap}/.gitpod.yml | 0 .../contracts/hooks/{Hookathon => SecretSwap}/.prettierignore | 0 .../contracts/hooks/{Hookathon => SecretSwap}/.prettierrc.yml | 0 .../contracts/hooks/{Hookathon => SecretSwap}/.solcover.js | 0 .../contracts/hooks/{Hookathon => SecretSwap}/.solhint.json | 0 .../contracts/hooks/{Hookathon => SecretSwap}/.solhintignore | 0 .../contracts/hooks/{Hookathon => SecretSwap}/CustomProvider.ts | 0 .../foundry/contracts/hooks/{Hookathon => SecretSwap}/LICENSE.md | 0 .../foundry/contracts/hooks/{Hookathon => SecretSwap}/Makefile | 0 .../foundry/contracts/hooks/{Hookathon => SecretSwap}/README.md | 0 .../hooks/{Hookathon => SecretSwap}/contracts/EncryptedERC20.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/BaseHooks.sol | 0 .../contracts/interfaces/EnumerableMap.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/FixedPoint.sol | 0 .../contracts/interfaces/IAllowanceTransfer.sol | 0 .../contracts/interfaces/IAuthentication.sol | 0 .../contracts/interfaces/IAuthorizer.sol | 0 .../contracts/interfaces/IBasePoolFactory.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/IEIP712.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/IHooks.sol | 0 .../contracts/interfaces/IProtocolFeeController.sol | 0 .../contracts/interfaces/IRateProvider.sol | 0 .../contracts/interfaces/IRouterCommon.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/IVault.sol | 0 .../contracts/interfaces/IVaultAdmin.sol | 0 .../contracts/interfaces/IVaultErrors.sol | 0 .../contracts/interfaces/IVaultEvents.sol | 0 .../contracts/interfaces/IVaultExtension.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/IVaultMain.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/LogExpMath.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/VaultGuard.sol | 0 .../{Hookathon => SecretSwap}/contracts/interfaces/VaultTypes.sol | 0 .../hooks/{Hookathon => SecretSwap}/contracts/secretSwap.sol | 0 .../contracts/hooks/{Hookathon => SecretSwap}/default.toml | 0 .../contracts/hooks/{Hookathon => SecretSwap}/deploy/deploy.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/deploy/instance.ts | 0 .../docker-compose/docker-compose-full.yml.template | 0 .../contracts/hooks/{Hookathon => SecretSwap}/eslint.config.mjs | 0 .../contracts/hooks/{Hookathon => SecretSwap}/hardhat.config.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/launch-fhevm.sh | 0 .../contracts/hooks/{Hookathon => SecretSwap}/package.json | 0 .../contracts/hooks/{Hookathon => SecretSwap}/pnpm-lock.yaml | 0 .../contracts/hooks/{Hookathon => SecretSwap}/remappings.txt | 0 .../hooks/{Hookathon => SecretSwap}/scripts/copy_fhe_keys.sh | 0 .../scripts/fund_test_addresses_docker.sh | 0 .../{Hookathon => SecretSwap}/scripts/get_kms_core_version.sh | 0 .../{Hookathon => SecretSwap}/scripts/get_repository_info.sh | 0 .../{Hookathon => SecretSwap}/scripts/precomputeAddresses.sh | 0 .../hooks/{Hookathon => SecretSwap}/scripts/prepare_test.sh | 0 .../scripts/prepare_volumes_from_kms_core.sh | 0 .../{Hookathon => SecretSwap}/scripts/rewrite-docker-compose.sh | 0 .../foundry/contracts/hooks/{Hookathon => SecretSwap}/setup.sh | 0 .../contracts/hooks/{Hookathon => SecretSwap}/tasks/accounts.ts | 0 .../hooks/{Hookathon => SecretSwap}/tasks/checkNodeVersion.js | 0 .../hooks/{Hookathon => SecretSwap}/tasks/deployERC20.ts | 0 .../hooks/{Hookathon => SecretSwap}/tasks/getEthereumAddress.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/tasks/taskDeploy.ts | 0 .../hooks/{Hookathon => SecretSwap}/tasks/taskGatewayRelayer.ts | 0 .../hooks/{Hookathon => SecretSwap}/tasks/taskOracleRelayer.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/tasks/taskTFHE.ts | 0 .../hooks/{Hookathon => SecretSwap}/test/asyncDecrypt.ts | 0 .../hooks/{Hookathon => SecretSwap}/test/coprocessorUtils.ts | 0 .../test/encryptedERC20/EncryptedERC20.fixture.ts | 0 .../test/encryptedERC20/EncryptedERC20.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/test/execution.ts | 0 .../hooks/{Hookathon => SecretSwap}/test/fhevmjsMocked.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/test/instance.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/test/signers.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/test/types.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/test/utils.ts | 0 .../contracts/hooks/{Hookathon => SecretSwap}/tsconfig.json | 0 76 files changed, 0 insertions(+), 0 deletions(-) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.czrc (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.editorconfig (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.env.example (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.github/workflows/ci.yml (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.gitignore (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.gitpod.yml (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.prettierignore (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.prettierrc.yml (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.solcover.js (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.solhint.json (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/.solhintignore (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/CustomProvider.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/LICENSE.md (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/Makefile (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/README.md (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/EncryptedERC20.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/BaseHooks.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/EnumerableMap.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/FixedPoint.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IAllowanceTransfer.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IAuthentication.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IAuthorizer.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IBasePoolFactory.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IEIP712.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IHooks.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IProtocolFeeController.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IRateProvider.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IRouterCommon.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IVault.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IVaultAdmin.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IVaultErrors.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IVaultEvents.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IVaultExtension.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/IVaultMain.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/LogExpMath.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/VaultGuard.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/interfaces/VaultTypes.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/contracts/secretSwap.sol (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/default.toml (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/deploy/deploy.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/deploy/instance.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/docker-compose/docker-compose-full.yml.template (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/eslint.config.mjs (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/hardhat.config.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/launch-fhevm.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/package.json (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/pnpm-lock.yaml (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/remappings.txt (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/copy_fhe_keys.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/fund_test_addresses_docker.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/get_kms_core_version.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/get_repository_info.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/precomputeAddresses.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/prepare_test.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/prepare_volumes_from_kms_core.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/scripts/rewrite-docker-compose.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/setup.sh (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/accounts.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/checkNodeVersion.js (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/deployERC20.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/getEthereumAddress.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/taskDeploy.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/taskGatewayRelayer.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/taskOracleRelayer.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tasks/taskTFHE.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/asyncDecrypt.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/coprocessorUtils.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/encryptedERC20/EncryptedERC20.fixture.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/encryptedERC20/EncryptedERC20.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/execution.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/fhevmjsMocked.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/instance.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/signers.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/types.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/test/utils.ts (100%) rename packages/foundry/contracts/hooks/{Hookathon => SecretSwap}/tsconfig.json (100%) diff --git a/packages/foundry/contracts/hooks/Hookathon/.czrc b/packages/foundry/contracts/hooks/SecretSwap/.czrc similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.czrc rename to packages/foundry/contracts/hooks/SecretSwap/.czrc diff --git a/packages/foundry/contracts/hooks/Hookathon/.editorconfig b/packages/foundry/contracts/hooks/SecretSwap/.editorconfig similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.editorconfig rename to packages/foundry/contracts/hooks/SecretSwap/.editorconfig diff --git a/packages/foundry/contracts/hooks/Hookathon/.env.example b/packages/foundry/contracts/hooks/SecretSwap/.env.example similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.env.example rename to packages/foundry/contracts/hooks/SecretSwap/.env.example diff --git a/packages/foundry/contracts/hooks/Hookathon/.github/workflows/ci.yml b/packages/foundry/contracts/hooks/SecretSwap/.github/workflows/ci.yml similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.github/workflows/ci.yml rename to packages/foundry/contracts/hooks/SecretSwap/.github/workflows/ci.yml diff --git a/packages/foundry/contracts/hooks/Hookathon/.gitignore b/packages/foundry/contracts/hooks/SecretSwap/.gitignore similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.gitignore rename to packages/foundry/contracts/hooks/SecretSwap/.gitignore diff --git a/packages/foundry/contracts/hooks/Hookathon/.gitpod.yml b/packages/foundry/contracts/hooks/SecretSwap/.gitpod.yml similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.gitpod.yml rename to packages/foundry/contracts/hooks/SecretSwap/.gitpod.yml diff --git a/packages/foundry/contracts/hooks/Hookathon/.prettierignore b/packages/foundry/contracts/hooks/SecretSwap/.prettierignore similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.prettierignore rename to packages/foundry/contracts/hooks/SecretSwap/.prettierignore diff --git a/packages/foundry/contracts/hooks/Hookathon/.prettierrc.yml b/packages/foundry/contracts/hooks/SecretSwap/.prettierrc.yml similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.prettierrc.yml rename to packages/foundry/contracts/hooks/SecretSwap/.prettierrc.yml diff --git a/packages/foundry/contracts/hooks/Hookathon/.solcover.js b/packages/foundry/contracts/hooks/SecretSwap/.solcover.js similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.solcover.js rename to packages/foundry/contracts/hooks/SecretSwap/.solcover.js diff --git a/packages/foundry/contracts/hooks/Hookathon/.solhint.json b/packages/foundry/contracts/hooks/SecretSwap/.solhint.json similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.solhint.json rename to packages/foundry/contracts/hooks/SecretSwap/.solhint.json diff --git a/packages/foundry/contracts/hooks/Hookathon/.solhintignore b/packages/foundry/contracts/hooks/SecretSwap/.solhintignore similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/.solhintignore rename to packages/foundry/contracts/hooks/SecretSwap/.solhintignore diff --git a/packages/foundry/contracts/hooks/Hookathon/CustomProvider.ts b/packages/foundry/contracts/hooks/SecretSwap/CustomProvider.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/CustomProvider.ts rename to packages/foundry/contracts/hooks/SecretSwap/CustomProvider.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/LICENSE.md b/packages/foundry/contracts/hooks/SecretSwap/LICENSE.md similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/LICENSE.md rename to packages/foundry/contracts/hooks/SecretSwap/LICENSE.md diff --git a/packages/foundry/contracts/hooks/Hookathon/Makefile b/packages/foundry/contracts/hooks/SecretSwap/Makefile similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/Makefile rename to packages/foundry/contracts/hooks/SecretSwap/Makefile diff --git a/packages/foundry/contracts/hooks/Hookathon/README.md b/packages/foundry/contracts/hooks/SecretSwap/README.md similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/README.md rename to packages/foundry/contracts/hooks/SecretSwap/README.md diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/EncryptedERC20.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/EncryptedERC20.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/EncryptedERC20.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/EncryptedERC20.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/BaseHooks.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BaseHooks.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/BaseHooks.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BaseHooks.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/EnumerableMap.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/EnumerableMap.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/EnumerableMap.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/EnumerableMap.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/FixedPoint.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/FixedPoint.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/FixedPoint.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/FixedPoint.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAllowanceTransfer.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IAllowanceTransfer.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAllowanceTransfer.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IAllowanceTransfer.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthentication.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IAuthentication.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthentication.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IAuthentication.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthorizer.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IAuthorizer.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IAuthorizer.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IAuthorizer.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IBasePoolFactory.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IBasePoolFactory.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IBasePoolFactory.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IBasePoolFactory.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IEIP712.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IEIP712.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IEIP712.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IEIP712.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IHooks.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IHooks.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IHooks.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IHooks.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IProtocolFeeController.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IProtocolFeeController.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IProtocolFeeController.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IProtocolFeeController.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRateProvider.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRateProvider.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRateProvider.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRateProvider.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRouterCommon.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRouterCommon.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IRouterCommon.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRouterCommon.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVault.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVault.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVault.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVault.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultAdmin.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultAdmin.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultAdmin.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultAdmin.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultErrors.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultErrors.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultErrors.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultErrors.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultEvents.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultEvents.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultEvents.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultEvents.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultExtension.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultExtension.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultExtension.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultExtension.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultMain.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultMain.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/IVaultMain.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultMain.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/LogExpMath.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/LogExpMath.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/LogExpMath.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/LogExpMath.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultGuard.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultGuard.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultGuard.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultGuard.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultTypes.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultTypes.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/interfaces/VaultTypes.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultTypes.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/contracts/secretSwap.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/secretSwap.sol similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/contracts/secretSwap.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/secretSwap.sol diff --git a/packages/foundry/contracts/hooks/Hookathon/default.toml b/packages/foundry/contracts/hooks/SecretSwap/default.toml similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/default.toml rename to packages/foundry/contracts/hooks/SecretSwap/default.toml diff --git a/packages/foundry/contracts/hooks/Hookathon/deploy/deploy.ts b/packages/foundry/contracts/hooks/SecretSwap/deploy/deploy.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/deploy/deploy.ts rename to packages/foundry/contracts/hooks/SecretSwap/deploy/deploy.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/deploy/instance.ts b/packages/foundry/contracts/hooks/SecretSwap/deploy/instance.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/deploy/instance.ts rename to packages/foundry/contracts/hooks/SecretSwap/deploy/instance.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/docker-compose/docker-compose-full.yml.template b/packages/foundry/contracts/hooks/SecretSwap/docker-compose/docker-compose-full.yml.template similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/docker-compose/docker-compose-full.yml.template rename to packages/foundry/contracts/hooks/SecretSwap/docker-compose/docker-compose-full.yml.template diff --git a/packages/foundry/contracts/hooks/Hookathon/eslint.config.mjs b/packages/foundry/contracts/hooks/SecretSwap/eslint.config.mjs similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/eslint.config.mjs rename to packages/foundry/contracts/hooks/SecretSwap/eslint.config.mjs diff --git a/packages/foundry/contracts/hooks/Hookathon/hardhat.config.ts b/packages/foundry/contracts/hooks/SecretSwap/hardhat.config.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/hardhat.config.ts rename to packages/foundry/contracts/hooks/SecretSwap/hardhat.config.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/launch-fhevm.sh b/packages/foundry/contracts/hooks/SecretSwap/launch-fhevm.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/launch-fhevm.sh rename to packages/foundry/contracts/hooks/SecretSwap/launch-fhevm.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/package.json b/packages/foundry/contracts/hooks/SecretSwap/package.json similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/package.json rename to packages/foundry/contracts/hooks/SecretSwap/package.json diff --git a/packages/foundry/contracts/hooks/Hookathon/pnpm-lock.yaml b/packages/foundry/contracts/hooks/SecretSwap/pnpm-lock.yaml similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/pnpm-lock.yaml rename to packages/foundry/contracts/hooks/SecretSwap/pnpm-lock.yaml diff --git a/packages/foundry/contracts/hooks/Hookathon/remappings.txt b/packages/foundry/contracts/hooks/SecretSwap/remappings.txt similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/remappings.txt rename to packages/foundry/contracts/hooks/SecretSwap/remappings.txt diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/copy_fhe_keys.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/copy_fhe_keys.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/copy_fhe_keys.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/copy_fhe_keys.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/fund_test_addresses_docker.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/fund_test_addresses_docker.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/fund_test_addresses_docker.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/fund_test_addresses_docker.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/get_kms_core_version.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/get_kms_core_version.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/get_kms_core_version.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/get_kms_core_version.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/get_repository_info.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/get_repository_info.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/get_repository_info.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/get_repository_info.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/precomputeAddresses.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/precomputeAddresses.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/precomputeAddresses.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/precomputeAddresses.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_test.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/prepare_test.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/prepare_test.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/prepare_test.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/prepare_volumes_from_kms_core.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/prepare_volumes_from_kms_core.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/prepare_volumes_from_kms_core.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/prepare_volumes_from_kms_core.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/scripts/rewrite-docker-compose.sh b/packages/foundry/contracts/hooks/SecretSwap/scripts/rewrite-docker-compose.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/scripts/rewrite-docker-compose.sh rename to packages/foundry/contracts/hooks/SecretSwap/scripts/rewrite-docker-compose.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/setup.sh b/packages/foundry/contracts/hooks/SecretSwap/setup.sh similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/setup.sh rename to packages/foundry/contracts/hooks/SecretSwap/setup.sh diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/accounts.ts b/packages/foundry/contracts/hooks/SecretSwap/tasks/accounts.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/accounts.ts rename to packages/foundry/contracts/hooks/SecretSwap/tasks/accounts.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/checkNodeVersion.js b/packages/foundry/contracts/hooks/SecretSwap/tasks/checkNodeVersion.js similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/checkNodeVersion.js rename to packages/foundry/contracts/hooks/SecretSwap/tasks/checkNodeVersion.js diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/deployERC20.ts b/packages/foundry/contracts/hooks/SecretSwap/tasks/deployERC20.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/deployERC20.ts rename to packages/foundry/contracts/hooks/SecretSwap/tasks/deployERC20.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/getEthereumAddress.ts b/packages/foundry/contracts/hooks/SecretSwap/tasks/getEthereumAddress.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/getEthereumAddress.ts rename to packages/foundry/contracts/hooks/SecretSwap/tasks/getEthereumAddress.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskDeploy.ts b/packages/foundry/contracts/hooks/SecretSwap/tasks/taskDeploy.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/taskDeploy.ts rename to packages/foundry/contracts/hooks/SecretSwap/tasks/taskDeploy.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskGatewayRelayer.ts b/packages/foundry/contracts/hooks/SecretSwap/tasks/taskGatewayRelayer.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/taskGatewayRelayer.ts rename to packages/foundry/contracts/hooks/SecretSwap/tasks/taskGatewayRelayer.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskOracleRelayer.ts b/packages/foundry/contracts/hooks/SecretSwap/tasks/taskOracleRelayer.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/taskOracleRelayer.ts rename to packages/foundry/contracts/hooks/SecretSwap/tasks/taskOracleRelayer.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/tasks/taskTFHE.ts b/packages/foundry/contracts/hooks/SecretSwap/tasks/taskTFHE.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tasks/taskTFHE.ts rename to packages/foundry/contracts/hooks/SecretSwap/tasks/taskTFHE.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/asyncDecrypt.ts b/packages/foundry/contracts/hooks/SecretSwap/test/asyncDecrypt.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/asyncDecrypt.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/asyncDecrypt.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/coprocessorUtils.ts b/packages/foundry/contracts/hooks/SecretSwap/test/coprocessorUtils.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/coprocessorUtils.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/coprocessorUtils.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.fixture.ts b/packages/foundry/contracts/hooks/SecretSwap/test/encryptedERC20/EncryptedERC20.fixture.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.fixture.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/encryptedERC20/EncryptedERC20.fixture.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.ts b/packages/foundry/contracts/hooks/SecretSwap/test/encryptedERC20/EncryptedERC20.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/encryptedERC20/EncryptedERC20.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/encryptedERC20/EncryptedERC20.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/execution.ts b/packages/foundry/contracts/hooks/SecretSwap/test/execution.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/execution.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/execution.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/fhevmjsMocked.ts b/packages/foundry/contracts/hooks/SecretSwap/test/fhevmjsMocked.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/fhevmjsMocked.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/fhevmjsMocked.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/instance.ts b/packages/foundry/contracts/hooks/SecretSwap/test/instance.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/instance.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/instance.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/signers.ts b/packages/foundry/contracts/hooks/SecretSwap/test/signers.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/signers.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/signers.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/types.ts b/packages/foundry/contracts/hooks/SecretSwap/test/types.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/types.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/types.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/test/utils.ts b/packages/foundry/contracts/hooks/SecretSwap/test/utils.ts similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/test/utils.ts rename to packages/foundry/contracts/hooks/SecretSwap/test/utils.ts diff --git a/packages/foundry/contracts/hooks/Hookathon/tsconfig.json b/packages/foundry/contracts/hooks/SecretSwap/tsconfig.json similarity index 100% rename from packages/foundry/contracts/hooks/Hookathon/tsconfig.json rename to packages/foundry/contracts/hooks/SecretSwap/tsconfig.json From 373e4a80be69d527dfe6739a27bd364e05274014 Mon Sep 17 00:00:00 2001 From: swayam karle Date: Wed, 23 Oct 2024 16:29:04 +0530 Subject: [PATCH 4/7] Added All Contracts --- .../contracts/BalancerPoolToken.sol | 179 ++ .../contracts/Mocks/InputHelpersMock.sol | 32 + .../contracts/Mocks/PoolFactoryMock.sol | 220 +++ .../SecretSwap/contracts/Mocks/PoolInfo.sol | 58 + .../SecretSwap/contracts/Mocks/PoolMock.sol | 112 ++ .../SecretSwap/contracts/Mocks/RouterMock.sol | 157 ++ .../SecretSwap/contracts/Mocks/VaultMock.sol | 722 ++++++++ .../hooks/SecretSwap/contracts/Router.sol | 1171 +++++++++++++ .../SecretSwap/contracts/RouterCommon.sol | 339 ++++ .../contracts/SingletonAuthentication.sol | 47 + .../hooks/SecretSwap/contracts/Vault.sol | 1544 +++++++++++++++++ .../SecretSwap/contracts/VaultCommon.sol | 414 +++++ .../SecretSwap/contracts/VaultStorage.sol | 186 ++ .../contracts/interfaces/Authentication.sol | 61 + .../contracts/interfaces/BasePoolMath.sol | 424 +++++ .../contracts/interfaces/BufferHelpers.sol | 79 + .../interfaces/Bytes32AddressLib.sol | 18 + .../contracts/interfaces/CREATE3.sol | 89 + .../contracts/interfaces/CastingHelpers.sol | 38 + .../interfaces/EVMCallModeHelpers.sol | 22 + .../interfaces/FactoryWidePauseWindow.sol | 73 + .../contracts/interfaces/IBasePool.sol | 80 + .../interfaces/IERC20MultiTokenErrors.sol | 11 + .../contracts/interfaces/IPermit2.sol | 11 + .../contracts/interfaces/IPoolInfo.sol | 63 + .../contracts/interfaces/IPoolLiquidity.sol | 60 + .../contracts/interfaces/IRouter.sol | 535 ++++++ .../interfaces/ISignatureTransfer.sol | 134 ++ .../interfaces/ISwapFeePercentageBounds.sol | 21 + ...nbalancedLiquidityInvariantRatioBounds.sol | 22 + .../contracts/interfaces/IVaultMainMock.sol | 291 ++++ .../contracts/interfaces/IVersion.sol | 15 + .../SecretSwap/contracts/interfaces/IWETH.sol | 17 + .../contracts/interfaces/InputHelpers.sol | 106 ++ .../interfaces/PackedTokenBalance.sol | 62 + .../interfaces/ReentrancyGuardTransient.sol | 55 + .../contracts/interfaces/RevertCodec.sol | 58 + .../contracts/interfaces/ScalingHelpers.sol | 221 +++ .../contracts/interfaces/SlotDerivation.sol | 143 ++ .../interfaces/StorageSlotExtension.sol | 141 ++ .../interfaces/TransientStorageHelpers.sol | 142 ++ .../contracts/interfaces/VaultExtension.sol | 913 ++++++++++ .../contracts/interfaces/VaultExtensions.sol | 913 ++++++++++ .../contracts/interfaces/Version.sol | 32 + .../contracts/interfaces/WordCodec.sol | 241 +++ .../contracts/lib/HooksConfigLib.sol | 488 ++++++ .../contracts/lib/PoolConfigConst.sol | 56 + .../contracts/lib/PoolConfigLib.sol | 308 ++++ .../SecretSwap/contracts/lib/PoolDataLib.sol | 214 +++ .../contracts/lib/VaultExtensionsLib.sol | 24 + .../contracts/lib/VaultStateLib.sol | 42 + .../contracts/token/ERC20MultiToken.sol | 202 +++ 52 files changed, 11606 insertions(+) create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/BalancerPoolToken.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/InputHelpersMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolFactoryMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolInfo.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/RouterMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Router.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/RouterCommon.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/SingletonAuthentication.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Vault.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/VaultCommon.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/VaultStorage.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Authentication.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BasePoolMath.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BufferHelpers.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Bytes32AddressLib.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CREATE3.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CastingHelpers.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/EVMCallModeHelpers.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/FactoryWidePauseWindow.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IBasePool.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IERC20MultiTokenErrors.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPermit2.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolInfo.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolLiquidity.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRouter.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISignatureTransfer.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISwapFeePercentageBounds.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IUnbalancedLiquidityInvariantRatioBounds.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultMainMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVersion.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IWETH.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/InputHelpers.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/PackedTokenBalance.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ReentrancyGuardTransient.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/RevertCodec.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ScalingHelpers.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/SlotDerivation.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/StorageSlotExtension.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/TransientStorageHelpers.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtensions.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Version.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/WordCodec.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/lib/HooksConfigLib.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigConst.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigLib.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolDataLib.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultExtensionsLib.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultStateLib.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/token/ERC20MultiToken.sol diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/BalancerPoolToken.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/BalancerPoolToken.sol new file mode 100644 index 00000000..fa49f0f7 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/BalancerPoolToken.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import { Nonces } from "@openzeppelin/contracts/utils/Nonces.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import { IRateProvider } from "./interfaces/IRateProvider.sol"; +import { IVault } from "./interfaces/IVault.sol"; + +import { VaultGuard } from "./interfaces/VaultGuard.sol"; + +/** + * @notice `BalancerPoolToken` is a fully ERC20-compatible token to be used as the base contract for Balancer Pools, + * with all the data and implementation delegated to the ERC20Multitoken contract. + + * @dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612]. + */ +contract BalancerPoolToken is IERC20, IERC20Metadata, IERC20Permit, IRateProvider, EIP712, Nonces, ERC165, VaultGuard { + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @notice Operation failed due to an expired permit signature. + * @param deadline The permit deadline that expired + */ + error ERC2612ExpiredSignature(uint256 deadline); + + /** + * @notice Operation failed due to a non-matching signature + * @param signer The address corresponding to the signature provider + * @param owner The address of the owner (expected value of the signature provider) + */ + error ERC2612InvalidSigner(address signer, address owner); + + // EIP712 also defines _name. + string private _bptName; + string private _bptSymbol; + + constructor(IVault vault_, string memory bptName, string memory bptSymbol) EIP712(bptName, "1") VaultGuard(vault_) { + _bptName = bptName; + _bptSymbol = bptSymbol; + } + + /// @inheritdoc IERC20Metadata + function name() external view returns (string memory) { + return _bptName; + } + + /// @inheritdoc IERC20Metadata + function symbol() external view returns (string memory) { + return _bptSymbol; + } + + /// @inheritdoc IERC20Metadata + function decimals() external pure returns (uint8) { + // Always 18 decimals for BPT. + return 18; + } + + /// @inheritdoc IERC20 + function totalSupply() public view returns (uint256) { + return _vault.totalSupply(address(this)); + } + + function getVault() public view returns (IVault) { + return _vault; + } + + /// @inheritdoc IERC20 + function balanceOf(address account) external view returns (uint256) { + return _vault.balanceOf(address(this), account); + } + + /// @inheritdoc IERC20 + function transfer(address to, uint256 amount) external returns (bool) { + // Vault will perform the transfer and call emitTransfer to emit the event from this contract. + _vault.transfer(msg.sender, to, amount); + return true; + } + + /// @inheritdoc IERC20 + function allowance(address owner, address spender) external view returns (uint256) { + return _vault.allowance(address(this), owner, spender); + } + + /// @inheritdoc IERC20 + function approve(address spender, uint256 amount) external returns (bool) { + // Vault will perform the approval and call emitApproval to emit the event from this contract. + _vault.approve(msg.sender, spender, amount); + return true; + } + + /// @inheritdoc IERC20 + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + // Vault will perform the transfer and call emitTransfer to emit the event from this contract. + _vault.transferFrom(msg.sender, from, to, amount); + return true; + } + + /** + * Accounting is centralized in the MultiToken contract, and the actual transfers and approvals are done there. + * Operations can be initiated from either the token contract or the MultiToken. + * + * To maintain compliance with the ERC-20 standard, and conform to the expectations of off-chain processes, + * the MultiToken calls `emitTransfer` and `emitApproval` during those operations, so that the event is emitted + * only from the token contract. These events are NOT defined in the MultiToken contract. + */ + + /// @dev Emit the Transfer event. This function can only be called by the MultiToken. + function emitTransfer(address from, address to, uint256 amount) external onlyVault { + emit Transfer(from, to, amount); + } + + /// @dev Emit the Approval event. This function can only be called by the MultiToken. + function emitApproval(address owner, address spender, uint256 amount) external onlyVault { + emit Approval(owner, spender, amount); + } + + // @inheritdoc IERC20Permit + function permit( + address owner, + address spender, + uint256 amount, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + // solhint-disable-next-line not-rely-on-time + if (block.timestamp > deadline) { + revert ERC2612ExpiredSignature(deadline); + } + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + if (signer != owner) { + revert ERC2612InvalidSigner(signer, owner); + } + + _vault.approve(owner, spender, amount); + } + + // @inheritdoc IERC20Permit + function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } + + /// @notice Increment the sender's nonce to revoke any currently granted (but not yet executed) `permit`. + function revokePermit() external { + _useNonce(msg.sender); + } + + // @inheritdoc IERC20Permit + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @notice Get the BPT rate, which is defined as: pool invariant/total supply. + * @dev The VaultExtension contract defines a default implementation (`getBptRate`) to calculate the rate + * of any given pool, which should be sufficient in nearly all cases. + * + * @return rate Rate of the pool's BPT + */ + function getRate() public view virtual returns (uint256) { + return getVault().getBptRate(address(this)); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/InputHelpersMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/InputHelpersMock.sol new file mode 100644 index 00000000..6d30497a --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/InputHelpersMock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { TokenConfig } from "../interfaces/VaultTypes.sol"; + +import { InputHelpers } from "../interfaces/InputHelpers.sol"; + +contract InputHelpersMock { + function sortTokens(IERC20[] memory tokens) external pure returns (IERC20[] memory) { + return InputHelpers.sortTokens(tokens); + } + + function ensureSortedTokens(IERC20[] memory tokens) external pure { + InputHelpers.ensureSortedTokens(tokens); + } + + function sortTokenConfig(TokenConfig[] memory tokenConfig) public pure returns (TokenConfig[] memory) { + for (uint256 i = 0; i < tokenConfig.length - 1; ++i) { + for (uint256 j = 0; j < tokenConfig.length - i - 1; j++) { + if (tokenConfig[j].token > tokenConfig[j + 1].token) { + // Swap if they're out of order. + (tokenConfig[j], tokenConfig[j + 1]) = (tokenConfig[j + 1], tokenConfig[j]); + } + } + } + + return tokenConfig; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolFactoryMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolFactoryMock.sol new file mode 100644 index 00000000..323d95af --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolFactoryMock.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IBasePoolFactory } from "../interfaces/IBasePoolFactory.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import "../interfaces/VaultTypes.sol"; + +import { FactoryWidePauseWindow } from "../interfaces/FactoryWidePauseWindow.sol"; +import { CREATE3 } from "../interfaces/CREATE3.sol"; + +import { SingletonAuthentication } from "../SingletonAuthentication.sol"; +import { PoolMock } from "./PoolMock.sol"; + +contract PoolFactoryMock is IBasePoolFactory, SingletonAuthentication, FactoryWidePauseWindow { + uint256 private constant DEFAULT_SWAP_FEE = 0; + + IVault private immutable _vault; + + // Avoid dependency on BasePoolFactory; copy storage here. + mapping(address pool => bool isFromFactory) private _isPoolFromFactory; + bool private _disabled; + + constructor( + IVault vault, + uint32 pauseWindowDuration + ) SingletonAuthentication(vault) FactoryWidePauseWindow(pauseWindowDuration) { + _vault = vault; + } + + function createPool(string memory name, string memory symbol) external returns (address) { + PoolMock newPool = new PoolMock(IVault(address(_vault)), name, symbol); + _registerPoolWithFactory(address(newPool)); + return address(newPool); + } + + function registerTestPool(address pool, TokenConfig[] memory tokenConfig) external { + PoolRoleAccounts memory roleAccounts; + + _vault.registerPool( + pool, + tokenConfig, + DEFAULT_SWAP_FEE, + getNewPoolPauseWindowEndTime(), + false, + roleAccounts, + address(0), // No hook contract + _getDefaultLiquidityManagement() + ); + } + + function registerTestPool(address pool, TokenConfig[] memory tokenConfig, address poolHooksContract) external { + PoolRoleAccounts memory roleAccounts; + + _vault.registerPool( + pool, + tokenConfig, + DEFAULT_SWAP_FEE, + getNewPoolPauseWindowEndTime(), + false, + roleAccounts, + poolHooksContract, + _getDefaultLiquidityManagement() + ); + } + + function registerTestPool( + address pool, + TokenConfig[] memory tokenConfig, + address poolHooksContract, + address poolCreator + ) external { + PoolRoleAccounts memory roleAccounts; + roleAccounts.poolCreator = poolCreator; + + _vault.registerPool( + pool, + tokenConfig, + DEFAULT_SWAP_FEE, + getNewPoolPauseWindowEndTime(), + false, + roleAccounts, + poolHooksContract, + _getDefaultLiquidityManagement() + ); + } + + function registerGeneralTestPool( + address pool, + TokenConfig[] memory tokenConfig, + uint256 swapFee, + uint32 pauseWindowDuration, + bool protocolFeeExempt, + PoolRoleAccounts memory roleAccounts, + address poolHooksContract + ) external { + _vault.registerPool( + pool, + tokenConfig, + swapFee, + uint32(block.timestamp) + pauseWindowDuration, + protocolFeeExempt, + roleAccounts, + poolHooksContract, + _getDefaultLiquidityManagement() + ); + } + + function registerPool( + address pool, + TokenConfig[] memory tokenConfig, + PoolRoleAccounts memory roleAccounts, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external { + _vault.registerPool( + pool, + tokenConfig, + DEFAULT_SWAP_FEE, + getNewPoolPauseWindowEndTime(), + false, + roleAccounts, + poolHooksContract, + liquidityManagement + ); + } + + function registerPoolWithSwapFee( + address pool, + TokenConfig[] memory tokenConfig, + uint256 swapFeePercentage, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external { + PoolRoleAccounts memory roleAccounts; + + _vault.registerPool( + pool, + tokenConfig, + swapFeePercentage, + getNewPoolPauseWindowEndTime(), + false, + roleAccounts, + poolHooksContract, + liquidityManagement + ); + } + + // For tests; otherwise can't get the exact event arguments. + function registerPoolAtTimestamp( + address pool, + TokenConfig[] memory tokenConfig, + uint32 timestamp, + PoolRoleAccounts memory roleAccounts, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external { + _vault.registerPool( + pool, + tokenConfig, + DEFAULT_SWAP_FEE, + timestamp, + false, + roleAccounts, + poolHooksContract, + liquidityManagement + ); + } + + function _getDefaultLiquidityManagement() private pure returns (LiquidityManagement memory) { + LiquidityManagement memory liquidityManagement; + liquidityManagement.enableAddLiquidityCustom = true; + liquidityManagement.enableRemoveLiquidityCustom = true; + return liquidityManagement; + } + + /// @inheritdoc IBasePoolFactory + function isPoolFromFactory(address pool) external view returns (bool) { + return _isPoolFromFactory[pool]; + } + + /// @inheritdoc IBasePoolFactory + function isDisabled() public view returns (bool) { + return _disabled; + } + + /// @inheritdoc IBasePoolFactory + function getDeploymentAddress(bytes32 salt) public view returns (address) { + return CREATE3.getDeployed(_computeFinalSalt(salt)); + } + + /// @inheritdoc IBasePoolFactory + function disable() external authenticate { + _ensureEnabled(); + + _disabled = true; + + emit FactoryDisabled(); + } + + function _registerPoolWithFactory(address pool) internal virtual { + _ensureEnabled(); + + _isPoolFromFactory[pool] = true; + + emit PoolCreated(pool); + } + + // Functions from BasePoolFactory + + function _ensureEnabled() internal view { + if (isDisabled()) { + revert Disabled(); + } + } + + function _computeFinalSalt(bytes32 salt) internal view virtual returns (bytes32) { + return keccak256(abi.encode(msg.sender, block.chainid, salt)); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolInfo.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolInfo.sol new file mode 100644 index 00000000..7575e067 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolInfo.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IPoolInfo } from "../interfaces/IPoolInfo.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { TokenInfo, PoolConfig } from "../interfaces/VaultTypes.sol"; + +contract PoolInfo is IPoolInfo { + IVault private immutable _vault; + + constructor(IVault vault) { + _vault = vault; + } + + /// @inheritdoc IPoolInfo + function getTokens() external view returns (IERC20[] memory tokens) { + return _vault.getPoolTokens(address(this)); + } + + /// @inheritdoc IPoolInfo + function getTokenInfo() + external + view + returns ( + IERC20[] memory tokens, + TokenInfo[] memory tokenInfo, + uint256[] memory balancesRaw, + uint256[] memory lastBalancesLiveScaled18 + ) + { + return _vault.getPoolTokenInfo(address(this)); + } + + /// @inheritdoc IPoolInfo + function getCurrentLiveBalances() external view returns (uint256[] memory balancesLiveScaled18) { + return _vault.getCurrentLiveBalances(address(this)); + } + + /// @inheritdoc IPoolInfo + function getStaticSwapFeePercentage() external view returns (uint256) { + return _vault.getStaticSwapFeePercentage((address(this))); + } + + /// @inheritdoc IPoolInfo + function getAggregateFeePercentages() + external + view + returns (uint256 aggregateSwapFeePercentage, uint256 aggregateYieldFeePercentage) + { + PoolConfig memory poolConfig = _vault.getPoolConfig(address(this)); + + aggregateSwapFeePercentage = poolConfig.aggregateSwapFeePercentage; + aggregateYieldFeePercentage = poolConfig.aggregateYieldFeePercentage; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolMock.sol new file mode 100644 index 00000000..2bb6ba8a --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/PoolMock.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { ISwapFeePercentageBounds } from "../interfaces/ISwapFeePercentageBounds.sol"; +import { IBasePool } from "../interfaces/IBasePool.sol"; +import { IPoolLiquidity } from "../interfaces/IPoolLiquidity.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import "../interfaces/VaultTypes.sol"; + +import { FixedPoint } from "../interfaces/FixedPoint.sol"; +import { PoolInfo } from "./PoolInfo.sol"; + +import { BalancerPoolToken } from "../BalancerPoolToken.sol"; + +contract PoolMock is IBasePool, IPoolLiquidity, BalancerPoolToken, PoolInfo { + using FixedPoint for uint256; + + // Amounts in are multiplied by the multiplier, amounts out are divided by it. + uint256 private _multiplier = FixedPoint.ONE; + + // If non-zero, use this return value for `getRate` (otherwise, defer to BalancerPoolToken's base implementation). + uint256 private _mockRate; + + constructor( + IVault vault, + string memory name, + string memory symbol + ) BalancerPoolToken(vault, name, symbol) PoolInfo(vault) { + // solhint-previous-line no-empty-blocks + } + + function computeInvariant(uint256[] memory balances, Rounding) public pure returns (uint256) { + // inv = x + y + uint256 invariant; + for (uint256 i = 0; i < balances.length; ++i) { + invariant += balances[i]; + } + return invariant; + } + + /// @inheritdoc IBasePool + function computeBalance( + uint256[] memory balances, + uint256 tokenInIndex, + uint256 invariantRatio + ) external pure returns (uint256 newBalance) { + // inv = x + y + uint256 invariant = computeInvariant(balances, Rounding.ROUND_DOWN); + return (balances[tokenInIndex] + invariant.mulDown(invariantRatio)) - invariant; + } + + function setMultiplier(uint256 newMultiplier) external { + _multiplier = newMultiplier; + } + + function onSwap(PoolSwapParams calldata params) external view override returns (uint256 amountCalculated) { + return + params.kind == SwapKind.EXACT_IN + ? params.amountGivenScaled18.mulDown(_multiplier) + : params.amountGivenScaled18.divDown(_multiplier); + } + + function onAddLiquidityCustom( + address, + uint256[] memory maxAmountsInScaled18, + uint256 minBptAmountOut, + uint256[] memory, + bytes memory userData + ) external pure override returns (uint256[] memory, uint256, uint256[] memory, bytes memory) { + return (maxAmountsInScaled18, minBptAmountOut, new uint256[](maxAmountsInScaled18.length), userData); + } + + function onRemoveLiquidityCustom( + address, + uint256 maxBptAmountIn, + uint256[] memory minAmountsOut, + uint256[] memory, + bytes memory userData + ) external pure override returns (uint256, uint256[] memory, uint256[] memory, bytes memory) { + return (maxBptAmountIn, minAmountsOut, new uint256[](minAmountsOut.length), userData); + } + + /// @dev Even though pools do not handle scaling, we still need this for the tests. + function getDecimalScalingFactors() external view returns (uint256[] memory scalingFactors) { + (scalingFactors, ) = _vault.getPoolTokenRates(address(this)); + } + + function getMinimumSwapFeePercentage() external pure override returns (uint256) { + return 0; + } + + function getMaximumSwapFeePercentage() external pure override returns (uint256) { + return FixedPoint.ONE; + } + + function getMinimumInvariantRatio() external view virtual override returns (uint256) { + return 0; + } + + function getMaximumInvariantRatio() external view virtual override returns (uint256) { + return 1e40; // Something just really big; should always work + } + + function setMockRate(uint256 mockRate) external { + _mockRate = mockRate; + } + + function getRate() public view override returns (uint256) { + return _mockRate == 0 ? super.getRate() : _mockRate; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/RouterMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/RouterMock.sol new file mode 100644 index 00000000..dea69216 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/RouterMock.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IPermit2 } from "../interfaces/IPermit2.sol"; + +import { IWETH } from "../interfaces/IWETH.sol"; +import { SwapKind } from "../interfaces/VaultTypes.sol"; +import { IRouter } from "../interfaces/IRouter.sol"; +import { IVault } from "../interfaces/IVault.sol"; + +import { RevertCodec } from "../interfaces/RevertCodec.sol"; + +import { Router } from "../Router.sol"; + +contract RouterMock is Router { + error MockErrorCode(); + + constructor(IVault vault, IWETH weth, IPermit2 permit2) Router(vault, weth, permit2) {} + + function manualReentrancyInitializeHook() external nonReentrant { + IRouter.InitializeHookParams memory hookParams; + Router(payable(this)).initializeHook(hookParams); + } + + function manualReentrancyAddLiquidityHook() external nonReentrant { + AddLiquidityHookParams memory params; + Router(payable(this)).addLiquidityHook(params); + } + + function manualReentrancyRemoveLiquidityHook() external nonReentrant { + RemoveLiquidityHookParams memory params; + Router(payable(this)).removeLiquidityHook(params); + } + + function manualReentrancyRemoveLiquidityRecoveryHook() external nonReentrant { + Router(payable(this)).removeLiquidityRecoveryHook(address(0), address(0), 0); + } + + function manualReentrancySwapSingleTokenHook() external nonReentrant { + IRouter.SwapSingleTokenHookParams memory params; + Router(payable(this)).swapSingleTokenHook(params); + } + + function manualReentrancyAddLiquidityToBufferHook() external nonReentrant { + Router(payable(this)).addLiquidityToBufferHook(IERC4626(address(0)), 0, address(0)); + } + + function manualReentrancyQuerySwapHook() external nonReentrant { + IRouter.SwapSingleTokenHookParams memory params; + Router(payable(this)).querySwapHook(params); + } + + function getSingleInputArrayAndTokenIndex( + address pool, + IERC20 token, + uint256 amountGiven + ) external view returns (uint256[] memory amountsGiven, uint256 tokenIndex) { + return _getSingleInputArrayAndTokenIndex(pool, token, amountGiven); + } + + function querySwapSingleTokenExactInAndRevert( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + bytes calldata userData + ) external returns (uint256 amountCalculated) { + try + _vault.quoteAndRevert( + abi.encodeWithSelector( + Router.querySwapHook.selector, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_IN, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountIn, + limit: 0, + deadline: _MAX_AMOUNT, + wethIsEth: false, + userData: userData + }) + ) + ) + { + revert("Unexpected success"); + } catch (bytes memory result) { + return abi.decode(RevertCodec.catchEncodedResult(result), (uint256)); + } + } + + function querySpoof() external returns (uint256) { + try _vault.quoteAndRevert(abi.encodeWithSelector(RouterMock.querySpoofHook.selector)) { + revert("Unexpected success"); + } catch (bytes memory result) { + return abi.decode(RevertCodec.catchEncodedResult(result), (uint256)); + } + } + + function querySpoofHook() external pure { + revert RevertCodec.Result(abi.encode(uint256(1234))); + } + + function queryRevertErrorCode() external returns (uint256) { + try _vault.quoteAndRevert(abi.encodeWithSelector(RouterMock.queryRevertErrorCodeHook.selector)) { + revert("Unexpected success"); + } catch (bytes memory result) { + return abi.decode(RevertCodec.catchEncodedResult(result), (uint256)); + } + } + + function queryRevertErrorCodeHook() external pure { + revert MockErrorCode(); + } + + function queryRevertLegacy() external returns (uint256) { + try _vault.quoteAndRevert(abi.encodeWithSelector(RouterMock.queryRevertLegacyHook.selector)) { + revert("Unexpected success"); + } catch (bytes memory result) { + return abi.decode(RevertCodec.catchEncodedResult(result), (uint256)); + } + } + + function queryRevertLegacyHook() external pure { + revert("Legacy revert reason"); + } + + function queryRevertPanic() external returns (uint256) { + try _vault.quoteAndRevert(abi.encodeWithSelector(RouterMock.queryRevertPanicHook.selector)) { + revert("Unexpected success"); + } catch (bytes memory result) { + return abi.decode(RevertCodec.catchEncodedResult(result), (uint256)); + } + } + + function queryRevertPanicHook() external pure returns (uint256) { + uint256 a = 10; + uint256 b = 0; + return a / b; + } + + function queryRevertNoReason() external returns (uint256) { + try _vault.quoteAndRevert(abi.encodeWithSelector(RouterMock.queryRevertNoReasonHook.selector)) { + revert("Unexpected success"); + } catch (bytes memory result) { + return abi.decode(RevertCodec.catchEncodedResult(result), (uint256)); + } + } + + function queryRevertNoReasonHook() external pure returns (uint256) { + revert(); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultMock.sol new file mode 100644 index 00000000..c7868eaf --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultMock.sol @@ -0,0 +1,722 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IProtocolFeeController } from "../interfaces/IProtocolFeeController.sol"; +import { IRateProvider } from "../interfaces/IRateProvider.sol"; +import { IVaultExtension } from "../interfaces/IVaultExtension.sol"; +import { IVaultMainMock } from "../interfaces/IVaultMainMock.sol"; +import { IAuthorizer } from "../interfaces/IAuthorizer.sol"; +import { IVaultAdmin } from "../interfaces/IVaultAdmin.sol"; +import { IVault } from "../interfaces/IVault.sol"; +import { IHooks } from "../interfaces/IHooks.sol"; +import "../interfaces/VaultTypes.sol"; + +import { StorageSlotExtension } from "../interfaces/StorageSlotExtension.sol"; +import { PackedTokenBalance } from "../interfaces/PackedTokenBalance.sol"; +import { BufferHelpers } from "../interfaces/BufferHelpers.sol"; +import { + TransientStorageHelpers, + TokenDeltaMappingSlotType +} from "../interfaces/TransientStorageHelpers.sol"; + +import { VaultStateLib, VaultStateBits } from "../lib/VaultStateLib.sol"; +import { PoolConfigLib, PoolConfigBits } from "../lib/PoolConfigLib.sol"; +import { HooksConfigLib } from "../lib/HooksConfigLib.sol"; +import { InputHelpersMock } from "./InputHelpersMock.sol"; +import { PoolFactoryMock } from "./PoolFactoryMock.sol"; +import { VaultExtension } from "../interfaces/VaultExtension.sol"; +import { PoolDataLib } from "../lib/PoolDataLib.sol"; +import { Vault } from "../Vault.sol"; + +struct SwapInternalStateLocals { + VaultSwapParams vaultSwapParams; + SwapState swapState; + PoolData poolData; + VaultState vaultState; +} + +contract VaultMock is IVaultMainMock, Vault { + using PackedTokenBalance for bytes32; + using PoolConfigLib for PoolConfigBits; + using HooksConfigLib for PoolConfigBits; + using VaultStateLib for VaultStateBits; + using BufferHelpers for bytes32; + using PoolDataLib for PoolData; + using TransientStorageHelpers for *; + using StorageSlotExtension for *; + + PoolFactoryMock private immutable _poolFactoryMock; + InputHelpersMock private immutable _inputHelpersMock; + + constructor( + IVaultExtension vaultExtension, + IAuthorizer authorizer, + IProtocolFeeController protocolFeeController + ) Vault(vaultExtension, authorizer, protocolFeeController) { + uint32 pauseWindowEndTime = IVaultAdmin(address(vaultExtension)).getPauseWindowEndTime(); + uint32 bufferPeriodDuration = IVaultAdmin(address(vaultExtension)).getBufferPeriodDuration(); + _poolFactoryMock = new PoolFactoryMock(IVault(address(this)), pauseWindowEndTime - bufferPeriodDuration); + _inputHelpersMock = new InputHelpersMock(); + } + + function getPoolFactoryMock() external view returns (address) { + return address(_poolFactoryMock); + } + + function burnERC20(address token, address from, uint256 amount) external { + _burn(token, from, amount); + } + + function mintERC20(address token, address to, uint256 amount) external { + _mint(token, to, amount); + } + + // Used for testing pool registration, which is ordinarily done in the pool factory. + // The Mock pool has an argument for whether or not to register on deployment. To call register pool + // separately, deploy it with the registration flag false, then call this function. + function manualRegisterPool(address pool, IERC20[] memory tokens) external whenVaultNotPaused { + PoolRoleAccounts memory roleAccounts; + + _poolFactoryMock.registerPool( + pool, + buildTokenConfig(tokens), + roleAccounts, + address(0), // No hook contract + _getDefaultLiquidityManagement() + ); + } + + function manualRegisterPoolWithSwapFee( + address pool, + IERC20[] memory tokens, + uint256 swapFeePercentage + ) external whenVaultNotPaused { + LiquidityManagement memory liquidityManagement = _getDefaultLiquidityManagement(); + liquidityManagement.disableUnbalancedLiquidity = true; + + _poolFactoryMock.registerPoolWithSwapFee( + pool, + buildTokenConfig(tokens), + swapFeePercentage, + address(0), // No hook contract + liquidityManagement + ); + } + + function manualRegisterPoolPassThruTokens(address pool, IERC20[] memory tokens) external { + TokenConfig[] memory tokenConfig = new TokenConfig[](tokens.length); + PoolRoleAccounts memory roleAccounts; + + for (uint256 i = 0; i < tokens.length; ++i) { + tokenConfig[i].token = tokens[i]; + } + + _poolFactoryMock.registerPool( + pool, + tokenConfig, + roleAccounts, + address(0), // No hook contract + _getDefaultLiquidityManagement() + ); + } + + function manualRegisterPoolAtTimestamp( + address pool, + IERC20[] memory tokens, + uint32 timestamp, + PoolRoleAccounts memory roleAccounts + ) external whenVaultNotPaused { + _poolFactoryMock.registerPoolAtTimestamp( + pool, + buildTokenConfig(tokens), + timestamp, + roleAccounts, + address(0), // No hook contract + _getDefaultLiquidityManagement() + ); + } + + function manualSetPoolRegistered(address pool, bool status) public { + _poolConfigBits[pool] = _poolConfigBits[pool].setPoolRegistered(status); + } + + function manualSetInitializedPool(address pool, bool isPoolInitialized) public { + _poolConfigBits[pool] = _poolConfigBits[pool].setPoolInitialized(isPoolInitialized); + } + + function manualSetPoolPauseWindowEndTime(address pool, uint32 pauseWindowEndTime) public { + _poolConfigBits[pool] = _poolConfigBits[pool].setPauseWindowEndTime(pauseWindowEndTime); + } + + function manualSetPoolPaused(address pool, bool isPoolPaused) public { + _poolConfigBits[pool] = _poolConfigBits[pool].setPoolPaused(isPoolPaused); + } + + function manualSetVaultPaused(bool isVaultPaused) public { + _vaultStateBits = _vaultStateBits.setVaultPaused(isVaultPaused); + } + + function manualSetVaultState(bool isVaultPaused, bool isQueryDisabled) public { + _vaultStateBits = _vaultStateBits.setVaultPaused(isVaultPaused).setQueryDisabled(isQueryDisabled); + } + + function manualSetPoolConfig(address pool, PoolConfig memory config) public { + PoolConfigBits poolConfigBits = _poolConfigBits[pool]; + + poolConfigBits = poolConfigBits.setPoolRegistered(config.isPoolRegistered); + poolConfigBits = poolConfigBits.setPoolInitialized(config.isPoolInitialized); + poolConfigBits = poolConfigBits.setPoolInRecoveryMode(config.isPoolInRecoveryMode); + poolConfigBits = poolConfigBits.setPoolPaused(config.isPoolPaused); + poolConfigBits = poolConfigBits.setStaticSwapFeePercentage(config.staticSwapFeePercentage); + poolConfigBits = poolConfigBits.setAggregateSwapFeePercentage(config.aggregateSwapFeePercentage); + poolConfigBits = poolConfigBits.setAggregateYieldFeePercentage(config.aggregateYieldFeePercentage); + poolConfigBits = poolConfigBits.setTokenDecimalDiffs(config.tokenDecimalDiffs); + poolConfigBits = poolConfigBits.setPauseWindowEndTime(config.pauseWindowEndTime); + poolConfigBits = poolConfigBits.setDisableUnbalancedLiquidity( + config.liquidityManagement.disableUnbalancedLiquidity + ); + poolConfigBits = poolConfigBits.setAddLiquidityCustom(config.liquidityManagement.enableAddLiquidityCustom); + poolConfigBits = poolConfigBits.setRemoveLiquidityCustom( + config.liquidityManagement.enableRemoveLiquidityCustom + ); + poolConfigBits = poolConfigBits.setDonation(config.liquidityManagement.enableDonation); + + _poolConfigBits[pool] = poolConfigBits; + } + + function manualSetStaticSwapFeePercentage(address pool, uint256 value) public { + _setStaticSwapFeePercentage(pool, value); + } + + function manualUnsafeSetStaticSwapFeePercentage(address pool, uint256 value) public { + _poolConfigBits[pool] = _poolConfigBits[pool].setStaticSwapFeePercentage(value); + } + + function manualSetHooksConfig(address pool, HooksConfig memory hooksConfig) public { + PoolConfigBits poolConfigBits = _poolConfigBits[pool]; + + poolConfigBits = poolConfigBits.setHookAdjustedAmounts(hooksConfig.enableHookAdjustedAmounts); + poolConfigBits = poolConfigBits.setShouldCallBeforeInitialize(hooksConfig.shouldCallBeforeInitialize); + poolConfigBits = poolConfigBits.setShouldCallAfterInitialize(hooksConfig.shouldCallAfterInitialize); + poolConfigBits = poolConfigBits.setShouldCallComputeDynamicSwapFee(hooksConfig.shouldCallComputeDynamicSwapFee); + poolConfigBits = poolConfigBits.setShouldCallBeforeSwap(hooksConfig.shouldCallBeforeSwap); + poolConfigBits = poolConfigBits.setShouldCallAfterSwap(hooksConfig.shouldCallAfterSwap); + poolConfigBits = poolConfigBits.setShouldCallBeforeAddLiquidity(hooksConfig.shouldCallBeforeAddLiquidity); + poolConfigBits = poolConfigBits.setShouldCallAfterAddLiquidity(hooksConfig.shouldCallAfterAddLiquidity); + poolConfigBits = poolConfigBits.setShouldCallBeforeRemoveLiquidity(hooksConfig.shouldCallBeforeRemoveLiquidity); + poolConfigBits = poolConfigBits.setShouldCallAfterRemoveLiquidity(hooksConfig.shouldCallAfterRemoveLiquidity); + + _poolConfigBits[pool] = poolConfigBits; + _hooksContracts[pool] = IHooks(hooksConfig.hooksContract); + } + + function manualSetPoolConfigBits(address pool, PoolConfigBits config) public { + _poolConfigBits[pool] = config; + } + + function manualSetPoolTokenInfo(address pool, TokenConfig[] memory tokenConfig) public { + for (uint256 i = 0; i < tokenConfig.length; ++i) { + _poolTokenInfo[pool][tokenConfig[i].token] = TokenInfo({ + tokenType: tokenConfig[i].tokenType, + rateProvider: tokenConfig[i].rateProvider, + paysYieldFees: tokenConfig[i].paysYieldFees + }); + } + } + + function manualSetPoolTokenInfo(address pool, IERC20[] memory tokens, TokenInfo[] memory tokenInfo) public { + for (uint256 i = 0; i < tokens.length; ++i) { + _poolTokenInfo[pool][tokens[i]] = tokenInfo[i]; + } + } + + function manualSetPoolTokens(address pool, IERC20[] memory tokens) public { + _poolTokens[pool] = tokens; + } + + function manualSetPoolTokensAndBalances( + address pool, + IERC20[] memory tokens, + uint256[] memory tokenBalanceRaw, + uint256[] memory tokenBalanceLiveScaled18 + ) public { + require(tokens.length == tokenBalanceRaw.length, "VaultMock: TOKENS_LENGTH_MISMATCH"); + require(tokens.length == tokenBalanceLiveScaled18.length, "VaultMock: TOKENS_LENGTH_MISMATCH"); + + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + for (uint256 i = 0; i < tokens.length; ++i) { + poolTokenBalances[i] = PackedTokenBalance.toPackedBalance(tokenBalanceRaw[i], tokenBalanceLiveScaled18[i]); + } + + _poolTokens[pool] = tokens; + } + + function manualSetPoolBalances( + address pool, + uint256[] memory tokenBalanceRaw, + uint256[] memory tokenBalanceLiveScaled18 + ) public { + IERC20[] memory tokens = _poolTokens[pool]; + + require(tokens.length == tokenBalanceRaw.length, "VaultMock: TOKENS_LENGTH_MISMATCH"); + require(tokens.length == tokenBalanceLiveScaled18.length, "VaultMock: TOKENS_LENGTH_MISMATCH"); + + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + for (uint256 i = 0; i < tokens.length; ++i) { + poolTokenBalances[i] = PackedTokenBalance.toPackedBalance(tokenBalanceRaw[i], tokenBalanceLiveScaled18[i]); + } + } + + function mockIsUnlocked() public view onlyWhenUnlocked {} + + function mockWithInitializedPool(address pool) public view withInitializedPool(pool) {} + + function ensurePoolNotPaused(address pool) public view { + _ensurePoolNotPaused(pool); + } + + function ensureUnpausedAndGetVaultState(address pool) public view returns (VaultState memory vaultState) { + _ensureUnpaused(pool); + VaultStateBits state = _vaultStateBits; + vaultState = VaultState({ + isQueryDisabled: state.isQueryDisabled(), + isVaultPaused: state.isVaultPaused(), + areBuffersPaused: state.areBuffersPaused() + }); + } + + function buildTokenConfig(IERC20[] memory tokens) public view returns (TokenConfig[] memory tokenConfig) { + tokenConfig = new TokenConfig[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + tokenConfig[i].token = tokens[i]; + } + + tokenConfig = _inputHelpersMock.sortTokenConfig(tokenConfig); + } + + function buildTokenConfig( + IERC20[] memory tokens, + IRateProvider[] memory rateProviders + ) public view returns (TokenConfig[] memory tokenConfig) { + tokenConfig = new TokenConfig[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + tokenConfig[i].token = tokens[i]; + tokenConfig[i].rateProvider = rateProviders[i]; + tokenConfig[i].tokenType = rateProviders[i] == IRateProvider(address(0)) + ? TokenType.STANDARD + : TokenType.WITH_RATE; + } + + tokenConfig = _inputHelpersMock.sortTokenConfig(tokenConfig); + } + + function buildTokenConfig( + IERC20[] memory tokens, + IRateProvider[] memory rateProviders, + bool[] memory yieldFeeFlags + ) public view returns (TokenConfig[] memory tokenConfig) { + tokenConfig = new TokenConfig[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + tokenConfig[i].token = tokens[i]; + tokenConfig[i].rateProvider = rateProviders[i]; + tokenConfig[i].tokenType = rateProviders[i] == IRateProvider(address(0)) + ? TokenType.STANDARD + : TokenType.WITH_RATE; + tokenConfig[i].paysYieldFees = yieldFeeFlags[i]; + } + + tokenConfig = _inputHelpersMock.sortTokenConfig(tokenConfig); + } + + function buildTokenConfig( + IERC20[] memory tokens, + TokenType[] memory tokenTypes, + IRateProvider[] memory rateProviders, + bool[] memory yieldFeeFlags + ) public view returns (TokenConfig[] memory tokenConfig) { + tokenConfig = new TokenConfig[](tokens.length); + for (uint256 i = 0; i < tokens.length; ++i) { + tokenConfig[i].token = tokens[i]; + tokenConfig[i].tokenType = tokenTypes[i]; + tokenConfig[i].rateProvider = rateProviders[i]; + tokenConfig[i].paysYieldFees = yieldFeeFlags[i]; + } + + tokenConfig = _inputHelpersMock.sortTokenConfig(tokenConfig); + } + + function recoveryModeExit(address pool) external view onlyInRecoveryMode(pool) { + // solhint-disable-previous-line no-empty-blocks + } + + function loadPoolDataUpdatingBalancesAndYieldFees( + address pool, + Rounding roundingDirection + ) external returns (PoolData memory) { + return _loadPoolDataUpdatingBalancesAndYieldFees(pool, roundingDirection); + } + + function loadPoolDataUpdatingBalancesAndYieldFeesReentrancy( + address pool, + Rounding roundingDirection + ) external nonReentrant returns (PoolData memory) { + return _loadPoolDataUpdatingBalancesAndYieldFees(pool, roundingDirection); + } + + function updateLiveTokenBalanceInPoolData( + PoolData memory poolData, + uint256 newRawBalance, + Rounding roundingDirection, + uint256 tokenIndex + ) external pure returns (PoolData memory) { + _updateRawAndLiveTokenBalancesInPoolData(poolData, newRawBalance, roundingDirection, tokenIndex); + return poolData; + } + + function computeYieldFeesDue( + PoolData memory poolData, + uint256 lastLiveBalance, + uint256 tokenIndex, + uint256 aggregateYieldFeePercentage + ) external pure returns (uint256) { + return PoolDataLib._computeYieldFeesDue(poolData, lastLiveBalance, tokenIndex, aggregateYieldFeePercentage); + } + + function manualWritePoolBalancesToStorage(address pool, PoolData memory poolData) external { + _writePoolBalancesToStorage(pool, poolData); + } + + function getRawBalances(address pool) external view returns (uint256[] memory balancesRaw) { + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + + uint256 numTokens = _poolTokens[pool].length; + balancesRaw = new uint256[](numTokens); + + for (uint256 i = 0; i < numTokens; ++i) { + balancesRaw[i] = poolTokenBalances[i].getBalanceRaw(); + } + } + + function getLastLiveBalances(address pool) external view returns (uint256[] memory lastBalancesLiveScaled18) { + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + + uint256 numTokens = _poolTokens[pool].length; + lastBalancesLiveScaled18 = new uint256[](numTokens); + + for (uint256 i = 0; i < numTokens; ++i) { + lastBalancesLiveScaled18[i] = poolTokenBalances[i].getBalanceDerived(); + } + } + + function guardedCheckEntered() external nonReentrant { + require(reentrancyGuardEntered()); + } + + function unguardedCheckNotEntered() external view { + require(!reentrancyGuardEntered()); + } + + function accountDelta(IERC20 token, int256 delta) external { + _accountDelta(token, delta); + } + + function supplyCredit(IERC20 token, uint256 credit) external { + _supplyCredit(token, credit); + } + + function takeDebt(IERC20 token, uint256 debt) external { + _takeDebt(token, debt); + } + + function manualSetAccountDelta(IERC20 token, int256 delta) external { + _tokenDeltas().tSet(token, delta); + } + + function manualSetNonZeroDeltaCount(uint256 deltaCount) external { + _nonZeroDeltaCount().tstore(deltaCount); + } + + function manualSetReservesOf(IERC20 token, uint256 reserves) external { + _reservesOf[token] = reserves; + } + + function manualInternalSwap( + VaultSwapParams memory vaultSwapParams, + SwapState memory state, + PoolData memory poolData + ) + external + returns ( + uint256 amountCalculatedRaw, + uint256 amountCalculatedScaled18, + uint256 amountIn, + uint256 amountOut, + VaultSwapParams memory, + SwapState memory, + PoolData memory + ) + { + PoolSwapParams memory poolSwapParams = _buildPoolSwapParams(vaultSwapParams, state, poolData); + + (amountCalculatedRaw, amountCalculatedScaled18, amountIn, amountOut) = _swap( + vaultSwapParams, + state, + poolData, + poolSwapParams + ); + + return (amountCalculatedRaw, amountCalculatedScaled18, amountIn, amountOut, vaultSwapParams, state, poolData); + } + + function manualReentrancySwap( + VaultSwapParams memory vaultSwapParams, + SwapState memory state, + PoolData memory poolData + ) external nonReentrant { + PoolSwapParams memory poolSwapParams = _buildPoolSwapParams(vaultSwapParams, state, poolData); + _swap(vaultSwapParams, state, poolData, poolSwapParams); + } + + function manualGetAggregateSwapFeeAmount(address pool, IERC20 token) external view returns (uint256) { + return _aggregateFeeAmounts[pool][token].getBalanceRaw(); + } + + function manualGetAggregateYieldFeeAmount(address pool, IERC20 token) external view returns (uint256) { + return _aggregateFeeAmounts[pool][token].getBalanceDerived(); + } + + function manualSetAggregateSwapFeeAmount(address pool, IERC20 token, uint256 value) external { + _aggregateFeeAmounts[pool][token] = _aggregateFeeAmounts[pool][token].setBalanceRaw(value); + } + + function manualSetAggregateYieldFeeAmount(address pool, IERC20 token, uint256 value) external { + _aggregateFeeAmounts[pool][token] = _aggregateFeeAmounts[pool][token].setBalanceDerived(value); + } + + function manualSetAggregateSwapFeePercentage(address pool, uint256 value) external { + _poolConfigBits[pool] = _poolConfigBits[pool].setAggregateSwapFeePercentage(value); + } + + function manualSetAggregateYieldFeePercentage(address pool, uint256 value) external { + _poolConfigBits[pool] = _poolConfigBits[pool].setAggregateYieldFeePercentage(value); + } + + function manualBuildPoolSwapParams( + VaultSwapParams memory vaultSwapParams, + SwapState memory state, + PoolData memory poolData + ) external view returns (PoolSwapParams memory) { + return _buildPoolSwapParams(vaultSwapParams, state, poolData); + } + + function manualComputeAndChargeAggregateSwapFees( + PoolData memory poolData, + uint256 totalSwapFeeAmountScaled18, + address pool, + IERC20 token, + uint256 index + ) external returns (uint256 totalSwapFeeAmountRaw, uint256 aggregateSwapFeeAmountRaw) { + return _computeAndChargeAggregateSwapFees(poolData, totalSwapFeeAmountScaled18, pool, token, index); + } + + function manualUpdatePoolDataLiveBalancesAndRates( + address pool, + PoolData memory poolData, + Rounding roundingDirection + ) external view returns (PoolData memory) { + poolData.reloadBalancesAndRates(_poolTokenBalances[pool], roundingDirection); + + return poolData; + } + + function manualAddLiquidity( + PoolData memory poolData, + AddLiquidityParams memory params, + uint256[] memory maxAmountsInScaled18 + ) + external + returns ( + PoolData memory updatedPoolData, + uint256[] memory amountsInRaw, + uint256[] memory amountsInScaled18, + uint256 bptAmountOut, + bytes memory returnData + ) + { + bytes32 paramsHashBefore = keccak256(abi.encode(params)); + + (amountsInRaw, amountsInScaled18, bptAmountOut, returnData) = _addLiquidity( + poolData, + params, + maxAmountsInScaled18 + ); + + require(paramsHashBefore == keccak256(abi.encode(params)), "Input parameters have changed"); + + updatedPoolData = poolData; + } + + function manualReentrancyAddLiquidity( + PoolData memory poolData, + AddLiquidityParams memory params, + uint256[] memory maxAmountsInScaled18 + ) external nonReentrant { + _addLiquidity(poolData, params, maxAmountsInScaled18); + } + + function manualRemoveLiquidity( + PoolData memory poolData, + RemoveLiquidityParams memory params, + uint256[] memory minAmountsOutScaled18 + ) + external + returns ( + PoolData memory updatedPoolData, + uint256 bptAmountIn, + uint256[] memory amountsOutRaw, + uint256[] memory amountsOutScaled18, + bytes memory returnData + ) + { + bytes32 paramsHashBefore = keccak256(abi.encode(params)); + + (bptAmountIn, amountsOutRaw, amountsOutScaled18, returnData) = _removeLiquidity( + poolData, + params, + minAmountsOutScaled18 + ); + + require(paramsHashBefore == keccak256(abi.encode(params)), "Input parameters have changed"); + + updatedPoolData = poolData; + } + + function manualReentrancyRemoveLiquidity( + PoolData memory poolData, + RemoveLiquidityParams memory params, + uint256[] memory minAmountsOutScaled18 + ) external nonReentrant { + _removeLiquidity(poolData, params, minAmountsOutScaled18); + } + + function internalGetBufferUnderlyingImbalance(IERC4626 wrappedToken) external view returns (int256) { + bytes32 bufferBalance = _bufferTokenBalances[wrappedToken]; + return bufferBalance.getBufferUnderlyingImbalance(wrappedToken); + } + + function internalGetBufferWrappedImbalance(IERC4626 wrappedToken) external view returns (int256) { + bytes32 bufferBalance = _bufferTokenBalances[wrappedToken]; + return bufferBalance.getBufferWrappedImbalance(wrappedToken); + } + + function getBufferTokenBalancesBytes(IERC4626 wrappedToken) external view returns (bytes32) { + return _bufferTokenBalances[wrappedToken]; + } + + function manualSettleWrap( + IERC20 underlyingToken, + IERC20 wrappedToken, + uint256 underlyingHint, + uint256 wrappedHint + ) external { + _settleWrap(underlyingToken, wrappedToken, underlyingHint, wrappedHint); + } + + function manualSettleUnwrap( + IERC20 underlyingToken, + IERC20 wrappedToken, + uint256 underlyingHint, + uint256 wrappedHint + ) external { + _settleUnwrap(underlyingToken, wrappedToken, underlyingHint, wrappedHint); + } + + function manualTransfer(IERC20 token, address to, uint256 amount) external { + token.transfer(to, amount); + } + + function forceUnlock() public { + _isUnlocked().tstore(true); + } + + function forceLock() public { + _isUnlocked().tstore(false); + } + + function manualGetPoolConfigBits(address pool) external view returns (PoolConfigBits) { + return _poolConfigBits[pool]; + } + + function manualGetIsUnlocked() external view returns (StorageSlotExtension.BooleanSlotType slot) { + return _isUnlocked(); + } + + function manualGetNonzeroDeltaCount() external view returns (StorageSlotExtension.Uint256SlotType slot) { + return _nonZeroDeltaCount(); + } + + function manualGetTokenDeltas() external view returns (TokenDeltaMappingSlotType slot) { + return _tokenDeltas(); + } + + function manualSetBufferAsset(IERC4626 wrappedToken, address underlyingToken) external { + _bufferAssets[wrappedToken] = underlyingToken; + } + + function manualSetBufferOwnerShares(IERC4626 wrappedToken, address owner, uint256 shares) external { + _bufferLpShares[wrappedToken][owner] = shares; + } + + function manualSetBufferTotalShares(IERC4626 wrappedToken, uint256 shares) external { + _bufferTotalShares[wrappedToken] = shares; + } + + function manualSetBufferBalances(IERC4626 wrappedToken, uint256 underlyingAmount, uint256 wrappedAmount) external { + _bufferTokenBalances[wrappedToken] = PackedTokenBalance.toPackedBalance(underlyingAmount, wrappedAmount); + } + + function manualErc4626BufferWrapOrUnwrapReentrancy( + BufferWrapOrUnwrapParams memory params + ) external nonReentrant returns (uint256 amountCalculatedRaw, uint256 amountInRaw, uint256 amountOutRaw) { + return IVault(address(this)).erc4626BufferWrapOrUnwrap(params); + } + + function manualSettleReentrancy(IERC20 token) public nonReentrant returns (uint256 paid) { + return IVault(address(this)).settle(token, 0); + } + + function manualSendToReentrancy(IERC20 token, address to, uint256 amount) public nonReentrant { + IVault(address(this)).sendTo(token, to, amount); + } + + function manualFindTokenIndex(IERC20[] memory tokens, IERC20 token) public pure returns (uint256 index) { + return _findTokenIndex(tokens, token); + } + + function manualSetAddLiquidityCalledFlag(address pool, bool flag) public { + _addLiquidityCalled().tSet(pool, flag); + } + + function _getDefaultLiquidityManagement() private pure returns (LiquidityManagement memory) { + LiquidityManagement memory liquidityManagement; + liquidityManagement.enableAddLiquidityCustom = true; + liquidityManagement.enableRemoveLiquidityCustom = true; + return liquidityManagement; + } + + function manualSetPoolCreator(address pool, address newPoolCreator) public { + _poolRoleAccounts[pool].poolCreator = newPoolCreator; + } + + function ensureValidTradeAmount(uint256 tradeAmount) external view { + _ensureValidTradeAmount(tradeAmount); + } + + function ensureValidSwapAmount(uint256 tradeAmount) external view { + _ensureValidSwapAmount(tradeAmount); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Router.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Router.sol new file mode 100644 index 00000000..c4b9e585 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Router.sol @@ -0,0 +1,1171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IPermit2 } from "./interfaces/IPermit2.sol"; + +import { IWETH } from "./interfaces/IWETH.sol"; +import { IRouter } from "./interfaces/IRouter.sol"; +import { IVault } from "./interfaces/IVault.sol"; +import "./interfaces/VaultTypes.sol"; + +import { + ReentrancyGuardTransient +} from "./interfaces/ReentrancyGuardTransient.sol"; + +import { RouterCommon } from "./RouterCommon.sol"; + +/** + * @notice Entrypoint for swaps, liquidity operations, and corresponding queries. + * @dev The external API functions unlock the Vault, which calls back into the corresponding hook functions. + * These interact with the Vault, transfer tokens, settle accounting, and handle wrapping and unwrapping ETH. + */ +contract Router is IRouter, RouterCommon, ReentrancyGuardTransient { + using Address for address payable; + using SafeCast for *; + + constructor(IVault vault, IWETH weth, IPermit2 permit2) RouterCommon(vault, weth, permit2) { + // solhint-disable-previous-line no-empty-blocks + } + + /******************************************************************************* + Pool Initialization + *******************************************************************************/ + + /// @inheritdoc IRouter + function initialize( + address pool, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256 bptAmountOut) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + Router.initializeHook, + InitializeHookParams({ + sender: msg.sender, + pool: pool, + tokens: tokens, + exactAmountsIn: exactAmountsIn, + minBptAmountOut: minBptAmountOut, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook for initialization. + * @dev Can only be called by the Vault. + * @param params Initialization parameters (see IRouter for struct definition) + * @return bptAmountOut BPT amount minted in exchange for the input tokens + */ + function initializeHook( + InitializeHookParams calldata params + ) external nonReentrant onlyVault returns (uint256 bptAmountOut) { + bptAmountOut = _vault.initialize( + params.pool, + params.sender, + params.tokens, + params.exactAmountsIn, + params.minBptAmountOut, + params.userData + ); + + for (uint256 i = 0; i < params.tokens.length; ++i) { + IERC20 token = params.tokens[i]; + uint256 amountIn = params.exactAmountsIn[i]; + + if (amountIn == 0) { + continue; + } + + // There can be only one WETH token in the pool. + if (params.wethIsEth && address(token) == address(_weth)) { + if (address(this).balance < amountIn) { + revert InsufficientEth(); + } + + _weth.deposit{ value: amountIn }(); + // Transfer WETH from the Router to the Vault. + _weth.transfer(address(_vault), amountIn); + _vault.settle(_weth, amountIn); + } else { + // Transfer tokens from the user to the Vault. + // Any value over MAX_UINT128 would revert above in `initialize`, so this SafeCast shouldn't be + // necessary. Done out of an abundance of caution. + _permit2.transferFrom(params.sender, address(_vault), amountIn.toUint160(), address(token)); + _vault.settle(token, amountIn); + } + } + + // Return ETH dust. + _returnEth(params.sender); + } + + /*************************************************************************** + Add Liquidity + ***************************************************************************/ + + /// @inheritdoc IRouter + function addLiquidityProportional( + address pool, + uint256[] memory maxAmountsIn, + uint256 exactBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable saveSender(msg.sender) returns (uint256[] memory amountsIn) { + (amountsIn, , ) = abi.decode( + _vault.unlock( + abi.encodeCall( + Router.addLiquidityHook, + AddLiquidityHookParams({ + sender: msg.sender, + pool: pool, + maxAmountsIn: maxAmountsIn, + minBptAmountOut: exactBptAmountOut, + kind: AddLiquidityKind.PROPORTIONAL, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + } + + /// @inheritdoc IRouter + function addLiquidityUnbalanced( + address pool, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable saveSender(msg.sender) returns (uint256 bptAmountOut) { + (, bptAmountOut, ) = abi.decode( + _vault.unlock( + abi.encodeCall( + Router.addLiquidityHook, + AddLiquidityHookParams({ + sender: msg.sender, + pool: pool, + maxAmountsIn: exactAmountsIn, + minBptAmountOut: minBptAmountOut, + kind: AddLiquidityKind.UNBALANCED, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + } + + /// @inheritdoc IRouter + function addLiquiditySingleTokenExactOut( + address pool, + IERC20 tokenIn, + uint256 maxAmountIn, + uint256 exactBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable saveSender(msg.sender) returns (uint256 amountIn) { + (uint256[] memory maxAmountsIn, uint256 tokenIndex) = _getSingleInputArrayAndTokenIndex( + pool, + tokenIn, + maxAmountIn + ); + + (uint256[] memory amountsIn, , ) = abi.decode( + _vault.unlock( + abi.encodeCall( + Router.addLiquidityHook, + AddLiquidityHookParams({ + sender: msg.sender, + pool: pool, + maxAmountsIn: maxAmountsIn, + minBptAmountOut: exactBptAmountOut, + kind: AddLiquidityKind.SINGLE_TOKEN_EXACT_OUT, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + + return amountsIn[tokenIndex]; + } + + /// @inheritdoc IRouter + function donate( + address pool, + uint256[] memory amountsIn, + bool wethIsEth, + bytes memory userData + ) external payable saveSender(msg.sender) { + _vault.unlock( + abi.encodeCall( + Router.addLiquidityHook, + AddLiquidityHookParams({ + sender: msg.sender, + pool: pool, + maxAmountsIn: amountsIn, + minBptAmountOut: 0, + kind: AddLiquidityKind.DONATION, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ); + } + + /// @inheritdoc IRouter + function addLiquidityCustom( + address pool, + uint256[] memory maxAmountsIn, + uint256 minBptAmountOut, + bool wethIsEth, + bytes memory userData + ) + external + payable + saveSender(msg.sender) + returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData) + { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + Router.addLiquidityHook, + AddLiquidityHookParams({ + sender: msg.sender, + pool: pool, + maxAmountsIn: maxAmountsIn, + minBptAmountOut: minBptAmountOut, + kind: AddLiquidityKind.CUSTOM, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + } + + /** + * @notice Hook for adding liquidity. + * @dev Can only be called by the Vault. + * @param params Add liquidity parameters (see IRouter for struct definition) + * @return amountsIn Actual amounts in required for the join + * @return bptAmountOut BPT amount minted in exchange for the input tokens + * @return returnData Arbitrary data with encoded response from the pool + */ + function addLiquidityHook( + AddLiquidityHookParams calldata params + ) + external + nonReentrant + onlyVault + returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData) + { + (amountsIn, bptAmountOut, returnData) = _vault.addLiquidity( + AddLiquidityParams({ + pool: params.pool, + to: params.sender, + maxAmountsIn: params.maxAmountsIn, + minBptAmountOut: params.minBptAmountOut, + kind: params.kind, + userData: params.userData + }) + ); + + // maxAmountsIn length is checked against tokens length at the Vault. + IERC20[] memory tokens = _vault.getPoolTokens(params.pool); + + for (uint256 i = 0; i < tokens.length; ++i) { + IERC20 token = tokens[i]; + uint256 amountIn = amountsIn[i]; + + if (amountIn == 0) { + continue; + } + + // There can be only one WETH token in the pool. + if (params.wethIsEth && address(token) == address(_weth)) { + if (address(this).balance < amountIn) { + revert InsufficientEth(); + } + + _weth.deposit{ value: amountIn }(); + _weth.transfer(address(_vault), amountIn); + _vault.settle(_weth, amountIn); + } else { + // Any value over MAX_UINT128 would revert above in `addLiquidity`, so this SafeCast shouldn't be + // necessary. Done out of an abundance of caution. + _permit2.transferFrom(params.sender, address(_vault), amountIn.toUint160(), address(token)); + _vault.settle(token, amountIn); + } + } + + // Send remaining ETH to the user. + _returnEth(params.sender); + } + + /*************************************************************************** + Remove Liquidity + ***************************************************************************/ + + /// @inheritdoc IRouter + function removeLiquidityProportional( + address pool, + uint256 exactBptAmountIn, + uint256[] memory minAmountsOut, + bool wethIsEth, + bytes memory userData + ) external payable saveSender(msg.sender) returns (uint256[] memory amountsOut) { + (, amountsOut, ) = abi.decode( + _vault.unlock( + abi.encodeCall( + Router.removeLiquidityHook, + RemoveLiquidityHookParams({ + sender: msg.sender, + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: exactBptAmountIn, + kind: RemoveLiquidityKind.PROPORTIONAL, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + } + + /// @inheritdoc IRouter + function removeLiquiditySingleTokenExactIn( + address pool, + uint256 exactBptAmountIn, + IERC20 tokenOut, + uint256 minAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable saveSender(msg.sender) returns (uint256 amountOut) { + (uint256[] memory minAmountsOut, uint256 tokenIndex) = _getSingleInputArrayAndTokenIndex( + pool, + tokenOut, + minAmountOut + ); + + (, uint256[] memory amountsOut, ) = abi.decode( + _vault.unlock( + abi.encodeCall( + Router.removeLiquidityHook, + RemoveLiquidityHookParams({ + sender: msg.sender, + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: exactBptAmountIn, + kind: RemoveLiquidityKind.SINGLE_TOKEN_EXACT_IN, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + + return amountsOut[tokenIndex]; + } + + /// @inheritdoc IRouter + function removeLiquiditySingleTokenExactOut( + address pool, + uint256 maxBptAmountIn, + IERC20 tokenOut, + uint256 exactAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable saveSender(msg.sender) returns (uint256 bptAmountIn) { + (uint256[] memory minAmountsOut, ) = _getSingleInputArrayAndTokenIndex(pool, tokenOut, exactAmountOut); + + (bptAmountIn, , ) = abi.decode( + _vault.unlock( + abi.encodeCall( + Router.removeLiquidityHook, + RemoveLiquidityHookParams({ + sender: msg.sender, + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: maxBptAmountIn, + kind: RemoveLiquidityKind.SINGLE_TOKEN_EXACT_OUT, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + + return bptAmountIn; + } + + /// @inheritdoc IRouter + function removeLiquidityCustom( + address pool, + uint256 maxBptAmountIn, + uint256[] memory minAmountsOut, + bool wethIsEth, + bytes memory userData + ) + external + payable + saveSender(msg.sender) + returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData) + { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + Router.removeLiquidityHook, + RemoveLiquidityHookParams({ + sender: msg.sender, + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: maxBptAmountIn, + kind: RemoveLiquidityKind.CUSTOM, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + } + + /// @inheritdoc IRouter + function removeLiquidityRecovery( + address pool, + uint256 exactBptAmountIn + ) external payable returns (uint256[] memory amountsOut) { + amountsOut = abi.decode( + _vault.unlock(abi.encodeCall(Router.removeLiquidityRecoveryHook, (pool, msg.sender, exactBptAmountIn))), + (uint256[]) + ); + } + + /** + * @notice Hook for removing liquidity. + * @dev Can only be called by the Vault. + * @param params Remove liquidity parameters (see IRouter for struct definition) + * @return bptAmountIn BPT amount burned for the output tokens + * @return amountsOut Actual token amounts transferred in exchange for the BPT + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function removeLiquidityHook( + RemoveLiquidityHookParams calldata params + ) + external + nonReentrant + onlyVault + returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData) + { + (bptAmountIn, amountsOut, returnData) = _vault.removeLiquidity( + RemoveLiquidityParams({ + pool: params.pool, + from: params.sender, + maxBptAmountIn: params.maxBptAmountIn, + minAmountsOut: params.minAmountsOut, + kind: params.kind, + userData: params.userData + }) + ); + + // minAmountsOut length is checked against tokens length at the Vault. + IERC20[] memory tokens = _vault.getPoolTokens(params.pool); + + for (uint256 i = 0; i < tokens.length; ++i) { + uint256 amountOut = amountsOut[i]; + if (amountOut == 0) { + continue; + } + + IERC20 token = tokens[i]; + + // There can be only one WETH token in the pool. + if (params.wethIsEth && address(token) == address(_weth)) { + // Send WETH here and unwrap to native ETH. + _vault.sendTo(_weth, address(this), amountOut); + _weth.withdraw(amountOut); + // Send ETH to sender. + payable(params.sender).sendValue(amountOut); + } else { + // Transfer the token to the sender (amountOut). + _vault.sendTo(token, params.sender, amountOut); + } + } + + _returnEth(params.sender); + } + + /** + * @notice Hook for removing liquidity in Recovery Mode. + * @dev Can only be called by the Vault, when the pool is in Recovery Mode. + * @param pool Address of the liquidity pool + * @param sender Account originating the remove liquidity operation + * @param exactBptAmountIn BPT amount burned for the output tokens + * @return amountsOut Actual token amounts transferred in exchange for the BPT + */ + function removeLiquidityRecoveryHook( + address pool, + address sender, + uint256 exactBptAmountIn + ) external nonReentrant onlyVault returns (uint256[] memory amountsOut) { + amountsOut = _vault.removeLiquidityRecovery(pool, sender, exactBptAmountIn); + + IERC20[] memory tokens = _vault.getPoolTokens(pool); + + for (uint256 i = 0; i < tokens.length; ++i) { + uint256 amountOut = amountsOut[i]; + if (amountOut > 0) { + // Transfer the token to the sender (amountOut). + _vault.sendTo(tokens[i], sender, amountOut); + } + } + + _returnEth(sender); + } + + /*************************************************************************** + Swaps + ***************************************************************************/ + + /// @inheritdoc IRouter + function swapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + uint256 minAmountOut, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable saveSender(msg.sender) returns (uint256) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + Router.swapSingleTokenHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_IN, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountIn, + limit: minAmountOut, + deadline: deadline, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /// @inheritdoc IRouter + function swapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + uint256 maxAmountIn, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable saveSender(msg.sender) returns (uint256) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + Router.swapSingleTokenHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_OUT, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountOut, + limit: maxAmountIn, + deadline: deadline, + wethIsEth: wethIsEth, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook for swaps. + * @dev Can only be called by the Vault. Also handles native ETH. + * @param params Swap parameters (see IRouter for struct definition) + * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) + */ + function swapSingleTokenHook( + SwapSingleTokenHookParams calldata params + ) external nonReentrant onlyVault returns (uint256) { + (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) = _swapHook(params); + + IERC20 tokenIn = params.tokenIn; + + _takeTokenIn(params.sender, tokenIn, amountIn, params.wethIsEth); + _sendTokenOut(params.sender, params.tokenOut, amountOut, params.wethIsEth); + + if (tokenIn == _weth) { + // Return the rest of ETH to sender + _returnEth(params.sender); + } + + return amountCalculated; + } + + function _swapHook( + SwapSingleTokenHookParams calldata params + ) internal returns (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) { + // The deadline is timestamp-based: it should not be relied upon for sub-minute accuracy. + // solhint-disable-next-line not-rely-on-time + if (block.timestamp > params.deadline) { + revert SwapDeadline(); + } + + (amountCalculated, amountIn, amountOut) = _vault.swap( + VaultSwapParams({ + kind: params.kind, + pool: params.pool, + tokenIn: params.tokenIn, + tokenOut: params.tokenOut, + amountGivenRaw: params.amountGiven, + limitRaw: params.limit, + userData: params.userData + }) + ); + } + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /// @inheritdoc IRouter + function initializeBuffer( + IERC4626 wrappedToken, + uint256 amountUnderlyingRaw, + uint256 amountWrappedRaw + ) external returns (uint256 issuedShares) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + Router.initializeBufferHook, + ( + wrappedToken, + amountUnderlyingRaw, + amountWrappedRaw, + msg.sender // sharesOwner + ) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook for initializing a vault buffer. + * @dev Can only be called by the Vault. Buffers must be initialized before use. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param amountUnderlyingRaw Amount of underlying tokens that will be deposited into the buffer + * @param amountWrappedRaw Amount of wrapped tokens that will be deposited into the buffer + * @param sharesOwner Address that will own the deposited liquidity. Only this address will be able to + * remove liquidity from the buffer + * @return issuedShares the amount of tokens sharesOwner has in the buffer, expressed in underlying token amounts. + * (This is the BPT of an internal ERC4626 buffer) + */ + function initializeBufferHook( + IERC4626 wrappedToken, + uint256 amountUnderlyingRaw, + uint256 amountWrappedRaw, + address sharesOwner + ) external nonReentrant onlyVault returns (uint256 issuedShares) { + issuedShares = _vault.initializeBuffer(wrappedToken, amountUnderlyingRaw, amountWrappedRaw, sharesOwner); + _takeTokenIn(sharesOwner, IERC20(wrappedToken.asset()), amountUnderlyingRaw, false); + _takeTokenIn(sharesOwner, IERC20(address(wrappedToken)), amountWrappedRaw, false); + } + + /// @inheritdoc IRouter + function addLiquidityToBuffer( + IERC4626 wrappedToken, + uint256 exactSharesToIssue + ) external returns (uint256 amountUnderlyingRaw, uint256 amountWrappedRaw) { + return + abi.decode( + _vault.unlock( + abi.encodeCall( + Router.addLiquidityToBufferHook, + ( + wrappedToken, + exactSharesToIssue, + msg.sender // sharesOwner + ) + ) + ), + (uint256, uint256) + ); + } + + /** + * @notice Hook for adding liquidity to vault buffers. The Vault will enforce that the buffer is initialized. + * @dev Can only be called by the Vault. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param exactSharesToIssue The value in underlying tokens that `sharesOwner` wants to add to the buffer, + * in underlying token decimals + * @param sharesOwner Address that will own the deposited liquidity. Only this address will be able to + * remove liquidity from the buffer + * @return amountUnderlyingRaw Amount of underlying tokens deposited into the buffer + * @return amountWrappedRaw Amount of wrapped tokens deposited into the buffer + */ + function addLiquidityToBufferHook( + IERC4626 wrappedToken, + uint256 exactSharesToIssue, + address sharesOwner + ) external nonReentrant onlyVault returns (uint256 amountUnderlyingRaw, uint256 amountWrappedRaw) { + (amountUnderlyingRaw, amountWrappedRaw) = _vault.addLiquidityToBuffer( + wrappedToken, + exactSharesToIssue, + sharesOwner + ); + _takeTokenIn(sharesOwner, IERC20(wrappedToken.asset()), amountUnderlyingRaw, false); + _takeTokenIn(sharesOwner, IERC20(address(wrappedToken)), amountWrappedRaw, false); + } + + /******************************************************************************* + Queries + *******************************************************************************/ + + /// @inheritdoc IRouter + function queryAddLiquidityProportional( + address pool, + uint256 exactBptAmountOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256[] memory amountsIn) { + (amountsIn, , ) = abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryAddLiquidityHook, + AddLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to add liquidity to any recipient. + sender: address(this), + pool: pool, + maxAmountsIn: _maxTokenLimits(pool), + minBptAmountOut: exactBptAmountOut, + kind: AddLiquidityKind.PROPORTIONAL, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + } + + /// @inheritdoc IRouter + function queryAddLiquidityUnbalanced( + address pool, + uint256[] memory exactAmountsIn, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 bptAmountOut) { + (, bptAmountOut, ) = abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryAddLiquidityHook, + AddLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to add liquidity to any recipient. + sender: address(this), + pool: pool, + maxAmountsIn: exactAmountsIn, + minBptAmountOut: 0, + kind: AddLiquidityKind.UNBALANCED, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + } + + /// @inheritdoc IRouter + function queryAddLiquiditySingleTokenExactOut( + address pool, + IERC20 tokenIn, + uint256 exactBptAmountOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 amountIn) { + (uint256[] memory maxAmountsIn, uint256 tokenIndex) = _getSingleInputArrayAndTokenIndex( + pool, + tokenIn, + _MAX_AMOUNT + ); + + (uint256[] memory amountsIn, , ) = abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryAddLiquidityHook, + AddLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to add liquidity to any recipient. + sender: address(this), + pool: pool, + maxAmountsIn: maxAmountsIn, + minBptAmountOut: exactBptAmountOut, + kind: AddLiquidityKind.SINGLE_TOKEN_EXACT_OUT, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + + return amountsIn[tokenIndex]; + } + + /// @inheritdoc IRouter + function queryAddLiquidityCustom( + address pool, + uint256[] memory maxAmountsIn, + uint256 minBptAmountOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData) { + return + abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryAddLiquidityHook, + AddLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to add liquidity to any recipient. + sender: address(this), + pool: pool, + maxAmountsIn: maxAmountsIn, + minBptAmountOut: minBptAmountOut, + kind: AddLiquidityKind.CUSTOM, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256[], uint256, bytes) + ); + } + + /** + * @notice Hook for add liquidity queries. + * @dev Can only be called by the Vault. + * @param params Add liquidity parameters (see IRouter for struct definition) + * @return amountsIn Actual token amounts in required as inputs + * @return bptAmountOut Expected pool tokens to be minted + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function queryAddLiquidityHook( + AddLiquidityHookParams calldata params + ) external onlyVault returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData) { + (amountsIn, bptAmountOut, returnData) = _vault.addLiquidity( + AddLiquidityParams({ + pool: params.pool, + to: params.sender, + maxAmountsIn: params.maxAmountsIn, + minBptAmountOut: params.minBptAmountOut, + kind: params.kind, + userData: params.userData + }) + ); + } + + /// @inheritdoc IRouter + function queryRemoveLiquidityProportional( + address pool, + uint256 exactBptAmountIn, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256[] memory amountsOut) { + uint256[] memory minAmountsOut = new uint256[](_vault.getPoolTokens(pool).length); + (, amountsOut, ) = abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryRemoveLiquidityHook, + RemoveLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to remove liquidity from any sender. + sender: address(this), + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: exactBptAmountIn, + kind: RemoveLiquidityKind.PROPORTIONAL, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + } + + /// @inheritdoc IRouter + function queryRemoveLiquiditySingleTokenExactIn( + address pool, + uint256 exactBptAmountIn, + IERC20 tokenOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 amountOut) { + // We cannot use 0 as min amount out, as this value is used to figure out the token index. + (uint256[] memory minAmountsOut, uint256 tokenIndex) = _getSingleInputArrayAndTokenIndex(pool, tokenOut, 1); + + (, uint256[] memory amountsOut, ) = abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryRemoveLiquidityHook, + RemoveLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to remove liquidity from any sender. + sender: address(this), + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: exactBptAmountIn, + kind: RemoveLiquidityKind.SINGLE_TOKEN_EXACT_IN, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + + return amountsOut[tokenIndex]; + } + + /// @inheritdoc IRouter + function queryRemoveLiquiditySingleTokenExactOut( + address pool, + IERC20 tokenOut, + uint256 exactAmountOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 bptAmountIn) { + (uint256[] memory minAmountsOut, ) = _getSingleInputArrayAndTokenIndex(pool, tokenOut, exactAmountOut); + + (bptAmountIn, , ) = abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryRemoveLiquidityHook, + RemoveLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to remove liquidity from any sender. + sender: address(this), + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: _MAX_AMOUNT, + kind: RemoveLiquidityKind.SINGLE_TOKEN_EXACT_OUT, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + + return bptAmountIn; + } + + /// @inheritdoc IRouter + function queryRemoveLiquidityCustom( + address pool, + uint256 maxBptAmountIn, + uint256[] memory minAmountsOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData) { + return + abi.decode( + _vault.quote( + abi.encodeCall( + Router.queryRemoveLiquidityHook, + RemoveLiquidityHookParams({ + // We use the Router as a sender to simplify basic query functions, + // but it is possible to remove liquidity from any sender. + sender: address(this), + pool: pool, + minAmountsOut: minAmountsOut, + maxBptAmountIn: maxBptAmountIn, + kind: RemoveLiquidityKind.CUSTOM, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256, uint256[], bytes) + ); + } + + /// @inheritdoc IRouter + function queryRemoveLiquidityRecovery( + address pool, + uint256 exactBptAmountIn + ) external returns (uint256[] memory amountsOut) { + return + abi.decode( + _vault.quote( + abi.encodeCall(Router.queryRemoveLiquidityRecoveryHook, (pool, address(this), exactBptAmountIn)) + ), + (uint256[]) + ); + } + + /** + * @notice Hook for remove liquidity queries. + * @dev Can only be called by the Vault. + * @param params Remove liquidity parameters (see IRouter for struct definition) + * @return bptAmountIn Pool token amount to be burned for the output tokens + * @return amountsOut Expected token amounts to be transferred to the sender + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function queryRemoveLiquidityHook( + RemoveLiquidityHookParams calldata params + ) external onlyVault returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData) { + return + _vault.removeLiquidity( + RemoveLiquidityParams({ + pool: params.pool, + from: params.sender, + maxBptAmountIn: params.maxBptAmountIn, + minAmountsOut: params.minAmountsOut, + kind: params.kind, + userData: params.userData + }) + ); + } + + /** + * @notice Hook for remove liquidity queries. + * @dev Can only be called by the Vault. + * @param pool The liquidity pool + * @param sender Account originating the remove liquidity operation + * @param exactBptAmountIn Pool token amount to be burned for the output tokens + * @return amountsOut Expected token amounts to be transferred to the sender + */ + function queryRemoveLiquidityRecoveryHook( + address pool, + address sender, + uint256 exactBptAmountIn + ) external onlyVault returns (uint256[] memory amountsOut) { + return _vault.removeLiquidityRecovery(pool, sender, exactBptAmountIn); + } + + /// @inheritdoc IRouter + function querySwapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 amountCalculated) { + return + abi.decode( + _vault.quote( + abi.encodeCall( + Router.querySwapHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_IN, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountIn, + limit: 0, + deadline: _MAX_AMOUNT, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /// @inheritdoc IRouter + function querySwapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + address sender, + bytes memory userData + ) external saveSender(sender) returns (uint256 amountCalculated) { + return + abi.decode( + _vault.quote( + abi.encodeCall( + Router.querySwapHook, + SwapSingleTokenHookParams({ + sender: msg.sender, + kind: SwapKind.EXACT_OUT, + pool: pool, + tokenIn: tokenIn, + tokenOut: tokenOut, + amountGiven: exactAmountOut, + limit: _MAX_AMOUNT, + deadline: type(uint256).max, + wethIsEth: false, + userData: userData + }) + ) + ), + (uint256) + ); + } + + /** + * @notice Hook for swap queries. + * @dev Can only be called by the Vault. Also handles native ETH. + * @param params Swap parameters (see IRouter for struct definition) + * @return amountCalculated Token amount calculated by the pool math (e.g., amountOut for a exact in swap) + */ + function querySwapHook( + SwapSingleTokenHookParams calldata params + ) external nonReentrant onlyVault returns (uint256) { + (uint256 amountCalculated, , ) = _swapHook(params); + + return amountCalculated; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/RouterCommon.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/RouterCommon.sol new file mode 100644 index 00000000..95ec34f6 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/RouterCommon.sol @@ -0,0 +1,339 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { IPermit2 } from "./interfaces/IPermit2.sol"; + +import { IRouterCommon } from "./interfaces/IRouterCommon.sol"; +import { IWETH } from "./interfaces/IWETH.sol"; +import { IAllowanceTransfer } from "./interfaces/IAllowanceTransfer.sol"; +import { IVault } from "./interfaces/IVault.sol"; + +import { + TransientStorageHelpers +} from "./interfaces/TransientStorageHelpers.sol"; +import { StorageSlotExtension } from "./interfaces/StorageSlotExtension.sol"; +import { RevertCodec } from "./interfaces/RevertCodec.sol"; + +import { VaultGuard } from "./interfaces/VaultGuard.sol"; + +/// @notice Contract for functions shared between the `Router` and `BatchRouter`. +abstract contract RouterCommon is IRouterCommon, VaultGuard { + using TransientStorageHelpers for StorageSlotExtension.Uint256SlotType; + using Address for address payable; + using StorageSlotExtension for *; + using SafeERC20 for IWETH; + using SafeCast for *; + + // NOTE: If you use a constant, then it is simply replaced everywhere when this constant is used by what is written + // after =. If you use immutable, the value is first calculated and then replaced everywhere. That means that if a + // constant has executable variables, they will be executed every time the constant is used. + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _SENDER_SLOT = TransientStorageHelpers.calculateSlot(type(RouterCommon).name, "sender"); + + // solhint-disable-next-line var-name-mixedcase + bytes32 private immutable _IS_RETURN_ETH_LOCKED_SLOT = + TransientStorageHelpers.calculateSlot(type(RouterCommon).name, "isReturnEthLocked"); + + /// @notice Incoming ETH transfer from an address that is not WETH. + error EthTransfer(); + + /// @notice The amount of ETH paid is insufficient to complete this operation. + error InsufficientEth(); + + /// @notice The swap transaction was not validated before the specified deadline timestamp. + error SwapDeadline(); + + // Raw token balances are stored in half a slot, so the max is uint128. Moreover, given that amounts are usually + // scaled inside the Vault, sending type(uint256).max would result in an overflow and revert. + uint256 internal constant _MAX_AMOUNT = type(uint128).max; + + // solhint-disable-next-line var-name-mixedcase + IWETH internal immutable _weth; + + IPermit2 internal immutable _permit2; + + /** + * @notice Saves the user or contract that initiated the current operation. + * @dev It is possible to nest router calls (e.g., with reentrant hooks), but the sender returned by the Router's + * `getSender` function will always be the "outermost" caller. Some transactions require the Router to identify + * multiple senders. Consider the following example: + * + * - ContractA has a function that calls the Router, then calls ContractB with the output. ContractB in turn + * calls back into the Router. + * - Imagine further that ContractA is a pool with a "before" hook that also calls the Router. + * + * When the user calls the function on ContractA, there are three calls to the Router in the same transaction: + * - 1st call: When ContractA calls the Router directly, to initiate an operation on the pool (say, a swap). + * (Sender is contractA, initiator of the operation.) + * + * - 2nd call: When the pool operation invokes a hook (say onBeforeSwap), which calls back into the Router. + * This is a "nested" call within the original pool operation. The nested call returns, then the + * before hook returns, the Router completes the operation, and finally returns back to ContractA + * with the result (e.g., a calculated amount of tokens). + * (Nested call; sender is still ContractA through all of this.) + * + * - 3rd call: When the first operation is complete, ContractA calls ContractB, which in turn calls the Router. + * (Not nested, as the original router call from contractA has returned. Sender is now ContractB.) + */ + modifier saveSender(address sender) { + bool isExternalSender = _saveSender(sender); + _; + _discardSenderIfRequired(isExternalSender); + } + + /** + * @notice Locks the return of excess ETH to the sender until the end of the function. + * @dev This also encompasses the `saveSender` functionality. + */ + modifier saveSenderAndManageEth() { + bool isExternalSender = _saveSender(msg.sender); + + // Lock the return of ETH during execution + _isReturnEthLockedSlot().tstore(true); + _; + _isReturnEthLockedSlot().tstore(false); + + _returnEth(_getSenderSlot().tload()); + _discardSenderIfRequired(isExternalSender); + } + + function _saveSender(address sender) internal returns (bool isExternalSender) { + address savedSender = _getSenderSlot().tload(); + + // NOTE: Only the most external sender will be saved by the Router. + if (savedSender == address(0)) { + _getSenderSlot().tstore(sender); + isExternalSender = true; + } + } + + function _discardSenderIfRequired(bool isExternalSender) internal { + // Only the external sender shall be cleaned up; if it's not an external sender it means that + // the value was not saved in this modifier. + if (isExternalSender) { + _getSenderSlot().tstore(address(0)); + } + } + + constructor(IVault vault, IWETH weth, IPermit2 permit2) VaultGuard(vault) { + _weth = weth; + _permit2 = permit2; + } + + /******************************************************************************* + Utilities + *******************************************************************************/ + + struct SignatureParts { + bytes32 r; + bytes32 s; + uint8 v; + } + + /// @inheritdoc IRouterCommon + function permitBatchAndCall( + PermitApproval[] calldata permitBatch, + bytes[] calldata permitSignatures, + IAllowanceTransfer.PermitBatch calldata permit2Batch, + bytes calldata permit2Signature, + bytes[] calldata multicallData + ) external payable virtual saveSender(msg.sender) returns (bytes[] memory results) { + // Use Permit (ERC-2612) to grant allowances to Permit2 for tokens to swap, + // and grant allowances to Vault for BPT tokens. + for (uint256 i = 0; i < permitBatch.length; ++i) { + bytes memory signature = permitSignatures[i]; + + SignatureParts memory signatureParts = _getSignatureParts(signature); + PermitApproval memory permitApproval = permitBatch[i]; + + try + IERC20Permit(permitApproval.token).permit( + permitApproval.owner, + address(this), + permitApproval.amount, + permitApproval.deadline, + signatureParts.v, + signatureParts.r, + signatureParts.s + ) + { + // solhint-disable-previous-line no-empty-blocks + // OK; carry on. + } catch (bytes memory returnData) { + // Did it fail because the permit was executed (possible DoS attack to make the transaction revert), + // or was it something else (e.g., deadline, invalid signature)? + if ( + IERC20(permitApproval.token).allowance(permitApproval.owner, address(this)) != permitApproval.amount + ) { + // It was something else, or allowance was used, so we should revert. Bubble up the revert reason. + RevertCodec.bubbleUpRevert(returnData); + } + } + } + + // Only call permit2 if there's something to do. + if (permit2Batch.details.length > 0) { + // Use Permit2 for tokens that are swapped and added into the Vault. + _permit2.permit(msg.sender, permit2Batch, permit2Signature); + } + + // Execute all the required operations once permissions have been granted. + return multicall(multicallData); + } + + /// @inheritdoc IRouterCommon + function multicall( + bytes[] calldata data + ) public payable virtual saveSenderAndManageEth returns (bytes[] memory results) { + results = new bytes[](data.length); + for (uint256 i = 0; i < data.length; ++i) { + results[i] = Address.functionDelegateCall(address(this), data[i]); + } + return results; + } + + function _getSignatureParts(bytes memory signature) private pure returns (SignatureParts memory signatureParts) { + bytes32 r; + bytes32 s; + uint8 v; + + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + + signatureParts.r = r; + signatureParts.s = s; + signatureParts.v = v; + } + + /** + * @dev Returns excess ETH back to the contract caller. Checks for sufficient ETH balance are made right before + * each deposit, ensuring it will revert with a friendly custom error. If there is any balance remaining when + * `_returnEth` is called, return it to the sender. + * + * Because the caller might not know exactly how much ETH a Vault action will require, they may send extra. + * Note that this excess value is returned *to the contract caller* (msg.sender). If caller and e.g. swap sender + * are not the same (because the caller is a relayer for the sender), then it is up to the caller to manage this + * returned ETH. + */ + function _returnEth(address sender) internal { + // It's cheaper to check the balance and return early than checking a transient variable. + // Moreover, most operations will not have ETH to return. + uint256 excess = address(this).balance; + if (excess == 0) { + return; + } + + // If the return of ETH is locked, then don't return it, + // because _returnEth will be called again at the end of the call. + if (_isReturnEthLockedSlot().tload()) { + return; + } + + payable(sender).sendValue(excess); + } + + /** + * @dev Returns an array with `amountGiven` at `tokenIndex`, and 0 for every other index. + * The returned array length matches the number of tokens in the pool. + * Reverts if the given index is greater than or equal to the pool number of tokens. + */ + function _getSingleInputArrayAndTokenIndex( + address pool, + IERC20 token, + uint256 amountGiven + ) internal view returns (uint256[] memory amountsGiven, uint256 tokenIndex) { + uint256 numTokens; + (numTokens, tokenIndex) = _vault.getPoolTokenCountAndIndexOfToken(pool, token); + amountsGiven = new uint256[](numTokens); + amountsGiven[tokenIndex] = amountGiven; + } + + function _takeTokenIn(address sender, IERC20 tokenIn, uint256 amountIn, bool wethIsEth) internal { + // If the tokenIn is ETH, then wrap `amountIn` into WETH. + if (wethIsEth && tokenIn == _weth) { + if (address(this).balance < amountIn) { + revert InsufficientEth(); + } + + // wrap amountIn to WETH. + _weth.deposit{ value: amountIn }(); + // send WETH to Vault. + _weth.safeTransfer(address(_vault), amountIn); + // update Vault accounting. + _vault.settle(_weth, amountIn); + } else { + if (amountIn > 0) { + // Send the tokenIn amount to the Vault + _permit2.transferFrom(sender, address(_vault), amountIn.toUint160(), address(tokenIn)); + _vault.settle(tokenIn, amountIn); + } + } + } + + function _sendTokenOut(address sender, IERC20 tokenOut, uint256 amountOut, bool wethIsEth) internal { + if (amountOut == 0) { + return; + } + + // If the tokenOut is ETH, then unwrap `amountOut` into ETH. + if (wethIsEth && tokenOut == _weth) { + // Receive the WETH amountOut. + _vault.sendTo(tokenOut, address(this), amountOut); + // Withdraw WETH to ETH. + _weth.withdraw(amountOut); + // Send ETH to sender. + payable(sender).sendValue(amountOut); + } else { + // Receive the tokenOut amountOut. + _vault.sendTo(tokenOut, sender, amountOut); + } + } + + function _maxTokenLimits(address pool) internal view returns (uint256[] memory maxLimits) { + uint256 numTokens = _vault.getPoolTokens(pool).length; + maxLimits = new uint256[](numTokens); + for (uint256 i = 0; i < numTokens; ++i) { + maxLimits[i] = _MAX_AMOUNT; + } + } + + /** + * @dev Enables the Router to receive ETH. This is required for it to be able to unwrap WETH, which sends ETH to the + * caller. + * + * Any ETH sent to the Router outside of the WETH unwrapping mechanism would be forever locked inside the Router, so + * we prevent that from happening. Other mechanisms used to send ETH to the Router (such as being the recipient of + * an ETH swap, Pool exit or withdrawal, contract self-destruction, or receiving the block mining reward) will + * result in locked funds, but are not otherwise a security or soundness issue. This check only exists as an attempt + * to prevent user error. + */ + receive() external payable { + if (msg.sender != address(_weth)) { + revert EthTransfer(); + } + } + + /// @inheritdoc IRouterCommon + function getSender() external view returns (address) { + return _getSenderSlot().tload(); + } + + function _getSenderSlot() internal view returns (StorageSlotExtension.AddressSlotType) { + return _SENDER_SLOT.asAddress(); + } + + function _isReturnEthLockedSlot() internal view returns (StorageSlotExtension.BooleanSlotType) { + return _IS_RETURN_ETH_LOCKED_SLOT.asBoolean(); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/SingletonAuthentication.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/SingletonAuthentication.sol new file mode 100644 index 00000000..91964dbc --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/SingletonAuthentication.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IVault } from "./interfaces/IVault.sol"; +import { IAuthorizer } from "./interfaces/IAuthorizer.sol"; + +import { Authentication } from "./interfaces/Authentication.sol"; + +/** + * @notice Base contract suitable for Singleton contracts (e.g., pool factories) that have permissioned functions. + * @dev The disambiguator is the contract's own address. This is used in the construction of actionIds for permissioned + * functions, to avoid conflicts when multiple contracts (or multiple versions of the same contract) use the same + * function name. + */ +abstract contract SingletonAuthentication is Authentication { + IVault private immutable _vault; + + // Use the contract's own address to disambiguate action identifiers + constructor(IVault vault) Authentication(bytes32(uint256(uint160(address(this))))) { + _vault = vault; + } + + /** + * @notice Get the address of the Balancer Vault. + * @return An interface pointer to the Vault + */ + function getVault() public view returns (IVault) { + return _vault; + } + + /** + * @notice Get the address of the Authorizer. + * @return An interface pointer to the Authorizer + */ + function getAuthorizer() public view returns (IAuthorizer) { + return getVault().getAuthorizer(); + } + + function _canPerform(bytes32 actionId, address account) internal view override returns (bool) { + return getAuthorizer().canPerform(actionId, account, address(this)); + } + + function _canPerform(bytes32 actionId, address account, address where) internal view returns (bool) { + return getAuthorizer().canPerform(actionId, account, where); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Vault.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Vault.sol new file mode 100644 index 00000000..3639a0dd --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Vault.sol @@ -0,0 +1,1544 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol"; + +import { IProtocolFeeController } from "./interfaces/IProtocolFeeController.sol"; +import { IVaultExtension } from "./interfaces/IVaultExtension.sol"; +import { IPoolLiquidity } from "./interfaces/IPoolLiquidity.sol"; +import { IAuthorizer } from "./interfaces/IAuthorizer.sol"; +import { IVaultAdmin } from "./interfaces/IVaultAdmin.sol"; +import { IVaultMain } from "./interfaces/IVaultMain.sol"; +import { IBasePool } from "./interfaces/IBasePool.sol"; +import { IHooks } from "./interfaces/IHooks.sol"; +import "./interfaces/VaultTypes.sol"; + +import { StorageSlotExtension } from "./interfaces/StorageSlotExtension.sol"; +import { EVMCallModeHelpers } from "./interfaces/EVMCallModeHelpers.sol"; +import { PackedTokenBalance } from "./interfaces/PackedTokenBalance.sol"; +import { ScalingHelpers } from "./interfaces/ScalingHelpers.sol"; +import { CastingHelpers } from "./interfaces/CastingHelpers.sol"; +import { BufferHelpers } from "./interfaces/BufferHelpers.sol"; +import { InputHelpers } from "./interfaces/InputHelpers.sol"; +import { FixedPoint } from "./interfaces/FixedPoint.sol"; +import { + TransientStorageHelpers +} from "./interfaces/TransientStorageHelpers.sol"; + +import { VaultStateLib, VaultStateBits } from "./lib/VaultStateLib.sol"; +import { HooksConfigLib } from "./lib/HooksConfigLib.sol"; +import { PoolConfigLib } from "./lib/PoolConfigLib.sol"; +import { PoolDataLib } from "./lib/PoolDataLib.sol"; +import { BasePoolMath } from "./interfaces/BasePoolMath.sol"; +import { VaultCommon } from "./VaultCommon.sol"; + +contract Vault is IVaultMain, VaultCommon, Proxy { + using PackedTokenBalance for bytes32; + using BufferHelpers for bytes32; + using InputHelpers for uint256; + using FixedPoint for *; + using Address for *; + using CastingHelpers for uint256[]; + using SafeCast for *; + using SafeERC20 for IERC20; + using PoolConfigLib for PoolConfigBits; + using HooksConfigLib for PoolConfigBits; + using VaultStateLib for VaultStateBits; + using ScalingHelpers for *; + using TransientStorageHelpers for *; + using StorageSlotExtension for *; + using PoolDataLib for PoolData; + + // Local reference to the Proxy pattern Vault extension contract. + IVaultExtension private immutable _vaultExtension; + + constructor(IVaultExtension vaultExtension, IAuthorizer authorizer, IProtocolFeeController protocolFeeController) { + if (address(vaultExtension.vault()) != address(this)) { + revert WrongVaultExtensionDeployment(); + } + + if (address(protocolFeeController.vault()) != address(this)) { + revert WrongProtocolFeeControllerDeployment(); + } + + _vaultExtension = vaultExtension; + _protocolFeeController = protocolFeeController; + + _vaultPauseWindowEndTime = IVaultAdmin(address(vaultExtension)).getPauseWindowEndTime(); + _vaultBufferPeriodDuration = IVaultAdmin(address(vaultExtension)).getBufferPeriodDuration(); + _vaultBufferPeriodEndTime = IVaultAdmin(address(vaultExtension)).getBufferPeriodEndTime(); + + _MINIMUM_TRADE_AMOUNT = IVaultAdmin(address(vaultExtension)).getMinimumTradeAmount(); + _MINIMUM_WRAP_AMOUNT = IVaultAdmin(address(vaultExtension)).getMinimumWrapAmount(); + + _authorizer = authorizer; + } + + /******************************************************************************* + Transient Accounting + *******************************************************************************/ + + /** + * @dev This modifier is used for functions that temporarily modify the token deltas + * of the Vault, but expect to revert or settle balances by the end of their execution. + * It works by ensuring that the balances are properly settled by the time the last + * operation is executed. + * + * This is useful for functions like `unlock`, which perform arbitrary external calls: + * we can keep track of temporary deltas changes, and make sure they are settled by the + * time the external call is complete. + */ + modifier transient() { + bool isUnlockedBefore = _isUnlocked().tload(); + + if (isUnlockedBefore == false) { + _isUnlocked().tstore(true); + } + + // The caller does everything here and has to settle all outstanding balances. + _; + + if (isUnlockedBefore == false) { + if (_nonZeroDeltaCount().tload() != 0) { + revert BalanceNotSettled(); + } + + _isUnlocked().tstore(false); + } + } + + /// @inheritdoc IVaultMain + function unlock(bytes calldata data) external transient returns (bytes memory result) { + return (msg.sender).functionCall(data); + } + + /// @inheritdoc IVaultMain + function settle(IERC20 token, uint256 amountHint) external nonReentrant onlyWhenUnlocked returns (uint256 credit) { + uint256 reservesBefore = _reservesOf[token]; + uint256 currentReserves = token.balanceOf(address(this)); + _reservesOf[token] = currentReserves; + credit = currentReserves - reservesBefore; + + // If the given hint is equal or greater to the reserve difference, we just take the actual reserve difference + // as the paid amount; the actual balance of the tokens in the Vault is what matters here. + if (credit > amountHint) { + // If the difference in reserves is higher than the amount claimed to be paid by the caller, there was some + // leftover that had been sent to the Vault beforehand, which was not incorporated into the reserves. + // In that case, we simply discard the leftover by considering the given hint as the amount paid. + // In turn, this gives the caller credit for the given amount hint, which is what the caller is expecting. + credit = amountHint; + } + + _supplyCredit(token, credit); + } + + /// @inheritdoc IVaultMain + function sendTo(IERC20 token, address to, uint256 amount) external nonReentrant onlyWhenUnlocked { + _takeDebt(token, amount); + _reservesOf[token] -= amount; + + token.safeTransfer(to, amount); + } + + /******************************************************************************* + Pool Operations + *******************************************************************************/ + + // The Vault performs all upscaling and downscaling (due to token decimals, rates, etc.), so that the pools + // don't have to. However, scaling inevitably leads to rounding errors, so we take great care to ensure that + // any rounding errors favor the Vault. An important invariant of the system is that there is no repeatable + // path where tokensOut > tokensIn. + // + // In general, this means rounding up any values entering the Vault, and rounding down any values leaving + // the Vault, so that external users either pay a little extra or receive a little less in the case of a + // rounding error. + // + // However, it's not always straightforward to determine the correct rounding direction, given the presence + // and complexity of intermediate steps. An "amountIn" sounds like it should be rounded up: but only if that + // is the amount actually being transferred. If instead it is an amount sent to the pool math, where rounding + // up would result in a *higher* calculated amount out, that would favor the user instead of the Vault. So in + // that case, amountIn should be rounded down. + // + // See comments justifying the rounding direction in each case. + // + // This reasoning applies to Weighted Pool math, and is likely to apply to others as well, but of course + // it's possible a new pool type might not conform. Duplicate the tests for new pool types (e.g., Stable Math). + // Also, the final code should ensure that we are not relying entirely on the rounding directions here, + // but have enough additional layers (e.g., minimum amounts, buffer wei on all transfers) to guarantee safety, + // even if it turns out these directions are incorrect for a new pool type. + + /******************************************************************************* + Swaps + *******************************************************************************/ + + /// @inheritdoc IVaultMain + function swap( + VaultSwapParams memory vaultSwapParams + ) + external + onlyWhenUnlocked + withInitializedPool(vaultSwapParams.pool) + returns (uint256 amountCalculated, uint256 amountIn, uint256 amountOut) + { + _ensureUnpaused(vaultSwapParams.pool); + + if (vaultSwapParams.amountGivenRaw == 0) { + revert AmountGivenZero(); + } + + if (vaultSwapParams.tokenIn == vaultSwapParams.tokenOut) { + revert CannotSwapSameToken(); + } + + // `_loadPoolDataUpdatingBalancesAndYieldFees` is non-reentrant, as it updates storage as well + // as filling in poolData in memory. Since the swap hooks are reentrant and could do anything, including + // change these balances, we cannot defer settlement until `_swap`. + // + // Sets all fields in `poolData`. Side effects: updates `_poolTokenBalances`, `_aggregateFeeAmounts` + // in storage. + PoolData memory poolData = _loadPoolDataUpdatingBalancesAndYieldFees(vaultSwapParams.pool, Rounding.ROUND_DOWN); + SwapState memory swapState = _loadSwapState(vaultSwapParams, poolData); + PoolSwapParams memory poolSwapParams = _buildPoolSwapParams(vaultSwapParams, swapState, poolData); + + if (poolData.poolConfigBits.shouldCallBeforeSwap()) { + HooksConfigLib.callBeforeSwapHook( + poolSwapParams, + vaultSwapParams.pool, + _hooksContracts[vaultSwapParams.pool] + ); + + // The call to `onBeforeSwap` could potentially update token rates and balances. + // We update `poolData.tokenRates`, `poolData.rawBalances` and `poolData.balancesLiveScaled18` + // to ensure the `onSwap` and `onComputeDynamicSwapFeePercentage` are called with the current values. + poolData.reloadBalancesAndRates(_poolTokenBalances[vaultSwapParams.pool], Rounding.ROUND_DOWN); + + // Also update amountGivenScaled18, as it will now be used in the swap, and the rates might have changed. + swapState.amountGivenScaled18 = _computeAmountGivenScaled18(vaultSwapParams, poolData, swapState); + + poolSwapParams = _buildPoolSwapParams(vaultSwapParams, swapState, poolData); + } + + // Note that this must be called *after* the before hook, to guarantee that the swap params are the same + // as those passed to the main operation. + // + // At this point, the static swap fee percentage is loaded in the `swapState` as the default, to be used + // unless the pool has a dynamic swap fee. It is also passed into the hook, to support common cases + // where the dynamic fee computation logic uses it. + if (poolData.poolConfigBits.shouldCallComputeDynamicSwapFee()) { + swapState.swapFeePercentage = HooksConfigLib.callComputeDynamicSwapFeeHook( + poolSwapParams, + vaultSwapParams.pool, + swapState.swapFeePercentage, + _hooksContracts[vaultSwapParams.pool] + ); + } + + // Non-reentrant call that updates accounting. + // The following side-effects are important to note: + // PoolData balancesRaw and balancesLiveScaled18 are adjusted for swap amounts and fees inside of _swap. + uint256 amountCalculatedScaled18; + (amountCalculated, amountCalculatedScaled18, amountIn, amountOut) = _swap( + vaultSwapParams, + swapState, + poolData, + poolSwapParams + ); + + // The new amount calculated is 'amountCalculated + delta'. If the underlying hook fails, or limits are + // violated, `onAfterSwap` will revert. Uses msg.sender as the Router (the contract that called the Vault). + if (poolData.poolConfigBits.shouldCallAfterSwap()) { + // `hooksContract` needed to fix stack too deep. + IHooks hooksContract = _hooksContracts[vaultSwapParams.pool]; + + amountCalculated = poolData.poolConfigBits.callAfterSwapHook( + amountCalculatedScaled18, + amountCalculated, + msg.sender, + vaultSwapParams, + swapState, + poolData, + hooksContract + ); + } + + if (vaultSwapParams.kind == SwapKind.EXACT_IN) { + amountOut = amountCalculated; + } else { + amountIn = amountCalculated; + } + } + + function _loadSwapState( + VaultSwapParams memory vaultSwapParams, + PoolData memory poolData + ) private pure returns (SwapState memory swapState) { + swapState.indexIn = _findTokenIndex(poolData.tokens, vaultSwapParams.tokenIn); + swapState.indexOut = _findTokenIndex(poolData.tokens, vaultSwapParams.tokenOut); + + swapState.amountGivenScaled18 = _computeAmountGivenScaled18(vaultSwapParams, poolData, swapState); + swapState.swapFeePercentage = poolData.poolConfigBits.getStaticSwapFeePercentage(); + } + + function _buildPoolSwapParams( + VaultSwapParams memory vaultSwapParams, + SwapState memory swapState, + PoolData memory poolData + ) internal view returns (PoolSwapParams memory) { + // Uses msg.sender as the Router (the contract that called the Vault). + return + PoolSwapParams({ + kind: vaultSwapParams.kind, + amountGivenScaled18: swapState.amountGivenScaled18, + balancesScaled18: poolData.balancesLiveScaled18, + indexIn: swapState.indexIn, + indexOut: swapState.indexOut, + router: msg.sender, + userData: vaultSwapParams.userData + }); + } + + /** + * @dev Preconditions: decimalScalingFactors and tokenRates in `poolData` must be current. + * Uses amountGivenRaw and kind from `vaultSwapParams`. + */ + function _computeAmountGivenScaled18( + VaultSwapParams memory vaultSwapParams, + PoolData memory poolData, + SwapState memory swapState + ) private pure returns (uint256) { + // If the amountGiven is entering the pool math (ExactIn), round down, since a lower apparent amountIn leads + // to a lower calculated amountOut, favoring the pool. + return + vaultSwapParams.kind == SwapKind.EXACT_IN + ? vaultSwapParams.amountGivenRaw.toScaled18ApplyRateRoundDown( + poolData.decimalScalingFactors[swapState.indexIn], + poolData.tokenRates[swapState.indexIn] + ) + : vaultSwapParams.amountGivenRaw.toScaled18ApplyRateRoundUp( + poolData.decimalScalingFactors[swapState.indexOut], + // If the swap is ExactOut, the amountGiven is the amount of tokenOut. So, we want to use the rate + // rounded up to calculate the amountGivenScaled18, because if this value is bigger, the + // amountCalculatedRaw will be bigger, implying that the user will pay for any rounding + // inconsistency, and not the Vault. + poolData.tokenRates[swapState.indexOut].computeRateRoundUp() + ); + } + + /** + * @dev Auxiliary struct to prevent stack-too-deep issues inside `_swap` function. + * Total swap fees include LP (pool) fees and aggregate (protocol + pool creator) fees. + */ + struct SwapInternalLocals { + uint256 totalSwapFeeAmountScaled18; + uint256 totalSwapFeeAmountRaw; + uint256 aggregateFeeAmountRaw; + } + + /** + * @dev Main non-reentrant portion of the swap, which calls the pool hook and updates accounting. `vaultSwapParams` + * are passed to the pool's `onSwap` hook. + * + * Preconditions: complete `SwapParams`, `SwapState`, and `PoolData`. + * Side effects: mutates balancesRaw and balancesLiveScaled18 in `poolData`. + * Updates `_aggregateFeeAmounts`, and `_poolTokenBalances` in storage. + * Emits Swap event. + */ + function _swap( + VaultSwapParams memory vaultSwapParams, + SwapState memory swapState, + PoolData memory poolData, + PoolSwapParams memory poolSwapParams + ) + internal + nonReentrant + returns ( + uint256 amountCalculatedRaw, + uint256 amountCalculatedScaled18, + uint256 amountInRaw, + uint256 amountOutRaw + ) + { + SwapInternalLocals memory locals; + + if (vaultSwapParams.kind == SwapKind.EXACT_IN) { + // Round up to avoid losses during precision loss. + locals.totalSwapFeeAmountScaled18 = poolSwapParams.amountGivenScaled18.mulUp(swapState.swapFeePercentage); + poolSwapParams.amountGivenScaled18 -= locals.totalSwapFeeAmountScaled18; + } + + _ensureValidSwapAmount(poolSwapParams.amountGivenScaled18); + + // Perform the swap request hook and compute the new balances for 'token in' and 'token out' after the swap. + amountCalculatedScaled18 = IBasePool(vaultSwapParams.pool).onSwap(poolSwapParams); + + _ensureValidSwapAmount(amountCalculatedScaled18); + + // Note that balances are kept in memory, and are not fully computed until the `setPoolBalances` below. + // Intervening code cannot read balances from storage, as they are temporarily out-of-sync here. This function + // is nonReentrant, to guard against read-only reentrancy issues. + + // (1) and (2): get raw amounts and check limits. + if (vaultSwapParams.kind == SwapKind.EXACT_IN) { + // Restore the original input value; this function should not mutate memory inputs. + // At this point swap fee amounts have already been computed for EXACT_IN. + poolSwapParams.amountGivenScaled18 = swapState.amountGivenScaled18; + + // For `ExactIn` the amount calculated is leaving the Vault, so we round down. + amountCalculatedRaw = amountCalculatedScaled18.toRawUndoRateRoundDown( + poolData.decimalScalingFactors[swapState.indexOut], + // If the swap is ExactIn, the amountCalculated is the amount of tokenOut. So, we want to use the rate + // rounded up to calculate the amountCalculatedRaw, because scale down (undo rate) is a division, the + // larger the rate, the smaller the amountCalculatedRaw. So, any rounding imprecision will stay in the + // Vault and not be drained by the user. + poolData.tokenRates[swapState.indexOut].computeRateRoundUp() + ); + + (amountInRaw, amountOutRaw) = (vaultSwapParams.amountGivenRaw, amountCalculatedRaw); + + if (amountOutRaw < vaultSwapParams.limitRaw) { + revert SwapLimit(amountOutRaw, vaultSwapParams.limitRaw); + } + } else { + // To ensure symmetry with EXACT_IN, the swap fee used by ExactOut is + // `amountCalculated * fee% / (100% - fee%)`. Add it to the calculated amountIn. Round up to avoid losses + // during precision loss. + locals.totalSwapFeeAmountScaled18 = amountCalculatedScaled18.mulDivUp( + swapState.swapFeePercentage, + swapState.swapFeePercentage.complement() + ); + + amountCalculatedScaled18 += locals.totalSwapFeeAmountScaled18; + + // For `ExactOut` the amount calculated is entering the Vault, so we round up. + amountCalculatedRaw = amountCalculatedScaled18.toRawUndoRateRoundUp( + poolData.decimalScalingFactors[swapState.indexIn], + poolData.tokenRates[swapState.indexIn] + ); + + (amountInRaw, amountOutRaw) = (amountCalculatedRaw, vaultSwapParams.amountGivenRaw); + + if (amountInRaw > vaultSwapParams.limitRaw) { + revert SwapLimit(amountInRaw, vaultSwapParams.limitRaw); + } + } + + // 3) Deltas: debit for token in, credit for token out. + _takeDebt(vaultSwapParams.tokenIn, amountInRaw); + _supplyCredit(vaultSwapParams.tokenOut, amountOutRaw); + + // 4) Compute and charge protocol and creator fees. + // Note that protocol fee storage is updated before balance storage, as the final raw balances need to take + // the fees into account. + (locals.totalSwapFeeAmountRaw, locals.aggregateFeeAmountRaw) = _computeAndChargeAggregateSwapFees( + poolData, + locals.totalSwapFeeAmountScaled18, + vaultSwapParams.pool, + vaultSwapParams.tokenIn, + swapState.indexIn + ); + + // 5) Pool balances: raw and live. + + poolData.updateRawAndLiveBalance( + swapState.indexIn, + poolData.balancesRaw[swapState.indexIn] + amountInRaw - locals.aggregateFeeAmountRaw, + Rounding.ROUND_DOWN + ); + poolData.updateRawAndLiveBalance( + swapState.indexOut, + poolData.balancesRaw[swapState.indexOut] - amountOutRaw, + Rounding.ROUND_DOWN + ); + + // 6) Store pool balances, raw and live (only index in and out). + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolBalances = _poolTokenBalances[ + vaultSwapParams.pool + ]; + poolBalances[swapState.indexIn] = PackedTokenBalance.toPackedBalance( + poolData.balancesRaw[swapState.indexIn], + poolData.balancesLiveScaled18[swapState.indexIn] + ); + poolBalances[swapState.indexOut] = PackedTokenBalance.toPackedBalance( + poolData.balancesRaw[swapState.indexOut], + poolData.balancesLiveScaled18[swapState.indexOut] + ); + + // 7) Off-chain events. + emit Swap( + vaultSwapParams.pool, + vaultSwapParams.tokenIn, + vaultSwapParams.tokenOut, + amountInRaw, + amountOutRaw, + swapState.swapFeePercentage, + locals.totalSwapFeeAmountRaw + ); + } + + /*************************************************************************** + Add Liquidity + ***************************************************************************/ + + /// @inheritdoc IVaultMain + function addLiquidity( + AddLiquidityParams memory params + ) + external + onlyWhenUnlocked + withInitializedPool(params.pool) + returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData) + { + // Round balances up when adding liquidity: + // If proportional, higher balances = higher proportional amountsIn, favoring the pool. + // If unbalanced, higher balances = lower invariant ratio with fees. + // bptOut = supply * (ratio - 1), so lower ratio = less bptOut, favoring the pool. + + _ensureUnpaused(params.pool); + _addLiquidityCalled().tSet(params.pool, true); + + // `_loadPoolDataUpdatingBalancesAndYieldFees` is non-reentrant, as it updates storage as well + // as filling in poolData in memory. Since the add liquidity hooks are reentrant and could do anything, + // including change these balances, we cannot defer settlement until `_addLiquidity`. + // + // Sets all fields in `poolData`. Side effects: updates `_poolTokenBalances`, and + // `_aggregateFeeAmounts` in storage. + PoolData memory poolData = _loadPoolDataUpdatingBalancesAndYieldFees(params.pool, Rounding.ROUND_UP); + InputHelpers.ensureInputLengthMatch(poolData.tokens.length, params.maxAmountsIn.length); + + // Amounts are entering pool math, so round down. + // Introducing `maxAmountsInScaled18` here and passing it through to _addLiquidity is not ideal, + // but it avoids the even worse options of mutating amountsIn inside AddLiquidityParams, + // or cluttering the AddLiquidityParams interface by adding amountsInScaled18. + uint256[] memory maxAmountsInScaled18 = params.maxAmountsIn.copyToScaled18ApplyRateRoundDownArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + + if (poolData.poolConfigBits.shouldCallBeforeAddLiquidity()) { + HooksConfigLib.callBeforeAddLiquidityHook( + msg.sender, + maxAmountsInScaled18, + params, + poolData, + _hooksContracts[params.pool] + ); + // The hook might have altered the balances, so we need to read them again to ensure that the data + // are fresh moving forward. We also need to upscale (adding liquidity, so round up) again. + poolData.reloadBalancesAndRates(_poolTokenBalances[params.pool], Rounding.ROUND_UP); + + // Also update maxAmountsInScaled18, as the rates might have changed. + maxAmountsInScaled18 = params.maxAmountsIn.copyToScaled18ApplyRateRoundDownArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + } + + // The bulk of the work is done here: the corresponding Pool hook is called, and the final balances + // are computed. This function is non-reentrant, as it performs the accounting updates. + // + // Note that poolData is mutated to update the Raw and Live balances, so they are accurate when passed + // into the AfterAddLiquidity hook. + // + // `amountsInScaled18` will be overwritten in the custom case, so we need to pass it back and forth to + // encapsulate that logic in `_addLiquidity`. + uint256[] memory amountsInScaled18; + (amountsIn, amountsInScaled18, bptAmountOut, returnData) = _addLiquidity( + poolData, + params, + maxAmountsInScaled18 + ); + + // AmountsIn can be changed by onAfterAddLiquidity if the hook charges fees or gives discounts. + // Uses msg.sender as the Router (the contract that called the Vault). + if (poolData.poolConfigBits.shouldCallAfterAddLiquidity()) { + // `hooksContract` needed to fix stack too deep. + IHooks hooksContract = _hooksContracts[params.pool]; + + amountsIn = poolData.poolConfigBits.callAfterAddLiquidityHook( + msg.sender, + amountsInScaled18, + amountsIn, + bptAmountOut, + params, + poolData, + hooksContract + ); + } + } + + // Avoid "stack too deep" - without polluting the Add/RemoveLiquidity params interface. + struct LiquidityLocals { + uint256 numTokens; + uint256 aggregateSwapFeeAmountRaw; + uint256 tokenIndex; + } + + /** + * @dev Calls the appropriate pool hook and calculates the required inputs and outputs for the operation + * considering the given kind, and updates the Vault's internal accounting. This includes: + * - Setting pool balances + * - Taking debt from the liquidity provider + * - Minting pool tokens + * - Emitting events + * + * It is non-reentrant, as it performs external calls and updates the Vault's state accordingly. + */ + function _addLiquidity( + PoolData memory poolData, + AddLiquidityParams memory params, + uint256[] memory maxAmountsInScaled18 + ) + internal + nonReentrant + returns ( + uint256[] memory amountsInRaw, + uint256[] memory amountsInScaled18, + uint256 bptAmountOut, + bytes memory returnData + ) + { + LiquidityLocals memory locals; + locals.numTokens = poolData.tokens.length; + amountsInRaw = new uint256[](locals.numTokens); + // `swapFeeAmounts` stores scaled18 amounts first, and is then reused to store raw amounts. + uint256[] memory swapFeeAmounts; + + if (params.kind == AddLiquidityKind.PROPORTIONAL) { + bptAmountOut = params.minBptAmountOut; + // Initializes the swapFeeAmounts empty array (no swap fees on proportional add liquidity). + swapFeeAmounts = new uint256[](locals.numTokens); + + amountsInScaled18 = BasePoolMath.computeProportionalAmountsIn( + poolData.balancesLiveScaled18, + _totalSupply(params.pool), + bptAmountOut + ); + } else if (params.kind == AddLiquidityKind.DONATION) { + poolData.poolConfigBits.requireDonationEnabled(); + + swapFeeAmounts = new uint256[](maxAmountsInScaled18.length); + bptAmountOut = 0; + amountsInScaled18 = maxAmountsInScaled18; + } else if (params.kind == AddLiquidityKind.UNBALANCED) { + poolData.poolConfigBits.requireUnbalancedLiquidityEnabled(); + + amountsInScaled18 = maxAmountsInScaled18; + // Deep copy given max amounts in raw to calculated amounts in raw to avoid scaling later, ensuring that + // `maxAmountsIn` is preserved. + ScalingHelpers.copyToArray(params.maxAmountsIn, amountsInRaw); + + (bptAmountOut, swapFeeAmounts) = BasePoolMath.computeAddLiquidityUnbalanced( + poolData.balancesLiveScaled18, + maxAmountsInScaled18, + _totalSupply(params.pool), + poolData.poolConfigBits.getStaticSwapFeePercentage(), + IBasePool(params.pool) + ); + } else if (params.kind == AddLiquidityKind.SINGLE_TOKEN_EXACT_OUT) { + poolData.poolConfigBits.requireUnbalancedLiquidityEnabled(); + + bptAmountOut = params.minBptAmountOut; + locals.tokenIndex = InputHelpers.getSingleInputIndex(maxAmountsInScaled18); + + amountsInScaled18 = maxAmountsInScaled18; + (amountsInScaled18[locals.tokenIndex], swapFeeAmounts) = BasePoolMath + .computeAddLiquiditySingleTokenExactOut( + poolData.balancesLiveScaled18, + locals.tokenIndex, + bptAmountOut, + _totalSupply(params.pool), + poolData.poolConfigBits.getStaticSwapFeePercentage(), + IBasePool(params.pool) + ); + } else if (params.kind == AddLiquidityKind.CUSTOM) { + poolData.poolConfigBits.requireAddLiquidityCustomEnabled(); + + // Uses msg.sender as the Router (the contract that called the Vault). + (amountsInScaled18, bptAmountOut, swapFeeAmounts, returnData) = IPoolLiquidity(params.pool) + .onAddLiquidityCustom( + msg.sender, + maxAmountsInScaled18, + params.minBptAmountOut, + poolData.balancesLiveScaled18, + params.userData + ); + } else { + revert InvalidAddLiquidityKind(); + } + + // At this point we have the calculated BPT amount. + if (bptAmountOut < params.minBptAmountOut) { + revert BptAmountOutBelowMin(bptAmountOut, params.minBptAmountOut); + } + + _ensureValidTradeAmount(bptAmountOut); + + for (uint256 i = 0; i < locals.numTokens; ++i) { + uint256 amountInRaw; + + // 1) Calculate raw amount in. + { + uint256 amountInScaled18 = amountsInScaled18[i]; + _ensureValidTradeAmount(amountInScaled18); + + // If the value in memory is not set, convert scaled amount to raw. + if (amountsInRaw[i] == 0) { + // amountsInRaw are amounts actually entering the Pool, so we round up. + // Do not mutate in place yet, as we need them scaled for the `onAfterAddLiquidity` hook. + amountInRaw = amountInScaled18.toRawUndoRateRoundUp( + poolData.decimalScalingFactors[i], + poolData.tokenRates[i] + ); + + amountsInRaw[i] = amountInRaw; + } else { + // Exact in requests will have the raw amount in memory already, so we use it moving forward and + // skip downscaling. + amountInRaw = amountsInRaw[i]; + } + } + + IERC20 token = poolData.tokens[i]; + + // 2) Check limits for raw amounts. + if (amountInRaw > params.maxAmountsIn[i]) { + revert AmountInAboveMax(token, amountInRaw, params.maxAmountsIn[i]); + } + + // 3) Deltas: Debit of token[i] for amountInRaw. + _takeDebt(token, amountInRaw); + + // 4) Compute and charge protocol and creator fees. + // swapFeeAmounts[i] is now raw instead of scaled. + (swapFeeAmounts[i], locals.aggregateSwapFeeAmountRaw) = _computeAndChargeAggregateSwapFees( + poolData, + swapFeeAmounts[i], + params.pool, + token, + i + ); + + // 5) Pool balances: raw and live. + // We need regular balances to complete the accounting, and the upscaled balances + // to use in the `after` hook later on. + + // A pool's token balance increases by amounts in after adding liquidity, minus fees. + poolData.updateRawAndLiveBalance( + i, + poolData.balancesRaw[i] + amountInRaw - locals.aggregateSwapFeeAmountRaw, + Rounding.ROUND_DOWN + ); + } + + // 6) Store pool balances, raw and live. + _writePoolBalancesToStorage(params.pool, poolData); + + // 7) BPT supply adjustment. + // When adding liquidity, we must mint tokens concurrently with updating pool balances, + // as the pool's math relies on totalSupply. + _mint(address(params.pool), params.to, bptAmountOut); + + // 8) Off-chain events. + emit PoolBalanceChanged( + params.pool, + params.to, + _totalSupply(params.pool), + amountsInRaw.unsafeCastToInt256(true), + swapFeeAmounts + ); + } + + /*************************************************************************** + Remove Liquidity + ***************************************************************************/ + + /// @inheritdoc IVaultMain + function removeLiquidity( + RemoveLiquidityParams memory params + ) + external + onlyWhenUnlocked + withInitializedPool(params.pool) + returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData) + { + // Round down when removing liquidity: + // If proportional, lower balances = lower proportional amountsOut, favoring the pool. + // If unbalanced, lower balances = lower invariant ratio without fees. + // bptIn = supply * (1 - ratio), so lower ratio = more bptIn, favoring the pool. + _ensureUnpaused(params.pool); + + // `_loadPoolDataUpdatingBalancesAndYieldFees` is non-reentrant, as it updates storage as well + // as filling in poolData in memory. Since the swap hooks are reentrant and could do anything, including + // change these balances, we cannot defer settlement until `_removeLiquidity`. + // + // Sets all fields in `poolData`. Side effects: updates `_poolTokenBalances` and + // `_aggregateFeeAmounts in storage. + PoolData memory poolData = _loadPoolDataUpdatingBalancesAndYieldFees(params.pool, Rounding.ROUND_DOWN); + InputHelpers.ensureInputLengthMatch(poolData.tokens.length, params.minAmountsOut.length); + + // Amounts are entering pool math; higher amounts would burn more BPT, so round up to favor the pool. + // Do not mutate minAmountsOut, so that we can directly compare the raw limits later, without potentially + // losing precision by scaling up and then down. + uint256[] memory minAmountsOutScaled18 = params.minAmountsOut.copyToScaled18ApplyRateRoundUpArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + + // Uses msg.sender as the Router (the contract that called the Vault). + if (poolData.poolConfigBits.shouldCallBeforeRemoveLiquidity()) { + HooksConfigLib.callBeforeRemoveLiquidityHook( + minAmountsOutScaled18, + msg.sender, + params, + poolData, + _hooksContracts[params.pool] + ); + + // The hook might alter the balances, so we need to read them again to ensure that the data is + // fresh moving forward. We also need to upscale (removing liquidity, so round down) again. + poolData.reloadBalancesAndRates(_poolTokenBalances[params.pool], Rounding.ROUND_DOWN); + + // Also update minAmountsOutScaled18, as the rates might have changed. + minAmountsOutScaled18 = params.minAmountsOut.copyToScaled18ApplyRateRoundUpArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + } + + // The bulk of the work is done here: the corresponding Pool hook is called, and the final balances + // are computed. This function is non-reentrant, as it performs the accounting updates. + // + // Note that poolData is mutated to update the Raw and Live balances, so they are accurate when passed + // into the AfterRemoveLiquidity hook. + uint256[] memory amountsOutScaled18; + (bptAmountIn, amountsOut, amountsOutScaled18, returnData) = _removeLiquidity( + poolData, + params, + minAmountsOutScaled18 + ); + + // AmountsOut can be changed by onAfterRemoveLiquidity if the hook charges fees or gives discounts. + // Uses msg.sender as the Router (the contract that called the Vault). + if (poolData.poolConfigBits.shouldCallAfterRemoveLiquidity()) { + // `hooksContract` needed to fix stack too deep. + IHooks hooksContract = _hooksContracts[params.pool]; + + amountsOut = poolData.poolConfigBits.callAfterRemoveLiquidityHook( + msg.sender, + amountsOutScaled18, + amountsOut, + bptAmountIn, + params, + poolData, + hooksContract + ); + } + } + + /** + * @dev Calls the appropriate pool hook and calculates the required inputs and outputs for the operation + * considering the given kind, and updates the Vault's internal accounting. This includes: + * - Setting pool balances + * - Supplying credit to the liquidity provider + * - Burning pool tokens + * - Emitting events + * + * It is non-reentrant, as it performs external calls and updates the Vault's state accordingly. + */ + function _removeLiquidity( + PoolData memory poolData, + RemoveLiquidityParams memory params, + uint256[] memory minAmountsOutScaled18 + ) + internal + nonReentrant + returns ( + uint256 bptAmountIn, + uint256[] memory amountsOutRaw, + uint256[] memory amountsOutScaled18, + bytes memory returnData + ) + { + LiquidityLocals memory locals; + locals.numTokens = poolData.tokens.length; + amountsOutRaw = new uint256[](locals.numTokens); + // `swapFeeAmounts` stores scaled18 amounts first, and is then reused to store raw amounts. + uint256[] memory swapFeeAmounts; + + if (params.kind == RemoveLiquidityKind.PROPORTIONAL) { + bptAmountIn = params.maxBptAmountIn; + swapFeeAmounts = new uint256[](locals.numTokens); + amountsOutScaled18 = BasePoolMath.computeProportionalAmountsOut( + poolData.balancesLiveScaled18, + _totalSupply(params.pool), + bptAmountIn + ); + + // Charge roundtrip fee. + if (_addLiquidityCalled().tGet(params.pool)) { + uint256 swapFeePercentage = poolData.poolConfigBits.getStaticSwapFeePercentage(); + for (uint256 i = 0; i < locals.numTokens; ++i) { + swapFeeAmounts[i] = amountsOutScaled18[i].mulUp(swapFeePercentage); + amountsOutScaled18[i] -= swapFeeAmounts[i]; + } + } + } else if (params.kind == RemoveLiquidityKind.SINGLE_TOKEN_EXACT_IN) { + poolData.poolConfigBits.requireUnbalancedLiquidityEnabled(); + bptAmountIn = params.maxBptAmountIn; + amountsOutScaled18 = minAmountsOutScaled18; + locals.tokenIndex = InputHelpers.getSingleInputIndex(params.minAmountsOut); + + (amountsOutScaled18[locals.tokenIndex], swapFeeAmounts) = BasePoolMath + .computeRemoveLiquiditySingleTokenExactIn( + poolData.balancesLiveScaled18, + locals.tokenIndex, + bptAmountIn, + _totalSupply(params.pool), + poolData.poolConfigBits.getStaticSwapFeePercentage(), + IBasePool(params.pool) + ); + } else if (params.kind == RemoveLiquidityKind.SINGLE_TOKEN_EXACT_OUT) { + poolData.poolConfigBits.requireUnbalancedLiquidityEnabled(); + amountsOutScaled18 = minAmountsOutScaled18; + locals.tokenIndex = InputHelpers.getSingleInputIndex(params.minAmountsOut); + amountsOutRaw[locals.tokenIndex] = params.minAmountsOut[locals.tokenIndex]; + + (bptAmountIn, swapFeeAmounts) = BasePoolMath.computeRemoveLiquiditySingleTokenExactOut( + poolData.balancesLiveScaled18, + locals.tokenIndex, + amountsOutScaled18[locals.tokenIndex], + _totalSupply(params.pool), + poolData.poolConfigBits.getStaticSwapFeePercentage(), + IBasePool(params.pool) + ); + } else if (params.kind == RemoveLiquidityKind.CUSTOM) { + poolData.poolConfigBits.requireRemoveLiquidityCustomEnabled(); + // Uses msg.sender as the Router (the contract that called the Vault). + (bptAmountIn, amountsOutScaled18, swapFeeAmounts, returnData) = IPoolLiquidity(params.pool) + .onRemoveLiquidityCustom( + msg.sender, + params.maxBptAmountIn, + minAmountsOutScaled18, + poolData.balancesLiveScaled18, + params.userData + ); + } else { + revert InvalidRemoveLiquidityKind(); + } + + if (bptAmountIn > params.maxBptAmountIn) { + revert BptAmountInAboveMax(bptAmountIn, params.maxBptAmountIn); + } + + _ensureValidTradeAmount(bptAmountIn); + + for (uint256 i = 0; i < locals.numTokens; ++i) { + uint256 amountOutRaw; + + // 1) Calculate raw amount out. + { + uint256 amountOutScaled18 = amountsOutScaled18[i]; + _ensureValidTradeAmount(amountOutScaled18); + + // If the value in memory is not set, convert scaled amount to raw. + if (amountsOutRaw[i] == 0) { + // amountsOut are amounts exiting the Pool, so we round down. + // Do not mutate in place yet, as we need them scaled for the `onAfterRemoveLiquidity` hook. + amountOutRaw = amountOutScaled18.toRawUndoRateRoundDown( + poolData.decimalScalingFactors[i], + poolData.tokenRates[i] + ); + amountsOutRaw[i] = amountOutRaw; + } else { + // Exact out requests will have the raw amount in memory already, so we use it moving forward and + // skip downscaling. + amountOutRaw = amountsOutRaw[i]; + } + } + + IERC20 token = poolData.tokens[i]; + // 2) Check limits for raw amounts. + if (amountOutRaw < params.minAmountsOut[i]) { + revert AmountOutBelowMin(token, amountOutRaw, params.minAmountsOut[i]); + } + + // 3) Deltas: Credit token[i] for amountOutRaw. + _supplyCredit(token, amountOutRaw); + + // 4) Compute and charge protocol and creator fees. + // swapFeeAmounts[i] is now raw instead of scaled. + (swapFeeAmounts[i], locals.aggregateSwapFeeAmountRaw) = _computeAndChargeAggregateSwapFees( + poolData, + swapFeeAmounts[i], + params.pool, + token, + i + ); + + // 5) Pool balances: raw and live. + // We need regular balances to complete the accounting, and the upscaled balances + // to use in the `after` hook later on. + + // A Pool's token balance always decreases after an exit (potentially by 0). + // Also adjust by protocol and pool creator fees. + poolData.updateRawAndLiveBalance( + i, + poolData.balancesRaw[i] - (amountOutRaw + locals.aggregateSwapFeeAmountRaw), + Rounding.ROUND_DOWN + ); + } + + // 6) Store pool balances, raw and live. + _writePoolBalancesToStorage(params.pool, poolData); + + // 7) BPT supply adjustment. + // Uses msg.sender as the Router (the contract that called the Vault). + _spendAllowance(address(params.pool), params.from, msg.sender, bptAmountIn); + + if (_isQueryContext()) { + // Increase `from` balance to ensure the burn function succeeds. + _queryModeBalanceIncrease(params.pool, params.from, bptAmountIn); + } + // When removing liquidity, we must burn tokens concurrently with updating pool balances, + // as the pool's math relies on totalSupply. + // Burning will be reverted if it results in a total supply less than the _POOL_MINIMUM_TOTAL_SUPPLY. + _burn(address(params.pool), params.from, bptAmountIn); + + // 8) Off-chain events + emit PoolBalanceChanged( + params.pool, + params.from, + _totalSupply(params.pool), + // We can unsafely cast to int256 because balances are stored as uint128 (see PackedTokenBalance). + amountsOutRaw.unsafeCastToInt256(false), + swapFeeAmounts + ); + } + + /** + * @dev Preconditions: poolConfigBits, decimalScalingFactors, tokenRates in `poolData`. + * Side effects: updates `_aggregateFeeAmounts` storage. + * Note that this computes the aggregate total of the protocol fees and stores it, without emitting any events. + * Splitting the fees and event emission occur during fee collection. + * Should only be called in a non-reentrant context. + * + * @return totalSwapFeeAmountRaw Total swap fees raw (LP + aggregate protocol fees) + * @return aggregateSwapFeeAmountRaw Sum of protocol and pool creator fees raw + */ + function _computeAndChargeAggregateSwapFees( + PoolData memory poolData, + uint256 totalSwapFeeAmountScaled18, + address pool, + IERC20 token, + uint256 index + ) internal returns (uint256 totalSwapFeeAmountRaw, uint256 aggregateSwapFeeAmountRaw) { + // If totalSwapFeeAmountScaled18 equals zero, no need to charge anything. + if (totalSwapFeeAmountScaled18 > 0 && poolData.poolConfigBits.isPoolInRecoveryMode() == false) { + // The total swap fee does not go into the pool; amountIn does, and the raw fee at this point does not + // modify it. Given that all of the fee may belong to the pool creator (i.e. outside pool balances), + // we round down to protect the invariant. + totalSwapFeeAmountRaw = totalSwapFeeAmountScaled18.toRawUndoRateRoundDown( + poolData.decimalScalingFactors[index], + poolData.tokenRates[index] + ); + + uint256 aggregateSwapFeePercentage = poolData.poolConfigBits.getAggregateSwapFeePercentage(); + + // We have already calculated raw total fees rounding up. + // Total fees = LP fees + aggregate fees, so by rounding aggregate fees down we round the fee split in + // the LPs' favor, in turn increasing token balances and the pool invariant. + aggregateSwapFeeAmountRaw = totalSwapFeeAmountRaw.mulDown(aggregateSwapFeePercentage); + + // Ensure we can never charge more than the total swap fee. + if (aggregateSwapFeeAmountRaw > totalSwapFeeAmountRaw) { + revert ProtocolFeesExceedTotalCollected(); + } + + // Both Swap and Yield fees are stored together in a PackedTokenBalance. + // We have designated "Raw" the derived half for Swap fee storage. + bytes32 currentPackedBalance = _aggregateFeeAmounts[pool][token]; + _aggregateFeeAmounts[pool][token] = currentPackedBalance.setBalanceRaw( + currentPackedBalance.getBalanceRaw() + aggregateSwapFeeAmountRaw + ); + } + } + + /******************************************************************************* + Pool Information + *******************************************************************************/ + + /// @inheritdoc IVaultMain + function getPoolTokenCountAndIndexOfToken( + address pool, + IERC20 token + ) external view withRegisteredPool(pool) returns (uint256, uint256) { + IERC20[] memory poolTokens = _poolTokens[pool]; + + uint256 index = _findTokenIndex(poolTokens, token); + + return (poolTokens.length, index); + } + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /// @inheritdoc IVaultMain + function erc4626BufferWrapOrUnwrap( + BufferWrapOrUnwrapParams memory params + ) + external + onlyWhenUnlocked + whenVaultBuffersAreNotPaused + withInitializedBuffer(params.wrappedToken) + nonReentrant + returns (uint256 amountCalculatedRaw, uint256 amountInRaw, uint256 amountOutRaw) + { + IERC20 underlyingToken = IERC20(params.wrappedToken.asset()); + _ensureCorrectBufferAsset(params.wrappedToken, address(underlyingToken)); + + if (params.amountGivenRaw < _MINIMUM_WRAP_AMOUNT) { + // If amount given is too small, rounding issues can be introduced that favors the user and can drain + // the buffer. _MINIMUM_WRAP_AMOUNT prevents it. Most tokens have protections against it already, this + // is just an extra layer of security. + revert WrapAmountTooSmall(params.wrappedToken); + } + + if (params.direction == WrappingDirection.UNWRAP) { + (amountInRaw, amountOutRaw) = _unwrapWithBuffer( + params.kind, + underlyingToken, + params.wrappedToken, + params.amountGivenRaw + ); + emit Unwrap(params.wrappedToken, underlyingToken, amountInRaw, amountOutRaw); + } else { + (amountInRaw, amountOutRaw) = _wrapWithBuffer( + params.kind, + underlyingToken, + params.wrappedToken, + params.amountGivenRaw + ); + emit Wrap(underlyingToken, params.wrappedToken, amountInRaw, amountOutRaw); + } + + if (params.kind == SwapKind.EXACT_IN) { + if (amountOutRaw < params.limitRaw) { + revert SwapLimit(amountOutRaw, params.limitRaw); + } + amountCalculatedRaw = amountOutRaw; + } else { + if (amountInRaw > params.limitRaw) { + revert SwapLimit(amountInRaw, params.limitRaw); + } + amountCalculatedRaw = amountInRaw; + } + } + + /** + * @dev If the buffer has enough liquidity, it uses the internal ERC4626 buffer to perform the wrap + * operation without any external calls. If not, it wraps the assets needed to fulfill the trade + the imbalance + * of assets in the buffer, so that the buffer is rebalanced at the end of the operation. + * + * Updates `_reservesOf` and token deltas in storage. + */ + function _wrapWithBuffer( + SwapKind kind, + IERC20 underlyingToken, + IERC4626 wrappedToken, + uint256 amountGiven + ) private returns (uint256 amountInUnderlying, uint256 amountOutWrapped) { + if (kind == SwapKind.EXACT_IN) { + // EXACT_IN wrap, so AmountGiven is an underlying amount. `deposit` is the ERC4626 operation that receives + // an underlying amount in and calculates the wrapped amount out with the correct rounding. + (amountInUnderlying, amountOutWrapped) = (amountGiven, wrappedToken.previewDeposit(amountGiven)); + } else { + // EXACT_OUT wrap, so AmountGiven is a wrapped amount. `mint` is the ERC4626 operation that receives a + // wrapped amount out and calculates the underlying amount in with the correct rounding. + (amountInUnderlying, amountOutWrapped) = (wrappedToken.previewMint(amountGiven), amountGiven); + } + + // If it's a query, the Vault may not have enough underlying tokens to wrap. Since in a query we do not expect + // the sender to pay for underlying tokens to wrap upfront, return the calculated amount without checking for + // the imbalance. + if (_isQueryContext()) { + return (amountInUnderlying, amountOutWrapped); + } + + bytes32 bufferBalances = _bufferTokenBalances[wrappedToken]; + + if (bufferBalances.getBalanceDerived() >= amountOutWrapped) { + // The buffer has enough liquidity to facilitate the wrap without making an external call. + uint256 newDerivedBalance; + unchecked { + // We have verified above that this is safe to do unchecked. + newDerivedBalance = bufferBalances.getBalanceDerived() - amountOutWrapped; + } + + bufferBalances = PackedTokenBalance.toPackedBalance( + bufferBalances.getBalanceRaw() + amountInUnderlying, + newDerivedBalance + ); + _bufferTokenBalances[wrappedToken] = bufferBalances; + } else { + // The buffer does not have enough liquidity to facilitate the wrap without making an external call. + // We wrap the user's tokens via an external call and additionally rebalance the buffer if it has an + // imbalance of underlying tokens. + + // Expected amount of underlying deposited into the wrapper protocol. + uint256 vaultUnderlyingDeltaHint; + // Expected amount of wrapped minted by the wrapper protocol. + uint256 vaultWrappedDeltaHint; + + if (kind == SwapKind.EXACT_IN) { + // EXACT_IN requires the exact amount of underlying tokens to be deposited, so we call deposit. + // The amount of underlying tokens to deposit is the necessary amount to fulfill the trade + // (amountInUnderlying), plus the amount needed to leave the buffer rebalanced 50/50 at the end + // (bufferUnderlyingImbalance). `bufferUnderlyingImbalance` may be positive if the buffer has an excess + // of underlying, or negative if the buffer has an excess of wrapped tokens. `vaultUnderlyingDeltaHint` + // will always be a positive number, because if `abs(bufferUnderlyingImbalance) > amountInUnderlying` + // and `bufferUnderlyingImbalance < 0`, it means the buffer has enough liquidity to fulfill the trade + // (i.e. `bufferBalances.getBalanceDerived() >= amountOutWrapped`). + int256 bufferUnderlyingImbalance = bufferBalances.getBufferUnderlyingImbalance(wrappedToken); + vaultUnderlyingDeltaHint = (amountInUnderlying.toInt256() + bufferUnderlyingImbalance).toUint256(); + underlyingToken.forceApprove(address(wrappedToken), vaultUnderlyingDeltaHint); + vaultWrappedDeltaHint = wrappedToken.deposit(vaultUnderlyingDeltaHint, address(this)); + } else { + // EXACT_OUT requires the exact amount of wrapped tokens to be minted, so we call mint. + // The amount of wrapped tokens to mint is the amount necessary to fulfill the trade + // (amountOutWrapped), minus the excess amount of wrapped tokens in the buffer (bufferWrappedImbalance). + // `bufferWrappedImbalance` may be positive if buffer has an excess of wrapped assets or negative if + // the buffer has an excess of underlying assets. `vaultWrappedDeltaHint` will always be a positive + // number, because if `abs(bufferWrappedImbalance) > amountOutWrapped` and `bufferWrappedImbalance > 0`, + // it means the buffer has enough liquidity to fulfill the trade + // (i.e. `bufferBalances.getBalanceDerived() >= amountOutWrapped`). + int256 bufferWrappedImbalance = bufferBalances.getBufferWrappedImbalance(wrappedToken); + vaultWrappedDeltaHint = (amountOutWrapped.toInt256() - bufferWrappedImbalance).toUint256(); + + // For Wrap ExactOut, we also need to calculate `vaultUnderlyingDeltaHint` before the mint operation, + // to approve the transfer of underlying tokens to the wrapper protocol. + vaultUnderlyingDeltaHint = wrappedToken.previewMint(vaultWrappedDeltaHint); + + // The mint operation returns exactly `vaultWrappedDeltaHint` shares. To do so, it withdraws underlying + // tokens from the Vault and returns the shares. So, the Vault needs to approve the transfer of + // underlying tokens to the wrapper. + underlyingToken.forceApprove(address(wrappedToken), vaultUnderlyingDeltaHint); + + vaultUnderlyingDeltaHint = wrappedToken.mint(vaultWrappedDeltaHint, address(this)); + } + + // Remove approval, in case deposit/mint consumed less tokens than we approved. + // E.g., A malicious wrapper could not consume all of the underlying tokens and use the Vault approval to + // drain the Vault. + underlyingToken.forceApprove(address(wrappedToken), 0); + + // Check if the Vault's underlying balance decreased by `vaultUnderlyingDeltaHint` and the Vault's + // wrapped balance increased by `vaultWrappedDeltaHint`. If not, it reverts. + _settleWrap(underlyingToken, IERC20(wrappedToken), vaultUnderlyingDeltaHint, vaultWrappedDeltaHint); + + // In a wrap operation, the buffer underlying balance increases by `amountInUnderlying` (the amount that + // the caller deposited into the buffer) and decreases by `vaultUnderlyingDeltaHint` (the amount of + // underlying deposited by the buffer into the wrapper protocol). Conversely, the buffer wrapped balance + // decreases by `amountOutWrapped` (the amount of wrapped tokens that the buffer returned to the caller) + // and increases by `vaultWrappedDeltaHint` (the amount of wrapped tokens minted by the wrapper protocol). + bufferBalances = PackedTokenBalance.toPackedBalance( + bufferBalances.getBalanceRaw() + amountInUnderlying - vaultUnderlyingDeltaHint, + bufferBalances.getBalanceDerived() + vaultWrappedDeltaHint - amountOutWrapped + ); + _bufferTokenBalances[wrappedToken] = bufferBalances; + } + + _takeDebt(underlyingToken, amountInUnderlying); + _supplyCredit(wrappedToken, amountOutWrapped); + } + + /** + * @dev If the buffer has enough liquidity, it uses the internal ERC4626 buffer to perform the unwrap + * operation without any external calls. If not, it unwraps the assets needed to fulfill the trade + the imbalance + * of assets in the buffer, so that the buffer is rebalanced at the end of the operation. + * + * Updates `_reservesOf` and token deltas in storage. + */ + function _unwrapWithBuffer( + SwapKind kind, + IERC20 underlyingToken, + IERC4626 wrappedToken, + uint256 amountGiven + ) private returns (uint256 amountInWrapped, uint256 amountOutUnderlying) { + if (kind == SwapKind.EXACT_IN) { + // EXACT_IN unwrap, so AmountGiven is a wrapped amount. `redeem` is the ERC4626 operation that receives a + // wrapped amount in and calculates the underlying amount out with the correct rounding. + (amountInWrapped, amountOutUnderlying) = (amountGiven, wrappedToken.previewRedeem(amountGiven)); + } else { + // EXACT_OUT unwrap, so AmountGiven is an underlying amount. `withdraw` is the ERC4626 operation that + // receives an underlying amount out and calculates the wrapped amount in with the correct rounding. + (amountInWrapped, amountOutUnderlying) = (wrappedToken.previewWithdraw(amountGiven), amountGiven); + } + + // If it's a query, the Vault may not have enough wrapped tokens to unwrap. Since in a query we do not expect + // the sender to pay for wrapped tokens to unwrap upfront, return the calculated amount without checking for + // the imbalance. + if (_isQueryContext()) { + return (amountInWrapped, amountOutUnderlying); + } + + bytes32 bufferBalances = _bufferTokenBalances[wrappedToken]; + + if (bufferBalances.getBalanceRaw() >= amountOutUnderlying) { + // The buffer has enough liquidity to facilitate the wrap without making an external call. + uint256 newRawBalance; + unchecked { + // We have verified above that this is safe to do unchecked. + newRawBalance = bufferBalances.getBalanceRaw() - amountOutUnderlying; + } + bufferBalances = PackedTokenBalance.toPackedBalance( + newRawBalance, + bufferBalances.getBalanceDerived() + amountInWrapped + ); + _bufferTokenBalances[wrappedToken] = bufferBalances; + } else { + // The buffer does not have enough liquidity to facilitate the unwrap without making an external call. + // We unwrap the user's tokens via an external call and additionally rebalance the buffer if it has an + // imbalance of wrapped tokens. + + // Expected amount of underlying withdrawn from the wrapper protocol. + uint256 vaultUnderlyingDeltaHint; + // Expected amount of wrapped burned by the wrapper protocol. + uint256 vaultWrappedDeltaHint; + + if (kind == SwapKind.EXACT_IN) { + // EXACT_IN requires the exact amount of wrapped tokens to be unwrapped, so we call redeem. The amount + // of wrapped tokens to redeem is the amount necessary to fulfill the trade (amountInWrapped), plus the + // amount needed to leave the buffer rebalanced 50/50 at the end (bufferWrappedImbalance). + // `bufferWrappedImbalance` may be positive if the buffer has an excess of wrapped, or negative if the + // buffer has an excess of underlying tokens. `vaultWrappedDeltaHint` will always be a positive number, + // because if `abs(bufferWrappedImbalance) > amountInWrapped` and `bufferWrappedImbalance < 0`, it + // means the buffer has enough liquidity to fulfill the trade + // (i.e. `bufferBalances.getBalanceRaw() >= amountOutUnderlying`). + int256 bufferWrappedImbalance = bufferBalances.getBufferWrappedImbalance(wrappedToken); + vaultWrappedDeltaHint = (amountInWrapped.toInt256() + bufferWrappedImbalance).toUint256(); + vaultUnderlyingDeltaHint = wrappedToken.redeem(vaultWrappedDeltaHint, address(this), address(this)); + } else { + // EXACT_OUT requires the exact amount of underlying tokens to be returned, so we call withdraw. + // The amount of underlying tokens to withdraw is the amount necessary to fulfill the trade + // (amountOutUnderlying), minus the excess amount of underlying assets in the buffer + // (bufferUnderlyingImbalance). `bufferUnderlyingImbalance` may be positive if the buffer has an excess + // of underlying, or negative if the buffer has an excess of wrapped tokens. `vaultUnderlyingDeltaHint` + // will always be a positive number, because if `abs(bufferUnderlyingImbalance) > amountOutUnderlying` + // and `bufferUnderlyingImbalance > 0`, it means the buffer has enough liquidity to fulfill the trade + // (i.e. `bufferBalances.getBalanceRaw() >= amountOutUnderlying`). + int256 bufferUnderlyingImbalance = bufferBalances.getBufferUnderlyingImbalance(wrappedToken); + vaultUnderlyingDeltaHint = (amountOutUnderlying.toInt256() - bufferUnderlyingImbalance).toUint256(); + vaultWrappedDeltaHint = wrappedToken.withdraw(vaultUnderlyingDeltaHint, address(this), address(this)); + } + + // Check if the Vault's underlying balance increased by `vaultUnderlyingDeltaHint` and the Vault's + // wrapped balance decreased by `vaultWrappedDeltaHint`. If not, it reverts. + _settleUnwrap(underlyingToken, IERC20(wrappedToken), vaultUnderlyingDeltaHint, vaultWrappedDeltaHint); + + // In an unwrap operation, the buffer underlying balance increases by `vaultUnderlyingDeltaHint` (the + // amount of underlying withdrawn by the buffer from the wrapper protocol) and decreases by + // `amountOutUnderlying` (the amount of underlying assets that the caller withdrawn from the buffer). + // Conversely, the buffer wrapped balance increases by `amountInWrapped` (the amount of wrapped tokens that + // the caller sent to the buffer) and decreases by `vaultWrappedDeltaHint` (the amount of wrapped tokens + // burned by the wrapper protocol). + bufferBalances = PackedTokenBalance.toPackedBalance( + bufferBalances.getBalanceRaw() + vaultUnderlyingDeltaHint - amountOutUnderlying, + bufferBalances.getBalanceDerived() + amountInWrapped - vaultWrappedDeltaHint + ); + _bufferTokenBalances[wrappedToken] = bufferBalances; + } + + _takeDebt(wrappedToken, amountInWrapped); + _supplyCredit(underlyingToken, amountOutUnderlying); + } + + function _isQueryContext() internal view returns (bool) { + return EVMCallModeHelpers.isStaticCall() && _vaultStateBits.isQueryDisabled() == false; + } + + /** + * @notice Updates the reserves of the Vault after an ERC4626 wrap (deposit/mint) operation. + * @dev If there are extra tokens in the Vault balances, these will be added to the reserves (which, in practice, + * is equal to discarding such tokens). This approach avoids DoS attacks, when a frontrunner leaves vault balances + * and reserves out of sync before a transaction starts. + * + * @param underlyingToken Underlying token of the ERC4626 wrapped token + * @param wrappedToken ERC4626 wrapped token + * @param underlyingDeltaHint Amount of underlying tokens the wrapper should have removed from the Vault + * @param wrappedDeltaHint Amount of wrapped tokens the wrapper should have added to the Vault + */ + function _settleWrap( + IERC20 underlyingToken, + IERC20 wrappedToken, + uint256 underlyingDeltaHint, + uint256 wrappedDeltaHint + ) internal { + // A wrap operation removes underlying tokens from the Vault, so the Vault's expected underlying balance after + // the operation is `underlyingReservesBefore - underlyingDeltaHint`. + uint256 expectedUnderlyingReservesAfter = _reservesOf[underlyingToken] - underlyingDeltaHint; + + // A wrap operation adds wrapped tokens to the Vault, so the Vault's expected wrapped balance after the + // operation is `wrappedReservesBefore + wrappedDeltaHint`. + uint256 expectedWrappedReservesAfter = _reservesOf[wrappedToken] + wrappedDeltaHint; + + _settleWrapUnwrap(underlyingToken, wrappedToken, expectedUnderlyingReservesAfter, expectedWrappedReservesAfter); + } + + /** + * @notice Updates the reserves of the Vault after an ERC4626 unwrap (withdraw/redeem) operation. + * @dev If there are extra tokens in the Vault balances, these will be added to the reserves (which, in practice, + * is equal to discarding such tokens). This approach avoids DoS attacks, when a frontrunner leaves vault balances + * and state of reserves out of sync before a transaction starts. + * + * @param underlyingToken Underlying of ERC4626 wrapped token + * @param wrappedToken ERC4626 wrapped token + * @param underlyingDeltaHint Amount of underlying tokens supposedly added to the Vault + * @param wrappedDeltaHint Amount of wrapped tokens supposedly removed from the Vault + */ + function _settleUnwrap( + IERC20 underlyingToken, + IERC20 wrappedToken, + uint256 underlyingDeltaHint, + uint256 wrappedDeltaHint + ) internal { + // An unwrap operation adds underlying tokens to the Vault, so the Vault's expected underlying balance after + // the operation is `underlyingReservesBefore + underlyingDeltaHint`. + uint256 expectedUnderlyingReservesAfter = _reservesOf[underlyingToken] + underlyingDeltaHint; + + // An unwrap operation removes wrapped tokens from the Vault, so the Vault's expected wrapped balance after the + // operation is `wrappedReservesBefore - wrappedDeltaHint`. + uint256 expectedWrappedReservesAfter = _reservesOf[wrappedToken] - wrappedDeltaHint; + + _settleWrapUnwrap(underlyingToken, wrappedToken, expectedUnderlyingReservesAfter, expectedWrappedReservesAfter); + } + + /** + * @notice Updates the reserves of the Vault after an ERC4626 wrap/unwrap operation. + * @dev If reserves of underlying or wrapped tokens are bigger than expected, the extra tokens will be discarded, + * which avoids a possible DoS. However, if reserves are smaller than expected, it means that the wrapper didn't + * respect the amount given and/or the amount calculated (informed by the wrapper operation and stored as a hint + * variable), so the token is not ERC4626 compliant and the function should be reverted. + * + * @param underlyingToken Underlying of ERC4626 wrapped token + * @param wrappedToken ERC4626 wrapped token + * @param expectedUnderlyingReservesAfter Vault's expected reserves of underlying after the wrap/unwrap operation + * @param expectedWrappedReservesAfter Vault's expected reserves of wrapped after the wrap/unwrap operation + */ + function _settleWrapUnwrap( + IERC20 underlyingToken, + IERC20 wrappedToken, + uint256 expectedUnderlyingReservesAfter, + uint256 expectedWrappedReservesAfter + ) private { + // Update the Vault's underlying reserves. + uint256 underlyingBalancesAfter = underlyingToken.balanceOf(address(this)); + if (underlyingBalancesAfter < expectedUnderlyingReservesAfter) { + // If Vault's underlying balance is smaller than expected, the Vault was drained and the operation should + // revert. It may happen in different ways, depending on the wrap/unwrap operation: + // * deposit: the wrapper didn't respect the exact amount in of underlying; + // * mint: the underlying amount subtracted from the Vault is bigger than wrapper's calculated amount in; + // * withdraw: the wrapper didn't respect the exact amount out of underlying; + // * redeem: the underlying amount added to the Vault is smaller than wrapper's calculated amount out. + revert NotEnoughUnderlying( + IERC4626(address(wrappedToken)), + expectedUnderlyingReservesAfter, + underlyingBalancesAfter + ); + } + // Update the Vault's underlying reserves, discarding any unexpected imbalance of tokens (difference between + // actual and expected vault balance). + _reservesOf[underlyingToken] = underlyingBalancesAfter; + + // Update the Vault's wrapped reserves. + uint256 wrappedBalancesAfter = wrappedToken.balanceOf(address(this)); + if (wrappedBalancesAfter < expectedWrappedReservesAfter) { + // If the Vault's wrapped balance is smaller than expected, the Vault was drained and the operation should + // revert. It may happen in different ways, depending on the wrap/unwrap operation: + // * deposit: the wrapped amount added to the Vault is smaller than wrapper's calculated amount out; + // * mint: the wrapper didn't respect the exact amount out of wrapped; + // * withdraw: the wrapped amount subtracted from the Vault is bigger than wrapper's calculated amount in; + // * redeem: the wrapper didn't respect the exact amount in of wrapped. + revert NotEnoughWrapped( + IERC4626(address(wrappedToken)), + expectedWrappedReservesAfter, + wrappedBalancesAfter + ); + } + // Update the Vault's wrapped reserves, discarding any unexpected surplus of tokens (difference between + // the Vault's actual and expected balances). + _reservesOf[wrappedToken] = wrappedBalancesAfter; + } + + // Minimum token value in or out (applied to scaled18 values), enforced as a security measure to block potential + // exploitation of rounding errors. This is called in the context of adding or removing liquidity, so zero is + // allowed to support single-token operations. + function _ensureValidTradeAmount(uint256 tradeAmount) internal view { + if (tradeAmount != 0) { + _ensureValidSwapAmount(tradeAmount); + } + } + + // Minimum token value in or out (applied to scaled18 values), enforced as a security measure to block potential + // exploitation of rounding errors. This is called in the swap context, so zero is not a valid amount. + function _ensureValidSwapAmount(uint256 tradeAmount) internal view { + if (tradeAmount < _MINIMUM_TRADE_AMOUNT) { + revert TradeAmountTooSmall(); + } + } + + /******************************************************************************* + Authentication + *******************************************************************************/ + + /// @inheritdoc IVaultMain + function getAuthorizer() external view returns (IAuthorizer) { + return _authorizer; + } + + /******************************************************************************* + Default handlers + *******************************************************************************/ + + receive() external payable { + revert CannotReceiveEth(); + } + + // solhint-disable no-complex-fallback + + /** + * @inheritdoc Proxy + * @dev Override proxy implementation of `fallback` to disallow incoming ETH transfers. + * This function actually returns whatever the VaultExtension does when handling the request. + */ + fallback() external payable override { + if (msg.value > 0) { + revert CannotReceiveEth(); + } + + _fallback(); + } + + /******************************************************************************* + Miscellaneous + *******************************************************************************/ + + /// @inheritdoc IVaultMain + function getVaultExtension() external view returns (address) { + return _implementation(); + } + + /** + * @inheritdoc Proxy + * @dev Returns the VaultExtension contract, to which fallback requests are forwarded. + */ + function _implementation() internal view override returns (address) { + return address(_vaultExtension); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultCommon.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultCommon.sol new file mode 100644 index 00000000..1ef109f1 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultCommon.sol @@ -0,0 +1,414 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { IVaultErrors } from "./interfaces/IVaultErrors.sol"; +import { IVaultEvents } from "./interfaces/IVaultEvents.sol"; +import { ISwapFeePercentageBounds } from "./interfaces/ISwapFeePercentageBounds.sol"; +import { PoolData, Rounding } from "./interfaces/VaultTypes.sol"; + +import { ScalingHelpers } from "./interfaces/ScalingHelpers.sol"; +import { + TransientStorageHelpers +} from "./interfaces/TransientStorageHelpers.sol"; +import { StorageSlotExtension } from "./interfaces/StorageSlotExtension.sol"; +import { + ReentrancyGuardTransient +} from "./interfaces/ReentrancyGuardTransient.sol"; +import { PackedTokenBalance } from "./interfaces/PackedTokenBalance.sol"; + +import { VaultStateBits, VaultStateLib } from "./lib/VaultStateLib.sol"; +import { PoolConfigBits, PoolConfigLib } from "./lib/PoolConfigLib.sol"; +import { VaultStorage } from "./VaultStorage.sol"; +import { ERC20MultiToken } from "./token/ERC20MultiToken.sol"; +import { PoolDataLib } from "./lib/PoolDataLib.sol"; + +/** + * @notice Functions and modifiers shared between the main Vault and its extension contracts. + * @dev This contract contains common utilities in the inheritance chain that require storage to work, + * and will be required in both the main Vault and its extensions. + */ +abstract contract VaultCommon is IVaultEvents, IVaultErrors, VaultStorage, ReentrancyGuardTransient, ERC20MultiToken { + using PoolConfigLib for PoolConfigBits; + using VaultStateLib for VaultStateBits; + using SafeCast for *; + using TransientStorageHelpers for *; + using StorageSlotExtension for *; + using PoolDataLib for PoolData; + + /******************************************************************************* + Transient Accounting + *******************************************************************************/ + + /** + * @dev This modifier ensures that the function it modifies can only be called + * when a tab has been opened. + */ + modifier onlyWhenUnlocked() { + _ensureUnlocked(); + _; + } + + function _ensureUnlocked() internal view { + if (_isUnlocked().tload() == false) { + revert VaultIsNotUnlocked(); + } + } + + /** + * @notice Expose the state of the Vault's reentrancy guard. + * @return True if the Vault is currently executing a nonReentrant function + */ + function reentrancyGuardEntered() public view returns (bool) { + return _reentrancyGuardEntered(); + } + + /** + * @notice Records the `credit` for a given token. + * @param token The ERC20 token for which the 'credit' will be accounted + * @param credit The amount of `token` supplied to the Vault in favor of the caller + */ + function _supplyCredit(IERC20 token, uint256 credit) internal { + _accountDelta(token, -credit.toInt256()); + } + + /** + * @notice Records the `debt` for a given token. + * @param token The ERC20 token for which the `debt` will be accounted + * @param debt The amount of `token` taken from the Vault in favor of the caller + */ + function _takeDebt(IERC20 token, uint256 debt) internal { + _accountDelta(token, debt.toInt256()); + } + + /** + * @dev Accounts the delta for the given token. A positive delta represents debt, + * while a negative delta represents surplus. + * + * @param token The ERC20 token for which the delta is being accounted + * @param delta The difference in the token balance + * Positive indicates a debit or a decrease in Vault's tokens, + * negative indicates a credit or an increase in Vault's tokens. + */ + function _accountDelta(IERC20 token, int256 delta) internal { + // If the delta is zero, there's nothing to account for. + if (delta == 0) return; + + // Get the current recorded delta for this token. + int256 current = _tokenDeltas().tGet(token); + + // Calculate the new delta after accounting for the change. + int256 next = current + delta; + + if (next == 0) { + // If the resultant delta becomes zero after this operation, + // decrease the count of non-zero deltas. + _nonZeroDeltaCount().tDecrement(); + } else if (current == 0) { + // If there was no previous delta (i.e., it was zero) and now we have one, + // increase the count of non-zero deltas. + _nonZeroDeltaCount().tIncrement(); + } + + // Update the delta for this token. + _tokenDeltas().tSet(token, next); + } + + /******************************************************************************* + Vault Pausing + *******************************************************************************/ + + /// @dev Modifier to make a function callable only when the Vault is not paused. + modifier whenVaultNotPaused() { + _ensureVaultNotPaused(); + _; + } + + /// @dev Reverts if the Vault is paused. + function _ensureVaultNotPaused() internal view { + if (_isVaultPaused()) { + revert VaultPaused(); + } + } + + /// @dev Reverts if the Vault or the given pool are paused. + function _ensureUnpaused(address pool) internal view { + _ensureVaultNotPaused(); + _ensurePoolNotPaused(pool); + } + + /** + * @dev For gas efficiency, storage is only read before `_vaultBufferPeriodEndTime`. Once we're past that + * timestamp, the expression short-circuits false, and the Vault is permanently unpaused. + */ + function _isVaultPaused() internal view returns (bool) { + // solhint-disable-next-line not-rely-on-time + return block.timestamp <= _vaultBufferPeriodEndTime && _vaultStateBits.isVaultPaused(); + } + + /******************************************************************************* + Pool Pausing + *******************************************************************************/ + + /** + * @dev Reverts if the pool is paused. + * @param pool The pool + */ + function _ensurePoolNotPaused(address pool) internal view { + if (_isPoolPaused(pool)) { + revert PoolPaused(pool); + } + } + + /// @dev Check both the flag and timestamp to determine whether the pool is paused. + function _isPoolPaused(address pool) internal view returns (bool) { + (bool paused, ) = _getPoolPausedState(pool); + + return paused; + } + + /// @dev Lowest level routine that plucks only the minimum necessary parts from storage. + function _getPoolPausedState(address pool) internal view returns (bool, uint32) { + PoolConfigBits config = _poolConfigBits[pool]; + + bool isPoolPaused = config.isPoolPaused(); + uint32 pauseWindowEndTime = config.getPauseWindowEndTime(); + + // Use the Vault's buffer period. + // solhint-disable-next-line not-rely-on-time + return (isPoolPaused && block.timestamp <= pauseWindowEndTime + _vaultBufferPeriodDuration, pauseWindowEndTime); + } + + /******************************************************************************* + Buffer Pausing + *******************************************************************************/ + /// @dev Modifier to make a function callable only when vault buffers are not paused. + modifier whenVaultBuffersAreNotPaused() { + _ensureVaultBuffersAreNotPaused(); + _; + } + + /// @dev Reverts if vault buffers are paused. + function _ensureVaultBuffersAreNotPaused() internal view { + if (_vaultStateBits.areBuffersPaused()) { + revert VaultBuffersArePaused(); + } + } + + /******************************************************************************* + Pool Registration and Initialization + *******************************************************************************/ + + /// @dev Reverts unless `pool` is a registered Pool. + modifier withRegisteredPool(address pool) { + _ensureRegisteredPool(pool); + _; + } + + /// @dev Reverts unless `pool` is an initialized Pool. + modifier withInitializedPool(address pool) { + _ensureInitializedPool(pool); + _; + } + + function _ensureRegisteredPool(address pool) internal view { + if (!_isPoolRegistered(pool)) { + revert PoolNotRegistered(pool); + } + } + + /// @dev See `isPoolRegistered` + function _isPoolRegistered(address pool) internal view returns (bool) { + PoolConfigBits config = _poolConfigBits[pool]; + return config.isPoolRegistered(); + } + + function _ensureInitializedPool(address pool) internal view { + if (!_isPoolInitialized(pool)) { + revert PoolNotInitialized(pool); + } + } + + /// @dev See `isPoolInitialized` + function _isPoolInitialized(address pool) internal view returns (bool) { + PoolConfigBits config = _poolConfigBits[pool]; + return config.isPoolInitialized(); + } + + /******************************************************************************* + Buffer Initialization & Validation + *******************************************************************************/ + + modifier withInitializedBuffer(IERC4626 wrappedToken) { + _ensureBufferInitialized(wrappedToken); + _; + } + + function _ensureBufferInitialized(IERC4626 wrappedToken) internal view { + if (_bufferAssets[wrappedToken] == address(0)) { + revert BufferNotInitialized(wrappedToken); + } + } + + /** + * @dev This assumes `underlyingToken` is non-zero; should be called by functions that have already ensured the + * buffer has been initialized (e.g., those protected by `withInitializedBuffer`). + */ + function _ensureCorrectBufferAsset(IERC4626 wrappedToken, address underlyingToken) internal view { + if (_bufferAssets[wrappedToken] != underlyingToken) { + // Asset was changed since the buffer was initialized. + revert WrongUnderlyingToken(wrappedToken, underlyingToken); + } + } + + /******************************************************************************* + Pool Information + *******************************************************************************/ + + /** + * @dev Packs and sets the raw and live balances of a Pool's tokens to the current values in poolData.balancesRaw + * and poolData.liveBalances in the same storage slot. + */ + function _writePoolBalancesToStorage(address pool, PoolData memory poolData) internal { + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolBalances = _poolTokenBalances[pool]; + + for (uint256 i = 0; i < poolData.balancesRaw.length; ++i) { + // Since we assume all newBalances are properly ordered + poolBalances[i] = PackedTokenBalance.toPackedBalance( + poolData.balancesRaw[i], + poolData.balancesLiveScaled18[i] + ); + } + } + + /** + * @dev Fill in PoolData, including paying protocol yield fees and computing final raw and live balances. + * In normal operation, we update both balances and fees together. However, while Recovery Mode is enabled, + * we cannot track yield fees, as that would involve making external calls that could fail and block withdrawals. + * + * Therefore, disabling Recovery Mode requires writing *only* the balances to storage, so we still need this + * as a separate function. It is normally called by `_loadPoolDataUpdatingBalancesAndYieldFees`, but in the + * Recovery Mode special case, it is called separately, with the result passed into `_writePoolBalancesToStorage`. + */ + function _loadPoolData(address pool, Rounding roundingDirection) internal view returns (PoolData memory poolData) { + poolData.load( + _poolTokenBalances[pool], + _poolConfigBits[pool], + _poolTokenInfo[pool], + _poolTokens[pool], + roundingDirection + ); + } + + /** + * @dev Fill in PoolData, including paying protocol yield fees and computing final raw and live balances. + * This function modifies protocol fees and balance storage. Out of an abundance of caution, since `_loadPoolData` + * makes external calls, we are making anything that calls it and then modifies storage non-reentrant. + * Side effects: updates `_aggregateFeeAmounts` and `_poolTokenBalances` in storage. + */ + function _loadPoolDataUpdatingBalancesAndYieldFees( + address pool, + Rounding roundingDirection + ) internal nonReentrant returns (PoolData memory poolData) { + // Initialize poolData with base information for subsequent calculations. + poolData.load( + _poolTokenBalances[pool], + _poolConfigBits[pool], + _poolTokenInfo[pool], + _poolTokens[pool], + roundingDirection + ); + + PoolDataLib.syncPoolBalancesAndFees(poolData, _poolTokenBalances[pool], _aggregateFeeAmounts[pool]); + } + + /** + * @dev Updates the raw and live balance of a given token in poolData, scaling the given raw balance by both decimal + * and token rates, and rounding the result in the given direction. Assumes scaling factors and rates are current + * in PoolData. + */ + function _updateRawAndLiveTokenBalancesInPoolData( + PoolData memory poolData, + uint256 newRawBalance, + Rounding roundingDirection, + uint256 tokenIndex + ) internal pure returns (uint256) { + poolData.balancesRaw[tokenIndex] = newRawBalance; + + function(uint256, uint256, uint256) internal pure returns (uint256) _upOrDown = roundingDirection == + Rounding.ROUND_UP + ? ScalingHelpers.toScaled18ApplyRateRoundUp + : ScalingHelpers.toScaled18ApplyRateRoundDown; + + poolData.balancesLiveScaled18[tokenIndex] = _upOrDown( + newRawBalance, + poolData.decimalScalingFactors[tokenIndex], + poolData.tokenRates[tokenIndex] + ); + + return _upOrDown(newRawBalance, poolData.decimalScalingFactors[tokenIndex], poolData.tokenRates[tokenIndex]); + } + + function _setStaticSwapFeePercentage(address pool, uint256 swapFeePercentage) internal { + // These cannot be called during pool construction. Pools must be deployed first, then registered. + if (swapFeePercentage < ISwapFeePercentageBounds(pool).getMinimumSwapFeePercentage()) { + revert SwapFeePercentageTooLow(); + } + + if (swapFeePercentage > ISwapFeePercentageBounds(pool).getMaximumSwapFeePercentage()) { + revert SwapFeePercentageTooHigh(); + } + + // The library also checks that the percentage is <= FP(1), regardless of what the pool defines. + _poolConfigBits[pool] = _poolConfigBits[pool].setStaticSwapFeePercentage(swapFeePercentage); + + emit SwapFeePercentageChanged(pool, swapFeePercentage); + } + + /// @dev Find the index of a token in a token array. Reverts if not found. + function _findTokenIndex(IERC20[] memory tokens, IERC20 token) internal pure returns (uint256) { + for (uint256 i = 0; i < tokens.length; i++) { + if (tokens[i] == token) { + return i; + } + } + + revert TokenNotRegistered(token); + } + + /******************************************************************************* + Recovery Mode + *******************************************************************************/ + + /** + * @dev Place on functions that may only be called when the associated pool is in recovery mode. + * @param pool The pool + */ + modifier onlyInRecoveryMode(address pool) { + _ensurePoolInRecoveryMode(pool); + _; + } + + /** + * @dev Reverts if the pool is not in recovery mode. + * @param pool The pool + */ + function _ensurePoolInRecoveryMode(address pool) internal view { + if (!_isPoolInRecoveryMode(pool)) { + revert PoolNotInRecoveryMode(pool); + } + } + + /** + * @notice Checks whether a pool is in recovery mode. + * @param pool Address of the pool to check + * @return True if the pool is initialized, false otherwise + */ + function _isPoolInRecoveryMode(address pool) internal view returns (bool) { + return _poolConfigBits[pool].isPoolInRecoveryMode(); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultStorage.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultStorage.sol new file mode 100644 index 00000000..cf80810c --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultStorage.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IAuthorizer } from "./interfaces/IAuthorizer.sol"; +import { IHooks } from "./interfaces/IHooks.sol"; +import { IVaultExtension } from "./interfaces/IVaultExtension.sol"; +import { IProtocolFeeController } from "./interfaces/IProtocolFeeController.sol"; +import "./interfaces/VaultTypes.sol"; + +import { StorageSlotExtension } from "./interfaces/StorageSlotExtension.sol"; +import { + TransientStorageHelpers, + TokenDeltaMappingSlotType, + AddressToBooleanMappingSlot +} from "./interfaces/TransientStorageHelpers.sol"; + +import { VaultStateBits } from "./lib/VaultStateLib.sol"; +import { PoolConfigBits } from "./lib/PoolConfigLib.sol"; + +// solhint-disable max-states-count + +/** + * @notice Storage layout for the Vault. + * @dev This contract has no code, but is inherited by all three Vault contracts. In order to ensure that *only* the + * Vault contract's storage is actually used, calls to the extension contracts must be delegate calls made through the + * main Vault. + */ +contract VaultStorage { + using StorageSlotExtension for *; + + /*************************************************************************** + Constants + ***************************************************************************/ + + // Pools can have between two and eight tokens. + uint256 internal constant _MIN_TOKENS = 2; + // This maximum token count is also implicitly hard-coded in `PoolConfigLib` (through packing `tokenDecimalDiffs`). + uint256 internal constant _MAX_TOKENS = 8; + // Tokens with more than 18 decimals are not supported. Tokens must also implement `IERC20Metadata.decimals`. + uint8 internal constant _MAX_TOKEN_DECIMALS = 18; + + // Maximum pause and buffer period durations. + uint256 internal constant _MAX_PAUSE_WINDOW_DURATION = 365 days * 4; + uint256 internal constant _MAX_BUFFER_PERIOD_DURATION = 90 days; + + // Minimum swap amount (applied to scaled18 values), enforced as a security measure to block potential + // exploitation of rounding errors. + // solhint-disable-next-line var-name-mixedcase + uint256 internal immutable _MINIMUM_TRADE_AMOUNT; + + // Minimum given amount to wrap/unwrap (applied to native decimal values), to avoid rounding issues. + // solhint-disable-next-line var-name-mixedcase + uint256 internal immutable _MINIMUM_WRAP_AMOUNT; + + /*************************************************************************** + Transient Storage Declarations + ***************************************************************************/ + + // NOTE: If you use a constant, then it is simply replaced everywhere when this constant is used + // by what is written after =. If you use immutable, the value is first calculated and + // then replaced everywhere. That means that if a constant has executable variables, + // they will be executed every time the constant is used. + + // solhint-disable var-name-mixedcase + bytes32 private immutable _IS_UNLOCKED_SLOT = _calculateVaultStorageSlot("isUnlocked"); + bytes32 private immutable _NON_ZERO_DELTA_COUNT_SLOT = _calculateVaultStorageSlot("nonZeroDeltaCount"); + bytes32 private immutable _TOKEN_DELTAS_SLOT = _calculateVaultStorageSlot("tokenDeltas"); + bytes32 private immutable _ADD_LIQUIDITY_CALLED_SLOT = _calculateVaultStorageSlot("addLiquidityCalled"); + // solhint-enable var-name-mixedcase + + /*************************************************************************** + Pool State + ***************************************************************************/ + + // Pool-specific configuration data (e.g., fees, pause window, configuration flags). + mapping(address pool => PoolConfigBits poolConfig) internal _poolConfigBits; + + // Accounts assigned to specific roles; e.g., pauseManager, swapManager. + mapping(address pool => PoolRoleAccounts roleAccounts) internal _poolRoleAccounts; + + // The hooks contracts associated with each pool. + mapping(address pool => IHooks hooksContract) internal _hooksContracts; + + // The set of tokens associated with each pool. + mapping(address pool => IERC20[] poolTokens) internal _poolTokens; + + // The token configuration of each Pool's tokens. + mapping(address pool => mapping(IERC20 token => TokenInfo tokenInfo)) internal _poolTokenInfo; + + // Structure containing the current raw and "last live" scaled balances. Last live balances are used for + // yield fee computation, and since these have rates applied, they are stored as scaled 18-decimal FP values. + // Each value takes up half the storage slot (i.e., 128 bits). + mapping(address pool => mapping(uint256 tokenIndex => bytes32 packedTokenBalance)) internal _poolTokenBalances; + + // Aggregate protocol swap/yield fees accumulated in the Vault for harvest. + // Reusing PackedTokenBalance for the bytes32 values to save bytecode (despite differing semantics). + // It's arbitrary which is which: we define raw = swap; derived = yield. + mapping(address pool => mapping(IERC20 token => bytes32 packedFeeAmounts)) internal _aggregateFeeAmounts; + + /*************************************************************************** + Vault State + ***************************************************************************/ + + // The Pause Window and Buffer Period are timestamp-based: they should not be relied upon for sub-minute accuracy. + uint32 internal immutable _vaultPauseWindowEndTime; + uint32 internal immutable _vaultBufferPeriodEndTime; + + // Stored as a convenience, to avoid calculating it on every operation. + uint32 internal immutable _vaultBufferPeriodDuration; + + // Bytes32 with pause flags for the Vault, buffers, and queries. + VaultStateBits internal _vaultStateBits; + + /** + * @dev Represents the total reserve of each ERC20 token. It should be always equal to `token.balanceOf(vault)`, + * except during `unlock`. + */ + mapping(IERC20 token => uint256 vaultBalance) internal _reservesOf; + + /*************************************************************************** + Contract References + ***************************************************************************/ + + // Upgradeable contract in charge of setting permissions. + IAuthorizer internal _authorizer; + + // Contract that receives aggregate swap and yield fees. + IProtocolFeeController internal _protocolFeeController; + + /*************************************************************************** + ERC4626 Buffers + ***************************************************************************/ + + // Any ERC4626 token can trade using a buffer, which is like a pool, but internal to the Vault. + // The registry key is the wrapped token address, so there can only ever be one buffer per wrapped token. + // This means they are permissionless, and have no registration function. + // + // Anyone can add liquidity to a buffer + + // A buffer will only ever have two tokens: wrapped and underlying. We pack the wrapped and underlying balances + // into a single bytes32, interpreted with the `PackedTokenBalance` library. + + // ERC4626 token address -> PackedTokenBalance, which stores both the underlying and wrapped token balances. + // Reusing PackedTokenBalance to save bytecode (despite differing semantics). + // It's arbitrary which is which: we define raw = underlying token; derived = wrapped token. + mapping(IERC4626 wrappedToken => bytes32 packedTokenBalance) internal _bufferTokenBalances; + + // The LP balances for buffers. LP balances are not tokenized (i.e., represented by ERC20 tokens like BPT), but + // rather accounted for within the Vault. + + // Track the internal "BPT" shares of each buffer depositor. + mapping(IERC4626 wrappedToken => mapping(address user => uint256 userShares)) internal _bufferLpShares; + + // Total LP shares. + mapping(IERC4626 wrappedToken => uint256 totalShares) internal _bufferTotalShares; + + // Prevents a malicious ERC4626 from changing the asset after the buffer was initialized. + mapping(IERC4626 wrappedToken => address underlyingToken) internal _bufferAssets; + + /*************************************************************************** + Transient Storage Access + ***************************************************************************/ + + function _isUnlocked() internal view returns (StorageSlotExtension.BooleanSlotType slot) { + return _IS_UNLOCKED_SLOT.asBoolean(); + } + + function _nonZeroDeltaCount() internal view returns (StorageSlotExtension.Uint256SlotType slot) { + return _NON_ZERO_DELTA_COUNT_SLOT.asUint256(); + } + + function _tokenDeltas() internal view returns (TokenDeltaMappingSlotType slot) { + return TokenDeltaMappingSlotType.wrap(_TOKEN_DELTAS_SLOT); + } + + function _addLiquidityCalled() internal view returns (AddressToBooleanMappingSlot slot) { + return AddressToBooleanMappingSlot.wrap(_ADD_LIQUIDITY_CALLED_SLOT); + } + + function _calculateVaultStorageSlot(string memory key) private pure returns (bytes32) { + return TransientStorageHelpers.calculateSlot(type(VaultStorage).name, key); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Authentication.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Authentication.sol new file mode 100644 index 00000000..4bf4607b --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Authentication.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IAuthentication } from "./IAuthentication.sol"; + +/** + * @notice Building block for performing access control on external functions. + * @dev This contract is used via the `authenticate` modifier (or the `_authenticateCaller` function), which can be + * applied to external functions to make them only callable by authorized accounts. + * + * Derived contracts must implement the `_canPerform` function, which holds the actual access control logic. + */ +abstract contract Authentication is IAuthentication { + bytes32 private immutable _actionIdDisambiguator; + + /** + * @dev The main purpose of the `actionIdDisambiguator` is to prevent accidental function selector collisions in + * multi-contract systems. + * + * There are two main uses for it: + * - if the contract is a singleton, any unique identifier can be used to make the associated action identifiers + * unique. The contract's own address is a good option. + * - if the contract belongs to a family that shares action identifiers for the same functions, an identifier + * shared by the entire family (and no other contract) should be used instead. + */ + constructor(bytes32 actionIdDisambiguator) { + _actionIdDisambiguator = actionIdDisambiguator; + } + + /// @dev Reverts unless the caller is allowed to call this function. Should only be applied to external functions. + modifier authenticate() { + _authenticateCaller(); + _; + } + + /// @dev Reverts unless the caller is allowed to call the entry point function. + function _authenticateCaller() internal view { + bytes32 actionId = getActionId(msg.sig); + + if (!_canPerform(actionId, msg.sender)) { + revert SenderNotAllowed(); + } + } + + /// @inheritdoc IAuthentication + function getActionId(bytes4 selector) public view override returns (bytes32) { + // Each external function is dynamically assigned an action identifier as the hash of the disambiguator and the + // function selector. Disambiguation is necessary to avoid potential collisions in the function selectors of + // multiple contracts. + return keccak256(abi.encodePacked(_actionIdDisambiguator, selector)); + } + + /** + * @dev Derived contracts must implement this function to perform the actual access control logic. + * @param actionId The action identifier associated with an external function + * @param user The account performing the action + * @return success True if the action is permitted + */ + function _canPerform(bytes32 actionId, address user) internal view virtual returns (bool); +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BasePoolMath.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BasePoolMath.sol new file mode 100644 index 00000000..59fcac67 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BasePoolMath.sol @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IBasePool } from "./IBasePool.sol"; +import { Rounding } from "./VaultTypes.sol"; + +import { FixedPoint } from "./FixedPoint.sol"; + +library BasePoolMath { + using FixedPoint for uint256; + + /** + * @notice An add liquidity operation increased the invariant above the limit. + * @dev This value is determined by each pool type, and depends on the specific math used to compute + * the price curve. + * + * @param invariantRatio The ratio of the new invariant (after an operation) to the old + * @param maxInvariantRatio The maximum allowed invariant ratio + */ + error InvariantRatioAboveMax(uint256 invariantRatio, uint256 maxInvariantRatio); + + /** + * @notice A remove liquidity operation decreased the invariant below the limit. + * @dev This value is determined by each pool type, and depends on the specific math used to compute + * the price curve. + * + * @param invariantRatio The ratio of the new invariant (after an operation) to the old + * @param minInvariantRatio The minimum allowed invariant ratio + */ + error InvariantRatioBelowMin(uint256 invariantRatio, uint256 minInvariantRatio); + + // For security reasons, to help ensure that for all possible "round trip" paths the caller always receives the + // same or fewer tokens than supplied, we have chosen the rounding direction to favor the protocol in all cases. + + /** + * @notice Computes the proportional amounts of tokens to be deposited into the pool. + * @dev This function computes the amount of each token that needs to be deposited in order to mint a specific + * amount of pool tokens (BPT). It ensures that the amounts of tokens deposited are proportional to the current + * pool balances. + * + * Calculation: For each token, amountIn = balance * (bptAmountOut / bptTotalSupply). + * Rounding up is used to ensure that the pool is not underfunded. + * + * @param balances Array of current token balances in the pool + * @param bptTotalSupply Total supply of the pool tokens (BPT) + * @param bptAmountOut The amount of pool tokens that need to be minted + * @return amountsIn Array of amounts for each token to be deposited + */ + function computeProportionalAmountsIn( + uint256[] memory balances, + uint256 bptTotalSupply, + uint256 bptAmountOut + ) internal pure returns (uint256[] memory amountsIn) { + /************************************************************************************ + // computeProportionalAmountsIn // + // (per token) // + // aI = amountIn / bptOut \ // + // b = balance aI = b * | ----------------- | // + // bptOut = bptAmountOut \ bptTotalSupply / // + // bpt = bptTotalSupply // + ************************************************************************************/ + + // Create a new array to hold the amounts of each token to be deposited. + amountsIn = new uint256[](balances.length); + for (uint256 i = 0; i < balances.length; ++i) { + // Since we multiply and divide we don't need to use FP math. + // We're calculating amounts in so we round up. + amountsIn[i] = balances[i].mulDivUp(bptAmountOut, bptTotalSupply); + } + } + + /** + * @notice Computes the proportional amounts of tokens to be withdrawn from the pool. + * @dev This function computes the amount of each token that will be withdrawn in exchange for burning + * a specific amount of pool tokens (BPT). It ensures that the amounts of tokens withdrawn are proportional + * to the current pool balances. + * + * Calculation: For each token, amountOut = balance * (bptAmountIn / bptTotalSupply). + * Rounding down is used to prevent withdrawing more than the pool can afford. + * + * @param balances Array of current token balances in the pool + * @param bptTotalSupply Total supply of the pool tokens (BPT) + * @param bptAmountIn The amount of pool tokens that will be burned + * @return amountsOut Array of amounts for each token to be withdrawn + */ + function computeProportionalAmountsOut( + uint256[] memory balances, + uint256 bptTotalSupply, + uint256 bptAmountIn + ) internal pure returns (uint256[] memory amountsOut) { + /********************************************************************************************** + // computeProportionalAmountsOut // + // (per token) // + // aO = tokenAmountOut / bptIn \ // + // b = tokenBalance a0 = b * | --------------------- | // + // bptIn = bptAmountIn \ bptTotalSupply / // + // bpt = bptTotalSupply // + **********************************************************************************************/ + + // Create a new array to hold the amounts of each token to be withdrawn. + amountsOut = new uint256[](balances.length); + for (uint256 i = 0; i < balances.length; ++i) { + // Since we multiply and divide we don't need to use FP math. + // Round down since we're calculating amounts out. + amountsOut[i] = (balances[i] * bptAmountIn) / bptTotalSupply; + } + } + + /** + * @notice Computes the amount of pool tokens (BPT) to be minted for an unbalanced liquidity addition. + * @dev This function handles liquidity addition where the proportion of tokens deposited does not match + * the current pool composition. It considers the current balances, exact amounts of tokens to be added, + * total supply, and swap fee percentage. The function calculates a new invariant with the added tokens, + * applying swap fees if necessary, and then calculates the amount of BPT to mint based on the change + * in the invariant. + * + * @param currentBalances Current pool balances, sorted in token registration order + * @param exactAmounts Array of exact amounts for each token to be added to the pool + * @param totalSupply The current total supply of the pool tokens (BPT) + * @param swapFeePercentage The swap fee percentage applied to the transaction + * @param pool The pool to which we're adding liquidity + * @return bptAmountOut The amount of pool tokens (BPT) that will be minted as a result of the liquidity addition + * @return swapFeeAmounts The amount of swap fees charged for each token + */ + function computeAddLiquidityUnbalanced( + uint256[] memory currentBalances, + uint256[] memory exactAmounts, + uint256 totalSupply, + uint256 swapFeePercentage, + IBasePool pool + ) internal view returns (uint256 bptAmountOut, uint256[] memory swapFeeAmounts) { + /*********************************************************************** + // // + // s = totalSupply (iFees - iCur) // + // b = tokenBalance bptOut = s * -------------- // + // bptOut = bptAmountOut iCur // + // iFees = invariantWithFeesApplied // + // iCur = currentInvariant // + // iNew = newInvariant // + ***********************************************************************/ + + // Determine the number of tokens in the pool. + uint256 numTokens = currentBalances.length; + + // Create a new array to hold the updated balances after the addition. + uint256[] memory newBalances = new uint256[](numTokens); + // Create a new array to hold the swap fee amount for each token. + swapFeeAmounts = new uint256[](numTokens); + + // Loop through each token, updating the balance with the added amount. + for (uint256 i = 0; i < numTokens; ++i) { + newBalances[i] = currentBalances[i] + exactAmounts[i] - 1; // Undo balance round up for new balances. + } + + // Calculate the new invariant ratio by dividing the new invariant by the old invariant. + // Rounding current invariant up reduces BPT amount out at the end (see comments below). + uint256 currentInvariant = pool.computeInvariant(currentBalances, Rounding.ROUND_UP); + // Round down to make `taxableAmount` larger below. + uint256 invariantRatio = pool.computeInvariant(newBalances, Rounding.ROUND_DOWN).divDown(currentInvariant); + + ensureInvariantRatioBelowMaximumBound(pool, invariantRatio); + + // Loop through each token to apply fees if necessary. + for (uint256 i = 0; i < numTokens; ++i) { + // Check if the new balance is greater than the equivalent proportional balance. + // If so, calculate the taxable amount, rounding in favor of the protocol. + // We round the second term down to subtract less and get a higher `taxableAmount`, + // which charges higher swap fees. This will lower `newBalances`, which in turn lowers + // `invariantWithFeesApplied` below. + uint256 proportionalTokenBalance = invariantRatio.mulDown(currentBalances[i]); + if (newBalances[i] > proportionalTokenBalance) { + uint256 taxableAmount; + unchecked { + taxableAmount = newBalances[i] - proportionalTokenBalance; + } + // Calculate the fee amount. + swapFeeAmounts[i] = taxableAmount.mulUp(swapFeePercentage); + + // Subtract the fee from the new balance. + // We are essentially imposing swap fees on non-proportional incoming amounts. + // Note: `swapFeeAmounts` should always be <= `taxableAmount` since `swapFeePercentage` is <= FP(1), + // but since that's not verifiable within this contract, a checked subtraction is preferred. + newBalances[i] = newBalances[i] - swapFeeAmounts[i]; + } + } + + // Calculate the new invariant with fees applied. + // This invariant should be lower than the original one, so we don't need to check invariant ratio bounds again. + // Rounding down makes bptAmountOut go down (see comment below). + uint256 invariantWithFeesApplied = pool.computeInvariant(newBalances, Rounding.ROUND_DOWN); + + // Calculate the amount of BPT to mint. This is done by multiplying the + // total supply with the ratio of the change in invariant. + // Since we multiply and divide we don't need to use FP math. + // Round down since we're calculating BPT amount out. This is the most important result of this function, + // equivalent to: + // `totalSupply * (invariantWithFeesApplied / currentInvariant - 1)` + + // Then, to round `bptAmountOut` down we use `invariantWithFeesApplied` rounded down and `currentInvariant` + // rounded up. + // If rounding makes `invariantWithFeesApplied` smaller or equal to `currentInvariant`, this would effectively + // be a donation. In that case we just let checked math revert for simplicity; it's not a valid use-case to + // support at this point. + bptAmountOut = (totalSupply * (invariantWithFeesApplied - currentInvariant)) / currentInvariant; + } + + /** + * @notice Computes the amount of input token needed to receive an exact amount of pool tokens (BPT) in a + * single-token liquidity addition. + * @dev This function is used when a user wants to add liquidity to the pool by specifying the exact amount + * of pool tokens they want to receive, and the function calculates the corresponding amount of the input token. + * It considers the current pool balances, total supply, swap fee percentage, and the desired BPT amount. + * + * @param currentBalances Array of current token balances in the pool, sorted in token registration order + * @param tokenInIndex Index of the input token for which the amount needs to be calculated + * @param exactBptAmountOut Exact amount of pool tokens (BPT) the user wants to receive + * @param totalSupply The current total supply of the pool tokens (BPT) + * @param swapFeePercentage The swap fee percentage applied to the taxable amount + * @param pool The pool to which we're adding liquidity + * @return amountInWithFee The amount of input token needed, including the swap fee, to receive the exact BPT amount + * @return swapFeeAmounts The amount of swap fees charged for each token + */ + function computeAddLiquiditySingleTokenExactOut( + uint256[] memory currentBalances, + uint256 tokenInIndex, + uint256 exactBptAmountOut, + uint256 totalSupply, + uint256 swapFeePercentage, + IBasePool pool + ) internal view returns (uint256 amountInWithFee, uint256[] memory swapFeeAmounts) { + // Calculate new supply after minting exactBptAmountOut. + uint256 newSupply = exactBptAmountOut + totalSupply; + + // Calculate the initial amount of the input token needed for the desired amount of BPT out + // "divUp" leads to a higher "newBalance", which in turn results in a larger "amountIn". + // This leads to receiving more tokens for the same amount of BPT minted. + uint256 invariantRatio = newSupply.divUp(totalSupply); + ensureInvariantRatioBelowMaximumBound(pool, invariantRatio); + + uint256 newBalance = pool.computeBalance(currentBalances, tokenInIndex, invariantRatio); + + // Compute the amount to be deposited into the pool. + uint256 amountIn = newBalance - currentBalances[tokenInIndex]; + + // Calculate the non-taxable amount, which is the new balance proportionate to the BPT minted. + // Since we multiply and divide we don't need to use FP math. + // Rounding down makes `taxableAmount` larger, which in turn makes `fee` larger below. + uint256 nonTaxableBalance = (newSupply * currentBalances[tokenInIndex]) / totalSupply; + + // Calculate the taxable amount, which is the difference between the actual new balance and + // the non-taxable balance. + uint256 taxableAmount = newBalance - nonTaxableBalance; + + // Calculate the swap fee based on the taxable amount and the swap fee percentage. + uint256 fee = taxableAmount.divUp(swapFeePercentage.complement()) - taxableAmount; + + // Create swap fees amount array and set the single fee we charge. + swapFeeAmounts = new uint256[](currentBalances.length); + swapFeeAmounts[tokenInIndex] = fee; + + // Return the total amount of input token needed, including the swap fee. + amountInWithFee = amountIn + fee; + } + + /** + * @notice Computes the amount of pool tokens to burn to receive exact amount out. + * @param currentBalances Current pool balances, sorted in token registration order + * @param tokenOutIndex Index of the token to receive in exchange for pool tokens burned + * @param exactAmountOut Exact amount of tokens to receive + * @param totalSupply The current total supply of the pool tokens (BPT) + * @param swapFeePercentage The swap fee percentage applied to the taxable amount + * @param pool The pool from which we're removing liquidity + * @return bptAmountIn Amount of pool tokens to burn + * @return swapFeeAmounts The amount of swap fees charged for each token + */ + function computeRemoveLiquiditySingleTokenExactOut( + uint256[] memory currentBalances, + uint256 tokenOutIndex, + uint256 exactAmountOut, + uint256 totalSupply, + uint256 swapFeePercentage, + IBasePool pool + ) internal view returns (uint256 bptAmountIn, uint256[] memory swapFeeAmounts) { + // Determine the number of tokens in the pool. + uint256 numTokens = currentBalances.length; + + // Create a new array to hold the updated balances. + uint256[] memory newBalances = new uint256[](numTokens); + + // Copy currentBalances to newBalances. + for (uint256 i = 0; i < numTokens; ++i) { + newBalances[i] = currentBalances[i] - 1; + } + + // Update the balance of tokenOutIndex with exactAmountOut. + newBalances[tokenOutIndex] = newBalances[tokenOutIndex] - exactAmountOut; + + // Calculate the new invariant using the new balances (after the removal). + // Calculate the new invariant ratio by dividing the new invariant by the old invariant. + // Calculate the new proportional balance by multiplying the new invariant ratio by the current balance. + // Calculate the taxable amount by subtracting the new balance from the equivalent proportional balance. + // We round `currentInvariant` up as it affects the calculated `bptAmountIn` directly (see below). + uint256 currentInvariant = pool.computeInvariant(currentBalances, Rounding.ROUND_UP); + + // We round invariant ratio up (see reason below). + // This invariant ratio could be rounded up even more by rounding `currentInvariant` down. But since it only + // affects the taxable amount and the fee calculation, whereas `currentInvariant` affects BPT in more directly, + // we use `currentInvariant` rounded up here as well. + uint256 invariantRatio = pool.computeInvariant(newBalances, Rounding.ROUND_UP).divUp(currentInvariant); + + ensureInvariantRatioAboveMinimumBound(pool, invariantRatio); + + // Taxable amount is proportional to invariant ratio; a larger taxable amount rounds in the Vault's favor. + uint256 taxableAmount = invariantRatio.mulUp(currentBalances[tokenOutIndex]) - newBalances[tokenOutIndex]; + + // Calculate the swap fee based on the taxable amount and the swap fee percentage. + // Fee is proportional to taxable amount; larger fee rounds in the Vault's favor. + uint256 fee = taxableAmount.divUp(swapFeePercentage.complement()) - taxableAmount; + + // Update new balances array with a fee. + newBalances[tokenOutIndex] = newBalances[tokenOutIndex] - fee; + + // Calculate the new invariant with fees applied. + // Larger fee means `invariantWithFeesApplied` goes lower. + uint256 invariantWithFeesApplied = pool.computeInvariant(newBalances, Rounding.ROUND_DOWN); + + // Create swap fees amount array and set the single fee we charge. + swapFeeAmounts = new uint256[](numTokens); + swapFeeAmounts[tokenOutIndex] = fee; + + // Calculate the amount of BPT to burn. This is done by multiplying the total supply by the ratio of the + // invariant delta to the current invariant. + // + // Calculating BPT amount in, so we round up. This is the most important result of this function, equivalent to: + // `totalSupply * (1 - invariantWithFeesApplied / currentInvariant)`. + // Then, to round `bptAmountIn` up we use `invariantWithFeesApplied` rounded down and `currentInvariant` + // rounded up. + // + // Since `currentInvariant` is rounded up and `invariantWithFeesApplied` is rounded down, the difference + // should always be positive. The checked math will revert if that is not the case. + bptAmountIn = totalSupply.mulDivUp(currentInvariant - invariantWithFeesApplied, currentInvariant); + } + + /** + * @notice Computes the amount of a single token to withdraw for a given amount of BPT to burn. + * @dev It computes the output token amount for an exact input of BPT, considering current balances, + * total supply, and swap fees. + * + * @param currentBalances The current token balances in the pool + * @param tokenOutIndex The index of the token to be withdrawn + * @param exactBptAmountIn The exact amount of BPT the user wants to burn + * @param totalSupply The current total supply of the pool tokens (BPT) + * @param swapFeePercentage The swap fee percentage applied to the taxable amount + * @param pool The pool from which we're removing liquidity + * @return amountOutWithFee The amount of the output token the user receives, accounting for swap fees + */ + function computeRemoveLiquiditySingleTokenExactIn( + uint256[] memory currentBalances, + uint256 tokenOutIndex, + uint256 exactBptAmountIn, + uint256 totalSupply, + uint256 swapFeePercentage, + IBasePool pool + ) internal view returns (uint256 amountOutWithFee, uint256[] memory swapFeeAmounts) { + // Calculate new supply accounting for burning exactBptAmountIn. + uint256 newSupply = totalSupply - exactBptAmountIn; + uint256 invariantRatio = newSupply.divUp(totalSupply); + ensureInvariantRatioAboveMinimumBound(pool, invariantRatio); + + // Calculate the new balance of the output token after the BPT burn. + // "divUp" leads to a higher "newBalance", which in turn results in a lower "amountOut", but also a lower + // "taxableAmount". Although the former leads to giving less tokens for the same amount of BPT burned, + // the latter leads to charging less swap fees. In consequence, a conflict of interests arises regarding + // the rounding of "newBalance"; we prioritize getting a lower "amountOut". + uint256 newBalance = pool.computeBalance(currentBalances, tokenOutIndex, invariantRatio); + + // Compute the amount to be withdrawn from the pool. + uint256 amountOut = currentBalances[tokenOutIndex] - newBalance; + + // Calculate the new balance proportionate to the amount of BPT burned. + // We round up: higher `newBalanceBeforeTax` makes `taxableAmount` go up, which rounds in the Vault's favor. + uint256 newBalanceBeforeTax = newSupply.mulDivUp(currentBalances[tokenOutIndex], totalSupply); + + // Compute the taxable amount: the difference between the new proportional and disproportional balances. + uint256 taxableAmount = newBalanceBeforeTax - newBalance; + + // Calculate the swap fee on the taxable amount. + uint256 fee = taxableAmount.mulUp(swapFeePercentage); + + // Create swap fees amount array and set the single fee we charge. + swapFeeAmounts = new uint256[](currentBalances.length); + swapFeeAmounts[tokenOutIndex] = fee; + + // Return the net amount after subtracting the fee. + amountOutWithFee = amountOut - fee; + } + + /** + * @notice Validate the invariant ratio against the maximum bound. + * @dev This is checked when we're adding liquidity, so the `invariantRatio` > 1. + * @param pool The pool to which we're adding liquidity + * @param invariantRatio The ratio of the new invariant (after an operation) to the old + */ + function ensureInvariantRatioBelowMaximumBound(IBasePool pool, uint256 invariantRatio) internal view { + uint256 maxInvariantRatio = pool.getMaximumInvariantRatio(); + if (invariantRatio > maxInvariantRatio) { + revert InvariantRatioAboveMax(invariantRatio, maxInvariantRatio); + } + } + + /** + * @notice Validate the invariant ratio against the maximum bound. + * @dev This is checked when we're removing liquidity, so the `invariantRatio` < 1. + * @param pool The pool from which we're removing liquidity + * @param invariantRatio The ratio of the new invariant (after an operation) to the old + */ + function ensureInvariantRatioAboveMinimumBound(IBasePool pool, uint256 invariantRatio) internal view { + uint256 minInvariantRatio = pool.getMinimumInvariantRatio(); + if (invariantRatio < minInvariantRatio) { + revert InvariantRatioBelowMin(invariantRatio, minInvariantRatio); + } + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BufferHelpers.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BufferHelpers.sol new file mode 100644 index 00000000..74bfcc9c --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/BufferHelpers.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +import { PackedTokenBalance } from "./PackedTokenBalance.sol"; + +library BufferHelpers { + using PackedTokenBalance for bytes32; + using SafeCast for *; + + /** + * @notice Returns the imbalance of a buffer in terms of the underlying asset. + * @dev The imbalance refers to the difference between the buffer's underlying asset balance and its wrapped asset + * balance, both expressed in terms of the underlying asset. A positive imbalance means the buffer holds more + * underlying assets than wrapped assets, indicating that the excess underlying should be wrapped to restore + * balance. Conversely, a negative imbalance means the buffer has more wrapped assets than underlying assets, so + * during a wrap operation, fewer underlying tokens need to be wrapped, and the surplus wrapped tokens can be + * returned to the caller. + * For instance, consider the following scenario: + * - buffer balances: 2 wrapped and 10 underlying + * - wrapped rate: 2 + * - normalized buffer balances: 4 wrapped as underlying (2 wrapped * rate) and 10 underlying + * - underlying token imbalance = (10 - 4) / 2 = 3 underlying + * We need to wrap 3 underlying tokens to rebalance the buffer. + * - 3 underlying = 1.5 wrapped + * - final balances: 3.5 wrapped (2 existing + 1.5 new) and 7 underlying (10 existing - 3) + * These balances are equal value, given the rate. + */ + function getBufferUnderlyingImbalance(bytes32 bufferBalance, IERC4626 wrappedToken) internal view returns (int256) { + int256 underlyingBalance = bufferBalance.getBalanceRaw().toInt256(); + + int256 wrappedBalanceAsUnderlying = 0; + if (bufferBalance.getBalanceDerived() > 0) { + // The buffer underlying imbalance is used when wrapping (it means, deposit underlying and get wrapped + // tokens), so we use `previewMint` to convert wrapped balance to underlying. The `mint` function is used + // here, as it performs the inverse of a `deposit` operation. + wrappedBalanceAsUnderlying = wrappedToken.previewMint(bufferBalance.getBalanceDerived()).toInt256(); + } + + // The return value may be positive (excess of underlying) or negative (excess of wrapped). + return (underlyingBalance - wrappedBalanceAsUnderlying) / 2; + } + + /** + * @notice Returns the imbalance of a buffer in terms of the wrapped asset. + * @dev The imbalance refers to the difference between the buffer's underlying asset balance and its wrapped asset + * balance, both expressed in terms of the wrapped asset. A positive imbalance means the buffer holds more + * wrapped assets than underlying assets, indicating that the excess wrapped should be unwrapped to restore + * balance. Conversely, a negative imbalance means the buffer has more underlying assets than wrapped assets, so + * during an unwrap operation, fewer wrapped tokens need to be unwrapped, and the surplus underlying tokens can be + * returned to the caller. + * For instance, consider the following scenario: + * - buffer balances: 10 wrapped and 4 underlying + * - wrapped rate: 2 + * - normalized buffer balances: 10 wrapped and 2 underlying as wrapped (2 underlying / rate) + * - imbalance of wrapped = (10 - 2) / 2 = 4 wrapped + * We need to unwrap 4 wrapped tokens to rebalance the buffer. + * - 4 wrapped = 8 underlying + * - final balances: 6 wrapped (10 existing - 4) and 12 underlying (4 existing + 8 new) + * These balances are equal value, given the rate. + */ + function getBufferWrappedImbalance(bytes32 bufferBalance, IERC4626 wrappedToken) internal view returns (int256) { + int256 wrappedBalance = bufferBalance.getBalanceDerived().toInt256(); + + int256 underlyingBalanceAsWrapped = 0; + if (bufferBalance.getBalanceRaw() > 0) { + // The buffer wrapped imbalance is used when unwrapping (it means, deposit wrapped and get underlying + // tokens), so we use `previewWithdraw` to convert underlying balance to wrapped. The `withdraw` function + // is used here, as it performs the inverse of a `redeem` operation. + underlyingBalanceAsWrapped = wrappedToken.previewWithdraw(bufferBalance.getBalanceRaw()).toInt256(); + } + + // The return value may be positive (excess of wrapped) or negative (excess of underlying). + return (wrappedBalance - underlyingBalanceAsWrapped) / 2; + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Bytes32AddressLib.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Bytes32AddressLib.sol new file mode 100644 index 00000000..aed2a441 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Bytes32AddressLib.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +pragma solidity ^0.8.24; + +/** + * @notice Library for converting between addresses and bytes32 values. + * @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/Bytes32AddressLib.sol) + * @dev Used in CREATE3 contract deployment. + */ +library Bytes32AddressLib { + function fromLast20Bytes(bytes32 bytesValue) internal pure returns (address) { + return address(uint160(uint256(bytesValue))); + } + + function fillLast12Bytes(address addressValue) internal pure returns (bytes32) { + return bytes32(bytes20(addressValue)); + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CREATE3.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CREATE3.sol new file mode 100644 index 00000000..62c749e8 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CREATE3.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +pragma solidity ^0.8.24; + +import { Bytes32AddressLib } from "./Bytes32AddressLib.sol"; + +/** + * @notice Deploy to deterministic addresses without an initcode factor. + * @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/CREATE3.sol) + * @dev Modified from 0xSequence (https://github.com/0xSequence/create3/blob/master/contracts/Create3.sol) + * Also avoids dependence on a particular deployer account, and allows for more secure "salt mining" of addresses, + * vs. web-based vanity address mining. + */ +library CREATE3 { + using Bytes32AddressLib for bytes32; + // solhint-disable no-inline-assembly + + //--------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //--------------------------------------------------------------------------------// + // 0x36 | 0x36 | CALLDATASIZE | size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 0 size // + // 0x37 | 0x37 | CALLDATACOPY | // + // 0x36 | 0x36 | CALLDATASIZE | size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 size // + // 0x34 | 0x34 | CALLVALUE | value 0 size // + // 0xf0 | 0xf0 | CREATE | newContract // + //--------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //--------------------------------------------------------------------------------// + // 0x67 | 0x67XXXXXXXXXXXXXXXX | PUSH8 bytecode | bytecode // + // 0x3d | 0x3d | RETURNDATASIZE | 0 bytecode // + // 0x52 | 0x52 | MSTORE | // + // 0x60 | 0x6008 | PUSH1 08 | 8 // + // 0x60 | 0x6018 | PUSH1 18 | 24 8 // + // 0xf3 | 0xf3 | RETURN | // + //--------------------------------------------------------------------------------// + bytes internal constant _PROXY_BYTECODE = hex"67_36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3"; + + bytes32 internal constant _PROXY_BYTECODE_HASH = keccak256(_PROXY_BYTECODE); + + function deploy(bytes32 salt, bytes memory creationCode, uint256 value) internal returns (address deployed) { + bytes memory proxyChildBytecode = _PROXY_BYTECODE; + + address proxy; + /// @solidity memory-safe-assembly + assembly { + // Deploy a new contract with our pre-made bytecode via CREATE2. + // We start 32 bytes into the code to avoid copying the byte length. + proxy := create2(0, add(proxyChildBytecode, 32), mload(proxyChildBytecode), salt) + } + require(proxy != address(0), "DEPLOYMENT_FAILED"); + + deployed = getDeployed(salt); + (bool success, ) = proxy.call{ value: value }(creationCode); + require(success && deployed.code.length != 0, "INITIALIZATION_FAILED"); + } + + function getDeployed(bytes32 salt) internal view returns (address) { + return getDeployed(salt, address(this)); + } + + function getDeployed(bytes32 salt, address creator) internal pure returns (address) { + address proxy = keccak256( + abi.encodePacked( + // Prefix: + bytes1(0xFF), + // Creator: + creator, + // Salt: + salt, + // Bytecode hash: + _PROXY_BYTECODE_HASH + ) + ).fromLast20Bytes(); + + return + keccak256( + abi.encodePacked( + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01) + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex) + hex"d6_94", + proxy, + hex"01" // Nonce of the proxy contract (1) + ) + ).fromLast20Bytes(); + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CastingHelpers.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CastingHelpers.sol new file mode 100644 index 00000000..67e96363 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/CastingHelpers.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @notice Library of helper functions related to typecasting arrays. +library CastingHelpers { + /** + * @dev Casts an array of uint256 to int256, setting the sign of the result according to the `positive` flag, + * without checking whether the values fit in the signed 256 bit range. + */ + function unsafeCastToInt256( + uint256[] memory values, + bool positive + ) internal pure returns (int256[] memory signedValues) { + signedValues = new int256[](values.length); + for (uint256 i = 0; i < values.length; ++i) { + signedValues[i] = positive ? int256(values[i]) : -int256(values[i]); + } + } + + /// @dev Returns a native array of addresses as an IERC20[] array. + function asIERC20(address[] memory addresses) internal pure returns (IERC20[] memory tokens) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + tokens := addresses + } + } + + /// @dev Returns an IERC20[] array as an address[] array. + function asAddress(IERC20[] memory tokens) internal pure returns (address[] memory addresses) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + addresses := tokens + } + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/EVMCallModeHelpers.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/EVMCallModeHelpers.sol new file mode 100644 index 00000000..b0b9881a --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/EVMCallModeHelpers.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/// @notice Library used to check whether the current operation was initiated through a static call. +library EVMCallModeHelpers { + /// @notice A state-changing transaction was initiated in a context that only allows static calls. + error NotStaticCall(); + + /** + * @dev Detects whether the current transaction is a static call. + * A static call is one where `tx.origin` equals 0x0 for most implementations. + * See this tweet for a table on how transaction parameters are set on different platforms: + * https://twitter.com/0xkarmacoma/status/1493380279309717505 + * + * Solidity eth_call reference docs are here: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call + */ + function isStaticCall() internal view returns (bool) { + return tx.origin == address(0); + // solhint-disable-previous-line avoid-tx-origin + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/FactoryWidePauseWindow.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/FactoryWidePauseWindow.sol new file mode 100644 index 00000000..32285abd --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/FactoryWidePauseWindow.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/** + * @notice Base contract for v3 factories to support pause windows for pools based on the factory deployment time. + * @dev Each pool deployment calls `getPauseWindowDuration` on the factory so that all Pools created by this factory + * will share the same Pause Window end time, after which both old and new Pools will not be pausable. + * + * All pools are reversibly pausable until the pause window expires. Afterward, there is an additional buffer + * period, set to the same duration as the Vault's buffer period. If a pool was paused, it will remain paused + * through this buffer period, and cannot be unpaused. + * + * When the buffer period expires, it will unpause automatically, and remain permissionless forever after. + */ +contract FactoryWidePauseWindow { + // This contract relies on timestamps - the usual caveats apply. + // solhint-disable not-rely-on-time + + // The pause window end time is stored in 32 bits. + uint32 private constant _MAX_TIMESTAMP = type(uint32).max; + + uint32 private immutable _pauseWindowDuration; + + // Time when the pause window for all created Pools expires. + uint32 private immutable _poolsPauseWindowEndTime; + + /// @notice The factory deployer gave a duration that would overflow the Unix timestamp. + error PoolPauseWindowDurationOverflow(); + + constructor(uint32 pauseWindowDuration) { + uint256 pauseWindowEndTime = block.timestamp + pauseWindowDuration; + + if (pauseWindowEndTime > _MAX_TIMESTAMP) { + revert PoolPauseWindowDurationOverflow(); + } + + _pauseWindowDuration = pauseWindowDuration; + + // Direct cast is safe, as it was checked above. + _poolsPauseWindowEndTime = uint32(pauseWindowEndTime); + } + + /** + * @notice Return the pause window duration. This is the time pools will be pausable after factory deployment. + * @return The duration in seconds + */ + function getPauseWindowDuration() external view returns (uint32) { + return _pauseWindowDuration; + } + + /** + * @notice Returns the original factory pauseWindowEndTime, regardless of the current time. + * @return The end time as a timestamp + */ + function getOriginalPauseWindowEndTime() external view returns (uint32) { + return _poolsPauseWindowEndTime; + } + + /** + * @notice Returns the current pauseWindowEndTime that will be applied to Pools created by this factory. + * @dev We intend for all pools deployed by this factory to have the same pause window end time (i.e., after + * this date, all future pools will be unpausable). This function will return `_poolsPauseWindowEndTime` + * until it passes, after which it will return 0. + * + * @return The resolved pause window end time (0 indicating it's no longer pausable) + */ + function getNewPoolPauseWindowEndTime() public view returns (uint32) { + // We know _poolsPauseWindowEndTime <= _MAX_TIMESTAMP (checked above). + // Do not truncate timestamp; it should still return 0 after _MAX_TIMESTAMP. + return (block.timestamp < _poolsPauseWindowEndTime) ? _poolsPauseWindowEndTime : 0; + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IBasePool.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IBasePool.sol new file mode 100644 index 00000000..21bcba44 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IBasePool.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { ISwapFeePercentageBounds } from "./ISwapFeePercentageBounds.sol"; +import { IUnbalancedLiquidityInvariantRatioBounds } from "./IUnbalancedLiquidityInvariantRatioBounds.sol"; +import { PoolSwapParams, Rounding, SwapKind } from "./VaultTypes.sol"; + +/** + * @notice Base interface for a Balancer Pool. + * @dev All pool types should implement this interface. Note that it also requires implementation of: + * - `ISwapFeePercentageBounds` to specify the minimum and maximum swap fee percentages. + * - `IUnbalancedLiquidityInvariantRatioBounds` to specify how much the invariant can change during an unbalanced + * liquidity operation. + */ +interface IBasePool is ISwapFeePercentageBounds, IUnbalancedLiquidityInvariantRatioBounds { + /*************************************************************************** + Invariant + ***************************************************************************/ + + /** + * @notice Computes the pool's invariant. + * @dev This function computes the invariant based on current balances (and potentially other pool state). + * The rounding direction must be respected for the Vault to round in the pool's favor when calling this function. + * If the invariant computation involves no precision loss (e.g. simple sum of balances), the same result can be + * returned for both rounding directions. + * + * You can think of the invariant as a measure of the "value" of the pool, which is related to the total liquidity + * (i.e., the "BPT rate" is `invariant` / `totalSupply`). Two critical properties must hold: + * + * 1) The invariant should not change due to a swap. In practice, it can *increase* due to swap fees, which + * effectively add liquidity after the swap - but it should never decrease. + * + * 2) The invariant must be "linear"; i.e., increasing the balances proportionally must increase the invariant in + * the same proportion: inv(a * n, b * n, c * n) = inv(a, b, c) * n + * + * Property #1 is required to prevent "round trip" paths that drain value from the pool (and all LP shareholders). + * Intuitively, an accurate pricing algorithm ensures the user gets an equal value of token out given token in, so + * the total value should not change. + * + * Property #2 is essential for the "fungibility" of LP shares. If it did not hold, then different users depositing + * the same total value would get a different number of LP shares. In that case, LP shares would not be + * interchangeable, as they must be in a fair DEX. + * + * @param balancesLiveScaled18 Token balances after paying yield fees, applying decimal scaling and rates + * @param rounding Rounding direction to consider when computing the invariant + * @return invariant The calculated invariant of the pool, represented as a uint256 + */ + function computeInvariant( + uint256[] memory balancesLiveScaled18, + Rounding rounding + ) external view returns (uint256 invariant); + + /** + * @dev Computes the new balance of a token after an operation, given the invariant growth ratio and all other + * balances. Similar to V2's `_getTokenBalanceGivenInvariantAndAllOtherBalances` in StableMath. + * The pool must round up for the Vault to round in the protocol's favor when calling this function. + * + * @param balancesLiveScaled18 Token balances after paying yield fees, applying decimal scaling and rates + * @param tokenInIndex The index of the token we're computing the balance for, sorted in token registration order + * @param invariantRatio The ratio of the new invariant (after an operation) to the old + * @return newBalance The new balance of the selected token, after the operation + */ + function computeBalance( + uint256[] memory balancesLiveScaled18, + uint256 tokenInIndex, + uint256 invariantRatio + ) external view returns (uint256 newBalance); + + /*************************************************************************** + Swaps + ***************************************************************************/ + + /** + * @notice Execute a swap in the pool. + * @param params Swap parameters (see above for struct definition) + * @return amountCalculatedScaled18 Calculated amount for the swap operation + */ + function onSwap(PoolSwapParams calldata params) external returns (uint256 amountCalculatedScaled18); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IERC20MultiTokenErrors.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IERC20MultiTokenErrors.sol new file mode 100644 index 00000000..e58ab753 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IERC20MultiTokenErrors.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +interface IERC20MultiTokenErrors { + /** + * @notice The total supply of a pool token can't be lower than the absolute minimum. + * @param totalSupply The total supply value that was below the minimum + */ + error PoolTotalSupplyTooLow(uint256 totalSupply); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPermit2.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPermit2.sol new file mode 100644 index 00000000..5be0075c --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPermit2.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ISignatureTransfer} from "./ISignatureTransfer.sol"; +import {IAllowanceTransfer} from "./IAllowanceTransfer.sol"; + +/// @notice Permit2 handles signature-based transfers in SignatureTransfer and allowance-based transfers in AllowanceTransfer. +/// @dev Users must approve Permit2 before calling any of the transfer functions. +interface IPermit2 is ISignatureTransfer, IAllowanceTransfer { +// IPermit2 unifies the two interfaces so users have maximal flexibility with their approval. +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolInfo.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolInfo.sol new file mode 100644 index 00000000..ff620af7 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolInfo.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { TokenInfo } from "./VaultTypes.sol"; + +/** + * @notice Convenience interface for pools, to get easy access to information stored in the Vault. + * Intended mostly for off-chain requests; pools do not need to implement this to work properly. + */ +interface IPoolInfo { + /** + * @notice Gets the tokens registered in the pool. + * @return tokens List of tokens in the pool, sorted in registration order + */ + function getTokens() external view returns (IERC20[] memory tokens); + + /** + * @notice Gets the raw data for the pool: tokens, token info, raw balances, and last live balances. + * @return tokens Pool tokens, sorted in pool registration order + * @return tokenInfo Token info structs (type, rate provider, yield flag), sorted in pool registration order + * @return balancesRaw Current native decimal balances of the pool tokens, sorted in pool registration order + * @return lastBalancesLiveScaled18 Last saved live balances, sorted in token registration order + */ + function getTokenInfo() + external + view + returns ( + IERC20[] memory tokens, + TokenInfo[] memory tokenInfo, + uint256[] memory balancesRaw, + uint256[] memory lastBalancesLiveScaled18 + ); + + /** + * @notice Gets the current live balances of the pool as fixed point, 18-decimal numbers. + * @dev Note that live balances will not necessarily be accurate if the pool is in Recovery Mode. + * Withdrawals in Recovery Mode do not make external calls (including those necessary for updating live balances), + * so if there are withdrawals, raw and live balances will be out of sync until Recovery Mode is disabled. + * + * @return balancesLiveScaled18 Token balances after paying yield fees, applying decimal scaling and rates + */ + function getCurrentLiveBalances() external view returns (uint256[] memory balancesLiveScaled18); + + /** + * @notice Fetches the static swap fee percentage for the pool. + * @return staticSwapFeePercentage 18-decimal FP value of the static swap fee percentage + */ + function getStaticSwapFeePercentage() external view returns (uint256 staticSwapFeePercentage); + + /** + * @notice Gets the aggregate swap and yield fee percentages for a pool. + * @dev These are determined by the current protocol and pool creator fees, set in the `ProtocolFeeController`. + * @return aggregateSwapFeePercentage The aggregate percentage fee applied to swaps + * @return aggregateYieldFeePercentage The aggregate percentage fee applied to yield + */ + function getAggregateFeePercentages() + external + view + returns (uint256 aggregateSwapFeePercentage, uint256 aggregateYieldFeePercentage); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolLiquidity.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolLiquidity.sol new file mode 100644 index 00000000..c3e2ab62 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IPoolLiquidity.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/// @notice Interface for custom liquidity operations +interface IPoolLiquidity { + /** + * @notice Add liquidity to the pool with a custom hook. + * @param router The address (usually a router contract) that initiated a swap operation on the Vault + * @param maxAmountsInScaled18 Maximum input amounts, sorted in token registration order + * @param minBptAmountOut Minimum amount of output pool tokens + * @param balancesScaled18 Current pool balances, sorted in token registration order + * @param userData Arbitrary data sent with the encoded request + * @return amountsInScaled18 Input token amounts, sorted in token registration order + * @return bptAmountOut Calculated pool token amount to receive + * @return swapFeeAmountsScaled18 The amount of swap fees charged for each token + * @return returnData Arbitrary data with an encoded response from the pool + */ + function onAddLiquidityCustom( + address router, + uint256[] memory maxAmountsInScaled18, + uint256 minBptAmountOut, + uint256[] memory balancesScaled18, + bytes memory userData + ) + external + returns ( + uint256[] memory amountsInScaled18, + uint256 bptAmountOut, + uint256[] memory swapFeeAmountsScaled18, + bytes memory returnData + ); + + /** + * @notice Remove liquidity from the pool with a custom hook. + * @param router The address (usually a router contract) that initiated a swap operation on the Vault + * @param maxBptAmountIn Maximum amount of input pool tokens + * @param minAmountsOutScaled18 Minimum output amounts, sorted in token registration order + * @param balancesScaled18 Current pool balances, sorted in token registration order + * @param userData Arbitrary data sent with the encoded request + * @return bptAmountIn Calculated pool token amount to burn + * @return amountsOutScaled18 Amount of tokens to receive, sorted in token registration order + * @return swapFeeAmountsScaled18 The amount of swap fees charged for each token + * @return returnData Arbitrary data with an encoded response from the pool + */ + function onRemoveLiquidityCustom( + address router, + uint256 maxBptAmountIn, + uint256[] memory minAmountsOutScaled18, + uint256[] memory balancesScaled18, + bytes memory userData + ) + external + returns ( + uint256 bptAmountIn, + uint256[] memory amountsOutScaled18, + uint256[] memory swapFeeAmountsScaled18, + bytes memory returnData + ); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRouter.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRouter.sol new file mode 100644 index 00000000..d4ae64ab --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IRouter.sol @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { AddLiquidityKind, RemoveLiquidityKind, SwapKind } from "./VaultTypes.sol"; + +/// @notice User-friendly interface to basic Vault operations: swap, add/remove liquidity, and associated queries. +interface IRouter { + /*************************************************************************** + Pool Initialization + ***************************************************************************/ + + /** + * @notice Data for the pool initialization hook. + * @param sender Account originating the pool initialization operation + * @param pool Address of the liquidity pool + * @param tokens Pool tokens, in token registration order + * @param exactAmountsIn Exact amounts of tokens to be added, sorted in token registration order + * @param minBptAmountOut Minimum amount of pool tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to add initial liquidity + */ + struct InitializeHookParams { + address sender; + address pool; + IERC20[] tokens; + uint256[] exactAmountsIn; + uint256 minBptAmountOut; + bool wethIsEth; + bytes userData; + } + + /** + * @notice Initialize a liquidity pool. + * @param pool Address of the liquidity pool + * @param tokens Pool tokens, in token registration order + * @param exactAmountsIn Exact amounts of tokens to be added, sorted in token registration order + * @param minBptAmountOut Minimum amount of pool tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to add initial liquidity + * @return bptAmountOut Actual amount of pool tokens minted in exchange for initial liquidity + */ + function initialize( + address pool, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256 bptAmountOut); + + /*************************************************************************** + Add Liquidity + ***************************************************************************/ + + /** + * @notice Adds liquidity to a pool with proportional token amounts, receiving an exact amount of pool tokens. + * @param pool Address of the liquidity pool + * @param maxAmountsIn Maximum amounts of tokens to be added, sorted in token registration order + * @param exactBptAmountOut Exact amount of pool tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to add liquidity + * @return amountsIn Actual amounts of tokens added, sorted in token registration order + */ + function addLiquidityProportional( + address pool, + uint256[] memory maxAmountsIn, + uint256 exactBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256[] memory amountsIn); + + /** + * @notice Adds liquidity to a pool with arbitrary token amounts. + * @param pool Address of the liquidity pool + * @param exactAmountsIn Exact amounts of tokens to be added, sorted in token registration order + * @param minBptAmountOut Minimum amount of pool tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to add liquidity + * @return bptAmountOut Actual amount of pool tokens received + */ + function addLiquidityUnbalanced( + address pool, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256 bptAmountOut); + + /** + * @notice Adds liquidity to a pool in a single token, receiving an exact amount of pool tokens. + * @param pool Address of the liquidity pool + * @param tokenIn Token used to add liquidity + * @param maxAmountIn Maximum amount of tokens to be added + * @param exactBptAmountOut Exact amount of pool tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to add liquidity + * @return amountIn Actual amount of tokens added + */ + function addLiquiditySingleTokenExactOut( + address pool, + IERC20 tokenIn, + uint256 maxAmountIn, + uint256 exactBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256 amountIn); + + /** + * @notice Adds liquidity to a pool by donating the amounts in (no BPT out). + * @dev To support donation, the pool config `enableDonation` flag must be set to true. + * @param pool Address of the liquidity pool + * @param amountsIn Amounts of tokens to be donated, sorted in token registration order + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to donate liquidity + */ + function donate(address pool, uint256[] memory amountsIn, bool wethIsEth, bytes memory userData) external payable; + + /** + * @notice Adds liquidity to a pool with a custom request. + * @dev The given maximum and minimum amounts given may be interpreted as exact depending on the pool type. + * In any case the caller can expect them to be hard boundaries for the request. + * + * @param pool Address of the liquidity pool + * @param maxAmountsIn Maximum amounts of tokens to be added, sorted in token registration order + * @param minBptAmountOut Minimum amount of pool tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to add liquidity + * @return amountsIn Actual amounts of tokens added, sorted in token registration order + * @return bptAmountOut Actual amount of pool tokens received + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function addLiquidityCustom( + address pool, + uint256[] memory maxAmountsIn, + uint256 minBptAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData); + + /*************************************************************************** + Remove Liquidity + ***************************************************************************/ + + /** + * @notice Removes liquidity with proportional token amounts from a pool, burning an exact pool token amount. + * @param pool Address of the liquidity pool + * @param exactBptAmountIn Exact amount of pool tokens provided + * @param minAmountsOut Minimum amounts of tokens to be received, sorted in token registration order + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to remove liquidity + * @return amountsOut Actual amounts of tokens received, sorted in token registration order + */ + function removeLiquidityProportional( + address pool, + uint256 exactBptAmountIn, + uint256[] memory minAmountsOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256[] memory amountsOut); + + /** + * @notice Removes liquidity from a pool via a single token, burning an exact pool token amount. + * @param pool Address of the liquidity pool + * @param exactBptAmountIn Exact amount of pool tokens provided + * @param tokenOut Token used to remove liquidity + * @param minAmountOut Minimum amount of tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to remove liquidity + * @return amountOut Actual amount of tokens received + */ + function removeLiquiditySingleTokenExactIn( + address pool, + uint256 exactBptAmountIn, + IERC20 tokenOut, + uint256 minAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256 amountOut); + + /** + * @notice Removes liquidity from a pool via a single token, specifying the exact amount of tokens to receive. + * @param pool Address of the liquidity pool + * @param maxBptAmountIn Maximum amount of pool tokens provided + * @param tokenOut Token used to remove liquidity + * @param exactAmountOut Exact amount of tokens to be received + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to remove liquidity + * @return bptAmountIn Actual amount of pool tokens burned + */ + function removeLiquiditySingleTokenExactOut( + address pool, + uint256 maxBptAmountIn, + IERC20 tokenOut, + uint256 exactAmountOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256 bptAmountIn); + + /** + * @notice Removes liquidity from a pool with a custom request. + * @dev The given maximum and minimum amounts given may be interpreted as exact depending on the pool type. + * In any case the caller can expect them to be hard boundaries for the request. + * + * @param pool Address of the liquidity pool + * @param maxBptAmountIn Maximum amount of pool tokens provided + * @param minAmountsOut Minimum amounts of tokens to be received, sorted in token registration order + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the request to remove liquidity + * @return bptAmountIn Actual amount of pool tokens burned + * @return amountsOut Actual amounts of tokens received, sorted in token registration order + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function removeLiquidityCustom( + address pool, + uint256 maxBptAmountIn, + uint256[] memory minAmountsOut, + bool wethIsEth, + bytes memory userData + ) external payable returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData); + + /** + * @notice Removes liquidity proportionally, burning an exact pool token amount. Only available in Recovery Mode. + * @param pool Address of the liquidity pool + * @param exactBptAmountIn Exact amount of pool tokens provided + * @return amountsOut Actual amounts of tokens received, sorted in token registration order + */ + function removeLiquidityRecovery( + address pool, + uint256 exactBptAmountIn + ) external payable returns (uint256[] memory amountsOut); + + /*************************************************************************** + Swaps + ***************************************************************************/ + + /** + * @notice Data for the swap hook. + * @param sender Account initiating the swap operation + * @param kind Type of swap (exact in or exact out) + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param amountGiven Amount given based on kind of the swap (e.g., tokenIn for exact in) + * @param limit Maximum or minimum amount based on the kind of swap (e.g., maxAmountIn for exact out) + * @param deadline Deadline for the swap, after which it will revert + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the swap request + */ + struct SwapSingleTokenHookParams { + address sender; + SwapKind kind; + address pool; + IERC20 tokenIn; + IERC20 tokenOut; + uint256 amountGiven; + uint256 limit; + uint256 deadline; + bool wethIsEth; + bytes userData; + } + + /** + * @notice Executes a swap operation specifying an exact input token amount. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountIn Exact amounts of input tokens to send + * @param minAmountOut Minimum amount of tokens to be received + * @param deadline Deadline for the swap, after which it will revert + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @param userData Additional (optional) data sent with the swap request + * @return amountOut Calculated amount of output tokens to be received in exchange for the given input tokens + */ + function swapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + uint256 minAmountOut, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable returns (uint256 amountOut); + + /** + * @notice Executes a swap operation specifying an exact output token amount. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountOut Exact amounts of input tokens to receive + * @param maxAmountIn Maximum amount of tokens to be sent + * @param deadline Deadline for the swap, after which it will revert + * @param userData Additional (optional) data sent with the swap request + * @param wethIsEth If true, incoming ETH will be wrapped to WETH and outgoing WETH will be unwrapped to ETH + * @return amountIn Calculated amount of input tokens to be sent in exchange for the requested output tokens + */ + function swapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + uint256 maxAmountIn, + uint256 deadline, + bool wethIsEth, + bytes calldata userData + ) external payable returns (uint256 amountIn); + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /** + * @notice Adds liquidity for the first time to an internal ERC4626 buffer in the Vault. + * @dev Calling this method binds the wrapped token to its underlying asset internally; the asset in the wrapper + * cannot change afterwards, or every other operation on that wrapper (add / remove / wrap / unwrap) will fail. + * + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param amountUnderlyingRaw Amount of underlying tokens that will be deposited into the buffer + * @param amountWrappedRaw Amount of wrapped tokens that will be deposited into the buffer + * @return issuedShares the amount of tokens sharesOwner has in the buffer, denominated in underlying tokens + * (This is the BPT of the Vault's internal ERC4626 buffer.) + */ + function initializeBuffer( + IERC4626 wrappedToken, + uint256 amountUnderlyingRaw, + uint256 amountWrappedRaw + ) external returns (uint256 issuedShares); + + /** + * @notice Adds liquidity proportionally to an internal ERC4626 buffer in the Vault. + * @dev Requires the buffer to be initialized beforehand. Restricting adds to proportional simplifies the Vault + * code, avoiding rounding issues and minimum amount checks. It is possible to add unbalanced by interacting + * with the wrapper contract directly. + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param exactSharesToIssue The value in underlying tokens that `sharesOwner` wants to add to the buffer, + * in underlying token decimals + * @return amountUnderlyingRaw Amount of underlying tokens deposited into the buffer + * @return amountWrappedRaw Amount of wrapped tokens deposited into the buffer + */ + function addLiquidityToBuffer( + IERC4626 wrappedToken, + uint256 exactSharesToIssue + ) external returns (uint256 amountUnderlyingRaw, uint256 amountWrappedRaw); + + /*************************************************************************** + Queries + ***************************************************************************/ + + /** + * @notice Queries an `addLiquidityProportional` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param exactBptAmountOut Exact amount of pool tokens to be received + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountsIn Expected amounts of tokens to add, sorted in token registration order + */ + function queryAddLiquidityProportional( + address pool, + uint256 exactBptAmountOut, + address sender, + bytes memory userData + ) external returns (uint256[] memory amountsIn); + + /** + * @notice Queries an `addLiquidityUnbalanced` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param exactAmountsIn Exact amounts of tokens to be added, sorted in token registration order + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return bptAmountOut Expected amount of pool tokens to receive + */ + function queryAddLiquidityUnbalanced( + address pool, + uint256[] memory exactAmountsIn, + address sender, + bytes memory userData + ) external returns (uint256 bptAmountOut); + + /** + * @notice Queries an `addLiquiditySingleTokenExactOut` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param tokenIn Token used to add liquidity + * @param exactBptAmountOut Expected exact amount of pool tokens to receive + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountIn Expected amount of tokens to add + */ + function queryAddLiquiditySingleTokenExactOut( + address pool, + IERC20 tokenIn, + uint256 exactBptAmountOut, + address sender, + bytes memory userData + ) external returns (uint256 amountIn); + + /** + * @notice Queries an `addLiquidityCustom` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param maxAmountsIn Maximum amounts of tokens to be added, sorted in token registration order + * @param minBptAmountOut Expected minimum amount of pool tokens to receive + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountsIn Expected amounts of tokens to add, sorted in token registration order + * @return bptAmountOut Expected amount of pool tokens to receive + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function queryAddLiquidityCustom( + address pool, + uint256[] memory maxAmountsIn, + uint256 minBptAmountOut, + address sender, + bytes memory userData + ) external returns (uint256[] memory amountsIn, uint256 bptAmountOut, bytes memory returnData); + + /** + * @notice Queries a `removeLiquidityProportional` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param exactBptAmountIn Exact amount of pool tokens provided for the query + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountsOut Expected amounts of tokens to receive, sorted in token registration order + */ + function queryRemoveLiquidityProportional( + address pool, + uint256 exactBptAmountIn, + address sender, + bytes memory userData + ) external returns (uint256[] memory amountsOut); + + /** + * @notice Queries a `removeLiquiditySingleTokenExactIn` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param exactBptAmountIn Exact amount of pool tokens provided for the query + * @param tokenOut Token used to remove liquidity + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountOut Expected amount of tokens to receive + */ + function queryRemoveLiquiditySingleTokenExactIn( + address pool, + uint256 exactBptAmountIn, + IERC20 tokenOut, + address sender, + bytes memory userData + ) external returns (uint256 amountOut); + + /** + * @notice Queries a `removeLiquiditySingleTokenExactOut` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param tokenOut Token used to remove liquidity + * @param exactAmountOut Expected exact amount of tokens to receive + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return bptAmountIn Expected amount of pool tokens to burn + */ + function queryRemoveLiquiditySingleTokenExactOut( + address pool, + IERC20 tokenOut, + uint256 exactAmountOut, + address sender, + bytes memory userData + ) external returns (uint256 bptAmountIn); + + /** + * @notice Queries a `removeLiquidityCustom` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param maxBptAmountIn Maximum amount of pool tokens provided + * @param minAmountsOut Expected minimum amounts of tokens to receive, sorted in token registration order + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return bptAmountIn Expected amount of pool tokens to burn + * @return amountsOut Expected amounts of tokens to receive, sorted in token registration order + * @return returnData Arbitrary (optional) data with an encoded response from the pool + */ + function queryRemoveLiquidityCustom( + address pool, + uint256 maxBptAmountIn, + uint256[] memory minAmountsOut, + address sender, + bytes memory userData + ) external returns (uint256 bptAmountIn, uint256[] memory amountsOut, bytes memory returnData); + + /** + * @notice Queries a `removeLiquidityRecovery` operation without actually executing it. + * @param pool Address of the liquidity pool + * @param exactBptAmountIn Exact amount of pool tokens provided for the query + * @return amountsOut Expected amounts of tokens to receive, sorted in token registration order + */ + function queryRemoveLiquidityRecovery( + address pool, + uint256 exactBptAmountIn + ) external returns (uint256[] memory amountsOut); + + /** + * @notice Queries a swap operation specifying an exact input token amount without actually executing it. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountIn Exact amounts of input tokens to send + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountOut Calculated amount of output tokens to be received in exchange for the given input tokens + */ + function querySwapSingleTokenExactIn( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountIn, + address sender, + bytes calldata userData + ) external returns (uint256 amountOut); + + /** + * @notice Queries a swap operation specifying an exact output token amount without actually executing it. + * @param pool Address of the liquidity pool + * @param tokenIn Token to be swapped from + * @param tokenOut Token to be swapped to + * @param exactAmountOut Exact amounts of input tokens to receive + * @param sender The address passed to the operation as the sender. It influences results (e.g., with user-dependent hooks) + * @param userData Additional (optional) data sent with the query request + * @return amountIn Calculated amount of input tokens to be sent in exchange for the requested output tokens + */ + function querySwapSingleTokenExactOut( + address pool, + IERC20 tokenIn, + IERC20 tokenOut, + uint256 exactAmountOut, + address sender, + bytes calldata userData + ) external returns (uint256 amountIn); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISignatureTransfer.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISignatureTransfer.sol new file mode 100644 index 00000000..328322d6 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISignatureTransfer.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IEIP712} from "./IEIP712.sol"; + +/// @title SignatureTransfer +/// @notice Handles ERC20 token transfers through signature based actions +/// @dev Requires user's token approval on the Permit2 contract +interface ISignatureTransfer is IEIP712 { + /// @notice Thrown when the requested amount for a transfer is larger than the permissioned amount + /// @param maxAmount The maximum amount a spender can request to transfer + error InvalidAmount(uint256 maxAmount); + + /// @notice Thrown when the number of tokens permissioned to a spender does not match the number of tokens being transferred + /// @dev If the spender does not need to transfer the number of tokens permitted, the spender can request amount 0 to be transferred + error LengthMismatch(); + + /// @notice Emits an event when the owner successfully invalidates an unordered nonce. + event UnorderedNonceInvalidation(address indexed owner, uint256 word, uint256 mask); + + /// @notice The token and amount details for a transfer signed in the permit transfer signature + struct TokenPermissions { + // ERC20 token address + address token; + // the maximum amount that can be spent + uint256 amount; + } + + /// @notice The signed permit message for a single token transfer + struct PermitTransferFrom { + TokenPermissions permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice Specifies the recipient address and amount for batched transfers. + /// @dev Recipients and amounts correspond to the index of the signed token permissions array. + /// @dev Reverts if the requested amount is greater than the permitted signed amount. + struct SignatureTransferDetails { + // recipient address + address to; + // spender requested amount + uint256 requestedAmount; + } + + /// @notice Used to reconstruct the signed permit message for multiple token transfers + /// @dev Do not need to pass in spender address as it is required that it is msg.sender + /// @dev Note that a user still signs over a spender address + struct PermitBatchTransferFrom { + // the tokens and corresponding amounts permitted for a transfer + TokenPermissions[] permitted; + // a unique value for every token owner's signature to prevent signature replays + uint256 nonce; + // deadline on the permit signature + uint256 deadline; + } + + /// @notice A map from token owner address and a caller specified word index to a bitmap. Used to set bits in the bitmap to prevent against signature replay protection + /// @dev Uses unordered nonces so that permit messages do not need to be spent in a certain order + /// @dev The mapping is indexed first by the token owner, then by an index specified in the nonce + /// @dev It returns a uint256 bitmap + /// @dev The index, or wordPosition is capped at type(uint248).max + function nonceBitmap(address, uint256) external view returns (uint256); + + /// @notice Transfers a token using a signed permit message + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param signature The signature to verify + function permitTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers a token using a signed permit message + /// @notice Includes extra data provided by the caller to verify signature over + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @dev Reverts if the requested amount is greater than the permitted signed amount + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails The spender's requested transfer details for the permitted token + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param signature The signature to verify + function permitTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes calldata signature + ) external; + + /// @notice Transfers multiple tokens using a signed permit message + /// @dev The witness type string must follow EIP712 ordering of nested structs and must include the TokenPermissions type definition + /// @notice Includes extra data provided by the caller to verify signature over + /// @param permit The permit data signed over by the owner + /// @param owner The owner of the tokens to transfer + /// @param transferDetails Specifies the recipient and requested amount for the token transfer + /// @param witness Extra data to include when checking the user signature + /// @param witnessTypeString The EIP-712 type definition for remaining string stub of the typehash + /// @param signature The signature to verify + function permitWitnessTransferFrom( + PermitBatchTransferFrom memory permit, + SignatureTransferDetails[] calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + /// @notice Invalidates the bits specified in mask for the bitmap at the word position + /// @dev The wordPos is maxed at type(uint248).max + /// @param wordPos A number to index the nonceBitmap at + /// @param mask A bitmap masked against msg.sender's current bitmap at the word position + function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISwapFeePercentageBounds.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISwapFeePercentageBounds.sol new file mode 100644 index 00000000..69ad4d55 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ISwapFeePercentageBounds.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/** + * @notice Return the minimum/maximum swap fee percentages for a pool. + * @dev The Vault does not enforce bounds on swap fee percentages; `IBasePool` implements this interface to ensure + * that new pool developers think about and set these bounds according to their specific pool type. + * + * A minimum swap fee might be necessary to ensure mathematical soundness (e.g., Weighted Pools, which use the power + * function in the invariant). A maximum swap fee is general protection for users. With no limits at the Vault level, + * a pool could specify a 100% swap fee, effectively disabling trading. Though there are some use cases, such as + * LVR/MEV strategies, where a 100% fee makes sense. + */ +interface ISwapFeePercentageBounds { + /// @return minimumSwapFeePercentage The minimum swap fee percentage for a pool + function getMinimumSwapFeePercentage() external view returns (uint256); + + /// @return maximumSwapFeePercentage The maximum swap fee percentage for a pool + function getMaximumSwapFeePercentage() external view returns (uint256); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IUnbalancedLiquidityInvariantRatioBounds.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IUnbalancedLiquidityInvariantRatioBounds.sol new file mode 100644 index 00000000..d7f688a3 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IUnbalancedLiquidityInvariantRatioBounds.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/** + * @notice Return the minimum/maximum invariant ratios allowed during an unbalanced liquidity operation. + * @dev The Vault does not enforce any "baseline" bounds on invariant ratios, since such bounds are highly specific + * and dependent on the math of each pool type. Instead, the Vault reads invariant ratio bounds from the pools. + * `IBasePool` implements this interface to ensure that new pool developers think about and set these bounds according + * to their pool type's math. + * + * For instance, Balancer Weighted Pool math involves exponentiation (the `pow` function), which uses natural + * logarithms and a discrete Taylor series expansion to compute x^y values for the 18-decimal floating point numbers + * used in all Vault computations. See `LogExpMath` and `WeightedMath` for a derivation of the bounds for these pools. + */ +interface IUnbalancedLiquidityInvariantRatioBounds { + /// @return minimumInvariantRatio The minimum invariant ratio for a pool during unbalanced remove liquidity + function getMinimumInvariantRatio() external view returns (uint256); + + /// @return maximumInvariantRatio The maximum invariant ratio for a pool during unbalanced add liquidity + function getMaximumInvariantRatio() external view returns (uint256); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultMainMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultMainMock.sol new file mode 100644 index 00000000..cd4f7768 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultMainMock.sol @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { IRateProvider } from "./IRateProvider.sol"; +import "./VaultTypes.sol"; + +interface IVaultMainMock { + function getPoolFactoryMock() external view returns (address); + + function burnERC20(address token, address from, uint256 amount) external; + + function mintERC20(address token, address to, uint256 amount) external; + + function manualRegisterPool(address pool, IERC20[] memory tokens) external; + + function manualRegisterPoolWithSwapFee(address pool, IERC20[] memory tokens, uint256 swapFeePercentage) external; + + function manualRegisterPoolPassThruTokens(address pool, IERC20[] memory tokens) external; + + function manualRegisterPoolAtTimestamp( + address pool, + IERC20[] memory tokens, + uint32 timestamp, + PoolRoleAccounts memory roleAccounts + ) external; + + function manualSetPoolRegistered(address pool, bool status) external; + + function manualSetInitializedPool(address pool, bool isPoolInitialized) external; + + function manualSetPoolPaused(address, bool) external; + + function manualSetPoolPauseWindowEndTime(address, uint32) external; + + function manualSetVaultPaused(bool) external; + + function manualSetVaultState(bool, bool) external; + + function manualSetPoolTokenInfo(address, TokenConfig[] memory) external; + + function manualSetPoolTokenInfo(address, IERC20[] memory, TokenInfo[] memory) external; + + function manualSetPoolConfig(address pool, PoolConfig memory config) external; + + function manualSetHooksConfig(address pool, HooksConfig memory config) external; + + function manualSetStaticSwapFeePercentage(address pool, uint256 value) external; + + /// @dev Does not check the value against any min/max limits normally enforced by the pool. + function manualUnsafeSetStaticSwapFeePercentage(address pool, uint256 value) external; + + function manualSetPoolTokens(address pool, IERC20[] memory tokens) external; + + function manualSetPoolTokensAndBalances(address, IERC20[] memory, uint256[] memory, uint256[] memory) external; + + function manualSetPoolBalances(address, uint256[] memory, uint256[] memory) external; + + function manualSetPoolConfigBits(address pool, PoolConfigBits config) external; + + function mockIsUnlocked() external view; + + function mockWithInitializedPool(address pool) external view; + + function ensurePoolNotPaused(address) external view; + + function ensureUnpausedAndGetVaultState(address) external view returns (VaultState memory); + + function internalGetBufferUnderlyingImbalance(IERC4626 wrappedToken) external view returns (int256); + + function internalGetBufferWrappedImbalance(IERC4626 wrappedToken) external view returns (int256); + + function getBufferTokenBalancesBytes(IERC4626 wrappedToken) external view returns (bytes32); + + function recoveryModeExit(address pool) external view; + + function loadPoolDataUpdatingBalancesAndYieldFees( + address pool, + Rounding roundingDirection + ) external returns (PoolData memory); + + function loadPoolDataUpdatingBalancesAndYieldFeesReentrancy( + address pool, + Rounding roundingDirection + ) external returns (PoolData memory); + + function manualWritePoolBalancesToStorage(address pool, PoolData memory poolData) external; + + function getRawBalances(address pool) external view returns (uint256[] memory balancesRaw); + + function getLastLiveBalances(address pool) external view returns (uint256[] memory lastBalancesLiveScaled18); + + function updateLiveTokenBalanceInPoolData( + PoolData memory poolData, + uint256 newRawBalance, + Rounding roundingDirection, + uint256 tokenIndex + ) external pure returns (PoolData memory); + + function computeYieldFeesDue( + PoolData memory poolData, + uint256 lastLiveBalance, + uint256 tokenIndex, + uint256 aggregateYieldFeePercentage + ) external pure returns (uint256); + + function guardedCheckEntered() external; + + function unguardedCheckNotEntered() external view; + + // Convenience functions for constructing TokenConfig arrays + + function buildTokenConfig(IERC20[] memory tokens) external view returns (TokenConfig[] memory tokenConfig); + + /// @dev Infers TokenType (STANDARD or WITH_RATE) from the presence or absence of the rate provider. + function buildTokenConfig( + IERC20[] memory tokens, + IRateProvider[] memory rateProviders + ) external view returns (TokenConfig[] memory tokenConfig); + + /// @dev Infers TokenType (STANDARD or WITH_RATE) from the presence or absence of the rate provider. + function buildTokenConfig( + IERC20[] memory tokens, + IRateProvider[] memory rateProviders, + bool[] memory yieldFeeFlags + ) external view returns (TokenConfig[] memory tokenConfig); + + function buildTokenConfig( + IERC20[] memory tokens, + TokenType[] memory tokenTypes, + IRateProvider[] memory rateProviders, + bool[] memory yieldFeeFlags + ) external view returns (TokenConfig[] memory tokenConfig); + + function accountDelta(IERC20 token, int256 delta) external; + + function supplyCredit(IERC20 token, uint256 credit) external; + + function takeDebt(IERC20 token, uint256 debt) external; + + function manualSetAccountDelta(IERC20 token, int256 delta) external; + + function manualSetNonZeroDeltaCount(uint256 deltaCount) external; + + function manualSetReservesOf(IERC20 token, uint256 reserves) external; + + function manualInternalSwap( + VaultSwapParams memory vaultSwapParams, + SwapState memory state, + PoolData memory poolData + ) + external + returns ( + uint256 amountCalculatedRaw, + uint256 amountCalculatedScaled18, + uint256 amountIn, + uint256 amountOut, + VaultSwapParams memory, + SwapState memory, + PoolData memory + ); + + function manualReentrancySwap( + VaultSwapParams memory vaultSwapParams, + SwapState memory state, + PoolData memory poolData + ) external; + + function manualGetAggregateSwapFeeAmount(address pool, IERC20 token) external view returns (uint256); + + function manualGetAggregateYieldFeeAmount(address pool, IERC20 token) external view returns (uint256); + + function manualSetAggregateSwapFeeAmount(address pool, IERC20 token, uint256 value) external; + + function manualSetAggregateYieldFeeAmount(address pool, IERC20 token, uint256 value) external; + + function manualSetAggregateSwapFeePercentage(address pool, uint256 value) external; + + function manualSetAggregateYieldFeePercentage(address pool, uint256 value) external; + + function manualBuildPoolSwapParams( + VaultSwapParams memory vaultSwapParams, + SwapState memory state, + PoolData memory poolData + ) external view returns (PoolSwapParams memory); + + function manualComputeAndChargeAggregateSwapFees( + PoolData memory poolData, + uint256 totalSwapFeeAmountScaled18, + address pool, + IERC20 token, + uint256 index + ) external returns (uint256 totalSwapFeeAmountRaw, uint256 aggregateSwapFeeAmountRaw); + + function manualUpdatePoolDataLiveBalancesAndRates( + address pool, + PoolData memory poolData, + Rounding roundingDirection + ) external view returns (PoolData memory); + + function manualAddLiquidity( + PoolData memory poolData, + AddLiquidityParams memory params, + uint256[] memory maxAmountsInScaled18 + ) + external + returns ( + PoolData memory updatedPoolData, + uint256[] memory amountsInRaw, + uint256[] memory amountsInScaled18, + uint256 bptAmountOut, + bytes memory returnData + ); + + function manualReentrancyAddLiquidity( + PoolData memory poolData, + AddLiquidityParams memory params, + uint256[] memory maxAmountsInScaled18 + ) external; + + function forceUnlock() external; + + function forceLock() external; + + function manualRemoveLiquidity( + PoolData memory poolData, + RemoveLiquidityParams memory params, + uint256[] memory minAmountsOutScaled18 + ) + external + returns ( + PoolData memory updatedPoolData, + uint256 bptAmountIn, + uint256[] memory amountsOutRaw, + uint256[] memory amountsOutScaled18, + bytes memory returnData + ); + + function manualReentrancyRemoveLiquidity( + PoolData memory poolData, + RemoveLiquidityParams memory params, + uint256[] memory minAmountsOutScaled18 + ) external; + + function manualSettleWrap( + IERC20 underlyingToken, + IERC20 wrappedToken, + uint256 underlyingHint, + uint256 wrappedHint + ) external; + + function manualSettleUnwrap( + IERC20 underlyingToken, + IERC20 wrappedToken, + uint256 underlyingHint, + uint256 wrappedHint + ) external; + + function manualTransfer(IERC20 token, address to, uint256 amount) external; + + function manualGetPoolConfigBits(address pool) external view returns (PoolConfigBits); + + function manualErc4626BufferWrapOrUnwrapReentrancy( + BufferWrapOrUnwrapParams memory params + ) external returns (uint256 amountCalculatedRaw, uint256 amountInRaw, uint256 amountOutRaw); + + function manualSetBufferAsset(IERC4626 wrappedToken, address underlyingToken) external; + + function manualSetBufferOwnerShares(IERC4626 wrappedToken, address owner, uint256 shares) external; + + function manualSetBufferTotalShares(IERC4626 wrappedToken, uint256 shares) external; + + function manualSetBufferBalances(IERC4626 wrappedToken, uint256 underlyingAmount, uint256 wrappedAmount) external; + + function manualSettleReentrancy(IERC20 token) external returns (uint256 paid); + + function manualSendToReentrancy(IERC20 token, address to, uint256 amount) external; + + function manualFindTokenIndex(IERC20[] memory tokens, IERC20 token) external pure returns (uint256 index); + + function manualSetAddLiquidityCalledFlag(address pool, bool flag) external; + + function manualSetPoolCreator(address pool, address newPoolCreator) external; + + function ensureValidTradeAmount(uint256 tradeAmount) external view; + + function ensureValidSwapAmount(uint256 tradeAmount) external view; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVersion.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVersion.sol new file mode 100644 index 00000000..08a34765 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVersion.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/// @notice Simple interface to retrieve the version of a deployed contract. +interface IVersion { + /** + * @notice Return arbitrary text representing the version of a contract. + * @dev For standard Balancer contracts, returns a JSON representation of the contract version containing name, + * version number and task ID. See real examples in the deployment repo; local tests just use plain text strings. + * + * @return version The version string corresponding to the current deployed contract + */ + function version() external view returns (string memory); +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IWETH.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IWETH.sol new file mode 100644 index 00000000..387e4856 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IWETH.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @notice Interface for WETH9. + * See https://github.com/gnosis/canonical-weth/blob/0dd1ea3e295eef916d0c6223ec63141137d22d67/contracts/WETH9.sol + */ +interface IWETH is IERC20 { + /// @dev "wrap" native ETH to WETH. + function deposit() external payable; + + /// @dev "unwrap" WETH to native ETH. + function withdraw(uint256 amount) external; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/InputHelpers.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/InputHelpers.sol new file mode 100644 index 00000000..c8b8700f --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/InputHelpers.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { CastingHelpers } from "./CastingHelpers.sol"; + +library InputHelpers { + /// @notice Arrays passed to a function and intended to be parallel have different lengths. + error InputLengthMismatch(); + + /** + * @notice More than one non-zero value was given for a single token operation. + * @dev Input arrays for single token add/remove liquidity operations are expected to have only one non-zero value, + * corresponding to the token being added or removed. This error results if there are multiple non-zero entries. + */ + error MultipleNonZeroInputs(); + + /** + * @notice No valid input was given for a single token operation. + * @dev Input arrays for single token add/remove liquidity operations are expected to have one non-zero value, + * corresponding to the token being added or removed. This error results if all entries are zero. + */ + error AllZeroInputs(); + + /** + * @notice The tokens supplied to an array argument were not sorted in numerical order. + * @dev Tokens are not sorted by address on registration. This is an optimization so that off-chain processes can + * predict the token order without having to query the Vault. (It is also legacy v2 behavior.) + */ + error TokensNotSorted(); + + function ensureInputLengthMatch(uint256 a, uint256 b) internal pure { + if (a != b) { + revert InputLengthMismatch(); + } + } + + function ensureInputLengthMatch(uint256 a, uint256 b, uint256 c) internal pure { + if (a != b || b != c) { + revert InputLengthMismatch(); + } + } + + // Find the single non-zero input; revert if there is not exactly one such value. + function getSingleInputIndex(uint256[] memory maxAmountsIn) internal pure returns (uint256 inputIndex) { + uint256 length = maxAmountsIn.length; + inputIndex = length; + + for (uint256 i = 0; i < length; ++i) { + if (maxAmountsIn[i] != 0) { + if (inputIndex != length) { + revert MultipleNonZeroInputs(); + } + inputIndex = i; + } + } + + if (inputIndex >= length) { + revert AllZeroInputs(); + } + + return inputIndex; + } + + /** + * @dev Sort an array of tokens, mutating in place (and also returning them). + * This assumes the tokens have been (or will be) validated elsewhere for length + * and non-duplication. All this does is the sorting. + * + * A bubble sort should be gas- and bytecode-efficient enough for such small arrays. + * Could have also done "manual" comparisons for each of the cases, but this is + * about the same number of operations, and more concise. + * + * This is less efficient for larger token count (i.e., above 4), but such pools should + * be rare. And in any case, sorting is only done on-chain in test code. + */ + function sortTokens(IERC20[] memory tokens) internal pure returns (IERC20[] memory) { + for (uint256 i = 0; i < tokens.length - 1; ++i) { + for (uint256 j = 0; j < tokens.length - i - 1; ++j) { + if (tokens[j] > tokens[j + 1]) { + // Swap if they're out of order. + (tokens[j], tokens[j + 1]) = (tokens[j + 1], tokens[j]); + } + } + } + + return tokens; + } + + /// @dev Ensure an array of tokens is sorted. As above, does not validate length or uniqueness. + function ensureSortedTokens(IERC20[] memory tokens) internal pure { + IERC20 previous = tokens[0]; + + for (uint256 i = 1; i < tokens.length; ++i) { + IERC20 current = tokens[i]; + + if (previous > current) { + revert TokensNotSorted(); + } + + previous = current; + } + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/PackedTokenBalance.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/PackedTokenBalance.sol new file mode 100644 index 00000000..1614fd55 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/PackedTokenBalance.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +/** + * @notice This library represents a data structure for packing a token's current raw and derived balances. A derived + * balance can be the "last" live balance scaled18 of the raw token, or the balance of the wrapped version of the + * token in a vault buffer, among others. + * + * @dev We could use a Solidity struct to pack balance values together in a single storage slot, but unfortunately + * Solidity only allows for structs to live in either storage, calldata or memory. Because a memory struct still takes + * up a slot in the stack (to store its memory location), and because the entire balance fits in a single stack slot + * (two 128 bit values), using memory is strictly less gas performant. Therefore, we do manual packing and unpacking. + * + * We could also use custom types now, but given the simplicity here, and the existing EnumerableMap type, it seemed + * easier to leave it as a bytes32. + */ +library PackedTokenBalance { + // The 'rawBalance' portion of the balance is stored in the least significant 128 bits of a 256 bit word, while the + // The 'derivedBalance' part uses the remaining 128 bits. + uint256 private constant _MAX_BALANCE = 2 ** (128) - 1; + + /// @notice One of the balances is above the maximum value that can be stored. + error BalanceOverflow(); + + function getBalanceRaw(bytes32 balance) internal pure returns (uint256) { + return uint256(balance) & _MAX_BALANCE; + } + + function getBalanceDerived(bytes32 balance) internal pure returns (uint256) { + return uint256(balance >> 128) & _MAX_BALANCE; + } + + /// @dev Sets only the raw balance of balances and returns the new bytes32 balance + function setBalanceRaw(bytes32 balance, uint256 newBalanceRaw) internal pure returns (bytes32) { + return toPackedBalance(newBalanceRaw, getBalanceDerived(balance)); + } + + /// @dev Sets only the raw balance of balances and returns the new bytes32 balance + function setBalanceDerived(bytes32 balance, uint256 newBalanceDerived) internal pure returns (bytes32) { + return toPackedBalance(getBalanceRaw(balance), newBalanceDerived); + } + + /// @dev Validates the size of `balanceRaw` and `balanceDerived`, then returns a packed balance bytes32. + function toPackedBalance(uint256 balanceRaw, uint256 balanceDerived) internal pure returns (bytes32) { + if (balanceRaw > _MAX_BALANCE || balanceDerived > _MAX_BALANCE) { + revert BalanceOverflow(); + } + + return _pack(balanceRaw, balanceDerived); + } + + /// @dev Decode and fetch both balances. + function fromPackedBalance(bytes32 balance) internal pure returns (uint256 balanceRaw, uint256 balanceDerived) { + return (getBalanceRaw(balance), getBalanceDerived(balance)); + } + + /// @dev Packs two uint128 values into a packed balance bytes32. It does not check balance sizes. + function _pack(uint256 leastSignificant, uint256 mostSignificant) private pure returns (bytes32) { + return bytes32((mostSignificant << 128) + leastSignificant); + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ReentrancyGuardTransient.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ReentrancyGuardTransient.sol new file mode 100644 index 00000000..6f2a1658 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ReentrancyGuardTransient.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import { StorageSlotExtension } from "./StorageSlotExtension.sol"; + +/** + * @notice Variant of {ReentrancyGuard} that uses transient storage. + * @dev NOTE: This variant only works on networks where EIP-1153 is available. + */ +abstract contract ReentrancyGuardTransient { + using StorageSlotExtension for *; + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant _REENTRANCY_GUARD_STORAGE = + 0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00; + + /// @notice Unauthorized reentrant call. + error ReentrancyGuardReentrantCall(); + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + _nonReentrantBefore(); + _; + _nonReentrantAfter(); + } + + function _nonReentrantBefore() private { + // On the first call to nonReentrant, _status will be NOT_ENTERED + if (_reentrancyGuardEntered()) { + revert ReentrancyGuardReentrantCall(); + } + + // Any calls to nonReentrant after this point will fail + _REENTRANCY_GUARD_STORAGE.asBoolean().tstore(true); + } + + function _nonReentrantAfter() private { + _REENTRANCY_GUARD_STORAGE.asBoolean().tstore(false); + } + + /** + * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a + * `nonReentrant` function in the call stack. + */ + function _reentrancyGuardEntered() internal view returns (bool) { + return _REENTRANCY_GUARD_STORAGE.asBoolean().tload(); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/RevertCodec.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/RevertCodec.sol new file mode 100644 index 00000000..beb5327a --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/RevertCodec.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +// solhint-disable no-inline-assembly + +/// @notice Support `quoteAndRevert`: a v2-style query which always reverts, and returns the result in the return data. +library RevertCodec { + /** + * @notice On success of the primary operation in a `quoteAndRevert`, this error is thrown with the return data. + * @param result The result of the query operation + */ + error Result(bytes result); + + /// @notice Handle the "reverted without a reason" case (i.e., no return data). + error ErrorSelectorNotFound(); + + function catchEncodedResult(bytes memory resultRaw) internal pure returns (bytes memory) { + bytes4 errorSelector = RevertCodec.parseSelector(resultRaw); + if (errorSelector != Result.selector) { + // Bubble up error message if the revert reason is not the expected one. + RevertCodec.bubbleUpRevert(resultRaw); + } + + uint256 resultRawLength = resultRaw.length; + assembly ("memory-safe") { + resultRaw := add(resultRaw, 0x04) // Slice the sighash + mstore(resultRaw, sub(resultRawLength, 4)) // Set proper length + } + + return abi.decode(resultRaw, (bytes)); + } + + /// @dev Returns the first 4 bytes in an array, reverting if the length is < 4. + function parseSelector(bytes memory callResult) internal pure returns (bytes4 errorSelector) { + if (callResult.length < 4) { + revert ErrorSelectorNotFound(); + } + assembly ("memory-safe") { + errorSelector := mload(add(callResult, 0x20)) // Load the first 4 bytes from data (skip length offset) + } + } + + /// @dev Taken from Openzeppelin's Address. + function bubbleUpRevert(bytes memory returnData) internal pure { + // Look for revert reason and bubble it up if present. + if (returnData.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly. + + assembly ("memory-safe") { + let return_data_size := mload(returnData) + revert(add(32, returnData), return_data_size) + } + } else { + revert ErrorSelectorNotFound(); + } + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ScalingHelpers.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ScalingHelpers.sol new file mode 100644 index 00000000..067e43c0 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/ScalingHelpers.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import { FixedPoint } from "./FixedPoint.sol"; +import { InputHelpers } from "./InputHelpers.sol"; + +/** + * @notice Helper functions to apply/undo token decimal and rate adjustments, rounding in the direction indicated. + * @dev To simplify Pool logic, all token balances and amounts are normalized to behave as if the token had + * 18 decimals. When comparing DAI (18 decimals) and USDC (6 decimals), 1 USDC and 1 DAI would both be + * represented as 1e18. This allows us to not consider differences in token decimals in the internal Pool + * math, simplifying it greatly. + * + * The Vault does not support tokens with more than 18 decimals (see `_MAX_TOKEN_DECIMALS` in `VaultStorage`), + * or tokens that do not implement `IERC20Metadata.decimals`. + * + * These helpers can also be used to scale amounts by other 18-decimal floating point values, such as rates. + */ +library ScalingHelpers { + using FixedPoint for *; + using ScalingHelpers for uint256; + + /*************************************************************************** + Single Value Functions + ***************************************************************************/ + + /** + * @notice Applies `scalingFactor` and `tokenRate` to `amount`. + * @dev This may result in a larger or equal value, depending on whether it needed scaling/rate adjustment or not. + * The result is rounded down. + * + * @param amount Amount to be scaled up to 18 decimals + * @param scalingFactor The token decimal scaling factor, `10^(18-tokenDecimals)` + * @param tokenRate The token rate scaling factor + * @return result The final 18-decimal precision result, rounded down + */ + function toScaled18ApplyRateRoundDown( + uint256 amount, + uint256 scalingFactor, + uint256 tokenRate + ) internal pure returns (uint256) { + return (amount * scalingFactor).mulDown(tokenRate); + } + + /** + * @notice Applies `scalingFactor` and `tokenRate` to `amount`. + * @dev This may result in a larger or equal value, depending on whether it needed scaling/rate adjustment or not. + * The result is rounded up. + * + * @param amount Amount to be scaled up to 18 decimals + * @param scalingFactor The token decimal scaling factor, `10^(18-tokenDecimals)` + * @param tokenRate The token rate scaling factor + * @return result The final 18-decimal precision result, rounded up + */ + function toScaled18ApplyRateRoundUp( + uint256 amount, + uint256 scalingFactor, + uint256 tokenRate + ) internal pure returns (uint256) { + return (amount * scalingFactor).mulUp(tokenRate); + } + + /** + * @notice Reverses the `scalingFactor` and `tokenRate` applied to `amount`. + * @dev This may result in a smaller or equal value, depending on whether it needed scaling/rate adjustment or not. + * The result is rounded down. + * + * @param amount Amount to be scaled down to native token decimals + * @param scalingFactor The token decimal scaling factor, `10^(18-tokenDecimals)` + * @param tokenRate The token rate scaling factor + * @return result The final native decimal result, rounded down + */ + function toRawUndoRateRoundDown( + uint256 amount, + uint256 scalingFactor, + uint256 tokenRate + ) internal pure returns (uint256) { + // Do division last. Scaling factor is not a FP18, but a FP18 normalized by FP(1). + // `scalingFactor * tokenRate` is a precise FP18, so there is no rounding direction here. + return FixedPoint.divDown(amount, scalingFactor * tokenRate); + } + + /** + * @notice Reverses the `scalingFactor` and `tokenRate` applied to `amount`. + * @dev This may result in a smaller or equal value, depending on whether it needed scaling/rate adjustment or not. + * The result is rounded up. + * + * @param amount Amount to be scaled down to native token decimals + * @param scalingFactor The token decimal scaling factor, `10^(18-tokenDecimals)` + * @param tokenRate The token rate scaling factor + * @return result The final native decimal result, rounded up + */ + function toRawUndoRateRoundUp( + uint256 amount, + uint256 scalingFactor, + uint256 tokenRate + ) internal pure returns (uint256) { + // Do division last. Scaling factor is not a FP18, but a FP18 normalized by FP(1). + // `scalingFactor * tokenRate` is a precise FP18, so there is no rounding direction here. + return FixedPoint.divUp(amount, scalingFactor * tokenRate); + } + + /*************************************************************************** + Array Functions + ***************************************************************************/ + + function copyToArray(uint256[] memory from, uint256[] memory to) internal pure { + uint256 length = from.length; + InputHelpers.ensureInputLengthMatch(length, to.length); + + for (uint256 i = 0; i < length; ++i) { + to[i] = from[i]; + } + } + + /** + * @notice Same as `toScaled18ApplyRateRoundDown`, but for an entire array. + * @dev This function does not return anything, but instead *mutates* the `amounts` array. + * @param amounts Amounts to be scaled up to 18 decimals, sorted in token registration order + * @param scalingFactors The token decimal scaling factors, sorted in token registration order + * @param tokenRates The token rate scaling factors, sorted in token registration order + */ + function toScaled18ApplyRateRoundDownArray( + uint256[] memory amounts, + uint256[] memory scalingFactors, + uint256[] memory tokenRates + ) internal pure { + uint256 length = amounts.length; + InputHelpers.ensureInputLengthMatch(length, scalingFactors.length, tokenRates.length); + + for (uint256 i = 0; i < length; ++i) { + amounts[i] = amounts[i].toScaled18ApplyRateRoundDown(scalingFactors[i], tokenRates[i]); + } + } + + /** + * @notice Same as `toScaled18ApplyRateRoundDown`, but returns a new array, leaving the original intact. + * @param amounts Amounts to be scaled up to 18 decimals, sorted in token registration order + * @param scalingFactors The token decimal scaling factors, sorted in token registration order + * @param tokenRates The token rate scaling factors, sorted in token registration order + * @return results The final 18 decimal results, sorted in token registration order, rounded down + */ + function copyToScaled18ApplyRateRoundDownArray( + uint256[] memory amounts, + uint256[] memory scalingFactors, + uint256[] memory tokenRates + ) internal pure returns (uint256[] memory) { + uint256 length = amounts.length; + InputHelpers.ensureInputLengthMatch(length, scalingFactors.length, tokenRates.length); + uint256[] memory amountsScaled18 = new uint256[](length); + + for (uint256 i = 0; i < length; ++i) { + amountsScaled18[i] = amounts[i].toScaled18ApplyRateRoundDown(scalingFactors[i], tokenRates[i]); + } + + return amountsScaled18; + } + + /** + * @notice Same as `toScaled18ApplyRateRoundUp`, but for an entire array. + * @dev This function does not return anything, but instead *mutates* the `amounts` array. + * @param amounts Amounts to be scaled up to 18 decimals, sorted in token registration order + * @param scalingFactors The token decimal scaling factors, sorted in token registration order + * @param tokenRates The token rate scaling factors, sorted in token registration order + */ + function toScaled18ApplyRateRoundUpArray( + uint256[] memory amounts, + uint256[] memory scalingFactors, + uint256[] memory tokenRates + ) internal pure { + uint256 length = amounts.length; + InputHelpers.ensureInputLengthMatch(length, scalingFactors.length, tokenRates.length); + + for (uint256 i = 0; i < length; ++i) { + amounts[i] = amounts[i].toScaled18ApplyRateRoundUp(scalingFactors[i], tokenRates[i]); + } + } + + /** + * @notice Same as `toScaled18ApplyRateRoundUp`, but returns a new array, leaving the original intact. + * @param amounts Amounts to be scaled up to 18 decimals, sorted in token registration order + * @param scalingFactors The token decimal scaling factors, sorted in token registration order + * @param tokenRates The token rate scaling factors, sorted in token registration order + * @return The final 18 decimal results, sorted in token registration order, rounded up + */ + function copyToScaled18ApplyRateRoundUpArray( + uint256[] memory amounts, + uint256[] memory scalingFactors, + uint256[] memory tokenRates + ) internal pure returns (uint256[] memory) { + uint256 length = amounts.length; + InputHelpers.ensureInputLengthMatch(length, scalingFactors.length, tokenRates.length); + uint256[] memory amountsScaled18 = new uint256[](length); + + for (uint256 i = 0; i < length; ++i) { + amountsScaled18[i] = amounts[i].toScaled18ApplyRateRoundUp(scalingFactors[i], tokenRates[i]); + } + + return amountsScaled18; + } + + /** + * @notice Rounds up a rate informed by a rate provider. + * @dev Rates calculated by an external rate provider have rounding errors. Intuitively, a rate provider + * rounds the rate down so the pool math is executed with conservative amounts. However, when upscaling or + * downscaling the amount out, the rate should be rounded up to make sure the amounts scaled are conservative. + */ + function computeRateRoundUp(uint256 rate) internal pure returns (uint256) { + uint256 roundedRate; + // If rate is divisible by FixedPoint.ONE, roundedRate and rate will be equal. It means that rate has 18 zeros, + // so there's no rounding issue and the rate should not be rounded up. + unchecked { + roundedRate = (rate / FixedPoint.ONE) * FixedPoint.ONE; + } + return roundedRate == rate ? rate : rate + 1; + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/SlotDerivation.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/SlotDerivation.sol new file mode 100644 index 00000000..71238b27 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/SlotDerivation.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/SlotDerivation.js. + +// Taken from https://raw.githubusercontent.com/Amxx/openzeppelin-contracts/ce497cb05ca05bb9aa2b86ec1d99e6454e7ab2e9/contracts/utils/SlotDerivation.sol + +pragma solidity ^0.8.20; + +/** + * @notice Library for computing storage (and transient storage) locations from namespaces and deriving slots + * corresponding to standard patterns. + * @dev The derivation method for array and mapping matches the storage layout used by the solidity language/compiler. + * + * See https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_storage.html#mappings-and-dynamic-arrays[Solidity docs for mappings and dynamic arrays.]. + * + * Example usage: + * ```solidity + * contract Example { + * // Add the library methods + * using StorageSlot for bytes32; + * using SlotDerivation for bytes32; + * + * // Declare a namespace + * string private constant _NAMESPACE = "" // eg. OpenZeppelin.Slot + * + * function setValueInNamespace(uint256 key, address newValue) internal { + * _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value = newValue; + * } + * + * function getValueInNamespace(uint256 key) internal view returns (address) { + * return _NAMESPACE.erc7201Slot().deriveMapping(key).getAddressSlot().value; + * } + * } + * ``` + * + * TIP: Consider using this library along with {StorageSlot}. + * + * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking + * upgrade safety will ignore the slots accessed through this library. + */ +library SlotDerivation { + /// @dev Derive an ERC-7201 slot from a string (namespace). + function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1)) + slot := and(keccak256(0x00, 0x20), not(0xff)) + } + } + + /// @dev Add an offset to a slot to get the n-th element of a structure or an array. + function offset(bytes32 slot, uint256 pos) internal pure returns (bytes32 result) { + unchecked { + return bytes32(uint256(slot) + pos); + } + } + + /// @dev Derive the location of the first element in an array from the slot where the length is stored. + function deriveArray(bytes32 slot) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, slot) + result := keccak256(0x00, 0x20) + } + } + + /// @dev Derive the location of a mapping element from the key. + function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /// @dev Derive the location of a mapping element from the key. + function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /// @dev Derive the location of a mapping element from the key. + function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /// @dev Derive the location of a mapping element from the key. + function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /// @dev Derive the location of a mapping element from the key. + function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, key) + mstore(0x20, slot) + result := keccak256(0x00, 0x40) + } + } + + /// @dev Derive the location of a mapping element from the key. + function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + let length := mload(key) + let begin := add(key, 0x20) + let end := add(begin, length) + let cache := mload(end) + mstore(end, slot) + result := keccak256(begin, add(length, 0x20)) + mstore(end, cache) + } + } + + /// @dev Derive the location of a mapping element from the key. + function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) { + /// @solidity memory-safe-assembly + assembly { + let length := mload(key) + let begin := add(key, 0x20) + let end := add(begin, length) + let cache := mload(end) + mstore(end, slot) + result := keccak256(begin, add(length, 0x20)) + mstore(end, cache) + } + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/StorageSlotExtension.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/StorageSlotExtension.sol new file mode 100644 index 00000000..9f22034f --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/StorageSlotExtension.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +/** + * @notice Library for reading and writing primitive types to specific storage slots. Based on OpenZeppelin; just adding support for int256. + * @dev TIP: Consider using this library along with {SlotDerivation}. + */ +library StorageSlotExtension { + struct Int256Slot { + int256 value; + } + + /// @dev Returns an `Int256Slot` with member `value` located at `slot`. + function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /// @dev Custom type that represents a slot holding an address. + type AddressSlotType is bytes32; + + /// @dev Cast an arbitrary slot to a AddressSlotType. + function asAddress(bytes32 slot) internal pure returns (AddressSlotType) { + return AddressSlotType.wrap(slot); + } + + /// @dev Custom type that represents a slot holding a boolean. + type BooleanSlotType is bytes32; + + /// @dev Cast an arbitrary slot to a BooleanSlotType. + function asBoolean(bytes32 slot) internal pure returns (BooleanSlotType) { + return BooleanSlotType.wrap(slot); + } + + /// @dev Custom type that represents a slot holding a bytes32. + type Bytes32SlotType is bytes32; + + /// @dev Cast an arbitrary slot to a Bytes32SlotType. + function asBytes32(bytes32 slot) internal pure returns (Bytes32SlotType) { + return Bytes32SlotType.wrap(slot); + } + + /// @dev Custom type that represents a slot holding a uint256. + type Uint256SlotType is bytes32; + + /// @dev Cast an arbitrary slot to a Uint256SlotType. + function asUint256(bytes32 slot) internal pure returns (Uint256SlotType) { + return Uint256SlotType.wrap(slot); + } + + /// @dev Custom type that represents a slot holding an int256. + type Int256SlotType is bytes32; + + /// @dev Cast an arbitrary slot to an Int256SlotType. + function asInt256(bytes32 slot) internal pure returns (Int256SlotType) { + return Int256SlotType.wrap(slot); + } + + /// @dev Load the value held at location `slot` in transient storage. + function tload(AddressSlotType slot) internal view returns (address value) { + /// @solidity memory-safe-assembly + assembly { + value := tload(slot) + } + } + + /// @dev Store `value` at location `slot` in transient storage. + function tstore(AddressSlotType slot, address value) internal { + /// @solidity memory-safe-assembly + assembly { + tstore(slot, value) + } + } + + /// @dev Load the value held at location `slot` in transient storage. + function tload(BooleanSlotType slot) internal view returns (bool value) { + /// @solidity memory-safe-assembly + assembly { + value := tload(slot) + } + } + + /// @dev Store `value` at location `slot` in transient storage. + function tstore(BooleanSlotType slot, bool value) internal { + /// @solidity memory-safe-assembly + assembly { + tstore(slot, value) + } + } + + /// @dev Load the value held at location `slot` in transient storage. + function tload(Bytes32SlotType slot) internal view returns (bytes32 value) { + /// @solidity memory-safe-assembly + assembly { + value := tload(slot) + } + } + + /// @dev Store `value` at location `slot` in transient storage. + function tstore(Bytes32SlotType slot, bytes32 value) internal { + /// @solidity memory-safe-assembly + assembly { + tstore(slot, value) + } + } + + /// @dev Load the value held at location `slot` in transient storage. + function tload(Uint256SlotType slot) internal view returns (uint256 value) { + /// @solidity memory-safe-assembly + assembly { + value := tload(slot) + } + } + + /// @dev Store `value` at location `slot` in transient storage. + function tstore(Uint256SlotType slot, uint256 value) internal { + /// @solidity memory-safe-assembly + assembly { + tstore(slot, value) + } + } + + /// @dev Load the value held at location `slot` in transient storage. + function tload(Int256SlotType slot) internal view returns (int256 value) { + /// @solidity memory-safe-assembly + assembly { + value := tload(slot) + } + } + + /// @dev Store `value` at location `slot` in transient storage. + function tstore(Int256SlotType slot, int256 value) internal { + /// @solidity memory-safe-assembly + assembly { + tstore(slot, value) + } + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/TransientStorageHelpers.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/TransientStorageHelpers.sol new file mode 100644 index 00000000..9b56a5cd --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/TransientStorageHelpers.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { SlotDerivation } from "./SlotDerivation.sol"; +import { StorageSlotExtension } from "./StorageSlotExtension.sol"; + +type TokenDeltaMappingSlotType is bytes32; +type AddressToUintMappingSlot is bytes32; +type AddressToBooleanMappingSlot is bytes32; +type AddressArraySlotType is bytes32; + +/** + * @notice Helper functions to read and write values from transient storage, including support for arrays and mappings. + * @dev This is temporary, based on Open Zeppelin's partially released library. When the final version is published, we + * should be able to remove our copies and import directly from OZ. When Solidity catches up and puts direct support + * for transient storage in the language, we should be able to get rid of this altogether. + * + * This only works on networks where EIP-1153 is supported. + */ +library TransientStorageHelpers { + using SlotDerivation for *; + using StorageSlotExtension for *; + + /// @notice An index is out of bounds on an array operation (e.g., at). + error TransientIndexOutOfBounds(); + + // Calculate the slot for a transient storage variable. + function calculateSlot(string memory domain, string memory varName) internal pure returns (bytes32) { + return + keccak256( + abi.encode(uint256(keccak256(abi.encodePacked("balancer-labs.v3.storage.", domain, ".", varName))) - 1) + ) & ~bytes32(uint256(0xff)); + } + + /*************************************************************************** + Mappings + ***************************************************************************/ + + function tGet(TokenDeltaMappingSlotType slot, IERC20 k1) internal view returns (int256) { + return TokenDeltaMappingSlotType.unwrap(slot).deriveMapping(address(k1)).asInt256().tload(); + } + + function tSet(TokenDeltaMappingSlotType slot, IERC20 k1, int256 value) internal { + TokenDeltaMappingSlotType.unwrap(slot).deriveMapping(address(k1)).asInt256().tstore(value); + } + + function tGet(AddressToUintMappingSlot slot, address key) internal view returns (uint256) { + return AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tload(); + } + + function tSet(AddressToUintMappingSlot slot, address key, uint256 value) internal { + AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(value); + } + + function tGet(AddressToBooleanMappingSlot slot, address key) internal view returns (bool) { + return AddressToBooleanMappingSlot.unwrap(slot).deriveMapping(key).asBoolean().tload(); + } + + function tSet(AddressToBooleanMappingSlot slot, address key, bool value) internal { + AddressToBooleanMappingSlot.unwrap(slot).deriveMapping(key).asBoolean().tstore(value); + } + + // Implement the common "+=" operation: map[key] += value. + function tAdd(AddressToUintMappingSlot slot, address key, uint256 value) internal { + AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(tGet(slot, key) + value); + } + + function tSub(AddressToUintMappingSlot slot, address key, uint256 value) internal { + AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(tGet(slot, key) - value); + } + + /*************************************************************************** + Arrays + ***************************************************************************/ + + function tLength(AddressArraySlotType slot) internal view returns (uint256) { + return AddressArraySlotType.unwrap(slot).asUint256().tload(); + } + + function tAt(AddressArraySlotType slot, uint256 index) internal view returns (address) { + _ensureIndexWithinBounds(slot, index); + return AddressArraySlotType.unwrap(slot).deriveArray().offset(index).asAddress().tload(); + } + + function tSet(AddressArraySlotType slot, uint256 index, address value) internal { + _ensureIndexWithinBounds(slot, index); + AddressArraySlotType.unwrap(slot).deriveArray().offset(index).asAddress().tstore(value); + } + + function _ensureIndexWithinBounds(AddressArraySlotType slot, uint256 index) private view { + uint256 length = AddressArraySlotType.unwrap(slot).asUint256().tload(); + if (index >= length) { + revert TransientIndexOutOfBounds(); + } + } + + function tUncheckedAt(AddressArraySlotType slot, uint256 index) internal view returns (address) { + return AddressArraySlotType.unwrap(slot).deriveArray().offset(index).asAddress().tload(); + } + + function tUncheckedSet(AddressArraySlotType slot, uint256 index, address value) internal { + AddressArraySlotType.unwrap(slot).deriveArray().offset(index).asAddress().tstore(value); + } + + function tPush(AddressArraySlotType slot, address value) internal { + // Store the value at offset corresponding to the current length. + uint256 length = AddressArraySlotType.unwrap(slot).asUint256().tload(); + AddressArraySlotType.unwrap(slot).deriveArray().offset(length).asAddress().tstore(value); + // Update current length to consider the new value. + AddressArraySlotType.unwrap(slot).asUint256().tstore(length + 1); + } + + function tPop(AddressArraySlotType slot) internal returns (address value) { + uint256 lastElementIndex = AddressArraySlotType.unwrap(slot).asUint256().tload() - 1; + // Update length to last element. When the index is 0, the slot that holds the length is cleared out. + AddressArraySlotType.unwrap(slot).asUint256().tstore(lastElementIndex); + StorageSlotExtension.AddressSlotType lastElementSlot = AddressArraySlotType + .unwrap(slot) + .deriveArray() + .offset(lastElementIndex) + .asAddress(); + // Return last element. + value = lastElementSlot.tload(); + // Clear value in temporary storage. + lastElementSlot.tstore(address(0)); + } + + /*************************************************************************** + Uint256 Values + ***************************************************************************/ + + function tIncrement(StorageSlotExtension.Uint256SlotType slot) internal { + slot.tstore(slot.tload() + 1); + } + + function tDecrement(StorageSlotExtension.Uint256SlotType slot) internal { + slot.tstore(slot.tload() - 1); + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol new file mode 100644 index 00000000..05f66bd9 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol @@ -0,0 +1,913 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +import { IProtocolFeeController } from "./IProtocolFeeController.sol"; +import { IRateProvider } from "./IRateProvider.sol"; +import { IVaultExtension } from "./IVaultExtension.sol"; +import { IVaultAdmin } from "./IVaultAdmin.sol"; +import { IBasePool } from "./IBasePool.sol"; +import { IHooks } from "./IHooks.sol"; +import { IVault } from "./IVault.sol"; +import "./VaultTypes.sol"; + +import { CastingHelpers } from "./CastingHelpers.sol"; +import { InputHelpers } from "./InputHelpers.sol"; +import { RevertCodec } from "./RevertCodec.sol"; +import { ScalingHelpers } from "./ScalingHelpers.sol"; +import { EVMCallModeHelpers } from "./EVMCallModeHelpers.sol"; +import { + TransientStorageHelpers +} from "./TransientStorageHelpers.sol"; +import { StorageSlotExtension } from "./StorageSlotExtension.sol"; +import { FixedPoint } from "./FixedPoint.sol"; +import { PackedTokenBalance } from "./PackedTokenBalance.sol"; + +import { VaultStateBits, VaultStateLib } from "../lib/VaultStateLib.sol"; +import { PoolConfigLib, PoolConfigBits } from "../lib/PoolConfigLib.sol"; +import { HooksConfigLib } from "../lib/HooksConfigLib.sol"; +import { VaultExtensionsLib } from "../lib/VaultExtensionsLib.sol"; +import { VaultCommon } from "../VaultCommon.sol"; +import { PoolDataLib } from "../lib/PoolDataLib.sol"; +import { BasePoolMath } from "./BasePoolMath.sol"; + +/** + * @notice Bytecode extension for the Vault containing permissionless functions outside the critical path. + * It has access to the same storage layout as the main vault. + * + * The functions in this contract are not meant to be called directly. They must only be called by the Vault + * via delegate calls, so that any state modifications produced by this contract's code will actually target + * the main Vault's state. + * + * The storage of this contract is in practice unused. + */ +contract VaultExtension is IVaultExtension, VaultCommon, Proxy { + using Address for *; + using CastingHelpers for uint256[]; + using FixedPoint for uint256; + using PackedTokenBalance for bytes32; + using PoolConfigLib for PoolConfigBits; + using HooksConfigLib for PoolConfigBits; + using VaultStateLib for VaultStateBits; + using InputHelpers for uint256; + using ScalingHelpers for *; + using VaultExtensionsLib for IVault; + using TransientStorageHelpers for *; + using StorageSlotExtension for *; + using PoolDataLib for PoolData; + + IVault private immutable _vault; + IVaultAdmin private immutable _vaultAdmin; + + /// @dev Functions with this modifier can only be delegate-called by the Vault. + modifier onlyVaultDelegateCall() { + _ensureVaultDelegateCall(); + _; + } + + function _ensureVaultDelegateCall() internal view { + _vault.ensureVaultDelegateCall(); + } + + constructor(IVault mainVault, IVaultAdmin vaultAdmin) { + if (vaultAdmin.vault() != mainVault) { + revert WrongVaultAdminDeployment(); + } + + _vaultPauseWindowEndTime = vaultAdmin.getPauseWindowEndTime(); + _vaultBufferPeriodDuration = vaultAdmin.getBufferPeriodDuration(); + _vaultBufferPeriodEndTime = vaultAdmin.getBufferPeriodEndTime(); + + _vault = mainVault; + _vaultAdmin = vaultAdmin; + } + + /******************************************************************************* + Constants and immutables + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function vault() external view returns (IVault) { + return _vault; + } + + /// @inheritdoc IVaultExtension + function getVaultAdmin() external view returns (address) { + return _implementation(); + } + + /******************************************************************************* + Transient Accounting + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isUnlocked() external view onlyVaultDelegateCall returns (bool) { + return _isUnlocked().tload(); + } + + /// @inheritdoc IVaultExtension + function getNonzeroDeltaCount() external view onlyVaultDelegateCall returns (uint256) { + return _nonZeroDeltaCount().tload(); + } + + /// @inheritdoc IVaultExtension + function getTokenDelta(IERC20 token) external view onlyVaultDelegateCall returns (int256) { + return _tokenDeltas().tGet(token); + } + + /// @inheritdoc IVaultExtension + function getReservesOf(IERC20 token) external view onlyVaultDelegateCall returns (uint256) { + return _reservesOf[token]; + } + + /// @inheritdoc IVaultExtension + function getAddLiquidityCalledFlag(address pool) external view onlyVaultDelegateCall returns (bool) { + return _addLiquidityCalled().tGet(pool); + } + + /******************************************************************************* + Pool Registration + *******************************************************************************/ + + struct PoolRegistrationParams { + TokenConfig[] tokenConfig; + uint256 swapFeePercentage; + uint32 pauseWindowEndTime; + bool protocolFeeExempt; + PoolRoleAccounts roleAccounts; + address poolHooksContract; + LiquidityManagement liquidityManagement; + } + + /// @inheritdoc IVaultExtension + function registerPool( + address pool, + TokenConfig[] memory tokenConfig, + uint256 swapFeePercentage, + uint32 pauseWindowEndTime, + bool protocolFeeExempt, + PoolRoleAccounts calldata roleAccounts, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external onlyVaultDelegateCall nonReentrant whenVaultNotPaused { + _registerPool( + pool, + PoolRegistrationParams({ + tokenConfig: tokenConfig, + swapFeePercentage: swapFeePercentage, + pauseWindowEndTime: pauseWindowEndTime, + protocolFeeExempt: protocolFeeExempt, + roleAccounts: roleAccounts, + poolHooksContract: poolHooksContract, + liquidityManagement: liquidityManagement + }) + ); + } + + /** + * @dev The function will register the pool, setting its tokens with an initial balance of zero. + * The function also checks for valid token addresses and ensures that the pool and tokens aren't + * already registered. + * + * Emits a `PoolRegistered` event upon successful registration. + */ + function _registerPool(address pool, PoolRegistrationParams memory params) internal { + // Ensure the pool isn't already registered + if (_isPoolRegistered(pool)) { + revert PoolAlreadyRegistered(pool); + } + + uint256 numTokens = params.tokenConfig.length; + if (numTokens < _MIN_TOKENS) { + revert MinTokens(); + } + if (numTokens > _MAX_TOKENS) { + revert MaxTokens(); + } + + uint8[] memory tokenDecimalDiffs = new uint8[](numTokens); + IERC20 previousToken; + + for (uint256 i = 0; i < numTokens; ++i) { + TokenConfig memory tokenData = params.tokenConfig[i]; + IERC20 token = tokenData.token; + + // Ensure that the token address is valid + if (address(token) == address(0) || address(token) == pool) { + revert InvalidToken(); + } + + // Enforce token sorting. (`previousToken` will be the zero address on the first iteration.) + if (token < previousToken) { + revert InputHelpers.TokensNotSorted(); + } + + if (token == previousToken) { + revert TokenAlreadyRegistered(token); + } + + bool hasRateProvider = tokenData.rateProvider != IRateProvider(address(0)); + + _poolTokenInfo[pool][token] = TokenInfo({ + tokenType: tokenData.tokenType, + rateProvider: tokenData.rateProvider, + paysYieldFees: tokenData.paysYieldFees + }); + + if (tokenData.tokenType == TokenType.STANDARD) { + if (hasRateProvider || tokenData.paysYieldFees) { + revert InvalidTokenConfiguration(); + } + } else if (tokenData.tokenType == TokenType.WITH_RATE) { + if (hasRateProvider == false) { + revert InvalidTokenConfiguration(); + } + } else { + revert InvalidTokenType(); + } + + // Store the token decimal conversion factor as a delta from the maximum supported value. + uint8 tokenDecimals = IERC20Metadata(address(token)).decimals(); + + if (tokenDecimals > _MAX_TOKEN_DECIMALS) { + revert InvalidTokenDecimals(); + } else { + unchecked { + tokenDecimalDiffs[i] = _MAX_TOKEN_DECIMALS - tokenDecimals; + } + } + + // Store token and seed the next iteration. + _poolTokens[pool].push(token); + previousToken = token; + } + + // Store the role account addresses (for getters). + _poolRoleAccounts[pool] = params.roleAccounts; + + PoolConfigBits poolConfigBits; + + // Store the configuration, and mark the pool as registered. + { + // Initialize the pool-specific protocol fee values to the current global defaults. + (uint256 aggregateSwapFeePercentage, uint256 aggregateYieldFeePercentage) = _protocolFeeController + .registerPool(pool, params.roleAccounts.poolCreator, params.protocolFeeExempt); + + poolConfigBits = poolConfigBits.setPoolRegistered(true); + poolConfigBits = poolConfigBits.setDisableUnbalancedLiquidity( + params.liquidityManagement.disableUnbalancedLiquidity + ); + poolConfigBits = poolConfigBits.setAddLiquidityCustom(params.liquidityManagement.enableAddLiquidityCustom); + poolConfigBits = poolConfigBits.setRemoveLiquidityCustom( + params.liquidityManagement.enableRemoveLiquidityCustom + ); + poolConfigBits = poolConfigBits.setDonation(params.liquidityManagement.enableDonation); + poolConfigBits = poolConfigBits.setTokenDecimalDiffs(PoolConfigLib.toTokenDecimalDiffs(tokenDecimalDiffs)); + poolConfigBits = poolConfigBits.setPauseWindowEndTime(params.pauseWindowEndTime); + poolConfigBits = poolConfigBits.setAggregateSwapFeePercentage(aggregateSwapFeePercentage); + poolConfigBits = poolConfigBits.setAggregateYieldFeePercentage(aggregateYieldFeePercentage); + + if (params.poolHooksContract != address(0)) { + // If a hook address was passed, make sure that hook trusts the pool factory. + if ( + IHooks(params.poolHooksContract).onRegister( + msg.sender, + pool, + params.tokenConfig, + params.liquidityManagement + ) == false + ) { + revert HookRegistrationFailed(params.poolHooksContract, pool, msg.sender); + } + + // Gets the default HooksConfig from the hook contract and saves it in the Vault state. + // Storing into hooksConfig first avoids stack-too-deep. + HookFlags memory hookFlags = IHooks(params.poolHooksContract).getHookFlags(); + + // When enableHookAdjustedAmounts == true, hooks are able to modify the result of a liquidity or swap + // operation by implementing an after hook. For simplicity, the Vault only supports modifying the + // calculated part of the operation. As such, when a hook supports adjusted amounts, it cannot support + // unbalanced liquidity operations, as this would introduce instances where the amount calculated is the + // input amount (EXACT_OUT). + if ( + hookFlags.enableHookAdjustedAmounts && + params.liquidityManagement.disableUnbalancedLiquidity == false + ) { + revert HookRegistrationFailed(params.poolHooksContract, pool, msg.sender); + } + + poolConfigBits = poolConfigBits.setHookAdjustedAmounts(hookFlags.enableHookAdjustedAmounts); + poolConfigBits = poolConfigBits.setShouldCallBeforeInitialize(hookFlags.shouldCallBeforeInitialize); + poolConfigBits = poolConfigBits.setShouldCallAfterInitialize(hookFlags.shouldCallAfterInitialize); + poolConfigBits = poolConfigBits.setShouldCallComputeDynamicSwapFee( + hookFlags.shouldCallComputeDynamicSwapFee + ); + poolConfigBits = poolConfigBits.setShouldCallBeforeSwap(hookFlags.shouldCallBeforeSwap); + poolConfigBits = poolConfigBits.setShouldCallAfterSwap(hookFlags.shouldCallAfterSwap); + poolConfigBits = poolConfigBits.setShouldCallBeforeAddLiquidity(hookFlags.shouldCallBeforeAddLiquidity); + poolConfigBits = poolConfigBits.setShouldCallAfterAddLiquidity(hookFlags.shouldCallAfterAddLiquidity); + poolConfigBits = poolConfigBits.setShouldCallBeforeRemoveLiquidity( + hookFlags.shouldCallBeforeRemoveLiquidity + ); + poolConfigBits = poolConfigBits.setShouldCallAfterRemoveLiquidity( + hookFlags.shouldCallAfterRemoveLiquidity + ); + } + + _poolConfigBits[pool] = poolConfigBits; + _hooksContracts[pool] = IHooks(params.poolHooksContract); + } + + // Static swap fee percentage has special limits, so we don't use the library function directly. + _setStaticSwapFeePercentage(pool, params.swapFeePercentage); + + // Emit an event to log the pool registration (pass msg.sender as the factory argument). + emit PoolRegistered( + pool, + msg.sender, + params.tokenConfig, + params.swapFeePercentage, + params.pauseWindowEndTime, + params.roleAccounts, + poolConfigBits.toHooksConfig(IHooks(params.poolHooksContract)), + params.liquidityManagement + ); + } + + /// @inheritdoc IVaultExtension + function isPoolRegistered(address pool) external view onlyVaultDelegateCall returns (bool) { + return _isPoolRegistered(pool); + } + + /// @inheritdoc IVaultExtension + function initialize( + address pool, + address to, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bytes memory userData + ) + external + onlyVaultDelegateCall + onlyWhenUnlocked + withRegisteredPool(pool) + nonReentrant + returns (uint256 bptAmountOut) + { + _ensureUnpaused(pool); + + // Balances are zero until after initialize is called, so there is no need to charge pending yield fee here. + PoolData memory poolData = _loadPoolData(pool, Rounding.ROUND_DOWN); + + if (poolData.poolConfigBits.isPoolInitialized()) { + revert PoolAlreadyInitialized(pool); + } + uint256 numTokens = poolData.tokens.length; + + InputHelpers.ensureInputLengthMatch(numTokens, exactAmountsIn.length); + + // Amounts are entering pool math, so round down. A lower invariant after the join means less bptOut, + // favoring the pool. + uint256[] memory exactAmountsInScaled18 = exactAmountsIn.copyToScaled18ApplyRateRoundDownArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + + if (poolData.poolConfigBits.shouldCallBeforeInitialize()) { + HooksConfigLib.callBeforeInitializeHook(exactAmountsInScaled18, userData, _hooksContracts[pool]); + // The before hook is reentrant, and could have changed token rates. + // Updating balances here is unnecessary since they're 0, but we do not special case before init + // for the sake of bytecode size. + poolData.reloadBalancesAndRates(_poolTokenBalances[pool], Rounding.ROUND_DOWN); + + // Also update `exactAmountsInScaled18`, in case the underlying rates changed. + exactAmountsInScaled18 = exactAmountsIn.copyToScaled18ApplyRateRoundDownArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + } + + bptAmountOut = _initialize(pool, to, poolData, tokens, exactAmountsIn, exactAmountsInScaled18, minBptAmountOut); + + if (poolData.poolConfigBits.shouldCallAfterInitialize()) { + // `hooksContract` needed to fix stack too deep. + IHooks hooksContract = _hooksContracts[pool]; + + HooksConfigLib.callAfterInitializeHook(exactAmountsInScaled18, bptAmountOut, userData, hooksContract); + } + } + + function _initialize( + address pool, + address to, + PoolData memory poolData, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256[] memory exactAmountsInScaled18, + uint256 minBptAmountOut + ) internal returns (uint256 bptAmountOut) { + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolBalances = _poolTokenBalances[pool]; + + for (uint256 i = 0; i < poolData.tokens.length; ++i) { + IERC20 actualToken = poolData.tokens[i]; + + // Tokens passed into `initialize` are the "expected" tokens. + if (actualToken != tokens[i]) { + revert TokensMismatch(pool, address(tokens[i]), address(actualToken)); + } + + // Debit token[i] for amountIn. + _takeDebt(actualToken, exactAmountsIn[i]); + + // Store the new Pool balances (and initial last live balances). + poolBalances[i] = PackedTokenBalance.toPackedBalance(exactAmountsIn[i], exactAmountsInScaled18[i]); + } + + poolData.poolConfigBits = poolData.poolConfigBits.setPoolInitialized(true); + + // Store config and mark the pool as initialized. + _poolConfigBits[pool] = poolData.poolConfigBits; + + // Pass scaled balances to the pool. + bptAmountOut = IBasePool(pool).computeInvariant(exactAmountsInScaled18, Rounding.ROUND_DOWN); + + _ensurePoolMinimumTotalSupply(bptAmountOut); + + // At this point we know that bptAmountOut >= _POOL_MINIMUM_TOTAL_SUPPLY, so this will not revert. + bptAmountOut -= _POOL_MINIMUM_TOTAL_SUPPLY; + // When adding liquidity, we must mint tokens concurrently with updating pool balances, + // as the pool's math relies on totalSupply. + // Minting will be reverted if it results in a total supply less than the _POOL_MINIMUM_TOTAL_SUPPLY. + _mintMinimumSupplyReserve(address(pool)); + _mint(address(pool), to, bptAmountOut); + + // At this point we have the calculated BPT amount. + if (bptAmountOut < minBptAmountOut) { + revert BptAmountOutBelowMin(bptAmountOut, minBptAmountOut); + } + + emit PoolBalanceChanged( + pool, + to, + _totalSupply(pool), + exactAmountsIn.unsafeCastToInt256(true), + new uint256[](poolData.tokens.length) + ); + + // Emit an event to log the pool initialization. + emit PoolInitialized(pool); + } + + /******************************************************************************* + Pool Information + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isPoolInitialized( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool) { + return _isPoolInitialized(pool); + } + + /// @inheritdoc IVaultExtension + function getPoolTokens( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (IERC20[] memory tokens) { + return _poolTokens[pool]; + } + + /// @inheritdoc IVaultExtension + function getPoolTokenRates( + address pool + ) + external + view + onlyVaultDelegateCall + withRegisteredPool(pool) + returns (uint256[] memory decimalScalingFactors, uint256[] memory tokenRates) + { + // Retrieve the mapping of tokens and their balances for the specified pool. + PoolConfigBits poolConfig = _poolConfigBits[pool]; + + IERC20[] memory tokens = _poolTokens[pool]; + uint256 numTokens = tokens.length; + decimalScalingFactors = PoolConfigLib.getDecimalScalingFactors(poolConfig, numTokens); + tokenRates = new uint256[](numTokens); + + for (uint256 i = 0; i < numTokens; ++i) { + TokenInfo memory tokenInfo = _poolTokenInfo[pool][tokens[i]]; + tokenRates[i] = PoolDataLib.getTokenRate(tokenInfo); + } + } + + /// @inheritdoc IVaultExtension + function getPoolData( + address pool + ) external view onlyVaultDelegateCall withInitializedPool(pool) returns (PoolData memory) { + return _loadPoolData(pool, Rounding.ROUND_DOWN); + } + + /// @inheritdoc IVaultExtension + function getPoolTokenInfo( + address pool + ) + external + view + onlyVaultDelegateCall + withRegisteredPool(pool) + returns ( + IERC20[] memory tokens, + TokenInfo[] memory tokenInfo, + uint256[] memory balancesRaw, + uint256[] memory lastBalancesLiveScaled18 + ) + { + // Retrieve the mapping of tokens and their balances for the specified pool. + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + tokens = _poolTokens[pool]; + uint256 numTokens = tokens.length; + tokenInfo = new TokenInfo[](numTokens); + balancesRaw = new uint256[](numTokens); + lastBalancesLiveScaled18 = new uint256[](numTokens); + + for (uint256 i = 0; i < numTokens; ++i) { + bytes32 packedBalance = poolTokenBalances[i]; + tokenInfo[i] = _poolTokenInfo[pool][tokens[i]]; + balancesRaw[i] = packedBalance.getBalanceRaw(); + lastBalancesLiveScaled18[i] = packedBalance.getBalanceDerived(); + } + } + + /// @inheritdoc IVaultExtension + function getCurrentLiveBalances( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256[] memory balancesLiveScaled18) { + return _loadPoolData(pool, Rounding.ROUND_DOWN).balancesLiveScaled18; + } + + /// @inheritdoc IVaultExtension + function getPoolConfig( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (PoolConfig memory) { + PoolConfigBits config = _poolConfigBits[pool]; + + return + PoolConfig({ + isPoolRegistered: config.isPoolRegistered(), + isPoolInitialized: config.isPoolInitialized(), + isPoolPaused: config.isPoolPaused(), + isPoolInRecoveryMode: config.isPoolInRecoveryMode(), + staticSwapFeePercentage: config.getStaticSwapFeePercentage(), + aggregateSwapFeePercentage: config.getAggregateSwapFeePercentage(), + aggregateYieldFeePercentage: config.getAggregateYieldFeePercentage(), + tokenDecimalDiffs: config.getTokenDecimalDiffs(), + pauseWindowEndTime: config.getPauseWindowEndTime(), + liquidityManagement: LiquidityManagement({ + // NOTE: In contrast to the other flags, supportsUnbalancedLiquidity is enabled by default. + disableUnbalancedLiquidity: !config.supportsUnbalancedLiquidity(), + enableAddLiquidityCustom: config.supportsAddLiquidityCustom(), + enableRemoveLiquidityCustom: config.supportsRemoveLiquidityCustom(), + enableDonation: config.supportsDonation() + }) + }); + } + + /// @inheritdoc IVaultExtension + function getHooksConfig( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (HooksConfig memory) { + return _poolConfigBits[pool].toHooksConfig(_hooksContracts[pool]); + } + + /// @inheritdoc IVaultExtension + function getBptRate( + address pool + ) external view onlyVaultDelegateCall withInitializedPool(pool) returns (uint256 rate) { + PoolData memory poolData = _loadPoolData(pool, Rounding.ROUND_DOWN); + uint256 invariant = IBasePool(pool).computeInvariant(poolData.balancesLiveScaled18, Rounding.ROUND_DOWN); + + return invariant.divDown(_totalSupply(pool)); + } + + /******************************************************************************* + Balancer Pool Tokens + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function totalSupply(address token) external view onlyVaultDelegateCall returns (uint256) { + return _totalSupply(token); + } + + /// @inheritdoc IVaultExtension + function balanceOf(address token, address account) external view onlyVaultDelegateCall returns (uint256) { + return _balanceOf(token, account); + } + + /// @inheritdoc IVaultExtension + function allowance( + address token, + address owner, + address spender + ) external view onlyVaultDelegateCall returns (uint256) { + return _allowance(token, owner, spender); + } + + /// @inheritdoc IVaultExtension + function approve(address owner, address spender, uint256 amount) external onlyVaultDelegateCall returns (bool) { + _approve(msg.sender, owner, spender, amount); + return true; + } + + /// @inheritdoc IVaultExtension + function transfer(address owner, address to, uint256 amount) external onlyVaultDelegateCall returns (bool) { + _transfer(msg.sender, owner, to, amount); + return true; + } + + /// @inheritdoc IVaultExtension + function transferFrom( + address spender, + address from, + address to, + uint256 amount + ) external onlyVaultDelegateCall returns (bool) { + _spendAllowance(msg.sender, from, spender, amount); + _transfer(msg.sender, from, to, amount); + return true; + } + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isERC4626BufferInitialized(IERC4626 wrappedToken) external view onlyVaultDelegateCall returns (bool) { + return _bufferAssets[wrappedToken] != address(0); + } + + /******************************************************************************* + Pool Pausing + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isPoolPaused(address pool) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool) { + return _isPoolPaused(pool); + } + + /// @inheritdoc IVaultExtension + function getPoolPausedState( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool, uint32, uint32, address) { + (bool paused, uint32 pauseWindowEndTime) = _getPoolPausedState(pool); + + return ( + paused, + pauseWindowEndTime, + pauseWindowEndTime + _vaultBufferPeriodDuration, + _poolRoleAccounts[pool].pauseManager + ); + } + + /******************************************************************************* + Fees + *******************************************************************************/ + + // Swap and Yield fees are both stored using the PackedTokenBalance library, which is usually used for + // balances that are related (e.g., raw and live). In this case, it holds two uncorrelated values: swap + // and yield fee amounts, arbitrarily assigning "Raw" to Swap and "Derived" to Yield. + + /// @inheritdoc IVaultExtension + function getAggregateSwapFeeAmount( + address pool, + IERC20 token + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256) { + return _aggregateFeeAmounts[pool][token].getBalanceRaw(); + } + + /// @inheritdoc IVaultExtension + function getAggregateYieldFeeAmount( + address pool, + IERC20 token + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256) { + return _aggregateFeeAmounts[pool][token].getBalanceDerived(); + } + + /// @inheritdoc IVaultExtension + function getStaticSwapFeePercentage( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256) { + PoolConfigBits config = _poolConfigBits[pool]; + return config.getStaticSwapFeePercentage(); + } + + /// @inheritdoc IVaultExtension + function getPoolRoleAccounts( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (PoolRoleAccounts memory) { + return _poolRoleAccounts[pool]; + } + + /// @inheritdoc IVaultExtension + function computeDynamicSwapFeePercentage( + address pool, + PoolSwapParams memory swapParams + ) external view onlyVaultDelegateCall withInitializedPool(pool) returns (uint256 dynamicSwapFeePercentage) { + return + HooksConfigLib.callComputeDynamicSwapFeeHook( + swapParams, + pool, + _poolConfigBits[pool].getStaticSwapFeePercentage(), + _hooksContracts[pool] + ); + } + + /// @inheritdoc IVaultExtension + function getProtocolFeeController() external view onlyVaultDelegateCall returns (IProtocolFeeController) { + return _protocolFeeController; + } + + /******************************************************************************* + Recovery Mode + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isPoolInRecoveryMode( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool) { + return _isPoolInRecoveryMode(pool); + } + + /// @inheritdoc IVaultExtension + function removeLiquidityRecovery( + address pool, + address from, + uint256 exactBptAmountIn + ) + external + onlyVaultDelegateCall + onlyWhenUnlocked + nonReentrant + withInitializedPool(pool) + onlyInRecoveryMode(pool) + returns (uint256[] memory amountsOutRaw) + { + // Retrieve the mapping of tokens and their balances for the specified pool. + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + + // Initialize arrays to store tokens and balances based on the number of tokens in the pool. + IERC20[] memory tokens = _poolTokens[pool]; + uint256 numTokens = tokens.length; + + uint256[] memory balancesRaw = new uint256[](numTokens); + bytes32 packedBalances; + + for (uint256 i = 0; i < numTokens; ++i) { + balancesRaw[i] = poolTokenBalances[i].getBalanceRaw(); + } + + amountsOutRaw = BasePoolMath.computeProportionalAmountsOut(balancesRaw, _totalSupply(pool), exactBptAmountIn); + + for (uint256 i = 0; i < numTokens; ++i) { + // Credit token[i] for amountOut. + _supplyCredit(tokens[i], amountsOutRaw[i]); + + // Compute the new Pool balances. A Pool's token balance always decreases after an exit + // (potentially by 0). + balancesRaw[i] -= amountsOutRaw[i]; + } + + // Store the new pool balances - raw only, since we don't have rates in Recovery Mode. + // In Recovery Mode, raw and last live balances will get out of sync. This is corrected when the pool is taken + // out of Recovery Mode. + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolBalances = _poolTokenBalances[pool]; + + for (uint256 i = 0; i < numTokens; ++i) { + packedBalances = poolBalances[i]; + poolBalances[i] = packedBalances.setBalanceRaw(balancesRaw[i]); + } + + _spendAllowance(pool, from, msg.sender, exactBptAmountIn); + + if (_isQueryContext()) { + // Increase `from` balance to ensure the burn function succeeds. + _queryModeBalanceIncrease(pool, from, exactBptAmountIn); + } + // When removing liquidity, we must burn tokens concurrently with updating pool balances, + // as the pool's math relies on totalSupply. + // + // Burning will be reverted if it results in a total supply less than the _MINIMUM_TOTAL_SUPPLY. + _burn(pool, from, exactBptAmountIn); + + emit PoolBalanceChanged( + pool, + from, + _totalSupply(pool), + // We can unsafely cast to int256 because balances are stored as uint128 (see PackedTokenBalance). + amountsOutRaw.unsafeCastToInt256(false), + new uint256[](numTokens) + ); + } + + /******************************************************************************* + Queries + *******************************************************************************/ + + /// @dev Ensure that only static calls are made to the functions with this modifier. + modifier query() { + _setupQuery(); + _; + } + + function _setupQuery() internal { + if (EVMCallModeHelpers.isStaticCall() == false) { + revert EVMCallModeHelpers.NotStaticCall(); + } + + bool _isQueryDisabled = _vaultStateBits.isQueryDisabled(); + if (_isQueryDisabled) { + revert QueriesDisabled(); + } + + // Unlock so that `onlyWhenUnlocked` does not revert. + _isUnlocked().tstore(true); + } + + function _isQueryContext() internal view returns (bool) { + return EVMCallModeHelpers.isStaticCall() && _vaultStateBits.isQueryDisabled() == false; + } + + /// @inheritdoc IVaultExtension + function quote(bytes calldata data) external query onlyVaultDelegateCall returns (bytes memory result) { + // Forward the incoming call to the original sender of this transaction. + return (msg.sender).functionCall(data); + } + + /// @inheritdoc IVaultExtension + function quoteAndRevert(bytes calldata data) external query onlyVaultDelegateCall { + // Forward the incoming call to the original sender of this transaction. + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = (msg.sender).call(data); + if (success) { + // This will only revert if result is empty and sender account has no code. + Address.verifyCallResultFromTarget(msg.sender, success, result); + // Send result in revert reason. + revert RevertCodec.Result(result); + } else { + // If the call reverted with a spoofed `QuoteResult`, we catch it and bubble up a different reason. + bytes4 errorSelector = RevertCodec.parseSelector(result); + if (errorSelector == RevertCodec.Result.selector) { + revert QuoteResultSpoofed(); + } + + // Otherwise we bubble up the original revert reason. + RevertCodec.bubbleUpRevert(result); + } + } + + /// @inheritdoc IVaultExtension + function isQueryDisabled() external view onlyVaultDelegateCall returns (bool) { + return _vaultStateBits.isQueryDisabled(); + } + + /******************************************************************************* + Default handlers + *******************************************************************************/ + + receive() external payable { + revert CannotReceiveEth(); + } + + // solhint-disable no-complex-fallback + + /** + * @inheritdoc Proxy + * @dev Override proxy implementation of `fallback` to disallow incoming ETH transfers. + * This function actually returns whatever the VaultAdmin does when handling the request. + */ + fallback() external payable override { + if (msg.value > 0) { + revert CannotReceiveEth(); + } + + _fallback(); + } + + /******************************************************************************* + Miscellaneous + *******************************************************************************/ + + /** + * @inheritdoc Proxy + * @dev Returns the VaultAdmin contract, to which fallback requests are forwarded. + */ + function _implementation() internal view override returns (address) { + return address(_vaultAdmin); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtensions.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtensions.sol new file mode 100644 index 00000000..05f66bd9 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtensions.sol @@ -0,0 +1,913 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +import { IProtocolFeeController } from "./IProtocolFeeController.sol"; +import { IRateProvider } from "./IRateProvider.sol"; +import { IVaultExtension } from "./IVaultExtension.sol"; +import { IVaultAdmin } from "./IVaultAdmin.sol"; +import { IBasePool } from "./IBasePool.sol"; +import { IHooks } from "./IHooks.sol"; +import { IVault } from "./IVault.sol"; +import "./VaultTypes.sol"; + +import { CastingHelpers } from "./CastingHelpers.sol"; +import { InputHelpers } from "./InputHelpers.sol"; +import { RevertCodec } from "./RevertCodec.sol"; +import { ScalingHelpers } from "./ScalingHelpers.sol"; +import { EVMCallModeHelpers } from "./EVMCallModeHelpers.sol"; +import { + TransientStorageHelpers +} from "./TransientStorageHelpers.sol"; +import { StorageSlotExtension } from "./StorageSlotExtension.sol"; +import { FixedPoint } from "./FixedPoint.sol"; +import { PackedTokenBalance } from "./PackedTokenBalance.sol"; + +import { VaultStateBits, VaultStateLib } from "../lib/VaultStateLib.sol"; +import { PoolConfigLib, PoolConfigBits } from "../lib/PoolConfigLib.sol"; +import { HooksConfigLib } from "../lib/HooksConfigLib.sol"; +import { VaultExtensionsLib } from "../lib/VaultExtensionsLib.sol"; +import { VaultCommon } from "../VaultCommon.sol"; +import { PoolDataLib } from "../lib/PoolDataLib.sol"; +import { BasePoolMath } from "./BasePoolMath.sol"; + +/** + * @notice Bytecode extension for the Vault containing permissionless functions outside the critical path. + * It has access to the same storage layout as the main vault. + * + * The functions in this contract are not meant to be called directly. They must only be called by the Vault + * via delegate calls, so that any state modifications produced by this contract's code will actually target + * the main Vault's state. + * + * The storage of this contract is in practice unused. + */ +contract VaultExtension is IVaultExtension, VaultCommon, Proxy { + using Address for *; + using CastingHelpers for uint256[]; + using FixedPoint for uint256; + using PackedTokenBalance for bytes32; + using PoolConfigLib for PoolConfigBits; + using HooksConfigLib for PoolConfigBits; + using VaultStateLib for VaultStateBits; + using InputHelpers for uint256; + using ScalingHelpers for *; + using VaultExtensionsLib for IVault; + using TransientStorageHelpers for *; + using StorageSlotExtension for *; + using PoolDataLib for PoolData; + + IVault private immutable _vault; + IVaultAdmin private immutable _vaultAdmin; + + /// @dev Functions with this modifier can only be delegate-called by the Vault. + modifier onlyVaultDelegateCall() { + _ensureVaultDelegateCall(); + _; + } + + function _ensureVaultDelegateCall() internal view { + _vault.ensureVaultDelegateCall(); + } + + constructor(IVault mainVault, IVaultAdmin vaultAdmin) { + if (vaultAdmin.vault() != mainVault) { + revert WrongVaultAdminDeployment(); + } + + _vaultPauseWindowEndTime = vaultAdmin.getPauseWindowEndTime(); + _vaultBufferPeriodDuration = vaultAdmin.getBufferPeriodDuration(); + _vaultBufferPeriodEndTime = vaultAdmin.getBufferPeriodEndTime(); + + _vault = mainVault; + _vaultAdmin = vaultAdmin; + } + + /******************************************************************************* + Constants and immutables + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function vault() external view returns (IVault) { + return _vault; + } + + /// @inheritdoc IVaultExtension + function getVaultAdmin() external view returns (address) { + return _implementation(); + } + + /******************************************************************************* + Transient Accounting + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isUnlocked() external view onlyVaultDelegateCall returns (bool) { + return _isUnlocked().tload(); + } + + /// @inheritdoc IVaultExtension + function getNonzeroDeltaCount() external view onlyVaultDelegateCall returns (uint256) { + return _nonZeroDeltaCount().tload(); + } + + /// @inheritdoc IVaultExtension + function getTokenDelta(IERC20 token) external view onlyVaultDelegateCall returns (int256) { + return _tokenDeltas().tGet(token); + } + + /// @inheritdoc IVaultExtension + function getReservesOf(IERC20 token) external view onlyVaultDelegateCall returns (uint256) { + return _reservesOf[token]; + } + + /// @inheritdoc IVaultExtension + function getAddLiquidityCalledFlag(address pool) external view onlyVaultDelegateCall returns (bool) { + return _addLiquidityCalled().tGet(pool); + } + + /******************************************************************************* + Pool Registration + *******************************************************************************/ + + struct PoolRegistrationParams { + TokenConfig[] tokenConfig; + uint256 swapFeePercentage; + uint32 pauseWindowEndTime; + bool protocolFeeExempt; + PoolRoleAccounts roleAccounts; + address poolHooksContract; + LiquidityManagement liquidityManagement; + } + + /// @inheritdoc IVaultExtension + function registerPool( + address pool, + TokenConfig[] memory tokenConfig, + uint256 swapFeePercentage, + uint32 pauseWindowEndTime, + bool protocolFeeExempt, + PoolRoleAccounts calldata roleAccounts, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external onlyVaultDelegateCall nonReentrant whenVaultNotPaused { + _registerPool( + pool, + PoolRegistrationParams({ + tokenConfig: tokenConfig, + swapFeePercentage: swapFeePercentage, + pauseWindowEndTime: pauseWindowEndTime, + protocolFeeExempt: protocolFeeExempt, + roleAccounts: roleAccounts, + poolHooksContract: poolHooksContract, + liquidityManagement: liquidityManagement + }) + ); + } + + /** + * @dev The function will register the pool, setting its tokens with an initial balance of zero. + * The function also checks for valid token addresses and ensures that the pool and tokens aren't + * already registered. + * + * Emits a `PoolRegistered` event upon successful registration. + */ + function _registerPool(address pool, PoolRegistrationParams memory params) internal { + // Ensure the pool isn't already registered + if (_isPoolRegistered(pool)) { + revert PoolAlreadyRegistered(pool); + } + + uint256 numTokens = params.tokenConfig.length; + if (numTokens < _MIN_TOKENS) { + revert MinTokens(); + } + if (numTokens > _MAX_TOKENS) { + revert MaxTokens(); + } + + uint8[] memory tokenDecimalDiffs = new uint8[](numTokens); + IERC20 previousToken; + + for (uint256 i = 0; i < numTokens; ++i) { + TokenConfig memory tokenData = params.tokenConfig[i]; + IERC20 token = tokenData.token; + + // Ensure that the token address is valid + if (address(token) == address(0) || address(token) == pool) { + revert InvalidToken(); + } + + // Enforce token sorting. (`previousToken` will be the zero address on the first iteration.) + if (token < previousToken) { + revert InputHelpers.TokensNotSorted(); + } + + if (token == previousToken) { + revert TokenAlreadyRegistered(token); + } + + bool hasRateProvider = tokenData.rateProvider != IRateProvider(address(0)); + + _poolTokenInfo[pool][token] = TokenInfo({ + tokenType: tokenData.tokenType, + rateProvider: tokenData.rateProvider, + paysYieldFees: tokenData.paysYieldFees + }); + + if (tokenData.tokenType == TokenType.STANDARD) { + if (hasRateProvider || tokenData.paysYieldFees) { + revert InvalidTokenConfiguration(); + } + } else if (tokenData.tokenType == TokenType.WITH_RATE) { + if (hasRateProvider == false) { + revert InvalidTokenConfiguration(); + } + } else { + revert InvalidTokenType(); + } + + // Store the token decimal conversion factor as a delta from the maximum supported value. + uint8 tokenDecimals = IERC20Metadata(address(token)).decimals(); + + if (tokenDecimals > _MAX_TOKEN_DECIMALS) { + revert InvalidTokenDecimals(); + } else { + unchecked { + tokenDecimalDiffs[i] = _MAX_TOKEN_DECIMALS - tokenDecimals; + } + } + + // Store token and seed the next iteration. + _poolTokens[pool].push(token); + previousToken = token; + } + + // Store the role account addresses (for getters). + _poolRoleAccounts[pool] = params.roleAccounts; + + PoolConfigBits poolConfigBits; + + // Store the configuration, and mark the pool as registered. + { + // Initialize the pool-specific protocol fee values to the current global defaults. + (uint256 aggregateSwapFeePercentage, uint256 aggregateYieldFeePercentage) = _protocolFeeController + .registerPool(pool, params.roleAccounts.poolCreator, params.protocolFeeExempt); + + poolConfigBits = poolConfigBits.setPoolRegistered(true); + poolConfigBits = poolConfigBits.setDisableUnbalancedLiquidity( + params.liquidityManagement.disableUnbalancedLiquidity + ); + poolConfigBits = poolConfigBits.setAddLiquidityCustom(params.liquidityManagement.enableAddLiquidityCustom); + poolConfigBits = poolConfigBits.setRemoveLiquidityCustom( + params.liquidityManagement.enableRemoveLiquidityCustom + ); + poolConfigBits = poolConfigBits.setDonation(params.liquidityManagement.enableDonation); + poolConfigBits = poolConfigBits.setTokenDecimalDiffs(PoolConfigLib.toTokenDecimalDiffs(tokenDecimalDiffs)); + poolConfigBits = poolConfigBits.setPauseWindowEndTime(params.pauseWindowEndTime); + poolConfigBits = poolConfigBits.setAggregateSwapFeePercentage(aggregateSwapFeePercentage); + poolConfigBits = poolConfigBits.setAggregateYieldFeePercentage(aggregateYieldFeePercentage); + + if (params.poolHooksContract != address(0)) { + // If a hook address was passed, make sure that hook trusts the pool factory. + if ( + IHooks(params.poolHooksContract).onRegister( + msg.sender, + pool, + params.tokenConfig, + params.liquidityManagement + ) == false + ) { + revert HookRegistrationFailed(params.poolHooksContract, pool, msg.sender); + } + + // Gets the default HooksConfig from the hook contract and saves it in the Vault state. + // Storing into hooksConfig first avoids stack-too-deep. + HookFlags memory hookFlags = IHooks(params.poolHooksContract).getHookFlags(); + + // When enableHookAdjustedAmounts == true, hooks are able to modify the result of a liquidity or swap + // operation by implementing an after hook. For simplicity, the Vault only supports modifying the + // calculated part of the operation. As such, when a hook supports adjusted amounts, it cannot support + // unbalanced liquidity operations, as this would introduce instances where the amount calculated is the + // input amount (EXACT_OUT). + if ( + hookFlags.enableHookAdjustedAmounts && + params.liquidityManagement.disableUnbalancedLiquidity == false + ) { + revert HookRegistrationFailed(params.poolHooksContract, pool, msg.sender); + } + + poolConfigBits = poolConfigBits.setHookAdjustedAmounts(hookFlags.enableHookAdjustedAmounts); + poolConfigBits = poolConfigBits.setShouldCallBeforeInitialize(hookFlags.shouldCallBeforeInitialize); + poolConfigBits = poolConfigBits.setShouldCallAfterInitialize(hookFlags.shouldCallAfterInitialize); + poolConfigBits = poolConfigBits.setShouldCallComputeDynamicSwapFee( + hookFlags.shouldCallComputeDynamicSwapFee + ); + poolConfigBits = poolConfigBits.setShouldCallBeforeSwap(hookFlags.shouldCallBeforeSwap); + poolConfigBits = poolConfigBits.setShouldCallAfterSwap(hookFlags.shouldCallAfterSwap); + poolConfigBits = poolConfigBits.setShouldCallBeforeAddLiquidity(hookFlags.shouldCallBeforeAddLiquidity); + poolConfigBits = poolConfigBits.setShouldCallAfterAddLiquidity(hookFlags.shouldCallAfterAddLiquidity); + poolConfigBits = poolConfigBits.setShouldCallBeforeRemoveLiquidity( + hookFlags.shouldCallBeforeRemoveLiquidity + ); + poolConfigBits = poolConfigBits.setShouldCallAfterRemoveLiquidity( + hookFlags.shouldCallAfterRemoveLiquidity + ); + } + + _poolConfigBits[pool] = poolConfigBits; + _hooksContracts[pool] = IHooks(params.poolHooksContract); + } + + // Static swap fee percentage has special limits, so we don't use the library function directly. + _setStaticSwapFeePercentage(pool, params.swapFeePercentage); + + // Emit an event to log the pool registration (pass msg.sender as the factory argument). + emit PoolRegistered( + pool, + msg.sender, + params.tokenConfig, + params.swapFeePercentage, + params.pauseWindowEndTime, + params.roleAccounts, + poolConfigBits.toHooksConfig(IHooks(params.poolHooksContract)), + params.liquidityManagement + ); + } + + /// @inheritdoc IVaultExtension + function isPoolRegistered(address pool) external view onlyVaultDelegateCall returns (bool) { + return _isPoolRegistered(pool); + } + + /// @inheritdoc IVaultExtension + function initialize( + address pool, + address to, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bytes memory userData + ) + external + onlyVaultDelegateCall + onlyWhenUnlocked + withRegisteredPool(pool) + nonReentrant + returns (uint256 bptAmountOut) + { + _ensureUnpaused(pool); + + // Balances are zero until after initialize is called, so there is no need to charge pending yield fee here. + PoolData memory poolData = _loadPoolData(pool, Rounding.ROUND_DOWN); + + if (poolData.poolConfigBits.isPoolInitialized()) { + revert PoolAlreadyInitialized(pool); + } + uint256 numTokens = poolData.tokens.length; + + InputHelpers.ensureInputLengthMatch(numTokens, exactAmountsIn.length); + + // Amounts are entering pool math, so round down. A lower invariant after the join means less bptOut, + // favoring the pool. + uint256[] memory exactAmountsInScaled18 = exactAmountsIn.copyToScaled18ApplyRateRoundDownArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + + if (poolData.poolConfigBits.shouldCallBeforeInitialize()) { + HooksConfigLib.callBeforeInitializeHook(exactAmountsInScaled18, userData, _hooksContracts[pool]); + // The before hook is reentrant, and could have changed token rates. + // Updating balances here is unnecessary since they're 0, but we do not special case before init + // for the sake of bytecode size. + poolData.reloadBalancesAndRates(_poolTokenBalances[pool], Rounding.ROUND_DOWN); + + // Also update `exactAmountsInScaled18`, in case the underlying rates changed. + exactAmountsInScaled18 = exactAmountsIn.copyToScaled18ApplyRateRoundDownArray( + poolData.decimalScalingFactors, + poolData.tokenRates + ); + } + + bptAmountOut = _initialize(pool, to, poolData, tokens, exactAmountsIn, exactAmountsInScaled18, minBptAmountOut); + + if (poolData.poolConfigBits.shouldCallAfterInitialize()) { + // `hooksContract` needed to fix stack too deep. + IHooks hooksContract = _hooksContracts[pool]; + + HooksConfigLib.callAfterInitializeHook(exactAmountsInScaled18, bptAmountOut, userData, hooksContract); + } + } + + function _initialize( + address pool, + address to, + PoolData memory poolData, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256[] memory exactAmountsInScaled18, + uint256 minBptAmountOut + ) internal returns (uint256 bptAmountOut) { + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolBalances = _poolTokenBalances[pool]; + + for (uint256 i = 0; i < poolData.tokens.length; ++i) { + IERC20 actualToken = poolData.tokens[i]; + + // Tokens passed into `initialize` are the "expected" tokens. + if (actualToken != tokens[i]) { + revert TokensMismatch(pool, address(tokens[i]), address(actualToken)); + } + + // Debit token[i] for amountIn. + _takeDebt(actualToken, exactAmountsIn[i]); + + // Store the new Pool balances (and initial last live balances). + poolBalances[i] = PackedTokenBalance.toPackedBalance(exactAmountsIn[i], exactAmountsInScaled18[i]); + } + + poolData.poolConfigBits = poolData.poolConfigBits.setPoolInitialized(true); + + // Store config and mark the pool as initialized. + _poolConfigBits[pool] = poolData.poolConfigBits; + + // Pass scaled balances to the pool. + bptAmountOut = IBasePool(pool).computeInvariant(exactAmountsInScaled18, Rounding.ROUND_DOWN); + + _ensurePoolMinimumTotalSupply(bptAmountOut); + + // At this point we know that bptAmountOut >= _POOL_MINIMUM_TOTAL_SUPPLY, so this will not revert. + bptAmountOut -= _POOL_MINIMUM_TOTAL_SUPPLY; + // When adding liquidity, we must mint tokens concurrently with updating pool balances, + // as the pool's math relies on totalSupply. + // Minting will be reverted if it results in a total supply less than the _POOL_MINIMUM_TOTAL_SUPPLY. + _mintMinimumSupplyReserve(address(pool)); + _mint(address(pool), to, bptAmountOut); + + // At this point we have the calculated BPT amount. + if (bptAmountOut < minBptAmountOut) { + revert BptAmountOutBelowMin(bptAmountOut, minBptAmountOut); + } + + emit PoolBalanceChanged( + pool, + to, + _totalSupply(pool), + exactAmountsIn.unsafeCastToInt256(true), + new uint256[](poolData.tokens.length) + ); + + // Emit an event to log the pool initialization. + emit PoolInitialized(pool); + } + + /******************************************************************************* + Pool Information + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isPoolInitialized( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool) { + return _isPoolInitialized(pool); + } + + /// @inheritdoc IVaultExtension + function getPoolTokens( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (IERC20[] memory tokens) { + return _poolTokens[pool]; + } + + /// @inheritdoc IVaultExtension + function getPoolTokenRates( + address pool + ) + external + view + onlyVaultDelegateCall + withRegisteredPool(pool) + returns (uint256[] memory decimalScalingFactors, uint256[] memory tokenRates) + { + // Retrieve the mapping of tokens and their balances for the specified pool. + PoolConfigBits poolConfig = _poolConfigBits[pool]; + + IERC20[] memory tokens = _poolTokens[pool]; + uint256 numTokens = tokens.length; + decimalScalingFactors = PoolConfigLib.getDecimalScalingFactors(poolConfig, numTokens); + tokenRates = new uint256[](numTokens); + + for (uint256 i = 0; i < numTokens; ++i) { + TokenInfo memory tokenInfo = _poolTokenInfo[pool][tokens[i]]; + tokenRates[i] = PoolDataLib.getTokenRate(tokenInfo); + } + } + + /// @inheritdoc IVaultExtension + function getPoolData( + address pool + ) external view onlyVaultDelegateCall withInitializedPool(pool) returns (PoolData memory) { + return _loadPoolData(pool, Rounding.ROUND_DOWN); + } + + /// @inheritdoc IVaultExtension + function getPoolTokenInfo( + address pool + ) + external + view + onlyVaultDelegateCall + withRegisteredPool(pool) + returns ( + IERC20[] memory tokens, + TokenInfo[] memory tokenInfo, + uint256[] memory balancesRaw, + uint256[] memory lastBalancesLiveScaled18 + ) + { + // Retrieve the mapping of tokens and their balances for the specified pool. + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + tokens = _poolTokens[pool]; + uint256 numTokens = tokens.length; + tokenInfo = new TokenInfo[](numTokens); + balancesRaw = new uint256[](numTokens); + lastBalancesLiveScaled18 = new uint256[](numTokens); + + for (uint256 i = 0; i < numTokens; ++i) { + bytes32 packedBalance = poolTokenBalances[i]; + tokenInfo[i] = _poolTokenInfo[pool][tokens[i]]; + balancesRaw[i] = packedBalance.getBalanceRaw(); + lastBalancesLiveScaled18[i] = packedBalance.getBalanceDerived(); + } + } + + /// @inheritdoc IVaultExtension + function getCurrentLiveBalances( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256[] memory balancesLiveScaled18) { + return _loadPoolData(pool, Rounding.ROUND_DOWN).balancesLiveScaled18; + } + + /// @inheritdoc IVaultExtension + function getPoolConfig( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (PoolConfig memory) { + PoolConfigBits config = _poolConfigBits[pool]; + + return + PoolConfig({ + isPoolRegistered: config.isPoolRegistered(), + isPoolInitialized: config.isPoolInitialized(), + isPoolPaused: config.isPoolPaused(), + isPoolInRecoveryMode: config.isPoolInRecoveryMode(), + staticSwapFeePercentage: config.getStaticSwapFeePercentage(), + aggregateSwapFeePercentage: config.getAggregateSwapFeePercentage(), + aggregateYieldFeePercentage: config.getAggregateYieldFeePercentage(), + tokenDecimalDiffs: config.getTokenDecimalDiffs(), + pauseWindowEndTime: config.getPauseWindowEndTime(), + liquidityManagement: LiquidityManagement({ + // NOTE: In contrast to the other flags, supportsUnbalancedLiquidity is enabled by default. + disableUnbalancedLiquidity: !config.supportsUnbalancedLiquidity(), + enableAddLiquidityCustom: config.supportsAddLiquidityCustom(), + enableRemoveLiquidityCustom: config.supportsRemoveLiquidityCustom(), + enableDonation: config.supportsDonation() + }) + }); + } + + /// @inheritdoc IVaultExtension + function getHooksConfig( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (HooksConfig memory) { + return _poolConfigBits[pool].toHooksConfig(_hooksContracts[pool]); + } + + /// @inheritdoc IVaultExtension + function getBptRate( + address pool + ) external view onlyVaultDelegateCall withInitializedPool(pool) returns (uint256 rate) { + PoolData memory poolData = _loadPoolData(pool, Rounding.ROUND_DOWN); + uint256 invariant = IBasePool(pool).computeInvariant(poolData.balancesLiveScaled18, Rounding.ROUND_DOWN); + + return invariant.divDown(_totalSupply(pool)); + } + + /******************************************************************************* + Balancer Pool Tokens + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function totalSupply(address token) external view onlyVaultDelegateCall returns (uint256) { + return _totalSupply(token); + } + + /// @inheritdoc IVaultExtension + function balanceOf(address token, address account) external view onlyVaultDelegateCall returns (uint256) { + return _balanceOf(token, account); + } + + /// @inheritdoc IVaultExtension + function allowance( + address token, + address owner, + address spender + ) external view onlyVaultDelegateCall returns (uint256) { + return _allowance(token, owner, spender); + } + + /// @inheritdoc IVaultExtension + function approve(address owner, address spender, uint256 amount) external onlyVaultDelegateCall returns (bool) { + _approve(msg.sender, owner, spender, amount); + return true; + } + + /// @inheritdoc IVaultExtension + function transfer(address owner, address to, uint256 amount) external onlyVaultDelegateCall returns (bool) { + _transfer(msg.sender, owner, to, amount); + return true; + } + + /// @inheritdoc IVaultExtension + function transferFrom( + address spender, + address from, + address to, + uint256 amount + ) external onlyVaultDelegateCall returns (bool) { + _spendAllowance(msg.sender, from, spender, amount); + _transfer(msg.sender, from, to, amount); + return true; + } + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isERC4626BufferInitialized(IERC4626 wrappedToken) external view onlyVaultDelegateCall returns (bool) { + return _bufferAssets[wrappedToken] != address(0); + } + + /******************************************************************************* + Pool Pausing + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isPoolPaused(address pool) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool) { + return _isPoolPaused(pool); + } + + /// @inheritdoc IVaultExtension + function getPoolPausedState( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool, uint32, uint32, address) { + (bool paused, uint32 pauseWindowEndTime) = _getPoolPausedState(pool); + + return ( + paused, + pauseWindowEndTime, + pauseWindowEndTime + _vaultBufferPeriodDuration, + _poolRoleAccounts[pool].pauseManager + ); + } + + /******************************************************************************* + Fees + *******************************************************************************/ + + // Swap and Yield fees are both stored using the PackedTokenBalance library, which is usually used for + // balances that are related (e.g., raw and live). In this case, it holds two uncorrelated values: swap + // and yield fee amounts, arbitrarily assigning "Raw" to Swap and "Derived" to Yield. + + /// @inheritdoc IVaultExtension + function getAggregateSwapFeeAmount( + address pool, + IERC20 token + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256) { + return _aggregateFeeAmounts[pool][token].getBalanceRaw(); + } + + /// @inheritdoc IVaultExtension + function getAggregateYieldFeeAmount( + address pool, + IERC20 token + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256) { + return _aggregateFeeAmounts[pool][token].getBalanceDerived(); + } + + /// @inheritdoc IVaultExtension + function getStaticSwapFeePercentage( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (uint256) { + PoolConfigBits config = _poolConfigBits[pool]; + return config.getStaticSwapFeePercentage(); + } + + /// @inheritdoc IVaultExtension + function getPoolRoleAccounts( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (PoolRoleAccounts memory) { + return _poolRoleAccounts[pool]; + } + + /// @inheritdoc IVaultExtension + function computeDynamicSwapFeePercentage( + address pool, + PoolSwapParams memory swapParams + ) external view onlyVaultDelegateCall withInitializedPool(pool) returns (uint256 dynamicSwapFeePercentage) { + return + HooksConfigLib.callComputeDynamicSwapFeeHook( + swapParams, + pool, + _poolConfigBits[pool].getStaticSwapFeePercentage(), + _hooksContracts[pool] + ); + } + + /// @inheritdoc IVaultExtension + function getProtocolFeeController() external view onlyVaultDelegateCall returns (IProtocolFeeController) { + return _protocolFeeController; + } + + /******************************************************************************* + Recovery Mode + *******************************************************************************/ + + /// @inheritdoc IVaultExtension + function isPoolInRecoveryMode( + address pool + ) external view onlyVaultDelegateCall withRegisteredPool(pool) returns (bool) { + return _isPoolInRecoveryMode(pool); + } + + /// @inheritdoc IVaultExtension + function removeLiquidityRecovery( + address pool, + address from, + uint256 exactBptAmountIn + ) + external + onlyVaultDelegateCall + onlyWhenUnlocked + nonReentrant + withInitializedPool(pool) + onlyInRecoveryMode(pool) + returns (uint256[] memory amountsOutRaw) + { + // Retrieve the mapping of tokens and their balances for the specified pool. + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances = _poolTokenBalances[pool]; + + // Initialize arrays to store tokens and balances based on the number of tokens in the pool. + IERC20[] memory tokens = _poolTokens[pool]; + uint256 numTokens = tokens.length; + + uint256[] memory balancesRaw = new uint256[](numTokens); + bytes32 packedBalances; + + for (uint256 i = 0; i < numTokens; ++i) { + balancesRaw[i] = poolTokenBalances[i].getBalanceRaw(); + } + + amountsOutRaw = BasePoolMath.computeProportionalAmountsOut(balancesRaw, _totalSupply(pool), exactBptAmountIn); + + for (uint256 i = 0; i < numTokens; ++i) { + // Credit token[i] for amountOut. + _supplyCredit(tokens[i], amountsOutRaw[i]); + + // Compute the new Pool balances. A Pool's token balance always decreases after an exit + // (potentially by 0). + balancesRaw[i] -= amountsOutRaw[i]; + } + + // Store the new pool balances - raw only, since we don't have rates in Recovery Mode. + // In Recovery Mode, raw and last live balances will get out of sync. This is corrected when the pool is taken + // out of Recovery Mode. + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolBalances = _poolTokenBalances[pool]; + + for (uint256 i = 0; i < numTokens; ++i) { + packedBalances = poolBalances[i]; + poolBalances[i] = packedBalances.setBalanceRaw(balancesRaw[i]); + } + + _spendAllowance(pool, from, msg.sender, exactBptAmountIn); + + if (_isQueryContext()) { + // Increase `from` balance to ensure the burn function succeeds. + _queryModeBalanceIncrease(pool, from, exactBptAmountIn); + } + // When removing liquidity, we must burn tokens concurrently with updating pool balances, + // as the pool's math relies on totalSupply. + // + // Burning will be reverted if it results in a total supply less than the _MINIMUM_TOTAL_SUPPLY. + _burn(pool, from, exactBptAmountIn); + + emit PoolBalanceChanged( + pool, + from, + _totalSupply(pool), + // We can unsafely cast to int256 because balances are stored as uint128 (see PackedTokenBalance). + amountsOutRaw.unsafeCastToInt256(false), + new uint256[](numTokens) + ); + } + + /******************************************************************************* + Queries + *******************************************************************************/ + + /// @dev Ensure that only static calls are made to the functions with this modifier. + modifier query() { + _setupQuery(); + _; + } + + function _setupQuery() internal { + if (EVMCallModeHelpers.isStaticCall() == false) { + revert EVMCallModeHelpers.NotStaticCall(); + } + + bool _isQueryDisabled = _vaultStateBits.isQueryDisabled(); + if (_isQueryDisabled) { + revert QueriesDisabled(); + } + + // Unlock so that `onlyWhenUnlocked` does not revert. + _isUnlocked().tstore(true); + } + + function _isQueryContext() internal view returns (bool) { + return EVMCallModeHelpers.isStaticCall() && _vaultStateBits.isQueryDisabled() == false; + } + + /// @inheritdoc IVaultExtension + function quote(bytes calldata data) external query onlyVaultDelegateCall returns (bytes memory result) { + // Forward the incoming call to the original sender of this transaction. + return (msg.sender).functionCall(data); + } + + /// @inheritdoc IVaultExtension + function quoteAndRevert(bytes calldata data) external query onlyVaultDelegateCall { + // Forward the incoming call to the original sender of this transaction. + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = (msg.sender).call(data); + if (success) { + // This will only revert if result is empty and sender account has no code. + Address.verifyCallResultFromTarget(msg.sender, success, result); + // Send result in revert reason. + revert RevertCodec.Result(result); + } else { + // If the call reverted with a spoofed `QuoteResult`, we catch it and bubble up a different reason. + bytes4 errorSelector = RevertCodec.parseSelector(result); + if (errorSelector == RevertCodec.Result.selector) { + revert QuoteResultSpoofed(); + } + + // Otherwise we bubble up the original revert reason. + RevertCodec.bubbleUpRevert(result); + } + } + + /// @inheritdoc IVaultExtension + function isQueryDisabled() external view onlyVaultDelegateCall returns (bool) { + return _vaultStateBits.isQueryDisabled(); + } + + /******************************************************************************* + Default handlers + *******************************************************************************/ + + receive() external payable { + revert CannotReceiveEth(); + } + + // solhint-disable no-complex-fallback + + /** + * @inheritdoc Proxy + * @dev Override proxy implementation of `fallback` to disallow incoming ETH transfers. + * This function actually returns whatever the VaultAdmin does when handling the request. + */ + fallback() external payable override { + if (msg.value > 0) { + revert CannotReceiveEth(); + } + + _fallback(); + } + + /******************************************************************************* + Miscellaneous + *******************************************************************************/ + + /** + * @inheritdoc Proxy + * @dev Returns the VaultAdmin contract, to which fallback requests are forwarded. + */ + function _implementation() internal view override returns (address) { + return address(_vaultAdmin); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Version.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Version.sol new file mode 100644 index 00000000..d3211b31 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/Version.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IVersion } from "./IVersion.sol"; + +/** + * @notice Retrieves a contract's version from storage. + * @dev The version is set at deployment time and cannot be changed. It would be immutable, but immutable strings + * are not yet supported. + * + * Contracts like factories and pools should have versions. These typically take the form of JSON strings containing + * detailed information about the deployment. For instance: + * + * `{name: 'ChildChainGaugeFactory', version: 2, deployment: '20230316-child-chain-gauge-factory-v2'}` + */ +contract Version is IVersion { + string private _version; + + constructor(string memory version_) { + _setVersion(version_); + } + + function version() external view returns (string memory) { + return _version; + } + + /// @dev Internal setter that allows this contract to be used in proxies. + function _setVersion(string memory newVersion) internal { + _version = newVersion; + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/WordCodec.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/WordCodec.sol new file mode 100644 index 00000000..9789cb10 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/WordCodec.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; +import { SignedMath } from "@openzeppelin/contracts/utils/math/SignedMath.sol"; + +/** + * @dev Library for encoding and decoding values stored inside a 256 bit word. Typically used to pack multiple values in + * a single storage slot, saving gas by performing less storage accesses. + * + * Each value is defined by its size and the least significant bit in the word, also known as offset. For example, two + * 128 bit values may be encoded in a word by assigning one an offset of 0, and the other an offset of 128. + * + * We could use Solidity structs to pack values together in a single storage slot instead of relying on a custom and + * error-prone library, but unfortunately Solidity only allows for structs to live in either storage, calldata or + * memory. Because a memory struct uses not just memory but also a slot in the stack (to store its memory location), + * using memory for word-sized values (i.e. of 256 bits or less) is strictly less gas performant, and doesn't even + * prevent stack-too-deep issues. This is compounded by the fact that Balancer contracts typically are memory-intensive, + * and the cost of accessing memory increases quadratically with the number of allocated words. Manual packing and + * unpacking is therefore the preferred approach. + */ +library WordCodec { + using Math for uint256; + using SignedMath for int256; + + // solhint-disable no-inline-assembly + + /// @notice Function called with an invalid value. + error CodecOverflow(); + + /// @notice Function called with an invalid bitLength or offset. + error OutOfBounds(); + + /*************************************************************************** + In-place Insertion + ***************************************************************************/ + + /** + * @dev Inserts an unsigned integer of bitLength, shifted by an offset, into a 256 bit word, + * replacing the old value. Returns the new word. + */ + function insertUint( + bytes32 word, + uint256 value, + uint256 offset, + uint256 bitLength + ) internal pure returns (bytes32 result) { + _validateEncodingParams(value, offset, bitLength); + // Equivalent to: + // uint256 mask = (1 << bitLength) - 1; + // bytes32 clearedWord = bytes32(uint256(word) & ~(mask << offset)); + // result = clearedWord | bytes32(value << offset); + + assembly ("memory-safe") { + let mask := sub(shl(bitLength, 1), 1) + let clearedWord := and(word, not(shl(offset, mask))) + result := or(clearedWord, shl(offset, value)) + } + } + + /** + * @dev Inserts an address (160 bits), shifted by an offset, into a 256 bit word, + * replacing the old value. Returns the new word. + */ + function insertAddress(bytes32 word, address value, uint256 offset) internal pure returns (bytes32 result) { + uint256 addressBitLength = 160; + _validateEncodingParams(uint256(uint160(value)), offset, addressBitLength); + // Equivalent to: + // uint256 mask = (1 << bitLength) - 1; + // bytes32 clearedWord = bytes32(uint256(word) & ~(mask << offset)); + // result = clearedWord | bytes32(value << offset); + + assembly ("memory-safe") { + let mask := sub(shl(addressBitLength, 1), 1) + let clearedWord := and(word, not(shl(offset, mask))) + result := or(clearedWord, shl(offset, value)) + } + } + + /** + * @dev Inserts a signed integer shifted by an offset into a 256 bit word, replacing the old value. Returns + * the new word. + * + * Assumes `value` can be represented using `bitLength` bits. + */ + function insertInt(bytes32 word, int256 value, uint256 offset, uint256 bitLength) internal pure returns (bytes32) { + _validateEncodingParams(value, offset, bitLength); + + uint256 mask = (1 << bitLength) - 1; + bytes32 clearedWord = bytes32(uint256(word) & ~(mask << offset)); + // Integer values need masking to remove the upper bits of negative values. + return clearedWord | bytes32((uint256(value) & mask) << offset); + } + + /*************************************************************************** + Encoding + ***************************************************************************/ + + /** + * @dev Encodes an unsigned integer shifted by an offset. Ensures value fits within + * `bitLength` bits. + * + * The return value can be ORed bitwise with other encoded values to form a 256 bit word. + */ + function encodeUint(uint256 value, uint256 offset, uint256 bitLength) internal pure returns (bytes32) { + _validateEncodingParams(value, offset, bitLength); + + return bytes32(value << offset); + } + + /** + * @dev Encodes a signed integer shifted by an offset. + * + * The return value can be ORed bitwise with other encoded values to form a 256 bit word. + */ + function encodeInt(int256 value, uint256 offset, uint256 bitLength) internal pure returns (bytes32) { + _validateEncodingParams(value, offset, bitLength); + + uint256 mask = (1 << bitLength) - 1; + // Integer values need masking to remove the upper bits of negative values. + return bytes32((uint256(value) & mask) << offset); + } + + /*************************************************************************** + Decoding + ***************************************************************************/ + + /// @dev Decodes and returns an unsigned integer with `bitLength` bits, shifted by an offset, from a 256 bit word. + function decodeUint(bytes32 word, uint256 offset, uint256 bitLength) internal pure returns (uint256 result) { + // Equivalent to: + // result = uint256(word >> offset) & ((1 << bitLength) - 1); + + assembly ("memory-safe") { + result := and(shr(offset, word), sub(shl(bitLength, 1), 1)) + } + } + + /// @dev Decodes and returns a signed integer with `bitLength` bits, shifted by an offset, from a 256 bit word. + function decodeInt(bytes32 word, uint256 offset, uint256 bitLength) internal pure returns (int256 result) { + int256 maxInt = int256((1 << (bitLength - 1)) - 1); + uint256 mask = (1 << bitLength) - 1; + + int256 value = int256(uint256(word >> offset) & mask); + // In case the decoded value is greater than the max positive integer that can be represented with bitLength + // bits, we know it was originally a negative integer. Therefore, we mask it to restore the sign in the 256 bit + // representation. + // + // Equivalent to: + // result = value > maxInt ? (value | int256(~mask)) : value; + + assembly ("memory-safe") { + result := or(mul(gt(value, maxInt), not(mask)), value) + } + } + + /// @dev Decodes and returns an address (160 bits), shifted by an offset, from a 256 bit word. + function decodeAddress(bytes32 word, uint256 offset) internal pure returns (address result) { + // Equivalent to: + // result = address(word >> offset) & ((1 << bitLength) - 1); + + assembly ("memory-safe") { + result := and(shr(offset, word), sub(shl(160, 1), 1)) + } + } + + /*************************************************************************** + Special Cases + ***************************************************************************/ + + /// @dev Decodes and returns a boolean shifted by an offset from a 256 bit word. + function decodeBool(bytes32 word, uint256 offset) internal pure returns (bool result) { + // Equivalent to: + // result = (uint256(word >> offset) & 1) == 1; + + assembly ("memory-safe") { + result := and(shr(offset, word), 1) + } + } + + /** + * @dev Inserts a boolean value shifted by an offset into a 256 bit word, replacing the old value. + * Returns the new word. + */ + function insertBool(bytes32 word, bool value, uint256 offset) internal pure returns (bytes32 result) { + // Equivalent to: + // bytes32 clearedWord = bytes32(uint256(word) & ~(1 << offset)); + // bytes32 referenceInsertBool = clearedWord | bytes32(uint256(value ? 1 : 0) << offset); + + assembly ("memory-safe") { + let clearedWord := and(word, not(shl(offset, 1))) + result := or(clearedWord, shl(offset, value)) + } + } + + /*************************************************************************** + Helpers + ***************************************************************************/ + + function _validateEncodingParams(uint256 value, uint256 offset, uint256 bitLength) private pure { + if (offset >= 256) { + revert OutOfBounds(); + } + // We never accept 256 bit values (which would make the codec pointless), and the larger the offset the smaller + // the maximum bit length. + if (!(bitLength >= 1 && bitLength <= Math.min(255, 256 - offset))) { + revert OutOfBounds(); + } + + // Testing unsigned values for size is straightforward: their upper bits must be cleared. + if (value >> bitLength != 0) { + revert CodecOverflow(); + } + } + + function _validateEncodingParams(int256 value, uint256 offset, uint256 bitLength) private pure { + if (offset >= 256) { + revert OutOfBounds(); + } + // We never accept 256 bit values (which would make the codec pointless), and the larger the offset the smaller + // the maximum bit length. + if (!(bitLength >= 1 && bitLength <= Math.min(255, 256 - offset))) { + revert OutOfBounds(); + } + + // Testing signed values for size is a bit more involved. + if (value >= 0) { + // For positive values, we can simply check that the upper bits are clear. Notice we remove one bit from the + // length for the sign bit. + if (value >> (bitLength - 1) != 0) { + revert CodecOverflow(); + } + } else { + // Negative values can receive the same treatment by making them positive, with the caveat that the range + // for negative values in two's complement supports one more value than for the positive case. + if ((value + 1).abs() >> (bitLength - 1) != 0) { + revert CodecOverflow(); + } + } + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/HooksConfigLib.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/HooksConfigLib.sol new file mode 100644 index 00000000..186afd8d --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/HooksConfigLib.sol @@ -0,0 +1,488 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IHooks } from "../interfaces/IHooks.sol"; +import { IVaultErrors } from "../interfaces/IVaultErrors.sol"; +import "../interfaces/VaultTypes.sol"; + +import { WordCodec } from "../interfaces/WordCodec.sol"; +import { FixedPoint } from "../interfaces/FixedPoint.sol"; + +import { PoolConfigConst } from "./PoolConfigConst.sol"; + +/** + * @notice Helper functions to read and write the packed hook configuration flags stored in `_poolConfigBits`. + * @dev This library has two additional functions. `toHooksConfig` constructs a `HooksConfig` structure from the + * PoolConfig and the hooks contract address. Also, there are `call` functions that forward the arguments + * to the corresponding functions in the hook contract, then validate and return the results. + * + * Note that the entire configuration of each pool is stored in the `_poolConfigBits` mapping (one slot per pool). + * This includes the data in the `PoolConfig` struct, plus the data in the `HookFlags` struct. The layout (i.e., + * offsets for each data field) is specified in `PoolConfigConst`. + * + * There are two libraries for interpreting these data. This one parses fields related to hooks, and also + * contains helpers for the struct building and hooks contract forwarding functions described above. `PoolConfigLib` + * contains helpers related to the non-hook-related flags, along with aggregate fee percentages and other data + * associated with pools. + * + * The `PoolData` struct contains the raw bitmap with the entire pool state (`PoolConfigBits`), plus the token + * configuration, scaling factors, and dynamic information such as current balances and rates. + * + * The hooks contract addresses themselves are stored in a separate `_hooksContracts` mapping. + */ +library HooksConfigLib { + using WordCodec for bytes32; + using HooksConfigLib for PoolConfigBits; + + function enableHookAdjustedAmounts(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.ENABLE_HOOK_ADJUSTED_AMOUNTS_OFFSET); + } + + function setHookAdjustedAmounts(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.ENABLE_HOOK_ADJUSTED_AMOUNTS_OFFSET) + ); + } + + function shouldCallBeforeInitialize(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.BEFORE_INITIALIZE_OFFSET); + } + + function setShouldCallBeforeInitialize(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.BEFORE_INITIALIZE_OFFSET) + ); + } + + function shouldCallAfterInitialize(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.AFTER_INITIALIZE_OFFSET); + } + + function setShouldCallAfterInitialize(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.AFTER_INITIALIZE_OFFSET) + ); + } + + function shouldCallComputeDynamicSwapFee(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.DYNAMIC_SWAP_FEE_OFFSET); + } + + function setShouldCallComputeDynamicSwapFee( + PoolConfigBits config, + bool value + ) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.DYNAMIC_SWAP_FEE_OFFSET) + ); + } + + function shouldCallBeforeSwap(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.BEFORE_SWAP_OFFSET); + } + + function setShouldCallBeforeSwap(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return PoolConfigBits.wrap(PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.BEFORE_SWAP_OFFSET)); + } + + function shouldCallAfterSwap(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.AFTER_SWAP_OFFSET); + } + + function setShouldCallAfterSwap(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return PoolConfigBits.wrap(PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.AFTER_SWAP_OFFSET)); + } + + function shouldCallBeforeAddLiquidity(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.BEFORE_ADD_LIQUIDITY_OFFSET); + } + + function setShouldCallBeforeAddLiquidity(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.BEFORE_ADD_LIQUIDITY_OFFSET) + ); + } + + function shouldCallAfterAddLiquidity(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.AFTER_ADD_LIQUIDITY_OFFSET); + } + + function setShouldCallAfterAddLiquidity(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.AFTER_ADD_LIQUIDITY_OFFSET) + ); + } + + function shouldCallBeforeRemoveLiquidity(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.BEFORE_REMOVE_LIQUIDITY_OFFSET); + } + + function setShouldCallBeforeRemoveLiquidity( + PoolConfigBits config, + bool value + ) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.BEFORE_REMOVE_LIQUIDITY_OFFSET) + ); + } + + function shouldCallAfterRemoveLiquidity(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.AFTER_REMOVE_LIQUIDITY_OFFSET); + } + + function setShouldCallAfterRemoveLiquidity( + PoolConfigBits config, + bool value + ) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.AFTER_REMOVE_LIQUIDITY_OFFSET) + ); + } + + function toHooksConfig(PoolConfigBits config, IHooks hooksContract) internal pure returns (HooksConfig memory) { + return + HooksConfig({ + enableHookAdjustedAmounts: config.enableHookAdjustedAmounts(), + shouldCallBeforeInitialize: config.shouldCallBeforeInitialize(), + shouldCallAfterInitialize: config.shouldCallAfterInitialize(), + shouldCallBeforeAddLiquidity: config.shouldCallBeforeAddLiquidity(), + shouldCallAfterAddLiquidity: config.shouldCallAfterAddLiquidity(), + shouldCallBeforeRemoveLiquidity: config.shouldCallBeforeRemoveLiquidity(), + shouldCallAfterRemoveLiquidity: config.shouldCallAfterRemoveLiquidity(), + shouldCallComputeDynamicSwapFee: config.shouldCallComputeDynamicSwapFee(), + shouldCallBeforeSwap: config.shouldCallBeforeSwap(), + shouldCallAfterSwap: config.shouldCallAfterSwap(), + hooksContract: address(hooksContract) + }); + } + + /** + * @dev Call the `onComputeDynamicSwapFeePercentage` hook and return the result. Reverts on failure. + * @param swapParams The swap parameters used to calculate the fee + * @param pool Pool address + * @param staticSwapFeePercentage Value of the static swap fee, for reference + * @param hooksContract Storage slot with the address of the hooks contract + * @return swapFeePercentage The calculated swap fee percentage + */ + function callComputeDynamicSwapFeeHook( + PoolSwapParams memory swapParams, + address pool, + uint256 staticSwapFeePercentage, + IHooks hooksContract + ) internal view returns (uint256) { + (bool success, uint256 swapFeePercentage) = hooksContract.onComputeDynamicSwapFeePercentage( + swapParams, + pool, + staticSwapFeePercentage + ); + + if (success == false) { + revert IVaultErrors.DynamicSwapFeeHookFailed(); + } + + if (swapFeePercentage > FixedPoint.ONE) { + revert IVaultErrors.PercentageAboveMax(); + } + + return swapFeePercentage; + } + + /** + * @dev Call the `onBeforeSwap` hook. Reverts on failure. + * @param swapParams The swap parameters used in the hook + * @param pool Pool address + * @param hooksContract Storage slot with the address of the hooks contract + */ + function callBeforeSwapHook(PoolSwapParams memory swapParams, address pool, IHooks hooksContract) internal { + if (hooksContract.onBeforeSwap(swapParams, pool) == false) { + // Hook contract implements onBeforeSwap, but it has failed, so reverts the transaction. + revert IVaultErrors.BeforeSwapHookFailed(); + } + } + + /** + * @dev Call the `onAfterSwap` hook, then validate and return the result. Reverts on failure, or if the limits + * are violated. If the hook contract did not enable hook-adjusted amounts, it will ignore the hook results and + * return the original `amountCalculatedRaw`. + * + * @param config The encoded pool configuration + * @param amountCalculatedScaled18 Token amount calculated by the swap + * @param amountCalculatedRaw Token amount calculated by the swap + * @param router Router address + * @param vaultSwapParams The swap parameters + * @param state Temporary state used in swap operations + * @param poolData Struct containing balance and token information of the pool + * @param hooksContract Storage slot with the address of the hooks contract + * @return hookAdjustedAmountCalculatedRaw New amount calculated, potentially modified by the hook + */ + function callAfterSwapHook( + PoolConfigBits config, + uint256 amountCalculatedScaled18, + uint256 amountCalculatedRaw, + address router, + VaultSwapParams memory vaultSwapParams, + SwapState memory state, + PoolData memory poolData, + IHooks hooksContract + ) internal returns (uint256) { + // Adjust balances for the AfterSwap hook. + (uint256 amountInScaled18, uint256 amountOutScaled18) = vaultSwapParams.kind == SwapKind.EXACT_IN + ? (state.amountGivenScaled18, amountCalculatedScaled18) + : (amountCalculatedScaled18, state.amountGivenScaled18); + + (bool success, uint256 hookAdjustedAmountCalculatedRaw) = hooksContract.onAfterSwap( + AfterSwapParams({ + kind: vaultSwapParams.kind, + tokenIn: vaultSwapParams.tokenIn, + tokenOut: vaultSwapParams.tokenOut, + amountInScaled18: amountInScaled18, + amountOutScaled18: amountOutScaled18, + tokenInBalanceScaled18: poolData.balancesLiveScaled18[state.indexIn], + tokenOutBalanceScaled18: poolData.balancesLiveScaled18[state.indexOut], + amountCalculatedScaled18: amountCalculatedScaled18, + amountCalculatedRaw: amountCalculatedRaw, + router: router, + pool: vaultSwapParams.pool, + userData: vaultSwapParams.userData + }) + ); + + if (success == false) { + // Hook contract implements onAfterSwap, but it has failed, so reverts the transaction. + revert IVaultErrors.AfterSwapHookFailed(); + } + + // If hook adjusted amounts is not enabled, ignore amounts returned by the hook + if (config.enableHookAdjustedAmounts() == false) { + return amountCalculatedRaw; + } + + if ( + (vaultSwapParams.kind == SwapKind.EXACT_IN && hookAdjustedAmountCalculatedRaw < vaultSwapParams.limitRaw) || + (vaultSwapParams.kind == SwapKind.EXACT_OUT && hookAdjustedAmountCalculatedRaw > vaultSwapParams.limitRaw) + ) { + revert IVaultErrors.HookAdjustedSwapLimit(hookAdjustedAmountCalculatedRaw, vaultSwapParams.limitRaw); + } + + return hookAdjustedAmountCalculatedRaw; + } + + /** + * @dev Call the `onBeforeAddLiquidity` hook. Reverts on failure. + * @param router Router address + * @param maxAmountsInScaled18 An array with maximum amounts for each input token of the add liquidity operation + * @param params The add liquidity parameters + * @param poolData Struct containing balance and token information of the pool + * @param hooksContract Storage slot with the address of the hooks contract + */ + function callBeforeAddLiquidityHook( + address router, + uint256[] memory maxAmountsInScaled18, + AddLiquidityParams memory params, + PoolData memory poolData, + IHooks hooksContract + ) internal { + if ( + hooksContract.onBeforeAddLiquidity( + router, + params.pool, + params.kind, + maxAmountsInScaled18, + params.minBptAmountOut, + poolData.balancesLiveScaled18, + params.userData + ) == false + ) { + revert IVaultErrors.BeforeAddLiquidityHookFailed(); + } + } + + /** + * @dev Call the `onAfterAddLiquidity` hook, then validate and return the result. Reverts on failure, or if + * the limits are violated. If the contract did not enable hook-adjusted amounts, it will ignore the hook + * results and return the original `amountsInRaw`. + * + * @param config The encoded pool configuration + * @param router Router address + * @param amountsInScaled18 An array with amounts for each input token of the add liquidity operation + * @param amountsInRaw An array with amounts for each input token of the add liquidity operation + * @param bptAmountOut The BPT amount a user will receive after add liquidity operation succeeds + * @param params The add liquidity parameters + * @param poolData Struct containing balance and token information of the pool + * @param hooksContract Storage slot with the address of the hooks contract + * @return hookAdjustedAmountsInRaw New amountsInRaw, potentially modified by the hook + */ + function callAfterAddLiquidityHook( + PoolConfigBits config, + address router, + uint256[] memory amountsInScaled18, + uint256[] memory amountsInRaw, + uint256 bptAmountOut, + AddLiquidityParams memory params, + PoolData memory poolData, + IHooks hooksContract + ) internal returns (uint256[] memory) { + (bool success, uint256[] memory hookAdjustedAmountsInRaw) = hooksContract.onAfterAddLiquidity( + router, + params.pool, + params.kind, + amountsInScaled18, + amountsInRaw, + bptAmountOut, + poolData.balancesLiveScaled18, + params.userData + ); + + if (success == false || hookAdjustedAmountsInRaw.length != amountsInRaw.length) { + revert IVaultErrors.AfterAddLiquidityHookFailed(); + } + + // If hook adjusted amounts is not enabled, ignore amounts returned by the hook + if (config.enableHookAdjustedAmounts() == false) { + return amountsInRaw; + } + + for (uint256 i = 0; i < hookAdjustedAmountsInRaw.length; i++) { + if (hookAdjustedAmountsInRaw[i] > params.maxAmountsIn[i]) { + revert IVaultErrors.HookAdjustedAmountInAboveMax( + poolData.tokens[i], + hookAdjustedAmountsInRaw[i], + params.maxAmountsIn[i] + ); + } + } + + return hookAdjustedAmountsInRaw; + } + + /** + * @dev Call the `onBeforeRemoveLiquidity` hook. Reverts on failure. + * @param minAmountsOutScaled18 Minimum amounts for each output token of the remove liquidity operation + * @param router Router address + * @param params The remove liquidity parameters + * @param poolData Struct containing balance and token information of the pool + * @param hooksContract Storage slot with the address of the hooks contract + */ + function callBeforeRemoveLiquidityHook( + uint256[] memory minAmountsOutScaled18, + address router, + RemoveLiquidityParams memory params, + PoolData memory poolData, + IHooks hooksContract + ) internal { + if ( + hooksContract.onBeforeRemoveLiquidity( + router, + params.pool, + params.kind, + params.maxBptAmountIn, + minAmountsOutScaled18, + poolData.balancesLiveScaled18, + params.userData + ) == false + ) { + revert IVaultErrors.BeforeRemoveLiquidityHookFailed(); + } + } + + /** + * @dev Call the `onAfterRemoveLiquidity` hook, then validate and return the result. Reverts on failure, or if + * the limits are violated. If the contract did not enable hook-adjusted amounts, it will ignore the hook + * results and return the original `amountsOutRaw`. + * + * @param config The encoded pool configuration + * @param router Router address + * @param amountsOutScaled18 Scaled amount of tokens to receive, sorted in token registration order + * @param amountsOutRaw Actual amount of tokens to receive, sorted in token registration order + * @param bptAmountIn The BPT amount a user will need burn to remove the liquidity of the pool + * @param params The remove liquidity parameters + * @param poolData Struct containing balance and token information of the pool + * @param hooksContract Storage slot with the address of the hooks contract + * @return hookAdjustedAmountsOutRaw New amountsOutRaw, potentially modified by the hook + */ + function callAfterRemoveLiquidityHook( + PoolConfigBits config, + address router, + uint256[] memory amountsOutScaled18, + uint256[] memory amountsOutRaw, + uint256 bptAmountIn, + RemoveLiquidityParams memory params, + PoolData memory poolData, + IHooks hooksContract + ) internal returns (uint256[] memory) { + (bool success, uint256[] memory hookAdjustedAmountsOutRaw) = hooksContract.onAfterRemoveLiquidity( + router, + params.pool, + params.kind, + bptAmountIn, + amountsOutScaled18, + amountsOutRaw, + poolData.balancesLiveScaled18, + params.userData + ); + + if (success == false || hookAdjustedAmountsOutRaw.length != amountsOutRaw.length) { + revert IVaultErrors.AfterRemoveLiquidityHookFailed(); + } + + // If hook adjusted amounts is not enabled, ignore amounts returned by the hook + if (config.enableHookAdjustedAmounts() == false) { + return amountsOutRaw; + } + + for (uint256 i = 0; i < hookAdjustedAmountsOutRaw.length; i++) { + if (hookAdjustedAmountsOutRaw[i] < params.minAmountsOut[i]) { + revert IVaultErrors.HookAdjustedAmountOutBelowMin( + poolData.tokens[i], + hookAdjustedAmountsOutRaw[i], + params.minAmountsOut[i] + ); + } + } + + return hookAdjustedAmountsOutRaw; + } + + /** + * @dev Call the `onBeforeInitialize` hook. Reverts on failure. + * @param exactAmountsInScaled18 An array with the initial liquidity of the pool + * @param userData Additional (optional) data required for adding initial liquidity + * @param hooksContract Storage slot with the address of the hooks contract + */ + function callBeforeInitializeHook( + uint256[] memory exactAmountsInScaled18, + bytes memory userData, + IHooks hooksContract + ) internal { + if (hooksContract.onBeforeInitialize(exactAmountsInScaled18, userData) == false) { + revert IVaultErrors.BeforeInitializeHookFailed(); + } + } + + /** + * @dev Call the `onAfterInitialize` hook. Reverts on failure. + * @param exactAmountsInScaled18 An array with the initial liquidity of the pool + * @param bptAmountOut The BPT amount a user will receive after initialization operation succeeds + * @param userData Additional (optional) data required for adding initial liquidity + * @param hooksContract Storage slot with the address of the hooks contract + */ + function callAfterInitializeHook( + uint256[] memory exactAmountsInScaled18, + uint256 bptAmountOut, + bytes memory userData, + IHooks hooksContract + ) internal { + if (hooksContract.onAfterInitialize(exactAmountsInScaled18, bptAmountOut, userData) == false) { + revert IVaultErrors.AfterInitializeHookFailed(); + } + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigConst.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigConst.sol new file mode 100644 index 00000000..a70ad8ac --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigConst.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { FEE_BITLENGTH } from "../interfaces/VaultTypes.sol"; + +/** + * @notice Helper functions to read and write the packed configuration flags stored in `_poolConfigBits`. + * @dev Note that the entire configuration of each pool is stored in the `_poolConfigBits` mapping (one slot per pool). + * This includes the data in the `PoolConfig` struct, plus the data in the `HookFlags` struct. The layout (i.e., + * offsets for each data field) is specified here. + * + * There are two libraries for interpreting these data. `HooksConfigLib` parses fields related to hooks, while + * `PoolConfigLib` contains helpers related to the non-hook-related flags, along with aggregate fee percentages + * and other data associated with pools. + */ +library PoolConfigConst { + // Bit offsets for main pool config settings. + uint8 public constant POOL_REGISTERED_OFFSET = 0; + uint8 public constant POOL_INITIALIZED_OFFSET = POOL_REGISTERED_OFFSET + 1; + uint8 public constant POOL_PAUSED_OFFSET = POOL_INITIALIZED_OFFSET + 1; + uint8 public constant POOL_RECOVERY_MODE_OFFSET = POOL_PAUSED_OFFSET + 1; + + // Bit offsets for liquidity operations. + uint8 public constant UNBALANCED_LIQUIDITY_OFFSET = POOL_RECOVERY_MODE_OFFSET + 1; + uint8 public constant ADD_LIQUIDITY_CUSTOM_OFFSET = UNBALANCED_LIQUIDITY_OFFSET + 1; + uint8 public constant REMOVE_LIQUIDITY_CUSTOM_OFFSET = ADD_LIQUIDITY_CUSTOM_OFFSET + 1; + uint8 public constant DONATION_OFFSET = REMOVE_LIQUIDITY_CUSTOM_OFFSET + 1; + + // Bit offsets for hooks config. + uint8 public constant BEFORE_INITIALIZE_OFFSET = DONATION_OFFSET + 1; + uint8 public constant ENABLE_HOOK_ADJUSTED_AMOUNTS_OFFSET = BEFORE_INITIALIZE_OFFSET + 1; + uint8 public constant AFTER_INITIALIZE_OFFSET = ENABLE_HOOK_ADJUSTED_AMOUNTS_OFFSET + 1; + uint8 public constant DYNAMIC_SWAP_FEE_OFFSET = AFTER_INITIALIZE_OFFSET + 1; + uint8 public constant BEFORE_SWAP_OFFSET = DYNAMIC_SWAP_FEE_OFFSET + 1; + uint8 public constant AFTER_SWAP_OFFSET = BEFORE_SWAP_OFFSET + 1; + uint8 public constant BEFORE_ADD_LIQUIDITY_OFFSET = AFTER_SWAP_OFFSET + 1; + uint8 public constant AFTER_ADD_LIQUIDITY_OFFSET = BEFORE_ADD_LIQUIDITY_OFFSET + 1; + uint8 public constant BEFORE_REMOVE_LIQUIDITY_OFFSET = AFTER_ADD_LIQUIDITY_OFFSET + 1; + uint8 public constant AFTER_REMOVE_LIQUIDITY_OFFSET = BEFORE_REMOVE_LIQUIDITY_OFFSET + 1; + + // Bit offsets for uint values. + uint8 public constant STATIC_SWAP_FEE_OFFSET = AFTER_REMOVE_LIQUIDITY_OFFSET + 1; + uint256 public constant AGGREGATE_SWAP_FEE_OFFSET = STATIC_SWAP_FEE_OFFSET + FEE_BITLENGTH; + uint256 public constant AGGREGATE_YIELD_FEE_OFFSET = AGGREGATE_SWAP_FEE_OFFSET + FEE_BITLENGTH; + uint256 public constant DECIMAL_SCALING_FACTORS_OFFSET = AGGREGATE_YIELD_FEE_OFFSET + FEE_BITLENGTH; + uint256 public constant PAUSE_WINDOW_END_TIME_OFFSET = + DECIMAL_SCALING_FACTORS_OFFSET + TOKEN_DECIMAL_DIFFS_BITLENGTH; + + // Uses a uint40 to pack the values: 8 tokens * 5 bits/token. + // This maximum token count is also hard-coded in the Vault. + uint8 public constant TOKEN_DECIMAL_DIFFS_BITLENGTH = 40; + uint8 public constant DECIMAL_DIFF_BITLENGTH = 5; + + uint8 public constant TIMESTAMP_BITLENGTH = 32; +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigLib.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigLib.sol new file mode 100644 index 00000000..ff77a6f1 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolConfigLib.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IVaultErrors } from "../interfaces/IVaultErrors.sol"; +import "../interfaces/VaultTypes.sol"; + +import { WordCodec } from "../interfaces/WordCodec.sol"; +import { FixedPoint } from "../interfaces/FixedPoint.sol"; + +import { PoolConfigConst } from "./PoolConfigConst.sol"; + +/** + * @notice Helper functions to read and write the packed hook configuration flags stored in `_poolConfigBits`. + * @dev Note that the entire configuration of each pool is stored in the `_poolConfigBits` mapping (one slot + * per pool). This includes the data in the `PoolConfig` struct, plus the data in the `HookFlags` struct. + * The layout (i.e., offsets for each data field) is specified in `PoolConfigConst`. + * + * There are two libraries for interpreting these data. `HooksConfigLib` parses fields related to hooks, while + * this one contains helpers related to the non-hook-related flags, along with aggregate fee percentages and + * other data associated with pools. + * + * The `PoolData` struct contains the raw bitmap with the entire pool state (`PoolConfigBits`), plus the token + * configuration, scaling factors, and dynamic information such as current balances and rates. + */ +library PoolConfigLib { + using WordCodec for bytes32; + using PoolConfigLib for PoolConfigBits; + + // Bit offsets for main pool config settings. + function isPoolRegistered(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.POOL_REGISTERED_OFFSET); + } + + function setPoolRegistered(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.POOL_REGISTERED_OFFSET) + ); + } + + function isPoolInitialized(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.POOL_INITIALIZED_OFFSET); + } + + function setPoolInitialized(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.POOL_INITIALIZED_OFFSET) + ); + } + + function isPoolPaused(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.POOL_PAUSED_OFFSET); + } + + function setPoolPaused(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return PoolConfigBits.wrap(PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.POOL_PAUSED_OFFSET)); + } + + function isPoolInRecoveryMode(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.POOL_RECOVERY_MODE_OFFSET); + } + + function setPoolInRecoveryMode(PoolConfigBits config, bool value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(value, PoolConfigConst.POOL_RECOVERY_MODE_OFFSET) + ); + } + + // Bit offsets for liquidity operations. + function supportsUnbalancedLiquidity(PoolConfigBits config) internal pure returns (bool) { + // NOTE: The unbalanced liquidity flag is default-on (false means it is supported). + // This function returns the inverted value. + return !PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.UNBALANCED_LIQUIDITY_OFFSET); + } + + function requireUnbalancedLiquidityEnabled(PoolConfigBits config) internal pure { + if (config.supportsUnbalancedLiquidity() == false) { + revert IVaultErrors.DoesNotSupportUnbalancedLiquidity(); + } + } + + function setDisableUnbalancedLiquidity( + PoolConfigBits config, + bool disableUnbalancedLiquidity + ) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool( + disableUnbalancedLiquidity, + PoolConfigConst.UNBALANCED_LIQUIDITY_OFFSET + ) + ); + } + + function supportsAddLiquidityCustom(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.ADD_LIQUIDITY_CUSTOM_OFFSET); + } + + function requireAddLiquidityCustomEnabled(PoolConfigBits config) internal pure { + if (config.supportsAddLiquidityCustom() == false) { + revert IVaultErrors.DoesNotSupportAddLiquidityCustom(); + } + } + + function setAddLiquidityCustom( + PoolConfigBits config, + bool enableAddLiquidityCustom + ) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool( + enableAddLiquidityCustom, + PoolConfigConst.ADD_LIQUIDITY_CUSTOM_OFFSET + ) + ); + } + + function supportsRemoveLiquidityCustom(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.REMOVE_LIQUIDITY_CUSTOM_OFFSET); + } + + function requireRemoveLiquidityCustomEnabled(PoolConfigBits config) internal pure { + if (config.supportsRemoveLiquidityCustom() == false) { + revert IVaultErrors.DoesNotSupportRemoveLiquidityCustom(); + } + } + + function setRemoveLiquidityCustom( + PoolConfigBits config, + bool enableRemoveLiquidityCustom + ) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool( + enableRemoveLiquidityCustom, + PoolConfigConst.REMOVE_LIQUIDITY_CUSTOM_OFFSET + ) + ); + } + + function supportsDonation(PoolConfigBits config) internal pure returns (bool) { + return PoolConfigBits.unwrap(config).decodeBool(PoolConfigConst.DONATION_OFFSET); + } + + function setDonation(PoolConfigBits config, bool enableDonation) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertBool(enableDonation, PoolConfigConst.DONATION_OFFSET) + ); + } + + function requireDonationEnabled(PoolConfigBits config) internal pure { + if (config.supportsDonation() == false) { + revert IVaultErrors.DoesNotSupportDonation(); + } + } + + // Bit offsets for uint values. + function getStaticSwapFeePercentage(PoolConfigBits config) internal pure returns (uint256) { + return + PoolConfigBits.unwrap(config).decodeUint(PoolConfigConst.STATIC_SWAP_FEE_OFFSET, FEE_BITLENGTH) * + FEE_SCALING_FACTOR; + } + + function setStaticSwapFeePercentage(PoolConfigBits config, uint256 value) internal pure returns (PoolConfigBits) { + if (value > MAX_FEE_PERCENTAGE) { + revert IVaultErrors.PercentageAboveMax(); + } + value /= FEE_SCALING_FACTOR; + + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertUint(value, PoolConfigConst.STATIC_SWAP_FEE_OFFSET, FEE_BITLENGTH) + ); + } + + function getAggregateSwapFeePercentage(PoolConfigBits config) internal pure returns (uint256) { + return + PoolConfigBits.unwrap(config).decodeUint(PoolConfigConst.AGGREGATE_SWAP_FEE_OFFSET, FEE_BITLENGTH) * + FEE_SCALING_FACTOR; + } + + function setAggregateSwapFeePercentage( + PoolConfigBits config, + uint256 value + ) internal pure returns (PoolConfigBits) { + if (value > MAX_FEE_PERCENTAGE) { + revert IVaultErrors.PercentageAboveMax(); + } + value /= FEE_SCALING_FACTOR; + + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertUint( + value, + PoolConfigConst.AGGREGATE_SWAP_FEE_OFFSET, + FEE_BITLENGTH + ) + ); + } + + function getAggregateYieldFeePercentage(PoolConfigBits config) internal pure returns (uint256) { + return + PoolConfigBits.unwrap(config).decodeUint(PoolConfigConst.AGGREGATE_YIELD_FEE_OFFSET, FEE_BITLENGTH) * + FEE_SCALING_FACTOR; + } + + function setAggregateYieldFeePercentage( + PoolConfigBits config, + uint256 value + ) internal pure returns (PoolConfigBits) { + if (value > MAX_FEE_PERCENTAGE) { + revert IVaultErrors.PercentageAboveMax(); + } + value /= FEE_SCALING_FACTOR; + + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertUint( + value, + PoolConfigConst.AGGREGATE_YIELD_FEE_OFFSET, + FEE_BITLENGTH + ) + ); + } + + function getTokenDecimalDiffs(PoolConfigBits config) internal pure returns (uint40) { + return + uint40( + PoolConfigBits.unwrap(config).decodeUint( + PoolConfigConst.DECIMAL_SCALING_FACTORS_OFFSET, + PoolConfigConst.TOKEN_DECIMAL_DIFFS_BITLENGTH + ) + ); + } + + function getDecimalScalingFactors( + PoolConfigBits config, + uint256 numTokens + ) internal pure returns (uint256[] memory) { + uint256[] memory scalingFactors = new uint256[](numTokens); + + bytes32 tokenDecimalDiffs = bytes32(uint256(config.getTokenDecimalDiffs())); + + for (uint256 i = 0; i < numTokens; ++i) { + uint256 decimalDiff = tokenDecimalDiffs.decodeUint( + i * PoolConfigConst.DECIMAL_DIFF_BITLENGTH, + PoolConfigConst.DECIMAL_DIFF_BITLENGTH + ); + + // This is a "raw" factor, not a fixed point number. It should be applied using raw math to raw amounts + // instead of using FP multiplication. + scalingFactors[i] = 10 ** decimalDiff; + } + + return scalingFactors; + } + + function setTokenDecimalDiffs(PoolConfigBits config, uint40 value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertUint( + value, + PoolConfigConst.DECIMAL_SCALING_FACTORS_OFFSET, + PoolConfigConst.TOKEN_DECIMAL_DIFFS_BITLENGTH + ) + ); + } + + function getPauseWindowEndTime(PoolConfigBits config) internal pure returns (uint32) { + return + uint32( + PoolConfigBits.unwrap(config).decodeUint( + PoolConfigConst.PAUSE_WINDOW_END_TIME_OFFSET, + PoolConfigConst.TIMESTAMP_BITLENGTH + ) + ); + } + + function setPauseWindowEndTime(PoolConfigBits config, uint32 value) internal pure returns (PoolConfigBits) { + return + PoolConfigBits.wrap( + PoolConfigBits.unwrap(config).insertUint( + value, + PoolConfigConst.PAUSE_WINDOW_END_TIME_OFFSET, + PoolConfigConst.TIMESTAMP_BITLENGTH + ) + ); + } + + // Convert from an array of decimal differences, to the encoded 40-bit value (8 tokens * 5 bits/token). + function toTokenDecimalDiffs(uint8[] memory tokenDecimalDiffs) internal pure returns (uint40) { + bytes32 value; + + for (uint256 i = 0; i < tokenDecimalDiffs.length; ++i) { + value = value.insertUint( + tokenDecimalDiffs[i], + i * PoolConfigConst.DECIMAL_DIFF_BITLENGTH, + PoolConfigConst.DECIMAL_DIFF_BITLENGTH + ); + } + + return uint40(uint256(value)); + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolDataLib.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolDataLib.sol new file mode 100644 index 00000000..2d4f7cee --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/PoolDataLib.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { PoolData, TokenInfo, TokenType, Rounding } from "../interfaces/VaultTypes.sol"; +import { IVaultErrors } from "../interfaces/IVaultErrors.sol"; + +import { FixedPoint } from "../interfaces/FixedPoint.sol"; +import { ScalingHelpers } from "../interfaces/ScalingHelpers.sol"; +import { PackedTokenBalance } from "../interfaces/PackedTokenBalance.sol"; + +import { PoolConfigBits, PoolConfigLib } from "./PoolConfigLib.sol"; + +/** + * @notice Helper functions to read/write a `PoolData` struct. + * @dev Note that the entire configuration of each pool is stored in the `_poolConfigBits` mapping (one slot per pool). + * This includes the data in the `PoolConfig` struct, plus the data in the `HookFlags` struct. The layout (i.e., + * offsets for each data field) is specified in `PoolConfigConst`. + * + * The `PoolData` struct contains the raw bitmap with the entire pool state (`PoolConfigBits`), plus the token + * configuration, scaling factors, and dynamic information such as current balances and rates. + */ +library PoolDataLib { + using PackedTokenBalance for bytes32; + using FixedPoint for *; + using ScalingHelpers for *; + using PoolConfigLib for PoolConfigBits; + + function load( + PoolData memory poolData, + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances, + PoolConfigBits poolConfigBits, + mapping(IERC20 poolToken => TokenInfo tokenInfo) storage poolTokenInfo, + IERC20[] storage tokens, + Rounding roundingDirection + ) internal view { + uint256 numTokens = tokens.length; + + poolData.poolConfigBits = poolConfigBits; + poolData.tokens = tokens; + poolData.tokenInfo = new TokenInfo[](numTokens); + poolData.balancesRaw = new uint256[](numTokens); + poolData.balancesLiveScaled18 = new uint256[](numTokens); + poolData.decimalScalingFactors = PoolConfigLib.getDecimalScalingFactors(poolData.poolConfigBits, numTokens); + poolData.tokenRates = new uint256[](numTokens); + + bool poolSubjectToYieldFees = poolData.poolConfigBits.isPoolInitialized() && + poolData.poolConfigBits.getAggregateYieldFeePercentage() > 0 && + poolData.poolConfigBits.isPoolInRecoveryMode() == false; + + for (uint256 i = 0; i < numTokens; ++i) { + TokenInfo memory tokenInfo = poolTokenInfo[poolData.tokens[i]]; + bytes32 packedBalance = poolTokenBalances[i]; + + poolData.tokenInfo[i] = tokenInfo; + poolData.tokenRates[i] = getTokenRate(tokenInfo); + updateRawAndLiveBalance(poolData, i, packedBalance.getBalanceRaw(), roundingDirection); + + // If there are no yield fees, we can save gas by skipping to the next token now. + if (poolSubjectToYieldFees == false) { + continue; + } + + // `poolData` already has live balances computed from raw balances according to the token rates and the + // given rounding direction. Charging a yield fee changes the raw balance, in which case the safest and + // most numerically precise way to adjust the live balance is to simply repeat the scaling (hence the + // second call below). + + // The Vault actually guarantees that a token with paysYieldFees set is a WITH_RATE token, so technically + // we could just check the flag, but we don't want to introduce that dependency for a slight gas savings. + bool tokenSubjectToYieldFees = tokenInfo.paysYieldFees && tokenInfo.tokenType == TokenType.WITH_RATE; + + // Do not charge yield fees before the pool is initialized, or in recovery mode. + if (tokenSubjectToYieldFees) { + uint256 aggregateYieldFeePercentage = poolData.poolConfigBits.getAggregateYieldFeePercentage(); + uint256 balanceRaw = poolData.balancesRaw[i]; + + uint256 aggregateYieldFeeAmountRaw = _computeYieldFeesDue( + poolData, + packedBalance.getBalanceDerived(), + i, + aggregateYieldFeePercentage + ); + + if (aggregateYieldFeeAmountRaw > 0) { + updateRawAndLiveBalance(poolData, i, balanceRaw - aggregateYieldFeeAmountRaw, roundingDirection); + } + } + } + } + + function syncPoolBalancesAndFees( + PoolData memory poolData, + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances, + mapping(IERC20 token => bytes32 packedFeeAmounts) storage poolAggregateProtocolFeeAmounts + ) internal { + uint256 numTokens = poolData.balancesRaw.length; + + for (uint256 i = 0; i < numTokens; ++i) { + IERC20 token = poolData.tokens[i]; + bytes32 packedBalances = poolTokenBalances[i]; + uint256 storedBalanceRaw = packedBalances.getBalanceRaw(); + + // `poolData` now has balances updated with yield fees. + // If yield fees are not 0, then the stored balance is greater than the one in memory. + if (storedBalanceRaw > poolData.balancesRaw[i]) { + // Both Swap and Yield fees are stored together in a `PackedTokenBalance`. + // We have designated "Derived" the derived half for Yield fee storage. + bytes32 packedProtocolFeeAmounts = poolAggregateProtocolFeeAmounts[token]; + poolAggregateProtocolFeeAmounts[token] = packedProtocolFeeAmounts.setBalanceDerived( + packedProtocolFeeAmounts.getBalanceDerived() + (storedBalanceRaw - poolData.balancesRaw[i]) + ); + } + + poolTokenBalances[i] = PackedTokenBalance.toPackedBalance( + poolData.balancesRaw[i], + poolData.balancesLiveScaled18[i] + ); + } + } + + /** + * @dev This is typically called after a reentrant callback (e.g., a "before" liquidity operation callback), + * to refresh the poolData struct with any balances (or rates) that might have changed. + * + * Preconditions: tokenConfig, balancesRaw, and decimalScalingFactors must be current in `poolData`. + * Side effects: mutates tokenRates, balancesLiveScaled18 in `poolData`. + */ + function reloadBalancesAndRates( + PoolData memory poolData, + mapping(uint256 tokenIndex => bytes32 packedTokenBalance) storage poolTokenBalances, + Rounding roundingDirection + ) internal view { + uint256 numTokens = poolData.tokens.length; + + // It's possible a reentrant hook changed the raw balances in Vault storage. + // Update them before computing the live balances. + bytes32 packedBalance; + + for (uint256 i = 0; i < numTokens; ++i) { + poolData.tokenRates[i] = getTokenRate(poolData.tokenInfo[i]); + + packedBalance = poolTokenBalances[i]; + + // Note the order dependency. This requires up-to-date tokenRate for the token at index `i` in `poolData` + updateRawAndLiveBalance(poolData, i, packedBalance.getBalanceRaw(), roundingDirection); + } + } + + function getTokenRate(TokenInfo memory tokenInfo) internal view returns (uint256 rate) { + TokenType tokenType = tokenInfo.tokenType; + + if (tokenType == TokenType.STANDARD) { + rate = FixedPoint.ONE; + } else if (tokenType == TokenType.WITH_RATE) { + rate = tokenInfo.rateProvider.getRate(); + } else { + revert IVaultErrors.InvalidTokenConfiguration(); + } + } + + function updateRawAndLiveBalance( + PoolData memory poolData, + uint256 tokenIndex, + uint256 newRawBalance, + Rounding roundingDirection + ) internal pure { + poolData.balancesRaw[tokenIndex] = newRawBalance; + + function(uint256, uint256, uint256) internal pure returns (uint256) _upOrDown = roundingDirection == + Rounding.ROUND_UP + ? ScalingHelpers.toScaled18ApplyRateRoundUp + : ScalingHelpers.toScaled18ApplyRateRoundDown; + + poolData.balancesLiveScaled18[tokenIndex] = _upOrDown( + newRawBalance, + poolData.decimalScalingFactors[tokenIndex], + poolData.tokenRates[tokenIndex] + ); + } + + // solhint-disable-next-line private-vars-leading-underscore + function _computeYieldFeesDue( + PoolData memory poolData, + uint256 lastLiveBalance, + uint256 tokenIndex, + uint256 aggregateYieldFeePercentage + ) internal pure returns (uint256 aggregateYieldFeeAmountRaw) { + uint256 currentLiveBalance = poolData.balancesLiveScaled18[tokenIndex]; + + // Do not charge fees if rates go down. If the rate were to go up, down, and back up again, protocol fees + // would be charged multiple times on the "same" yield. For tokens subject to yield fees, this should not + // happen, or at least be very rare. It can be addressed for known volatile rates by setting the yield fee + // exempt flag on registration, or compensated off-chain if there is an incident with a normally + // well-behaved rate provider. + if (currentLiveBalance > lastLiveBalance) { + unchecked { + // Magnitudes are checked above, so it's safe to do unchecked math here. + uint256 aggregateYieldFeeAmountScaled18 = (currentLiveBalance - lastLiveBalance).mulUp( + aggregateYieldFeePercentage + ); + + // A pool is subject to yield fees if poolSubjectToYieldFees is true, meaning that + // `protocolYieldFeePercentage > 0`. So, we don't need to check this again in here, saving some gas. + aggregateYieldFeeAmountRaw = aggregateYieldFeeAmountScaled18.toRawUndoRateRoundDown( + poolData.decimalScalingFactors[tokenIndex], + poolData.tokenRates[tokenIndex] + ); + } + } + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultExtensionsLib.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultExtensionsLib.sol new file mode 100644 index 00000000..b221f1f3 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultExtensionsLib.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IVault } from "../interfaces/IVault.sol"; +import { IVaultErrors } from "../interfaces/IVaultErrors.sol"; + +/** + * @notice Ensure functions in extension contracts can only be called through the main Vault. + * @dev The Vault is composed of three contracts, using the Proxy pattern from OpenZeppelin. `ensureVaultDelegateCall` + * can be called on the locally stored Vault address by modifiers in extension contracts to ensure that their functions + * can only be called through the main Vault. Because the storage *layout* is shared (through inheritance of + * `VaultStorage`), but each contract actually has its own storage, we need to make sure we are always calling in the + * main Vault context, to avoid referencing storage in the extension contracts. + */ +library VaultExtensionsLib { + function ensureVaultDelegateCall(IVault vault) internal view { + // If this is a delegate call from the Vault, the address of the contract should be the Vault's, + // not the extension. + if (address(this) != address(vault)) { + revert IVaultErrors.NotVaultDelegateCall(); + } + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultStateLib.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultStateLib.sol new file mode 100644 index 00000000..859811bb --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/lib/VaultStateLib.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { WordCodec } from "../interfaces/WordCodec.sol"; + +// @notice Custom type to store the Vault configuration. +type VaultStateBits is bytes32; + +/// @notice Helper functions for reading and writing the `VaultState` struct. +library VaultStateLib { + using WordCodec for bytes32; + + // Bit offsets for the Vault state flags. + uint256 public constant QUERY_DISABLED_OFFSET = 0; + uint256 public constant VAULT_PAUSED_OFFSET = QUERY_DISABLED_OFFSET + 1; + uint256 public constant BUFFER_PAUSED_OFFSET = VAULT_PAUSED_OFFSET + 1; + + function isQueryDisabled(VaultStateBits config) internal pure returns (bool) { + return VaultStateBits.unwrap(config).decodeBool(QUERY_DISABLED_OFFSET); + } + + function setQueryDisabled(VaultStateBits config, bool value) internal pure returns (VaultStateBits) { + return VaultStateBits.wrap(VaultStateBits.unwrap(config).insertBool(value, QUERY_DISABLED_OFFSET)); + } + + function isVaultPaused(VaultStateBits config) internal pure returns (bool) { + return VaultStateBits.unwrap(config).decodeBool(VAULT_PAUSED_OFFSET); + } + + function setVaultPaused(VaultStateBits config, bool value) internal pure returns (VaultStateBits) { + return VaultStateBits.wrap(VaultStateBits.unwrap(config).insertBool(value, VAULT_PAUSED_OFFSET)); + } + + function areBuffersPaused(VaultStateBits config) internal pure returns (bool) { + return VaultStateBits.unwrap(config).decodeBool(BUFFER_PAUSED_OFFSET); + } + + function setBuffersPaused(VaultStateBits config, bool value) internal pure returns (VaultStateBits) { + return VaultStateBits.wrap(VaultStateBits.unwrap(config).insertBool(value, BUFFER_PAUSED_OFFSET)); + } +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/token/ERC20MultiToken.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/token/ERC20MultiToken.sol new file mode 100644 index 00000000..4c029a08 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/token/ERC20MultiToken.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; + +import { IERC20MultiTokenErrors } from "../interfaces/IERC20MultiTokenErrors.sol"; + +import { EVMCallModeHelpers } from "../interfaces/EVMCallModeHelpers.sol"; + +import { BalancerPoolToken } from "../BalancerPoolToken.sol"; + +/** + * @notice Store Token data and handle accounting for pool tokens in the Vault. + * @dev The ERC20MultiToken is an ERC20-focused multi-token implementation that is fully compatible with the ERC20 API + * on the token side. It also allows for the minting and burning of tokens on the multi-token side. + */ +abstract contract ERC20MultiToken is IERC20Errors, IERC20MultiTokenErrors { + // Minimum total supply amount. + uint256 internal constant _POOL_MINIMUM_TOTAL_SUPPLY = 1e6; + + /** + * @notice Pool tokens are moved from one account (`from`) to another (`to`). Note that `value` may be zero. + * @param pool The pool token being transferred + * @param from The token source + * @param to The token destination + * @param value The number of tokens + */ + event Transfer(address indexed pool, address indexed from, address indexed to, uint256 value); + + /** + * @notice The allowance of a `spender` for an `owner` is set by a call to {approve}. `value` is the new allowance. + * @param pool The pool token receiving the allowance + * @param owner The token holder + * @param spender The account being authorized to spend a given amount of the token + * @param value The number of tokens spender is authorized to transfer from owner + */ + event Approval(address indexed pool, address indexed owner, address indexed spender, uint256 value); + + // Users' pool token (BPT) balances. + mapping(address token => mapping(address owner => uint256 balance)) private _balances; + + // Users' pool token (BPT) allowances. + mapping(address token => mapping(address owner => mapping(address spender => uint256 allowance))) + private _allowances; + + // Total supply of all pool tokens (BPT). These are tokens minted and burned by the Vault. + // The Vault balances of regular pool tokens are stored in `_reservesOf`. + mapping(address token => uint256 totalSupply) private _totalSupplyOf; + + function _totalSupply(address pool) internal view returns (uint256) { + return _totalSupplyOf[pool]; + } + + function _balanceOf(address pool, address account) internal view returns (uint256) { + return _balances[pool][account]; + } + + function _allowance(address pool, address owner, address spender) internal view returns (uint256) { + // Owner can spend anything without approval + if (owner == spender) { + return type(uint256).max; + } else { + return _allowances[pool][owner][spender]; + } + } + + /** + * @dev DO NOT CALL THIS METHOD! + * Only `removeLiquidity` in the Vault may call this - in a query context - to allow burning tokens the caller + * does not have. + */ + function _queryModeBalanceIncrease(address pool, address to, uint256 amount) internal { + // Enforce that this can only be called in a read-only, query context. + if (EVMCallModeHelpers.isStaticCall() == false) { + revert EVMCallModeHelpers.NotStaticCall(); + } + + // Increase `to` balance to ensure the burn function succeeds during query. + _balances[address(pool)][to] += amount; + } + + function _mint(address pool, address to, uint256 amount) internal { + if (to == address(0)) { + revert ERC20InvalidReceiver(to); + } + + uint256 newTotalSupply = _totalSupplyOf[pool] + amount; + unchecked { + // Overflow is not possible. balance + amount is at most totalSupply + amount, which is checked above. + _balances[pool][to] += amount; + } + + _ensurePoolMinimumTotalSupply(newTotalSupply); + + _totalSupplyOf[pool] = newTotalSupply; + + emit Transfer(pool, address(0), to, amount); + + // We also emit the "transfer" event on the pool token to ensure full compliance with the ERC20 standard. + BalancerPoolToken(pool).emitTransfer(address(0), to, amount); + } + + function _ensurePoolMinimumTotalSupply(uint256 newTotalSupply) internal pure { + if (newTotalSupply < _POOL_MINIMUM_TOTAL_SUPPLY) { + revert PoolTotalSupplyTooLow(newTotalSupply); + } + } + + function _mintMinimumSupplyReserve(address pool) internal { + _totalSupplyOf[pool] += _POOL_MINIMUM_TOTAL_SUPPLY; + unchecked { + // Overflow is not possible. balance + amount is at most totalSupply + amount, which is checked above. + _balances[pool][address(0)] += _POOL_MINIMUM_TOTAL_SUPPLY; + } + emit Transfer(pool, address(0), address(0), _POOL_MINIMUM_TOTAL_SUPPLY); + + // We also emit the "transfer" event on the pool token to ensure full compliance with the ERC20 standard. + BalancerPoolToken(pool).emitTransfer(address(0), address(0), _POOL_MINIMUM_TOTAL_SUPPLY); + } + + function _burn(address pool, address from, uint256 amount) internal { + if (from == address(0)) { + revert ERC20InvalidSender(from); + } + + uint256 accountBalance = _balances[pool][from]; + if (amount > accountBalance) { + revert ERC20InsufficientBalance(from, accountBalance, amount); + } + + unchecked { + _balances[pool][from] = accountBalance - amount; + } + uint256 newTotalSupply = _totalSupplyOf[pool] - amount; + + _ensurePoolMinimumTotalSupply(newTotalSupply); + + _totalSupplyOf[pool] = newTotalSupply; + + emit Transfer(pool, from, address(0), amount); + + // We also emit the "transfer" event on the pool token to ensure full compliance with the ERC20 standard. + BalancerPoolToken(pool).emitTransfer(from, address(0), amount); + } + + function _transfer(address pool, address from, address to, uint256 amount) internal { + if (from == address(0)) { + revert ERC20InvalidSender(from); + } + + if (to == address(0)) { + revert ERC20InvalidReceiver(to); + } + + uint256 fromBalance = _balances[pool][from]; + if (amount > fromBalance) { + revert ERC20InsufficientBalance(from, fromBalance, amount); + } + + unchecked { + _balances[pool][from] = fromBalance - amount; + // Overflow is not possible. The sum of all balances is capped by totalSupply, and that sum is preserved by + // decrementing then incrementing. + _balances[pool][to] += amount; + } + + emit Transfer(pool, from, to, amount); + + // We also emit the "transfer" event on the pool token to ensure full compliance with the ERC20 standard. + BalancerPoolToken(pool).emitTransfer(from, to, amount); + } + + function _approve(address pool, address owner, address spender, uint256 amount) internal { + if (owner == address(0)) { + revert ERC20InvalidApprover(owner); + } + + if (spender == address(0)) { + revert ERC20InvalidSpender(spender); + } + + _allowances[pool][owner][spender] = amount; + + emit Approval(pool, owner, spender, amount); + // We also emit the "approve" event on the pool token to ensure full compliance with the ERC20 standard. + BalancerPoolToken(pool).emitApproval(owner, spender, amount); + } + + function _spendAllowance(address pool, address owner, address spender, uint256 amount) internal { + uint256 currentAllowance = _allowance(pool, owner, spender); + if (currentAllowance != type(uint256).max) { + if (amount > currentAllowance) { + revert ERC20InsufficientAllowance(spender, currentAllowance, amount); + } + + unchecked { + _approve(pool, owner, spender, currentAllowance - amount); + } + } + } +} \ No newline at end of file From 172a8924b7a144049487b6b8faa0cfe846e651e5 Mon Sep 17 00:00:00 2001 From: swayam karle Date: Wed, 23 Oct 2024 19:31:06 +0530 Subject: [PATCH 5/7] Mocks Added --- .../contracts/Mocks/BasicAuthorizerMock.sol | 45 ++ .../contracts/Mocks/IVaultAdminMock.sol | 57 ++ .../Mocks/ProtocolFeeControllerMock.sol | 34 + .../contracts/Mocks/VaultAdminMock.sol | 132 +++ .../contracts/Mocks/VaultExtensionMock.sol | 64 ++ .../contracts/ProtocolFeeController.sol | 559 +++++++++++++ .../{secretSwap.sol => SecretSwapHook.sol} | 0 .../hooks/SecretSwap/contracts/VaultAdmin.sol | 760 ++++++++++++++++++ .../interfaces/IVaultExtensionMock.sol | 32 + .../contracts/interfaces/VaultExtension.sol | 6 +- .../contracts/hooks/SecretSwap/pnpm-lock.yaml | 20 - .../SecretSwapTests/SecretSwap.fixture.ts | 0 .../test/SecretSwapTests/SecretSwapTest.ts | 0 13 files changed, 1685 insertions(+), 24 deletions(-) create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/BasicAuthorizerMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/IVaultAdminMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/ProtocolFeeControllerMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultAdminMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultExtensionMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/ProtocolFeeController.sol rename packages/foundry/contracts/hooks/SecretSwap/contracts/{secretSwap.sol => SecretSwapHook.sol} (100%) create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/VaultAdmin.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultExtensionMock.sol create mode 100644 packages/foundry/contracts/hooks/SecretSwap/test/SecretSwapTests/SecretSwap.fixture.ts create mode 100644 packages/foundry/contracts/hooks/SecretSwap/test/SecretSwapTests/SecretSwapTest.ts diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/BasicAuthorizerMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/BasicAuthorizerMock.sol new file mode 100644 index 00000000..fa5ea15c --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/BasicAuthorizerMock.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IAuthorizer } from "../interfaces/IAuthorizer.sol"; + +contract BasicAuthorizerMock is IAuthorizer { + // Simple, to avoid bringing in EnumerableSet, etc. + mapping(bytes32 actionId => mapping(address account => bool hasRole)) private _roles; + + // Could generalize better, but wanted to make minimal changes. + mapping(bytes32 actionId => mapping(address account => mapping(address whereAddress => bool hasRole))) + private _specificRoles; + + /// @inheritdoc IAuthorizer + function canPerform(bytes32 role, address account, address where) external view returns (bool) { + return hasSpecificRole(role, account, where) || hasRole(role, account); + } + + function grantRole(bytes32 role, address account) external { + _roles[role][account] = true; + } + + function revokeRole(bytes32 role, address account) external { + _roles[role][account] = false; + } + + function hasRole(bytes32 role, address account) public view returns (bool) { + return _roles[role][account]; + } + + // Functions for targeted permissions + + function grantSpecificRole(bytes32 role, address account, address where) external { + _specificRoles[role][account][where] = true; + } + + function revokeSpecificRole(bytes32 role, address account, address where) external { + _specificRoles[role][account][where] = false; + } + + function hasSpecificRole(bytes32 role, address account, address where) public view returns (bool) { + return _specificRoles[role][account][where]; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/IVaultAdminMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/IVaultAdminMock.sol new file mode 100644 index 00000000..95c5c1dc --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/IVaultAdminMock.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +interface IVaultAdminMock { + function manualPauseVault() external; + + function manualUnpauseVault() external; + + function manualPausePool(address pool) external; + + function manualUnpausePool(address pool) external; + + function manualEnableRecoveryMode(address pool) external; + + function manualDisableRecoveryMode(address pool) external; + + function manualReentrancyInitializeBuffer( + IERC4626 wrappedToken, + uint256 amountUnderlying, + uint256 amountWrapped, + address sharesOwner + ) external; + + /// @dev Adds liquidity to buffer unbalanced, so it can unbalance the buffer. + function addLiquidityToBufferUnbalancedForTests( + IERC4626 wrappedToken, + uint256 underlyingAmount, + uint256 wrappedAmount + ) external; + + function manualReentrancyAddLiquidityToBuffer( + IERC4626 wrappedToken, + uint256 exactSharesToIssue, + address sharesOwner + ) external; + + function manualReentrancyRemoveLiquidityFromBufferHook( + IERC4626 wrappedToken, + uint256 sharesToRemove, + address sharesOwner + ) external; + + function manualReentrancyDisableRecoveryMode(address pool) external; + + function mockWithValidPercentage(uint256 percentage) external view; + + function mockEnsurePoolNotInRecoveryMode(address pool) external view; + + function manualMintBufferShares(IERC4626 wrappedToken, address to, uint256 amount) external; + + function manualBurnBufferShares(IERC4626 wrappedToken, address from, uint256 amount) external; + + function manualMintMinimumBufferSupplyReserve(IERC4626 wrappedToken) external; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/ProtocolFeeControllerMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/ProtocolFeeControllerMock.sol new file mode 100644 index 00000000..d51b90b2 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/ProtocolFeeControllerMock.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IVault } from "../interfaces/IVault.sol"; + +import { ProtocolFeeController } from "../ProtocolFeeController.sol"; + +contract ProtocolFeeControllerMock is ProtocolFeeController { + constructor(IVault vault_) ProtocolFeeController(vault_) { + // solhint-disable-previous-line no-empty-blocks + } + + function getPoolTokensAndCount(address pool) external view returns (IERC20[] memory tokens, uint256 numTokens) { + return _getPoolTokensAndCount(pool); + } + + function getPoolCreatorInfo( + address pool + ) external view returns (address poolCreator, uint256 creatorSwapFeePercentage, uint256 creatorYieldFeePercentage) { + return (_poolCreators[pool], _poolCreatorSwapFeePercentages[pool], _poolCreatorYieldFeePercentages[pool]); + } + + /** + * @notice Sets the pool creator address, allowing the address to change the pool creator fee percentage. + * @dev Standard Balancer Pools specifically disallow pool creators to be passed in through PoolRoleAccounts; + * otherwise, this wouldn't be necessary. + */ + function manualSetPoolCreator(address pool, address poolCreator) external { + _poolCreators[pool] = poolCreator; + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultAdminMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultAdminMock.sol new file mode 100644 index 00000000..56509cf7 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultAdminMock.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; + +import { IVaultAdminMock } from "./IVaultAdminMock.sol"; +import { IVault } from "../interfaces/IVault.sol"; + +import { PackedTokenBalance } from "../interfaces/PackedTokenBalance.sol"; + +import { VaultAdmin } from "../VaultAdmin.sol"; + +contract VaultAdminMock is IVaultAdminMock, VaultAdmin { + using PackedTokenBalance for bytes32; + + constructor( + IVault mainVault, + uint32 pauseWindowDuration, + uint32 bufferPeriodDuration, + uint256 minTradeAmount, + uint256 minWrapAmount + ) VaultAdmin(mainVault, pauseWindowDuration, bufferPeriodDuration, minTradeAmount, minWrapAmount) {} + + function manualPauseVault() external { + _setVaultPaused(true); + } + + function manualUnpauseVault() external { + _setVaultPaused(false); + } + + function manualPausePool(address pool) external { + _poolRoleAccounts[pool].pauseManager = msg.sender; + _setPoolPaused(pool, true); + } + + function manualUnpausePool(address pool) external { + _poolRoleAccounts[pool].pauseManager = msg.sender; + _setPoolPaused(pool, false); + } + + function manualEnableRecoveryMode(address pool) external { + _ensurePoolNotInRecoveryMode(pool); + _setPoolRecoveryMode(pool, true); + } + + function manualDisableRecoveryMode(address pool) external { + _ensurePoolInRecoveryMode(pool); + _setPoolRecoveryMode(pool, false); + } + + function manualReentrancyInitializeBuffer( + IERC4626 wrappedToken, + uint256 amountUnderlying, + uint256 amountWrapped, + address sharesOwner + ) external nonReentrant { + IVault(address(this)).initializeBuffer(wrappedToken, amountUnderlying, amountWrapped, sharesOwner); + } + + /// @dev Adds liquidity to buffer unbalanced, so it can unbalance the buffer. + function addLiquidityToBufferUnbalancedForTests( + IERC4626 wrappedToken, + uint256 underlyingAmount, + uint256 wrappedAmount + ) public { + bytes32 bufferBalances = _bufferTokenBalances[wrappedToken]; + + if (underlyingAmount > 0) { + IERC20(wrappedToken.asset()).transferFrom(msg.sender, address(this), underlyingAmount); + _reservesOf[IERC20(wrappedToken.asset())] += underlyingAmount; + // Issued shares amount = underlying amount. + _bufferTotalShares[wrappedToken] += underlyingAmount; + _bufferLpShares[wrappedToken][msg.sender] += underlyingAmount; + } + if (wrappedAmount > 0) { + IERC20(address(wrappedToken)).transferFrom(msg.sender, address(this), wrappedAmount); + _reservesOf[IERC20(address(wrappedToken))] += wrappedAmount; + uint256 issuedSharesAmount = wrappedToken.previewRedeem(wrappedAmount); + _bufferTotalShares[wrappedToken] += issuedSharesAmount; + _bufferLpShares[wrappedToken][msg.sender] += issuedSharesAmount; + } + + bufferBalances = PackedTokenBalance.toPackedBalance( + bufferBalances.getBalanceRaw() + underlyingAmount, + bufferBalances.getBalanceDerived() + wrappedAmount + ); + _bufferTokenBalances[wrappedToken] = bufferBalances; + } + + function manualReentrancyAddLiquidityToBuffer( + IERC4626 wrappedToken, + uint256 exactSharesToIssue, + address sharesOwner + ) external nonReentrant { + IVault(address(this)).addLiquidityToBuffer(wrappedToken, exactSharesToIssue, sharesOwner); + } + + function manualReentrancyRemoveLiquidityFromBufferHook( + IERC4626 wrappedToken, + uint256 sharesToRemove, + address sharesOwner + ) external nonReentrant { + this.removeLiquidityFromBufferHook(wrappedToken, sharesToRemove, sharesOwner); + } + + function manualReentrancyDisableRecoveryMode(address pool) external nonReentrant { + this.disableRecoveryMode(pool); + } + + function mockWithValidPercentage(uint256 percentage) external pure withValidPercentage(percentage) { + // solhint-disable-previous-line no-empty-blocks + } + + function mockEnsurePoolNotInRecoveryMode(address pool) external view { + _ensurePoolNotInRecoveryMode(pool); + } + + function manualMintBufferShares(IERC4626 wrappedToken, address to, uint256 amount) external { + _mintBufferShares(wrappedToken, to, amount); + } + + function manualBurnBufferShares(IERC4626 wrappedToken, address from, uint256 amount) external { + _burnBufferShares(wrappedToken, from, amount); + } + + function manualMintMinimumBufferSupplyReserve(IERC4626 wrappedToken) external { + _mintMinimumBufferSupplyReserve(wrappedToken); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultExtensionMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultExtensionMock.sol new file mode 100644 index 00000000..9503ec8c --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/Mocks/VaultExtensionMock.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { + TokenConfig, + PoolRoleAccounts, + LiquidityManagement +} from "../interfaces/VaultTypes.sol"; +import { IVaultExtensionMock } from "../interfaces/IVaultExtensionMock.sol"; +import { IVaultAdmin } from "../interfaces/IVaultAdmin.sol"; +import { IVault } from "../interfaces/IVault.sol"; + +import { PoolConfigLib, PoolConfigBits } from "../lib/PoolConfigLib.sol"; +import { VaultExtension } from "../interfaces/VaultExtension.sol"; + +contract VaultExtensionMock is IVaultExtensionMock, VaultExtension { + using PoolConfigLib for PoolConfigBits; + + constructor(IVault vault, IVaultAdmin vaultAdmin) VaultExtension(vault, vaultAdmin) {} + + function mockExtensionHash(bytes calldata input) external payable returns (bytes32) { + return keccak256(input); + } + + function manuallySetSwapFee(address pool, uint256 newSwapFee) external { + _poolConfigBits[pool] = _poolConfigBits[pool].setStaticSwapFeePercentage(newSwapFee); + } + + function manualRegisterPoolReentrancy( + address pool, + TokenConfig[] memory tokenConfig, + uint256 swapFeePercentage, + uint32 pauseWindowEndTime, + bool protocolFeeExempt, + PoolRoleAccounts calldata roleAccounts, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external nonReentrant { + IVault(address(this)).registerPool( + pool, + tokenConfig, + swapFeePercentage, + pauseWindowEndTime, + protocolFeeExempt, + roleAccounts, + poolHooksContract, + liquidityManagement + ); + } + + function manualInitializePoolReentrancy( + address pool, + address to, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bytes memory userData + ) external nonReentrant { + IVault(address(this)).initialize(pool, to, tokens, exactAmountsIn, minBptAmountOut, userData); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/ProtocolFeeController.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/ProtocolFeeController.sol new file mode 100644 index 00000000..7e2348cd --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/ProtocolFeeController.sol @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IProtocolFeeController } from "./interfaces/IProtocolFeeController.sol"; +import { FEE_SCALING_FACTOR } from "./interfaces/VaultTypes.sol"; +import { IVaultErrors } from "./interfaces/IVaultErrors.sol"; +import { IVault } from "./interfaces/IVault.sol"; + +import { + ReentrancyGuardTransient +} from "./interfaces/ReentrancyGuardTransient.sol"; +import { FixedPoint } from "./interfaces/FixedPoint.sol"; + +import { SingletonAuthentication } from "./SingletonAuthentication.sol"; +import { VaultGuard } from "./interfaces//VaultGuard.sol"; + +/** + * @notice Helper contract to manage protocol and creator fees outside the Vault. + * @dev This contract stores global default protocol swap and yield fees, and also tracks the values of those fees + * for each pool (the `PoolFeeConfig` described below). Protocol fees can always be overwritten by governance, but + * pool creator fees are controlled by the registered poolCreator (see `PoolRoleAccounts`). + * + * The Vault stores a single aggregate percentage for swap and yield fees; only this `ProtocolFeeController` knows + * the component fee percentages, and how to compute the aggregate from the components. This is done for performance + * reasons, to minimize gas on the critical path, as this way the Vault simply applies a single "cut", and stores the + * fee amounts separately from the pool balances. + * + * The pool creator fees are "net" protocol fees, meaning the protocol fee is taken first, and the pool creator fee + * percentage is applied to the remainder. Essentially, the protocol is paid first, then the remainder is divided + * between the pool creator and the LPs. + * + * There is a permissionless function (`collectAggregateFees`) that transfers these tokens from the Vault to this + * contract, and distributes them between the protocol and pool creator, after which they can be withdrawn at any + * time by governance and the pool creator, respectively. + * + * Protocol fees can be zero in some cases (e.g., the token is registered as exempt), and pool creator fees are zero + * if there is no creator role address defined. Protocol fees are capped at a maximum percentage (50%); pool creator + * fees are computed "net" protocol fees, so they can be any value from 0 to 100%. Any combination is possible. + * A protocol-fee-exempt pool with a 100% pool creator fee would send all fees to the creator. If there is no pool + * creator, a pool with a 50% protocol fee would divide the fees evenly between the protocol and LPs. + * + * This contract is deployed with the Vault, but can be changed by governance. + */ +contract ProtocolFeeController is + IProtocolFeeController, + SingletonAuthentication, + ReentrancyGuardTransient, + VaultGuard +{ + using FixedPoint for uint256; + using SafeERC20 for IERC20; + using SafeCast for *; + + enum ProtocolFeeType { + SWAP, + YIELD + } + + /** + * @notice Fee configuration stored in the swap and yield fee mappings. + * @dev Instead of storing only the fee in the mapping, also store a flag to indicate whether the fee has been + * set by governance through a permissioned call. (The fee is stored in 64-bits, so that the struct fits + * within a single slot.) + * + * We know the percentage is an 18-decimal FP value, which only takes 60 bits, so it's guaranteed to fit, + * and we can do simple casts to truncate the high bits without needed SafeCast. + * + * We want to enable permissionless updates for pools, so that it is less onerous to update potentially + * hundreds of pools if the global protocol fees change. However, we don't want to overwrite pools that + * have had their fee percentages manually set by the DAO (i.e., after off-chain negotiation and agreement). + * + * @param feePercentage The raw swap or yield fee percentage + * @param isOverride When set, this fee is controlled by governance, and cannot be changed permissionlessly + */ + struct PoolFeeConfig { + uint64 feePercentage; + bool isOverride; + } + + // Maximum protocol swap fee percentage. FixedPoint.ONE corresponds to a 100% fee. + uint256 internal constant _MAX_PROTOCOL_SWAP_FEE_PERCENTAGE = 50e16; // 50% + + // Maximum protocol yield fee percentage. + uint256 internal constant _MAX_PROTOCOL_YIELD_FEE_PERCENTAGE = 50e16; // 50% + + // Global protocol swap fee. + uint256 private _globalProtocolSwapFeePercentage; + + // Global protocol yield fee. + uint256 private _globalProtocolYieldFeePercentage; + + // Store the pool-specific swap fee percentages (the Vault's poolConfigBits stores the aggregate percentage). + mapping(address pool => PoolFeeConfig swapFeeConfig) internal _poolProtocolSwapFeePercentages; + + // Store the pool-specific yield fee percentages (the Vault's poolConfigBits stores the aggregate percentage). + mapping(address pool => PoolFeeConfig yieldFeeConfig) internal _poolProtocolYieldFeePercentages; + + // Pool creators for each pool (empowered to set pool creator fee percentages, and withdraw creator fees). + mapping(address pool => address poolCreator) internal _poolCreators; + + // Pool creator swap fee percentages for each pool. + mapping(address pool => uint256 poolCreatorSwapFee) internal _poolCreatorSwapFeePercentages; + + // Pool creator yield fee percentages for each pool. + mapping(address pool => uint256 poolCreatorYieldFee) internal _poolCreatorYieldFeePercentages; + + // Disaggregated protocol fees (from swap and yield), available for withdrawal by governance. + mapping(address pool => mapping(IERC20 poolToken => uint256 feeAmount)) internal _protocolFeeAmounts; + + // Disaggregated pool creator fees (from swap and yield), available for withdrawal by the pool creator. + mapping(address pool => mapping(IERC20 poolToken => uint256 feeAmount)) internal _poolCreatorFeeAmounts; + + // Ensure that the caller is the pool creator. + modifier onlyPoolCreator(address pool) { + _ensureCallerIsPoolCreator(pool); + _; + } + + // Validate the swap fee percentage against the maximum. + modifier withValidSwapFee(uint256 newSwapFeePercentage) { + if (newSwapFeePercentage > _MAX_PROTOCOL_SWAP_FEE_PERCENTAGE) { + revert ProtocolSwapFeePercentageTooHigh(); + } + _ensureValidPrecision(newSwapFeePercentage); + _; + } + + // Validate the yield fee percentage against the maximum. + modifier withValidYieldFee(uint256 newYieldFeePercentage) { + if (newYieldFeePercentage > _MAX_PROTOCOL_YIELD_FEE_PERCENTAGE) { + revert ProtocolYieldFeePercentageTooHigh(); + } + _ensureValidPrecision(newYieldFeePercentage); + _; + } + + modifier withValidPoolCreatorFee(uint256 newPoolCreatorFeePercentage) { + if (newPoolCreatorFeePercentage > FixedPoint.ONE) { + revert PoolCreatorFeePercentageTooHigh(); + } + _; + } + + // Force collection and disaggregation (e.g., before changing protocol fee percentages). + modifier withLatestFees(address pool) { + collectAggregateFees(pool); + _; + } + + constructor(IVault vault_) SingletonAuthentication(vault_) VaultGuard(vault_) { + // solhint-disable-previous-line no-empty-blocks + } + + /// @inheritdoc IProtocolFeeController + function vault() external view returns (IVault) { + return _vault; + } + + /// @inheritdoc IProtocolFeeController + function collectAggregateFees(address pool) public { + _vault.unlock(abi.encodeCall(ProtocolFeeController.collectAggregateFeesHook, pool)); + } + + /** + * @dev Copy and zero out the `aggregateFeeAmounts` collected in the Vault accounting, supplying credit + * for each token. Then have the Vault transfer tokens to this contract, debiting each token for the amount + * transferred so that the transaction settles when the hook returns. + */ + function collectAggregateFeesHook(address pool) external onlyVault { + (uint256[] memory totalSwapFees, uint256[] memory totalYieldFees) = _vault.collectAggregateFees(pool); + _receiveAggregateFees(pool, totalSwapFees, totalYieldFees); + } + + /** + * @notice Settle fee credits from the Vault. + * @dev This must be called after calling `collectAggregateFees` in the Vault. Note that since charging protocol + * fees (i.e., distributing tokens between pool and fee balances) occurs in the Vault, but fee collection + * happens in the ProtocolFeeController, the swap fees reported here may encompass multiple operations. + * + * @param pool The address of the pool on which the swap fees were charged + * @param swapFeeAmounts An array with the total swap fees collected, sorted in token registration order + * @param yieldFeeAmounts An array with the total yield fees collected, sorted in token registration order + */ + function _receiveAggregateFees( + address pool, + uint256[] memory swapFeeAmounts, + uint256[] memory yieldFeeAmounts + ) internal { + _receiveAggregateFees(pool, ProtocolFeeType.SWAP, swapFeeAmounts); + _receiveAggregateFees(pool, ProtocolFeeType.YIELD, yieldFeeAmounts); + } + + function _receiveAggregateFees(address pool, ProtocolFeeType feeType, uint256[] memory feeAmounts) private { + // There are two cases when we don't need to split fees (in which case we can save gas and avoid rounding + // errors by skipping calculations) if either the protocol or pool creator fee percentage is zero. + + uint256 protocolFeePercentage = feeType == ProtocolFeeType.SWAP + ? _poolProtocolSwapFeePercentages[pool].feePercentage + : _poolProtocolYieldFeePercentages[pool].feePercentage; + + uint256 poolCreatorFeePercentage = feeType == ProtocolFeeType.SWAP + ? _poolCreatorSwapFeePercentages[pool] + : _poolCreatorYieldFeePercentages[pool]; + + uint256 aggregateFeePercentage; + + bool needToSplitFees = poolCreatorFeePercentage > 0 && protocolFeePercentage > 0; + if (needToSplitFees) { + // Calculate once, outside the loop. + aggregateFeePercentage = _computeAggregateFeePercentage(protocolFeePercentage, poolCreatorFeePercentage); + } + + (IERC20[] memory poolTokens, uint256 numTokens) = _getPoolTokensAndCount(pool); + for (uint256 i = 0; i < numTokens; ++i) { + if (feeAmounts[i] > 0) { + IERC20 token = poolTokens[i]; + + _vault.sendTo(token, address(this), feeAmounts[i]); + + // It should be easier for off-chain processes to handle two events, rather than parsing the type + // out of a single event. + if (feeType == ProtocolFeeType.SWAP) { + emit ProtocolSwapFeeCollected(pool, token, feeAmounts[i]); + } else { + emit ProtocolYieldFeeCollected(pool, token, feeAmounts[i]); + } + + if (needToSplitFees) { + uint256 totalVolume = feeAmounts[i].divUp(aggregateFeePercentage); + uint256 protocolPortion = totalVolume.mulUp(protocolFeePercentage); + + _protocolFeeAmounts[pool][token] += protocolPortion; + _poolCreatorFeeAmounts[pool][token] += feeAmounts[i] - protocolPortion; + } else { + // If we don't need to split, one of them must be zero. + if (poolCreatorFeePercentage == 0) { + _protocolFeeAmounts[pool][token] += feeAmounts[i]; + } else { + _poolCreatorFeeAmounts[pool][token] += feeAmounts[i]; + } + } + } + } + } + + /// @inheritdoc IProtocolFeeController + function getGlobalProtocolSwapFeePercentage() external view returns (uint256) { + return _globalProtocolSwapFeePercentage; + } + + /// @inheritdoc IProtocolFeeController + function getGlobalProtocolYieldFeePercentage() external view returns (uint256) { + return _globalProtocolYieldFeePercentage; + } + + /// @inheritdoc IProtocolFeeController + function getPoolProtocolSwapFeeInfo(address pool) external view returns (uint256, bool) { + PoolFeeConfig memory config = _poolProtocolSwapFeePercentages[pool]; + + return (config.feePercentage, config.isOverride); + } + + /// @inheritdoc IProtocolFeeController + function getPoolProtocolYieldFeeInfo(address pool) external view returns (uint256, bool) { + PoolFeeConfig memory config = _poolProtocolYieldFeePercentages[pool]; + + return (config.feePercentage, config.isOverride); + } + + /// @inheritdoc IProtocolFeeController + function getProtocolFeeAmounts(address pool) external view returns (uint256[] memory feeAmounts) { + (IERC20[] memory poolTokens, uint256 numTokens) = _getPoolTokensAndCount(pool); + + feeAmounts = new uint256[](numTokens); + for (uint256 i = 0; i < numTokens; ++i) { + feeAmounts[i] = _protocolFeeAmounts[pool][poolTokens[i]]; + } + } + + /// @inheritdoc IProtocolFeeController + function getPoolCreatorFeeAmounts(address pool) external view returns (uint256[] memory feeAmounts) { + (IERC20[] memory poolTokens, uint256 numTokens) = _getPoolTokensAndCount(pool); + + feeAmounts = new uint256[](numTokens); + for (uint256 i = 0; i < numTokens; ++i) { + feeAmounts[i] = _poolCreatorFeeAmounts[pool][poolTokens[i]]; + } + } + + /// @inheritdoc IProtocolFeeController + function computeAggregateFeePercentage( + uint256 protocolFeePercentage, + uint256 poolCreatorFeePercentage + ) external pure returns (uint256) { + return _computeAggregateFeePercentage(protocolFeePercentage, poolCreatorFeePercentage); + } + + /// @inheritdoc IProtocolFeeController + function updateProtocolSwapFeePercentage(address pool) external withLatestFees(pool) { + PoolFeeConfig memory feeConfig = _poolProtocolSwapFeePercentages[pool]; + uint256 globalProtocolSwapFee = _globalProtocolSwapFeePercentage; + + if (feeConfig.isOverride == false && globalProtocolSwapFee != feeConfig.feePercentage) { + _updatePoolSwapFeePercentage(pool, globalProtocolSwapFee, false); + } + } + + /// @inheritdoc IProtocolFeeController + function updateProtocolYieldFeePercentage(address pool) external withLatestFees(pool) { + PoolFeeConfig memory feeConfig = _poolProtocolYieldFeePercentages[pool]; + uint256 globalProtocolYieldFee = _globalProtocolYieldFeePercentage; + + if (feeConfig.isOverride == false && globalProtocolYieldFee != feeConfig.feePercentage) { + _updatePoolYieldFeePercentage(pool, globalProtocolYieldFee, false); + } + } + + function _getAggregateFeePercentage(address pool, ProtocolFeeType feeType) internal view returns (uint256) { + uint256 protocolFeePercentage; + uint256 poolCreatorFeePercentage; + + if (feeType == ProtocolFeeType.SWAP) { + protocolFeePercentage = _poolProtocolSwapFeePercentages[pool].feePercentage; + poolCreatorFeePercentage = _poolCreatorSwapFeePercentages[pool]; + } else { + protocolFeePercentage = _poolProtocolYieldFeePercentages[pool].feePercentage; + poolCreatorFeePercentage = _poolCreatorYieldFeePercentages[pool]; + } + + return _computeAggregateFeePercentage(protocolFeePercentage, poolCreatorFeePercentage); + } + + function _computeAggregateFeePercentage( + uint256 protocolFeePercentage, + uint256 poolCreatorFeePercentage + ) internal pure returns (uint256 aggregateFeePercentage) { + aggregateFeePercentage = + protocolFeePercentage + + protocolFeePercentage.complement().mulDown(poolCreatorFeePercentage); + + _ensureValidPrecision(aggregateFeePercentage); + } + + function _ensureCallerIsPoolCreator(address pool) internal view { + address poolCreator = _poolCreators[pool]; + + if (poolCreator == address(0)) { + revert PoolCreatorNotRegistered(pool); + } + + if (poolCreator != msg.sender) { + revert CallerIsNotPoolCreator(msg.sender, pool); + } + } + + function _getPoolTokensAndCount(address pool) internal view returns (IERC20[] memory tokens, uint256 numTokens) { + tokens = _vault.getPoolTokens(pool); + numTokens = tokens.length; + } + + /*************************************************************************** + Permissioned Functions + ***************************************************************************/ + + /// @inheritdoc IProtocolFeeController + function registerPool( + address pool, + address poolCreator, + bool protocolFeeExempt + ) external onlyVault returns (uint256 aggregateSwapFeePercentage, uint256 aggregateYieldFeePercentage) { + _poolCreators[pool] = poolCreator; + + // Set local storage of the actual percentages for the pool (default to global). + aggregateSwapFeePercentage = protocolFeeExempt ? 0 : _globalProtocolSwapFeePercentage; + aggregateYieldFeePercentage = protocolFeeExempt ? 0 : _globalProtocolYieldFeePercentage; + + // `isOverride` is true if the pool is protocol fee exempt; otherwise, default to false. + // If exempt, this pool cannot be updated to the current global percentage permissionlessly. + // The percentages are 18 decimal floating point numbers, bound between 0 and the max fee (<= FixedPoint.ONE). + // Since this fits in 64 bits, the SafeCast shouldn't be necessary, and is done out of an abundance of caution. + _poolProtocolSwapFeePercentages[pool] = PoolFeeConfig({ + feePercentage: aggregateSwapFeePercentage.toUint64(), + isOverride: protocolFeeExempt + }); + _poolProtocolYieldFeePercentages[pool] = PoolFeeConfig({ + feePercentage: aggregateYieldFeePercentage.toUint64(), + isOverride: protocolFeeExempt + }); + } + + /// @inheritdoc IProtocolFeeController + function setGlobalProtocolSwapFeePercentage( + uint256 newProtocolSwapFeePercentage + ) external withValidSwapFee(newProtocolSwapFeePercentage) authenticate { + _globalProtocolSwapFeePercentage = newProtocolSwapFeePercentage; + + emit GlobalProtocolSwapFeePercentageChanged(newProtocolSwapFeePercentage); + } + + /// @inheritdoc IProtocolFeeController + function setGlobalProtocolYieldFeePercentage( + uint256 newProtocolYieldFeePercentage + ) external withValidYieldFee(newProtocolYieldFeePercentage) authenticate { + _globalProtocolYieldFeePercentage = newProtocolYieldFeePercentage; + + emit GlobalProtocolYieldFeePercentageChanged(newProtocolYieldFeePercentage); + } + + /// @inheritdoc IProtocolFeeController + function setProtocolSwapFeePercentage( + address pool, + uint256 newProtocolSwapFeePercentage + ) external authenticate withValidSwapFee(newProtocolSwapFeePercentage) withLatestFees(pool) { + _updatePoolSwapFeePercentage(pool, newProtocolSwapFeePercentage, true); + } + + /// @inheritdoc IProtocolFeeController + function setProtocolYieldFeePercentage( + address pool, + uint256 newProtocolYieldFeePercentage + ) external authenticate withValidYieldFee(newProtocolYieldFeePercentage) withLatestFees(pool) { + _updatePoolYieldFeePercentage(pool, newProtocolYieldFeePercentage, true); + } + + /// @inheritdoc IProtocolFeeController + function setPoolCreatorSwapFeePercentage( + address pool, + uint256 poolCreatorSwapFeePercentage + ) external onlyPoolCreator(pool) withValidPoolCreatorFee(poolCreatorSwapFeePercentage) withLatestFees(pool) { + _setPoolCreatorFeePercentage(pool, poolCreatorSwapFeePercentage, ProtocolFeeType.SWAP); + } + + /// @inheritdoc IProtocolFeeController + function setPoolCreatorYieldFeePercentage( + address pool, + uint256 poolCreatorYieldFeePercentage + ) external onlyPoolCreator(pool) withValidPoolCreatorFee(poolCreatorYieldFeePercentage) withLatestFees(pool) { + _setPoolCreatorFeePercentage(pool, poolCreatorYieldFeePercentage, ProtocolFeeType.YIELD); + } + + function _setPoolCreatorFeePercentage( + address pool, + uint256 poolCreatorFeePercentage, + ProtocolFeeType feeType + ) internal { + // Need to set locally, and update the aggregate percentage in the Vault. + if (feeType == ProtocolFeeType.SWAP) { + _poolCreatorSwapFeePercentages[pool] = poolCreatorFeePercentage; + + // The Vault will also emit an `AggregateSwapFeePercentageChanged` event. + _vault.updateAggregateSwapFeePercentage(pool, _getAggregateFeePercentage(pool, ProtocolFeeType.SWAP)); + + emit PoolCreatorSwapFeePercentageChanged(pool, poolCreatorFeePercentage); + } else { + _poolCreatorYieldFeePercentages[pool] = poolCreatorFeePercentage; + + // The Vault will also emit an `AggregateYieldFeePercentageChanged` event. + _vault.updateAggregateYieldFeePercentage(pool, _getAggregateFeePercentage(pool, ProtocolFeeType.YIELD)); + + emit PoolCreatorYieldFeePercentageChanged(pool, poolCreatorFeePercentage); + } + } + + /// @inheritdoc IProtocolFeeController + function withdrawProtocolFees(address pool, address recipient) external authenticate { + (IERC20[] memory poolTokens, uint256 numTokens) = _getPoolTokensAndCount(pool); + + for (uint256 i = 0; i < numTokens; ++i) { + IERC20 token = poolTokens[i]; + + uint256 amountToWithdraw = _protocolFeeAmounts[pool][token]; + if (amountToWithdraw > 0) { + _protocolFeeAmounts[pool][token] = 0; + token.safeTransfer(recipient, amountToWithdraw); + + emit ProtocolFeesWithdrawn(pool, token, recipient, amountToWithdraw); + } + } + } + + /// @inheritdoc IProtocolFeeController + function withdrawPoolCreatorFees(address pool, address recipient) external onlyPoolCreator(pool) { + _withdrawPoolCreatorFees(pool, recipient); + } + + /// @inheritdoc IProtocolFeeController + function withdrawPoolCreatorFees(address pool) external { + _withdrawPoolCreatorFees(pool, _poolCreators[pool]); + } + + function _withdrawPoolCreatorFees(address pool, address recipient) private { + (IERC20[] memory poolTokens, uint256 numTokens) = _getPoolTokensAndCount(pool); + + for (uint256 i = 0; i < numTokens; ++i) { + IERC20 token = poolTokens[i]; + + uint256 amountToWithdraw = _poolCreatorFeeAmounts[pool][token]; + if (amountToWithdraw > 0) { + _poolCreatorFeeAmounts[pool][token] = 0; + token.safeTransfer(recipient, amountToWithdraw); + + emit PoolCreatorFeesWithdrawn(pool, token, recipient, amountToWithdraw); + } + } + } + + /// @dev Common code shared between set/update. `isOverride` will be true if governance is setting the percentage. + function _updatePoolSwapFeePercentage(address pool, uint256 newProtocolSwapFeePercentage, bool isOverride) private { + // Update local storage of the raw percentage. + // + // The percentages are 18 decimal floating point numbers, bound between 0 and the max fee (<= FixedPoint.ONE). + // Since this fits in 64 bits, the SafeCast shouldn't be necessary, and is done out of an abundance of caution. + _poolProtocolSwapFeePercentages[pool] = PoolFeeConfig({ + feePercentage: newProtocolSwapFeePercentage.toUint64(), + isOverride: isOverride + }); + + // Update the resulting aggregate swap fee value in the Vault (PoolConfig). + _vault.updateAggregateSwapFeePercentage(pool, _getAggregateFeePercentage(pool, ProtocolFeeType.SWAP)); + + emit ProtocolSwapFeePercentageChanged(pool, newProtocolSwapFeePercentage); + } + + /// @dev Common code shared between set/update. `isOverride` will be true if governance is setting the percentage. + function _updatePoolYieldFeePercentage( + address pool, + uint256 newProtocolYieldFeePercentage, + bool isOverride + ) private { + // Update local storage of the raw percentage. + // The percentages are 18 decimal floating point numbers, bound between 0 and the max fee (<= FixedPoint.ONE). + // Since this fits in 64 bits, the SafeCast shouldn't be necessary, and is done out of an abundance of caution. + _poolProtocolYieldFeePercentages[pool] = PoolFeeConfig({ + feePercentage: newProtocolYieldFeePercentage.toUint64(), + isOverride: isOverride + }); + + // Update the resulting aggregate yield fee value in the Vault (PoolConfig). + _vault.updateAggregateYieldFeePercentage(pool, _getAggregateFeePercentage(pool, ProtocolFeeType.YIELD)); + + emit ProtocolYieldFeePercentageChanged(pool, newProtocolYieldFeePercentage); + } + + function _ensureValidPrecision(uint256 feePercentage) private pure { + // Primary fee percentages are 18-decimal values, stored here in 64 bits, and calculated with full 256-bit + // precision. However, the resulting aggregate fees are stored in the Vault with 24-bit precision, which + // corresponds to 0.00001% resolution (i.e., a fee can be 1%, 1.00001%, 1.00002%, but not 1.000005%). + // Ensure there will be no precision loss in the Vault - which would lead to a discrepancy between the + // aggregate fee calculated here and that stored in the Vault. + if ((feePercentage / FEE_SCALING_FACTOR) * FEE_SCALING_FACTOR != feePercentage) { + revert IVaultErrors.FeePrecisionTooHigh(); + } + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/secretSwap.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/SecretSwapHook.sol similarity index 100% rename from packages/foundry/contracts/hooks/SecretSwap/contracts/secretSwap.sol rename to packages/foundry/contracts/hooks/SecretSwap/contracts/SecretSwapHook.sol diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultAdmin.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultAdmin.sol new file mode 100644 index 00000000..68cb0160 --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/VaultAdmin.sol @@ -0,0 +1,760 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { IProtocolFeeController } from "./interfaces/IProtocolFeeController.sol"; +import { IAuthorizer } from "./interfaces/IAuthorizer.sol"; +import { IVaultAdmin } from "./interfaces/IVaultAdmin.sol"; +import { Rounding } from "./interfaces/VaultTypes.sol"; +import { IVault } from "./interfaces/IVault.sol"; + +import { PackedTokenBalance } from "./interfaces/PackedTokenBalance.sol"; +import { Authentication } from "./interfaces/Authentication.sol"; +import { FixedPoint } from "./interfaces/FixedPoint.sol"; + +import { VaultStateBits, VaultStateLib } from "./lib/VaultStateLib.sol"; +import { PoolConfigLib, PoolConfigBits } from "./lib/PoolConfigLib.sol"; +import { VaultExtensionsLib } from "./lib/VaultExtensionsLib.sol"; +import { VaultCommon } from "./VaultCommon.sol"; +import { VaultGuard } from "./interfaces/VaultGuard.sol"; + +/** + * @dev Bytecode extension for the Vault containing permissioned functions. Complementary to `VaultExtension`, + * it has access to the same storage layout as the main vault. + * + * The functions in this contract are not meant to be called directly. They must only be called by the Vault + * via delegate calls, so that any state modifications produced by this contract's code will actually target + * the main Vault's state. + * + * The storage of this contract is in practice unused. + */ +contract VaultAdmin is IVaultAdmin, VaultCommon, Authentication, VaultGuard { + using PackedTokenBalance for bytes32; + using PoolConfigLib for PoolConfigBits; + using VaultStateLib for VaultStateBits; + using VaultExtensionsLib for IVault; + using FixedPoint for uint256; + using SafeERC20 for IERC20; + using SafeCast for *; + + // Minimum BPT amount minted upon initialization. + uint256 internal constant _BUFFER_MINIMUM_TOTAL_SUPPLY = 1e4; + + /// @dev Functions with this modifier can only be delegate-called by the Vault. + modifier onlyVaultDelegateCall() { + _vault.ensureVaultDelegateCall(); + _; + } + + /// @dev Functions with this modifier can only be called by the pool creator. + modifier onlyProtocolFeeController() { + if (msg.sender != address(_protocolFeeController)) { + revert SenderNotAllowed(); + } + _; + } + + /// @dev Validate aggregate percentage values. + modifier withValidPercentage(uint256 aggregatePercentage) { + if (aggregatePercentage > FixedPoint.ONE) { + revert ProtocolFeesExceedTotalCollected(); + } + _; + } + + constructor( + IVault mainVault, + uint32 pauseWindowDuration, + uint32 bufferPeriodDuration, + uint256 minTradeAmount, + uint256 minWrapAmount + ) Authentication(bytes32(uint256(uint160(address(mainVault))))) VaultGuard(mainVault) { + if (pauseWindowDuration > _MAX_PAUSE_WINDOW_DURATION) { + revert VaultPauseWindowDurationTooLarge(); + } + if (bufferPeriodDuration > _MAX_BUFFER_PERIOD_DURATION) { + revert PauseBufferPeriodDurationTooLarge(); + } + + // solhint-disable-next-line not-rely-on-time + uint32 pauseWindowEndTime = (block.timestamp + pauseWindowDuration).toUint32(); + + _vaultPauseWindowEndTime = pauseWindowEndTime; + _vaultBufferPeriodDuration = bufferPeriodDuration; + _vaultBufferPeriodEndTime = pauseWindowEndTime + bufferPeriodDuration; + + _MINIMUM_TRADE_AMOUNT = minTradeAmount; + _MINIMUM_WRAP_AMOUNT = minWrapAmount; + } + + /******************************************************************************* + Constants and immutables + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function vault() external view returns (IVault) { + return _vault; + } + + /// @inheritdoc IVaultAdmin + function getPauseWindowEndTime() external view returns (uint32) { + return _vaultPauseWindowEndTime; + } + + /// @inheritdoc IVaultAdmin + function getBufferPeriodDuration() external view returns (uint32) { + return _vaultBufferPeriodDuration; + } + + /// @inheritdoc IVaultAdmin + function getBufferPeriodEndTime() external view returns (uint32) { + return _vaultBufferPeriodEndTime; + } + + /// @inheritdoc IVaultAdmin + function getMinimumPoolTokens() external pure returns (uint256) { + return _MIN_TOKENS; + } + + /// @inheritdoc IVaultAdmin + function getMaximumPoolTokens() external pure returns (uint256) { + return _MAX_TOKENS; + } + + /// @inheritdoc IVaultAdmin + function getPoolMinimumTotalSupply() external pure returns (uint256) { + return _POOL_MINIMUM_TOTAL_SUPPLY; + } + + /// @inheritdoc IVaultAdmin + function getBufferMinimumTotalSupply() external pure returns (uint256) { + return _BUFFER_MINIMUM_TOTAL_SUPPLY; + } + + /// @inheritdoc IVaultAdmin + function getMinimumTradeAmount() external view returns (uint256) { + return _MINIMUM_TRADE_AMOUNT; + } + + /// @inheritdoc IVaultAdmin + function getMinimumWrapAmount() external view returns (uint256) { + return _MINIMUM_WRAP_AMOUNT; + } + + /******************************************************************************* + Vault Pausing + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function isVaultPaused() external view onlyVaultDelegateCall returns (bool) { + return _isVaultPaused(); + } + + /// @inheritdoc IVaultAdmin + function getVaultPausedState() external view onlyVaultDelegateCall returns (bool, uint32, uint32) { + return (_isVaultPaused(), _vaultPauseWindowEndTime, _vaultBufferPeriodEndTime); + } + + /// @inheritdoc IVaultAdmin + function pauseVault() external onlyVaultDelegateCall authenticate { + _setVaultPaused(true); + } + + /// @inheritdoc IVaultAdmin + function unpauseVault() external onlyVaultDelegateCall authenticate { + _setVaultPaused(false); + } + + /** + * @dev The contract can only be paused until the end of the Pause Window, and + * unpaused until the end of the Buffer Period. + */ + function _setVaultPaused(bool pausing) internal { + if (_isVaultPaused()) { + if (pausing) { + // Already paused, and we're trying to pause it again. + revert VaultPaused(); + } + + // The Vault can always be unpaused while it's paused. + // When the buffer period expires, `_isVaultPaused` will return false, so we would be in the outside + // else clause, where trying to unpause will revert unconditionally. + } else { + if (pausing) { + // Not already paused; we can pause within the window. + // solhint-disable-next-line not-rely-on-time + if (block.timestamp >= _vaultPauseWindowEndTime) { + revert VaultPauseWindowExpired(); + } + } else { + // Not paused, and we're trying to unpause it. + revert VaultNotPaused(); + } + } + + VaultStateBits vaultState = _vaultStateBits; + vaultState = vaultState.setVaultPaused(pausing); + _vaultStateBits = vaultState; + + emit VaultPausedStateChanged(pausing); + } + + /******************************************************************************* + Pool Pausing + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function pausePool(address pool) external onlyVaultDelegateCall withRegisteredPool(pool) { + _setPoolPaused(pool, true); + } + + /// @inheritdoc IVaultAdmin + function unpausePool(address pool) external onlyVaultDelegateCall withRegisteredPool(pool) { + _setPoolPaused(pool, false); + } + + function _setPoolPaused(address pool, bool pausing) internal { + _ensureAuthenticatedByRole(pool, _poolRoleAccounts[pool].pauseManager); + + PoolConfigBits config = _poolConfigBits[pool]; + + if (_isPoolPaused(pool)) { + if (pausing) { + // Already paused, and we're trying to pause it again. + revert PoolPaused(pool); + } + + // The pool can always be unpaused while it's paused. + // When the buffer period expires, `_isPoolPaused` will return false, so we would be in the outside + // else clause, where trying to unpause will revert unconditionally. + } else { + if (pausing) { + // Not already paused; we can pause within the window. + // solhint-disable-next-line not-rely-on-time + if (block.timestamp >= config.getPauseWindowEndTime()) { + revert PoolPauseWindowExpired(pool); + } + } else { + // Not paused, and we're trying to unpause it. + revert PoolNotPaused(pool); + } + } + + // Update poolConfigBits. + _poolConfigBits[pool] = config.setPoolPaused(pausing); + + emit PoolPausedStateChanged(pool, pausing); + } + + /******************************************************************************* + Fees + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function setStaticSwapFeePercentage( + address pool, + uint256 swapFeePercentage + ) external onlyVaultDelegateCall withRegisteredPool(pool) { + _ensureAuthenticatedByExclusiveRole(pool, _poolRoleAccounts[pool].swapFeeManager); + _ensureUnpaused(pool); + + _setStaticSwapFeePercentage(pool, swapFeePercentage); + } + + /// @inheritdoc IVaultAdmin + function collectAggregateFees( + address pool + ) + public + onlyVaultDelegateCall + onlyWhenUnlocked + onlyProtocolFeeController + withRegisteredPool(pool) + returns (uint256[] memory totalSwapFees, uint256[] memory totalYieldFees) + { + IERC20[] memory poolTokens = _vault.getPoolTokens(pool); + uint256 numTokens = poolTokens.length; + + totalSwapFees = new uint256[](numTokens); + totalYieldFees = new uint256[](numTokens); + + for (uint256 i = 0; i < poolTokens.length; ++i) { + IERC20 token = poolTokens[i]; + + (totalSwapFees[i], totalYieldFees[i]) = _aggregateFeeAmounts[pool][token].fromPackedBalance(); + + if (totalSwapFees[i] > 0 || totalYieldFees[i] > 0) { + // Supply credit for the total amount of fees. + _aggregateFeeAmounts[pool][token] = 0; + _supplyCredit(token, totalSwapFees[i] + totalYieldFees[i]); + } + } + } + + /// @inheritdoc IVaultAdmin + function updateAggregateSwapFeePercentage( + address pool, + uint256 newAggregateSwapFeePercentage + ) + external + onlyVaultDelegateCall + withRegisteredPool(pool) + withValidPercentage(newAggregateSwapFeePercentage) + onlyProtocolFeeController + { + _poolConfigBits[pool] = _poolConfigBits[pool].setAggregateSwapFeePercentage(newAggregateSwapFeePercentage); + + emit AggregateSwapFeePercentageChanged(pool, newAggregateSwapFeePercentage); + } + + /// @inheritdoc IVaultAdmin + function updateAggregateYieldFeePercentage( + address pool, + uint256 newAggregateYieldFeePercentage + ) + external + onlyVaultDelegateCall + withRegisteredPool(pool) + withValidPercentage(newAggregateYieldFeePercentage) + onlyProtocolFeeController + { + _poolConfigBits[pool] = _poolConfigBits[pool].setAggregateYieldFeePercentage(newAggregateYieldFeePercentage); + + emit AggregateYieldFeePercentageChanged(pool, newAggregateYieldFeePercentage); + } + + /// @inheritdoc IVaultAdmin + function setProtocolFeeController( + IProtocolFeeController newProtocolFeeController + ) external onlyVaultDelegateCall authenticate nonReentrant { + _protocolFeeController = newProtocolFeeController; + + emit ProtocolFeeControllerChanged(newProtocolFeeController); + } + + /******************************************************************************* + Recovery Mode + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function enableRecoveryMode(address pool) external onlyVaultDelegateCall withRegisteredPool(pool) { + _ensurePoolNotInRecoveryMode(pool); + + // If the Vault or pool is pausable (and currently paused), this call is permissionless. + if (_isPoolPaused(pool) == false && _isVaultPaused() == false) { + // If not permissionless, authenticate with governance. + _authenticateCaller(); + } + + _setPoolRecoveryMode(pool, true); + } + + /// @inheritdoc IVaultAdmin + function disableRecoveryMode(address pool) external onlyVaultDelegateCall withRegisteredPool(pool) authenticate { + _ensurePoolInRecoveryMode(pool); + _setPoolRecoveryMode(pool, false); + } + + /** + * @dev Reverts if the pool is in recovery mode. + * @param pool The pool + */ + function _ensurePoolNotInRecoveryMode(address pool) internal view { + if (_isPoolInRecoveryMode(pool)) { + revert PoolInRecoveryMode(pool); + } + } + + /** + * @dev Change the recovery mode state of a pool, and emit an event. Assumes any validation (e.g., whether + * the proposed state change is consistent) has already been done. + * + * @param pool The pool + * @param recoveryMode The desired recovery mode state + */ + function _setPoolRecoveryMode(address pool, bool recoveryMode) internal { + if (recoveryMode == false) { + _syncPoolBalancesAfterRecoveryMode(pool); + } + + // Update poolConfigBits. `_writePoolBalancesToStorage` updates *only* balances, not yield fees, which are + // forfeited during Recovery Mode. To prevent yield fees from being charged, `_loadPoolData` must be called + // while still in Recovery Mode, so updating the Recovery Mode bit must be done last, after the accounting. + _poolConfigBits[pool] = _poolConfigBits[pool].setPoolInRecoveryMode(recoveryMode); + + emit PoolRecoveryModeStateChanged(pool, recoveryMode); + } + + /** + * @dev Raw and live balances will diverge as tokens are withdrawn during Recovery Mode. Live balances cannot + * be updated in Recovery Mode, as this would require making external calls to update rates, which could fail. + * When Recovery Mode is disabled, re-sync the balances. + */ + function _syncPoolBalancesAfterRecoveryMode(address pool) private nonReentrant { + _writePoolBalancesToStorage(pool, _loadPoolData(pool, Rounding.ROUND_DOWN)); + } + + /******************************************************************************* + Queries + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function disableQuery() external onlyVaultDelegateCall authenticate { + VaultStateBits vaultState = _vaultStateBits; + vaultState = vaultState.setQueryDisabled(true); + _vaultStateBits = vaultState; + + emit VaultQueriesDisabled(); + } + + /******************************************************************************* + ERC4626 Buffers + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function areBuffersPaused() external view onlyVaultDelegateCall returns (bool) { + return _vaultStateBits.areBuffersPaused(); + } + + /// @inheritdoc IVaultAdmin + function pauseVaultBuffers() external onlyVaultDelegateCall authenticate { + _setVaultBufferPauseState(true); + } + + /// @inheritdoc IVaultAdmin + function unpauseVaultBuffers() external onlyVaultDelegateCall authenticate { + _setVaultBufferPauseState(false); + } + + function _setVaultBufferPauseState(bool paused) private { + VaultStateBits vaultState = _vaultStateBits; + vaultState = vaultState.setBuffersPaused(paused); + _vaultStateBits = vaultState; + + emit VaultBuffersPausedStateChanged(paused); + } + + /// @inheritdoc IVaultAdmin + function initializeBuffer( + IERC4626 wrappedToken, + uint256 amountUnderlyingRaw, + uint256 amountWrappedRaw, + address sharesOwner + ) + public + onlyVaultDelegateCall + onlyWhenUnlocked + whenVaultBuffersAreNotPaused + nonReentrant + returns (uint256 issuedShares) + { + if (_bufferAssets[wrappedToken] != address(0)) { + revert BufferAlreadyInitialized(wrappedToken); + } + + address underlyingToken = wrappedToken.asset(); + + if (underlyingToken == address(0)) { + // Should never happen, but a malicious wrapper could return the zero address and cause the buffer + // initialization code to run more than once. + revert InvalidUnderlyingToken(wrappedToken); + } + + // Register asset of wrapper, so it cannot change. + _bufferAssets[wrappedToken] = underlyingToken; + + // Take debt for initialization assets. + _takeDebt(IERC20(underlyingToken), amountUnderlyingRaw); + _takeDebt(wrappedToken, amountWrappedRaw); + + // Update buffer balances. + _bufferTokenBalances[wrappedToken] = PackedTokenBalance.toPackedBalance(amountUnderlyingRaw, amountWrappedRaw); + + // At initialization, the initial "BPT rate" is 1, so the `issuedShares` is simply the sum of the initial + // buffer token balances, converted to underlying. We use `previewRedeem` to convert wrapped to underlying, + // since `redeem` is an EXACT_IN operation that rounds down the result. + issuedShares = wrappedToken.previewRedeem(amountWrappedRaw) + amountUnderlyingRaw; + _ensureBufferMinimumTotalSupply(issuedShares); + + // Divide `issuedShares` between the zero address, which receives the minimum supply, and the account + // depositing the tokens to initialize the buffer, which receives the balance. + issuedShares -= _BUFFER_MINIMUM_TOTAL_SUPPLY; + + _mintMinimumBufferSupplyReserve(wrappedToken); + _mintBufferShares(wrappedToken, sharesOwner, issuedShares); + + emit LiquidityAddedToBuffer(wrappedToken, amountUnderlyingRaw, amountWrappedRaw); + } + + /// @inheritdoc IVaultAdmin + function addLiquidityToBuffer( + IERC4626 wrappedToken, + uint256 exactSharesToIssue, + address sharesOwner + ) + public + onlyVaultDelegateCall + onlyWhenUnlocked + whenVaultBuffersAreNotPaused + withInitializedBuffer(wrappedToken) + nonReentrant + returns (uint256 amountUnderlyingRaw, uint256 amountWrappedRaw) + { + // Check wrapped token asset correctness. + address underlyingToken = wrappedToken.asset(); + _ensureCorrectBufferAsset(wrappedToken, underlyingToken); + + bytes32 bufferBalances = _bufferTokenBalances[wrappedToken]; + + // To proportionally add liquidity to buffer, we need to calculate the buffer invariant ratio. It's calculated + // as the amount of buffer shares the sender wants to issue (which in practice is the value that the sender + // will add to the buffer, expressed in underlying token amounts), divided by the total shares of + // the buffer. + // Multiply the current buffer balance by the invariant ratio to calculate the amount of underlying and wrapped + // tokens to add, keeping the proportion of the buffer. + uint256 totalShares = _bufferTotalShares[wrappedToken]; + amountUnderlyingRaw = bufferBalances.getBalanceRaw().mulDivUp(exactSharesToIssue, totalShares); + amountWrappedRaw = bufferBalances.getBalanceDerived().mulDivUp(exactSharesToIssue, totalShares); + + // Take debt for assets going into the buffer (wrapped and underlying). + _takeDebt(IERC20(underlyingToken), amountUnderlyingRaw); + _takeDebt(wrappedToken, amountWrappedRaw); + + // Add the amountsIn to the current buffer balances. + bufferBalances = PackedTokenBalance.toPackedBalance( + bufferBalances.getBalanceRaw() + amountUnderlyingRaw, + bufferBalances.getBalanceDerived() + amountWrappedRaw + ); + _bufferTokenBalances[wrappedToken] = bufferBalances; + + // Mint new shares to the owner. + _mintBufferShares(wrappedToken, sharesOwner, exactSharesToIssue); + + emit LiquidityAddedToBuffer(wrappedToken, amountUnderlyingRaw, amountWrappedRaw); + } + + function _mintMinimumBufferSupplyReserve(IERC4626 wrappedToken) internal { + _bufferTotalShares[wrappedToken] = _BUFFER_MINIMUM_TOTAL_SUPPLY; + _bufferLpShares[wrappedToken][address(0)] = _BUFFER_MINIMUM_TOTAL_SUPPLY; + + emit BufferSharesMinted(wrappedToken, address(0), _BUFFER_MINIMUM_TOTAL_SUPPLY); + } + + function _mintBufferShares(IERC4626 wrappedToken, address to, uint256 amount) internal { + if (to == address(0)) { + revert BufferSharesInvalidReceiver(); + } + + uint256 newTotalSupply = _bufferTotalShares[wrappedToken] + amount; + + // This is called on buffer initialization - after the minimum reserve amount has been minted - and during + // subsequent adds, when we're increasing it, so we do not really need to check it against the minimum. + // We do it anyway out of an abundance of caution, and to preserve symmetry with `_burnBufferShares`. + _ensureBufferMinimumTotalSupply(newTotalSupply); + + _bufferTotalShares[wrappedToken] = newTotalSupply; + _bufferLpShares[wrappedToken][to] += amount; + + emit BufferSharesMinted(wrappedToken, to, amount); + } + + /// @inheritdoc IVaultAdmin + function removeLiquidityFromBuffer( + IERC4626 wrappedToken, + uint256 sharesToRemove + ) external onlyVaultDelegateCall returns (uint256 removedUnderlyingBalanceRaw, uint256 removedWrappedBalanceRaw) { + return + abi.decode( + _vault.unlock( + abi.encodeCall(VaultAdmin.removeLiquidityFromBufferHook, (wrappedToken, sharesToRemove, msg.sender)) + ), + (uint256, uint256) + ); + } + + /** + * @dev Internal hook for `removeLiquidityFromBuffer`. Can only be called by the Vault itself via + * `removeLiquidityFromBuffer`, which correctly forwards the real sender as the `sharesOwner`. + * This function must be reentrant because it calls the nonReentrant function `sendTo`. However, + * since `sendTo` is the only function that makes external calls, `removeLiquidityFromBufferHook` + * cannot reenter the Vault. + * + * @param wrappedToken Address of the wrapped token that implements IERC4626 + * @param sharesToRemove Amount of shares to remove from the buffer. Cannot be greater than sharesOwner's + * total shares + * @param sharesOwner Owner of the shares (`msg.sender` for `removeLiquidityFromBuffer` entrypoint) + * @return removedUnderlyingBalanceRaw Amount of underlying tokens returned to the user + * @return removedWrappedBalanceRaw Amount of wrapped tokens returned to the user + */ + function removeLiquidityFromBufferHook( + IERC4626 wrappedToken, + uint256 sharesToRemove, + address sharesOwner + ) + external + onlyVaultDelegateCall + onlyVault + onlyWhenUnlocked + withInitializedBuffer(wrappedToken) + returns (uint256 removedUnderlyingBalanceRaw, uint256 removedWrappedBalanceRaw) + { + if (sharesToRemove > _bufferLpShares[wrappedToken][sharesOwner]) { + revert NotEnoughBufferShares(); + } + + bytes32 bufferBalances = _bufferTokenBalances[wrappedToken]; + uint256 totalShares = _bufferTotalShares[wrappedToken]; + + removedUnderlyingBalanceRaw = (bufferBalances.getBalanceRaw() * sharesToRemove) / totalShares; + removedWrappedBalanceRaw = (bufferBalances.getBalanceDerived() * sharesToRemove) / totalShares; + + // We get the underlying token stored internally as opposed to calling `asset()` in the wrapped token. + // This is to avoid any kind of unnecessary external call; the underlying token is set during initialization + // and can't change afterwards, so it is already validated at this point. There is no way to add liquidity + // with an asset that differs from the one set during initialization. + IERC20 underlyingToken = IERC20(_bufferAssets[wrappedToken]); + _supplyCredit(underlyingToken, removedUnderlyingBalanceRaw); + _supplyCredit(wrappedToken, removedWrappedBalanceRaw); + + bufferBalances = PackedTokenBalance.toPackedBalance( + bufferBalances.getBalanceRaw() - removedUnderlyingBalanceRaw, + bufferBalances.getBalanceDerived() - removedWrappedBalanceRaw + ); + + _bufferTokenBalances[wrappedToken] = bufferBalances; + + // Ensures we cannot drop the supply below the minimum. + _burnBufferShares(wrappedToken, sharesOwner, sharesToRemove); + + // This triggers an external call to itself; the Vault is acting as a Router in this case. + // `sendTo` makes external calls (`transfer`) but is non-reentrant. + if (removedUnderlyingBalanceRaw > 0) { + _vault.sendTo(underlyingToken, sharesOwner, removedUnderlyingBalanceRaw); + } + if (removedWrappedBalanceRaw > 0) { + _vault.sendTo(wrappedToken, sharesOwner, removedWrappedBalanceRaw); + } + + emit LiquidityRemovedFromBuffer(wrappedToken, removedUnderlyingBalanceRaw, removedWrappedBalanceRaw); + } + + function _burnBufferShares(IERC4626 wrappedToken, address from, uint256 amount) internal { + if (from == address(0)) { + revert BufferSharesInvalidOwner(); + } + + uint256 newTotalSupply = _bufferTotalShares[wrappedToken] - amount; + + // Ensure that the buffer can never be drained below the minimum total supply. + _ensureBufferMinimumTotalSupply(newTotalSupply); + + _bufferTotalShares[wrappedToken] = newTotalSupply; + _bufferLpShares[wrappedToken][from] -= amount; + + emit BufferSharesBurned(wrappedToken, from, amount); + } + + /// @inheritdoc IVaultAdmin + function getBufferAsset( + IERC4626 wrappedToken + ) external view onlyVaultDelegateCall returns (address underlyingToken) { + return _bufferAssets[wrappedToken]; + } + + /// @inheritdoc IVaultAdmin + function getBufferOwnerShares( + IERC4626 token, + address user + ) external view onlyVaultDelegateCall returns (uint256 shares) { + return _bufferLpShares[token][user]; + } + + /// @inheritdoc IVaultAdmin + function getBufferTotalShares(IERC4626 token) external view onlyVaultDelegateCall returns (uint256 shares) { + return _bufferTotalShares[token]; + } + + /// @inheritdoc IVaultAdmin + function getBufferBalance(IERC4626 token) external view onlyVaultDelegateCall returns (uint256, uint256) { + // The first balance is underlying, and the last is wrapped balance. + return (_bufferTokenBalances[token].getBalanceRaw(), _bufferTokenBalances[token].getBalanceDerived()); + } + + function _ensureBufferMinimumTotalSupply(uint256 newTotalSupply) private pure { + if (newTotalSupply < _BUFFER_MINIMUM_TOTAL_SUPPLY) { + revert BufferTotalSupplyTooLow(newTotalSupply); + } + } + + /******************************************************************************* + Authentication + *******************************************************************************/ + + /// @inheritdoc IVaultAdmin + function setAuthorizer(IAuthorizer newAuthorizer) external onlyVaultDelegateCall authenticate { + _authorizer = newAuthorizer; + + emit AuthorizerChanged(newAuthorizer); + } + + /// @dev Authenticate by role; otherwise fall through and check governance. + function _ensureAuthenticatedByRole(address pool, address roleAddress) private view { + if (msg.sender == roleAddress) { + return; + } + + _ensureAuthenticated(pool); + } + + /// @dev Authenticate exclusively by role; caller must match the `roleAddress`, if assigned. + function _ensureAuthenticatedByExclusiveRole(address pool, address roleAddress) private view { + if (roleAddress == address(0)) { + // Defer to governance if no role assigned. + _ensureAuthenticated(pool); + } else if (msg.sender != roleAddress) { + revert SenderNotAllowed(); + } + } + + /// @dev Delegate authentication to governance. + function _ensureAuthenticated(address pool) private view { + bytes32 actionId = getActionId(msg.sig); + + if (_canPerform(actionId, msg.sender, pool) == false) { + revert SenderNotAllowed(); + } + } + + /// @dev Access control is delegated to the Authorizer. + function _canPerform(bytes32 actionId, address user) internal view override returns (bool) { + return _authorizer.canPerform(actionId, user, address(this)); + } + + /// @dev Access control is delegated to the Authorizer. `where` refers to the target contract. + function _canPerform(bytes32 actionId, address user, address where) internal view returns (bool) { + return _authorizer.canPerform(actionId, user, where); + } + + /******************************************************************************* + Default handlers + *******************************************************************************/ + + receive() external payable { + revert CannotReceiveEth(); + } + + // solhint-disable no-complex-fallback + + fallback() external payable { + if (msg.value > 0) { + revert CannotReceiveEth(); + } + + revert("Not implemented"); + } +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultExtensionMock.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultExtensionMock.sol new file mode 100644 index 00000000..f4cfa81a --- /dev/null +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/IVaultExtensionMock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma solidity ^0.8.24; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { TokenConfig, PoolRoleAccounts, LiquidityManagement } from "./VaultTypes.sol"; + +interface IVaultExtensionMock { + // Used in tests to circumvent minimum swap fees. + function manuallySetSwapFee(address pool, uint256 swapFeePercentage) external; + + function manualRegisterPoolReentrancy( + address pool, + TokenConfig[] memory tokenConfig, + uint256 swapFeePercentage, + uint32 pauseWindowEndTime, + bool protocolFeeExempt, + PoolRoleAccounts calldata roleAccounts, + address poolHooksContract, + LiquidityManagement calldata liquidityManagement + ) external; + + function manualInitializePoolReentrancy( + address pool, + address to, + IERC20[] memory tokens, + uint256[] memory exactAmountsIn, + uint256 minBptAmountOut, + bytes memory userData + ) external; +} \ No newline at end of file diff --git a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol index 05f66bd9..d9a141d1 100644 --- a/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol +++ b/packages/foundry/contracts/hooks/SecretSwap/contracts/interfaces/VaultExtension.sol @@ -22,9 +22,7 @@ import { InputHelpers } from "./InputHelpers.sol"; import { RevertCodec } from "./RevertCodec.sol"; import { ScalingHelpers } from "./ScalingHelpers.sol"; import { EVMCallModeHelpers } from "./EVMCallModeHelpers.sol"; -import { - TransientStorageHelpers -} from "./TransientStorageHelpers.sol"; +import { TransientStorageHelpers } from "./TransientStorageHelpers.sol"; import { StorageSlotExtension } from "./StorageSlotExtension.sol"; import { FixedPoint } from "./FixedPoint.sol"; import { PackedTokenBalance } from "./PackedTokenBalance.sol"; @@ -910,4 +908,4 @@ contract VaultExtension is IVaultExtension, VaultCommon, Proxy { function _implementation() internal view override returns (address) { return address(_vaultAdmin); } -} \ No newline at end of file +} diff --git a/packages/foundry/contracts/hooks/SecretSwap/pnpm-lock.yaml b/packages/foundry/contracts/hooks/SecretSwap/pnpm-lock.yaml index 81af5e87..36e765b8 100644 --- a/packages/foundry/contracts/hooks/SecretSwap/pnpm-lock.yaml +++ b/packages/foundry/contracts/hooks/SecretSwap/pnpm-lock.yaml @@ -7,10 +7,6 @@ settings: importers: .: - dependencies: - '@uniswap/permit2-sdk': - specifier: ^1.3.0 - version: 1.3.0 devDependencies: '@eslint/js': specifier: ^9.9.0 @@ -784,9 +780,6 @@ packages: resolution: {integrity: sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@uniswap/permit2-sdk@1.3.0': - resolution: {integrity: sha512-LstYQWP47dwpQrgqBJ+ysFstne9LgI5FGiKHc2ewjj91MTY8Mq1reocu6U/VDncdR5ef30TUOcZ7gPExRY8r6Q==} - abbrev@1.0.9: resolution: {integrity: sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==} @@ -2575,9 +2568,6 @@ packages: resolution: {integrity: sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==} engines: {node: '>=6.0.0'} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -3757,14 +3747,6 @@ snapshots: '@typescript-eslint/types': 8.0.1 eslint-visitor-keys: 3.4.3 - '@uniswap/permit2-sdk@1.3.0': - dependencies: - ethers: 5.7.2 - tiny-invariant: 1.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - abbrev@1.0.9: {} acorn-jsx@5.3.2(acorn@8.12.1): @@ -5790,8 +5772,6 @@ snapshots: promise: 8.3.0 qs: 6.13.0 - tiny-invariant@1.3.3: {} - tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 diff --git a/packages/foundry/contracts/hooks/SecretSwap/test/SecretSwapTests/SecretSwap.fixture.ts b/packages/foundry/contracts/hooks/SecretSwap/test/SecretSwapTests/SecretSwap.fixture.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/foundry/contracts/hooks/SecretSwap/test/SecretSwapTests/SecretSwapTest.ts b/packages/foundry/contracts/hooks/SecretSwap/test/SecretSwapTests/SecretSwapTest.ts new file mode 100644 index 00000000..e69de29b From 6a6db9d95305f0697e2f828e61e7da9cdb0c3609 Mon Sep 17 00:00:00 2001 From: swayam karle Date: Wed, 23 Oct 2024 20:12:12 +0530 Subject: [PATCH 6/7] Final Readme and Demo --- packages/foundry/contracts/hooks/SecretSwap/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/foundry/contracts/hooks/SecretSwap/README.md b/packages/foundry/contracts/hooks/SecretSwap/README.md index f6e0fadc..faaf3b17 100644 --- a/packages/foundry/contracts/hooks/SecretSwap/README.md +++ b/packages/foundry/contracts/hooks/SecretSwap/README.md @@ -5,12 +5,20 @@ SecretSwap allows users to perform confidential token swaps using Balancer V3 ho - **Withdraw tokens** (operation `2`) - **Perform a standard swap** without any operation specified. +### Secret Swap Demo +https://www.loom.com/share/f5449f193f3441999f08af62047f4fa2?sid=8b221c9b-51a3-484f-aed1-bee617843ed3 + +### How FHE co-processor works +https://github.com/zama-ai/fhevm-L1-demo/blob/main/EthCC24-tkms.pdf + + ### How It Works 1. **Deposit:** - When a user performs a deposit (operation `1`), their tokens are securely transferred to the contract, and they receive **encrypted credits** representing their balance. - The contract uses FHE to keep the user's balance confidential, and credits are stored securely. + 2. **Withdraw:** - When a user withdraws (operation `2`), their **encrypted credits** are decrypted using Zama's Co-Processor Model, and the corresponding token amounts are transferred back to the user. - Users can withdraw their full balance in both `tokenA` and `tokenB` based on their current credit value. From 911d65157f81323bf7e99b022406c7b60d7122cf Mon Sep 17 00:00:00 2001 From: Swayam <99035115+DevSwayam@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:23:21 +0530 Subject: [PATCH 7/7] Update README.md --- .../contracts/hooks/SecretSwap/README.md | 125 ++++++++++++++++-- 1 file changed, 116 insertions(+), 9 deletions(-) diff --git a/packages/foundry/contracts/hooks/SecretSwap/README.md b/packages/foundry/contracts/hooks/SecretSwap/README.md index faaf3b17..70fdeae7 100644 --- a/packages/foundry/contracts/hooks/SecretSwap/README.md +++ b/packages/foundry/contracts/hooks/SecretSwap/README.md @@ -17,13 +17,18 @@ https://github.com/zama-ai/fhevm-L1-demo/blob/main/EthCC24-tkms.pdf 1. **Deposit:** - When a user performs a deposit (operation `1`), their tokens are securely transferred to the contract, and they receive **encrypted credits** representing their balance. - The contract uses FHE to keep the user's balance confidential, and credits are stored securely. - - -2. **Withdraw:** + Excalidraw + +2. **Encrypted Transfers:** + - User can transfer his credits in confidential way (hidden amount to different users) + Excalidraw + +3. **Withdraw:** - When a user withdraws (operation `2`), their **encrypted credits** are decrypted using Zama's Co-Processor Model, and the corresponding token amounts are transferred back to the user. - Users can withdraw their full balance in both `tokenA` and `tokenB` based on their current credit value. - -3. **Standard Swap:** + Cursor_and_Excalidraw + +4. **Standard Swap:** - If no operation is specified (or an invalid operation is provided), the tokens proceed with a standard Balancer V3 swap. ### Key Features @@ -32,11 +37,113 @@ https://github.com/zama-ai/fhevm-L1-demo/blob/main/EthCC24-tkms.pdf - **Encrypted Credits:** Token balances are encrypted and managed using FHE, enabling secure, private transactions. - **Decryption via Callback:** Upon withdrawal, encrypted credit balances are decrypted using the FHE gateway, and tokens are securely transferred to the user. +### Overview of FHE Code in the Contract + +This contract implements a confidential token swapping system using Fully Homomorphic Encryption (FHE) provided by the TFHE library. Users can deposit tokens and receive encrypted credits, transfer these encrypted credits between users, and withdraw tokens by decrypting their credits. Below is a breakdown of the three key operations: deposit, transfer, and withdraw. + +### How Fully Homomorphic Encryption (FHE) is Used + +**TFHE (Transciphering Fully Homomorphic Encryption)** allows computations to be done on encrypted data without decrypting it. In this contract, user balances are stored and manipulated in an encrypted form using `euint64` (encrypted 64-bit unsigned integers). This ensures that the sensitive balance information remains confidential throughout deposits, transfers, and withdrawals. + +The contract leverages TFHE to: +1. **Encrypt** balances and perform operations such as addition and subtraction while the data is encrypted. +2. **Decrypt** balances only when a withdrawal request is made, ensuring privacy throughout the token lifecycle. + +--- + ### Operations -- **Operation 1 (Deposit):** Deposit tokens into the SecretSwap contract. Users receive encrypted credits. -- **Operation 2 (Withdraw):** Withdraw tokens based on the encrypted credits. -- **No Operation:** Perform a normal swap using Balancer V3 without any encrypted actions. +#### 1. **Deposit Operation** + +**What it does:** +When a user deposits tokens into the contract, an equivalent amount of encrypted credits is assigned to their account. This allows the user to keep their token holdings confidential. + +**Code Snippet:** +```solidity +function _depositToken(address router, IERC20 token, uint256 amount) private returns (uint256) { + address user = IRouterCommon(router).getSender(); + if (amount > 0) { + _vault.sendTo(token, address(this), amount); + userAddressToCreditValue[user][token] = TFHE.add( + userAddressToCreditValue[user][token], + TFHE.asEuint64(amount / DIVISION_FACTOR) + ); + emit TokenDeposited(address(this), token, amount); + } + return amount; +} +``` + +**Explanation:** +- The user deposits tokens. +- The tokens are transferred to the vault. +- The corresponding encrypted credits (`euint64`) are added to the user’s encrypted balance. + +--- + +#### 2. **Encrypted Credits Transfer Operation** + +**What it does:** +Users can transfer their encrypted credits to another user without revealing the actual amount. This allows private transfers of balances. + +**Code Snippet:** +```solidity +function transferCredits(address to, IERC20 token, einput encryptedAmount, bytes calldata inputProof) public { + euint64 amount = TFHE.asEuint64(encryptedAmount, inputProof); + ebool canTransfer = TFHE.le(amount, userAddressToCreditValue[msg.sender][token]); + _transfer(msg.sender, to, amount, canTransfer, token); +} +``` + +**Explanation:** +- The sender transfers encrypted credits to another user. +- The transfer is validated to ensure the sender has enough credits. +- The encrypted amount is transferred between users without revealing the actual value. + +--- + +#### 3. **Withdraw Operation** + +**What it does:** +When a user wants to withdraw tokens, the contract decrypts the encrypted credits and transfers the corresponding tokens back to the user. + +**Code Snippet:** +```solidity +function _withdrawToken(address router, IERC20 token1, IERC20 token2) private returns (uint256) { + address user = IRouterCommon(router).getSender(); + euint64 token1Amount = TFHE.isInitialized(userAddressToCreditValue[user][token1]) + ? userAddressToCreditValue[user][token1] + : TFHE.asEuint64(0); + + euint64 token2Amount = TFHE.isInitialized(userAddressToCreditValue[user][token2]) + ? userAddressToCreditValue[user][token2] + : TFHE.asEuint64(0); + + // Request decryption and transfer tokens + uint256 requestId = Gateway.requestDecryption( + cts, + this.callBackResolver.selector, + 0, + block.timestamp + 100, + false + ); + + requestIdToCallBackStruct[requestId] = CallBackStruct(user, token1, token2); + return 0; +} +``` + +**Explanation:** +- The user's encrypted balance is decrypted through a callback. +- After decryption, the corresponding tokens are transferred back to the user. + +--- + +### Summary of the Operations + +- **Deposit:** Users deposit tokens and receive encrypted credits in return, ensuring their balances remain confidential. +- **Transfer:** Users can transfer encrypted credits to others, allowing private balance transfers without revealing the token amount. +- **Withdraw:** Users can withdraw tokens by decrypting their encrypted credits, and the corresponding token value is transferred back. --- ### Setup and Run Instructions @@ -126,4 +233,4 @@ This project is licensed under the MIT License. --- -This README provides a clear explanation of how SecretSwap works, instructions for setup, and testing guidance for running the contracts on the `fhevm` using Balancer V3 hooks. \ No newline at end of file +This README provides a clear explanation of how SecretSwap works, instructions for setup, and testing guidance for running the contracts on the `fhevm` using Balancer V3 hooks.