From d7c2184ccff63496df300a57b4033d441da36d28 Mon Sep 17 00:00:00 2001 From: johba Date: Sat, 4 Oct 2025 15:17:09 +0200 Subject: [PATCH] Add Solidity linting with solhint, Foundry formatter, and pre-commit hooks (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes ### Configuration - Added .solhint.json with recommended rules + custom config - 160 char line length (warn) - Double quotes enforcement (error) - Explicit visibility required (error) - Console statements allowed (scripts/tests need them) - Gas optimization warnings enabled - Ignores test/helpers/, lib/, out/, cache/, broadcast/ - Added foundry.toml [fmt] section - 160 char line length - 4-space tabs - Double quotes - Thousands separators for numbers - Sort imports enabled - Added .lintstagedrc.json for pre-commit auto-fix - Runs solhint --fix on .sol files - Runs forge fmt on .sol files - Added husky pre-commit hook via lint-staged ### NPM Scripts - lint:sol - run solhint - lint:sol:fix - auto-fix solhint issues - format:sol - format with forge fmt - format:sol:check - check formatting - lint / lint:fix - combined commands ### Code Changes - Added explicit visibility modifiers (internal) to constants in scripts and tests - Fixed quote style in DeployLocal.sol - All Solidity files formatted with forge fmt ## Verification - ✅ forge fmt --check passes - ✅ No solhint errors (warnings only) - ✅ forge build succeeds - ✅ forge test passes (107/107) resolves #44 Co-authored-by: johba Reviewed-on: https://codeberg.org/johba/harb/pulls/51 --- .husky/pre-commit | 4 + onchain/.husky/pre-commit | 6 + onchain/.lintstagedrc.json | 6 + onchain/.solhint.json | 20 + onchain/.solhintignore | 5 + onchain/foundry.toml | 10 + onchain/package-lock.json | 1760 +++++++++++++++++ onchain/package.json | 14 + onchain/script/DeployBase.sol | 30 +- onchain/script/DeployBaseMainnet.sol | 4 +- onchain/script/DeployBaseSepolia.sol | 4 +- onchain/script/DeployLocal.sol | 40 +- onchain/script/DeployScript2.sol | 25 +- onchain/src/Kraiken.sol | 8 +- onchain/src/LiquidityManager.sol | 89 +- onchain/src/Optimizer.sol | 67 +- onchain/src/Stake.sol | 79 +- onchain/src/VWAPTracker.sol | 10 +- onchain/src/abstracts/PriceOracle.sol | 16 +- .../src/abstracts/ThreePositionStrategy.sol | 104 +- onchain/src/helpers/UniswapHelpers.sol | 2 +- onchain/src/libraries/UniswapMath.sol | 6 +- onchain/test/Kraiken.t.sol | 30 +- onchain/test/LiquidityManager.t.sol | 184 +- onchain/test/Optimizer.t.sol | 82 +- onchain/test/ReplayProfitableScenario.t.sol | 156 +- onchain/test/Stake.t.sol | 32 +- onchain/test/VWAPTracker.t.sol | 123 +- onchain/test/abstracts/PriceOracle.t.sol | 183 +- .../abstracts/ThreePositionStrategy.t.sol | 308 ++- .../test/helpers/LiquidityBoundaryHelper.sol | 84 +- onchain/test/helpers/TestBase.sol | 170 +- onchain/test/helpers/UniswapTestBase.sol | 48 +- onchain/test/libraries/UniswapMath.t.sol | 126 +- onchain/test/mocks/BearMarketOptimizer.sol | 24 +- onchain/test/mocks/BullMarketOptimizer.sol | 22 +- onchain/test/mocks/ExtremeOptimizer.sol | 55 +- onchain/test/mocks/MaliciousOptimizer.sol | 52 +- onchain/test/mocks/MockKraiken.sol | 6 +- onchain/test/mocks/MockOptimizer.sol | 24 +- onchain/test/mocks/MockStake.sol | 10 +- onchain/test/mocks/MockVWAPTracker.sol | 2 +- onchain/test/mocks/NeutralMarketOptimizer.sol | 24 +- .../test/mocks/RandomScenarioOptimizer.sol | 12 +- onchain/test/mocks/WhaleOptimizer.sol | 12 +- 45 files changed, 2853 insertions(+), 1225 deletions(-) create mode 100644 onchain/.husky/pre-commit create mode 100644 onchain/.lintstagedrc.json create mode 100644 onchain/.solhint.json create mode 100644 onchain/.solhintignore diff --git a/.husky/pre-commit b/.husky/pre-commit index 1a5ff85..7cfe91c 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,10 @@ #!/usr/bin/env sh set -e +if [ -d "onchain" ]; then + (cd onchain && npx lint-staged) +fi + if [ -d "kraiken-lib" ]; then (cd kraiken-lib && npx lint-staged) fi diff --git a/onchain/.husky/pre-commit b/onchain/.husky/pre-commit new file mode 100644 index 0000000..b5df69d --- /dev/null +++ b/onchain/.husky/pre-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e + +cd "$(dirname -- "$0")/.." + +npx lint-staged diff --git a/onchain/.lintstagedrc.json b/onchain/.lintstagedrc.json new file mode 100644 index 0000000..4422941 --- /dev/null +++ b/onchain/.lintstagedrc.json @@ -0,0 +1,6 @@ +{ + "**/*.sol": [ + "solhint --fix", + "forge fmt" + ] +} diff --git a/onchain/.solhint.json b/onchain/.solhint.json new file mode 100644 index 0000000..00913b6 --- /dev/null +++ b/onchain/.solhint.json @@ -0,0 +1,20 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error", "^0.8.0"], + "func-visibility": ["error", {"ignoreConstructors": true}], + "state-visibility": "error", + "max-line-length": ["warn", 160], + "quotes": ["error", "double"], + "reason-string": ["warn", {"maxLength": 64}], + "no-empty-blocks": "warn", + "no-unused-vars": "warn", + "no-console": "off", + "code-complexity": "off", + "function-max-lines": "off", + "gas-custom-errors": "warn", + "gas-calldata-parameters": "warn", + "not-rely-on-time": "off", + "avoid-low-level-calls": "off" + } +} diff --git a/onchain/.solhintignore b/onchain/.solhintignore new file mode 100644 index 0000000..a5cc2c3 --- /dev/null +++ b/onchain/.solhintignore @@ -0,0 +1,5 @@ +test/helpers/ +lib/ +out/ +cache/ +broadcast/ diff --git a/onchain/foundry.toml b/onchain/foundry.toml index 6875de1..0b6aff8 100644 --- a/onchain/foundry.toml +++ b/onchain/foundry.toml @@ -12,3 +12,13 @@ optimizer_runs = 200 [rpc_endpoints] goerli = "${GOERLI_RPC_URL}" # Remappings in remappings.txt + +[fmt] +line_length = 160 +tab_width = 4 +bracket_spacing = true +int_types = "long" +multiline_func_header = "all" +quote_style = "double" +number_underscore = "thousands" +sort_imports = true diff --git a/onchain/package-lock.json b/onchain/package-lock.json index 1325913..c627c73 100644 --- a/onchain/package-lock.json +++ b/onchain/package-lock.json @@ -6,6 +6,46 @@ "": { "dependencies": { "@uniswap/universal-router": "^2.0.0" + }, + "devDependencies": { + "husky": "^9.1.7", + "lint-staged": "^16.2.3", + "solhint": "^6.0.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" } }, "node_modules/@openzeppelin/contracts": { @@ -14,6 +54,84 @@ "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==", "license": "MIT" }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@solidity-parser/parser": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.2.tgz", + "integrity": "sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, "node_modules/@uniswap/universal-router": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@uniswap/universal-router/-/universal-router-2.0.0.tgz", @@ -45,6 +163,1648 @@ "engines": { "node": ">=10" } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antlr4": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.2.tgz", + "integrity": "sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/ast-parents": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz", + "integrity": "sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-ajv-errors": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-2.0.2.tgz", + "integrity": "sha512-1cLrJXEq46n0hjV8dDYwg9LKYjDb3KbeW7nZTv4kvfoDD9c2DXHIE31nxM+Y/cIfXMggLUfmxbm6h/JoM/yotA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@humanwhocodes/momoa": "^2.0.4", + "chalk": "^4.1.2", + "jsonpointer": "^5.0.1", + "leven": "^3.1.0 < 4" + }, + "engines": { + "node": ">= 18.20.6" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", + "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.3.tgz", + "integrity": "sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.1", + "listr2": "^9.0.4", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.3", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", + "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nano-spawn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz", + "integrity": "sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "dev": true, + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/solhint": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-6.0.1.tgz", + "integrity": "sha512-Lew5nhmkXqHPybzBzkMzvvWkpOJSSLTkfTZwRriWvfR2naS4YW2PsjVGaoX9tZFmHh7SuS+e2GEGo5FPYYmJ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@solidity-parser/parser": "^0.20.2", + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "antlr4": "^4.13.1-patch-1", + "ast-parents": "^0.0.1", + "better-ajv-errors": "^2.0.2", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "latest-version": "^7.0.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "semver": "^7.5.2", + "table": "^6.8.1", + "text-table": "^0.2.0" + }, + "bin": { + "solhint": "solhint.js" + }, + "optionalDependencies": { + "prettier": "^2.8.3" + } + }, + "node_modules/solhint/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } } } } diff --git a/onchain/package.json b/onchain/package.json index c3a6b19..43527f1 100644 --- a/onchain/package.json +++ b/onchain/package.json @@ -1,5 +1,19 @@ { "dependencies": { "@uniswap/universal-router": "^2.0.0" + }, + "devDependencies": { + "husky": "^9.1.7", + "lint-staged": "^16.2.3", + "solhint": "^6.0.1" + }, + "scripts": { + "lint:sol": "solhint 'src/**/*.sol' 'script/**/*.sol' 'test/**/*.sol'", + "lint:sol:fix": "solhint --fix 'src/**/*.sol' 'script/**/*.sol' 'test/**/*.sol'", + "format:sol": "forge fmt", + "format:sol:check": "forge fmt --check", + "lint": "npm run lint:sol && npm run format:sol:check", + "lint:fix": "npm run lint:sol:fix && npm run format:sol", + "prepare": "husky" } } diff --git a/onchain/script/DeployBase.sol b/onchain/script/DeployBase.sol index ca22493..193084e 100644 --- a/onchain/script/DeployBase.sol +++ b/onchain/script/DeployBase.sol @@ -1,15 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "forge-std/Script.sol"; +import "../src/Kraiken.sol"; + +import { LiquidityManager } from "../src/LiquidityManager.sol"; +import "../src/Optimizer.sol"; +import "../src/Stake.sol"; +import "../src/helpers/UniswapHelpers.sol"; +import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import "../src/Kraiken.sol"; -import "../src/Stake.sol"; -import "../src/Optimizer.sol"; -import "../src/helpers/UniswapHelpers.sol"; -import {LiquidityManager} from "../src/LiquidityManager.sol"; -import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; +import "forge-std/Script.sol"; uint24 constant FEE = uint24(10_000); @@ -74,11 +75,7 @@ contract DeployBase is Script { address optimizerAddress; if (optimizer == address(0)) { Optimizer optimizerImpl = new Optimizer(); - bytes memory params = abi.encodeWithSignature( - "initialize(address,address)", - address(kraiken), - address(stake) - ); + bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake)); ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params); optimizerAddress = address(proxy); console.log("Optimizer deployed at:", optimizerAddress); @@ -88,12 +85,7 @@ contract DeployBase is Script { } // Deploy LiquidityManager - liquidityManager = new LiquidityManager( - v3Factory, - weth, - address(kraiken), - optimizerAddress - ); + liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress); console.log("LiquidityManager deployed at:", address(liquidityManager)); // Set fee destination @@ -115,4 +107,4 @@ contract DeployBase is Script { vm.stopBroadcast(); } -} \ No newline at end of file +} diff --git a/onchain/script/DeployBaseMainnet.sol b/onchain/script/DeployBaseMainnet.sol index 6a68ecc..ec6dd3b 100644 --- a/onchain/script/DeployBaseMainnet.sol +++ b/onchain/script/DeployBaseMainnet.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {DeployBase} from "./DeployBase.sol"; +import { DeployBase } from "./DeployBase.sol"; /** * @title DeployBaseMainnet @@ -21,4 +21,4 @@ contract DeployBaseMainnet is DeployBase { // Leave as address(0) to deploy new optimizer optimizer = address(0); } -} \ No newline at end of file +} diff --git a/onchain/script/DeployBaseSepolia.sol b/onchain/script/DeployBaseSepolia.sol index f3a505c..4e4ef87 100644 --- a/onchain/script/DeployBaseSepolia.sol +++ b/onchain/script/DeployBaseSepolia.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {DeployBase} from "./DeployBase.sol"; +import { DeployBase } from "./DeployBase.sol"; /** * @title DeployBaseSepolia @@ -19,4 +19,4 @@ contract DeployBaseSepolia is DeployBase { // optimizer = 0xFCFa3b066981027516121bd27a9B1cBb9C00c5Fd; optimizer = address(0); // Deploy new optimizer } -} \ No newline at end of file +} diff --git a/onchain/script/DeployLocal.sol b/onchain/script/DeployLocal.sol index d7a2e48..895e6a1 100644 --- a/onchain/script/DeployLocal.sol +++ b/onchain/script/DeployLocal.sol @@ -1,15 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "forge-std/Script.sol"; +import "../src/Kraiken.sol"; + +import { LiquidityManager } from "../src/LiquidityManager.sol"; +import "../src/Optimizer.sol"; +import "../src/Stake.sol"; +import "../src/helpers/UniswapHelpers.sol"; +import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import "../src/Kraiken.sol"; -import "../src/Stake.sol"; -import "../src/Optimizer.sol"; -import "../src/helpers/UniswapHelpers.sol"; -import {LiquidityManager} from "../src/LiquidityManager.sol"; -import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; +import "forge-std/Script.sol"; /** * @title DeployLocal @@ -19,12 +20,12 @@ import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; contract DeployLocal is Script { using UniswapHelpers for IUniswapV3Pool; - uint24 constant FEE = uint24(10_000); + uint24 internal constant FEE = uint24(10_000); // Configuration - address constant feeDest = 0xf6a3eef9088A255c32b6aD2025f83E57291D9011; - address constant weth = 0x4200000000000000000000000000000000000006; - address constant v3Factory = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24; + address internal constant feeDest = 0xf6a3eef9088A255c32b6aD2025f83E57291D9011; + address internal constant weth = 0x4200000000000000000000000000000000000006; + address internal constant v3Factory = 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24; // Deployed contracts Kraiken public kraiken; @@ -87,22 +88,13 @@ contract DeployLocal is Script { // Deploy Optimizer Optimizer optimizerImpl = new Optimizer(); - bytes memory params = abi.encodeWithSignature( - "initialize(address,address)", - address(kraiken), - address(stake) - ); + bytes memory params = abi.encodeWithSignature("initialize(address,address)", address(kraiken), address(stake)); ERC1967Proxy proxy = new ERC1967Proxy(address(optimizerImpl), params); address optimizerAddress = address(proxy); console.log("\n[4/6] Optimizer deployed:", optimizerAddress); // Deploy LiquidityManager - liquidityManager = new LiquidityManager( - v3Factory, - weth, - address(kraiken), - optimizerAddress - ); + liquidityManager = new LiquidityManager(v3Factory, weth, address(kraiken), optimizerAddress); console.log("\n[5/6] LiquidityManager deployed:", address(liquidityManager)); // Configure contracts @@ -126,8 +118,8 @@ contract DeployLocal is Script { console.log("1. Fund LiquidityManager with ETH:"); console.log(" cast send", address(liquidityManager), "--value 0.1ether"); console.log("2. Call recenter to initialize positions:"); - console.log(" cast send", address(liquidityManager), '"recenter()"'); + console.log(" cast send", address(liquidityManager), "\"recenter()\""); vm.stopBroadcast(); } -} \ No newline at end of file +} diff --git a/onchain/script/DeployScript2.sol b/onchain/script/DeployScript2.sol index 2c8b765..eed56d6 100644 --- a/onchain/script/DeployScript2.sol +++ b/onchain/script/DeployScript2.sol @@ -1,25 +1,26 @@ pragma solidity ^0.8.19; -import "forge-std/Script.sol"; +import "../src/Kraiken.sol"; + +import { LiquidityManager } from "../src/LiquidityManager.sol"; +import "../src/Optimizer.sol"; +import "../src/Stake.sol"; +import "../src/helpers/UniswapHelpers.sol"; +import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import "../src/Kraiken.sol"; -import "../src/Stake.sol"; -import "../src/Optimizer.sol"; -import "../src/helpers/UniswapHelpers.sol"; -import {LiquidityManager} from "../src/LiquidityManager.sol"; -import {ERC1967Proxy} from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; +import "forge-std/Script.sol"; uint24 constant FEE = uint24(10_000); contract DeployScript is Script { using UniswapHelpers for IUniswapV3Pool; - bool token0isWeth; - address feeDest; - address weth; - address v3Factory; - address twabc; + bool internal token0isWeth; + address internal feeDest; + address internal weth; + address internal v3Factory; + address internal twabc; function run() public { string memory seedPhrase = vm.readFile(".secret"); diff --git a/onchain/src/Kraiken.sol b/onchain/src/Kraiken.sol index b722ec6..af3f935 100644 --- a/onchain/src/Kraiken.sol +++ b/onchain/src/Kraiken.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; -import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; -import {Math} from "@openzeppelin/utils/math/Math.sol"; +import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol"; +import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; +import { Math } from "@openzeppelin/utils/math/Math.sol"; /** * @title stakeable ERC20 Token @@ -42,7 +42,7 @@ contract Kraiken is ERC20, ERC20Permit { * @param name_ The name of the token * @param symbol_ The symbol of the token */ - constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) {} + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) { } /** * @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract. diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 881343f..2b5d2d9 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -1,28 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "@uniswap-v3-periphery/libraries/PositionKey.sol"; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import "@aperture/uni-v3-lib/PoolAddress.sol"; -import "@aperture/uni-v3-lib/CallbackValidation.sol"; -import "@openzeppelin/token/ERC20/IERC20.sol"; -import "./interfaces/IWETH9.sol"; -import {Kraiken} from "./Kraiken.sol"; -import {Optimizer} from "./Optimizer.sol"; -import "./abstracts/ThreePositionStrategy.sol"; +import { Kraiken } from "./Kraiken.sol"; +import { Optimizer } from "./Optimizer.sol"; + import "./abstracts/PriceOracle.sol"; +import "./abstracts/ThreePositionStrategy.sol"; +import "./interfaces/IWETH9.sol"; +import "@aperture/uni-v3-lib/CallbackValidation.sol"; +import "@aperture/uni-v3-lib/PoolAddress.sol"; +import "@openzeppelin/token/ERC20/IERC20.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; +import "@uniswap-v3-periphery/libraries/PositionKey.sol"; /** * @title LiquidityManager * @notice Manages liquidity provisioning on Uniswap V3 using the three-position anti-arbitrage strategy * @dev Inherits from modular contracts for better separation of concerns and testability - * + * * Key features: * - Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR) * - Dynamic parameter adjustment via Optimizer contract * - Asymmetric slippage profile prevents profitable arbitrage * - Exclusive minting rights for KRAIKEN token - * + * * Price Validation: * - 5-minute TWAP with 50-tick tolerance * - Prevents oracle manipulation attacks @@ -74,17 +75,17 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { /// @param amount1Owed Amount of token1 owed for the liquidity provision function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); - + // Handle KRAIKEN minting uint256 kraikenPulled = token0isWeth ? amount1Owed : amount0Owed; kraiken.mint(kraikenPulled); - + // Handle WETH conversion uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed; if (weth.balanceOf(address(this)) < ethOwed) { - weth.deposit{value: address(this).balance}(); + weth.deposit{ value: address(this).balance }(); } - + // Transfer tokens to pool if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); @@ -135,19 +136,14 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { // Remove all existing positions and collect fees _scrapePositions(); - + // Update total supply tracking if price moved up if (isUp) { kraiken.setPreviousTotalSupply(kraiken.totalSupply()); } // Get optimizer parameters and set new positions - try optimizer.getLiquidityParams() returns ( - uint256 capitalInefficiency, - uint256 anchorShare, - uint24 anchorWidth, - uint256 discoveryDepth - ) { + try optimizer.getLiquidityParams() returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { // Clamp parameters to valid ranges PositionParams memory params = PositionParams({ capitalInefficiency: (capitalInefficiency > 10 ** 18) ? 10 ** 18 : capitalInefficiency, @@ -155,17 +151,17 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { anchorWidth: (anchorWidth > 100) ? 100 : anchorWidth, discoveryDepth: (discoveryDepth > 10 ** 18) ? 10 ** 18 : discoveryDepth }); - + _setPositions(currentTick, params); } catch { // Fallback to default parameters if optimizer fails PositionParams memory defaultParams = PositionParams({ - capitalInefficiency: 5 * 10 ** 17, // 50% - anchorShare: 5 * 10 ** 17, // 50% - anchorWidth: 50, // 50% - discoveryDepth: 5 * 10 ** 17 // 50% - }); - + capitalInefficiency: 5 * 10 ** 17, // 50% + anchorShare: 5 * 10 ** 17, // 50% + anchorWidth: 50, // 50% + discoveryDepth: 5 * 10 ** 17 // 50% + }); + _setPositions(currentTick, defaultParams); } } @@ -175,29 +171,20 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { uint256 fee0 = 0; uint256 fee1 = 0; uint256 currentPrice; - + for (uint256 i = uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) { TokenPosition storage position = positions[Stage(i)]; if (position.liquidity > 0) { // Burn liquidity and collect tokens + fees - (uint256 amount0, uint256 amount1) = pool.burn( - position.tickLower, - position.tickUpper, - position.liquidity - ); - - (uint256 collected0, uint256 collected1) = pool.collect( - address(this), - position.tickLower, - position.tickUpper, - type(uint128).max, - type(uint128).max - ); + (uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity); + + (uint256 collected0, uint256 collected1) = + pool.collect(address(this), position.tickLower, position.tickUpper, type(uint128).max, type(uint128).max); // Calculate fees fee0 += collected0 - amount0; fee1 += collected1 - amount1; - + // Record price from anchor position for VWAP if (i == uint256(Stage.ANCHOR)) { int24 tick = position.tickLower + ((position.tickUpper - position.tickLower) / 2); @@ -215,7 +202,7 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { IERC20(address(kraiken)).transfer(feeDestination, fee0); } } - + if (fee1 > 0) { if (token0isWeth) { IERC20(address(kraiken)).transfer(feeDestination, fee1); @@ -224,13 +211,13 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { _recordVolumeAndPrice(currentPrice, fee1); } } - + // Burn any remaining KRAIKEN tokens kraiken.burn(kraiken.balanceOf(address(this))); } /// @notice Allow contract to receive ETH - receive() external payable {} + receive() external payable { } // ======================================== // ABSTRACT FUNCTION IMPLEMENTATIONS @@ -259,11 +246,7 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { /// @notice Implementation of abstract function from ThreePositionStrategy function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override { pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey)); - positions[stage] = TokenPosition({ - liquidity: liquidity, - tickLower: tickLower, - tickUpper: tickUpper - }); + positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper }); } /// @notice Implementation of abstract function from ThreePositionStrategy @@ -275,4 +258,4 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { function _getOutstandingSupply() internal view override returns (uint256) { return kraiken.outstandingSupply(); } -} \ No newline at end of file +} diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol index 5c78b11..2d9a1de 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -1,17 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {Kraiken} from "./Kraiken.sol"; -import {Stake} from "./Stake.sol"; -import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; -import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; +import { Kraiken } from "./Kraiken.sol"; +import { Stake } from "./Stake.sol"; + +import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; /** * @title Optimizer * @notice This contract (formerly Sentimenter) calculates a "sentiment" value and liquidity parameters * based on the tax rate and the percentage of Kraiken staked. * @dev It is upgradeable using UUPS. Only the admin (set during initialization) can upgrade. - * + * * Key features: * - Analyzes staking sentiment (% staked, average tax rate) * - Returns four key parameters for liquidity management: @@ -20,7 +21,7 @@ import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; * 3. anchorWidth (0 to 100): Anchor position width % * 4. discoveryDepth (0 to 1e18): Discovery liquidity density (2x-10x) * - Upgradeable for future algorithm improvements - * + * * AnchorWidth Price Ranges: * The anchor position's price range depends on anchorWidth value: * - anchorWidth = 10: ±9% range (0.92x to 1.09x current price) @@ -28,7 +29,7 @@ import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; * - anchorWidth = 50: ±42% range (0.70x to 1.43x current price) * - anchorWidth = 80: ±74% range (0.57x to 1.75x current price) * - anchorWidth = 100: -50% to +100% range (0.50x to 2.00x current price) - * + * * The formula: anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100) * creates a non-linear price range due to Uniswap V3's tick-based system */ @@ -62,7 +63,7 @@ contract Optimizer is Initializable, UUPSUpgradeable { } } - function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {} + function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { } /** * @notice Calculates the sentiment based on the average tax rate and the percentage staked. @@ -70,11 +71,7 @@ contract Optimizer is Initializable, UUPSUpgradeable { * @param percentageStaked The percentage (in 1e18 precision) of the authorized stake that is currently staked. * @return sentimentValue A value in the range 0 to 1e18 where 1e18 represents the worst sentiment. */ - function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) - public - pure - returns (uint256 sentimentValue) - { + function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) { // Ensure percentageStaked doesn't exceed 100% require(percentageStaked <= 1e18, "Invalid percentage staked"); @@ -120,51 +117,47 @@ contract Optimizer is Initializable, UUPSUpgradeable { * @param percentageStaked The percentage of tokens staked (0 to 1e18) * @param averageTaxRate The average tax rate across all stakers (0 to 1e18) * @return anchorWidth The calculated anchor width (10 to 80) - * + * * @dev This function implements a staking-based approach to determine anchor width: - * + * * Base Strategy: * - Start with base width of 40% (balanced default) - * + * * Staking Adjustment (-20% to +20%): * - High staking (>70%) indicates bullish confidence → narrow anchor for fee optimization * - Low staking (<30%) indicates bearish/uncertainty → wide anchor for safety * - Inverse relationship: higher staking = lower width adjustment - * - * Tax Rate Adjustment (-10% to +30%): + * + * Tax Rate Adjustment (-10% to +30%): * - High tax rates signal expected volatility → wider anchor to reduce rebalancing * - Low tax rates signal expected stability → narrower anchor for fee collection * - Direct relationship: higher tax = higher width adjustment - * + * * The Harberger tax mechanism acts as a decentralized prediction market where: * - Tax rates reflect holders' expectations of being "snatched" (volatility) * - Staking percentage reflects overall market confidence - * + * * Final width is clamped between 10 (minimum safe) and 80 (maximum effective) */ - function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) - internal - pure - returns (uint24) - { + function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) internal pure returns (uint24) { // Base width: 40% is our neutral starting point int256 baseWidth = 40; - + // Staking adjustment: -20% to +20% based on staking percentage // Formula: 20 - (percentageStaked * 40 / 1e18) // High staking (1e18) → -20 adjustment → narrower width // Low staking (0) → +20 adjustment → wider width int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18); - + // Tax rate adjustment: -10% to +30% based on average tax rate // Formula: (averageTaxRate * 40 / 1e18) - 10 // High tax (1e18) → +30 adjustment → wider width for volatility // Low tax (0) → -10 adjustment → narrower width for stability int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10; - + // Combine all adjustments int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment; - + // Clamp to safe bounds (10 to 80) // Below 10%: rebalancing costs exceed benefits // Above 80%: capital efficiency degrades significantly @@ -174,7 +167,7 @@ contract Optimizer is Initializable, UUPSUpgradeable { if (totalWidth > 80) { return 80; } - + return uint24(uint256(totalWidth)); } @@ -184,23 +177,19 @@ contract Optimizer is Initializable, UUPSUpgradeable { * @return anchorShare Set equal to the sentiment. % of non-floor ETH in anchor (0-1e18) * @return anchorWidth Dynamically adjusted based on staking metrics. Anchor position width % (1-100) * @return discoveryDepth Set equal to the sentiment. - * + * * @dev AnchorWidth Strategy: * The anchorWidth parameter controls the price range of the anchor liquidity position. * - anchorWidth = 50: Price range from 0.70x to 1.43x current price * - anchorWidth = 100: Price range from 0.50x to 2.00x current price - * + * * We use staking metrics as a decentralized prediction market: * - High staking % → Bullish sentiment → Narrower width (30-50%) for fee optimization * - Low staking % → Bearish/uncertain → Wider width (60-80%) for defensive positioning * - High avg tax rate → Expects volatility → Wider anchor to reduce rebalancing * - Low avg tax rate → Expects stability → Narrower anchor for fee collection */ - function getLiquidityParams() - external - view - returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) - { + function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { uint256 percentageStaked = stake.getPercentageStaked(); uint256 averageTaxRate = stake.getAverageTaxRate(); uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked); @@ -212,10 +201,10 @@ contract Optimizer is Initializable, UUPSUpgradeable { } capitalInefficiency = 1e18 - sentiment; anchorShare = sentiment; - + // Calculate dynamic anchorWidth based on staking metrics anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate); - + discoveryDepth = sentiment; } } diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index 22d12b7..b91b9f5 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; -import {IERC20Metadata} from "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; -import {ERC20Permit} from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; -import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; -import {Math} from "@openzeppelin/utils/math/Math.sol"; -import {Kraiken} from "./Kraiken.sol"; +import { Kraiken } from "./Kraiken.sol"; +import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol"; +import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; +import { IERC20Metadata } from "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; +import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import { Math } from "@openzeppelin/utils/math/Math.sol"; error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake); error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare); @@ -26,7 +26,7 @@ error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStak * * Tax rates and staking positions are adjustable, with a mechanism to prevent snatch-grieving by * enforcing a minimum tax payment duration. - * + * * @dev Self-assessed tax implementation: * - Continuous auction mechanism * - Self-assessed valuations create prediction market @@ -42,38 +42,8 @@ contract Stake { uint256 internal constant MAX_STAKE = 20; // 20% of KRAIKEN supply uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3; //this duration is the minimum basis for fee calculation, regardless of actual holding time. // the tax rates are discrete to prevent users from snatching by micro incroments of tax - uint256[] public TAX_RATES = [ - 1, - 3, - 5, - 8, - 12, - 18, - 24, - 30, - 40, - 50, - 60, - 80, - 100, - 130, - 180, - 250, - 320, - 420, - 540, - 700, - 920, - 1200, - 1600, - 2000, - 2600, - 3400, - 4400, - 5700, - 7500, - 9700 - ]; + uint256[] public TAX_RATES = + [1, 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700]; // this is the base for the values in the array above: e.g. 1/100 = 1% uint256 internal constant TAX_RATE_BASE = 100; /** @@ -85,12 +55,8 @@ contract Stake { error NoPermission(address requester, address owner); error PositionNotFound(uint256 positionId, address requester); - event PositionCreated( - uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate - ); - event PositionTaxPaid( - uint256 indexed positionId, address indexed owner, uint256 taxPaid, uint256 newShares, uint256 taxRate - ); + event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate); + event PositionTaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxPaid, uint256 newShares, uint256 taxRate); event PositionRateHiked(uint256 indexed positionId, address indexed owner, uint256 newTaxRate); event PositionShrunk(uint256 indexed positionId, address indexed owner, uint256 newShares, uint256 kraikenPayout); event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 kraikenPayout); @@ -123,7 +89,7 @@ contract Stake { taxReceiver = _taxReceiver; totalSupply = 10 ** (kraiken.decimals() + DECIMAL_OFFSET); // start counting somewhere - nextPositionId = 654321; + nextPositionId = 654_321; // Initialize totalSharesAtTaxRate array totalSharesAtTaxRate = new uint256[](TAX_RATES.length); } @@ -136,13 +102,10 @@ contract Stake { function _payTax(uint256 positionId, StakingPosition storage pos, uint256 taxFloorDuration) private { // existance of position should be checked before // ihet = Implied Holding Expiry Timestamp - uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) - ? pos.creationTime + taxFloorDuration - : block.timestamp; + uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp; uint256 elapsedTime = ihet - pos.lastTaxTime; uint256 assetsBefore = sharesToAssets(pos.share); - uint256 taxAmountDue = - assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; + uint256 taxAmountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; if (taxAmountDue >= assetsBefore) { // can not pay more tax than value of position taxAmountDue = assetsBefore; @@ -214,10 +177,7 @@ contract Stake { /// @param positionsToSnatch Array of position IDs that the new position will replace by snatching. /// @return positionId The ID of the newly created staking position. /// @dev Handles staking logic, including tax rate validation and position merging or dissolving. - function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) - public - returns (uint256 positionId) - { + function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) public returns (uint256 positionId) { // check lower boundary uint256 sharesWanted = assetsToShares(assets); { @@ -328,7 +288,10 @@ contract Stake { uint8 v, bytes32 r, bytes32 s - ) external returns (uint256 positionId) { + ) + external + returns (uint256 positionId) + { ERC20Permit(address(kraiken)).permit(receiver, address(this), assets, deadline, v, r, s); return snatch(assets, receiver, taxRate, positionsToSnatch); } @@ -389,9 +352,7 @@ contract Stake { function taxDue(uint256 positionId, uint256 taxFloorDuration) public view returns (uint256 amountDue) { StakingPosition storage pos = positions[positionId]; // ihet = Implied Holding Expiry Timestamp - uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) - ? pos.creationTime + taxFloorDuration - : block.timestamp; + uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp; uint256 elapsedTime = ihet - pos.lastTaxTime; uint256 assetsBefore = sharesToAssets(pos.share); amountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE; diff --git a/onchain/src/VWAPTracker.sol b/onchain/src/VWAPTracker.sol index 6f768bb..cf0ea59 100644 --- a/onchain/src/VWAPTracker.sol +++ b/onchain/src/VWAPTracker.sol @@ -7,7 +7,7 @@ import "@openzeppelin/utils/math/Math.sol"; * @title VWAPTracker * @notice Abstract contract for tracking Volume Weighted Average Price (VWAP) data * @dev Provides VWAP calculation and storage functionality that can be inherited by other contracts - * + * * Key features: * - Volume-weighted average with data compression (max 1000x compression) * - Prevents dormant whale manipulation through historical price memory @@ -31,7 +31,7 @@ abstract contract VWAPTracker { // assuming FEE is 1% uint256 volume = fee * 100; uint256 volumeWeightedPriceX96 = currentPriceX96 * volume; - + // ULTRA-RARE EDGE CASE: Check if the new data itself would overflow even before adding // This can only happen with impossibly large transactions (>10,000 ETH + $billion token prices) if (volumeWeightedPriceX96 > type(uint256).max / 2) { @@ -46,18 +46,18 @@ abstract contract VWAPTracker { // Find the MINIMUM compression factor needed to prevent overflow uint256 maxSafeValue = type(uint256).max / 10 ** 6; // Leave substantial room for future data uint256 compressionFactor = (cumulativeVolumeWeightedPriceX96 / maxSafeValue) + 1; - + // Cap maximum compression to preserve historical "eternal memory" // Even in extreme cases, historical data should retain significant weight if (compressionFactor > 1000) { compressionFactor = 1000; // Maximum 1000x compression to preserve history } - + // Ensure minimum compression effectiveness if (compressionFactor < 2) { compressionFactor = 2; // At least 2x compression when triggered } - + // Compress both values by the same minimal factor cumulativeVolumeWeightedPriceX96 = cumulativeVolumeWeightedPriceX96 / compressionFactor; cumulativeVolume = cumulativeVolume / compressionFactor; diff --git a/onchain/src/abstracts/PriceOracle.sol b/onchain/src/abstracts/PriceOracle.sol index 391590e..b73290d 100644 --- a/onchain/src/abstracts/PriceOracle.sol +++ b/onchain/src/abstracts/PriceOracle.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "@openzeppelin/utils/math/SignedMath.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; /** * @title PriceOracle @@ -23,7 +23,7 @@ abstract contract PriceOracle { /// @return isStable True if price is within acceptable deviation from TWAP function _isPriceStable(int24 currentTick) internal view returns (bool isStable) { IUniswapV3Pool pool = _getPool(); - + uint32[] memory secondsAgo = new uint32[](2); secondsAgo[0] = PRICE_STABILITY_INTERVAL; // 5 minutes ago secondsAgo[1] = 0; // current block timestamp @@ -51,15 +51,19 @@ abstract contract PriceOracle { /// @return isUp True if price moved up (relative to token ordering) /// @return isEnough True if movement amplitude is sufficient for recentering function _validatePriceMovement( - int24 currentTick, - int24 centerTick, + int24 currentTick, + int24 centerTick, int24 tickSpacing, bool token0isWeth - ) internal pure returns (bool isUp, bool isEnough) { + ) + internal + pure + returns (bool isUp, bool isEnough) + { uint256 minAmplitude = uint24(tickSpacing) * 2; // Determine the correct comparison direction based on token0isWeth isUp = token0isWeth ? currentTick < centerTick : currentTick > centerTick; isEnough = SignedMath.abs(currentTick - centerTick) > minAmplitude; } -} \ No newline at end of file +} diff --git a/onchain/src/abstracts/ThreePositionStrategy.sol b/onchain/src/abstracts/ThreePositionStrategy.sol index 145807f..eb3c38f 100644 --- a/onchain/src/abstracts/ThreePositionStrategy.sol +++ b/onchain/src/abstracts/ThreePositionStrategy.sol @@ -1,23 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import "@aperture/uni-v3-lib/TickMath.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; -import {Math} from "@openzeppelin/utils/math/Math.sol"; -import "../libraries/UniswapMath.sol"; import "../VWAPTracker.sol"; +import "../libraries/UniswapMath.sol"; +import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; +import "@aperture/uni-v3-lib/TickMath.sol"; +import { Math } from "@openzeppelin/utils/math/Math.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; /** * @title ThreePositionStrategy * @notice Abstract contract implementing the three-position liquidity strategy (Floor, Anchor, Discovery) * @dev Provides the core logic for anti-arbitrage asymmetric slippage profile - * + * * Three-Position Strategy: * - ANCHOR: Near current price, fast price discovery (1-100% width) * - DISCOVERY: Borders anchor, captures fees (11000 tick spacing) * - FLOOR: Deep liquidity at VWAP-adjusted prices - * + * * The asymmetric slippage profile prevents profitable arbitrage by making * buys progressively more expensive while sells remain liquid */ @@ -27,7 +27,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { /// @notice Tick spacing for the pool (base spacing) int24 internal constant TICK_SPACING = 200; /// @notice Discovery spacing (3x current price in ticks - 11000 ticks = ~3x price) - int24 internal constant DISCOVERY_SPACING = 11000; + int24 internal constant DISCOVERY_SPACING = 11_000; /// @notice Minimum discovery depth multiplier uint128 internal constant MIN_DISCOVERY_DEPTH = 200; @@ -73,7 +73,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { /// @param params Position parameters from optimizer function _setPositions(int24 currentTick, PositionParams memory params) internal { uint256 ethBalance = _getEthBalance(); - + // Calculate floor ETH allocation (75% to 95% of total) uint256 floorEthBalance = (19 * ethBalance / 20) - (2 * params.anchorShare * ethBalance / 10 ** 19); @@ -97,19 +97,22 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { int24 currentTick, uint256 anchorEthBalance, PositionParams memory params - ) internal returns (uint256 pulledKraiken, uint128 anchorLiquidity) { + ) + internal + returns (uint256 pulledKraiken, uint128 anchorLiquidity) + { // Enforce anchor range of 1% to 100% of the price int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); - + int24 tickLower = _clampToTickSpacing(currentTick - anchorSpacing, TICK_SPACING); int24 tickUpper = _clampToTickSpacing(currentTick + anchorSpacing, TICK_SPACING); - + uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); bool token0isWeth = _isToken0Weth(); - + if (token0isWeth) { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, anchorEthBalance); pulledKraiken = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, anchorLiquidity); @@ -117,7 +120,7 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, anchorEthBalance); pulledKraiken = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, anchorLiquidity); } - + _mintPosition(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity); } @@ -126,49 +129,36 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { /// @param anchorLiquidity Liquidity amount from anchor position /// @param params Position parameters /// @return discoveryAmount Amount of KRAIKEN used for discovery - function _setDiscoveryPosition( - int24 currentTick, - uint128 anchorLiquidity, - PositionParams memory params - ) internal returns (uint256 discoveryAmount) { + function _setDiscoveryPosition(int24 currentTick, uint128 anchorLiquidity, PositionParams memory params) internal returns (uint256 discoveryAmount) { currentTick = currentTick / TICK_SPACING * TICK_SPACING; bool token0isWeth = _isToken0Weth(); - + // Calculate anchor spacing (same as in anchor position) int24 anchorSpacing = TICK_SPACING + (34 * int24(params.anchorWidth) * TICK_SPACING / 100); - - int24 tickLower = _clampToTickSpacing( - token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing, - TICK_SPACING - ); - int24 tickUpper = _clampToTickSpacing( - token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing, - TICK_SPACING - ); - + + int24 tickLower = _clampToTickSpacing(token0isWeth ? currentTick - DISCOVERY_SPACING - anchorSpacing : currentTick + anchorSpacing, TICK_SPACING); + int24 tickUpper = _clampToTickSpacing(token0isWeth ? currentTick - anchorSpacing : currentTick + DISCOVERY_SPACING + anchorSpacing, TICK_SPACING); + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); // Calculate discovery liquidity to ensure X times more liquidity per tick than anchor // Discovery should have 2x to 10x more liquidity per tick (not just total liquidity) uint256 discoveryMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18); - + // Calculate anchor width in ticks int24 anchorWidth = 2 * anchorSpacing; - + // Adjust for width difference: discovery liquidity = anchor liquidity * multiplier * (discovery width / anchor width) - uint128 liquidity = uint128( - uint256(anchorLiquidity) * discoveryMultiplier * uint256(int256(DISCOVERY_SPACING)) - / (100 * uint256(int256(anchorWidth))) - ); - + uint128 liquidity = uint128(uint256(anchorLiquidity) * discoveryMultiplier * uint256(int256(DISCOVERY_SPACING)) / (100 * uint256(int256(anchorWidth)))); + // Calculate discoveryAmount for floor position calculation if (token0isWeth) { discoveryAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } else { discoveryAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); } - + _mintPosition(Stage.DISCOVERY, tickLower, tickUpper, liquidity); } @@ -188,26 +178,27 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { uint256 pulledKraiken, uint256 discoveryAmount, PositionParams memory params - ) internal { + ) + internal + { bool token0isWeth = _isToken0Weth(); - + // Calculate outstanding supply after position minting uint256 outstandingSupply = _getOutstandingSupply(); outstandingSupply -= pulledKraiken; outstandingSupply -= (outstandingSupply >= discoveryAmount) ? discoveryAmount : outstandingSupply; - + // Use VWAP for floor position (historical price memory for dormant whale protection) uint256 vwapX96 = getAdjustedVWAP(params.capitalInefficiency); uint256 ethBalance = _getEthBalance(); int24 vwapTick; - - + if (vwapX96 > 0) { // vwapX96 is price² in X96 format, need to convert to regular price // price = sqrt(price²) = sqrt(vwapX96) * 2^48 / 2^96 = sqrt(vwapX96) / 2^48 uint256 sqrtVwapX96 = Math.sqrt(vwapX96) << 48; // sqrt(price²) in X96 format uint256 requiredEthForBuyback = outstandingSupply.mulDiv(sqrtVwapX96, (1 << 96)); - + if (floorEthBalance < requiredEthForBuyback) { // ETH scarcity: not enough ETH to buy back at VWAP price uint256 balancedCapital = (7 * outstandingSupply / 10) + (outstandingSupply * params.capitalInefficiency / 10 ** 18); @@ -237,31 +228,22 @@ abstract contract ThreePositionStrategy is UniswapMath, VWAPTracker { // Normalize and create floor position vwapTick = _clampToTickSpacing(vwapTick, TICK_SPACING); - int24 floorTick = _clampToTickSpacing( - token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING, - TICK_SPACING - ); - + int24 floorTick = _clampToTickSpacing(token0isWeth ? vwapTick + TICK_SPACING : vwapTick - TICK_SPACING, TICK_SPACING); + // Use planned floor ETH balance, but fallback to remaining if insufficient uint256 remainingEthBalance = _getEthBalance(); uint256 actualFloorEthBalance = (remainingEthBalance >= floorEthBalance) ? floorEthBalance : remainingEthBalance; - + uint128 liquidity; if (token0isWeth) { // floor leg sits entirely above current tick when WETH is token0, so budget is token0 - liquidity = LiquidityAmounts.getLiquidityForAmount0( - TickMath.getSqrtRatioAtTick(vwapTick), - TickMath.getSqrtRatioAtTick(floorTick), - actualFloorEthBalance - ); + liquidity = + LiquidityAmounts.getLiquidityForAmount0(TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance); } else { - liquidity = LiquidityAmounts.getLiquidityForAmount1( - TickMath.getSqrtRatioAtTick(vwapTick), - TickMath.getSqrtRatioAtTick(floorTick), - actualFloorEthBalance - ); + liquidity = + LiquidityAmounts.getLiquidityForAmount1(TickMath.getSqrtRatioAtTick(vwapTick), TickMath.getSqrtRatioAtTick(floorTick), actualFloorEthBalance); } - + _mintPosition(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); } } diff --git a/onchain/src/helpers/UniswapHelpers.sol b/onchain/src/helpers/UniswapHelpers.sol index 75a7e52..57602a9 100644 --- a/onchain/src/helpers/UniswapHelpers.sol +++ b/onchain/src/helpers/UniswapHelpers.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; library UniswapHelpers { /** diff --git a/onchain/src/libraries/UniswapMath.sol b/onchain/src/libraries/UniswapMath.sol index 9558782..26f1089 100644 --- a/onchain/src/libraries/UniswapMath.sol +++ b/onchain/src/libraries/UniswapMath.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; +import { ABDKMath64x64 } from "@abdk/ABDKMath64x64.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; -import {Math} from "@openzeppelin/utils/math/Math.sol"; -import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol"; +import { Math } from "@openzeppelin/utils/math/Math.sol"; /** * @title UniswapMath @@ -65,4 +65,4 @@ abstract contract UniswapMath { if (clampedTick < TickMath.MIN_TICK) clampedTick = TickMath.MIN_TICK; if (clampedTick > TickMath.MAX_TICK) clampedTick = TickMath.MAX_TICK; } -} \ No newline at end of file +} diff --git a/onchain/test/Kraiken.t.sol b/onchain/test/Kraiken.t.sol index 3ce3e36..29219d2 100644 --- a/onchain/test/Kraiken.t.sol +++ b/onchain/test/Kraiken.t.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; +import "../src/Kraiken.sol"; +import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; -import "../src/Kraiken.sol"; contract KraikenTest is Test { - Kraiken kraiken; - address stakingPool; - address liquidityPool; - address liquidityManager; + Kraiken internal kraiken; + address internal stakingPool; + address internal liquidityPool; + address internal liquidityManager; function setUp() public { kraiken = new Kraiken("KRAIKEN", "KRK"); @@ -127,8 +127,7 @@ contract KraikenTest is Test { uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool); mintAmount = bound(mintAmount, 1, 500 * 1e18); - uint256 expectedNewStake = - initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance); + uint256 expectedNewStake = initialStakingPoolBalance * mintAmount / (initialTotalSupply - initialStakingPoolBalance); // Expect Transfer events vm.expectEmit(true, true, true, true, address(kraiken)); @@ -139,11 +138,7 @@ contract KraikenTest is Test { uint256 expectedStakingPoolBalance = initialStakingPoolBalance + expectedNewStake; uint256 expectedTotalSupply = initialTotalSupply + mintAmount + expectedNewStake; - assertEq( - kraiken.balanceOf(stakingPool), - expectedStakingPoolBalance, - "Staking pool balance did not adjust correctly after mint." - ); + assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after mint."); assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after mint."); } @@ -164,8 +159,7 @@ contract KraikenTest is Test { burnAmount = bound(burnAmount, 0, 200 * 1e18); uint256 initialTotalSupply = kraiken.totalSupply(); uint256 initialStakingPoolBalance = kraiken.balanceOf(stakingPool); - uint256 expectedExcessStake = - initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance); + uint256 expectedExcessStake = initialStakingPoolBalance * burnAmount / (initialTotalSupply - initialStakingPoolBalance); vm.prank(address(liquidityManager)); kraiken.burn(burnAmount); @@ -173,11 +167,7 @@ contract KraikenTest is Test { uint256 expectedStakingPoolBalance = initialStakingPoolBalance - expectedExcessStake; uint256 expectedTotalSupply = initialTotalSupply - burnAmount - expectedExcessStake; - assertEq( - kraiken.balanceOf(stakingPool), - expectedStakingPoolBalance, - "Staking pool balance did not adjust correctly after burn." - ); + assertEq(kraiken.balanceOf(stakingPool), expectedStakingPoolBalance, "Staking pool balance did not adjust correctly after burn."); assertEq(kraiken.totalSupply(), expectedTotalSupply, "Total supply did not match expected after burn."); } } diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index 3448618..c1e60dc 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -10,24 +10,25 @@ pragma solidity ^0.8.19; * - Edge case classification and recovery * @dev Uses setUp() pattern for consistent test initialization */ -import "forge-std/Test.sol"; +import { Kraiken } from "../src/Kraiken.sol"; +import "../src/interfaces/IWETH9.sol"; +import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; +import { PoolAddress, PoolKey } from "@aperture/uni-v3-lib/PoolAddress.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; -import {WETH} from "solmate/tokens/WETH.sol"; -import {PoolAddress, PoolKey} from "@aperture/uni-v3-lib/PoolAddress.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import "../src/interfaces/IWETH9.sol"; -import {Kraiken} from "../src/Kraiken.sol"; +import "forge-std/Test.sol"; +import { WETH } from "solmate/tokens/WETH.sol"; + +import { LiquidityManager } from "../src/LiquidityManager.sol"; -import {Stake, ExceededAvailableStake} from "../src/Stake.sol"; -import {LiquidityManager} from "../src/LiquidityManager.sol"; -import {ThreePositionStrategy} from "../src/abstracts/ThreePositionStrategy.sol"; -import "../src/helpers/UniswapHelpers.sol"; -import {UniSwapHelper} from "./helpers/UniswapTestBase.sol"; -import {TestEnvironment} from "./helpers/TestBase.sol"; import "../src/Optimizer.sol"; +import { ExceededAvailableStake, Stake } from "../src/Stake.sol"; +import { ThreePositionStrategy } from "../src/abstracts/ThreePositionStrategy.sol"; +import "../src/helpers/UniswapHelpers.sol"; import "../test/mocks/MockOptimizer.sol"; +import { TestEnvironment } from "./helpers/TestBase.sol"; +import { UniSwapHelper } from "./helpers/UniswapTestBase.sol"; // Test constants uint24 constant FEE = uint24(10_000); // 1% fee @@ -53,10 +54,8 @@ uint256 constant VWAP_TEST_BALANCE = 100 ether; // Error handling constants bytes32 constant AMPLITUDE_ERROR = keccak256("amplitude not reached."); -bytes32 constant EXPENSIVE_HARB_ERROR = - keccak256("HARB extremely expensive: perform swap to normalize price before recenter"); -bytes32 constant PROTOCOL_DEATH_ERROR = - keccak256("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices"); +bytes32 constant EXPENSIVE_HARB_ERROR = keccak256("HARB extremely expensive: perform swap to normalize price before recenter"); +bytes32 constant PROTOCOL_DEATH_ERROR = keccak256("Protocol death: Insufficient ETH reserves to support HARB at extremely low prices"); // Dummy.sol contract Dummy { @@ -80,7 +79,7 @@ contract LiquidityManagerTest is UniSwapHelper { LiquidityManager lm; Optimizer optimizer; address feeDestination = makeAddr("fees"); - + // Test environment instance TestEnvironment testEnv; @@ -115,7 +114,7 @@ contract LiquidityManagerTest is UniSwapHelper { if (address(testEnv) == address(0)) { testEnv = new TestEnvironment(feeDestination); } - + // Use test environment to set up protocol ( IUniswapV3Factory _factory, @@ -127,7 +126,7 @@ contract LiquidityManagerTest is UniSwapHelper { Optimizer _optimizer, bool _token0isWeth ) = testEnv.setupEnvironment(token0shouldBeWeth, RECENTER_CALLER); - + // Assign to state variables factory = _factory; pool = _pool; @@ -184,12 +183,11 @@ contract LiquidityManagerTest is UniSwapHelper { } } - /// @notice Validates recenter operation results /// @param isUp Whether the recenter moved positions up or down function _validateRecenterResult(bool isUp) internal view { Response memory liquidityResponse = inspectPositions(isUp ? "shift" : "slide"); - + // Debug logging console.log("=== POSITION ANALYSIS ==="); console.log("Floor ETH:", liquidityResponse.ethFloor); @@ -198,31 +196,23 @@ contract LiquidityManagerTest is UniSwapHelper { console.log("Floor HARB:", liquidityResponse.harbergFloor); console.log("Anchor HARB:", liquidityResponse.harbergAnchor); console.log("Discovery HARB:", liquidityResponse.harbergDiscovery); - + // TEMPORARILY COMMENT OUT THIS ASSERTION TO SEE ACTUAL VALUES // assertGt( // liquidityResponse.ethFloor, liquidityResponse.ethAnchor, "slide - Floor should hold more ETH than Anchor" // ); - assertGt( - liquidityResponse.harbergDiscovery, - liquidityResponse.harbergAnchor * 5, - "slide - Discovery should hold more HARB than Anchor" - ); - + assertGt(liquidityResponse.harbergDiscovery, liquidityResponse.harbergAnchor * 5, "slide - Discovery should hold more HARB than Anchor"); + // Check anchor-discovery contiguity (depends on token ordering) if (token0isWeth) { // When WETH is token0, discovery comes before anchor assertEq( - liquidityResponse.discoveryTickUpper, - liquidityResponse.anchorTickLower, - "Discovery and Anchor positions must be contiguous (WETH as token0)" + liquidityResponse.discoveryTickUpper, liquidityResponse.anchorTickLower, "Discovery and Anchor positions must be contiguous (WETH as token0)" ); } else { // When WETH is token1, discovery comes after anchor assertEq( - liquidityResponse.anchorTickUpper, - liquidityResponse.discoveryTickLower, - "Anchor and Discovery positions must be contiguous (WETH as token1)" + liquidityResponse.anchorTickUpper, liquidityResponse.discoveryTickLower, "Anchor and Discovery positions must be contiguous (WETH as token1)" ); } assertEq(liquidityResponse.harbergFloor, 0, "slide - Floor should have no HARB"); @@ -253,7 +243,6 @@ contract LiquidityManagerTest is UniSwapHelper { } } - /// @notice Retrieves liquidity position information for a specific stage /// @param s The liquidity stage (FLOOR, ANCHOR, DISCOVERY) /// @return currentTick Current price tick of the pool @@ -368,7 +357,7 @@ contract LiquidityManagerTest is UniSwapHelper { /// @notice Allows contract to receive ETH directly /// @dev Required for WETH unwrapping operations during testing - receive() external payable {} + receive() external payable { } /// @notice Override to provide LiquidityManager reference for liquidity-aware functions /// @return liquidityManager The LiquidityManager contract instance @@ -415,7 +404,7 @@ contract LiquidityManagerTest is UniSwapHelper { // Fund account and convert to WETH vm.deal(account, accountBalance); vm.prank(account); - weth.deposit{value: accountBalance}(); + weth.deposit{ value: accountBalance }(); // Setup initial liquidity recenterWithErrorHandling(false); @@ -425,29 +414,29 @@ contract LiquidityManagerTest is UniSwapHelper { // EXTREME PRICE HANDLING TESTS // ======================================== - /// @notice Tests system behavior when price approaches Uniswap MAX_TICK boundary + /// @notice Tests system behavior when price approaches Uniswap MAX_TICK boundary /// @dev Validates that massive trades can push price to extreme boundary conditions (MAX_TICK - 15000) /// without system failure. Tests system stability at tick boundaries. function testTickBoundaryReaching() public { // Skip automatic setup to reduce blocking liquidity disableAutoSetup(); - - // Custom minimal setup + + // Custom minimal setup deployProtocolWithTokenOrder(DEFAULT_TOKEN0_IS_WETH); - vm.deal(account, 15000 ether); + vm.deal(account, 15_000 ether); vm.prank(account); - weth.deposit{value: 15000 ether}(); - + weth.deposit{ value: 15_000 ether }(); + // Grant recenter access vm.prank(feeDestination); lm.setRecenterAccess(RECENTER_CALLER); - + // Setup approvals without creating blocking positions vm.startPrank(account); weth.approve(address(lm), type(uint256).max); harberg.approve(address(lm), type(uint256).max); vm.stopPrank(); - + // Record initial state - should be around -123891 (1 cent price) (, int24 initialTick,,,,,) = pool.slot0(); // Pool starts with 0 liquidity, positions created during first trade @@ -456,64 +445,64 @@ contract LiquidityManagerTest is UniSwapHelper { // Stage 1: Large initial push to approach MAX_TICK buyRaw(8000 ether); (, int24 stage1Tick,,,,,) = pool.slot0(); - - // Stage 2: Additional push if not yet at extreme boundary - if (stage1Tick < TickMath.MAX_TICK - 15000) { + + // Stage 2: Additional push if not yet at extreme boundary + if (stage1Tick < TickMath.MAX_TICK - 15_000) { buyRaw(2500 ether); (, int24 stage2Tick,,,,,) = pool.slot0(); - + // Stage 3: Final push with remaining ETH if still needed - if (stage2Tick < TickMath.MAX_TICK - 15000) { + if (stage2Tick < TickMath.MAX_TICK - 15_000) { uint256 remaining = weth.balanceOf(account) - 500 ether; // Keep some ETH for safety buyRaw(remaining); } } (, int24 postBuyTick,,,,,) = pool.slot0(); - - // Verify we reached extreme boundary condition - int24 targetBoundary = TickMath.MAX_TICK - 15000; // 872272 + + // Verify we reached extreme boundary condition + int24 targetBoundary = TickMath.MAX_TICK - 15_000; // 872272 assertGe(postBuyTick, targetBoundary, "Should reach extreme expensive boundary to validate boundary behavior"); - + // Test successfully demonstrates reaching extreme tick boundaries with buyRaw() // In real usage, client-side detection would trigger normalization swaps - + // Verify that recenter() fails at extreme tick positions (as expected) try lm.recenter() { revert("Recenter should fail at extreme tick positions"); } catch { // Expected behavior - recenter fails when trying to create positions near MAX_TICK } - + // Test passes: buyRaw() successfully reached tick boundaries } - // testEmptyPoolBoundaryJump() removed - was only needed for debugging "hidden liquidity mystery" + // testEmptyPoolBoundaryJump() removed - was only needed for debugging "hidden liquidity mystery" // Mystery was solved: conservative price limits in performSwap() were preventing MAX_TICK jumps function testLiquidityAwareTradeLimiting() public { // Test demonstrates liquidity-aware trade size limiting - + // Check calculated limits based on current position boundaries uint256 buyLimit = buyLimitToLiquidityBoundary(); uint256 sellLimit = sellLimitToLiquidityBoundary(); - + (, int24 initialTick,,,,,) = pool.slot0(); uint256 testAmount = 100 ether; - + // Regular buy() should be capped to position boundary buy(testAmount); (, int24 cappedTick,,,,,) = pool.slot0(); - + // Raw buy() should not be capped buyRaw(testAmount); (, int24 rawTick,,,,,) = pool.slot0(); - + // Verify that raw version moved price more than capped version assertGt(rawTick - cappedTick, 0, "Raw buy should move price more than capped buy"); - + // The exact limits depend on current position configuration: - // - buyLimit was calculated as ~7 ETH in current setup + // - buyLimit was calculated as ~7 ETH in current setup // - Regular buy(100 ETH) was capped to ~7 ETH, moved 2957 ticks // - Raw buyRaw(100 ETH) used full 100 ETH, moved additional 734 ticks } @@ -527,11 +516,7 @@ contract LiquidityManagerTest is UniSwapHelper { OTHER_ERROR } - function classifyFailure(bytes memory reason) - internal - view - returns (FailureType failureType, string memory details) - { + function classifyFailure(bytes memory reason) internal view returns (FailureType failureType, string memory details) { if (reason.length >= 4) { bytes4 selector = bytes4(reason); @@ -539,9 +524,7 @@ contract LiquidityManagerTest is UniSwapHelper { if (selector == 0xae47f702) { // FullMulDivFailed() - return ( - FailureType.ARITHMETIC_OVERFLOW, "FullMulDivFailed - arithmetic overflow in liquidity calculations" - ); + return (FailureType.ARITHMETIC_OVERFLOW, "FullMulDivFailed - arithmetic overflow in liquidity calculations"); } if (selector == 0x4e487b71) { @@ -648,7 +631,7 @@ contract LiquidityManagerTest is UniSwapHelper { console.log("Details:", details); // This might be acceptable if we're at extreme prices - if (currentTick <= TickMath.MIN_TICK + 50000 || currentTick >= TickMath.MAX_TICK - 50000) { + if (currentTick <= TickMath.MIN_TICK + 50_000 || currentTick >= TickMath.MAX_TICK - 50_000) { console.log("Overflow at extreme tick - this may be acceptable edge case handling"); } else { console.log("Overflow at normal tick - this indicates a problem"); @@ -713,9 +696,9 @@ contract LiquidityManagerTest is UniSwapHelper { // Diagnose the scenario type console.log("\n=== SCENARIO DIAGNOSIS ==="); - if (postBuyTick >= TickMath.MAX_TICK - 15000) { + if (postBuyTick >= TickMath.MAX_TICK - 15_000) { console.log("[DIAGNOSIS] EXTREME EXPENSIVE HARB - should trigger normalization"); - } else if (postBuyTick <= TickMath.MIN_TICK + 15000) { + } else if (postBuyTick <= TickMath.MIN_TICK + 15_000) { console.log("[DIAGNOSIS] EXTREME CHEAP HARB - potential protocol death"); } else { console.log("[DIAGNOSIS] NORMAL RANGE - may still have arithmetic issues"); @@ -797,9 +780,7 @@ contract LiquidityManagerTest is UniSwapHelper { uint256 traderBalanceAfter = weth.balanceOf(account); // Core unit test assertion: protocol should not allow trader profit - assertGe( - traderBalanceBefore, traderBalanceAfter, "Protocol must prevent trader profit through arbitrary trading" - ); + assertGe(traderBalanceBefore, traderBalanceAfter, "Protocol must prevent trader profit through arbitrary trading"); } /// @notice Helper to execute a sequence of random trades and recentering @@ -816,12 +797,12 @@ contract LiquidityManagerTest is UniSwapHelper { // Handle extreme price conditions to prevent test failures (, int24 currentTick,,,,,) = pool.slot0(); - if (currentTick < -887270) { + if (currentTick < -887_270) { // Price too low - small buy to stabilize uint256 wethBal = weth.balanceOf(account); if (wethBal > 0) buy(wethBal / 100); } - if (currentTick > 887270) { + if (currentTick > 887_270) { // Price too high - small sell to stabilize uint256 harbBal = harberg.balanceOf(account); if (harbBal > 0) sell(harbBal / 100); @@ -844,7 +825,6 @@ contract LiquidityManagerTest is UniSwapHelper { recenterWithErrorHandling(true); } - // ======================================== // ANTI-ARBITRAGE STRATEGY TESTS // ======================================== @@ -857,24 +837,24 @@ contract LiquidityManagerTest is UniSwapHelper { // Phase 1: Record initial state and execute first large trade (, int24 initialTick,,,,,) = pool.slot0(); uint256 wethBefore = weth.balanceOf(account); - + console.log("=== PHASE 1: Initial Trade ==="); console.log("Initial tick:", vm.toString(initialTick)); // Execute first large trade (buy HARB) to move price significantly buy(30 ether); - + uint256 wethAfter1 = weth.balanceOf(account); uint256 wethSpent = wethBefore - wethAfter1; uint256 harbReceived = harberg.balanceOf(account); - + console.log("Spent", wethSpent / 1e18, "ETH, received", harbReceived / 1e18); // Phase 2: Trigger recenter to rebalance liquidity positions console.log("\n=== PHASE 2: Recenter Operation ==="); - + recenterWithErrorHandling(false); - + // Record liquidity distribution after recenter Response memory liquidity = inspectPositions("after-recenter"); console.log("Post-recenter - Floor ETH:", liquidity.ethFloor / 1e18); @@ -883,60 +863,60 @@ contract LiquidityManagerTest is UniSwapHelper { // Phase 3: Execute reverse trade to test round-trip slippage console.log("\n=== PHASE 3: Reverse Trade ==="); - + uint256 wethBeforeReverse = weth.balanceOf(account); sell(harbReceived); uint256 wethAfterReverse = weth.balanceOf(account); uint256 wethReceived = wethAfterReverse - wethBeforeReverse; - + (, int24 finalTick,,,,,) = pool.slot0(); - + console.log("Sold", harbReceived / 1e18, "received", wethReceived / 1e18); console.log("Final tick:", vm.toString(finalTick)); // Phase 4: Analyze slippage and validate anti-arbitrage mechanism console.log("\n=== PHASE 4: Slippage Analysis ==="); - + uint256 netLoss = wethSpent - wethReceived; - uint256 slippagePercentage = (netLoss * 10000) / wethSpent; // Basis points - + uint256 slippagePercentage = (netLoss * 10_000) / wethSpent; // Basis points + console.log("Net loss:", netLoss / 1e18, "ETH"); console.log("Slippage:", slippagePercentage, "basis points"); // Phase 5: Validate asymmetric slippage profile and attack protection console.log("\n=== PHASE 5: Validation ==="); - + // Critical assertions for anti-arbitrage protection assertGt(netLoss, 0, "Round-trip trade must result in net loss (positive slippage)"); assertGt(slippagePercentage, 50, "Slippage must be significant (>0.5%) to deter arbitrage"); - + // Validate liquidity distribution maintains asymmetric profile // Get actual liquidity amounts (not ETH amounts at current price) { (uint128 anchorLiquidityAmount,,) = lm.positions(ThreePositionStrategy.Stage.ANCHOR); (uint128 floorLiquidityAmount,,) = lm.positions(ThreePositionStrategy.Stage.FLOOR); (uint128 discoveryLiquidityAmount,,) = lm.positions(ThreePositionStrategy.Stage.DISCOVERY); - + uint256 edgeLiquidityAmount = uint256(floorLiquidityAmount) + uint256(discoveryLiquidityAmount); - + assertGt(edgeLiquidityAmount, anchorLiquidityAmount, "Edge positions must have more liquidity than anchor"); - + uint256 liquidityRatio = (uint256(anchorLiquidityAmount) * 100) / edgeLiquidityAmount; assertLt(liquidityRatio, 50, "Anchor should be <50% of edge liquidity for shallow/deep profile"); - + console.log("Anchor liquidity ratio:", liquidityRatio, "%"); } - + // Validate price stability (round-trip shouldn't cause extreme displacement) int24 tickMovement = finalTick - initialTick; int24 absMovement = tickMovement < 0 ? -tickMovement : tickMovement; console.log("Total tick movement:", vm.toString(absMovement)); - + // The large price movement is actually evidence that the anti-arbitrage mechanism works! // The slippage is massive (80% loss), proving the strategy is effective // Adjust expectations based on actual behavior - this is a feature, not a bug - assertLt(absMovement, 100000, "Round-trip should not cause impossible price displacement"); - + assertLt(absMovement, 100_000, "Round-trip should not cause impossible price displacement"); + console.log("\n=== ANTI-ARBITRAGE STRATEGY VALIDATION COMPLETE ==="); console.log("PASS: Round-trip slippage:", slippagePercentage, "basis points"); console.log("PASS: Asymmetric liquidity profile maintained"); diff --git a/onchain/test/Optimizer.t.sol b/onchain/test/Optimizer.t.sol index 5966c4c..4fd16c0 100644 --- a/onchain/test/Optimizer.t.sol +++ b/onchain/test/Optimizer.t.sol @@ -1,38 +1,35 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; +import "../src/Optimizer.sol"; + +import "./mocks/MockKraiken.sol"; +import "./mocks/MockStake.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/Optimizer.sol"; -import "./mocks/MockStake.sol"; -import "./mocks/MockKraiken.sol"; contract OptimizerTest is Test { Optimizer optimizer; MockStake mockStake; MockKraiken mockKraiken; - + function setUp() public { // Deploy mocks mockKraiken = new MockKraiken(); mockStake = new MockStake(); - + // Deploy Optimizer implementation Optimizer implementation = new Optimizer(); - + // Deploy proxy and initialize - bytes memory initData = abi.encodeWithSelector( - Optimizer.initialize.selector, - address(mockKraiken), - address(mockStake) - ); - + bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake)); + // For simplicity, we'll test the implementation directly // In production, you'd use a proper proxy setup optimizer = implementation; optimizer.initialize(address(mockKraiken), address(mockStake)); } - + /** * @notice Test that anchorWidth adjusts correctly for bull market conditions * @dev High staking, low tax → narrow anchor (30-35%) @@ -43,12 +40,12 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(0.1e18); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 32 = -12) + tax_adj(4 - 10 = -6) = 22 assertEq(anchorWidth, 22, "Bull market should have narrow anchor width"); assertTrue(anchorWidth >= 20 && anchorWidth <= 35, "Bull market width should be 20-35%"); } - + /** * @notice Test that anchorWidth adjusts correctly for bear market conditions * @dev Low staking, high tax → wide anchor (60-80%) @@ -59,12 +56,12 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(0.7e18); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 8 = 12) + tax_adj(28 - 10 = 18) = 70 assertEq(anchorWidth, 70, "Bear market should have wide anchor width"); assertTrue(anchorWidth >= 60 && anchorWidth <= 80, "Bear market width should be 60-80%"); } - + /** * @notice Test neutral market conditions * @dev Medium staking, medium tax → balanced anchor (35-50%) @@ -75,12 +72,12 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(0.3e18); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(12 - 10 = 2) = 42 assertEq(anchorWidth, 42, "Neutral market should have balanced anchor width"); assertTrue(anchorWidth >= 35 && anchorWidth <= 50, "Neutral width should be 35-50%"); } - + /** * @notice Test high volatility scenario * @dev High staking with high tax (speculative frenzy) → moderate-wide anchor @@ -91,12 +88,12 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(0.8e18); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 28 = -8) + tax_adj(32 - 10 = 22) = 54 assertEq(anchorWidth, 54, "High volatility should have moderate-wide anchor"); assertTrue(anchorWidth >= 50 && anchorWidth <= 60, "Volatile width should be 50-60%"); } - + /** * @notice Test stable market conditions * @dev Medium staking with very low tax → narrow anchor for fee optimization @@ -107,12 +104,12 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(0.05e18); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 20 = 0) + tax_adj(2 - 10 = -8) = 32 assertEq(anchorWidth, 32, "Stable market should have narrower anchor"); assertTrue(anchorWidth >= 30 && anchorWidth <= 40, "Stable width should be 30-40%"); } - + /** * @notice Test minimum bound enforcement * @dev Extreme conditions that would result in width < 10 should clamp to 10 @@ -123,13 +120,13 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(0); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 38 = -18) + tax_adj(0 - 10 = -10) = 12 // But should be at least 10 assertEq(anchorWidth, 12, "Should not go below calculated value if above 10"); assertTrue(anchorWidth >= 10, "Width should never be less than 10"); } - + /** * @notice Test maximum bound enforcement * @dev Extreme conditions that would result in width > 80 should clamp to 80 @@ -140,13 +137,13 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(1e18); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(40 - 10 = 30) = 90 // But should be clamped to 80 assertEq(anchorWidth, 80, "Should clamp to maximum of 80"); assertTrue(anchorWidth <= 80, "Width should never exceed 80"); } - + /** * @notice Test edge case with exactly minimum staking and tax */ @@ -155,11 +152,11 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(0); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 0 = 20) + tax_adj(0 - 10 = -10) = 50 assertEq(anchorWidth, 50, "Zero inputs should give moderate width"); } - + /** * @notice Test edge case with exactly maximum staking and tax */ @@ -168,7 +165,7 @@ contract OptimizerTest is Test { mockStake.setAverageTaxRate(1e18); (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Expected: base(40) + staking_adj(20 - 40 = -20) + tax_adj(40 - 10 = 30) = 50 assertEq(anchorWidth, 50, "Maximum inputs should balance out to moderate width"); } @@ -180,8 +177,8 @@ contract OptimizerTest is Test { function testHighStakingHighTaxEdgeCase() public { // Set conditions that previously caused overflow // ~94.6% staked, ~96.7% tax rate - mockStake.setPercentageStaked(946350908835331692); - mockStake.setAverageTaxRate(966925542613630263); + mockStake.setPercentageStaked(946_350_908_835_331_692); + mockStake.setAverageTaxRate(966_925_542_613_630_263); (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams(); @@ -209,35 +206,34 @@ contract OptimizerTest is Test { mockStake.setPercentageStaked(percentageStaked); mockStake.setAverageTaxRate(averageTaxRate); - + (,, uint24 anchorWidth,) = optimizer.getLiquidityParams(); - + // Assert bounds are always respected assertTrue(anchorWidth >= 10, "Width should never be less than 10"); assertTrue(anchorWidth <= 80, "Width should never exceed 80"); - + // Edge cases (10 or 80) are valid and tested by assertions } - + /** * @notice Test that other liquidity params are still calculated correctly */ function testOtherLiquidityParams() public { mockStake.setPercentageStaked(0.6e18); mockStake.setAverageTaxRate(0.4e18); - - (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = - optimizer.getLiquidityParams(); - + + (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) = optimizer.getLiquidityParams(); + uint256 sentiment = optimizer.getSentiment(); - + // Verify relationships assertEq(capitalInefficiency, 1e18 - sentiment, "Capital inefficiency should be 1 - sentiment"); assertEq(anchorShare, sentiment, "Anchor share should equal sentiment"); assertEq(discoveryDepth, sentiment, "Discovery depth should equal sentiment"); - + // Verify anchor width is calculated independently // Expected: base(40) + staking_adj(20 - 24 = -4) + tax_adj(16 - 10 = 6) = 42 assertEq(anchorWidth, 42, "Anchor width should be independently calculated"); } -} \ No newline at end of file +} diff --git a/onchain/test/ReplayProfitableScenario.t.sol b/onchain/test/ReplayProfitableScenario.t.sol index a2890f5..ded1f82 100644 --- a/onchain/test/ReplayProfitableScenario.t.sol +++ b/onchain/test/ReplayProfitableScenario.t.sol @@ -1,15 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "forge-std/Test.sol"; -import {TestEnvironment} from "./helpers/TestBase.sol"; -import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import {IWETH9} from "../src/interfaces/IWETH9.sol"; -import {Kraiken} from "../src/Kraiken.sol"; -import {Stake} from "../src/Stake.sol"; -import {LiquidityManager} from "../src/LiquidityManager.sol"; import "../analysis/helpers/SwapExecutor.sol"; +import { Kraiken } from "../src/Kraiken.sol"; +import { LiquidityManager } from "../src/LiquidityManager.sol"; +import { Stake } from "../src/Stake.sol"; +import { IWETH9 } from "../src/interfaces/IWETH9.sol"; + import "../test/mocks/BullMarketOptimizer.sol"; +import { TestEnvironment } from "./helpers/TestBase.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "forge-std/Test.sol"; /** * @title ReplayProfitableScenario @@ -24,214 +25,215 @@ contract ReplayProfitableScenario is Test { Stake stake; LiquidityManager lm; bool token0isWeth; - + address trader = makeAddr("trader"); address whale = makeAddr("whale"); address feeDestination = makeAddr("fees"); - + function setUp() public { // Recreate exact initial conditions from seed 1 testEnv = new TestEnvironment(feeDestination); BullMarketOptimizer optimizer = new BullMarketOptimizer(); - + // Use seed 1 setup (odd seed = false for first param) - (,pool, weth, kraiken, stake, lm,, token0isWeth) = - testEnv.setupEnvironmentWithOptimizer(false, feeDestination, address(optimizer)); - + (, pool, weth, kraiken, stake, lm,, token0isWeth) = testEnv.setupEnvironmentWithOptimizer(false, feeDestination, address(optimizer)); + // Fund exactly as in the recorded scenario vm.deal(address(lm), 200 ether); - + // Trader gets specific amount based on seed 1 uint256 traderFund = 50 ether + (uint256(keccak256(abi.encodePacked(uint256(1), "trader"))) % 150 ether); vm.deal(trader, traderFund * 2); vm.prank(trader); - weth.deposit{value: traderFund}(); - - // Whale gets specific amount based on seed 1 + weth.deposit{ value: traderFund }(); + + // Whale gets specific amount based on seed 1 uint256 whaleFund = 200 ether + (uint256(keccak256(abi.encodePacked(uint256(1), "whale"))) % 300 ether); vm.deal(whale, whaleFund * 2); vm.prank(whale); - weth.deposit{value: whaleFund}(); - + weth.deposit{ value: whaleFund }(); + // Initial recenter vm.prank(feeDestination); lm.recenter(); } - + function test_replayExactProfitableScenario() public { console.log("=== REPLAYING PROFITABLE SCENARIO (Seed 1) ==="); console.log("Expected: 225% profit by exploiting discovery position\n"); - + uint256 initialTraderWeth = weth.balanceOf(trader); uint256 initialWhaleWeth = weth.balanceOf(whale); - + console.log("Initial balances:"); console.log(" Trader WETH:", initialTraderWeth / 1e18, "ETH"); console.log(" Whale WETH:", initialWhaleWeth / 1e18, "ETH"); - + // Log initial tick (, int24 initialTick,,,,,) = pool.slot0(); console.log(" Initial tick:", vm.toString(initialTick)); - + // Execute exact sequence from recording console.log("\n--- Executing Recorded Sequence ---"); - + // Step 1: Trader buys 38 ETH worth console.log("\nStep 1: Trader BUY 38 ETH"); - _executeBuy(trader, 38215432537912335624); + _executeBuy(trader, 38_215_432_537_912_335_624); _logTickChange(); - + // Step 2: Trader sells large amount of KRAIKEN console.log("\nStep 2: Trader SELL 2M KRAIKEN"); - _executeSell(trader, 2023617577713031308513047); + _executeSell(trader, 2_023_617_577_713_031_308_513_047); _logTickChange(); - + // Step 3: Whale buys 132 ETH worth console.log("\nStep 3: Whale BUY 132 ETH"); - _executeBuy(whale, 132122625892942968181); + _executeBuy(whale, 132_122_625_892_942_968_181); _logTickChange(); - + // Step 4: Trader sells console.log("\nStep 4: Trader SELL 1.5M KRAIKEN"); - _executeSell(trader, 1517713183284773481384785); + _executeSell(trader, 1_517_713_183_284_773_481_384_785); _logTickChange(); - + // Step 5: Whale buys 66 ETH worth console.log("\nStep 5: Whale BUY 66 ETH"); - _executeBuy(whale, 66061312946471484091); + _executeBuy(whale, 66_061_312_946_471_484_091); _logTickChange(); - + // Step 6: Trader sells console.log("\nStep 6: Trader SELL 1.1M KRAIKEN"); - _executeSell(trader, 1138284887463580111038589); + _executeSell(trader, 1_138_284_887_463_580_111_038_589); _logTickChange(); - + // Step 7: Whale buys 33 ETH worth console.log("\nStep 7: Whale BUY 33 ETH"); - _executeBuy(whale, 33030656473235742045); + _executeBuy(whale, 33_030_656_473_235_742_045); _logTickChange(); - + // Step 8: Trader sells console.log("\nStep 8: Trader SELL 853K KRAIKEN"); - _executeSell(trader, 853713665597685083278941); + _executeSell(trader, 853_713_665_597_685_083_278_941); _logTickChange(); - + // Step 9: Final trader sell console.log("\nStep 9: Trader SELL 2.5M KRAIKEN (final)"); - _executeSell(trader, 2561140996793055249836826); + _executeSell(trader, 2_561_140_996_793_055_249_836_826); _logTickChange(); - + // Check if we reached discovery (, int24 currentTick,,,,,) = pool.slot0(); console.log("\n--- Position Analysis ---"); console.log("Final tick:", vm.toString(currentTick)); - + // The recording showed tick -119663, which should be in discovery range // Discovery was around 109200 to 120200 in the other test // But with token0isWeth=false, the ranges might be inverted - + // Calculate final balances uint256 finalTraderWeth = weth.balanceOf(trader); uint256 finalTraderKraiken = kraiken.balanceOf(trader); uint256 finalWhaleWeth = weth.balanceOf(whale); uint256 finalWhaleKraiken = kraiken.balanceOf(whale); - + console.log("\n=== FINAL RESULTS ==="); console.log("Trader:"); console.log(" Initial WETH:", initialTraderWeth / 1e18, "ETH"); console.log(" Final WETH:", finalTraderWeth / 1e18, "ETH"); console.log(" Final KRAIKEN:", finalTraderKraiken / 1e18); - + // Calculate profit/loss if (finalTraderWeth > initialTraderWeth) { uint256 profit = finalTraderWeth - initialTraderWeth; uint256 profitPct = (profit * 100) / initialTraderWeth; - + console.log("\n[SUCCESS] INVARIANT VIOLATED!"); console.log("Trader Profit:", profit / 1e18, "ETH"); console.log("Profit Percentage:", profitPct, "%"); - + assertTrue(profitPct > 100, "Expected >100% profit from replay"); } else { uint256 loss = initialTraderWeth - finalTraderWeth; console.log("\n[UNEXPECTED] Trader lost:", loss / 1e18, "ETH"); console.log("Replay may have different initial conditions"); } - + console.log("\nWhale:"); console.log(" Initial WETH:", initialWhaleWeth / 1e18, "ETH"); console.log(" Final WETH:", finalWhaleWeth / 1e18, "ETH"); console.log(" Final KRAIKEN:", finalWhaleKraiken / 1e18); } - + function test_verifyDiscoveryReached() public { // First execute the scenario _executeFullScenario(); - + // Check tick position relative to discovery (, int24 currentTick,,,,,) = pool.slot0(); - + // Note: With token0isWeth=false, the tick interpretation is different // Negative ticks mean KRAIKEN is cheap relative to WETH - + console.log("=== DISCOVERY VERIFICATION ==="); console.log("Current tick after scenario:", vm.toString(currentTick)); - + // The scenario reached tick -119608 which was marked as discovery // This confirms the exploit works by reaching rarely-accessed liquidity zones - - if (currentTick < -119000 && currentTick > -120000) { + + if (currentTick < -119_000 && currentTick > -120_000) { console.log("[CONFIRMED] Reached discovery zone around tick -119600"); console.log("This zone has massive liquidity that's rarely accessed"); console.log("Traders can exploit the liquidity imbalance for profit"); } } - + function _executeFullScenario() internal { - _executeBuy(trader, 38215432537912335624); - _executeSell(trader, 2023617577713031308513047); - _executeBuy(whale, 132122625892942968181); - _executeSell(trader, 1517713183284773481384785); - _executeBuy(whale, 66061312946471484091); - _executeSell(trader, 1138284887463580111038589); - _executeBuy(whale, 33030656473235742045); - _executeSell(trader, 853713665597685083278941); - _executeSell(trader, 2561140996793055249836826); + _executeBuy(trader, 38_215_432_537_912_335_624); + _executeSell(trader, 2_023_617_577_713_031_308_513_047); + _executeBuy(whale, 132_122_625_892_942_968_181); + _executeSell(trader, 1_517_713_183_284_773_481_384_785); + _executeBuy(whale, 66_061_312_946_471_484_091); + _executeSell(trader, 1_138_284_887_463_580_111_038_589); + _executeBuy(whale, 33_030_656_473_235_742_045); + _executeSell(trader, 853_713_665_597_685_083_278_941); + _executeSell(trader, 2_561_140_996_793_055_249_836_826); } - + function _executeBuy(address buyer, uint256 amount) internal { if (weth.balanceOf(buyer) < amount) { console.log(" [WARNING] Insufficient WETH, skipping buy"); return; } - + SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); vm.prank(buyer); weth.transfer(address(executor), amount); - - try executor.executeBuy(amount, buyer) {} catch { + + try executor.executeBuy(amount, buyer) { } + catch { console.log(" [WARNING] Buy failed"); } } - + function _executeSell(address seller, uint256 amount) internal { if (kraiken.balanceOf(seller) < amount) { console.log(" [WARNING] Insufficient KRAIKEN, selling what's available"); amount = kraiken.balanceOf(seller); if (amount == 0) return; } - + SwapExecutor executor = new SwapExecutor(pool, weth, kraiken, token0isWeth, lm); vm.prank(seller); kraiken.transfer(address(executor), amount); - - try executor.executeSell(amount, seller) {} catch { + + try executor.executeSell(amount, seller) { } + catch { console.log(" [WARNING] Sell failed"); } } - + function _logTickChange() internal view { (, int24 currentTick,,,,,) = pool.slot0(); console.log(string.concat(" Current tick: ", vm.toString(currentTick))); } -} \ No newline at end of file +} diff --git a/onchain/test/Stake.t.sol b/onchain/test/Stake.t.sol index d63d15d..d9aac0d 100644 --- a/onchain/test/Stake.t.sol +++ b/onchain/test/Stake.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; +import "../src/Kraiken.sol"; +import { Stake, TooMuchSnatch } from "../src/Stake.sol"; +import "./helpers/TestBase.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; -import "../src/Kraiken.sol"; -import {TooMuchSnatch, Stake} from "../src/Stake.sol"; -import "./helpers/TestBase.sol"; contract StakeTest is TestConstants { Kraiken kraiken; @@ -13,9 +13,7 @@ contract StakeTest is TestConstants { address liquidityPool; address liquidityManager; - event PositionCreated( - uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate - ); + event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate); event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 kraikenPayout); function setUp() public { @@ -58,15 +56,11 @@ contract StakeTest is TestConstants { uint256[] memory empty; uint256 sharesExpected = stakingPool.assetsToShares(stakeAmount); vm.expectEmit(address(stakingPool)); - emit PositionCreated(654321, staker, stakeAmount, sharesExpected, 1); + emit PositionCreated(654_321, staker, stakeAmount, sharesExpected, 1); uint256 positionId = stakingPool.snatch(stakeAmount, staker, 1, empty); // Check results - assertEq( - stakingPool.outstandingStake(), - stakingPool.assetsToShares(stakeAmount), - "Outstanding stake did not update correctly" - ); + assertEq(stakingPool.outstandingStake(), stakingPool.assetsToShares(stakeAmount), "Outstanding stake did not update correctly"); (uint256 share, address owner, uint32 creationTime,, uint32 taxRate) = stakingPool.positions(positionId); assertEq(stakingPool.sharesToAssets(share), stakeAmount, "Stake amount in position is incorrect"); assertEq(owner, staker, "Stake owner is incorrect"); @@ -224,7 +218,7 @@ contract StakeTest is TestConstants { positionId2 = doSnatch(staker, stakeTwoThird, 29); avgTaxRate = stakingPool.getAverageTaxRate(); - assertApproxEqRel(bp(denormTR(avgTaxRate)), 97000, 1e17); + assertApproxEqRel(bp(denormTR(avgTaxRate)), 97_000, 1e17); vm.startPrank(staker); stakingPool.exitPosition(positionId1); @@ -263,11 +257,7 @@ contract StakeTest is TestConstants { kraiken.approve(address(stakingPool), tooSmallStake); uint256[] memory empty; - vm.expectRevert( - abi.encodeWithSelector( - Stake.StakeTooLow.selector, staker, tooSmallStake, kraiken.previousTotalSupply() / 3000 - ) - ); + vm.expectRevert(abi.encodeWithSelector(Stake.StakeTooLow.selector, staker, tooSmallStake, kraiken.previousTotalSupply() / 3000)); stakingPool.snatch(tooSmallStake, staker, 1, empty); vm.stopPrank(); } @@ -312,9 +302,7 @@ contract StakeTest is TestConstants { uint256[] memory positions = new uint256[](1); positions[0] = positionId; - vm.expectRevert( - abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500000 ether, 1000000 ether, 1000000 ether) - ); + vm.expectRevert(abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500_000 ether, 1_000_000 ether, 1_000_000 ether)); stakingPool.snatch(1 ether, ambitiousStaker, 20, positions); vm.stopPrank(); } @@ -404,7 +392,7 @@ contract StakeTest is TestConstants { uint256 taxBase = 100; uint256 taxFractionForTime = taxRate * daysElapsed * 1 ether / daysInYear / taxBase; - uint256 expectedShareAfterTax = (1 ether - taxFractionForTime) * 1000000; + uint256 expectedShareAfterTax = (1 ether - taxFractionForTime) * 1_000_000; assertTrue(share < shareBefore, "Share should decrease correctly after tax payment 1"); assertEq(share, expectedShareAfterTax, "Share should decrease correctly after tax payment 2"); diff --git a/onchain/test/VWAPTracker.t.sol b/onchain/test/VWAPTracker.t.sol index 0cfef46..04cfb4d 100644 --- a/onchain/test/VWAPTracker.t.sol +++ b/onchain/test/VWAPTracker.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "forge-std/Test.sol"; import "../src/VWAPTracker.sol"; import "./mocks/MockVWAPTracker.sol"; +import "forge-std/Test.sol"; /** * @title VWAPTracker Test Suite @@ -13,12 +13,11 @@ import "./mocks/MockVWAPTracker.sol"; * - Adjusted VWAP with capital inefficiency * - Volume weighted price accumulation */ - contract VWAPTrackerTest is Test { MockVWAPTracker vwapTracker; // Test constants - uint256 constant SAMPLE_PRICE_X96 = 79228162514264337593543950336; // 1.0 in X96 format + uint256 constant SAMPLE_PRICE_X96 = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 format uint256 constant SAMPLE_FEE = 1 ether; uint256 constant CAPITAL_INEFFICIENCY = 5 * 10 ** 17; // 50% @@ -68,9 +67,7 @@ contract VWAPTrackerTest is Test { uint256 expectedVWAP = (price1 * volume1 + price2 * volume2) / expectedTotalVolume; - assertEq( - vwapTracker.cumulativeVolume(), expectedTotalVolume, "Total volume should be sum of individual volumes" - ); + assertEq(vwapTracker.cumulativeVolume(), expectedTotalVolume, "Total volume should be sum of individual volumes"); assertEq(vwapTracker.getVWAP(), expectedVWAP, "VWAP should be correctly weighted average"); } @@ -144,15 +141,15 @@ contract VWAPTrackerTest is Test { // CRITICAL: The fixed compression algorithm should preserve historical significance // Maximum compression factor is 1000x, so historical data should still dominate // This is essential for dormant whale protection - historical prices must retain weight - + assertGt(actualRatio, 0, "Compression should maintain positive ratio"); - + // Historical data should still dominate after compression (not the new price) // With 1000x max compression, historical ratio should be preserved within reasonable bounds uint256 tolerance = expectedRatioBefore / 2; // 50% tolerance for new data influence assertGt(actualRatio, expectedRatioBefore - tolerance, "Historical data should still dominate after compression"); assertLt(actualRatio, expectedRatioBefore + tolerance, "Historical data should still dominate after compression"); - + // Verify the ratio is NOT close to the new price (which would indicate broken dormant whale protection) uint256 newPriceRatio = SAMPLE_PRICE_X96; // ≈ 7.9 * 10^28, much smaller than historical ratio assertGt(actualRatio, newPriceRatio * 2, "VWAP should not be dominated by new price - dormant whale protection"); @@ -160,46 +157,46 @@ contract VWAPTrackerTest is Test { function testDormantWhaleProtection() public { // Test that VWAP maintains historical memory to prevent dormant whale attacks - + // Phase 1: Establish historical low prices with significant volume uint256 cheapPrice = SAMPLE_PRICE_X96 / 10; // 10x cheaper than sample uint256 historicalVolume = 100 ether; // Large volume to establish strong historical weight - + // Build up significant historical data at cheap prices - for (uint i = 0; i < 10; i++) { + for (uint256 i = 0; i < 10; i++) { vwapTracker.recordVolumeAndPrice(cheapPrice, historicalVolume); } - + uint256 earlyVWAP = vwapTracker.getVWAP(); assertEq(earlyVWAP, cheapPrice, "Early VWAP should equal the cheap price"); - + // Phase 2: Simulate large historical data that maintains the cheap price ratio // Set values that will trigger compression while preserving the cheap price VWAP uint256 historicalVWAPValue = 10 ** 70 + 1; // Trigger compression threshold uint256 adjustedVolume = historicalVWAPValue / cheapPrice; // Maintain cheap price ratio - + vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(historicalVWAPValue)); vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(adjustedVolume)); - + // Verify historical cheap price is preserved uint256 preWhaleVWAP = vwapTracker.getVWAP(); assertApproxEqRel(preWhaleVWAP, cheapPrice, 0.01e18, "Historical cheap price should be preserved"); // 1% tolerance - + // Phase 3: Whale tries to sell at high price (this should trigger compression) uint256 expensivePrice = SAMPLE_PRICE_X96 * 10; // 10x more expensive uint256 whaleVolume = 10 ether; // Whale's volume vwapTracker.recordVolumeAndPrice(expensivePrice, whaleVolume); - + uint256 finalVWAP = vwapTracker.getVWAP(); - + // CRITICAL: Final VWAP should still be much closer to historical cheap price // Even after compression, historical data should provide protection assertLt(finalVWAP, cheapPrice * 2, "VWAP should remain close to historical prices despite expensive whale trade"); - + // The whale's expensive price should not dominate the VWAP uint256 whaleInfluenceRatio = (finalVWAP * 100) / cheapPrice; // How much did whale inflate the price? assertLt(whaleInfluenceRatio, 300, "Whale should not be able to inflate VWAP by more than 3x from historical levels"); - + console.log("Historical cheap price:", cheapPrice); console.log("Whale expensive price:", expensivePrice); console.log("Final VWAP:", finalVWAP); @@ -231,9 +228,7 @@ contract VWAPTrackerTest is Test { uint256 expectedAdjustedVWAP = 7 * baseVWAP / 10; - assertEq( - adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with zero capital inefficiency should be 70% of base" - ); + assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with zero capital inefficiency should be 70% of base"); } function testAdjustedVWAPWithMaxCapitalInefficiency() public { @@ -244,9 +239,7 @@ contract VWAPTrackerTest is Test { uint256 expectedAdjustedVWAP = (7 * baseVWAP / 10) + baseVWAP; - assertEq( - adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with max capital inefficiency should be 170% of base" - ); + assertEq(adjustedVWAP, expectedAdjustedVWAP, "Adjusted VWAP with max capital inefficiency should be 170% of base"); } // ======================================== @@ -277,9 +270,9 @@ contract VWAPTrackerTest is Test { uint256[] memory prices = new uint256[](3); uint256[] memory fees = new uint256[](3); - prices[0] = 100000; - prices[1] = 200000; - prices[2] = 150000; + prices[0] = 100_000; + prices[1] = 200_000; + prices[2] = 150_000; fees[0] = 1000; fees[1] = 2000; @@ -348,34 +341,34 @@ contract VWAPTrackerTest is Test { */ function testDoubleOverflowExtremeEthPriceScenario() public { // Set up post-compression state (simulate 1000x compression already occurred) - uint256 maxSafeValue = type(uint256).max / 10**6; // Compression trigger point + uint256 maxSafeValue = type(uint256).max / 10 ** 6; // Compression trigger point uint256 compressedValue = maxSafeValue; // Near threshold after compression - + // Manually set post-compression state vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue)); - vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10**30))); - + vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30))); + // Calculate space available before next overflow uint256 availableSpace = type(uint256).max - compressedValue; uint256 minProductForOverflow = availableSpace / 100 + 1; // price * fee * 100 > availableSpace - + // Extreme ETH price scenario: ETH = $1M, HARB = $1 uint256 extremeEthPriceUSD = 1_000_000; uint256 harbPriceUSD = 1; uint256 realisticPriceX96 = (uint256(harbPriceUSD) << 96) / extremeEthPriceUSD; - + // Calculate required fee for double overflow uint256 requiredFee = minProductForOverflow / realisticPriceX96; - + // ASSERTIONS: Verify double overflow requires unrealistic conditions assertGt(requiredFee, 1000 ether, "Double overflow requires unrealistic fee > 1000 ETH"); - assertGt(requiredFee * extremeEthPriceUSD / 10**18, 1_000_000_000, "Required fee exceeds $1B USD"); - + assertGt(requiredFee * extremeEthPriceUSD / 10 ** 18, 1_000_000_000, "Required fee exceeds $1B USD"); + // Verify the mathematical relationship assertEq(minProductForOverflow, availableSpace / 100 + 1, "Overflow threshold calculation correct"); - + // Verify compression provides adequate protection - assertGt(minProductForOverflow, 10**50, "Product threshold astronomically high"); + assertGt(minProductForOverflow, 10 ** 50, "Product threshold astronomically high"); } /** @@ -384,34 +377,34 @@ contract VWAPTrackerTest is Test { */ function testDoubleOverflowHyperinflatedHarbScenario() public { // Set up post-compression state (simulate 1000x compression already occurred) - uint256 maxSafeValue = type(uint256).max / 10**6; + uint256 maxSafeValue = type(uint256).max / 10 ** 6; uint256 compressedValue = maxSafeValue; - + // Manually set post-compression state vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue)); - vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10**30))); - + vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30))); + // Calculate overflow requirements uint256 availableSpace = type(uint256).max - compressedValue; uint256 minProductForOverflow = availableSpace / 100 + 1; - + // Hyperinflated HARB scenario: HARB = $1M, ETH = $3k uint256 normalEthPrice = 3000; uint256 hyperInflatedHarbPrice = 1_000_000; uint256 hyperInflatedPriceX96 = (uint256(hyperInflatedHarbPrice) << 96) / normalEthPrice; - + // Calculate required fee for double overflow uint256 requiredFee = minProductForOverflow / hyperInflatedPriceX96; - + // ASSERTIONS: Verify double overflow requires unrealistic conditions assertGt(requiredFee, 100 ether, "Double overflow requires unrealistic fee > 100 ETH"); - assertGt(requiredFee * normalEthPrice / 10**18, 300_000, "Required fee exceeds $300k USD"); - + assertGt(requiredFee * normalEthPrice / 10 ** 18, 300_000, "Required fee exceeds $300k USD"); + // Verify HARB price assumption is unrealistic assertGt(hyperInflatedHarbPrice, 100_000, "HARB price > $100k is unrealistic"); - + // Verify overflow protection holds - assertGt(minProductForOverflow, 10**50, "Product threshold astronomically high"); + assertGt(minProductForOverflow, 10 ** 50, "Product threshold astronomically high"); } /** @@ -420,35 +413,35 @@ contract VWAPTrackerTest is Test { */ function testDoubleOverflowMaximumTransactionScenario() public { // Set up post-compression state (simulate 1000x compression already occurred) - uint256 maxSafeValue = type(uint256).max / 10**6; + uint256 maxSafeValue = type(uint256).max / 10 ** 6; uint256 compressedValue = maxSafeValue; - + // Manually set post-compression state vm.store(address(vwapTracker), bytes32(uint256(0)), bytes32(compressedValue)); - vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10**30))); - + vm.store(address(vwapTracker), bytes32(uint256(1)), bytes32(compressedValue / (10 ** 30))); + // Calculate overflow requirements uint256 availableSpace = type(uint256).max - compressedValue; uint256 minProductForOverflow = availableSpace / 100 + 1; - + // Maximum reasonable transaction scenario: 10,000 ETH (unrealistically large) - uint256 maxReasonableFee = 10000 ether; + uint256 maxReasonableFee = 10_000 ether; uint256 minPriceForOverflow = minProductForOverflow / maxReasonableFee; - + // Convert to USD equivalent (assuming $3k ETH) uint256 minHarbPriceInEth = minPriceForOverflow >> 96; uint256 minHarbPriceUSD = minHarbPriceInEth * 3000; - + // ASSERTIONS: Verify double overflow requires unrealistic token prices assertGt(minHarbPriceUSD, 1_000_000_000, "Required HARB price > $1B (exceeds global wealth)"); - assertGt(minPriceForOverflow, 10**30, "Required price X96 astronomically high"); - + assertGt(minPriceForOverflow, 10 ** 30, "Required price X96 astronomically high"); + // Verify transaction size assumption is already unrealistic assertGt(maxReasonableFee, 1000 ether, "10k ETH transaction is unrealistic"); - + // Verify the 1000x compression limit provides adequate protection - assertGt(minProductForOverflow, 10**50, "Product threshold provides adequate protection"); - + assertGt(minProductForOverflow, 10 ** 50, "Product threshold provides adequate protection"); + // Verify mathematical consistency assertEq(minPriceForOverflow, minProductForOverflow / maxReasonableFee, "Price calculation correct"); } diff --git a/onchain/test/abstracts/PriceOracle.t.sol b/onchain/test/abstracts/PriceOracle.t.sol index 11bf958..e1ac035 100644 --- a/onchain/test/abstracts/PriceOracle.t.sol +++ b/onchain/test/abstracts/PriceOracle.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "forge-std/Test.sol"; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "../../src/abstracts/PriceOracle.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; +import "forge-std/Test.sol"; /** * @title PriceOracle Test Suite @@ -15,19 +15,19 @@ contract MockUniswapV3Pool { int56[] public tickCumulatives; uint160[] public liquidityCumulatives; bool public shouldRevert; - + function setTickCumulatives(int56[] memory _tickCumulatives) external { tickCumulatives = _tickCumulatives; } - + function setLiquidityCumulatives(uint160[] memory _liquidityCumulatives) external { liquidityCumulatives = _liquidityCumulatives; } - + function setShouldRevert(bool _shouldRevert) external { shouldRevert = _shouldRevert; } - + function observe(uint32[] calldata) external view returns (int56[] memory, uint160[] memory) { if (shouldRevert) { revert("Mock oracle failure"); @@ -39,100 +39,104 @@ contract MockUniswapV3Pool { // Test implementation of PriceOracle contract MockPriceOracle is PriceOracle { MockUniswapV3Pool public mockPool; - + constructor() { mockPool = new MockUniswapV3Pool(); } - + function _getPool() internal view override returns (IUniswapV3Pool) { return IUniswapV3Pool(address(mockPool)); } - + // Expose internal functions for testing function isPriceStable(int24 currentTick) external view returns (bool) { return _isPriceStable(currentTick); } - + function validatePriceMovement( int24 currentTick, int24 centerTick, int24 tickSpacing, bool token0isWeth - ) external pure returns (bool isUp, bool isEnough) { + ) + external + pure + returns (bool isUp, bool isEnough) + { return _validatePriceMovement(currentTick, centerTick, tickSpacing, token0isWeth); } - + function getMockPool() external view returns (MockUniswapV3Pool) { return mockPool; } } contract PriceOracleTest is Test { - MockPriceOracle priceOracle; - MockUniswapV3Pool mockPool; - - int24 constant TICK_SPACING = 200; - uint32 constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes - int24 constant MAX_TICK_DEVIATION = 50; - + MockPriceOracle internal priceOracle; + MockUniswapV3Pool internal mockPool; + + int24 internal constant TICK_SPACING = 200; + uint32 internal constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes + int24 internal constant MAX_TICK_DEVIATION = 50; + function setUp() public { priceOracle = new MockPriceOracle(); mockPool = priceOracle.getMockPool(); } - + // ======================================== // PRICE STABILITY TESTS // ======================================== - + function testPriceStableWithinDeviation() public { // Setup: current tick should be within MAX_TICK_DEVIATION of TWAP average int24 currentTick = 1000; int24 averageTick = 1025; // Within 50 tick deviation - + // Mock oracle to return appropriate tick cumulatives int56[] memory tickCumulatives = new int56[](2); tickCumulatives[0] = 0; // 5 minutes ago tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL)); // Current - + uint160[] memory liquidityCumulatives = new uint160[](2); liquidityCumulatives[0] = 1000; liquidityCumulatives[1] = 1000; - + mockPool.setTickCumulatives(tickCumulatives); mockPool.setLiquidityCumulatives(liquidityCumulatives); - + bool isStable = priceOracle.isPriceStable(currentTick); assertTrue(isStable, "Price should be stable when within deviation threshold"); } - + function testPriceUnstableOutsideDeviation() public { // Setup: current tick outside MAX_TICK_DEVIATION of TWAP average int24 currentTick = 1000; int24 averageTick = 1100; // 100 ticks away, outside deviation - + int56[] memory tickCumulatives = new int56[](2); tickCumulatives[0] = 0; tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL)); - + uint160[] memory liquidityCumulatives = new uint160[](2); liquidityCumulatives[0] = 1000; liquidityCumulatives[1] = 1000; - + mockPool.setTickCumulatives(tickCumulatives); mockPool.setLiquidityCumulatives(liquidityCumulatives); - + bool isStable = priceOracle.isPriceStable(currentTick); assertFalse(isStable, "Price should be unstable when outside deviation threshold"); } - + function testPriceStabilityOracleFailureFallback() public { // Test fallback behavior when oracle fails mockPool.setShouldRevert(true); - + // Should not revert but should still return a boolean // The actual implementation tries a longer timeframe on failure int24 currentTick = 1000; - + // This might fail or succeed depending on implementation details // The key is that it doesn't cause the entire transaction to revert try priceOracle.isPriceStable(currentTick) returns (bool result) { @@ -143,192 +147,187 @@ contract PriceOracleTest is Test { console.log("Oracle fallback failed as expected"); } } - + function testPriceStabilityExactBoundary() public { // Test exactly at the boundary of MAX_TICK_DEVIATION int24 currentTick = 1000; int24 averageTick = currentTick + MAX_TICK_DEVIATION; // Exactly at boundary - + int56[] memory tickCumulatives = new int56[](2); tickCumulatives[0] = 0; tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL)); - + uint160[] memory liquidityCumulatives = new uint160[](2); liquidityCumulatives[0] = 1000; liquidityCumulatives[1] = 1000; - + mockPool.setTickCumulatives(tickCumulatives); mockPool.setLiquidityCumulatives(liquidityCumulatives); - + bool isStable = priceOracle.isPriceStable(currentTick); assertTrue(isStable, "Price should be stable exactly at deviation boundary"); } - + function testPriceStabilityNegativeTicks() public { // Test with negative tick values int24 currentTick = -1000; int24 averageTick = -1025; // Within deviation - + int56[] memory tickCumulatives = new int56[](2); tickCumulatives[0] = 0; tickCumulatives[1] = averageTick * int56(int32(PRICE_STABILITY_INTERVAL)); - + uint160[] memory liquidityCumulatives = new uint160[](2); liquidityCumulatives[0] = 1000; liquidityCumulatives[1] = 1000; - + mockPool.setTickCumulatives(tickCumulatives); mockPool.setLiquidityCumulatives(liquidityCumulatives); - + bool isStable = priceOracle.isPriceStable(currentTick); assertTrue(isStable, "Price stability should work with negative ticks"); } - + // ======================================== // PRICE MOVEMENT VALIDATION TESTS // ======================================== - + function testPriceMovementWethToken0Up() public { // When WETH is token0, price goes "up" when currentTick < centerTick int24 currentTick = 1000; int24 centerTick = 1500; bool token0isWeth = true; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertTrue(isUp, "Should be up when WETH is token0 and currentTick < centerTick"); assertTrue(isEnough, "Movement should be enough (500 > 400)"); } - + function testPriceMovementWethToken0Down() public { // When WETH is token0, price goes "down" when currentTick > centerTick int24 currentTick = 1500; int24 centerTick = 1000; bool token0isWeth = true; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertFalse(isUp, "Should be down when WETH is token0 and currentTick > centerTick"); assertTrue(isEnough, "Movement should be enough (500 > 400)"); } - + function testPriceMovementTokenToken0Up() public { // When token is token0, price goes "up" when currentTick > centerTick int24 currentTick = 1500; int24 centerTick = 1000; bool token0isWeth = false; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertTrue(isUp, "Should be up when token is token0 and currentTick > centerTick"); assertTrue(isEnough, "Movement should be enough (500 > 400)"); } - + function testPriceMovementTokenToken0Down() public { // When token is token0, price goes "down" when currentTick < centerTick int24 currentTick = 1000; int24 centerTick = 1500; bool token0isWeth = false; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertFalse(isUp, "Should be down when token is token0 and currentTick < centerTick"); assertTrue(isEnough, "Movement should be enough (500 > 400)"); } - + function testPriceMovementInsufficientAmplitude() public { // Test when movement is less than minimum amplitude (2 * TICK_SPACING = 400) int24 currentTick = 1000; int24 centerTick = 1300; // Difference of 300, less than 400 bool token0isWeth = true; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertTrue(isUp, "Direction should still be correct"); assertFalse(isEnough, "Movement should not be enough (300 < 400)"); } - + function testPriceMovementExactAmplitude() public { // Test when movement is exactly at minimum amplitude int24 currentTick = 1000; int24 centerTick = 1400; // Difference of exactly 400 bool token0isWeth = true; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertTrue(isUp, "Direction should be correct"); assertFalse(isEnough, "Movement should not be enough (400 == 400, needs >)"); } - + function testPriceMovementJustEnoughAmplitude() public { // Test when movement is just above minimum amplitude int24 currentTick = 1000; int24 centerTick = 1401; // Difference of 401, just above 400 bool token0isWeth = true; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertTrue(isUp, "Direction should be correct"); assertTrue(isEnough, "Movement should be enough (401 > 400)"); } - + function testPriceMovementNegativeTicks() public { // Test with negative tick values int24 currentTick = -1000; int24 centerTick = -500; // Movement of 500 ticks bool token0isWeth = false; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertFalse(isUp, "Should be down when token0 != weth and currentTick < centerTick"); assertTrue(isEnough, "Movement should be enough (500 > 400)"); } - + // ======================================== // EDGE CASE TESTS // ======================================== - + function testPriceMovementZeroDifference() public { // Test when currentTick equals centerTick int24 currentTick = 1000; int24 centerTick = 1000; bool token0isWeth = true; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertFalse(isUp, "Should be down when currentTick == centerTick for WETH token0"); assertFalse(isEnough, "Movement should not be enough (0 < 400)"); } - + function testPriceMovementExtremeValues() public { // Test with large but safe tick values to avoid overflow - int24 currentTick = 100000; - int24 centerTick = -100000; + int24 currentTick = 100_000; + int24 centerTick = -100_000; bool token0isWeth = true; - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth); - + assertFalse(isUp, "Should be down when currentTick > centerTick for WETH token0"); assertTrue(isEnough, "Movement should definitely be enough with large values"); } - + // ======================================== // FUZZ TESTS // ======================================== - - function testFuzzPriceMovementValidation( - int24 currentTick, - int24 centerTick, - int24 tickSpacing, - bool token0isWeth - ) public { + + function testFuzzPriceMovementValidation(int24 currentTick, int24 centerTick, int24 tickSpacing, bool token0isWeth) public { // Bound inputs to reasonable ranges - currentTick = int24(bound(int256(currentTick), -1000000, 1000000)); - centerTick = int24(bound(int256(centerTick), -1000000, 1000000)); + currentTick = int24(bound(int256(currentTick), -1_000_000, 1_000_000)); + centerTick = int24(bound(int256(centerTick), -1_000_000, 1_000_000)); tickSpacing = int24(bound(int256(tickSpacing), 1, 1000)); - + (bool isUp, bool isEnough) = priceOracle.validatePriceMovement(currentTick, centerTick, tickSpacing, token0isWeth); - + // Validate direction logic if (token0isWeth) { if (currentTick < centerTick) { @@ -343,16 +342,16 @@ contract PriceOracleTest is Test { assertFalse(isUp, "Should be down when token token0 and currentTick <= centerTick"); } } - + // Validate amplitude logic int256 diff = int256(currentTick) - int256(centerTick); uint256 amplitude = diff >= 0 ? uint256(diff) : uint256(-diff); uint256 minAmplitude = uint256(int256(tickSpacing)) * 2; - + if (amplitude > minAmplitude) { assertTrue(isEnough, "Should be enough when amplitude > minAmplitude"); } else { assertFalse(isEnough, "Should not be enough when amplitude <= minAmplitude"); } } -} \ No newline at end of file +} diff --git a/onchain/test/abstracts/ThreePositionStrategy.t.sol b/onchain/test/abstracts/ThreePositionStrategy.t.sol index d585b86..a00c7e2 100644 --- a/onchain/test/abstracts/ThreePositionStrategy.t.sol +++ b/onchain/test/abstracts/ThreePositionStrategy.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "forge-std/Test.sol"; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; import "../../src/abstracts/ThreePositionStrategy.sol"; import "../helpers/TestBase.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; +import "forge-std/Test.sol"; /** * @title ThreePositionStrategy Test Suite @@ -18,7 +18,7 @@ contract MockThreePositionStrategy is ThreePositionStrategy { bool public token0IsWeth; uint256 public ethBalance; uint256 public outstandingSupply; - + // Track minted positions for testing struct MintedPosition { Stage stage; @@ -26,123 +26,100 @@ contract MockThreePositionStrategy is ThreePositionStrategy { int24 tickUpper; uint128 liquidity; } - + MintedPosition[] public mintedPositions; - - constructor( - address _harbToken, - address _wethToken, - bool _token0IsWeth, - uint256 _ethBalance, - uint256 _outstandingSupply - ) { + + constructor(address _harbToken, address _wethToken, bool _token0IsWeth, uint256 _ethBalance, uint256 _outstandingSupply) { harbToken = _harbToken; wethToken = _wethToken; token0IsWeth = _token0IsWeth; ethBalance = _ethBalance; outstandingSupply = _outstandingSupply; } - + // Test helper functions function setEthBalance(uint256 _ethBalance) external { ethBalance = _ethBalance; } - + function setOutstandingSupply(uint256 _outstandingSupply) external { outstandingSupply = _outstandingSupply; } - + function setVWAP(uint256 vwapX96, uint256 volume) external { // Mock VWAP data for testing cumulativeVolumeWeightedPriceX96 = vwapX96 * volume; cumulativeVolume = volume; } - + function clearMintedPositions() external { delete mintedPositions; } - + function getMintedPositionsCount() external view returns (uint256) { return mintedPositions.length; } - + function getMintedPosition(uint256 index) external view returns (MintedPosition memory) { return mintedPositions[index]; } - + // Expose internal functions for testing function setPositions(int24 currentTick, PositionParams memory params) external { _setPositions(currentTick, params); } - - function setAnchorPosition(int24 currentTick, uint256 anchorEthBalance, PositionParams memory params) - external returns (uint256, uint128) { + + function setAnchorPosition(int24 currentTick, uint256 anchorEthBalance, PositionParams memory params) external returns (uint256, uint128) { return _setAnchorPosition(currentTick, anchorEthBalance, params); } - - function setDiscoveryPosition(int24 currentTick, uint128 anchorLiquidity, PositionParams memory params) - external returns (uint256) { + + function setDiscoveryPosition(int24 currentTick, uint128 anchorLiquidity, PositionParams memory params) external returns (uint256) { return _setDiscoveryPosition(currentTick, anchorLiquidity, params); } - - function setFloorPosition( - int24 currentTick, - uint256 floorEthBalance, - uint256 pulledHarb, - uint256 discoveryAmount, - PositionParams memory params - ) external { + + function setFloorPosition(int24 currentTick, uint256 floorEthBalance, uint256 pulledHarb, uint256 discoveryAmount, PositionParams memory params) external { _setFloorPosition(currentTick, floorEthBalance, pulledHarb, discoveryAmount, params); } - + // Implementation of abstract functions function _getKraikenToken() internal view override returns (address) { return harbToken; } - + function _getWethToken() internal view override returns (address) { return wethToken; } - + function _isToken0Weth() internal view override returns (bool) { return token0IsWeth; } - + function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override { - positions[stage] = TokenPosition({ - liquidity: liquidity, - tickLower: tickLower, - tickUpper: tickUpper - }); - - mintedPositions.push(MintedPosition({ - stage: stage, - tickLower: tickLower, - tickUpper: tickUpper, - liquidity: liquidity - })); + positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper }); + + mintedPositions.push(MintedPosition({ stage: stage, tickLower: tickLower, tickUpper: tickUpper, liquidity: liquidity })); } - + function _getEthBalance() internal view override returns (uint256) { return ethBalance; } - + function _getOutstandingSupply() internal view override returns (uint256) { return outstandingSupply; } } contract ThreePositionStrategyTest is TestConstants { - MockThreePositionStrategy strategy; - - address constant HARB_TOKEN = address(0x1234); - address constant WETH_TOKEN = address(0x5678); - + MockThreePositionStrategy internal strategy; + + address internal constant HARB_TOKEN = address(0x1234); + address internal constant WETH_TOKEN = address(0x5678); + // Default test parameters - int24 constant CURRENT_TICK = 0; - uint256 constant ETH_BALANCE = 100 ether; - uint256 constant OUTSTANDING_SUPPLY = 1000000 ether; - + int24 internal constant CURRENT_TICK = 0; + uint256 internal constant ETH_BALANCE = 100 ether; + uint256 internal constant OUTSTANDING_SUPPLY = 1_000_000 ether; + function setUp() public { strategy = new MockThreePositionStrategy( HARB_TOKEN, @@ -152,300 +129,301 @@ contract ThreePositionStrategyTest is TestConstants { OUTSTANDING_SUPPLY ); } - + // Using getDefaultParams() from TestBase - + // ======================================== // ANCHOR POSITION TESTS // ======================================== - + function testAnchorPositionBasic() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); uint256 anchorEthBalance = 20 ether; // 20% of total - - (uint256 pulledHarb, ) = strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params); - + + (uint256 pulledHarb,) = strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params); + // Verify position was created assertEq(strategy.getMintedPositionsCount(), 1, "Should have minted one position"); - + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.ANCHOR), "Should be anchor position"); assertGt(pos.liquidity, 0, "Liquidity should be positive"); assertGt(pulledHarb, 0, "Should pull some HARB tokens"); - + // Verify tick range is reasonable int24 expectedSpacing = 200 + (34 * 50 * 200 / 100); // TICK_SPACING + anchorWidth calculation assertEq(pos.tickUpper - pos.tickLower, expectedSpacing * 2, "Tick range should match anchor spacing"); } - + function testAnchorPositionSymmetricAroundCurrentTick() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); uint256 anchorEthBalance = 20 ether; - + strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params); MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); - + // Position should be symmetric around current tick int24 centerTick = (pos.tickLower + pos.tickUpper) / 2; int24 normalizedCurrentTick = CURRENT_TICK / 200 * 200; // Normalize to tick spacing - - assertApproxEqAbs(uint256(int256(centerTick)), uint256(int256(normalizedCurrentTick)), 200, - "Anchor should be centered around current tick"); + + assertApproxEqAbs(uint256(int256(centerTick)), uint256(int256(normalizedCurrentTick)), 200, "Anchor should be centered around current tick"); } - + function testAnchorPositionWidthScaling() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); params.anchorWidth = 100; // Maximum width uint256 anchorEthBalance = 20 ether; - + strategy.setAnchorPosition(CURRENT_TICK, anchorEthBalance, params); MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); - + // Calculate expected spacing for 100% width int24 expectedSpacing = 200 + (34 * 100 * 200 / 100); // Should be 7000 assertEq(pos.tickUpper - pos.tickLower, expectedSpacing * 2, "Width should scale with anchorWidth parameter"); } - + // ======================================== // DISCOVERY POSITION TESTS // ======================================== - + function testDiscoveryPositionDependsOnAnchor() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); uint128 anchorLiquidity = 1000e18; // Simulated anchor liquidity - + uint256 discoveryAmount = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); - + // Discovery amount should be proportional to anchor liquidity assertGt(discoveryAmount, 0, "Discovery amount should be positive"); - + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.DISCOVERY), "Should be discovery position"); - + // Discovery liquidity should ensure multiple times more liquidity per tick uint256 expectedMultiplier = 200 + (800 * params.discoveryDepth / 10 ** 18); // Calculate anchor width (same calculation as in _setDiscoveryPosition) int24 anchorSpacing = 200 + (34 * int24(params.anchorWidth) * 200 / 100); int24 anchorWidth = 2 * anchorSpacing; // Adjust for width difference - uint128 expectedLiquidity = uint128( - uint256(anchorLiquidity) * expectedMultiplier * 11000 / (100 * uint256(int256(anchorWidth))) - ); + uint128 expectedLiquidity = uint128(uint256(anchorLiquidity) * expectedMultiplier * 11_000 / (100 * uint256(int256(anchorWidth)))); assertEq(pos.liquidity, expectedLiquidity, "Discovery liquidity should match expected multiple adjusted for width"); } - + function testDiscoveryPositionPlacement() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); bool token0IsWeth = true; - + // Test with WETH as token0 strategy = new MockThreePositionStrategy(HARB_TOKEN, WETH_TOKEN, token0IsWeth, ETH_BALANCE, OUTSTANDING_SUPPLY); - + uint128 anchorLiquidity = 1000e18; strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); - + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); - + // When WETH is token0, discovery should be positioned below current price // (covering the range where HARB gets cheaper) assertLt(pos.tickUpper, CURRENT_TICK, "Discovery should be below current tick when WETH is token0"); } - + function testDiscoveryDepthScaling() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); params.discoveryDepth = 10 ** 18; // Maximum depth (100%) - + uint128 anchorLiquidity = 1000e18; uint256 discoveryAmount1 = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); - + strategy.clearMintedPositions(); params.discoveryDepth = 0; // Minimum depth uint256 discoveryAmount2 = strategy.setDiscoveryPosition(CURRENT_TICK, anchorLiquidity, params); - + assertGt(discoveryAmount1, discoveryAmount2, "Higher discovery depth should result in more tokens"); } - + // ======================================== // FLOOR POSITION TESTS // ======================================== - + function testFloorPositionUsesVWAP() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - + // Set up VWAP data - uint256 vwapX96 = 79228162514264337593543950336; // 1.0 in X96 format + uint256 vwapX96 = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 format strategy.setVWAP(vwapX96, 1000 ether); - + uint256 floorEthBalance = 80 ether; uint256 pulledHarb = 1000 ether; uint256 discoveryAmount = 500 ether; - + strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params); - + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); assertEq(uint256(pos.stage), uint256(ThreePositionStrategy.Stage.FLOOR), "Should be floor position"); - + // Floor position should not be at current tick (should use VWAP) int24 centerTick = (pos.tickLower + pos.tickUpper) / 2; assertNotEq(centerTick, CURRENT_TICK, "Floor should not be positioned at current tick when VWAP available"); } - + function testFloorPositionEthScarcity() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - + // Set up scenario where ETH is insufficient for VWAP price - uint256 vwapX96 = 79228162514264337593543950336 * 10; // High VWAP price + uint256 vwapX96 = 79_228_162_514_264_337_593_543_950_336 * 10; // High VWAP price strategy.setVWAP(vwapX96, 1000 ether); - + uint256 smallEthBalance = 1 ether; // Insufficient ETH uint256 pulledHarb = 1000 ether; uint256 discoveryAmount = 500 ether; - + // Should emit EthScarcity event (check event type, not exact values) vm.expectEmit(true, false, false, false); emit ThreePositionStrategy.EthScarcity(CURRENT_TICK, 0, 0, 0, 0); - + strategy.setFloorPosition(CURRENT_TICK, smallEthBalance, pulledHarb, discoveryAmount, params); } - + function testFloorPositionEthAbundance() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - - // Set up scenario where ETH is sufficient for VWAP price - uint256 baseVwap = 79228162514264337593543950336; // 1.0 in X96 format - uint256 vwapX96 = baseVwap / 100000; // Very low VWAP price to ensure abundance + + // Set up scenario where ETH is sufficient for VWAP price + uint256 baseVwap = 79_228_162_514_264_337_593_543_950_336; // 1.0 in X96 format + uint256 vwapX96 = baseVwap / 100_000; // Very low VWAP price to ensure abundance strategy.setVWAP(vwapX96, 1000 ether); - - uint256 largeEthBalance = 100000 ether; // Very large ETH balance + + uint256 largeEthBalance = 100_000 ether; // Very large ETH balance uint256 pulledHarb = 1000 ether; uint256 discoveryAmount = 500 ether; - + // Should emit EthAbundance event (check event type, not exact values) // The exact VWAP and vwapTick values are calculated, so we just check the event type vm.expectEmit(true, false, false, false); emit ThreePositionStrategy.EthAbundance(CURRENT_TICK, 0, 0, 0, 0); - + strategy.setFloorPosition(CURRENT_TICK, largeEthBalance, pulledHarb, discoveryAmount, params); } - + function testFloorPositionNoVWAP() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - + // No VWAP data (volume = 0) strategy.setVWAP(0, 0); - + uint256 floorEthBalance = 80 ether; uint256 pulledHarb = 1000 ether; uint256 discoveryAmount = 500 ether; - + strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params); - + MockThreePositionStrategy.MintedPosition memory pos = strategy.getMintedPosition(0); - + // Without VWAP, should default to current tick but adjusted for anchor spacing int24 centerTick = (pos.tickLower + pos.tickUpper) / 2; // Expected spacing: TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100) = 200 + (34 * 50 * 200 / 100) = 3600 int24 expectedSpacing = 200 + (34 * 50 * 200 / 100); - assertApproxEqAbs(uint256(int256(centerTick)), uint256(int256(CURRENT_TICK + expectedSpacing)), 200, - "Floor should be positioned away from current tick to avoid anchor overlap"); + assertApproxEqAbs( + uint256(int256(centerTick)), + uint256(int256(CURRENT_TICK + expectedSpacing)), + 200, + "Floor should be positioned away from current tick to avoid anchor overlap" + ); } - + function testFloorPositionOutstandingSupplyCalculation() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - - uint256 initialSupply = 1000000 ether; - uint256 pulledHarb = 50000 ether; - uint256 discoveryAmount = 30000 ether; - + + uint256 initialSupply = 1_000_000 ether; + uint256 pulledHarb = 50_000 ether; + uint256 discoveryAmount = 30_000 ether; + strategy.setOutstandingSupply(initialSupply); - + uint256 floorEthBalance = 80 ether; strategy.setFloorPosition(CURRENT_TICK, floorEthBalance, pulledHarb, discoveryAmount, params); - + // The outstanding supply calculation should account for both pulled and discovery amounts // We can't directly observe this, but it affects the VWAP price calculation // This test ensures the function completes without reverting assertEq(strategy.getMintedPositionsCount(), 1, "Floor position should be created"); } - + // ======================================== // INTEGRATED POSITION SETTING TESTS // ======================================== - + function testSetPositionsOrder() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - + strategy.setPositions(CURRENT_TICK, params); - + // Should have created all three positions assertEq(strategy.getMintedPositionsCount(), 3, "Should create three positions"); - + // Verify order: ANCHOR, DISCOVERY, FLOOR MockThreePositionStrategy.MintedPosition memory pos1 = strategy.getMintedPosition(0); MockThreePositionStrategy.MintedPosition memory pos2 = strategy.getMintedPosition(1); MockThreePositionStrategy.MintedPosition memory pos3 = strategy.getMintedPosition(2); - + assertEq(uint256(pos1.stage), uint256(ThreePositionStrategy.Stage.ANCHOR), "First should be anchor"); assertEq(uint256(pos2.stage), uint256(ThreePositionStrategy.Stage.DISCOVERY), "Second should be discovery"); assertEq(uint256(pos3.stage), uint256(ThreePositionStrategy.Stage.FLOOR), "Third should be floor"); } - + function testSetPositionsEthAllocation() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); params.anchorShare = 2 * 10 ** 17; // 20% - + uint256 totalEth = 100 ether; strategy.setEthBalance(totalEth); - + strategy.setPositions(CURRENT_TICK, params); - + // Floor should get majority of ETH (75-95% according to contract logic) // Anchor should get remainder // This is validated by the positions being created successfully assertEq(strategy.getMintedPositionsCount(), 3, "All positions should be created with proper ETH allocation"); } - + function testSetPositionsAsymmetricProfile() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - + strategy.setPositions(CURRENT_TICK, params); - + MockThreePositionStrategy.MintedPosition memory anchor = strategy.getMintedPosition(0); MockThreePositionStrategy.MintedPosition memory discovery = strategy.getMintedPosition(1); MockThreePositionStrategy.MintedPosition memory floor = strategy.getMintedPosition(2); - + // Verify asymmetric slippage profile // Anchor should have smaller range (shallow liquidity, high slippage) int24 anchorRange = anchor.tickUpper - anchor.tickLower; int24 discoveryRange = discovery.tickUpper - discovery.tickLower; int24 floorRange = floor.tickUpper - floor.tickLower; - + // Discovery and floor should generally have wider ranges than anchor assertGt(discoveryRange, anchorRange / 2, "Discovery should have meaningful range"); assertGt(floorRange, 0, "Floor should have positive range"); - + // All positions should be positioned relative to current tick assertGt(anchor.liquidity, 0, "Anchor should have liquidity"); assertGt(discovery.liquidity, 0, "Discovery should have liquidity"); assertGt(floor.liquidity, 0, "Floor should have liquidity"); } - + // ======================================== // POSITION BOUNDARY TESTS // ======================================== - + function testPositionBoundaries() public { ThreePositionStrategy.PositionParams memory params = getDefaultParams(); - + strategy.setPositions(CURRENT_TICK, params); - + MockThreePositionStrategy.MintedPosition memory anchor = strategy.getMintedPosition(0); MockThreePositionStrategy.MintedPosition memory discovery = strategy.getMintedPosition(1); MockThreePositionStrategy.MintedPosition memory floor = strategy.getMintedPosition(2); - + // Verify positions don't overlap inappropriately // This is important for the valley liquidity strategy - + // All ticks should be properly aligned to tick spacing assertEq(anchor.tickLower % 200, 0, "Anchor lower tick should be aligned"); assertEq(anchor.tickUpper % 200, 0, "Anchor upper tick should be aligned"); @@ -454,22 +432,22 @@ contract ThreePositionStrategyTest is TestConstants { assertEq(floor.tickLower % 200, 0, "Floor lower tick should be aligned"); assertEq(floor.tickUpper % 200, 0, "Floor upper tick should be aligned"); } - + // ======================================== // PARAMETER VALIDATION TESTS // ======================================== - + function testParameterBounding() public { // Test that large but realistic parameters are handled gracefully ThreePositionStrategy.PositionParams memory extremeParams = ThreePositionStrategy.PositionParams({ - capitalInefficiency: 10**18, // 100% (maximum reasonable value) - anchorShare: 10**18, // 100% (maximum reasonable value) - anchorWidth: 1000, // Very wide anchor - discoveryDepth: 10**18 // 100% (maximum reasonable value) - }); - + capitalInefficiency: 10 ** 18, // 100% (maximum reasonable value) + anchorShare: 10 ** 18, // 100% (maximum reasonable value) + anchorWidth: 1000, // Very wide anchor + discoveryDepth: 10 ** 18 // 100% (maximum reasonable value) + }); + // Should not revert even with extreme parameters strategy.setPositions(CURRENT_TICK, extremeParams); assertEq(strategy.getMintedPositionsCount(), 3, "Should handle extreme parameters gracefully"); } -} \ No newline at end of file +} diff --git a/onchain/test/helpers/LiquidityBoundaryHelper.sol b/onchain/test/helpers/LiquidityBoundaryHelper.sol index f019d08..5e27ad5 100644 --- a/onchain/test/helpers/LiquidityBoundaryHelper.sol +++ b/onchain/test/helpers/LiquidityBoundaryHelper.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; -import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; -import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol"; +import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol"; +import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; +import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; /** * @title LiquidityBoundaryHelper @@ -15,11 +15,7 @@ library LiquidityBoundaryHelper { /** * @notice Calculates the ETH required to push price to the outer discovery bound */ - function calculateBuyLimit( - IUniswapV3Pool pool, - ThreePositionStrategy liquidityManager, - bool token0isWeth - ) internal view returns (uint256) { + function calculateBuyLimit(IUniswapV3Pool pool, ThreePositionStrategy liquidityManager, bool token0isWeth) internal view returns (uint256) { (, int24 currentTick,,,,,) = pool.slot0(); (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); @@ -30,36 +26,16 @@ library LiquidityBoundaryHelper { } if (token0isWeth) { - return _calculateBuyLimitToken0IsWeth( - currentTick, - anchorLiquidity, - anchorLower, - anchorUpper, - discoveryLiquidity, - discoveryLower, - discoveryUpper - ); + return _calculateBuyLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper); } - return _calculateBuyLimitToken1IsWeth( - currentTick, - anchorLiquidity, - anchorLower, - anchorUpper, - discoveryLiquidity, - discoveryLower, - discoveryUpper - ); + return _calculateBuyLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, discoveryLiquidity, discoveryLower, discoveryUpper); } /** * @notice Calculates the HARB required to push price to the outer floor bound */ - function calculateSellLimit( - IUniswapV3Pool pool, - ThreePositionStrategy liquidityManager, - bool token0isWeth - ) internal view returns (uint256) { + function calculateSellLimit(IUniswapV3Pool pool, ThreePositionStrategy liquidityManager, bool token0isWeth) internal view returns (uint256) { (, int24 currentTick,,,,,) = pool.slot0(); (uint128 anchorLiquidity, int24 anchorLower, int24 anchorUpper) = liquidityManager.positions(ThreePositionStrategy.Stage.ANCHOR); @@ -70,26 +46,10 @@ library LiquidityBoundaryHelper { } if (token0isWeth) { - return _calculateSellLimitToken0IsWeth( - currentTick, - anchorLiquidity, - anchorLower, - anchorUpper, - floorLiquidity, - floorLower, - floorUpper - ); + return _calculateSellLimitToken0IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper); } - return _calculateSellLimitToken1IsWeth( - currentTick, - anchorLiquidity, - anchorLower, - anchorUpper, - floorLiquidity, - floorLower, - floorUpper - ); + return _calculateSellLimitToken1IsWeth(currentTick, anchorLiquidity, anchorLower, anchorUpper, floorLiquidity, floorLower, floorUpper); } function _calculateBuyLimitToken0IsWeth( @@ -100,7 +60,11 @@ library LiquidityBoundaryHelper { uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper - ) private pure returns (uint256) { + ) + private + pure + returns (uint256) + { if (discoveryLiquidity == 0) { return type(uint256).max; } @@ -135,7 +99,11 @@ library LiquidityBoundaryHelper { uint128 discoveryLiquidity, int24 discoveryLower, int24 discoveryUpper - ) private pure returns (uint256) { + ) + private + pure + returns (uint256) + { if (discoveryLiquidity == 0) { return type(uint256).max; } @@ -170,7 +138,11 @@ library LiquidityBoundaryHelper { uint128 floorLiquidity, int24 floorLower, int24 floorUpper - ) private pure returns (uint256) { + ) + private + pure + returns (uint256) + { if (floorLiquidity == 0) { return type(uint256).max; } @@ -205,7 +177,11 @@ library LiquidityBoundaryHelper { uint128 floorLiquidity, int24 floorLower, int24 floorUpper - ) private pure returns (uint256) { + ) + private + pure + returns (uint256) + { if (floorLiquidity == 0) { return type(uint256).max; } diff --git a/onchain/test/helpers/TestBase.sol b/onchain/test/helpers/TestBase.sol index 603f171..7fd51b3 100644 --- a/onchain/test/helpers/TestBase.sol +++ b/onchain/test/helpers/TestBase.sol @@ -1,19 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "forge-std/Test.sol"; +import { Kraiken } from "../../src/Kraiken.sol"; + +import { LiquidityManager } from "../../src/LiquidityManager.sol"; + +import "../../src/Optimizer.sol"; +import { Stake } from "../../src/Stake.sol"; import "../../src/abstracts/ThreePositionStrategy.sol"; + +import "../../src/helpers/UniswapHelpers.sol"; +import "../../src/interfaces/IWETH9.sol"; + +import "../../test/mocks/MockOptimizer.sol"; import "@aperture/uni-v3-lib/TickMath.sol"; -import {WETH} from "solmate/tokens/WETH.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import "../../src/interfaces/IWETH9.sol"; -import {Kraiken} from "../../src/Kraiken.sol"; -import {Stake} from "../../src/Stake.sol"; -import {LiquidityManager} from "../../src/LiquidityManager.sol"; -import "../../src/helpers/UniswapHelpers.sol"; -import "../../src/Optimizer.sol"; -import "../../test/mocks/MockOptimizer.sol"; +import "forge-std/Test.sol"; +import { WETH } from "solmate/tokens/WETH.sol"; // Constants uint24 constant FEE = uint24(10_000); // 1% fee @@ -33,11 +37,11 @@ abstract contract TestConstants is Test { */ function getDefaultParams() internal pure returns (ThreePositionStrategy.PositionParams memory) { return ThreePositionStrategy.PositionParams({ - capitalInefficiency: 5 * 10 ** 17, // 50% - anchorShare: 5 * 10 ** 17, // 50% - anchorWidth: 50, // 50% - discoveryDepth: 5 * 10 ** 17 // 50% - }); + capitalInefficiency: 5 * 10 ** 17, // 50% + anchorShare: 5 * 10 ** 17, // 50% + anchorWidth: 50, // 50% + discoveryDepth: 5 * 10 ** 17 // 50% + }); } /** @@ -66,7 +70,7 @@ abstract contract TestConstants is Test { */ contract TestEnvironment is TestConstants { using UniswapHelpers for IUniswapV3Pool; - + // Core contracts IUniswapV3Factory public factory; IUniswapV3Pool public pool; @@ -75,15 +79,15 @@ contract TestEnvironment is TestConstants { Stake public stake; LiquidityManager public lm; Optimizer public optimizer; - + // State variables bool public token0isWeth; address public feeDestination; - + constructor(address _feeDestination) { feeDestination = _feeDestination; } - + /** * @notice Deploy all contracts and set up the environment * @param token0shouldBeWeth Whether WETH should be token0 @@ -97,38 +101,44 @@ contract TestEnvironment is TestConstants { * @return _optimizer The optimizer contract * @return _token0isWeth Whether token0 is WETH */ - function setupEnvironment(bool token0shouldBeWeth, address recenterCaller) external returns ( - IUniswapV3Factory _factory, - IUniswapV3Pool _pool, - IWETH9 _weth, - Kraiken _harberg, - Stake _stake, - LiquidityManager _lm, - Optimizer _optimizer, - bool _token0isWeth - ) { + function setupEnvironment( + bool token0shouldBeWeth, + address recenterCaller + ) + external + returns ( + IUniswapV3Factory _factory, + IUniswapV3Pool _pool, + IWETH9 _weth, + Kraiken _harberg, + Stake _stake, + LiquidityManager _lm, + Optimizer _optimizer, + bool _token0isWeth + ) + { // Deploy factory factory = UniswapHelpers.deployUniswapFactory(); - + // Deploy tokens in correct order _deployTokensWithOrder(token0shouldBeWeth); - + // Create and initialize pool _createAndInitializePool(); - + // Deploy protocol contracts _deployProtocolContracts(); - + // Configure permissions _configurePermissions(); - + // Grant recenter access to specified caller vm.prank(feeDestination); lm.setRecenterAccess(recenterCaller); - + return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } - + /** * @notice Deploy tokens ensuring the desired ordering * @param token0shouldBeWeth Whether WETH should be token0 @@ -159,7 +169,7 @@ contract TestEnvironment is TestConstants { } require(setupComplete, "Setup failed to meet the condition after several retries"); } - + /** * @notice Create and initialize the Uniswap pool */ @@ -168,7 +178,7 @@ contract TestEnvironment is TestConstants { token0isWeth = address(weth) < address(harberg); pool.initializePoolFor1Cent(token0isWeth); } - + /** * @notice Deploy protocol contracts (Stake, Optimizer, LiquidityManager) */ @@ -179,7 +189,7 @@ contract TestEnvironment is TestConstants { lm = new LiquidityManager(address(factory), address(weth), address(harberg), address(optimizer)); lm.setFeeDestination(feeDestination); } - + /** * @notice Configure permissions and initial funding */ @@ -189,7 +199,7 @@ contract TestEnvironment is TestConstants { harberg.setLiquidityManager(address(lm)); vm.deal(address(lm), INITIAL_LM_ETH_BALANCE); } - + /** * @notice Setup environment with specific optimizer * @param token0shouldBeWeth Whether WETH should be token0 @@ -205,44 +215,47 @@ contract TestEnvironment is TestConstants { * @return _token0isWeth Whether token0 is WETH */ function setupEnvironmentWithOptimizer( - bool token0shouldBeWeth, + bool token0shouldBeWeth, address recenterCaller, address optimizerAddress - ) external returns ( - IUniswapV3Factory _factory, - IUniswapV3Pool _pool, - IWETH9 _weth, - Kraiken _harberg, - Stake _stake, - LiquidityManager _lm, - Optimizer _optimizer, - bool _token0isWeth - ) { + ) + external + returns ( + IUniswapV3Factory _factory, + IUniswapV3Pool _pool, + IWETH9 _weth, + Kraiken _harberg, + Stake _stake, + LiquidityManager _lm, + Optimizer _optimizer, + bool _token0isWeth + ) + { // Deploy factory factory = UniswapHelpers.deployUniswapFactory(); - + // Deploy tokens in correct order _deployTokensWithOrder(token0shouldBeWeth); - + // Create and initialize pool _createAndInitializePool(); - + // Deploy protocol contracts with custom optimizer stake = new Stake(address(harberg), feeDestination); optimizer = Optimizer(optimizerAddress); lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress); lm.setFeeDestination(feeDestination); - + // Configure permissions _configurePermissions(); - + // Grant recenter access to specified caller vm.prank(feeDestination); lm.setRecenterAccess(recenterCaller); - + return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } - + /** * @notice Setup environment with existing factory and specific optimizer * @param existingFactory The existing Uniswap factory to use @@ -260,44 +273,47 @@ contract TestEnvironment is TestConstants { */ function setupEnvironmentWithExistingFactory( IUniswapV3Factory existingFactory, - bool token0shouldBeWeth, + bool token0shouldBeWeth, address recenterCaller, address optimizerAddress - ) external returns ( - IUniswapV3Factory _factory, - IUniswapV3Pool _pool, - IWETH9 _weth, - Kraiken _harberg, - Stake _stake, - LiquidityManager _lm, - Optimizer _optimizer, - bool _token0isWeth - ) { + ) + external + returns ( + IUniswapV3Factory _factory, + IUniswapV3Pool _pool, + IWETH9 _weth, + Kraiken _harberg, + Stake _stake, + LiquidityManager _lm, + Optimizer _optimizer, + bool _token0isWeth + ) + { // Use existing factory factory = existingFactory; - + // Deploy tokens in correct order _deployTokensWithOrder(token0shouldBeWeth); - + // Create and initialize pool _createAndInitializePool(); - + // Deploy protocol contracts with custom optimizer stake = new Stake(address(harberg), feeDestination); optimizer = Optimizer(optimizerAddress); lm = new LiquidityManager(address(factory), address(weth), address(harberg), optimizerAddress); lm.setFeeDestination(feeDestination); - + // Configure permissions _configurePermissions(); - + // Grant recenter access to specified caller vm.prank(feeDestination); lm.setRecenterAccess(recenterCaller); - + return (factory, pool, weth, harberg, stake, lm, optimizer, token0isWeth); } - + /** * @notice Perform recenter with proper time warp and oracle updates * @param liquidityManager The LiquidityManager instance to recenter @@ -306,9 +322,9 @@ contract TestEnvironment is TestConstants { function performRecenter(LiquidityManager liquidityManager, address caller) external { // Update oracle time vm.warp(block.timestamp + ORACLE_UPDATE_INTERVAL); - + // Perform recenter vm.prank(caller); liquidityManager.recenter(); } -} \ No newline at end of file +} diff --git a/onchain/test/helpers/UniswapTestBase.sol b/onchain/test/helpers/UniswapTestBase.sol index e3cb5ec..a4483d4 100644 --- a/onchain/test/helpers/UniswapTestBase.sol +++ b/onchain/test/helpers/UniswapTestBase.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "forge-std/Test.sol"; -import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; -import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol"; -import {LiquidityAmounts} from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; -import {SqrtPriceMath} from "@aperture/uni-v3-lib/SqrtPriceMath.sol"; +import { Kraiken } from "../../src/Kraiken.sol"; +import { ThreePositionStrategy } from "../../src/abstracts/ThreePositionStrategy.sol"; import "../../src/interfaces/IWETH9.sol"; -import {Kraiken} from "../../src/Kraiken.sol"; -import {ThreePositionStrategy} from "../../src/abstracts/ThreePositionStrategy.sol"; -import {LiquidityBoundaryHelper} from "./LiquidityBoundaryHelper.sol"; +import { LiquidityBoundaryHelper } from "./LiquidityBoundaryHelper.sol"; +import { LiquidityAmounts } from "@aperture/uni-v3-lib/LiquidityAmounts.sol"; +import { SqrtPriceMath } from "@aperture/uni-v3-lib/SqrtPriceMath.sol"; +import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; +import "forge-std/Test.sol"; /** * @title UniSwapHelper @@ -99,7 +99,7 @@ abstract contract UniSwapHelper is Test { // Use very aggressive limit close to MIN_SQRT_RATIO limit = TickMath.MIN_SQRT_RATIO + 1; } else { - // Swapping token1 for token0 - price goes up + // Swapping token1 for token0 - price goes up // Use very aggressive limit close to MAX_SQRT_RATIO limit = TickMath.MAX_SQRT_RATIO - 1; } @@ -115,13 +115,12 @@ abstract contract UniSwapHelper is Test { if (amount0Delta == 0 && amount1Delta == 0) { return; } - + require(amount0Delta > 0 || amount1Delta > 0); (address seller,, bool isBuy) = abi.decode(_data, (address, uint256, bool)); - (, uint256 amountToPay) = - amount0Delta > 0 ? (!token0isWeth, uint256(amount0Delta)) : (token0isWeth, uint256(amount1Delta)); + (, uint256 amountToPay) = amount0Delta > 0 ? (!token0isWeth, uint256(amount0Delta)) : (token0isWeth, uint256(amount1Delta)); if (isBuy) { weth.transfer(msg.sender, amountToPay); } else { @@ -145,7 +144,7 @@ abstract contract UniSwapHelper is Test { // pack ETH uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed; if (weth.balanceOf(address(this)) < ethOwed) { - weth.deposit{value: address(this).balance}(); + weth.deposit{ value: address(this).balance }(); } if (ethOwed > 0) { weth.transfer(msg.sender, amount1Owed); @@ -157,8 +156,8 @@ abstract contract UniSwapHelper is Test { // ======================================== // Safety margin to prevent tick boundary violations (conservative approach) - int24 constant TICK_BOUNDARY_SAFETY_MARGIN = 15000; - + int24 constant TICK_BOUNDARY_SAFETY_MARGIN = 15_000; + // Price normalization constants uint256 constant NORMALIZATION_HARB_PERCENTAGE = 100; // 1% of HARB balance uint256 constant NORMALIZATION_ETH_AMOUNT = 0.01 ether; // Fixed ETH amount for normalization @@ -172,10 +171,10 @@ abstract contract UniSwapHelper is Test { */ function handleExtremePrice() internal { uint256 attempts = 0; - + while (attempts < MAX_NORMALIZATION_ATTEMPTS) { (, int24 currentTick,,,,,) = pool.slot0(); - + if (currentTick >= TickMath.MAX_TICK - TICK_BOUNDARY_SAFETY_MARGIN) { _executeNormalizingTrade(true); // Move price down attempts++; @@ -188,7 +187,6 @@ abstract contract UniSwapHelper is Test { } } } - /** * @notice Executes a small trade to move price away from tick boundaries @@ -203,24 +201,24 @@ abstract contract UniSwapHelper is Test { // Use 1% of account's HARB balance (conservative approach like original) uint256 harbToSell = harbBalance / NORMALIZATION_HARB_PERCENTAGE; if (harbToSell == 0) harbToSell = 1; - + vm.prank(account); harberg.transfer(address(this), harbToSell); harberg.approve(address(pool), harbToSell); - + // Sell HARB for ETH with aggressive price limits for normalization performSwapWithAggressiveLimits(harbToSell, false); } } else { - // Need to move price UP (increase HARB price) + // Need to move price UP (increase HARB price) // This means: buy HARB with ETH (reduce HARB supply in pool) uint256 ethBalance = weth.balanceOf(account); if (ethBalance > 0) { // Use small amount for normalization (like original) uint256 ethToBuy = NORMALIZATION_ETH_AMOUNT; if (ethToBuy > ethBalance) ethToBuy = ethBalance; - - // Buy HARB with ETH with aggressive price limits for normalization + + // Buy HARB with ETH with aggressive price limits for normalization performSwapWithAggressiveLimits(ethToBuy, true); } } @@ -246,7 +244,7 @@ abstract contract UniSwapHelper is Test { } /** - * @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits + * @notice Calculates the maximum HARB amount that can be traded (sell HARB) without exceeding position liquidity limits * @dev When currentTick is in anchor range, calculates trade size to make anchor and floor positions "full" of HARB * @return maxHarbAmount Maximum HARB that can be safely traded, 0 if no positions exist or already at limit */ @@ -269,7 +267,7 @@ abstract contract UniSwapHelper is Test { /** * @notice Raw buy operation without liquidity limit checking - * @param amountEth Amount of ETH to spend buying HARB + * @param amountEth Amount of ETH to spend buying HARB */ function buyRaw(uint256 amountEth) internal { performSwap(amountEth, true); diff --git a/onchain/test/libraries/UniswapMath.t.sol b/onchain/test/libraries/UniswapMath.t.sol index fe09897..85276c6 100644 --- a/onchain/test/libraries/UniswapMath.t.sol +++ b/onchain/test/libraries/UniswapMath.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import "forge-std/Test.sol"; -import "@aperture/uni-v3-lib/TickMath.sol"; import "../../src/libraries/UniswapMath.sol"; +import "@aperture/uni-v3-lib/TickMath.sol"; +import "forge-std/Test.sol"; /** * @title UniswapMath Test Suite @@ -14,227 +14,227 @@ contract MockUniswapMath is UniswapMath { function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) external pure returns (int24) { return _tickAtPrice(t0isWeth, tokenAmount, ethAmount); } - + function tickAtPriceRatio(int128 priceRatioX64) external pure returns (int24) { return _tickAtPriceRatio(priceRatioX64); } - + function priceAtTick(int24 tick) external pure returns (uint256) { return _priceAtTick(tick); } - + function clampToTickSpacing(int24 tick, int24 spacing) external pure returns (int24) { return _clampToTickSpacing(tick, spacing); } } contract UniswapMathTest is Test { - MockUniswapMath uniswapMath; - - int24 constant TICK_SPACING = 200; - + MockUniswapMath internal uniswapMath; + + int24 internal constant TICK_SPACING = 200; + function setUp() public { uniswapMath = new MockUniswapMath(); } - + // ======================================== // TICK AT PRICE TESTS // ======================================== - + function testTickAtPriceBasic() public { // Test 1:1 ratio (equal amounts) uint256 tokenAmount = 1 ether; uint256 ethAmount = 1 ether; - + int24 tickWethToken0 = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); int24 tickTokenToken0 = uniswapMath.tickAtPrice(false, tokenAmount, ethAmount); - + // Ticks should be opposite signs for different token orderings assertEq(tickWethToken0, -tickTokenToken0, "Ticks should be negatives of each other"); assertGt(tickWethToken0, -1000, "Tick should be reasonable for 1:1 ratio"); assertLt(tickWethToken0, 1000, "Tick should be reasonable for 1:1 ratio"); } - + function testTickAtPriceZeroToken() public { // When token amount is 0, should return MAX_TICK int24 tick = uniswapMath.tickAtPrice(true, 0, 1 ether); assertEq(tick, TickMath.MAX_TICK, "Zero token amount should return MAX_TICK"); } - + function testTickAtPriceZeroEthReverts() public { // When ETH amount is 0, should revert vm.expectRevert("ETH amount cannot be zero"); uniswapMath.tickAtPrice(true, 1 ether, 0); } - + function testTickAtPriceHighRatio() public { // Test when token is much more expensive than ETH uint256 tokenAmount = 1 ether; uint256 ethAmount = 1000 ether; // Token is cheap relative to ETH - + int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); - + // Should be a large negative tick (cheap token) - assertLt(tick, -10000, "Cheap token should result in large negative tick"); + assertLt(tick, -10_000, "Cheap token should result in large negative tick"); assertGt(tick, TickMath.MIN_TICK, "Tick should be within valid range"); } - + function testTickAtPriceLowRatio() public { // Test when token is much cheaper than ETH uint256 tokenAmount = 1000 ether; // Token is expensive relative to ETH uint256 ethAmount = 1 ether; - + int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); - + // Should be a large positive tick (expensive token) - assertGt(tick, 10000, "Expensive token should result in large positive tick"); + assertGt(tick, 10_000, "Expensive token should result in large positive tick"); assertLt(tick, TickMath.MAX_TICK, "Tick should be within valid range"); } - + // ======================================== // PRICE AT TICK TESTS // ======================================== - + function testPriceAtTickZero() public { // Tick 0 should give price ratio of 1 (in X96 format) uint256 price = uniswapMath.priceAtTick(0); uint256 expectedPrice = 1 << 96; // 1.0 in X96 format - + assertEq(price, expectedPrice, "Tick 0 should give price ratio of 1"); } - + function testPriceAtTickPositive() public { // Positive tick should give price > 1 uint256 price = uniswapMath.priceAtTick(1000); uint256 basePrice = 1 << 96; - + assertGt(price, basePrice, "Positive tick should give price > 1"); } - + function testPriceAtTickNegative() public { // Negative tick should give price < 1 uint256 price = uniswapMath.priceAtTick(-1000); uint256 basePrice = 1 << 96; - + assertLt(price, basePrice, "Negative tick should give price < 1"); } - + function testPriceAtTickSymmetry() public { // Test that positive and negative ticks are reciprocals int24 tick = 5000; uint256 pricePositive = uniswapMath.priceAtTick(tick); uint256 priceNegative = uniswapMath.priceAtTick(-tick); - + // pricePositive * priceNegative should approximately equal (1 << 96)^2 uint256 product = (pricePositive >> 48) * (priceNegative >> 48); // Scale down to prevent overflow uint256 expected = 1 << 96; - + // Allow small tolerance for rounding errors assertApproxEqRel(product, expected, 0.01e18, "Positive and negative ticks should be reciprocals"); } - + // ======================================== // CLAMP TO TICK SPACING TESTS // ======================================== - + function testClampToTickSpacingExact() public { // Test tick that's already aligned int24 alignedTick = 1000; // Already multiple of 200 int24 result = uniswapMath.clampToTickSpacing(alignedTick, TICK_SPACING); - + assertEq(result, alignedTick, "Already aligned tick should remain unchanged"); } - + function testClampToTickSpacingRoundDown() public { // Test tick that needs rounding down int24 unalignedTick = 1150; // Should round down to 1000 int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING); - + assertEq(result, 1000, "Tick should round down to nearest multiple"); } - + function testClampToTickSpacingRoundUp() public { // Test negative tick that needs rounding int24 unalignedTick = -1150; // Should round to -1000 (towards zero) int24 result = uniswapMath.clampToTickSpacing(unalignedTick, TICK_SPACING); - + assertEq(result, -1000, "Negative tick should round towards zero"); } - + function testClampToTickSpacingMinBound() public { // Test tick below minimum int24 result = uniswapMath.clampToTickSpacing(TickMath.MIN_TICK - 1000, TICK_SPACING); - + assertEq(result, TickMath.MIN_TICK, "Tick below minimum should clamp to MIN_TICK"); } - + function testClampToTickSpacingMaxBound() public { // Test tick above maximum int24 result = uniswapMath.clampToTickSpacing(TickMath.MAX_TICK + 1000, TICK_SPACING); - + assertEq(result, TickMath.MAX_TICK, "Tick above maximum should clamp to MAX_TICK"); } - + // ======================================== // ROUND-TRIP CONVERSION TESTS // ======================================== - + function testTickPriceRoundTrip() public { // Test that tick → price → tick preserves the original value - int24 originalTick = 12345; + int24 originalTick = 12_345; originalTick = uniswapMath.clampToTickSpacing(originalTick, TICK_SPACING); // Align to spacing - + uint256 price = uniswapMath.priceAtTick(originalTick); - - // Note: Direct round-trip through tickAtPriceRatio isn't possible since + + // Note: Direct round-trip through tickAtPriceRatio isn't possible since // priceAtTick returns uint256 while tickAtPriceRatio expects int128 // This test validates that the price calculation is reasonable assertGt(price, 0, "Price should be positive"); assertLt(price, type(uint128).max, "Price should be within reasonable bounds"); } - + // ======================================== // FUZZ TESTS // ======================================== - + function testFuzzTickAtPrice(uint256 tokenAmount, uint256 ethAmount) public { // Bound inputs to reasonable ranges to avoid overflow in ABDKMath64x64 conversions // int128 max is ~1.7e38, but we need to be more conservative for price ratios tokenAmount = bound(tokenAmount, 1, 1e18); ethAmount = bound(ethAmount, 1, 1e18); - + int24 tick = uniswapMath.tickAtPrice(true, tokenAmount, ethAmount); - + // Tick should be within valid bounds assertGe(tick, TickMath.MIN_TICK, "Tick should be >= MIN_TICK"); assertLe(tick, TickMath.MAX_TICK, "Tick should be <= MAX_TICK"); } - + function testFuzzPriceAtTick(int24 tick) public { // Bound tick to reasonable range to avoid extreme prices // Further restrict to prevent overflow in price calculations - tick = int24(bound(int256(tick), -200000, 200000)); - + tick = int24(bound(int256(tick), -200_000, 200_000)); + uint256 price = uniswapMath.priceAtTick(tick); - + // Price should be positive and within reasonable bounds assertGt(price, 0, "Price should be positive"); assertLt(price, type(uint128).max, "Price should be within reasonable bounds"); } - + function testFuzzClampToTickSpacing(int24 tick, int24 spacing) public { // Bound spacing to reasonable positive values spacing = int24(bound(int256(spacing), 1, 1000)); - + int24 clampedTick = uniswapMath.clampToTickSpacing(tick, spacing); - + // Result should be within valid bounds assertGe(clampedTick, TickMath.MIN_TICK, "Clamped tick should be >= MIN_TICK"); assertLe(clampedTick, TickMath.MAX_TICK, "Clamped tick should be <= MAX_TICK"); - + // Result should be aligned to spacing (unless at boundaries) if (clampedTick != TickMath.MIN_TICK && clampedTick != TickMath.MAX_TICK) { assertEq(clampedTick % spacing, 0, "Clamped tick should be aligned to spacing"); } } -} \ No newline at end of file +} diff --git a/onchain/test/mocks/BearMarketOptimizer.sol b/onchain/test/mocks/BearMarketOptimizer.sol index 551c73b..c58b2ce 100644 --- a/onchain/test/mocks/BearMarketOptimizer.sol +++ b/onchain/test/mocks/BearMarketOptimizer.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {Kraiken} from "../../src/Kraiken.sol"; -import {Stake} from "../../src/Stake.sol"; +import { Kraiken } from "../../src/Kraiken.sol"; +import { Stake } from "../../src/Stake.sol"; contract BearMarketOptimizer { /// @notice Calculate sentiment (not used, but required for interface compatibility) @@ -10,7 +10,7 @@ contract BearMarketOptimizer { return 0; // Placeholder implementation } - /// @notice Get sentiment (not used, but required for interface compatibility) + /// @notice Get sentiment (not used, but required for interface compatibility) function getSentiment() external pure returns (uint256) { return 0; // Placeholder implementation } @@ -18,19 +18,15 @@ contract BearMarketOptimizer { /// @notice Returns bear market liquidity parameters /// @return capitalInefficiency 80% - conservative /// @return anchorShare 20% - small anchor - /// @return anchorWidth 80 - wide width + /// @return anchorWidth 80 - wide width /// @return discoveryDepth 20% - shallow discovery - function getLiquidityParams() - external - pure - returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) - { - capitalInefficiency = 8 * 10 ** 17; // 80% - conservative - anchorShare = 2 * 10 ** 17; // 20% - small anchor - anchorWidth = 1000; // wide width - discoveryDepth = 2 * 10 ** 17; // 20% - shallow discovery + function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { + capitalInefficiency = 8 * 10 ** 17; // 80% - conservative + anchorShare = 2 * 10 ** 17; // 20% - small anchor + anchorWidth = 1000; // wide width + discoveryDepth = 2 * 10 ** 17; // 20% - shallow discovery } - + function getDescription() external pure returns (string memory) { return "Bear Market (Low Risk)"; } diff --git a/onchain/test/mocks/BullMarketOptimizer.sol b/onchain/test/mocks/BullMarketOptimizer.sol index c4dfdd7..f64c79f 100644 --- a/onchain/test/mocks/BullMarketOptimizer.sol +++ b/onchain/test/mocks/BullMarketOptimizer.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {Kraiken} from "../../src/Kraiken.sol"; -import {Stake} from "../../src/Stake.sol"; +import { Kraiken } from "../../src/Kraiken.sol"; +import { Stake } from "../../src/Stake.sol"; contract BullMarketOptimizer { /// @notice Calculate sentiment (not used, but required for interface compatibility) @@ -10,7 +10,7 @@ contract BullMarketOptimizer { return 0; // Placeholder implementation } - /// @notice Get sentiment (not used, but required for interface compatibility) + /// @notice Get sentiment (not used, but required for interface compatibility) function getSentiment() external pure returns (uint256) { return 0; // Placeholder implementation } @@ -20,17 +20,13 @@ contract BullMarketOptimizer { /// @return anchorShare 95% - reduces floor allocation to 90.1% /// @return anchorWidth 50 - medium width for concentrated liquidity /// @return discoveryDepth 1e18 - maximum discovery depth - function getLiquidityParams() - external - pure - returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) - { - capitalInefficiency = 0; // 0% - aggressive bull market stance - anchorShare = 1e18; // 95% - reduces floor to 90.1% of ETH - anchorWidth = 50; // 50% - medium width for concentrated liquidity - discoveryDepth = 1e18; // Maximum discovery depth + function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { + capitalInefficiency = 0; // 0% - aggressive bull market stance + anchorShare = 1e18; // 95% - reduces floor to 90.1% of ETH + anchorWidth = 50; // 50% - medium width for concentrated liquidity + discoveryDepth = 1e18; // Maximum discovery depth } - + function getDescription() external pure returns (string memory) { return "Bull Market (Aggressive)"; } diff --git a/onchain/test/mocks/ExtremeOptimizer.sol b/onchain/test/mocks/ExtremeOptimizer.sol index 6dab686..859caf1 100644 --- a/onchain/test/mocks/ExtremeOptimizer.sol +++ b/onchain/test/mocks/ExtremeOptimizer.sol @@ -8,57 +8,60 @@ pragma solidity ^0.8.19; */ contract ExtremeOptimizer { uint256 private callCount; - + function getOptimalParameters( uint256, // percentageStaked uint256, // avgTaxRate - uint256 // sentiment - ) external returns (uint256, uint256, uint256, uint256) { + uint256 // sentiment + ) + external + returns (uint256, uint256, uint256, uint256) + { callCount++; - + // Cycle through extreme scenarios uint256 scenario = callCount % 5; - + if (scenario == 0) { // Extreme capital inefficiency with minimal anchor return ( - 1e18, // 100% capital inefficiency (KRAIKEN valued at 170%) - 0.01e18, // 1% anchor share (99% to floor) - 1, // 1% anchor width (extremely narrow) - 10e18 // 10x discovery depth + 1e18, // 100% capital inefficiency (KRAIKEN valued at 170%) + 0.01e18, // 1% anchor share (99% to floor) + 1, // 1% anchor width (extremely narrow) + 10e18 // 10x discovery depth ); } else if (scenario == 1) { // Zero capital inefficiency with maximum anchor return ( - 0, // 0% capital inefficiency (KRAIKEN valued at 70%) - 0.99e18, // 99% anchor share (minimal floor) - 100, // 100% anchor width (maximum range) - 0.1e18 // 0.1x discovery depth (minimal discovery) + 0, // 0% capital inefficiency (KRAIKEN valued at 70%) + 0.99e18, // 99% anchor share (minimal floor) + 100, // 100% anchor width (maximum range) + 0.1e18 // 0.1x discovery depth (minimal discovery) ); } else if (scenario == 2) { // Oscillating between extremes return ( - callCount % 2 == 0 ? 1e18 : 0, // Flip between 0% and 100% - 0.5e18, // 50% anchor share - 50, // 50% width - callCount % 2 == 0 ? 10e18 : 0.1e18 // Flip discovery depth + callCount % 2 == 0 ? 1e18 : 0, // Flip between 0% and 100% + 0.5e18, // 50% anchor share + 50, // 50% width + callCount % 2 == 0 ? 10e18 : 0.1e18 // Flip discovery depth ); } else if (scenario == 3) { // Edge case: Everything at minimum return ( - 0, // Minimum capital inefficiency - 0, // Minimum anchor share (all to floor) - 1, // Minimum width - 0 // No discovery liquidity + 0, // Minimum capital inefficiency + 0, // Minimum anchor share (all to floor) + 1, // Minimum width + 0 // No discovery liquidity ); } else { // Edge case: Everything at maximum return ( - 1e18, // Maximum capital inefficiency - 1e18, // Maximum anchor share (no floor) - 100, // Maximum width - 100e18 // Extreme discovery depth + 1e18, // Maximum capital inefficiency + 1e18, // Maximum anchor share (no floor) + 100, // Maximum width + 100e18 // Extreme discovery depth ); } } -} \ No newline at end of file +} diff --git a/onchain/test/mocks/MaliciousOptimizer.sol b/onchain/test/mocks/MaliciousOptimizer.sol index 484eb9b..dbb5dda 100644 --- a/onchain/test/mocks/MaliciousOptimizer.sol +++ b/onchain/test/mocks/MaliciousOptimizer.sol @@ -8,55 +8,53 @@ pragma solidity ^0.8.19; */ contract MaliciousOptimizer { uint256 private callCount; - + function getOptimalParameters( uint256, // percentageStaked uint256, // avgTaxRate - uint256 // sentiment - ) external returns (uint256, uint256, uint256, uint256) { + uint256 // sentiment + ) + external + returns (uint256, uint256, uint256, uint256) + { callCount++; - + // Return parameters that should cause problems: // 1. First call: All liquidity in floor (no anchor protection) if (callCount == 1) { return ( - 0, // 0% capital inefficiency (minimum KRAIKEN value) - 0, // 0% anchor share (100% to floor) - 1, // Minimal width - 0 // No discovery + 0, // 0% capital inefficiency (minimum KRAIKEN value) + 0, // 0% anchor share (100% to floor) + 1, // Minimal width + 0 // No discovery ); } - + // 2. Second call: Suddenly switch to all anchor (no floor protection) if (callCount == 2) { return ( - 1e18, // 100% capital inefficiency (maximum KRAIKEN value) - 1e18, // 100% anchor share (0% to floor) - 100, // Maximum width - 0 // No discovery + 1e18, // 100% capital inefficiency (maximum KRAIKEN value) + 1e18, // 100% anchor share (0% to floor) + 100, // Maximum width + 0 // No discovery ); } - + // 3. Third call: Create huge discovery position if (callCount == 3) { return ( - 0.5e18, // 50% capital inefficiency - 0.1e18, // 10% anchor share - 10, // Small width - 100e18 // Massive discovery depth + 0.5e18, // 50% capital inefficiency + 0.1e18, // 10% anchor share + 10, // Small width + 100e18 // Massive discovery depth ); } - + // 4. Oscillate wildly - return ( - callCount % 2 == 0 ? 0 : 1e18, - callCount % 2 == 0 ? 0 : 1e18, - callCount % 2 == 0 ? 1 : 100, - callCount % 2 == 0 ? 0 : 10e18 - ); + return (callCount % 2 == 0 ? 0 : 1e18, callCount % 2 == 0 ? 0 : 1e18, callCount % 2 == 0 ? 1 : 100, callCount % 2 == 0 ? 0 : 10e18); } - + function calculateSentiment(uint256, uint256) public pure returns (uint256) { return 0; } -} \ No newline at end of file +} diff --git a/onchain/test/mocks/MockKraiken.sol b/onchain/test/mocks/MockKraiken.sol index d30d974..0adf7cb 100644 --- a/onchain/test/mocks/MockKraiken.sol +++ b/onchain/test/mocks/MockKraiken.sol @@ -7,8 +7,8 @@ pragma solidity ^0.8.19; */ contract MockKraiken { uint8 public constant decimals = 18; - + function totalSupply() external pure returns (uint256) { - return 1000000 * 10**18; // 1M tokens + return 1_000_000 * 10 ** 18; // 1M tokens } -} \ No newline at end of file +} diff --git a/onchain/test/mocks/MockOptimizer.sol b/onchain/test/mocks/MockOptimizer.sol index 88afc4d..c59dbac 100644 --- a/onchain/test/mocks/MockOptimizer.sol +++ b/onchain/test/mocks/MockOptimizer.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {Kraiken} from "../../src/Kraiken.sol"; -import {Stake} from "../../src/Stake.sol"; -import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; -import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; +import { Kraiken } from "../../src/Kraiken.sol"; +import { Stake } from "../../src/Stake.sol"; + +import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; contract MockOptimizer is Initializable, UUPSUpgradeable { Kraiken internal kraiken; @@ -44,19 +45,14 @@ contract MockOptimizer is Initializable, UUPSUpgradeable { } } - function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {} + function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { } /// @notice Set liquidity parameters for sentiment analysis testing /// @param capitalInefficiency Capital inefficiency parameter (0-1e18) /// @param anchorShare Anchor share parameter (0-1e18) /// @param anchorWidth Anchor width parameter /// @param discoveryDepth Discovery depth parameter (0-1e18) - function setLiquidityParams( - uint256 capitalInefficiency, - uint256 anchorShare, - uint24 anchorWidth, - uint256 discoveryDepth - ) external { + function setLiquidityParams(uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) external { _capitalInefficiency = capitalInefficiency; _anchorShare = anchorShare; _anchorWidth = anchorWidth; @@ -80,11 +76,7 @@ contract MockOptimizer is Initializable, UUPSUpgradeable { /// @return anchorShare Configurable anchor share /// @return anchorWidth Configurable anchor width /// @return discoveryDepth Configurable discovery depth - function getLiquidityParams() - external - view - returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) - { + function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { capitalInefficiency = _capitalInefficiency; anchorShare = _anchorShare; anchorWidth = _anchorWidth; diff --git a/onchain/test/mocks/MockStake.sol b/onchain/test/mocks/MockStake.sol index 4055f28..71b83e4 100644 --- a/onchain/test/mocks/MockStake.sol +++ b/onchain/test/mocks/MockStake.sol @@ -9,7 +9,7 @@ pragma solidity ^0.8.19; contract MockStake { uint256 private _percentageStaked; uint256 private _averageTaxRate; - + /** * @notice Set the percentage staked for testing * @param percentage Value between 0 and 1e18 @@ -18,7 +18,7 @@ contract MockStake { require(percentage <= 1e18, "Percentage too high"); _percentageStaked = percentage; } - + /** * @notice Set the average tax rate for testing * @param rate Value between 0 and 1e18 @@ -27,7 +27,7 @@ contract MockStake { require(rate <= 1e18, "Rate too high"); _averageTaxRate = rate; } - + /** * @notice Returns the mocked percentage staked * @return percentageStaked A number between 0 and 1e18 @@ -35,7 +35,7 @@ contract MockStake { function getPercentageStaked() external view returns (uint256) { return _percentageStaked; } - + /** * @notice Returns the mocked average tax rate * @return averageTaxRate A number between 0 and 1e18 @@ -43,4 +43,4 @@ contract MockStake { function getAverageTaxRate() external view returns (uint256) { return _averageTaxRate; } -} \ No newline at end of file +} diff --git a/onchain/test/mocks/MockVWAPTracker.sol b/onchain/test/mocks/MockVWAPTracker.sol index 988c0a0..36a4247 100644 --- a/onchain/test/mocks/MockVWAPTracker.sol +++ b/onchain/test/mocks/MockVWAPTracker.sol @@ -24,4 +24,4 @@ contract MockVWAPTracker is VWAPTracker { function resetVWAP() external { _resetVWAP(); } -} \ No newline at end of file +} diff --git a/onchain/test/mocks/NeutralMarketOptimizer.sol b/onchain/test/mocks/NeutralMarketOptimizer.sol index 75dbaca..d976d29 100644 --- a/onchain/test/mocks/NeutralMarketOptimizer.sol +++ b/onchain/test/mocks/NeutralMarketOptimizer.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {Kraiken} from "../../src/Kraiken.sol"; -import {Stake} from "../../src/Stake.sol"; +import { Kraiken } from "../../src/Kraiken.sol"; +import { Stake } from "../../src/Stake.sol"; contract NeutralMarketOptimizer { /// @notice Calculate sentiment (not used, but required for interface compatibility) @@ -10,27 +10,23 @@ contract NeutralMarketOptimizer { return 0; // Placeholder implementation } - /// @notice Get sentiment (not used, but required for interface compatibility) + /// @notice Get sentiment (not used, but required for interface compatibility) function getSentiment() external pure returns (uint256) { return 0; // Placeholder implementation } - /// @notice Returns neutral market liquidity parameters + /// @notice Returns neutral market liquidity parameters /// @return capitalInefficiency 50% - balanced /// @return anchorShare 50% - balanced anchor /// @return anchorWidth 50 - standard width /// @return discoveryDepth 50% - balanced discovery - function getLiquidityParams() - external - pure - returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) - { - capitalInefficiency = 5 * 10 ** 17; // 50% - balanced - anchorShare = 5 * 10 ** 17; // 50% - balanced anchor - anchorWidth = 1000; // standard width - discoveryDepth = 5 * 10 ** 17; // 50% - balanced discovery + function getLiquidityParams() external pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { + capitalInefficiency = 5 * 10 ** 17; // 50% - balanced + anchorShare = 5 * 10 ** 17; // 50% - balanced anchor + anchorWidth = 1000; // standard width + discoveryDepth = 5 * 10 ** 17; // 50% - balanced discovery } - + function getDescription() external pure returns (string memory) { return "Neutral Market (Balanced)"; } diff --git a/onchain/test/mocks/RandomScenarioOptimizer.sol b/onchain/test/mocks/RandomScenarioOptimizer.sol index 91c2288..1b4c35a 100644 --- a/onchain/test/mocks/RandomScenarioOptimizer.sol +++ b/onchain/test/mocks/RandomScenarioOptimizer.sol @@ -5,28 +5,30 @@ import "./MockOptimizer.sol"; contract RandomScenarioOptimizer is MockOptimizer { string private description; - + function initialize(address _kraiken, address _stake) public override initializer { _changeAdmin(msg.sender); kraiken = Kraiken(_kraiken); stake = Stake(_stake); } - + function setRandomParams( uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth, string memory scenarioDescription - ) external { + ) + external + { _capitalInefficiency = capitalInefficiency; _anchorShare = anchorShare; _anchorWidth = anchorWidth; _discoveryDepth = discoveryDepth; description = scenarioDescription; } - + function getDescription() external view returns (string memory) { return description; } -} \ No newline at end of file +} diff --git a/onchain/test/mocks/WhaleOptimizer.sol b/onchain/test/mocks/WhaleOptimizer.sol index e46594d..a654ac3 100644 --- a/onchain/test/mocks/WhaleOptimizer.sol +++ b/onchain/test/mocks/WhaleOptimizer.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; -import {Kraiken} from "../../src/Kraiken.sol"; -import {Stake} from "../../src/Stake.sol"; +import { Kraiken } from "../../src/Kraiken.sol"; +import { Stake } from "../../src/Stake.sol"; /** * @title WhaleOptimizer @@ -20,14 +20,14 @@ contract WhaleOptimizer { function getLiquidityParams() external pure returns (uint256, uint256, uint24, uint256) { return ( - 1e17, // capitalInefficiency: 10% (very aggressive) + 1e17, // capitalInefficiency: 10% (very aggressive) 95e16, // anchorShare: 95% (massive anchor position) - 10, // anchorWidth: 10 (extremely narrow) - 5e16 // discoveryDepth: 5% (minimal discovery) + 10, // anchorWidth: 10 (extremely narrow) + 5e16 // discoveryDepth: 5% (minimal discovery) ); } function getDescription() external pure returns (string memory) { return "Whale Market - Massive concentrated liquidity"; } -} \ No newline at end of file +}